Mesh Parenting

Play Demo

MeshInstance3D rendering inside different parent node types.

Demonstrates that MeshInstance3D renders correctly when parented to:

  1. Scene root directly (baseline)

  2. Node3D intermediate

  3. KinematicBody3D

  4. StaticBody3D

  5. StaticBody3D with collision shape

  6. 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()