Custom Shader Textures¶
sampling a texture and blending from a ShaderMaterial.
▶ Run in browserTags: 3d
A ShaderMaterial runs your own GLSL in place of the engine’s standard shader.
Its per-material resources live in descriptor set 2: the uniform block at
binding 0, then the textures it samples. Declare each texture as a separate
texture2D and sampler pair rather than a combined sampler2D, because the
separated form is what both the Vulkan and the WebGPU backends can bind.
material.set_texture("albedo", source) binds an image to the texture2D the
shader declares under that name; the source can be a file path, encoded bytes,
an RGBA array or a Texture, and a name the shader does not declare raises.
set_uniform(name, value) fills the uniform block by name. Attach the material
to a mesh with mesh_instance.shader_material = material.
The scene uses one shader three ways:
an opaque cube,
a pane in front of it built with
transparent=True, so the alpha the fragment shader writes composites over the cube instead of replacing it,a floor that tiles the texture 24 times each way (the
uv_scaleuniform). Its far half is minified well below one texel per pixel, so it reads from the texture’s mip chain.
Pressing T (or the on-screen button) calls set_texture again on all three
materials while they are on screen. The texture sits in a descriptor set that
may be rewritten after binding, so the swap needs no pipeline rebuild: frames
already in flight may show either image and every frame after samples the new
one. Drag or use the mouse wheel to orbit and zoom the OrbitCamera3D; A and D
orbit from the keyboard.
Source¶
1"""Custom Shader Textures: sampling a texture and blending from a ShaderMaterial.
2
3A `ShaderMaterial` runs your own GLSL in place of the engine's standard shader.
4Its per-material resources live in descriptor set 2: the uniform block at
5binding 0, then the textures it samples. Declare each texture as a separate
6`texture2D` and `sampler` pair rather than a combined `sampler2D`, because the
7separated form is what both the Vulkan and the WebGPU backends can bind.
8
9`material.set_texture("albedo", source)` binds an image to the `texture2D` the
10shader declares under that name; the source can be a file path, encoded bytes,
11an RGBA array or a `Texture`, and a name the shader does not declare raises.
12`set_uniform(name, value)` fills the uniform block by name. Attach the material
13to a mesh with `mesh_instance.shader_material = material`.
14
15The scene uses one shader three ways:
16
17- an opaque cube,
18- a pane in front of it built with `transparent=True`, so the alpha the fragment
19 shader writes composites over the cube instead of replacing it,
20- a floor that tiles the texture 24 times each way (the `uv_scale` uniform).
21 Its far half is minified well below one texel per pixel, so it reads from the
22 texture's mip chain.
23
24Pressing T (or the on-screen button) calls `set_texture` again on all three
25materials while they are on screen. The texture sits in a descriptor set that
26may be rewritten after binding, so the swap needs no pipeline rebuild: frames
27already in flight may show either image and every frame after samples the new
28one. Drag or use the mouse wheel to orbit and zoom the `OrbitCamera3D`; A and D
29orbit from the keyboard.
30"""
31
32import math
33
34import numpy as np
35
36from simvx.core import (
37 Anchor,
38 Axis1D,
39 Button,
40 CanvasLayer,
41 DirectionalLight3D,
42 Input,
43 Key,
44 Label,
45 Mesh,
46 MeshInstance3D,
47 Node,
48 OrbitCamera3D,
49 on_input,
50)
51from simvx.graphics import App, ShaderMaterial
52
53VERTEX = """\
54#version 450
55layout(location = 0) in vec3 inPosition;
56layout(location = 1) in vec3 inNormal;
57layout(location = 2) in vec2 inUV;
58
59layout(set = 0, binding = 0) uniform Camera {
60 mat4 view;
61 mat4 proj;
62};
63layout(std430, set = 1, binding = 0) readonly buffer Transforms {
64 mat4 models[];
65};
66
67layout(location = 0) out vec2 fragUV;
68
69void main() {
70 fragUV = inUV;
71 gl_Position = proj * view * models[gl_InstanceIndex] * vec4(inPosition, 1.0);
72}
73"""
74
75FRAGMENT = """\
76#version 450
77layout(location = 0) in vec2 fragUV;
78layout(location = 0) out vec4 outColour;
79
80layout(set = 2, binding = 0) uniform Params {
81 float tint;
82 float alpha;
83 float uv_scale;
84};
85layout(set = 2, binding = 1) uniform texture2D albedo;
86layout(set = 2, binding = 2) uniform sampler albedoSampler;
87
88void main() {
89 vec4 texel = texture(sampler2D(albedo, albedoSampler), fragUV * uv_scale);
90 outColour = vec4(texel.rgb * tint, texel.a * alpha);
91}
92"""
93
94
95def two_tone(mask: np.ndarray, on: tuple, off: tuple) -> np.ndarray:
96 """An RGBA image painted `on` where the mask holds and `off` elsewhere."""
97 pixels = np.empty((*mask.shape, 4), dtype=np.uint8)
98 pixels[mask], pixels[~mask] = on, off
99 return pixels
100
101
102ys, xs = np.mgrid[0:64, 0:64]
103CHECKERBOARD = two_tone((xs // 8 + ys // 8) % 2 == 0, (230, 120, 40, 255), (40, 90, 220, 255))
104DIAGONALS = two_tone((xs + ys) // 8 % 2 == 0, (60, 200, 110, 255), (150, 60, 210, 255))
105
106
107def textured(tint: float, alpha: float = 1.0, uv_scale: float = 1.0, transparent: bool = False) -> ShaderMaterial:
108 material = ShaderMaterial(vertex_source=VERTEX, fragment_source=FRAGMENT, transparent=transparent)
109 material.set_uniform("tint", tint)
110 material.set_uniform("alpha", alpha)
111 material.set_uniform("uv_scale", uv_scale)
112 material.set_texture("albedo", CHECKERBOARD)
113 return material
114
115
116class TextureShaderScene(Node):
117 """A textured cube behind a transparent textured pane, over a tiled floor."""
118
119 input_context = {"orbit": [Axis1D(low=Key.A, high=Key.D)], "swap_texture": [Key.T]}
120
121 def on_ready(self):
122 self.camera = self.add_child(
123 OrbitCamera3D(
124 distance=7.0,
125 pitch=math.radians(-10),
126 max_pitch=math.radians(-3),
127 min_distance=4.0,
128 max_distance=30.0,
129 )
130 )
131 self.add_child(DirectionalLight3D())
132 self.materials = [textured(1.0), textured(0.4, alpha=0.45, transparent=True), textured(0.85, uv_scale=24.0)]
133 surfaces = [
134 MeshInstance3D(name="TexturedCube", mesh=Mesh.box(2.6)),
135 MeshInstance3D(name="GlassPane", mesh=Mesh.box((3.4, 3.4, 0.068)), position=(0.0, 0.0, 2.4)),
136 MeshInstance3D(name="TiledFloor", mesh=Mesh.plane(60.0), position=(0.0, -1.6, 0.0)),
137 ]
138 for surface, material in zip(surfaces, self.materials, strict=True):
139 surface.shader_material = material
140 self.add_child(surface)
141
142 # Example help overlay: not part of the technique shown.
143 hud = self.add_child(CanvasLayer())
144 hud.add_child(Label("Drag or A / D: orbit, wheel: zoom", anchor=Anchor.TOP_LEFT, inset=16))
145 hud.add_child(Button(action="swap_texture", text="Swap texture", anchor=Anchor.BOTTOM_LEFT, inset=16))
146
147 @on_input("swap_texture")
148 def swap_texture(self, _event):
149 image = DIAGONALS if self.materials[0].get_texture("albedo") is CHECKERBOARD else CHECKERBOARD
150 for material in self.materials:
151 material.set_texture("albedo", image)
152
153 def on_update(self, dt):
154 self.camera.orbit(Input.value("orbit") * dt, 0.0)
155
156
157if __name__ == "__main__":
158 App(width=1024, height=640, title="Custom Shader Textures").run(TextureShaderScene())