From 56728776a6c7dae08cbfa6e8db35582b683525c2 Mon Sep 17 00:00:00 2001 From: Lila Date: Mon, 19 Aug 2024 21:21:45 +0100 Subject: [PATCH 1/6] [SM64] Custom draw layers Gives the user full control over their available draw layers, allowing for custom draw layers! --- __init__.py | 19 +- fast64_internal/f3d/f3d_material.py | 51 +--- fast64_internal/repo_settings.py | 2 +- fast64_internal/sm64/__init__.py | 20 ++ .../sm64/settings/repo_settings.py | 17 +- fast64_internal/sm64/sm64_f3d_writer.py | 237 +++++++++++++++--- fast64_internal/sm64/sm64_geolayout_bone.py | 8 +- .../sm64/sm64_geolayout_classes.py | 35 +-- fast64_internal/utility.py | 29 ++- 9 files changed, 292 insertions(+), 126 deletions(-) diff --git a/__init__.py b/__init__.py index 82517f8c9..76f49c7d2 100644 --- a/__init__.py +++ b/__init__.py @@ -13,7 +13,7 @@ repo_settings_operators_unregister, ) -from .fast64_internal.sm64 import sm64_register, sm64_unregister +from .fast64_internal.sm64 import sm64_register, sm64_unregister, SM64_WorldProperties from .fast64_internal.sm64.sm64_constants import sm64_world_defaults from .fast64_internal.sm64.settings.properties import SM64_Properties from .fast64_internal.sm64.sm64_geolayout_bone import SM64_BoneProperties @@ -245,6 +245,19 @@ class Fast64_ObjectProperties(bpy.types.PropertyGroup): oot: bpy.props.PointerProperty(type=OOT_ObjectProperties, name="OOT Object Properties") +class Fast64_WorldProperties(bpy.types.PropertyGroup): + """ + Properties in world.fast64 (bpy.types.World) + All new world properties should be children of this property group. + """ + + sm64: bpy.props.PointerProperty(type=SM64_WorldProperties, name="SM64 World Properties") + + @staticmethod + def upgrade_changed_props(): + SM64_WorldProperties.upgrade_changed_props() + + class UpgradeF3DMaterialsDialog(bpy.types.Operator): bl_idname = "dialog.upgrade_f3d_materials" bl_label = "Upgrade F3D Materials" @@ -314,6 +327,7 @@ def draw(self, context): Fast64_Properties, Fast64_BoneProperties, Fast64_ObjectProperties, + Fast64_WorldProperties, F3D_GlobalSettingsPanel, Fast64_GlobalSettingsPanel, Fast64_GlobalToolsPanel, @@ -323,6 +337,7 @@ def draw(self, context): def upgrade_changed_props(): """Set scene properties after a scene loads, used for migrating old properties""" + Fast64_WorldProperties.upgrade_changed_props() SM64_Properties.upgrade_changed_props() MK64_Properties.upgrade_changed_props() SM64_ObjectProperties.upgrade_changed_props() @@ -443,6 +458,7 @@ def register(): bpy.types.Scene.fast64 = bpy.props.PointerProperty(type=Fast64_Properties, name="Fast64 Properties") bpy.types.Bone.fast64 = bpy.props.PointerProperty(type=Fast64_BoneProperties, name="Fast64 Bone Properties") bpy.types.Object.fast64 = bpy.props.PointerProperty(type=Fast64_ObjectProperties, name="Fast64 Object Properties") + bpy.types.World.fast64 = bpy.props.PointerProperty(type=Fast64_WorldProperties, name="Fast64 World Properties") bpy.app.handlers.load_post.append(after_load) @@ -472,6 +488,7 @@ def unregister(): del bpy.types.Scene.fast64 del bpy.types.Bone.fast64 del bpy.types.Object.fast64 + del bpy.types.World.fast64 repo_settings_operators_unregister() diff --git a/fast64_internal/f3d/f3d_material.py b/fast64_internal/f3d/f3d_material.py index 49b8bf80a..195c218b0 100644 --- a/fast64_internal/f3d/f3d_material.py +++ b/fast64_internal/f3d/f3d_material.py @@ -80,16 +80,9 @@ } -sm64EnumDrawLayers = [ - ("0", "Background (0x00)", "Background"), - ("1", "Opaque (0x01)", "Opaque"), - ("2", "Opaque Decal (0x02)", "Opaque Decal"), - ("3", "Opaque Intersecting (0x03)", "Opaque Intersecting"), - ("4", "Cutout (0x04)", "Cutout"), - ("5", "Transparent (0x05)", "Transparent"), - ("6", "Transparent Decal (0x06)", "Transparent Decal"), - ("7", "Transparent Intersecting (0x07)", "Transparent Intersecting"), -] +def get_sm64_draw_layers(self, context): + return create_or_get_world(context.scene).fast64.sm64.draw_layers.to_enum() + ootEnumDrawLayers = [ ("Opaque", "Opaque", "Opaque"), @@ -97,24 +90,6 @@ ("Overlay", "Overlay", "Overlay"), ] - -drawLayerSM64toOOT = { - "0": "Opaque", - "1": "Opaque", - "2": "Opaque", - "3": "Opaque", - "4": "Opaque", - "5": "Transparent", - "6": "Transparent", - "7": "Transparent", -} - -drawLayerOOTtoSM64 = { - "Opaque": "1", - "Transparent": "5", - "Overlay": "1", -} - drawLayerSM64Alpha = { "0": "OPA", "1": "OPA", @@ -165,20 +140,17 @@ def update_draw_layer(self, context): if not material: return - drawLayer = material.f3d_mat.draw_layer - if context.scene.gameEditorMode == "SM64": - drawLayer.oot = drawLayerSM64toOOT[drawLayer.sm64] - elif context.scene.gameEditorMode == "OOT": - if material.f3d_mat.draw_layer.oot == "Opaque": - if int(material.f3d_mat.draw_layer.sm64) > 4: - material.f3d_mat.draw_layer.sm64 = "1" - elif material.f3d_mat.draw_layer.oot == "Transparent": - if int(material.f3d_mat.draw_layer.sm64) < 5: - material.f3d_mat.draw_layer.sm64 = "5" material.f3d_mat.presetName = "Custom" update_blend_method(material, context) set_output_node_groups(material) + drawLayer = material.f3d_mat.draw_layer + output_method = get_output_method(material) + if context.scene.gameEditorMode == "SM64": + drawLayer.oot = {"OPA": "Opaque", "XLU": "Transparent"}.get(output_method, "Opaque") + elif context.scene.gameEditorMode == "OOT": + drawLayer.sm64 = {"OPA": "1", "XLU": "5"}.get(output_method, "1") # expects vanilla draw layers + def rendermode_preset_to_advanced(material: bpy.types.Material): """ @@ -295,7 +267,7 @@ def update_blend_method(material: Material, context): class DrawLayerProperty(PropertyGroup): - sm64: bpy.props.EnumProperty(items=sm64EnumDrawLayers, default="1", update=update_draw_layer) + sm64: bpy.props.EnumProperty(items=get_sm64_draw_layers, update=update_draw_layer) oot: bpy.props.EnumProperty(items=ootEnumDrawLayers, default="Opaque", update=update_draw_layer) def key(self): @@ -4812,7 +4784,6 @@ def mat_register(): World.menu_upper = bpy.props.BoolProperty() World.menu_lower = bpy.props.BoolProperty() World.menu_other = bpy.props.BoolProperty() - World.menu_layers = bpy.props.BoolProperty() Material.is_f3d = bpy.props.BoolProperty() Material.mat_ver = bpy.props.IntProperty(default=1) diff --git a/fast64_internal/repo_settings.py b/fast64_internal/repo_settings.py index 04418e821..1f45b802b 100644 --- a/fast64_internal/repo_settings.py +++ b/fast64_internal/repo_settings.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from .f3d.f3d_material import RDPSettings -CUR_VERSION = 1.0 +CUR_VERSION = 1.1 class SaveRepoSettings(OperatorBase): diff --git a/fast64_internal/sm64/__init__.py b/fast64_internal/sm64/__init__.py index 81fdbf596..e23d2966c 100644 --- a/fast64_internal/sm64/__init__.py +++ b/fast64_internal/sm64/__init__.py @@ -1,3 +1,7 @@ +from bpy.utils import register_class, unregister_class +from bpy.types import PropertyGroup +from bpy.props import PointerProperty + from .settings import ( settings_props_register, settings_props_unregister, @@ -81,6 +85,7 @@ sm64_dl_writer_panel_unregister, sm64_dl_writer_register, sm64_dl_writer_unregister, + SM64_DrawLayersProperties, ) from .sm64_anim import ( @@ -91,6 +96,17 @@ ) +class SM64_WorldProperties(PropertyGroup): + draw_layers: PointerProperty(type=SM64_DrawLayersProperties) + + @staticmethod + def upgrade_changed_props(): + SM64_DrawLayersProperties.upgrade_changed_props() + + +classes = (SM64_WorldProperties,) + + def sm64_panel_register(): settings_panels_register() tools_panels_register() @@ -136,6 +152,8 @@ def sm64_register(register_panels: bool): sm64_dl_parser_register() sm64_anim_register() settings_props_register() + for cls in classes: + register_class(cls) if register_panels: sm64_panel_register() @@ -156,6 +174,8 @@ def sm64_unregister(unregister_panels: bool): sm64_dl_parser_unregister() sm64_anim_unregister() settings_props_unregister() + for cls in classes: + unregister_class(cls) if unregister_panels: sm64_panel_unregister() diff --git a/fast64_internal/sm64/settings/repo_settings.py b/fast64_internal/sm64/settings/repo_settings.py index f74b7eb3d..fc7ef045f 100644 --- a/fast64_internal/sm64/settings/repo_settings.py +++ b/fast64_internal/sm64/settings/repo_settings.py @@ -8,14 +8,7 @@ def save_sm64_repo_settings(scene: Scene): world = scene.world data: dict[str, Any] = {} - draw_layers: dict[str, Any] = {} - data["draw_layers"] = draw_layers - - for layer in range(8): - draw_layers[layer] = { - "cycle_1": getattr(world, f"draw_layer_{layer}_cycle_1"), - "cycle_2": getattr(world, f"draw_layer_{layer}_cycle_2"), - } + data["draw_layers"] = world.fast64.sm64.draw_layers.to_dict() sm64_props = scene.fast64.sm64 data["refresh_version"] = sm64_props.refresh_version @@ -29,13 +22,7 @@ def save_sm64_repo_settings(scene: Scene): def load_sm64_repo_settings(scene: Scene, data: dict[str, Any]): world = scene.world - draw_layers = data.get("draw_layers", {}) - for layer in range(8): - draw_layer = draw_layers.get(str(layer), {}) - if "cycle_1" in draw_layer: - setattr(world, f"draw_layer_{layer}_cycle_1", draw_layer["cycle_1"]) - if "cycle_2" in draw_layer: - setattr(world, f"draw_layer_{layer}_cycle_2", draw_layer["cycle_2"]) + world.fast64.sm64.draw_layers.from_dict(data.get("draw_layers", {})) sm64_props = scene.fast64.sm64 sm64_props.refresh_version = data.get("refresh_version", sm64_props.refresh_version) diff --git a/fast64_internal/sm64/sm64_f3d_writer.py b/fast64_internal/sm64/sm64_f3d_writer.py index 4ccb05256..9f6658fdc 100644 --- a/fast64_internal/sm64/sm64_f3d_writer.py +++ b/fast64_internal/sm64/sm64_f3d_writer.py @@ -3,12 +3,15 @@ from math import ceil, log, radians from mathutils import Matrix, Vector from bpy.utils import register_class, unregister_class +from bpy.types import PropertyGroup, UILayout, World +from bpy.props import StringProperty, BoolProperty, CollectionProperty, IntProperty + from ..panels import SM64_Panel from ..f3d.f3d_writer import exportF3DCommon from ..f3d.f3d_texture_writer import TexInfo from ..f3d.f3d_material import TextureProperty, tmemUsageUI, all_combiner_uses, ui_procAnim from .sm64_texscroll import modifyTexScrollFiles, modifyTexScrollHeadersGroup -from .sm64_utility import export_rom_checks, starSelectWarning +from .sm64_utility import export_rom_checks, starSelectWarning, string_int_prop from .sm64_level_parser import parseLevelAtPointer from .sm64_rom_tweaks import ExtendBank0x04 from typing import Tuple, Union, Iterable @@ -50,12 +53,15 @@ CData, CScrollData, PluginError, + copyPropertyGroup, raisePluginError, prop_split, + draw_and_check_tab, encodeSegmentedAddr, applyRotation, toAlnum, checkIfPathExists, + upgrade_old_prop, writeIfNotFound, overwriteData, getExportDir, @@ -73,7 +79,9 @@ makeWriteInfoBox, writeBoxExportType, enumExportHeaderType, + create_or_get_world, ) +from ..operators import OperatorBase from .sm64_constants import ( level_enums, @@ -101,14 +109,13 @@ class SM64Model(FModel): def __init__(self, name, DLFormat, matWriteMethod): FModel.__init__(self, name, DLFormat, matWriteMethod) self.no_light_direction = bpy.context.scene.fast64.sm64.matstack_fix + self.draw_layers = create_or_get_world(bpy.context.scene).fast64.sm64.draw_layers def getDrawLayerV3(self, obj): return int(obj.draw_layer_static) def getRenderMode(self, drawLayer): - cycle1 = getattr(bpy.context.scene.world, "draw_layer_" + str(drawLayer) + "_cycle_1") - cycle2 = getattr(bpy.context.scene.world, "draw_layer_" + str(drawLayer) + "_cycle_2") - return [cycle1, cycle2] + return self.draw_layers.layers_by_prop[drawLayer].preset class SM64GfxFormatter(GfxFormatter): @@ -855,6 +862,7 @@ def draw(self, context): class SM64_DrawLayersPanel(bpy.types.Panel): bl_label = "SM64 Draw Layers" bl_idname = "WORLD_PT_SM64_Draw_Layers_Panel" + bl_parent_id = "WORLD_PT_RDP_Default_Inspector" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "world" @@ -862,27 +870,13 @@ class SM64_DrawLayersPanel(bpy.types.Panel): @classmethod def poll(cls, context): - return context.scene.gameEditorMode == "SM64" + return context.scene.gameEditorMode == "SM64" and context.scene.world is not None def draw(self, context): - world = context.scene.world - layout = self.layout - - inputGroup = layout.column() - inputGroup.prop( - world, "menu_layers", text="Draw Layers", icon="TRIA_DOWN" if world.menu_layers else "TRIA_RIGHT" - ) - if world.menu_layers: - for i in range(8): - drawLayerUI(inputGroup, i, world) - - -def drawLayerUI(layout, drawLayer, world): - box = layout.box() - box.label(text="Layer " + str(drawLayer)) - row = box.row() - row.prop(world, "draw_layer_" + str(drawLayer) + "_cycle_1", text="") - row.prop(world, "draw_layer_" + str(drawLayer) + "_cycle_2", text="") + col = self.layout.column() + draw_layer_props = context.scene.world.fast64.sm64.draw_layers + if draw_and_check_tab(col, draw_layer_props, "tab"): + draw_layer_props.draw_props(col) class SM64_MaterialPanel(bpy.types.Panel): @@ -912,7 +906,182 @@ def draw(self, context): ui_procAnim(material, col, useDict["Texture 0"], useDict["Texture 1"], "SM64 UV Texture Scroll", False) +class SM64_DrawLayerOps(OperatorBase): + bl_idname = "scene.sm64_draw_layer_ops" + bl_label = "" + bl_description = "Move, remove, clear or add draw layers to the world" + bl_options = {"UNDO"} + + index: IntProperty() + op_name: StringProperty() + + def execute_operator(self, context): + layer_props: SM64_DrawLayerProperties = context.scene.world.fast64.sm64.draw_layers + layers = layer_props.layers + if self.op_name == "ADD": + layers.add() + added_layer = layers[-1] + copyPropertyGroup(layers[self.index], added_layer) + layers.move(len(layers) - 1, self.index + 1) + elif self.op_name == "REMOVE": # Upgrade all materials once this runs + layer_props.layers.remove(self.index) + + +class SM64_DrawLayerProperties(PropertyGroup): + name: StringProperty(name="Name", default="Custom") + enum: StringProperty(name="Enum", default="LAYER_CUSTOM") + index: StringProperty(name="Index", default="0x08") + cycle_1: StringProperty(name="", default="G_RM_AA_ZB_OPA_SURF") + cycle_2: StringProperty(name="", default="G_RM_AA_ZB_OPA_SURF2") + + @property + def preset(self): + return [self.cycle_1, self.cycle_2] + + def to_dict(self): + return {"enum": self.enum, "index": self.index, "preset": self.preset} + + def from_dict(self, data: dict): + self.enum = data.get("enum", "LAYER_CUSTOM") + self.index = data.get("index", "0x08") + self.cycle_1, self.cycle_2 = data.get("preset", ["G_RM_AA_ZB_OPA_SURF", "G_RM_AA_ZB_OPA_SURF2"]) + + def draw_props(self, layout: UILayout): + col = layout.column() + prop_split(col, self, "name", "Name") + + row = col.row() + row.prop(self, "enum") + string_int_prop(row, self, "index", "Index", split=False) + + split = col.split(factor=0.18) + split.label(text="Render Mode:") + row = split.row() + row.prop(self, "cycle_1") + row.prop(self, "cycle_2") + + +class SM64_DrawLayersProperties(PropertyGroup): + internal_defaults_set: BoolProperty() + tab: BoolProperty(name="Draw Layers") + layers: CollectionProperty(type=SM64_DrawLayerProperties) + + @property + def layers_by_enum(self): + return {layer.enum: layer for layer in self.layers} + + @property + def layers_by_prop(self): + return {str(int(layer.index, 0)): layer for layer in self.layers} + + @property + def layers_by_index(self): + return {int(layer.index, 0): layer for layer in self.layers} + + def from_dict(self, data: dict): + self.layers.clear() + for name, layer in data.items(): + self.layers.add() + self.layers[-1].from_dict(layer) + self.layers[-1].name = name + + def to_dict(self): + layers = {} + layer: SM64_DrawLayerProperties + for layer in self.layers: + layers[layer.name] = layer.to_dict() + return layers + + def to_enum(self): + return [ + ( + str(int(layer.index, 0)), + f"{name} ({layer.index})", + f"{layer.enum} ({layer.index})\n{layer.cycle_1}, {layer.cycle_2}", + ) + for name, layer in self.layers.items() + ] + + def populate(self, world: World | None = None): + """If a world is passed in, try to upgrade properties from it""" + if self.internal_defaults_set: + return + default_draw_layer_info = { + "Background": { + "enum": "LAYER_FORCE", + "index": "0x00", + "preset": ["G_RM_ZB_OPA_SURF", "G_RM_ZB_OPA_SURF2"], + }, + "Opaque": { + "enum": "LAYER_OPAQUE", + "index": "0x01", + "preset": ["G_RM_AA_ZB_OPA_SURF", "G_RM_AA_ZB_OPA_SURF2"], + }, + "Opaque Decal": { + "enum": "LAYER_OPAQUE_DECAL", + "index": "0x02", + "preset": ["G_RM_AA_ZB_OPA_DECAL", "G_RM_AA_ZB_OPA_DECAL2"], + }, + "Opaque Intersecting": { + "enum": "LAYER_OPAQUE_INTER", + "index": "0x03", + "preset": ["G_RM_AA_ZB_OPA_INTER", "G_RM_AA_ZB_OPA_INTER2"], + }, + "Cutout": { + "enum": "LAYER_ALPHA", + "index": "0x04", + "preset": ["G_RM_AA_ZB_TEX_EDGE", "G_RM_AA_ZB_TEX_EDGE2"], + }, + "Transparent": { + "enum": "LAYER_TRANSPARENT", + "index": "0x05", + "preset": ["G_RM_AA_ZB_XLU_SURF", "G_RM_AA_ZB_XLU_SURF2"], + }, + "Transparent Decal": { + "enum": "LAYER_TRANSPARENT_DECAL", + "index": "0x06", + "preset": ["G_RM_AA_ZB_XLU_DECAL", "G_RM_AA_ZB_XLU_DECAL2"], + }, + "Transparent Intersecting": { + "enum": "LAYER_TRANSPARENT_INTER", + "index": "0x07", + "preset": ["G_RM_AA_ZB_XLU_INTER", "G_RM_AA_ZB_XLU_INTER2"], + }, + } + self.from_dict(default_draw_layer_info) + + if world is not None: + for i, layer in self.layers_by_index.items(): + upgrade_old_prop(layer, "cycle_1", world, f"draw_layer_{i}_cycle_1") + upgrade_old_prop(layer, "cycle_2", world, f"draw_layer_{i}_cycle_2") + self.internal_defaults_set = True + + @staticmethod + def upgrade_changed_props(): + for world in bpy.data.worlds: + world.fast64.sm64.draw_layers.populate(world) + + def draw_props(self, layout: UILayout): + col = layout.column() + draw_layer: SM64_DrawLayerProperties + # Enumerate, then sort by index + for i, draw_layer in sorted(enumerate(self.layers), key=lambda layer: layer[1].index): + box = col.box().column() + row = box.row() + SM64_DrawLayerOps.draw_props(row, "ADD", op_name="ADD", index=i) + SM64_DrawLayerOps.draw_props(row, "REMOVE", op_name="REMOVE", index=i) + draw_layer.draw_props(box) + + +def populate_draw_layers(scene): + if scene.world is not None: + scene.world.fast64.sm64.draw_layers.populate() + + sm64_dl_writer_classes = ( + SM64_DrawLayerOps, + SM64_DrawLayerProperties, + SM64_DrawLayersProperties, SM64_ExportDL, ExportTexRectDraw, UnlinkTexRect, @@ -940,23 +1109,6 @@ def sm64_dl_writer_register(): for cls in sm64_dl_writer_classes: register_class(cls) - bpy.types.World.draw_layer_0_cycle_1 = bpy.props.StringProperty(default="G_RM_ZB_OPA_SURF") - bpy.types.World.draw_layer_0_cycle_2 = bpy.props.StringProperty(default="G_RM_ZB_OPA_SURF2") - bpy.types.World.draw_layer_1_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_OPA_SURF") - bpy.types.World.draw_layer_1_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_OPA_SURF2") - bpy.types.World.draw_layer_2_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_OPA_DECAL") - bpy.types.World.draw_layer_2_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_OPA_DECAL2") - bpy.types.World.draw_layer_3_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_OPA_INTER") - bpy.types.World.draw_layer_3_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_OPA_INTER2") - bpy.types.World.draw_layer_4_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_TEX_EDGE") - bpy.types.World.draw_layer_4_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_TEX_EDGE2") - bpy.types.World.draw_layer_5_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_XLU_SURF") - bpy.types.World.draw_layer_5_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_XLU_SURF2") - bpy.types.World.draw_layer_6_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_XLU_DECAL") - bpy.types.World.draw_layer_6_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_XLU_DECAL2") - bpy.types.World.draw_layer_7_cycle_1 = bpy.props.StringProperty(default="G_RM_AA_ZB_XLU_INTER") - bpy.types.World.draw_layer_7_cycle_2 = bpy.props.StringProperty(default="G_RM_AA_ZB_XLU_INTER2") - bpy.types.Scene.DLExportStart = bpy.props.StringProperty(name="Start", default="11D8930") bpy.types.Scene.DLExportEnd = bpy.props.StringProperty(name="End", default="11FFF00") bpy.types.Scene.levelDLExport = bpy.props.EnumProperty(items=level_enums, name="Level", default="WF") @@ -988,6 +1140,8 @@ def sm64_dl_writer_register(): bpy.types.Scene.TexRectCustomExport = bpy.props.BoolProperty(name="Custom Export Path") bpy.types.Scene.TexRectExportType = bpy.props.EnumProperty(name="Export Type", items=enumHUDExportLocation) + bpy.app.handlers.depsgraph_update_post.append(populate_draw_layers) + def sm64_dl_writer_unregister(): for cls in reversed(sm64_dl_writer_classes): @@ -1021,3 +1175,6 @@ def sm64_dl_writer_unregister(): del bpy.types.Scene.texrectImageTexture del bpy.types.Scene.TexRectCustomExport del bpy.types.Scene.TexRectExportType + + if populate_draw_layers in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.remove(populate_draw_layers) diff --git a/fast64_internal/sm64/sm64_geolayout_bone.py b/fast64_internal/sm64/sm64_geolayout_bone.py index d432210d5..8b5c9c85b 100644 --- a/fast64_internal/sm64/sm64_geolayout_bone.py +++ b/fast64_internal/sm64/sm64_geolayout_bone.py @@ -3,7 +3,7 @@ from bpy.types import Bone, Object, Panel, Operator, Armature, Mesh, Material, PropertyGroup from bpy.utils import register_class, unregister_class from ..utility import PluginError, prop_split, obj_scale_is_unified -from ..f3d.f3d_material import sm64EnumDrawLayers +from ..f3d.f3d_material import get_sm64_draw_layers from .sm64_geolayout_utility import createBoneGroups, addBoneToGroup from bpy.props import ( @@ -274,7 +274,7 @@ class SwitchOptionProperty(PropertyGroup): specificOverrideArray: CollectionProperty(type=MaterialPointerProperty, name="Specified Materials To Override") specificIgnoreArray: CollectionProperty(type=MaterialPointerProperty, name="Specified Materials To Ignore") overrideDrawLayer: BoolProperty() - drawLayer: EnumProperty(items=sm64EnumDrawLayers, name="Draw Layer") + drawLayer: EnumProperty(items=get_sm64_draw_layers, name="Draw Layer") expand: BoolProperty() @@ -506,7 +506,7 @@ def sm64_bone_register(): name="Geolayout Command", items=enumBoneType, default="DisplayListWithOffset", update=updateBone ) - Bone.draw_layer = EnumProperty(name="Draw Layer", items=sm64EnumDrawLayers, default="1") + Bone.draw_layer = EnumProperty(name="Draw Layer", items=get_sm64_draw_layers) # Scale Bone.geo_scale = FloatProperty(name="Scale", min=2 ** (-16), max=2 ** (16), default=1) @@ -540,7 +540,7 @@ def sm64_bone_register(): # Static Geolayout Object.geo_cmd_static = EnumProperty(name="Geolayout Command", items=enumGeoStaticType, default="Optimal") - Object.draw_layer_static = EnumProperty(name="Draw Layer", items=sm64EnumDrawLayers, default="1") + Object.draw_layer_static = EnumProperty(name="Draw Layer", items=get_sm64_draw_layers) Object.use_render_area = BoolProperty(name="Use Render Area") Object.culling_radius = FloatProperty(name="Culling Radius", default=10) diff --git a/fast64_internal/sm64/sm64_geolayout_classes.py b/fast64_internal/sm64/sm64_geolayout_classes.py index a0e82306e..b32ac1157 100644 --- a/fast64_internal/sm64/sm64_geolayout_classes.py +++ b/fast64_internal/sm64/sm64_geolayout_classes.py @@ -18,6 +18,7 @@ join_c_args, radians_to_s16, geoNodeRotateOrder, + create_or_get_world, ) from ..f3d.f3d_bleed import BleedGraphics from ..f3d.f3d_gbi import FModel @@ -52,30 +53,16 @@ GEO_SET_BG, ) -drawLayerNames = { - 0: "LAYER_FORCE", - 1: "LAYER_OPAQUE", - 2: "LAYER_OPAQUE_DECAL", - 3: "LAYER_OPAQUE_INTER", - 4: "LAYER_ALPHA", - 5: "LAYER_TRANSPARENT", - 6: "LAYER_TRANSPARENT_DECAL", - 7: "LAYER_TRANSPARENT_INTER", -} - - -def getDrawLayerName(drawLayer): - layer = drawLayer - if drawLayer is not None: - try: - # Cast draw layer to int so it can be mapped to a name - layer = int(drawLayer) - except ValueError: - pass - if layer in drawLayerNames: - return drawLayerNames[layer] - else: - return str(drawLayer) + +def getDrawLayerName(draw_layer: str): + layer_props = create_or_get_world(bpy.context.scene).fast64.sm64.draw_layers + if draw_layer in layer_props.layers_by_enum: + return draw_layer + try: + assert draw_layer in layer_props.layers_by_prop + return layer_props.layers_by_prop[draw_layer].enum + except ValueError: + raise PluginError(f"{draw_layer} is not a valid int") def addFuncAddress(command, func): diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index ad05ab2d05..d59c59276 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -3,7 +3,7 @@ from mathutils import * from .utility_anim import * from typing import Callable, Iterable, Any, Optional, Tuple, TypeVar, Union -from bpy.types import UILayout +from bpy.types import UILayout, Scene, World CollectionProperty = Any # collection prop as defined by using bpy.props.CollectionProperty @@ -1868,3 +1868,30 @@ def upgrade_old_prop( print(f"Failed to upgrade {new_prop} from old location {old_loc} with props {old_props}") traceback.print_exc() return False + + +WORLD_WARNING_COUNT = 0 + + +def create_or_get_world(scene: Scene) -> World: + """ + Given a scene, this function will return: + - The world selected in the scene if the scene has a selected world. + - The first world in bpy.data.worlds if the current file has a world. (Which it almost always does because of the f3d nodes library) + - Create a world named "Fast64" and return it if no world exits. + This function does not assign any world to the scene. + """ + global WORLD_WARNING_COUNT + if scene.world: + WORLD_WARNING_COUNT = 0 + return scene.world + elif bpy.data.worlds: + world: World = bpy.data.worlds.values()[0] + if WORLD_WARNING_COUNT < 10: + print(f'No world selected in scene, selected the first one found in this file "{world.name}".') + WORLD_WARNING_COUNT += 1 + return world + else: # Almost never reached because the node library has its own world + WORLD_WARNING_COUNT = 0 + print(f'No world in this file, creating world named "Fast64".') + return bpy.data.worlds.new("Fast64") From 3989d253aefbca100493ac0884f005ee128cc24f Mon Sep 17 00:00:00 2001 From: Lila Date: Tue, 20 Aug 2024 13:07:36 +0100 Subject: [PATCH 2/6] Draw layer can now update fog --- fast64_internal/f3d/f3d_material.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fast64_internal/f3d/f3d_material.py b/fast64_internal/f3d/f3d_material.py index 57c870dcd..1c21f1f63 100644 --- a/fast64_internal/f3d/f3d_material.py +++ b/fast64_internal/f3d/f3d_material.py @@ -142,6 +142,7 @@ def update_draw_layer(self, context): material.f3d_mat.presetName = "Custom" update_blend_method(material, context) + update_fog_nodes(material, context) set_output_node_groups(material) drawLayer = material.f3d_mat.draw_layer From a13d6fe7764c383c9098a75edddcea57b34f4091 Mon Sep 17 00:00:00 2001 From: Lila Date: Tue, 20 Aug 2024 21:45:42 +0100 Subject: [PATCH 3/6] Warn and error on repeated index Having index is meant to give the user control over which layers they will use in fast64, it shouldn't cause headaches instead amend for black formatting --- fast64_internal/sm64/sm64_f3d_writer.py | 64 ++++++++++++++++++++----- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/fast64_internal/sm64/sm64_f3d_writer.py b/fast64_internal/sm64/sm64_f3d_writer.py index 9f2b9ada5..872ba00d2 100644 --- a/fast64_internal/sm64/sm64_f3d_writer.py +++ b/fast64_internal/sm64/sm64_f3d_writer.py @@ -116,6 +116,10 @@ def __init__(self, name, DLFormat, matWriteMethod): FModel.__init__(self, name, DLFormat, matWriteMethod) self.no_light_direction = bpy.context.scene.fast64.sm64.matstack_fix self.draw_layers = create_or_get_world(bpy.context.scene).fast64.sm64.draw_layers + if self.draw_layers.repeated_indices: + raise PluginError( + f"World draw layers have repeated indexes: " + self.draw_layers.repeated_indices_str.replace("\n", " ") + ) def getDrawLayerV3(self, obj): return int(obj.draw_layer_static) @@ -938,7 +942,7 @@ def execute_operator(self, context): class SM64_DrawLayerProperties(PropertyGroup): name: StringProperty(name="Name", default="Custom") enum: StringProperty(name="Enum", default="LAYER_CUSTOM") - index: StringProperty(name="Index", default="0x08", update=update_world_default_rendermode) + str_index: StringProperty(name="Index", default="0x08", update=update_world_default_rendermode) cycle_1: StringProperty(name="", default="G_RM_AA_ZB_OPA_SURF", update=update_world_default_rendermode) cycle_2: StringProperty(name="", default="G_RM_AA_ZB_OPA_SURF2", update=update_world_default_rendermode) @@ -946,12 +950,16 @@ class SM64_DrawLayerProperties(PropertyGroup): def preset(self): return [self.cycle_1, self.cycle_2] + @property + def index(self): + return int(self.str_index, 0) + def to_dict(self): - return {"enum": self.enum, "index": self.index, "preset": self.preset} + return {"enum": self.enum, "index": self.str_index, "preset": self.preset} def from_dict(self, data: dict): self.enum = data.get("enum", "LAYER_CUSTOM") - self.index = data.get("index", "0x08") + self.str_index = data.get("index", "0x08") self.cycle_1, self.cycle_2 = data.get("preset", ["G_RM_AA_ZB_OPA_SURF", "G_RM_AA_ZB_OPA_SURF2"]) def draw_props(self, layout: UILayout): @@ -960,7 +968,7 @@ def draw_props(self, layout: UILayout): row = col.row() row.prop(self, "enum") - string_int_prop(row, self, "index", "Index", split=False) + string_int_prop(row, self, "str_index", "Index", split=False) split = col.split(factor=0.18) split.label(text="Render Mode:") @@ -977,17 +985,35 @@ class SM64_DrawLayersProperties(PropertyGroup): ) # As custom draw layers are very niche, it's best to lock the operators by default layers: CollectionProperty(type=SM64_DrawLayerProperties) + @property + def repeated_indices(self) -> dict: + indices, repeated_indices = {}, {} + for layer in self.layers: + if layer.index in indices: + repeated_indices[layer.index] = indices[layer.index] + indices[layer.index].append(layer) + else: + indices[layer.index] = [layer] + return repeated_indices + + @property + def repeated_indices_str(self) -> str: + return "\n".join( + f"{index}: [{', '.join([layer.name for layer in layers])}]" + for index, layers in self.repeated_indices.items() + ) + @property def layers_by_enum(self): return {layer.enum: layer for layer in self.layers} @property def layers_by_prop(self): - return {str(int(layer.index, 0)): layer for layer in self.layers} + return {str(layer.index): layer for layer in self.layers} @property def layers_by_index(self): - return {int(layer.index, 0): layer for layer in self.layers} + return {layer.index: layer for layer in self.layers} def from_dict(self, data: dict): self.layers.clear() @@ -1006,9 +1032,10 @@ def to_dict(self): def to_enum(self): return [ ( - str(int(layer.index, 0)), - f"{name} ({layer.index})", + str(layer.index), + f"{name} ({layer.str_index})", f"{layer.enum} ({layer.index})\n{layer.cycle_1}, {layer.cycle_2}", + layer.index, ) for name, layer in self.layers.items() ] @@ -1082,6 +1109,7 @@ def draw_props(self, layout: UILayout): ) col.row().prop(self, "lock", icon="LOCKED" if self.lock else "UNLOCKED") if not self.lock: + col.separator() multilineLabel( col, "These won´t modify your repo's draw layers automatically!\n" @@ -1092,14 +1120,28 @@ def draw_props(self, layout: UILayout): layers_col = col.column() layers_col.enabled = not self.lock + if self.repeated_indices: + col.separator() + error_box = layers_col.box() + error_box.alert = True + multilineLabel(error_box, text=f"Repeated indices:\n{self.repeated_indices_str}", icon="ERROR") + col.separator() + draw_layer: SM64_DrawLayerProperties # Enumerate, then sort by index for i, draw_layer in sorted(enumerate(self.layers), key=lambda layer: layer[1].index): - box = layers_col.box().column() - row = box.row() + layer_box = layers_col.box().column() + if draw_layer.index in self.repeated_indices: + bad_layers = self.repeated_indices[draw_layer.index] + bad_layers.remove(draw_layer) + layer_box.alert = True + layer_box.box().label( + text=f"Duplicate index at {', '.join(layer.name for layer in bad_layers)}", icon="ERROR" + ) + row = layer_box.row() SM64_DrawLayerOps.draw_props(row, "ADD", op_name="ADD", index=i) SM64_DrawLayerOps.draw_props(row, "REMOVE", op_name="REMOVE", index=i) - draw_layer.draw_props(box) + draw_layer.draw_props(layer_box) def populate_draw_layers(scene): From 122ed57d3754579c520938c8f975a439be3f8bac Mon Sep 17 00:00:00 2001 From: Lila Date: Wed, 21 Aug 2024 11:27:47 +0100 Subject: [PATCH 4/6] small naming change --- fast64_internal/sm64/sm64_f3d_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast64_internal/sm64/sm64_f3d_writer.py b/fast64_internal/sm64/sm64_f3d_writer.py index 872ba00d2..0f147fef9 100644 --- a/fast64_internal/sm64/sm64_f3d_writer.py +++ b/fast64_internal/sm64/sm64_f3d_writer.py @@ -1034,7 +1034,7 @@ def to_enum(self): ( str(layer.index), f"{name} ({layer.str_index})", - f"{layer.enum} ({layer.index})\n{layer.cycle_1}, {layer.cycle_2}", + f"{layer.enum} ({layer.str_index})\n{layer.cycle_1}, {layer.cycle_2}", layer.index, ) for name, layer in self.layers.items() From c24a278adc162d6c5d0ffa028feb5dfeab04eaa8 Mon Sep 17 00:00:00 2001 From: Lila Date: Wed, 21 Aug 2024 11:35:30 +0100 Subject: [PATCH 5/6] put warning into lock description --- fast64_internal/sm64/sm64_f3d_writer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/fast64_internal/sm64/sm64_f3d_writer.py b/fast64_internal/sm64/sm64_f3d_writer.py index 0f147fef9..36e425ddb 100644 --- a/fast64_internal/sm64/sm64_f3d_writer.py +++ b/fast64_internal/sm64/sm64_f3d_writer.py @@ -981,8 +981,11 @@ class SM64_DrawLayersProperties(PropertyGroup): internal_defaults_set: BoolProperty() tab: BoolProperty(name="Draw Layers") lock: BoolProperty( - name="", default=True - ) # As custom draw layers are very niche, it's best to lock the operators by default + name="", + default=True, + description="(This feature is for advanced users, use at your own risk)\n" + "Disable the lock to remove or add new draw layers.", + ) layers: CollectionProperty(type=SM64_DrawLayerProperties) @property From 728f36889ddbda168ce2a9735aa2f4b0f4fbaafe Mon Sep 17 00:00:00 2001 From: Lila Date: Wed, 21 Aug 2024 12:15:45 +0100 Subject: [PATCH 6/6] Some last usability improvements --- fast64_internal/operators.py | 6 +++-- fast64_internal/sm64/sm64_f3d_writer.py | 36 ++++++++++++++++++++----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/fast64_internal/operators.py b/fast64_internal/operators.py index e4a2041cd..cbaeca5ec 100644 --- a/fast64_internal/operators.py +++ b/fast64_internal/operators.py @@ -22,10 +22,12 @@ class OperatorBase(Operator): icon = "NONE" @classmethod - def draw_props(cls, layout: UILayout, icon="", text: Optional[str] = None, **op_values): + def draw_props(cls, layout: UILayout, icon="", text: Optional[str] = None, enabled=True, **op_values): """Op args are passed to the operator via setattr()""" icon = icon if icon else cls.icon - op = layout.operator(cls.bl_idname, icon=icon, text=text) + col = layout.column() + col.enabled = enabled + op = col.operator(cls.bl_idname, icon=icon, text=text) for key, value in op_values.items(): setattr(op, key, value) return op diff --git a/fast64_internal/sm64/sm64_f3d_writer.py b/fast64_internal/sm64/sm64_f3d_writer.py index 36e425ddb..d20f5d9ff 100644 --- a/fast64_internal/sm64/sm64_f3d_writer.py +++ b/fast64_internal/sm64/sm64_f3d_writer.py @@ -86,6 +86,7 @@ writeBoxExportType, enumExportHeaderType, create_or_get_world, + intToHex, ) from ..operators import OperatorBase @@ -919,7 +920,7 @@ def draw(self, context): class SM64_DrawLayerOps(OperatorBase): bl_idname = "scene.sm64_draw_layer_ops" bl_label = "" - bl_description = "Move, remove, clear or add draw layers to the world" + bl_description = "Remove or add draw layers to the world" bl_options = {"UNDO"} index: IntProperty() @@ -933,10 +934,19 @@ def execute_operator(self, context): if self.op_name == "ADD": layers.add() added_layer = layers[-1] - copyPropertyGroup(layers[self.index], added_layer) + base_layer = layers[self.index] + copyPropertyGroup(base_layer, added_layer) + added_layer.name = f"{base_layer.name} (Copy)" + added_layer.enum = f"{base_layer.enum}_COPY" + added_layer.str_index = intToHex(layers[self.index].index + 1, 1) layers.move(len(layers) - 1, self.index + 1) - elif self.op_name == "REMOVE": # Upgrade all materials once this runs + elif self.op_name == "REMOVE": layer_props.layers.remove(self.index) + layer_props.update_all_materials(self.index) + elif self.op_name == "DEFAULTS": + layer_props.populate(force_default=True) + else: + raise NotImplementedError(f'Unimplemented internal draw layer op "{self.op_name}"') class SM64_DrawLayerProperties(PropertyGroup): @@ -1043,9 +1053,17 @@ def to_enum(self): for name, layer in self.layers.items() ] - def populate(self, world: World | None = None): + def update_all_materials(self, layer_index=-1): + """Update draw layers in materials to ensure any removed draw layer is not being used""" + fallback = list(self.layers_by_prop.values())[0] + for material in bpy.data.materials: + if not material.is_f3d: + continue + material.f3d_mat.draw_layer.sm64 = str(self.layers_by_index.get(layer_index - 1, fallback).index) + + def populate(self, world: World | None = None, force_default=False): """If a world is passed in, try to upgrade properties from it""" - if self.internal_defaults_set: + if self.internal_defaults_set and not force_default: return default_draw_layer_info = { "Background": { @@ -1091,7 +1109,7 @@ def populate(self, world: World | None = None): } self.from_dict(default_draw_layer_info) - if world is not None: + if world is not None and not force_default: for i, layer in self.layers_by_index.items(): upgrade_old_prop(layer, "cycle_1", world, f"draw_layer_{i}_cycle_1") upgrade_old_prop(layer, "cycle_2", world, f"draw_layer_{i}_cycle_2") @@ -1123,6 +1141,10 @@ def draw_props(self, layout: UILayout): layers_col = col.column() layers_col.enabled = not self.lock + SM64_DrawLayerOps.draw_props( + layers_col.row(), "MODIFIER_OFF", text="Reset to vanilla SM64 defaults", op_name="DEFAULTS" + ) + if self.repeated_indices: col.separator() error_box = layers_col.box() @@ -1143,7 +1165,7 @@ def draw_props(self, layout: UILayout): ) row = layer_box.row() SM64_DrawLayerOps.draw_props(row, "ADD", op_name="ADD", index=i) - SM64_DrawLayerOps.draw_props(row, "REMOVE", op_name="REMOVE", index=i) + op = SM64_DrawLayerOps.draw_props(row, "REMOVE", enabled=len(self.layers) > 1, op_name="REMOVE", index=i) draw_layer.draw_props(layer_box)