From bffa87ce0b82e8523698e99d89f59ffe24a9d0cf Mon Sep 17 00:00:00 2001 From: Gino Date: Tue, 4 Feb 2025 18:04:45 +0800 Subject: [PATCH] Add camera and stage preview --- .../core/actions/state/load/floor.py | 158 +++++++++++++++++ .../core/actions/state/load/load.py | 3 +- .../core/actions/state/load/objects.py | 71 +++----- editor-blender/operators/__init__.py | 3 + editor-blender/operators/camera/__init__.py | 162 ++++++++++++++++++ editor-blender/panels/__init__.py | 3 + editor-blender/panels/camera/__init__.py | 80 +++++++++ editor-blender/properties/ui/__init__.py | 3 + editor-blender/properties/ui/camera.py | 65 +++++++ editor-blender/properties/ui/types.py | 8 + 10 files changed, 511 insertions(+), 45 deletions(-) create mode 100644 editor-blender/core/actions/state/load/floor.py create mode 100644 editor-blender/operators/camera/__init__.py create mode 100644 editor-blender/panels/camera/__init__.py create mode 100644 editor-blender/properties/ui/camera.py diff --git a/editor-blender/core/actions/state/load/floor.py b/editor-blender/core/actions/state/load/floor.py new file mode 100644 index 000000000..9866d6d63 --- /dev/null +++ b/editor-blender/core/actions/state/load/floor.py @@ -0,0 +1,158 @@ +from typing import cast + +import bpy + +from ....config import config +from ....utils.convert import rgb_to_float + + +def setup_floor() -> None: + if not bpy.context: + return + + data_objects: dict[str, bpy.types.Object] = cast( + dict[str, bpy.types.Object], bpy.data.objects + ) + + # Create floor + stage_scale: float = cast(float, getattr(config, "stage_scale", 1.0)) + stage_width: float = cast(float, getattr(config, "stage_width", 1.0)) * stage_scale + stage_length: float = ( + cast(float, getattr(config, "stage_length", 1.0)) * stage_scale + ) + stage_stroke: float = 0.02 + stage_color: tuple[float, float, float, float] = cast( + tuple[float, float, float, float], (*rgb_to_float((38, 123, 216)), 1.0) + ) + + edge_locations: list[tuple[float, float, float]] = [ + (0, stage_width / 2, 0), + (0, -stage_width / 2, 0), + (stage_length / 2, 0, 0), + (-stage_length / 2, 0, 0), + ] + edge_scales: list[tuple[float, float, float]] = [ + (stage_length + stage_stroke, stage_stroke, stage_stroke), + (stage_length + stage_stroke, stage_stroke, stage_stroke), + (stage_stroke, stage_width + stage_stroke, stage_stroke), + (stage_stroke, stage_width + stage_stroke, stage_stroke), + ] + + for i in range(4): + name = f"FloorEdge{i}" + if name in data_objects: + bpy.data.objects.remove(data_objects[name]) + + bpy.ops.mesh.primitive_cube_add(size=1) + edge_obj: bpy.types.Object | None = bpy.context.object + if edge_obj is None: + return + + edge_obj.name = name + edge_obj.location = edge_locations[i] + edge_obj.scale = edge_scales[i] + edge_obj.color = stage_color + edge_obj.hide_select = True + + for obj in cast(list[bpy.types.Object], bpy.context.view_layer.objects.selected): + obj.select_set(False) + + # Floor Material setup + material_wooden = bpy.data.materials.new(name="Wooden") + material_wooden.use_nodes = True + + node_tree = material_wooden.node_tree + if node_tree is None: + return + + material_output = node_tree.nodes.get("Material Output") + pri_bsdf = node_tree.nodes.get("Principled BSDF") + + if not material_output or not pri_bsdf: + return + + material_output.location = (400, 500) + pri_bsdf.location = (100, 500) + + # Create nodes + brick_texture = node_tree.nodes.new("ShaderNodeTexBrick") + map1 = node_tree.nodes.new("ShaderNodeMapping") + coord1 = node_tree.nodes.new("ShaderNodeTexCoord") + bump = node_tree.nodes.new("ShaderNodeBump") + noise = node_tree.nodes.new("ShaderNodeTexNoise") + map2 = node_tree.nodes.new("ShaderNodeMapping") + coord2 = node_tree.nodes.new("ShaderNodeTexCoord") + mix = node_tree.nodes.new("ShaderNodeMixRGB") + ramp = node_tree.nodes.new("ShaderNodeValToRGB") + + # Set node locations (For manual adjustment) + brick_texture.location = (-600, 300) + map1.location = (-800, 300) + coord1.location = (-1000, 300) + bump.location = (-400, 200) + noise.location = (-600, 750) + map2.location = (-800, 750) + coord2.location = (-1000, 750) + mix.location = (-400, 500) + ramp.location = (-200, 500) + + # Connect nodes + node_tree.links.new(coord1.outputs[3], map1.inputs[0]) + node_tree.links.new(coord2.outputs[3], map2.inputs[0]) + node_tree.links.new(map1.outputs[0], brick_texture.inputs[0]) + node_tree.links.new(map2.outputs[0], noise.inputs[0]) + node_tree.links.new(brick_texture.outputs[0], mix.inputs[2]) + node_tree.links.new(noise.outputs[0], mix.inputs[1]) + node_tree.links.new(mix.outputs[0], ramp.inputs[0]) + node_tree.links.new(ramp.outputs[0], pri_bsdf.inputs[0]) + node_tree.links.new(bump.outputs[0], pri_bsdf.inputs[5]) + + # Modify node values + setattr(brick_texture.inputs[1], "default_value", (0.226, 0.226, 0.226, 1)) + setattr(brick_texture.inputs[2], "default_value", (0.413, 0.413, 0.413, 1)) + setattr(brick_texture.inputs[4], "default_value", 7.00) + setattr(brick_texture.inputs[5], "default_value", 0.005) + setattr(brick_texture.inputs[6], "default_value", 1.00) + setattr(brick_texture.inputs[7], "default_value", 0.02) + setattr(brick_texture.inputs[8], "default_value", 4.00) + setattr(brick_texture.inputs[9], "default_value", 0.60) + + setattr(noise.inputs[2], "default_value", 6.00) + setattr(noise.inputs[3], "default_value", 16.00) + setattr(noise.inputs[4], "default_value", 0.80) + setattr(noise.inputs[8], "default_value", 4.00) + + vec = list(getattr(map2.inputs[3], "default_value")) + vec[1] = 10.00 + setattr(map2.inputs[3], "default_value", vec) + + setattr(mix.inputs[0], "default_value", 1.00) + setattr(mix, "blend_type", "MULTIPLY") + + ramp.color_ramp.elements.new(0.200) # type: ignore + ramp.color_ramp.elements.new(0.500) # type: ignore + setattr(ramp.color_ramp.elements[3], "position", 0.450) # type: ignore + setattr(ramp.color_ramp.elements[2], "position", 0.250) # type: ignore + setattr(ramp.color_ramp.elements[1], "position", 0.100) # type: ignore + setattr(ramp.color_ramp.elements[3], "color", (1.00, 0.89, 0.81, 1.00)) # type: ignore + setattr(ramp.color_ramp.elements[2], "color", (0.90, 0.65, 0.47, 1.00)) # type: ignore + setattr(ramp.color_ramp.elements[1], "color", (0.42, 0.27, 0.18, 1.00)) # type: ignore + + setattr(pri_bsdf.inputs[2], "default_value", 0.240) + + flr = "wooden_floor" + + # Remove existing floor + if flr in bpy.data.objects: + obj = bpy.data.objects.get(flr) + if obj: + bpy.data.objects.remove(obj, do_unlink=True) + + # Add new floor + bpy.ops.mesh.primitive_cube_add(scale=(stage_width / 2, stage_length / 2, 0.01)) + obj = bpy.context.object + if obj: + obj.rotation_euler.z = 1.5708 + obj.name = flr + obj.color = (0.00315199, 0.00315199, 0.00315199, 1) + obj.active_material = material_wooden diff --git a/editor-blender/core/actions/state/load/load.py b/editor-blender/core/actions/state/load/load.py index ca4b40ef7..1f7fc1121 100644 --- a/editor-blender/core/actions/state/load/load.py +++ b/editor-blender/core/actions/state/load/load.py @@ -9,8 +9,9 @@ from ....utils.ui import update_user_log from .animation import setup_animation_data from .display import setup_display +from .floor import setup_floor from .music import setup_music -from .objects import setup_floor, setup_objects +from .objects import setup_objects from .render import setup_render diff --git a/editor-blender/core/actions/state/load/objects.py b/editor-blender/core/actions/state/load/objects.py index fb9c06511..8ccceb13e 100644 --- a/editor-blender/core/actions/state/load/objects.py +++ b/editor-blender/core/actions/state/load/objects.py @@ -9,7 +9,6 @@ from ....log import logger from ....models import DancersArrayPartsItem, ModelName, PartType from ....states import state -from ....utils.convert import rgb_to_float from ....utils.object import set_bpy_props @@ -110,6 +109,33 @@ async def import_model_to_asset( # avoid part name conflict obj.name = f"{model_name}.{obj.name}" + # Add material to each object + material = bpy.data.materials.new(name=f"{obj.name}_Material") + material.use_nodes = True + + # Ensure the object has valid data + if obj.data is not None and hasattr(obj.data, "materials"): + # Create new nodes and some setup + bsdf_node = material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled") # type: ignore + object_node = material.node_tree.nodes.new(type="ShaderNodeObjectInfo") # type: ignore + + material.node_tree.links.new(object_node.outputs[1], bsdf_node.inputs[0]) # type: ignore + material.node_tree.links.new(object_node.outputs[1], bsdf_node.inputs[26]) # type: ignore + setattr(bsdf_node.inputs[27], "default_value", 5.0) + + # Material Output node if it doesn't exist + material_output_node = material.node_tree.nodes.get("Material Output") # type: ignore + if not material_output_node: + material_output_node = material.node_tree.nodes.new(type="ShaderNodeOutputMaterial") # type: ignore + + material.node_tree.links.new(bsdf_node.outputs[0], material_output_node.inputs["Surface"]) # type: ignore + + # Assign material to the object + if obj.data.materials: # type: ignore + obj.data.materials[0] = material # type: ignore + else: + obj.data.materials.append(material) # type: ignore + # Clean meshes sphere_mesh = find_first_mesh("Sphere") if sphere_mesh is not None: @@ -385,46 +411,3 @@ async def setup_objects(): ] setup_dancer_part_objects_map() - - -def setup_floor(): - if not bpy.context: - return - data_objects = cast(dict[str, bpy.types.Object], bpy.data.objects) - - # Create floor - stage_scale: float = getattr(config, "stage_scale") - stage_width: float = getattr(config, "stage_width") * stage_scale - stage_length: float = getattr(config, "stage_length") * stage_scale - stage_stroke = 0.02 - stage_color = (*rgb_to_float((38, 123, 216)), 1) - - edge_locations = [ - (0, stage_width / 2, 0), - (0, -stage_width / 2, 0), - (stage_length / 2, 0, 0), - (-stage_length / 2, 0, 0), - ] - edge_scales = [ - (stage_length + stage_stroke, stage_stroke, stage_stroke), - (stage_length + stage_stroke, stage_stroke, stage_stroke), - (stage_stroke, stage_width + stage_stroke, stage_stroke), - (stage_stroke, stage_width + stage_stroke, stage_stroke), - ] - - for i in range(4): - name = f"FloorEdge{i}" - if data_objects.get(name) is not None: - bpy.data.objects.remove(data_objects[name]) - - bpy.ops.mesh.primitive_cube_add(size=1) - if not (edge_obj := bpy.context.object): - return - edge_obj.name = f"FloorEdge{i}" - edge_obj.location = edge_locations[i] - edge_obj.scale = edge_scales[i] - edge_obj.color = cast(bpy.types.bpy_prop_array, stage_color) - edge_obj.hide_select = True - - for obj in cast(list[bpy.types.Object], bpy.context.view_layer.objects.selected): - obj.select_set(False) diff --git a/editor-blender/operators/__init__.py b/editor-blender/operators/__init__.py index 861e00f16..bf57e226c 100644 --- a/editor-blender/operators/__init__.py +++ b/editor-blender/operators/__init__.py @@ -3,6 +3,7 @@ assets, async_core, auth, + camera, clipboard, color_palette, command_center, @@ -31,6 +32,7 @@ def register(): animation.register() pos_editor.register() color_palette.register() + camera.register() utils.register() notification.register() timeline.register() @@ -54,6 +56,7 @@ def unregister(): animation.unregister() pos_editor.unregister() color_palette.unregister() + camera.unregister() utils.unregister() notification.unregister() timeline.unregister() diff --git a/editor-blender/operators/camera/__init__.py b/editor-blender/operators/camera/__init__.py new file mode 100644 index 000000000..e92f2215e --- /dev/null +++ b/editor-blender/operators/camera/__init__.py @@ -0,0 +1,162 @@ +import bpy +import mathutils + +from ...properties.ui.types import CameraStatusType + + +def toggle_camera_view(context): + if not bpy.context: + return + scene = context.scene + area = next(area for area in context.screen.areas if area.type == "VIEW_3D") + + # Control On/Off based on whether the user is in Camera View + if area.spaces[0].region_3d.view_perspective != "CAMERA": + area.spaces[0].region_3d.view_perspective = "CAMERA" + + # Ensure 'lightdance_camera' Camera + camera = bpy.data.objects.get("lightdance_camera") + if not camera: + bpy.ops.object.camera_add() + camera = bpy.context.object + camera.name = "lightdance_camera" # type: ignore + + scene.camera = camera + + # Default location and rotaion. + if not camera: + return + camera.location = mathutils.Vector((10, 0, 2.7)) + target = mathutils.Vector((0, 0, 1.5)) + direction = camera.location - target + camera.rotation_euler = direction.to_track_quat("Z", "Y").to_euler() + + # Remember the previous location and rotaion. + scene["pre_camera_location"] = area.spaces[0].region_3d.view_location.copy() + scene["pre_camera_rotation"] = area.spaces[0].region_3d.view_rotation.copy() + + # Switch rendering modes and set rendering-related settings + if not bpy.context: + return + bpy.context.space_data.shading.type = "RENDERED" # type: ignore + bpy.context.space_data.overlay.show_overlays = False # type: ignore + bpy.context.scene.render.engine = "BLENDER_EEVEE_NEXT" + bpy.context.scene.eevee.use_raytracing = True + bpy.context.scene.eevee.ray_tracing_options.resolution_scale = "1" + bpy.context.scene.eevee.ray_tracing_options.trace_max_roughness = 0.95 + bpy.context.space_data.lock_camera = True # type: ignore + else: + # Return to the original location/rotation. + if "pre_camera_location" in scene and "pre_camera_rotation" in scene: + area.spaces[0].region_3d.view_location = scene["pre_camera_location"] + area.spaces[0].region_3d.view_rotation = scene["pre_camera_rotation"] + + area.spaces[0].region_3d.view_perspective = "PERSP" + bpy.context.space_data.shading.type = "SOLID" # type: ignore + bpy.context.space_data.overlay.show_overlays = True # type: ignore + + +class SetCameraY(bpy.types.Operator): + bl_idname = "view3d.set_camera_y" + bl_label = "Set Camera Y" + target_y: bpy.props.FloatProperty() # type: ignore + + def execute(self, context): + camera = bpy.data.objects.get("lightdance_camera") + if not bpy.context: + return + ld_camera_status: CameraStatusType = getattr( + bpy.context.window_manager, "ld_ui_camera" + ) + + if camera: + camera.location.y = self.target_y + ld_camera_status.camera_y = self.target_y # Update the value on slider + + return {"FINISHED"} + + +class SetCameraX(bpy.types.Operator): + bl_idname = "view3d.set_camera_x" + bl_label = "Set Camera X" + target_x: bpy.props.FloatProperty() # type: ignore + + def execute(self, context): + camera = bpy.data.objects.get("lightdance_camera") + if not bpy.context: + return + ld_camera_status: CameraStatusType = getattr( + bpy.context.window_manager, "ld_ui_camera" + ) + + if camera: + camera.location.x = self.target_x + ld_camera_status.camera_x = self.target_x # Updtate the value on slider + + return {"FINISHED"} + + +class SetCameraFocal(bpy.types.Operator): + bl_idname = "view3d.set_camera_focal" + bl_label = "Set Camera Focal Length" + target_focal_length: bpy.props.FloatProperty() # type: ignore + + def execute(self, context): + if not bpy.context: + return + camera = bpy.data.objects.get("lightdance_camera") + + ld_camera_status: CameraStatusType = getattr( + bpy.context.window_manager, "ld_ui_camera" + ) + + if camera: + camera.data.lens = self.target_focal_length # type: ignore + ld_camera_status.camera_focal_length = ( + self.target_focal_length + ) # Update the value on slider + + return {"FINISHED"} + + +class ToggleCameraOperator(bpy.types.Operator): + bl_idname = "view3d.toggle_camera_operator" + bl_label = "Toggle Camera View" + + def execute(self, context): + if not bpy.context: + return + ld_camera_status: CameraStatusType = getattr( + bpy.context.window_manager, "ld_ui_camera" + ) + setattr( + ld_camera_status, "camera_on", not getattr(ld_camera_status, "camera_on") + ) + toggle_camera_view(context) + + return {"FINISHED"} + + +class AutoFitFullscreenOperator(bpy.types.Operator): + bl_idname = "view3d.auto_fit_fullscreen" + bl_label = "Auto Fit to Fullscreen" + + def execute(self, context): + bpy.ops.view3d.view_center_camera() + return {"FINISHED"} + + +def register(): + bpy.utils.register_class(ToggleCameraOperator) + bpy.utils.register_class(SetCameraX) + bpy.utils.register_class(SetCameraY) + bpy.utils.register_class(SetCameraFocal) + bpy.utils.register_class(AutoFitFullscreenOperator) + + +def unregister(): + bpy.utils.unregister_class(ToggleCameraOperator) + bpy.utils.unregister_class(SetCameraX) + bpy.utils.unregister_class(SetCameraY) + bpy.utils.unregister_class(SetCameraFocal) + bpy.utils.unregister_class(AutoFitFullscreenOperator) diff --git a/editor-blender/panels/__init__.py b/editor-blender/panels/__init__.py index ca9f1691c..2c10bbd78 100644 --- a/editor-blender/panels/__init__.py +++ b/editor-blender/panels/__init__.py @@ -1,5 +1,6 @@ from . import ( auth, + camera, color_palette, command_center, control_editor, @@ -21,6 +22,7 @@ def register(): control_editor.register() led_editor.register() color_palette.register() + camera.register() timeline.register() command_center.register() @@ -33,6 +35,7 @@ def unregister(): editor.unregister() pos_editor.unregister() color_palette.unregister() + camera.unregister() timeline.unregister() command_center.unregister() control_editor.unregister() diff --git a/editor-blender/panels/camera/__init__.py b/editor-blender/panels/camera/__init__.py new file mode 100644 index 000000000..cad28b077 --- /dev/null +++ b/editor-blender/panels/camera/__init__.py @@ -0,0 +1,80 @@ +import bpy + +from ...properties.ui.types import CameraStatusType + + +class CameraPanel(bpy.types.Panel): + bl_label = "Camera View Panel" + bl_idname = "VIEW_PT_CameraView" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "CameraView" + + def draw(self, context): + if not bpy.context: + return + layout = self.layout + ld_camera_status: CameraStatusType = getattr( + bpy.context.window_manager, "ld_ui_camera" + ) + + row = layout.row() + row.operator( + "view3d.toggle_camera_operator", + text="Turn On" + if not getattr(ld_camera_status, "camera_on", False) + else "Turn Off", + ) + + # Show other buttons/slider when it's on + if getattr(ld_camera_status, "camera_on", False): + layout.label(text="Adjust Camera Position:") + + layout.prop(ld_camera_status, "camera_y", slider=True) + row = layout.row() + + op = row.operator("view3d.set_camera_y", text="Left") + setattr(op, "target_y", -10) + + op = row.operator("view3d.set_camera_y", text="Middle") + setattr(op, "target_y", 0) + + op = row.operator("view3d.set_camera_y", text="Right") + setattr(op, "target_y", 10) + + layout.prop(ld_camera_status, "camera_x", slider=True) + row = layout.row() + + op = row.operator("view3d.set_camera_x", text="Front") + setattr(op, "target_x", 4.5) + + op = row.operator("view3d.set_camera_x", text="Middle") + setattr(op, "target_x", 10) + + op = row.operator("view3d.set_camera_x", text="Back") + setattr(op, "target_x", 17.5) + + layout.prop(ld_camera_status, "camera_focal_length", slider=True) + row = layout.row() + + op = row.operator("view3d.set_camera_focal", text="35mm") + setattr(op, "target_focal_length", 35) + + op = row.operator("view3d.set_camera_focal", text="50mm") + setattr(op, "target_focal_length", 50) + + op = row.operator("view3d.set_camera_focal", text="80mm") + setattr(op, "target_focal_length", 80) + + row = layout.row() + row.operator("view3d.auto_fit_fullscreen", text="Auto Fit to Fullscreen") + + layout.prop(ld_camera_status, "world_light_intensity", slider=True) + + +def register(): + bpy.utils.register_class(CameraPanel) + + +def unregister(): + bpy.utils.unregister_class(CameraPanel) diff --git a/editor-blender/properties/ui/__init__.py b/editor-blender/properties/ui/__init__.py index 769dfaee8..6c4eed695 100644 --- a/editor-blender/properties/ui/__init__.py +++ b/editor-blender/properties/ui/__init__.py @@ -1,4 +1,5 @@ from . import ( + camera, color_palette, command_center, control_editor, @@ -10,6 +11,7 @@ def register(): + camera.register() login.register() pos_editor.register() color_palette.register() @@ -20,6 +22,7 @@ def register(): def unregister(): + camera.unregister() login.unregister() pos_editor.unregister() color_palette.unregister() diff --git a/editor-blender/properties/ui/camera.py b/editor-blender/properties/ui/camera.py new file mode 100644 index 000000000..c199cb06c --- /dev/null +++ b/editor-blender/properties/ui/camera.py @@ -0,0 +1,65 @@ +import bpy +import mathutils + + +def update_camera(self, context): + camera = bpy.data.objects.get("lightdance_camera") + if camera: + # update camera position + camera.location.y = getattr(self, "camera_y") + camera.location.x = max(4.5, min(getattr(self, "camera_x"), 17.5)) + + # adjust z postion according to x + if 4.5 <= camera.location.x <= 5.5: + camera.location.z = ( + 0.3 + (camera.location.x - 4.5) * 1.5 + ) # the first row is relatively low. + else: + camera.location.z = 1.8 + (camera.location.x - 5.5) * 0.2 + + camera.data.lens = getattr(self, "camera_focal_length") # type: ignore + + # Let camera face the center of the stage + target = mathutils.Vector((0, 0, 1.5)) + direction = camera.location - target + camera.rotation_euler = direction.to_track_quat("Z", "Y").to_euler() + if not bpy.context: + return + bpy.context.view_layer.update() + + +def update_world_light(self, context): + if not bpy.context: + return + world = bpy.context.scene.world + if world and world.node_tree: + node = world.node_tree.nodes.get("Background") + if node: + setattr( + node.inputs[1], "default_value", getattr(self, "world_light_intensity") + ) + + +class CameraStatus(bpy.types.PropertyGroup): + edit_index: bpy.props.IntProperty() # type: ignore + camera_on: bpy.props.BoolProperty(default=False) # type: ignore + camera_y: bpy.props.FloatProperty(name="Camera Y", min=-10, max=10, default=0, update=update_camera) # type: ignore + camera_x: bpy.props.FloatProperty(name="Camera X", min=4.5, max=17.5, default=4.5, update=update_camera) # type: ignore + camera_focal_length: bpy.props.FloatProperty(name="Focal Length", min=35, max=80, default=50, update=update_camera) # type: ignore + world_light_intensity: bpy.props.FloatProperty( # type: ignore + name="World Light", min=0, max=1, default=1, update=update_world_light + ) # type: ignore + + +def register(): + bpy.utils.register_class(CameraStatus) + setattr( + bpy.types.WindowManager, + "ld_ui_camera", + bpy.props.PointerProperty(type=CameraStatus), + ) + + +def unregister(): + bpy.utils.unregister_class(CameraStatus) + delattr(bpy.types.WindowManager, "ld_ui_camera") diff --git a/editor-blender/properties/ui/types.py b/editor-blender/properties/ui/types.py index 0b61b1f7c..332d3a2e8 100644 --- a/editor-blender/properties/ui/types.py +++ b/editor-blender/properties/ui/types.py @@ -78,3 +78,11 @@ class TimeShiftStatusType: start: int end: int displacement: int + + +class CameraStatusType: + camera_on: bool + camera_y: float + camera_x: float + camera_focal_length: float + world_light_intensity: float