Custom Shaders¶
ShaderMaterial lets you write custom GLSL vertex and fragment shaders for any MeshInstance3D.
Basic Usage¶
from simvx.graphics.materials.custom_shader import ShaderMaterial
from simvx.core import MeshInstance3D, Mesh
mat = ShaderMaterial(
vertex_source="""
#version 450
layout(location = 0) in vec3 position;
layout(push_constant) uniform PC { mat4 mvp; };
void main() {
gl_Position = mvp * vec4(position, 1.0);
}
""",
fragment_source="""
#version 450
layout(location = 0) out vec4 frag_colour;
layout(set = 2, binding = 0) uniform UBO { float time; vec3 tint; };
void main() {
frag_colour = vec4(tint * (0.5 + 0.5 * sin(time)), 1.0);
}
""",
)
mat.set_uniform("time", 0.0)
mat.set_uniform("tint", (1.0, 0.3, 0.1))
cube = MeshInstance3D(mesh=Mesh.cube(), material=mat)
self.add_child(cube)
Update uniforms each frame in on_update():
def on_update(self, dt):
mat.set_uniform("time", self.elapsed_time)
Loading from Files¶
mat = ShaderMaterial(
vertex_path="shaders/wave.vert",
fragment_path="shaders/wave.frag",
)
GLSL files are compiled to SPIR-V automatically via glslc. Hot-reload is supported: modified shader files are detected and recompiled at runtime.
Uniforms¶
Set uniforms by name. Types are inferred from the Python value:
mat.set_uniform("speed", 2.5) # float
mat.set_uniform("offset", (1.0, 0.0)) # vec2
mat.set_uniform("colour", (1.0, 0.5, 0.0)) # vec3
mat.set_uniform("tint", (1.0, 0.5, 0.0, 1.0)) # vec4
mat.set_uniform("count", 10) # int
For explicit type control:
mat.set_uniform_typed("grid_size", (8, 8), "ivec2")
Supported types: float, int, uint, vec2, vec3, vec4, ivec2, ivec3, ivec4, mat4.
Textures¶
Declare a separated texture and sampler in the material’s own group and bind
an image to the name the shader declares. There is no combined sampler2D
uniform: WebGPU has no equivalent, so the engine does not accept one on either
backend.
layout(set = 2, binding = 1) uniform texture2D albedo;
layout(set = 2, binding = 2) uniform sampler albedoSampler;
void main() {
outColour = texture(sampler2D(albedo, albedoSampler), fragUV);
}
mat.set_texture("albedo", "assets/crate.png") # colour map (sRGB)
mat.set_texture("height", heightfield, colour_space="linear") # data map
The source is anything the engine loads a texture from: a path, encoded bytes,
an RGBA array, or a Texture resource. Binding a
name the shader never declared raises rather than leaving a silently white
surface, and a texture the material never bound reads 1×1 white.
Calling set_texture again at any point repoints the binding, on both backends:
the frames drawn after it sample the new texture.
Every declared sampler gets the same one: linear filtering, repeat addressing,
and the full mip chain of whatever texture it reads (a colour map is mipped, a
colour_space="linear" data map is not). A per-sampler filter mode is not
configurable yet, and the limit is identical on both backends.
The declarations are read out of the shader source by a lexical reader.
Commenting one out really does remove it, but the preprocessor is not evaluated:
a declaration inside #if 0, one produced by a macro, and one living in an
#included chunk are invisible to it. Declare a material’s textures and
samplers in the stage source itself.
Transparency¶
mat = ShaderMaterial(..., transparent=True)
The material is drawn source-over (straight alpha) after the opaque ones, depth
tested but not depth writing, and double-sided – the same treatment the standard
transparent pass gives a blended Material. The fragment shader’s alpha is what
composites.
Web export¶
ShaderMaterials are transpiled GLSL→WGSL at export time (build-time naga)
and render through a per-material pipeline on WebGPU, textures and transparency
included. To be web-portable a shader must follow the unified binding ABI:
camera UBO at
set = 0, binding = 0(mat4 view; mat4 proj;), not apush_constant(push constants don’t translate to WGSL)transforms SSBO at
set = 1, binding = 0(mat4[], indexed bygl_InstanceIndex)per-material UBO at
set = 2, binding = 0, then separated textures/samplers atbinding = 1+
The exporter reads the material group out of the emitted WGSL and publishes it,
so the browser builds the same bind-group layout the Vulkan renderer does. A
declaration neither backend can bind – a combined sampler, a storage image, a
cube or array texture, a second uniform buffer in set = 2 – is named at export
time, quoting the shader line that declared it, rather than failing pipeline
creation in the browser.
Such a shader is treated exactly like one that fails to transpile at all: the
export warns and the object falls back to its underlying Material. Export with
simvx export web --strict to fail the build on any fallback instead of
degrading silently.
Example¶
See examples/features/3d/custom_shader.py for custom ShaderMaterial with animated uniforms.
API Reference¶
See simvx.graphics.materials.custom_shader for the complete shader API.