diff --git a/__init__.py b/__init__.py index caa71c75a..eb7f78e93 100644 --- a/__init__.py +++ b/__init__.py @@ -37,6 +37,8 @@ on_update_render_settings, ) +from .gltf_extension import * + # info about add on bl_info = { "name": "Fast64", @@ -145,6 +147,25 @@ def draw(self, context): col.operator(SM64_AddWaterBox.bl_idname) +glTFFormatEnum = ( + ( + "GLB", + "glTF Binary", + "(.glb) Exports a single file, with all data packed in binary form. Most efficient and portable, but more difficult to edit later.", + ), + ( + "GLTF_SEPARATE", + "glTF Separate", + "(.gltf + .bin + textures) Exports multiple files, with separate JSON, binary and texture data. Easiest to edit later.", + ), + ( + "GLTF_EMBEDDED", + "glTF Embedded", + "(.gltf) Exports a single file, with all data packed in JSON. Less efficient than binary, but easier to edit later.", + ), +) + + class F3D_GlobalSettingsPanel(bpy.types.Panel): bl_idname = "F3D_PT_global_settings" bl_label = "F3D Global Settings" @@ -200,12 +221,15 @@ def draw(self, context): col.prop(scene, "exportHiddenGeometry") col.prop(scene, "fullTraceback") prop_split(col, fast64_settings, "anim_range_choice", "Anim Range") - col.separator() col.prop(fast64_settings, "auto_pick_texture_format") if fast64_settings.auto_pick_texture_format: col.prop(fast64_settings, "prefer_rgba_over_ci") + col.separator() + + col.label(text="Fast64 glTF Settings") + fast64_settings.glTF.draw_props(col) class Fast64_GlobalToolsPanel(bpy.types.Panel): @@ -228,9 +252,29 @@ def draw(self, context): addon_updater_ops.update_notice_box_ui(self, context) +class Fast64_glTFProperties(bpy.types.PropertyGroup): + """ + Properties in scene.fast64.settings.glTF. + """ + + exportFormat: bpy.props.EnumProperty(name="Format", default="GLTF_SEPARATE", items=glTFFormatEnum) + copyright: bpy.props.StringProperty(name="Copyright") + useMeshCompression: bpy.props.BoolProperty(name="Use Mesh Compression") + meshCompressionLevel: bpy.props.IntProperty(name="Mesh Compression Level", min=0, max=6) + + def draw_props(self, layout: bpy.types.UILayout): + col = layout.column() + col.prop(self, "exportFormat") + col.prop(self, "copyright") + col.prop(self, "useMeshCompression") + col.prop(self, "meshCompressionLevel") + + class Fast64Settings_Properties(bpy.types.PropertyGroup): """Settings affecting exports for all games found in scene.fast64.settings""" + glTF: bpy.props.PointerProperty(type=Fast64_glTFProperties, name="glTF Properties") + version: bpy.props.IntProperty(name="Fast64Settings_Properties Version", default=0) anim_range_choice: bpy.props.EnumProperty( @@ -385,6 +429,7 @@ def draw(self, context): classes = ( + Fast64_glTFProperties, Fast64Settings_Properties, Fast64RenderSettings_Properties, Fast64_Properties, diff --git a/fast64_internal/f3d/f3d_gltf.py b/fast64_internal/f3d/f3d_gltf.py new file mode 100644 index 000000000..2ba5b2425 --- /dev/null +++ b/fast64_internal/f3d/f3d_gltf.py @@ -0,0 +1,328 @@ +from dataclasses import dataclass +import bpy +from ..gltf_utility import FlagAttrToGlTFInfo, appendGlTF2Extension, blenderColorToGlTFColor, flagAttrsToGlTFArray +from .f3d_writer import getRenderModeFlagList +from ..utility import getObjDirectionVec +from .f3d_material import all_combiner_uses + +fast64_extension_name = "EXT_fast64" + +from io_scene_gltf2.io.com import gltf2_io +from io_scene_gltf2.io.com.gltf2_io_constants import TextureFilter, TextureWrap +from io_scene_gltf2.blender.exp.material.extensions.gltf2_blender_image import ExportImage +from io_scene_gltf2.blender.exp.material.gltf2_blender_gather_image import ( + __gather_name, + __gather_uri, + __gather_original_uri, + __gather_buffer_view, + __make_image, +) +from io_scene_gltf2.blender.exp.gltf2_blender_gather_sampler import __sampler_by_value +from io_scene_gltf2.blender.exp.gltf2_blender_gather_cache import cached +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension +import traceback + + +def add_fast64_f3d_light_to_list(blenderLight: bpy.types.Light, lights: list[dict[str, list[float]]]): + if blenderLight is None: + return + + for obj in bpy.context.scene.objects: + if obj.data == blenderLight.original: + lights.append({"color": list(blenderLight.color), "direction": list(getObjDirectionVec(obj, True))}) + + +def blender_image_to_gltf2_image(extension, bl_image, f3d_tex, export_settings): + image_data = ExportImage.from_blender_image(bl_image) + mime_type = "image/png" + name = __gather_name(image_data, export_settings) + buffer_view, factor_buffer_view = __gather_buffer_view(image_data, mime_type, name, export_settings) + buffer_view = None # FIX + if image_data.original is None: + uri, factor_uri = __gather_uri(image_data, mime_type, name, export_settings) + else: + # Retrieve URI relative to exported glTF files + uri = __gather_original_uri(image_data.original.filepath, export_settings) + # In case we can't retrieve image (for example packed images, with original moved) + # We don't create invalid image without uri + factor_uri = None + if uri is None: + return None + + image = __make_image(buffer_view, None, None, mime_type, name, uri, export_settings) + + return image + + +def sampler_from_f3d(extension, f3dMat, f3d_tex, export_settings): + use_nearest = f3dMat.rdp_settings.g_mdsft_text_filt == "G_TF_POINT" + mag_filter = TextureFilter.Nearest if use_nearest else TextureFilter.Linear + min_filter = TextureFilter.NearestMipmapNearest if use_nearest else TextureFilter.LinearMipmapLinear + + clampS = f3d_tex.S.clamp + clampT = f3d_tex.T.clamp + mirrorS = f3d_tex.S.mirror + mirrorT = f3d_tex.T.mirror + maskS = f3d_tex.S.mask + maskT = f3d_tex.T.mask + shiftS = f3d_tex.S.shift + shiftT = f3d_tex.T.shift + + wrap_s = TextureWrap.ClampToEdge if clampS else (TextureWrap.MirroredRepeat if mirrorS else TextureWrap.Repeat) + wrap_t = TextureWrap.ClampToEdge if clampT else (TextureWrap.MirroredRepeat if mirrorT else TextureWrap.Repeat) + + sampler = __sampler_by_value(mag_filter, min_filter, wrap_s, wrap_t, export_settings) + + if sampler.extensions is None: + sampler.extensions = {} + + extensionData = {} + + if not f3d_tex.autoprop: + extensionData["maskS"] = maskS + extensionData["maskT"] = maskT + extensionData["shiftS"] = shiftS + extensionData["shiftT"] = shiftT + + extensionData["format"] = f3d_tex.tex_format + + sampler.extensions[fast64_extension_name] = extension( + name=fast64_extension_name, extension=extensionData, required=False + ) + + return sampler + + +def texture_by_value(sampler: gltf2_io.Sampler, image: gltf2_io.Image, export_settings: dict) -> gltf2_io.Texture: + return gltf2_io.Texture(extensions={}, extras=None, name=None, sampler=sampler, source=image) + + +def fogToGlTF(f3dMat): + if f3dMat.set_fog: + return {"color": blenderColorToGlTFColor(f3dMat.fog_color), "range": list(f3dMat.fog_position)} + + +def textureSettingsToGlTF(f3dMat): + textureSettingsData = {} + if not f3dMat.scale_autoprop: + textureSettingsData["scale"] = [f3dMat.tex_scale[0], f3dMat.tex_scale[1]] + + rdpSettings = f3dMat.rdp_settings + if rdpSettings.g_mdsft_textlod == "G_TL_LOD": + textureSettingsData["mipmapAmount"] = rdpSettings.num_textures_mipmapped + + return textureSettingsData + + +def largeTextureModeToGlTF(f3dMat): + if f3dMat.use_large_textures: + return {"largeTextureEdges": f3dMat.large_edges} + + +def lightsToGlTF(useDict, f3dMat): + if useDict["Shade"] and f3dMat.rdp_settings.g_lighting and f3dMat.set_lights: + lights: list[dict[str, list[float]]] = [] + ambientColor: list[float] = blenderColorToGlTFColor(f3dMat.ambient_light_color) + + if f3dMat.use_default_lighting: + lights.append(blenderColorToGlTFColor(f3dMat.default_light_color)) + if f3dMat.set_ambient_from_light: + ambientColor = None + else: + for i in range(1, 8): + add_fast64_f3d_light_to_list(f3dMat.get(f"f3d_light{str(i)}"), lights) + + lightData = {"lights": lights} + if ambientColor: + lightData["ambientColor"] = ambientColor + + return lightData + + +def yuvConvertToGlTF(useDict, f3dMat): + if useDict["Convert"] and f3dMat.set_k0_5: + yuvConvertData = [f3dMat.k0, f3dMat.k1, f3dMat.k2, f3dMat.k3, f3dMat.k4, f3dMat.k5] + return [round(value, 3) for value in yuvConvertData] + + +def chromaKeyToGlTF(useDict, f3dMat): + if useDict["Key"] and f3dMat.set_key: + return {"center": list(f3dMat.key_center), "scale": list(f3dMat.key_scale), "width": list(f3dMat.key_width)} + + +def primitiveColorToGlTF(useDict, f3dMat): + if useDict["Primitive"] and f3dMat.set_prim: + color = blenderColorToGlTFColor(f3dMat.prim_color, True) + + if f3dMat.prim_lod_min != 0 or f3dMat.prim_lod_frac != 0: + primativeColorData = {} + primativeColorData["minLoDRatio"] = f3dMat.prim_lod_min + primativeColorData["loDFraction"] = f3dMat.prim_lod_frac + primativeColorData["color"] = color + else: + primativeColorData = color + + return primativeColorData + + +def environmentColorToGlTF(useDict, f3dMat): + if useDict["Environment"] and f3dMat.set_env: + return blenderColorToGlTFColor(f3dMat.env_color, True) + + +def allColorRegistersToGlTF(useDict, f3dMat): + return { + "environmentColor": environmentColorToGlTF(useDict, f3dMat), + "primativeColor": primitiveColorToGlTF(useDict, f3dMat), + "chromaKey": chromaKeyToGlTF(useDict, f3dMat), + "yuvConvert": yuvConvertToGlTF(useDict, f3dMat), + } + + +@dataclass +class EnumAttrToGlTFInfo: + gltfKey: str + materialAttr: str + default: object + + +# TODO: Needs better naming +def enum_attributes_to_glTF_dict(materialSettings, enumAttributesInfo: dict[EnumAttrToGlTFInfo]): + data = {} + for info in enumAttributesInfo: + value = getattr(materialSettings, info.materialAttr) + + if value != info.default: + data[info.gltfKey] = value + return data + + +otherModeLAttrsToGlTF = [ + EnumAttrToGlTFInfo("alphaCompare", "g_mdsft_alpha_compare", "G_AC_NONE"), + EnumAttrToGlTFInfo("zSourceSelection", "g_mdsft_zsrcsel", "G_ZS_PIXEL"), +] + + +def othermodeLToGlTF(f3dMat): + rdpSettings = f3dMat.rdp_settings + + mode = enum_attributes_to_glTF_dict(rdpSettings, otherModeLAttrsToGlTF) + if rdpSettings.g_mdsft_zsrcsel == "G_ZS_PRIM": + prim_depth = rdpSettings.prim_depth + primDepthData = {} + primDepthData["z"] = prim_depth.z + primDepthData["deltaZ"] = prim_depth.dz + mode["primDepth"] = primDepthData + + # Render mode and blender + if rdpSettings.set_rendermode: + renderMode, colorBlender = getRenderModeFlagList(rdpSettings, f3dMat) + if colorBlender is None: + mode["renderMode"] = renderMode + else: + mode["renderMode"] = {"flags": renderMode, "blender": colorBlender} + + return mode + + +otherModeHAttrsToGlTF = [ + EnumAttrToGlTFInfo("colorDither", "g_mdsft_color_dither", "G_CD_ENABLE"), # Hardware V1 + EnumAttrToGlTFInfo("alphaDither", "g_mdsft_alpha_dither", "G_AD_NOISE"), # Hardware V2 + EnumAttrToGlTFInfo("rgbDither", "g_mdsft_rgb_dither", "G_CD_MAGICSQ"), # Hardware V2 + EnumAttrToGlTFInfo("chromaKey", "g_mdsft_combkey", "G_CK_NONE"), + EnumAttrToGlTFInfo("textureConvert", "g_mdsft_textconv", "G_TC_FILT"), + EnumAttrToGlTFInfo("textureFilterType", "g_mdsft_text_filt", "G_TF_BILERP"), + EnumAttrToGlTFInfo("textureLut", "g_mdsft_textlut", "G_TT_NONE"), + EnumAttrToGlTFInfo("textureLoD", "g_mdsft_textlod", "G_TL_TILE"), + EnumAttrToGlTFInfo("textureDetail", "g_mdsft_textdetail", "G_TD_CLAMP"), + EnumAttrToGlTFInfo("perspectiveCorrection", "g_mdsft_textpersp", "G_TP_PERSP"), + EnumAttrToGlTFInfo("cycleType", "g_mdsft_cycletype", "G_CYC_1CYCLE"), + EnumAttrToGlTFInfo("pipelineMode", "g_mdsft_pipeline", "G_PM_1PRIMITIVE"), +] + +geoModesToGlTF = [ + FlagAttrToGlTFInfo("G_ZBUFFER", "g_zbuffer"), + FlagAttrToGlTFInfo("G_SHADE", "g_shade"), + FlagAttrToGlTFInfo("G_CULL_FRONT", "g_cull_front"), + FlagAttrToGlTFInfo("G_CULL_BACK", "g_cull_back"), + FlagAttrToGlTFInfo("G_FOG", "g_fog"), + FlagAttrToGlTFInfo("G_LIGHTING", "g_lighting"), + FlagAttrToGlTFInfo("G_TEXTURE_GEN", "g_tex_gen"), + FlagAttrToGlTFInfo("G_TEXTURE_GEN_LINEAR", "g_tex_gen_linear"), + FlagAttrToGlTFInfo("G_SHADING_SMOOTH", "g_shade_smooth"), + FlagAttrToGlTFInfo("G_CLIPPING", "g_clipping"), # f3dlx2 only +] + + +def get_cycle(combiner): + return [ + combiner.A, + combiner.B, + combiner.C, + combiner.D, + combiner.A_alpha, + combiner.B_alpha, + combiner.C_alpha, + combiner.D_alpha, + ] + + +def combinersToGlTF(f3dMat): + if f3dMat.set_combiner: + combiner = get_cycle(f3dMat.combiner1) + if f3dMat.rdp_settings.g_mdsft_cycletype == "G_CYC_2CYCLE": + combiner.extend(get_cycle(f3dMat.combiner2)) + return combiner + return None + + +def f3d_texture_to_gltf2_texture(extension, f3dMat, f3d_texture, export_settings): + cur_bl_image = f3d_texture.tex + cur_image = blender_image_to_gltf2_image(extension.Extension, cur_bl_image, f3d_texture, export_settings) + cur_sampler = sampler_from_f3d(extension.Extension, f3dMat, f3d_texture, export_settings) + cur_texture = texture_by_value(cur_sampler, cur_image, export_settings) + + return cur_sampler + + +def gather_material_pbr_metallic_roughness_hook_fast64( + extension, gltf2_material, blender_material, orm_texture, export_settings +): + # Unfinished + if blender_material.is_f3d: + if gltf2_material.extensions is None: + gltf2_material.extensions = {} + + f3dMat = blender_material.f3d_mat + useDict = all_combiner_uses(f3dMat) + + gltf2_material.metallic_factor = 0.0 + gltf2_material.roughness_factor = 0.5 + + return + + +def gather_material_hook_fast64(extension, gltf2_material, blender_material, export_settings): + if blender_material.is_f3d: + extensionData = {} + + f3dMat = blender_material.f3d_mat + rdpSettings = f3dMat.rdp_settings + useDict = all_combiner_uses(f3dMat) + + extensionData["combiner"] = combinersToGlTF(f3dMat) + + extensionData["geometryMode"] = flagAttrsToGlTFArray(rdpSettings, geoModesToGlTF) + extensionData["otherModeH"] = enum_attributes_to_glTF_dict(rdpSettings, otherModeHAttrsToGlTF) + extensionData["otherModeL"] = othermodeLToGlTF(f3dMat) + + extensionData.update(allColorRegistersToGlTF(useDict, f3dMat)) + + extensionData["lightData"] = lightsToGlTF(useDict, f3dMat) + + extensionData["largeTextureMode"] = largeTextureModeToGlTF(f3dMat) + + extensionData["textureSettings"] = textureSettingsToGlTF(f3dMat) + extensionData["fog"] = fogToGlTF(f3dMat) + + appendGlTF2Extension(extension, fast64_extension_name, gltf2_material, extensionData) diff --git a/fast64_internal/gltf_utility.py b/fast64_internal/gltf_utility.py new file mode 100644 index 000000000..a01d9df83 --- /dev/null +++ b/fast64_internal/gltf_utility.py @@ -0,0 +1,60 @@ +from dataclasses import dataclass +from ..fast64_internal.utility import gammaCorrect + + +@dataclass +class FlagAttrToGlTFInfo: + flag: str + materialAttr: str + + +def flagAttrsToGlTFArray(materialSettings, enumAttributesInfo: dict[FlagAttrToGlTFInfo]): + data = [] + for info in enumAttributesInfo: + if getattr(materialSettings, info.materialAttr) == True: + data.append(info.flag) + return data + + +class TypeToGlTF: + def __init__(self, typeName: str, function=None): + self.typeName = typeName + self.function = function + + def toGltf(self, obj): + if isinstance(self.function, dict): + data = {} + for key in self.function: + data[key] = getattr(obj, self.function[key]) + return data + if isinstance(self.function, str): + return getattr(obj, self.function) + elif self.function: + return self.function(obj) + + +def blenderColorToGlTFColor(color, hasAlpha=False) -> list: + correctColor = [round(channel, 3) for channel in gammaCorrect(color)] + if hasAlpha: + correctColor.append(color[3]) + + return correctColor + + +def appendGlTF2Extension(extension, extensionName, gltf2Object, dataDictionary): + for key, value in dataDictionary.copy().items(): + # For some reason, the glTF exporter is not doing this for the sm64 extension. + if value is None or (isinstance(value, dict) and not any(value)): + dataDictionary.pop(key) + + if not any(dataDictionary): + return + + if gltf2Object.extensions is None: + gltf2Object.extensions = {} + + gltf2Object.extensions[extensionName] = extension.Extension( + name=extensionName, + extension=dataDictionary, + required=False, + ) diff --git a/fast64_internal/panels.py b/fast64_internal/panels.py index adc5f97db..e30322dd7 100644 --- a/fast64_internal/panels.py +++ b/fast64_internal/panels.py @@ -17,8 +17,7 @@ class SM64_Panel(bpy.types.Panel): bl_options = {"DEFAULT_CLOSED"} # goal refers to the selected sm64GoalTypeEnum, a different selection than this goal will filter this panel out goal = None - # if this is True, the panel is hidden whenever the scene's exportType is not 'C' - decomp_only = False + supports = {"C", "Insertable Binary", "Binary"} @classmethod def poll(cls, context): @@ -30,7 +29,7 @@ def poll(cls, context): elif cls.goal == sm64GoalImport: # Only show if importing is enabled return sm64Props.showImportingMenus - elif cls.decomp_only and sm64Props.exportType != "C": + elif sm64Props.exportType not in cls.supports: return False sceneGoal = sm64Props.goal diff --git a/fast64_internal/sm64/sm64_f3d_writer.py b/fast64_internal/sm64/sm64_f3d_writer.py index 059e40815..a364e9099 100644 --- a/fast64_internal/sm64/sm64_f3d_writer.py +++ b/fast64_internal/sm64/sm64_f3d_writer.py @@ -815,7 +815,7 @@ class ExportTexRectDrawPanel(SM64_Panel): bl_idname = "TEXTURE_PT_export_texrect" bl_label = "SM64 UI Image Exporter" goal = "Export UI Image" - decomp_only = True + supports = {"C"} # called every frame def draw(self, context): diff --git a/fast64_internal/sm64/sm64_gltf.py b/fast64_internal/sm64/sm64_gltf.py new file mode 100644 index 000000000..6fe095326 --- /dev/null +++ b/fast64_internal/sm64/sm64_gltf.py @@ -0,0 +1,717 @@ +sm64_extension_name = "EXT_sm64" + +import bpy +import os +import bpy, mathutils + +from ..gltf_utility import TypeToGlTF, appendGlTF2Extension, blenderColorToGlTFColor +from ..utility import ( + PluginError, + checkIdentityRotation, + toAlnum, + getExportDir, + applyBasicTweaks, +) + +from .sm64_geolayout_classes import drawLayerNames +from .sm64_geolayout_bone import enumBoneType +from .sm64_objects import WarpNodeProperty +from .sm64_constants import levelIDNames + +geoCommandsToGlTF = { + "Start": TypeToGlTF("START"), + "StartRenderArea": TypeToGlTF("CULL", {"radius": "culling_radius"}), + "Shadow": TypeToGlTF( + "DRAW_SHADOW", {"type": "shadow_type", "solidity": "shadow_solidity", "scale": "shadow_scale"} + ), + "Scale": TypeToGlTF("SCALE", {"scale": "geo_scale"}), + "TranslateRotate": TypeToGlTF("TRANSLATE_ROTATE"), + "Translate": TypeToGlTF("TRANSLATE"), + "Rotate": TypeToGlTF("ROTATE"), + "Billboard": TypeToGlTF("BILLBOARD"), + "DisplayList": TypeToGlTF("DISPLAY_LIST"), # This probably should be a warning in the future + "DisplayListWithOffset": TypeToGlTF("ANIMATE"), + "Switch": TypeToGlTF("SWITCH"), + "Function": TypeToGlTF("FUNCTION"), + "HeldObject": TypeToGlTF("HELD_OBJECT"), + "SwitchOption": TypeToGlTF("SWITCH_OPTION"), + "Ignore": TypeToGlTF("IGNORE"), +} + + +def nonCustomGeoCommandToGlTF(bone, command, hasDisplayList): + info: TypeToGlTF = geoCommandsToGlTF[command] + blenderCommandName = (lambda x: x[0] == command, enumBoneType)[1] + + defaultMessageStart = ( + f'Bone named "{bone.name}" uses the command ({blenderCommandName}) which is normally used in a' + ) + + if command in ["DisplayList"] and len(bone.children) > 0: + message = f"{defaultMessageStart} childless bone." + raise Exception(message) + + if command in ["Function"] and not hasDisplayList: + message = f"{defaultMessageStart} deformable bone." + raise Exception(message) + + if command in ["Function", "Switch"]: + return { + "function": bone.geo_func, + "parameter": bone.func_param, + } + elif command == "HeldObject": + return {"function": bone.geo_func} + + commandname = info.commandInGlTF + arguments = {} + + for attr in info.argumentAttrsToGlTF: + arguments[attr] = getattr(bone, info.argumentAttrsToGlTF[attr]) + + return {"command": commandname, "arguments": arguments} + + +def geoCommandToGlTF(bone): + command = bone.geo_cmd + hasDisplayList = bone.use_deform + + if command in geoCommandsToGlTF: + geoCommandData = nonCustomGeoCommandToGlTF(bone, command, hasDisplayList) + else: + commandname = bone.fast64.sm64.custom_geo_cmd_macro + arguments = bone.fast64.sm64.custom_geo_cmd_args + + geoCommandData = {"customCommand": commandname, "arguments": arguments} + + if hasDisplayList: + geoCommandData["hasDisplayList"] = hasDisplayList + + return geoCommandData + + +def texTileScrollToGlTF(tileScroll): + s, t, interval = tileScroll.s, tileScroll.t, tileScroll.interval + + if s != 0 or t != 0: + return {"s": s, "t": t, "interval": interval} + + +def tileScrollToGlTF(f3dMat): + tileScrollData = {} + tex0, tex1 = texTileScrollToGlTF(f3dMat.tex0.tile_scroll), texTileScrollToGlTF(f3dMat.tex0.tile_scroll) + + if tex0: + tileScrollData["tex0"] = tex0 + if tex1: + tileScrollData["tex1"] = tex1 + + return tileScrollData + + +def uvAxisScrollToGlTF(axis): + if axis.animType == "None": + return + + axisScrollData = {} + axisScrollData["type"] = axis.animType + if axis.animType == "Linear": + axisScrollData["speed"] = axis.speed + elif axis.animType == "Sine": + axisScrollData["amplitude"] = axis.amplitude + axisScrollData["frequency"] = axis.frequency + axisScrollData["offset"] = axis.offset + elif axis.animType == "Noise": + axisScrollData["noiseAmplitude"] = axis.noiseAmplitude + + return axisScrollData + + +def uvScrollToGlTF(f3dMat): + UVanim0 = f3dMat.UVanim0 + + xCombined = UVanim0.x.animType == "Rotation" + yCombined = UVanim0.y.animType == "Rotation" + + if xCombined or yCombined: + return {"rotation": {"pivot": UVanim0.pivot, "angularSpeed": UVanim0.angularSpeed}} + else: + uvScrollData = {} + u, v = uvAxisScrollToGlTF(UVanim0.x), uvAxisScrollToGlTF(UVanim0.y) + + if u or v: + uvScrollData["u"] = u + uvScrollData["v"] = v + + return uvScrollData + + +def drawLayerToGlTF(f3dMat): + drawLayer = f3dMat.draw_layer.sm64 + if drawLayer != "1": + if drawLayer in drawLayerNames: + return drawLayerNames[drawLayer] + else: + return str(drawLayer) + + +def collisionToGlTF(blenderMaterial, extension): + if extension.actorExport: + return + + collision_data = {} + + if blenderMaterial.collision_all_options: + colType = blenderMaterial.collision_type + else: + colType = blenderMaterial.collision_type_simple + + if colType == "Custom": + collision_data["type"] = blenderMaterial.collision_custom + elif colType != "SURFACE_DEFAULT": + collision_data["type"] = colType + + if blenderMaterial.use_collision_param: + collision_data["paramater"] = blenderMaterial.collision_param + + return collision_data + + +def objectFunctionToGlTF(obj: bpy.types.Object): + if obj.add_func: + func = obj.fast64.sm64.geo_asm + return { + "function": func.func, + "parameter": func.param, + } + + +def objectShadowToGlTF(obj: bpy.types.Object): + if obj.add_shadow: + return { + "type": obj.shadow_type, + "solidity": obj.shadow_solidity, + "scale": obj.shadow_scale, + } + + +puppyCamFlagAttrsToGlTF = [ + "NC_FLAG_XTURN", + "NC_FLAG_YTURN", + "NC_FLAG_ZOOM", + "NC_FLAG_8D", + "NC_FLAG_4D", + "NC_FLAG_2D", + "NC_FLAG_FOCUSX", + "NC_FLAG_FOCUSY", + "NC_FLAG_FOCUSZ", + "NC_FLAG_POSX", + "NC_FLAG_POSY", + "NC_FLAG_POSZ", + "NC_FLAG_COLLISION", + "NC_FLAG_SLIDECORRECT", +] + + +def puppyCamVolumeToGlTF(obj): + checkIdentityRotation(obj, obj.matrix_basis.to_quaternion(), False) + puppyCam = obj.puppycamProp + + specialData = {"function": puppyCam.puppycamVolumeFunction, "permaSwap": puppyCam.puppycamVolumePermaswap} + + if puppyCam.puppycamUseFlags: + flags = [] + for flagAttr in puppyCamFlagAttrsToGlTF: + flags.append(getattr(puppyCam, flagAttr)) + specialData["flags"] = flags + else: + specialData["mode"] = puppyCam.puppycamMode if puppyCam.puppycamMode != "Custom" else puppyCam.puppycamType + + pos, camFocus = (32767, 32767, 32767), (32767, 32767, 32767) + + if puppyCam.puppycamUseEmptiesForPos: + if puppyCam.puppycamCamPos != "": + posObject = bpy.context.scene.objects[puppyCam.puppycamCamPos] + pos = posObject.location + if puppyCam.puppycamCamFocus != "": + focObject = bpy.context.scene.objects[puppyCam.puppycamCamFocus] + camFocus = focObject.location + else: + camera = puppyCam.puppycamCamera + if camera is not None: + pos = camera.location + camFocus = (camera.matrix_local @ mathutils.Vector((0, 0, -1)))[:] + + specialData["pos"] = list(pos) + specialData["camFocus"] = list(camFocus) + + return specialData + + +waterBoxTypeToGlTFDict = {"Water": "WATER", "Toxic Haze": "TOXIC_HAZE"} + + +def waterBoxToGlTF(obj: bpy.types.Object): + checkIdentityRotation(obj, obj.matrix_basis.to_quaternion(), False) + return {"boxType": waterBoxTypeToGlTFDict[obj.waterBoxType]} + + +def behaviorToGlTF(obj: bpy.types.Object, alwaysHasBParm: bool = False): + if obj.sm64_obj_set_bparam or alwaysHasBParm: + return obj.fast64.sm64.game_object.get_behavior_params() + + +def specialToGlTF(obj: bpy.types.Object): + specialData = {} + + specialData["preset"] = obj.sm64_special_enum if obj.sm64_special_enum != "Custom" else obj.sm64_obj_preset + specialData["setYaw"] = obj.sm64_obj_set_yaw + if obj.sm64_obj_set_yaw: + specialData["params"] = behaviorToGlTF(obj) + + return specialData + + +def macroToGlTF(obj: bpy.types.Object): + macrotData = {} + + macrotData["macro"] = obj.sm64_macro_enum if obj.sm64_macro_enum != "Custom" else obj.sm64_obj_preset + macrotData["params"] = behaviorToGlTF(obj) + + return macrotData + + +def gameObjectToGlTF(obj: bpy.types.Object): + objectData = {} + + objectData["model"] = obj.sm64_model_enum if obj.sm64_model_enum != "Custom" else obj.sm64_obj_model + + objectData["behaviour"] = obj.sm64_behaviour_enum if obj.sm64_behaviour_enum != "Custom" else obj.sm64_obj_behaviour + objectData["params"] = behaviorToGlTF(obj, True) + + excludedFromActs = [] + for i in range(1, 7, 1): + isInAct = getattr(obj, f"sm64_obj_use_act{i}") + if not isInAct: + excludedFromActs.append(i) + objectData["excludedFromActs"] = excludedFromActs + + return objectData + + +warpTypeToGlTFDict = { + "Warp": "WARP", + "Painting": "PAINTING", + "Instant": "INSTANT", +} + + +def areaObjectToGlTF(obj: bpy.types.Object): + def warpNodeToGlTF(warpNode: WarpNodeProperty): + warpData = {} + warpData["warpType"] = warpTypeToGlTFDict[warpNode.warpType] + warpData["warpID"] = warpNode.warpID + + warpData["area"] = warpNode.destArea + + if warpNode.warpType == "Instant": + if warpNode.useOffsetObjects: + offset = warpNode.calc_offsets_from_objects(warpNode.uses_area_nodes()) + else: + offset = warpNode.instantOffset + offset = [offset[0], offset[1], offset[2]] + + warpData["instantOffset"] = list(offset) + return warpData + + # Not instant warp + warpData["level"] = ( + levelIDNames[warpNode.destLevelEnum] if warpNode.destLevelEnum != "custom" else warpNode.destLevel + ) + warpData["node"] = warpNode.destNode + warpData["flags"] = warpNode.warpFlagEnum if warpNode.warpFlagEnum != "Custom" else warpNode.warpFlags + + return warpData + + def areaScreenRectToGlTF(obj: bpy.types.Object): + if obj.useDefaultScreenRect: + return + return { + "pos": list(obj.screenPos), + "size": list(obj.screenSize), + } + + def areaBackgroundToGlTF(obj: bpy.types.Object): + if obj.fast64.sm64.area.disable_background: + return {"disableBackground": True} + elif obj.areaOverrideBG: + return {"backgroundColor": blenderColorToGlTFColor(obj.areaBGColor)} + return {} + + def areaMusicToGlTF(obj: bpy.types.Object): + if obj.noMusic: + return + + seq, customSeq = obj.musicSeqEnum, obj.music_seq + + return { + "enum": customSeq if seq == "Custom" else seq, + "preset": obj.music_preset, + } + + areaData = {} + areaData["areaIndex"] = obj.areaIndex + + areaData["music"] = areaMusicToGlTF(obj) + + areaData["terrain"] = obj.terrainEnum if obj.terrainEnum != "Custom" else obj.terrain_type + + env, customEnv = obj.envOption, obj.envType + areaData["envfx"] = customEnv if env == "Custom" else env + + camera, customCamera = obj.camOption, obj.camType + areaData["cameraType"] = customCamera if camera == "Custom" else camera + + areaData["fog"] = { + "color": blenderColorToGlTFColor(obj.area_fog_color), + "range": list(obj.area_fog_position), + } + + areaData["echoLevel"] = obj.echoLevel + areaData["zoomOutOnPause"] = obj.zoomOutOnPause + areaData.update(areaBackgroundToGlTF(obj)) + areaData["startDialog"] = obj.startDialog if obj.showStartDialog else None + areaData["enableRooms"] = obj.enableRoomSwitch + areaData["screenRect"] = areaScreenRectToGlTF(obj) + + warpNodesData = [] + for warpNode in obj.warpNodes: + warpNodesData.append(warpNodeToGlTF(warpNode)) + areaData["warpNodes"] = warpNodesData + + return areaData + + +starGetCutsceneToGlTFDict = { + "0": "LAKITU_FLIES_AWAY", + "1": "ROTATE_AROUND_MARIO", + "2": "CLOSEUP_OF_MARIO", + "3": "BOWSER_KEYS", + "4": "COIN_STAR", +} + + +def areaObjectChecks(areaDict, obj: bpy.types.Object): + if len(obj.children) == 0: + error = f"\ +Area for {obj.name} has no children." + raise PluginError(error) + + if obj.areaIndex in areaDict: + error = f"\ +{obj.name} shares the same area index as {areaDict[obj.areaIndex].name}" + raise PluginError(error) + + areaDict[obj.areaIndex] = obj + + +def levelObjectToGlTF(obj: bpy.types.Object): + def levelBackgroundToGlTF(obj: bpy.types.Object): + if obj.useBackgroundColor: + return {"backgroundColor": blenderColorToGlTFColor(obj.backgroundColor)} + else: + if obj.background == "CUSTOM": + return {"skyboxSegment": obj.fast64.sm64.level.backgroundSegment} + else: + return {"skybox": obj.background} + + def starCutscenesToGlTF(obj: bpy.types.Object): + starGetCutscenes = obj.starGetCutscenes + starCutscenes = [] + isDefault = True + for i in range(1, 8, 1): + starOption = getattr(starGetCutscenes, f"star{i}_option") + + if starOption != "4": + isDefault = False + + if starOption == "Custom": + starValue = getattr(starGetCutscenes, f"star{i}_value") + else: + starValue = starGetCutsceneToGlTFDict[starOption] + + starCutscenes.append(starValue) + if not isDefault: + return starCutscenes + + def segmentToGlTF(segementEnum, customSegment, customGroup): + if segementEnum != "Do Not Write": + if segementEnum == "Custom": + return {"segment": customSegment, "group": customGroup} + else: + return {"segment": segementEnum} + + levelData = {} + + childAreas = [child for child in obj.children if child.data is None and child.sm64_obj_type == "Area Root"] + if len(childAreas) == 0: + raise PluginError("The level root has no child empties with the 'Area Root' object type.") + + areaDict = {} + + for area in childAreas: + areaObjectChecks(areaDict, area) + + levelData.update(levelBackgroundToGlTF(obj)) + levelData["hasStarSelect"] = False if obj.actSelectorIgnore else None + # TODO: Set as start level is not included, it should remain on the fast64 side. + # This becomes a problem when exporting through the glTF export window. + segmentLoads = obj.fast64.sm64.segment_loads + levelData["segment5"] = segmentToGlTF( + segmentLoads.seg5_enum, segmentLoads.seg5_load_custom, segmentLoads.seg5_group_custom + ) + levelData["segment6"] = segmentToGlTF( + segmentLoads.seg6_enum, segmentLoads.seg6_load_custom, segmentLoads.seg6_group_custom + ) + + levelData["acousticReach"] = obj.acousticReach if obj.acousticReach == 20000 else None + levelData["starCutscenes"] = starCutscenesToGlTF(obj) + + return levelData + + +emptyTypesToGlTFDict = { + "Level Root": TypeToGlTF("LEVEL_ROOT", levelObjectToGlTF), + "Area Root": TypeToGlTF("AREA_ROOT", areaObjectToGlTF), + "Object": TypeToGlTF("OBJECT", gameObjectToGlTF), + "Macro": TypeToGlTF("MACRO", macroToGlTF), + "Special": TypeToGlTF("SPECIAL", specialToGlTF), + "Mario Start": TypeToGlTF("MARIO_START", {"area": "sm64_obj_mario_start_area"}), + "Whirlpool": TypeToGlTF( + "WHIRLPOOL", {"index": "whirpool_index", "condition": "whirpool_condition", "strength": "whirpool_strength"} + ), + "Water Box": TypeToGlTF("WATER_BOX", waterBoxToGlTF), + "Camera Volume": TypeToGlTF("CAMERA_VOLUME", {"function": "cameraVolumeFunction", "global": "cameraVolumeGlobal"}), + "Switch": TypeToGlTF("SWITCH_NODE", {"function": "switchFunc", "parameter": "switchParam"}), + "Puppycam Volume": TypeToGlTF("PUPPYCAM_VOLUME", puppyCamVolumeToGlTF), +} + + +def emptyToGlTF(extension, obj: bpy.types.Object): + objectData = {} + if obj.sm64_obj_type == "None": + return objectData + + toGlTFInfo = emptyTypesToGlTFDict[obj.sm64_obj_type] + objectData["type"] = toGlTFInfo.typeName + + objectData.update(toGlTFInfo.toGltf(obj)) + + return objectData + + +def gather_asset_hook_sm64(extension, gltf2_asset, export_settings): + if "sm64" in extension.game_modes: + return + + extension.level = None + extension.actorExport = export_settings["gltf_export_id"] == "fast64_sm64_geolayout_export" + + +def gather_gltf_extensions_hook_sm64(extension, gltf2_plan, export_settings): + if "sm64" in extension.game_modes: + return + + scene = bpy.context.scene + + extensionData = {} + + extensionData["scale"] = scene.fast64.sm64.blender_to_sm64_scale + + if scene.fast64.sm64.disable_scroll: + extensionData["scroll"] = not scene.fast64.sm64.disable_scroll + + appendGlTF2Extension(extension, sm64_extension_name, gltf2_plan, extensionData) + + +def gather_node_hook_sm64(extension, gltf2Node, obj, exportSettings): + if "sm64" in extension.game_modes: + return + + extensionData = {} + + if obj.type == "EMPTY": + extensionData.update(emptyToGlTF(extension, obj)) + + appendGlTF2Extension(extension, sm64_extension_name, gltf2Node, extensionData) + + +def gather_mesh_hook_sm64(extension, gltf2_mesh, blenderMesh, obj, vertexGroups, modifiers, materials, exportSettings): + if not extension.sm64 or obj is None: + return + + extensionData = {} + + if obj.use_render_area: + extensionData["cullingRadius"] = obj.culling_radius + + if obj.use_render_range: + extensionData["renderRange"] = obj.render_range + + extensionData["shadow"] = objectShadowToGlTF(obj) + extensionData["function"] = objectFunctionToGlTF(obj) + + if obj.ignore_render: + extensionData["render"] = obj.ignore_render + + if obj.ignore_collision: + extensionData["useCollision"] = obj.ignore_collision + + if obj.use_f3d_culling: + extensionData["useCulling"] = obj.use_f3d_culling + + appendGlTF2Extension(extension, sm64_extension_name, gltf2_mesh, extensionData) + + +def gather_skin_hook_sm64(extension, gltf2_skin, obj, exportSettings): + if "sm64" in extension.game_modes: + return + + extensionData = {} + + if obj.use_render_area: + extensionData["cullingRadius"] = obj.culling_radius + + appendGlTF2Extension(extension, sm64_extension_name, gltf2_skin, extensionData) + + +def gather_joint_hook_sm64(extension, gltf2Node, blender_bone, exportSettings): + if "sm64" in extension.game_modes: + return + + if gltf2Node.extensions is None: + gltf2Node.extensions = {} + + gltf2Node.extensions[sm64_extension_name] = extension.Extension( + name=sm64_extension_name, + extension={"geoLayoutCommand": geoCommandToGlTF(blender_bone.bone)}, + required=False, + ) + + +def gather_scene_hook_sm64(extension, gltf2_scene, blender_scene, exportSettings): + if "sm64" in extension.game_modes: + return + + +def gather_material_hook_sm64(extension, gltf2_material, blenderMaterial, exportSettings): + if "sm64" in extension.game_modes: + return + + extensionData = {} + + extensionData["collision"] = collisionToGlTF(blenderMaterial, extension) + + if blenderMaterial.is_f3d: + f3dMat = blenderMaterial.f3d_mat + + extensionData["drawLayer"] = drawLayerToGlTF(f3dMat) + + if f3dMat.set_fog: + fogInfo = {} + if f3dMat.use_global_fog: + fogInfo["setAccordingToArea"] = True + extensionData["fog"] = fogInfo + + extensionData["uvScroll"] = uvScrollToGlTF(f3dMat) + extensionData["tileScroll"] = tileScrollToGlTF(f3dMat) + + appendGlTF2Extension(extension, sm64_extension_name, gltf2_material, extensionData) + + +def exportSm64GlTFGeolayout(): + scene: bpy.types.Scene = bpy.context.scene + exportPath, levelName = getPathAndLevel( + scene.geoCustomExport, + scene.geoExportPath, + scene.geoLevelName, + scene.geoLevelOption, + ) + + saveTextures = scene.saveTextures + if not scene.geoCustomExport: + applyBasicTweaks(exportPath) + + dirPath, texDir = getExportDir( + scene.geoCustomExport, + exportPath, + scene.geoExportHeaderType, + scene.geoLevelName, + scene.geoTexDir, + scene.geoName, + ) + + dirName = toAlnum(scene.geoName) + groupName = toAlnum(scene.geoGroupName) + geoDirPath = os.path.join(dirPath, toAlnum(dirName)) + + glTFProps: Fast64_glTFProperties = scene.fast64.settings.glTF + + bpy.ops.export_scene.gltf( + filepath=f"{geoDirPath}/{scene.geoName}", + gltf_export_id="fast64_sm64_geolayout_export", + export_format=glTFProps.exportFormat, + ui_tab="GENERAL", + export_copyright=glTFProps.copyright, + export_image_format="AUTO", + export_texture_dir="textures", + export_jpeg_quality=100, + export_texcoords=True, + export_normals=True, + use_visible=(not scene.exportHiddenGeometry), + use_selection=True, + export_draco_mesh_compression_enable=glTFProps.useMeshCompression, + export_draco_mesh_compression_level=glTFProps.meshCompressionLevel, + export_tangents=True, # TODO: Are tangents really needed? + export_materials="EXPORT", + export_original_specular=False, + export_colors=True, + export_attributes=True, + use_mesh_edges=False, # I do not think there is any pratical porpuse for this and the following + use_mesh_vertices=False, + export_cameras=False, + use_renderable=False, + use_active_collection_with_nested=False, # ? + use_active_collection=False, + use_active_scene=True, + export_yup=False, # Fast64 already does this + export_apply=True, # Breaks shape keys but those are not supported anyways + export_animations=False, # This panel will be used to export specifically geolayouts, there will be a panel to export an entire actor´s data with glTF. + export_frame_range=False, + export_frame_step=1, # I think one should be correct. + export_animation_mode="ACTIONS", # A lot of users don´t use the NLA tracks, this is another concern with glTF tbh. + # export_nla_strips_merged_animation_name="", + export_def_bones=True, + export_optimize_animation_size=True, + export_optimize_animation_keep_anim_armature=False, + export_optimize_animation_keep_anim_object=False, + export_negative_frame="CROP", # Still need to think more about this one + export_anim_slide_to_zero=False, + export_bake_animation=False, + export_anim_single_armature=True, # Maybe set this to false? + export_reset_pose_bones=True, + export_current_frame=False, + export_rest_position_armature=True, + export_anim_scene_split_object=False, # Probably a bad idea to set this to true + export_skins=True, + export_all_influences=False, # This has not much porpuse, even for real skinning + export_morph=False, + export_morph_normal=False, + export_morph_tangent=False, + export_morph_animation=False, + export_morph_reset_sk_data=False, + export_lights=False, # Maybe enable this for levels + export_nla_strips=False, + will_save_settings=False, + # filter_glob="" # What + ) diff --git a/fast64_internal/sm64/sm64_level_writer.py b/fast64_internal/sm64/sm64_level_writer.py index 0d51e99b0..fb0ca5343 100644 --- a/fast64_internal/sm64/sm64_level_writer.py +++ b/fast64_internal/sm64/sm64_level_writer.py @@ -1216,7 +1216,7 @@ class SM64_ExportLevelPanel(SM64_Panel): bl_idname = "SM64_PT_export_level" bl_label = "SM64 Level Exporter" goal = "Export Level" - decomp_only = True + supports = {"C"} # called every frame def draw(self, context): diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index 212e83e2d..e56f526cf 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -37,6 +37,7 @@ class VertexWeightError(PluginError): ("C", "C", "C"), ("Binary", "Binary", "Binary"), ("Insertable Binary", "Insertable Binary", "Insertable Binary"), + ("glTF (EXPERIMENTAL)", "glTF (EXPERIMENTAL)", "glTF (EXPERIMENTAL)"), ] enumExportHeaderType = [ diff --git a/gltf_extension.py b/gltf_extension.py new file mode 100644 index 000000000..5d2be1eb4 --- /dev/null +++ b/gltf_extension.py @@ -0,0 +1,109 @@ +# Original implementation from github.com/Mr-Wiseguy/gltf64-blender +from .fast64_internal.f3d.f3d_gltf import ( + gather_material_hook_fast64, + gather_material_pbr_metallic_roughness_hook_fast64, +) +from .fast64_internal.sm64.sm64_gltf import ( + gather_asset_hook_sm64, + gather_gltf_extensions_hook_sm64, + gather_joint_hook_sm64, + gather_mesh_hook_sm64, + gather_node_hook_sm64, + gather_scene_hook_sm64, + gather_material_hook_sm64, + gather_skin_hook_sm64, +) +from io_scene_gltf2.io.com.gltf2_io_extensions import Extension + + +fast64_extension_name = "EXT_fast64" + +# Changes made from the original glTF64: +# Property names (keys) will now all use the glTF standard naming, camelCase. +# Rework of geometry modes to better suit different microcodes and for readability. +# Rework of upper modes to use a dictionary of gbi enums for each mode, all fast64 upper modes added. +# Fog added (including sm64 "global" fog) +# Lights (including custom lights) added +# Chroma key and yuv convert values added. + +# TODO: +# Fix texture appending. Wiseguy´s approach will not work. +# Put texture format in the sampler rather than in the image, as image´s in fast64 can be exported into +# different texture types. +# Add options for using fast64/sm64 extensions in the glTF exporting tab. Add a panel for glTF exporting. +# Improve the materials to make them as accurate as glTf allows outside of an n64 rendering context. + +oldMaterialWarning = '\ +Warning: Unsupported material version. \ +Please upgrade your materials using the "Recreate F3D Materials As V5" \ +button under the "Fast64" tab. Using outdated materials may lead to bugs and errors.' + + +class glTF2ExportUserExtension: + def __init__(self): + # We need to wait until we create the gltf2UserExtension to import the gltf2 modules + # Otherwise, it may fail because the gltf2 may not be loaded yet + self.Extension = Extension + self.game_modes = {"sm64", "oot"} + self.sm64_actor_export = False + + def gather_asset_hook(self, gltf2_asset, export_settings): + gather_asset_hook_sm64(self, gltf2_asset, export_settings) + + def gather_gltf_extensions_hook(self, gltf2_plan, export_settings): + gather_gltf_extensions_hook_sm64(self, gltf2_plan, export_settings) + + def gather_scene_hook(self, gltf2_scene, blender_scene, export_settings): + try: + gather_scene_hook_sm64(self, gltf2_scene, blender_scene, export_settings) + except Exception as exc: + raise Exception(f'Exception at scene "{blender_scene.name}".\n' + str(exc)) + + def gather_node_hook(self, gltf2_node, blender_object, export_settings): + try: + gather_node_hook_sm64(self, gltf2_node, blender_object, export_settings) + except Exception as exc: + raise Exception(f'Exception at object "{blender_object.name}".\n' + str(exc)) + + def gather_mesh_hook( + self, gltf2_mesh, blender_mesh, blender_object, vertex_groups, modifiers, materials, export_settings + ): + try: + gather_mesh_hook_sm64( + self, gltf2_mesh, blender_mesh, blender_object, vertex_groups, modifiers, materials, export_settings + ) + except Exception as exc: + raise Exception( + f'Exception at mesh "{blender_mesh.name}" from the object "{blender_object.name}".\n' + str(exc) + ) + + def gather_skin_hook(self, gltf2_skin, blender_object, export_settings): + try: + gather_skin_hook_sm64(self, gltf2_skin, blender_object, export_settings) + except Exception as exc: + raise Exception(f'Exception at armature "{blender_object.name}".\n' + str(exc)) + + def gather_joint_hook(self, gltf2_node, blender_bone, export_settings): + try: + gather_joint_hook_sm64(self, gltf2_node, blender_bone, export_settings) + except Exception as exc: + raise Exception(f'Exception at joint/bone "{blender_bone.name}".\n' + str(exc)) + + def gather_material_pbr_metallic_roughness_hook( + self, gltf2_material, blender_material, orm_texture, export_settings + ): + try: + gather_material_pbr_metallic_roughness_hook_fast64( + self, gltf2_material, blender_material, orm_texture, export_settings + ) + except Exception as exc: + raise Exception(f'Exception at material "{blender_material.name}".\n' + str(exc)) + + def gather_material_hook(self, gltf2_material, blender_material, export_settings): + try: + if blender_material.is_f3d and blender_material.mat_ver < 5: + print(oldMaterialWarning) + gather_material_hook_sm64(self, gltf2_material, blender_material, export_settings) + gather_material_hook_fast64(self, gltf2_material, blender_material, export_settings) + except Exception as exc: + raise Exception(f'Exception at material "{blender_material.name}".\n' + str(exc))