diff --git a/.vscode/settings.json b/.vscode/settings.json index 2021d12..37a3b13 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "D:\\SteamLibrary\\steamapps\\common\\Blender\\4.3\\scripts\\addons", "C:\\Users\\Onan\\AppData\\Roaming\\Blender Foundation\\Blender\\4.3\\extensions\\user_default\\",//C:/Users/Onan/AppData/Roaming/Blender Foundation/Blender/4.0/scripts/addons "D:\\blender stuff\\blendercodestuff\\4.3", + "D:\\SteamLibrary\\steamapps\\common\\Blender\\4.3\\python\\lib\\site-packages", "/Users/frankche/Documents/blendercoding/4.1/", "/Users/frankche/Library/Application Support/Blender/4.3/extensions/user_default/" ], diff --git a/core/common.py b/core/common.py index c4e7300..d6696b3 100644 --- a/core/common.py +++ b/core/common.py @@ -1,8 +1,18 @@ import bpy import numpy as np +import threading +import time +import webbrowser +import typing +import struct +from io import BytesIO + +from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable from mathutils import Vector from bpy.types import Context, Object, Modifier, EditBone, Operator -from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable +from functools import lru_cache +from bpy.props import PointerProperty, IntProperty, StringProperty +from bpy.utils import register_class from ..core.logging_setup import logger from ..core.translations import t from ..core.dictionaries import bone_names @@ -615,4 +625,17 @@ def add_armature_modifier(mesh: Object, armature: Object): mesh.modifiers.remove(mod) modifier = mesh.modifiers.new('Armature', 'ARMATURE') - modifier.object = armature \ No newline at end of file + modifier.object = armature + +#Binary tools + + + + +#encoding FrooxEngine/C# types in binary: + + + + + + diff --git a/core/exporters/export_resonite.py b/core/exporters/export_resonite.py deleted file mode 100644 index 51c6c6f..0000000 --- a/core/exporters/export_resonite.py +++ /dev/null @@ -1,31 +0,0 @@ -import bpy -from typing import List, Optional -from ...core.common import get_active_armature -from bpy.types import Object, ShapeKey, Mesh, Context, Operator -from functools import lru_cache -from ...core.translations import t - -class AvatarToolKit_OT_ExportResonite(Operator): - bl_idname = 'avatar_toolkit.export_resonite' - bl_label = t("Importer.export_resonite.label") - bl_description = t("Importer.export_resonite.desc") - bl_options = {'REGISTER', 'UNDO'} - filepath: bpy.props.StringProperty() - - @classmethod - def poll(cls, context: Context): - if get_active_armature(context) is None: - return False - return True - - def execute(self, context: Context): - #settings stolen from cats. - bpy.ops.export_scene.gltf('INVOKE_AREA', - export_image_format = 'WEBP', - export_image_quality = 75, - export_materials = 'EXPORT', - export_animations = True, - export_animation_mode = 'ACTIONS', - export_nla_strips_merged_animation_name = 'Animation', - export_nla_strips = True) - return {'FINISHED'} \ No newline at end of file diff --git a/core/importer.py b/core/importer.py new file mode 100644 index 0000000..76f9b59 --- /dev/null +++ b/core/importer.py @@ -0,0 +1,55 @@ +import bpy + +# Importers which don't need much code should be added here, however if a importer needs alot of code +# Like the PMX and PMD importers, they should be added to their own files and referenced in the import_types str->lambda dictionary. + +#See below comments on how the system works. - @989onan + +import importlib.util +import os +import typing +from .import_pmx import import_pmx +from .import_pmd import import_pmd + +if importlib.util.find_spec("io_scene_valvesource") is not None: + #from .....scripts.addons.io_scene_valvesource.import_smd import SmdImporter #<- use this to check if your IDE is working properly. idfk + from io_scene_valvesource.import_smd import SmdImporter #ignore IDE bitching this is fine, trust me, also above comment should be okay to an IDE usually if set up right. ^_^ - @989onan + +def import_multi_files(method = None, directory: typing.Optional[str] = None, files: list[dict[str,str]] = None, filepath: typing.Optional[str] = ""): + if not files: + method(directory, filepath) + else: + for file in files: + fullpath = os.path.join(directory,os.path.basename(file["name"])) + print("run method!") + method(directory, fullpath) +#each import should map to a type. even in the case that multiple methods should import together, or have the same import method. Make sure the lambdas match so they get grouped together +#In the case of a file importer that takes only one file argument and each one needs individual import, use above method. (example of it in use is ".dae" format) +import_types: dict[str, typing.Callable[[str, list[dict[str,str]], str], None]] = { + "fbx": (lambda directory, files, filepath : bpy.ops.import_scene.fbx(files=files, directory=directory, filepath=filepath,automatic_bone_orientation=False,use_prepost_rot=False,use_anim=False)), + "smd": (lambda directory, files, filepath : eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)")), + "dmx": (lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)")), + "gltf": (lambda directory, files, filepath : bpy.ops.import_scene.gltf(files=files, filepath=filepath)), + "glb": (lambda directory, files, filepath : bpy.ops.import_scene.gltf(files=files, filepath=filepath)), + "qc": (lambda directory, files, filepath : eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)")), + "obj": (lambda directory, files, filepath : bpy.ops.wm.obj_import(files=files, directory=directory, filepath=filepath)), + "dae": (lambda directory, files, filepath : import_multi_files(directory=directory, files=files, filepath=filepath, method = (lambda directory, filepath: bpy.ops.wm.collada_import(filepath=filepath, auto_connect = True, find_chains = True, fix_orientation = True)))), + "3ds": (lambda directory, files, filepath : bpy.ops.import_scene.max3ds(files=files, directory=directory, filepath=filepath)), + "stl": (lambda directory, files, filepath : bpy.ops.import_mesh.stl(files=files, directory=directory, filepath=filepath)), + "mtl": (lambda directory, files, filepath : bpy.ops.wm.obj_import(files=files, directory=directory, filepath=filepath)), + "x3d": (lambda directory, files, filepath : bpy.ops.import_scene.x3d(files=files, directory=directory, filepath=filepath)), + "wrl": (lambda directory, files, filepath : bpy.ops.import_scene.x3d(files=files, directory=directory, filepath=filepath)), + "vmd": (lambda directory, files, filepath : import_multi_files(directory=directory, files=files, filepath=filepath, method = (lambda directory, filepath: bpy.ops.tuxedo.import_mmd_animation(directory=directory, filepath=filepath)))), + "vrm": (lambda directory, files, filepath: bpy.ops.import_scene.vrm(filepath=filepath)), + "pmx": (lambda directory, files, filepath : import_pmx(filepath)), + "pmd": (lambda directory, files, filepath : import_pmd(filepath)), + "animx": (lambda directory, files, filepath : bpy.ops.avatar_toolkit.animx_importer(directory=directory,files=files,filepath=filepath)), +} + +def concat_imports_filter(imports): + names = "" + for importer in imports.keys(): + names = names+"*."+importer+";" + return names + +imports = concat_imports_filter(import_types) \ No newline at end of file diff --git a/core/resonite_loader/common.py b/core/resonite_loader/common.py new file mode 100644 index 0000000..49ad0d9 --- /dev/null +++ b/core/resonite_loader/common.py @@ -0,0 +1,66 @@ +import ctypes +import typing +import struct +from io import BytesIO + +def writeNullable(data: BytesIO, value: = None): + + data.write(struct.pack("?", value == None)) + if(value == None): + return + data.write() + + + +def ReadCSharp_str(data: BytesIO) -> str: + charamount = read7bitEncoded_int(data) + string: str = data.read(charamount).decode('utf-8', errors="replace") + #print("read string: "+string) + return string + +def WriteCSharp_str(data: BytesIO, string: str) -> str: + write7bitEncoded_int(len(string)) + return data.write(string.encode("utf-8", errors="replace")) + +def read7bitEncoded_ulong(data: BytesIO) -> int: + num: int = int(0) + num2: int = 0 + flag: bool = True + + while (flag): + b: int = int(struct.unpack(' 0) + num |= ((b & 127) << num2) + num2 += 7 + if not flag: + break + + return num + +def read7bitEncoded_int(data: BytesIO) -> int: + num: int = int(0) + num2:int = int(0) + while (num2 != 35): + b: int = int(struct.unpack(' None: + while integer > int(0): + b: int = int(integer & int(127)) + integer >>= 7 + if integer > int(0): + b |= 128 + data.write(b) + if integer <= int(0): + return + +def write7bitEncoded_int(data: BytesIO, value: int) -> None: + num: int = int(value) + while(num >= int(128)): + data.write(int(num | int(128))) + num >>= 7 + data.Write(int(num)) diff --git a/core/resonite_loader/resonite_animx.py b/core/resonite_loader/resonite_animx.py new file mode 100644 index 0000000..ad6c4cf --- /dev/null +++ b/core/resonite_loader/resonite_animx.py @@ -0,0 +1,568 @@ +from __future__ import annotations +from os import replace +from re import S +from types import FrameType + +import lz4.block +from . import resonite_types +from . import common + +import typing +import struct +from io import BytesIO + + +KeyframeInterpolation: dict[str, int] = { + "Hold": 1, + "Linear": 2, + "Tangent": 3, + "CubicBezier": 4 +} + +class KeyFrame(): + time: resonite_types.float + interpolation: resonite_types.byte + value: resonite_types.ResoType + left_tan: resonite_types.ResoType + right_tan: resonite_types.ResoType + + + + def __init__(self): + self.time = resonite_types.float(0) + self.interpolation = resonite_types.byte(0) + + + def RequiresTangents(self) -> bool: + if KeyframeInterpolation[self.interpolation.x] == "Tangent" or KeyframeInterpolation[self.interpolation.x] == "CubicBezier": + return True + return False + +class ResoTrack(resonite_types.ResoType): + node: resonite_types.string + property: resonite_types.string + Owner: AnimX + FrameType: str + keyframes: list[KeyFrame] + + def __init__(self,FrameType): + self.FrameType = FrameType + self.keyframes = [] + self.node = resonite_types.string("") + self.property = resonite_types.string("") + + def write(self, data: BytesIO): + self.node.write(data) + self.property.write(data) + common.write7bitEncoded_ulong(data, len(self.keyframes)) + + def read(self, data:BytesIO): + self.node.read(data) + self.property.read(data) + + track_amount: int = int(common.read7bitEncoded_ulong(data)) + #print(track_amount) + for i in range(0, track_amount): + key: KeyFrame = KeyFrame() + key.value = eval(self.FrameType+"()") + self.keyframes.append(key) + + def removeKeyframe(self, time: float | int) -> bool: + """Takes a time and removes one with the same time""" + if (time < 0): + raise IndexError("Keyframe time cannot be lower than 0. Value: " + str(time)) + if(type(time) == float): + num: int = -1 + for num2 in range(0,len(self.keyframes)): + if (self.keyframes[num2].time.x == float(time)): + num = num2 + + if num == -1: + return False + + self.keyframes.remove(self.keyframes[num]) + return True + else: + if (int(time) >= len(self.keyframes)): + raise IndexError("Keyframe time cannot be bigger than the amount of keyframes. Value: " + str(time)) + self.keyframes.remove(self.keyframes[int(time)]) + + + + + def replaceKeyframe(self, keyframe: KeyFrame) -> bool: + """Takes a keyframe and replaces one with the same time""" + if (keyframe.time.x < 0): + raise IndexError("Keyframe time cannot be lower than 0. Value: " + str(keyframe.time.x)) + + + num: int = 0 + + if (keyframe.time.x == self.keyframes[self.GetKeyframeIndex(keyframe.time.x)].time.x): + num = len(self.keyframes) + else: + return False + + self.keyframes[num] = keyframe + return True + + def addKeyframe(self, keyframe: KeyFrame) -> int: + if (keyframe.time.x < 0): + raise IndexError("Keyframe time cannot be lower than 0. Value: " + str(keyframe.time.x)) + num: int + if (len(self.keyframes) == 0): + num = 0 + elif (keyframe.time.x >= self.keyframes[-1].time.x): + num = len(self.keyframes) + else: + num = self.GetKeyframeIndex(keyframe.time.x) + 1 + + self.keyframes.insert(num, keyframe) + + return num + + def GetKeyframeIndex(self, time:float | int)-> int: + if(type(time) == float): + if (len(self.keyframes) > 0): + num: int = 0 + if (self.keyframes[-1].time < float(time)): + num = len(self.keyframes) + + while (num < len(self.keyframes) and self.keyframes[num].time < time): + num += 1 + + return num - 1 + + return -1 + else: + return int(time) + + +class RawTrack(ResoTrack): + interval: resonite_types.float + + def __getattr__(self, name: str): + if name == "interval": + return self.Owner.interval + return super().__getattribute__(name) + + def __init__(self, FrameType): + super().__init__(FrameType) + self.interval = resonite_types.float(0) + + def write(self, data: BytesIO): + super().write(data) + self.interval.write(data) + for key in self.keyframes: + if self.FrameType == "resonite_types.string": + resonite_types.writeNullable(data, key.value) + else: + key.value.write(data) + + + def read(self, data:BytesIO): + super().read(data) + self.interval.read(data) + for key in self.keyframes: + if self.FrameType == "resonite_types.string": + resonite_types.readNullable(data, key.value) + else: + key.value.read(data) + + def addKeyframe(self, keyframe: KeyFrame) -> int: + num: int = super().addKeyframe(keyframe) + for i in range(0,len(self.keyframes)): + self.keyframes[i].time = i + return num + def removeKeyframe(self, time: float | int) -> bool: + success: bool = super().removeKeyframe(int(time)) + for i in range(0,len(self.keyframes)): + self.keyframes[i].time = i + return success + + + + +class DiscreteTrack(ResoTrack): + + def __init__(self, FrameType): + super().__init__(FrameType) + + def write(self, data: BytesIO): + super().write(data) + self.interval.write(data) + for key in self.keyframes: + if key.value == None: + key.value = eval(self.FrameType+"()") + if self.FrameType == "resonite_types.string": + resonite_types.writeNullable(data, key.value) + else: + key.value.write(data) + key.time.write(data) + + + def read(self, data:BytesIO): + super().read(data) + self.interval.read(data) + for key in self.keyframes: + if key.value == None: + key.value = eval(self.FrameType+"()") + if self.FrameType == "resonite_types.string": + resonite_types.readNullable(data, key.value) + else: + key.value.read(data) + key.time.read(data) + + def addKeyframe(self, keyframe: KeyFrame) -> int: + num: int = super().addKeyframe(keyframe) + return num + def removeKeyframe(self, time: float | int) -> bool: + success: bool = super().removeKeyframe(time) + return success + + + + +class CurveTrack(ResoTrack): + interpolations: bool + tangents: bool + sharedinterpolation: resonite_types.byte + + def __getattr__(self, name: str): + if name == "interpolations": + integerframe: int = self.keyframes[0].interpolation.x + for key in self.keyframes: + if key.interpolation.x != integerframe: + return True + elif name == "tangents": + for key in self.keyframes: + if key.RequiresTangents(): + return True + return super().__getattribute__(name) + + def __init__(self, FrameType): + super().__init__(FrameType) + self.sharedinterpolation = resonite_types.byte(-1) + self.interpolations = False + self.tangents = False + + def write(self, data: BytesIO): + super().write(data) + resonite_types.byte((1 if self.interpolations else 0) | (2 if self.tangents else 0)).write(data) + + + if(self.interpolations): + for key in self.keyframes: + key.interpolation.write(data) + else: + self.sharedinterpolation.write(data) + + for key in self.keyframes: + if key.value == None: + key.value = eval(self.FrameType+"()") + if self.FrameType == "resonite_types.string": + resonite_types.writeNullable(data, key.value) + else: + key.value.write(data) + key.time.write(data) + + if(self.tangents): + for key in self.keyframes: + if self.FrameType == "resonite_types.string": + resonite_types.writeNullable(data, key.left_tan) + resonite_types.writeNullable(data, key.right_tan) + else: + key.left_tan.write(data) + key.right_tan.write(data) + + def read(self, data:BytesIO): + super().read(data) + flags: int = struct.unpack(" 0 + tan: bool = (flags & 2) > 0 + + #print(str(interp)) + #print(str(tan)) + #print(flags) + #print(len(self.keyframes)) + + if(interp): + for key in self.keyframes: + #print("reading interp") + key.interpolation.read(data) + else: + self.sharedinterpolation.read(data) + + for key in self.keyframes: + if key.value == None: + key.value = eval(self.FrameType+"()") + if self.FrameType == "resonite_types.string": + resonite_types.readNullable(data, key.value) + else: + key.value.read(data) + key.time.read(data) + + if(tan): + for key in self.keyframes: + if self.FrameType == "resonite_types.string": + resonite_types.readNullable(data, key.left_tan) + resonite_types.readNullable(data, key.right_tan) + else: + key.left_tan.read(data) + key.right_tan.read(data) + + + + + + + + + +class BezierTrack(ResoTrack): + """PLACE HOLDER CLASS, DO NOT USE""" + + + def __init__(self, FrameType): + super().__init__(FrameType) + """PLACE HOLDER METHOD, DO NOT USE""" + #raise Exception("BezierTrack track type is unsupported in resonite's code") + + def write(self, data: BytesIO): + """PLACE HOLDER METHOD, DO NOT USE""" + raise Exception("BezierTrack track type is unsupported in resonite's code") + def read(self, data:BytesIO): + """PLACE HOLDER METHOD, DO NOT USE""" + raise Exception("BezierTrack track type is unsupported in resonite's code") + + def removeKeyframe(self, keyframe: KeyFrame) -> bool: + """PLACE HOLDER METHOD, DO NOT USE""" + raise Exception("BezierTrack track type is unsupported in resonite's code") + + def replaceKeyframe(self, keyframe: KeyFrame) -> bool: + """PLACE HOLDER METHOD, DO NOT USE""" + raise Exception("BezierTrack track type is unsupported in resonite's code") + def addKeyframe(self, keyframe: KeyFrame) -> int: + """PLACE HOLDER METHOD, DO NOT USE""" + raise Exception("BezierTrack track type is unsupported in resonite's code") + + def GetKeyframeIndex(self, time:float)-> int: + """PLACE HOLDER METHOD, DO NOT USE""" + raise Exception("BezierTrack track type is unsupported in resonite's code") +#This is weird, but thank you python - @989onan +TrackTypes: list[str] = [ + "RawTrack", + "DiscreteTrack", + "CurveTrack", + "BezierTrack" +] + +#TODO: add all types here +#wooooo - @989onan +elementTypes: list[str] = [ + "resonite_types.bool", + "resonite_types.bool2", + "resonite_types.bool3", + "resonite_types.bool4", + "resonite_types.byte", + "resonite_types.ushort", + "resonite_types.uint", + "resonite_types.ulong", + "resonite_types.sbyte", + "resonite_types.short", + "resonite_types.int", + "resonite_types.long", + "resonite_types.int2", + "resonite_types.int3", + "resonite_types.int4", + "resonite_types.uint2", + "resonite_types.uint3", + "resonite_types.uint4", + "resonite_types.long2", + "resonite_types.long3", + "resonite_types.long4", + "resonite_types.float", + "resonite_types.float2", + "resonite_types.float3", + "resonite_types.float4", + "resonite_types.floatQ", + "resonite_types.float2x2", + "resonite_types.float3x3", + "resonite_types.float4x4", + "resonite_types.double", + "resonite_types.double2", + "resonite_types.double3", + "resonite_types.double4", + "resonite_types.doubleQ", + "resonite_types.double2x2", + "resonite_types.double3x3", + "resonite_types.double4x4", + "resonite_types.color", + "resonite_types.color32", + "resonite_types.string" + ] + + +most_recent_AnimX_vers: int = 1 +import lzma#HALLLOOOYAH HALLOYAH!! - @989onan +class AnimX(): + + + """ + To use Raw Track properly, please set interval (seconds between frames) after creating.\n + Represents data to be written to or read from an AnimX file.\n + default interval to use would be 30. + """ + + file_version: resonite_types.int + track_amount: resonite_types.int + global_duration: resonite_types.float + name: resonite_types.string + + tracks: list[ResoTrack] + + interval: resonite_types.float + + def __init__(self): + self.tracks = [] + self.file_version = resonite_types.int() + self.track_amount = resonite_types.int() + self.global_duration = resonite_types.float() + self.name = resonite_types.string() + self.interval = resonite_types.float(1/25) #default value + pass + + @classmethod + def decompress_lzma(cls,data, format, filters) -> list: + results = [] + while True: + decomp = lzma.LZMADecompressor(format, None, filters) + try: + res = decomp.decompress(data) + except lzma.LZMAError: + if results: + break # Leftover data is not a valid LZMA/XZ stream; ignore it. + else: + raise # Error on the first iteration; bail out. + results.append(res) + data = decomp.unused_data + if not data: + break + if not decomp.eof: + raise lzma.LZMAError("Compressed data ended before the end-of-stream marker was reached") + return b"".join(results) + + def read(self, file: str) -> bool: + """ + Takes an absolute file path and reads a binary animx file with it, and populates this class object with the data. + """ + with open(file, 'rb') as filecontents: + data: BytesIO = BytesIO(filecontents.read()) + magic_word = common.ReadCSharp_str(data) + if magic_word != 'AnimX': + print("AnimX != "+magic_word) + return False + self.file_version.read(data) + if self.file_version.x > 1: + raise Exception("AnimX version is higher than the supported one") + + self.track_amount.x = common.read7bitEncoded_ulong(data) + self.global_duration.read(data) + print("track amount: "+str(self.track_amount.x)) + print("file vers: "+str(self.file_version.x)) + + self.name.read(data) + print("name: "+self.name.x) + + match (struct.unpack('> 1 + else: + trackType2 = int(struct.unpack(' bool: + """ + Takes an absolute file path and writes a binary animx file into it's contents, replacing them using this class's data. + """ + with open(file, 'rb') as filecontents: + data: BytesIO = BytesIO(filecontents) + common.WriteCSharp_str(data, 'AnimX') + self.file_version.x = most_recent_AnimX_vers #we wanna write an up to date file version type. + self.file_version.write(data) + + self.track_amount.x = len(self.tracks) + common.write7bitEncoded_ulong(self.track_amount.x) + self.global_duration.write(data) + + + self.name.write(data) + + data.write(struct.pack(' ResoTrack: + Track: ResoTrack = eval(TrackTypes[trackType2]+"(value_type)") + #print(value_type) + #print(type(Track)) + Track.read(data) + return Track + diff --git a/core/resonite_loader/resonite_types.py b/core/resonite_loader/resonite_types.py new file mode 100644 index 0000000..dbdaf7f --- /dev/null +++ b/core/resonite_loader/resonite_types.py @@ -0,0 +1,665 @@ +import typing +from io import BytesIO +import struct +from . import common + +class ResoType(): + + + + + def __init__(self): + pass + + def write(self, data: BytesIO): + pass + + def read(cls, data: BytesIO): + pass + +def writeNullable(data: BytesIO, value: ResoType = None): + + data.write(struct.pack("?", value == None)) + if(value == None): + return + value.write(data) + +def readNullable(data: BytesIO, value: ResoType = None): + + hasval: bool = struct.unpack("?", data.read(1)) + if not hasval: + return + value.read(data) +#These below are collection of the basic resonite typing made from C#. This is in order to store data in a sane way and decode/encode it. + +class color(ResoType): + r: float = 0 + g: float = 0 + b: float = 0 + a: float = 0 + + + def __init__(self): + pass + + def write(self, data: BytesIO): + data.write(struct.pack(" str: + return self.x + + def __init__(self,value=""): + self.x = value + + def write(self, data: BytesIO): + common.WriteCSharp_str(data,self.x) + + def read(self,data): + + self.x = common.ReadCSharp_str(data) + + +class byte(ResoType): + x: int = 0 + + def __int__(self): + return self.x + + def __init__(self,value=0): + self.x = value + + def write(self, data: BytesIO): + data.write(struct.pack(" bool: + return self.x + def __init__(self,value=False): + self.x = value + + def write(self, data: BytesIO): + data.write(struct.pack("?", self.x)) + + def read(self,data): + self.x = struct.unpack("?", data.read(1))[0] + +class bool2(bool): + y: bool = False + + def __init__(self): + pass + + def read(self,data: BytesIO): + byte: int = int(struct.unpack(" 0 + self.y = (byte & 2) > 0 + + def createflags(self) -> int: + flags: int = int(0) + flags |= (1 if self.x else 0) + flags |= (2 if self.y else 0) + return flags + + def write(self, data: BytesIO): + data.write(struct.pack(" 0 + self.y = (byte & 2) > 0 + self.z = (byte & 4) > 0 + + def createflags(self) -> int: + flags: int = int(0) + flags |= (1 if self.x else 0) + flags |= (2 if self.y else 0) + flags |= (3 if self.z else 0) + return flags + + def write(self, data: BytesIO): + data.write(struct.pack(" 0 + self.y = (byte & 2) > 0 + self.z = (byte & 4) > 0 + self.w = (byte & 8) > 0 + + def createflags(self) -> int: + flags: int = int(0) + flags |= (1 if self.x else 0) + flags |= (2 if self.y else 0) + flags |= (4 if self.z else 0) + flags |= (8 if self.w else 0) + return flags + + def write(self, data: BytesIO): + data.write(struct.pack(" bool: + armature = get_selected_armature(context) + return armature is not None and is_valid_armature(armature) + + def execute(self, context: Context) -> set: + armature = get_selected_armature(context) + if not armature: + self.report({'WARNING'}, t("Tools.no_armature_selected")) + return {'CANCELLED'} + + translate_bone_fails = 0 + untranslated_bones = set() + + reverse_bone_lookup = dict() + for (preferred_name, name_list) in bone_names.items(): + for name in name_list: + reverse_bone_lookup[name] = preferred_name + + resonite_translations = { + 'hips': "Hips", + 'spine': "Spine", + 'chest': "Chest", + 'neck': "Neck", + 'head': "Head", + 'left_eye': "Eye.L", + 'right_eye': "Eye.R", + 'right_leg': "UpperLeg.R", + 'right_knee': "Calf.R", + 'right_ankle': "Foot.R", + 'right_toe': 'Toes.R', + 'right_shoulder': "Shoulder.R", + 'right_arm': "UpperArm.R", + 'right_elbow': "ForeArm.R", + 'right_wrist': "Hand.R", + 'left_leg': "UpperLeg.L", + 'left_knee': "Calf.L", + 'left_ankle': "Foot.L", + 'left_toe': "Toes.L", + 'left_shoulder': "Shoulder.L", + 'left_arm': "UpperArm.L", + 'left_elbow': "ForeArm.L", + 'left_wrist': "Hand.R", + + 'pinkie_1_l': "pinkie1.L", + 'pinkie_2_l': "pinkie2.L", + 'pinkie_3_l': "pinkie3.L", + 'ring_1_l': "ring1.L", + 'ring_2_l': "ring2.L", + 'ring_3_l': "ring3.L", + 'middle_1_l': "middle1.L", + 'middle_2_l': "middle2.L", + 'middle_3_l': "middle3.L", + 'index_1_l': "index1.L", + 'index_2_l': "index2.L", + 'index_3_l': "index3.L", + 'thumb_1_l': "thumb1.L", + 'thumb_2_l': "thumb2.L", + 'thumb_3_l': "thumb3.L", + + 'pinkie_1_r': "pinkie1.R", + 'pinkie_2_r': "pinkie2.R", + 'pinkie_3_r': "pinkie3.R", + 'ring_1_r': "ring1.R", + 'ring_2_r': "ring2.R", + 'ring_3_r': "ring3.R", + 'middle_1_r': "middle1.R", + 'middle_2_r': "middle2.R", + 'middle_3_r': "middle3.R", + 'index_1_r': "index1.R", + 'index_2_r': "index2.R", + 'index_3_r': "index3.R", + 'thumb_1_r': "thumb1.R", + 'thumb_2_r': "thumb2.R", + 'thumb_3_r': "thumb3.R" + } + + context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='EDIT') + + bpy.ops.object.mode_set(mode='OBJECT') + bone.name = re.compile(re.escape(""), re.IGNORECASE).sub("",bone.name) #remove "NOIK" from bones before translating again, in case an update was done that fixes a translation. + for bone in armature.data.bones: + if simplify_bonename(bone.name) in reverse_bone_lookup and reverse_bone_lookup[simplify_bonename(bone.name)] in resonite_translations: + bone.name = resonite_translations[reverse_bone_lookup[simplify_bonename(bone.name)]] + else: + untranslated_bones.add(bone.name) + + bone.name = bone.name+"" + translate_bone_fails += 1 + + bpy.ops.object.mode_set(mode='OBJECT') + + if translate_bone_fails > 0: + self.report({'INFO'}, t("Tools.bones_translated_with_fails").format(translate_bone_fails=translate_bone_fails)) + else: + self.report({'INFO'}, t("Tools.bones_translated_success")) + + return {'FINISHED'} + + +def makeorexistingfcurve(action: bpy.types.Action,data_path: str,action_group: str, index=0) -> bpy.types.FCurve: + fcurve = action.fcurves.find(data_path=data_path,index=index) + if fcurve == None: + return action.fcurves.new(data_path,action_group=action_group,index=index) + else: + print("fcurve with data \""+data_path+"\" already exists") + return fcurve + +@register_wrap +class AvatarToolKit_OT_AnimX_Importer(Operator,bpy_extras.io_utils.ImportHelper): + bl_idname = 'avatar_toolkit.animx_importer' + bl_label = t('Tools.animx_importer.label') + bl_description = t('Tools.animx_importer.desc') + bl_options = {'REGISTER', 'UNDO'} + + #fps = bpy.props.FloatProperty(default=25) #25 fps + + filter_glob: bpy.props.StringProperty( + default="*.animx", + options={'HIDDEN'} + ) + files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'}) + filepath: bpy.props.StringProperty() + + directory:bpy.props.StringProperty(subtype='DIR_PATH') + + @classmethod + def poll(cls, context: Context) -> bool: + return context.active_object != None + + def execute(self, context: Context) -> set: + + Froox_animations: list[resonite_animx.AnimX] = [] + + #decoding using self contained library: + files = [file.name for file in self.files] + #files.append(self.filepath) + for file in files: + froox_animation: resonite_animx.AnimX = resonite_animx.AnimX() + froox_animation.interval.x = 30 #should be default fps + froox_animation.read(file = os.path.join(self.directory,file)) + Froox_animations.append(froox_animation) + + #TODO: Allow multiple targets and setting animations to each one somehow with an interface. + target: bpy.types.Object = context.active_object + if target.animation_data == None: + target.animation_data_create() + + #Load data into Blender Animations. + for froox_animation in Froox_animations: + action: bpy.types.Action = bpy.data.actions.new(froox_animation.name.x) + target.animation_data.action = action + action.use_fake_user = True + for track in froox_animation.tracks: + data_path: str + actualproperty: str = track.property.x + + match(actualproperty): + case("Position"): + actualproperty = "location" + case("Rotation"): + actualproperty = "rotation_quaternion" + case("Scale"): + actualproperty = "scale" + data_path = actualproperty + + if target.type == "ARMATURE": + data_path = "pose.bones[\""+track.node.x+"\"]."+data_path + + for posebone in target.pose.bones: + posebone.rotation_mode = "QUATERNION" + + print("reading frames for "+data_path) + if(track.FrameType == "resonite_types.double" or track.FrameType == "resonite_types.double"): + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=0),".x") + elif (track.FrameType == "resonite_types.float3" or track.FrameType == "resonite_types.double3"): + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=0),".x") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=2),".y") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=1),".z") + elif (track.FrameType == "resonite_types.float4" or track.FrameType == "resonite_types.double4"): + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=0),".x") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=1),".y") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=2),".z") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=3),".w") + elif (track.FrameType == "resonite_types.doubleQ" or track.FrameType == "resonite_types.floatQ"): + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=3),".w") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=0),".x") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=2),".y") + self.readTrackData(track,makeorexistingfcurve(action=action,data_path=data_path,action_group=track.node.x,index=1),".z") + else: + continue + return {'FINISHED'} + + def readTrackData(self,track: resonite_animx.ResoTrack, fcurve_reso: bpy.types.FCurve, valuetype: str = ""): + tracktype = type(track) + match(tracktype): + case (resonite_animx.RawTrack): + rawtrack: resonite_animx.RawTrack = track + + + + fcurve_reso.keyframe_points.add(count=len(rawtrack.keyframes)) + # populate points + fcurve_reso.keyframe_points.foreach_set("co", [x for co in zip([frame.time.x*track.Owner.interval.x for frame in rawtrack.keyframes], [eval("frame.value"+valuetype) for frame in rawtrack.keyframes]) for x in co]) + fcurve_reso.update() + + case (resonite_animx.DiscreteTrack): + discretetrack: resonite_animx.DiscreteTrack = track + + fcurve_reso.keyframe_points.add(count=len(discretetrack.keyframes)) + # populate points + fcurve_reso.keyframe_points.foreach_set("co", [x for co in zip([frame.time.x*track.Owner.interval.x for frame in discretetrack.keyframes], [eval("frame.value"+valuetype) for frame in discretetrack.keyframes]) for x in co]) + fcurve_reso.update() + + case(resonite_animx.CurveTrack): + curvetrack: resonite_animx.CurveTrack = track + + fcurve_reso.keyframe_points.add(count=len(curvetrack.keyframes)) + # populate points + fcurve_reso.keyframe_points.foreach_set("co", [x for co in zip([frame.time.x*track.Owner.interval.x for frame in curvetrack.keyframes], [eval("frame.value"+valuetype) for frame in curvetrack.keyframes]) for x in co]) + interp: bool = curvetrack.tangents + #print("has tangents? "+str(interp)) + + for idx,frame in enumerate(curvetrack.keyframes): + + if interp: + fcurve_reso.keyframe_points[idx].handle_left = float(eval("frame.left_tan"+valuetype)) + fcurve_reso.keyframe_points[idx].handle_right = float(eval("frame.right_tan"+valuetype)) + fcurve_reso.keyframe_points[idx].interpolation = "BEZIER" + fcurve_reso.keyframe_points[idx].easing = "EASE_IN" + fcurve_reso.update() + + case(resonite_animx.BezierTrack): + beziertrack: resonite_animx.BezierTrack = track + # Bezier is not supported rn, ignore. + case _: + print("invalid track type, ignoring") + print(track) + + + + + + + + + + + + diff --git a/functions/resonite_functions.py b/functions/resonite_functions.py new file mode 100644 index 0000000..e69de29 diff --git a/functions/uv_tools.py b/functions/uv_tools.py new file mode 100644 index 0000000..66e6aae --- /dev/null +++ b/functions/uv_tools.py @@ -0,0 +1,299 @@ +from typing import TypedDict +import bpy +from bpy.types import Operator, Object, Context, Mesh, MeshUVLoopLayer +import bmesh +import numpy as np +import math +from ..functions.translations import t +from ..core.register import register_wrap + +class GenerateLoopTreeResult(TypedDict): + tree: dict[str, set[str]] + selected_loops: dict[str,list[int]] + selected_verts: dict[str,int] + +@register_wrap +class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator): + bl_idname = "avatar_toolkit.align_uv_edges_to_target" + bl_label = t("avatar_toolkit.align_uv_edges_to_target.label") + bl_description = t("avatar_toolkit.align_uv_edges_to_target.desc") + bl_options = {'REGISTER', 'UNDO'} + + + + #all selected objects need to be meshes for this to work - @989onan + @classmethod + def poll(cls, context: Context): + if not ((context.view_layer.objects.active is not None) and (len(context.view_layer.objects.selected) > 0)): + return False + if context.mode != "EDIT_MESH": + return False + for obj in context.view_layer.objects.selected: + if obj.type != "MESH": + return False + if not context.space_data: + return False + if not context.space_data.show_uvedit: + return False + if context.scene.tool_settings.use_uv_select_sync: + return False + return True + + def execute(self, context: Context): + + + target: str = context.view_layer.objects.active.name #The object which we want to align every other selected object's selected UV vertex line to + + sources: list[str] = [i.name for i in context.view_layer.objects.selected] #The objects which we want to align their selected UV lines to the target's UV line + + prev_mode: str = bpy.context.object.mode + bpy.ops.object.mode_set(mode='OBJECT') + + + def generate_loop_tree(obj_name: str) -> GenerateLoopTreeResult: + print("Finding selected line for: \""+obj_name+"\"!") + + + vert_target_loops: dict[str,list[int]] = {} + vert_target_verts: dict[str,int] = {} + + me: Mesh = bpy.data.objects[obj_name].data + uv_lay: MeshUVLoopLayer = me.uv_layers.active + bm: bmesh.types.BMesh = bmesh.new() + bm.from_mesh(me) + bm.verts.ensure_lookup_table() + + + + # To explain: + # So loops in UV maps are X polygons that make up a face (So a MeshLoop represent a face and each vertex on that face is in order) + # + # For some preknowledge: + # When a mesh is UV unwrapped, if a vertice is shared by two different faces on the model in the viewport and the vertice of both faces are in + # the same position on the UV map, then it considers it one point and the user can move it + # (is why the uv map doesn't split apart when you try to move a vertex because that would be annoying) + # + # The problem: + # The problem is that the data for whether the uv corners of two faces that share a vertex physically being connected and selected as one vertex on the uv map does not exist + # Though thankfully, blender forcibly (whether you like it or not) merges vertices of a uv map if the vertex of two different faces are actually shared in the UI, + # allowing for the moving of vertices of 4 faces connected by a single vertex. Behavior every normal blender user is familiar with. + # + # The solution + # We can use this to our advantage, by finding vertices on the uv map that share the same coridinate as another vertex that is also selected. + # that way we can group each pair shared in a line as the same vertex, and identify the line using these pairs and using the data that says for certain + # that two vertices share the same face loop, and therefore are connected. + + #hmmm real stupid grimlin hours with this one. Using a string as the index of a dictionary of loop corners that end up on the same coordinate + + for k,i in enumerate(uv_lay.vertex_selection): #go through the selected vertices on object. + if (i.value == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False): #filter out vertices that are hidden from UV port + key = np.array(uv_lay.uv[k].vector[:]) + key = key.round(decimals=5) #make a key that is the position of a selected vertex + + if str(key) not in vert_target_loops: + vert_target_loops[str(key)] = [] #if the vertex's position is not a list yet, add it. + vert_target_loops[str(key)].append(k) #Basically, group vertices based on their position on a UV map as a list. + vert_target_verts[str(key)] = me.loops[k].vertex_index #associate the index of the physical vertex in real space with the coordinate of the uv vertices that share a position (Basically associate UV vert with real vert) + if len(vert_target_loops) > 4000: #This usually indicates that the user has a bunch of crap selected. + self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.too_much")) + return + print("Finding connections on line for \""+obj_name+"\"!") + me.validate() + + bm = bmesh.new() + bm.from_mesh(me) + + + #print(vert_target_loops) + #print(vert_target_verts) + tree: dict[str, set[str]] = {} + selected_verts = np.hstack(list(vert_target_loops.values())) + #print(selected_verts) + bm.verts.ensure_lookup_table() + for uvcoordsstr in vert_target_loops: + + uv_lay = me.uv_layers.active + + + #before this section, each vert_target_loops is just groupings of vertices that share coordinates. + # Using the data that determines UV face corners (uvloops) that are associated with the real vertex, + # and the uv face corners (loops) that are on the same faces as the vertices that share coordinates in + # vert_target_loops, we can now identify them + #TL;DR: pairs of vertices that share cooridinates (chain links) find their buddies (make chain connected) + + # Someone explain this better than me if you can please - @989onan + extension_loops = [] + loops = bm.verts[vert_target_verts[uvcoordsstr]].link_loops + loops_indexes = [i.index for i in loops] + for loop in vert_target_loops[uvcoordsstr]: + if loop in loops_indexes: + loop_obj = loops[loops_indexes.index(loop)] + extension_loops.append(loop_obj.link_loop_next.index) + extension_loops.append(loop_obj.link_loop_prev.index) + + + + + + #make a tree out of the vertices we identified as sharing faces with the vertices in vert_target_loops, and then link them together in a dictionary. + #the order of this dictionary is unknown. + # Someone explain this better than me if you can please - @989onan + tree[uvcoordsstr] = set() + + for i in extension_loops: + if i in selected_verts: + key = np.array(uv_lay.uv[i].vector[:]) + key = key.round(decimals=5) + tree[uvcoordsstr].add(str(key)) + + if uvcoordsstr in tree: + if len(tree[uvcoordsstr]) > 2: + self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.need_a_line").format(obj=obj_name)) + return {'FINISHED'} + + uv_lay = me.uv_layers.active + for uvcoordstr in vert_target_loops: + for loop in vert_target_loops[uvcoordstr]: + uv_lay.vertex_selection[loop].value = True + + + bm.free() + me.validate() + print("found UV line connections for \""+obj_name+"\":") + #print(tree) + + return {"tree":tree,"selected_loops":vert_target_loops,"selected_verts":vert_target_verts} + + + + #This function uses the previous point to find the next point based on connected loops and faces. + def sort_uv_tree(originaltree: dict[str, set[str]], obj_name: str): + sortedtree: dict[str, set[str]] = originaltree.copy() + startpoints: list[str] = [] + for i in sortedtree: + if len(sortedtree[i]) < 2: + startpoints.append(i) + + if len(startpoints) != 2: + self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.need_a_line").format(obj=obj_name)) + return + + a_list1 = startpoints[0].replace(", "," ").replace("[","").replace("]","").split() + map_object1 = map(float, a_list1) + uvcoords1 = list(map_object1) + a_list2 = startpoints[1].replace(", "," ").replace("[","").replace("]","").split() + map_object2 = map(float, a_list2) + uvcoords2 = list(map_object2) + + cursor = context.space_data.cursor_location + + startpoint = None + if math.sqrt( (((uvcoords1[0]) - (cursor[0])) **2) + (((uvcoords1[1]) - (cursor[1])) **2) ) > math.sqrt( (((uvcoords2[0]) - (cursor[0])) **2) + (((uvcoords2[1]) - (cursor[1])) **2) ): + startpoint = startpoints[0] + else: + startpoint = startpoints[1] + + #Wew my first actual recursive sort! - @989onan + def recursive_sort_uv_tree(point: str, sortedfinal: list[str]): + #print("appending "+point) + sortedfinal.append(point) + + new_point: str = "" + for i in sortedtree: + if point in sortedtree[i]: + new_point = i + removed_value = sortedtree.pop(i) + #print(removed_value) + break + + if new_point == "": + print("BROKE OUT OF SORTING, FINAL TREE (Should be empty, if not you errored here!):") + print(sortedtree) + + return sortedfinal + + return recursive_sort_uv_tree(new_point, sortedfinal) + + array = [] + + sortedtree.pop(startpoint) + return recursive_sort_uv_tree(startpoint, array) + + def lerp(v0, v1, t): + return v0 + t * (v1 - v0) + + + target_data: GenerateLoopTreeResult = generate_loop_tree(target) + sorted_target_tree = sort_uv_tree(target_data["tree"], target) + print("sorted target.") + #print(sorted_target_tree) + + for source in sources: + if source == target: + continue + + #create our list of points that is a chain. then sort the chain into the correct order based on connections of vertices and the faces that the vertices make up in the UV map. + try: + source_data = generate_loop_tree(source) + sorted_source_tree = sort_uv_tree(source_data["tree"], source) + print("Sorted source "+source) + print(sorted_source_tree) + + vertex_factor = float(len(sorted_target_tree)-1) / (float(len(sorted_source_tree)-1)) + + print(str(vertex_factor)+" = "+str(float(len(sorted_target_tree)-1)) + " / " + str((float(len(sorted_source_tree)-1)))+")") + except Exception as e: + print(e) + return {'FINISHED'} + + for k,i in enumerate(sorted_source_tree): + + try: + #find where we are on the target edges, to interpolate the current point we're placing along the target point's line. + progress_along_edge = (float(k)*vertex_factor) + previous_vertex_index = math.floor(progress_along_edge) + next_vertex_index = math.ceil(progress_along_edge) + + + #find the uv coordinates of the previous and next points on the target uv line. + a_list1 = sorted_target_tree[previous_vertex_index].replace(", "," ").replace("[","").replace("]","").split() + map_object1 = map(float, a_list1) + previous_point = list(map_object1) + a_list2 = sorted_target_tree[next_vertex_index].replace(", "," ").replace("[","").replace("]","").split() + map_object2 = map(float, a_list2) + next_point = list(map_object2) + + + + #create a point between these two values that represents a decimal 0-1 going where we are to where we are going between the two current points on the edge we are targeting this whole shebang with. + progress_between_points = progress_along_edge - int(progress_along_edge) + lerped_point = [lerp(previous_point[0],next_point[0],progress_between_points),lerp(previous_point[1],next_point[1],progress_between_points)] + + #grab our uv face corners for each uv coord that we saved. + #Since each face is considered separate internally, we have to treat each connected face to a vertex in a uv map as separate entities/vertexes. + #basically pretend they are split apart. + uv_face_corners = source_data["selected_loops"][i] + #print("doing from vertex "+str(previous_vertex_index)+" to "+str(next_vertex_index)+" total progress: "+str(progress_along_edge)) + + + + me: Mesh = bpy.data.objects[source].data + me.validate() + bm: bmesh.types.BMesh = bmesh.new() + bm.from_mesh(me) + uv_lay: MeshUVLoopLayer = me.uv_layers.active + bm.verts.ensure_lookup_table() + for corner in uv_face_corners: + uv_lay.uv[corner].vector = lerped_point #put the vertcies at the point we calculated. + except: + print("This is probably fine? - @989onan") #TODO: What happened here? The magic of making code so complex you forget if this is even an issue. - @989onan + #Note: The above print statement may require reading what this entire operator does before understanding if the above print statment means anything + #so if you value your time, just do a bunch of tests until the above print stament is ran or something + #if in doubt and you cannot get it to run, just spend some time trying to understand the operator - @989onan + + print("Finished mesh \""+source+"\" for UV's") + + + + bpy.ops.object.mode_set(mode=prev_mode) + return {'FINISHED'} \ No newline at end of file