Material & Shader System
Overview
Materials are immutable typed property bags (MaterialData) that drive shader compilation and GPU data upload. The engine auto-generates Slang shader code from material properties and compiles shaders using the native Slang FFM bindings with generic specialization.
MaterialData
Immutable wrapper over PropertyMap. Each property is a PropertyKey<T> with a name and type. Setting a property returns a new instance.
1
2
var mat = MaterialData.pbr(new Vec3(0.9f, 0.2f, 0.2f), 0.3f, 0.8f);
var mat2 = mat.set(MaterialData.ROUGHNESS, 0.5f); // new instance
Factory methods: MaterialData.pbr(albedo, roughness, metallic), MaterialData.unlit(color), MaterialData.create(shaderHint).
Standard keys: ALBEDO_COLOR, ROUGHNESS, METALLIC, EMISSIVE, OPACITY, ALBEDO_MAP, NORMAL_MAP, etc.
The shaderHint() (e.g., “PBR”, “UNLIT”) determines which .slang file is loaded.
Shader Param Blocks — SlangParamsBlock
Generates Slang interface + struct + cbuffer + implementation from either:
- Java records (fixed params: camera, engine, object, user-defined)
- PropertyKey sets (dynamic params: materials)
1
2
3
4
5
6
7
8
// From record — generates ICameraParams interface + UboCameraParams impl
SlangParamsBlock.fromRecord("Camera", CameraParams.class)
.withBinding(1) // → cbuffer CameraBuffer : register(b1)
.generateUbo();
// From material keys — generates IMaterialParams + UboMaterialParams
SlangParamsBlock.fromKeys("Material", materialKeys)
.generateUbo(false); // no static global — uses generic specialization
Generated Slang output (example for camera):
interface ICameraParams {
float4x4 viewProjection();
float4x4 view();
float4x4 projection();
float3 position();
float near();
float far();
};
struct CameraParamsData {
float4x4 viewProjection;
float4x4 view;
float4x4 projection;
float3 position;
float near;
float far;
};
cbuffer CameraBuffer : register(b1) {
CameraParamsData cameraData;
};
struct UboCameraParams : ICameraParams {
float4x4 viewProjection() { return cameraData.viewProjection; }
// ... etc
};
withBinding(int) — explicit register annotations
Fixed binding slots use register(bN) for stable, predictable UBO layout:
- Engine: b0
- Camera: b1
- Object: b2
- Material: no fixed binding (resolved via reflection)
Global Params Registry
User-extensible registry for engine-wide and per-frame shader parameters. All registered params are prepended to every shader as Slang code.
1
2
3
4
5
6
7
8
9
10
// Engine registers defaults
renderer.registerGlobalParams("Engine", EngineParams.class, 0);
renderer.registerGlobalParams("Camera", CameraParams.class, 1);
renderer.registerGlobalParams("Object", ObjectParams.class, 2);
// User registers custom params
renderer.registerGlobalParams("Light", LightParams.class); // auto-assigns next binding
// Update per frame
renderer.updateGlobalParams("Light", new LightParams(dir, color, intensity));
Built-in param records
| Record | Binding | Upload | Fields |
|---|---|---|---|
EngineParams |
b0 | per frame | time, deltaTime, resolution, frameCount |
CameraParams |
b1 | per camera | viewProjection, view, projection, position, near, far |
ObjectParams |
b2 | per draw | world |
Shader access
Shaders access params through well-known globals (for non-generic params) or generic type parameters:
// camera/engine/object use static globals
static UboCameraParams camera;
camera.viewProjection();
// materials use generic specialization
[shader("fragment")]
float4 fragmentMain<M : IMaterialParams>(...) {
M material;
material.albedoColor();
}
Generic Specialization
All shader params use Slang’s generic specialization. Entry points declare their dependencies:
[shader("vertex")]
VertexOutput vertexMain<C : ICameraParams, O : IObjectParams>(VertexInput input) {
C camera;
O object;
float4x4 mvp = mul(object.world(), camera.viewProjection());
output.position = mul(float4(input.position, 1.0), mvp);
output.worldNormal = mul(float4(input.normal, 0.0), object.world()).xyz;
...
}
[shader("fragment")]
float4 fragmentMain<C : ICameraParams, M : IMaterialParams>(VertexOutput input) : SV_Target {
C camera;
M material;
...
}
Auto-specialization flow
ShaderManagerprepends interfaces + structs + cbuffers + impls (no static globals)SlangCompilerNative.compileWithTypeMap()composes the program- Parses shader source for generic declarations (
<C : ICameraParams, O : IObjectParams>) - Matches each interface to a concrete type via registry map (
Camera → UboCameraParams) - Calls
specialize("UboCameraParams", "UboObjectParams", "UboCameraParams", "UboMaterialParams") - Slang monomorphizes — all interface calls become direct cbuffer reads in output GLSL
Specialization args order
Args are positional, matching the order generics appear across entry points:
- VS
<C, O>→ param 0 = C, param 1 = O - FS
<C, M>→ param 2 = C, param 3 = M
The compileWithTypeMap() method auto-discovers this by parsing generic declarations from source.
Name-Based Binding Resolution
The Renderable stores a Map<String, Integer> of buffer name → binding slot, extracted from Slang reflection after compilation. The renderer binds UBOs by name:
1
2
draw.bindUniformBuffer(r.bindingFor("CameraBuffer", 1), cameraUbo);
draw.bindUniformBuffer(r.bindingFor("EngineBuffer", 0), engineUbo);
Fallback values match the register(bN) conventions. Reflection provides the actual slots when available.
BufferWriter / BufferReader
Single utility for all GPU buffer serialization. Used by StructLayout, material upload, and any mapped buffer.
1
2
3
4
BufferWriter.write(segment, offset, 3.14f); // float → 4 bytes
BufferWriter.write(segment, offset, new Vec3(1,2,3)); // Vec3 → 12 bytes
BufferWriter.write(segment, offset, myMat4); // Mat4 → 64 bytes, column-major
BufferWriter.writeTextureHandle(segment, offset, h); // uint64 → 8 bytes (bindless)
Matrix convention
Mat4in Java: row-major (m00, m01, m02, m03 is first row)- GPU upload: column-major (
Mat4.writeGpu()transposes) - Slang source: row-major convention (
defaultMatrixLayoutMode = ROW_MAJOR) - GLSL output: Slang converts mul() order, emits
layout(column_major) - Slang handles the math conversion,
BufferWriterhandles the memory layout
Compilation Pipeline
- Entity has
MaterialDatawithshaderHint()and property keys - Renderer calls
shaderManager.getShaderWithMaterial(hint, keys)lazily on first render - ShaderManager loads
shaders/<hint>.slang prependParamBlocks()generates all global param blocks + material param blockcompileWithAutoSpecialize()compiles via native Slang FFM:- Builds type map from registry + material
- Parses generic declarations from source
- Specializes with concrete types
- Links and extracts GLSL + reflection
- Creates pipeline, caches by
hint + sorted key names
Shader cache key
"PBR_albedoColor_emissive_metallic_opacity_roughness" — shader hint + sorted property key names. Different materials with same hint and keys share the same compiled pipeline.
Techniques (Design — Not Yet Implemented)
Rendering strategies (fog, output mode, shading model) as typed material properties:
1
2
3
4
5
6
7
8
9
10
11
// Technique interfaces
sealed interface FogTechnique {}
record LinearFog(float start, float end, Vec3 color) implements FogTechnique {}
record ExponentialFog(float density, Vec3 color) implements FogTechnique {}
record NoFog() implements FogTechnique {}
// Set on material via PropertyKey
static final PropertyKey<FogTechnique> FOG = PropertyKey.of("fog", FogTechnique.class);
var mat = MaterialData.pbr(...)
.set(FOG, new LinearFog(10f, 100f, new Vec3(0.7f, 0.8f, 0.9f)));
The engine would:
- See a record-typed property on the material
- Generate Slang struct from the concrete record
- Generate
typealias Fog = LinearFogImpl; - Upload record fields to material UBO via
BufferWriter
Shader uses the typealias:
Fog fog;
color = fog.apply(color, depth);
Open design question
How to distinguish technique-typed properties from regular data:
- Marker interface (
implements Technique) — explicit opt-in - Any record — treat all non-primitive record-typed properties as techniques
- Both approaches work; marker is more explicit but adds classification burden
Scope
Techniques can be:
- Per-material: different objects use different techniques (fog, shading model)
- Global defaults:
renderer.setTechnique("Fog", "NoFog")— material can override
Different technique implementations can have different parameters (LinearFog has start/end, ExponentialFog has density). The concrete record type determines both the typealias and the data layout. Type safety is enforced by Java’s type system through PropertyKey<FogTechnique>.