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