Mesh Parenting¶
MeshInstance3D rendering under different parent node types.
▶ Run in browserTags: 3d
Demonstrates that MeshInstance3D renders correctly when parented to:
Scene root directly (baseline)
Node3D intermediate
KinematicBody3D
StaticBody3D
StaticBody3D with collision shape
Many objects (transform buffer scaling)
Run: uv run python examples/features/3d/mesh_parenting.py uv run python examples/features/3d/mesh_parenting.py –test
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 when parented to:
81. Scene root directly (baseline)
92. Node3D intermediate
103. KinematicBody3D
114. StaticBody3D
125. StaticBody3D with collision shape
136. Many objects (transform buffer scaling)
14
15Run: uv run python examples/features/3d/mesh_parenting.py
16 uv run python examples/features/3d/mesh_parenting.py --test
17"""
18
19import sys
20
21from simvx.core import (
22 Camera3D,
23 DirectionalLight3D,
24 Material,
25 Mesh,
26 MeshInstance3D,
27 Node,
28 Node3D,
29 Vec3,
30)
31from simvx.core.collision import BoxShape
32from simvx.core.physics import KinematicBody3D, PhysicsMaterial, PhysicsServer, StaticBody3D
33from simvx.graphics import App
34
35# Shared meshes (like working examples do)
36_CUBE = Mesh.cube(size=1.0)
37_SPHERE = Mesh.sphere(radius=0.5, rings=12, segments=12)
38_CYLINDER = Mesh.cylinder(radius=1.0, height=0.3, segments=16)
39
40
41class StaticBodyRenderTest(Node):
42 """Tests all parenting patterns side by side."""
43
44 def on_ready(self):
45 super().on_ready()
46 PhysicsServer.reset()
47 srv = PhysicsServer.get()
48 srv.gravity = Vec3(0, -9.8, 0)
49
50 # Camera
51 self.add_child(Camera3D(name="Cam", position=Vec3(0, 12, 22), fov=60, look_at=Vec3(0, 0, 0)))
52
53 # Lighting
54 sun = self.add_child(DirectionalLight3D(name="Sun"))
55 sun.direction = Vec3(-0.3, -1, -0.5)
56
57 x = -12
58 self._results = []
59
60 # --- Test 1: Direct child (baseline, always works) ---
61 self.add_child(MeshInstance3D(
62 name="T1_Direct",
63 mesh=_CUBE,
64 material=Material(colour=(1, 0, 0, 1)),
65 scale=Vec3(3, 0.5, 6),
66 position=Vec3(x, 0, 0),
67 ))
68 self._results.append(("T1: Direct child", x))
69 x += 5
70
71 # --- Test 2: Inside Node3D ---
72 group = self.add_child(Node3D(name="T2_Group", position=Vec3(x, 0, 0)))
73 group.add_child(MeshInstance3D(
74 name="T2_InNode3D",
75 mesh=_CUBE,
76 material=Material(colour=(0, 1, 0, 1)),
77 scale=Vec3(3, 0.5, 6),
78 ))
79 self._results.append(("T2: Inside Node3D", x))
80 x += 5
81
82 # --- Test 3: Inside KinematicBody3D ---
83 body3 = self.add_child(KinematicBody3D(name="T3_Kin", position=Vec3(x, 0, 0)))
84 body3.add_child(MeshInstance3D(
85 name="T3_InKinBody",
86 mesh=_CUBE,
87 material=Material(colour=(0, 0, 1, 1)),
88 scale=Vec3(3, 0.5, 6),
89 ))
90 self._results.append(("T3: Inside KinematicBody3D", x))
91 x += 5
92
93 # --- Test 4: Inside StaticBody3D (no collision shape) ---
94 sb4 = self.add_child(StaticBody3D(name="T4_SB", position=Vec3(x, 0, 0)))
95 sb4.add_child(MeshInstance3D(
96 name="T4_InSB",
97 mesh=_CUBE,
98 material=Material(colour=(1, 1, 0, 1)),
99 scale=Vec3(3, 0.5, 6),
100 ))
101 self._results.append(("T4: Inside StaticBody3D", x))
102 x += 5
103
104 # --- Test 5: Inside StaticBody3D WITH collision shape (Marble Rally pattern) ---
105 sb5 = StaticBody3D(name="T5_SBCol", position=Vec3(x, 0, 0))
106 sb5.collision_shape = BoxShape(half_extents=(1.5, 0.25, 3.0))
107 sb5.physics_material = PhysicsMaterial(friction=0.8, restitution=0.2)
108 sb5.add_child(MeshInstance3D(
109 name="T5_InSBCol",
110 mesh=_CUBE,
111 material=Material(colour=(1, 0, 1, 1)),
112 scale=Vec3(3, 0.5, 6),
113 ))
114 self.add_child(sb5)
115 self._results.append(("T5: Inside SB3D+collision", x))
116 x += 5
117
118 # --- Test 6: Cylinder inside StaticBody3D (round pad test) ---
119 sb6 = self.add_child(StaticBody3D(name="T6_SBCyl", position=Vec3(x, 0, 0)))
120 sb6.add_child(MeshInstance3D(
121 name="T6_Cylinder",
122 mesh=_CYLINDER,
123 material=Material(colour=(0, 1, 1, 1)),
124 ))
125 self._results.append(("T6: Cylinder in SB3D", x))
126
127 # Print expected layout
128 for label, xpos in self._results:
129 print(f" x={xpos:+3d} {label}")
130
131
132def main():
133 test_mode = "--test" in sys.argv
134
135 if test_mode:
136 print("=== Static Body Rendering Test (headless) ===\n")
137 app = App(title="SB3D Test", width=1280, height=720, visible=False, mode="3d")
138 scene = StaticBodyRenderTest(name="Test")
139 frames = app.run_headless(scene, frames=10, capture_frames=[9])
140
141 if not frames:
142 print("ERROR: No frames captured!")
143 # sys.exit OK here: headless path, app.run_headless() already returned (no live event loop / audio threads).
144 sys.exit(1)
145
146 import numpy as np
147 from PIL import Image
148
149 img = np.array(frames[0])
150 Image.fromarray(img).save("/tmp/static_body_test.png")
151 print("Screenshot: /tmp/static_body_test.png\n")
152
153 colours = {
154 "T1: Direct child": ("red", lambda i: (i[:,:,0] > 100) & (i[:,:,1] < 80) & (i[:,:,2] < 80)),
155 "T2: Inside Node3D": ("green", lambda i: (i[:,:,1] > 100) & (i[:,:,0] < 80) & (i[:,:,2] < 80)),
156 "T3: Inside KinematicBody3D": ("blue", lambda i: (i[:,:,2] > 100) & (i[:,:,0] < 80) & (i[:,:,1] < 80)),
157 "T4: Inside StaticBody3D": ("yellow", lambda i: (i[:,:,0] > 100) & (i[:,:,1] > 100) & (i[:,:,2] < 80)),
158 "T5: Inside SB3D+collision": ("magenta", lambda i: (i[:,:,0] > 100) & (i[:,:,2] > 100) & (i[:,:,1] < 80)),
159 "T6: Cylinder in SB3D": ("cyan", lambda i: (i[:,:,1] > 100) & (i[:,:,2] > 100) & (i[:,:,0] < 80)),
160 }
161
162 all_pass = True
163 for label, (colour_name, detect_fn) in colours.items():
164 count = int(np.sum(detect_fn(img)))
165 status = "PASS" if count > 50 else "FAIL"
166 flag = " " if count > 50 else "!!"
167 if count <= 50:
168 all_pass = False
169 print(f" {flag} {status}: {label} ({colour_name}, {count} px)")
170
171 print(f"\n{'ALL TESTS PASSED' if all_pass else 'SOME TESTS FAILED'}")
172 # sys.exit OK here: headless path, app.run_headless() already returned (no live event loop / audio threads).
173 sys.exit(0 if all_pass else 1)
174 else:
175 print("Static Body Rendering Demo: visual inspection")
176 print("You should see 6 coloured slabs/cylinders in a row.\n")
177 app = App(title="StaticBody3D Render Test", width=1280, height=720, mode="3d")
178 app.run(StaticBodyRenderTest(name="Test"))
179
180
181if __name__ == "__main__":
182 main()