From 744cf648b18d505a9a100c4849bf1c1d67e38f69 Mon Sep 17 00:00:00 2001 From: kurethedead Date: Tue, 10 Jan 2023 00:07:05 -0800 Subject: [PATCH 1/6] Restarted branch based on current main (no import/export) --- fast64_internal/oot/__init__.py | 15 + .../oot/actor_collider/__init__.py | 26 + .../oot/actor_collider/exporter.py | 235 +++++++ .../oot/actor_collider/importer.py | 526 +++++++++++++++ .../oot/actor_collider/operators.py | 108 +++ fast64_internal/oot/actor_collider/panels.py | 64 ++ .../oot/actor_collider/properties.py | 638 ++++++++++++++++++ fast64_internal/oot/actor_collider/utility.py | 259 +++++++ fast64_internal/oot/f3d/panels.py | 4 +- fast64_internal/oot/f3d/properties.py | 7 + fast64_internal/oot/oot_utility.py | 12 + fast64_internal/oot/tools/panel.py | 26 + fast64_internal/utility.py | 6 +- 13 files changed, 1923 insertions(+), 3 deletions(-) create mode 100644 fast64_internal/oot/actor_collider/__init__.py create mode 100644 fast64_internal/oot/actor_collider/exporter.py create mode 100644 fast64_internal/oot/actor_collider/importer.py create mode 100644 fast64_internal/oot/actor_collider/operators.py create mode 100644 fast64_internal/oot/actor_collider/panels.py create mode 100644 fast64_internal/oot/actor_collider/properties.py create mode 100644 fast64_internal/oot/actor_collider/utility.py diff --git a/fast64_internal/oot/__init__.py b/fast64_internal/oot/__init__.py index d9f19d7f0..6d8db46a2 100644 --- a/fast64_internal/oot/__init__.py +++ b/fast64_internal/oot/__init__.py @@ -52,6 +52,15 @@ oot_operator_unregister, ) +from .actor_collider import ( + actor_collider_props_register, + actor_collider_props_unregister, + actor_collider_ops_register, + actor_collider_ops_unregister, + actor_collider_panel_register, + actor_collider_panel_unregister, +) + class OOT_Properties(bpy.types.PropertyGroup): """Global OOT Scene Properties found under scene.fast64.oot""" @@ -84,6 +93,7 @@ def oot_panel_register(): anim_panels_register() skeleton_panels_register() cutscene_panels_register() + actor_collider_panel_register(), def oot_panel_unregister(): @@ -96,6 +106,7 @@ def oot_panel_unregister(): anim_panels_unregister() skeleton_panels_unregister() cutscene_panels_unregister() + actor_collider_panel_unregister(), def oot_register(registerPanels): @@ -120,6 +131,8 @@ def oot_register(registerPanels): f3d_ops_register() file_register() anim_props_register() + actor_collider_props_register(), + actor_collider_ops_register(), for cls in oot_classes: register_class(cls) @@ -153,6 +166,8 @@ def oot_unregister(unregisterPanels): f3d_ops_unregister() file_unregister() anim_props_unregister() + actor_collider_props_unregister(), + actor_collider_ops_unregister(), if unregisterPanels: oot_panel_unregister() diff --git a/fast64_internal/oot/actor_collider/__init__.py b/fast64_internal/oot/actor_collider/__init__.py new file mode 100644 index 000000000..62d4f4bc0 --- /dev/null +++ b/fast64_internal/oot/actor_collider/__init__.py @@ -0,0 +1,26 @@ +from .properties import ( + OOTActorColliderImportExportSettings, + drawColliderVisibilityOperators, + actor_collider_props_register, + actor_collider_props_unregister, +) +from .operators import ( + OOT_AddActorCollider, + OOT_CopyColliderProperties, + actor_collider_ops_register, + actor_collider_ops_unregister, +) +from .panels import ( + actor_collider_panel_register, + actor_collider_panel_unregister, +) + +from .importer import parseColliderData +from .exporter import getColliderData, removeExistingColliderData, writeColliderData + +# TODO: Code in other files +# oot_operators (operators) +# oot_f3d_writer, oot_skeleton (properties, functions) + +# getActorFilepath in exporter? +# movement of some functions? diff --git a/fast64_internal/oot/actor_collider/exporter.py b/fast64_internal/oot/actor_collider/exporter.py new file mode 100644 index 000000000..7e05523b2 --- /dev/null +++ b/fast64_internal/oot/actor_collider/exporter.py @@ -0,0 +1,235 @@ +import bpy, mathutils, os, re, math +from ...utility import CData, PluginError, readFile, writeFile +from ..oot_utility import getOrderedBoneList, getOOTScale +from ..oot_f3d_writer import getActorFilepath + + +def removeExistingColliderData(exportPath: str, overlayName: str, isLink: bool, newColliderData: str) -> str: + actorPath = getActorFilepath(exportPath, overlayName, isLink) + + data = readFile(actorPath) + newActorData = data + + for colliderMatch in re.finditer( + r"static\s*Collider[a-zA-Z0-9]*\s*([a-zA-Z0-9\_]*)", newColliderData, flags=re.DOTALL + ): + name = colliderMatch.group(1) + match = re.search( + r"static\s*Collider[a-zA-Z0-9]*\s*" + re.escape(name) + r".*?\}\s*;", newActorData, flags=re.DOTALL + ) + if match: + newActorData = newActorData[: match.start(0)] + newActorData[match.end(0) :] + + if newActorData != data: + writeFile(actorPath, newActorData) + + +def writeColliderData(obj: bpy.types.Object, exportPath: str, overlayName: str, isLink: bool) -> str: + actorFilePath = getActorFilepath(exportPath, overlayName, isLink) + actor = os.path.basename(actorFilePath)[:-2] + colliderData = getColliderData(obj) + + colliderFilename = actor + "_colliders.c" + colliderInclude = f'#include "{colliderFilename}"' + + actorData = readFile(actorFilePath) + if colliderInclude not in actorData: + actorData = colliderInclude + "\n" + actorData + writeFile(actorFilePath, actorData) + + colliderFilePath = os.path.join(os.path.dirname(actorFilePath), colliderFilename) + writeFile(colliderFilePath, f'#include "global.h"\n\n' + colliderData.source) + + +def getColliderData(parentObj: bpy.types.Object) -> CData: + + # TODO: Handle hidden? + colliderObjs = [obj for obj in parentObj.children if obj.ootGeometryType == "Actor Collider"] + + data = CData() + data.source += getColliderDataSingle(colliderObjs, "COLSHAPE_CYLINDER", "ColliderCylinderInit") + data.source += getColliderDataJointSphere(colliderObjs) + data.source += getColliderDataMesh(colliderObjs) + data.source += getColliderDataSingle(colliderObjs, "COLSHAPE_QUAD", "ColliderQuadInit") + + return data + + +def getShapeData(obj: bpy.types.Object, bone: bpy.types.Bone | None = None) -> str: + shape = obj.ootActorCollider.colliderShape + translate, rotate, scale = obj.matrix_local.decompose() + yUpToZUp = mathutils.Quaternion((1, 0, 0), math.radians(90.0)) + noXYRotation = rotate.to_euler()[0] < 0.01 and rotate.to_euler()[1] < 0.01 + + if shape == "COLSHAPE_JNTSPH": + if obj.parent is None: + raise PluginError(f"Joint sphere collider {obj.name} must be parented to a mesh or armature.") + + isUniform = abs(scale[0] - scale[1]) < 0.001 and abs(scale[1] - scale[2]) < 0.001 + if not isUniform: + raise PluginError(f"Sphere collider {obj.name} must have uniform scale (radius).") + + if isinstance(obj.parent.data, bpy.types.Armature) and bone is not None: + boneList = getOrderedBoneList(obj.parent) + limb = boneList.index(bone) + 1 + else: + limb = obj.ootActorColliderItem.limbOverride + + # When object is parented to bone, its matrix_local is relative to the tail(?) of that bone. + # No need to apply yUpToZUp here? + translateData = ", ".join( + [ + str(round(value)) + for value in getOOTScale(obj.parent.ootActorScale) + * (translate + (mathutils.Vector((0, bone.length, 0))) if bone is not None else translate) + ] + ) + scale = bpy.context.scene.ootBlenderScale * scale + radius = round(abs(scale[0])) + + return f"{{ {limb}, {{ {{ {translateData} }} , {radius} }}, 100 }},\n" + + elif shape == "COLSHAPE_CYLINDER": + if not noXYRotation: + raise PluginError(f"Cylinder collider {obj.name} must have zero rotation around XY axis.") + + isUniformXY = abs(scale[0] - scale[1]) < 0.001 + + # Convert to OOT space transforms + translate = bpy.context.scene.ootBlenderScale * (yUpToZUp.inverted() @ translate) + scale = bpy.context.scene.ootBlenderScale * (yUpToZUp.inverted() @ scale) + + if not isUniformXY: + raise PluginError(f"Cylinder collider {obj.name} must have uniform XY scale (radius).") + radius = round(abs(scale[0])) + height = round(scale[1] * 2) + + yShift = round(translate[1]) + position = [round(translate[0]), 0, round(translate[2])] + + return f"{{ {radius}, {height}, {yShift}, {{ {position[0]}, {position[1]}, {position[2]} }} }},\n" + + elif shape == "COLSHAPE_TRIS": + pass # handled in its own function + elif shape == "COLSHAPE_QUAD": + # geometry data ignored + return "{ { { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } } },\n" + else: + raise PluginError(f"Invalid shape: {shape} for {obj.name}") + + +def getColliderDataSingle(colliderObjs: list[bpy.types.Object], shape: str, structName: str) -> str: + filteredObjs = [obj for obj in colliderObjs if obj.ootActorCollider.colliderShape == shape] + colliderData = "" + for obj in filteredObjs: + collider = obj.ootActorCollider + colliderItem = obj.ootActorColliderItem + data = f"static {structName}{'Type1' if collider.physics.isType1 else ''} {collider.name} = {{\n" + data += collider.to_c(1) + data += colliderItem.to_c(1) + data += "\t" + getShapeData(obj) + data += "};\n\n" + + colliderData += data + + return colliderData + + +def getColliderDataJointSphere(colliderObjs: list[bpy.types.Object]) -> str: + sphereObjs = [ + obj + for obj in colliderObjs + if obj.ootActorCollider.colliderShape == "COLSHAPE_JNTSPH" and obj.parent is not None + ] + if len(sphereObjs) == 0: + return "" + + collider = sphereObjs[0].parent.ootActorCollider + name = collider.name + if "Init" in name: + elementsName = name[: name.index("Init")] + "Items" + name[name.index("Init") :] + else: + elementsName = collider.name + "Items" + colliderData = "" + + colliderData += f"static ColliderJntSphElementInit {elementsName}[{len(sphereObjs)}] = {{\n" + for obj in sphereObjs: + if obj.parent is not None and isinstance(obj.parent.data, bpy.types.Armature) and obj.parent_bone != "": + bone = obj.parent.data.bones[obj.parent_bone] + else: + bone = None + + data = "\t{\n" + data += obj.ootActorColliderItem.to_c(2) + data += "\t\t" + getShapeData(obj, bone) + data += "\t},\n" + + colliderData += data + colliderData += "};\n\n" + + # Required to make export use correct shape, otherwise unused so not an issue modifying here + collider.shape = "COLSHAPE_JNTSPH" + + colliderData += f"static ColliderJntSphInit{'Type1' if collider.physics.isType1 else ''} {name} = {{\n" + colliderData += collider.to_c(1) + colliderData += f"\t{len(sphereObjs)},\n" + colliderData += f"\t{elementsName},\n" + colliderData += "};\n\n" + + return colliderData + + +def getColliderDataMesh(colliderObjs: list[bpy.types.Object]) -> str: + meshObjs = [obj for obj in colliderObjs if obj.ootActorCollider.colliderShape == "COLSHAPE_TRIS"] + colliderData = "" + + yUpToZUp = mathutils.Quaternion((1, 0, 0), math.radians(90.0)) + transformMatrix = ( + mathutils.Matrix.Diagonal(mathutils.Vector([bpy.context.scene.ootBlenderScale for i in range(3)] + [1])) + @ yUpToZUp.to_matrix().to_4x4().inverted() + ) + + for obj in meshObjs: + collider = obj.ootActorCollider + name = collider.name + if "Init" in name: + elementsName = name[: name.index("Init")] + "Items" + name[name.index("Init") :] + else: + elementsName = collider.name + "Items" + mesh = obj.data + + if not (isinstance(mesh, bpy.types.Mesh) and len(mesh.materials) > 0): + raise PluginError(f"Mesh collider object {obj.name} must have a mesh with at least one material.") + + obj.data.calc_loop_triangles() + meshData = "" + for face in obj.data.loop_triangles: + material = obj.material_slots[face.material_index].material + + tris = [ + ", ".join( + [ + format(value, "0.4f") + "f" + for value in (transformMatrix @ obj.matrix_local @ mesh.vertices[face.vertices[i]].co) + ] + ) + for i in range(3) + ] + + triData = f"{{ {{ {{ {tris[0]} }}, {{ {tris[1]} }}, {{ {tris[2]} }} }} }},\n" + + meshData += "\t{\n" + meshData += material.ootActorColliderItem.to_c(2) + meshData += "\t\t" + triData + meshData += "\t},\n" + + colliderData += ( + f"static ColliderTrisElementInit {elementsName}[{len(mesh.loop_triangles)}] = {{\n{meshData}}};\n\n" + ) + colliderData += f"static ColliderTrisInit{'Type1' if collider.physics.isType1 else ''} {name} = {{\n" + colliderData += collider.to_c(1) + colliderData += f"\t{len(obj.data.loop_triangles)},\n" + colliderData += f"\t{elementsName},\n" + colliderData += "};\n\n" + + return colliderData diff --git a/fast64_internal/oot/actor_collider/importer.py b/fast64_internal/oot/actor_collider/importer.py new file mode 100644 index 000000000..17fb5250f --- /dev/null +++ b/fast64_internal/oot/actor_collider/importer.py @@ -0,0 +1,526 @@ +from typing import Callable +import bpy, mathutils, os, re, math +from ...utility import PluginError, hexOrDecInt +from ..oot_model_classes import ootGetActorData, ootGetIncludedAssetData, ootGetActorDataPaths, ootGetLinkData +from .properties import ( + OOTActorColliderItemProperty, + OOTActorColliderProperty, + OOTColliderHitboxItemProperty, + OOTColliderHitboxProperty, + OOTColliderHurtboxItemProperty, + OOTColliderHurtboxProperty, + OOTColliderPhysicsItemProperty, + OOTColliderPhysicsProperty, + OOTDamageFlagsProperty, + OOTActorColliderImportExportSettings, +) +from ..oot_utility import getOrderedBoneList, getOOTScale +from .utility import addColliderThenParent, ootEnumColliderType, ootEnumColliderElement, ootEnumHitboxSound + + +# has 1 capture group +def flagRegex(commaTerminating: bool = True) -> str: + return r"\s*([0-9a-zA-Z\_\s\|]*)\s*" + ("," if commaTerminating else ",?") + + +# has {count} capture groups +def flagListRegex(count: int) -> str: + regex = "" + if count < 1: + return "" + for i in range(count - 1): + regex += flagRegex() + regex += flagRegex(False) + return regex + + +# has 3 capture groups +def touchBumpRegex() -> str: + return r"\{\s*" + flagListRegex(3) + r"\s*\}\s*," + + +# has 7 capture groups +def colliderInitRegex() -> str: + return r"\{\s*" + flagListRegex(5) + "(" + flagRegex() + ")?" + r"\s*\}\s*," + + +# has 10 capture groups +def colliderInfoInitRegex() -> str: + return r"\{\s*" + flagRegex() + touchBumpRegex() + touchBumpRegex() + flagListRegex(3) + r"\s*\}\s*," + + +# assumes enums are in numerical order. +def getEnumValue(value: str, enumTuples: list[tuple[str, str, str]]) -> str: + enumList = [i[0] for i in enumTuples] + + if value in enumList: + return value + else: + try: + parsedValue = hexOrDecInt(value) + if parsedValue < len(enumList): + return parsedValue + else: + raise PluginError(f"Out of bounds index: {value}") + except ValueError: + raise PluginError(f"Invalid value: {value}") + + +def parseATFlags(flagData: str, atProp: OOTColliderHitboxProperty): + flags = [flag.strip() for flag in flagData.split("|")] + atProp.enable = "AT_ON" in flags + atProp.alignPlayer = "AT_TYPE_PLAYER" in flags or "AT_TYPE_ALL" in flags + atProp.alignEnemy = "AT_TYPE_ENEMY" in flags or "AT_TYPE_ALL" in flags + atProp.alignOther = "AT_TYPE_OTHER" in flags or "AT_TYPE_ALL" in flags + atProp.alignSelf = "AT_TYPE_SELF" in flags + + +def parseACFlags(flagData: str, acProp: OOTColliderHurtboxProperty): + flags = [flag.strip() for flag in flagData.split("|")] + acProp.enable = "AC_ON" in flags + acProp.attacksBounceOff = "AC_HARD" in flags + acProp.hurtByPlayer = "AC_TYPE_PLAYER" in flags or "AC_TYPE_ALL" in flags + acProp.hurtByEnemy = "AC_TYPE_ENEMY" in flags or "AC_TYPE_ALL" in flags + acProp.hurtByOther = "AC_TYPE_OTHER" in flags or "AC_TYPE_ALL" in flags + acProp.noDamage = "AC_NO_DAMAGE" in flags + + +def parseOCFlags(oc1Data: str, oc2Data: str | None, ocProp: OOTColliderPhysicsProperty): + flags1 = [flag.strip() for flag in oc1Data.split("|")] + ocProp.enable = "OC1_ON" in flags1 + ocProp.noPush = "OC1_NO_PUSH" in flags1 + ocProp.collidesWith.player = "OC1_TYPE_PLAYER" in flags1 or "OC1_TYPE_ALL" in flags1 + ocProp.collidesWith.type1 = "OC1_TYPE_1" in flags1 or "OC1_TYPE_ALL" in flags1 + ocProp.collidesWith.type2 = "OC1_TYPE_2" in flags1 or "OC1_TYPE_ALL" in flags1 + + if oc2Data is not None: + flags2 = [flag.strip() for flag in oc2Data.split("|")] + ocProp.isCollider.player = "OC2_TYPE_PLAYER" in flags2 + ocProp.isCollider.type1 = "OC2_TYPE_1" in flags2 + ocProp.isCollider.type2 = "OC2_TYPE_2" in flags2 + ocProp.skipHurtboxCheck = "OC2_FIRST_ONLY" in flags2 + ocProp.unk1 = "OC2_UNK1" in flags2 + ocProp.unk2 = "OC2_UNK2" in flags2 + + +def parseColliderInit(dataList: list[str], colliderProp: OOTActorColliderProperty): + colliderProp.colliderType = getEnumValue(dataList[0].strip(), ootEnumColliderType) + parseATFlags(dataList[1], colliderProp.hitbox) + parseACFlags(dataList[2], colliderProp.hurtbox) + parseOCFlags(dataList[3], dataList[4], colliderProp.physics) + + +def parseDamageFlags(flags: int, flagProp: OOTDamageFlagsProperty): + flagProp.dekuNut = flags & (1 << 0) != 0 + flagProp.dekuStick = flags & (1 << 1) != 0 + flagProp.slingshot = flags & (1 << 2) != 0 + flagProp.explosive = flags & (1 << 3) != 0 + flagProp.boomerang = flags & (1 << 4) != 0 + flagProp.arrowNormal = flags & (1 << 5) != 0 + flagProp.hammerSwing = flags & (1 << 6) != 0 + flagProp.hookshot = flags & (1 << 7) != 0 + flagProp.slashKokiriSword = flags & (1 << 8) != 0 + flagProp.slashMasterSword = flags & (1 << 9) != 0 + flagProp.slashGiantSword = flags & (1 << 10) != 0 + flagProp.arrowFire = flags & (1 << 11) != 0 + flagProp.arrowIce = flags & (1 << 12) != 0 + flagProp.arrowLight = flags & (1 << 13) != 0 + flagProp.arrowUnk1 = flags & (1 << 14) != 0 + flagProp.arrowUnk2 = flags & (1 << 15) != 0 + flagProp.arrowUnk3 = flags & (1 << 16) != 0 + flagProp.magicFire = flags & (1 << 17) != 0 + flagProp.magicIce = flags & (1 << 18) != 0 + flagProp.magicLight = flags & (1 << 19) != 0 + flagProp.shield = flags & (1 << 20) != 0 + flagProp.mirrorRay = flags & (1 << 21) != 0 + flagProp.spinKokiriSword = flags & (1 << 22) != 0 + flagProp.spinGiantSword = flags & (1 << 23) != 0 + flagProp.spinMasterSword = flags & (1 << 24) != 0 + flagProp.jumpKokiriSword = flags & (1 << 25) != 0 + flagProp.jumpGiantSword = flags & (1 << 26) != 0 + flagProp.jumpMasterSword = flags & (1 << 27) != 0 + flagProp.unknown1 = flags & (1 << 28) != 0 + flagProp.unblockable = flags & (1 << 29) != 0 + flagProp.hammerJump = flags & (1 << 30) != 0 + flagProp.unknown2 = flags & (1 << 31) != 0 + + +def parseTouch(dataList: list[str], touch: OOTColliderHitboxItemProperty, startIndex: int): + dmgFlags = int(dataList[startIndex + 1].strip(), 16) + parseDamageFlags(dmgFlags, touch.damageFlags) + touch.effect = hexOrDecInt(dataList[startIndex + 2]) + touch.damage = hexOrDecInt(dataList[startIndex + 3]) + + flags = [flag.strip() for flag in dataList[startIndex + 7].split("|")] + touch.enable = "TOUCH_ON" in flags + + for flag in flags: + if flag in [i[0] for i in ootEnumHitboxSound]: + touch.soundEffect = flag + + touch.drawHitmarksForEveryCollision = "TOUCH_AT_HITMARK" in flags + touch.closestBumper = "TOUCH_NEAREST" in flags + touch.unk7 = "TOUCH_UNK7" in flags + + +def parseBump(dataList: list[str], bump: OOTColliderHurtboxItemProperty, startIndex: int): + dmgFlags = int(dataList[startIndex + 4].strip(), 16) + parseDamageFlags(dmgFlags, bump.damageFlags) + bump.effect = hexOrDecInt(dataList[startIndex + 5]) + bump.defense = hexOrDecInt(dataList[startIndex + 6]) + + flags = [flag.strip() for flag in dataList[startIndex + 8].split("|")] + bump.enable = "BUMP_ON" in flags + bump.hookable = "BUMP_HOOKABLE" in flags + bump.giveInfoToHit = "BUMP_NO_AT_INFO" not in flags + bump.takesDamage = "BUMP_NO_DAMAGE" not in flags + bump.hasSound = "BUMP_NO_SWORD_SFX" not in flags + bump.hasHitmark = "BUMP_NO_HITMARK" not in flags + + +def parseObjectElement(dataList: list[str], objectElem: OOTColliderPhysicsItemProperty, startIndex: int): + flags = [flag.strip() for flag in dataList[startIndex + 9].split("|")] + objectElem.enable = "OCELEM_ON" in flags + objectElem.unk3 = "OCELEM_UNK3" in flags + + +def parseColliderInfoInit(dataList: list[str], colliderItemProp: OOTActorColliderItemProperty, startIndex: int): + colliderItemProp.element = getEnumValue(dataList[startIndex], ootEnumColliderElement) + parseTouch(dataList, colliderItemProp.touch, startIndex) + parseBump(dataList, colliderItemProp.bump, startIndex) + parseObjectElement(dataList, colliderItemProp.objectElem, startIndex) + + +def parseColliderData( + geometryName: str, + basePath: str, + overlayName: str, + isLink: bool, + parentObj: bpy.types.Object, + colliderSettings: OOTActorColliderImportExportSettings, +): + if not isLink: + actorData = ootGetActorData(basePath, overlayName) + currentPaths = ootGetActorDataPaths(basePath, overlayName) + else: + actorData = ootGetLinkData(basePath) + currentPaths = [os.path.join(basePath, f"src/code/z_player_lib.c")] + actorData = ootGetIncludedAssetData(basePath, currentPaths, actorData) + actorData + + if colliderSettings.chooseSpecific: + filterNameFunc = lambda geometryName, name: name in [ + value.strip() for value in colliderSettings.specificColliders.split(",") if value.strip() != "" + ] + + else: + filterNameFunc = noFilter + if overlayName == "ovl_Boss_Sst": + filterNameFunc = filterForSst + elif overlayName == "ovl_Boss_Va": + filterNameFunc = filterForVa + + if colliderSettings.cylinder: + parseCylinderColliders(actorData, parentObj, geometryName, filterNameFunc) + + if colliderSettings.jointSphere: + parseJointSphereColliders( + actorData, parentObj, geometryName, filterNameFunc, colliderSettings.parentJointSpheresToBone + ) + + if colliderSettings.mesh: + parseMeshColliders(actorData, parentObj, geometryName, filterNameFunc) + + if colliderSettings.quad: + parseQuadColliders(actorData, parentObj, geometryName, filterNameFunc) + + +def noFilter(name: str, colliderName: str): + return True + + +def filterForSst(geometryName: str, colliderName: str): + if "Hand" in geometryName: + return "Hand" in colliderName + elif "Head" in geometryName: + return "Head" in colliderName + else: + return False + + +def filterForVa(geometryName: str, colliderName: str): + if "BodySkel" in geometryName: + return colliderName == "sCylinderInit" + elif "SupportSkel" in geometryName: + return colliderName == "sJntSphInitSupport" + elif "ZapperSkel" in geometryName: + return colliderName == "sQuadInit" + elif "StumpSkel" in geometryName: + return False + elif "BariSkel" in geometryName: + return colliderName == "sJntSphInitBari" or colliderName == "sQuadInit" + else: + return False + + +def parseCylinderColliders( + data: str, parentObj: bpy.types.Object, geometryName: str | None, filterNameFunc: Callable[[str, str], bool] +): + handledColliders = [] + for match in re.finditer( + r"ColliderCylinderInit(Type1)?\s*([0-9a-zA-Z\_]*)\s*=\s*\{(.*?)\}\s*;", + data, + flags=re.DOTALL, + ): + + name = match.group(2) + colliderData = match.group(3) + + if not filterNameFunc(geometryName, name): + continue + + # This happens because our file including is not ideal and doesn't check for duplicate includes + if name in handledColliders: + continue + handledColliders.append(name) + + dataList = [ + item.strip() for item in colliderData.replace("{", "").replace("}", "").split(",") if item.strip() != "" + ] + if len(dataList) < 16 + 6: + raise PluginError(f"Collider {name} has unexpected struct format.") + + obj = addColliderThenParent("COLSHAPE_CYLINDER", parentObj, None) + parseColliderInit(dataList, obj.ootActorCollider) + parseColliderInfoInit(dataList, obj.ootActorColliderItem, 6) + + obj.name = f"Collider {name}" + obj.ootActorCollider.name = name + + radius = hexOrDecInt(dataList[16]) / bpy.context.scene.ootBlenderScale + height = hexOrDecInt(dataList[17]) / bpy.context.scene.ootBlenderScale + yShift = hexOrDecInt(dataList[18]) / bpy.context.scene.ootBlenderScale + position = [hexOrDecInt(value) / bpy.context.scene.ootBlenderScale for value in dataList[19:22]] + + yUpToZUp = mathutils.Quaternion((1, 0, 0), math.radians(90.0)) + location = mathutils.Vector((0, yShift, 0)) + mathutils.Vector(position) + obj.matrix_local = mathutils.Matrix.Diagonal( + mathutils.Vector((radius, radius, height / 2, 1)) + ) @ mathutils.Matrix.Translation(yUpToZUp @ location) + + +def parseJointSphereColliders( + data: str, + parentObj: bpy.types.Object, + geometryName: str | None, + filterNameFunc: Callable[[str, str], bool], + parentToBones: bool, +): + handledColliders = [] + for match in re.finditer( + r"ColliderJntSphInit\s*([0-9a-zA-Z\_]*)\s*=\s*\{(.*?)\}\s*;", + data, + flags=re.DOTALL, + ): + name = match.group(1) + colliderData = match.group(2) + + if not filterNameFunc(geometryName, name): + continue + + # This happens because our file including is not ideal and doesn't check for duplicate includes + if name in handledColliders: + continue + handledColliders.append(name) + + dataList = [ + item.strip() for item in colliderData.replace("{", "").replace("}", "").split(",") if item.strip() != "" + ] + if len(dataList) < 2 + 6: + raise PluginError(f"Collider {name} has unexpected struct format.") + + itemsName = dataList[7] + + parentObj.ootActorCollider.name = name + + parseColliderInit(dataList, parentObj.ootActorCollider) + parseJointSphereCollidersItems(data, parentObj, itemsName, name, parentToBones) + + +def parseJointSphereCollidersItems( + data: str, parentObj: bpy.types.Object, itemsName: str, name: str, parentToBones: bool +): + match = re.search( + r"ColliderJntSphElementInit\s*" + re.escape(itemsName) + r"\s*\[\s*[0-9A-Fa-fx]*\s*\]\s*=\s*\{(.*?)\}\s*;", + data, + flags=re.DOTALL, + ) + + if match is None: + raise PluginError(f"Could not find {itemsName}.") + + matchData = match.group(1) + + dataList = [item.strip() for item in matchData.replace("{", "").replace("}", "").split(",") if item.strip() != ""] + if len(dataList) % 16 != 0: + raise PluginError(f"{itemsName} has unexpected struct format.") + + willParentToBones = isinstance(parentObj.data, bpy.types.Armature) and parentToBones + + if willParentToBones: + boneList = getOrderedBoneList(parentObj) + else: + boneList = None + + count = int(round(len(dataList) / 16)) + for item in [dataList[16 * i : 16 * (i + 1)] for i in range(count)]: + + # Why subtract 1??? + # in SkelAnime_InitFlex: skelAnime->limbCount = skeletonHeader->sh.limbCount + 1; + # possibly? + # Note: works with king dodongo, not with ganon2 + # Note: king dodongo count = numElements - 1 + limb = hexOrDecInt(item[10]) - 1 + + location = mathutils.Vector( + [hexOrDecInt(value) / ((getOOTScale(parentObj.ootActorScale))) for value in item[11:14]] + ) + radius = hexOrDecInt(item[14]) / bpy.context.scene.ootBlenderScale + scale = hexOrDecInt(item[15]) / 100 # code defined constant + + obj = addColliderThenParent("COLSHAPE_JNTSPH", parentObj, boneList[limb] if willParentToBones else None) + parseColliderInfoInit(item, obj.ootActorColliderItem, 0) + + yUpToZUp = mathutils.Quaternion((1, 0, 0), math.radians(90.0)) + + if willParentToBones: + obj.matrix_world = ( + parentObj.matrix_world + @ parentObj.pose.bones[boneList[limb].name].matrix + @ mathutils.Matrix.Translation(location) + ) + else: + obj.matrix_local = mathutils.Matrix.Translation(yUpToZUp @ location) + obj.ootActorColliderItem.limbOverride = hexOrDecInt(item[10]) + + obj.scale.x = radius * scale + obj.scale.y = radius * scale + obj.scale.z = radius * scale + + +def parseMeshColliders( + data: str, parentObj: bpy.types.Object, geometryName: str | None, filterNameFunc: Callable[[str, str], bool] +): + handledColliders = [] + for match in re.finditer( + r"ColliderTrisInit(Type1)?\s*([0-9a-zA-Z\_]*)\s*=\s*\{(.*?)\}\s*;", + data, + flags=re.DOTALL, + ): + name = match.group(2) + colliderData = match.group(3) + + if not filterNameFunc(geometryName, name): + continue + + # This happens because our file including is not ideal and doesn't check for duplicate includes + if name in handledColliders: + continue + handledColliders.append(name) + + dataList = [ + item.strip() for item in colliderData.replace("{", "").replace("}", "").split(",") if item.strip() != "" + ] + if len(dataList) < 2 + 6: + raise PluginError(f"Collider {name} has unexpected struct format.") + + itemsName = dataList[7] + + obj = addColliderThenParent("COLSHAPE_TRIS", parentObj, None) + obj.name = f"Collider {name}" + obj.ootActorCollider.name = name + parseColliderInit(dataList, obj.ootActorCollider) + parseMeshCollidersItems(data, obj, itemsName, name) + + +def parseMeshCollidersItems(data: str, obj: bpy.types.Object, itemsName: str, name: str): + match = re.search( + r"ColliderTrisElementInit\s*" + re.escape(itemsName) + r"\s*\[\s*[0-9A-Fa-fx]*\s*\]\s*=\s*\{(.*?)\}\s*;", + data, + flags=re.DOTALL, + ) + + if match is None: + raise PluginError(f"Could not find {itemsName}.") + + matchData = match.group(1) + + dataList = [item.strip() for item in matchData.replace("{", "").replace("}", "").split(",") if item.strip() != ""] + if len(dataList) % 19 != 0: + raise PluginError(f"{itemsName} has unexpected struct format.") + + yUpToZUp = mathutils.Quaternion((1, 0, 0), math.radians(90.0)) + materialDict = {} # collider item hash : material index + vertList = [] + materialIndexList = [] + count = int(round(len(dataList) / 19)) + for item in [dataList[19 * i : 19 * (i + 1)] for i in range(count)]: + colliderHash = tuple(item[:10]) + if colliderHash not in materialDict: + material = bpy.data.materials.new(f"{name} Collider Material") + obj.data.materials.append(material) + materialDict[colliderHash] = material + parseColliderInfoInit(item, material.ootActorColliderItem, 0) + else: + material = materialDict[colliderHash] + + verts = [ + [ + float(value[:-1] if value[-1] == "f" else value) / bpy.context.scene.ootBlenderScale + for value in item[3 * i + 10 : 3 * i + 13] + ] + for i in range(3) + ] + for i in range(3): + transformedVert = yUpToZUp @ mathutils.Vector(verts[i]) + vertList.append(transformedVert[:]) + materialIndexList.append(obj.data.materials[:].index(material)) + + triangleCount = int(len(vertList) / 3) + faces = [[3 * i + j for j in range(3)] for i in range(triangleCount)] + obj.data.from_pydata(vertices=vertList, edges=[], faces=faces) + for i in range(triangleCount): + obj.data.polygons[i].material_index = materialIndexList[i] + + +def parseQuadColliders( + data: str, parentObj: bpy.types.Object, geometryName: str | None, filterNameFunc: Callable[[str, str], bool] +): + handledColliders = [] + for match in re.finditer( + r"ColliderQuadInit(Type1)?\s*([0-9a-zA-Z\_]*)\s*=\s*\{(.*?)\}\s*;", + data, + flags=re.DOTALL, + ): + name = match.group(2) + colliderData = match.group(3) + + if not filterNameFunc(geometryName, name): + continue + + # This happens because our file including is not ideal and doesn't check for duplicate includes + if name in handledColliders: + continue + handledColliders.append(name) + + dataList = [ + item.strip() for item in colliderData.replace("{", "").replace("}", "").split(",") if item.strip() != "" + ] + if len(dataList) < 16 + 6: + raise PluginError(f"Collider {name} has unexpected struct format.") + + obj = addColliderThenParent("COLSHAPE_QUAD", parentObj, None) + parseColliderInit(dataList, obj.ootActorCollider) + parseColliderInfoInit(dataList, obj.ootActorColliderItem, 6) + + obj.name = f"Collider {name}" + obj.ootActorCollider.name = name diff --git a/fast64_internal/oot/actor_collider/operators.py b/fast64_internal/oot/actor_collider/operators.py new file mode 100644 index 000000000..8c3db81c2 --- /dev/null +++ b/fast64_internal/oot/actor_collider/operators.py @@ -0,0 +1,108 @@ +import bpy +from ...utility import PluginError, raisePluginError, copyPropertyGroup +from .utility import updateColliderOnObj, addColliderThenParent, ootEnumColliderShape +from bpy.utils import register_class, unregister_class + + +class OOT_AddActorCollider(bpy.types.Operator): + bl_idname = "object.oot_add_actor_collider_operator" + bl_label = "Add Actor Collider" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + shape: bpy.props.EnumProperty(items=ootEnumColliderShape) + parentToBone: bpy.props.BoolProperty(default=False) + + def execute(self, context): + try: + activeObj = bpy.context.view_layer.objects.active + selectedObjs = bpy.context.selected_objects + + if activeObj is None: + raise PluginError("No object selected.") + + if context.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + + if self.parentToBone and self.shape == "COLSHAPE_JNTSPH": + if isinstance(activeObj.data, bpy.types.Armature): + selectedBones = [bone for bone in activeObj.data.bones if bone.select] + if len(selectedBones) == 0: + raise PluginError("Cannot add joint spheres since no bones are selected on armature.") + for bone in selectedBones: + addColliderThenParent(self.shape, activeObj, bone, self.shape != "COLSHAPE_TRIS") + else: + raise PluginError("Non armature object selected.") + else: + addColliderThenParent(self.shape, activeObj, None, self.shape != "COLSHAPE_TRIS") + + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} + + return {"FINISHED"} + + +class OOT_CopyColliderProperties(bpy.types.Operator): + bl_idname = "object.oot_copy_collider_properties_operator" + bl_label = "Copy Collider Properties" + bl_options = {"REGISTER", "UNDO", "PRESET"} + + def execute(self, context): + try: + activeObj = bpy.context.view_layer.objects.active + selectedObjs = [obj for obj in bpy.context.selected_objects if obj.ootGeometryType == "Actor Collider"] + + if activeObj is None: + raise PluginError("No object selected.") + + if activeObj.ootGeometryType != "Actor Collider": + raise PluginError("Active object is not an actor collider.") + + if context.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + + if ( + activeObj.ootActorCollider.colliderShape == "COLSHAPE_JNTSPH" + and activeObj.parent is not None + and isinstance(activeObj.parent.data, bpy.types.Armature) + ): + parentCollider = activeObj.parent.ootActorCollider + else: + parentCollider = activeObj.ootActorCollider + + for obj in selectedObjs: + if ( + obj.ootActorCollider.colliderShape == "COLSHAPE_JNTSPH" + and obj.parent is not None + and isinstance(obj.parent.data, bpy.types.Armature) + ): + copyPropertyGroup(parentCollider, obj.parent.ootActorCollider) + else: + copyPropertyGroup(parentCollider, obj.ootActorCollider) + copyPropertyGroup(activeObj.ootActorColliderItem, obj.ootActorColliderItem) + + updateColliderOnObj(obj) + + except Exception as e: + raisePluginError(self, e) + return {"CANCELLED"} + + return {"FINISHED"} + + +actor_collider_ops_classes = ( + OOT_AddActorCollider, + OOT_CopyColliderProperties, +) + + +def actor_collider_ops_register(): + for cls in actor_collider_ops_classes: + register_class(cls) + + +def actor_collider_ops_unregister(): + for cls in reversed(actor_collider_ops_classes): + unregister_class(cls) diff --git a/fast64_internal/oot/actor_collider/panels.py b/fast64_internal/oot/actor_collider/panels.py new file mode 100644 index 000000000..3f994bf33 --- /dev/null +++ b/fast64_internal/oot/actor_collider/panels.py @@ -0,0 +1,64 @@ +import bpy +from .utility import shapeNameToSimpleName +from bpy.utils import register_class, unregister_class + + +class OOT_ActorColliderPanel(bpy.types.Panel): + bl_label = "OOT Actor Collider Inspector" + bl_idname = "OBJECT_PT_OOT_Actor_Collider_Inspector" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + bl_options = {"HIDE_HEADER"} + + @classmethod + def poll(cls, context: bpy.types.Context): + return context.scene.gameEditorMode == "OOT" and ( + context.object is not None and isinstance(context.object.data, bpy.types.Mesh) + ) + + def draw(self, context: bpy.types.Context): + obj = context.object + if obj.ootGeometryType == "Actor Collider": + box = self.layout.box().column() + name = shapeNameToSimpleName(obj.ootActorCollider.colliderShape) + box.box().label(text=f"OOT Actor {name} Collider Inspector") + obj.ootActorCollider.draw(obj, box) + obj.ootActorColliderItem.draw(obj, box) + + +class OOT_ActorColliderMaterialPanel(bpy.types.Panel): + bl_label = "OOT Actor Collider Material Inspector" + bl_idname = "OBJECT_PT_OOT_Actor_Collider_Material_Inspector" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + bl_options = {"HIDE_HEADER"} + + @classmethod + def poll(cls, context: bpy.types.Context): + return ( + context.scene.gameEditorMode == "OOT" + and (context.object is not None and isinstance(context.object.data, bpy.types.Mesh)) + and context.object.ootGeometryType == "Actor Collider" + and context.material is not None + ) + + def draw(self, context: bpy.types.Context): + material = context.material + box = self.layout.box().column() + box.box().label(text=f"OOT Actor Mesh Collider Inspector") + material.ootActorColliderItem.draw(None, box) + + +actor_collider_panel_classes = (OOT_ActorColliderPanel, OOT_ActorColliderMaterialPanel) + + +def actor_collider_panel_register(): + for cls in actor_collider_panel_classes: + register_class(cls) + + +def actor_collider_panel_unregister(): + for cls in reversed(actor_collider_panel_classes): + unregister_class(cls) diff --git a/fast64_internal/oot/actor_collider/properties.py b/fast64_internal/oot/actor_collider/properties.py new file mode 100644 index 000000000..34931fb07 --- /dev/null +++ b/fast64_internal/oot/actor_collider/properties.py @@ -0,0 +1,638 @@ +import bpy, os +from bpy.utils import register_class, unregister_class +from ...utility import prop_split +from .utility import ( + updateCollider, + ootEnumColliderShape, + ootEnumColliderType, + ootEnumColliderElement, + ootEnumHitboxSound, +) + + +class OOTActorColliderImportExportSettings(bpy.types.PropertyGroup): + enable: bpy.props.BoolProperty(name="Actor Colliders", default=False) + chooseSpecific: bpy.props.BoolProperty(name="Choose Specific Colliders") + specificColliders: bpy.props.StringProperty(name="Colliders (Comma Separated List)") + jointSphere: bpy.props.BoolProperty(name="Joint Sphere", default=True) + cylinder: bpy.props.BoolProperty(name="Cylinder", default=True) + mesh: bpy.props.BoolProperty(name="Mesh", default=True) + quad: bpy.props.BoolProperty(name="Quad", default=True) + parentJointSpheresToBone: bpy.props.BoolProperty(name="Parent Joint Spheres To Bones", default=True) + + def draw(self, layout: bpy.types.UILayout, title: str, isImport: bool): + col = layout.column() + col.prop(self, "enable", text=title) + if self.enable: + col.prop(self, "chooseSpecific") + if self.chooseSpecific: + col.prop(self, "specificColliders") + row = col.row(align=True) + row.prop(self, "jointSphere", text="Joint Sphere", toggle=1) + row.prop(self, "cylinder", text="Cylinder", toggle=1) + row.prop(self, "mesh", text="Mesh", toggle=1) + row.prop(self, "quad", text="Quad", toggle=1) + + if isImport: + col.prop(self, "parentJointSpheresToBone") + + return col + + +# Defaults are from DMG_DEFAULT. +class OOTDamageFlagsProperty(bpy.types.PropertyGroup): + expandTab: bpy.props.BoolProperty(default=False, name="Damage Flags") + dekuNut: bpy.props.BoolProperty(default=True, name="Deku Nut") + dekuStick: bpy.props.BoolProperty(default=True, name="Deku Stick") + slingshot: bpy.props.BoolProperty(default=True, name="Slingshot") + explosive: bpy.props.BoolProperty(default=True, name="Bomb") + boomerang: bpy.props.BoolProperty(default=True, name="Boomerang") + arrowNormal: bpy.props.BoolProperty(default=True, name="Normal") + hammerSwing: bpy.props.BoolProperty(default=True, name="Hammer Swing") + hookshot: bpy.props.BoolProperty(default=True, name="Hookshot") + slashKokiriSword: bpy.props.BoolProperty(default=True, name="Kokiri") + slashMasterSword: bpy.props.BoolProperty(default=True, name="Master") + slashGiantSword: bpy.props.BoolProperty(default=True, name="Giant") + arrowFire: bpy.props.BoolProperty(default=True, name="Fire") + arrowIce: bpy.props.BoolProperty(default=True, name="Ice") + arrowLight: bpy.props.BoolProperty(default=True, name="Light") + arrowUnk1: bpy.props.BoolProperty(default=True, name="Unk1") + arrowUnk2: bpy.props.BoolProperty(default=True, name="Unk2") + arrowUnk3: bpy.props.BoolProperty(default=True, name="Unk3") + magicFire: bpy.props.BoolProperty(default=True, name="Fire") + magicIce: bpy.props.BoolProperty(default=True, name="Ice") + magicLight: bpy.props.BoolProperty(default=True, name="Light") + shield: bpy.props.BoolProperty(default=False, name="Shield") + mirrorRay: bpy.props.BoolProperty(default=False, name="Mirror Ray") + spinKokiriSword: bpy.props.BoolProperty(default=True, name="Kokiri") + spinGiantSword: bpy.props.BoolProperty(default=True, name="Giant") + spinMasterSword: bpy.props.BoolProperty(default=True, name="Master") + jumpKokiriSword: bpy.props.BoolProperty(default=True, name="Kokiri") + jumpGiantSword: bpy.props.BoolProperty(default=True, name="Giant") + jumpMasterSword: bpy.props.BoolProperty(default=True, name="Master") + unknown1: bpy.props.BoolProperty(default=True, name="Unknown 1") + unblockable: bpy.props.BoolProperty(default=True, name="Unblockable") + hammerJump: bpy.props.BoolProperty(default=True, name="Hammer Jump") + unknown2: bpy.props.BoolProperty(default=True, name="Unknown 2") + + def draw(self, layout: bpy.types.UILayout): + layout.prop(self, "expandTab", text="Damage Flags", icon="TRIA_DOWN" if self.expandTab else "TRIA_RIGHT") + + if self.expandTab: + row = layout.row(align=True) + row.prop(self, "dekuNut", toggle=1) + row.prop(self, "dekuStick", toggle=1) + row.prop(self, "slingshot", toggle=1) + + row = layout.row(align=True) + row.prop(self, "explosive", toggle=1) + row.prop(self, "boomerang", toggle=1) + row.prop(self, "hookshot", toggle=1) + + row = layout.row(align=True) + row.prop(self, "hammerSwing", toggle=1) + row.prop(self, "hammerJump", toggle=1) + + row = layout.row(align=True) + row.label(text="Slash") + row.prop(self, "slashKokiriSword", toggle=1) + row.prop(self, "slashMasterSword", toggle=1) + row.prop(self, "slashGiantSword", toggle=1) + + row = layout.row(align=True) + row.label(text="Spin") + row.prop(self, "spinKokiriSword", toggle=1) + row.prop(self, "spinMasterSword", toggle=1) + row.prop(self, "spinGiantSword", toggle=1) + + row = layout.row(align=True) + row.label(text="Jump") + row.prop(self, "jumpKokiriSword", toggle=1) + row.prop(self, "jumpMasterSword", toggle=1) + row.prop(self, "jumpGiantSword", toggle=1) + + row = layout.row(align=True) + row.label(text="Arrow") + row.prop(self, "arrowNormal", toggle=1) + row.prop(self, "arrowFire", toggle=1) + row.prop(self, "arrowIce", toggle=1) + row.prop(self, "arrowLight", toggle=1) + + row = layout.row(align=True) + row.label(text="Arrow Unknown") + row.prop(self, "arrowUnk1", toggle=1) + row.prop(self, "arrowUnk2", toggle=1) + row.prop(self, "arrowUnk3", toggle=1) + + row = layout.row(align=True) + row.label(text="Magic") + row.prop(self, "magicFire", toggle=1) + row.prop(self, "magicIce", toggle=1) + row.prop(self, "magicLight", toggle=1) + + row = layout.row(align=True) + row.prop(self, "shield", toggle=1) + row.prop(self, "mirrorRay", toggle=1) + + row = layout.row(align=True) + row.prop(self, "unblockable", toggle=1) + row.prop(self, "unknown1", toggle=1) + row.prop(self, "unknown2", toggle=1) + + def to_c(self): + flags = ( + ((1 if self.dekuNut else 0) << 0) + | ((1 if self.dekuStick else 0) << 1) + | ((1 if self.slingshot else 0) << 2) + | ((1 if self.explosive else 0) << 3) + | ((1 if self.boomerang else 0) << 4) + | ((1 if self.arrowNormal else 0) << 5) + | ((1 if self.hammerSwing else 0) << 6) + | ((1 if self.hookshot else 0) << 7) + | ((1 if self.slashKokiriSword else 0) << 8) + | ((1 if self.slashMasterSword else 0) << 9) + | ((1 if self.slashGiantSword else 0) << 10) + | ((1 if self.arrowFire else 0) << 11) + | ((1 if self.arrowIce else 0) << 12) + | ((1 if self.arrowLight else 0) << 13) + | ((1 if self.arrowUnk1 else 0) << 14) + | ((1 if self.arrowUnk2 else 0) << 15) + | ((1 if self.arrowUnk3 else 0) << 16) + | ((1 if self.magicFire else 0) << 17) + | ((1 if self.magicIce else 0) << 18) + | ((1 if self.magicLight else 0) << 19) + | ((1 if self.shield else 0) << 20) + | ((1 if self.mirrorRay else 0) << 21) + | ((1 if self.spinKokiriSword else 0) << 22) + | ((1 if self.spinGiantSword else 0) << 23) + | ((1 if self.spinMasterSword else 0) << 24) + | ((1 if self.jumpKokiriSword else 0) << 25) + | ((1 if self.jumpGiantSword else 0) << 26) + | ((1 if self.jumpMasterSword else 0) << 27) + | ((1 if self.unknown1 else 0) << 28) + | ((1 if self.unblockable else 0) << 29) + | ((1 if self.hammerJump else 0) << 30) + | ((1 if self.unknown2 else 0) << 31) + ) + return format(flags, "#010x") + + +# AT +class OOTColliderHitboxProperty(bpy.types.PropertyGroup): + enable: bpy.props.BoolProperty(name="Hitbox (AT)", update=updateCollider, default=False) + alignPlayer: bpy.props.BoolProperty(name="Player", default=False) + alignEnemy: bpy.props.BoolProperty(name="Enemy", default=True) + alignOther: bpy.props.BoolProperty(name="Other", default=False) + alignSelf: bpy.props.BoolProperty(name="Self", default=False) + + def draw(self, layout: bpy.types.UILayout): + layout = layout.box().column() + layout.prop(self, "enable") + if self.enable: + alignToggles = layout.row(align=True) + alignToggles.label(text="Aligned") + alignToggles.prop(self, "alignPlayer", toggle=1) + alignToggles.prop(self, "alignEnemy", toggle=1) + alignToggles.prop(self, "alignOther", toggle=1) + alignToggles.prop(self, "alignSelf", toggle=1) + + # Note that z_boss_sst_colchk has case where _ON is not set, but other flags are still set. + def to_c(self): + flagList = [] + flagList.append("AT_ON") if self.enable else None + if self.alignPlayer and self.alignEnemy and self.alignOther: + flagList.append("AT_TYPE_ALL") + else: + flagList.append("AT_TYPE_PLAYER") if self.alignPlayer else None + flagList.append("AT_TYPE_ENEMY") if self.alignEnemy else None + flagList.append("AT_TYPE_OTHER") if self.alignOther else None + flagList.append("AT_TYPE_SELF") if self.alignSelf else None + + flagList = ["AT_NONE"] if len(flagList) == 0 else flagList + + return " | ".join(flagList) + + +# AC +class OOTColliderHurtboxProperty(bpy.types.PropertyGroup): + enable: bpy.props.BoolProperty(name="Hurtbox (AC)", update=updateCollider, default=True) + attacksBounceOff: bpy.props.BoolProperty(name="Attacks Bounce Off") + hurtByPlayer: bpy.props.BoolProperty(name="Player", default=True) + hurtByEnemy: bpy.props.BoolProperty(name="Enemy", default=False) + hurtByOther: bpy.props.BoolProperty(name="Other", default=False) + noDamage: bpy.props.BoolProperty(name="Doesn't Take Damage", default=False) + + def draw(self, layout: bpy.types.UILayout): + layout = layout.box().column() + layout.prop(self, "enable") + if self.enable: + layout.prop(self, "attacksBounceOff") + layout.prop(self, "noDamage") + hurtToggles = layout.row(align=True) + hurtToggles.label(text="Hurt By") + hurtToggles.prop(self, "hurtByPlayer", toggle=1) + hurtToggles.prop(self, "hurtByEnemy", toggle=1) + hurtToggles.prop(self, "hurtByOther", toggle=1) + + # Note that z_boss_sst_colchk has case where _ON is not set, but other flags are still set. + def to_c(self): + flagList = [] + flagList.append("AC_ON") if self.enable else None + flagList.append("AC_HARD") if self.attacksBounceOff else None + if self.hurtByPlayer and self.hurtByEnemy and self.hurtByOther: + flagList.append("AC_TYPE_ALL") + else: + flagList.append("AC_TYPE_PLAYER") if self.hurtByPlayer else None + flagList.append("AC_TYPE_ENEMY") if self.hurtByEnemy else None + flagList.append("AC_TYPE_OTHER") if self.hurtByOther else None + flagList.append("AC_NO_DAMAGE") if self.noDamage else None + + flagList = ["AC_NONE"] if len(flagList) == 0 else flagList + + return " | ".join(flagList) + + +class OOTColliderLayers(bpy.types.PropertyGroup): + player: bpy.props.BoolProperty(name="Player", default=False) + type1: bpy.props.BoolProperty(name="Type 1", default=True) + type2: bpy.props.BoolProperty(name="Type 2", default=False) + + def draw(self, layout: bpy.types.UILayout, name: str): + collisionLayers = layout.row(align=True) + collisionLayers.label(text=name) + collisionLayers.prop(self, "player", toggle=1) + collisionLayers.prop(self, "type1", toggle=1) + collisionLayers.prop(self, "type2", toggle=1) + + +# OC +class OOTColliderPhysicsProperty(bpy.types.PropertyGroup): + enable: bpy.props.BoolProperty(name="Physics (OC)", update=updateCollider, default=True) + noPush: bpy.props.BoolProperty(name="Don't Push Others") + collidesWith: bpy.props.PointerProperty(type=OOTColliderLayers) + isCollider: bpy.props.PointerProperty(type=OOTColliderLayers) + skipHurtboxCheck: bpy.props.BoolProperty(name="Skip Hurtbox Check After First Collision") + isType1: bpy.props.BoolProperty(name="Is Type 1", default=False) + unk1: bpy.props.BoolProperty(name="Unknown 1", default=False) + unk2: bpy.props.BoolProperty(name="Unknown 2", default=False) + + def draw(self, layout: bpy.types.UILayout): + layout = layout.box().column() + layout.prop(self, "enable") + if self.enable: + layout.prop(self, "noPush") + layout.prop(self, "skipHurtboxCheck") + layout.prop(self, "isType1") + self.collidesWith.draw(layout, "Hits Type") + if not self.isType1: + self.isCollider.draw(layout, "Is Type") + row = layout.row(align=True) + row.prop(self, "unk1") + row.prop(self, "unk2") + + # Note that z_boss_sst_colchk has case where _ON is not set, but other flags are still set. + def to_c_1(self): + flagList = [] + flagList.append("OC1_ON") if self.enable else None + flagList.append("OC1_NO_PUSH") if self.noPush else None + if self.collidesWith.player and self.collidesWith.type1 and self.collidesWith.type2: + flagList.append("OC1_TYPE_ALL") + else: + flagList.append("OC1_TYPE_PLAYER") if self.collidesWith.player else None + flagList.append("OC1_TYPE_1") if self.collidesWith.type1 else None + flagList.append("OC1_TYPE_2") if self.collidesWith.type2 else None + + flagList = ["OC1_NONE"] if len(flagList) == 0 else flagList + + return " | ".join(flagList) + + # Note that z_boss_sst_colchk has case where _ON is not set, but other flags are still set. + def to_c_2(self): + flagList = [] + flagList.append("OC2_UNK1") if self.unk1 else None + flagList.append("OC2_UNK2") if self.unk2 else None + + flagList.append("OC2_TYPE_PLAYER") if self.isCollider.player else None + flagList.append("OC2_TYPE_1") if self.isCollider.type1 else None + flagList.append("OC2_TYPE_2") if self.isCollider.type2 else None + + flagList.append("OC2_FIRST_ONLY") if self.skipHurtboxCheck else None + + flagList = ["OC2_NONE"] if len(flagList) == 0 else flagList + + return " | ".join(flagList) + + +# Touch +class OOTColliderHitboxItemProperty(bpy.types.PropertyGroup): + # Flags + enable: bpy.props.BoolProperty(name="Touch") + soundEffect: bpy.props.EnumProperty(name="Sound Effect", items=ootEnumHitboxSound) + drawHitmarksForEveryCollision: bpy.props.BoolProperty(name="Draw Hitmarks For Every Collision") + closestBumper: bpy.props.BoolProperty(name="Only Collide With Closest Bumper (Quads)") + + # ColliderTouch + damageFlags: bpy.props.PointerProperty(type=OOTDamageFlagsProperty, name="Damage Flags") + effect: bpy.props.IntProperty(min=0, max=255, name="Effect") + damage: bpy.props.IntProperty(min=0, max=255, name="Damage") + unk7: bpy.props.BoolProperty(name="Unknown 7") + + def draw(self, layout: bpy.types.UILayout): + layout = layout.box().column() + layout.prop(self, "enable") + if self.enable: + prop_split(layout, self, "soundEffect", "Sound Effect") + layout.prop(self, "drawHitmarksForEveryCollision") + layout.prop(self, "closestBumper") + layout.prop(self, "unk7") + prop_split(layout, self, "effect", "Effect") + prop_split(layout, self, "damage", "Damage") + self.damageFlags.draw(layout) + + # Note that z_boss_sst_colchk has case where _ON is not set, but other flags are still set. + def to_c_flags(self): + flagList = [] + flagList.append("TOUCH_ON") if self.enable else None + flagList.append("TOUCH_NEAREST") if self.closestBumper else None + flagList.append(self.soundEffect) + + flagList.append("TOUCH_AT_HITMARK") if self.drawHitmarksForEveryCollision else None + flagList.append("TOUCH_UNK7") if self.unk7 else None + + # note that TOUCH_SFX_NORMAL is the same as 0, but since it is an enum it would usually be included anyway + flagList = ( + ["TOUCH_NONE"] + if len(flagList) == 0 or (len(flagList) == 1 and flagList[0] == "TOUCH_SFX_NORMAL") + else flagList + ) + + return " | ".join(flagList) + + def to_c_damage_flags(self): + flagList = [self.damageFlags.to_c()] + flagList.append(format(self.effect, "#04x")) + flagList.append(format(self.damage, "#04x")) + + return "{ " + ", ".join(flagList) + " }" + + +# Bump +class OOTColliderHurtboxItemProperty(bpy.types.PropertyGroup): + # Flags + enable: bpy.props.BoolProperty(name="Bump") + hookable: bpy.props.BoolProperty(name="Hookable") + giveInfoToHit: bpy.props.BoolProperty(name="Give Info To Hit") + takesDamage: bpy.props.BoolProperty(name="Damageable", default=True) + hasSound: bpy.props.BoolProperty(name="Has SFX", default=True) + hasHitmark: bpy.props.BoolProperty(name="Has Hitmark", default=True) + + # ColliderBumpInit + damageFlags: bpy.props.PointerProperty(type=OOTDamageFlagsProperty, name="Damage Flags") + effect: bpy.props.IntProperty(min=0, max=255, name="Effect") + defense: bpy.props.IntProperty(min=0, max=255, name="Damage") + + def draw(self, layout: bpy.types.UILayout): + layout = layout.box().column() + layout.prop(self, "enable") + if self.enable: + layout.prop(self, "hookable") + layout.prop(self, "giveInfoToHit") + row = layout.row(align=True) + row.prop(self, "takesDamage", toggle=1) + row.prop(self, "hasSound", toggle=1) + row.prop(self, "hasHitmark", toggle=1) + prop_split(layout, self, "effect", "Effect") + prop_split(layout, self, "defense", "Defense") + self.damageFlags.draw(layout) + + # Note that z_boss_sst_colchk has case where _ON is not set, but other flags are still set. + def to_c_flags(self): + flagList = [] + flagList.append("BUMP_ON") if self.enable else None + flagList.append("BUMP_HOOKABLE") if self.hookable else None + flagList.append("BUMP_NO_AT_INFO") if not self.giveInfoToHit else None + flagList.append("BUMP_NO_DAMAGE") if not self.takesDamage else None + flagList.append("BUMP_NO_SWORD_SFX") if not self.hasSound else None + flagList.append("BUMP_NO_HITMARK") if not self.hasHitmark else None + flagList.append("BUMP_DRAW_HITMARK") if self.hookable else None + + flagList = ["BUMP_NONE"] if len(flagList) == 0 else flagList + + return " | ".join(flagList) + + def to_c_damage_flags(self): + flagList = [self.damageFlags.to_c()] + flagList.append(format(self.effect, "#04x")) + flagList.append(format(self.defense, "#04x")) + + return "{ " + ", ".join(flagList) + " }" + + +# OCElem +class OOTColliderPhysicsItemProperty(bpy.types.PropertyGroup): + enable: bpy.props.BoolProperty(name="Object Element") + unk3: bpy.props.BoolProperty(name="Unknown 3", default=False) + + def draw(self, layout: bpy.types.UILayout): + layout = layout.box().column() + layout.prop(self, "enable") + if self.enable: + layout.prop(self, "unk3") + + def to_c_flags(self): + if not self.enable: + return "OCELEM_NONE" + + flagList = ["OCELEM_ON"] + flagList.append("OCELEM_UNK3") if self.unk3 else None + + return " | ".join(flagList) + + +# ColliderInit is for entire collection. +# ColliderInfoInit is for each item of a collection. + +# Triangle/Cylinder will use their own object for ColliderInit. +# Joint Sphere will use armature object for ColliderInit. + + +class OOTActorColliderProperty(bpy.types.PropertyGroup): + # ColliderInit + colliderShape: bpy.props.EnumProperty( + items=ootEnumColliderShape, name="Shape", default="COLSHAPE_CYLINDER", update=updateCollider + ) + colliderType: bpy.props.EnumProperty(items=ootEnumColliderType, name="Hit Reaction") + hitbox: bpy.props.PointerProperty(type=OOTColliderHitboxProperty, name="Hitbox (AT)") + hurtbox: bpy.props.PointerProperty(type=OOTColliderHurtboxProperty, name="Hurtbox (AC)") + physics: bpy.props.PointerProperty(type=OOTColliderPhysicsProperty, name="Physics (OC)") + name: bpy.props.StringProperty(name="Struct Name", default="sColliderInit") + + def draw(self, obj: bpy.types.Object, layout: bpy.types.UILayout): + if obj.ootActorCollider.colliderShape == "COLSHAPE_JNTSPH": + if obj.parent is not None: + collider = obj.parent.ootActorCollider + layout.label(text="Joint Shared", icon="INFO") + prop_split(layout, collider, "name", "Struct Name") + prop_split(layout, collider, "colliderType", "Collider Type") + collider.hitbox.draw(layout) + collider.hurtbox.draw(layout) + collider.physics.draw(layout) + else: + layout.label(text="Joint sphere colliders must be parented to a bone or object.", icon="ERROR") + + else: + prop_split(layout, self, "name", "Struct Name") + if obj.ootActorCollider.colliderShape == "COLSHAPE_QUAD": + layout.label(text="Geometry is ignored and zeroed.", icon="INFO") + layout.label(text="Only properties are exported.") + prop_split(layout, self, "colliderType", "Collider Type") + self.hitbox.draw(layout) + self.hurtbox.draw(layout) + self.physics.draw(layout) + + def to_c(self, tabDepth: int): + indent = "\t" * tabDepth + nextIndent = "\t" * (tabDepth + 1) + + physics2 = f"{nextIndent}{self.physics.to_c_2()},\n" if not self.physics.isType1 else "" + + data = ( + f"{indent}{{\n" + f"{nextIndent}{self.colliderType},\n" + f"{nextIndent}{self.hitbox.to_c()},\n" + f"{nextIndent}{self.hurtbox.to_c()},\n" + f"{nextIndent}{self.physics.to_c_1()},\n" + f"{physics2}" + f"{nextIndent}{self.colliderShape},\n" + f"{indent}}},\n" + ) + + return data + + +class OOTActorColliderItemProperty(bpy.types.PropertyGroup): + # ColliderInfoInit + element: bpy.props.EnumProperty(items=ootEnumColliderElement, name="Element Type") + limbOverride: bpy.props.IntProperty(min=0, max=256, name="Limb Index") + touch: bpy.props.PointerProperty(type=OOTColliderHitboxItemProperty, name="Touch") + bump: bpy.props.PointerProperty(type=OOTColliderHurtboxItemProperty, name="Bump") + objectElem: bpy.props.PointerProperty(type=OOTColliderPhysicsItemProperty, name="Object Element") + + # obj is None when using mesh collider, where property is on material + def draw(self, obj: bpy.types.Object | None, layout: bpy.types.UILayout): + if obj is not None and obj.ootActorCollider.colliderShape == "COLSHAPE_JNTSPH": + layout.label(text="Joint Specific", icon="INFO") + if not ( + obj.parent is not None and isinstance(obj.parent.data, bpy.types.Armature) and obj.parent_bone != "" + ): + prop_split(layout, self, "limbOverride", "Limb Index") + + if obj is not None and obj.ootActorCollider.colliderShape == "COLSHAPE_TRIS": + layout = layout.column() + layout.label(text="Touch/bump defined in materials.", icon="INFO") + layout.label(text="Materials will not be visualized.") + else: + layout = layout.column() + prop_split(layout, self, "element", "Element Type") + self.touch.draw(layout) + self.bump.draw(layout) + self.objectElem.draw(layout) + + def to_c(self, tabDepth: int): + indent = "\t" * tabDepth + nextIndent = "\t" * (tabDepth + 1) + + data = ( + f"{indent}{{\n" + f"{nextIndent}{self.element},\n" + f"{nextIndent}{self.touch.to_c_damage_flags()},\n" + f"{nextIndent}{self.bump.to_c_damage_flags()},\n" + f"{nextIndent}{self.touch.to_c_flags()},\n" + f"{nextIndent}{self.bump.to_c_flags()},\n" + f"{nextIndent}{self.objectElem.to_c_flags()},\n" + f"{indent}}},\n" + ) + + return data + + +def drawColliderVisibilityOperators(layout: bpy.types.UILayout): + col = layout.column() + col.label(text="Toggle Visibility (Excluding Selected)") + row = col.row(align=True) + visibilitySettings = bpy.context.scene.ootColliderVisibility + row.prop(visibilitySettings, "jointSphere", text="Joint Sphere", toggle=1) + row.prop(visibilitySettings, "cylinder", text="Cylinder", toggle=1) + row.prop(visibilitySettings, "mesh", text="Mesh", toggle=1) + row.prop(visibilitySettings, "quad", text="Quad", toggle=1) + + +def updateVisibilityJointSphere(self, context): + updateVisibilityCollider("COLSHAPE_JNTSPH", self.jointSphere) + + +def updateVisibilityCylinder(self, context): + updateVisibilityCollider("COLSHAPE_CYLINDER", self.cylinder) + + +def updateVisibilityMesh(self, context): + updateVisibilityCollider("COLSHAPE_TRIS", self.mesh) + + +def updateVisibilityQuad(self, context): + updateVisibilityCollider("COLSHAPE_QUAD", self.quad) + + +def updateVisibilityCollider(shapeName: str, visibility: bool) -> None: + selectedObjs = bpy.context.selected_objects + for obj in bpy.data.objects: + if ( + isinstance(obj.data, bpy.types.Mesh) + and obj.ootGeometryType == "Actor Collider" + and obj.ootActorCollider.colliderShape == shapeName + and obj not in selectedObjs + ): + obj.hide_set(not visibility) + + +class OOTColliderVisibilitySettings(bpy.types.PropertyGroup): + jointSphere: bpy.props.BoolProperty(name="Joint Sphere", default=True, update=updateVisibilityJointSphere) + cylinder: bpy.props.BoolProperty(name="Cylinder", default=True, update=updateVisibilityCylinder) + mesh: bpy.props.BoolProperty(name="Mesh", default=True, update=updateVisibilityMesh) + quad: bpy.props.BoolProperty(name="Quad", default=True, update=updateVisibilityQuad) + + +actor_collider_props_classes = ( + OOTColliderLayers, + OOTDamageFlagsProperty, + OOTColliderHitboxItemProperty, + OOTColliderHurtboxItemProperty, + OOTColliderPhysicsItemProperty, + OOTColliderHitboxProperty, + OOTColliderHurtboxProperty, + OOTColliderPhysicsProperty, + OOTActorColliderProperty, + OOTActorColliderItemProperty, + OOTColliderVisibilitySettings, +) + + +def actor_collider_props_register(): + for cls in actor_collider_props_classes: + register_class(cls) + + bpy.types.Object.ootActorCollider = bpy.props.PointerProperty(type=OOTActorColliderProperty) + bpy.types.Object.ootActorColliderItem = bpy.props.PointerProperty(type=OOTActorColliderItemProperty) + bpy.types.Material.ootActorColliderItem = bpy.props.PointerProperty(type=OOTActorColliderItemProperty) + bpy.types.Scene.ootColliderLibVer = bpy.props.IntProperty(default=1) + bpy.types.Scene.ootColliderVisibility = bpy.props.PointerProperty(type=OOTColliderVisibilitySettings) + + +def actor_collider_props_unregister(): + for cls in reversed(actor_collider_props_classes): + unregister_class(cls) + + del bpy.types.Object.ootActorCollider + del bpy.types.Object.ootActorColliderItem + del bpy.types.Scene.ootColliderLibVer + del bpy.types.Scene.ootColliderVisibility diff --git a/fast64_internal/oot/actor_collider/utility.py b/fast64_internal/oot/actor_collider/utility.py new file mode 100644 index 000000000..6fc8b6396 --- /dev/null +++ b/fast64_internal/oot/actor_collider/utility.py @@ -0,0 +1,259 @@ +from ...utility import PluginError, parentObject +from ...f3d.f3d_material import createF3DMat, update_preset_manual +import bpy, math, mathutils + +ootEnumColliderShape = [ + ("COLSHAPE_JNTSPH", "Joint Sphere", "Joint Sphere"), + ("COLSHAPE_CYLINDER", "Cylinder", "Cylinder"), + ("COLSHAPE_TRIS", "Triangles", "Triangles"), + ("COLSHAPE_QUAD", "Quad (Properties Only)", "Quad"), +] + +ootEnumColliderType = [ + ("COLTYPE_HIT0", "Blue Blood, White Hitmark", "Blue Blood, White Hitmark"), + ("COLTYPE_HIT1", "No Blood, Dust Hitmark", "No Blood, Dust Hitmark"), + ("COLTYPE_HIT2", "Green Blood, Dust Hitmark", "Green Blood, Dust Hitmark"), + ("COLTYPE_HIT3", "No Blood, White Hitmark", "No Blood, White Hitmark"), + ("COLTYPE_HIT4", "Water Burst, No hitmark", "Water Burst, No hitmark"), + ("COLTYPE_HIT5", "No blood, Red Hitmark", "No blood, Red Hitmark"), + ("COLTYPE_HIT6", "Green Blood, White Hitmark", "Green Blood, White Hitmark"), + ("COLTYPE_HIT7", "Red Blood, White Hitmark", "Red Blood, White Hitmark"), + ("COLTYPE_HIT8", "Blue Blood, Red Hitmark", "Blue Blood, Red Hitmark"), + ("COLTYPE_METAL", "Metal", "Metal"), + ("COLTYPE_NONE", "None", "None"), + ("COLTYPE_WOOD", "Wood", "Wood"), + ("COLTYPE_HARD", "Hard", "Hard"), + ("COLTYPE_TREE", "Tree", "Tree"), +] + +ootEnumColliderElement = [ + ("ELEMTYPE_UNK0", "Element 0", "Element 0"), + ("ELEMTYPE_UNK1", "Element 1", "Element 1"), + ("ELEMTYPE_UNK2", "Element 2", "Element 2"), + ("ELEMTYPE_UNK3", "Element 3", "Element 3"), + ("ELEMTYPE_UNK4", "Element 4", "Element 4"), + ("ELEMTYPE_UNK5", "Element 5", "Element 5"), + ("ELEMTYPE_UNK6", "Element 6", "Element 6"), + ("ELEMTYPE_UNK7", "Element 7", "Element 7"), +] + +ootEnumHitboxSound = [ + ("TOUCH_SFX_NORMAL", "Hurtbox", "Hurtbox"), + ("TOUCH_SFX_HARD", "Hard", "Hard"), + ("TOUCH_SFX_WOOD", "Wood", "Wood"), + ("TOUCH_SFX_NONE", "None", "None"), +] + + +def getGeometryNodes(shapeName: str): + nodesName = shapeNameToBlenderName(shapeName) + if nodesName in bpy.data.node_groups: + return bpy.data.node_groups[nodesName] + else: + node_group = bpy.data.node_groups.new(nodesName, "GeometryNodeTree") + node_group.use_fake_user = True + inNode = node_group.nodes.new("NodeGroupInput") + node_group.inputs.new("NodeSocketGeometry", "Geometry") + node_group.inputs.new("NodeSocketMaterial", "Material") + outNode = node_group.nodes.new("NodeGroupOutput") + node_group.outputs.new("NodeSocketGeometry", "Geometry") + + if nodesName == "oot_collider_sphere": + # Sphere + shape = node_group.nodes.new("GeometryNodeMeshUVSphere") + shape.inputs["Segments"].default_value = 16 + shape.inputs["Rings"].default_value = 8 + shape.inputs["Radius"].default_value = 1 + + # Shade Smooth + smooth = node_group.nodes.new("GeometryNodeSetShadeSmooth") + node_group.links.new(shape.outputs["Mesh"], smooth.inputs["Geometry"]) + lastNode = smooth + + elif nodesName == "oot_collider_cylinder": + + # Cylinder + shape = node_group.nodes.new("GeometryNodeMeshCylinder") + shape.inputs["Vertices"].default_value = 16 + shape.inputs["Radius"].default_value = 1 + shape.inputs["Depth"].default_value = 2 + + # Shade Smooth + smooth = node_group.nodes.new("GeometryNodeSetShadeSmooth") + node_group.links.new(shape.outputs["Mesh"], smooth.inputs["Geometry"]) + node_group.links.new(shape.outputs["Side"], smooth.inputs["Selection"]) + + # Transform + transform = node_group.nodes.new("GeometryNodeTransform") + node_group.links.new(smooth.outputs["Geometry"], transform.inputs["Geometry"]) + transform.inputs["Translation"].default_value[2] = 1 + lastNode = transform + + elif nodesName == "oot_collider_triangles": + lastNode = inNode + + elif nodesName == "oot_collider_quad": + # Grid + shape = node_group.nodes.new("GeometryNodeMeshGrid") + shape.inputs["Size X"].default_value = 2 + shape.inputs["Size Y"].default_value = 2 + shape.inputs["Vertices X"].default_value = 2 + shape.inputs["Vertices Y"].default_value = 2 + + # Transform + transform = node_group.nodes.new("GeometryNodeTransform") + node_group.links.new(shape.outputs["Mesh"], transform.inputs["Geometry"]) + transform.inputs["Rotation"].default_value[0] = math.radians(90) + lastNode = transform + + else: + raise PluginError(f"Could not find node group name: {nodesName}") + + # Set Material + setMat = node_group.nodes.new("GeometryNodeSetMaterial") + node_group.links.new(lastNode.outputs["Geometry"], setMat.inputs["Geometry"]) + node_group.links.new(inNode.outputs["Material"], setMat.inputs["Material"]) + node_group.links.new(setMat.outputs["Geometry"], outNode.inputs["Geometry"]) + + return node_group + + +# Apply geometry nodes for the correct collider shape +def applyColliderGeoNodes(obj: bpy.types.Object, material: bpy.types.Material, shapeName: str) -> None: + if "Collider Shape" not in obj.modifiers: + modifier = obj.modifiers.new("Collider Shape", "NODES") + else: + modifier = obj.modifiers["Collider Shape"] + modifier.node_group = getGeometryNodes(shapeName) + modifier["Input_1"] = material + + +# Creates a semi-transparent solid color material (cached) +def getColliderMat(name: str, color: tuple[float, float, float, float]) -> bpy.types.Material: + if name not in bpy.data.materials: + newMat = createF3DMat(None, preset="oot_shaded_texture_transparent", index=0) + newMat.name = name + newMat.f3d_mat.combiner1.A = "0" + newMat.f3d_mat.combiner1.C = "0" + newMat.f3d_mat.combiner1.D = "SHADE" + newMat.f3d_mat.combiner1.D_alpha = "1" + newMat.f3d_mat.prim_color = color + update_preset_manual(newMat, bpy.context) + return newMat + else: + return bpy.data.materials[name] + + +# Update collider callback for collider type property +def updateCollider(self, context: bpy.types.Context) -> None: + updateColliderOnObj(context.object) + + +def updateColliderOnObj(obj: bpy.types.Object, updateJointSiblings: bool = True) -> None: + if obj.ootGeometryType == "Actor Collider": + colliderProp = obj.ootActorCollider + if colliderProp.colliderShape == "COLSHAPE_JNTSPH": + if obj.parent == None: + return + queryProp = obj.parent.ootActorCollider + else: + queryProp = colliderProp + + alpha = 0.7 + # if colliderProp.colliderShape == "COLSHAPE_TRIS": + # material = getColliderMat("oot_collider_cyan", (0, 0.5, 1, alpha)) + if colliderProp.colliderShape == "COLSHAPE_QUAD": + material = getColliderMat("oot_collider_orange", (0.2, 0.05, 0, alpha)) + elif queryProp.hitbox.enable and queryProp.hurtbox.enable: + material = getColliderMat("oot_collider_purple", (0.15, 0, 0.05, alpha)) + elif queryProp.hitbox.enable: + material = getColliderMat("oot_collider_red", (0.2, 0, 0, alpha)) + elif queryProp.hurtbox.enable: + material = getColliderMat("oot_collider_blue", (0, 0, 0.2, alpha)) + else: + material = getColliderMat("oot_collider_white", (0.2, 0.2, 0.2, alpha)) + applyColliderGeoNodes(obj, material, colliderProp.colliderShape) + + if updateJointSiblings and colliderProp.colliderShape == "COLSHAPE_JNTSPH" and obj.parent is not None: + for child in obj.parent.children: + updateColliderOnObj(child, False) + + +def addColliderThenParent( + shapeName: str, obj: bpy.types.Object, bone: bpy.types.Bone | None, notMeshCollider: bool = True +) -> bpy.types.Object: + colliderObj = addCollider(shapeName, notMeshCollider) + if bone is not None: + + # If no active bone is set, then parenting operator fails. + obj.data.bones.active = obj.data.bones[0] + obj.data.bones[0].select = True + + parentObject(obj, colliderObj, "BONE") + colliderObj.parent_bone = bone.name + colliderObj.matrix_world = obj.matrix_world @ obj.pose.bones[bone.name].matrix + else: + parentObject(obj, colliderObj) + # 10 = default value for ootBlenderScale + colliderObj.matrix_local = mathutils.Matrix.Diagonal(colliderObj.matrix_local.decompose()[2].to_4d()) + updateColliderOnObj(colliderObj) + return colliderObj + + +def addCollider(shapeName: str, notMeshCollider: bool) -> bpy.types.Object: + if bpy.context.mode != "OBJECT": + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action="DESELECT") + + # Mesh shape only matters for Triangle shape, otherwise will be controlled by geometry nodes. + location = mathutils.Vector(bpy.context.scene.cursor.location) + bpy.ops.mesh.primitive_plane_add(size=2, enter_editmode=False, align="WORLD", location=location[:]) + planeObj = bpy.context.view_layer.objects.active + if notMeshCollider: + planeObj.data.clear_geometry() + else: + material = bpy.data.materials.new(f"Mesh Collider Material") + planeObj.data.materials.append(material) + planeObj.name = "Collider" + planeObj.ootGeometryType = "Actor Collider" + + if shapeName == "COLSHAPE_CYLINDER": + planeObj.lock_location = (True, True, False) + planeObj.lock_rotation = (True, True, True) + + actorCollider = planeObj.ootActorCollider + actorCollider.colliderShape = shapeName + actorCollider.physics.enable = True + return planeObj + + +def shapeNameToBlenderName(shapeName: str) -> str: + return shapeNameLookup( + shapeName, + { + "COLSHAPE_JNTSPH": "oot_collider_sphere", + "COLSHAPE_CYLINDER": "oot_collider_cylinder", + "COLSHAPE_TRIS": "oot_collider_triangles", + "COLSHAPE_QUAD": "oot_collider_quad", + }, + ) + + +def shapeNameToSimpleName(shapeName: str) -> str: + return shapeNameLookup( + shapeName, + { + "COLSHAPE_JNTSPH": "Sphere", + "COLSHAPE_CYLINDER": "Cylinder", + "COLSHAPE_TRIS": "Mesh", + "COLSHAPE_QUAD": "Quad", + }, + ) + + +def shapeNameLookup(shapeName: str, nameDict: dict[str, str]) -> str: + if shapeName in nameDict: + name = nameDict[shapeName] + return name + else: + raise PluginError(f"Could not find shape name {shapeName} in name dictionary.") diff --git a/fast64_internal/oot/f3d/panels.py b/fast64_internal/oot/f3d/panels.py index 70eff9046..18a6ef691 100644 --- a/fast64_internal/oot/f3d/panels.py +++ b/fast64_internal/oot/f3d/panels.py @@ -22,7 +22,9 @@ class OOT_DisplayListPanel(Panel): @classmethod def poll(cls, context): return context.scene.gameEditorMode == "OOT" and ( - context.object is not None and isinstance(context.object.data, Mesh) + context.object is not None + and isinstance(context.object.data, Mesh) + and not context.object.ootGeometryType == "Actor Collider" ) def draw(self, context): diff --git a/fast64_internal/oot/f3d/properties.py b/fast64_internal/oot/f3d/properties.py index 747674608..b98799f7b 100644 --- a/fast64_internal/oot/f3d/properties.py +++ b/fast64_internal/oot/f3d/properties.py @@ -4,6 +4,11 @@ from ...f3d.f3d_parser import ootEnumDrawLayers from ...utility import prop_split +ootEnumGeometryType = [ + ("Regular", "Regular", "Regular"), + ("Actor Collider", "Actor Collider", "Actor Collider"), +] + class OOTDLExportSettings(PropertyGroup): isCustomFilename: BoolProperty( @@ -198,6 +203,7 @@ def f3d_props_register(): World.ootDefaultRenderModes = PointerProperty(type=OOTDefaultRenderModesProperty) Material.ootMaterial = PointerProperty(type=OOTDynamicMaterialProperty) Object.ootObjectMenu = EnumProperty(items=ootEnumObjectMenu) + Object.ootGeometryType = EnumProperty(items=ootEnumGeometryType, name="Geometry Type") def f3d_props_unregister(): @@ -206,3 +212,4 @@ def f3d_props_unregister(): del Material.ootMaterial del Object.ootObjectMenu + del Object.ootGeometryType diff --git a/fast64_internal/oot/oot_utility.py b/fast64_internal/oot/oot_utility.py index 1d7ca39d4..f4f8658c0 100644 --- a/fast64_internal/oot/oot_utility.py +++ b/fast64_internal/oot/oot_utility.py @@ -420,6 +420,18 @@ def getNextBone(boneStack: list[str], armatureObj: bpy.types.Object): return bone, boneStack +def getOrderedBoneList(armatureObj: bpy.types.Object): + startBoneName = getStartBone(armatureObj) + boneList = [] + boneStack = [startBoneName] + + while len(boneStack) > 0: + bone, boneStack = getNextBone(boneStack, armatureObj) + boneList.append(bone) + + return boneList + + def checkForStartBone(armatureObj): pass # if "root" not in armatureObj.data.bones: diff --git a/fast64_internal/oot/tools/panel.py b/fast64_internal/oot/tools/panel.py index 1e5151701..f693b65ec 100644 --- a/fast64_internal/oot/tools/panel.py +++ b/fast64_internal/oot/tools/panel.py @@ -9,6 +9,9 @@ OOT_AddPath, ) +from ..actor_collider import OOT_AddActorCollider, OOT_CopyColliderProperties, drawColliderVisibilityOperators +from ...utility_anim import ArmatureApplyWithMeshOperator + class OoT_ToolsPanel(OOT_Panel): bl_idname = "OOT_PT_tools" @@ -23,6 +26,29 @@ def draw(self, context): col.operator(OOT_AddCutscene.bl_idname) col.operator(OOT_AddPath.bl_idname) + col.label(text="") + col.label(text="Armatures") + col.operator(ArmatureApplyWithMeshOperator.bl_idname) + + col.label(text="") + col.label(text="Actor Colliders") + col.label(text="Do not scale armatures with joint sphere colliders.", icon="ERROR") + col.label(text="Applying scale will mess up joint sphere translations.") + addOp = col.operator(OOT_AddActorCollider.bl_idname, text="Add Joint Sphere Collider (Bones)") + addOp.shape = "COLSHAPE_JNTSPH" + addOp.parentToBone = True + + col.operator( + OOT_AddActorCollider.bl_idname, text="Add Joint Sphere Collider (Object)" + ).shape = "COLSHAPE_JNTSPH" + col.operator(OOT_AddActorCollider.bl_idname, text="Add Cylinder Collider").shape = "COLSHAPE_CYLINDER" + col.operator(OOT_AddActorCollider.bl_idname, text="Add Mesh Collider").shape = "COLSHAPE_TRIS" + col.operator(OOT_AddActorCollider.bl_idname, text="Add Quad Collider (Properties Only)").shape = "COLSHAPE_QUAD" + + drawColliderVisibilityOperators(col) + + col.operator(OOT_CopyColliderProperties.bl_idname, text="Copy Collider Properties (From Active To Selected)") + oot_operator_panel_classes = [ OoT_ToolsPanel, diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index 84f2abecc..20723e5c8 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -124,13 +124,13 @@ def selectSingleObject(obj: bpy.types.Object): bpy.context.view_layer.objects.active = obj -def parentObject(parent, child): +def parentObject(parent, child, type="OBJECT"): bpy.ops.object.select_all(action="DESELECT") child.select_set(True) parent.select_set(True) bpy.context.view_layer.objects.active = parent - bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) + bpy.ops.object.parent_set(type=type, keep_transform=True) def getFMeshName(vertexGroup, namePrefix, drawLayer, isSkinned): @@ -816,6 +816,7 @@ def get_obj_temp_mesh(obj): if o.get("temp_export") and o.get("instanced_mesh_name") == obj.get("instanced_mesh_name"): return o + def apply_objects_modifiers_and_transformations(allObjs: Iterable[bpy.types.Object]): # first apply modifiers so that any objects that affect each other are taken into consideration for selectedObj in allObjs: @@ -834,6 +835,7 @@ def apply_objects_modifiers_and_transformations(allObjs: Iterable[bpy.types.Obje bpy.ops.object.transform_apply(location=False, rotation=True, scale=True, properties=False) + def duplicateHierarchy(obj, ignoreAttr, includeEmpties, areaIndex): # Duplicate objects to apply scale / modifiers / linked data bpy.ops.object.select_all(action="DESELECT") From f777539efb927011b74d3884f8d7272a58d67ca3 Mon Sep 17 00:00:00 2001 From: kurethedead Date: Tue, 10 Jan 2023 00:51:31 -0800 Subject: [PATCH 2/6] Call import/export code --- fast64_internal/oot/__init__.py | 8 +++---- .../oot/actor_collider/__init__.py | 9 +++----- .../oot/actor_collider/properties.py | 1 + fast64_internal/oot/actor_collider/utility.py | 18 +-------------- fast64_internal/oot/f3d/operators.py | 22 +++++++++++++++++++ fast64_internal/oot/f3d/properties.py | 5 +++++ .../oot/skeleton/exporter/functions.py | 18 +++++++++++++++ .../oot/skeleton/importer/functions.py | 11 ++++++++++ fast64_internal/oot/skeleton/properties.py | 5 +++++ 9 files changed, 70 insertions(+), 27 deletions(-) diff --git a/fast64_internal/oot/__init__.py b/fast64_internal/oot/__init__.py index 6d8db46a2..5e9b5c003 100644 --- a/fast64_internal/oot/__init__.py +++ b/fast64_internal/oot/__init__.py @@ -123,6 +123,8 @@ def oot_register(registerPanels): actor_props_register() oot_obj_register() spline_props_register() + actor_collider_props_register(), # register before f3d + actor_collider_ops_register(), f3d_props_register() anim_ops_register() skeleton_ops_register() @@ -131,8 +133,6 @@ def oot_register(registerPanels): f3d_ops_register() file_register() anim_props_register() - actor_collider_props_register(), - actor_collider_ops_register(), for cls in oot_classes: register_class(cls) @@ -159,6 +159,8 @@ def oot_unregister(unregisterPanels): actor_props_unregister() spline_props_unregister() f3d_props_unregister() + actor_collider_props_unregister(), + actor_collider_ops_unregister(), anim_ops_unregister() skeleton_ops_unregister() skeleton_props_unregister() @@ -166,8 +168,6 @@ def oot_unregister(unregisterPanels): f3d_ops_unregister() file_unregister() anim_props_unregister() - actor_collider_props_unregister(), - actor_collider_ops_unregister(), if unregisterPanels: oot_panel_unregister() diff --git a/fast64_internal/oot/actor_collider/__init__.py b/fast64_internal/oot/actor_collider/__init__.py index 62d4f4bc0..dd0a89125 100644 --- a/fast64_internal/oot/actor_collider/__init__.py +++ b/fast64_internal/oot/actor_collider/__init__.py @@ -18,9 +18,6 @@ from .importer import parseColliderData from .exporter import getColliderData, removeExistingColliderData, writeColliderData -# TODO: Code in other files -# oot_operators (operators) -# oot_f3d_writer, oot_skeleton (properties, functions) - -# getActorFilepath in exporter? -# movement of some functions? +# TODO: +# test getActorFilepath changes on export +# fix Link collider import diff --git a/fast64_internal/oot/actor_collider/properties.py b/fast64_internal/oot/actor_collider/properties.py index 34931fb07..1b1340ad7 100644 --- a/fast64_internal/oot/actor_collider/properties.py +++ b/fast64_internal/oot/actor_collider/properties.py @@ -614,6 +614,7 @@ class OOTColliderVisibilitySettings(bpy.types.PropertyGroup): OOTActorColliderProperty, OOTActorColliderItemProperty, OOTColliderVisibilitySettings, + OOTActorColliderImportExportSettings, ) diff --git a/fast64_internal/oot/actor_collider/utility.py b/fast64_internal/oot/actor_collider/utility.py index 6fc8b6396..5cc124a49 100644 --- a/fast64_internal/oot/actor_collider/utility.py +++ b/fast64_internal/oot/actor_collider/utility.py @@ -1,5 +1,5 @@ from ...utility import PluginError, parentObject -from ...f3d.f3d_material import createF3DMat, update_preset_manual +from ..oot_f3d_writer import getColliderMat import bpy, math, mathutils ootEnumColliderShape = [ @@ -128,22 +128,6 @@ def applyColliderGeoNodes(obj: bpy.types.Object, material: bpy.types.Material, s modifier["Input_1"] = material -# Creates a semi-transparent solid color material (cached) -def getColliderMat(name: str, color: tuple[float, float, float, float]) -> bpy.types.Material: - if name not in bpy.data.materials: - newMat = createF3DMat(None, preset="oot_shaded_texture_transparent", index=0) - newMat.name = name - newMat.f3d_mat.combiner1.A = "0" - newMat.f3d_mat.combiner1.C = "0" - newMat.f3d_mat.combiner1.D = "SHADE" - newMat.f3d_mat.combiner1.D_alpha = "1" - newMat.f3d_mat.prim_color = color - update_preset_manual(newMat, bpy.context) - return newMat - else: - return bpy.data.materials[name] - - # Update collider callback for collider type property def updateCollider(self, context: bpy.types.Context) -> None: updateColliderOnObj(context.object) diff --git a/fast64_internal/oot/f3d/operators.py b/fast64_internal/oot/f3d/operators.py index b153c52fe..da856443f 100644 --- a/fast64_internal/oot/f3d/operators.py +++ b/fast64_internal/oot/f3d/operators.py @@ -15,6 +15,13 @@ from ..oot_f3d_writer import ootReadActorScale, writeTextureArraysNew, writeTextureArraysExisting from .properties import OOTDLImportSettings, OOTDLExportSettings +from ..actor_collider import ( + parseColliderData, + getColliderData, + removeExistingColliderData, + writeColliderData, +) + from ..oot_utility import ( OOTObjectCategorizer, ootDuplicateHierarchy, @@ -82,6 +89,10 @@ def ootConvertMeshToC( textureArrayData = writeTextureArraysNew(fModel, flipbookArrayIndex2D) data.append(textureArrayData) + if settings.handleColliders.enable: + colliderData = getColliderData(originalObj) + data.append(colliderData) + filename = settings.filename if settings.isCustomFilename else name writeCData(data, os.path.join(path, filename + ".h"), os.path.join(path, filename + ".c")) @@ -93,6 +104,12 @@ def ootConvertMeshToC( sourcePath = os.path.join(path, folderName + ".c") removeDL(sourcePath, headerPath, name) + if settings.handleColliders.enable: + removeExistingColliderData( + bpy.context.scene.ootDecompPath, settings.actorOverlayName, False, colliderData.source + ) + writeColliderData(originalObj, bpy.context.scene.ootDecompPath, settings.actorOverlayName, False) + class OOT_ImportDL(Operator): # set bl_ properties @@ -145,6 +162,11 @@ def execute(self, context): ) obj.ootActorScale = scale / context.scene.ootBlenderScale + if settings.handleColliders.enable: + parseColliderData( + settings.name, basePath, settings.actorOverlayName, False, obj, settings.handleColliders + ) + self.report({"INFO"}, "Success!") return {"FINISHED"} diff --git a/fast64_internal/oot/f3d/properties.py b/fast64_internal/oot/f3d/properties.py index b98799f7b..f094c6492 100644 --- a/fast64_internal/oot/f3d/properties.py +++ b/fast64_internal/oot/f3d/properties.py @@ -3,6 +3,7 @@ from bpy.utils import register_class, unregister_class from ...f3d.f3d_parser import ootEnumDrawLayers from ...utility import prop_split +from ..actor_collider import OOTActorColliderImportExportSettings ootEnumGeometryType = [ ("Regular", "Regular", "Regular"), @@ -25,6 +26,7 @@ class OOTDLExportSettings(PropertyGroup): actorOverlayName: StringProperty(name="Overlay", default="") flipbookUses2DArray: BoolProperty(name="Has 2D Flipbook Array", default=False) flipbookArrayIndex2D: IntProperty(name="Index if 2D Array", default=0, min=0) + handleColliders: PointerProperty(type=OOTActorColliderImportExportSettings) customAssetIncludeDir: StringProperty( name="Asset Include Directory", default="assets/objects/gameplay_keep", @@ -47,6 +49,7 @@ def draw_props(self, layout: UILayout): box = layout.box().column() prop_split(box, self, "flipbookArrayIndex2D", "Flipbook Index") + self.handleColliders.draw(layout, "Export Actor Colliders", False) prop_split(layout, self, "drawLayer", "Export Draw Layer") layout.prop(self, "isCustom") layout.prop(self, "removeVanillaData") @@ -63,6 +66,7 @@ class OOTDLImportSettings(PropertyGroup): actorOverlayName: StringProperty(name="Overlay", default="") flipbookUses2DArray: BoolProperty(name="Has 2D Flipbook Array", default=False) flipbookArrayIndex2D: IntProperty(name="Index if 2D Array", default=0, min=0) + handleColliders: PointerProperty(type=OOTActorColliderImportExportSettings) autoDetectActorScale: BoolProperty(name="Auto Detect Actor Scale", default=True) actorScale: FloatProperty(name="Actor Scale", min=0, default=100) @@ -80,6 +84,7 @@ def draw_props(self, layout: UILayout): if self.flipbookUses2DArray: box = layout.box().column() prop_split(box, self, "flipbookArrayIndex2D", "Flipbook Index") + self.handleColliders.draw(layout, "Import Actor Colliders", True) prop_split(layout, self, "drawLayer", "Import Draw Layer") layout.prop(self, "isCustom") diff --git a/fast64_internal/oot/skeleton/exporter/functions.py b/fast64_internal/oot/skeleton/exporter/functions.py index 17b026849..b34f533cb 100644 --- a/fast64_internal/oot/skeleton/exporter/functions.py +++ b/fast64_internal/oot/skeleton/exporter/functions.py @@ -7,6 +7,11 @@ from ..properties import OOTSkeletonExportSettings from ..utility import ootDuplicateArmatureAndRemoveRotations, getGroupIndices, ootRemoveSkeleton from .classes import OOTLimb, OOTSkeleton +from ...actor_collider import ( + getColliderData, + removeExistingColliderData, + writeColliderData, +) from ....utility import ( PluginError, @@ -296,6 +301,10 @@ def ootConvertArmatureToC( textureArrayData = writeTextureArraysNew(fModel, flipbookArrayIndex2D) data.append(textureArrayData) + if settings.handleColliders.enable: + colliderData = getColliderData(originalArmatureObj) + data.append(colliderData) + writeCData(data, os.path.join(path, filename + ".h"), os.path.join(path, filename + ".c")) if not isCustomExport: @@ -303,3 +312,12 @@ def ootConvertArmatureToC( addIncludeFiles(folderName, path, filename) if removeVanillaData: ootRemoveSkeleton(path, folderName, skeletonName) + + if settings.handleColliders.enable: + colliderData = getColliderData(originalArmatureObj) + removeExistingColliderData( + bpy.context.scene.ootDecompPath, settings.actorOverlayName, isLink, colliderData.source + ) + writeColliderData( + originalArmatureObj, bpy.context.scene.ootDecompPath, settings.actorOverlayName, isLink + ) diff --git a/fast64_internal/oot/skeleton/importer/functions.py b/fast64_internal/oot/skeleton/importer/functions.py index bdf548442..a0e582c79 100644 --- a/fast64_internal/oot/skeleton/importer/functions.py +++ b/fast64_internal/oot/skeleton/importer/functions.py @@ -9,6 +9,7 @@ from ..constants import ootSkeletonImportDict from ..properties import OOTSkeletonImportSettings from ..utility import ootGetLimb, ootGetLimbs, ootGetSkeleton, applySkeletonRestPose +from ...actor_collider import parseColliderData class OOTDLEntry: @@ -276,6 +277,16 @@ def ootImportSkeletonC(basePath: str, importSettings: OOTSkeletonImportSettings) f3dContext.deleteMaterialContext() + if importSettings.handleColliders.enable: + parseColliderData( + importSettings.name, + basePath, + overlayName, + isLink, + armatureObj, + importSettings.handleColliders, + ) + if importSettings.applyRestPose and restPoseData is not None: applySkeletonRestPose(restPoseData, armatureObj) if isLOD: diff --git a/fast64_internal/oot/skeleton/properties.py b/fast64_internal/oot/skeleton/properties.py index f0d81ac6e..5ea118cc4 100644 --- a/fast64_internal/oot/skeleton/properties.py +++ b/fast64_internal/oot/skeleton/properties.py @@ -4,6 +4,7 @@ from ...f3d.f3d_material import ootEnumDrawLayers from ...utility import prop_split from .constants import ootEnumSkeletonImportMode +from ..actor_collider import OOTActorColliderImportExportSettings ootEnumBoneType = [ @@ -64,6 +65,7 @@ class OOTSkeletonExportSettings(PropertyGroup): actorOverlayName: StringProperty(name="Overlay", default="ovl_En_GeldB") flipbookUses2DArray: BoolProperty(name="Has 2D Flipbook Array", default=False) flipbookArrayIndex2D: IntProperty(name="Index if 2D Array", default=0, min=0) + handleColliders: PointerProperty(type=OOTActorColliderImportExportSettings) customAssetIncludeDir: StringProperty( name="Asset Include Directory", default="assets/objects/object_geldb", @@ -83,6 +85,7 @@ def draw_props(self, layout: UILayout): b = layout.box().column() b.label(icon="LIBRARY_DATA_BROKEN", text="Do not draw anything in SkelAnime") b.label(text="callbacks or cull limbs, will be corrupted.") + self.handleColliders.draw(layout, "Export Actor Colliders", False) layout.prop(self, "isCustom") layout.label(text="Object name used for export.", icon="INFO") layout.prop(self, "isCustomFilename") @@ -119,6 +122,7 @@ class OOTSkeletonImportSettings(PropertyGroup): actorOverlayName: StringProperty(name="Overlay", default="ovl_En_GeldB") flipbookUses2DArray: BoolProperty(name="Has 2D Flipbook Array", default=False) flipbookArrayIndex2D: IntProperty(name="Index if 2D Array", default=0, min=0) + handleColliders: PointerProperty(type=OOTActorColliderImportExportSettings) autoDetectActorScale: BoolProperty(name="Auto Detect Actor Scale", default=True) actorScale: FloatProperty(name="Actor Scale", min=0, default=100) @@ -155,6 +159,7 @@ def draw_props(self, layout: UILayout): ) else: layout.prop(self, "applyRestPose") + self.handleColliders.draw(layout, "Import Actor Colliders", True) oot_skeleton_classes = ( From 6d8d75b6869cf3eec465b6d812416561b15d1b63 Mon Sep 17 00:00:00 2001 From: kurethedead Date: Tue, 10 Jan 2023 12:29:28 -0800 Subject: [PATCH 3/6] Refactored file reading code, fixed filepaths for Link colliders. --- fast64_internal/f3d/f3d_parser.py | 9 -- .../oot/actor_collider/__init__.py | 4 - .../oot/actor_collider/exporter.py | 13 ++- .../oot/actor_collider/importer.py | 4 +- fast64_internal/oot/f3d/operators.py | 3 +- fast64_internal/oot/file_reading.py | 91 +++++++++++++++++++ fast64_internal/oot/oot_anim.py | 2 +- fast64_internal/oot/oot_f3d_writer.py | 28 ++---- fast64_internal/oot/oot_model_classes.py | 63 ------------- fast64_internal/oot/oot_texture_array.py | 7 +- .../oot/skeleton/importer/functions.py | 3 +- fast64_internal/utility.py | 9 ++ 12 files changed, 129 insertions(+), 107 deletions(-) create mode 100644 fast64_internal/oot/file_reading.py diff --git a/fast64_internal/f3d/f3d_parser.py b/fast64_internal/f3d/f3d_parser.py index d20ad2ef4..e8e8a34c4 100644 --- a/fast64_internal/f3d/f3d_parser.py +++ b/fast64_internal/f3d/f3d_parser.py @@ -2156,15 +2156,6 @@ def parseMacroArgs(data): return params -def getImportData(filepaths): - data = "" - for path in filepaths: - if os.path.exists(path): - data += readFile(path) - - return data - - def parseMatrices(sceneData: str, f3dContext: F3DContext, importScale: float = 1): for match in re.finditer(rf"Mtx\s*([a-zA-Z0-9\_]+)\s*=\s*\{{(.*?)\}}\s*;", sceneData, flags=re.DOTALL): name = "&" + match.group(1) diff --git a/fast64_internal/oot/actor_collider/__init__.py b/fast64_internal/oot/actor_collider/__init__.py index dd0a89125..124834c7d 100644 --- a/fast64_internal/oot/actor_collider/__init__.py +++ b/fast64_internal/oot/actor_collider/__init__.py @@ -17,7 +17,3 @@ from .importer import parseColliderData from .exporter import getColliderData, removeExistingColliderData, writeColliderData - -# TODO: -# test getActorFilepath changes on export -# fix Link collider import diff --git a/fast64_internal/oot/actor_collider/exporter.py b/fast64_internal/oot/actor_collider/exporter.py index 7e05523b2..fb2b49fee 100644 --- a/fast64_internal/oot/actor_collider/exporter.py +++ b/fast64_internal/oot/actor_collider/exporter.py @@ -1,11 +1,14 @@ import bpy, mathutils, os, re, math from ...utility import CData, PluginError, readFile, writeFile from ..oot_utility import getOrderedBoneList, getOOTScale -from ..oot_f3d_writer import getActorFilepath +from ..file_reading import getNonLinkActorFilepath, getLinkColliderFilepath def removeExistingColliderData(exportPath: str, overlayName: str, isLink: bool, newColliderData: str) -> str: - actorPath = getActorFilepath(exportPath, overlayName, isLink) + if not isLink: + actorPath = getNonLinkActorFilepath(exportPath, overlayName) + else: + actorPath = getLinkColliderFilepath(exportPath) data = readFile(actorPath) newActorData = data @@ -25,7 +28,11 @@ def removeExistingColliderData(exportPath: str, overlayName: str, isLink: bool, def writeColliderData(obj: bpy.types.Object, exportPath: str, overlayName: str, isLink: bool) -> str: - actorFilePath = getActorFilepath(exportPath, overlayName, isLink) + if not isLink: + actorFilePath = getNonLinkActorFilepath(exportPath, overlayName) + else: + actorFilePath = getLinkColliderFilepath(exportPath) + actor = os.path.basename(actorFilePath)[:-2] colliderData = getColliderData(obj) diff --git a/fast64_internal/oot/actor_collider/importer.py b/fast64_internal/oot/actor_collider/importer.py index 17fb5250f..9fb15b277 100644 --- a/fast64_internal/oot/actor_collider/importer.py +++ b/fast64_internal/oot/actor_collider/importer.py @@ -1,7 +1,7 @@ from typing import Callable import bpy, mathutils, os, re, math from ...utility import PluginError, hexOrDecInt -from ..oot_model_classes import ootGetActorData, ootGetIncludedAssetData, ootGetActorDataPaths, ootGetLinkData +from ..file_reading import ootGetActorData, ootGetIncludedAssetData, ootGetActorDataPaths, ootGetLinkColliderData from .properties import ( OOTActorColliderItemProperty, OOTActorColliderProperty, @@ -203,7 +203,7 @@ def parseColliderData( actorData = ootGetActorData(basePath, overlayName) currentPaths = ootGetActorDataPaths(basePath, overlayName) else: - actorData = ootGetLinkData(basePath) + actorData = ootGetLinkColliderData(basePath) currentPaths = [os.path.join(basePath, f"src/code/z_player_lib.c")] actorData = ootGetIncludedAssetData(basePath, currentPaths, actorData) + actorData diff --git a/fast64_internal/oot/f3d/operators.py b/fast64_internal/oot/f3d/operators.py index da856443f..c2af2ac8b 100644 --- a/fast64_internal/oot/f3d/operators.py +++ b/fast64_internal/oot/f3d/operators.py @@ -9,7 +9,8 @@ from ...f3d.f3d_gbi import DLFormat, F3D, TextureExportSettings, ScrollMethod from ...f3d.f3d_writer import TriangleConverterInfo, removeDL, saveStaticModel, getInfoDict from ..oot_utility import ootGetObjectPath, getOOTScale -from ..oot_model_classes import OOTF3DContext, ootGetIncludedAssetData +from ..oot_model_classes import OOTF3DContext +from ..file_reading import ootGetIncludedAssetData from ..oot_texture_array import ootReadTextureArrays from ..oot_model_classes import OOTModel, OOTGfxFormatter from ..oot_f3d_writer import ootReadActorScale, writeTextureArraysNew, writeTextureArraysExisting diff --git a/fast64_internal/oot/file_reading.py b/fast64_internal/oot/file_reading.py new file mode 100644 index 000000000..9e22e1009 --- /dev/null +++ b/fast64_internal/oot/file_reading.py @@ -0,0 +1,91 @@ +import os, re +from ..utility import getImportData + + +def getNonLinkActorFilepath(basePath: str, overlayName: str, checkDataPath: bool = False) -> str: + actorFilePath = os.path.join(basePath, f"src/overlays/actors/{overlayName}/z_{overlayName[4:].lower()}.c") + actorFileDataPath = f"{actorFilePath[:-2]}_data.c" # some bosses store texture arrays here + + if checkDataPath and os.path.exists(actorFileDataPath): + actorFilePath = actorFileDataPath + + return actorFilePath + + +def getLinkColliderFilepath(basePath: str) -> str: + return os.path.join(basePath, f"src/overlays/actors/ovl_player_actor/z_player.c") + + +def getLinkTextureFilepath(basePath: str) -> str: + return os.path.join(basePath, f"src/code/z_player_lib.c") + + # read included asset data + + +def ootGetIncludedAssetData(basePath: str, currentPaths: list[str], data: str) -> str: + includeData = "" + searchedPaths = currentPaths[:] + + print("Included paths:") + + # search assets + for includeMatch in re.finditer(r"\#include\s*\"(assets/objects/(.*?))\.h\"", data): + path = os.path.join(basePath, includeMatch.group(1) + ".c") + if path in searchedPaths: + continue + searchedPaths.append(path) + subIncludeData = getImportData([path]) + "\n" + includeData += subIncludeData + print(path) + + for subIncludeMatch in re.finditer(r"\#include\s*\"(((?![/\"]).)*)\.c\"", subIncludeData): + subPath = os.path.join(os.path.dirname(path), subIncludeMatch.group(1) + ".c") + if subPath in searchedPaths: + continue + searchedPaths.append(subPath) + print(subPath) + includeData += getImportData([subPath]) + "\n" + + # search same directory c includes, both in current path and in included object files + # these are usually fast64 exported files + for includeMatch in re.finditer(r"\#include\s*\"(((?![/\"]).)*)\.c\"", data): + sameDirPaths = [ + os.path.join(os.path.dirname(currentPath), includeMatch.group(1) + ".c") for currentPath in currentPaths + ] + sameDirPathsToSearch = [] + for sameDirPath in sameDirPaths: + if sameDirPath not in searchedPaths: + sameDirPathsToSearch.append(sameDirPath) + + for sameDirPath in sameDirPathsToSearch: + print(sameDirPath) + + includeData += getImportData(sameDirPathsToSearch) + "\n" + return includeData + + +def ootGetActorDataPaths(basePath: str, overlayName: str) -> list[str]: + actorFilePath = os.path.join(basePath, f"src/overlays/actors/{overlayName}/z_{overlayName[4:].lower()}.c") + actorFileDataPath = f"{actorFilePath[:-2]}_data.c" # some bosses store texture arrays here + + return [actorFileDataPath, actorFilePath] + + +# read actor data +def ootGetActorData(basePath: str, overlayName: str) -> str: + actorData = getImportData(ootGetActorDataPaths(basePath, overlayName)) + return actorData + + +def ootGetLinkTextureData(basePath: str) -> str: + linkFilePath = os.path.join(basePath, f"src/code/z_player_lib.c") + actorData = getImportData([linkFilePath]) + + return actorData + + +def ootGetLinkColliderData(basePath: str) -> str: + linkFilePath = os.path.join(basePath, f"src/overlays/actors/ovl_player_actor/z_player.c") + actorData = getImportData([linkFilePath]) + + return actorData diff --git a/fast64_internal/oot/oot_anim.py b/fast64_internal/oot/oot_anim.py index 49fc71b9c..72fd54e80 100644 --- a/fast64_internal/oot/oot_anim.py +++ b/fast64_internal/oot/oot_anim.py @@ -2,7 +2,7 @@ from ..utility import CData, PluginError, toAlnum, hexOrDecInt from ..f3d.f3d_parser import getImportData from .skeleton.exporter import ootConvertArmatureToSkeletonWithoutMesh -from .oot_model_classes import ootGetIncludedAssetData +from .file_reading import ootGetIncludedAssetData from ..utility_anim import ( ValueFrameData, diff --git a/fast64_internal/oot/oot_f3d_writer.py b/fast64_internal/oot/oot_f3d_writer.py index 8207f23c1..01427159f 100644 --- a/fast64_internal/oot/oot_f3d_writer.py +++ b/fast64_internal/oot/oot_f3d_writer.py @@ -12,12 +12,8 @@ saveMeshByFaces, ) -from .oot_model_classes import ( - OOTTriangleConverterInfo, - OOTModel, - ootGetActorData, - ootGetLinkData, -) +from .oot_model_classes import OOTTriangleConverterInfo, OOTModel +from .file_reading import ootGetActorData, getNonLinkActorFilepath, ootGetLinkTextureData # Creates a semi-transparent solid color material (cached) @@ -207,23 +203,13 @@ def writeTextureArraysNew(fModel: OOTModel, arrayIndex: int): return textureArrayData -def getActorFilepath(basePath: str, overlayName: str | None, isLink: bool, checkDataPath: bool = False): - if isLink: - actorFilePath = os.path.join(basePath, f"src/code/z_player_lib.c") - else: - actorFilePath = os.path.join(basePath, f"src/overlays/actors/{overlayName}/z_{overlayName[4:].lower()}.c") - actorFileDataPath = f"{actorFilePath[:-2]}_data.c" # some bosses store texture arrays here - - if checkDataPath and os.path.exists(actorFileDataPath): - actorFilePath = actorFileDataPath - - return actorFilePath - - def writeTextureArraysExisting( exportPath: str, overlayName: str, isLink: bool, flipbookArrayIndex2D: int, fModel: OOTModel ): - actorFilePath = getActorFilepath(exportPath, overlayName, isLink, True) + if not isLink: + actorFilePath = getNonLinkActorFilepath(exportPath, overlayName, True) + else: + actorFilePath = os.path.join(exportPath, f"src/code/z_player_lib.c") if not os.path.exists(actorFilePath): print(f"{actorFilePath} not found, ignoring texture array writing.") @@ -331,7 +317,7 @@ def ootReadActorScale(basePath: str, overlayName: str, isLink: bool) -> float: if not isLink: actorData = ootGetActorData(basePath, overlayName) else: - actorData = ootGetLinkData(basePath) + actorData = ootGetLinkTextureData(basePath) chainInitMatch = re.search(r"CHAIN_VEC3F_DIV1000\s*\(\s*scale\s*,\s*(.*?)\s*,", actorData, re.DOTALL) if chainInitMatch is not None: diff --git a/fast64_internal/oot/oot_model_classes.py b/fast64_internal/oot/oot_model_classes.py index ed6816432..52dbdf300 100644 --- a/fast64_internal/oot/oot_model_classes.py +++ b/fast64_internal/oot/oot_model_classes.py @@ -35,69 +35,6 @@ ) -# read included asset data -def ootGetIncludedAssetData(basePath: str, currentPaths: list[str], data: str) -> str: - includeData = "" - searchedPaths = currentPaths[:] - - print("Included paths:") - - # search assets - for includeMatch in re.finditer(r"\#include\s*\"(assets/objects/(.*?))\.h\"", data): - path = os.path.join(basePath, includeMatch.group(1) + ".c") - if path in searchedPaths: - continue - searchedPaths.append(path) - subIncludeData = getImportData([path]) + "\n" - includeData += subIncludeData - print(path) - - for subIncludeMatch in re.finditer(r"\#include\s*\"(((?![/\"]).)*)\.c\"", subIncludeData): - subPath = os.path.join(os.path.dirname(path), subIncludeMatch.group(1) + ".c") - if subPath in searchedPaths: - continue - searchedPaths.append(subPath) - print(subPath) - includeData += getImportData([subPath]) + "\n" - - # search same directory c includes, both in current path and in included object files - # these are usually fast64 exported files - for includeMatch in re.finditer(r"\#include\s*\"(((?![/\"]).)*)\.c\"", data): - sameDirPaths = [ - os.path.join(os.path.dirname(currentPath), includeMatch.group(1) + ".c") for currentPath in currentPaths - ] - sameDirPathsToSearch = [] - for sameDirPath in sameDirPaths: - if sameDirPath not in searchedPaths: - sameDirPathsToSearch.append(sameDirPath) - - for sameDirPath in sameDirPathsToSearch: - print(sameDirPath) - - includeData += getImportData(sameDirPathsToSearch) + "\n" - return includeData - - -def ootGetActorDataPaths(basePath: str, overlayName: str) -> list[str]: - actorFilePath = os.path.join(basePath, f"src/overlays/actors/{overlayName}/z_{overlayName[4:].lower()}.c") - actorFileDataPath = f"{actorFilePath[:-2]}_data.c" # some bosses store texture arrays here - - return [actorFileDataPath, actorFilePath] - - -# read actor data -def ootGetActorData(basePath: str, overlayName: str) -> str: - actorData = getImportData(ootGetActorDataPaths(basePath, overlayName)) - return actorData - - -def ootGetLinkData(basePath: str) -> str: - linkFilePath = os.path.join(basePath, f"src/code/z_player_lib.c") - actorData = getImportData([linkFilePath]) - - return actorData - - class OOTModel(FModel): def __init__(self, f3dType, isHWv1, name, DLFormat, drawLayerOverride): self.drawLayerOverride = drawLayerOverride diff --git a/fast64_internal/oot/oot_texture_array.py b/fast64_internal/oot/oot_texture_array.py index 061d98d38..b2c0e6268 100644 --- a/fast64_internal/oot/oot_texture_array.py +++ b/fast64_internal/oot/oot_texture_array.py @@ -5,10 +5,13 @@ from .oot_model_classes import ( OOTF3DContext, TextureFlipbook, +) + +from .file_reading import ( ootGetActorData, ootGetActorDataPaths, ootGetIncludedAssetData, - ootGetLinkData, + ootGetLinkTextureData, ) # Special cases: @@ -26,7 +29,7 @@ def ootReadTextureArrays( actorData = ootGetActorData(basePath, overlayName) currentPaths = ootGetActorDataPaths(basePath, overlayName) else: - actorData = ootGetLinkData(basePath) + actorData = ootGetLinkTextureData(basePath) currentPaths = [os.path.join(basePath, f"src/code/z_player_lib.c")] actorData = ootGetIncludedAssetData(basePath, currentPaths, actorData) + actorData diff --git a/fast64_internal/oot/skeleton/importer/functions.py b/fast64_internal/oot/skeleton/importer/functions.py index a0e582c79..e0eeb4e52 100644 --- a/fast64_internal/oot/skeleton/importer/functions.py +++ b/fast64_internal/oot/skeleton/importer/functions.py @@ -3,7 +3,8 @@ from ....f3d.f3d_parser import getImportData, parseF3D from ....utility import hexOrDecInt, applyRotation from ...oot_f3d_writer import ootReadActorScale -from ...oot_model_classes import OOTF3DContext, ootGetIncludedAssetData +from ...oot_model_classes import OOTF3DContext +from ...file_reading import ootGetIncludedAssetData from ...oot_utility import ootGetObjectPath, getOOTScale from ...oot_texture_array import ootReadTextureArrays from ..constants import ootSkeletonImportDict diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index 20723e5c8..3120d54e2 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -50,6 +50,15 @@ class VertexWeightError(PluginError): ] +def getImportData(filepaths: list[str]) -> str: + data = "" + for path in filepaths: + if os.path.exists(path): + data += readFile(path) + + return data + + def isPowerOf2(n): return (n & (n - 1) == 0) and n != 0 From ace132404c55dba46380ce71167714818d1edb34 Mon Sep 17 00:00:00 2001 From: kurethedead Date: Tue, 10 Jan 2023 14:34:17 -0800 Subject: [PATCH 4/6] Hide OOT menus on materials if the object is a mesh actor collider --- fast64_internal/oot/actor_collider/__init__.py | 1 + fast64_internal/oot/actor_collider/panels.py | 15 +++++++++------ fast64_internal/oot/collision/panels.py | 3 ++- fast64_internal/oot/f3d/panels.py | 3 ++- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/fast64_internal/oot/actor_collider/__init__.py b/fast64_internal/oot/actor_collider/__init__.py index 124834c7d..0330455ab 100644 --- a/fast64_internal/oot/actor_collider/__init__.py +++ b/fast64_internal/oot/actor_collider/__init__.py @@ -13,6 +13,7 @@ from .panels import ( actor_collider_panel_register, actor_collider_panel_unregister, + isActorCollider, ) from .importer import parseColliderData diff --git a/fast64_internal/oot/actor_collider/panels.py b/fast64_internal/oot/actor_collider/panels.py index 3f994bf33..043adb6db 100644 --- a/fast64_internal/oot/actor_collider/panels.py +++ b/fast64_internal/oot/actor_collider/panels.py @@ -27,6 +27,14 @@ def draw(self, context: bpy.types.Context): obj.ootActorColliderItem.draw(obj, box) +def isActorCollider(context: bpy.types.Context) -> bool: + return ( + (context.object is not None and isinstance(context.object.data, bpy.types.Mesh)) + and context.object.ootGeometryType == "Actor Collider" + and context.material is not None + ) + + class OOT_ActorColliderMaterialPanel(bpy.types.Panel): bl_label = "OOT Actor Collider Material Inspector" bl_idname = "OBJECT_PT_OOT_Actor_Collider_Material_Inspector" @@ -37,12 +45,7 @@ class OOT_ActorColliderMaterialPanel(bpy.types.Panel): @classmethod def poll(cls, context: bpy.types.Context): - return ( - context.scene.gameEditorMode == "OOT" - and (context.object is not None and isinstance(context.object.data, bpy.types.Mesh)) - and context.object.ootGeometryType == "Actor Collider" - and context.material is not None - ) + return context.scene.gameEditorMode == "OOT" and isActorCollider(context) def draw(self, context: bpy.types.Context): material = context.material diff --git a/fast64_internal/oot/collision/panels.py b/fast64_internal/oot/collision/panels.py index b10bc8c72..615eac273 100644 --- a/fast64_internal/oot/collision/panels.py +++ b/fast64_internal/oot/collision/panels.py @@ -5,6 +5,7 @@ from ..oot_utility import drawEnumWithCustom from .properties import OOTCollisionExportSettings, OOTCameraPositionProperty, OOTMaterialCollisionProperty from .operators import OOT_ExportCollision +from ..actor_collider import isActorCollider class OOT_CameraPosPanel(Panel): @@ -38,7 +39,7 @@ class OOT_CollisionPanel(Panel): @classmethod def poll(cls, context): - return context.scene.gameEditorMode == "OOT" and context.material is not None + return context.scene.gameEditorMode == "OOT" and context.material is not None and not isActorCollider(context) def draw(self, context): box = self.layout.box().column() diff --git a/fast64_internal/oot/f3d/panels.py b/fast64_internal/oot/f3d/panels.py index 18a6ef691..fa4aede48 100644 --- a/fast64_internal/oot/f3d/panels.py +++ b/fast64_internal/oot/f3d/panels.py @@ -9,6 +9,7 @@ OOTDynamicMaterialProperty, OOTDefaultRenderModesProperty, ) +from ..actor_collider import isActorCollider class OOT_DisplayListPanel(Panel): @@ -55,7 +56,7 @@ class OOT_MaterialPanel(Panel): @classmethod def poll(cls, context): - return context.material is not None and context.scene.gameEditorMode == "OOT" + return context.material is not None and context.scene.gameEditorMode == "OOT" and not isActorCollider(context) def draw(self, context): layout = self.layout From da1616a68dfa3474a3cfa0a5579aa94d69ab9a72 Mon Sep 17 00:00:00 2001 From: kurethedead Date: Tue, 10 Jan 2023 14:38:50 -0800 Subject: [PATCH 5/6] Copied over README --- fast64_internal/oot/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fast64_internal/oot/README.md b/fast64_internal/oot/README.md index 69ba2e02d..1f3148b5f 100644 --- a/fast64_internal/oot/README.md +++ b/fast64_internal/oot/README.md @@ -124,6 +124,21 @@ Heavy modifications of Links model can cause his matrices array to shift from wh 6. In the actor header file, (in src/overlays/actors/\/), set the joint/morph table sizes to be (number of bones + 1) 7. In the actor source file, this value should also be used for the limbCount argument in SkelAnime_InitFlex(). +### Actor Colliders +You can add actor colliders to an armature or display list using the operators in the OOT Tools menu. The collider properties will be shown under the object properties tab, except for mesh collider which will store part of the collider properties in the material properties tab. The struct name is also stored in the object properties, which is what determines what the export collider name will be. You can also toggle collider visibility by type in the OOT Tools menu. + +- For quad colliders, the actual shape is ignored and only the properties are relevant. +- For joint sphere colliders, the armature specific section is shared among all joint spheres on the armature. This means that an armature can only have one joint sphere collider collection. + +The scale of the collider objects determines their sizes, so don't worry if they have unapplied scales. + +### Actor Collider Importing +Actor collider importing is a bit finicky. +1. Some actors will modify their collider data, so that the actual data is not representative of in game shape. For example, most shield/sword collider vertices are modified by actors. Quad colliders seem to exclusively fall under this case, so the actual shape is ignored when importing/exporting. +2. While skeletons are transformed by Actor_SetScale(), colliders are not. Therefore, having the correct actor scale is important to matching collider sizes. Currently fast64 tries to find the first instance of CHAIN_VEC3F_DIV1000(scale, ....) or Actor_SetScale(), but sometimes this is not correct. Make sure to check the actor file and manually set an actor scale for importing colliders if things are scaled weirdly. +3. Some actor files contain multiple "parts", which means multiple skeletons and multiple colliders. You may want to manually specify which colliders to import in the import settings. Only Bongo Bongo and Barinade imports handle these cases automatically currently. +4. Ganon2 has two sphere joint colliders, which is not supported by fast64 currently. Ganon2's joint sphere colliders also seem to not follow the same rules as other sphere joint colliders. + ### Creating a Cutscene **Creating the cutscene itself:** From 7c72218416bc85593ef5365af7ad6a5bc8d73aac Mon Sep 17 00:00:00 2001 From: kurethedead Date: Tue, 10 Jan 2023 17:43:26 -0800 Subject: [PATCH 6/6] Updated README --- fast64_internal/oot/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fast64_internal/oot/README.md b/fast64_internal/oot/README.md index 1f3148b5f..c5edf8301 100644 --- a/fast64_internal/oot/README.md +++ b/fast64_internal/oot/README.md @@ -137,7 +137,8 @@ Actor collider importing is a bit finicky. 1. Some actors will modify their collider data, so that the actual data is not representative of in game shape. For example, most shield/sword collider vertices are modified by actors. Quad colliders seem to exclusively fall under this case, so the actual shape is ignored when importing/exporting. 2. While skeletons are transformed by Actor_SetScale(), colliders are not. Therefore, having the correct actor scale is important to matching collider sizes. Currently fast64 tries to find the first instance of CHAIN_VEC3F_DIV1000(scale, ....) or Actor_SetScale(), but sometimes this is not correct. Make sure to check the actor file and manually set an actor scale for importing colliders if things are scaled weirdly. 3. Some actor files contain multiple "parts", which means multiple skeletons and multiple colliders. You may want to manually specify which colliders to import in the import settings. Only Bongo Bongo and Barinade imports handle these cases automatically currently. -4. Ganon2 has two sphere joint colliders, which is not supported by fast64 currently. Ganon2's joint sphere colliders also seem to not follow the same rules as other sphere joint colliders. +4. Some actors like Ganon2 have multiple joint sphere collider "groups", which is not supported by fast64. Only the first group will be imported, so if a different one is needed then the name will have to be manually set in the import settings. +5. Some actors with joint sphere colliders do not use the joint index field as an actual joint index, so when importing the parenting hierarchy won't make sense. Uncheck "parent joint spheres to bones" to do object parenting instead. ### Creating a Cutscene **Creating the cutscene itself:**