diff --git a/fast64_internal/f3d/f3d_gbi.py b/fast64_internal/f3d/f3d_gbi.py index e79b5871c..80a25510e 100644 --- a/fast64_internal/f3d/f3d_gbi.py +++ b/fast64_internal/f3d/f3d_gbi.py @@ -1,7 +1,7 @@ # Macros are all copied over from gbi.h from __future__ import annotations -from typing import Sequence, Union, Tuple +from typing import Sequence, Union, Tuple, TypeVar from dataclasses import dataclass, fields, field import bpy, os, enum, copy from ..utility import * @@ -2334,6 +2334,86 @@ def __eq__(self, __o: object) -> bool: return self.palFormat == __o.palFormat and self.imagesSharingPalette == __o.imagesSharingPalette +class FMesh: + def __init__(self, name, DLFormat): + self.name = name + # GfxList + self.draw = GfxList(name, GfxListTag.Draw, DLFormat) + # list of FTriGroup + self.triangleGroups: list[FTriGroup] = [] + # VtxList + self.cullVertexList = None + self.draw_overrides: list[GfxList] = [] + self.DLFormat = DLFormat + + # Used to avoid consecutive calls to the same material if unnecessary + self.currentFMaterial = None + + def add_material_call(self, fMaterial): + sameMaterial = self.currentFMaterial is fMaterial + if not sameMaterial: + self.currentFMaterial = fMaterial + self.draw.commands.append(SPDisplayList(fMaterial.material)) + else: + lastCommand = self.draw.commands[-1] + if isinstance(lastCommand, SPDisplayList) and lastCommand.displayList == fMaterial.revert: + self.draw.commands.remove(lastCommand) + + def add_cull_vtx(self): + self.cullVertexList = VtxList(self.name + "_vtx_cull") + + def get_ptr_addresses(self, f3d): + addresses = self.draw.get_ptr_addresses(f3d) + for triGroup in self.triangleGroups: + addresses.extend(triGroup.get_ptr_addresses(f3d)) + for cmd_list in self.draw_overrides: + addresses.extend(cmd_list.get_ptr_addresses(f3d)) + return addresses + + def tri_group_new(self, fMaterial): + # Always static DL + triGroup = FTriGroup(self.name, len(self.triangleGroups), fMaterial) + self.triangleGroups.append(triGroup) + return triGroup + + def set_addr(self, startAddress, f3d): + addrRange = self.draw.set_addr(startAddress, f3d) + startAddress = addrRange[0] + for triGroup in self.triangleGroups: + addrRange = triGroup.set_addr(addrRange[1], f3d) + if self.cullVertexList is not None: + addrRange = self.cullVertexList.set_addr(addrRange[1]) + for cmd_list in self.draw_overrides: + addrRange = cmd_list.set_addr(addrRange[1], f3d) + return startAddress, addrRange[1] + + def save_binary(self, romfile, f3d, segments): + self.draw.save_binary(romfile, f3d, segments) + for triGroup in self.triangleGroups: + triGroup.save_binary(romfile, f3d, segments) + if self.cullVertexList is not None: + self.cullVertexList.save_binary(romfile) + for cmd_list in self.draw_overrides: + cmd_list.save_binary(romfile, f3d, segments) + + def to_c(self, f3d: F3D, gfxFormatter: GfxFormatter): + staticData = CData() + + if self.cullVertexList is not None: + staticData.append(self.cullVertexList.to_c()) + + for triGroup in self.triangleGroups: + staticData.append(triGroup.to_c(f3d, gfxFormatter)) + + draw_layer = "Opaque" if "Opaque" in self.name else "Transparent" if "Transparent" in self.name else "Overlay" + dynamicData = gfxFormatter.drawToC(f3d, self.draw, layer=draw_layer) + + for cmd_list in self.draw_overrides: + dynamicData.append(cmd_list.to_c(f3d)) + + return staticData, dynamicData + + class FModel: def __init__( self, @@ -2449,7 +2529,11 @@ def addLight(self, key, value, fMaterial): fMaterial.usedLights.append(key) self.lights[key] = value - def addMesh(self, name, namePrefix, drawLayer, isSkinned, contextObj, dedup=False): + MT = TypeVar("MT", bound=FMesh) + + def addMesh( + self, name, namePrefix, drawLayer, isSkinned, contextObj, dedup=False, meshOverride: type[MT] = FMesh + ) -> MT: final_name = getFMeshName(name, namePrefix, drawLayer, isSkinned) if dedup: base_name = final_name @@ -2457,7 +2541,7 @@ def addMesh(self, name, namePrefix, drawLayer, isSkinned, contextObj, dedup=Fals if final_name in self.meshes: final_name = f"{base_name}_{i:03}" checkUniqueBoneNames(self, final_name, name) - self.meshes[final_name] = mesh = FMesh(final_name, self.DLFormat) + self.meshes[final_name] = mesh = meshOverride(final_name, self.DLFormat) self.onAddMesh(mesh, contextObj) return mesh @@ -2888,86 +2972,6 @@ def create_data(self): self.draw.commands.append(SPEndDisplayList()) -class FMesh: - def __init__(self, name, DLFormat): - self.name = name - # GfxList - self.draw = GfxList(name, GfxListTag.Draw, DLFormat) - # list of FTriGroup - self.triangleGroups: list[FTriGroup] = [] - # VtxList - self.cullVertexList = None - self.draw_overrides: list[GfxList] = [] - self.DLFormat = DLFormat - - # Used to avoid consecutive calls to the same material if unnecessary - self.currentFMaterial = None - - def add_material_call(self, fMaterial): - sameMaterial = self.currentFMaterial is fMaterial - if not sameMaterial: - self.currentFMaterial = fMaterial - self.draw.commands.append(SPDisplayList(fMaterial.material)) - else: - lastCommand = self.draw.commands[-1] - if isinstance(lastCommand, SPDisplayList) and lastCommand.displayList == fMaterial.revert: - self.draw.commands.remove(lastCommand) - - def add_cull_vtx(self): - self.cullVertexList = VtxList(self.name + "_vtx_cull") - - def get_ptr_addresses(self, f3d): - addresses = self.draw.get_ptr_addresses(f3d) - for triGroup in self.triangleGroups: - addresses.extend(triGroup.get_ptr_addresses(f3d)) - for cmd_list in self.draw_overrides: - addresses.extend(cmd_list.get_ptr_addresses(f3d)) - return addresses - - def tri_group_new(self, fMaterial): - # Always static DL - triGroup = FTriGroup(self.name, len(self.triangleGroups), fMaterial) - self.triangleGroups.append(triGroup) - return triGroup - - def set_addr(self, startAddress, f3d): - addrRange = self.draw.set_addr(startAddress, f3d) - startAddress = addrRange[0] - for triGroup in self.triangleGroups: - addrRange = triGroup.set_addr(addrRange[1], f3d) - if self.cullVertexList is not None: - addrRange = self.cullVertexList.set_addr(addrRange[1]) - for cmd_list in self.draw_overrides: - addrRange = cmd_list.set_addr(addrRange[1], f3d) - return startAddress, addrRange[1] - - def save_binary(self, romfile, f3d, segments): - self.draw.save_binary(romfile, f3d, segments) - for triGroup in self.triangleGroups: - triGroup.save_binary(romfile, f3d, segments) - if self.cullVertexList is not None: - self.cullVertexList.save_binary(romfile) - for cmd_list in self.draw_overrides: - cmd_list.save_binary(romfile, f3d, segments) - - def to_c(self, f3d: F3D, gfxFormatter: GfxFormatter): - staticData = CData() - - if self.cullVertexList is not None: - staticData.append(self.cullVertexList.to_c()) - - for triGroup in self.triangleGroups: - staticData.append(triGroup.to_c(f3d, gfxFormatter)) - - draw_layer = "Opaque" if "Opaque" in self.name else "Transparent" if "Transparent" in self.name else "Overlay" - dynamicData = gfxFormatter.drawToC(f3d, self.draw, layer=draw_layer) - - for cmd_list in self.draw_overrides: - dynamicData.append(cmd_list.to_c(f3d)) - - return staticData, dynamicData - - class FTriGroup: def __init__(self, name, index, fMaterial): self.fMaterial = fMaterial diff --git a/fast64_internal/f3d/f3d_parser.py b/fast64_internal/f3d/f3d_parser.py index baa1c0fcc..0d20d2221 100644 --- a/fast64_internal/f3d/f3d_parser.py +++ b/fast64_internal/f3d/f3d_parser.py @@ -7,7 +7,8 @@ import ast from typing import Union, Optional, Callable, Any, TYPE_CHECKING -from mathutils import Vector +from collections import defaultdict +from mathutils import Vector, Matrix from bpy.utils import register_class, unregister_class # TODO: remove `import *` @@ -440,15 +441,6 @@ def renderModeMask(rendermode, cycle, blendOnly): return rendermode & (3 << 28 | 3 << 24 | 3 << 20 | 3 << 16 | nonBlend) -def convertF3DUV(value, maxSize): - try: - valueBytes = int.to_bytes(round(value), 2, "big", signed=True) - except OverflowError: - valueBytes = int.to_bytes(round(value), 2, "big", signed=False) - - return ((int.from_bytes(valueBytes, "big", signed=True) / 32) + 0.5) / (maxSize if maxSize > 0 else 1) - - class F3DTextureReference: def __init__(self, name, width): self.name = name @@ -474,6 +466,7 @@ def __init__(self, f3d: F3D, basePath: str, materialContext: bpy.types.Material) # If this is not disabled, then tex_scale will auto-update on manual node update. self.materialContext.f3d_mat.scale_autoprop = False self.draw_layer_prop: str | None = None + self.vertOverride: type[F3DVert] = F3DVert self.initContext() # This is separate as we want to call __init__ in clearGeometry, but don't want same behaviour for child classes @@ -522,7 +515,7 @@ def initContext(self): # data for Mesh.from_pydata, list of BufferVertex tuples # use BufferVertex to also form uvs / normals / colors self.verts: list[F3DVert] = [] - self.limbGroups: dict[str, list[int]] = {} # dict of groupName : vertex indices + self.limbGroups: dict[str, list[int]] = defaultdict(list) # dict of groupName : vertex indices self.lights: Lights = Lights("lights_context", self.f3d) @@ -647,48 +640,60 @@ def setCurrentTransform(self, name, flagList="G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_ else: self.currentTransformName = name - def getTransformedVertex(self, index: int): - bufferVert = self.vertexBuffer[index] + def transformNormal( + self, has_normal: bool, has_packed_normals: bool, f3dVert: F3DVert, transform: Matrix + ) -> Vector: + normal = Vector([0.0, 0.0, 0.0]) # Zero normal makes normals_split_custom_set use auto + if has_normal: + if has_packed_normals: + normal = f3dVert.normal + else: + normal = Vector([v - 0x100 if v >= 0x80 else v for v in f3dVert.rgb]).normalized() + normal = (transform.inverted().transposed() @ normal).normalized() + return normal + def getVertexTransforms( + self, bufferVert: BufferVertex, has_normal: bool, has_packed_normals: bool + ) -> tuple[Vector, Vector]: # NOTE: The groupIndex here does NOT correspond to a vertex group, but to the name of the limb (c variable) matrixName = bufferVert.groupIndex if matrixName in self.matrixData: transform = self.matrixData[matrixName] else: print(self.matrixData) - raise PluginError("Transform matrix not specified for " + matrixName) + raise PluginError("Transform matrix not specified for " + str(matrixName)) - mat = self.mat() f3dVert = bufferVert.f3dVert position = transform @ Vector(f3dVert.position) - if mat.tex0.tex is not None: - texDimensions = mat.tex0.tex.size - elif mat.tex0.use_tex_reference: - texDimensions = mat.tex0.tex_reference_size - elif mat.tex1.tex is not None: - texDimensions = mat.tex1.tex.size - elif mat.tex1.use_tex_reference: - texDimensions = mat.tex1.tex_reference_size - else: - texDimensions = [32, 32] + normal = self.transformNormal(has_normal, has_packed_normals, f3dVert, transform) - uv = [convertF3DUV(f3dVert.uv[i], texDimensions[i]) for i in range(2)] - uv[1] = 1 - uv[1] + return position, normal - has_rgb, has_normal, has_packed_normals = getRgbNormalSettings(self.mat()) + def convertVertexValues(self, mat: F3DMaterialProperty, has_rgb: bool, f3dVert: F3DVert): + uv = convertF3DUV(mat, f3dVert) rgb = Vector([v / 0xFF for v in f3dVert.rgb]) if has_rgb else Vector([1.0, 1.0, 1.0]) alpha = f3dVert.alpha / 0xFF - normal = Vector([0.0, 0.0, 0.0]) # Zero normal makes normals_split_custom_set use auto - if has_normal: - if has_packed_normals: - normal = f3dVert.normal - else: - normal = Vector([v - 0x100 if v >= 0x80 else v for v in f3dVert.rgb]).normalized() - normal = (transform.inverted().transposed() @ normal).normalized() + + return uv, rgb, alpha + + def getTransformedVertex(self, index: int) -> BufferVertex: + bufferVert = self.vertexBuffer[index] + + if bufferVert is None: + raise PluginError("Vertex Buffer is empty.") + + mat = self.mat() + has_rgb, has_normal, has_packed_normals = getRgbNormalSettings(mat) + position, normal = self.getVertexTransforms(bufferVert, has_normal, has_packed_normals) + uv, rgb, alpha = self.convertVertexValues(mat, has_rgb, bufferVert.f3dVert) # NOTE: The groupIndex here does NOT correspond to a vertex group, but to the name of the limb (c variable) return BufferVertex(F3DVert(position, uv, rgb, normal, alpha), bufferVert.groupIndex, bufferVert.materialIndex) + def updateBuffer(self, count, start, vertexData, vertexDataOffset): + for i in range(count): + self.vertexBuffer[start + i] = BufferVertex(vertexData[vertexDataOffset + i], self.currentTransformName, 0) + def addVertices(self, num, start, vertexDataName, vertexDataOffset): vertexData = self.vertexData[vertexDataName] @@ -711,8 +716,18 @@ def addVertices(self, num, start, vertexDataName, vertexDataOffset): f"{vertexDataName} is of size {len(vertexData)}, " f"attemped read from ({vertexDataOffset}, {vertexDataOffset + count})" ) - for i in range(count): - self.vertexBuffer[start + i] = BufferVertex(vertexData[vertexDataOffset + i], self.currentTransformName, 0) + + self.updateBuffer(count, start, vertexData, vertexDataOffset) + + def processLimbGroups(self, verts: list[BufferVertex]) -> None: + for i in range(len(verts)): + vert = verts[i] + + # NOTE: The groupIndex here does NOT correspond to a vertex group, but to the name of the limb (c variable) + if vert.groupIndex not in self.limbGroups: + self.limbGroups[vert.groupIndex] = [] + self.limbGroups[vert.groupIndex].append(len(self.verts) + i) + self.verts.extend([vert.f3dVert for vert in verts]) def addTriangle(self, indices, dlData): if self.materialChanged: @@ -742,14 +757,8 @@ def addTriangle(self, indices, dlData): # verts[0].groupIndex != verts[2].groupIndex or\ # verts[2].groupIndex != verts[1].groupIndex: # return - for i in range(len(verts)): - vert = verts[i] - # NOTE: The groupIndex here does NOT correspond to a vertex group, but to the name of the limb (c variable) - if vert.groupIndex not in self.limbGroups: - self.limbGroups[vert.groupIndex] = [] - self.limbGroups[vert.groupIndex].append(len(self.verts) + i) - self.verts.extend([vert.f3dVert for vert in verts]) + self.processLimbGroups(verts) for i in range(int(len(indices) / 3)): self.triMatIndices.append(self.lastMaterialIndex) @@ -1565,6 +1574,7 @@ def getVertexDataStart(self, vertexDataParam: str, f3d: F3D): raise PluginError("SPVertex param " + vertexDataParam + " is malformed.") offset = 0 + if matchResult.group(3): offset += math_eval(matchResult.group(3), f3d) if matchResult.group(5): @@ -1572,6 +1582,9 @@ def getVertexDataStart(self, vertexDataParam: str, f3d: F3D): return matchResult.group(1), offset + def getVertexSegmentData(self, segment: str, count: str, start: str, vertOverride: type[F3DVert] = F3DVert) -> None: + raise NotImplementedError(f"Importing vertices from segments is not supported. Vertex List: {segment}") + def processCommands(self, dlData: str, dlName: str, dlCommands: "list[ParsedMacro]"): callStack = [F3DParsedCommands(dlName, dlCommands, 0)] while len(callStack) > 0: @@ -1586,7 +1599,9 @@ def processCommands(self, dlData: str, dlName: str, dlCommands: "list[ParsedMacr # print(command.name + " " + str(command.params)) if command.name == "gsSPVertex": vertexDataName, vertexDataOffset = self.getVertexDataStart(command.params[0], self.f3d) - parseVertexData(dlData, vertexDataName, self) + if vertexDataName.lower().startswith("0x"): + self.getVertexSegmentData(vertexDataName, command.params[1], command.params[2]) + parseVertexData(dlData, vertexDataName, self, self.vertOverride) self.addVertices(command.params[1], command.params[2], vertexDataName, vertexDataOffset) elif command.name == "gsSPMatrix": self.setCurrentTransform(command.params[0], command.params[1]) @@ -1863,6 +1878,11 @@ def deleteMaterialContext(self): else: raise PluginError("Attempting to delete material context that is None.") + def createVertexGroups(self, obj: bpy.types.Object) -> None: + for groupName, indices in self.limbGroups.items(): + group = obj.vertex_groups.new(name=self.limbToBoneName[groupName]) + group.add(indices, 1, "REPLACE") + # if deleteMaterialContext is False, then manually call self.deleteMaterialContext() later. def createMesh(self, obj: bpy.types.Object, removeDoubles, importNormals, callDeleteMaterialContext: bool): mesh = obj.data @@ -1886,9 +1906,7 @@ def createMesh(self, obj: bpy.types.Object, removeDoubles, importNormals, callDe mesh.use_auto_smooth = True mesh.normals_split_custom_set([f3dVert.normal for f3dVert in self.verts]) - for groupName, indices in self.limbGroups.items(): - group = obj.vertex_groups.new(name=self.limbToBoneName[groupName]) - group.add(indices, 1, "REPLACE") + self.createVertexGroups(obj) for i in range(len(mesh.polygons)): mesh.polygons[i].material_index = self.triMatIndices[i] @@ -1950,6 +1968,32 @@ def createMesh(self, obj: bpy.types.Object, removeDoubles, importNormals, callDe self.deleteMaterialContext() +def convertF3DUV(mat, vertex): + if mat.tex0.tex is not None: + texDimensions = mat.tex0.tex.size + elif mat.tex0.use_tex_reference: + texDimensions = mat.tex0.tex_reference_size + elif mat.tex1.tex is not None: + texDimensions = mat.tex1.tex.size + elif mat.tex1.use_tex_reference: + texDimensions = mat.tex1.tex_reference_size + else: + texDimensions = [32, 32] + + def convert(value, maxSize): + try: + valueBytes = int.to_bytes(round(value), 2, "big", signed=True) + except OverflowError: + valueBytes = int.to_bytes(round(value), 2, "big", signed=False) + + return ((int.from_bytes(valueBytes, "big", signed=True) / 32) + 0.5) / (maxSize if maxSize > 0 else 1) + + uv = [convert(vertex.uv[i], texDimensions[i]) for i in range(2)] + uv[1] = 1 - uv[1] + + return uv + + class ParsedMacro: def __init__(self, name: str, params: "list[str]"): self.name = name @@ -2010,7 +2054,9 @@ def parseDLData(dlData: str, dlName: str): return dlCommands -def parseVertexData(dlData: str, vertexDataName: str, f3dContext: F3DContext): +def parseVertexData( + dlData: str, vertexDataName: str, f3dContext: F3DContext, vertOverride: type[F3DVert] = F3DVert +) -> list[F3DVert]: if vertexDataName in f3dContext.vertexData: return f3dContext.vertexData[vertexDataName] @@ -2039,7 +2085,7 @@ def parseVertexData(dlData: str, vertexDataName: str, f3dContext: F3DContext): # A format without the flag / packed normal values = values[0:3] + [0] + values[3:9] vertexData.append( - F3DVert( + vertOverride( Vector(values[0:3]), Vector(values[4:6]), Vector(values[6:9]), diff --git a/fast64_internal/f3d/f3d_writer.py b/fast64_internal/f3d/f3d_writer.py index aee93c37e..f6f2c5b1c 100644 --- a/fast64_internal/f3d/f3d_writer.py +++ b/fast64_internal/f3d/f3d_writer.py @@ -1,4 +1,4 @@ -from typing import Union, Optional, Callable, Any, List +from typing import Union, Optional, Callable, Any, List, TypeVar, Generic, cast from dataclasses import dataclass import functools import bpy, mathutils, os, re, copy, math @@ -64,8 +64,11 @@ def __init__(self): self.vertexGroupToLimb = {} -class MeshInfo: - def __init__(self): +VG = TypeVar("VG", bound=VertexGroupInfo | None) + + +class MeshInfo(Generic[VG]): + def __init__(self, groupInfo: VG = None) -> None: self.vert = {} # all faces connected to a vert self.edge = {} # all faces connected to an edge self.f3dVert = {} # f3d vertex of a given loop @@ -73,16 +76,16 @@ def __init__(self): self.validNeighbors = {} # all neighbors of a face with a valid connecting edge self.texDimensions = {} # texture dimensions for each material - self.vertexGroupInfo = None + self.vertexGroupInfo: VG = groupInfo def get_original_name(obj: bpy.types.Object): return getattr(obj, "original_name", obj.name) -def getInfoDict(obj: bpy.types.Object): +def getInfoDict(obj: bpy.types.Object, groupInfo: VG = None) -> MeshInfo[VG]: try: - return getInfoDict_impl(obj) + return getInfoDict_impl(obj, groupInfo) except: print(f"Error in getInfoDict_impl(obj name = {get_original_name(obj)!r})") raise @@ -129,7 +132,7 @@ def check_face_materials( ) -def getInfoDict_impl(obj: bpy.types.Object): +def getInfoDict_impl(obj: bpy.types.Object, groupInfo: VG) -> MeshInfo[VG]: mesh: bpy.types.Mesh = obj.data material_slots = obj.material_slots if len(mesh.materials) == 0 or len(material_slots) == 0: @@ -147,7 +150,7 @@ def getInfoDict_impl(obj: bpy.types.Object): if bpy.app.version < (4, 1, 0): mesh.calc_normals_split() - infoDict = MeshInfo() + infoDict = MeshInfo(groupInfo) vertDict = infoDict.vert edgeDict = infoDict.edge @@ -708,52 +711,6 @@ def saveTriangleStrip(triConverter, faces, faceSTOffsets, mesh, terminateDL): return triConverter.currentGroupIndex -def saveMeshByFaces( - material, - faces, - fModel, - fMesh, - obj, - drawLayer, - convertTextureData, - currentGroupIndex, - triConverterInfo, - existingVertData, - matRegionDict, - lastMaterialName, -): - """ - lastMaterialName is for optimization; set it to None to disable optimization. - """ - - if len(faces) == 0: - print("0 Faces Provided.") - return - fMaterial, texDimensions = saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData) - - if material.name != lastMaterialName: - fMesh.add_material_call(fMaterial) - triGroup = fMesh.tri_group_new(fMaterial) - fMesh.draw.commands.append(SPDisplayList(triGroup.triList)) - - triConverter = TriangleConverter( - triConverterInfo, - texDimensions, - material, - currentGroupIndex, - triGroup, - copy.deepcopy(existingVertData), - copy.deepcopy(matRegionDict), - ) - - currentGroupIndex = saveTriangleStrip(triConverter, faces, None, obj.data, True) - - if fMaterial.revert is not None: - fMesh.draw.commands.append(SPDisplayList(fMaterial.revert)) - - return currentGroupIndex - - @dataclass class LoopConvertInfo: uv_data: bpy.types.bpy_prop_collection | list[bpy.types.MeshUVLoop] @@ -801,10 +758,10 @@ def __eq__(self, other): and self.alpha == other.alpha ) - def toVtx(self, mesh, texDimensions, transformMatrix, isPointSampled: bool, tex_scale=(1, 1)) -> Vtx: - # Position (8 bytes) - position = [int(round(floatValue)) for floatValue in (transformMatrix @ self.position)] + def convertPosition(self, transformMatrix: Matrix) -> list[int]: + return [int(round(floatValue)) for floatValue in (transformMatrix @ self.position)] + def convertUV(self, texDimensions, isPointSampled: bool, tex_scale=(1, 1)) -> List[int]: # UV (4 bytes) # For F3D, Bilinear samples the point from the center of the pixel. # However, Point samples from the corner. @@ -821,7 +778,9 @@ def toVtx(self, mesh, texDimensions, transformMatrix, isPointSampled: bool, tex_ convertFloatToFixed16(self.uv[0] * texDimensions[0] - pixelOffset[0]), convertFloatToFixed16(self.uv[1] * texDimensions[1] - pixelOffset[1]), ] + return uv + def convertNormalRGB(self, transformMatrix: Matrix): packedNormal = 0 if self.normal is not None: # normal transformed correctly. @@ -839,6 +798,14 @@ def toVtx(self, mesh, texDimensions, transformMatrix, isPointSampled: bool, tex_ ] colorOrNormal.append(scaleToU8(self.alpha).to_bytes(1, "big")[0]) + return colorOrNormal, packedNormal + + def toVtx(self, mesh, texDimensions, transformMatrix, isPointSampled: bool, tex_scale=(1, 1)) -> Vtx: + # Position (8 bytes) + position = self.convertPosition(transformMatrix) + uv = self.convertUV(texDimensions, isPointSampled, tex_scale) + colorOrNormal, packedNormal = self.convertNormalRGB(transformMatrix) + return Vtx(position, uv, colorOrNormal, packedNormal) @@ -877,15 +844,17 @@ def getMatrixAddrFromGroup(self, groupIndex): "TriangleConverterInfo must be extended with getMatrixAddrFromGroup implemented for game specific uses." ) - def getTransformMatrix(self, groupIndex): + def getTransformMatrix(self, groupIndex) -> Matrix: if self.armature is None or groupIndex is None: groupMatrix = mathutils.Matrix.Identity(4) else: if groupIndex not in self.groupNames: self.groupNames[groupIndex] = getGroupNameFromIndex(self.obj, groupIndex) name = self.groupNames[groupIndex] - if name not in self.armature.bones: - print("Vertex group " + name + " not found in bones.") + if name is None: + groupMatrix = mathutils.Matrix.Identity(4) + elif name not in self.armature.bones: + print("Vertex group " + str(name) + " not found in bones.") groupMatrix = mathutils.Matrix.Identity(4) else: groupMatrix = self.armature.bones[name].matrix_local.inverted() @@ -930,8 +899,8 @@ def __init__( material: bpy.types.Material, currentGroupIndex, triGroup: FTriGroup, - existingVertexData: list[BufferVertex], - existingVertexMaterialRegions, + existingVertexData: list[BufferVertex] | None, + existingVertexMaterialRegions: dict[int, tuple[int, int]] | None, ): self.triConverterInfo = triConverterInfo self.currentGroupIndex = currentGroupIndex @@ -976,6 +945,14 @@ def getSortedBuffer(self) -> dict[int, list[BufferVertex]]: return limbVerts + def getBufferVert( + self, loop: bpy.types.MeshLoop, face: bpy.types.MeshLoopTriangle, groupIndex: int | None + ) -> BufferVertex: + bufferVert = BufferVertex( + getF3DVert(loop, face, self.convertInfo, self.triConverterInfo.mesh), groupIndex, face.material_index + ) + return bufferVert + def processGeometry(self): # Sort verts by limb index, then load current limb verts bufferStart = self.bufferStart @@ -1142,7 +1119,7 @@ def writeCelLevels(self, celTriList: Optional[GfxList] = None, triCmds: Optional # Disable alpha compare culling for future DLs self.triList.commands.append(SPAlphaCompareCull("G_ALPHA_COMPARE_CULL_DISABLE", 0)) - def addFace(self, face, stOffset): + def addFace(self, face: bpy.types.MeshLoopTriangle, stOffset): triIndices = [] addedVerts = [] # verts added to existing vertexBuffer allVerts = [] # all verts not in 'untouched' buffer region @@ -1154,9 +1131,8 @@ def addFace(self, face, stOffset): if self.triConverterInfo.vertexGroupInfo is not None else None ) - bufferVert = BufferVertex( - getF3DVert(loop, face, self.convertInfo, self.triConverterInfo.mesh), vertexGroup, face.material_index - ) + + bufferVert = self.getBufferVert(loop, face, vertexGroup) bufferVert.f3dVert.stOffset = stOffset triIndices.append(bufferVert) if not self.vertInBuffer(bufferVert, face.material_index): @@ -1185,7 +1161,16 @@ def finish(self, terminateDL): self.triList.commands.append(SPEndDisplayList()) -def getF3DVert(loop: bpy.types.MeshLoop, face, convertInfo: LoopConvertInfo, mesh: bpy.types.Mesh): +VT = TypeVar("VT", bound="F3DVert") + + +def getF3DVert( + loop: bpy.types.MeshLoop, + face, + convertInfo: LoopConvertInfo, + mesh: bpy.types.Mesh, + vertOverride: type[VT] = F3DVert, +) -> VT: position: Vector = mesh.vertices[loop.vertex_index].co.copy().freeze() # N64 is -Y, Blender is +Y uv: Vector = convertInfo.uv_data[loop.index].uv.copy() @@ -1200,7 +1185,7 @@ def getF3DVert(loop: bpy.types.MeshLoop, face, convertInfo: LoopConvertInfo, mes normal = getLoopNormal(loop) if has_normal else None alpha = color[3] - return F3DVert(position, uv, rgb, normal, alpha) + return vertOverride(position, uv, rgb, normal, alpha) def getLoopNormal(loop: bpy.types.MeshLoop) -> Vector: @@ -1217,6 +1202,53 @@ def getLoopNormal(loop: bpy.types.MeshLoop) -> Vector: ).freeze() +def saveMeshByFaces( + material: bpy.types.Material, + faces: list[bpy.types.MeshLoopTriangle], + fModel: FModel, + fMesh: FMesh, + obj: bpy.types.Object, + drawLayer: str, + convertTextureData: bool, + currentGroupIndex: int | None, + triConverterInfo: TriangleConverterInfo, + existingVertData: list[BufferVertex] | None, + matRegionDict: dict[int, tuple[int, int]] | None, + lastMaterialName: str | None, + converterOverride: type[TriangleConverter] = TriangleConverter, +): + """ + lastMaterialName is for optimization; set it to None to disable optimization. + """ + + if len(faces) == 0: + print("0 Faces Provided.") + return + fMaterial, texDimensions = saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData) + + if material.name != lastMaterialName: + fMesh.add_material_call(fMaterial) + triGroup = fMesh.tri_group_new(fMaterial) + fMesh.draw.commands.append(SPDisplayList(triGroup.triList)) + + triConverter = converterOverride( + triConverterInfo, + texDimensions, + material, + currentGroupIndex, + triGroup, + copy.deepcopy(existingVertData), + copy.deepcopy(matRegionDict), + ) + + currentGroupIndex = saveTriangleStrip(triConverter, faces, None, obj.data, True) + + if fMaterial.revert is not None: + fMesh.draw.commands.append(SPDisplayList(fMaterial.revert)) + + return currentGroupIndex + + @functools.lru_cache(0) def is3_2_or_above(): return bpy.app.version >= (3, 2, 0) diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index cccd9bea2..0ff38af46 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -261,6 +261,17 @@ def getGroupNameFromIndex(obj, index): return None +def getBoneIndexFromGroupIndex(obj: bpy.types.Object, armature: bpy.types.Armature, index: int) -> int: + group = obj.vertex_groups[index] + groupName = group.name + boneIndex = armature.bones.find(groupName) + + if boneIndex == -1: + raise PluginError(f"Bone: {groupName} not found in Armature: {armature.name}") + else: + return boneIndex + + def copyPropertyCollection(from_prop, to_prop, do_clear: bool = True): if do_clear: to_prop.clear() diff --git a/fast64_internal/z64/README.md b/fast64_internal/z64/README.md index ee684aa2c..b85f82a57 100644 --- a/fast64_internal/z64/README.md +++ b/fast64_internal/z64/README.md @@ -78,6 +78,8 @@ Default is a regular deformation bone. Ignore will not be handled by the exporte The armature properties window also has the option to set a LOD armature. This armature must have the same bone structure as your current armature. +In the armature properties you can also set the armature to export using SkinLimbs. This will export the skeletons mesh in a format compatible with how OoT handles smooth skinning. This setting is automatically turned on when importing a skeleton that uses SkinLimbs (horses). Armatures exported this way will ignore any LOD armature as SkinLimbs are unable to use LODs. Also note that any actor not already set up to use this limb type will require changes to its source and header files to use them. + To export a skeletal mesh, select an armature and then click "Export" for the armature exporter. Make sure there is only one bone without a parent (the root bone), as the exporter will choose the first parentless bone as the start bone of the armature. To import a skeletal mesh, just click "Import" for the armature importer. You may encounter a couple issues: diff --git a/fast64_internal/z64/animation/importer/functions.py b/fast64_internal/z64/animation/importer/functions.py index ca6a5a86c..4948c67e6 100644 --- a/fast64_internal/z64/animation/importer/functions.py +++ b/fast64_internal/z64/animation/importer/functions.py @@ -65,30 +65,51 @@ def getJointIndices(filepath, animData, jointIndicesName): return jointIndicesData -def ootImportNonLinkAnimationC(armatureObj, filepath, animName, actorScale, isCustomImport: bool): - animData = getImportData([filepath]) +def getAnimData(filepath: str, importData: str, animName: str, isCustomImport: bool): + # importData = getImportData([filepath]) if not isCustomImport: basePath = bpy.path.abspath(bpy.context.scene.ootDecompPath) - animData = ootGetIncludedAssetData([basePath], [filepath], animData) + animData + importData = ootGetIncludedAssetData([basePath], [filepath], importData) + importData - matchResult = re.search(re.escape(animName) + r"\s*=\s*\{(.*?)\}\s*;", animData, re.DOTALL | re.MULTILINE) + matchResult = re.search(re.escape(animName) + r"\s*=\s*\{(.*?)\}\s*;", importData, re.DOTALL | re.MULTILINE) if matchResult is None: raise PluginError("Cannot find definition named " + animName + " in " + filepath) if "#include" in matchResult.group(1): - anim_data = removeComments(get_include_data(matchResult.group(1))).replace("\n", "").replace(" ", "") + animData = removeComments(get_include_data(matchResult.group(1))).replace("\n", "").replace(" ", "") + regex = r"\{(.*?),?\},(.*?),(.*?),(.*?)," + else: + animData = importData + regex = ( + re.escape(animName) + + r"\s*=\s*\{\s*\{\s*([^,\s]*)\s*\}*\s*,\s*([^,\s]*)\s*,\s*([^,\s]*)\s*,\s*([^,\s]*)\s*\}\s*;" + ) + + matchResult = re.search(re.escape(animName) + r"\s*=\s*\{(.*?)\}\s*;", importData, re.DOTALL | re.MULTILINE) + + if matchResult is None: + raise PluginError("Cannot find definition named " + animName + " in " + filepath) + + if "#include" in matchResult.group(1): + animData = removeComments(get_include_data(matchResult.group(1))).replace("\n", "").replace(" ", "") regex = r"\{(.*?),?\},(.*?),(.*?),(.*?)," else: - anim_data = animData + animData = importData regex = ( re.escape(animName) + r"\s*=\s*\{\s*\{\s*([^,\s]*)\s*\}*\s*,\s*([^,\s]*)\s*,\s*([^,\s]*)\s*,\s*([^,\s]*)\s*\}\s*;" ) + return animData, regex + + +def ootGetAnimationData(filepath: str, importData: str, animName: str, isCustomImport: bool): + animData, regex = getAnimData(filepath, importData, animName, isCustomImport) + matchResult = re.search( regex, - anim_data, + animData, ) if matchResult is None: raise PluginError("Cannot find animation named " + animName + " in " + filepath) @@ -97,8 +118,46 @@ def ootImportNonLinkAnimationC(armatureObj, filepath, animName, actorScale, isCu jointIndicesName = matchResult.group(3).strip() staticIndexMax = hexOrDecInt(matchResult.group(4).strip()) - frameData = getFrameData(filepath, animData, frameDataName) - jointIndices = getJointIndices(filepath, animData, jointIndicesName) + frameData = getFrameData(filepath, importData, frameDataName) + jointIndices = getJointIndices(filepath, importData, jointIndicesName) + + return frameData, jointIndices, staticIndexMax, frameCount + + +def ootGetAnimRawTranslation(frame, staticIndexMax, frameData, jointIndex, actorScale) -> mathutils.Vector: + rawTranslation = mathutils.Vector((0, 0, 0)) + for propertyIndex in range(3): + if jointIndex[propertyIndex] < staticIndexMax: + value = ootTranslationValue(frameData[jointIndex[propertyIndex]], actorScale) + else: + value = ootTranslationValue(frameData[jointIndex[propertyIndex] + frame], actorScale) + + rawTranslation[propertyIndex] = value + + return rawTranslation + + +def ootGetAnimRawRotation( + frame: int, staticIndexMax: int, frameData: list[int], jointIndex: list[int], actorScale: float +) -> mathutils.Euler: + rawRotation = mathutils.Euler((0, 0, 0), "XYZ") + for propertyIndex in range(3): + if jointIndex[propertyIndex] < staticIndexMax: + value = binangToRadians(frameData[jointIndex[propertyIndex]]) + else: + value = binangToRadians(frameData[jointIndex[propertyIndex] + frame]) + + rawRotation[propertyIndex] = value + + return rawRotation + + +def ootImportNonLinkAnimationC(armatureObj, filepath, animName, actorScale, isCustomImport: bool): + importData = getImportData([filepath]) + + frameData, jointIndices, staticIndexMax, frameCount = ootGetAnimationData( + filepath, importData, animName, isCustomImport + ) # print(frameDataName + " " + jointIndicesName) # print(str(frameData) + "\n" + str(jointIndices)) @@ -126,15 +185,7 @@ def ootImportNonLinkAnimationC(armatureObj, filepath, animName, actorScale, isCu for propertyIndex in range(3) ] for frame in range(frameCount): - rawTranslation = mathutils.Vector((0, 0, 0)) - for propertyIndex in range(3): - if jointIndex[propertyIndex] < staticIndexMax: - value = ootTranslationValue(frameData[jointIndex[propertyIndex]], actorScale) - else: - value = ootTranslationValue(frameData[jointIndex[propertyIndex] + frame], actorScale) - - rawTranslation[propertyIndex] = value - + rawTranslation = ootGetAnimRawTranslation(frame, staticIndexMax, frameData, jointIndex, actorScale) trueTranslation = getTranslationRelativeToRest(armatureObj.data.bones[startBoneName], rawTranslation) for propertyIndex in range(3): @@ -157,15 +208,7 @@ def ootImportNonLinkAnimationC(armatureObj, filepath, animName, actorScale, isCu ] for frame in range(frameCount): - rawRotation = mathutils.Euler((0, 0, 0), "XYZ") - for propertyIndex in range(3): - if jointIndex[propertyIndex] < staticIndexMax: - value = binangToRadians(frameData[jointIndex[propertyIndex]]) - else: - value = binangToRadians(frameData[jointIndex[propertyIndex] + frame]) - - rawRotation[propertyIndex] = value - + rawRotation = ootGetAnimRawRotation(frame, staticIndexMax, frameData, jointIndex, actorScale) trueRotation = getRotationRelativeToRest(bone, rawRotation) for propertyIndex in range(3): diff --git a/fast64_internal/z64/exporter/skeleton/classes.py b/fast64_internal/z64/exporter/skeleton/classes.py index 502b535e1..87fc5ca26 100644 --- a/fast64_internal/z64/exporter/skeleton/classes.py +++ b/fast64_internal/z64/exporter/skeleton/classes.py @@ -1,17 +1,238 @@ -import mathutils - +from __future__ import annotations +from mathutils import Vector +from dataclasses import dataclass, field, InitVar +from abc import abstractmethod, ABC +from typing import Generic, TypeVar +from ....f3d.f3d_gbi import FMesh from ....f3d.f3d_writer import GfxList from ....utility import CData, toAlnum +from ...model_classes import LimbType, LimbSkinType, SkinAnimData + + +@dataclass +class OOTBaseLimb(ABC): + skeletonName: str + boneName: str + index: int + translation: Vector + typeName: LimbType = field(init=False, default=LimbType.INVALID) + _children: list[OOTBaseLimb] = field(default_factory=list, init=False) + firstChildIndex: int = field(init=False, default=0xFF) + nextSiblingIndex: int = field(init=False, default=0xFF) + mesh: InitVar[FMesh | None] = None + limbSkinType: InitVar[LimbSkinType] = LimbSkinType.EMPTY + + def __post_init__(self, mesh: FMesh | None, limbSkinType: LimbSkinType) -> None: + if mesh is None: + self.DL = None + else: + self.DL = mesh.draw + @property + def name(self) -> str: + return f"{self.skeletonName}Limb_{self.index:03}" -class OOTSkeleton: - def __init__(self, name): - self.name = name - self.segmentID = None - self.limbRoot = None - self.hasLOD = False + @property + def children(self) -> list[OOTBaseLimb]: + return self._children + + @children.setter + def children(self, children: list[OOTBaseLimb]) -> None: + self._children = children + self.firstChildIndex = self._children[0].index + + def addChild(self, child: OOTBaseLimb, index: int | None = None) -> None: + index = index if index is not None else len(self.children) + + self.children.insert(index, child) + self.setLinks() - def createLimbList(self): + def recursiveChildren(self) -> list[OOTBaseLimb]: + children = [] + for child in self.children: + children.append(child) + children.extend(child.children) + return children + + def setLinks(self) -> None: + if len(self.children) > 0: + self.firstChildIndex = self.children[0].index + for i in range(len(self.children)): + if i < len(self.children) - 1: + self.children[i].nextSiblingIndex = self.children[i + 1].index + self.children[i].setLinks() + + def getList(self, limbList: list[OOTBaseLimb]) -> None: + limbList.append(self) + for child in self.children: + child.getList(limbList) + + def getNumLimbs(self): + numLimbs = 1 + for child in self.children: + numLimbs += child.getNumLimbs() + return numLimbs + + @abstractmethod + def getNumDLs(self) -> int: + ... + + @abstractmethod + def typeData(self) -> str: + ... + + def toC(self) -> str: + data = f"{self.typeName.value}Limb " + + data += ( + self.name + + " = { " + + "{ " + + str(int(round(self.translation[0]))) + + ", " + + str(int(round(self.translation[1]))) + + ", " + + str(int(round(self.translation[2]))) + + " }, " + + str(self.firstChildIndex) + + ", " + + str(self.nextSiblingIndex) + + ", " + ) + + data += self.typeData() + + data += " };\n" + + return data + + +@dataclass +class StandardLimb(OOTBaseLimb): + typeName: LimbType = field(init=False, default=LimbType.STANDARD) + DL: GfxList | OOTDLReference | None = field(init=False, default=None) + + def getNumDLs(self) -> int: + numDLs = 0 + if self.DL is not None: + numDLs += 1 + for child in self.children: + numDLs += child.getNumDLs() + + return numDLs + + def typeData(self) -> str: + return self.DL.name if self.DL is not None else "NULL" + + +@dataclass +class LODLimb(OOTBaseLimb): + lodDL: GfxList | OOTDLReference | None = None + typeName: LimbType = field(init=False, default=LimbType.LOD) + DL: GfxList | OOTDLReference | None = field(init=False, default=None) + + @property + def dLists(self) -> list[GfxList | OOTDLReference | None]: + return [self.DL, self.lodDL] + + def getNumDLs(self) -> int: + numDLs = 0 + if self.DL is not None or self.lodDL is not None: + numDLs += 1 + + for child in self.children: + numDLs += child.getNumDLs() + + return numDLs + + def typeData(self) -> str: + data = "" + data += f"{{ {self.DL.name if self.DL is not None else 'NULL'}, " + data += f"{self.lodDL.name if self.lodDL is not None else 'NULL'} }}" + return data + + +@dataclass +class SkinLimb(OOTBaseLimb): + typeName: LimbType = field(init=False, default=LimbType.SKIN) + _segment: SkinAnimData | GfxList | OOTDLReference | None = field(init=False, default=None) + + def __post_init__(self, mesh: FMesh | None, limbSkinType: LimbSkinType) -> None: + self.setSegment(mesh, limbSkinType) + + def setSegment( + self, + segment: FMesh | None, + segmentType: LimbSkinType, + ) -> None: + self._segmentType: LimbSkinType = segmentType + match segmentType: + case LimbSkinType.SKIN_LIMB_TYPE_ANIMATED if isinstance(segment, SkinAnimData): + self._segment = segment + case LimbSkinType.SKIN_LIMB_TYPE_NORMAL if segment is not None: + self._segment = segment.draw + case LimbSkinType.EMPTY | LimbSkinType.SKINNED if segment is None: + self._segment = None + case _: + raise ValueError( + f"SkinLimb {self.name} has invalid segmentType, segment combination\n" + + f"segmentType is: {segmentType.value}\n" + + f"segment is type {type(segment).__name__}" + ) + + @property + def segmentType(self) -> LimbSkinType: + return self._segmentType + + @property + def segment(self) -> SkinAnimData | GfxList | OOTDLReference | None: + return self._segment + + def getNumDLs(self) -> int: + numDLs = 0 + + if self.segmentType in (LimbSkinType.SKIN_LIMB_TYPE_ANIMATED, LimbSkinType.SKIN_LIMB_TYPE_NORMAL): + numDLs += 1 + + for child in self.children: + numDLs += child.getNumDLs() + return numDLs + + def typeData(self) -> str: + data = "" + + data += f"{self.segmentType.value}, " + + match self.segmentType: + case LimbSkinType.EMPTY | LimbSkinType.SKINNED: + data += "NULL" + case LimbSkinType.SKIN_LIMB_TYPE_ANIMATED if self.segment is not None: + data += f"&{self.segment.name}" + case LimbSkinType.SKIN_LIMB_TYPE_NORMAL if self.segment is not None: + data += self.segment.name + case _: + raise ValueError(f"Invalid segment, segmentType combination in SkinLimb {self.name}") + + return data + + +OOTLimb = TypeVar("OOTLimb", bound=OOTBaseLimb) + + +@dataclass +class OOTBaseSkeleton(ABC, Generic[OOTLimb]): + name: str + limbType: type[OOTLimb] + limbRoot: OOTLimb | None = None + + @property + def skeletonName(self) -> str: + return self.name + + def addChild(self, child: OOTLimb) -> None: + self.limbRoot = child + + def createLimbList(self) -> list[OOTLimb]: if self.limbRoot is None: return [] @@ -20,28 +241,20 @@ def createLimbList(self): self.limbRoot.setLinks() return limbList - def getNumDLs(self): - if self.limbRoot is not None: - return self.limbRoot.getNumDLs() - else: - return 0 - - def getNumLimbs(self): + def getNumLimbs(self) -> int: if self.limbRoot is not None: return self.limbRoot.getNumLimbs() else: return 0 - def isFlexSkeleton(self): - if self.limbRoot is not None: - return self.limbRoot.isFlexSkeleton() - else: - return False + def limbsName(self) -> str: + return f"{self.name}Limbs" - def limbsName(self): - return self.name + "Limbs" + @abstractmethod + def headerData(self) -> CData: + ... - def toC(self): + def toC(self) -> CData: limbData = CData() data = CData() @@ -49,155 +262,61 @@ def toC(self): return data limbList = self.createLimbList() - isFlex = self.isFlexSkeleton() data.source += "void* " + self.limbsName() + "[" + str(self.getNumLimbs()) + "] = {\n" for limb in limbList: - limbData.source += limb.toC(self.hasLOD) - data.source += "\t&" + limb.name() + ",\n" + limbData.source += limb.toC() + data.source += "\t&" + limb.name + ",\n" limbData.source += "\n" data.source += "};\n\n" - if isFlex: - data.source += ( - "FlexSkeletonHeader " - + self.name - + " = { " - + self.limbsName() - + ", " - + str(self.getNumLimbs()) - + ", " - + str(self.getNumDLs()) - + " };\n\n" - ) - data.header = "extern FlexSkeletonHeader " + self.name + ";\n" - else: - data.source += ( - "SkeletonHeader " + self.name + " = { " + self.limbsName() + ", " + str(self.getNumLimbs()) + " };\n\n" - ) - data.header = "extern SkeletonHeader " + self.name + ";\n" + data.append(self.headerData()) for limb in limbList: - name = (self.name + "_" + toAlnum(limb.boneName)).upper() + name = f"{self.name}_{toAlnum(limb.boneName)}".upper() if limb.index == 0: - data.header += "#define " + name + "_POS_LIMB 0\n" - data.header += "#define " + name + "_ROT_LIMB 1\n" + data.header += f"#define {name}_POS_LIMB 0\n" + data.header += f"#define {name}_ROT_LIMB 1\n" else: - data.header += "#define " + name + "_LIMB " + str(limb.index + 1) + "\n" - data.header += "#define " + self.name.upper() + "_NUM_LIMBS " + str(len(limbList) + 1) + "\n" + data.header += f"#define {name}_LIMB {limb.index + 1}\n" + data.header += f"#define {self.name.upper()}_NUM_LIMBS {len(limbList) + 1}\n" limbData.append(data) return limbData -class OOTDLReference: - def __init__(self, name: str): - self.name = name - - -class OOTLimb: - def __init__( - self, - skeletonName: str, - boneName: str, - index: int, - translation: mathutils.Vector, - DL: GfxList | OOTDLReference, - lodDL: GfxList | OOTDLReference, - ): - self.skeletonName = skeletonName - self.boneName = boneName - self.translation = translation - self.firstChildIndex = 0xFF - self.nextSiblingIndex = 0xFF - self.DL = DL - self.lodDL = lodDL - - self.isFlex = False - self.index = index - self.children = [] - self.inverseRotation = None - - def toC(self, isLOD): - if not isLOD: - data = "StandardLimb " - else: - data = "LodLimb " - - data += ( - self.name() - + " = { " - + "{ " - + str(int(round(self.translation[0]))) - + ", " - + str(int(round(self.translation[1]))) - + ", " - + str(int(round(self.translation[2]))) - + " }, " - + str(self.firstChildIndex) - + ", " - + str(self.nextSiblingIndex) - + ", " - ) - - if not isLOD: - data += self.DL.name if self.DL is not None else "NULL" - else: - data += ( - "{ " - + (self.DL.name if self.DL is not None else "NULL") - + ", " - + (self.lodDL.name if self.lodDL is not None else "NULL") - + " }" - ) +@dataclass +class StandardSkeleton(OOTBaseSkeleton[OOTLimb]): + def headerData(self) -> CData: + data = CData() - data += " };\n" + data.source += f"SkeletonHeader {self.name} = {{ {self.limbsName()}, {self.getNumLimbs()} }};\n\n" + data.header = f"extern SkeletonHeader {self.name};\n" return data - def name(self): - return self.skeletonName + "Limb_" + format(self.index, "03") - - def getNumLimbs(self): - numLimbs = 1 - for child in self.children: - numLimbs += child.getNumLimbs() - return numLimbs - def getNumDLs(self): - numDLs = 0 - if self.DL is not None or self.lodDL is not None: - numDLs += 1 +FlexLimb = TypeVar("FlexLimb", StandardLimb, LODLimb) - for child in self.children: - numDLs += child.getNumDLs() - return numDLs +@dataclass +class FlexSkeleton(OOTBaseSkeleton[FlexLimb]): + def headerData(self) -> CData: + data = CData() + data.source += ( + f"FlexSkeletonHeader {self.name} = {{ {self.limbsName()}, {self.getNumLimbs()}, {self.getNumDLs()} }};\n\n" + ) + data.header = f"extern FlexSkeletonHeader {self.name};\n" + return data - def isFlexSkeleton(self): - if self.isFlex: - return True + def getNumDLs(self) -> int: + if self.limbRoot is not None: + return self.limbRoot.getNumDLs() else: - for child in self.children: - if child.isFlexSkeleton(): - return True - return False - - def getList(self, limbList): - # Like ootProcessBone, this must be in depth-first order to match the - # OoT SkelAnime draw code, so the bones are listed in the file in the - # same order as they are drawn. This is needed to enable the programmer - # to get the limb indices and to enable optimization between limbs. - limbList.append(self) - for child in self.children: - child.getList(limbList) + return 0 - def setLinks(self): - if len(self.children) > 0: - self.firstChildIndex = self.children[0].index - for i in range(len(self.children)): - if i < len(self.children) - 1: - self.children[i].nextSiblingIndex = self.children[i + 1].index - self.children[i].setLinks() - # self -> child -> sibling + +class OOTDLReference: + def __init__(self, name: str): + self.name = name diff --git a/fast64_internal/z64/exporter/skeleton/functions.py b/fast64_internal/z64/exporter/skeleton/functions.py index f60219a1f..37943854d 100644 --- a/fast64_internal/z64/exporter/skeleton/functions.py +++ b/fast64_internal/z64/exporter/skeleton/functions.py @@ -4,13 +4,21 @@ from pathlib import Path from ....f3d.f3d_gbi import DLFormat, FMesh, TextureExportSettings, ScrollMethod -from ....f3d.f3d_writer import getInfoDict +from ....f3d.f3d_writer import getInfoDict, MeshInfo from ...f3d_writer import ootProcessVertexGroup, writeTextureArraysNew, writeTextureArraysExisting -from ...model_classes import OOTModel, OOTGfxFormatter +from ...model_classes import LimbType, LimbSkinType, OOTModel, OOTGfxFormatter, OOTVertexGroupInfo, OOTVert from ...skeleton.constants import ootSkeletonImportDict from ...skeleton.properties import OOTSkeletonExportSettings -from ...skeleton.utility import ootDuplicateArmatureAndRemoveRotations, getGroupIndices, ootRemoveSkeleton -from .classes import OOTLimb, OOTSkeleton +from ...skeleton.utility import ( + ootDuplicateArmatureAndRemoveRotations, + getGroupIndices, + ootRemoveSkeleton, + getRecursiveSortedChildren, + ootConstructSkeleton, +) + +from .classes import OOTBaseLimb, OOTBaseSkeleton + from ....utility import ( PluginError, @@ -32,27 +40,33 @@ def ootProcessBone( - fModel, - boneName, - parentLimb, - nextIndex, - meshObj, - armatureObj, - convertTransformMatrix, - meshInfo, - convertTextureData, - namePrefix, - skeletonOnly, - drawLayer, - lastMaterialName, + fModel: OOTModel, + boneName: str, + parentLimb: OOTBaseSkeleton | OOTBaseLimb, + limbOverride: type[OOTBaseLimb], + nextIndex: int, + meshObj: bpy.types.Object, + armatureObj: bpy.types.Object, + convertTransformMatrix: mathutils.Matrix, + meshInfo: MeshInfo[OOTVertexGroupInfo], + convertTextureData: bool, + namePrefix: str, + skeletonOnly: bool, + drawLayer: str, + lastMaterialName: str | None, optimize: bool, -): +) -> tuple[int, str | None]: + if not isinstance(meshInfo.vertexGroupInfo, OOTVertexGroupInfo): + raise PluginError("'meshInfo.vertexGroupInfo' must be of type 'OOTVertexGroupInfo' in function ootProcessBone.") + bone = armatureObj.data.bones[boneName] if bone.parent is not None: transform = convertTransformMatrix @ bone.parent.matrix_local.inverted() @ bone.matrix_local else: transform = convertTransformMatrix @ bone.matrix_local + limbSkinType = meshInfo.vertexGroupInfo.skinnedVertexGroups[boneName].type + translate, rotate, scale = transform.decompose() groupIndex = getGroupIndexFromname(meshObj, boneName) @@ -62,6 +76,7 @@ def ootProcessBone( if skeletonOnly: mesh = None hasSkinnedFaces = None + limbSkinType = LimbSkinType.EMPTY else: mesh, hasSkinnedFaces, lastMaterialName = ootProcessVertexGroup( fModel, @@ -89,22 +104,16 @@ def ootProcessBone( DL = None if mesh is not None: - if not bone.use_deform: + if not bone.use_deform and limbSkinType != LimbSkinType.SKIN_LIMB_TYPE_ANIMATED: raise PluginError( bone.name + " has vertices in its vertex group but is not set to deformable. Make sure to enable deform on this bone." ) DL = mesh.draw - if isinstance(parentLimb, OOTSkeleton): - skeleton = parentLimb - limb = OOTLimb(skeleton.name, boneName, nextIndex, translate, DL, None) - skeleton.limbRoot = limb - else: - limb = OOTLimb(parentLimb.skeletonName, boneName, nextIndex, translate, DL, None) - parentLimb.children.append(limb) + limb = limbOverride(parentLimb.skeletonName, boneName, nextIndex, translate, mesh, limbSkinType) + parentLimb.addChild(limb) - limb.isFlex = hasSkinnedFaces nextIndex += 1 # This must be in depth-first order to match the OoT SkelAnime draw code, so @@ -117,6 +126,7 @@ def ootProcessBone( fModel, childName, limb, + limbOverride, nextIndex, meshObj, armatureObj, @@ -145,11 +155,11 @@ def ootConvertArmatureToSkeleton( ): checkEmptyName(name) - armatureObj, meshObjs = ootDuplicateArmatureAndRemoveRotations(originalArmatureObj) + isSkinLimbExport = originalArmatureObj.ootSkeleton.isSkinLimb + # SkinLimbs need to be exported in their rest pose + armatureObj, meshObjs = ootDuplicateArmatureAndRemoveRotations(originalArmatureObj, not isSkinLimbExport) try: - skeleton = OOTSkeleton(name) - if len(armatureObj.children) == 0: raise PluginError("No mesh parented to armature.") @@ -159,17 +169,29 @@ def ootConvertArmatureToSkeleton( startBoneName = getStartBone(armatureObj) meshObj = meshObjs[0] - meshInfo = getInfoDict(meshObj) - getGroupIndices(meshInfo, armatureObj, meshObj, getGroupIndexFromname(meshObj, startBoneName)) + vertexGroupInfo = getGroupIndices(armatureObj, meshObj, getGroupIndexFromname(meshObj, startBoneName)) + skeleton = ootConstructSkeleton(name, armatureObj, meshObj.data.loop_triangles, vertexGroupInfo) + meshInfo = getInfoDict(meshObj, vertexGroupInfo) convertTransformMatrix = convertTransformMatrix @ mathutils.Matrix.Diagonal(armatureObj.scale).to_4x4() + limbIndex = 0 + meshInfo.vertexGroupInfo.boneIndexToLimbIndex[armatureObj.data.bones.find(startBoneName)] = limbIndex + startBone = armatureObj.data.bones[startBoneName] + + for child in getRecursiveSortedChildren(startBone): + limbIndex += 1 + childName = child.name + boneIndex = armatureObj.data.bones.find(childName) + meshInfo.vertexGroupInfo.boneIndexToLimbIndex[boneIndex] = limbIndex + # for i in range(len(startBoneNames)): # startBoneName = startBoneNames[i] ootProcessBone( fModel, startBoneName, skeleton, + skeleton.limbType, 0, meshObj, armatureObj, @@ -247,7 +269,7 @@ def ootConvertArmatureToC( originalArmatureObj, convertTransformMatrix, fModel, skeletonName, not savePNG, drawLayer, optimize ) - if originalArmatureObj.ootSkeleton.LOD is not None: + if originalArmatureObj.ootSkeleton.LOD is not None and not originalArmatureObj.ootSkeleton.isSkinLimb: lodSkeleton, fModel = ootConvertArmatureToSkeletonWithMesh( originalArmatureObj.ootSkeleton.LOD, convertTransformMatrix, @@ -275,7 +297,6 @@ def ootConvertArmatureToC( for i in range(len(limbList)): limbList[i].lodDL = lodLimbList[i].DL - limbList[i].isFlex |= lodLimbList[i].isFlex header_filename = Path(filename).parts[-1] data = CData() @@ -289,6 +310,9 @@ def ootConvertArmatureToC( else: data.header += '#include "ultra64.h"\n' + '#include "array_count.h"\n' + '#include "animation.h"\n' + if skeleton.limbType.typeName == "Skin": + data.header += '#include "skin.h"\n' + data.source = f'#include "{header_filename}.h"\n\n' if not isCustomExport: data.header += f'#include "{folderName}.h"\n\n' diff --git a/fast64_internal/z64/f3d_writer.py b/fast64_internal/z64/f3d_writer.py index 7bdeb85f8..4d2485a74 100644 --- a/fast64_internal/z64/f3d_writer.py +++ b/fast64_internal/z64/f3d_writer.py @@ -3,25 +3,32 @@ import bpy from typing import Optional +from mathutils import Matrix -from ..utility import CData, getGroupIndexFromname, readFile, writeFile +from ..utility import CData, getGroupIndexFromname, readFile, writeFile, PluginError from ..f3d.flipbook import flipbook_to_c, flipbook_2d_to_c, flipbook_data_to_c from ..f3d.f3d_material import createF3DMat, F3DMaterial_UpdateLock, update_preset_manual from .utility import replaceMatchContent, getOOTScale, ootStripComments from .texture_array import TextureFlipbook +from ..f3d.f3d_gbi import FMesh from ..f3d.f3d_writer import ( checkForF3dMaterialInFaces, saveOrGetF3DMaterial, saveMeshWithLargeTexturesByFaces, saveMeshByFaces, + MeshInfo, ) from .model_classes import ( OOTTriangleConverterInfo, + OOTTriangleConverter, OOTModel, ootGetActorData, ootGetLinkData, + OOTVertexGroupInfo, + LimbSkinType, + SkinAnimData, ) @@ -53,24 +60,34 @@ def getColliderMat(name: str, color: tuple[float, float, float, float]) -> bpy.t # mesh, # anySkinnedFaces (to determine if skeleton should be flex) def ootProcessVertexGroup( - fModel, - meshObj, - vertexGroup, - convertTransformMatrix, - armatureObj, - namePrefix, - meshInfo, - drawLayerOverride, - convertTextureData, - lastMaterialName, + fModel: OOTModel, + meshObj: bpy.types.Object, + vertexGroup: str, + convertTransformMatrix: Matrix, + armatureObj: bpy.types.Object, + namePrefix: str, + meshInfo: MeshInfo[OOTVertexGroupInfo], + drawLayerOverride: str, + convertTextureData: bool, + lastMaterialName: str | None, optimize: bool, -): +) -> tuple[FMesh | None, bool, str | None]: if not optimize: lastMaterialName = None mesh = meshObj.data currentGroupIndex = getGroupIndexFromname(meshObj, vertexGroup) nextDLIndex = len(meshInfo.vertexGroupInfo.vertexGroupToMatrixIndex) + + limbSkinType = meshInfo.vertexGroupInfo.skinnedVertexGroups[vertexGroup].type + smoothSkinned = False + + if limbSkinType in (LimbSkinType.EMPTY, LimbSkinType.SKINNED): + return None, False, lastMaterialName + elif limbSkinType == LimbSkinType.SKIN_LIMB_TYPE_ANIMATED: + smoothSkinned = True + currentGroupIndex = -1 + vertIndices = [ vert.index for vert in meshObj.data.vertices @@ -106,7 +123,10 @@ def ootProcessVertexGroup( vertGroupIndex = meshInfo.vertexGroupInfo.vertexGroups[faceVertIndex] if vertGroupIndex != currentGroupIndex: hasSkinnedFaces = True - if vertGroupIndex not in meshInfo.vertexGroupInfo.vertexGroupToLimb: + if ( + limbSkinType != LimbSkinType.SKIN_LIMB_TYPE_ANIMATED + and vertGroupIndex not in meshInfo.vertexGroupInfo.vertexGroupToLimb + ): # Connected to a bone not processed yet # These skinned faces will be handled by that limb connectedToUnhandledBone = True @@ -154,7 +174,10 @@ def ootProcessVertexGroup( # however it seems like OOT skeletons don't have this ability. # Therefore we always use the drawLayerOverride as the draw layer key. # This means everything will be saved to one mesh. - fMesh = fModel.addMesh(vertexGroup, namePrefix, drawLayerOverride, False, bone) + if not smoothSkinned: + fMesh = fModel.addMesh(vertexGroup, namePrefix, drawLayerOverride, False, bone) + else: + fMesh = fModel.addMesh(vertexGroup, namePrefix, drawLayerOverride, False, bone, meshOverride=SkinAnimData) for material_index, faces in groupFaces.items(): material = meshObj.material_slots[material_index].material @@ -164,6 +187,9 @@ def ootProcessVertexGroup( ) if fMaterial.isTexLarge[0] or fMaterial.isTexLarge[1]: + if smoothSkinned: + raise NotImplementedError("Large Texture Mode isn't implemented for SkinLimb Exports") + currentGroupIndex = saveMeshWithLargeTexturesByFaces( material, faces, @@ -192,6 +218,7 @@ def ootProcessVertexGroup( None, None, lastMaterialName, + OOTTriangleConverter, ) lastMaterialName = material.name if optimize else None diff --git a/fast64_internal/z64/model_classes.py b/fast64_internal/z64/model_classes.py index a1611e7db..b5d1537d5 100644 --- a/fast64_internal/z64/model_classes.py +++ b/fast64_internal/z64/model_classes.py @@ -2,18 +2,35 @@ import os from pathlib import Path import re +from bpy.types import MeshLoop, MeshLoopTriangle import mathutils -from typing import Union, Optional -from dataclasses import dataclass - +from enum import Enum +from typing import Union, Optional, NamedTuple, Generic, TypeVar +from collections import defaultdict +from dataclasses import dataclass, field from ..f3d.f3d_parser import F3DContext, F3DTextureReference, getImportData + from ..f3d.f3d_material import TextureProperty, createF3DMat, texFormatOf, texBitSizeF3D -from ..utility import PluginError, hexOrDecInt, create_or_get_world, indent +from ..utility import ( + PluginError, + CData, + hexOrDecInt, + create_or_get_world, + indent, + getBoneIndexFromGroupIndex, + getRgbNormalSettings, +) from ..f3d.flipbook import TextureFlipbook, usesFlipbook, ootFlipbookReferenceIsValid -from ..f3d.f3d_writer import VertexGroupInfo, TriangleConverterInfo - +from ..f3d.f3d_writer import ( + VertexGroupInfo, + TriangleConverterInfo, + TriangleConverter, + BufferVertex, + F3DVert, + getF3DVert, +) from ..f3d.f3d_texture_writer import ( getColorsUsedInImage, mergePalettes, @@ -31,11 +48,17 @@ SPDisplayList, GfxList, GfxListTag, + Vtx, + VtxList, + FTriGroup, + FMesh, DLFormat, SPMatrix, GfxFormatter, DPSetTile, + F3D, MTX_SIZE, + VTX_SIZE, ) from .utility import is_hackeroot @@ -141,6 +164,404 @@ def to_c(self, static=True): return indent + f"gsSPDisplayList({self.displayList.name}),\n" +@dataclass +class SkinVertex: + index: int + uv: list[int] + normal: list[int] + alpha: int + + # region properties + @property + def s(self): + return self.uv[0] + + @s.setter + def s(self, val) -> None: + self.uv[0] = val + + @property + def t(self): + return self.uv[1] + + @t.setter + def t(self, val) -> None: + self.uv[1] = val + + @property + def normX(self): + return (self.normal[0] + 128) % 256 - 128 + + @normX.setter + def normX(self, val) -> None: + self.normal[0] = val + + @property + def normY(self): + return (self.normal[1] + 128) % 256 - 128 + + @normY.setter + def normY(self, val) -> None: + self.normal[1] = val + + @property + def normZ(self): + return (self.normal[2] + 128) % 256 - 128 + + @normZ.setter + def normZ(self, val) -> None: + self.normal[2] = val + + # endregion + + def to_c(self) -> str: + return f"{{ {self.index}, {self.s}, {self.t}, {self.normX}, {self.normY}, {self.normZ}, {self.alpha} }}" + + +@dataclass +class SkinTransformation: + """Represents a vertex position multiplied by the inverse binding matrix of a limb""" + + limbIndex: int + x: int + y: int + z: int + scale: int + + def to_c(self) -> str: + return f"{{ {self.limbIndex}, {self.x}, {self.y}, {self.z}, {self.scale} }}" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SkinTransformation): + return False + return ( + self.limbIndex == other.limbIndex + and self.x == other.x + and self.y == other.y + and self.z == other.z + and self.scale == other.scale + ) + + +@dataclass +class SkinLimbModif: + skinVertices: list[SkinVertex] + limbTransformations: list[SkinTransformation] + + @property + def vtxCount(self) -> int: + return len(self.skinVertices) + + @property + def transformCount(self) -> int: + return len(self.limbTransformations) + + @property + def unk_4(self) -> int: + """The index of the SkinTransformation in limbTransformations with the greatest scale""" + index = self.limbTransformations.index(max(self.limbTransformations, key=lambda transform: transform.scale)) + return index + + def addVertex(self, other: "SkinLimbModif") -> None: + for skinVertex in other.skinVertices: + if skinVertex not in self.skinVertices: + self.skinVertices.append(skinVertex) + self.skinVertices.sort(key=lambda vertex: vertex.index) + + def to_c(self, vertexName: str, transformName: str) -> str: + data = f"{{ {self.vtxCount}, {self.transformCount}, {self.unk_4}, {vertexName}, {transformName} }},\n" + return data + + def __eq__(self, other) -> bool: + # Since this is only used for merging SkinLimbModifs, we only care if the SkinTransforms are the same + if not isinstance(other, SkinLimbModif): + return False + return sorted(self.limbTransformations, key=lambda transformation: transformation.limbIndex) == sorted( + other.limbTransformations, key=lambda transformation: transformation.limbIndex + ) + + +GT = TypeVar("GT", int, str) + + +@dataclass(frozen=True) +class VertexWeight(Generic[GT]): + group: GT + weight: float + + +VertexTransform = NamedTuple("Transform", [("limbIndex", int), ("pos", mathutils.Vector), ("weight", float)]) +IntTransform = NamedTuple("Transform", [("limbIndex", int), ("pos", tuple[int, int, int]), ("weight", int)]) + + +class OOTVtx(Vtx): + """subclass of Vtx that supports OoT style smooth skinning""" + + def __init__( + self, + position: list[int], + uv: list[int], + colorOrNormal: list[int], + packedNormal: int = 0, + transforms: list[IntTransform] | None = None, + ) -> None: + super().__init__(position, uv, colorOrNormal, packedNormal) + self.transforms = transforms or [] + + @property + def normal(self) -> list[int]: + return self.colorOrNormal[:3] + + @property + def alpha(self) -> int: + return self.colorOrNormal[3] + + @alpha.setter + def alpha(self, val) -> None: + self.colorOrNormal[3] = val + + @property + def groups(self) -> list[tuple[int, int]]: + return [(transform.limbIndex, transform.weight) for transform in self.transforms] + + +class OOTVtxList(VtxList): + """Subclass of VtxList extended to support SkinLimbModifs""" + + def __init__( + self, + name: str, + vertices: list[OOTVtx] | None = None, + modifs: list[SkinLimbModif] | None = None, + ) -> None: + super().__init__(name) + self.vertices = vertices or [] + self.modifs = modifs or [] + + def vtxToModifs(self) -> list[SkinLimbModif]: + if len(self.modifs) > 0: + return self.modifs + + skinLimbModifs: list[SkinLimbModif] = [] + + for index, vtx in enumerate(self.vertices): + skinTransforms: list[SkinTransformation] = [] + + for transform in vtx.transforms: + skinTransforms.append( + SkinTransformation( + transform.limbIndex, transform.pos[0], transform.pos[1], transform.pos[2], transform.weight + ) + ) + # To match the order in the extracted files. May matter when setting unk_4 + skinTransforms.sort(key=lambda transform: transform.limbIndex) + skinVertex = SkinVertex(index, vtx.uv, vtx.normal, vtx.alpha) + modif = SkinLimbModif([skinVertex], skinTransforms) + + if modif not in skinLimbModifs: + skinLimbModifs.append(modif) + else: + for skinLimbModif in skinLimbModifs: + if skinLimbModif == modif: + skinLimbModif.addVertex(modif) + + self.modifs = skinLimbModifs + return skinLimbModifs + + def to_c(self) -> CData: + if len(self.modifs) > 0: + return CData() + else: + return super().to_c() + + +class SkinAnimData(FMesh): + """subclass of FMesh for exporting SkinAnimatedLimbData""" + + def __init__(self, name: str, DLFormat: DLFormat) -> None: + super().__init__(name, DLFormat) + self.namePrefix = name.partition("mesh")[0] + self.name = self.namePrefix + "SkinAnimatedLimbData" + self.vtxList: OOTVtxList = OOTVtxList("(Vtx*)0x08000000") + + @property + def limbModifications(self) -> list[SkinLimbModif]: + return self.vtxList.vtxToModifs() + + @property + def totalVtxCount(self) -> int: + vtxCount = 0 + for modif in self.limbModifications: + vtxCount += modif.vtxCount + return vtxCount + + @property + def limbModifCount(self) -> int: + return len(self.limbModifications) + + @property + def dlist(self) -> str: + return self.draw.name + + def tri_group_new(self, fMaterial) -> FTriGroup: + triGroup = super().tri_group_new(fMaterial) + triGroup.vertexList = self.vtxList + return triGroup + + def to_c(self, f3d: F3D, gfxFormatter: GfxFormatter) -> tuple[CData, CData]: + staticData = CData() + transformData = CData() + vertexData = CData() + modifData = CData() + + modifName = f"{self.namePrefix}SkinLimbModif" + modifData.header += f"extern SkinLimbModif {modifName}[{self.limbModifCount}];\n" + modifData.source += f"SkinLimbModif {modifName}[{self.limbModifCount}] = {{\n" + + for index, modif in enumerate(self.limbModifications): + transformName = f"{self.namePrefix}SkinTransformation_{index:003}" + vertexName = f"{self.namePrefix}SkinVertex_{index:003}" + modifData.source += f"\t{modif.to_c(vertexName, transformName)}" + + transformData.header += "extern SkinTransformation " + f"{transformName}[{modif.transformCount}];\n" + transformData.source += "SkinTransformation " + f"{transformName}[{modif.transformCount}] = {{\n" + for transform in modif.limbTransformations: + transformData.source += f"\t{transform.to_c()},\n" + transformData.source += "};\n\n" + + vertexData.header += f"extern SkinVertex {vertexName}[{modif.vtxCount}];\n" + vertexData.source += f"SkinVertex {vertexName}[{modif.vtxCount}] = {{\n" + for vertex in modif.skinVertices: + vertexData.source += f"\t{vertex.to_c()},\n" + vertexData.source += "};\n\n" + + staticData.append(transformData) + staticData.append(vertexData) + staticData.append(modifData) + staticData.source += "};\n\n" + + staticData.header += f"extern SkinAnimatedLimbData {self.name};\n" + staticData.source += ( + f"SkinAnimatedLimbData {self.name} = {{\n" + + f"\t{self.totalVtxCount}, {self.limbModifCount},\n" + + f"\t{modifName}, {self.draw.name}\n" + + "};\n\n" + ) + + for triGroup in self.triangleGroups: + staticData.append(triGroup.to_c(f3d, gfxFormatter)) + + draw_layer = "Opaque" if "Opaque" in self.name else "Transparent" if "Transparent" in self.name else "Overlay" + dynamicData = gfxFormatter.drawToC(f3d, self.draw, layer=draw_layer) + + for cmd_list in self.draw_overrides: + dynamicData.append(cmd_list.to_c(f3d)) + + return staticData, dynamicData + + +class OOTVert(F3DVert): + """Subclass of F3DVert that can store multiple vertex groups and their weights; for OoT style smooth skinning""" + + def __init__( + self, + position: mathutils.Vector, + uv: mathutils.Vector, + rgb: mathutils.Vector | None, + normal: mathutils.Vector | None, + alpha: float, + transforms: list[VertexTransform] | None = None, + skinVert: bool = False, + ) -> None: + super().__init__(position, uv, rgb, normal, alpha) + self.transforms = transforms or [] + self.skinVert = skinVert + + @property + def unk_4(self) -> int: + """The index of the transform in transforms with the greatest weight""" + transform = sorted(self.transforms, key=lambda transform: transform[2], reverse=True)[0] + index = self.transforms.index(transform) + return index + + @property + def groups(self) -> list[VertexWeight]: + return [VertexWeight(transform.limbIndex, transform.weight) for transform in self.transforms] + + def addTransform(self, limbIndex: int, pos: mathutils.Vector, weight: float): + self.transforms.append(VertexTransform(limbIndex, pos, weight)) + self.transforms.sort(key=lambda transform: transform.limbIndex) + + def toVtx( + self, mesh, texDimensions, transformMatrix: mathutils.Matrix, isPointSampled: bool, tex_scale=(1, 1) + ) -> OOTVtx: + position = self.convertPosition(transformMatrix) + uv = self.convertUV(texDimensions, isPointSampled, tex_scale) + colorOrNormal, packedNormal = self.convertNormalRGB(transformMatrix) + intTransforms: list[IntTransform] = [] + for transform in self.transforms: + intTransforms.append( + IntTransform( + transform.limbIndex, + (round(transform.pos[0]), round(transform.pos[1]), round(transform.pos[2])), + round(transform.weight * 100), + ) + ) + return OOTVtx(position, uv, colorOrNormal, packedNormal, intTransforms) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, OOTVert): + return NotImplemented + + return ( + self.position == other.position + and self.uv == other.uv + and self.stOffset == other.stOffset + and self.rgb == other.rgb + and self.normal == other.normal + and self.alpha == other.alpha + and self.transforms == other.transforms + ) + + +@dataclass +class SkinAnimatedLimbData: + dlName: str = "" + vertexData: list[OOTVert] = field(default_factory=list) + baseAddr: str | None = None + + +class OOTTriangleConverter(TriangleConverter): + def getBufferVert(self, loop: MeshLoop, face: MeshLoopTriangle, groupIndex: int | None) -> BufferVertex: + vertexGroupInfo: OOTVertexGroupInfo = self.triConverterInfo.vertexGroupInfo + mesh: bpy.types.Mesh = self.triConverterInfo.mesh + groups = vertexGroupInfo.weights[loop.vertex_index] + transforms: list[VertexTransform] = [] + unk_4 = sorted(groups, key=lambda group: group.weight, reverse=True)[0].group + normMat = self.triConverterInfo.getTransformMatrix(unk_4).inverted().transposed() + normal = loop.normal.copy().freeze() + + for group in groups: + index = group.group + weight = group.weight + position: mathutils.Vector = mesh.vertices[loop.vertex_index].co.copy().freeze() + mat = self.triConverterInfo.getTransformMatrix(index) + boneIndex = getBoneIndexFromGroupIndex(self.triConverterInfo.obj, self.triConverterInfo.armature, index) + limbIndex = vertexGroupInfo.boneIndexToLimbIndex[boneIndex] + transforms.append(VertexTransform(limbIndex, mat @ position, weight)) + + vert = getF3DVert(loop, face, self.convertInfo, mesh, OOTVert) + vert.transforms = transforms + + if self.currentGroupIndex == -1: + vert.normal = (normMat @ normal).normalized() + vert.skinVert = True + + bufferVert = BufferVertex(vert, groupIndex, face.material_index) + + return bufferVert + + class OOTModel(FModel): def __init__(self, name, DLFormat, drawLayerOverride, draw_config: Optional[str] = None): self.drawLayerOverride = drawLayerOverride @@ -370,9 +791,39 @@ def getMatrixAddrFromGroup(self, groupIndex): return format((0x0D << 24) + MTX_SIZE * self.vertexGroupInfo.vertexGroupToMatrixIndex[groupIndex], "#010x") +# StrEnum is Python 3.11+ +class LimbType(str, Enum): + INVALID = "Invalid" + STANDARD = "Standard" + LOD = "Lod" + SKIN = "Skin" + + +class LimbSkinType(str, Enum): + # Contains no mesh data, segment is NULL + EMPTY = "0" + # Contains the smooth skinned mesh data, segment is SkinAnimatedLimbData + SKIN_LIMB_TYPE_ANIMATED = "SKIN_LIMB_TYPE_ANIMATED" + # Is a limb responsible for smooth skinned deformation, segment is NULL + SKINNED = "5" + # Functions like a StandardLimb, segment is DisplayList + SKIN_LIMB_TYPE_NORMAL = "SKIN_LIMB_TYPE_NORMAL" + + +@dataclass(frozen=True) +class SkinLimbGroup: + name: str + vertices: list[int] = field(default_factory=list) # vertex indices + weights: list[float] = field(default_factory=list) # vertex group weights out of 1.0 + type: LimbSkinType = LimbSkinType.EMPTY + + class OOTVertexGroupInfo(VertexGroupInfo): def __init__(self): - self.vertexGroupToMatrixIndex = {} + self.vertexGroupToMatrixIndex: dict[int | None, int] = {} + self.weights: dict[int, list[VertexWeight[int]]] = {} # vertex index to list of (group index, weight) + self.skinnedVertexGroups: dict[str, SkinLimbGroup] = {} # boneName to SkinLimbGroup + self.boneIndexToLimbIndex: dict[int, int] = {} VertexGroupInfo.__init__(self) @@ -397,6 +848,18 @@ def __init__(self, f3d, limbList, basePath): # materialContext.f3d_mat.rdp_settings.g_mdsft_cycletype = "G_CYC_1CYCLE" F3DContext.__init__(self, f3d, basePath, materialContext) self.draw_layer_prop = "oot" + self.vertOverride = OOTVert + self.initContext() + + def initContext(self): + super().initContext() + # nested dict of groupName{weight : [vertex index] } + self.ootLimbGroups: defaultdict[str, dict[float, list[int]]] = defaultdict(lambda: defaultdict(list)) + + # For handling SkinLimbs + self.skinAnimatedLimbData: SkinAnimatedLimbData | None = None + self.skinLimbType: list[LimbSkinType | None] = [] + self.isSkinDL: bool = False def getLimbName(self, index): return self.limbList[index] @@ -471,8 +934,91 @@ def getMaterialKey(self, material: bpy.types.Material): def clearGeometry(self): self.dlList = [] self.isBillboard = False + # self.initContext() super().clearGeometry() + def transformPosition(self, vert: OOTVert) -> mathutils.Vector: + limbMatrices = list(self.matrixData.values()) + position = mathutils.Vector((0.0, 0.0, 0.0)) + for transform in vert.transforms: + position += limbMatrices[transform.limbIndex] @ transform.pos * (transform.weight) + + return position + + def getVertexTransforms( + self, bufferVert: BufferVertex, has_normal: bool, has_packed_normals: bool + ) -> tuple[mathutils.Vector, mathutils.Vector]: + vert = bufferVert.f3dVert + if not isinstance(vert, OOTVert): + raise PluginError("vert must be of type OOTVert") + + if len(vert.transforms) == 0: + if isinstance(bufferVert.groupIndex, int): + groupIndex = bufferVert.groupIndex + else: + groupIndex = list(self.matrixData).index(bufferVert.groupIndex) + vert.addTransform(groupIndex, vert.position, 1.0) + + position = self.transformPosition(vert) + limbIndex = vert.transforms[vert.unk_4].limbIndex + transform = self.matrixData[self.getLimbName(limbIndex)] + normal = self.transformNormal(has_normal, has_packed_normals, vert, transform) + return position, normal + + def getTransformedVertex(self, index: int) -> BufferVertex: + bufferVert = self.vertexBuffer[index] + + if bufferVert is None: + raise PluginError("Vertex Buffer is empty.") + + vert = bufferVert.f3dVert + if not isinstance(vert, OOTVert): + raise PluginError("vert must be of type OOTVert") + + mat = self.mat() + has_rgb, has_normal, has_packed_normals = getRgbNormalSettings(mat) + has_packed_normals = has_packed_normals and not vert.skinVert + + position, normal = self.getVertexTransforms(bufferVert, has_normal, has_packed_normals) + uv, rgb, alpha = self.convertVertexValues(mat, has_rgb, vert) + transformedVert = OOTVert(position, uv, rgb, normal, alpha, transforms=vert.transforms) + + return BufferVertex(transformedVert, bufferVert.groupIndex, bufferVert.materialIndex) + + def updateBuffer(self, count, start, vertexData, vertexDataOffset): + for i in range(count): + vert: OOTVert = vertexData[vertexDataOffset + i] + self.vertexBuffer[start + i] = BufferVertex(vert, self.currentTransformName, 0) + + def processLimbGroups(self, verts: list[BufferVertex]) -> None: + for idx, bufferVert in enumerate(verts): + vert = bufferVert.f3dVert + assert isinstance(vert, OOTVert) + + for vertexWeight in vert.groups: + weight = vertexWeight.weight + group = vertexWeight.group + if isinstance(group, int): + boneName = self.getBoneName(group) + else: + boneName = self.limbToBoneName[group] + self.ootLimbGroups[boneName][weight].append(len(self.verts) + idx) + + self.verts.extend([vert.f3dVert for vert in verts]) + + def createVertexGroups(self, obj): + for limbGroup, weights in self.ootLimbGroups.items(): + if isinstance(limbGroup, str): + groupName = limbGroup + else: + groupName = self.getBoneName(limbGroup) + if not obj.vertex_groups.get(groupName): + group = obj.vertex_groups.new(name=groupName) + else: + group = obj.vertex_groups.get(groupName) + for weight, indices in weights.items(): + group.add(indices, weight, "REPLACE") + def clearMaterial(self): self.isBillboard = False @@ -566,6 +1112,16 @@ def loadTLUTPal(self, name: str, dlData: str, count: int): if not self.ignore_tlut: super().loadTLUTPal(name, dlData, count) + def getVertexSegmentData(self, segment: str, count: str, start: str, vertOverride: type[F3DVert] = F3DVert) -> None: + if not self.isSkinDL: + super().getVertexSegmentData(segment, count, start, vertOverride) + if self.skinAnimatedLimbData.baseAddr is None: + self.skinAnimatedLimbData.baseAddr = int(segment, 16) + + offset = (int(segment, 16) - self.skinAnimatedLimbData.baseAddr) // VTX_SIZE + end = offset + int(count) + int(start) + self.vertexData[segment] = self.skinAnimatedLimbData.vertexData[offset:end] + def clearOOTFlipbookProperty(flipbookProp): flipbookProp.enable = False diff --git a/fast64_internal/z64/skeleton/importer/functions.py b/fast64_internal/z64/skeleton/importer/functions.py index ce55f3c58..c911f17a0 100644 --- a/fast64_internal/z64/skeleton/importer/functions.py +++ b/fast64_internal/z64/skeleton/importer/functions.py @@ -18,13 +18,14 @@ removeComments, ) from ...f3d_writer import ootReadActorScale -from ...model_classes import OOTF3DContext, ootGetIncludedAssetData +from ...model_classes import LimbType, LimbSkinType, OOTF3DContext, ootGetIncludedAssetData from ...utility import OOTEnum, ootGetObjectPath, getOOTScale, ootGetObjectHeaderPath, ootGetEnums, ootStripComments from ...texture_array import ootReadTextureArrays from ..constants import ootSkeletonImportDict from ..properties import OOTSkeletonImportSettings from ..utility import ootGetLimb, ootGetLimbs, ootGetSkeleton, applySkeletonRestPose, get_anim_names from ...tools.quick_import import quick_import_exec +from .skinLimb_parser import parseSkinAnimatedLimbData, getSkinLimbRestPose class OOTDLEntry: @@ -33,18 +34,21 @@ def __init__(self, dlName, limbIndex): self.limbIndex = limbIndex -def ootAddBone(armatureObj, boneName, parentBoneName, currentTransform, loadDL): +def ootAddBone(armatureObj, boneName, parentBoneName, currentTransform, loadDL, limbSkinType): if bpy.context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") selectSingleObject(armatureObj) bpy.ops.object.mode_set(mode="EDIT") bone = armatureObj.data.edit_bones.new(boneName) bone.use_connect = False - bone.use_deform = loadDL + bone.use_deform = (loadDL and limbSkinType != LimbSkinType.SKIN_LIMB_TYPE_ANIMATED) | ( + limbSkinType == LimbSkinType.SKINNED + ) if parentBoneName is not None: bone.parent = armatureObj.data.edit_bones[parentBoneName] bone.head = currentTransform @ mathutils.Vector((0, 0, 0)) bone.tail = bone.head + (currentTransform.to_quaternion() @ mathutils.Vector((0, 0.3, 0))) + bone.align_roll(currentTransform.to_quaternion() @ mathutils.Vector((0, 0, 0.3))) # Connect bone to parent if it is possible without changing parent direction. @@ -70,21 +74,34 @@ def ootAddLimbRecursively( obj: bpy.types.Object, armatureObj: bpy.types.Object, parentTransform: mathutils.Matrix, - parentBoneName: str, + parentBoneName: str | None, f3dContext: OOTF3DContext, useFarLOD: bool, enums: List["OOTEnum"], + restPoseData: list[tuple[float, float, float]] | None = None, ): limbName = f3dContext.getLimbName(limbIndex) boneName = f3dContext.getBoneName(limbIndex) + f3dContext.limbToBoneName[limbName] = boneName limb_info = ootGetLimb(skeletonData, limbName, False) assert limb_info is not None - if limb_info.is_lod and useFarLOD: + if limb_info.limb_type == LimbType.LOD and useFarLOD: dlName = limb_info.far_dl_name + elif limb_info.limb_type == LimbType.SKIN: + if limb_info.skin_type == LimbSkinType.SKIN_LIMB_TYPE_ANIMATED: + f3dContext.skinAnimatedLimbData = parseSkinAnimatedLimbData(skeletonData, limb_info.dl_name) + dlName = f3dContext.skinAnimatedLimbData.dlName + else: + dlName = limb_info.dl_name else: dlName = limb_info.dl_name + if restPoseData is not None: + rotation = mathutils.Euler(restPoseData[limbIndex + 1]) + else: + rotation = mathutils.Euler((0, 0, 0)) + # Animations override the root translation, so we just ignore importing them as well. if limbIndex == 0: translation = [0, 0, 0] @@ -102,22 +119,38 @@ def ootAddLimbRecursively( # str(limbIndex) + " " + str(translation) + " " + str(nextChildIndex) + " " + \ # str(nextSiblingIndex) + " " + str(dlName)) - currentTransform = parentTransform @ mathutils.Matrix.Translation(mathutils.Vector(translation)) + if not limb_info.limb_type == LimbType.SKIN: + f3dContext.skinLimbType.append(None) + else: + f3dContext.skinLimbType.append(limb_info.skin_type) + + translationMatrix = mathutils.Matrix.Translation(translation) + rotationMatrix = rotation.to_matrix().to_4x4() + currentTransform = parentTransform @ translationMatrix @ rotationMatrix f3dContext.matrixData[limbName] = currentTransform loadDL = dlName != "NULL" - ootAddBone(armatureObj, boneName, parentBoneName, currentTransform, loadDL) + ootAddBone(armatureObj, boneName, parentBoneName, currentTransform, loadDL, limb_info.skin_type) # DLs can access bone transforms not yet processed. # Therefore were delay F3D parsing until after skeleton is processed. if loadDL: f3dContext.dlList.append(OOTDLEntry(dlName, limbIndex)) - isLOD = limb_info.is_lod + isLOD = limb_info.limb_type == LimbType.LOD if nextChildIndex != LIMB_DONE: isLOD |= ootAddLimbRecursively( - nextChildIndex, skeletonData, obj, armatureObj, currentTransform, boneName, f3dContext, useFarLOD, enums + nextChildIndex, + skeletonData, + obj, + armatureObj, + currentTransform, + boneName, + f3dContext, + useFarLOD, + enums, + restPoseData, ) if nextSiblingIndex != LIMB_DONE: @@ -131,6 +164,7 @@ def ootAddLimbRecursively( f3dContext, useFarLOD, enums, + restPoseData, ) return isLOD @@ -175,6 +209,7 @@ def ootBuildSkeleton( isLink, flipbookArrayIndex2D: int, f3dContext: OOTF3DContext, + restPoseData: list[tuple[float, float, float]] | None = None, ): lodString = "_lod" if useFarLOD else "" @@ -204,11 +239,25 @@ def ootBuildSkeleton( transformMatrix = mathutils.Matrix.Scale(1 / actorScale, 4) isLOD = ootAddLimbRecursively( - 0, skeletonData, obj, armatureObj, transformMatrix, None, f3dContext, useFarLOD, enums + 0, + skeletonData, + obj, + armatureObj, + transformMatrix, + None, + f3dContext, + useFarLOD, + enums, + restPoseData, ) for dlEntry in f3dContext.dlList: limbName = f3dContext.getLimbName(dlEntry.limbIndex) boneName = f3dContext.getBoneName(dlEntry.limbIndex) + f3dContext.isSkinDL = False + + if f3dContext.skinLimbType[dlEntry.limbIndex] == LimbSkinType.SKIN_LIMB_TYPE_ANIMATED: + f3dContext.isSkinDL = True + parseF3D( skeletonData, dlEntry.dlName, @@ -339,6 +388,16 @@ def ootImportSkeletonC(basePath: str, importSettings: OOTSkeletonImportSettings) if actorScale is None: actorScale = getOOTScale(importSettings.actorScale) + smoothSkinned = "SkinAnimatedLimbData" in skeletonData + + # SkinLimbs need a rest pose to import meshes correctly, + # but other limb types will import normals incorrectly if rest pose is set before mesh is imported + skinLimbRestPoseData = None + if smoothSkinned: + skinLimbRestPoseData = restPoseData or getSkinLimbRestPose( + filepaths[0], skeletonData, isCustomImport, actorScale + ) + isLOD, armatureObj = ootBuildSkeleton( skeletonName, overlayName, @@ -352,6 +411,7 @@ def ootImportSkeletonC(basePath: str, importSettings: OOTSkeletonImportSettings) isLink, flipbookArrayIndex2D, f3dContext, + skinLimbRestPoseData, ) if isLOD: isLOD, LODArmatureObj = ootBuildSkeleton( @@ -367,17 +427,24 @@ def ootImportSkeletonC(basePath: str, importSettings: OOTSkeletonImportSettings) isLink, flipbookArrayIndex2D, f3dContext, + skinLimbRestPoseData, ) armatureObj.ootSkeleton.LOD = LODArmatureObj LODArmatureObj.location += mathutils.Vector((10, 0, 0)) f3dContext.deleteMaterialContext() - if importSettings.applyRestPose and restPoseData is not None: + if not smoothSkinned and importSettings.applyRestPose and restPoseData is not None: applySkeletonRestPose(restPoseData, armatureObj) if isLOD: applySkeletonRestPose(restPoseData, LODArmatureObj) + armatureObj.ootSkeleton.isSkinLimb = smoothSkinned + + armatureObj.update_tag() + if isLOD: + LODArmatureObj.update_tag() + if import_animations: if armatureObj is not None: selectSingleObject(armatureObj) diff --git a/fast64_internal/z64/skeleton/importer/skinLimb_parser.py b/fast64_internal/z64/skeleton/importer/skinLimb_parser.py new file mode 100644 index 000000000..fb0bdb40e --- /dev/null +++ b/fast64_internal/z64/skeleton/importer/skinLimb_parser.py @@ -0,0 +1,114 @@ +import re +from mathutils import Vector +from ....utility import hexOrDecInt, PluginError, get_include_data +from ...model_classes import SkinAnimatedLimbData, OOTVert, VertexTransform, VertexWeight +from ...animation.importer.functions import ootGetAnimationData, ootGetAnimRawTranslation, ootGetAnimRawRotation + + +def getSkinLimbRestPose( + filepath: str, importData: str, isCustomImport: bool, actorScale: float +) -> list[tuple[float, float, float]]: + animName = re.search(r"AnimationHeader (.*?IdleAnim)", importData).group(1) + frameData, jointIndices, staticIndexMax, _ = ootGetAnimationData(filepath, importData, animName, isCustomImport) + + restPoseData: list[tuple[float, float, float]] = [ + tuple(ootGetAnimRawTranslation(0, staticIndexMax, frameData, jointIndices[0], actorScale)) + ] + for jointIndex in jointIndices[1:]: + restPoseData.append(tuple(ootGetAnimRawRotation(0, staticIndexMax, frameData, jointIndex, actorScale))) + + return restPoseData + + +def parseSkinVertex(includeData: str, skinVertexName: str, vertexData: list[OOTVert]): + pattern = r"SkinVertex\s+?" + re.escape(skinVertexName) + r"\[\]\s+=\s+?{\s*#include (.*?)};" + skinVertexData = re.search(pattern, includeData, re.DOTALL) + + if skinVertexData is None: + raise PluginError("Cannot find SkinVertex named: " + skinVertexName) + + data = get_include_data(skinVertexData.group(1), strip=True) + + verts: list[OOTVert] = [] + for skinVert in re.finditer(r"{(.*?)}", data, re.DOTALL): + values = skinVert.group(1).split(",") + index = hexOrDecInt(values[0]) + vert = vertexData[index] + uv = Vector((hexOrDecInt(values[1]), hexOrDecInt(values[2]))) + normal = Vector((hexOrDecInt(values[3]), hexOrDecInt(values[4]), hexOrDecInt(values[5]))) + alpha = hexOrDecInt(values[6]) + vert.uv = uv + # store normal in rgb to match how Vtx are imported + vert.rgb = normal + vert.alpha = alpha + vert.skinVert = True + verts.append(vert) + + return verts + + +def parseSkinTransformation( + includeData: str, skinTransformName: str +) -> tuple[list[VertexTransform], list[VertexWeight[int]]]: + pattern = r"SkinTransformation\s+?" + re.escape(skinTransformName) + r"\[\]\s+=\s+?{\s*#include (.*?)};" + skinTransformData = re.search(pattern, includeData, re.DOTALL) + + if skinTransformData is None: + raise PluginError("Cannot find SkinTransformation named: " + skinTransformName) + + data = get_include_data(skinTransformData.group(1), strip=True) + transforms: list[VertexTransform] = [] + weights: list[VertexWeight[int]] = [] + + for transform in re.finditer("{(.*?)}", data): + values = transform.group(1).split(",") + limbIndex = hexOrDecInt(values[0]) + pos = Vector((hexOrDecInt(values[1]), hexOrDecInt(values[2]), hexOrDecInt(values[3]))) + scale = hexOrDecInt(values[4]) * 0.01 + + transforms.append(VertexTransform(limbIndex, pos, scale)) + weights.append(VertexWeight(limbIndex, scale)) + + return transforms, weights + + +def parseSkinLimbModifs(includeData: str, modifName: str, vertexData: list[OOTVert]) -> None: + pattern = r"SkinLimbModif\s+?" + re.escape(modifName) + r"\[\]\s+=\s+?{\s*#include (.*?)};" + verts: list[OOTVert] = [] + modifData = re.search(pattern, includeData, re.DOTALL) + + if modifData is None: + raise PluginError("Cannot find SkinLimbModif named: " + modifName) + + data = get_include_data(modifData.group(1), strip=True) # .split(",") + + for modif in re.finditer(r"{.*?}", data, re.DOTALL): + value = modif.group(0).split(",") + limbTransformations, groups = parseSkinTransformation(includeData, value[4]) + verts = parseSkinVertex(includeData, value[3], vertexData) + + for vert in verts: + vert.transforms = limbTransformations + + +def parseSkinAnimatedLimbData(includeData: str, dataName: str) -> SkinAnimatedLimbData: + pattern = r"SkinAnimatedLimbData\s*?" + re.escape(dataName[1:]) + r"\s*?=\s*?{\s*?#include (.*?)};" + + animatedLimbText = re.search(pattern, includeData, re.DOTALL) + + if animatedLimbText is None: + raise PluginError(f"Cannot find SkinAnimatedLimbData named: {dataName}") + + data = get_include_data(animatedLimbText.group(1), strip=True).split(",") + + totalVtxCount = int(data[0]) + vertexData = [ + OOTVert(Vector([0, 0, 0]), Vector([0, 0]), Vector([0, 0, 0]), Vector([0, 0, 0]), 0) + for _ in range(totalVtxCount) + ] + + parseSkinLimbModifs(includeData, data[2], vertexData) + + skinAnimatedLimbData = SkinAnimatedLimbData(data[3], vertexData) + + return skinAnimatedLimbData diff --git a/fast64_internal/z64/skeleton/properties.py b/fast64_internal/z64/skeleton/properties.py index c3ca21bc1..079dacfd7 100644 --- a/fast64_internal/z64/skeleton/properties.py +++ b/fast64_internal/z64/skeleton/properties.py @@ -44,9 +44,13 @@ def draw_props(self, layout: UILayout): class OOTSkeletonProperty(PropertyGroup): LOD: PointerProperty(type=Object, poll=pollArmature) + isSkinLimb: BoolProperty() def draw_props(self, layout: UILayout): - prop_split(layout, self, "LOD", "LOD Skeleton") + prop_split(layout, self, "isSkinLimb", "Export as SkinLimbs (horses)") + if not self.isSkinLimb: + prop_split(layout, self, "LOD", "LOD Skeleton") + if self.LOD is not None: layout.label(text="Make sure LOD has same bone structure.", icon="BONE_DATA") diff --git a/fast64_internal/z64/skeleton/utility.py b/fast64_internal/z64/skeleton/utility.py index e42a1c5f9..e82a52ace 100644 --- a/fast64_internal/z64/skeleton/utility.py +++ b/fast64_internal/z64/skeleton/utility.py @@ -2,8 +2,9 @@ import mathutils, bpy, os, re from typing import Optional from ...utility_anim import armatureApplyWithMesh -from ..model_classes import OOTVertexGroupInfo -from ..utility import checkForStartBone, getStartBone, getNextBone, ootStripComments +from ..model_classes import OOTVertexGroupInfo, SkinLimbGroup, VertexWeight, LimbType, LimbSkinType +from ..utility import checkForStartBone, getStartBone, getNextBone, ootStripComments, getSortedChildren +from ...f3d.f3d_writer import MeshInfo from ...utility import ( PluginError, @@ -22,6 +23,16 @@ selectSingleObject, ) +from ..exporter.skeleton.classes import ( + OOTBaseLimb, + StandardLimb, + LODLimb, + SkinLimb, + OOTBaseSkeleton, + StandardSkeleton, + FlexSkeleton, +) + @dataclasses.dataclass class SkeletonInfo: @@ -96,10 +107,11 @@ class LimbInfo: translationZ_str: str nextChildIndex_str: str nextSiblingIndex_str: str - is_lod: bool dl_name: str far_dl_name: Optional[str] uses_include: bool + limb_type: LimbType + skin_type: LimbSkinType | None = None def ootGetLimb(skeletonData, limbName, continueOnError): @@ -124,11 +136,12 @@ def ootGetLimb(skeletonData, limbName, continueOnError): limb_data = result limbType = matchResultIni.group(1) - if limbType == "Lod": - is_lod = True + + if limbType == LimbType.LOD: dlRegex = r"\{\s*([^,\s]*)\s*,\s*([^,\s]*)\s*,?\}" + elif limbType == LimbType.SKIN: + dlRegex = r"([^,\s]*),\s*([^,\s]*)" else: - is_lod = False dlRegex = r"([^,\s]*)" matchResult = re.search( @@ -151,8 +164,19 @@ def ootGetLimb(skeletonData, limbName, continueOnError): dl_name = matchResult.group(6) - if is_lod: + far_dl_name = None + skin_type = None + + if limbType == LimbType.LOD: far_dl_name = matchResult.group(7) + elif limbType == LimbType.SKIN: + try: + skin_type = LimbSkinType(matchResult.group(6)) + except ValueError: + if continueOnError: + return None + raise PluginError(f"Invalid segmentType: {matchResult.group(6)} in SkinLimb named {limbName}") + dl_name = matchResult.group(7) else: far_dl_name = None @@ -164,10 +188,11 @@ def ootGetLimb(skeletonData, limbName, continueOnError): translationZ_str, nextChildIndex_str, nextSiblingIndex_str, - is_lod, dl_name, far_dl_name, uses_include, + limbType, + skin_type, ) @@ -181,13 +206,59 @@ def get_anim_names(skeleton_data: str, is_link: bool): return re.findall(rf"{struct_name}\s+(\w+)", skeleton_data) -def getGroupIndexOfVert(vert, armatureObj, obj, rootGroupIndex): - actualGroups = [] +def vertexGroupGenerator(vert: bpy.types.MeshVertex, armature: bpy.types.Armature, obj: bpy.types.Object): + for group in vert.groups: + if groupName := getGroupNameFromIndex(obj, group.group): + if groupName in armature.bones: + if armature.bones[groupName].ootBone.boneType != "Ignore": + yield groupName, group.weight + + +def getSkinLimbType( + vertices: list[int], weights: list[float], bone: bpy.types.Bone, isSkinLimbExport: bool +) -> LimbSkinType: + if len(vertices) == 0 and bone.ootBone.boneType != "Custom DL": + return LimbSkinType.EMPTY + elif all(weight == 1.0 for weight in weights): + return LimbSkinType.SKIN_LIMB_TYPE_NORMAL + else: + if not isSkinLimbExport: + return LimbSkinType.SKIN_LIMB_TYPE_NORMAL + return LimbSkinType.SKINNED + + +def getSkinAnimatedLimb(armatureObj: bpy.types.Object) -> dict[str, SkinLimbGroup]: + # Doesn't match vanilla assets, but it doesn't seem to matter + startBoneName = getStartBone(armatureObj) + return {startBoneName: SkinLimbGroup(startBoneName, [], [], LimbSkinType.SKIN_LIMB_TYPE_ANIMATED)} + + +def getSkinLimbGroups( + armatureObj: bpy.types.Object, skinnedVertexGroups: dict[str, tuple[list[int], list[float]]] +) -> tuple[set[int], dict[str, SkinLimbGroup]]: + isSkinLimbExport: bool = armatureObj.ootSkeleton.isSkinLimb + skinnedVertices: set[int] = set() + skinLimbGroups: dict[str, SkinLimbGroup] = {} + for groupName, (vertices, weights) in skinnedVertexGroups.items(): + bone = armatureObj.data.bones[groupName] + limbSkinType = getSkinLimbType(vertices, weights, bone, isSkinLimbExport) + skinLimbGroups[groupName] = SkinLimbGroup(groupName, vertices, weights, limbSkinType) + if limbSkinType == LimbSkinType.SKINNED: + skinnedVertices.update(vertices) + + if isSkinLimbExport: + skinLimbGroups |= getSkinAnimatedLimb(armatureObj) + + return skinnedVertices, skinLimbGroups + + +def getGroupIndexOfVert(vert: bpy.types.MeshVertex, armature: bpy.types.Armature, obj, rootGroupIndex): + actualGroups: list[bpy.types.VertexGroupElement] = [] nonBoneGroups = [] for group in vert.groups: groupName = getGroupNameFromIndex(obj, group.group) if groupName is not None: - if groupName in armatureObj.data.bones: + if groupName in armature.bones: actualGroups.append(group) else: nonBoneGroups.append(groupName) @@ -206,21 +277,35 @@ def getGroupIndexOfVert(vert, armatureObj, obj, rootGroupIndex): else: raise VertexWeightError("There are unweighted vertices in the mesh that must be weighted to a bone.") - vertGroup = actualGroups[0] - for group in actualGroups: - if group.weight > vertGroup.weight: - vertGroup = group + weights = [VertexWeight[int](group.group, group.weight) for group in actualGroups] + vertGroup = sorted(actualGroups, key=lambda group: group.weight, reverse=True)[0] # if vertGroup not in actualGroups: # raise VertexWeightError("A vertex was found that was primarily weighted to a group that does not correspond to a bone in #the armature. (" + getGroupNameFromIndex(obj, vertGroup.group) + ') Either decrease the weights of this vertex group or remove it. If you think this group should correspond to a bone, make sure to check your spelling.') - return vertGroup.group + return vertGroup.group, weights + +def getGroupIndices(armatureObj, meshObj, rootGroupIndex) -> OOTVertexGroupInfo: + armature = armatureObj.data + vertexGroupInfo = OOTVertexGroupInfo() + skinnedVertexGroups: dict[str, tuple[list[int], list[float]]] = {bone.name: ([], []) for bone in armature.bones} -def getGroupIndices(meshInfo, armatureObj, meshObj, rootGroupIndex): - meshInfo.vertexGroupInfo = OOTVertexGroupInfo() for vertex in meshObj.data.vertices: - meshInfo.vertexGroupInfo.vertexGroups[vertex.index] = getGroupIndexOfVert( - vertex, armatureObj, meshObj, rootGroupIndex - ) + ( + vertexGroupInfo.vertexGroups[vertex.index], + vertexGroupInfo.weights[vertex.index], + ) = getGroupIndexOfVert(vertex, armature, meshObj, rootGroupIndex) + + for boneName, weight in vertexGroupGenerator(vertex, armature, meshObj): + skinnedVertexGroups[boneName][0].append(vertex.index) + skinnedVertexGroups[boneName][1].append(weight) + + skinnedVertices, skinLimbGroups = getSkinLimbGroups(armatureObj, skinnedVertexGroups) + vertexGroupInfo.skinnedVertexGroups = skinLimbGroups + + for vertIndex in skinnedVertices: + # We just need a group index that won't overwrite any other vertex group + vertexGroupInfo.vertexGroups[vertIndex] = -1 + return vertexGroupInfo def ootRemoveSkeleton(filepath, objectName, skeletonName): @@ -301,7 +386,7 @@ def ootRemoveRotationsFromArmature(armatureObj: bpy.types.Object) -> None: armatureApplyWithMesh(armatureObj, bpy.context) -def ootDuplicateArmatureAndRemoveRotations(originalArmatureObj: bpy.types.Object): +def ootDuplicateArmatureAndRemoveRotations(originalArmatureObj: bpy.types.Object, removeRotations: bool): # Duplicate objects to apply scale / modifiers / linked data deselectAllObjects() @@ -324,7 +409,8 @@ def ootDuplicateArmatureAndRemoveRotations(originalArmatureObj: bpy.types.Object selectSingleObject(armatureObj) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True, properties=False) - ootRemoveRotationsFromArmature(armatureObj) + if removeRotations: + ootRemoveRotationsFromArmature(armatureObj) # Apply modifiers/data to mesh objs deselectAllObjects() @@ -377,3 +463,49 @@ def applySkeletonRestPose(boneData: list[tuple[float, float, float]], armatureOb bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.armature_apply_w_mesh() + + +def getRecursiveSortedChildren(bone: bpy.types.Bone): + children = sorted( + [child for child in bone.children if child.ootBone.boneType != "Ignore"], + key=lambda child: child.name.lower(), + ) + for child in children: + yield child + yield from getRecursiveSortedChildren(child) + + +def ootDetermineLimbType(armatureObj: bpy.types.Object) -> type[OOTBaseLimb]: + if armatureObj.ootSkeleton.isSkinLimb: + return SkinLimb + if armatureObj.ootSkeleton.LOD is not None: + return LODLimb + return StandardLimb + + +def ootDetermineSkeletonType( + faces: list[bpy.types.MeshLoopTriangle], vertexGroupInfo: OOTVertexGroupInfo +) -> type[OOTBaseSkeleton]: + for face in faces: + for vertex in face.vertices: + vertGroupIndex = vertexGroupInfo.vertexGroups[vertex] + if vertGroupIndex != vertexGroupInfo.vertexGroups[face.vertices[0]]: + return FlexSkeleton + + return StandardSkeleton + + +def ootConstructSkeleton( + name: str, + armatureObj: bpy.types.Object, + faces: list[bpy.types.MeshLoopTriangle], + vertexGroupInfo: OOTVertexGroupInfo, +) -> OOTBaseSkeleton: + limbClass = ootDetermineLimbType(armatureObj) + + if limbClass is SkinLimb: + skeleton = StandardSkeleton + else: + skeleton = ootDetermineSkeletonType(faces, vertexGroupInfo) + + return skeleton(name, limbClass)