Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions fast64_internal/f3d/f3d_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions fast64_internal/oot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,22 @@ Heavy modifications of Links model can cause his matrices array to shift from wh
6. In the actor header file, (in src/overlays/actors/\<name\>/), 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. 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:**

Expand Down
15 changes: 15 additions & 0 deletions fast64_internal/oot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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():
Expand All @@ -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):
Expand All @@ -112,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()
Expand Down Expand Up @@ -146,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()
Expand Down
20 changes: 20 additions & 0 deletions fast64_internal/oot/actor_collider/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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,
isActorCollider,
)

from .importer import parseColliderData
from .exporter import getColliderData, removeExistingColliderData, writeColliderData
242 changes: 242 additions & 0 deletions fast64_internal/oot/actor_collider/exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import bpy, mathutils, os, re, math
from ...utility import CData, PluginError, readFile, writeFile
from ..oot_utility import getOrderedBoneList, getOOTScale
from ..file_reading import getNonLinkActorFilepath, getLinkColliderFilepath


def removeExistingColliderData(exportPath: str, overlayName: str, isLink: bool, newColliderData: str) -> str:
if not isLink:
actorPath = getNonLinkActorFilepath(exportPath, overlayName)
else:
actorPath = getLinkColliderFilepath(exportPath)

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:
if not isLink:
actorFilePath = getNonLinkActorFilepath(exportPath, overlayName)
else:
actorFilePath = getLinkColliderFilepath(exportPath)

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
Loading