Transform Hierarchy

a MeshInstance3D follows any parent transform.

▶ Run in browser

Tags: 3d

A MeshInstance3D draws at its own transform composed with every ancestor’s, whatever those ancestors are. There is no special “visual” slot on a group or a physics body: to make a mesh follow something, make it that thing’s child.

Four identical slabs sit side by side, each attached a different way:

  1. Red, a direct child of the scene root. Its own position is its world position.

  2. Green, inside a Node3D group. The slab’s own transform is zero, so it sits exactly where the group is. The group turns with rotate_y() every frame and the slab turns with it, in place.

  3. Blue, inside a KINEMATIC PhysicsBody3D. A kinematic body is moved by code and ignores gravity. Writing the body’s position moves it in the physics world, and the slab under it follows the body’s pose.

  4. Yellow, inside a STATIC PhysicsBody3D with a CollisionShape3D beside the mesh: the usual way to build solid level geometry, collider and visual as siblings under one body.

Only the parents are animated. The slabs themselves are never touched after on_ready, so every slab that moves is inheriting its parent’s motion.

The bodies need no PhysicsRoot: a body outside one joins the scene tree’s default physics world. A body with no collider child is inert, so each body here carries a BoxShape3D sized to its slab (half_extents is half of the Mesh.box size). A body is built when it enters the tree, so give it its CollisionShape3D before adding the body to the scene.

The legend in the top-left corner is a VBoxContainer of Label nodes on a CanvasLayer, each tinted with its slab’s colour.

Source

  1# /// simvx
  2# web = { width = 1280, height = 720 }
  3# ///
  4"""Transform Hierarchy: a MeshInstance3D follows any parent transform.
  5
  6A ``MeshInstance3D`` draws at its own transform composed with every ancestor's,
  7whatever those ancestors are. There is no special "visual" slot on a group or a
  8physics body: to make a mesh follow something, make it that thing's child.
  9
 10Four identical slabs sit side by side, each attached a different way:
 11
 121. **Red, a direct child of the scene root.** Its own ``position`` is its world
 13   position.
 142. **Green, inside a ``Node3D`` group.** The slab's own transform is zero, so it
 15   sits exactly where the group is. The group turns with ``rotate_y()`` every
 16   frame and the slab turns with it, in place.
 173. **Blue, inside a ``KINEMATIC`` ``PhysicsBody3D``.** A kinematic body is moved
 18   by code and ignores gravity. Writing the body's ``position`` moves it in the
 19   physics world, and the slab under it follows the body's pose.
 204. **Yellow, inside a ``STATIC`` ``PhysicsBody3D``** with a ``CollisionShape3D``
 21   beside the mesh: the usual way to build solid level geometry, collider and
 22   visual as siblings under one body.
 23
 24Only the parents are animated. The slabs themselves are never touched after
 25``on_ready``, so every slab that moves is inheriting its parent's motion.
 26
 27The bodies need no ``PhysicsRoot``: a body outside one joins the scene tree's
 28default physics world. A body with no collider child is inert, so each body here
 29carries a ``BoxShape3D`` sized to its slab (``half_extents`` is half of the
 30``Mesh.box`` size). A body is built when it enters the tree, so give it its
 31``CollisionShape3D`` before adding the body to the scene.
 32
 33The legend in the top-left corner is a ``VBoxContainer`` of ``Label`` nodes on a
 34``CanvasLayer``, each tinted with its slab's colour.
 35"""
 36
 37import math
 38
 39from simvx.core import (
 40    Anchor,
 41    BodyMode,
 42    BoxShape3D,
 43    Camera3D,
 44    CanvasLayer,
 45    CollisionShape3D,
 46    DirectionalLight3D,
 47    Label,
 48    Material,
 49    Mesh,
 50    MeshInstance3D,
 51    Node,
 52    Node3D,
 53    PhysicsBody3D,
 54    VBoxContainer,
 55    WorldEnvironment,
 56)
 57from simvx.graphics import App
 58
 59# Every slab is the same geometry; only the parent differs.
 60SLAB = Mesh.box((3, 0.5, 6))
 61SLAB_COLLIDER = BoxShape3D(half_extents=(1.5, 0.25, 3))
 62
 63RED = (1, 0, 0, 1)
 64GREEN = (0, 1, 0, 1)
 65BLUE = (0, 0, 1, 1)
 66YELLOW = (1, 1, 0, 1)
 67
 68
 69def slab(name: str, colour, **kwargs) -> MeshInstance3D:
 70    return MeshInstance3D(name=name, mesh=SLAB, material=Material(colour=colour), **kwargs)
 71
 72
 73class MeshParentingDemo(Node):
 74    def on_ready(self):
 75        self.add_child(WorldEnvironment(ambient_light_energy=0.5))
 76        self.add_child(Camera3D(position=(0, 12, 22), fov=60, look_at=(0, 0, 0)))
 77        self.add_child(DirectionalLight3D(direction=(-0.3, -1, -0.5)))
 78        ground = Material(colour=(0.10, 0.11, 0.13, 1), metallic=0.1, roughness=0.9)
 79        self.add_child(MeshInstance3D(mesh=Mesh.plane((32, 16)), material=ground, position=(0, -0.75, 0)))
 80
 81        # 1. Direct child of the root: no transform between the slab and the world.
 82        self.add_child(slab("DirectChild", RED, position=(-7.5, 0, 0)))
 83
 84        # 2. A Node3D group: the slab inherits everything the group does.
 85        self.group = self.add_child(Node3D(name="SpinGroup", position=(-2.5, 0, 0)))
 86        self.group.add_child(slab("GroupSlab", GREEN))
 87
 88        # 3. A KINEMATIC body, moved by code in on_update. A body is built when it
 89        # enters the tree and a collider added after that is never picked up, so
 90        # both bodies get their children before they are added to the scene.
 91        body = PhysicsBody3D(name="BobbingBody", mode=BodyMode.KINEMATIC, position=(2.5, 0, 0))
 92        body.add_child(CollisionShape3D(shape=SLAB_COLLIDER))
 93        body.add_child(slab("KinematicSlab", BLUE))
 94        self.body = self.add_child(body)
 95
 96        # 4. A STATIC body: collider and visual as siblings, as level geometry is built.
 97        wall = PhysicsBody3D(name="StaticBody", mode=BodyMode.STATIC, position=(7.5, 0, 0))
 98        wall.add_child(CollisionShape3D(shape=SLAB_COLLIDER))
 99        wall.add_child(slab("StaticSlab", YELLOW))
100        self.add_child(wall)
101
102        self.time = 0.0
103
104        # Legend: one line per slab, in that slab's colour.
105        legend = self.add_child(CanvasLayer()).add_child(VBoxContainer(anchor=Anchor.TOP_LEFT, inset=16))
106        legend.add_child(Label("TRANSFORM HIERARCHY", font_size=32))
107        for text, colour in (
108            ("1. Direct child of the scene root", RED),
109            ("2. Inside a spinning Node3D group", GREEN),
110            ("3. Inside a bobbing KINEMATIC body", BLUE),
111            ("4. Inside a STATIC body + CollisionShape3D", YELLOW),
112        ):
113            line = legend.add_child(Label(text, font_size=22))
114            line.text_colour = colour
115
116    def on_update(self, dt: float):
117        # Only the parents move; the slabs follow through the hierarchy.
118        self.time += dt
119        self.group.rotate_y(0.8 * dt)
120        self.body.position = (2.5, 0.4 * math.sin(1.5 * self.time), 0)
121
122
123if __name__ == "__main__":
124    App(title="Transform Hierarchy", width=1280, height=720).run(MeshParentingDemo())