Source code for gs_nyx_plugin.nyx_scene_exporter

"""Nyx Scene Exporter for Genesis."""

# External dependencies
import os
import torch
import math
import numpy as np
from trimesh.visual.color import ColorVisuals

# Internal dependencies
import genesis as gs
from genesis.utils.misc import qd_to_torch
from gs_nyx import nyx_py_sdk as nps

# Relative imports
from .nyx_camera_shared_metadata import _nyx_random_string
from .nyx_scene_utils import (
    get_rel_asset_path,
    instance_type,
    primitive_type,
    primitive_scale,
    file_morph_scale,
    surface_for_material,
    camera_get_quaternion,
    particle_recon_budget,
    is_exportable_entity,
    is_rigid_entity,
    is_pbd_entity,
    is_fem_entity,
    is_dynamic_mesh_entity,
    is_mpm_visual_entity,
    is_mpm_recon_entity,
    is_sph_recon_entity,
    should_export_at_geom_level,
)


# Only the main exporter class is part of the public API
__all__ = ["NyxSceneExporter"]

# Look up table for metal colors (We'll handle complex IORs later)
METAL_PROPERTIES = {
    "aluminium": {"metallic_color": (0.916, 0.923, 0.924)},
    "gold": {"metallic_color": (1.0, 0.773, 0.307)},
    "copper": {"metallic_color": (0.932, 0.623, 0.522)},
    "brass": {"metallic_color": (0.910, 0.778, 0.423)},
    "iron": {"metallic_color": (0.530, 0.513, 0.494)},
    "titanium": {"metallic_color": (0.441, 0.400, 0.361)},
    "vanadium": {"metallic_color": (0.534, 0.526, 0.546)},
    "lithium": {"metallic_color": (0.916, 0.890, 0.807)},
}


# ============================================================================
# SceneAsset Exporter
# ============================================================================
[docs] class NyxSceneExporter: """Translate a Genesis scene into a Nyx ``SceneAsset`` JSON description. Walks the built Genesis scene once to populate a :class:`gs_nyx.nyx_py_sdk.SceneAsset` with one instance per renderable sub-geometry, the cameras, lights, environment maps and light fields that the renderer should use, then serializes the result with :meth:`export_to_file`. The Nyx renderer reads the written JSON during its scene load step. Instances are emitted in solver order (rigid → FEM → PBD → MPM-visual → MPM-recon) so the deformable index space matches the layout :class:`NyxPyRenderer` uses when streaming per-frame vertex data and calling ``set_deform_entity_active_triangles``. Conversion handles ------------------ - Z-up (Genesis) → Y-up (Nyx) for static transforms and scales. - WXYZ → XYZW quaternion handedness. - Rigid entities are split into one Nyx instance per vgeom; dynamic-mesh entities (FEM / PBD / MPM) emit a single ``DynamicMesh`` instance with worst-case vertex / triangle budgets pre-allocated. The :attr:`_entity_uuid_pairs` list it builds is the canonical mapping used elsewhere in the plugin to translate Nyx UUIDs back to Genesis entities (see :meth:`NyxPyRenderer.pick_pixel`). """
[docs] def __init__( self, scene, export_folder, cameras=None, lights=None, asset_root_path=None, env_maps=None, light_fields=None, ): """Build the Nyx ``SceneAsset`` from a Genesis scene. Runs the full export pipeline (instance / camera / light / env-map / light-field build) immediately; call :meth:`export_to_file` after construction to serialize the result. Asset side effects (e.g. MJCF / procedural-mesh ``.obj`` exports) are written under ``export_folder`` during construction. Parameters ---------- scene : genesis.Scene Built Genesis scene to export. All entities with ``morph.visualization=True`` and a renderable type are included. export_folder : str Directory under ``__nyx_cache__/`` where the scene JSON and any per-vgeom ``.obj`` files are written. Must already exist. cameras : list of dict or list of genesis.Camera, optional Cameras to write into the scene description. Pass camera-definition dicts (the format produced by :meth:`NyxCameraSensor.build`) when driving the renderer through Nyx sensors. When ``None``, the scene's non-debug Genesis ``Camera`` objects are exported instead. lights : list of gs_nyx.nyx_py_sdk.LightAsset, optional Lights to include. ``None`` (default) means no lights. asset_root_path : str, optional Root prefix recorded in the scene description for resolving relative asset paths. Defaults to empty string (paths are relative to ``export_folder``). env_maps : list of gs_nyx.nyx_py_sdk.EnvironmentMapAsset, optional Environment maps to include. ``None`` (default) means none. light_fields : list of gs_nyx.nyx_py_sdk.LightFieldAsset, optional Pre-baked light fields to include. ``None`` (default) means none. """ # Keep track of the scene and additional properties self._scene = scene self._export_folder = export_folder self._asset_root_path = asset_root_path if asset_root_path is not None else "" self._genesis_assets_dir = gs.utils.get_assets_dir() # Map that allows us to retrived uuids based on entities. self._entity_uuid_pairs = [] # Create SceneAsset self._scene_asset = nps.SceneAsset() self._scene_asset.version = 1 self._scene_asset.rootFolder = "../../" # Process cameras (only Genesis Camera objects) if cameras is None: self._cameras = [cam for cam in scene.visualizer.cameras if not cam.debug] else: self._cameras = cameras # Process lights - if None, use empty list (caller should provide lights) self._lights = lights if lights is not None else [] # Process env_maps - if None, use empty list (caller should provide env_maps) self._env_maps = env_maps if env_maps is not None else [] # Process light_fields - if None, use empty list (caller should provide light_fields) self._light_fields = light_fields if light_fields is not None else [] # Animation tracking self._frame_counter = 0 # Build scene self._build_scene()
def _build_scene(self): # First we simply count how many instances will endup in our scene (scene level instances) num_instances = 0 for entity in self._scene.entities: # Some entities are simply not supported if not is_exportable_entity(entity): continue # If we support the sub-scene export we simply export one instance per scene for this entity. if should_export_at_geom_level(entity): num_instances += entity.n_vgeoms else: num_instances += 1 # Enumerate the other components. num_cameras = len(self._cameras) num_lights = len(self._lights) num_env_maps = len(self._env_maps) num_light_fields = len(self._light_fields) # Resize the buffers self._scene_asset.instance_resize(num_instances) self._scene_asset.camera_resize(num_cameras) self._scene_asset.light_resize(num_lights) self._scene_asset.env_map_resize(num_env_maps) self._scene_asset.light_field_resize(num_light_fields) # Build all the scene components. self._build_instances() self._build_cameras() self._build_lights() self._build_env_maps() self._build_light_fields() def _build_instances(self): """Build all mesh/primitive instances, sorted by solver type (rigid first, then FEM, PBD, MPM).""" instance_idx = 0 # Collect exportable entities preserving original order exportable_entities = [ e for e in self._scene.entities if is_exportable_entity(e) ] # Order: rigid -> FEM -> PBD -> MPM (visual then recon) -> SPH (recon). This matches the # order the renderer walks deformables when building the reference / indirection table, so # the deform index used by set_deform_entity_active_triangles() lines up. rigid_entities = [e for e in exportable_entities if is_rigid_entity(e)] fem_entities = [e for e in exportable_entities if is_fem_entity(e)] pbd_entities = [e for e in exportable_entities if is_pbd_entity(e)] mpm_visual_entities = [ e for e in exportable_entities if is_mpm_visual_entity(e) ] mpm_recon_entities = [e for e in exportable_entities if is_mpm_recon_entity(e)] sph_recon_entities = [e for e in exportable_entities if is_sph_recon_entity(e)] sorted_entities = ( rigid_entities + fem_entities + pbd_entities + mpm_visual_entities + mpm_recon_entities + sph_recon_entities ) # Loop through the entities and process them. for entity in sorted_entities: if should_export_at_geom_level(entity): instance_idx = self._build_vgeom_instances(entity, instance_idx) else: instance_idx = self._build_entity_instance(entity, instance_idx) # Function that is able to apply textures to a material override. def _apply_texture( self, surface, mat, vmesh, surface_attr, mat_property, mat_attr, metadata_key=None, ): texture = getattr(surface, surface_attr, None) if texture is None: return False # Raise the property, it will be overriden mat.set_properties(mat_property) # Is it a full image or just a value? if isinstance(texture, gs.textures.ImageTexture): # Non genesis resource if texture.image_path: # Use the path as is. setattr(mat, mat_attr, texture.image_path) # Genesis resource, use relative paths elif ( vmesh is not None and hasattr(vmesh, "metadata") and metadata_key and metadata_key in vmesh.metadata ): # Need to apply the genesis path setattr( mat, mat_attr, get_rel_asset_path( vmesh.metadata[metadata_key], self._genesis_assets_dir, "." ), ) elif isinstance(texture, gs.textures.ColorTexture): setattr(mat, mat_attr, "") # Succesfully overriden return True def _build_material_override( self, surface, entity=None, vmesh=None, overriddenSurface=False ): """Build material override from Genesis surface. Args: surface: Genesis surface object entity: Optional entity object (needed for plane UV scale handling) vmesh: Optional vmesh object (for accessing metadata like texture_path) """ mat = nps.MaterialAsset() # If the surface was overriden, unset all the textures. if overriddenSurface: mat.set_properties(nps.EMaterialProperty.AlbedoTexture) mat.albedoTexture = "" mat.set_properties(nps.EMaterialProperty.NormalTexture) mat.normalTexture = "" mat.set_properties(nps.EMaterialProperty.ArmTexture) mat.armTexture = "" mat.set_properties(nps.EMaterialProperty.EmissionTexture) mat.emissionTexture = "" # Handle metals if isinstance(surface, gs.surfaces.Metal) and surface.metal_type is not None: metal_props = METAL_PROPERTIES.get(surface.metal_type) if metal_props is not None: mat.set_properties(nps.EMaterialProperty.AlbedoColor) mat.set_properties(nps.EMaterialProperty.Metalness) color = metal_props["metallic_color"] mat.albedoColor = nps.float4(color[0], color[1], color[2], 1.0) mat.metalness = 1.0 # Colors and alpha if hasattr(surface, "color") and surface.color is not None: mat.set_properties(nps.EMaterialProperty.AlbedoColor) color = surface.color alpha = getattr(surface, "opacity", None) if alpha is None: alpha = 1.0 mat.albedoColor = nps.float4(color[0], color[1], color[2], alpha) elif hasattr(surface, "opacity") and surface.opacity is not None: mat.set_properties(nps.EMaterialProperty.AlbedoColor) mat.albedoColor = nps.float4(1.0, 1.0, 1.0, surface.opacity) # Emission hasEmission = False if hasattr(surface, "emissive") and surface.emissive is not None: mat.set_properties(nps.EMaterialProperty.Emission) mat.emission = nps.float3(*surface.emissive) hasEmission = True else: emissive_tex = getattr(surface, "emissive_texture", None) if isinstance(emissive_tex, gs.textures.ColorTexture): # Genesis routes Emission(color=...) # This converts back the genesis texture to an RGB value. c = np.atleast_1d(np.asarray(emissive_tex.color)).ravel() r = float(c[0]) g = float(c[1]) if c.size > 1 else r b = float(c[2]) if c.size > 2 else r mat.set_properties(nps.EMaterialProperty.Emission) mat.emission = nps.float3(r, g, b) hasEmission = True # Roughness if hasattr(surface, "roughness") and surface.roughness is not None: mat.set_properties(nps.EMaterialProperty.PerceptualRoughness) mat.perceptualRoughness = surface.roughness # Metallic if hasattr(surface, "metallic") and surface.metallic is not None: mat.set_properties(nps.EMaterialProperty.Metalness) mat.metalness = surface.metallic # Albedo texture hasDiffuse = self._apply_texture( surface=surface, mat=mat, vmesh=vmesh, surface_attr="diffuse_texture", mat_property=nps.EMaterialProperty.AlbedoTexture, mat_attr="albedoTexture", metadata_key="texture_path", ) # Normal texture self._apply_texture( surface=surface, mat=mat, vmesh=vmesh, surface_attr="normal_texture", mat_property=nps.EMaterialProperty.NormalTexture, mat_attr="normalTexture", metadata_key="normal_texture_path", ) # Emission texture hasEmission = hasEmission or self._apply_texture( surface=surface, mat=mat, vmesh=vmesh, surface_attr="emissive_texture", mat_property=nps.EMaterialProperty.EmissionTexture, mat_attr="emissionTexture", metadata_key="emissive_texture_path", ) # Double sided support if hasattr(surface, "double_sided"): mat.set_properties(nps.EMaterialProperty.Sidedness) mat.sidedness = nps.EMaterialSidedness.Double # In the specific case of the diffuse texture and a plane, we handle UVs if ( hasDiffuse and entity is not None and isinstance(entity.morph, gs.morphs.Plane) ): plane_size = entity.morph.plane_size mat.set_properties(nps.EMaterialProperty.UVScale) mat.uvScale = nps.float2(float(plane_size[0]), float(plane_size[1])) # For now, if it emissive, we kill the other lobes. if hasEmission: mat.set_properties(nps.EMaterialProperty.AlbedoColor) mat.albedoColor = nps.float4(0.0, 0.0, 0.0, 1.0) mat.set_properties(nps.EMaterialProperty.Metalness) mat.metalness = 1.0 return mat def _build_full_material(self, surface, entity=None): """Build a complete material description for dynamic meshes. Unlike _build_material_override which only exports properties that differ from defaults, this method exports all material properties needed for rendering since dynamic meshes don't have a mesh file with embedded materials to fall back on. """ mat = nps.MaterialAsset() # Always set albedo color (default to white if not specified) mat.set_properties(nps.EMaterialProperty.AlbedoColor) if hasattr(surface, "color") and surface.color is not None: color = surface.color alpha = ( getattr(surface, "opacity", 1.0) if surface.opacity is not None else 1.0 ) mat.albedoColor = nps.float4(color[0], color[1], color[2], alpha) else: alpha = ( getattr(surface, "opacity", 1.0) if hasattr(surface, "opacity") and surface.opacity is not None else 1.0 ) mat.albedoColor = nps.float4(1.0, 1.0, 1.0, alpha) # Only set roughness and metallic if they are specified, otherwise leave them at Nyx default if hasattr(surface, "roughness") and surface.roughness is not None: mat.set_properties(nps.EMaterialProperty.PerceptualRoughness) mat.perceptualRoughness = surface.roughness # Only set metallic if it's specified, otherwise leave it at Nyx default if hasattr(surface, "metallic") and surface.metallic is not None: mat.set_properties(nps.EMaterialProperty.Metalness) mat.metalness = surface.metallic # Textures (if available) if hasattr(surface, "diffuse_texture") and surface.diffuse_texture is not None: texture = surface.diffuse_texture if isinstance(texture, gs.textures.ImageTexture) and texture.image_path: mat.set_properties(nps.EMaterialProperty.AlbedoTexture) mat.albedoTexture = get_rel_asset_path( texture.image_path, self._genesis_assets_dir ) if hasattr(surface, "normal_texture") and surface.normal_texture is not None: texture = surface.normal_texture if isinstance(texture, gs.textures.ImageTexture) and texture.image_path: mat.set_properties(nps.EMaterialProperty.NormalTexture) mat.normalTexture = get_rel_asset_path( texture.image_path, self._genesis_assets_dir ) # Double sided support (default to single sided) if hasattr(surface, "double_sided") and surface.double_sided: mat.set_properties(nps.EMaterialProperty.Sidedness) mat.sidedness = nps.EMaterialSidedness.Double return mat def _build_entity_instance(self, entity, instance_idx): """Build a single entity instance (not expanded to vgeoms).""" instance = self._scene_asset.get_instance(instance_idx) # Type and scale - FEM, PBD, and MPM entities use DynamicMesh type if is_dynamic_mesh_entity(entity): instance.type = nps.EInstanceType.DynamicMesh # Set vertex and triangle counts from entity if is_fem_entity(entity): instance.dynamicMesh_numVertices = entity.n_vertices instance.dynamicMesh_numTriangles = entity.n_surfaces elif is_pbd_entity(entity): instance.dynamicMesh_numVertices = entity.n_vverts instance.dynamicMesh_numTriangles = entity.n_vfaces elif is_mpm_visual_entity(entity): # Skinning mesh: fixed topology, animated verts (via mpm_solver.vverts_render) instance.dynamicMesh_numVertices = entity.n_vverts instance.dynamicMesh_numTriangles = entity.n_vfaces instance.dynamicMesh_dynamicConnectivity = True elif is_mpm_recon_entity(entity): # Variable topology: pre-allocate worst-case from solver domain + recon voxel scale max_v, max_t = particle_recon_budget(self._scene.mpm_solver, entity) instance.dynamicMesh_numVertices = max_v instance.dynamicMesh_numTriangles = max_t instance.dynamicMesh_dynamicConnectivity = True elif is_sph_recon_entity(entity): # SPH liquids share the MPM-recon variable-topology path: budget the same way # using the SPH domain + particle radius. max_v, max_t = particle_recon_budget(self._scene.sph_solver, entity) instance.dynamicMesh_numVertices = max_v instance.dynamicMesh_numTriangles = max_t instance.dynamicMesh_dynamicConnectivity = True # DynamicMesh vertices are in absolute world position, so use identity transform instance.position = nps.float3(0.0, 0.0, 0.0) instance.rotation = nps.quaternion(0.0, 0.0, 0.0, 1.0) scale = nps.float3( 1.0, 1.0, 1.0 ) # Identity scale - vertices are already world-space else: # Transform for non-dynamic meshes pos = nps.float3_z_up_to_y_up_a( nps.float3( entity.morph.pos[0], entity.morph.pos[1], entity.morph.pos[2] ) ) instance.position = pos # Rotation instance.rotation = nps.quaternion( entity.morph.quat[0], entity.morph.quat[1], entity.morph.quat[2], entity.morph.quat[3], ) instance.rotation = nps.quaternion_z_up_to_y_up_a( nps.quaternion_wxyz_to_xyzw(instance.rotation) ) # Type and scale instance.type = instance_type(entity.morph) if isinstance(entity.morph, gs.morphs.Primitive): instance.primitive_type = primitive_type(entity.morph) scale = primitive_scale(entity.morph) elif isinstance(entity.morph, gs.morphs.URDF): subscene_path = os.path.abspath(entity.morph.file).replace("\\", "/") instance.subscene_uri = subscene_path instance.convertAxis = entity.morph.file_meshes_are_zup scale = file_morph_scale(entity.morph) # Sanity check - we don't support URDFs with merged fixed links since we can't export the sub-scene with the correct structure to handle them. if entity.morph.merge_fixed_links: raise RuntimeError( f"The Nyx Renderer does not support exporting an URDF entity with merged fixed links: {entity.morph.file}" ) elif isinstance(entity.morph, gs.morphs.FileMorph): mesh_path = os.path.abspath(entity.morph.file).replace("\\", "/") instance.mesh_uri = mesh_path instance.convertAxis = entity.vgeoms[0].metadata["imported_as_zup"] scale = file_morph_scale(entity.morph) else: scale = nps.float3(1.0, 1.0, 1.0) # Convert the scale to Yup before leaving. instance.scale = nps.float3_z_up_to_y_up_a(scale) # Material surface, overriddenSurface = surface_for_material(entity) if is_dynamic_mesh_entity(entity): # Dynamic meshes need complete material description (no mesh file with embedded materials) instance.matOverride = self._build_full_material(surface, entity) else: # Get vmesh for primitives (to access metadata like texture_path) vmesh = None if ( isinstance(entity.morph, gs.morphs.Primitive) and hasattr(entity, "vgeoms") and entity.vgeoms ): vmesh = entity.vgeoms[0].vmesh instance.matOverride = self._build_material_override( surface, entity, vmesh, overriddenSurface ) # Flags instance.smooth = is_mpm_recon_entity(entity) or is_sph_recon_entity(entity) instance.enabled = True instance.uuid = nps.generate_uuid() # Keep track of the mapping self._entity_uuid_pairs.append((entity, instance.uuid)) self._scene_asset.set_instance(instance_idx, instance) return instance_idx + 1 def _export_vgeom_to_obj(self, vgeom): """Serialize a vgeom's trimesh to an .obj inside the export folder and return its path. Used for vgeoms that have no source mesh file on disk, either because the source is a container Genesis re-meshes (MJCF) or because the mesh is procedurally generated (e.g. gs.morphs.Terrain). """ filename = _nyx_random_string() + ".obj" full_path = os.path.join(self._export_folder, filename) vgeom.vmesh.trimesh.export(full_path) return full_path def _build_vgeom_instances(self, entity, instance_idx): """Build instances from entity vgeoms.""" vgeoms_pos = qd_to_torch(self._scene.rigid_solver.vgeoms_state.pos).cpu() vgeoms_quat = qd_to_torch(self._scene.rigid_solver.vgeoms_state.quat).cpu() # Generate a unique UUID for the whole entity entity_uuid = nps.generate_uuid() # Keep track of the mapping (only done once) self._entity_uuid_pairs.append((entity, entity_uuid)) # Loop through the geometries for vgeom in entity.vgeoms: # Grab the instance to fill instance = self._scene_asset.get_instance(instance_idx) # Handle the position pos = vgeoms_pos[vgeom.idx, 0] instance.position = nps.float3(pos[0], pos[1], pos[2]) instance.position = nps.float3_z_up_to_y_up_a(instance.position) # Handle the rotation vgeom_quat = vgeoms_quat[vgeom.idx, 0] quat = vgeom_quat if torch.all(vgeom_quat == 0): quat[0] = 1.0 instance.rotation = nps.quaternion( quat[0].item(), quat[1].item(), quat[2].item(), quat[3].item() ) instance.rotation = nps.quaternion_z_up_to_y_up_a( nps.quaternion_wxyz_to_xyzw(instance.rotation) ) # Type if hasattr(vgeom, "type"): if vgeom.type == gs.GEOM_TYPE.BOX: instance.type = nps.EInstanceType.Primitive instance.primitive_type = nps.EPrimitiveType.Box extents = vgeom.data instance.scale = nps.float3(extents[0], extents[2], extents[1]) elif vgeom.type == gs.GEOM_TYPE.SPHERE: instance.type = nps.EInstanceType.Primitive instance.primitive_type = nps.EPrimitiveType.Sphere r = vgeom.data[0] instance.scale = nps.float3(r, r, r) elif vgeom.type == gs.GEOM_TYPE.CYLINDER: instance.type = nps.EInstanceType.Primitive instance.primitive_type = nps.EPrimitiveType.Cylinder r, h = vgeom.data[0], vgeom.data[1] instance.scale = nps.float3(r, h, r) else: instance.type = nps.EInstanceType.Mesh # Get scale from entity morph if available if isinstance(vgeom.entity.morph, gs.morphs.FileMorph): instance.scale = file_morph_scale(vgeom.entity.morph) else: instance.scale = nps.float3(1.0, 1.0, 1.0) else: instance.type = nps.EInstanceType.Mesh instance.convertAxis = vgeom.metadata["imported_as_zup"] # Get scale from entity morph if available if isinstance(vgeom.entity.morph, gs.morphs.FileMorph): instance.scale = file_morph_scale(vgeom.entity.morph) else: instance.scale = nps.float3(1.0, 1.0, 1.0) # Convert the scale instance.scale = nps.float3_z_up_to_y_up_a(instance.scale) # URI for mesh types if instance.type == nps.EInstanceType.Mesh: if isinstance(vgeom.entity.morph, gs.morphs.MJCF): uri = self._export_vgeom_to_obj(vgeom) elif "mesh_path" in vgeom.metadata: uri = os.path.relpath(vgeom.metadata["mesh_path"]).replace( "\\", "/" ) else: # Procedural meshes (e.g. gs.morphs.Terrain) have no source file uri = self._export_vgeom_to_obj(vgeom) if uri: instance.mesh_uri = uri # Material (try to get from vgeom visual) visual = vgeom.get_trimesh().visual if isinstance(visual, ColorVisuals): geom_color = visual.main_color / 255.0 mat = nps.MaterialAsset() mat.set_properties(nps.EMaterialProperty.AlbedoColor) mat.albedoColor = nps.float4( geom_color[0], geom_color[1], geom_color[2], geom_color[3] ) instance.matOverride = mat else: # Pass vgeom.vmesh to access metadata (e.g., texture_path for primitives) vmesh = vgeom.vmesh if hasattr(vgeom, "vmesh") else None instance.matOverride = self._build_material_override( entity.surface, entity, vmesh ) # Flags instance.smooth = False instance.enabled = True instance.uuid = entity_uuid # For MJCFs we want to keep track of the link and subgeometry name. if isinstance(vgeom.entity.morph, gs.morphs.MJCF): instance.name = f"{vgeom.link.name}_{vgeom.link.vgeoms.index(vgeom)}" self._scene_asset.set_instance(instance_idx, instance) instance_idx += 1 return instance_idx def _build_cameras(self): """Build all cameras from Genesis Camera objects or dict definitions.""" for camera_idx, camera in enumerate(self._cameras): cam_asset = self._scene_asset.get_camera(camera_idx) if isinstance(camera, dict): self._build_camera_from_dict(cam_asset, camera) else: self._build_camera_from_object(cam_asset, camera) self._scene_asset.set_camera(camera_idx, cam_asset) def _build_camera_from_dict(self, cam_asset, camera_def): """Build camera from dictionary definition.""" # Evaluate the position and convert to y-up pos = camera_def["pos"] cam_asset.position = nps.float3(pos[0], pos[1], pos[2]) cam_asset.position = nps.float3_z_up_to_y_up_a(cam_asset.position) # Rotation quat_z_up_wxyz = camera_get_quaternion(camera_def) quat_wxyz = nps.quaternion( quat_z_up_wxyz[0], quat_z_up_wxyz[1], quat_z_up_wxyz[2], quat_z_up_wxyz[3] ) quat_xyzw = nps.quaternion_wxyz_to_xyzw(quat_wxyz) quat_xyzw = nps.quaternion_z_up_to_y_up_a(quat_xyzw) quat_xyzw = nps.quaternion_conjugate(quat_xyzw) cam_asset.rotation = nps.quaternion_mul( nps.quaternion(0.5, 0.5, 0.5, 0.5), quat_xyzw ) # Properties cam_asset.fovY = math.radians(camera_def["fov"]) cam_asset.resolution = nps.uint2(camera_def["res"][0], camera_def["res"][1]) cam_asset.aperture = camera_def.get("aperture", 2.8) cam_asset.focalLength = camera_def.get("focal_len", 10.0) cam_asset.nearPlane = camera_def.get("near", 0.1) cam_asset.farPlane = camera_def.get("far", 1000.0) cam_asset.spp = camera_def.get("spp", 16) cam_asset.denoise = camera_def.get("denoise", False) cam_asset.toneMapper = camera_def.get("tone_mapper", nps.EToneMapper.Reinhard) cam_asset.antiAliasing = camera_def.get("anti_aliasing", nps.EAntiAliasing.SMAA) def _build_camera_from_object(self, cam_asset, camera): """Build camera from Genesis Camera object.""" # Position pos = camera._initial_pos cam_asset.position = nps.float3(pos[0], pos[1], pos[2]) cam_asset.position = nps.float3_z_up_to_y_up_a(cam_asset.position) # Rotation # TODO: FIX ME LATER cam_asset.rotation = nps.quaternion(0.0, 0.0, 0.0, 1.0) assert False # Properties cam_asset.fovY = math.radians(camera.fov) cam_asset.resolution = nps.uint2(camera.res[0], camera.res[1]) cam_asset.aperture = camera.aperture cam_asset.focalLength = camera.focal_len cam_asset.nearPlane = camera.near cam_asset.farPlane = camera.far cam_asset.spp = camera.spp cam_asset.denoise = camera.denoise cam_asset.toneMapper = getattr(nps.EToneMapper, "None") cam_asset.antiAliasing = getattr(nps.EAntiAliasing, "None") def _build_lights(self): """Build all lights from LightAsset definitions.""" for light_idx, light in enumerate(self._lights): assert hasattr(light, "type") and hasattr(light, "intensity"), ( f"Light at index {light_idx} must be a LightAsset, got {type(light)}" ) self._scene_asset.set_light(light_idx, light) def _build_env_maps(self): for env_map_idx, env_map in enumerate(self._env_maps): assert hasattr(env_map, "texture"), ( f"EnvMap at index {env_map_idx} must be a EnvironmentMapAsset, got {type(env_map)}" ) self._scene_asset.set_env_map(env_map_idx, env_map) def _build_light_fields(self): for light_field_idx, light_field in enumerate(self._light_fields): self._scene_asset.set_light_field(light_field_idx, light_field)
[docs] def export_to_file(self, export_path): """Serialize the built ``SceneAsset`` to JSON on disk. Parameters ---------- export_path : str Output path for the scene-description JSON. The Nyx renderer reads this file in :meth:`NyxRenderer.load_scene_from_file`, so the path must be readable when the renderer builds. Conventionally written as ``<export_folder>/nyx_scene.json``. """ nps.export_scene_file(export_path, self._scene_asset)
[docs] def destroy(self): del self._scene_asset self._scene_asset = None