simvx.graphics.materials.custom_shader

Custom shader material system: user-facing API for custom GLSL shaders.

Provides ShaderMaterial for per-object custom shaders, UniformBuffer for GPU-side uniform data, and ShaderMaterialManager for pipeline caching and hot-reload.

Module Contents

Classes

ShaderMaterial

User-facing custom shader material.

UniformBuffer

GPU buffer for custom shader uniforms (std140 layout).

ShaderMaterialManager

Caches custom-shader pipelines and drives the desktop custom-shader pass.

Data

API

simvx.graphics.materials.custom_shader.__all__

[‘ShaderMaterial’, ‘UniformBuffer’, ‘ShaderMaterialManager’]

simvx.graphics.materials.custom_shader.log

‘getLogger(…)’

class simvx.graphics.materials.custom_shader.ShaderMaterial(vertex_path: str | pathlib.Path | None = None, fragment_path: str | pathlib.Path | None = None, *, vertex_source: str | None = None, fragment_source: str | None = None, language: str = 'glsl', wgsl_vertex: str | None = None, wgsl_fragment: str | None = None, wgsl_vertex_path: str | pathlib.Path | None = None, wgsl_fragment_path: str | pathlib.Path | None = None)[source]

User-facing custom shader material.

Allows using custom GLSL vertex/fragment shaders with user-defined uniforms. Works alongside the engine’s existing uber-shader pipeline by creating its own separate Vulkan pipeline.

Example::

mat = ShaderMaterial(
    vertex_path="shaders/wave.vert",
    fragment_path="shaders/gradient.frag",
)
mat.set_uniform("time", 0.0)
mat.set_uniform("colour", (1.0, 0.5, 0.2, 1.0))

Or with inline source::

mat = ShaderMaterial(
    vertex_source="""
        #version 450
        layout(location=0) in vec3 pos;
        void main() { gl_Position = vec4(pos, 1.0); }
    """,
    fragment_source="""
        #version 450
        layout(location=0) out vec4 out_color;
        void main() { out_color = vec4(1.0, 0.0, 0.0, 1.0); }
    """,
)

Initialization

property is_compiled: bool[source]

Whether shaders have been compiled to SPIR-V and loaded.

property uniforms: dict[str, Any][source]

All current uniform values.

set_uniform(name: str, value: Any) None[source]

Set a shader uniform by name.

Supported types: float, int, vec2, vec3, vec4, mat4, and numpy arrays. Type is inferred automatically from the value.

set_uniform_typed(name: str, value: Any, utype: str) None[source]

Set a uniform with an explicit GLSL type string.

get_uniform(name: str) Any[source]

Get the current value of a uniform. Raises KeyError if not set.

compile(device: Any, shader_dir: pathlib.Path | None = None) None[source]

Compile shaders to SPIR-V and create Vulkan shader modules.

Uses file paths if provided, otherwise writes inline source to temp files for compilation via glslc.

Args: device: Vulkan logical device handle. shader_dir: Base directory for resolving relative shader paths and includes.

resolve_wgsl(base_dir: pathlib.Path | None = None) tuple[str | None, str | None][source]

Return (vertex_wgsl, fragment_wgsl) for the web escape hatch, or (None, None).

Inline wgsl_vertex/wgsl_fragment take precedence over their *_path counterparts. Used by the web exporter to skip naga transpilation when hand-written WGSL is supplied. A partial escape hatch (only one stage) is an error: both stages must be provided together.

property has_wgsl_escape_hatch: bool[source]

Whether hand-written WGSL is supplied for both stages (web only).

get_pipeline_key() tuple[source]

Return a hashable key unique to this shader combination.

Used for pipeline caching in ShaderMaterialManager.

has_source_changed() bool[source]

Check if shader source files have been modified since last compile.

cleanup(device: Any) None[source]

Destroy Vulkan shader modules.

class simvx.graphics.materials.custom_shader.UniformBuffer(max_size: int = 1024)[source]

GPU buffer for custom shader uniforms (std140 layout).

Manages a host-visible Vulkan buffer and descriptor set for binding user-defined uniforms to a custom shader pipeline.

The buffer is laid out according to std140 rules so it can be directly consumed by a GLSL uniform block.

Initialization

property is_created: bool[source]
create(device: Any, physical_device: Any) None[source]

Create the GPU buffer and descriptor resources.

update(device: Any, uniform_data: dict[str, Any], uniform_types: dict[str, str]) None[source]

Upload uniform values to the GPU buffer using std140 layout.

Args: device: Vulkan logical device. uniform_data: Name-to-value mapping of uniforms. uniform_types: Name-to-GLSL-type mapping (e.g. {“time”: “float”}).

get_descriptor_set() Any[source]

Return the Vulkan descriptor set for binding to a pipeline.

get_descriptor_layout() Any[source]

Return the descriptor set layout for pipeline creation.

cleanup(device: Any) None[source]

Destroy GPU resources.

class simvx.graphics.materials.custom_shader.ShaderMaterialManager[source]

Caches custom-shader pipelines and drives the desktop custom-shader pass.

Tracks all registered ShaderMaterial instances and their compiled pipelines. Pipelines are cached by the shader source/path combination so that multiple objects sharing the same shaders reuse one pipeline.

The manager also owns the shared per-frame camera UBO (group0) and a per-frame transforms SSBO (group1) used by every custom-shader draw. Per-material uniforms live in a group2 UBO created lazily per material.

The descriptor layout ABI is identical to the web WebGPU custom-shader ABI: group0 camera UBO, group1 transforms SSBO addressed by gl_InstanceIndex, group2 per-material UBO (+ separated textures/samplers at binding 1.., not yet wired for the canary).

Initialization

register_material(material: simvx.graphics.materials.custom_shader.ShaderMaterial) None[source]

Track a ShaderMaterial for hot-reload monitoring.

get_or_create_pipeline(material: simvx.graphics.materials.custom_shader.ShaderMaterial, device: Any, physical_device: Any, render_pass: Any, extent: tuple[int, int], shader_dir: pathlib.Path | None = None) tuple[Any, Any][source]

Get a cached pipeline for this material, or compile and create one.

Uses the unified ABI layouts: group0 camera UBO, group1 transforms SSBO, group2 per-material UBO. Returns (VkPipeline, VkPipelineLayout).

get_uniform_buffer(material: simvx.graphics.materials.custom_shader.ShaderMaterial) simvx.graphics.materials.custom_shader.UniformBuffer | None[source]

Get the per-material (group2) UniformBuffer for a material, if any.

update_uniforms(material: simvx.graphics.materials.custom_shader.ShaderMaterial, device: Any) None[source]

Upload current uniform values for a material to its group2 GPU buffer.

draw(cmd: Any, submissions: list, device: Any, physical_device: Any, render_pass: Any, extent: tuple[int, int], view: numpy.ndarray, proj: numpy.ndarray, registry: Any, shader_dir: pathlib.Path | None = None, frame_globals_buf: Any = None, gbuffer: bool = False) None[source]

Draw all ShaderMaterial-backed submissions inside the main render pass.

submissions is the renderer’s per-frame [(mesh_handle, transform, material_id, shader_material)] list. Each submission is drawn individually (one transforms-SSBO slot per draw, addressed via gl_InstanceIndex), binding the cached custom pipeline, the shared camera UBO (group0), the transforms SSBO (group1), and the per-material UBO (group2). Per-material pipeline switching is the cost of the custom-shader path.

check_hot_reload(device: Any, physical_device: Any, render_pass: Any, extent: tuple[int, int], shader_dir: pathlib.Path | None = None) list[simvx.graphics.materials.custom_shader.ShaderMaterial][source]

Check all registered materials for source file changes and recompile.

Returns a list of materials that were recompiled.

cleanup(device: Any) None[source]

Destroy all cached pipelines, uniform buffers, shared resources, and modules.