diff --git a/fast64_internal/utility.py b/fast64_internal/utility.py index 61b503dcd..ea5a730ee 100644 --- a/fast64_internal/utility.py +++ b/fast64_internal/utility.py @@ -2058,3 +2058,20 @@ def get_include_data(include: str, strip: bool = False): # return the data as a string return data + + +def get_new_object( + name: str, data: Optional[Any], selectObject: bool, parentObj: Optional[bpy.types.Object] +) -> bpy.types.Object: + newObj = bpy.data.objects.new(name=name, object_data=data) + bpy.context.view_layer.active_layer_collection.collection.objects.link(newObj) + + if selectObject: + newObj.select_set(True) + bpy.context.view_layer.objects.active = newObj + + newObj.parent = parentObj + newObj.location = [0.0, 0.0, 0.0] + newObj.rotation_euler = [0.0, 0.0, 0.0] + newObj.scale = [1.0, 1.0, 1.0] + return newObj diff --git a/fast64_internal/z64/collection_utility.py b/fast64_internal/z64/collection_utility.py index 98da2cc66..8b5c77af8 100644 --- a/fast64_internal/z64/collection_utility.py +++ b/fast64_internal/z64/collection_utility.py @@ -80,6 +80,8 @@ def getCollection(objName, collectionType, subIndex): collection = obj.ootAlternateSceneHeaders.cutsceneHeaders elif collectionType == "Light": collection = getCollectionFromIndex(obj, "lightList", subIndex, False) + elif collectionType == "ToD Light": + collection = getCollectionFromIndex(obj, "tod_lights", subIndex, False) elif collectionType == "Exit": collection = getCollectionFromIndex(obj, "exitList", subIndex, False) elif collectionType == "Object": diff --git a/fast64_internal/z64/cutscene/classes.py b/fast64_internal/z64/cutscene/classes.py index 5745f085a..ffb6cbc90 100644 --- a/fast64_internal/z64/cutscene/classes.py +++ b/fast64_internal/z64/cutscene/classes.py @@ -3,6 +3,8 @@ from dataclasses import dataclass, field from bpy.types import Object from typing import Optional + +from ...utility import get_new_object from ...game_data import game_data from .motion.utility import getBlenderPosition, getBlenderRotation, getRotation, getInteger @@ -498,26 +500,14 @@ class Cutscene: class CutsceneObjectFactory: """This class contains functions to create new Blender objects""" - def getNewObject(self, name: str, data, selectObject: bool, parentObj: Object) -> Object: - newObj = bpy.data.objects.new(name=name, object_data=data) - bpy.context.view_layer.active_layer_collection.collection.objects.link(newObj) - if selectObject: - newObj.select_set(True) - bpy.context.view_layer.objects.active = newObj - newObj.parent = parentObj - newObj.location = [0.0, 0.0, 0.0] - newObj.rotation_euler = [0.0, 0.0, 0.0] - newObj.scale = [1.0, 1.0, 1.0] - return newObj - def getNewEmptyObject(self, name: str, selectObject: bool, parentObj: Object): - return self.getNewObject(name, None, selectObject, parentObj) + return get_new_object(name, None, selectObject, parentObj) def getNewArmatureObject(self, name: str, selectObject: bool, parentObj: Object): newArmatureData = bpy.data.armatures.new(name) newArmatureData.display_type = "STICK" newArmatureData.show_names = True - newArmatureObject = self.getNewObject(name, newArmatureData, selectObject, parentObj) + newArmatureObject = get_new_object(name, newArmatureData, selectObject, parentObj) return newArmatureObject def getNewCutsceneObject(self, name: str, frameCount: int, parentObj: Object): @@ -595,7 +585,7 @@ def getNewCameraObject( self, name: str, displaySize: float, clipStart: float, clipEnd: float, alpha: float, parentObj: Object ): newCamera = bpy.data.cameras.new(name) - newCameraObj = self.getNewObject(name, newCamera, False, parentObj) + newCameraObj = get_new_object(name, newCamera, False, parentObj) newCameraObj.data.display_size = displaySize newCameraObj.data.clip_start = clipStart newCameraObj.data.clip_end = clipEnd diff --git a/fast64_internal/z64/exporter/scene/general.py b/fast64_internal/z64/exporter/scene/general.py index 572574590..b18effb60 100644 --- a/fast64_internal/z64/exporter/scene/general.py +++ b/fast64_internal/z64/exporter/scene/general.py @@ -4,7 +4,7 @@ from bpy.types import Object from ....utility import PluginError, CData, exportColor, ootGetBaseOrCustomLight, hexOrDecInt, indent -from ...scene.properties import OOTSceneHeaderProperty, OOTLightProperty +from ...scene.properties import OOTSceneHeaderProperty, OOTLightProperty, OOTLightGroupProperty from ...utility import getEvalParamsInt from ..utility import Utility @@ -14,6 +14,7 @@ class EnvLightSettings: """This class defines the information of one environment light setting""" envLightMode: str + setting_name: str ambientColor: tuple[int, int, int] light1Color: tuple[int, int, int] light1Dir: tuple[int, int, int] @@ -65,6 +66,7 @@ def from_data(raw_data: str, not_zapd_assets: bool): lights.append( EnvLightSettings( "Custom", + "Custom Light Settings", tuple(colors_and_dirs[0]), tuple(colors_and_dirs[1]), tuple(colors_and_dirs[2]), @@ -94,11 +96,9 @@ def getDirectionValues(self, vector: tuple[int, int, int]): return ", ".join(f"{v - 0x100 if v > 0x7F else v:5}" for v in vector) - def getEntryC(self, index: int): + def getEntryC(self): """Returns an environment light entry""" - isLightingCustom = self.envLightMode == "Custom" - vectors = [ (self.ambientColor, "Ambient Color", self.getColorValues), (self.light1Dir, "Diffuse0 Direction", self.getDirectionValues), @@ -113,21 +113,8 @@ def getEntryC(self, index: int): (f"{self.zFar}", "Fog Far"), ] - lightDescs = ["Dawn", "Day", "Dusk", "Night"] - - if not isLightingCustom and self.envLightMode == "LIGHT_MODE_TIME": - # TODO: Improve the lighting system. - # Currently Fast64 assumes there's only 4 possible settings for "Time of Day" lighting. - # This is not accurate and more complicated, - # for now we are doing ``index % 4`` to avoid having an OoB read in the list - # but this will need to be changed the day the lighting system is updated. - lightDesc = f"// {lightDescs[index % 4]} Lighting\n" - else: - isIndoor = not isLightingCustom and self.envLightMode == "LIGHT_MODE_SETTINGS" - lightDesc = f"// {'Indoor' if isIndoor else 'Custom'} No. {index + 1} Lighting\n" - lightData = ( - (indent + lightDesc) + (indent + f"// {self.setting_name}\n") + (indent + "{\n") + "".join( indent * 2 + f"{'{ ' + vecToC(vector) + ' },':26} // {desc}\n" for (vector, desc, vecToC) in vectors @@ -152,12 +139,22 @@ def new(name: str, props: OOTSceneHeaderProperty): envLightMode = Utility.getPropValue(props, "skyboxLighting") lightList: dict[str, OOTLightProperty] = {} settings: list[EnvLightSettings] = [] + is_custom = props.skyboxLighting == "Custom" + + if not is_custom and envLightMode == "LIGHT_MODE_TIME": + tod_lights: list[OOTLightGroupProperty] = [props.timeOfDayLights] + list(props.tod_lights) - if envLightMode == "LIGHT_MODE_TIME": - todLights = props.timeOfDayLights - lightList = {"Dawn": todLights.dawn, "Day": todLights.day, "Dusk": todLights.dusk, "Night": todLights.night} + for i, tod_light in enumerate(tod_lights): + for tod_type in ["Dawn", "Day", "Dusk", "Night"]: + setting_name = ( + f"Default Settings ({tod_type})" if i == 0 else f"Light Settings No. {i} ({tod_type})" + ) + lightList[setting_name] = getattr(tod_light, tod_type.lower()) else: - lightList = {str(i): light for i, light in enumerate(props.lightList)} + is_indoor = not is_custom and envLightMode == "LIGHT_MODE_SETTINGS" + lightList = { + f"{'Indoor' if is_indoor else 'Custom'} No. {i + 1}": light for i, light in enumerate(props.lightList) + } for setting_name, lightProp in lightList.items(): try: @@ -166,6 +163,7 @@ def new(name: str, props: OOTSceneHeaderProperty): settings.append( EnvLightSettings( envLightMode, + setting_name, exportColor(lightProp.ambient), light1[0], light1[1], @@ -199,7 +197,7 @@ def getC(self): # .c lightSettingsC.source = ( - (lightName + " = {\n") + "".join(light.getEntryC(i) for i, light in enumerate(self.settings)) + "};\n\n" + (lightName + " = {\n") + "".join(light.getEntryC() for i, light in enumerate(self.settings)) + "};\n\n" ) return lightSettingsC diff --git a/fast64_internal/z64/importer/scene_header.py b/fast64_internal/z64/importer/scene_header.py index 5afed4355..5674cfcae 100644 --- a/fast64_internal/z64/importer/scene_header.py +++ b/fast64_internal/z64/importer/scene_header.py @@ -1,5 +1,4 @@ import math -import os import re import bpy import mathutils @@ -8,12 +7,12 @@ from typing import Optional from ...game_data import game_data -from ...utility import PluginError, readFile, parentObject, hexOrDecInt, gammaInverse +from ...utility import PluginError, get_new_object, parentObject, hexOrDecInt, gammaInverse from ...f3d.f3d_parser import parseMatrices from ..exporter.scene.general import EnvLightSettings from ..model_classes import OOTF3DContext from ..scene.properties import OOTSceneHeaderProperty, OOTLightProperty -from ..utility import getEvalParams, setCustomProperty +from ..utility import setCustomProperty from .constants import headerNames from .utility import getDataMatch, stripName, parse_commands_data from .classes import SharedSceneData @@ -58,7 +57,7 @@ def parseDirection(index: int, values: tuple[int, int, int]) -> tuple[float, flo def parseLight( - lightHeader: OOTLightProperty, index: int, rotation: mathutils.Euler, color: mathutils.Vector + lightHeader: OOTLightProperty, index: int, rotation: mathutils.Euler, color: mathutils.Vector, desc: str ) -> bpy.types.Object | None: setattr(lightHeader, f"useCustomDiffuse{index}", rotation != "Zero" and rotation != "Default") @@ -67,8 +66,8 @@ def parseLight( setattr(lightHeader, f"diffuse{index}", color + (1,)) return None else: - light = bpy.data.lights.new("Light", "SUN") - lightObj = bpy.data.objects.new("Light", light) + light = bpy.data.lights.new(f"{desc} Diffuse {index} Light", "SUN") + lightObj = bpy.data.objects.new(f"{desc} Diffuse {index}", light) bpy.context.scene.collection.objects.link(lightObj) setattr(lightHeader, f"diffuse{index}Custom", lightObj.data) lightObj.rotation_euler = rotation @@ -77,6 +76,39 @@ def parseLight( return lightObj +def set_light_props( + parent_obj: bpy.types.Object, + light_props: OOTLightProperty, + header_index: int, + index: int, + light_entry: EnvLightSettings, + desc: str, +): + ambient_col = parseColor(light_entry.ambientColor) + diffuse0_dir = parseDirection(0, light_entry.light1Dir) + diffuse0_col = parseColor(light_entry.light1Color) + diffuse1_dir = parseDirection(1, light_entry.light2Dir) + diffuse1_col = parseColor(light_entry.light2Color) + fog_col = parseColor(light_entry.fogColor) + + light_props.ambient = ambient_col + (1,) + + lightObj0 = parseLight(light_props, 0, diffuse0_dir, diffuse0_col, desc) + lightObj1 = parseLight(light_props, 1, diffuse1_dir, diffuse1_col, desc) + + if lightObj0 is not None: + parentObject(parent_obj, lightObj0) + lightObj0.location = [4 + header_index * 2, 0, -index * 2] + if lightObj1 is not None: + parentObject(parent_obj, lightObj1) + lightObj1.location = [4 + header_index * 2, 2, -index * 2] + + light_props.fogColor = fog_col + (1,) + light_props.fogNear = light_entry.fogNear + light_props.z_far = light_entry.zFar + light_props.transitionSpeed = light_entry.blendRate + + def parseLightList( sceneObj: bpy.types.Object, sceneHeader: OOTSceneHeaderProperty, @@ -86,41 +118,61 @@ def parseLightList( sharedSceneData: SharedSceneData, ): lightData = getDataMatch(sceneData, lightListName, ["LightSettings", "EnvLightSettings"], "light list", strip=True) + lightList = EnvLightSettings.from_data(lightData, sharedSceneData.not_zapd_assets) - # I currently don't understand the light list format in respect to this lighting flag. - # So we'll set it to custom instead. - if sceneHeader.skyboxLighting != "Custom": - sceneHeader.skyboxLightingCustom = sceneHeader.skyboxLighting - sceneHeader.skyboxLighting = "Custom" + sceneHeader.tod_lights.clear() sceneHeader.lightList.clear() - lightList = EnvLightSettings.from_data(lightData, sharedSceneData.not_zapd_assets) + lights_empty = None + if len(lightList) > 0: + lights_empty = get_new_object(f"{sceneObj.name} Lights (header {headerIndex})", None, False, sceneObj) + lights_empty.ootEmptyType = "None" + + parent_obj = lights_empty if lights_empty is not None else sceneObj + + custom_value = None + if sceneHeader.skyboxLighting == "Custom": + # try to convert the custom value to an int + try: + custom_value = hexOrDecInt(sceneHeader.skyboxLightingCustom) + except: + custom_value = None + + # for older decomps, make sure it's using the right thing for convenience + if custom_value is not None and custom_value <= 1: + sceneHeader.skyboxLighting = "LIGHT_MODE_TIME" if custom_value == 0 else "LIGHT_MODE_SETTINGS" + + for i, lightEntry in enumerate(lightList): + if sceneHeader.skyboxLighting == "LIGHT_MODE_TIME": + new_tod_light = sceneHeader.tod_lights.add() if i > 0 else None + + settings_name = "Default Settings" if i == 0 else f"Light Settings {i}" + sub_lights_empty = get_new_object(f"(Header {headerIndex}) {settings_name}", None, False, parent_obj) + sub_lights_empty.ootEmptyType = "None" + + for tod_type in ["Dawn", "Day", "Dusk", "Night"]: + desc = f"{settings_name} ({tod_type})" + + if i == 0: + set_light_props( + sub_lights_empty, + getattr(sceneHeader.timeOfDayLights, tod_type.lower()), + headerIndex, + i, + lightEntry, + desc, + ) + else: + assert new_tod_light is not None + set_light_props( + sub_lights_empty, getattr(new_tod_light, tod_type.lower()), headerIndex, i, lightEntry, desc + ) + else: + settings_name = "Indoor" if sceneHeader.skyboxLighting != "Custom" else "Custom" + desc = f"{settings_name} {i}" - for index, lightEntry in enumerate(lightList): - ambientColor = parseColor(lightEntry.ambientColor) - diffuseDir0 = parseDirection(0, lightEntry.light1Dir) - diffuseColor0 = parseColor(lightEntry.light1Color) - diffuseDir1 = parseDirection(1, lightEntry.light2Dir) - diffuseColor1 = parseColor(lightEntry.light2Color) - fogColor = parseColor(lightEntry.fogColor) - - lightHeader = sceneHeader.lightList.add() - lightHeader.ambient = ambientColor + (1,) - - lightObj0 = parseLight(lightHeader, 0, diffuseDir0, diffuseColor0) - lightObj1 = parseLight(lightHeader, 1, diffuseDir1, diffuseColor1) - - if lightObj0 is not None: - parentObject(sceneObj, lightObj0) - lightObj0.location = [4 + headerIndex * 2, 0, -index * 2] - if lightObj1 is not None: - parentObject(sceneObj, lightObj1) - lightObj1.location = [4 + headerIndex * 2, 2, -index * 2] - - lightHeader.fogColor = fogColor + (1,) - lightHeader.fogNear = lightEntry.fogNear - lightHeader.z_far = lightEntry.zFar - lightHeader.transitionSpeed = lightEntry.blendRate + # indoor and custom modes shares the same properties + set_light_props(parent_obj, sceneHeader.lightList.add(), headerIndex, i, lightEntry, desc) def parseExitList(sceneHeader: OOTSceneHeaderProperty, sceneData: str, exitListName: str): diff --git a/fast64_internal/z64/scene/properties.py b/fast64_internal/z64/scene/properties.py index 78ba22db7..34dca46f0 100644 --- a/fast64_internal/z64/scene/properties.py +++ b/fast64_internal/z64/scene/properties.py @@ -1,5 +1,6 @@ import bpy +from typing import Optional from bpy.types import PropertyGroup, Object, Light, UILayout, Scene, Context from bpy.props import ( EnumProperty, @@ -176,7 +177,14 @@ class OOTLightProperty(PropertyGroup): expandTab: BoolProperty(name="Expand Tab") def draw_props( - self, layout: UILayout, name: str, showExpandTab: bool, index: int, sceneHeaderIndex: int, objName: str + self, + layout: UILayout, + name: str, + showExpandTab: bool, + index: Optional[int], + sceneHeaderIndex: Optional[int], + objName: Optional[str], + collection_type: Optional[str], ): if showExpandTab: box = layout.box().column() @@ -187,28 +195,25 @@ def draw_props( expandTab = True if expandTab: - if index is not None: - drawCollectionOps(box, index, "Light", sceneHeaderIndex, objName) + if index is not None and collection_type is not None: + drawCollectionOps(box, index, collection_type, sceneHeaderIndex, objName) prop_split(box, self, "ambient", "Ambient Color") - if self.useCustomDiffuse0: - prop_split(box, self, "diffuse0Custom", "Diffuse 0") - box.label(text="Make sure light is not part of scene hierarchy.", icon="FILE_PARENT") - else: - prop_split(box, self, "diffuse0", "Diffuse 0") - box.prop(self, "useCustomDiffuse0") - - if self.useCustomDiffuse1: - prop_split(box, self, "diffuse1Custom", "Diffuse 1") - box.label(text="Make sure light is not part of scene hierarchy.", icon="FILE_PARENT") - else: - prop_split(box, self, "diffuse1", "Diffuse 1") - box.prop(self, "useCustomDiffuse1") + def draw_diffuse(index: int): + layout_diffuse = box.box() + layout_diffuse.prop(self, f"useCustomDiffuse{index}") + if self.useCustomDiffuse0: + layout_diffuse.label(text="Make sure light is not part of scene hierarchy.") + prop_split(layout_diffuse, self, f"diffuse{index}Custom", f"Diffuse {index} Object") + else: + prop_split(layout_diffuse, self, f"diffuse{index}", f"Diffuse{index} Color") + draw_diffuse(0) + draw_diffuse(1) prop_split(box, self, "fogColor", "Fog Color") prop_split(box, self, "fogNear", "Fog Near (Fog Far=1000)") prop_split(box, self, "z_far", "Z Far (Draw Distance)") - prop_split(box, self, "transitionSpeed", "Transition Speed") + prop_split(box, self, "transitionSpeed", "Blend Rate") class OOTLightGroupProperty(PropertyGroup): @@ -220,17 +225,23 @@ class OOTLightGroupProperty(PropertyGroup): night: PointerProperty(type=OOTLightProperty) defaultsSet: BoolProperty() - def draw_props(self, layout: UILayout): + def draw_props(self, layout: UILayout, index: Optional[int], header_index: int, obj_name: str): box = layout.column() - box.row().prop(self, "menuTab", expand=True) - if self.menuTab == "Dawn": - self.dawn.draw_props(box, "Dawn", False, None, None, None) - if self.menuTab == "Day": - self.day.draw_props(box, "Day", False, None, None, None) - if self.menuTab == "Dusk": - self.dusk.draw_props(box, "Dusk", False, None, None, None) - if self.menuTab == "Night": - self.night.draw_props(box, "Night", False, None, None, None) + + text = "Default Settings" if index is None else f"Light Settings No. {index + 1}" + box.prop(self, "expandTab", text=text, icon="TRIA_DOWN" if self.expandTab else "TRIA_RIGHT") + + if self.expandTab: + if index is not None: + drawCollectionOps(box, index, "ToD Light", header_index, obj_name) + + box.row().prop(self, "menuTab", expand=True) + + for tod_type in ["Dawn", "Day", "Dusk", "Night"]: + if self.menuTab == tod_type: + getattr(self, tod_type.lower()).draw_props( + box, tod_type, False, index, header_index, obj_name, None + ) class OOTSceneTableEntryProperty(PropertyGroup): @@ -289,8 +300,12 @@ class OOTSceneHeaderProperty(PropertyGroup): audioSessionPreset: EnumProperty(name="Audio Session Preset", items=ootEnumAudioSessionPreset, default="0x00") audioSessionPresetCustom: StringProperty(name="Audio Session Preset", default="0x00") + # ideally `timeOfDayLights` would be removed in favor of `tod_lights` + # but it's easier to keep it since we need at least one element in the collection timeOfDayLights: PointerProperty(type=OOTLightGroupProperty, name="Time Of Day Lighting") + tod_lights: CollectionProperty(type=OOTLightGroupProperty) lightList: CollectionProperty(type=OOTLightProperty, name="Lighting List") + exitList: CollectionProperty(type=OOTExitProperty, name="Exit List") writeCutscene: BoolProperty(name="Write Cutscene") @@ -371,11 +386,16 @@ def draw_props(self, layout: UILayout, dropdownLabel: str, headerIndex: int, obj lighting = layout.column() lighting.box().label(text="Lighting List") drawEnumWithCustom(lighting, self, "skyboxLighting", "Lighting Mode", "") + if self.skyboxLighting == "LIGHT_MODE_TIME": # Time of Day - self.timeOfDayLights.draw_props(lighting) + self.timeOfDayLights.draw_props(lighting.box(), None, headerIndex, objName) + + for i, tod_light in enumerate(self.tod_lights): + tod_light.draw_props(lighting.box(), i, headerIndex, objName) + drawAddButton(lighting, len(self.tod_lights), "ToD Light", headerIndex, objName) else: for i in range(len(self.lightList)): - self.lightList[i].draw_props(lighting, "Lighting " + str(i), True, i, headerIndex, objName) + self.lightList[i].draw_props(lighting, f"Lighting {i}", True, i, headerIndex, objName, "Light") drawAddButton(lighting, len(self.lightList), "Light", headerIndex, objName) elif menuTab == "Cutscene":