Skip to content

[FEATURE] Surface Override Logic Refactor#2923

Open
ACMLCZH wants to merge 6 commits into
Genesis-Embodied-AI:mainfrom
ACMLCZH:pr/surface-refactor
Open

[FEATURE] Surface Override Logic Refactor#2923
ACMLCZH wants to merge 6 commits into
Genesis-Embodied-AI:mainfrom
ACMLCZH:pr/surface-refactor

Conversation

@ACMLCZH

@ACMLCZH ACMLCZH commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Refactor Surface's texture overwriting structure and logic. We now have three layers of surface textures:

    • User overwriting textures: Shortcuts like Surface.color and Surface.opacity. Full texture names like Surface.diffuse_texture and Surface.roughness_texture.
    • Asset textures: Read from asset files by parse_* functions.
    • Textures that are weaker than the asset: Default textures like Surface.default_roughness and Surface.default_ior.
  • Surface updating logic:

    • surface_override in BaseEntity stores the "raw" user specified overwriting surface. (Nyx's build_entity_instance should read this surface.) It will be never updated or finalized. It is used by model_copy to generate each geom's surface in asset parsing and then update_textures with asset textures. The "finalized" surface will be store as surface in geom/vgeom (for RigidEntity), render_mesh (for FEMEntity) and entity (for ParticleEntity). (Nyx's build_vgeom_instance should 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.
    • Code path:
      1. User customizes surface_override.
      2. surface = surface_override.model_copy().
      3. surface.update_textures(asset_textures) for one or multiple times.
      4. surface.finalize_textures() for only once (in Mesh.__init__).

Surface / Texture API

  • Surface: _finalized PrivateAttr + finalize_texture() + update_texture() (fill-if-None, raises post-finalize) + default_color / default_opacity / default_ior fields.
  • Surface: rename emission property → emissive_tex. New opacity_tex / roughness_tex / metallic_tex accessors. New get_arm() (AO + roughness + metallic combined texture).
  • Surface._make_rgba / _make_arm refactored to share _combine_textures helper, @functools.lru_cache-memoized on (texture, batch) so multi-instance GLBs still produce one shared image_array per material.
  • METAL_COLOR: dict[MetalType, tuple[float, float, float]] table; consumed by Metal._resolve_shortcuts to set default_color from metal_type. Move from Nyx's scene exporter.
  • Texture: new explicit_channels: dict[str, bool] field (per-channel user-vs-default provenance, stamped by _combine_textures) and has_image cached_property (False for ColorTexture, reflects image_array is not None for ImageTexture, any() for BatchTexture). Convenience for Nyx's texture overriding detection.

Entity-side wiring

  • Entity.__init__ stores user input as self._surface_override; exposes surface_override property.
  • Mesh.__init__ finalizes its _surface and rebuilds mesh.visual from the resolved textures.
  • ParticleEntity / FEMEntity / ToolEntity set self._surface = self.surface_override.model_copy() and finalize it.
  • RigidEntity migrates self._surface references → self._surface_override (un-finalized; per-Mesh surfaces own the finalized copies).
  • entity.vis_mode property + setter on base Entity (thin façade over surface_override.vis_mode). Callers in rasterizer_context.py, pyrender/overlay/plugin.py, interactive_scene.py migrated from entity.surface.vis_modeentity.vis_mode, and isinstance(entity.surface, Surface)isinstance(entity, RigidEntity).

Metadata

  • Drops the metadata["is_visual_overwritten"] flag, which originally only incompletely checks ColorTexture's overriding. It can now be checked by explicit_channels
  • Drops the metadata["imported_as_zup"] flag, which can be easily check by morph.file_meshes_are_zup and morph types.

TODO:

  • Wire this change into Nyx.
  • For entity instances: default_* are now ignored by Nyx.
  • A graceful way to remove set_color which is only used by Collision.

Tests

tests/test_mesh.py:

  • test_surface_lifecycle_gatesupdate_texture and a second finalize_texture both raise post-finalize.
  • test_finalize_fills_defaults — default fields populated; also walks METAL_COLOR and verifies each Metal(metal_type=...) picks up the right color.
  • test_surface_lifecycle_across_morphs — for Box / Sphere / Cylinder / Plane / Mesh-obj / Mesh-glb / URDF / MJCF, asserts entity.surface_override._finalized is False and every vgeom.vmesh._surface._finalized / geom.mesh._surface._finalized is True.
  • 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.
  • Multi-OS CI matrix.

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)
@ACMLCZH ACMLCZH force-pushed the pr/surface-refactor branch from eba024f to 102bc2f Compare June 9, 2026 06:46
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

⚠️ Abnormal Benchmark Result Detected ➡️ Report

@ACMLCZH ACMLCZH changed the title [REFACTOR] Surface lifecycle: introduce finalize_texture() gate [REFACTOR] Surface lifecycle: finalize_texture() + entity-side wiring Jun 9, 2026
@ACMLCZH ACMLCZH changed the title [REFACTOR] Surface lifecycle: finalize_texture() + entity-side wiring [New Feature] Surface lifecycle: finalize_texture() + entity-side wiring Jun 9, 2026
@ACMLCZH ACMLCZH changed the title [New Feature] Surface lifecycle: finalize_texture() + entity-side wiring [New Feature] Surface Override Logic Refactor Jun 10, 2026
ACMLCZH and others added 3 commits June 9, 2026 17:54
…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>
@ACMLCZH ACMLCZH changed the title [New Feature] Surface Override Logic Refactor [FEATURE] Surface Override Logic Refactor Jun 10, 2026
@ACMLCZH ACMLCZH marked this pull request as ready for review June 11, 2026 17:58

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread genesis/engine/mesh.py
self.decimate(decimate_face_num, decimate_aggressiveness)

has_overwrite_color = self._surface.texture is not None
self._surface.finalize_texture()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant