Procedural Textures

NumPy Arrays as Material Maps.

▶ Run in browser

Tags: 3d

A texture does not have to be an image file. Material(albedo_map=...) accepts a NumPy array of shape (height, width, 4) holding RGBA bytes, and the engine uploads it to the GPU as it is. A texture computed in Python needs no PNG, no image library and no file on disk, so the same code runs in a browser export with nothing extra to ship::

ys, xs = np.indices((64, 64))
light = (xs // 8 + ys // 8) % 2 == 0
checker = np.where(light[..., None], np.uint8([255, 255, 255, 255]), np.uint8([80, 80, 80, 255]))
cube = MeshInstance3D(mesh=Mesh.box(), material=Material(albedo_map=checker))

Here three spinning cubes each wear one generated texture: a checkerboard, a red-to-blue gradient and green and yellow stripes. The cubes share a single Mesh; only their materials differ.

A float32 array with values in [0, 1] is accepted too; the material scales it to bytes when it is built.

Source

 1"""Procedural Textures: NumPy Arrays as Material Maps.
 2
 3A texture does not have to be an image file. ``Material(albedo_map=...)``
 4accepts a NumPy array of shape ``(height, width, 4)`` holding RGBA bytes, and
 5the engine uploads it to the GPU as it is. A texture computed in Python needs
 6no PNG, no image library and no file on disk, so the same code runs in a
 7browser export with nothing extra to ship::
 8
 9    ys, xs = np.indices((64, 64))
10    light = (xs // 8 + ys // 8) % 2 == 0
11    checker = np.where(light[..., None], np.uint8([255, 255, 255, 255]), np.uint8([80, 80, 80, 255]))
12    cube = MeshInstance3D(mesh=Mesh.box(), material=Material(albedo_map=checker))
13
14Here three spinning cubes each wear one generated texture: a checkerboard, a
15red-to-blue gradient and green and yellow stripes. The cubes share a single
16``Mesh``; only their materials differ.
17
18A ``float32`` array with values in ``[0, 1]`` is accepted too; the material
19scales it to bytes when it is built.
20"""
21
22import numpy as np
23
24from simvx.core import (
25    Anchor,
26    Camera3D,
27    CanvasLayer,
28    DirectionalLight3D,
29    Label,
30    Material,
31    Mesh,
32    MeshInstance3D,
33    Node,
34    WorldEnvironment,
35)
36from simvx.graphics import App
37
38
39def checkerboard(size=64, block=8):
40    ys, xs = np.indices((size, size))
41    light = (xs // block + ys // block) % 2 == 0
42    return np.where(light[..., None], np.uint8([255, 255, 255, 255]), np.uint8([80, 80, 80, 255]))
43
44
45def gradient(size=64):
46    t = np.linspace(0.0, 1.0, size)
47    img = np.zeros((size, size, 4), dtype=np.uint8)
48    img[..., 0] = 255 * (1.0 - t)
49    img[..., 2] = 255 * t
50    img[..., 3] = 255
51    return img
52
53
54def stripes(size=64, width=4):
55    band = (np.arange(size) // width) % 2 == 0
56    img = np.empty((size, size, 4), dtype=np.uint8)
57    img[band] = [50, 200, 50, 255]
58    img[~band] = [200, 200, 50, 255]
59    return img
60
61
62class TexturedCubesScene(Node):
63    def on_ready(self):
64        self.add_child(Camera3D(position=(0, 2.5, 7), look_at=(0, 0, 0)))
65        self.add_child(WorldEnvironment(ambient_light_energy=0.5))
66        self.add_child(DirectionalLight3D(intensity=1.5, direction=(-1, -2, -1)))
67        self.add_child(DirectionalLight3D(intensity=0.4, colour=(0.6, 0.7, 1.0), direction=(1, -0.5, -1)))
68
69        cube = Mesh.box()
70        textures = [checkerboard(), gradient(), stripes()]
71        self.cubes = [
72            self.add_child(MeshInstance3D(mesh=cube, material=Material(albedo_map=tex), position=(x, 0, 0)))
73            for x, tex in zip((-3, 0, 3), textures, strict=True)
74        ]
75
76        hud = self.add_child(CanvasLayer())
77        hud.add_child(Label("Procedural textures from NumPy arrays", anchor=Anchor.TOP_LEFT, inset=20, font_size=24))
78
79    def on_update(self, dt):
80        for cube in self.cubes:
81            cube.rotate_y(0.5 * dt)
82
83
84if __name__ == "__main__":
85    App(title="Textured Cubes", width=1280, height=720).run(TexturedCubesScene())