Merge pull request #83 from Yusarina/reeee

Reeee
This commit is contained in:
Yusarina
2024-12-16 13:10:51 +00:00
committed by GitHub
10 changed files with 1982 additions and 33 deletions
+1
View File
@@ -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/"
],
+24 -1
View File
@@ -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
@@ -616,3 +626,16 @@ def add_armature_modifier(mesh: Object, armature: Object):
modifier = mesh.modifiers.new('Armature', 'ARMATURE')
modifier.object = armature
#Binary tools
#encoding FrooxEngine/C# types in binary:
-31
View File
@@ -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'}
+55
View File
@@ -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)
+66
View File
@@ -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('<B', data.read(1))[0])
flag = ((b & 128) > 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('<B', data.read(1))[0])
num |= int(b & 127) << num2
num2 += 7
if ((b & 128) == 0):
return num
return -1
def write7bitEncoded_ulong(data: BytesIO, integer: int) -> 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))
+568
View File
@@ -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("<B",data.read(1))[0]
interp: bool = (flags & 1) > 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('<B', data.read(1))[0]):
case 0:
pass
case 1:
from lz4.frame import decompress #why do you have to be a wheel? - @989onan
data = BytesIO(decompress(data.read()))
case 2:
filters = [
{"id" : lzma.FILTER_LZMA1, #idfk man - @989onan
"dict_size" : 2097152,
"lc" : 3,
"lp" : 0,
"pb" : 2, # private static int posStateBits = 2; //<-froox engine derived.
"mode" : lzma.MODE_NORMAL,
"nice_len" : 32, # private static int numFastBytes = 32; //<-froox engine derived.
"mf" : lzma.MF_BT4,
},
]
data.read(5) #fuck off headers - @989onan
data.read(8) #fuck off stream headers - @989onan
data.read(8) #fuck off stream headers - @989onan
filelmza: bytes = bytes(AnimX.decompress_lzma(data.read(), lzma.FORMAT_RAW, filters))
#print("binary below:")
#print("b'{}'".format(''.join('\\x{:02x}'.format(b) for b in filelmza[:100])))
data = BytesIO(filelmza)
case _:
raise Exception("Invalid encoding")
for i in range(0,self.track_amount.x):
trackType2: int = 0
num4: int = 0
if (self.file_version == 0):
b: int = int(struct.unpack('<B', data.read(1))[0])
num3: int = int(b & 1)
trackType: int = 0
if (num3 != 0):
if (num3 != 1):
raise Exception("[InvalidDataException]: Invalid track type data: " + str(b))
trackType = 2
else:
trackType = 0
trackType2 = trackType
num4 = b >> 1
else:
trackType2 = int(struct.unpack('<B', data.read(1))[0])
num4 = int(struct.unpack('<B', data.read(1))[0])
try:
animationTrack = AnimX.GetTrackType(trackType2, elementTypes[num4], data)
animationTrack.Owner = self
self.tracks.append(animationTrack)
except:
raise Exception("[InvalidDataException]: element type exception, beyond range: "+str(num4))
return True
def write(self, file: str) -> 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('<B', 0)) #default encoding, so we don't have to use lzma.
for i in range(0,self.track_amount.x):
data.write(struct.pack('<B', TrackTypes.index(type(self.tracks[i]))))
data.write(struct.pack('<B', elementTypes.index(self.tracks[i].FrameType)))
self.tracks[i].write(data)
return True
@classmethod
def GetTrackType(cls, trackType2: int, value_type: str, data: BytesIO) -> ResoTrack:
Track: ResoTrack = eval(TrackTypes[trackType2]+"(value_type)")
#print(value_type)
#print(type(Track))
Track.read(data)
return Track
+665
View File
@@ -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("<ffff", self.r, self.g, self.b, self.a))
def read(self,data):
self.r, self.g, self.b, self.a = struct.unpack("<ffff", data.read(4*4))[0]
class color32(ResoType):
r: int = 0
g: int = 0
b: int = 0
a: int = 0
def __init__(self):
pass
def write(self, data: BytesIO):
data.write(struct.pack("<BBBB", self.r, self.g, self.b, self.a))
def read(self,data):
self.r, self.g, self.b, self.a = struct.unpack("<BBBB", data.read(4))[0]
class string(ResoType):
x: str
def __str__(self) -> 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("<B", self.x))
def read(self,data):
self.x = struct.unpack("<B", data.read(1))[0]
class sbyte(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("<b", self.x))
def read(self,data):
self.x = struct.unpack("<b", data.read(1))[0]
class ushort(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("<H", self.x))
def read(self,data):
self.x = struct.unpack("<H", data.read(2))[0]
class short(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("<h", self.x))
def read(self,data):
self.x = struct.unpack("<h", data.read(2))[0]
class bool(ResoType):
x: bool = False
def __bool__(self) -> 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("<B",data.read(1))[0])
self.x = (byte & 1) > 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("<B", self.createflags()))
class bool3(bool2):
z: bool = False
def __init__(self):
pass
def read(self,data):
byte: int = int(struct.unpack("<B",data.read(1))[0])
self.x = (byte & 1) > 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("<B", self.createflags()))
class bool4(bool3):
w: bool = False
def __init__(self):
pass
def read(self,data: BytesIO):
byte: int = int(struct.unpack("<B",data.read(1))[0])
self.x = (byte & 1) > 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("<B", self.createflags()))
class int(ResoType):
x: int = 0
def __int__(self):
return self.x
def __init__(self,value=0):
self.x = value
def read(self,data: BytesIO):
self.x = struct.unpack("<i", data.read(4))[0]
def write(self, data: BytesIO):
data.write(struct.pack("<i", self.x))
class int2(int):
y: int = 0
def __init__(self):
pass
def read(self,data: BytesIO):
super().write(data)
self.y = struct.unpack("<i", data.read(4))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<i", self.y))
class int3(int2):
z: int = 0
def __init__(self):
pass
def read(self,data: BytesIO):
super().write(data)
self.z = struct.unpack("<i", data.read(4))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<i", self.z))
class int4(int3):
w: int = 0
def __init__(self):
pass
def read(self,data: BytesIO):
super().read(data)
self.w = struct.unpack("<i", data.read(4))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<i", self.w))
class uint(ResoType):
x: int = 0
def __int__(self):
return self.x
def __init__(self,value=0):
self.x = value
def read(self,data: BytesIO):
self.x = struct.unpack("<I", data.read(4))[0]
def write(self, data: BytesIO):
data.write(struct.pack("<I", self.x))
class uint2(uint):
y: int = 0
def __init__(self):
pass
def read(self,data: BytesIO):
super().read(data)
self.y = struct.unpack("<I", data.read(4))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<I", self.y))
class uint3(uint2):
z: int = 0
def __init__(self):
pass
def read(self,data: BytesIO):
super().read(data)
self.z = struct.unpack("<I", data.read(4))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<I", self.z))
class uint4(uint3):
w: int = 0
def __init__(self):
pass
def read(self,data: BytesIO):
super().read(data)
self.w = struct.unpack("<I", data.read(4))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<I", self.w))
class ulong(ResoType):
x: int = 0
def __int__(self):
return self.x
def __init__(self,value=0):
self.x = value
def read(self,data: BytesIO):
self.x = struct.unpack("<Q", data.read(8))[0]
def write(self, data: BytesIO):
data.write(struct.pack("<Q", self.x))
class long(ResoType):
x: int = 0
def __init__(self):
pass
def read(self,data: BytesIO):
self.x = struct.unpack("<q", data.read(8))[0]
def write(self, data: BytesIO):
data.write(struct.pack("<q", self.x))
class long2(long):
y: int = 0
def __init__(self):
pass
def read(self,data: BytesIO):
super().read(data)
self.y = struct.unpack("<q", data.read(8))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<q", self.y))
class long3(long2):
z: int = 0
def __init__(self):
pass
def read(self,data: BytesIO):
super().read(data)
self.z = struct.unpack("<q", data.read(8))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<q", self.z))
class long4(long3):
w: int = 0
def __init__(self):
pass
def __init__(self):
pass
def read(self,data: BytesIO):
super().read(data)
self.w = struct.unpack("<q", data.read(8))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<q", self.w))
class double(ResoType):
x: float = 0
def __float__(self):
return self.x
def __init__(self,value=0):
self.x = value
def read(self,data: BytesIO):
self.x = struct.unpack("<d", data.read(8))[0]
def write(self, data: BytesIO):
data.write(struct.pack("<d", self.x))
class double2(double):
y: float = 0
def __init__(self):
pass
def read(self,data: BytesIO):
super().read(data)
self.y = struct.unpack("<d", data.read(8))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<d", self.y))
class double3(double2):
z: float = 0
def __init__(self):
pass
def read(self,data: BytesIO):
super().read(data)
self.z = struct.unpack("<d", data.read(8))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<d", self.z))
class double4(double3):
w: float = 0
def __init__(self):
pass
def read(self,data: BytesIO):
super().read(data)
self.w = struct.unpack("<d", data.read(8))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<d", self.w))
class double2x2(ResoType):
m00: float = 0
m01: float = 0
m10: float = 0
m11: float = 0
def __init__(self):
pass
def read(self,data: BytesIO):
self.m00,self.m01,self.m10,self.m11 = struct.unpack("<dddd", data.read(8*(2*2)))
def write(self, data: BytesIO):
data.write(struct.pack("<dddd", self.m00,self.m01, self.m10,self.m11))
class double3x3(double2x2):
m02: float = 0
m12: float = 0
m22: float = 0
def __init__(self):
pass
def read(self,data: BytesIO):
self.m00,self.m01,self.m02,self.m10,self.m11,self.m12,self.m20,self.m21,self.m22 = struct.unpack("<ddddddddd", data.read(8*(3*3)))
def write(self, data: BytesIO):
data.write(struct.pack("<ddddddddd", self.m00,self.m01,self.m02,self.m10,self.m11,self.m12,self.m20,self.m21,self.m22))
class double4x4(double3x3):
m03: float = 0
m13: float = 0
m23: float = 0
m33: float = 0
def __init__(self):
pass
def read(self,data: BytesIO):
self.m00,self.m01,self.m02,self.m03,self.m10,self.m11,self.m12,self.m13,self.m20,self.m21,self.m22,self.m23,self.m30,self.m31,self.m32,self.m33 = struct.unpack("<dddddddddddddddd", data.read(8*(4*4)))
def write(self, data: BytesIO):
data.write(struct.pack("<dddddddddddddddd", self.m00,self.m01,self.m02,self.m03,self.m10,self.m11,self.m12,self.m13,self.m20,self.m21,self.m22,self.m23,self.m30,self.m31,self.m32,self.m33))
class doubleQ(double4):
def __init__(self):
pass
def write(self, data: BytesIO):
super().write(data)
def read(self, data: BytesIO):
super().read(data)
class float(ResoType):
x: float = 0
def __float__(self):
return self.x
def __init__(self, value=0):
self.x = value
def read(self,data: BytesIO):
self.x = struct.unpack("<f", data.read(4))[0]
def write(self, data: BytesIO):
data.write(struct.pack("<f", self.x))
class float2(float):
y: float = 0
def __init__(self):
pass
def read(self,data: BytesIO):
super().read(data)
self.y = struct.unpack("<f", data.read(4))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<f", self.y))
class float3(float2):
z: float = 0
def __init__(self):
pass
def read(self,data: BytesIO):
super().read(data)
self.z = struct.unpack("<f", data.read(4))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<f", self.z))
class float4(float3):
w: float = 0
def __init__(self):
pass
def read(self, data: BytesIO):
super().read(data)
self.w = struct.unpack("<f", data.read(4))[0]
def write(self, data: BytesIO):
super().write(data)
data.write(struct.pack("<f", self.w))
class float2x2(ResoType):
m00: float = 0
m01: float = 0
m10: float = 0
m11: float = 0
def __init__(self):
pass
def read(self,data: BytesIO):
self.m00,self.m01,self.m10,self.m11 = struct.unpack("<ffff", data.read(4*(2*2)))
def write(self, data: BytesIO):
data.write(struct.pack("<ffff", self.m00,self.m01, self.m10,self.m11))
class float3x3(float2x2):
m02: float = 0
m12: float = 0
m22: float = 0
def __init__(self):
pass
def read(self,data: BytesIO):
self.m00,self.m01,self.m02,self.m10,self.m11,self.m12,self.m20,self.m21,self.m22 = struct.unpack("<fffffffff", data.read(4*(3*3)))
def write(self, data: BytesIO):
data.write(struct.pack("<fffffffff", self.m00,self.m01,self.m02,self.m10,self.m11,self.m12,self.m20,self.m21,self.m22))
class float4x4(float3x3):
m03: float = 0
m13: float = 0
m23: float = 0
m33: float = 0
def __init__(self):
pass
def read(self,data: BytesIO):
self.m00,self.m01,self.m02,self.m03,self.m10,self.m11,self.m12,self.m13,self.m20,self.m21,self.m22,self.m23,self.m30,self.m31,self.m32,self.m33 = struct.unpack("<ffffffffffffffff", data.read(4*(4*4)))
def write(self, data: BytesIO):
data.write(struct.pack("<ffffffffffffffff", self.m00,self.m01,self.m02,self.m03,self.m10,self.m11,self.m12,self.m13,self.m20,self.m21,self.m22,self.m23,self.m30,self.m31,self.m32,self.m33))
class floatQ(float4):
def __init__(self):
pass
def write(self, data: BytesIO):
super().write(data)
def read(self, data: BytesIO):
super().read(data)
+303
View File
@@ -0,0 +1,303 @@
from types import FrameType
import bpy
import bpy_extras
from numpy import double
from .common import get_armature, get_selected_armature, simplify_bonename, is_valid_armature
from bpy.types import Object, ShapeKey, Mesh, Context, Operator
from functools import lru_cache
from ..core.register import register_wrap
from ..functions.translations import t
from ..core.dictionaries import bone_names
import re
from .resonite_loader import resonite_animx, resonite_types
import os
@register_wrap
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_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'}
@register_wrap
class AvatarToolKit_OT_ConvertToResonite(Operator):
bl_idname = 'avatar_toolkit.convert_to_resonite'
bl_label = t('Tools.convert_to_resonite.label')
bl_description = t('Tools.convert_to_resonite.desc')
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> 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("<noik>"), 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+"<noik>"
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)
View File
+299
View File
@@ -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'}