OOT Link Import/Export + Flipbook Import/Export + Animation Updates/Fixes (#126)

* Format oot_f3d_writer with black.

* Add properties/UI for flipbook textures.

* Form oot_skeleton with black.

* Added parameter for actor name to be passed into skeleton parsing code.

* Formatted oot_model_classes with black

* Added basic flipbook texture array importing.

Note this only works under specific conditions right now (texture array name must be present in the gSPSegment() call)

* Added visualization option to texture array items.

* Added better texture array detection handling most cases.

Notable exceptions: z_en_wf (branch between two different segment loads), z_obj_switch (2d texture array)

* Added warning labels for edge case overlays.

* EnOssan (shopkeepers) and DemoEc (end credit) overlays now supported for flipbook textures.

* Added support for Link for flipbook textures.z

* Removed debug prints.

* Fixed issue with texture reference palettes not being loaded correctly.

* Added name property for flipbook textures when in "individual" mode.

* Added exporting of flipbook textures along with skeletons.

* Added load button to flipbook textures.

* Fixed bug where visualizing a flipbook texture auto optimized the texture format.

* Fixed minor bug with texture array definition not being found

* Added support for reading/writing elements from 2D texture arrays.

Note that there is current import lag (probably due to regex processing) after implementing asset including for files.

* Added flipbook texture handling to DLs, fixed lag issue with including link_animetion.c for regex searches. Fixed some asset includes.

* Fixed minor bugs related to overlayName parameter. Added palette applying to flipbooks, which will be incorrect for importing fast64 exports.

* Added warning message for LOD overriding flipbook textures.

* Attempted to add palette sharing.

One import/export cycle works fine, next results in compiler errors

* Fixed issue with trying to re-process existing flipbook textures.

* Fixed issue with DL import not finding textures.

* Fixed issue where importing an export would cause CI flipbook issues with skeletons using LOD.

* Commented out some debug messages.

* Improved debug message for flipbook errors.

* Fixed issue with checking if flipbook property should be processed. Refactored flipbook CI exporting code.

* Fixed issue with bad flipbook CI for LOD skeletons.

There's a possibility that two CI flipbooks write to the same flipbook name. Before this fix the later one would override the older one (ex. LOD), which would cause the first flipbook to use the LOD version textures with its own palette, causing issues.

* Minor UI / error handling for flipbooks.

* Removed outdated UI message.

* Updated README.

* Format oot_anim with black.

* Added Link animation importing.

* Added Link animation import/export, including texture animation. Fixed signed short bug with animation data.

* Placed all oot_anim import/export settings into their own property groups.

* Updated README.

* Fixed issue with bad normals on export due to non-folded rest pose.

* Fixed issues with incorrect animation exports for rotated armatures. Fixed skeleton export for armatures with non-zero rest pose rotations.

* Fixed bug where vanilla limb removal caused unwanted removed data.

* Updated README for skeleton exporting process.

* Added ability to import animation rotations correctly for non-zero rotation rest poses.

* Can now import translations correctly for non-zero rotation armatures.

* Refactored flipbook code to be more generalized. Fixed direct setting of texture nodes for flipbook animation.

* Black format oot_collision_classes

* Black format oot_collision

* Fixed typo error

Co-authored-by: krm01 <[email protected]>

* Fixed typo error

Co-authored-by: krm01 <[email protected]>

* Fixed 2d array index issue when doing a custom Link export. Added handling for gEmptyDL import for skeletons.

* Skeletons now preserve minium DL count using gEmptyDL, fixed issue with comments in texture arrays.

* Format oot_f3d_writer with black

* Format oot_skeleton with black

* Fixed texture file saving for oot skeleton/dl

* Moved skeleton import/export settings into property groups.

* Custom skeleton export now has options for folder and asset include directory.

* Fixed skeleton export settings bug

* Moved oot dl import/export settings into property groups

* Custom dl export now has options for folder and asset include directory.

* Format sm64_geolayout_writer with black.

* Format sm64_level_writer with black.

* Re-allowed texture c array conversion when ignoring texture restrictions

* Large texture mode now converts textures, updated getOptimalFormat

* Update fast64_internal/f3d/f3d_material.py

Co-authored-by: Dragorn421 <[email protected]>

* Move export/import settings to OOT_Properties, renamed assetIncludeDir

* Fixed register ordering

* Fixed possible unbound useLargeTextures

* Update fast64_internal/oot/oot_skeleton.py

Co-authored-by: Dragorn421 <[email protected]>

* Removed unused imports

* Changed name of ArmatureApplyWithMesh operator for more clarity from function of same name

* Update fast64_internal/oot/oot_model_classes.py

Co-authored-by: Dragorn421 <[email protected]>

* Changed typing for validateCIFlipbook for clarity.

* Added easy option for importing/exporting Link

* Added convenient rest poses for importing Link

* Removed global actor scale, moved to object local property. Imports will try to parse actor scale.

* Refactored skeleton import/export settings to use property groups

* Fixed actor scale detection. Handled collider import cases for Va and Sst.

* Fixed actor scale importing.

* Update fast64_internal/f3d/flipbook.py

Co-authored-by: Dragorn421 <[email protected]>

* Used dataclass

* Update fast64_internal/f3d/flipbook.py

Co-authored-by: Dragorn421 <[email protected]>

* Add optional typing to checkFlipbookReference

* Renamed ImagePointerProperty to FlipbookImagePointerProperty

* Removed outdated comments

* Renamed drawFlipbookRequirementMessage

* Update fast64_internal/f3d/flipbook.py

Co-authored-by: Dragorn421 <[email protected]>

* Update fast64_internal/f3d/flipbook.py

Co-authored-by: Dragorn421 <[email protected]>

* Update fast64_internal/f3d/flipbook.py

Co-authored-by: Dragorn421 <[email protected]>

* Fixed bug with flipbook anim updating

* Fixed another bug with flipbook anim updating

* Removed flipbookAnimHandler on unregister

* Update fast64_internal/oot/oot_anim.py

Co-authored-by: Dragorn421 <[email protected]>

* Update fast64_internal/oot/oot_anim.py

Co-authored-by: Dragorn421 <[email protected]>

* Update fast64_internal/oot/oot_anim.py

Co-authored-by: Dragorn421 <[email protected]>

* Renamed ootRotationValue to binangToRadians

* Update fast64_internal/oot/oot_anim.py

Co-authored-by: Dragorn421 <[email protected]>

* Renamed link texture animation callbacks

* Update fast64_internal/oot/oot_anim.py

Co-authored-by: Dragorn421 <[email protected]>

* Move anim import/export settings to fast64.oot property group

* Renamed is2DArray to flipbookUses2DArray

* Renamed arrayIndex2D to flipbookArrayIndex2D

* Renamed overlay to actorOverlayName

* Update fast64_internal/oot/oot_texture_array.py

Co-authored-by: Dragorn421 <[email protected]>

* Added CollectionProperty alias for typing purposes.

* Update fast64_internal/oot/oot_texture_array.py

Co-authored-by: Dragorn421 <[email protected]>

* Update fast64_internal/f3d/f3d_parser.py

Co-authored-by: Dragorn421 <[email protected]>

* Added typing to F3DContext, refactored tuples into classes.

- moved clearMaterial()/clearGeometry() into parser/writer functions, so game specific writers/parsers doesn't have to call them directly
- fixed some variable name bugs in oot_f3d_writer

* Fixed bug with DL importing due to function order.

* Updated README for custom link import/export.

* Add F3D parsing fixes and material optimizations.

Also fixes bug where dynamic gfx calls weren't being imported correctly for Link.

* Fixed issue where files were being written outside of folder for custom export

* Added error message for unweighted vertices.

* Updated README

* Add import

* Fixed material caching issue for tex gen

* Fixed issue with incorrectly preserved material properties

Note that this fix also takes into account that dynamic material properties should be preserved between limbs (Link).

* Minor hash related issue with Light

* Update fast64_internal/f3d/f3d_material.py

Co-authored-by: Dragorn421 <[email protected]>

* Update fast64_internal/f3d/f3d_writer.py

Co-authored-by: Dragorn421 <[email protected]>

* Modified comments in oot_anim

* Fixed comment in f3d_parser

* Fixed material key with lighting properties

* Changed types to sequences

* Fixed divide by 0 error when tex scale = 0

* Fixed tuple

* Remove scale_autoprop setting

* Added error message

* Fixed vertex buffer error message

Co-authored-by: krm01 <[email protected]>
Co-authored-by: Dragorn421 <[email protected]>
This commit is contained in:
kurethedead
2022-11-03 13:03:07 +01:00
committed by GitHub
co-authored by Dragorn421 krm01
parent fbb5d19085
commit 31a9680f5d
23 changed files with 3031 additions and 521 deletions
+8 -60
View File
@@ -13,11 +13,13 @@ from .fast64_internal.sm64.sm64_geolayout_parser import generateMetarig
from .fast64_internal.oot import OOT_Properties, oot_register, oot_unregister
from .fast64_internal.oot.oot_level import OOT_ObjectProperties
from .fast64_internal.utility_anim import utility_anim_register, utility_anim_unregister, ArmatureApplyWithMeshOperator
from .fast64_internal.f3d.f3d_material import mat_register, mat_unregister
from .fast64_internal.f3d.f3d_render_engine import render_engine_register, render_engine_unregister
from .fast64_internal.f3d.f3d_writer import f3d_writer_register, f3d_writer_unregister
from .fast64_internal.f3d.f3d_parser import f3d_parser_register, f3d_parser_unregister
from .fast64_internal.f3d.flipbook import flipbook_register, flipbook_unregister
from .fast64_internal.f3d_material_converter import (
MatUpdateConvert,
@@ -51,63 +53,6 @@ gameEditorEnum = (
)
class ArmatureApplyWithMesh(bpy.types.Operator):
# set bl_ properties
bl_description = (
"Applies current pose as default pose. Useful for "
+ "rigging an armature that is not in T/A pose. Note that when using "
+ " with an SM64 armature, you must revert to the default pose after "
+ "skinning."
)
bl_idname = "object.armature_apply_w_mesh"
bl_label = "Apply As Rest Pose"
bl_options = {"REGISTER", "UNDO", "PRESET"}
# Called on demand (i.e. button press, menu item)
# Can also be called from operator search menu (Spacebar)
def execute(self, context):
try:
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
if len(context.selected_objects) == 0:
raise PluginError("Armature not selected.")
elif type(context.selected_objects[0].data) is not bpy.types.Armature:
raise PluginError("Armature not selected.")
armatureObj = context.selected_objects[0]
for child in armatureObj.children:
if type(child.data) is not bpy.types.Mesh:
continue
armatureModifier = None
for modifier in child.modifiers:
if isinstance(modifier, bpy.types.ArmatureModifier):
armatureModifier = modifier
if armatureModifier is None:
continue
print(armatureModifier.name)
bpy.ops.object.select_all(action="DESELECT")
context.view_layer.objects.active = child
bpy.ops.object.modifier_copy(modifier=armatureModifier.name)
print(len(child.modifiers))
attemptModifierApply(armatureModifier)
bpy.ops.object.select_all(action="DESELECT")
context.view_layer.objects.active = armatureObj
bpy.ops.object.mode_set(mode="POSE")
bpy.ops.pose.armature_apply()
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
except Exception as e:
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
raisePluginError(self, e)
return {"CANCELLED"}
self.report({"INFO"}, "Applied armature with mesh.")
return {"FINISHED"} # must return a set
class AddBoneGroups(bpy.types.Operator):
# set bl_ properties
bl_description = (
@@ -193,7 +138,7 @@ class SM64_ArmatureToolsPanel(SM64_Panel):
# called every frame
def draw(self, context):
col = self.layout.column()
col.operator(ArmatureApplyWithMesh.bl_idname)
col.operator(ArmatureApplyWithMeshOperator.bl_idname)
col.operator(AddBoneGroups.bl_idname)
col.operator(CreateMetarig.bl_idname)
col.operator(SM64_AddWaterBox.bl_idname)
@@ -277,7 +222,7 @@ class Fast64_GlobalToolsPanel(bpy.types.Panel):
# called every frame
def draw(self, context):
col = self.layout.column()
col.operator(ArmatureApplyWithMesh.bl_idname)
col.operator(ArmatureApplyWithMeshOperator.bl_idname)
# col.operator(CreateMetarig.bl_idname)
addon_updater_ops.update_notice_box_ui(self, context)
@@ -435,7 +380,6 @@ classes = (
Fast64_Properties,
Fast64_BoneProperties,
Fast64_ObjectProperties,
ArmatureApplyWithMesh,
AddBoneGroups,
CreateMetarig,
SM64_AddWaterBox,
@@ -497,6 +441,7 @@ def register():
register_class(ExampleAddonPreferences)
addon_updater_ops.register(bl_info)
utility_anim_register()
mat_register()
render_engine_register()
bsdf_conv_register()
@@ -508,6 +453,7 @@ def register():
bsdf_conv_panel_regsiter()
f3d_writer_register()
flipbook_register()
f3d_parser_register()
# ROM
@@ -534,6 +480,8 @@ def register():
# called on add-on disabling
def unregister():
utility_anim_unregister()
flipbook_unregister()
f3d_writer_unregister()
f3d_parser_unregister()
sm64_unregister(True)
+33 -7
View File
@@ -1,4 +1,5 @@
# Macros are all copied over from gbi.h
from typing import Sequence
import bpy, os, enum
from ..utility import *
@@ -2265,7 +2266,7 @@ class FModel:
self.texturesSavedLastExport = 0 # hacky
# Called before SPEndDisplayList
def onMaterialCommandsBuilt(self, gfxList, revertList, material, drawLayer):
def onMaterialCommandsBuilt(self, fMaterial, material, drawLayer):
return
def getTextureSuffixFromFormat(self, texFmt):
@@ -2320,7 +2321,16 @@ class FModel:
# Check if texture is in self
if imageKey in self.textures:
fImage = self.textures[imageKey]
fPalette = self.textures[fImage.paletteKey] if fImage.paletteKey is not None else None
if fImage.paletteKey is not None:
if fImage.paletteKey in self.textures:
fPalette = self.textures[fImage.paletteKey]
else:
print(f"Can't find {str(fImage.paletteKey)}")
fPalette = None
else:
# print("Palette key is None")
fPalette = None
return fImage, fPalette
if self.parentModel is not None:
@@ -2974,9 +2984,17 @@ class Vp:
class Light:
def __init__(self, color, normal):
self.color = color
self.normal = normal
def __init__(self, color: Sequence, normal: Sequence):
self.color: Sequence = color
self.normal: Sequence = normal
def __eq__(self, other):
if not isinstance(other, Light):
return False
return self.color == other.color and self.normal == other.normal
def __hash__(self):
return hash((self.color[:], self.normal[:]))
def to_binary(self):
return bytearray(self.color + [0x00] + self.color + [0x00] + self.normal + [0x00] + [0x00] * 4)
@@ -3024,8 +3042,16 @@ class Light:
class Ambient:
def __init__(self, color):
self.color = color
def __init__(self, color: Sequence):
self.color: Sequence = color
def __eq__(self, other):
if not isinstance(other, Ambient):
return False
return self.color == other.color
def __hash__(self):
return hash(self.color[:])
def to_binary(self):
return bytearray(self.color + [0x00] + self.color + [0x00])
+183 -7
View File
@@ -11,8 +11,9 @@ from ..utility import *
from ..render_settings import Fast64RenderSettings_Properties, update_scene_props_from_render_settings
from .f3d_material_helpers import F3DMaterial_UpdateLock
from bpy.app.handlers import persistent
from typing import Generator, Optional, Tuple
from typing import Generator, Optional, Tuple, Any
F3DMaterialHash = Any # giant tuple
logging.basicConfig(format="%(asctime)s: %(message)s", datefmt="%m/%d/%Y %I:%M:%S %p")
logger = logging.getLogger(__name__)
@@ -212,6 +213,9 @@ class DrawLayerProperty(bpy.types.PropertyGroup):
sm64: bpy.props.EnumProperty(items=sm64EnumDrawLayers, default="1", update=update_draw_layer)
oot: bpy.props.EnumProperty(items=ootEnumDrawLayers, default="Opaque", update=update_draw_layer)
def key(self):
return (self.sm64, self.oot)
def getTmemWordUsage(texFormat, width, height):
texelsPerLine = 64 / bitSizeDict[texBitSizeOf[texFormat]]
@@ -2166,12 +2170,18 @@ class TextureFieldProperty(bpy.types.PropertyGroup):
update=update_tex_field_prop,
)
def key(self):
return (self.clamp, self.mirror, round(self.low * 4), round(self.high * 4), self.mask, self.shift)
class SetTileSizeScrollProperty(bpy.types.PropertyGroup):
s: bpy.props.IntProperty(min=-4095, max=4095, default=0)
t: bpy.props.IntProperty(min=-4095, max=4095, default=0)
interval: bpy.props.IntProperty(min=1, soft_max=1000, default=1)
def key(self):
return (self.s, self.t, self.interval)
class TextureProperty(bpy.types.PropertyGroup):
tex: bpy.props.PointerProperty(
@@ -2241,6 +2251,26 @@ class TextureProperty(bpy.types.PropertyGroup):
return self.tex_reference_size
return [0, 0]
def key(self):
texSet = self.tex_set
isCI = self.tex_format == "CI8" or self.tex_format == "CI4"
useRef = self.use_tex_reference
return (
self.tex_set,
self.tex if texSet else None,
self.tex_format if texSet else None,
self.ci_format if texSet and isCI else None,
self.S.key() if texSet else None,
self.T.key() if texSet else None,
self.autoprop if texSet else None,
self.tile_scroll.key() if texSet else None,
self.use_tex_reference if texSet else None,
self.tex_reference if texSet and useRef else None,
self.tex_reference_size if texSet and useRef else None,
self.pal_reference if texSet and useRef and isCI else None,
self.pal_reference_size if texSet and useRef and isCI else None,
)
def on_tex_autoprop(texProperty, context):
if texProperty.autoprop and texProperty.tex is not None:
@@ -2332,6 +2362,18 @@ class CombinerProperty(bpy.types.PropertyGroup):
update=update_combiner_connections_and_preset,
)
def key(self):
return (
self.A,
self.B,
self.C,
self.D,
self.A_alpha,
self.B_alpha,
self.C_alpha,
self.D_alpha,
)
class ProceduralAnimProperty(bpy.types.PropertyGroup):
speed: bpy.props.FloatProperty(name="Speed", default=1)
@@ -2343,6 +2385,19 @@ class ProceduralAnimProperty(bpy.types.PropertyGroup):
animate: bpy.props.BoolProperty()
animType: bpy.props.EnumProperty(name="Type", items=enumTexScroll)
def key(self):
anim = self.animate
return (
self.animate,
round(self.speed, 4) if anim else None,
round(self.amplitude, 4) if anim else None,
round(self.frequency, 4) if anim else None,
round(self.spaceFrequency, 4) if anim else None,
round(self.offset, 4) if anim else None,
round(self.noiseAmplitude, 4) if anim else None,
self.animType if anim else None,
)
class ProcAnimVectorProperty(bpy.types.PropertyGroup):
x: bpy.props.PointerProperty(type=ProceduralAnimProperty)
@@ -2352,6 +2407,16 @@ class ProcAnimVectorProperty(bpy.types.PropertyGroup):
angularSpeed: bpy.props.FloatProperty(default=1, name="Angular Speed")
menu: bpy.props.BoolProperty()
def key(self):
return (
self.x.key(),
self.y.key(),
self.z.key(),
round(self.pivot[0], 4),
round(self.pivot[1], 4),
round(self.angularSpeed, 4),
)
class PrimDepthSettings(bpy.types.PropertyGroup):
z: bpy.props.IntProperty(
@@ -2380,6 +2445,9 @@ class PrimDepthSettings(bpy.types.PropertyGroup):
),
)
def key(self):
return (self.z, self.dz)
class RDPSettings(bpy.types.PropertyGroup):
g_zbuffer: bpy.props.BoolProperty(
@@ -2640,6 +2708,59 @@ class RDPSettings(bpy.types.PropertyGroup):
update=update_node_values_with_preset,
)
def key(self):
setRM = self.set_rendermode
rmAdv = self.rendermode_advanced_enabled
prim = self.g_mdsft_zsrcsel == "G_ZS_PRIM"
return (
self.g_zbuffer,
self.g_shade,
self.g_cull_front,
self.g_cull_back,
self.g_fog,
self.g_lighting,
self.g_tex_gen,
self.g_tex_gen_linear,
self.g_shade_smooth,
self.g_clipping,
self.g_mdsft_alpha_dither,
self.g_mdsft_rgb_dither,
self.g_mdsft_combkey,
self.g_mdsft_textconv,
self.g_mdsft_text_filt,
self.g_mdsft_textlod,
self.g_mdsft_textdetail,
self.g_mdsft_textpersp,
self.g_mdsft_cycletype,
self.g_mdsft_color_dither,
self.g_mdsft_pipeline,
self.g_mdsft_alpha_compare,
self.g_mdsft_zsrcsel,
self.prim_depth.key() if prim else None,
self.clip_ratio,
self.set_rendermode,
self.aa_en if setRM and rmAdv else None,
self.z_cmp if setRM and rmAdv else None,
self.z_upd if setRM and rmAdv else None,
self.im_rd if setRM and rmAdv else None,
self.clr_on_cvg if setRM and rmAdv else None,
self.cvg_dst if setRM and rmAdv else None,
self.zmode if setRM and rmAdv else None,
self.cvg_x_alpha if setRM and rmAdv else None,
self.alpha_cvg_sel if setRM and rmAdv else None,
self.force_bl if setRM and rmAdv else None,
self.blend_p1 if setRM and rmAdv else None,
self.blend_p2 if setRM and rmAdv else None,
self.blend_m1 if setRM and rmAdv else None,
self.blend_m2 if setRM and rmAdv else None,
self.blend_a1 if setRM and rmAdv else None,
self.blend_a2 if setRM and rmAdv else None,
self.blend_b1 if setRM and rmAdv else None,
self.blend_b2 if setRM and rmAdv else None,
self.rendermode_preset_cycle_1 if setRM and not rmAdv else None,
self.rendermode_preset_cycle_2 if setRM and not rmAdv else None,
)
class DefaultRDPSettingsPanel(bpy.types.Panel):
bl_label = "RDP Default Settings"
@@ -2668,7 +2789,7 @@ def getOptimalFormat(tex, curFormat, isMultitexture):
texFormat = "RGBA16"
if isMultitexture:
return curFormat
if (tex.size[0] * tex.size[1] > 8192): # Image too big
if tex.size[0] * tex.size[1] > 8192: # Image too big
return curFormat
isGreyscale = True
@@ -3322,6 +3443,60 @@ class F3DMaterialProperty(bpy.types.PropertyGroup):
draw_layer: bpy.props.PointerProperty(type=DrawLayerProperty)
use_large_textures: bpy.props.BoolProperty(name="Large Texture Mode")
def key(self) -> F3DMaterialHash:
useDefaultLighting = self.set_lights and self.use_default_lighting
return (
self.scale_autoprop,
self.uv_basis,
self.UVanim0.key(),
self.UVanim1.key(),
tuple([round(value, 4) for value in self.tex_scale]),
self.tex0.key(),
self.tex1.key(),
self.rdp_settings.key(),
self.draw_layer.key(),
self.use_large_textures,
self.use_default_lighting,
self.set_blend,
self.set_prim,
self.set_env,
self.set_key,
self.set_k0_5,
self.set_combiner,
self.set_lights,
self.set_fog,
tuple([round(value, 4) for value in self.blend_color]) if self.set_blend else None,
tuple([round(value, 4) for value in self.prim_color]) if self.set_prim else None,
round(self.prim_lod_frac, 4) if self.set_prim else None,
round(self.prim_lod_min, 4) if self.set_prim else None,
tuple([round(value, 4) for value in self.env_color]) if self.set_env else None,
tuple([round(value, 4) for value in self.key_center]) if self.set_key else None,
tuple([round(value, 4) for value in self.key_scale]) if self.set_key else None,
tuple([round(value, 4) for value in self.key_width]) if self.set_key else None,
round(self.k0, 4) if self.set_k0_5 else None,
round(self.k1, 4) if self.set_k0_5 else None,
round(self.k2, 4) if self.set_k0_5 else None,
round(self.k3, 4) if self.set_k0_5 else None,
round(self.k4, 4) if self.set_k0_5 else None,
round(self.k5, 4) if self.set_k0_5 else None,
self.combiner1.key() if self.set_combiner else None,
self.combiner2.key() if self.set_combiner else None,
tuple([round(value, 4) for value in self.fog_color]) if self.set_fog else None,
tuple([round(value, 4) for value in self.fog_position]) if self.set_fog else None,
tuple([round(value, 4) for value in self.default_light_color]) if useDefaultLighting else None,
self.set_ambient_from_light if useDefaultLighting else None,
tuple([round(value, 4) for value in self.ambient_light_color])
if useDefaultLighting and not self.set_ambient_from_light
else None,
self.f3d_light1 if not useDefaultLighting else None,
self.f3d_light2 if not useDefaultLighting else None,
self.f3d_light3 if not useDefaultLighting else None,
self.f3d_light4 if not useDefaultLighting else None,
self.f3d_light5 if not useDefaultLighting else None,
self.f3d_light6 if not useDefaultLighting else None,
self.f3d_light7 if not useDefaultLighting else None,
)
class UnlinkF3DImage0(bpy.types.Operator):
bl_idname = "image.tex0_unlink"
@@ -3423,7 +3598,7 @@ class F3DRenderSettingsPanel(bpy.types.Panel):
gameSettingsBox.prop(renderSettings, "useObjectRenderPreview", text="Use Scene for Preview")
gameSettingsBox.prop(renderSettings, "ootSceneObject")
if renderSettings.ootSceneObject is not None:
b = gameSettingsBox.column()
r = b.row().split(factor=0.4)
@@ -3434,21 +3609,22 @@ class F3DRenderSettingsPanel(bpy.types.Panel):
False,
)
if header is None:
r.label(text = "Header does not exist.", icon="QUESTION")
r.label(text="Header does not exist.", icon="QUESTION")
else:
numLightsNeeded = 1
if header.skyboxLighting == "Custom":
r2 = b.row()
r2.prop(renderSettings, "ootForceTimeOfDay")
if renderSettings.ootForceTimeOfDay:
r2.label(text = "Light Index sets first of four lights.", icon="INFO")
r2.label(text="Light Index sets first of four lights.", icon="INFO")
numLightsNeeded = 4
if header.skyboxLighting != "0x00":
r.prop(renderSettings, "ootLightIdx")
if renderSettings.ootLightIdx + numLightsNeeded > len(header.lightList):
b.label(text = "Light does not exist.", icon="QUESTION")
b.label(text="Light does not exist.", icon="QUESTION")
if header.skyboxLighting == "0x00" or (
header.skyboxLighting == "Custom" and renderSettings.ootForceTimeOfDay):
header.skyboxLighting == "Custom" and renderSettings.ootForceTimeOfDay
):
r.prop(renderSettings, "ootTime")
case _:
pass
+270 -157
View File
@@ -1,4 +1,5 @@
import bmesh, bpy, mathutils, pprint, re, math, traceback
from typing import Union
import bmesh, bpy, mathutils, re, math, traceback
from bpy.utils import register_class, unregister_class
from .f3d_gbi import *
from .f3d_material import (
@@ -6,10 +7,15 @@ from .f3d_material import (
update_preset_manual,
all_combiner_uses,
ootEnumDrawLayers,
TextureProperty,
F3DMaterialProperty,
update_node_values_of_material,
F3DMaterialHash,
)
from .f3d_writer import BufferVertex
from .f3d_writer import BufferVertex, F3DVert
from ..utility import *
import ast, operator
from .f3d_material_helpers import F3DMaterial_UpdateLock
colorCombinationCommands = [
0x03, # load lighting data
@@ -397,7 +403,7 @@ def math_eval(s, f3d):
def bytesToNormal(normal):
return [int.from_bytes([value], "big", signed=True) / 128 if value > 0 else value / 128 for value in normal]
return [int.from_bytes([round(value)], "big", signed=True) / 128 if value > 0 else value / 128 for value in normal]
def getTileFormat(value, f3d):
@@ -435,13 +441,19 @@ def renderModeMask(rendermode, cycle, blendOnly):
def convertF3DUV(value, maxSize):
try:
valueBytes = int.to_bytes(value, 2, "big", signed=True)
valueBytes = int.to_bytes(round(value), 2, "big", signed=True)
except OverflowError:
valueBytes = int.to_bytes(value, 2, "big", signed=False)
valueBytes = int.to_bytes(round(value), 2, "big", signed=False)
return ((int.from_bytes(valueBytes, "big", signed=True) / 32) + 0.5) / (maxSize if maxSize > 0 else 1)
class F3DTextureReference:
def __init__(self, name, width):
self.name = name
self.width = width
class F3DParsedCommands:
def __init__(self, name, commands, index):
self.name = name
@@ -453,27 +465,34 @@ class F3DParsedCommands:
class F3DContext:
def __init__(self, f3d, basePath, materialContext):
self.f3d = f3d
self.vertexBuffer = [None] * f3d.vert_load_size
self.basePath = basePath
self.materialContext = materialContext
def __init__(self, f3d: F3D, basePath: str, materialContext: bpy.types.Material):
self.f3d: F3D = f3d
self.basePath: str = basePath
self.materialContext: bpy.types.Material = materialContext
self.materialContext.f3d_update_flag = True # Don't want visual updates while parsing
# If this is not disabled, then tex_scale will auto-update on manual node update.
self.materialContext.f3d_mat.scale_autoprop = False
self.initContext()
# This is separate as we want to call __init__ in clearGeometry, but don't want same behaviour for child classes
def initContext(self):
self.vertexBuffer: list[None | BufferVertex] = [None] * self.f3d.vert_load_size
self.clearMaterial()
mat = self.mat()
mat: F3DMaterialProperty = self.mat()
mat.set_combiner = False
self.materials = [] # saved materials
self.triMatIndices = [] # material indices per triangle
self.materialChanged = True
self.lastMaterialIndex = None
self.materials: list[bpy.types.Material] = [] # current material list
self.materialDict: dict[F3DMaterialHash, bpy.types.Material] = {} # cached materials for all imports
self.triMatIndices: list[int] = [] # material indices per triangle
self.materialChanged: bool = True
self.lastMaterialIndex: bool = None
self.vertexData = {} # c name : parsed data
self.textureData = {} # c name : blender texture
self.vertexData: dict[str, list[F3DVert]] = {} # c name : parsed data
self.textureData: dict[str, bpy.types.Image] = {} # c name : blender texture
self.tlutAppliedTextures = [] # c name
self.currentTextureName = None
self.imagesDontApplyTlut = set() # image
self.tlutAppliedTextures: str = [] # c name
self.currentTextureName: str | None = None
self.imagesDontApplyTlut: set[bpy.types.Image] = set() # image
# Determines if images in CI formats loaded from png files,
# should have the TLUT set by the dlist applied on top of them (False),
@@ -481,29 +500,31 @@ class F3DContext:
# OoT64 and SM64 stores CI images as pngs in actual colors (with the TLUT accounted for),
# So for now this can be always True.
# In the future this could be an option if for example pngs for CI images were grayscale to represent the palette index.
self.ciImageFilesStoredAsFullColor = True # determines whether to apply tlut to file or import as is
self.ciImageFilesStoredAsFullColor: bool = True # determines whether to apply tlut to file or import as is
# This macro has all the tile setting properties, so we reuse it
self.tileSettings = [
self.tileSettings: list[DPSetTile] = [
DPSetTile("G_IM_FMT_RGBA", "G_IM_SIZ_16b", 5, 0, i, 0, [False, False], 0, 0, [False, False], 0, 0)
for i in range(8)
]
self.tileSizes = [DPSetTileSize(i, 0, 0, 32, 32) for i in range(8)]
self.tileSizes: list[DPSetTileSize] = [DPSetTileSize(i, 0, 0, 32, 32) for i in range(8)]
# When a tile is loaded, store dict of tmem : texture
self.tmemDict = {}
# When a tile is loaded, store dict of tmem : texture name
self.tmemDict: dict[int, str] = {}
# This should be modified before parsing f3d
self.matrixData = {} # bone name : matrix
self.currentTransformName = None
self.limbToBoneName = {} # limb name (c variable) : bone name (blender vertex group)
self.matrixData: dict[str, mathutils.Matrix] = {} # bone name : matrix
self.currentTransformName: str | None = None
self.limbToBoneName: dict[str, str] = {} # limb name (c variable) : bone name (blender vertex group)
# data for Mesh.from_pydata, list of BufferVertex tuples
# use BufferVertex to also form uvs / normals / colors
self.verts = []
self.limbGroups = {} # dict of groupName : vertex indices
self.verts: list[F3DVert] = []
self.limbGroups: dict[str : list[int]] = {} # dict of groupName : vertex indices
self.lights = Lights("lights_context")
self.lights: Lights = Lights("lights_context")
# Here these are ints, but when parsing the values will be normalized.
self.lights.l = [
Light([0, 0, 0], [0x28, 0x28, 0x28]),
Light([0, 0, 0], [0x28, 0x28, 0x28]),
@@ -514,10 +535,29 @@ class F3DContext:
Light([0, 0, 0], [0x28, 0x28, 0x28]),
]
self.lights.a = Ambient([0, 0, 0])
self.numLights = 0
self.lightData = {} # (color, normal) : list of blender light objects
self.numLights: int = 0
self.lightData: dict[Light, bpy.types.Object] = {} # Light : blender light object
"""
Restarts context, but keeps cached materials/textures.
Warning: calls initContext, make sure to save/restore preserved fields
"""
def clearGeometry(self):
savedMaterialDict = self.materialDict
savedTextureData = self.textureData
savedTlutAppliedTextures = self.tlutAppliedTextures
savedImagesDontApplyTlut = self.imagesDontApplyTlut
savedLightData = self.lightData
self.initContext()
self.materialDict = savedMaterialDict
self.textureData = savedTextureData
self.tlutAppliedTextures = savedTlutAppliedTextures
self.imagesDontApplyTlut = savedImagesDontApplyTlut
self.lightData = savedLightData
# MAKE SURE TO CALL THIS BETWEEN parseF3D() CALLS
def clearMaterial(self):
mat = self.mat()
@@ -529,15 +569,14 @@ class F3DContext:
mat.set_key = False
mat.set_k0_5 = False
mat.prim_color = [1, 1, 1, 1]
mat.env_color = [1, 1, 1, 1]
mat.blend_color = [1, 1, 1, 1]
for i in range(1, 8):
setattr(mat, "f3d_light" + str(i), None)
mat.tex0.tex = None
mat.tex1.tex = None
mat.tex0.tex_set = False
mat.tex1.tex_set = False
mat.tex0.autoprop = False
mat.tex1.autoprop = False
mat.tex0.tex_format = "RGBA16"
mat.tex1.tex_format = "RGBA16"
@@ -566,7 +605,7 @@ class F3DContext:
mat.presetName = "Custom"
def mat(self):
def mat(self) -> F3DMaterialProperty:
return self.materialContext.f3d_mat
def vertexFormatPatterns(self, data):
@@ -592,7 +631,7 @@ class F3DContext:
def setCurrentTransform(self, name):
self.currentTransformName = name
def getTransformedVertex(self, index):
def getTransformedVertex(self, index: int):
bufferVert = self.vertexBuffer[index]
# NOTE: The groupIndex here does NOT correspond to a vertex group, but to the name of the limb (c variable)
@@ -605,30 +644,33 @@ class F3DContext:
mat = self.mat()
f3dVert = bufferVert.f3dVert
position = transform @ mathutils.Vector(f3dVert[0])
position = transform @ mathutils.Vector(f3dVert.position)
if mat.tex0.tex is not None:
uv = [convertF3DUV(f3dVert[1][i], mat.tex0.tex.size[i]) for i in range(2)]
# uv = [1 - (f3dVert[1][i] / (self.materialContext.f3d_mat.tex0.tex.size[i] * 32)) for i in range(2)]
# uv = [((f3dVert[1][i] / 32) + 0.5) / mat.tex0.tex.size[i] for i in range(2)]
texDimensions = mat.tex0.tex.size
elif mat.tex0.use_tex_reference:
texDimensions = mat.tex0.tex_reference_size
elif mat.tex1.tex is not None:
texDimensions = mat.tex1.tex.size
elif mat.tex1.use_tex_reference:
texDimensions = mat.tex1.tex_reference_size
else:
uv = [convertF3DUV(f3dVert[1][i], 32) for i in range(2)]
# uv = [1 - (f3dVert[1][i] / (32 * 32)) for i in range(2)]
# uv = [((f3dVert[1][i] / 32) + 0.5) / 32 for i in range(2)]
texDimensions = [32, 32]
uv = [convertF3DUV(f3dVert.uv[i], texDimensions[i]) for i in range(2)]
uv[1] = 1 - uv[1]
color = [
value / 256
if value > 0
else int.from_bytes(value.to_bytes(1, "big", signed=True), "big", signed=False) / 256
for value in f3dVert[2]
else int.from_bytes(round(value).to_bytes(1, "big", signed=True), "big", signed=False) / 256
for value in f3dVert.getColorOrNormal()
]
normal = bytesToNormal(f3dVert[2][:3]) + [0]
normal = bytesToNormal(f3dVert.getColorOrNormal()[:3]) + [0]
normal = (transform.inverted().transposed() @ mathutils.Vector(normal)).normalized()[:3]
# This is not usual format of f3dVert, but we need separate color/normal data.
# NOTE: The groupIndex here does NOT correspond to a vertex group, but to the name of the limb (c variable)
return BufferVertex([position, uv, color, normal], bufferVert.groupIndex, bufferVert.materialIndex)
return BufferVertex(F3DVert(position, uv, color, normal), bufferVert.groupIndex, bufferVert.materialIndex)
def addVertices(self, num, start, vertexDataName, vertexDataOffset):
vertexData = self.vertexData[vertexDataName]
@@ -640,7 +682,7 @@ class F3DContext:
if start + count > len(self.vertexBuffer):
raise PluginError(
"Vertex buffer of size "
+ len(self.vertexBuffer)
+ str(len(self.vertexBuffer))
+ " too small, attempting load into "
+ str(start)
+ ", "
@@ -659,14 +701,14 @@ class F3DContext:
if tileSettings.tmem in self.tmemDict:
textureName = self.tmemDict[tileSettings.tmem]
self.loadTexture(dlData, textureName, region, tileSettings, False)
self.applyTileToMaterial(0, tileSettings, tileSizeSettings)
self.applyTileToMaterial(0, tileSettings, tileSizeSettings, dlData)
tileSettings = self.tileSettings[1]
tileSizeSettings = self.tileSizes[1]
if tileSettings.tmem in self.tmemDict:
textureName = self.tmemDict[tileSettings.tmem]
self.loadTexture(dlData, textureName, region, tileSettings, False)
self.applyTileToMaterial(1, tileSettings, tileSizeSettings)
self.applyTileToMaterial(1, tileSettings, tileSizeSettings, dlData)
self.applyLights()
@@ -690,16 +732,20 @@ class F3DContext:
for i in range(int(len(indices) / 3)):
self.triMatIndices.append(self.lastMaterialIndex)
def getMaterialIndex(self):
# We do this for now to update tile settings for S and T.
# Right now we don't handle those, so we need the auto-calculator to set it correctly.
overrideContext = bpy.context.copy()
overrideContext["material"] = self.materialContext
bpy.ops.material.update_f3d_nodes(overrideContext)
# Add items to this tuple in child classes
def getMaterialKey(self, material: bpy.types.Material):
return material.f3d_mat.key()
for material in self.materials:
if propertyGroupEquals(self.materialContext.f3d_mat, material.f3d_mat):
def getMaterialIndex(self):
key = self.getMaterialKey(self.materialContext)
if key in self.materialDict:
material = self.materialDict[key]
if material in self.materials:
return self.materials.index(material)
else:
self.materials.append(material)
return len(self.materials) - 1
self.addMaterial()
return len(self.materials) - 1
@@ -710,25 +756,31 @@ class F3DContext:
return name
return None
# override this to handle applying tlut to other texture
# ex. in oot, apply to flipbook textures
def handleApplyTLUT(
self,
material: bpy.types.Material,
texProp: TextureProperty,
tlut: bpy.types.Image,
index: int,
):
self.applyTLUT(texProp.tex, tlut)
self.tlutAppliedTextures.append(texProp.tex)
# we only want to apply tlut to an existing image under specific conditions.
# however we always want to record the changing tlut for texture references.
def applyTLUTToIndex(self, index):
mat = self.mat()
texProp = getattr(mat, "tex" + str(index))
combinerUses = all_combiner_uses(mat)
if (
combinerUses["Texture " + str(index)]
and (texProp.tex is not None or texProp.use_tex_reference)
and texProp.tex_set
and texProp.tex_format[:2] == "CI"
and (texProp.tex not in self.tlutAppliedTextures or texProp.use_tex_reference)
and (
texProp.tex not in self.imagesDontApplyTlut or not self.ciImageFilesStoredAsFullColor
) # oot currently stores CI textures in full color pngs
):
if texProp.tex_format[:2] == "CI":
# Only handles TLUT at 256
tlutName = self.tmemDict[256]
if 256 in self.tmemDict and tlutName is not None:
tlut = self.textureData[tlutName]
# print(f"TLUT: {tlutName}, {isinstance(tlut, F3DTextureReference)}")
if isinstance(tlut, F3DTextureReference) or texProp.use_tex_reference:
if not texProp.use_tex_reference:
texProp.use_tex_reference = True
@@ -745,9 +797,19 @@ class F3DContext:
texProp.pal_reference = tlutName
texProp.pal_reference_size = min(tlut.size[0] * tlut.size[1], 256)
else:
self.applyTLUT(texProp.tex, tlut)
self.tlutAppliedTextures.append(texProp.tex)
if (
not isinstance(tlut, F3DTextureReference)
and combinerUses["Texture " + str(index)]
and (texProp.tex is not None)
and texProp.tex_set
and (texProp.tex not in self.tlutAppliedTextures or texProp.use_tex_reference)
and (
texProp.tex not in self.imagesDontApplyTlut or not self.ciImageFilesStoredAsFullColor
) # oot currently stores CI textures in full color pngs
):
# print(f"Apply tlut {tlutName} ({str(tlut)}) to {self.getImageName(texProp.tex)}")
# print(f"Size: {str(tlut.size[0])} x {str(tlut.size[1])}, Data: {str(len(tlut.pixels))}")
self.handleApplyTLUT(self.materialContext, texProp, tlut, index)
else:
print("Ignoring TLUT.")
@@ -760,11 +822,17 @@ class F3DContext:
self.applyTLUTToIndex(0)
self.applyTLUTToIndex(1)
material = self.materialContext.copy()
overrideContext = bpy.context.copy()
overrideContext["material"] = material
bpy.ops.material.update_f3d_nodes(overrideContext)
materialCopy = self.materialContext.copy()
# disable flag so that we can lock it, then unlock after update
materialCopy.f3d_update_flag = False
with F3DMaterial_UpdateLock(materialCopy) as material:
update_node_values_of_material(material, bpy.context)
material.f3d_mat.presetName = "Custom"
self.materials.append(material)
self.materialDict[self.getMaterialKey(materialCopy)] = materialCopy
self.materialChanged = False
self.postMaterialChanged()
@@ -1065,9 +1133,8 @@ class F3DContext:
else int(lightCountString[-1:])
)
def getLightObj(self, light):
lightKey = (tuple(light.color), tuple(light.normal))
if lightKey not in self.lightData:
def getLightObj(self, light: Light):
if light not in self.lightData:
lightName = "Light"
bLight = bpy.data.lights.new(lightName, "SUN")
lightObj = bpy.data.objects.new(lightName, bLight)
@@ -1082,15 +1149,15 @@ class F3DContext:
bLight.color = light.color
bpy.context.scene.collection.objects.link(lightObj)
self.lightData[lightKey] = lightObj
return self.lightData[lightKey]
self.lightData[light] = lightObj
return self.lightData[light]
def applyLights(self):
mat = self.mat()
allCombinerUses = all_combiner_uses(mat)
if allCombinerUses["Shade"] and mat.rdp_settings.g_lighting and mat.set_lights:
mat.use_default_lighting = False
mat.ambient_light_color = self.lights.a.color + ([1] if len(self.lights.a.color) == 3 else [])
mat.ambient_light_color = tuple(self.lights.a.color[:]) + ((1,) if len(self.lights.a.color) == 3 else ())
for i in range(self.numLights):
lightObj = self.getLightObj(self.lights.l[i])
@@ -1100,7 +1167,9 @@ class F3DContext:
self.mat().set_lights = True
lightIndex = self.getLightIndex(command.params[0])
colorData = math_eval(command.params[1], self.f3d)
color = [((colorData >> 24) & 0xFF) / 0xFF, ((colorData >> 16) & 0xFF) / 0xFF, ((colorData >> 8) & 0xFF) / 0xFF]
color = mathutils.Vector(
[((colorData >> 24) & 0xFF) / 0xFF, ((colorData >> 16) & 0xFF) / 0xFF, ((colorData >> 8) & 0xFF) / 0xFF]
)
if lightIndex != self.numLights + 1:
self.lights.l[lightIndex - 1].color = color
@@ -1156,17 +1225,17 @@ class F3DContext:
def createLights(self, data, lightsName):
numLights, lightValues = parseLightsData(data, lightsName, self)
ambientColor = gammaInverse([value / 255 for value in lightValues[0:3]])
ambientColor = mathutils.Vector(gammaInverse([value / 255 for value in lightValues[0:3]]))
lightList = []
for i in range(numLights):
color = gammaInverse([value / 255 for value in lightValues[3 + 6 * i : 3 + 6 * i + 3]])
direction = bytesToNormal(lightValues[3 + 6 * i + 3 : 3 + 6 * i + 6])
color = mathutils.Vector(gammaInverse([value / 255 for value in lightValues[3 + 6 * i : 3 + 6 * i + 3]]))
direction = mathutils.Vector(bytesToNormal(lightValues[3 + 6 * i + 3 : 3 + 6 * i + 6]))
lightList.append(Light(color, direction))
while len(lightList) < 7:
lightList.append(Light([0, 0, 0], [0x28, 0x28, 0x28]))
lightList.append(Light(mathutils.Vector([0, 0, 0]), mathutils.Vector([0x28, 0x28, 0x28])))
# normally a and l are Ambient and Light objects,
# but here they will be a color and blender light object array.
@@ -1298,7 +1367,32 @@ class F3DContext:
self.setTile([0, 0, 0, 256, "G_TX_LOADTILE", 0, 0, 0, 0, 0, 0, 0], dlData)
self.loadTLUT(["G_TX_LOADTILE", count], dlData)
def applyTileToMaterial(self, index, tileSettings, tileSizeSettings):
# override this in a child context to handle texture references.
# keep material parameter for use by parent.
# ex. In OOT, you can call self.loadTexture() here based on texture arrays.
def handleTextureReference(
self,
name: str,
image: F3DTextureReference,
material: bpy.types.Material,
index: int,
tileSettings: DPSetTile,
data: str,
):
texProp = getattr(material.f3d_mat, "tex" + str(index))
texProp.tex = None
texProp.use_tex_reference = True
texProp.tex_reference = name
size = texProp.tex_reference_size
# add to this by overriding in a parent context, to handle clearing settings related to previous texture references.
def handleTextureValue(self, material: bpy.types.Material, image: bpy.types.Image, index: int):
texProp = getattr(material.f3d_mat, "tex" + str(index))
texProp.tex = image
texProp.use_tex_reference = False
size = texProp.tex.size
def applyTileToMaterial(self, index, tileSettings, tileSizeSettings, dlData: str):
mat = self.mat()
texProp = getattr(mat, "tex" + str(index))
@@ -1306,23 +1400,13 @@ class F3DContext:
name = self.tmemDict[tileSettings.tmem]
image = self.textureData[name]
if isinstance(image, F3DTextureReference):
texProp.tex = None
texProp.use_tex_reference = True
texProp.tex_reference = name
size = texProp.tex_reference_size
self.handleTextureReference(name, image, self.materialContext, index, tileSettings, dlData)
else:
texProp.tex = image
texProp.use_tex_reference = False
size = texProp.tex.size
self.handleTextureValue(self.materialContext, image, index)
texProp.tex_set = True
# TODO: Handle low/high for image files?
if texProp.use_tex_reference:
# texProp.autoprop = False
# texProp.S.low = round(tileSizeSettings.uls / (2 ** self.f3d.G_TEXTURE_IMAGE_FRAC), 3)
# texProp.T.low = round(tileSizeSettings.ult / (2 ** self.f3d.G_TEXTURE_IMAGE_FRAC), 3)
# texProp.S.high = round(tileSizeSettings.lrs / (2 ** self.f3d.G_TEXTURE_IMAGE_FRAC), 3)
# texProp.T.high = round(tileSizeSettings.lrt / (2 ** self.f3d.G_TEXTURE_IMAGE_FRAC), 3)
# WARNING: Inferring texture size from tile size.
texProp.tex_reference_size = [
@@ -1330,30 +1414,25 @@ class F3DContext:
int(round(tileSizeSettings.lrt / (2**self.f3d.G_TEXTURE_IMAGE_FRAC) + 1)),
]
# if texProp.S.low == 0 and texProp.T.low == 0 and \
# texProp.S.high == size[0] - 1 and \
# (texProp.use_tex_reference or texProp.T.high == size[1] - 1):
# texProp.autoProp = True
# else:
# print(str(texProp.S.low) + " " + str(texProp.T.low) + " " + str(texProp.S.high) + " " + str(texProp.T.high))
# print(str(size[0]-1) + " " + str(size[1]-1))
texProp.tex_format = tileSettings.fmt[8:].replace("_", "") + tileSettings.siz[8:-1].replace("_", "")
texProp.S.clamp = tileSettings.cms[0]
texProp.S.mirror = tileSettings.cms[1]
texProp.S.mask = tileSettings.masks
texProp.S.shift = tileSettings.shifts
texProp.T.clamp = tileSettings.cmt[0]
texProp.T.mirror = tileSettings.cmt[1]
texProp.T.mask = tileSettings.maskt
texProp.T.shift = tileSettings.shiftt
# TODO: Handle S and T properties
# Override this to handle game specific references.
def handleTextureName(self, textureName):
return textureName
texProp.S.low = round(tileSizeSettings.uls / (2**self.f3d.G_TEXTURE_IMAGE_FRAC), 3)
texProp.T.low = round(tileSizeSettings.ult / (2**self.f3d.G_TEXTURE_IMAGE_FRAC), 3)
texProp.S.high = round(tileSizeSettings.lrs / (2**self.f3d.G_TEXTURE_IMAGE_FRAC), 3)
texProp.T.high = round(tileSizeSettings.lrt / (2**self.f3d.G_TEXTURE_IMAGE_FRAC), 3)
def loadTexture(self, data, name, region, tileSettings, isLUT):
textureName = self.handleTextureName(name)
textureName = name
if textureName in self.textureData:
return self.textureData[textureName]
@@ -1383,21 +1462,26 @@ class F3DContext:
def loadTLUT(self, params, dlData):
tileSettings = self.getTileSettings(params[0])
name = self.currentTextureName
textureName = self.handleTextureName(name)
textureName = name
self.tmemDict[tileSettings.tmem] = textureName
tlut = self.loadTexture(dlData, textureName, [0, 0, 16, 16], tileSettings, True)
self.materialChanged = True
def applyTLUT(self, image, tlut):
invalidIndicesDetected = False
for i in range(int(len(image.pixels) / 4)):
lutIndex = int(round(image.pixels[4 * i] * 255))
newValues = tlut.pixels[4 * lutIndex : 4 * (lutIndex + 1)]
if len(newValues) < 4:
print("Invalid lutIndex " + str(lutIndex))
# print("Invalid LUT Index " + str(lutIndex))
invalidIndicesDetected = True
else:
image.pixels[4 * i : 4 * (i + 1)] = newValues
if invalidIndicesDetected:
print("Invalid LUT Indices detected.")
def processCommands(self, dlData, dlName, dlCommands):
callStack = [F3DParsedCommands(dlName, dlCommands, 0)]
while len(callStack) > 0:
@@ -1462,11 +1546,11 @@ class F3DContext:
mat.fog_position = [math_eval(command.params[0], self.f3d), math_eval(command.params[1], self.f3d)]
mat.set_fog = True
elif command.name == "gsSPTexture" or command.name == "gsSPTextureL":
# scale_autoprop should always be false (set in init)
# This prevents issues with material caching where updating nodes on a material causes its key to change
if command.params[0] == 0xFFFF and command.params[1] == 0xFFFF:
mat.scale_autoprop = True
mat.tex_scale = (1, 1)
else:
mat.scale_autoprop = False
mat.tex_scale = [
math_eval(command.params[0], self.f3d) / (2**16),
math_eval(command.params[1], self.f3d) / (2**16),
@@ -1641,14 +1725,21 @@ class F3DContext:
def processDLName(self, name):
return name
def createMesh(self, obj, removeDoubles, importNormals):
def deleteMaterialContext(self):
if self.materialContext is not None:
bpy.data.materials.remove(self.materialContext)
else:
raise PluginError("Attempting to delete material context that is None.")
# if deleteMaterialContext is False, then manually call self.deleteMaterialContext() later.
def createMesh(self, obj, removeDoubles, importNormals, callDeleteMaterialContext: bool):
mesh = obj.data
if len(self.verts) % 3 != 0:
print(len(self.verts))
raise PluginError("Number of verts in mesh not divisible by 3, currently " + str(len(self.verts)))
triangleCount = int(len(self.verts) / 3)
verts = [f3dVert[0] for f3dVert in self.verts]
verts = [f3dVert.position for f3dVert in self.verts]
faces = [[3 * i + j for j in range(3)] for i in range(triangleCount)]
print("Vertices: " + str(len(self.verts)) + ", Triangles: " + str(triangleCount))
@@ -1661,7 +1752,7 @@ class F3DContext:
if importNormals:
mesh.use_auto_smooth = True
mesh.normals_split_custom_set([f3dVert[3] for f3dVert in self.verts])
mesh.normals_split_custom_set([f3dVert.normal for f3dVert in self.verts])
for groupName, indices in self.limbGroups.items():
group = obj.vertex_groups.new(name=self.limbToBoneName[groupName])
@@ -1673,11 +1764,11 @@ class F3DContext:
for i in range(len(mesh.loops)):
# This should be okay, since we aren't trying to optimize vertices
# There will be one loop for every vertex
uv_layer[i].uv = self.verts[i][1]
uv_layer[i].uv = self.verts[i].uv
# if self.materialContext.f3d_mat.rdp_settings.g_lighting:
color_layer[i].color = self.verts[i][2]
alpha_layer[i].color = [self.verts[i][2][3]] * 3 + [1]
color_layer[i].color = self.verts[i].color
alpha_layer[i].color = [self.verts[i].color[3]] * 3 + [1]
if bpy.context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
@@ -1695,8 +1786,6 @@ class F3DContext:
bpy.ops.mesh.remove_doubles()
bpy.ops.object.mode_set(mode="OBJECT")
bpy.data.materials.remove(self.materialContext)
obj.location = bpy.context.scene.cursor.location
i = 0
@@ -1704,6 +1793,11 @@ class F3DContext:
lightObj.location = bpy.context.scene.cursor.location + mathutils.Vector((i, 0, 0))
i += 1
self.clearGeometry()
if callDeleteMaterialContext:
self.deleteMaterialContext()
class ParsedMacro:
def __init__(self, name, params):
@@ -1718,7 +1812,17 @@ class ParsedMacro:
# we distinguish these because there is no guarantee of bone order in blender,
# so we usually rely on alphabetical naming.
# This means changing the c variable names.
def parseF3D(dlData, dlName, obj, transformMatrix, limbName, boneName, drawLayerPropName, drawLayer, f3dContext):
def parseF3D(
dlData: str,
dlName: str,
transformMatrix: mathutils.Matrix,
limbName: str,
boneName: str,
drawLayerPropName: str,
drawLayer: str,
f3dContext: F3DContext,
callClearMaterial: bool,
):
f3dContext.matrixData[limbName] = transformMatrix
f3dContext.setCurrentTransform(limbName)
@@ -1728,8 +1832,13 @@ def parseF3D(dlData, dlName, obj, transformMatrix, limbName, boneName, drawLayer
# vertexGroup = getOrMakeVertexGroup(obj, boneName)
# groupIndex = vertexGroup.index
dlCommands = parseDLData(dlData, dlName)
f3dContext.processCommands(dlData, dlName, dlCommands)
processedDLName = f3dContext.processDLName(dlName)
if processedDLName is not None:
dlCommands = parseDLData(dlData, processedDLName)
f3dContext.processCommands(dlData, processedDLName, dlCommands)
if callClearMaterial:
f3dContext.clearMaterial()
def parseDLData(dlData, dlName):
@@ -1761,7 +1870,7 @@ def getVertexDataStart(vertexDataParam, f3d):
return matchResult.group(1), offset
def parseVertexData(dlData, vertexDataName, f3dContext):
def parseVertexData(dlData: str, vertexDataName: str, f3dContext: F3DContext):
if vertexDataName in f3dContext.vertexData:
return f3dContext.vertexData[vertexDataName]
@@ -1781,16 +1890,26 @@ def parseVertexData(dlData, vertexDataName, f3dContext):
patterns = f3dContext.vertexFormatPatterns(data)
vertexData = []
for pattern in patterns:
# Note that color is None here, as we are just parsing vertex data.
# The same values should be used for color and normal, but getColorOrNormal() will be used later
# which returns whichever value is not None between the two.
# When loaded into the vertex buffer and transformed, the actual normal/color will be calculated.
vertexData = [
(
[math_eval(match.group(1), f3d), math_eval(match.group(2), f3d), math_eval(match.group(3), f3d)],
[math_eval(match.group(4), f3d), math_eval(match.group(5), f3d)],
[
math_eval(match.group(6), f3d),
math_eval(match.group(7), f3d),
math_eval(match.group(8), f3d),
math_eval(match.group(9), f3d),
],
F3DVert(
mathutils.Vector(
[math_eval(match.group(1), f3d), math_eval(match.group(2), f3d), math_eval(match.group(3), f3d)]
),
mathutils.Vector([math_eval(match.group(4), f3d), math_eval(match.group(5), f3d)]),
None,
mathutils.Vector(
[
math_eval(match.group(6), f3d),
math_eval(match.group(7), f3d),
math_eval(match.group(8), f3d),
math_eval(match.group(9), f3d),
]
),
)
for match in re.finditer(pattern, data, re.DOTALL)
]
@@ -1858,16 +1977,10 @@ def CI4toRGBA32(value):
return [value / 255, value / 255, value / 255, 1]
class F3DTextureReference:
def __init__(self, name, width):
self.name = name
self.width = width
def parseTextureData(dlData, textureName, f3dContext, imageFormat, imageSize, width, basePath, isLUT, f3d):
matchResult = re.search(
"([A-Za-z0-9\_]+)\s*" + re.escape(textureName) + "\s*\[\s*[0-9x]*\s*\]\s*=\s*\{([^\}]*)\s\}\s*;\s*",
r"([A-Za-z0-9\_]+)\s*" + re.escape(textureName) + r"\s*\[\s*[0-9a-fA-Fx]*\s*\]\s*=\s*\{([^\}]*)\s*\}\s*;\s*",
dlData,
re.DOTALL,
)
@@ -2038,8 +2151,9 @@ def getImportData(filepaths):
return data
def importMeshC(filepaths, name, scale, removeDoubles, importNormals, drawLayer, f3dContext):
data = getImportData(filepaths)
def importMeshC(
data: str, name: str, scale: float, removeDoubles: bool, importNormals: bool, drawLayer: str, f3dContext: F3DContext
) -> bpy.types.Object:
# Create new skinned mesh
mesh = bpy.data.meshes.new(name + "_mesh")
@@ -2049,12 +2163,11 @@ def importMeshC(filepaths, name, scale, removeDoubles, importNormals, drawLayer,
f3dContext.mat().draw_layer.oot = drawLayer
transformMatrix = mathutils.Matrix.Scale(1 / scale, 4)
parseF3D(data, name, obj, transformMatrix, name, name, "oot", drawLayer, f3dContext)
f3dContext.clearMaterial()
f3dContext.createMesh(obj, removeDoubles, importNormals)
parseF3D(data, name, transformMatrix, name, name, "oot", drawLayer, f3dContext, True)
f3dContext.createMesh(obj, removeDoubles, importNormals, True)
applyRotation([obj], math.radians(-90), "X")
return obj
class F3D_ImportDL(bpy.types.Operator):
@@ -2082,10 +2195,10 @@ class F3D_ImportDL(bpy.types.Operator):
f3dType = context.scene.f3d_type
isHWv1 = context.scene.isHWv1
importPaths = [importPath]
data = getImportData([importPath])
importMeshC(
importPaths,
data,
name,
scaleValue,
removeDoubles,
+190 -64
View File
@@ -1,3 +1,4 @@
from typing import Union
import functools
import bpy, bmesh, mathutils, os, re, copy, math
from math import pi, ceil
@@ -14,6 +15,7 @@ from .f3d_material import (
bitSizeDict,
texBitSizeOf,
texFormatOf,
TextureProperty,
)
from .f3d_gbi import *
from .f3d_gbi import _DPLoadTextureBlock
@@ -818,13 +820,50 @@ def getNewIndices(existingIndices, bufferStart):
return newIndices
class BufferVertex:
def __init__(self, f3dVert, groupIndex, materialIndex):
self.f3dVert = f3dVert
self.groupIndex = groupIndex
self.materialIndex = materialIndex
# Color and normal are separate, since for parsing, the normal must be transformed into
# bone/object space while the color should just be a regular conversion.
class F3DVert:
def __init__(
self,
position: mathutils.Vector,
uv: mathutils.Vector,
color: mathutils.Vector | None, # 4 components
normal: mathutils.Vector | None, # 4 components
):
self.position: mathutils.Vector = position
self.uv: mathutils.Vector = uv
self.color: mathutils.Vector | None = color
self.normal: mathutils.Vector | None = normal
def __eq__(self, other):
if not isinstance(other, F3DVert):
return False
return (
self.position == other.position
and self.uv == other.uv
and self.color == other.color
and self.normal == other.normal
)
def getColorOrNormal(self):
if self.color is None and self.normal is None:
raise PluginError("An F3D vert has neither a color or a normal.")
elif self.color is not None:
return self.color
else:
return self.normal
# groupIndex is either a vertex group (writing), or name of c variable identifying a transform group, like a limb (parsing)
class BufferVertex:
def __init__(self, f3dVert: F3DVert, groupIndex: int | str, materialIndex: int):
self.f3dVert: F3DVert = f3dVert
self.groupIndex: int | str = groupIndex
self.materialIndex: int = materialIndex
def __eq__(self, other):
if not isinstance(other, BufferVertex):
return False
return (
self.f3dVert == other.f3dVert
and self.groupIndex == other.groupIndex
@@ -871,12 +910,12 @@ class TriangleConverter:
def __init__(
self,
triConverterInfo: TriangleConverterInfo,
texDimensions,
texDimensions: tuple[int, int],
material: bpy.types.Material,
currentGroupIndex,
triList,
vtxList,
existingVertexData,
existingVertexData: list[BufferVertex],
existingVertexMaterialRegions,
):
self.triConverterInfo = triConverterInfo
@@ -884,11 +923,9 @@ class TriangleConverter:
self.originalGroupIndex = currentGroupIndex
# Existing data assumed to be already loaded in.
self.vertBuffer: list[BufferVertex] = []
if existingVertexData is not None:
# [(position, uv, colorOrNormal)]
self.vertBuffer = existingVertexData
else:
self.vertBuffer = []
self.vertBuffer: list[BufferVertex] = existingVertexData
self.existingVertexMaterialRegions = existingVertexMaterialRegions
self.bufferStart = len(self.vertBuffer)
self.vertexBufferTriangles = [] # [(index0, index1, index2)]
@@ -916,8 +953,8 @@ class TriangleConverter:
return bufferVert in self.vertBuffer[self.bufferStart :]
def getSortedBuffer(self):
limbVerts = {}
def getSortedBuffer(self) -> dict[int, list[BufferVertex]]:
limbVerts: dict[int, list[BufferVertex]] = {}
for bufferVert in self.vertBuffer[self.bufferStart :]:
if bufferVert.groupIndex not in limbVerts:
limbVerts[bufferVert.groupIndex] = []
@@ -945,9 +982,9 @@ class TriangleConverter:
self.vtxList.vertices.append(
convertVertexData(
self.triConverterInfo.mesh,
bufferVert.f3dVert[0],
bufferVert.f3dVert[1],
bufferVert.f3dVert[2],
bufferVert.f3dVert.position,
bufferVert.f3dVert.uv,
bufferVert.f3dVert.getColorOrNormal(),
self.texDimensions,
self.triConverterInfo.getTransformMatrix(bufferVert.groupIndex),
self.isPointSampled,
@@ -979,9 +1016,9 @@ class TriangleConverter:
self.vtxList.vertices.append(
convertVertexData(
self.triConverterInfo.mesh,
bufferVert.f3dVert[0],
bufferVert.f3dVert[1],
bufferVert.f3dVert[2],
bufferVert.f3dVert.position,
bufferVert.f3dVert.uv,
bufferVert.f3dVert.getColorOrNormal(),
self.texDimensions,
self.triConverterInfo.getTransformMatrix(bufferVert.groupIndex),
self.isPointSampled,
@@ -1046,11 +1083,11 @@ def getF3DVert(loop: bpy.types.MeshLoop, face, convertInfo: LoopConvertInfo, mes
uv[:] = [field if not math.isnan(field) else 0 for field in uv]
uv[1] = 1 - uv[1]
uv = uv.freeze()
colorOrNormal = getLoopColorOrNormal(
color, normal = getLoopColorOrNormal(
loop, face, convertInfo.obj.data, convertInfo.obj, convertInfo.exportVertexColors
)
return (position, uv, colorOrNormal)
return F3DVert(position, uv, color, normal)
def getLoopNormal(loop: bpy.types.MeshLoop, face, mesh, isFlatShaded):
@@ -1198,7 +1235,11 @@ def convertVertexData(
# However, Point samples from the corner.
# Thus we add 0.5 to the UV only if bilinear filtering.
# see section 13.7.5.3 in programming manual.
pixelOffset = (0, 0) if isPointSampled else (0.5 / tex_scale[0], 0.5 / tex_scale[1])
pixelOffset = (
(0, 0)
if (isPointSampled or tex_scale[0] == 0 or tex_scale[1] == 0)
else (0.5 / tex_scale[0], 0.5 / tex_scale[1])
)
uv = [
convertFloatToFixed16(loopUV[0] * texDimensions[0] - pixelOffset[0]),
@@ -1246,16 +1287,18 @@ def getLoopColor(loop: bpy.types.MeshLoop, mesh, mat_ver):
else:
normalizedA = 1
return (normalizedRGB[0], normalizedRGB[1], normalizedRGB[2], normalizedA)
return mathutils.Vector((normalizedRGB[0], normalizedRGB[1], normalizedRGB[2], normalizedA))
def getLoopColorOrNormal(loop: bpy.types.MeshLoop, face, mesh, obj, exportVertexColors):
def getLoopColorOrNormal(
loop: bpy.types.MeshLoop, face, mesh: bpy.types.Mesh, obj: bpy.types.Object, exportVertexColors: bool
) -> tuple[mathutils.Vector, None] | tuple[None, mathutils.Vector]:
material = obj.material_slots[face.material_index].material
isFlatShaded = checkIfFlatShaded(material)
if exportVertexColors:
return getLoopColor(loop, mesh, material.mat_ver)
return getLoopColor(loop, mesh, material.mat_ver), None
else:
return getLoopNormal(loop, face, mesh, isFlatShaded)
return None, getLoopNormal(loop, face, mesh, isFlatShaded)
def createTriangleCommands(triangles, vertexBuffer, useSP2Triangle):
@@ -1593,7 +1636,7 @@ def saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData):
]
)
fModel.onMaterialCommandsBuilt(fMaterial.material, fMaterial.revert, material, drawLayer)
fModel.onMaterialCommandsBuilt(fMaterial, material, drawLayer)
# End Display List
# For dynamic calls, materials will be called as functions and should not end the DL.
@@ -1617,6 +1660,33 @@ def saveOrGetF3DMaterial(material, fModel, obj, drawLayer, convertTextureData):
return fMaterial, texDimensions
def getTextureName(texProp: TextureProperty, fModelName: str, overrideName: str) -> str:
tex = texProp.tex
texFormat = texProp.tex_format
if not texProp.use_tex_reference:
if tex.filepath == "":
name = tex.name
else:
name = tex.filepath
else:
name = texProp.tex_reference
texName = (
fModelName
+ "_"
+ (getNameFromPath(name, True) + "_" + texFormat.lower() if overrideName is None else overrideName)
)
return texName
def getTextureNameTexRef(texProp: TextureProperty, fModelName: str) -> str:
texFormat = texProp.tex_format
name = texProp.tex_reference
texName = fModelName + "_" + (getNameFromPath(name, True) + "_" + texFormat.lower())
return texName
def saveTextureIndex(
propName,
fModel,
@@ -1653,18 +1723,7 @@ def saveTextureIndex(
isCITexture = texFormat[:2] == "CI"
palFormat = texProp.ci_format if isCITexture else ""
if not texProp.use_tex_reference:
if tex.filepath == "":
name = tex.name
else:
name = tex.filepath
else:
name = texProp.tex_reference
texName = (
fModel.name
+ "_"
+ (getNameFromPath(name, True) + "_" + texFormat.lower() if overrideName is None else overrideName)
)
texName = getTextureName(texProp, fModel.name, overrideName)
if tileSettingsOverride is not None:
tileSettings = tileSettingsOverride[index]
@@ -1726,8 +1785,9 @@ def saveTextureIndex(
fImage = FImage(texProp.tex_reference, None, None, width, height, None, False)
fPalette = FImage(texProp.pal_reference, None, None, 1, texProp.pal_reference_size, None, False)
else:
fImage, fPalette = saveOrGetPaletteDefinition(
fMaterial, fModel, tex, texName, texFormat, palFormat, convertTextureData
# fPalette should be an fImage here, since sharedPalette is None
fImage, fPalette, alreadyExists = saveOrGetPaletteAndImageDefinition(
fMaterial, fModel, tex, texName, texFormat, palFormat, convertTextureData, None
)
if loadPalettes:
@@ -1995,19 +2055,79 @@ def savePaletteLoading(loadTexGfx, revertTexGfx, fPalette, palFormat, pal, color
)
def saveOrGetPaletteDefinition(fMaterial, fModelOrTexRect, image, imageName, texFmt, palFmt, convertTextureData):
class FSharedPalette:
def __init__(self, name):
self.name = name
self.palette = []
def saveOrGetPaletteOnlyDefinition(
fMaterial: FMaterial,
fModel: FModel,
image: bpy.types.Image,
imageName: str,
texFmt: str,
palFmt: str,
convertTextureData: bool,
palette: list[int],
) -> FImage:
palFormat = texFormatOf[palFmt]
paletteName = checkDuplicateTextureName(fModel, toAlnum(imageName) + "_pal_" + palFmt.lower())
paletteKey = (image, (palFmt, "PAL"))
paletteFilename = getNameFromPath(imageName, True) + "." + fModel.getTextureSuffixFromFormat(texFmt) + ".pal"
fPalette = FImage(
paletteName,
palFormat,
"G_IM_SIZ_16b",
1,
len(palette),
paletteFilename,
convertTextureData,
)
if fMaterial.useLargeTextures:
fPalette.isLargeTexture = True
if convertTextureData:
for color in palette:
fPalette.data.extend(color.to_bytes(2, "big"))
# print(f"Palette data: {paletteName} - length {len(fPalette.data)}")
fModel.addTexture(paletteKey, fPalette, fMaterial)
return fPalette
def imageAlreadyExists(fModel: FModel, image: bpy.types.Image, texFmt: str, palFmt: str) -> bool:
texFormat = texFormatOf[texFmt]
palFormat = texFormatOf[palFmt]
bitSize = texBitSizeOf[texFmt]
# If image already loaded, return that data.
imageKey = (image, (texFmt, palFmt))
fImage, fPalette = fModel.getTextureAndHandleShared(imageKey)
return fImage is not None
def saveOrGetPaletteAndImageDefinition(
fMaterial, fModelOrTexRect, image, imageName, texFmt, palFmt, convertTextureData, sharedPalette: FSharedPalette
) -> tuple[FImage, FImage, bool]:
texFormat = texFormatOf[texFmt]
palFormat = texFormatOf[palFmt]
bitSize = texBitSizeOf[texFmt]
# If image already loaded, return that data.
paletteName = toAlnum(imageName) + "_pal_" + palFmt.lower()
imageKey = (image, (texFmt, palFmt))
paletteKey = (image, (palFmt, "PAL"))
fImage, fPalette = fModelOrTexRect.getTextureAndHandleShared(imageKey)
if fImage is not None:
return fImage, fPalette
# print(f"Image already exists")
return fImage, fPalette, True
palette = []
# print(f"Size: {str(image.size[0])} x {str(image.size[1])}, Data: {str(len(image.pixels))}")
if sharedPalette is not None:
palette = sharedPalette.palette
else:
palette = []
texture = []
maxColors = 16 if bitSize == "G_IM_SIZ_4b" else 256
if convertTextureData:
@@ -2028,7 +2148,13 @@ def saveOrGetPaletteDefinition(fMaterial, fModelOrTexRect, image, imageName, tex
if pixelColor not in palette:
palette.append(pixelColor)
if len(palette) > maxColors:
raise PluginError("Texture " + imageName + " has more than " + str(maxColors) + " colors.")
raise PluginError(
"Texture "
+ imageName
+ " has more than "
+ str(maxColors)
+ " colors, or is part of a shared palette with too many colors."
)
texture.append(palette.index(pixelColor))
if image.filepath == "":
@@ -2036,7 +2162,7 @@ def saveOrGetPaletteDefinition(fMaterial, fModelOrTexRect, image, imageName, tex
else:
name = image.filepath
filename = getNameFromPath(name, True) + "." + fModelOrTexRect.getTextureSuffixFromFormat(texFmt) + ".inc.c"
paletteFilename = getNameFromPath(name, True) + "." + fModelOrTexRect.getTextureSuffixFromFormat(texFmt) + ".pal"
# paletteFilename = getNameFromPath(name, True) + '.' + \
# fModelOrTexRect.getTextureSuffixFromFormat(palFmt) + '.inc.c'
fImage = FImage(
@@ -2049,37 +2175,37 @@ def saveOrGetPaletteDefinition(fMaterial, fModelOrTexRect, image, imageName, tex
convertTextureData,
)
fPalette = FImage(
checkDuplicateTextureName(fModelOrTexRect, paletteName),
palFormat,
"G_IM_SIZ_16b",
1,
len(palette),
paletteFilename,
convertTextureData,
)
if fMaterial.useLargeTextures:
fImage.isLargeTexture = True
fPalette.isLargeTexture = True
fImage.paletteKey = paletteKey
# paletteImage = bpy.data.images.new(paletteName, 1, len(palette))
# paletteImage.pixels = palette
# paletteImage.filepath = paletteFilename
if convertTextureData:
for color in palette:
fPalette.data.extend(color.to_bytes(2, "big"))
if bitSize == "G_IM_SIZ_4b":
fImage.data = compactNibbleArray(texture, image.size[0], image.size[1])
else:
fImage.data = bytearray(texture)
fModelOrTexRect.addTexture((image, (texFmt, palFmt)), fImage, fMaterial)
fModelOrTexRect.addTexture((image, (palFmt, "PAL")), fPalette, fMaterial)
return fImage, fPalette # , paletteImage
# For shared palettes, paletteName should be the same for the same imageName until
# the next saveOrGetPaletteOnlyDefinition
# Make sure paletteName is read here before saveOrGetPaletteOnlyDefinition is called.
paletteName = checkDuplicateTextureName(fModelOrTexRect, toAlnum(imageName) + "_pal_" + palFmt.lower())
if sharedPalette is None:
fPalette = saveOrGetPaletteOnlyDefinition(
fMaterial, fModelOrTexRect, image, imageName, texFmt, palFmt, convertTextureData, palette
)
paletteKey = (image, (palFmt, "PAL"))
fImage.paletteKey = paletteKey
else:
fPalette = None
fImage.paletteKey = None
return fImage, fPalette, False # , paletteImage
def compactNibbleArray(texture, width, height):
+332
View File
@@ -0,0 +1,332 @@
import bpy, re
from typing import Any, Callable, Optional
from bpy.utils import register_class, unregister_class
from bpy.app.handlers import persistent
from .f3d_material import all_combiner_uses, update_tex_values_manual, iter_tex_nodes, TextureProperty
from ..utility import prop_split, CollectionProperty
from dataclasses import dataclass
@dataclass
class TextureFlipbook:
name: str
exportMode: str
textureNames: list[str]
def flipbook_data_to_c(flipbook: TextureFlipbook):
newArrayData = ""
for textureName in flipbook.textureNames:
newArrayData += textureName + ",\n"
return newArrayData
def flipbook_to_c(flipbook: TextureFlipbook, isStatic: bool):
newArrayData = "void* " if not isStatic else "static void* "
newArrayData += f"{flipbook.name}[]" + " = { "
newArrayData += flipbook_data_to_c(flipbook)
newArrayData += " };"
return newArrayData
def flipbook_2d_to_c(flipbook: TextureFlipbook, isStatic: bool, count: int):
newArrayData = "void* " if not isStatic else "static void* "
newArrayData += f"{flipbook.name}[][{len(flipbook.textureNames)}] = {{ "
newArrayData += ("{ " + flipbook_data_to_c(flipbook) + " },\n") * count
newArrayData += " };"
return newArrayData
def usesFlipbook(
material: bpy.types.Material,
flipbookProperty: Any,
index: int,
checkEnable: bool,
checkFlipbookReference: Optional[Callable[[str], bool]],
) -> bool:
texProp = getattr(material.f3d_mat, f"tex{index}")
if all_combiner_uses(material.f3d_mat)["Texture " + str(index)] and texProp.use_tex_reference:
return (
checkFlipbookReference is not None
and checkFlipbookReference(texProp.tex_reference)
and (not checkEnable or flipbookProperty.enable)
)
else:
return False
class FlipbookImagePointerProperty(bpy.types.PropertyGroup):
image: bpy.props.PointerProperty(type=bpy.types.Image)
name: bpy.props.StringProperty(name="Name", default="gImage")
def drawTextureArray(layout: bpy.types.UILayout, textureArray: CollectionProperty, index: int, exportMode: str):
for i in range(len(textureArray)):
drawTextureArrayProperty(layout, textureArray[i], i, index, exportMode)
addOp = layout.operator(AddFlipbookTexture.bl_idname, text="Add Texture")
addOp.combinerTexIndex = index
addOp.arrayIndex = len(textureArray)
def drawTextureArrayProperty(
layout: bpy.types.UILayout,
texturePointer: FlipbookImagePointerProperty,
arrayIndex: int,
texNum: int,
exportMode: str,
):
col = layout.column()
box = col.box().column()
if exportMode == "Individual":
prop_split(box, texturePointer, "name", "Texture Name")
box.template_ID(texturePointer, "image", new="image.new", open="image.open")
row = box.row()
buttons = row.row(align=True)
visualizeOp = buttons.operator(VisualizeFlipbookTexture.bl_idname, text="Visualize", icon="VIEW_CAMERA")
visualizeOp.arrayIndex = arrayIndex
visualizeOp.combinerTexIndex = texNum
addOp = buttons.operator(AddFlipbookTexture.bl_idname, text="", icon="ADD")
addOp.arrayIndex = arrayIndex + 1
addOp.combinerTexIndex = texNum
removeOp = buttons.operator(RemoveFlipbookTexture.bl_idname, text="", icon="REMOVE")
removeOp.arrayIndex = arrayIndex
removeOp.combinerTexIndex = texNum
moveUp = buttons.operator(MoveFlipbookTexture.bl_idname, text="", icon="TRIA_UP")
moveUp.arrayIndex = arrayIndex
moveUp.offset = -1
moveUp.combinerTexIndex = texNum
moveDown = buttons.operator(MoveFlipbookTexture.bl_idname, text="", icon="TRIA_DOWN")
moveDown.arrayIndex = arrayIndex
moveDown.offset = 1
moveUp.combinerTexIndex = texNum
class AddFlipbookTexture(bpy.types.Operator):
bl_idname = "material.add_flipbook_texture"
bl_label = "Add Flipbook Texture"
bl_options = {"REGISTER", "UNDO"}
arrayIndex: bpy.props.IntProperty()
combinerTexIndex: bpy.props.IntProperty()
def execute(self, context):
material = context.material
flipbook = getattr(material.flipbookGroup, "flipbook" + str(self.combinerTexIndex))
flipbook.textures.add()
flipbook.textures.move(len(flipbook.textures) - 1, self.arrayIndex)
self.report({"INFO"}, "Success!")
return {"FINISHED"}
class RemoveFlipbookTexture(bpy.types.Operator):
bl_idname = "material.remove_flipbook_texture"
bl_label = "Remove Flipbook Texture"
bl_options = {"REGISTER", "UNDO"}
arrayIndex: bpy.props.IntProperty()
combinerTexIndex: bpy.props.IntProperty()
def execute(self, context):
material = context.material
flipbook = getattr(material.flipbookGroup, "flipbook" + str(self.combinerTexIndex))
flipbook.textures.remove(self.arrayIndex)
self.report({"INFO"}, "Success!")
return {"FINISHED"}
class MoveFlipbookTexture(bpy.types.Operator):
bl_idname = "material.move_flipbook_texture"
bl_label = "Move Flipbook Texture"
bl_options = {"REGISTER", "UNDO"}
combinerTexIndex: bpy.props.IntProperty()
arrayIndex: bpy.props.IntProperty()
offset: bpy.props.IntProperty()
def execute(self, context):
material = context.material
flipbook = getattr(material.flipbookGroup, "flipbook" + str(self.combinerTexIndex))
flipbook.textures.move(self.arrayIndex, self.arrayIndex + self.offset)
self.report({"INFO"}, "Success!")
return {"FINISHED"}
class VisualizeFlipbookTexture(bpy.types.Operator):
bl_idname = "material.visualize_flipbook_texture"
bl_label = "Visualize Flipbook Texture"
bl_options = {"REGISTER", "UNDO"}
combinerTexIndex: bpy.props.IntProperty()
arrayIndex: bpy.props.IntProperty()
def execute(self, context):
material = context.material
flipbook = getattr(material.flipbookGroup, "flipbook" + str(self.combinerTexIndex))
texProp = getattr(material.f3d_mat, "tex" + str(self.combinerTexIndex))
setTexNodeImage(context.material, self.combinerTexIndex, self.arrayIndex)
self.report({"INFO"}, "Success!")
return {"FINISHED"}
enumFlipbookExportMode = [
("Array", "Array", "Array"),
("Individual", "Individual", "Individual"),
]
class FlipbookProperty(bpy.types.PropertyGroup):
enable: bpy.props.BoolProperty()
name: bpy.props.StringProperty(default="sFlipbookTextures")
exportMode: bpy.props.EnumProperty(default="Array", items=enumFlipbookExportMode)
textures: bpy.props.CollectionProperty(type=FlipbookImagePointerProperty)
class FlipbookGroupProperty(bpy.types.PropertyGroup):
flipbook0: bpy.props.PointerProperty(type=FlipbookProperty)
flipbook1: bpy.props.PointerProperty(type=FlipbookProperty)
def drawFlipbookProperty(layout: bpy.types.UILayout, flipbookProp: FlipbookProperty, index: int):
box = layout.box().column()
box.prop(flipbookProp, "enable", text="Export Flipbook Textures " + str(index))
if flipbookProp.enable:
prop_split(box, flipbookProp, "exportMode", "Export Mode")
if flipbookProp.exportMode == "Array":
prop_split(box, flipbookProp, "name", "Array Name")
drawTextureArray(box.column(), flipbookProp.textures, index, flipbookProp.exportMode)
def drawFlipbookGroupProperty(
layout: bpy.types.UILayout,
material: bpy.types.Material,
checkFlipbookReference: Callable[[str], bool],
drawFlipbookRequirementMessage: Callable[[bpy.types.UILayout], None],
):
layout.box().column().label(text="Flipbook Properties")
if drawFlipbookRequirementMessage is not None:
drawFlipbookRequirementMessage(layout)
for i in range(2):
flipbook = getattr(material.flipbookGroup, "flipbook" + str(i))
if usesFlipbook(material, flipbook, i, False, checkFlipbookReference):
drawFlipbookProperty(layout.column(), flipbook, i)
if getattr(material.f3d_mat, "tex" + str(i)).tex_format[:2] == "CI":
layout.label(text="New shared CI palette will be generated.", icon="ERROR")
# START GAME SPECIFIC CALLBACKS
def ootFlipbookReferenceIsValid(texReference: str) -> bool:
return re.search(f"0x0([0-9A-F])000000", texReference) is not None
def ootFlipbookRequirementMessage(layout: bpy.types.UILayout):
layout.label(text="To use this, material must use a")
layout.label(text="texture reference with name = 0x0?000000.")
def ootFlipbookAnimUpdate(self, armatureObj: bpy.types.Object, segment: str, index: int):
for child in armatureObj.children:
if not isinstance(child.data, bpy.types.Mesh):
continue
for material in child.data.materials:
for i in range(2):
flipbook = getattr(material.flipbookGroup, "flipbook" + str(i))
texProp = getattr(material.f3d_mat, "tex" + str(i))
if usesFlipbook(material, flipbook, i, True, ootFlipbookReferenceIsValid):
match = re.search(f"0x0([0-9A-F])000000", texProp.tex_reference)
if match is None:
continue
if match.group(1) == segment:
# Remember that index 0 = auto, and keyframed values start at 1
flipbookIndex = min((index - 1 if index > 0 else 0), len(flipbook.textures) - 1)
setTexNodeImage(material, i, flipbookIndex)
# END GAME SPECIFIC CALLBACKS
# we use a handler since update functions are not called when a property is animated.
@persistent
def flipbookAnimHandler(dummy):
if bpy.context.scene.gameEditorMode == "OOT":
for obj in bpy.data.objects:
if isinstance(obj.data, bpy.types.Armature):
# we only want to update texture on keyframed armatures.
# this somewhat mitigates the issue of two skeletons using the same flipbook material.
if obj.animation_data is None or obj.animation_data.action is None:
continue
action = obj.animation_data.action
if not (
action.fcurves.find("ootLinkTextureAnim.eyes") is None
or action.fcurves.find("ootLinkTextureAnim.mouth") is None
):
ootFlipbookAnimUpdate(obj.data, obj, "8", obj.ootLinkTextureAnim.eyes)
ootFlipbookAnimUpdate(obj.data, obj, "9", obj.ootLinkTextureAnim.mouth)
else:
pass
class Flipbook_MaterialPanel(bpy.types.Panel):
bl_label = "Flipbook Material"
bl_idname = "MATERIAL_PT_Flipbook_Material_Inspector"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "material"
bl_options = {"HIDE_HEADER"}
@classmethod
def poll(cls, context):
return context.material is not None and context.scene.gameEditorMode in ["OOT"]
def draw(self, context):
layout = self.layout
mat = context.material
col = layout.column()
if context.scene.gameEditorMode == "OOT":
checkFlipbookReference = ootFlipbookReferenceIsValid
drawFlipbookRequirementMessage = ootFlipbookRequirementMessage
else:
checkFlipbookReference = None
drawFlipbookRequirementMessage = None
drawFlipbookGroupProperty(col.box().column(), mat, checkFlipbookReference, drawFlipbookRequirementMessage)
def setTexNodeImage(material: bpy.types.Material, texIndex: int, flipbookIndex: int):
flipbook = getattr(material.flipbookGroup, "flipbook" + str(texIndex))
for texNode in iter_tex_nodes(material.node_tree, texIndex):
if texNode.image is not flipbook.textures[flipbookIndex].image:
texNode.image = flipbook.textures[flipbookIndex].image
flipbook_classes = [
FlipbookImagePointerProperty,
AddFlipbookTexture,
RemoveFlipbookTexture,
MoveFlipbookTexture,
VisualizeFlipbookTexture,
FlipbookProperty,
FlipbookGroupProperty,
Flipbook_MaterialPanel,
]
def flipbook_register():
for cls in flipbook_classes:
register_class(cls)
bpy.app.handlers.frame_change_pre.append(flipbookAnimHandler)
bpy.types.Material.flipbookGroup = bpy.props.PointerProperty(type=FlipbookGroupProperty)
def flipbook_unregister():
for cls in reversed(flipbook_classes):
unregister_class(cls)
bpy.app.handlers.frame_change_pre.remove(flipbookAnimHandler)
del bpy.types.Material.flipbookGroup
@@ -158,6 +158,7 @@ def convertF3DtoNewVersion(obj: bpy.types.Object | bpy.types.Bone, index, materi
newMat.f3d_mat.draw_layer.sm64 = material.f3d_mat.draw_layer.sm64
copyPropertyGroup(material.ootMaterial, newMat.ootMaterial)
copyPropertyGroup(material.flipbookGroup, newMat.flipbookGroup)
copyPropertyGroup(material.ootCollisionProperty, newMat.ootCollisionProperty)
colSettings = CollisionSettings()
+43 -5
View File
@@ -71,18 +71,56 @@ To import a skeletal mesh, just click "Import" for the armature importer. You ma
![](/images/oot_imported_gerudo_textured.png)
![](/images/oot_imported_gerudo_solid.png)
1. Certain colors are white/different: Some graphical effects are achieved through dynamic Gfx commands, such as tinting white textures. These effects will not be imported.
2. Strange imported normals: Due to the behaviour of rotating vertices on a skinned triangle that differs between Blender and the N64, normals may look strange. Note that these normals will look correct if re exported back into the game (assuming the rest pose is not changed).
1. Eye/face textures are black: Texture pointers which are set dynamically will not be imported. Instead, the name of the pointer will be used instead of the actual data.
2. Certain colors are white/different: Some graphical effects are achieved through dynamic Gfx commands, such as tinting white textures. These effects will not be imported.
3. Strange imported normals: Due to the behaviour of rotating vertices on a skinned triangle that differs between Blender and the N64, normals may look strange. Note that these normals will look correct if re exported back into the game (assuming the rest pose is not changed).
Note that rest pose rotations are zeroed out on export, so you can modify the rest pose of imported armature while still preserving its structure. You can do this by using the "Apply As Rest Pose" operator under the Fast64 tab. Note that imported animations however still require the imported rest pose to work correctly.
Note that rest pose rotations are zeroed out on export, so you can modify the rest pose of imported armature while still preserving its structure. You can do this by using the "Apply As Rest Pose" operator under the Fast64 tab or the OOT Skeleton Exporter section. Note that imported animations however still require the imported rest pose to work correctly.
There may also be an issue where some meshes import completely black due to the assumption that the F3D cycle mode is set to 2-Cycle, when it should really be 1-Cycle. Try changing the cycle type to 1-Cycle in cases where a dynamic texture pointer is not expected.
To import an animation, select the armature the animation belongs to then click "Import" on the animation importer.
To export an animation, select an armature and click "Export", which will export the active animation of the armature.
### Flipbook Textures
Many actors in OOT will animate textures through code using a flipbook method, like with Link's eyes/mouth. A flipbook material will use a texture reference pointing to an address formatted as 0x0?000000. You can find the flipbook texture frames in the material properties tab underneath the dynamic material section.
![](/images/oot_flipbook.png)
On import, Fast64 will try to read the provided actors code for flipbook textures. On export, Fast64 will try to modify texture arrays used for flipbook textures.
For Link, the eyes/mouth materials use flipbook textures. For Link animations you can animate these flipbook indices in the Link Animation Inspector, located in the object properties tab for an armature object. Note that the 0 index is reserved for the "auto" setting, and that flipbook texture indices start at 1.
![](/images/oot_link_texture_anim.png)
### Custom Link Process
1. In the OOT Skeleton Exporter window, go to the Import Skeleton section, select "Mode" and switch it to "Adult Link."
2. Click "Import Skeleton" to import the skeleton from your decomp repo set up in the the "Getting Started" intro.
3. Replace/modify the mesh.
4. For any new materials, make sure to go to material properties -> OOT Dynamic Material Properties -> enable segment C. This handles rendering for Link's reflection.
5. To add your own eye/mouth materials, create a new F3D material, then go to material properties -> F3D Material Inspector -> Sources -> Texture 0 Properties:
- Set "Use Texture Reference".
- Set the texture size to the size of your textures.
- Ignore palette reference/size, those will be auto-generated if using CI textures.
- Set the texture reference to 0x08000000 (eyes) or 0x09000000 (mouth)
6. To add different eye/mouth texture frames, go to the material properties tab, then scroll down to the Flipbook Properties.
7. Once you've modified Link's mesh, go the OOT Skeleton Exporter window, go to the Export Skeleton section, select "Mode" and switch it to "Adult Link".
8. Select Link's armature and then hit "Export Skeleton".
9. If you're not using HackerOOT, make sure to set NON_MATCHING to 1 in the Makefile in the decomp repo.
10. Most of Link's items are combined with his hand mesh. There are plans to simplify the process, but for now these models must be manually replaced using the display list importer/exporter. You'll also have to modify the DL arrays at the start of src/code/z_player_lib.c to include your own DLs if you're appending and not replacing.
11. Common Issues:
- Corrupted mesh: Make sure the root, upper control, and lower control bones are the only bones set to non-deform.
- Incorrect waist DL: Go to src/code/z_player_lib.c and modify sPlayerWaistDLs to include your own waist DL.
### Custom Skeleton Mesh Process
1. Import the character you want to modify.
- Skeleton: The name of the skeleton struct, of type FlexSkeletonHeader or SkeletonHeader. Usually found in the object files.
- Object: The "asset group" the skeleton belongs to. The name will be from "assets/objects/\<name\>/"
- Overlay: The location of the actor code, if necessary. The name will be from "src/overlays/actors/\<name\>/"
2. Put it into a suitable rest pose, then click the "Apply As Rest Pose" button at the bottom of the OOT Skeleton Exporter section to apply it. It helps to import an existing animation to see how a good rest pose would look like.
- Animation Header Name: struct of type AnimationHeader or LinkAnimationHeader, found in the object files.
3. Replace the existing mesh with your own.
4. Export the skeleton back into the game. It is not necessary to re-fold the armature before export.
5. If "Replace Vanilla Headers On Export" is enabled, then any reference conflicts should be removed.
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().
### Creating a Cutscene
**Creating the cutscene itself:**
+2 -3
View File
@@ -64,7 +64,6 @@ class OOT_FileSettingsPanel(OOT_Panel):
col = self.layout.column()
col.scale_y = 1.1 # extra padding, makes it easier to see these main settings
prop_split(col, context.scene, "ootBlenderScale", "OOT Scene Scale")
prop_split(col, context.scene, "ootActorBlenderScale", "OOT Actor Scale")
prop_split(col, context.scene, "ootDecompPath", "Decomp Path")
col.prop(context.scene.fast64.oot, "hackerFeaturesEnabled")
@@ -80,6 +79,8 @@ class OOT_Properties(bpy.types.PropertyGroup):
DLImportSettings: bpy.props.PointerProperty(type=OOTDLImportSettings)
skeletonExportSettings: bpy.props.PointerProperty(type=oot_skeleton.OOTSkeletonExportSettings)
skeletonImportSettings: bpy.props.PointerProperty(type=oot_skeleton.OOTSkeletonImportSettings)
animExportSettings: bpy.props.PointerProperty(type=oot_anim.OOTAnimExportSettingsProperty)
animImportSettings: bpy.props.PointerProperty(type=oot_anim.OOTAnimImportSettingsProperty)
oot_classes = (
@@ -133,7 +134,6 @@ def oot_register(registerPanels):
bpy.types.Scene.ootBlenderScale = bpy.props.FloatProperty(
name="Blender To OOT Scale", default=10, update=on_update_render_settings
)
bpy.types.Scene.ootActorBlenderScale = bpy.props.FloatProperty(name="Blender To OOT Actor Scale", default=1000)
bpy.types.Scene.ootDecompPath = bpy.props.StringProperty(name="Decomp Folder", subtype="FILE_PATH")
@@ -156,5 +156,4 @@ def oot_unregister(unregisterPanels):
oot_panel_unregister()
del bpy.types.Scene.ootBlenderScale
del bpy.types.Scene.ootActorBlenderScale
del bpy.types.Scene.ootDecompPath
+494 -93
View File
@@ -7,11 +7,13 @@ from ..utility import CData, PluginError, toAlnum, writeCData, readFile, hexOrDe
from .oot_utility import (
checkForStartBone,
getStartBone,
getNextBone,
getSortedChildren,
ootGetPath,
addIncludeFiles,
checkEmptyName,
ootGetObjectPath,
getOOTScale,
)
from ..utility_anim import (
@@ -20,8 +22,36 @@ from ..utility_anim import (
saveQuaternionFrame,
squashFramesIfAllSame,
getFrameInterval,
getTranslationRelativeToRest,
getRotationRelativeToRest,
)
from ..f3d.f3d_material import iter_tex_nodes
from ..f3d.flipbook import usesFlipbook, ootFlipbookAnimUpdate
from .oot_model_classes import ootGetIncludedAssetData
from ..f3d.f3d_parser import getImportData
class OOTAnimExportSettingsProperty(bpy.types.PropertyGroup):
isCustom: bpy.props.BoolProperty(name="Use Custom Path")
customPath: bpy.props.StringProperty(name="Folder", subtype="FILE_PATH")
folderName: bpy.props.StringProperty(name="Animation Folder", default="object_geldb")
isLink: bpy.props.BoolProperty(name="Is Link", default=False)
skeletonName: bpy.props.StringProperty(name="Skeleton Name", default="gGerudoRedSkel")
class OOTAnimImportSettingsProperty(bpy.types.PropertyGroup):
isCustom: bpy.props.BoolProperty(name="Use Custom Path")
customPath: bpy.props.StringProperty(name="Folder", subtype="FILE_PATH")
folderName: bpy.props.StringProperty(name="Animation Folder", default="object_geldb")
isLink: bpy.props.BoolProperty(name="Is Link", default=False)
animName: bpy.props.StringProperty(name="Anim Name", default="gGerudoRedSpinAttackAnim")
def convertToUnsignedShort(value: int) -> int:
return int.from_bytes(value.to_bytes(2, "big", signed=(value < 0)), "big", signed=False)
class OOTAnimation:
def __init__(self, name):
@@ -48,7 +78,7 @@ class OOTAnimation:
for value in self.values:
if counter == 0:
data.source += "\t"
data.source += format(value, "#06x") + ", "
data.source += format(convertToUnsignedShort(value), "#06x") + ", "
counter += 1
if counter >= 16: # round number for finding/counting data
counter = 0
@@ -60,7 +90,13 @@ class OOTAnimation:
for index in range(-1, len(self.indices) - 1):
data.source += "\t{ "
for field in range(3):
data.source += format(self.indices[index][field], "#06x") + ", "
data.source += (
format(
convertToUnsignedShort(self.indices[index][field]),
"#06x",
)
+ ", "
)
data.source += "},\n"
data.source += "};\n\n"
@@ -83,6 +119,51 @@ class OOTAnimation:
return data
class OOTLinkAnimation:
def __init__(self, name):
self.headerName = toAlnum(name)
self.frameCount = None
self.data = []
def dataName(self):
return self.headerName + "Data"
def toC(self, isCustomExport: bool):
data = CData()
animHeaderData = CData()
data.source += '#include "ultra64.h"\n#include "global.h"\n\n'
animHeaderData.source += '#include "ultra64.h"\n#include "global.h"\n\n'
# TODO: handle custom import?
if isCustomExport:
animHeaderData.source += f'#include "{self.dataName()}.h"\n'
else:
animHeaderData.source += f'#include "assets/misc/link_animetion/{self.dataName()}.h"\n'
# data
data.header += f"extern s16 {self.dataName()}[];\n"
data.source += f"s16 {self.dataName()}[] = {{\n"
counter = 0
for value in self.data:
if counter == 0:
data.source += "\t"
data.source += format(convertToUnsignedShort(value), "#06x") + ", "
counter += 1
if counter >= 8: # round number for finding/counting data
counter = 0
data.source += "\n"
data.source += "\n};\n\n"
# header
animHeaderData.header += f"extern LinkAnimationHeader {self.headerName};\n"
animHeaderData.source += (
f"LinkAnimationHeader {self.headerName} = {{\n\t{{ {str(self.frameCount)} }}, {self.dataName()} \n}};\n\n"
)
return data, animHeaderData
def ootGetAnimBoneRot(bone, poseBone, convertTransformMatrix, isRoot):
# OoT draws limbs like this:
# limbMatrix = parentLimbMatrix @ limbFixedTranslationMatrix @ animRotMatrix
@@ -147,7 +228,7 @@ def ootGetAnimBoneRot(bone, poseBone, convertTransformMatrix, isRoot):
return finalRotation
def ootConvertAnimationData(anim, armatureObj, convertTransformMatrix, *, frame_start, frame_count):
def ootConvertNonLinkAnimationData(anim, armatureObj, convertTransformMatrix, *, frame_start, frame_count):
checkForStartBone(armatureObj)
bonesToProcess = [getStartBone(armatureObj)]
currentBone = armatureObj.data.bones[bonesToProcess[0]]
@@ -166,7 +247,7 @@ def ootConvertAnimationData(anim, armatureObj, convertTransformMatrix, *, frame_
bonesToProcess = childrenNames + bonesToProcess
# list of boneFrameData, which is [[x frames], [y frames], [z frames]]
# boneIndex is index in animBones in ootConvertAnimationData.
# boneIndex is index in animBones.
# since we are processing the bones in the same order as ootProcessBone,
# they should be the same as the limb indices.
@@ -213,7 +294,67 @@ def ootConvertAnimationData(anim, armatureObj, convertTransformMatrix, *, frame_
return armatureFrameData
def ootExportAnimationCommon(armatureObj, convertTransformMatrix, skeletonName):
def ootConvertLinkAnimationData(anim, armatureObj, convertTransformMatrix, *, frame_start, frame_count):
checkForStartBone(armatureObj)
bonesToProcess = [getStartBone(armatureObj)]
currentBone = armatureObj.data.bones[bonesToProcess[0]]
animBones = []
# Get animation bones in order
# must be SAME order as ootProcessBone
while len(bonesToProcess) > 0:
boneName = bonesToProcess[0]
currentBone = armatureObj.data.bones[boneName]
bonesToProcess = bonesToProcess[1:]
animBones.append(boneName)
childrenNames = getSortedChildren(armatureObj, currentBone)
bonesToProcess = childrenNames + bonesToProcess
# list of boneFrameData, which is [[x frames], [y frames], [z frames]]
# boneIndex is index in animBones.
# since we are processing the bones in the same order as ootProcessBone,
# they should be the same as the limb indices.
frameData = []
currentFrame = bpy.context.scene.frame_current
for frame in range(frame_start, frame_start + frame_count):
bpy.context.scene.frame_set(frame)
rootBone = armatureObj.data.bones[animBones[0]]
rootPoseBone = armatureObj.pose.bones[animBones[0]]
# Convert Z-up to Y-up for root translation animation
translation = (
mathutils.Quaternion((1, 0, 0), math.radians(-90.0))
@ (convertTransformMatrix @ rootPoseBone.matrix).decompose()[0]
)
for i in range(3):
frameData.append(min(int(round(translation[i])), 2**16 - 1))
for boneIndex in range(len(animBones)):
boneName = animBones[boneIndex]
currentBone = armatureObj.data.bones[boneName]
currentPoseBone = armatureObj.pose.bones[boneName]
rotation = ootGetAnimBoneRot(currentBone, currentPoseBone, convertTransformMatrix, boneIndex == 0)
for i in range(3):
field = rotation.to_euler()[i]
value = (math.degrees(field) % 360) / 360
frameData.append(min(int(round(value * (2**16 - 1))), 2**16 - 1))
textureAnimValue = (armatureObj.ootLinkTextureAnim.eyes & 0xF) | (
(armatureObj.ootLinkTextureAnim.mouth & 0xF) << 4
)
frameData.append(textureAnimValue)
bpy.context.scene.frame_set(currentFrame)
return frameData
def ootExportNonLinkAnimation(armatureObj, convertTransformMatrix, skeletonName):
if armatureObj.animation_data is None or armatureObj.animation_data.action is None:
raise PluginError("No active animation selected.")
anim = armatureObj.animation_data.action
@@ -224,7 +365,7 @@ def ootExportAnimationCommon(armatureObj, convertTransformMatrix, skeletonName):
frame_start, frame_last = getFrameInterval(anim)
ootAnim.frameCount = frame_last - frame_start + 1
armatureFrameData = ootConvertAnimationData(
armatureFrameData = ootConvertNonLinkAnimationData(
anim,
armatureObj,
convertTransformMatrix,
@@ -260,34 +401,114 @@ def ootExportAnimationCommon(armatureObj, convertTransformMatrix, skeletonName):
return ootAnim
def exportAnimationC(armatureObj, exportPath, isCustomExport, folderName, skeletonName):
checkEmptyName(folderName)
checkEmptyName(skeletonName)
def ootExportLinkAnimation(armatureObj, convertTransformMatrix, skeletonName):
if armatureObj.animation_data is None or armatureObj.animation_data.action is None:
raise PluginError("No active animation selected.")
anim = armatureObj.animation_data.action
ootAnim = OOTLinkAnimation(toAlnum(skeletonName + anim.name.capitalize() + "Anim"))
frame_start, frame_last = getFrameInterval(anim)
ootAnim.frameCount = frame_last - frame_start + 1
ootAnim.data = ootConvertLinkAnimationData(
anim,
armatureObj,
convertTransformMatrix,
frame_start=frame_start,
frame_count=(frame_last - frame_start + 1),
)
return ootAnim
def exportAnimationC(armatureObj: bpy.types.Object, settings: OOTAnimExportSettingsProperty):
path = bpy.path.abspath(settings.customPath)
exportPath = ootGetObjectPath(settings.isCustom, path, settings.folderName)
checkEmptyName(settings.folderName)
checkEmptyName(settings.skeletonName)
convertTransformMatrix = (
mathutils.Matrix.Scale(bpy.context.scene.ootActorBlenderScale, 4)
mathutils.Matrix.Scale(getOOTScale(armatureObj.ootActorScale), 4)
@ mathutils.Matrix.Diagonal(armatureObj.scale).to_4x4()
)
ootAnim = ootExportAnimationCommon(armatureObj, convertTransformMatrix, skeletonName)
ootAnimC = ootAnim.toC()
path = ootGetPath(exportPath, isCustomExport, "assets/objects/", folderName, False, False)
writeCData(ootAnimC, os.path.join(path, ootAnim.name + ".h"), os.path.join(path, ootAnim.name + ".c"))
if settings.isLink:
ootAnim = ootExportLinkAnimation(armatureObj, convertTransformMatrix, "gLink")
ootAnimC, ootAnimHeaderC = ootAnim.toC(settings.isCustom)
path = ootGetPath(
exportPath,
settings.isCustom,
"assets/misc/link_animetion",
settings.folderName if settings.isCustom else "",
False,
False,
)
headerPath = ootGetPath(
exportPath,
settings.isCustom,
"assets/objects/gameplay_keep",
settings.folderName if settings.isCustom else "",
False,
False,
)
writeCData(
ootAnimC, os.path.join(path, ootAnim.dataName() + ".h"), os.path.join(path, ootAnim.dataName() + ".c")
)
writeCData(
ootAnimHeaderC,
os.path.join(headerPath, ootAnim.headerName + ".h"),
os.path.join(headerPath, ootAnim.headerName + ".c"),
)
if not isCustomExport:
addIncludeFiles(folderName, path, ootAnim.name)
if not settings.isCustom:
addIncludeFiles("link_animetion", path, ootAnim.dataName())
addIncludeFiles("gameplay_keep", headerPath, ootAnim.headerName)
else:
ootAnim = ootExportNonLinkAnimation(armatureObj, convertTransformMatrix, settings.skeletonName)
ootAnimC = ootAnim.toC()
path = ootGetPath(exportPath, settings.isCustom, "assets/objects/", settings.folderName, False, False)
writeCData(ootAnimC, os.path.join(path, ootAnim.name + ".h"), os.path.join(path, ootAnim.name + ".c"))
if not settings.isCustom:
addIncludeFiles(settings.folderName, path, ootAnim.name)
def getNextBone(boneStack, armatureObj):
if len(boneStack) == 0:
raise PluginError("More bones in animation than on armature.")
bone = armatureObj.data.bones[boneStack[0]]
boneStack = boneStack[1:]
boneStack = getSortedChildren(armatureObj, bone) + boneStack
return bone, boneStack
def ootImportAnimationC(
armatureObj: bpy.types.Object,
settings: OOTAnimImportSettingsProperty,
actorScale: float,
):
importPath = bpy.path.abspath(settings.customPath)
filepath = ootGetObjectPath(settings.isCustom, importPath, settings.folderName)
if settings.isLink:
numLimbs = 21
if not settings.isCustom:
basePath = bpy.path.abspath(bpy.context.scene.ootDecompPath)
animFilepath = os.path.join(basePath, "assets/misc/link_animetion/link_animetion.c")
animHeaderFilepath = os.path.join(basePath, "assets/objects/gameplay_keep/gameplay_keep.c")
else:
animFilepath = filepath
animHeaderFilepath = filepath
ootImportLinkAnimationC(
armatureObj,
animHeaderFilepath,
animFilepath,
settings.animName,
actorScale,
numLimbs,
settings.isCustom,
)
else:
ootImportNonLinkAnimationC(armatureObj, filepath, settings.animName, actorScale, settings.isCustom)
def ootImportAnimationC(armatureObj, filepath, animName, actorScale):
animData = readFile(filepath)
def ootImportNonLinkAnimationC(armatureObj, filepath, animName, actorScale, isCustomImport: bool):
animData = getImportData([filepath])
if not isCustomImport:
basePath = bpy.path.abspath(bpy.context.scene.ootDecompPath)
animData = ootGetIncludedAssetData(basePath, [filepath], animData) + animData
matchResult = re.search(
re.escape(animName)
@@ -319,43 +540,192 @@ def ootImportAnimationC(armatureObj, filepath, animName, actorScale):
# property index = 0,1,2 (aka x,y,z)
for jointIndex in jointIndices:
if isRootTranslation:
for propertyIndex in range(3):
fcurve = anim.fcurves.new(
fcurves = [
anim.fcurves.new(
data_path='pose.bones["' + startBoneName + '"].location',
index=propertyIndex,
action_group=startBoneName,
)
if jointIndex[propertyIndex] < staticIndexMax:
value = frameData[jointIndex[propertyIndex]] / actorScale
fcurve.keyframe_points.insert(0, value)
else:
for frame in range(frameCount):
value = frameData[jointIndex[propertyIndex] + frame] / actorScale
fcurve.keyframe_points.insert(frame, value)
for propertyIndex in range(3)
]
for frame in range(frameCount):
rawTranslation = mathutils.Vector((0, 0, 0))
for propertyIndex in range(3):
if jointIndex[propertyIndex] < staticIndexMax:
value = ootTranslationValue(frameData[jointIndex[propertyIndex]], actorScale)
else:
value = ootTranslationValue(frameData[jointIndex[propertyIndex] + frame], actorScale)
rawTranslation[propertyIndex] = value
trueTranslation = getTranslationRelativeToRest(armatureObj.data.bones[startBoneName], rawTranslation)
for propertyIndex in range(3):
fcurves[propertyIndex].keyframe_points.insert(frame, trueTranslation[propertyIndex])
isRootTranslation = False
else:
# WARNING: This assumes the order bones are processed are in alphabetical order.
# If this changes in the future, then this won't work.
bone, boneStack = getNextBone(boneStack, armatureObj)
for propertyIndex in range(3):
fcurve = anim.fcurves.new(
fcurves = [
anim.fcurves.new(
data_path='pose.bones["' + bone.name + '"].rotation_euler',
index=propertyIndex,
action_group=bone.name,
)
if jointIndex[propertyIndex] < staticIndexMax:
value = math.radians(frameData[jointIndex[propertyIndex]] * 360 / (2**16))
fcurve.keyframe_points.insert(0, value)
else:
for frame in range(frameCount):
value = math.radians(frameData[jointIndex[propertyIndex] + frame] * 360 / (2**16))
fcurve.keyframe_points.insert(frame, value)
for propertyIndex in range(3)
]
for frame in range(frameCount):
rawRotation = mathutils.Euler((0, 0, 0), "XYZ")
for propertyIndex in range(3):
if jointIndex[propertyIndex] < staticIndexMax:
value = binangToRadians(frameData[jointIndex[propertyIndex]])
else:
value = binangToRadians(frameData[jointIndex[propertyIndex] + frame])
rawRotation[propertyIndex] = value
trueRotation = getRotationRelativeToRest(bone, rawRotation)
for propertyIndex in range(3):
fcurves[propertyIndex].keyframe_points.insert(frame, trueRotation[propertyIndex])
if armatureObj.animation_data is None:
armatureObj.animation_data_create()
armatureObj.animation_data.action = anim
# filepath is gameplay_keep.c
# animName is header name.
# numLimbs = 21 for link.
def ootImportLinkAnimationC(
armatureObj: bpy.types.Object,
animHeaderFilepath: str,
animFilepath: str,
animHeaderName: str,
actorScale: float,
numLimbs: int,
isCustomImport: bool,
):
animHeaderData = getImportData([animHeaderFilepath])
animData = getImportData([animFilepath])
if not isCustomImport:
basePath = bpy.path.abspath(bpy.context.scene.ootDecompPath)
animHeaderData = ootGetIncludedAssetData(basePath, [animHeaderFilepath], animHeaderData) + animHeaderData
animData = ootGetIncludedAssetData(basePath, [animFilepath], animData) + animData
matchResult = re.search(
re.escape(animHeaderName) + "\s*=\s*\{\s*\{\s*([^,\s]*)\s*\}\s*,\s*([^,\s]*)\s*\}\s*;",
animHeaderData,
)
if matchResult is None:
raise PluginError("Cannot find animation named " + animHeaderName + " in " + animHeaderFilepath)
frameCount = hexOrDecInt(matchResult.group(1).strip())
frameDataName = matchResult.group(2).strip()
frameData = getFrameData(animFilepath, animData, frameDataName)
print(f"{frameDataName}: {frameCount} frames, {len(frameData)} values.")
bpy.context.scene.frame_end = frameCount
anim = bpy.data.actions.new(animHeaderName)
# get ordered list of bone names
# create animation curves for each bone
startBoneName = getStartBone(armatureObj)
boneList = []
boneCurvesRotation = []
boneCurveTranslation = None
boneStack = [startBoneName]
eyesCurve = anim.fcurves.new(
data_path="ootLinkTextureAnim.eyes",
action_group="Texture Animations",
)
mouthCurve = anim.fcurves.new(
data_path="ootLinkTextureAnim.mouth",
action_group="Texture Animations",
)
# create all necessary fcurves
while len(boneStack) > 0:
bone, boneStack = getNextBone(boneStack, armatureObj)
boneList.append(bone)
if boneCurveTranslation is None:
boneCurveTranslation = [
anim.fcurves.new(
data_path='pose.bones["' + bone.name + '"].location',
index=propertyIndex,
action_group=startBoneName,
)
for propertyIndex in range(3)
]
boneCurvesRotation.append(
[
anim.fcurves.new(
data_path='pose.bones["' + bone.name + '"].rotation_euler',
index=propertyIndex,
action_group=bone.name,
)
for propertyIndex in range(3)
]
)
# vec3 = 3x s16 values
# padding = u8, tex anim = u8
# root trans vec3 + rot vec3 for each limb + (s16 with eye/mouth indices)
frameSize = 3 + 3 * numLimbs + 1
for frame in range(frameCount):
currentFrame = frameData[frame * frameSize : (frame + 1) * frameSize]
if len(currentFrame) < frameSize:
raise PluginError(
f"{frameDataName} has malformed data. Framesize = {frameSize}, CurrentFrame = {len(currentFrame)}"
)
translation = getTranslationRelativeToRest(
boneList[0], mathutils.Vector([ootTranslationValue(currentFrame[i], actorScale) for i in range(3)])
)
for i in range(3):
boneCurveTranslation[i].keyframe_points.insert(frame, translation[i])
for boneIndex in range(numLimbs):
bone = boneList[boneIndex]
rawRotation = mathutils.Euler(
[binangToRadians(currentFrame[i + (boneIndex + 1) * 3]) for i in range(3)], "XYZ"
)
trueRotation = getRotationRelativeToRest(bone, rawRotation)
for i in range(3):
boneCurvesRotation[boneIndex][i].keyframe_points.insert(frame, trueRotation[i])
# convert to unsigned short representation
texAnimValue = int.from_bytes(
currentFrame[(numLimbs + 1) * 3].to_bytes(2, "big", signed=True), "big", signed=False
)
eyesValue = texAnimValue & 0xF
mouthValue = texAnimValue >> 4 & 0xF
eyesCurve.keyframe_points.insert(frame, eyesValue).interpolation = "CONSTANT"
mouthCurve.keyframe_points.insert(frame, mouthValue).interpolation = "CONSTANT"
if armatureObj.animation_data is None:
armatureObj.animation_data_create()
armatureObj.animation_data.action = anim
def ootTranslationValue(value, actorScale):
return value / actorScale
def binangToRadians(value):
return math.radians(value * 360 / (2**16))
def getFrameData(filepath, animData, frameDataName):
matchResult = re.search(re.escape(frameDataName) + "\s*\[\s*[0-9]*\s*\]\s*=\s*\{([^\}]*)\}", animData, re.DOTALL)
if matchResult is None:
@@ -406,14 +776,8 @@ class OOT_ExportAnim(bpy.types.Operator):
return {"CANCELLED"}
try:
isCustomExport = context.scene.ootAnimIsCustomExport
exportPath = bpy.path.abspath(context.scene.ootAnimExportCustomPath)
folderName = context.scene.ootAnimExportFolderName
skeletonName = context.scene.ootAnimSkeletonName
path = ootGetObjectPath(isCustomExport, exportPath, folderName)
exportAnimationC(armatureObj, path, isCustomExport, folderName, skeletonName)
settings = context.scene.fast64.oot.animExportSettings
exportAnimationC(armatureObj, settings)
self.report({"INFO"}, "Success!")
except Exception as e:
@@ -441,20 +805,21 @@ class OOT_ImportAnim(bpy.types.Operator):
armatureObj = context.selected_objects[0]
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
# We need to apply scale otherwise translation imports won't be correct.
bpy.ops.object.select_all(action="DESELECT")
armatureObj.select_set(True)
bpy.context.view_layer.objects.active = armatureObj
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True, properties=False)
except Exception as e:
raisePluginError(self, e)
return {"CANCELLED"}
try:
isCustomImport = context.scene.ootAnimIsCustomImport
folderName = context.scene.ootAnimImportFolderName
importPath = bpy.path.abspath(context.scene.ootAnimImportCustomPath)
animName = context.scene.ootAnimName
actorScale = context.scene.ootActorBlenderScale
path = ootGetObjectPath(isCustomImport, importPath, folderName)
ootImportAnimationC(armatureObj, path, animName, actorScale)
actorScale = getOOTScale(armatureObj.ootActorScale)
settings = context.scene.fast64.oot.animImportSettings
ootImportAnimationC(armatureObj, settings, actorScale)
self.report({"INFO"}, "Success!")
except Exception as e:
@@ -473,29 +838,81 @@ class OOT_ExportAnimPanel(OOT_Panel):
col = self.layout.column()
col.operator(OOT_ExportAnim.bl_idname)
prop_split(col, context.scene, "ootAnimSkeletonName", "Skeleton Name")
if context.scene.ootAnimIsCustomExport:
prop_split(col, context.scene, "ootAnimExportCustomPath", "Folder")
else:
prop_split(col, context.scene, "ootAnimExportFolderName", "Object")
col.prop(context.scene, "ootAnimIsCustomExport")
exportSettings = context.scene.fast64.oot.animExportSettings
prop_split(col, exportSettings, "skeletonName", "Anim Name Prefix")
if exportSettings.isCustom:
prop_split(col, exportSettings, "customPath", "Folder")
elif not exportSettings.isLink:
prop_split(col, exportSettings, "folderName", "Object")
col.prop(exportSettings, "isLink")
col.prop(exportSettings, "isCustom")
col.operator(OOT_ImportAnim.bl_idname)
prop_split(col, context.scene, "ootAnimName", "Anim Name")
importSettings = context.scene.fast64.oot.animImportSettings
prop_split(col, importSettings, "animName", "Anim Header Name")
if importSettings.isCustom:
prop_split(col, importSettings, "customPath", "File")
elif not importSettings.isLink:
prop_split(col, importSettings, "folderName", "Object")
col.prop(importSettings, "isLink")
col.prop(importSettings, "isCustom")
if context.scene.ootAnimIsCustomImport:
prop_split(col, context.scene, "ootAnimImportCustomPath", "File")
else:
prop_split(col, context.scene, "ootAnimImportFolderName", "Object")
col.prop(context.scene, "ootAnimIsCustomImport")
# The update callbacks are for manually setting texture with visualize operator.
# They don't run from animation updates, see flipbookAnimHandler in flipbook.py
def ootUpdateLinkEyes(self, context):
index = self.eyes
ootFlipbookAnimUpdate(self, context.object, "8", index)
def ootUpdateLinkMouth(self, context):
index = self.mouth
ootFlipbookAnimUpdate(self, context.object, "9", index)
class OOTLinkTextureAnimProperty(bpy.types.PropertyGroup):
eyes: bpy.props.IntProperty(min=0, max=15, default=0, name="Eyes", update=ootUpdateLinkEyes)
mouth: bpy.props.IntProperty(min=0, max=15, default=0, name="Mouth", update=ootUpdateLinkMouth)
class OOT_LinkAnimPanel(bpy.types.Panel):
bl_idname = "OOT_PT_link_anim"
bl_label = "OOT Link Animation Properties"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_options = {"HIDE_HEADER"}
@classmethod
def poll(cls, context):
return (
context.scene.gameEditorMode == "OOT"
and hasattr(context, "object")
and context.object is not None
and isinstance(context.object.data, bpy.types.Armature)
)
# called every frame
def draw(self, context):
col = self.layout.box().column()
col.box().label(text="OOT Link Animation Inspector")
prop_split(col, context.object.ootLinkTextureAnim, "eyes", "Eyes")
prop_split(col, context.object.ootLinkTextureAnim, "mouth", "Mouth")
col.label(text="Index 0 is for auto, flipbook starts at index 1.", icon="INFO")
oot_anim_classes = (
OOT_ExportAnim,
OOT_ImportAnim,
OOTLinkTextureAnimProperty,
OOTAnimExportSettingsProperty,
OOTAnimImportSettingsProperty,
)
oot_anim_panels = (OOT_ExportAnimPanel,)
oot_anim_panels = (
OOT_ExportAnimPanel,
OOT_LinkAnimPanel,
)
def oot_anim_panel_register():
@@ -509,30 +926,14 @@ def oot_anim_panel_unregister():
def oot_anim_register():
bpy.types.Scene.ootAnimIsCustomExport = bpy.props.BoolProperty(name="Use Custom Path")
bpy.types.Scene.ootAnimExportCustomPath = bpy.props.StringProperty(name="Folder", subtype="FILE_PATH")
bpy.types.Scene.ootAnimExportFolderName = bpy.props.StringProperty(name="Animation Folder", default="object_geldb")
bpy.types.Scene.ootAnimIsCustomImport = bpy.props.BoolProperty(name="Use Custom Path")
bpy.types.Scene.ootAnimImportCustomPath = bpy.props.StringProperty(name="Folder", subtype="FILE_PATH")
bpy.types.Scene.ootAnimImportFolderName = bpy.props.StringProperty(name="Animation Folder", default="object_geldb")
bpy.types.Scene.ootAnimSkeletonName = bpy.props.StringProperty(name="Skeleton Name", default="gGerudoRedSkel")
bpy.types.Scene.ootAnimName = bpy.props.StringProperty(name="Anim Name", default="gGerudoRedSpinAttackAnim")
for cls in oot_anim_classes:
register_class(cls)
bpy.types.Object.ootLinkTextureAnim = bpy.props.PointerProperty(type=OOTLinkTextureAnimProperty)
def oot_anim_unregister():
del bpy.types.Scene.ootAnimIsCustomExport
del bpy.types.Scene.ootAnimExportCustomPath
del bpy.types.Scene.ootAnimExportFolderName
del bpy.types.Scene.ootAnimIsCustomImport
del bpy.types.Scene.ootAnimImportCustomPath
del bpy.types.Scene.ootAnimImportFolderName
del bpy.types.Scene.ootAnimSkeletonName
del bpy.types.Scene.ootAnimName
for cls in reversed(oot_anim_classes):
unregister_class(cls)
del bpy.types.Object.ootLinkTextureAnim
+1 -1
View File
@@ -577,7 +577,7 @@ class OOT_ExportCollision(bpy.types.Operator):
if type(obj.data) is not bpy.types.Mesh:
raise PluginError("No mesh object selected.")
finalTransform = mathutils.Matrix.Scale(context.scene.ootActorBlenderScale, 4)
finalTransform = mathutils.Matrix.Scale(getOOTScale(obj.ootActorScale), 4)
try:
scaleValue = bpy.context.scene.ootBlenderScale
+244 -14
View File
@@ -1,8 +1,18 @@
import bpy, os, mathutils
import bpy, os, mathutils, re
from bpy.utils import register_class, unregister_class
from ..panels import OOT_Panel
from ..utility import PluginError, CData, prop_split, writeCData, raisePluginError, getGroupIndexFromname, toAlnum
from ..f3d.f3d_parser import importMeshC, ootEnumDrawLayers
from ..utility import (
PluginError,
CData,
prop_split,
writeCData,
raisePluginError,
getGroupIndexFromname,
toAlnum,
readFile,
writeFile,
)
from ..f3d.f3d_parser import importMeshC, ootEnumDrawLayers, getImportData
from ..f3d.f3d_gbi import DLFormat, TextureExportSettings, ScrollMethod, F3D
from ..f3d.f3d_writer import (
@@ -23,6 +33,8 @@ from .oot_utility import (
ootCleanupScene,
ootGetPath,
addIncludeFiles,
replaceMatchContent,
getOOTScale,
)
from .oot_model_classes import (
@@ -31,8 +43,15 @@ from .oot_model_classes import (
OOTModel,
OOTGfxFormatter,
OOTDynamicTransformProperty,
ootGetActorData,
ootGetLinkData,
ootGetIncludedAssetData,
)
from .oot_scene_room import *
from .oot_texture_array import TextureFlipbook, ootReadTextureArrays
from ..f3d.flipbook import flipbook_to_c, flipbook_2d_to_c, flipbook_data_to_c
class OOTDLExportSettings(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(name="DL Name", default="gBoulderFragmentsDL")
@@ -41,6 +60,9 @@ class OOTDLExportSettings(bpy.types.PropertyGroup):
isCustom: bpy.props.BoolProperty(name="Use Custom Path")
removeVanillaData: bpy.props.BoolProperty(name="Replace Vanilla DLs")
drawLayer: bpy.props.EnumProperty(name="Draw Layer", items=ootEnumDrawLayers)
actorOverlayName: bpy.props.StringProperty(name="Overlay", default="")
flipbookUses2DArray: bpy.props.BoolProperty(name="Has 2D Flipbook Array", default=False)
flipbookArrayIndex2D: bpy.props.IntProperty(name="Index if 2D Array", default=0, min=0)
customAssetIncludeDir: bpy.props.StringProperty(
name="Asset Include Directory",
default="assets/objects/gameplay_keep",
@@ -56,6 +78,11 @@ class OOTDLImportSettings(bpy.types.PropertyGroup):
removeDoubles: bpy.props.BoolProperty(name="Remove Doubles", default=True)
importNormals: bpy.props.BoolProperty(name="Import Normals", default=True)
drawLayer: bpy.props.EnumProperty(name="Draw Layer", items=ootEnumDrawLayers)
actorOverlayName: bpy.props.StringProperty(name="Overlay", default="")
flipbookUses2DArray: bpy.props.BoolProperty(name="Has 2D Flipbook Array", default=False)
flipbookArrayIndex2D: bpy.props.IntProperty(name="Index if 2D Array", default=0, min=0)
autoDetectActorScale: bpy.props.BoolProperty(name="Auto Detect Actor Scale", default=True)
actorScale: bpy.props.FloatProperty(name="Actor Scale", min=0, default=100)
# returns:
@@ -231,6 +258,9 @@ def ootConvertMeshToC(
drawLayer = settings.drawLayer
removeVanillaData = settings.removeVanillaData
name = toAlnum(settings.name)
overlayName = settings.actorOverlayName
flipbookUses2DArray = settings.flipbookUses2DArray
flipbookArrayIndex2D = settings.flipbookArrayIndex2D if flipbookUses2DArray else None
try:
obj, allObjs = ootDuplicateHierarchy(originalObj, None, False, OOTObjectCategorizer())
@@ -266,9 +296,14 @@ def ootConvertMeshToC(
data.append(exportData.all())
if isCustomExport:
textureArrayData = writeTextureArraysNew(fModel, flipbookArrayIndex2D)
data.append(textureArrayData)
writeCData(data, os.path.join(path, name + ".h"), os.path.join(path, name + ".c"))
if not isCustomExport:
writeTextureArraysExisting(bpy.context.scene.ootDecompPath, overlayName, False, flipbookArrayIndex2D, fModel)
addIncludeFiles(folderName, path, name)
if removeVanillaData:
headerPath = os.path.join(path, folderName + ".h")
@@ -276,6 +311,154 @@ def ootConvertMeshToC(
removeDL(sourcePath, headerPath, name)
def writeTextureArraysNew(fModel: OOTModel, arrayIndex: int):
textureArrayData = CData()
for flipbook in fModel.flipbooks:
if flipbook.exportMode == "Array":
if arrayIndex is not None:
textureArrayData.source += flipbook_2d_to_c(flipbook, True, arrayIndex + 1) + "\n"
else:
textureArrayData.source += flipbook_to_c(flipbook, True) + "\n"
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 os.path.exists(actorFilePath):
print(f"{actorFilePath} not found, ignoring texture array writing.")
return
actorData = readFile(actorFilePath)
newData = actorData
for flipbook in fModel.flipbooks:
if flipbook.exportMode == "Array":
if flipbookArrayIndex2D is None:
newData = writeTextureArraysExisting1D(newData, flipbook)
else:
newData = writeTextureArraysExisting2D(newData, flipbook, flipbookArrayIndex2D)
if newData != actorData:
writeFile(actorFilePath, newData)
def writeTextureArraysExisting1D(data: str, flipbook: TextureFlipbook) -> str:
newData = data
arrayMatch = re.search(
r"(static\s*)?void\s*\*\s*" + re.escape(flipbook.name) + r"\s*\[\s*\]\s*=\s*\{(((?!\}).)*)\}\s*;",
newData,
flags=re.DOTALL,
)
# replace array if found
if arrayMatch:
newArrayData = flipbook_to_c(flipbook, arrayMatch.group(1))
newData = newData[: arrayMatch.start(0)] + newArrayData + newData[arrayMatch.end(0) :]
# otherwise, add to end of asset includes
else:
newArrayData = flipbook_to_c(flipbook, True)
# get last asset include
includeMatch = None
for includeMatchItem in re.finditer(r"\#include\s*\"assets/.*?\"\s*?\n", newData, flags=re.DOTALL):
includeMatch = includeMatchItem
if includeMatch:
newData = newData[: includeMatch.end(0)] + newArrayData + "\n" + newData[includeMatch.end(0) :]
else:
newData += newArrayData + "\n"
return newData
# for flipbook textures, we only replace one element of the 2D array.
def writeTextureArraysExisting2D(data: str, flipbook: TextureFlipbook, flipbookArrayIndex2D: int) -> str:
newData = data
# for !AVOID_UB, Link has textures in 2D Arrays
array2DMatch = re.search(
r"(static\s*)?void\s*\*\s*"
+ re.escape(flipbook.name)
+ r"\s*\[\s*\]\s*\[\s*[0-9a-fA-Fx]*\s*\]\s*=\s*\{(.*?)\}\s*;",
newData,
flags=re.DOTALL,
)
newArrayData = "{ " + flipbook_data_to_c(flipbook) + " }"
# build a list of arrays here
# replace existing element if list is large enough
# otherwise, pad list with repeated arrays
if array2DMatch:
arrayMatchData = [
arrayMatch.group(0) for arrayMatch in re.finditer(r"\{(.*?)\}", array2DMatch.group(2), flags=re.DOTALL)
]
if flipbookArrayIndex2D >= len(arrayMatchData):
while len(arrayMatchData) <= flipbookArrayIndex2D:
arrayMatchData.append(newArrayData)
else:
arrayMatchData[flipbookArrayIndex2D] = newArrayData
newArray2DData = ",\n".join([item for item in arrayMatchData])
newData = replaceMatchContent(newData, newArray2DData, array2DMatch, 2)
# otherwise, add to end of asset includes
else:
arrayMatchData = [newArrayData] * (flipbookArrayIndex2D + 1)
newArray2DData = ",\n".join([item for item in arrayMatchData])
# get last asset include
includeMatch = None
for includeMatchItem in re.finditer(r"\#include\s*\"assets/.*?\"\s*?\n", newData, flags=re.DOTALL):
includeMatch = includeMatchItem
if includeMatch:
newData = newData[: includeMatch.end(0)] + newArray2DData + "\n" + newData[includeMatch.end(0) :]
else:
newData += newArray2DData + "\n"
return newData
# Note this does not work well with actors containing multiple "parts". (z_en_honotrap)
def ootReadActorScale(basePath: str, overlayName: str, isLink: bool) -> float:
if not isLink:
actorData = ootGetActorData(basePath, overlayName)
else:
actorData = ootGetLinkData(basePath)
chainInitMatch = re.search(r"CHAIN_VEC3F_DIV1000\s*\(\s*scale\s*,\s*(.*?)\s*,", actorData, re.DOTALL)
if chainInitMatch is not None:
scale = chainInitMatch.group(1).strip()
if scale[-1] == "f":
scale = scale[:-1]
return getOOTScale(1 / (float(scale) / 1000))
actorScaleMatch = re.search(r"Actor\_SetScale\s*\(.*?,\s*(.*?)\s*\)", actorData, re.DOTALL)
if actorScaleMatch is not None:
scale = actorScaleMatch.group(1).strip()
if scale[-1] == "f":
scale = scale[:-1]
return getOOTScale(1 / float(scale))
return getOOTScale(100)
class OOT_DisplayListPanel(bpy.types.Panel):
bl_label = "Display List Inspector"
bl_idname = "OBJECT_PT_OOT_DL_Inspector"
@@ -299,6 +482,10 @@ class OOT_DisplayListPanel(bpy.types.Panel):
box.prop(obj, "ignore_render")
box.prop(obj, "ignore_collision")
if not (obj.parent is not None and isinstance(obj.parent.data, bpy.types.Armature)):
prop_split(box, obj, "ootActorScale", "Actor Scale")
box.label(text="This applies to actor exports only.", icon="INFO")
# Doesn't work since all static meshes are pre-transformed
# box.prop(obj.ootDynamicTransform, "billboard")
@@ -324,27 +511,37 @@ class OOT_ImportDL(bpy.types.Operator):
folderName = settings.folder
importPath = bpy.path.abspath(settings.customPath)
isCustomImport = settings.isCustom
scale = context.scene.ootActorBlenderScale
basePath = bpy.path.abspath(context.scene.ootDecompPath)
removeDoubles = settings.removeDoubles
importNormals = settings.importNormals
drawLayer = settings.drawLayer
overlayName = settings.actorOverlayName
flipbookUses2DArray = settings.flipbookUses2DArray
flipbookArrayIndex2D = settings.flipbookArrayIndex2D if flipbookUses2DArray else None
filepaths = [ootGetObjectPath(isCustomImport, importPath, folderName)]
paths = [ootGetObjectPath(isCustomImport, importPath, folderName)]
data = getImportData(paths)
f3dContext = OOTF3DContext(F3D("F3DEX2/LX2", False), [name], basePath)
scale = getOOTScale(settings.actorScale)
if not isCustomImport:
filepaths.append(
os.path.join(bpy.context.scene.ootDecompPath, "assets/objects/gameplay_keep/gameplay_keep.c")
)
data = ootGetIncludedAssetData(basePath, paths, data) + data
importMeshC(
filepaths,
if overlayName is not None:
ootReadTextureArrays(basePath, overlayName, name, f3dContext, False, flipbookArrayIndex2D)
if settings.autoDetectActorScale:
scale = ootReadActorScale(basePath, overlayName, False)
obj = importMeshC(
data,
name,
scale,
removeDoubles,
importNormals,
drawLayer,
OOTF3DContext(F3D("F3DEX2/LX2", False), [name], basePath),
f3dContext,
)
obj.ootActorScale = scale / bpy.context.scene.ootBlenderScale
self.report({"INFO"}, "Success!")
return {"FINISHED"}
@@ -374,7 +571,7 @@ class OOT_ExportDL(bpy.types.Operator):
if type(obj.data) is not bpy.types.Mesh:
raise PluginError("Mesh not selected.")
finalTransform = mathutils.Matrix.Scale(context.scene.ootActorBlenderScale, 4)
finalTransform = mathutils.Matrix.Scale(getOOTScale(obj.ootActorScale), 4)
try:
# exportPath, levelName = getPathAndLevel(context.scene.geoCustomExport,
@@ -421,6 +618,12 @@ class OOT_ExportDLPanel(OOT_Panel):
if exportSettings.isCustom:
prop_split(col, exportSettings, "customAssetIncludeDir", "Asset Include Path")
prop_split(col, exportSettings, "customPath", "Path")
else:
prop_split(col, exportSettings, "actorOverlayName", "Overlay (Optional)")
col.prop(exportSettings, "flipbookUses2DArray")
if exportSettings.flipbookUses2DArray:
box = col.box().column()
prop_split(box, exportSettings, "flipbookArrayIndex2D", "Flipbook Index")
prop_split(col, exportSettings, "drawLayer", "Export Draw Layer")
col.prop(exportSettings, "isCustom")
@@ -434,6 +637,14 @@ class OOT_ExportDLPanel(OOT_Panel):
prop_split(col, importSettings, "customPath", "File")
else:
prop_split(col, importSettings, "folder", "Object")
prop_split(col, importSettings, "actorOverlayName", "Overlay (Optional)")
col.prop(importSettings, "autoDetectActorScale")
if not importSettings.autoDetectActorScale:
prop_split(col, importSettings, "actorScale", "Actor Scale")
col.prop(importSettings, "flipbookUses2DArray")
if importSettings.flipbookUses2DArray:
box = col.box().column()
prop_split(box, importSettings, "flipbookArrayIndex2D", "Flipbook Index")
prop_split(col, importSettings, "drawLayer", "Import Draw Layer")
col.prop(importSettings, "isCustom")
@@ -512,7 +723,7 @@ class OOT_MaterialPanel(bpy.types.Panel):
else:
drawLayer = mat.f3d_mat.draw_layer.oot
drawOOTMaterialProperty(col.box().column(), mat.ootMaterial, drawLayer)
drawOOTMaterialProperty(col.box().column(), mat, drawLayer)
def drawOOTMaterialDrawLayerProperty(layout, matDrawLayerProp, suffix):
@@ -534,14 +745,18 @@ def drawOOTMaterialDrawLayerProperty(layout, matDrawLayerProp, suffix):
drawLayerSuffix = {"Opaque": "OPA", "Transparent": "XLU", "Overlay": "OVL"}
def drawOOTMaterialProperty(layout, matProp, drawLayer):
def drawOOTMaterialProperty(layout, mat, drawLayer):
if drawLayer == "Overlay":
return
matProp = mat.ootMaterial
suffix = "(" + drawLayerSuffix[drawLayer] + ")"
layout.box().column().label(text="OOT Dynamic Material Properties " + suffix)
layout.label(text="See gSPSegment calls in z_scene_table.c.")
layout.label(text="Based off draw config index in gSceneTable.")
drawOOTMaterialDrawLayerProperty(layout.column(), getattr(matProp, drawLayer.lower()), suffix)
if not mat.is_f3d:
return
f3d_mat = mat.f3d_mat
class OOTDynamicMaterialDrawLayerProperty(bpy.types.PropertyGroup):
@@ -556,6 +771,18 @@ class OOTDynamicMaterialDrawLayerProperty(bpy.types.PropertyGroup):
customCall1: bpy.props.BoolProperty()
customCall1_seg: bpy.props.StringProperty(description="Segment address of a display list to call, e.g. 0x08000010")
def key(self):
return (
self.segment8,
self.segment9,
self.segmentA,
self.segmentB,
self.segmentC,
self.segmentD,
self.customCall0_seg if self.customCall0 else None,
self.customCall1_seg if self.customCall1 else None,
)
# The reason these are separate is for the case when the user changes the material draw layer, but not the
# dynamic material calls. This could cause crashes which would be hard to detect.
@@ -563,6 +790,9 @@ class OOTDynamicMaterialProperty(bpy.types.PropertyGroup):
opaque: bpy.props.PointerProperty(type=OOTDynamicMaterialDrawLayerProperty)
transparent: bpy.props.PointerProperty(type=OOTDynamicMaterialDrawLayerProperty)
def key(self):
return (self.opaque.key(), self.transparent.key())
oot_dl_writer_classes = (
OOTDefaultRenderModesProperty,
+371 -12
View File
@@ -1,11 +1,26 @@
import bpy
from ..f3d.f3d_writer import VertexGroupInfo, TriangleConverterInfo
from ..f3d.f3d_parser import F3DContext
from ..f3d.f3d_material import createF3DMat
from ..utility import CData, hexOrDecInt
import bpy, os, re
from typing import Union
from ..f3d.f3d_writer import (
VertexGroupInfo,
TriangleConverterInfo,
saveOrGetTextureDefinition,
saveOrGetPaletteAndImageDefinition,
getTextureNameTexRef,
saveOrGetPaletteOnlyDefinition,
FSharedPalette,
DPLoadTLUTCmd,
DPSetTextureLUT,
DPSetTile,
texFormatOf,
)
from ..f3d.f3d_parser import F3DContext, F3DTextureReference, getImportData
from ..f3d.f3d_material import createF3DMat, TextureProperty
from ..utility import CData, hexOrDecInt, PluginError
from ..f3d.f3d_gbi import (
FModel,
FMaterial,
FImage,
GfxMatWriteMethod,
SPDisplayList,
GfxList,
@@ -15,11 +30,79 @@ from ..f3d.f3d_gbi import (
GfxFormatter,
MTX_SIZE,
)
from ..f3d.flipbook import TextureFlipbook, FlipbookProperty, usesFlipbook, ootFlipbookReferenceIsValid
# 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
self.flipbooks: list[TextureFlipbook] = []
# key: first flipbook image
# value: list of flipbook textures in order
self.processedFlipbooks: dict[bpy.types.Image, list[bpy.types.Image]] = {}
FModel.__init__(self, f3dType, isHWv1, name, DLFormat, GfxMatWriteMethod.WriteAll)
def getDrawLayerV3(self, obj):
@@ -41,7 +124,170 @@ class OOTModel(FModel):
else:
return texFmt.lower()
def onMaterialCommandsBuilt(self, gfxList, revertList, material, drawLayer):
def modifyDLForCIFlipbook(self, fMaterial: FMaterial, fPalette: FMaterial, texProp: TextureProperty):
# Modfiy DL to use new palette texture
tlutCmdIndex = 0
gfxList = fMaterial.material
while tlutCmdIndex < len(gfxList.commands):
if isinstance(gfxList.commands[tlutCmdIndex], DPLoadTLUTCmd):
loadTlutCmd = gfxList.commands[tlutCmdIndex]
loadTlutCmd.count = int(round(len(fPalette.data) / 2)) - 1
setTLUTCmd = gfxList.commands[tlutCmdIndex - 5]
setTImageCmd = gfxList.commands[tlutCmdIndex - 4]
if tlutCmdIndex < 5 or not isinstance(setTLUTCmd, DPSetTextureLUT):
raise PluginError("Error when processing flipbook CI textures: unexpected display list format.")
setTImageCmd.fmt = texFormatOf[texProp.ci_format]
setTImageCmd.image = fPalette
setTLUTCmd.mode = "G_TT_RGBA16" if setTImageCmd.fmt == "G_IM_FMT_RGBA" else "G_TT_IA16"
break
else:
tlutCmdIndex += 1
if tlutCmdIndex == len(gfxList.commands):
raise PluginError(f"Can not find TLUT command in material {fMaterial.name}")
def addFlipbookWithRepeatCheck(self, flipbook: TextureFlipbook):
for existingFlipbook in self.flipbooks:
if existingFlipbook.name == flipbook.name:
if len(existingFlipbook.textureNames) != len(flipbook.textureNames):
raise PluginError(
f"There are two flipbooks with differing elements trying to write to the same texture array name: {flipbook.name}."
+ f"\nMake sure that this flipbook name is unique, or that repeated uses of this name use the same textures is the same order/format."
)
for i in range(len(flipbook.textureNames)):
if existingFlipbook.textureNames[i] != flipbook.textureNames[i]:
raise PluginError(
f"There are two flipbooks with differing elements trying to write to the same texture array name: {flipbook.name}."
+ f"\nMake sure that this flipbook name is unique, or that repeated uses of this name use the same textures is the same order/format."
)
self.flipbooks.append(flipbook)
def validateCIFlipbook(
self, existingFPalette: FImage, alreadyExists: bool, fPalette: FImage, flipbookImage: bpy.types.Image
) -> Union[FImage, bool]:
if existingFPalette is None:
if alreadyExists:
if fPalette:
return fPalette
else:
raise PluginError("FPalette not found.")
else:
return False
else:
if (
alreadyExists # texture already processed for this export
and fPalette is not None # texture is not a repeat within flipbook
and existingFPalette != False # a previous texture used an existing palette
and fPalette != existingFPalette # the palettes do not match
):
raise PluginError(
f"Cannot reuse a CI texture across multiple flipbooks: {str(flipbookImage)}. "
+ f"Flipbook textures should only be reused if they are in the same grouping/order, including LOD skeletons."
)
elif (
not alreadyExists # current texture has not been processed yet
and existingFPalette is not None
and existingFPalette != False # a previous texture used an existing palette
):
raise PluginError(
f"Flipbook textures before this were part of a different palette: {str(flipbookImage)}. "
+ f"Flipbook textures should only be reused if they are in the same grouping/order, including LOD skeletons."
)
return existingFPalette
def processFlipbookCI(self, fMaterial: FMaterial, flipbookProp: FlipbookProperty, texProp: TextureProperty):
# print("Processing flipbook...")
flipbook = TextureFlipbook(flipbookProp.name, flipbookProp.exportMode, [])
sharedPalette = FSharedPalette(self.name + "_" + flipbookProp.textures[0].image.name + "_pal")
existingFPalette = None
fImages = []
for flipbookTexture in flipbookProp.textures:
if flipbookTexture.image is None:
raise PluginError(f"Flipbook for {fMaterial.name} has a texture array item that has not been set.")
# print(f"Texture: {str(flipbookTexture.image)}")
name = (
flipbookTexture.name
if flipbookProp.exportMode == "Individual"
else self.name + "_" + flipbookTexture.image.name + "_" + texProp.tex_format.lower()
)
texName = getTextureNameTexRef(texProp, self.name)
# fPalette should be None here, since sharedPalette is not None
fImage, fPalette, alreadyExists = saveOrGetPaletteAndImageDefinition(
fMaterial,
self,
flipbookTexture.image,
name,
texProp.tex_format,
texProp.ci_format,
True,
sharedPalette,
)
existingFPalette = self.validateCIFlipbook(existingFPalette, alreadyExists, fPalette, flipbookTexture.image)
fImages.append(fImage)
# do this here to check for modified names due to repeats
flipbook.textureNames.append(fImage.name)
self.addFlipbookWithRepeatCheck(flipbook)
# print(f"Palette length for {sharedPalette.name}: {len(sharedPalette.palette)}")
firstImage = flipbookProp.textures[0].image
self.processedFlipbooks[firstImage] = [flipbookTex.image for flipbookTex in flipbookProp.textures]
if existingFPalette == False:
palFormat = texProp.ci_format
fPalette = saveOrGetPaletteOnlyDefinition(
fMaterial,
self,
firstImage,
sharedPalette.name,
texProp.tex_format,
palFormat,
True,
sharedPalette.palette,
)
# using the first image for the key, apply paletteKey to all images
# while this is not ideal, its better to us an image for the key as
# names are modified when duplicates are found
paletteKey = (firstImage, (palFormat, "PAL"))
for fImage in fImages:
fImage.paletteKey = paletteKey
else:
fPalette = existingFPalette
self.modifyDLForCIFlipbook(fMaterial, fPalette, texProp)
def processFlipbookNonCI(self, fMaterial: FMaterial, flipbookProp: FlipbookProperty, texProp: TextureProperty):
flipbook = TextureFlipbook(flipbookProp.name, flipbookProp.exportMode, [])
for flipbookTexture in flipbookProp.textures:
if flipbookTexture.image is None:
raise PluginError(f"Flipbook for {fMaterial.name} has a texture array item that has not been set.")
# print(f"Texture: {str(flipbookTexture.image)}")
name = (
flipbookTexture.name
if flipbookProp.exportMode == "Individual"
else self.name + "_" + flipbookTexture.image.name + "_" + texProp.tex_format.lower()
)
fImage = saveOrGetTextureDefinition(
fMaterial,
self,
flipbookTexture.image,
name,
texProp.tex_format,
True,
)
# do this here to check for modified names due to repeats
flipbook.textureNames.append(fImage.name)
self.addFlipbookWithRepeatCheck(flipbook)
def onMaterialCommandsBuilt(self, fMaterial, material, drawLayer):
# handle dynamic material calls
gfxList = fMaterial.material
matDrawLayer = getattr(material.ootMaterial, drawLayer.lower())
for i in range(8, 14):
if getattr(matDrawLayer, "segment" + format(i, "X")):
@@ -55,6 +301,27 @@ class OOTModel(FModel):
SPDisplayList(GfxList(getattr(matDrawLayer, p + "_seg"), GfxListTag.Material, DLFormat.Static))
)
# save flipbook textures
for i in range(2):
flipbookProp = getattr(material.flipbookGroup, "flipbook" + str(i))
texProp = getattr(material.f3d_mat, "tex" + str(i))
if usesFlipbook(material, flipbookProp, i, True, ootFlipbookReferenceIsValid):
if len(flipbookProp.textures) == 0:
raise PluginError(f"{str(material)} cannot have a flipbook material with no flipbook textures.")
if texProp.tex_format[:2] == "CI":
self.processFlipbookCI(
fMaterial,
flipbookProp,
texProp,
)
else:
self.processFlipbookNonCI(
fMaterial,
flipbookProp,
texProp,
)
def onAddMesh(self, fMesh, contextObj):
if contextObj is not None and hasattr(contextObj, "ootDynamicTransform"):
if contextObj.ootDynamicTransform.billboard:
@@ -133,6 +400,8 @@ class OOTF3DContext(F3DContext):
self.limbList = limbList
self.dlList = [] # in the order they are rendered
self.isBillboard = False
self.flipbooks = {} # {(segment, draw layer) : TextureFlipbook}
materialContext = createF3DMat(None, preset="oot_shaded_solid")
# materialContext.f3d_mat.rdp_settings.g_mdsft_cycletype = "G_CYC_1CYCLE"
F3DContext.__init__(self, f3d, basePath, materialContext)
@@ -170,14 +439,15 @@ class OOTF3DContext(F3DContext):
try:
pointer = hexOrDecInt(name)
except:
if name == "gEmptyDL":
return None
return name
else:
segment = pointer >> 24
print("POINTER")
if segment >= 0x08 and segment <= 0x0D:
print("SETTING " + str(segment))
setattr(self.materialContext.ootMaterial.opaque, "segment" + format(segment, "1X"), True)
setattr(self.materialContext.ootMaterial.transparent, "segment" + format(segment, "1X"), True)
self.materialChanged = True
return None
return name
@@ -191,15 +461,104 @@ class OOTF3DContext(F3DContext):
# if (pointer >> 24) == 0x08:
# print("Unhandled OOT pointer: " + textureName)
def getMaterialKey(self, material: bpy.types.Material):
return (material.ootMaterial.key(), material.f3d_mat.key())
def clearGeometry(self):
self.dlList = []
self.isBillboard = False
super().clearGeometry()
def clearMaterial(self):
self.isBillboard = False
clearOOTMaterialDrawLayerProperty(self.materialContext.ootMaterial.opaque)
clearOOTMaterialDrawLayerProperty(self.materialContext.ootMaterial.transparent)
# Don't clear ootMaterial, some skeletons (Link) require dynamic material calls to be preserved between limbs
clearOOTFlipbookProperty(self.materialContext.flipbookGroup.flipbook0)
clearOOTFlipbookProperty(self.materialContext.flipbookGroup.flipbook1)
F3DContext.clearMaterial(self)
def postMaterialChanged(self):
clearOOTMaterialDrawLayerProperty(self.materialContext.ootMaterial.opaque)
clearOOTMaterialDrawLayerProperty(self.materialContext.ootMaterial.transparent)
pass
def handleTextureReference(
self,
name: str,
image: F3DTextureReference,
material: bpy.types.Material,
index: int,
tileSettings: DPSetTile,
data: str,
):
# check for texture arrays.
clearOOTFlipbookProperty(getattr(material.flipbookGroup, "flipbook" + str(index)))
match = re.search(f"(0x0[0-9a-fA-F])000000", name)
if match:
segment = int(match.group(1), 16)
flipbookKey = (segment, material.f3d_mat.draw_layer.oot)
if flipbookKey in self.flipbooks:
flipbook = self.flipbooks[flipbookKey]
flipbookProp = getattr(material.flipbookGroup, "flipbook" + str(index))
flipbookProp.enable = True
flipbookProp.exportMode = flipbook.exportMode
if flipbookProp.exportMode == "Array":
flipbookProp.name = flipbook.name
if len(flipbook.textureNames) == 0:
raise PluginError(
f'Texture array "{flipbookProp.name}" pointed at segment {hex(segment)} is a zero element array, which is invalid.'
)
for textureName in flipbook.textureNames:
image = self.loadTexture(data, textureName, None, tileSettings, False)
if not isinstance(image, bpy.types.Image):
raise PluginError(
f'Could not find texture "{textureName}", so it can not be used in a flipbook texture.'
)
flipbookProp.textures.add()
flipbookProp.textures[-1].image = image
if flipbookProp.exportMode == "Individual":
flipbookProp.textures[-1].name = textureName
texProp = getattr(material.f3d_mat, "tex" + str(index))
texProp.tex = flipbookProp.textures[0].image # for visual purposes only, will be ignored
texProp.use_tex_reference = True
texProp.tex_reference = name
else:
super().handleTextureReference(name, image, material, index, tileSettings, data)
else:
super().handleTextureReference(name, image, material, index, tileSettings, data)
def handleTextureValue(self, material: bpy.types.Material, image: bpy.types.Image, index: int):
clearOOTFlipbookProperty(getattr(material.flipbookGroup, "flipbook" + str(index)))
super().handleTextureValue(material, image, index)
def handleApplyTLUT(
self,
material: bpy.types.Material,
texProp: TextureProperty,
tlut: bpy.types.Image,
index: int,
):
flipbook = getattr(material.flipbookGroup, "flipbook" + str(index))
if usesFlipbook(material, flipbook, index, True, ootFlipbookReferenceIsValid):
# Don't apply TLUT to texProp.tex, as it is the same texture as the first flipbook texture.
# Make sure to check if tlut is already applied (ex. LOD skeleton uses same flipbook textures)
# applyTLUTToIndex() doesn't check for this if texProp.use_tex_reference.
for flipbookTexture in flipbook.textures:
if flipbookTexture.image not in self.tlutAppliedTextures:
self.applyTLUT(flipbookTexture.image, tlut)
self.tlutAppliedTextures.append(flipbookTexture.image)
else:
super().handleApplyTLUT(material, texProp, tlut, index)
def clearOOTFlipbookProperty(flipbookProp):
flipbookProp.enable = False
flipbookProp.name = "sFlipbookTextures"
flipbookProp.exportMode = "Array"
flipbookProp.textures.clear()
def clearOOTMaterialDrawLayerProperty(matDrawLayerProp):
+316 -84
View File
@@ -1,11 +1,18 @@
import mathutils, bpy, math, os, re
from ..panels import OOT_Panel
from ..f3d.f3d_gbi import DLFormat, FMesh, TextureExportSettings, ScrollMethod, F3D
from .oot_model_classes import OOTVertexGroupInfo, OOTModel, OOTGfxFormatter, OOTF3DContext, OOTDynamicTransformProperty
from .oot_model_classes import (
OOTVertexGroupInfo,
OOTModel,
OOTGfxFormatter,
OOTF3DContext,
OOTDynamicTransformProperty,
ootGetIncludedAssetData,
)
from bpy.utils import register_class, unregister_class
from ..f3d.f3d_writer import getInfoDict
from ..f3d.f3d_writer import getInfoDict, GfxList
from ..f3d.f3d_parser import getImportData, parseF3D
from .oot_f3d_writer import ootProcessVertexGroup
from .oot_f3d_writer import ootProcessVertexGroup, writeTextureArraysNew, writeTextureArraysExisting, ootReadActorScale
from ..f3d.f3d_material import ootEnumDrawLayers
from ..utility import (
@@ -25,6 +32,7 @@ from ..utility import (
getGroupNameFromIndex,
attemptModifierApply,
cleanupDuplicatedObjects,
VertexWeightError,
)
from .oot_utility import (
@@ -35,15 +43,29 @@ from .oot_utility import (
getSortedChildren,
ootGetPath,
addIncludeFiles,
getOOTScale,
)
from ..utility_anim import armatureApplyWithMesh
from .oot_texture_array import ootReadTextureArrays
from .oot_skeleton_import_data import (
ootEnumSkeletonImportMode,
applySkeletonRestPose,
OOT_SaveRestPose,
ootSkeletonImportDict,
)
class OOTSkeletonExportSettings(bpy.types.PropertyGroup):
mode: bpy.props.EnumProperty(name="Mode", items=ootEnumSkeletonImportMode)
name: bpy.props.StringProperty(name="Skeleton Name", default="gGerudoRedSkel")
folder: bpy.props.StringProperty(name="Skeleton Folder", default="object_geldb")
customPath: bpy.props.StringProperty(name="Custom Skeleton Path", subtype="FILE_PATH")
isCustom: bpy.props.BoolProperty(name="Use Custom Path")
removeVanillaData: bpy.props.BoolProperty(name="Replace Vanilla Skeletons On Export", default=True)
actorOverlayName: bpy.props.StringProperty(name="Overlay", default="ovl_En_GeldB")
flipbookUses2DArray: bpy.props.BoolProperty(name="Has 2D Flipbook Array", default=False)
flipbookArrayIndex2D: bpy.props.IntProperty(name="Index if 2D Array", default=0, min=0)
customAssetIncludeDir: bpy.props.StringProperty(
name="Asset Include Directory",
default="assets/objects/object_geldb",
@@ -58,6 +80,8 @@ class OOTSkeletonExportSettings(bpy.types.PropertyGroup):
class OOTSkeletonImportSettings(bpy.types.PropertyGroup):
mode: bpy.props.EnumProperty(name="Mode", items=ootEnumSkeletonImportMode)
applyRestPose: bpy.props.BoolProperty(name="Apply Friendly Rest Pose (If Available)", default=True)
name: bpy.props.StringProperty(name="Skeleton Name", default="gGerudoRedSkel")
folder: bpy.props.StringProperty(name="Skeleton Folder", default="object_geldb")
customPath: bpy.props.StringProperty(name="Custom Skeleton Path", subtype="FILE_PATH")
@@ -65,6 +89,11 @@ class OOTSkeletonImportSettings(bpy.types.PropertyGroup):
removeDoubles: bpy.props.BoolProperty(name="Remove Doubles On Import", default=True)
importNormals: bpy.props.BoolProperty(name="Import Normals", default=True)
drawLayer: bpy.props.EnumProperty(name="Import Draw Layer", items=ootEnumDrawLayers)
actorOverlayName: bpy.props.StringProperty(name="Overlay", default="ovl_En_GeldB")
flipbookUses2DArray: bpy.props.BoolProperty(name="Has 2D Flipbook Array", default=False)
flipbookArrayIndex2D: bpy.props.IntProperty(name="Index if 2D Array", default=0, min=0)
autoDetectActorScale: bpy.props.BoolProperty(name="Auto Detect Actor Scale", default=True)
actorScale: bpy.props.FloatProperty(name="Actor Scale", min=0, default=100)
ootEnumBoneType = [
@@ -74,6 +103,20 @@ ootEnumBoneType = [
]
def pollArmature(self, obj):
return isinstance(obj.data, bpy.types.Armature)
class OOTBoneProperty(bpy.types.PropertyGroup):
boneType: bpy.props.EnumProperty(name="Bone Type", items=ootEnumBoneType)
dynamicTransform: bpy.props.PointerProperty(type=OOTDynamicTransformProperty)
customDLName: bpy.props.StringProperty(name="Custom DL", default="gEmptyDL")
class OOTSkeletonProperty(bpy.types.PropertyGroup):
LOD: bpy.props.PointerProperty(type=bpy.types.Object, poll=pollArmature)
class OOTSkeleton:
def __init__(self, name):
self.name = name
@@ -161,8 +204,21 @@ class OOTSkeleton:
return limbData
class OOTDLReference:
def __init__(self, name: str):
self.name = name
class OOTLimb:
def __init__(self, skeletonName, boneName, index, translation, DL, lodDL):
def __init__(
self,
skeletonName: str,
boneName: str,
index: int,
translation: mathutils.Vector,
DL: GfxList | OOTDLReference,
lodDL: GfxList | OOTDLReference,
):
self.skeletonName = skeletonName
self.boneName = boneName
self.translation = translation
@@ -280,10 +336,18 @@ def getGroupIndexOfVert(vert, armatureObj, obj, rootGroupIndex):
nonBoneGroups.append(groupName)
if len(actualGroups) == 0:
return rootGroupIndex
# return rootGroupIndex
# highlightWeightErrors(obj, [vert], "VERT")
# raise VertexWeightError("All vertices must be part of a vertex group that corresponds to a bone in the armature.\n" +\
# "Groups of the bad vert that don't correspond to a bone: " + str(nonBoneGroups) + '. If a vert is supposed to belong to this group then either a bone is missing or you have the wrong group.')
if len(nonBoneGroups) > 0:
raise VertexWeightError(
"All vertices must be part of a vertex group "
+ "that corresponds to a bone in the armature.\n"
+ "Groups of the bad vert that don't correspond to a bone: "
+ str(nonBoneGroups)
+ ". If a vert is supposed to belong to this group then either a bone is missing or you have the wrong group."
)
else:
raise VertexWeightError("There are unweighted vertices in the mesh that must be weighted to a bone.")
vertGroup = actualGroups[0]
for group in actualGroups:
@@ -294,7 +358,7 @@ def getGroupIndexOfVert(vert, armatureObj, obj, rootGroupIndex):
return vertGroup.group
def ootDuplicateArmature(originalArmatureObj):
def ootDuplicateArmatureAndRemoveRotations(originalArmatureObj: bpy.types.Object):
# Duplicate objects to apply scale / modifiers / linked data
bpy.ops.object.select_all(action="DESELECT")
@@ -319,6 +383,8 @@ def ootDuplicateArmature(originalArmatureObj):
bpy.context.view_layer.objects.active = armatureObj
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True, properties=False)
ootRemoveRotationsFromArmature(armatureObj)
# Apply modifiers/data to mesh objs
bpy.ops.object.select_all(action="DESELECT")
for obj in meshObjs:
@@ -365,6 +431,42 @@ def ootConvertArmatureToSkeletonWithMesh(
)
def ootRemoveRotationsFromArmature(armatureObj: bpy.types.Object) -> None:
checkForStartBone(armatureObj)
startBoneName = getStartBone(armatureObj)
if bpy.context.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT")
for editBone in armatureObj.data.edit_bones:
editBone.use_connect = False
bpy.ops.object.mode_set(mode="OBJECT")
ootRemoveRotationsFromBone(armatureObj, armatureObj.data.bones[startBoneName])
armatureApplyWithMesh(armatureObj, bpy.context)
# TODO: check for bone type?
def ootRemoveRotationsFromBone(armatureObj: bpy.types.Object, bone: bpy.types.Bone):
for childBone in bone.children:
ootRemoveRotationsFromBone(armatureObj, childBone)
yUpToZUp = mathutils.Quaternion((1, 0, 0), math.radians(90.0)).to_matrix().to_4x4()
if bone.parent is not None:
transform = bone.parent.matrix_local.inverted() @ bone.matrix_local
else:
transform = bone.matrix_local
# extract local transform, excluding rotation/scale
# apply the inverse of that to the pose bone to get it to zero-rotation rest pose
translate = mathutils.Matrix.Translation(transform.decompose()[0])
undoRotationTransform = transform.inverted() @ translate
if bone.parent is None:
undoRotationTransform = undoRotationTransform @ yUpToZUp
poseBone = armatureObj.pose.bones[bone.name]
poseBone.matrix_basis = undoRotationTransform
def ootConvertArmatureToSkeleton(
originalArmatureObj,
convertTransformMatrix,
@@ -377,7 +479,7 @@ def ootConvertArmatureToSkeleton(
):
checkEmptyName(name)
armatureObj, meshObjs = ootDuplicateArmature(originalArmatureObj)
armatureObj, meshObjs = ootDuplicateArmatureAndRemoveRotations(originalArmatureObj)
try:
skeleton = OOTSkeleton(name)
@@ -473,7 +575,7 @@ def ootProcessBone(
optimize,
)
if bone.ootBoneType == "Custom DL":
if bone.ootBone.boneType == "Custom DL":
if mesh is not None:
raise PluginError(
bone.name
@@ -481,7 +583,7 @@ def ootProcessBone(
)
else:
# Dummy data, only used so that name is set correctly
mesh = FMesh(bone.ootCustomDLName, DLFormat.Static)
mesh = FMesh(bone.ootBone.customDLName, DLFormat.Static)
DL = None
if mesh is not None:
@@ -492,6 +594,12 @@ def ootProcessBone(
)
DL = mesh.draw
# Some skeletons will override the current drawn DL for a limb.
# If an override DL is not NULL but the non-override is NULL, then this causes issues.
# Thus for cases where we remove geometry, we need to have a dummy DL.
elif bone.use_deform:
DL = OOTDLReference("gEmptyDL")
if isinstance(parentLimb, OOTSkeleton):
skeleton = parentLimb
limb = OOTLimb(skeleton.name, boneName, nextIndex, translate, DL, None)
@@ -539,11 +647,25 @@ def ootConvertArmatureToC(
drawLayer: str,
settings: OOTSkeletonExportSettings,
):
folderName = settings.folder
if settings.mode != "Generic" and not settings.isCustom:
importInfo = ootSkeletonImportDict[settings.mode]
skeletonName = importInfo.skeletonName
folderName = importInfo.folderName
overlayName = importInfo.actorOverlayName
flipbookUses2DArray = importInfo.flipbookArrayIndex2D is not None
flipbookArrayIndex2D = importInfo.flipbookArrayIndex2D
isLink = importInfo.isLink
else:
skeletonName = toAlnum(settings.name)
folderName = settings.folder
overlayName = settings.actorOverlayName if not settings.isCustom else None
flipbookUses2DArray = settings.flipbookUses2DArray
flipbookArrayIndex2D = settings.flipbookArrayIndex2D if flipbookUses2DArray else None
isLink = False
exportPath = bpy.path.abspath(settings.customPath)
isCustomExport = settings.isCustom
removeVanillaData = settings.removeVanillaData
skeletonName = toAlnum(settings.name)
optimize = settings.optimize
fModel = OOTModel(f3dType, isHWv1, skeletonName, DLFormat, drawLayer)
@@ -551,9 +673,9 @@ def ootConvertArmatureToC(
originalArmatureObj, convertTransformMatrix, fModel, skeletonName, not savePNG, drawLayer, optimize
)
if originalArmatureObj.ootFarLOD is not None:
if originalArmatureObj.ootSkeleton.LOD is not None:
lodSkeleton, fModel = ootConvertArmatureToSkeletonWithMesh(
originalArmatureObj.ootFarLOD,
originalArmatureObj.ootSkeleton.LOD,
convertTransformMatrix,
fModel,
skeletonName + "_lod",
@@ -573,7 +695,7 @@ def ootConvertArmatureToC(
raise PluginError(
originalArmatureObj.name
+ " cannot use "
+ originalArmatureObj.ootFarLOD.name
+ originalArmatureObj.ootSkeleton.LOD.name
+ "as LOD because they do not have the same bone structure."
)
@@ -598,9 +720,14 @@ def ootConvertArmatureToC(
data.append(exportData.all())
data.append(skeletonC)
if isCustomExport:
textureArrayData = writeTextureArraysNew(fModel, flipbookArrayIndex2D)
data.append(textureArrayData)
writeCData(data, os.path.join(path, skeletonName + ".h"), os.path.join(path, skeletonName + ".c"))
if not isCustomExport:
writeTextureArraysExisting(bpy.context.scene.ootDecompPath, overlayName, isLink, flipbookArrayIndex2D, fModel)
addIncludeFiles(folderName, path, skeletonName)
if removeVanillaData:
ootRemoveSkeleton(path, folderName, skeletonName)
@@ -675,36 +802,107 @@ def ootGetLimb(skeletonData, limbName, continueOnError):
return matchResult
def ootImportSkeletonC(
filepaths: list[str], actorScale: float, basePath: str, importSettings: OOTSkeletonImportSettings
):
skeletonName = importSettings.name
def ootImportSkeletonC(basePath: str, importSettings: OOTSkeletonImportSettings):
importPath = bpy.path.abspath(importSettings.customPath)
isCustomImport = importSettings.isCustom
if importSettings.mode != "Generic" and not importSettings.isCustom:
importInfo = ootSkeletonImportDict[importSettings.mode]
skeletonName = importInfo.skeletonName
folderName = importInfo.folderName
overlayName = importInfo.actorOverlayName
flipbookUses2DArray = importInfo.flipbookArrayIndex2D is not None
flipbookArrayIndex2D = importInfo.flipbookArrayIndex2D
isLink = importInfo.isLink
restPoseData = importInfo.restPoseData
else:
skeletonName = importSettings.name
folderName = importSettings.folder
overlayName = importSettings.actorOverlayName if not importSettings.isCustom else None
flipbookUses2DArray = importSettings.flipbookUses2DArray
flipbookArrayIndex2D = importSettings.flipbookArrayIndex2D if flipbookUses2DArray else None
isLink = False
restPoseData = None
filepaths = [ootGetObjectPath(isCustomImport, importPath, folderName)]
removeDoubles = importSettings.removeDoubles
importNormals = importSettings.importNormals
drawLayer = importSettings.drawLayer
skeletonData = getImportData(filepaths)
if overlayName is not None or isLink:
skeletonData = ootGetIncludedAssetData(basePath, filepaths, skeletonData) + skeletonData
matchResult = ootGetSkeleton(skeletonData, skeletonName, False)
limbsName = matchResult.group(2)
matchResult = ootGetLimbs(skeletonData, limbsName, False)
limbsData = matchResult.group(2)
limbList = [entry.strip()[1:] for entry in limbsData.split(",")]
limbList = [entry.strip()[1:] for entry in limbsData.split(",") if entry.strip() != ""]
f3dContext = OOTF3DContext(F3D("F3DEX2/LX2", False), limbList, basePath)
f3dContext.mat().draw_layer.oot = drawLayer
if overlayName is not None and importSettings.autoDetectActorScale:
actorScale = ootReadActorScale(basePath, overlayName, isLink)
else:
actorScale = getOOTScale(importSettings.actorScale)
# print(limbList)
isLOD, armatureObj = ootBuildSkeleton(
skeletonName, skeletonData, limbList, actorScale, removeDoubles, importNormals, False, basePath, drawLayer
skeletonName,
overlayName,
skeletonData,
actorScale,
removeDoubles,
importNormals,
False,
basePath,
drawLayer,
isLink,
flipbookArrayIndex2D,
f3dContext,
)
if isLOD:
isLOD, LODArmatureObj = ootBuildSkeleton(
skeletonName, skeletonData, limbList, actorScale, removeDoubles, importNormals, True, basePath, drawLayer
skeletonName,
overlayName,
skeletonData,
actorScale,
removeDoubles,
importNormals,
True,
basePath,
drawLayer,
isLink,
flipbookArrayIndex2D,
f3dContext,
)
armatureObj.ootFarLOD = LODArmatureObj
armatureObj.ootSkeleton.LOD = LODArmatureObj
LODArmatureObj.location += mathutils.Vector((10, 0, 0))
f3dContext.deleteMaterialContext()
if importSettings.applyRestPose and restPoseData is not None:
applySkeletonRestPose(restPoseData, armatureObj)
if isLOD:
applySkeletonRestPose(restPoseData, LODArmatureObj)
def ootBuildSkeleton(
skeletonName, skeletonData, limbList, actorScale, removeDoubles, importNormals, useFarLOD, basePath, drawLayer
skeletonName,
overlayName,
skeletonData,
actorScale,
removeDoubles,
importNormals,
useFarLOD,
basePath,
drawLayer,
isLink,
flipbookArrayIndex2D: int,
f3dContext: OOTF3DContext,
):
lodString = "_lod" if useFarLOD else ""
@@ -724,8 +922,11 @@ def ootBuildSkeleton(
bpy.context.view_layer.objects.active = armatureObj
# bpy.ops.object.mode_set(mode = 'EDIT')
f3dContext = OOTF3DContext(F3D("F3DEX2/LX2", False), limbList, basePath)
f3dContext.mat().draw_layer.oot = armatureObj.ootDrawLayer
if overlayName is not None:
ootReadTextureArrays(basePath, overlayName, skeletonName, f3dContext, isLink, flipbookArrayIndex2D)
transformMatrix = mathutils.Matrix.Scale(1 / actorScale, 4)
isLOD = ootAddLimbRecursively(0, skeletonData, obj, armatureObj, transformMatrix, None, f3dContext, useFarLOD)
for dlEntry in f3dContext.dlList:
@@ -734,18 +935,17 @@ def ootBuildSkeleton(
parseF3D(
skeletonData,
dlEntry.dlName,
obj,
f3dContext.matrixData[limbName],
limbName,
boneName,
"oot",
drawLayer,
f3dContext,
True,
)
if f3dContext.isBillboard:
armatureObj.data.bones[boneName].ootDynamicTransform.billboard = True
f3dContext.clearMaterial() # THIS IS IMPORTANT
f3dContext.createMesh(obj, removeDoubles, importNormals)
armatureObj.data.bones[boneName].ootBone.dynamicTransform.billboard = True
f3dContext.createMesh(obj, removeDoubles, importNormals, False)
armatureObj.location = bpy.context.scene.cursor.location
# Set bone rotation mode.
@@ -766,6 +966,7 @@ def ootBuildSkeleton(
bpy.ops.object.parent_set(type="ARMATURE")
applyRotation([armatureObj], math.radians(-90), "X")
armatureObj.ootActorScale = actorScale / bpy.context.scene.ootBlenderScale
return isLOD, armatureObj
@@ -778,6 +979,7 @@ def ootAddBone(armatureObj, boneName, parentBoneName, currentTransform, loadDL):
bpy.ops.object.mode_set(mode="EDIT")
bone = armatureObj.data.edit_bones.new(boneName)
bone.use_connect = False
bone.use_deform = loadDL
if parentBoneName is not None:
bone.parent = armatureObj.data.edit_bones[parentBoneName]
bone.head = currentTransform @ mathutils.Vector((0, 0, 0))
@@ -802,9 +1004,15 @@ def ootAddBone(armatureObj, boneName, parentBoneName, currentTransform, loadDL):
def ootAddLimbRecursively(
limbIndex, skeletonData, obj, armatureObj, parentTransform, parentBoneName, f3dContext, useFarLOD
limbIndex: int,
skeletonData: str,
obj: bpy.types.Object,
armatureObj: bpy.types.Object,
parentTransform: mathutils.Matrix,
parentBoneName: str,
f3dContext: OOTF3DContext,
useFarLOD: bool,
):
limbName = f3dContext.getLimbName(limbIndex)
boneName = f3dContext.getBoneName(limbIndex)
matchResult = ootGetLimb(skeletonData, limbName, False)
@@ -845,7 +1053,6 @@ def ootAddLimbRecursively(
# Therefore were delay F3D parsing until after skeleton is processed.
if loadDL:
f3dContext.dlList.append(OOTDLEntry(dlName, limbIndex))
# parseF3D(skeletonData, dlName, obj, transformMatrix, boneName, f3dContext)
if nextChildIndex != LIMB_DONE:
isLOD |= ootAddLimbRecursively(
@@ -885,7 +1092,7 @@ def ootRemoveSkeleton(filepath, objectName, skeletonName):
return
skeletonDataC = skeletonDataC[: matchResult.start(0)] + skeletonDataC[matchResult.end(0) :]
limbsData = matchResult.group(2)
limbList = [entry.strip()[1:] for entry in limbsData.split(",")]
limbList = [entry.strip()[1:] for entry in limbsData.split(",") if entry.strip() != ""]
headerMatch = getDeclaration(skeletonDataH, limbsName)
if headerMatch is not None:
@@ -915,26 +1122,14 @@ class OOT_ImportSkeleton(bpy.types.Operator):
# Called on demand (i.e. button press, menu item)
# Can also be called from operator search menu (Spacebar)
def execute(self, context):
armatureObj = None
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
try:
importSettings: OOTSkeletonImportSettings = context.scene.fast64.oot.skeletonImportSettings
importPath = bpy.path.abspath(importSettings.customPath)
isCustomImport = importSettings.isCustom
folderName = importSettings.folder
scale = context.scene.ootActorBlenderScale
decompPath = bpy.path.abspath(bpy.context.scene.ootDecompPath)
filepaths = [ootGetObjectPath(isCustomImport, importPath, folderName)]
if not isCustomImport:
filepaths.append(
os.path.join(bpy.context.scene.ootDecompPath, "assets/objects/gameplay_keep/gameplay_keep.c")
)
ootImportSkeletonC(filepaths, scale, decompPath, importSettings)
ootImportSkeletonC(decompPath, importSettings)
self.report({"INFO"}, "Success!")
return {"FINISHED"}
@@ -968,7 +1163,14 @@ class OOT_ExportSkeleton(bpy.types.Operator):
raise PluginError("Armature does not have any mesh children, or " + "has a non-mesh child.")
obj = armatureObj.children[0]
finalTransform = mathutils.Matrix.Scale(context.scene.ootActorBlenderScale, 4)
finalTransform = mathutils.Matrix.Scale(getOOTScale(armatureObj.ootActorScale), 4)
# Rotation must be applied before exporting skeleton.
# For some reason this does not work if done on the duplicate generated later, so we have to do it before then.
bpy.ops.object.select_all(action="DESELECT")
armatureObj.select_set(True)
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True, properties=False)
bpy.ops.object.select_all(action="DESELECT")
try:
exportSettings: OOTSkeletonExportSettings = context.scene.fast64.oot.skeletonExportSettings
@@ -1002,33 +1204,67 @@ class OOT_ExportSkeletonPanel(OOT_Panel):
col.operator(OOT_ExportSkeleton.bl_idname)
exportSettings: OOTSkeletonExportSettings = context.scene.fast64.oot.skeletonExportSettings
prop_split(col, exportSettings, "name", "Skeleton")
prop_split(col, exportSettings, "folder", "Object" if not exportSettings.isCustom else "Folder")
if exportSettings.isCustom:
prop_split(col, exportSettings, "customAssetIncludeDir", "Asset Include Path")
prop_split(col, exportSettings, "customPath", "Path")
col.prop(exportSettings, "isCustom")
col.prop(exportSettings, "removeVanillaData")
col.prop(exportSettings, "optimize")
if exportSettings.optimize:
b = col.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.")
col.prop(exportSettings, "isCustom")
if exportSettings.isCustom:
prop_split(col, exportSettings, "name", "Skeleton")
prop_split(col, exportSettings, "folder", "Object" if not exportSettings.isCustom else "Folder")
prop_split(col, exportSettings, "customAssetIncludeDir", "Asset Include Path")
prop_split(col, exportSettings, "customPath", "Path")
else:
prop_split(col, exportSettings, "mode", "Mode")
if exportSettings.mode == "Generic":
prop_split(col, exportSettings, "name", "Skeleton")
prop_split(col, exportSettings, "folder", "Object" if not exportSettings.isCustom else "Folder")
prop_split(col, exportSettings, "actorOverlayName", "Overlay")
col.prop(exportSettings, "flipbookUses2DArray")
if exportSettings.flipbookUses2DArray:
box = col.box().column()
prop_split(box, exportSettings, "flipbookArrayIndex2D", "Flipbook Index")
elif exportSettings.mode == "Adult Link" or exportSettings.mode == "Child Link":
col.label(text="Requires enabling NON_MATCHING in Makefile.", icon="ERROR")
col.label(text="Preserve all bone deform toggles if modifying an imported skeleton.", icon="ERROR")
col.operator(OOT_ImportSkeleton.bl_idname)
importSettings: OOTSkeletonImportSettings = context.scene.fast64.oot.skeletonImportSettings
prop_split(col, importSettings, "name", "Skeleton")
if importSettings.isCustom:
prop_split(col, importSettings, "customPath", "File")
else:
prop_split(col, importSettings, "folder", "Object")
prop_split(col, importSettings, "drawLayer", "Import Draw Layer")
col.prop(importSettings, "isCustom")
col.prop(importSettings, "removeDoubles")
col.prop(importSettings, "importNormals")
col.prop(importSettings, "isCustom")
if importSettings.isCustom:
prop_split(col, importSettings, "name", "Skeleton")
prop_split(col, importSettings, "customPath", "File")
else:
prop_split(col, importSettings, "mode", "Mode")
if importSettings.mode == "Generic":
prop_split(col, importSettings, "name", "Skeleton")
prop_split(col, importSettings, "folder", "Object")
prop_split(col, importSettings, "actorOverlayName", "Overlay")
col.prop(importSettings, "autoDetectActorScale")
if not importSettings.autoDetectActorScale:
prop_split(col, importSettings, "actorScale", "Actor Scale")
col.prop(importSettings, "flipbookUses2DArray")
if importSettings.flipbookUses2DArray:
box = col.box().column()
prop_split(box, importSettings, "flipbookArrayIndex2D", "Flipbook Index")
if importSettings.actorOverlayName == "ovl_En_Wf":
col.box().column().label(
text="This actor has branching gSPSegment calls and will not import correctly unless one of the branches is deleted.",
icon="ERROR",
)
elif importSettings.actorOverlayName == "ovl_Obj_Switch":
col.box().column().label(
text="This actor has a 2D texture array and will not import correctly unless the array is flattened.",
icon="ERROR",
)
else:
col.prop(importSettings, "applyRestPose")
class OOT_SkeletonPanel(bpy.types.Panel):
@@ -1053,9 +1289,10 @@ class OOT_SkeletonPanel(bpy.types.Panel):
col = self.layout.box().column()
col.box().label(text="OOT Skeleton Inspector")
prop_split(col, context.object, "ootDrawLayer", "Draw Layer")
prop_split(col, context.object, "ootFarLOD", "LOD Skeleton")
if context.object.ootFarLOD is not None:
prop_split(col, context.object.ootSkeleton, "LOD", "LOD Skeleton")
if context.object.ootSkeleton.LOD is not None:
col.label(text="Make sure LOD has same bone structure.", icon="BONE_DATA")
prop_split(col, context.object, "ootActorScale", "Actor Scale")
class OOT_BonePanel(bpy.types.Panel):
@@ -1074,18 +1311,14 @@ class OOT_BonePanel(bpy.types.Panel):
def draw(self, context):
col = self.layout.box().column()
col.box().label(text="OOT Bone Inspector")
prop_split(col, context.bone, "ootBoneType", "Bone Type")
if context.bone.ootBoneType == "Custom DL":
prop_split(col, context.bone, "ootCustomDLName", "DL Name")
if context.bone.ootBoneType == "Custom DL" or context.bone.ootBoneType == "Ignore":
prop_split(col, context.bone.ootBone, "boneType", "Bone Type")
if context.bone.ootBone.boneType == "Custom DL":
prop_split(col, context.bone.ootBone, "customDLName", "DL Name")
if context.bone.ootBone.boneType == "Custom DL" or context.bone.ootBone.boneType == "Ignore":
col.label(text="Make sure no geometry is skinned to this bone.", icon="BONE_DATA")
if context.bone.ootBoneType != "Ignore":
col.prop(context.bone.ootDynamicTransform, "billboard")
def pollArmature(self, obj):
return isinstance(obj.data, bpy.types.Armature)
if context.bone.ootBone.boneType != "Ignore":
col.prop(context.bone.ootBone.dynamicTransform, "billboard")
oot_skeleton_classes = (
@@ -1093,6 +1326,9 @@ oot_skeleton_classes = (
OOT_ImportSkeleton,
OOTSkeletonExportSettings,
OOTSkeletonImportSettings,
OOT_SaveRestPose,
OOTBoneProperty,
OOTSkeletonProperty,
)
oot_skeleton_panels = (
@@ -1116,20 +1352,16 @@ def oot_skeleton_register():
for cls in oot_skeleton_classes:
register_class(cls)
bpy.types.Object.ootFarLOD = bpy.props.PointerProperty(type=bpy.types.Object, poll=pollArmature)
bpy.types.Bone.ootBoneType = bpy.props.EnumProperty(name="Bone Type", items=ootEnumBoneType)
bpy.types.Bone.ootDynamicTransform = bpy.props.PointerProperty(type=OOTDynamicTransformProperty)
bpy.types.Bone.ootCustomDLName = bpy.props.StringProperty(name="Custom DL", default="gEmptyDL")
bpy.types.Object.ootActorScale = bpy.props.FloatProperty(min=0, default=100)
bpy.types.Object.ootSkeleton = bpy.props.PointerProperty(type=OOTSkeletonProperty)
bpy.types.Bone.ootBone = bpy.props.PointerProperty(type=OOTBoneProperty)
def oot_skeleton_unregister():
del bpy.types.Object.ootFarLOD
del bpy.types.Bone.ootBoneType
del bpy.types.Bone.ootDynamicTransform
del bpy.types.Bone.ootCustomDLName
del bpy.types.Object.ootActorScale
del bpy.types.Bone.ootBone
del bpy.types.Object.ootSkeleton
for cls in reversed(oot_skeleton_classes):
unregister_class(cls)
@@ -0,0 +1,176 @@
from collections import OrderedDict
import bpy, mathutils
from ..utility import PluginError, raisePluginError
from .oot_utility import getSortedChildren, getStartBone, getNextBone
# Adding new rest pose entry:
# 1. Import a generic skeleton
# 2. Pose into a usable rest pose
# 3. Select skeleton, then run bpy.ops.object.oot_save_rest_pose()
# 4. Copy array data from console into an OOTSkeletonImportInfo object
# - list of tuples, first is root position, rest are euler XYZ rotations
# 5. Add object to ootSkeletonImportDict
def applySkeletonRestPose(boneData: list[tuple[float, float, float]], armatureObj: bpy.types.Object):
if bpy.context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action="DESELECT")
armatureObj.select_set(True)
bpy.ops.object.mode_set(mode="POSE")
startBoneName = getStartBone(armatureObj)
boneStack = [startBoneName]
index = 0
while len(boneStack) > 0:
bone, boneStack = getNextBone(boneStack, armatureObj)
poseBone = armatureObj.pose.bones[bone.name]
if index == 0:
poseBone.location = mathutils.Vector(boneData[index])
poseBone.rotation_mode = "XYZ"
poseBone.rotation_euler = mathutils.Euler(boneData[index + 1])
index += 1
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.armature_apply_w_mesh()
# Copy data from console into python file
class OOT_SaveRestPose(bpy.types.Operator):
# set bl_ properties
bl_idname = "object.oot_save_rest_pose"
bl_label = "Save Rest Pose"
bl_options = {"REGISTER", "UNDO"}
# path: bpy.props.StringProperty(name="Path", subtype="FILE_PATH")
def execute(self, context):
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
if len(context.selected_objects) == 0:
raise PluginError("Armature not selected.")
armatureObj = context.active_object
if type(armatureObj.data) is not bpy.types.Armature:
raise PluginError("Armature not selected.")
try:
data = "restPoseData = [\n"
startBoneName = getStartBone(armatureObj)
boneStack = [startBoneName]
firstBone = True
while len(boneStack) > 0:
bone, boneStack = getNextBone(boneStack, armatureObj)
poseBone = armatureObj.pose.bones[bone.name]
if firstBone:
data += str(poseBone.matrix_basis.decompose()[0][:]) + ", "
firstBone = False
data += str((poseBone.matrix_basis.decompose()[1]).to_euler()[:]) + ", "
data += "\n]"
print(data)
self.report({"INFO"}, "Success!")
return {"FINISHED"}
except Exception as e:
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
raisePluginError(self, e)
return {"CANCELLED"} # must return a set
# Link overlay will be "", since link texture array data is handled as a special case.
class OOTSkeletonImportInfo:
def __init__(
self,
skeletonName: str,
folderName: str,
actorOverlayName: str,
flipbookArrayIndex2D: int | None,
restPoseData: list[tuple[float, float, float]] | None,
):
self.skeletonName = skeletonName
self.folderName = folderName
self.actorOverlayName = actorOverlayName # Note that overlayName = None will disable texture array reading.
self.flipbookArrayIndex2D = flipbookArrayIndex2D
self.isLink = skeletonName == "gLinkAdultSkel" or skeletonName == "gLinkChildSkel"
self.restPoseData = restPoseData
ootSkeletonImportDict = OrderedDict(
{
"Adult Link": OOTSkeletonImportInfo(
"gLinkAdultSkel",
"object_link_boy",
"",
0,
[
(0.0, 3.6050000190734863, 0.0),
(0.0, -0.0, 0.0),
(-1.5708922147750854, -0.0, -1.5707963705062866),
(0.0, -0.0, 0.0),
(0.0, 0.05235987901687622, 0.0),
(0.0, -0.0, 0.0),
(0.0, 0.0, -1.5707964897155762),
(0.0, -0.05235987901687622, 0.0),
(0.0, -0.0, 0.0),
(0.0, 0.0, -1.5707964897155762),
(1.5707963705062866, -0.0, 1.5707963705062866),
(-4.740638548383913e-09, -5.356494803265832e-09, 1.4546878337860107),
(-4.114889869409654e-15, -1.1733899984468776e-14, 1.9080803394317627),
(0.0, -0.0, 0.0),
(1.0222795112391236e-15, -0.6981316804885864, -3.141592502593994),
(0.0, -0.0, 0.0),
(0.0, 0.0, -1.5707964897155762),
(-1.0222795112391236e-15, 0.6981316804885864, -3.141592502593994),
(0.0, -0.0, 0.0),
(0.0, 0.0, -1.5707964897155762),
(-1.5707963705062866, 2.611602306365967, -0.08726644515991211),
(0.0, -0.0, 0.0),
],
),
"Child Link": OOTSkeletonImportInfo(
"gLinkChildSkel",
"object_link_child",
"",
1,
[
(0.0, 2.3559017181396484, 0.0),
(0.0, -0.0, 0.0),
(-1.5708922147750854, -0.0, -1.5707963705062866),
(0.0, -0.0, 0.0),
(0.0, 0.05235987901687622, 0.0),
(0.0, -0.0, 0.0),
(0.0, 0.0, -1.5707964897155762),
(0.0, -0.05235987901687622, 0.0),
(0.0, -0.0, 0.0),
(0.0, 0.0, -1.5707964897155762),
(1.5707963705062866, -0.0, 1.5707963705062866),
(-4.740638548383913e-09, -5.356494803265832e-09, 1.4546878337860107),
(-4.114889869409654e-15, -1.1733899984468776e-14, 1.9080803394317627),
(0.0, -0.0, 0.0),
(1.0222795112391236e-15, -0.6981316804885864, -3.141592502593994),
(0.0, -0.0, 0.0),
(0.0, 0.0, -1.5707964897155762),
(-1.0222795112391236e-15, 0.6981316804885864, -3.141592502593994),
(0.0, -0.0, 0.0),
(0.0, 0.0, -1.5707964897155762),
(-1.5707963705062866, 2.611602306365967, -0.08726644515991211),
(0.0, -0.0, 0.0),
],
),
# "Gerudo": OOTSkeletonImportInfo("gGerudoRedSkel", "object_geldb", "ovl_En_GeldB", None, None),
}
)
ootEnumSkeletonImportMode = [
("Generic", "Generic", "Generic"),
]
for name, info in ootSkeletonImportDict.items():
ootEnumSkeletonImportMode.append((name, name, name))
+240
View File
@@ -0,0 +1,240 @@
from typing import Callable
import os, re
from ..utility import hexOrDecInt
from .oot_model_classes import (
OOTF3DContext,
TextureFlipbook,
ootGetActorData,
ootGetActorDataPaths,
ootGetIncludedAssetData,
ootGetLinkData,
)
# Special cases:
# z_en_xc: one texture is not stored in any array.
# skeletonName only used for en_ossan (shopkeepers) and demo_ec (end credits party), which have multiple skeletons
def ootReadTextureArrays(
basePath: str,
overlayName: str,
skeletonName: str,
f3dContext: OOTF3DContext,
isLink: bool,
flipbookArrayIndex2D: int,
):
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
# search for texture arrays
# this is done first so that its easier to tell which gSPSegment calls refer to texture data.
flipbookList = getTextureArrays(actorData, flipbookArrayIndex2D)
if not isLink and overlayName == "ovl_En_Ossan":
# remove function declarations
actorData = re.sub(r"void\s*EnOssan\_(((?!\{).)*)?\)\s*;", "", actorData)
ootReadTextureArraysFromMultiple(
flipbookList, skeletonName, actorData, f3dContext, "EnOssan", getSPSegmentCalls, flipbookArrayIndex2D
)
elif not isLink and overlayName == "ovl_Demo_Ec":
ootReadTextureArraysFromMultiple(
flipbookList, skeletonName, actorData, f3dContext, "DemoEc", getSPSegmentCallsDemoEc, flipbookArrayIndex2D
)
else:
ootReadTextureArraysGeneric(flipbookList, actorData, getSPSegmentCalls, f3dContext)
# we return when no matches found to handle cases where actor does not have dynamic textures.
def ootReadTextureArraysFromMultiple(
flipbookList: dict[str, TextureFlipbook],
skeletonName: str,
actorData: str,
f3dContext: OOTF3DContext,
functionPrefix: str,
getSegmentCallsFunc: Callable[[str], None],
flipbookArrayIndex2D: int,
):
# regex should ignore DemoEc_Init()
# relies on formatting convention (tabs indicating bracket scope)
initMatch = re.search(
r"void\s*"
+ re.escape(functionPrefix)
+ r"\_Init(?!SkelAnime)(((?![\s\(]).)*?)\((((?!\n\}).)*?)"
+ re.escape(skeletonName),
actorData,
flags=re.DOTALL,
)
if not initMatch:
return
# relies on formatting convention (tabs indicating scope of bracket)
name = initMatch.group(1)
drawMatch = re.search(
r"void\s*" + re.escape(functionPrefix) + r"\_Draw" + re.escape(name) + r"\s*\((.*?)\n\}",
actorData,
flags=re.DOTALL,
)
if not drawMatch:
return
drawData = drawMatch.group(1)
flipbookList = getTextureArrays(drawData, flipbookArrayIndex2D)
ootReadTextureArraysGeneric(flipbookList, drawData, getSegmentCallsFunc, f3dContext)
def ootReadTextureArraysGeneric(
flipbookList: dict[str, TextureFlipbook],
actorData: str,
getSegmentCallsFunc: Callable[[str], None],
f3dContext: OOTF3DContext,
):
# find gSPSegment() calls that reference texture arrays
for (flipbookKey, segmentParam, spSegmentMatch) in getSegmentCallsFunc(actorData):
# check for texture array reference
for (arrayName, flipbook) in flipbookList.items():
directArrayReference = findDirectArrayReference(arrayName, segmentParam)
indexIntoArrayReference = findIndexIntoArrayReference(arrayName, segmentParam, actorData)
if directArrayReference or indexIntoArrayReference:
f3dContext.flipbooks[flipbookKey] = flipbook
# check if single non-array texture reference (ex. z_en_ta, which uses a different texture in z_demo_ec (red nose))
# This is will not get correct texture name, but otherwise works fine.
if flipbookKey not in f3dContext.flipbooks and findSingleTextureReference(segmentParam):
f3dContext.flipbooks[flipbookKey] = TextureFlipbook("", "Individual", [segmentParam])
# check if array is directly referenced in gSPSegment
# SEGMENTED_TO_VIRTUAL(arrayName[...])
def findDirectArrayReference(arrayName: str, segmentParam: str) -> re.Match:
return re.search(re.escape(arrayName) + r"\s*\[", segmentParam, flags=re.DOTALL)
# check if an array element is referenced in gSPSegment
# void* segmentParam = arrayName[...];
# SEGMENTED_TO_VIRTUAL(segmentParam)
def findIndexIntoArrayReference(arrayName: str, segmentParam: str, actorData: str) -> re.Match:
return re.search(r"[a-zA-Z0-9\_]*", segmentParam, flags=re.DOTALL) and re.search(
r"void\s*\*\s*" + re.escape(segmentParam) + r"\s*=\s*" + re.escape(arrayName) + r"\s*\[",
actorData,
flags=re.DOTALL,
)
# check for single non-array reference
# convention: camel case starting with 'g'
# gSomeTexture
def findSingleTextureReference(segmentParam: str) -> re.Match:
return (
re.search(r"[a-zA-Z0-9\_]*", segmentParam, flags=re.DOTALL)
and segmentParam[0] == "g"
and segmentParam[1].isupper()
)
# check for texture arrays in data.
# void* ???[] = {a, b, c,}
def getTextureArrays(actorData: str, flipbookArrayIndex2D: int) -> dict[str, TextureFlipbook]:
flipbookList = {} # {array name : TextureFlipbook}
if flipbookArrayIndex2D is not None:
for texArray2DMatch in re.finditer(
r"void\s*\*\s*([0-9a-zA-Z\_]*)\s*\[\s*\]\s*\[[0-9a-fA-Fx]*\]\s*=\s*\{(.*?)\}\s*;",
actorData,
flags=re.DOTALL,
):
arrayMatchData = [
arrayMatch.group(1)
for arrayMatch in re.finditer(r"\{(((?!\}).)*)\}", texArray2DMatch.group(2), flags=re.DOTALL)
]
if flipbookArrayIndex2D >= len(arrayMatchData):
continue
arrayName = texArray2DMatch.group(1).strip()
textureList = stripComments([item for item in arrayMatchData[flipbookArrayIndex2D].split(",")])
# handle trailing comma
if textureList[-1] == "":
textureList.pop()
flipbookList[arrayName] = TextureFlipbook(arrayName, "Array", textureList)
else:
for texArrayMatch in re.finditer(
r"void\s*\*\s*([0-9a-zA-Z\_]*)\s*\[\s*\]\s*=\s*\{(((?!\}).)*)\}", actorData, flags=re.DOTALL
):
arrayName = texArrayMatch.group(1).strip()
textureList = stripComments([item for item in texArrayMatch.group(2).split(",")])
# handle trailing comma
if textureList[-1] == "":
textureList.pop()
flipbookList[arrayName] = TextureFlipbook(arrayName, "Array", textureList)
return flipbookList
def stripComments(textureNameList: list[str]) -> list[str]:
for i in range(len(textureNameList)):
try:
commentIndex = textureNameList[i].index("//")
except ValueError:
textureNameList[i] = textureNameList[i].strip()
else:
textureNameList[i] = re.sub(r"//.*?\n", "", textureNameList[i]).strip()
return textureNameList
def getSPSegmentCalls(actorData: str) -> list[tuple[tuple[int, str], str, re.Match]]:
segmentCalls = []
# find gSPSegment() calls that reference texture arrays
for spSegmentMatch in re.finditer(
r"gSPSegment\s*\(\s*POLY\_(OPA)?(XLU)?\_DISP\s*\+\+\s*,\s*([0-9a-fA-Fx]*)\s*,\s*SEGMENTED\_TO\_VIRTUAL\s*\(\s*(((?!;).)*)\)\s*\)\s*;",
actorData,
flags=re.DOTALL,
):
# see ootEnumDrawLayers
drawLayer = "Transparent" if spSegmentMatch.group(2) else "Opaque"
segment = hexOrDecInt(spSegmentMatch.group(3))
flipbookKey = (segment, drawLayer)
segmentParam = spSegmentMatch.group(4).strip()
segmentCalls.append((flipbookKey, segmentParam, spSegmentMatch))
return segmentCalls
# assumes DemoEc_DrawSkeleton()/DemoEc_DrawSkeletonCustomColor() is unmodified
def getSPSegmentCallsDemoEc(actorData: str) -> list[tuple[tuple[int, str], str, re.Match]]:
segmentCalls = getSPSegmentCalls(actorData)
functionMatch = re.search(
r"DemoEc_DrawSkeleton(CustomColor)?\s*\(.*?,.*?,(.*?),(.*?),", actorData, flags=re.DOTALL
)
if functionMatch:
isCustomColor = functionMatch.group(1) is not None
param1 = functionMatch.group(2).strip()
param2 = functionMatch.group(3).strip()
if param1 == "NULL" or param1 == "0":
param1 = None
if param2 == "NULL" or param2 == "0":
param2 = None
if isCustomColor:
if param1:
segmentCalls.append(((0x0A, "Opaque"), param1, functionMatch))
if param2:
segmentCalls.append(((0x0B, "Opaque"), param2, functionMatch))
else:
if param1:
segmentCalls.append(((0x08, "Opaque"), param1, functionMatch))
if not param2:
segmentCalls.append(((0x09, "Opaque"), param1, functionMatch))
if param2:
segmentCalls.append(((0x09, "Opaque"), param2, functionMatch))
return segmentCalls
+20 -3
View File
@@ -1,4 +1,4 @@
import bpy, math, os
import bpy, math, os, re
from bpy.utils import register_class, unregister_class
from ..utility import (
PluginError,
@@ -153,6 +153,14 @@ ootSceneDirs = {
}
def getOOTScale(actorScale: float) -> float:
return bpy.context.scene.ootBlenderScale * actorScale
def replaceMatchContent(data: str, newContent: str, match: re.Match, index: int) -> str:
return data[: match.start(index)] + newContent + data[match.end(index) :]
def addIncludeFiles(objectName, objectPath, assetName):
addIncludeFilesExtension(objectName, objectPath, assetName, "h")
addIncludeFilesExtension(objectName, objectPath, assetName, "c")
@@ -372,14 +380,14 @@ def ootGetPath(exportPath, isCustomExport, subPath, folderName, makeIfNotExists,
def getSortedChildren(armatureObj, bone):
return sorted(
[child.name for child in bone.children if child.ootBoneType != "Ignore"],
[child.name for child in bone.children if child.ootBone.boneType != "Ignore"],
key=lambda childName: childName.lower(),
)
def getStartBone(armatureObj):
startBoneNames = [
bone.name for bone in armatureObj.data.bones if bone.parent is None and bone.ootBoneType != "Ignore"
bone.name for bone in armatureObj.data.bones if bone.parent is None and bone.ootBone.boneType != "Ignore"
]
if len(startBoneNames) == 0:
raise PluginError(armatureObj.name + ' does not have any root bones that are not of the "Ignore" type.')
@@ -388,6 +396,15 @@ def getStartBone(armatureObj):
# return 'root'
def getNextBone(boneStack: list[str], armatureObj: bpy.types.Object):
if len(boneStack) == 0:
raise PluginError("More bones in animation than on armature.")
bone = armatureObj.data.bones[boneStack[0]]
boneStack = boneStack[1:]
boneStack = getSortedChildren(armatureObj, bone) + boneStack
return bone, boneStack
def checkForStartBone(armatureObj):
pass
# if "root" not in armatureObj.data.bones:
@@ -2590,9 +2590,9 @@ def saveSkinnedMeshByMaterial(
skinnedTriGroup.vertexList.vertices.append(
convertVertexData(
obj.data,
bufferVert.f3dVert[0],
bufferVert.f3dVert[1],
bufferVert.f3dVert[2],
bufferVert.f3dVert.position,
bufferVert.f3dVert.uv,
bufferVert.f3dVert.getColorOrNormal(),
texDimensions,
parentMatrix,
isPointSampled,
+3 -8
View File
@@ -2,7 +2,9 @@ import bpy, random, string, os, math, traceback, re, os, mathutils
from math import pi, ceil, degrees, radians
from mathutils import *
from .utility_anim import *
from typing import Callable, Iterable
from typing import Callable, Iterable, Any
CollectionProperty = Any # collection prop as defined by using bpy.props.CollectionProperty
class PluginError(Exception):
@@ -126,13 +128,6 @@ def parentObject(parent, child):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
def attemptModifierApply(modifier):
try:
bpy.ops.object.modifier_apply(modifier=modifier.name)
except Exception as e:
print("Skipping modifier " + str(modifier.name))
def getFMeshName(vertexGroup, namePrefix, drawLayer, isSkinned):
fMeshName = toAlnum(namePrefix + ("_" if namePrefix != "" else "") + vertexGroup)
if isSkinned:
+101
View File
@@ -1,4 +1,5 @@
import bpy, math, mathutils
from bpy.utils import register_class, unregister_class
from typing import TYPE_CHECKING
@@ -7,6 +8,91 @@ if TYPE_CHECKING:
from .. import Fast64Settings_Properties
class ArmatureApplyWithMeshOperator(bpy.types.Operator):
# set bl_ properties
bl_description = (
"Applies current pose as default pose. Useful for "
+ "rigging an armature that is not in T/A pose. Note that when using "
+ " with an SM64 armature, you must revert to the default pose after "
+ "skinning."
)
bl_idname = "object.armature_apply_w_mesh"
bl_label = "Apply As Rest Pose"
bl_options = {"REGISTER", "UNDO", "PRESET"}
# Called on demand (i.e. button press, menu item)
# Can also be called from operator search menu (Spacebar)
def execute(self, context):
try:
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
if len(context.selected_objects) == 0:
raise PluginError("Armature not selected.")
elif type(context.selected_objects[0].data) is not bpy.types.Armature:
raise PluginError("Armature not selected.")
armatureObj = context.selected_objects[0]
armatureApplyWithMesh(armatureObj, context)
except Exception as e:
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
raisePluginError(self, e)
return {"CANCELLED"}
self.report({"INFO"}, "Applied armature with mesh.")
return {"FINISHED"} # must return a set
# This code only handles root bone with no parent, which is the only bone that translates.
def getTranslationRelativeToRest(bone: bpy.types.Bone, inputVector: mathutils.Vector) -> mathutils.Vector:
zUpToYUp = mathutils.Quaternion((1, 0, 0), math.radians(-90.0)).to_matrix().to_4x4()
actualTranslation = (zUpToYUp @ bone.matrix_local).inverted() @ mathutils.Matrix.Translation(inputVector).to_4x4()
return actualTranslation.decompose()[0]
def getRotationRelativeToRest(bone: bpy.types.Bone, inputEuler: mathutils.Euler) -> mathutils.Euler:
if bone.parent is None:
parentRotation = mathutils.Quaternion((1, 0, 0), math.radians(90.0)).to_matrix().to_4x4()
else:
parentRotation = bone.parent.matrix_local
restRotation = (parentRotation.inverted() @ bone.matrix_local).decompose()[1].to_matrix().to_4x4()
return (restRotation.inverted() @ inputEuler.to_matrix().to_4x4()).to_euler("XYZ", inputEuler)
def attemptModifierApply(modifier):
try:
bpy.ops.object.modifier_apply(modifier=modifier.name)
except Exception as e:
print("Skipping modifier " + str(modifier.name))
def armatureApplyWithMesh(armatureObj: bpy.types.Object, context: bpy.types.Context):
for child in armatureObj.children:
if type(child.data) is not bpy.types.Mesh:
continue
armatureModifier = None
for modifier in child.modifiers:
if isinstance(modifier, bpy.types.ArmatureModifier):
armatureModifier = modifier
if armatureModifier is None:
continue
bpy.ops.object.select_all(action="DESELECT")
context.view_layer.objects.active = child
bpy.ops.object.modifier_copy(modifier=armatureModifier.name)
print(len(child.modifiers))
attemptModifierApply(armatureModifier)
bpy.ops.object.select_all(action="DESELECT")
context.view_layer.objects.active = armatureObj
bpy.ops.object.mode_set(mode="POSE")
bpy.ops.pose.armature_apply()
if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT")
class ValueFrameData:
def __init__(self, boneIndex, field, frames):
self.boneIndex = boneIndex
@@ -89,3 +175,18 @@ def getFrameInterval(action: bpy.types.Action):
}
return range_get_by_choice[anim_range_choice]()
classes = (ArmatureApplyWithMeshOperator,)
def utility_anim_register():
for cls in classes:
register_class(cls)
# called on add-on disabling
def utility_anim_unregister():
for cls in classes:
unregister_class(cls)
Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB