Textured cubes demo

generates checkerboard textures and renders them.

▶ Run in browser

Tags: 3d

Usage: uv run python examples/features/3d/textured_cubes.py

Source

  1"""Textured cubes demo: generates checkerboard textures and renders them.
  2
  3Usage:
  4    uv run python examples/features/3d/textured_cubes.py
  5"""
  6
  7import numpy as np
  8
  9from simvx.core import (
 10    Camera3D,
 11    DirectionalLight3D,
 12    Input,
 13    InputMap,
 14    Key,
 15    Material,
 16    Mesh,
 17    MeshInstance3D,
 18    Node,
 19    Text2D,
 20    WorldEnvironment,
 21)
 22from simvx.graphics import App
 23
 24
 25def _make_checkerboard(size=64, block=8):
 26    """Generate a checkerboard RGBA image."""
 27    img = np.zeros((size, size, 4), dtype=np.uint8)
 28    for y in range(size):
 29        for x in range(size):
 30            if ((x // block) + (y // block)) % 2 == 0:
 31                img[y, x] = [255, 255, 255, 255]
 32            else:
 33                img[y, x] = [80, 80, 80, 255]
 34    return img
 35
 36
 37def _make_gradient(size=64):
 38    """Generate a red-blue gradient RGBA image."""
 39    img = np.zeros((size, size, 4), dtype=np.uint8)
 40    for x in range(size):
 41        t = x / (size - 1)
 42        img[:, x] = [int(255 * (1 - t)), 0, int(255 * t), 255]
 43    return img
 44
 45
 46def _make_stripes(size=64, stripe_width=4):
 47    """Generate horizontal stripes RGBA image."""
 48    img = np.zeros((size, size, 4), dtype=np.uint8)
 49    for y in range(size):
 50        if (y // stripe_width) % 2 == 0:
 51            img[y, :] = [50, 200, 50, 255]
 52        else:
 53            img[y, :] = [200, 200, 50, 255]
 54    return img
 55
 56
 57class TexturedCubesScene(Node):
 58    def on_ready(self):
 59        InputMap.add_action("quit", [Key.ESCAPE])
 60
 61        # Camera: close enough that the textures are clearly readable
 62        self.add_child(Camera3D(position=(0, -7, 2), look_at=(0, 0, 0), up=(0, 0, 1)))
 63
 64        # Environment ambient + key/fill lights so the texture detail reads
 65        # clearly instead of sitting dim against a black void.
 66        env = self.add_child(WorldEnvironment())
 67        env.ambient_light_energy = 0.5
 68
 69        key = DirectionalLight3D(name="KeyLight", intensity=1.5)
 70        key.look_at((-1.0, -2.0, -1.0))
 71        self.add_child(key)
 72
 73        fill = DirectionalLight3D(name="FillLight", intensity=0.4, colour=(0.6, 0.7, 1.0))
 74        fill.look_at((1.0, -1.0, 2.0))
 75        self.add_child(fill)
 76
 77        # Procedural RGBA textures passed directly as numpy arrays. Material
 78        # accepts an ndarray albedo_map, so there is no PNG/PIL round-trip and
 79        # the demo works unchanged in a web export (no pillow dependency).
 80        textures = [_make_checkerboard(), _make_gradient(), _make_stripes()]
 81
 82        # Share one mesh across all cubes (required for single-batch rendering)
 83        cube_mesh = Mesh.cube()
 84        positions = [(-3, 0, 0), (0, 0, 0), (3, 0, 0)]
 85
 86        for pos, tex in zip(positions, textures, strict=True):
 87            mat = Material(colour=(1, 1, 1, 1), albedo_map=tex)
 88            cube = MeshInstance3D(
 89                mesh=cube_mesh,
 90                material=mat,
 91                position=pos,
 92            )
 93            self.add_child(cube)
 94
 95        # HUD
 96        self.add_child(Text2D(text="Textured Cubes Demo", position=(10, 10), font_scale=1.5))
 97        self.add_child(Text2D(text="Esc: quit", position=(10, 44)))
 98
 99    def on_update(self, dt):
100        if Input.is_action_just_pressed("quit"):
101            self.app.quit()
102            return
103        for child in self.children:
104            if isinstance(child, MeshInstance3D):
105                child.rotate_y(0.5 * dt)
106
107if __name__ == "__main__":
108    app = App(title="Textured Cubes", width=1280, height=720)
109    app.run(TexturedCubesScene())