[FEATURE] Surface Override Logic Refactor#2923
Conversation
Three small additions to `Surface` / `Texture` that downstream PRs (FEM
render meshes, Nyx exporter, mesh-init-surface-lifecycle) depend on.
No behavior change for upstream callers — purely additive + one rename
with a single in-repo caller.
genesis/options/surfaces.py
- Add METAL_COLOR: dict[MetalType, tuple[float, float, float]] with
measured reflectance for each MetalType (iron, aluminium, copper,
gold, brass, titanium, vanadium, lithium). Lets the Metal surface
default to a physically-plausible color per metal type.
- Rename `Surface.emission` property → `Surface.emissive_tex` across
all subclasses. The new name is unambiguous (it's a Texture, not a
color tuple) and matches the upcoming *_tex accessor family.
genesis/options/textures.py
- Add `Texture.explicit_channels: dict[str, bool]` field. Set by
`Surface._combine_textures` (next PR) to record per-channel
user-set vs default-filled provenance, so downstream exporters can
decide whether a child material's flat color should clear a
parent's embedded image texture slot.
- Add `Texture.has_image` cached_property:
ColorTexture → always False
ImageTexture → True iff image_array is not None
BatchTexture → any(child.has_image for child)
Used by future texture-sampling decisions; harmless dormant API
until the next PR exercises it.
genesis/vis/raytracer.py
- Update the only upstream caller of the renamed property
(`surface.emission` → `surface.emissive_tex`, two lines).
The `_finalized` lifecycle, `finalize_texture()`, `_combine_textures`
refactor, METAL_COLOR-to-default_color wiring, and `Collision`
randomization are deferred to `pr/mesh-init-surface-lifecycle` where
they will actually be exercised by entity init.
Test plan
- Linux CPU: tests/test_mesh.py (24 passed)
eba024f to
102bc2f
Compare
|
|
…ties scene.add_mesh_light: from_morph_surface now always returns a list, so unwrap meshes[0] before passing to the visualizer (was raising AttributeError 'list' object has no attribute 'uid' at raytracer.py:208). particle_entity.sample: pass surface_override (un-finalized) to trimesh_to_mesh when combining MeshSet sub-meshes — passing the already- finalized self._surface tripped the lifecycle gate inside Mesh.from_trimesh's update_texture path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1157f896eb
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| self.decimate(decimate_face_num, decimate_aggressiveness) | ||
|
|
||
| has_overwrite_color = self._surface.texture is not None | ||
| self._surface.finalize_texture() |
There was a problem hiding this comment.
Copy surfaces before finalizing from raw attrs
When callers build more than one mesh from raw arrays with the same Surface instance (for example gs.Mesh.from_attrs(..., surface=s) in a loop), the first construction now finalizes that shared object in place, and the next construction raises Cannot finalize_texture on a finalized .... from_trimesh already protects callers by surface.model_copy() before reaching this path, but from_attrs still passes the caller-owned surface directly, so this new finalization gate makes a previously reusable public surface object one-shot for that constructor.
Useful? React with 👍 / 👎.
Summary
Refactor
Surface's texture overwriting structure and logic. We now have three layers of surface textures:Surface.colorandSurface.opacity. Full texture names likeSurface.diffuse_textureandSurface.roughness_texture.parse_*functions.Surface.default_roughnessandSurface.default_ior.Surface updating logic:
surface_overrideinBaseEntitystores the "raw" user specified overwriting surface. (Nyx'sbuild_entity_instanceshould read this surface.) It will be neverupdatedorfinalized. It is used bymodel_copyto generate each geom's surface in asset parsing and thenupdate_textureswith asset textures. The "finalized" surface will be store assurfaceingeom/vgeom(forRigidEntity),render_mesh(forFEMEntity) andentity(forParticleEntity). (Nyx'sbuild_vgeom_instanceshould use it.)update_textures: designed to make asset textures update the surface. Use "update-if-none" strategy.finalize_textures: designed to make default textures update the surface. . Use "update-if-none" strategy.surface_override.surface = surface_override.model_copy().surface.update_textures(asset_textures)for one or multiple times.surface.finalize_textures()for only once (inMesh.__init__).Surface / Texture API
Surface:_finalizedPrivateAttr +finalize_texture()+update_texture()(fill-if-None, raises post-finalize) +default_color/default_opacity/default_iorfields.Surface: renameemissionproperty →emissive_tex. Newopacity_tex/roughness_tex/metallic_texaccessors. Newget_arm()(AO + roughness + metallic combined texture).Surface._make_rgba/_make_armrefactored to share_combine_textureshelper,@functools.lru_cache-memoized on (texture, batch) so multi-instance GLBs still produce one sharedimage_arrayper material.METAL_COLOR: dict[MetalType, tuple[float, float, float]]table; consumed byMetal._resolve_shortcutsto setdefault_colorfrommetal_type. Move from Nyx's scene exporter.Texture: newexplicit_channels: dict[str, bool]field (per-channel user-vs-default provenance, stamped by_combine_textures) andhas_imagecached_property (FalseforColorTexture, reflectsimage_array is not NoneforImageTexture,any()forBatchTexture). Convenience for Nyx's texture overriding detection.Entity-side wiring
Entity.__init__stores user input asself._surface_override; exposessurface_overrideproperty.Mesh.__init__finalizes its_surfaceand rebuildsmesh.visualfrom the resolved textures.ParticleEntity/FEMEntity/ToolEntitysetself._surface = self.surface_override.model_copy()and finalize it.RigidEntitymigratesself._surfacereferences →self._surface_override(un-finalized; per-Mesh surfaces own the finalized copies).entity.vis_modeproperty + setter on base Entity (thin façade oversurface_override.vis_mode). Callers inrasterizer_context.py,pyrender/overlay/plugin.py,interactive_scene.pymigrated fromentity.surface.vis_mode→entity.vis_mode, andisinstance(entity.surface, Surface)→isinstance(entity, RigidEntity).Metadata
metadata["is_visual_overwritten"]flag, which originally only incompletely checksColorTexture's overriding. It can now be checked byexplicit_channelsmetadata["imported_as_zup"]flag, which can be easily check bymorph.file_meshes_are_zupand morph types.TODO:
Nyx.default_*are now ignored by Nyx.set_colorwhich is only used byCollision.Tests
tests/test_mesh.py:test_surface_lifecycle_gates—update_textureand a secondfinalize_textureboth raise post-finalize.test_finalize_fills_defaults— default fields populated; also walksMETAL_COLORand verifies eachMetal(metal_type=...)picks up the right color.test_surface_lifecycle_across_morphs— for Box / Sphere / Cylinder / Plane / Mesh-obj / Mesh-glb / URDF / MJCF, assertsentity.surface_override._finalized is Falseand everyvgeom.vmesh._surface._finalized/geom.mesh._surface._finalizedisTrue.test_glb_shared_texture_not_duplicated— drops the surface-identity assertion (each Mesh now owns its Surface), keeps the image-array sharing check (the actual memory invariant).tests/test_usd.py::test_usd_parse_nodegraph: adds the same lifecycle asserts for the USD entity path.Test plan
tests/test_mesh.py— all 30 tests pass.tests/test_usd.py -k 'not slow'— 13 passed.tests/test_fem.py -k 'not slow'— 9 passed (1 pre-existing xfail).tests/test_rigid_physics.py -k 'not slow'— in progress.