Procedural textures

numpy RGBA arrays passed straight to Material.albedo_map.

▶ Run in browser

Tags: 3d

Three cubes each sample a texture generated in Python (checkerboard, gradient, stripes) and handed to Material(albedo_map=ndarray): the engine uploads uint8 RGBA arrays directly, so there is no image-file round-trip and the example runs unchanged in a web export.

Controls: ESC: Quit

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

Source

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