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 os
  8import shutil
  9import tempfile
 10
 11import numpy as np
 12from PIL import Image
 13
 14from simvx.core import (
 15    Camera3D,
 16    Input,
 17    InputMap,
 18    Key,
 19    Material,
 20    Mesh,
 21    MeshInstance3D,
 22    Node,
 23    Text2D,
 24)
 25from simvx.graphics import App
 26
 27
 28def _make_checkerboard(size=64, block=8):
 29    """Generate a checkerboard RGBA image."""
 30    img = np.zeros((size, size, 4), dtype=np.uint8)
 31    for y in range(size):
 32        for x in range(size):
 33            if ((x // block) + (y // block)) % 2 == 0:
 34                img[y, x] = [255, 255, 255, 255]
 35            else:
 36                img[y, x] = [80, 80, 80, 255]
 37    return img
 38
 39
 40def _make_gradient(size=64):
 41    """Generate a red-blue gradient RGBA image."""
 42    img = np.zeros((size, size, 4), dtype=np.uint8)
 43    for x in range(size):
 44        t = x / (size - 1)
 45        img[:, x] = [int(255 * (1 - t)), 0, int(255 * t), 255]
 46    return img
 47
 48
 49def _make_stripes(size=64, stripe_width=4):
 50    """Generate horizontal stripes RGBA image."""
 51    img = np.zeros((size, size, 4), dtype=np.uint8)
 52    for y in range(size):
 53        if (y // stripe_width) % 2 == 0:
 54            img[y, :] = [50, 200, 50, 255]
 55        else:
 56            img[y, :] = [200, 200, 50, 255]
 57    return img
 58
 59
 60def _save_texture(pixels, path):
 61    """Save RGBA numpy array as PNG."""
 62    Image.fromarray(pixels).save(path)
 63
 64
 65class TexturedCubesScene(Node):
 66    def on_ready(self):
 67        InputMap.add_action("quit", [Key.ESCAPE])
 68
 69        # Camera: looking down Y axis (proven setup)
 70        self.add_child(Camera3D(position=(0, -15, 0), look_at=(0, 0, 0), up=(0, 0, 1)))
 71
 72        # Generate textures to temp files
 73        self._tmp_dir = tempfile.mkdtemp(prefix="simvx_tex_")
 74        tex_paths = []
 75        for name, gen_fn in [
 76            ("checker.png", _make_checkerboard),
 77            ("gradient.png", _make_gradient),
 78            ("stripes.png", _make_stripes),
 79        ]:
 80            path = os.path.join(self._tmp_dir, name)
 81            _save_texture(gen_fn(), path)
 82            tex_paths.append(path)
 83
 84        # Share one mesh across all cubes (required for single-batch rendering)
 85        cube_mesh = Mesh.cube()
 86        positions = [(-3, 0, 0), (0, 0, 0), (3, 0, 0)]
 87
 88        for pos, tex_path in zip(positions, tex_paths, strict=True):
 89            mat = Material(colour=(1, 1, 1, 1), albedo_map=tex_path)
 90            cube = MeshInstance3D(
 91                mesh=cube_mesh,
 92                material=mat,
 93                position=pos,
 94            )
 95            self.add_child(cube)
 96
 97        # HUD
 98        self.add_child(Text2D(text="Textured Cubes Demo", x=10, y=10, font_scale=1.5))
 99
100    def on_process(self, dt):
101        if Input.is_action_just_pressed("quit"):
102            self.app.quit()
103            return
104        for child in self.children:
105            if isinstance(child, MeshInstance3D):
106                child.rotate_y(0.5 * dt)
107
108    def on_exit_tree(self):
109        shutil.rmtree(self._tmp_dir, ignore_errors=True)
110
111
112if __name__ == "__main__":
113    app = App(title="Textured Cubes", width=1280, height=720)
114    app.run(TexturedCubesScene())