mirror of
https://github.com/ApfelTeeSaft/lightspeed64.git
synced 2026-08-26 19:33:24 +00:00
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:
co-authored by
Dragorn421
krm01
parent
fbb5d19085
commit
31a9680f5d
+8
-60
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -71,18 +71,56 @@ To import a skeletal mesh, just click "Import" for the armature importer. You ma
|
||||

|
||||

|
||||
|
||||
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.
|
||||

|
||||
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.
|
||||

|
||||
|
||||
### 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:**
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user