Mesh Parenting

MeshInstance3D rendering under different parent node types.

▶ Run in browser

Tags: 3d

Demonstrates that MeshInstance3D renders correctly and follows its parent transform when parented to:

  1. Scene root directly (baseline)

  2. Node3D intermediate (animated: the group slowly spins)

  3. PhysicsBody3D(KINEMATIC) (animated: the body bobs up and down)

  4. PhysicsBody3D(STATIC)

  5. PhysicsBody3D(STATIC) with a CollisionShape3D

  6. PhysicsBody3D(STATIC) with a cylinder mesh

An on-screen legend maps each slab colour to its parenting pattern.

Run: uv run python examples/features/3d/mesh_parenting.py

Source

  1"""Mesh Parenting: MeshInstance3D rendering under different parent node types.
  2
  3# /// simvx
  4# web = { width = 1280, height = 720 }
  5# ///
  6
  7Demonstrates that MeshInstance3D renders correctly and follows its parent
  8transform when parented to:
  91. Scene root directly (baseline)
 102. Node3D intermediate (animated: the group slowly spins)
 113. PhysicsBody3D(KINEMATIC) (animated: the body bobs up and down)
 124. PhysicsBody3D(STATIC)
 135. PhysicsBody3D(STATIC) with a CollisionShape3D
 146. PhysicsBody3D(STATIC) with a cylinder mesh
 15
 16An on-screen legend maps each slab colour to its parenting pattern.
 17
 18Run: uv run python examples/features/3d/mesh_parenting.py
 19"""
 20
 21import math
 22
 23from simvx.core import (
 24    BodyMode,
 25    BoxShape3D,
 26    Camera3D,
 27    CollisionShape3D,
 28    DirectionalLight3D,
 29    Material,
 30    Mesh,
 31    MeshInstance3D,
 32    Node,
 33    Node3D,
 34    PhysicsBody3D,
 35    PhysicsMaterial,
 36    Quat,
 37    Text2D,
 38    Vec3,
 39    WorldEnvironment,
 40)
 41from simvx.core.physics.root import PhysicsRoot
 42from simvx.graphics import App
 43
 44# Shared meshes (like working examples do)
 45_CUBE = Mesh.cube(size=1.0)
 46_SPHERE = Mesh.sphere(radius=0.5, rings=12, segments=12)
 47_CYLINDER = Mesh.cylinder(radius=1.0, height=0.3, segments=16)
 48
 49# One colour per parenting pattern; the legend reuses these so the mapping is visible.
 50_C1 = (1, 0, 0, 1)  # red: direct child
 51_C2 = (0, 1, 0, 1)  # green: Node3D group
 52_C3 = (0, 0, 1, 1)  # blue: KINEMATIC body
 53_C4 = (1, 1, 0, 1)  # yellow: STATIC body
 54_C5 = (1, 0, 1, 1)  # magenta: STATIC + CollisionShape3D
 55_C6 = (0, 1, 1, 1)  # cyan: cylinder in STATIC body
 56
 57
 58class MeshParentingDemo(Node):
 59    """Shows all parenting patterns side by side, with the movable parents animated."""
 60
 61    def on_ready(self):
 62        super().on_ready()
 63        # One isolated 3D world the body nodes resolve to (Y-up gravity). The
 64        # bodies are STATIC/KINEMATIC and only serve as parent transforms for
 65        # the meshes; the KINEMATIC one is moved directly via ``position``.
 66        self._root = self.add_child(PhysicsRoot(name="World", gravity=Vec3(0, -9.8, 0)))
 67
 68        # Environment: default gradient sky + a touch of ambient so the scene
 69        # reads as a deliberate composition rather than slabs in a black void.
 70        env = self.add_child(WorldEnvironment())
 71        env.ambient_light_energy = 0.5
 72
 73        # Camera
 74        self.add_child(Camera3D(name="Cam", position=Vec3(0, 12, 22), fov=60, look_at=Vec3(0, 0, 0)))
 75
 76        # Lighting
 77        sun = self.add_child(DirectionalLight3D(name="Sun"))
 78        sun.direction = Vec3(-0.3, -1, -0.5)
 79
 80        # Ground plane so the slabs sit in a scene rather than float in space.
 81        self.add_child(MeshInstance3D(
 82            name="Ground",
 83            mesh=_CUBE,
 84            material=Material(colour=(0.10, 0.11, 0.13, 1), metallic=0.1, roughness=0.9),
 85            scale=Vec3(44, 0.1, 16),
 86            position=Vec3(0, -0.8, 0),
 87        ))
 88
 89        x = -12
 90        self._results = []
 91        self._t = 0.0
 92
 93        # --- Test 1: Direct child (baseline, always works) ---
 94        self.add_child(MeshInstance3D(
 95            name="T1_Direct",
 96            mesh=_CUBE,
 97            material=Material(colour=_C1),
 98            scale=Vec3(3, 0.5, 6),
 99            position=Vec3(x, 0, 0),
100        ))
101        self._results.append(("Direct child of scene root", _C1))
102        x += 5
103
104        # --- Test 2: Inside Node3D (the group spins; the mesh follows) ---
105        self._group = self.add_child(Node3D(name="T2_Group", position=Vec3(x, 0, 0)))
106        self._group.add_child(MeshInstance3D(
107            name="T2_InNode3D",
108            mesh=_CUBE,
109            material=Material(colour=_C2),
110            scale=Vec3(3, 0.5, 6),
111        ))
112        self._results.append(("Inside Node3D group (spinning)", _C2))
113        x += 5
114
115        # --- Test 3: Inside PhysicsBody3D(KINEMATIC) (the body bobs; the mesh follows) ---
116        self._kin = self.add_child(PhysicsBody3D(name="T3_Kin", mode=BodyMode.KINEMATIC, position=Vec3(x, 0, 0)))
117        self._kin_x = x
118        self._kin.add_child(MeshInstance3D(
119            name="T3_InKinBody",
120            mesh=_CUBE,
121            material=Material(colour=_C3),
122            scale=Vec3(3, 0.5, 6),
123        ))
124        self._results.append(("Inside PhysicsBody3D KINEMATIC (bobbing)", _C3))
125        x += 5
126
127        # --- Test 4: Inside PhysicsBody3D(STATIC) (no collision shape) ---
128        sb4 = self.add_child(PhysicsBody3D(name="T4_SB", mode=BodyMode.STATIC, position=Vec3(x, 0, 0)))
129        sb4.add_child(MeshInstance3D(
130            name="T4_InSB",
131            mesh=_CUBE,
132            material=Material(colour=_C4),
133            scale=Vec3(3, 0.5, 6),
134        ))
135        self._results.append(("Inside PhysicsBody3D STATIC", _C4))
136        x += 5
137
138        # --- Test 5: Inside PhysicsBody3D(STATIC) WITH a CollisionShape3D (Marble Rally pattern) ---
139        sb5 = PhysicsBody3D(
140            name="T5_SBCol", mode=BodyMode.STATIC, position=Vec3(x, 0, 0),
141            material=PhysicsMaterial(friction=0.8, restitution=0.2),
142        )
143        sb5.add_child(CollisionShape3D(shape=BoxShape3D(half_extents=Vec3(1.5, 0.25, 3.0))))
144        sb5.add_child(MeshInstance3D(
145            name="T5_InSBCol",
146            mesh=_CUBE,
147            material=Material(colour=_C5),
148            scale=Vec3(3, 0.5, 6),
149        ))
150        self.add_child(sb5)
151        self._results.append(("STATIC body + CollisionShape3D", _C5))
152        x += 5
153
154        # --- Test 6: Cylinder inside PhysicsBody3D(STATIC) (round pad test) ---
155        sb6 = self.add_child(PhysicsBody3D(name="T6_SBCyl", mode=BodyMode.STATIC, position=Vec3(x, 0, 0)))
156        sb6.add_child(MeshInstance3D(
157            name="T6_Cylinder",
158            mesh=_CYLINDER,
159            material=Material(colour=_C6),
160        ))
161        self._results.append(("Cylinder in STATIC body", _C6))
162
163        # On-screen legend (top-left): one line per column, left to right,
164        # tinted with the matching slab colour.
165        margin = 16
166        self.add_child(Text2D(
167            text="MESH PARENTING", font_scale=2.2, outline=0.08,
168            position=(margin, margin),
169        ))
170        for i, (label, colour) in enumerate(self._results):
171            self.add_child(Text2D(
172                text=f"{i + 1}. {label}",
173                font_scale=1.5,
174                colour=colour,
175                outline=0.08,
176                position=(margin, margin + 58 + i * 30),
177            ))
178
179    def on_update(self, dt: float):
180        # Animate the movable parents: only the PARENT transforms change, so the
181        # meshes visibly following proves transform propagation for each pattern.
182        self._t += dt
183        self._group.rotation = Quat.from_euler(0, self._t * 0.8, 0)
184        self._kin.position = Vec3(self._kin_x, 0.4 * math.sin(self._t * 1.5), 0)
185
186
187def main():
188    app = App(title="SimVX Mesh Parenting", width=1280, height=720)
189    app.run(MeshParentingDemo(name="MeshParenting"))
190
191
192if __name__ == "__main__":
193    main()