Custom Shader

ShaderMaterial API preview with animated uniforms.

📄 Docs only

Tags: 3d

Demonstrates:

  • ShaderMaterial with inline GLSL vertex and fragment source

  • Per-frame uniform animation (time, amplitude, colour)

  • Multiple objects with different shader parameters

  • Standard Material objects alongside custom shaders

Controls: A / D - Orbit camera W / S - Zoom in / out 1 / 2 - Adjust wave amplitude Escape - Quit

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

Source

  1"""Custom Shader: ShaderMaterial API preview with animated uniforms.
  2
  3# /// simvx
  4# web = { disabled = true, reason = "ShaderMaterial not yet applied on web: shapes render with default materials." }
  5# ///
  6
  7Demonstrates:
  8  - ShaderMaterial with inline GLSL vertex and fragment source
  9  - Per-frame uniform animation (time, amplitude, colour)
 10  - Multiple objects with different shader parameters
 11  - Standard Material objects alongside custom shaders
 12
 13Controls:
 14    A / D       - Orbit camera
 15    W / S       - Zoom in / out
 16    1 / 2       - Adjust wave amplitude
 17    Escape      - Quit
 18
 19Usage:
 20    uv run python examples/features/3d/custom_shader.py
 21"""
 22
 23
 24import math
 25
 26from simvx.core import (
 27    Camera3D,
 28    DirectionalLight3D,
 29    Input,
 30    InputMap,
 31    Key,
 32    Material,
 33    Mesh,
 34    MeshInstance3D,
 35    Node,
 36    Text2D,
 37)
 38from simvx.graphics import App, ShaderMaterial
 39
 40# -- Inline GLSL shaders --
 41# Wave vertex shader: sine displacement along Y based on time + position
 42WAVE_VERT = """\
 43#version 450
 44layout(location = 0) in vec3 inPosition;
 45layout(location = 1) in vec3 inNormal;
 46layout(location = 2) in vec2 inUV;
 47
 48layout(push_constant) uniform PC {
 49    mat4 view;
 50    mat4 proj;
 51};
 52
 53layout(std430, set = 0, binding = 0) readonly buffer Transforms {
 54    mat4 models[];
 55};
 56
 57layout(location = 0) out vec3 fragWorldPos;
 58layout(location = 1) out vec3 fragNormal;
 59layout(location = 2) out vec2 fragUV;
 60
 61layout(set = 1, binding = 0) uniform Params {
 62    float time;
 63    float amplitude;
 64};
 65
 66void main() {
 67    mat4 model = models[gl_InstanceIndex];
 68    vec3 pos = inPosition;
 69    // Sine-wave vertex displacement
 70    pos.y += sin(pos.x * 3.0 + time * 2.0) * amplitude * 0.3;
 71    pos.y += cos(pos.z * 2.5 + time * 1.5) * amplitude * 0.2;
 72
 73    vec4 worldPos = model * vec4(pos, 1.0);
 74    fragWorldPos = worldPos.xyz;
 75    fragNormal = mat3(model) * inNormal;
 76    fragUV = inUV;
 77    gl_Position = proj * view * worldPos;
 78}
 79"""
 80
 81# Animated gradient fragment shader: colour shifts with time
 82WAVE_FRAG = """\
 83#version 450
 84layout(location = 0) in vec3 fragWorldPos;
 85layout(location = 1) in vec3 fragNormal;
 86layout(location = 2) in vec2 fragUV;
 87
 88layout(location = 0) out vec4 outColour;
 89
 90layout(set = 1, binding = 0) uniform Params {
 91    float time;
 92    float amplitude;
 93};
 94
 95void main() {
 96    // Animated gradient based on world position and time
 97    float r = sin(fragWorldPos.x * 0.5 + time) * 0.5 + 0.5;
 98    float g = sin(fragWorldPos.z * 0.5 + time * 0.7 + 2.094) * 0.5 + 0.5;
 99    float b = sin(time * 0.5 + 4.189) * 0.5 + 0.5;
100    // Simple diffuse lighting
101    vec3 N = normalize(fragNormal);
102    vec3 L = normalize(vec3(1.0, 2.0, 1.0));
103    float diff = max(dot(N, L), 0.15);
104    outColour = vec4(vec3(r, g, b) * diff, 1.0);
105}
106"""
107
108# Colour-pulse fragment shader: single pulsing colour
109PULSE_FRAG = """\
110#version 450
111layout(location = 0) in vec3 fragWorldPos;
112layout(location = 1) in vec3 fragNormal;
113layout(location = 2) in vec2 fragUV;
114
115layout(location = 0) out vec4 outColour;
116
117layout(set = 1, binding = 0) uniform Params {
118    float time;
119    float amplitude;
120};
121
122void main() {
123    // Pulsing warm colour
124    float pulse = sin(time * 3.0) * 0.5 + 0.5;
125    vec3 colour = mix(vec3(0.9, 0.2, 0.1), vec3(1.0, 0.8, 0.2), pulse);
126    // Simple diffuse
127    vec3 N = normalize(fragNormal);
128    vec3 L = normalize(vec3(-1.0, 2.0, 0.5));
129    float diff = max(dot(N, L), 0.15);
130    outColour = vec4(colour * diff * (0.8 + amplitude * 0.1), 1.0);
131}
132"""
133
134
135class CustomShaderDemo(Node):
136    def on_ready(self):
137        InputMap.add_action("orbit_left", [Key.A])
138        InputMap.add_action("orbit_right", [Key.D])
139        InputMap.add_action("zoom_in", [Key.W])
140        InputMap.add_action("zoom_out", [Key.S])
141        InputMap.add_action("amp_up", [Key.KEY_1])
142        InputMap.add_action("amp_down", [Key.KEY_2])
143        InputMap.add_action("quit", [Key.ESCAPE])
144
145        # Camera orbit state
146        self._yaw = 30.0
147        self._distance = 14.0
148        self._pitch = 25.0
149
150        self._cam = Camera3D(name="Camera", fov=60, near=0.1, far=100.0)
151        self.add_child(self._cam)
152
153        # Directional light
154        sun = DirectionalLight3D(name="Sun", intensity=1.0)
155        sun.look_at((-1.0, -2.0, -1.0))
156        self.add_child(sun)
157
158        # -- Wave shader material (gradient + vertex displacement) --
159        self._wave_shader = ShaderMaterial(vertex_source=WAVE_VERT, fragment_source=WAVE_FRAG)
160        self._wave_shader.set_uniform("time", 0.0)
161        self._wave_shader.set_uniform("amplitude", 1.0)
162
163        # -- Pulse shader material (colour pulse, shared vertex shader) --
164        self._pulse_shader = ShaderMaterial(vertex_source=WAVE_VERT, fragment_source=PULSE_FRAG)
165        self._pulse_shader.set_uniform("time", 0.0)
166        self._pulse_shader.set_uniform("amplitude", 1.0)
167
168        # Sphere with wave shader (left)
169        wave_sphere = MeshInstance3D(
170            name="WaveSphere",
171            mesh=Mesh.sphere(radius=1.5, rings=32, segments=32),
172            material=Material(colour=(1.0, 1.0, 1.0)),
173            position=(-3.5, 1.5, 0.0),
174        )
175        wave_sphere.shader_material = self._wave_shader
176        self.add_child(wave_sphere)
177
178        # Cube with pulse shader (right)
179        pulse_cube = MeshInstance3D(
180            name="PulseCube",
181            mesh=Mesh.cube(size=2.0),
182            material=Material(colour=(1.0, 1.0, 1.0)),
183            position=(3.5, 1.5, 0.0),
184        )
185        pulse_cube.shader_material = self._pulse_shader
186        self.add_child(pulse_cube)
187
188        # Ground plane (standard material, no custom shader)
189        ground = MeshInstance3D(
190            name="Ground",
191            mesh=Mesh.cube(),
192            material=Material(colour=(0.35, 0.4, 0.35), roughness=0.9),
193            position=(0.0, -0.05, 0.0),
194            scale=(20.0, 0.1, 20.0),
195        )
196        self.add_child(ground)
197
198        # Reference cubes (standard materials) for comparison
199        for i, colour in enumerate([(0.8, 0.2, 0.3), (0.2, 0.3, 0.8), (0.2, 0.8, 0.3)]):
200            ref = MeshInstance3D(
201                name=f"Ref{i}",
202                mesh=Mesh.cube(),
203                material=Material(colour=colour, roughness=0.4, metallic=0.2),
204                position=(-3.0 + i * 3.0, 0.5, -4.0),
205            )
206            self.add_child(ref)
207
208        # HUD
209        self._hud = Text2D(name="HUD", text="", font_scale=1.2, x=10.0, y=10.0)
210        self.add_child(self._hud)
211
212        self._time = 0.0
213        self._amplitude = 1.0
214        self._update_camera()
215
216    def on_process(self, dt):
217        self._time += dt
218
219        # Camera orbit
220        if Input.is_action_pressed("orbit_left"):
221            self._yaw += 60.0 * dt
222        if Input.is_action_pressed("orbit_right"):
223            self._yaw -= 60.0 * dt
224        if Input.is_action_pressed("zoom_in"):
225            self._distance = max(5.0, self._distance - 8.0 * dt)
226        if Input.is_action_pressed("zoom_out"):
227            self._distance = min(30.0, self._distance + 8.0 * dt)
228
229        # Amplitude adjustment
230        if Input.is_action_pressed("amp_up"):
231            self._amplitude = min(3.0, self._amplitude + 1.5 * dt)
232        if Input.is_action_pressed("amp_down"):
233            self._amplitude = max(0.1, self._amplitude - 1.5 * dt)
234
235        if Input.is_action_just_pressed("quit"):
236            self.app.quit()
237            return
238
239        # Update shader uniforms each frame
240        self._wave_shader.set_uniform("time", self._time)
241        self._wave_shader.set_uniform("amplitude", self._amplitude)
242        self._pulse_shader.set_uniform("time", self._time)
243        self._pulse_shader.set_uniform("amplitude", self._amplitude)
244
245        # Slowly rotate the shader objects
246        for child in self.children:
247            if child.name in ("WaveSphere", "PulseCube"):
248                child.rotate_y(math.radians(30.0) * dt)
249
250        self._update_camera()
251        self._update_hud()
252
253    def _update_camera(self):
254        yaw_rad = math.radians(self._yaw)
255        pitch_rad = math.radians(self._pitch)
256        cp = math.cos(pitch_rad)
257        x = self._distance * cp * math.sin(yaw_rad)
258        y = self._distance * math.sin(pitch_rad)
259        z = self._distance * cp * math.cos(yaw_rad)
260        self._cam.position = (x, y, z)
261        self._cam.look_at((0.0, 1.0, 0.0))
262
263    def _update_hud(self):
264        wave_info = f"Uniforms: time={self._time:.1f}  amplitude={self._amplitude:.2f}"
265        compiled = "yes" if self._wave_shader.is_compiled else "no"
266        lines = [
267            "Custom Shader Demo",
268            f"  Wave shader compiled: {compiled}",
269            f"  Pulse shader uniforms: {len(self._pulse_shader.uniforms)}",
270            f"  {wave_info}",
271            "[1/2] Amplitude  [A/D] Orbit  [W/S] Zoom  [Esc] Quit",
272        ]
273        self._hud.text = "\n".join(lines)
274
275
276if __name__ == "__main__":
277    app = App(title="Custom Shader Demo", width=1280, height=720)
278    app.run(CustomShaderDemo())