Procedural textures¶
numpy RGBA arrays passed straight to Material.albedo_map.
▶ Run in browserTags: 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())