Mesh Parenting¶
MeshInstance3D rendering under different parent node types.
▶ Run in browserTags: 3d
Demonstrates that MeshInstance3D renders correctly and follows its parent transform when parented to:
Scene root directly (baseline)
Node3D intermediate (animated: the group slowly spins)
PhysicsBody3D(KINEMATIC) (animated: the body bobs up and down)
PhysicsBody3D(STATIC)
PhysicsBody3D(STATIC) with a CollisionShape3D
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()