| Lux Type | SPIR-V | Notes |
|---|---|---|
scalar |
OpTypeFloat 32 |
Always f32 |
int / uint |
OpTypeInt 32 |
Signed / unsigned |
bool |
OpTypeBool |
|
vec2 / vec3 / vec4 |
OpTypeVector |
Float vectors |
ivec2/3/4 / uvec2/3/4 |
OpTypeVector |
Integer vectors |
mat2 / mat3 / mat4 |
OpTypeMatrix |
Square column-major matrices |
mat4x3 |
OpTypeMatrix |
Non-square: 4 columns of 3-component vectors (used by RT object_to_world) |
sampler2d |
OpTypeSampledImage |
Combined image sampler |
samplerCube |
OpTypeSampledImage (Cube) |
Cubemap image sampler |
sampler2DArray |
OpTypeSampledImage (2D, Arrayed) |
2D texture array sampler (shadow maps) |
samplerCubeArray |
OpTypeSampledImage (Cube, Arrayed) |
Cubemap array sampler (point light shadows) |
storage_image |
OpTypeImage (sampled=0) |
Read/write storage image (see Storage Images) |
acceleration_structure |
OpTypeAccelerationStructureKHR |
RT top-level acceleration structure |
type strict Foo = vec3 |
Same as base type | Compile-time type safety, zero SPIR-V cost |
vertex {
in position: vec3; // auto location=0
in normal: vec3; // auto location=1
out frag_normal: vec3; // auto location=0
uniform MVP { // auto set=0, binding=0
model: mat4,
view: mat4,
projection: mat4,
}
push Camera { view_pos: vec3 }
fn main() {
builtin_position = projection * view * model * vec4(position, 1.0);
}
}
fragment {
in frag_normal: vec3;
out color: vec4;
sampler2d albedo_tex; // auto set=0, binding=0
fn main() {
color = vec4(normalize(frag_normal), 1.0);
}
}
Lux v0.2 introduces high-level material declarations that expand to shader stages:
import brdf;
// Geometry: vertex layout + transform + outputs
geometry StandardMesh {
position: vec3, normal: vec3, uv: vec2,
transform: MVP { model: mat4, view: mat4, projection: mat4, }
outputs {
world_pos: (model * vec4(position, 1.0)).xyz,
world_normal: normalize((model * vec4(normal, 0.0)).xyz),
frag_uv: uv,
clip_pos: projection * view * model * vec4(position, 1.0),
}
}
// Surface: material properties + BRDF
surface TexturedPBR {
sampler2d albedo_tex,
brdf: pbr(sample(albedo_tex, frag_uv).xyz, 0.5, 0.0),
}
// Pipeline: wires geometry to surface, compiler generates stages
pipeline PBRForward {
geometry: StandardMesh,
surface: TexturedPBR,
}
For complex PBR pipelines, use layers [...] instead of brdf: — one surface generates both forward and RT shaders. Illumination is declared separately in a lighting block:
import brdf;
import color;
import ibl;
surface GltfPBR {
sampler2d base_color_tex,
layers [
base(albedo: srgb_to_linear(sample(base_color_tex, uv).xyz),
roughness: sample(mr_tex, uv).y, metallic: sample(mr_tex, uv).z),
normal_map(map: sample(normal_tex, uv).xyz),
emission(color: srgb_to_linear(sample(emissive_tex, uv).xyz)),
]
}
lighting SceneLighting {
samplerCube env_specular,
samplerCube env_irradiance,
sampler2d brdf_lut,
properties Light {
light_dir: vec3 = vec3(0.0, -1.0, 0.0),
view_pos: vec3 = vec3(0.0, 0.0, 3.0),
},
layers [
directional(direction: Light.light_dir,
color: vec3(1.0, 0.98, 0.95)),
ibl(specular_map: env_specular, irradiance_map: env_irradiance,
brdf_lut: brdf_lut),
]
}
pipeline GltfForward {
geometry: StandardMesh,
surface: GltfPBR,
lighting: SceneLighting,
schedule: HighQuality,
}
pipeline GltfRT {
mode: raytrace,
surface: GltfPBR,
lighting: SceneLighting,
environment: HDRSky,
schedule: HighQuality,
}
The lighting block separates illumination configuration (light sources, IBL samplers) from material response (surface layers). Pipelines reference both via surface: and lighting:. Pipelines without a lighting: member fall back to legacy hardcoded behavior for backward compatibility.
Layers are listed bottom-to-top (base first, outermost last). The compiler generates energy-conserving evaluation with sample() auto-rewritten to sample_lod() for RT.
Define custom layers as annotated functions. The first 4 parameters (base color, normal, view, light) are provided automatically; remaining parameters come from layer arguments:
import toon;
@layer
fn cartoon(base: vec3, n: vec3, v: vec3, l: vec3,
bands: scalar, rim_power: scalar, rim_color: vec3) -> vec3 {
let n_dot_l: scalar = max(dot(n, l), 0.0);
let quantized: scalar = floor(n_dot_l * bands + 0.5) / bands;
let cel: vec3 = base * quantized;
let n_dot_v: scalar = max(dot(n, v), 0.0);
let rim: scalar = pow(1.0 - n_dot_v, rim_power);
return cel + rim_color * rim;
}
surface ToonSurface {
sampler2d albedo_tex,
layers [
base(albedo: sample(albedo_tex, uv).xyz, roughness: 0.8, metallic: 0.0),
cartoon(bands: 4.0, rim_power: 3.0, rim_color: vec3(0.3, 0.3, 0.5)),
]
}
Custom layers are validated at compile time (signature, return type, no name collision with built-in layers) and inserted in declaration order after built-in layers, before emission.
Built-in layer types:
| Layer | Block | Purpose | Parameters |
|---|---|---|---|
base |
surface | PBR direct lighting | albedo, roughness, metallic |
normal_map |
surface | TBN normal perturbation | map |
emission |
surface | Additive emission | color |
coat |
surface | Clearcoat | factor, roughness |
sheen |
surface | Sheen/fuzz | color, roughness |
transmission |
surface | Volumetric transmission | factor, ior, thickness |
directional |
lighting | Directional light source | direction, color |
ibl |
lighting | Image-based lighting | specular_map, irradiance_map, brdf_lut |
multi_light |
lighting | N-light evaluation with shadows | (reads from LightData + ShadowEntry SSBOs) |
| custom | surface | User-defined @layer function |
function-specific |
Declare boolean feature flags and use if guards to conditionally include declarations:
features {
has_normal_map: bool,
has_clearcoat: bool,
}
geometry StandardMesh {
position: vec3,
normal: vec3,
tangent: vec4 if has_normal_map,
// ...
}
surface GltfPBR {
sampler2d normal_tex if has_normal_map,
sampler2d clearcoat_tex if has_clearcoat,
layers [
base(albedo: ..., roughness: ..., metallic: ...),
normal_map(map: sample(normal_tex, uv).xyz) if has_normal_map,
]
}
Guards work on any comma-separated declaration item: surface samplers, layers, geometry fields, output bindings, schedule members, and pipeline members.
Module-level if blocks group multiple declarations:
if has_clearcoat {
import clearcoat;
}
Feature expressions support &&, ||, !, and parentheses:
sheen(color: ...) if (has_sheen && !has_clearcoat),
Features are resolved at compile time — disabled items are stripped before expansion. The generated SPIR-V contains no dead code.
The properties block declares an abstract data source for runtime material parameters inside a surface declaration. Instead of hardcoding BRDF inputs, you declare typed fields with defaults -- the compiler generates a UBO (std140 layout) and reflection JSON so engines can fill in values at runtime from glTF materials or any other source.
import brdf;
import color;
surface GltfPBR {
sampler2d base_color_tex,
sampler2d metallic_roughness_tex,
properties Material {
base_color_factor: vec4 = vec4(1.0, 1.0, 1.0, 1.0),
emissive_factor: vec3 = vec3(0.0),
metallic_factor: scalar = 1.0,
roughness_factor: scalar = 1.0,
emissive_strength: scalar = 1.0,
ior: scalar = 1.5,
clearcoat_factor: scalar = 0.0,
clearcoat_roughness_factor: scalar = 0.0,
sheen_roughness_factor: scalar = 0.0,
transmission_factor: scalar = 0.0,
sheen_color_factor: vec3 = vec3(0.0),
},
layers [
base(albedo: srgb_to_linear(sample(base_color_tex, uv).xyz)
* Material.base_color_factor.xyz,
roughness: sample(metallic_roughness_tex, uv).y
* Material.roughness_factor,
metallic: sample(metallic_roughness_tex, uv).z
* Material.metallic_factor),
]
}
Fields are accessed with qualified syntax (Material.roughness_factor) and can appear anywhere in layer expressions -- multiplied with texture samples, used directly, or composed with other fields. Swizzling works as expected (Material.base_color_factor.xyz).
The compiler generates:
- A UBO with std140 layout placed in the fragment shader (and closest-hit / mesh stages for RT and mesh pipelines)
- Reflection JSON (
*.lux.json) that lists each field's name, type, byte offset, and default value -- engines read this to build the buffer without hardcoding struct layouts
All three engines (Python/wgpu, C++/Vulkan, Rust/ash) use the reflection JSON to wire glTF material properties into the generated UBO automatically. When loading a glTF model, each engine reads the material's pbrMetallicRoughness, clearcoat, sheen, transmission, and emissive parameters and fills the corresponding UBO fields. Fields not present in the glTF material fall back to the defaults declared in the properties block.
Prevent mixing coordinate spaces at compile time with zero runtime cost:
type strict WorldPos = vec3;
type strict ViewPos = vec3;
type strict WorldNormal = vec3;
fn transform(p: WorldPos) -> ViewPos {
return (view * vec4(p, 1.0)).xyz;
}
// Compile error: cannot pass ViewPos where WorldPos is expected
let v: ViewPos = transform(my_view_pos); // error!
Builtins like normalize, dot, length accept semantic types transparently — they operate on the underlying base type.
Decouple what to render from how to render it:
surface CopperMetal {
brdf: pbr(vec3(0.95, 0.64, 0.54), 0.3, 0.9),
}
schedule HighQuality {
fresnel: schlick,
distribution: ggx,
geometry_term: smith_ggx,
tonemap: aces,
}
schedule Mobile {
distribution: ggx_fast,
geometry_term: smith_ggx_fast,
tonemap: reinhard,
}
pipeline DesktopForward {
geometry: StandardMesh,
surface: CopperMetal,
schedule: HighQuality,
}
Declarative RT pipelines expand surfaces to raygen + closest_hit + miss stages:
import brdf;
surface CopperMetal {
brdf: pbr(vec3(0.95, 0.64, 0.54), 0.3, 0.9),
}
environment GradientSky {
color: mix(vec3(1.0), vec3(0.5, 0.7, 1.0), 0.5),
}
pipeline PathTracer {
mode: raytrace,
surface: CopperMetal,
environment: GradientSky,
max_bounces: 1,
}
Or write RT stages manually with full control (see Ray Tracing Stages below).
| Variable | Type | SPIR-V BuiltIn | Valid Stages |
|---|---|---|---|
launch_id |
uvec3 |
LaunchIdKHR |
raygen |
launch_size |
uvec3 |
LaunchSizeKHR |
raygen |
world_ray_origin |
vec3 |
WorldRayOriginKHR |
closest_hit, any_hit, miss, intersection |
world_ray_direction |
vec3 |
WorldRayDirectionKHR |
closest_hit, any_hit, miss, intersection |
ray_tmin |
scalar |
RayTminKHR |
closest_hit, any_hit, miss, intersection |
ray_tmax |
scalar |
RayTmaxKHR |
closest_hit, any_hit, miss, intersection |
hit_t |
scalar |
RayTmaxKHR |
closest_hit, any_hit |
instance_id |
int |
InstanceCustomIndexKHR |
closest_hit, any_hit, intersection |
primitive_id |
int |
PrimitiveId |
closest_hit, any_hit, intersection |
hit_kind |
uint |
HitKindKHR |
closest_hit, any_hit |
object_to_world |
mat4x3 |
ObjectToWorldKHR |
closest_hit, any_hit, intersection |
world_to_object |
mat4x3 |
WorldToObjectKHR |
closest_hit, any_hit, intersection |
incoming_ray_flags |
uint |
IncomingRayFlagsKHR |
closest_hit, any_hit, miss, intersection |
geometry_index |
int |
RayGeometryIndexKHR |
closest_hit, any_hit, intersection |
Mesh shaders provide a third rendering pipeline mode alongside rasterization and ray tracing. Instead of the traditional vertex/index pipeline, mesh shaders operate on meshlets -- small clusters of triangles processed by workgroups -- enabling GPU-driven rendering with fine-grained culling.
The surface declaration works unchanged; only the geometry processing stage changes:
pipeline GltfMesh {
mode: mesh_shader,
geometry: StandardMesh,
surface: GltfPBR,
schedule: HighQuality,
}
The GltfMesh pipeline lives alongside GltfForward and GltfRT in the same gltf_pbr_layered.lux file. Compile with --define to set hardware-matched limits:
luxc gltf_pbr_layered.lux --pipeline GltfMesh --features has_emission --define max_vertices=64 --define max_primitives=124 --define workgroup_size=32The approach is data-driven: at runtime the engine queries hardware capabilities (maxMeshOutputVertices, maxMeshOutputPrimitives, maxMeshWorkGroupSize), builds meshlets that respect those limits, and compiles the shader with matching --define parameters. This ensures optimal meshlet sizing across different GPU vendors without source changes.
Mesh shaders require VK_EXT_mesh_shader and are supported in the C++ and Rust engines only -- Python/wgpu does not expose mesh shader support.
The Metal backend provides a native macOS renderer that runs the same Lux-compiled SPIR-V shaders as the Vulkan engines, transpiled to MSL at runtime via SPIRV-Cross.
Architecture:
.lux source
→ luxc compiler → .spv (SPIR-V binary)
→ SPIRV-Cross (CompilerMSL) → MSL source
→ MTL::Device::newLibrary() → MTLLibrary → MTLFunction
→ MTL::RenderPipelineState / MTL::MeshRenderPipelineState
The transpilation happens once at pipeline creation. SPIRV-Cross maps Vulkan descriptor bindings (set, binding) to Metal buffer/texture/sampler indices, and the transpiler records this mapping so renderers bind resources at the correct Metal indices.
Key implementation details:
| Aspect | Approach |
|---|---|
| Windowing | GLFW + ObjC++ bridge (metal_bridge.mm) attaches a CAMetalLayer to the GLFW NSWindow |
| Shader transpilation | SPIRV-Cross CompilerMSL with MSL 3.0, discrete bindings, force_native_arrays |
| Push constants | Detected via SPIR-V binary scan (OpVariable + PushConstant storage class), mapped to [[buffer(N)]] |
| Vertex data | Buffer index 0 reserved for vertex stage-in; UBOs/SSBOs assigned sequentially |
| Textures | Auto-assigned by SPIRV-Cross; combined image-samplers produce matching texture + sampler indices |
| Depth | MTL::PixelFormatDepth32Float, compare less, write enabled |
| Coordinate system | Vulkan Y-flip projection matrix reused as-is; MTL::WindingClockwise compensates inverted winding |
| Mesh shaders | Metal 3 MeshRenderPipelineDescriptor with [[mesh]] + [[fragment]] functions; one threadgroup per meshlet |
| SoA vertex buffers | Positions and normals uploaded as vec4 (16-byte stride) to match std430 SSBO layout |
Requirements:
- macOS 13+ (Ventura) with Metal 3 for mesh shaders, macOS 12+ for raster-only
- Apple Silicon or AMD GPU with Metal support
- Metal-cpp headers vendored in
playground_cpp/deps/metal-cpp/(download from Apple) - SPIRV-Cross (fetched automatically by CMake via FetchContent)
Storage images provide read/write image access in compute and raygen shaders. Declared with storage_image followed by a name and format:
compute {
storage_image output_img: rgba8;
fn main() {
let gid: uvec3 = global_invocation_id;
let color: vec4 = vec4(1.0, 0.0, 0.0, 1.0);
image_store(output_img, gid.xy, color);
}
}
Supported formats: rgba8, rgba16f, rgba32f, rg16f, rg32f, r16f, r32f, r32i, r32ui, r11g11b10f.
First-class 3D Gaussian splatting via the splat declaration. One block generates a complete 3-stage pipeline: compute preprocess (projection, covariance, SH evaluation, sort keys), instanced vertex shader (sorted quad rendering), and fragment shader (2D Gaussian evaluation with alpha compositing).
splat GaussianCloud {
sh_degree: 0, // SH bands: 0 (DC only), 1, 2, or 3
kernel: ellipse, // Gaussian kernel shape
color_space: srgb, // Output color space
sort: camera_distance, // Sorting method
alpha_cutoff: 0.004, // Minimum alpha threshold (OpKill below this)
}
pipeline SplatViewer {
mode: gaussian_splat,
splat: GaussianCloud,
}
The compiler generates:
- Compute shader (workgroup 256): transforms splat positions to screen space, computes 3D→2D covariance matrices via Jacobian projection, evaluates spherical harmonics for view-dependent color, and writes sort keys for depth ordering
- Vertex shader: reads sorted splat data and emits instanced screen-space quads (6 vertices per splat) sized by the 2D Gaussian radius
- Fragment shader: evaluates the 2D Gaussian kernel, applies alpha cutoff with
discard(OpKill), and outputs premultiplied alpha for back-to-front compositing
Push constants provide camera matrices, screen dimensions, focal lengths, splat count, and SH degree. SSBOs carry per-splat input data (positions, rotations, scales, opacities, SH coefficients) and intermediate projected data.
SH degrees 0–3 are supported (1/4/9/16 coefficient buffers). The stdlib module gaussian.lux provides quaternion-to-rotation, 3D/2D covariance, Gaussian evaluation, and quad radius helpers.
Reflection JSON includes a gaussian_splatting section with SH degree, kernel type, color space, and input/output buffer lists for engine integration.
All three rendering engines (C++/Vulkan, Rust/ash, Python/numpy) support Gaussian splat rendering with CPU-side depth sorting and instanced draw.
The --bindless flag enables uber-shaders with runtime descriptor arrays, eliminating per-material descriptor switching:
luxc gltf_pbr_layered.lux --pipeline GltfForward --bindlessIn bindless mode, texture samplers become runtime-indexed arrays with nonuniformEXT decoration. Materials are stored in an SSBO, and per-geometry gl_GeometryIndexEXT selects the material in RT pipelines. Requires descriptor indexing support (C++ and Rust engines only).
Texture sampling uses sample_bindless(tex_array, index, uv) and sample_bindless_lod(tex_array, index, uv, lod) instead of the standard sample().
Override automatic binding assignment with an explicit binding number:
fragment {
@binding(3)
sampler2d my_texture;
// ...
}
The compiler normally assigns bindings by declaration order. @binding(N) overrides this for specific resources when engine integration requires fixed binding slots.
Lux provides first-class debug features that compile to zero instructions in release builds. Use --debug to enable:
import debug;
// Semantic types: prevent mixing coordinate spaces at compile time
type strict WorldPos = vec3;
type strict WorldNormal = vec3;
fragment {
in world_normal: vec3;
out color: vec4;
push Material { roughness: scalar, metallic: scalar }
fn main() {
let n: WorldNormal = normalize(world_normal);
// Runtime assertions (prints failure, continues — no shader kill)
assert(roughness >= 0.0, "roughness must be non-negative");
assert(roughness <= 1.0, "roughness exceeds valid range");
// Runtime value inspection (visible in Vulkan validation layer output)
debug_print("roughness={} metallic={}", roughness, metallic);
// Entire block stripped in release — zero instructions, not just skipped
@[debug] {
debug_print("normal=({}, {}, {})", n.x, n.y, n.z);
assert(!any_nan(n), "NaN in normal!");
// Stdlib debug visualization helpers
let viz: vec3 = debug_normal(n);
let heat: vec3 = debug_heatmap(roughness);
}
color = vec4(n * 0.5 + vec3(0.5), 1.0);
}
}
# Debug mode: all instrumentation active
luxc debug_features_demo.lux --debug -o shadercache/
# Release mode: debug_print, assert, @[debug] blocks all stripped
luxc debug_features_demo.lux -o shadercache/
# Keep debug_print/assert in release (no full debug overhead)
luxc debug_features_demo.lux --debug-print -o shadercache/
# Assert kills fragment invocation on failure
luxc debug_features_demo.lux --debug --assert-kill -o shadercache/
# Static analysis: warns about unguarded division, sqrt, normalize, pow, log
luxc debug_features_demo.lux --warn-nan -o shadercache/Step through shader code on the CPU with no GPU required. Inspect every variable, detect NaN/Inf sources, and simulate different pixels with custom inputs.
# Interactive debugging (gdb-style REPL)
python -m luxc examples/debug_playground.lux --debug-run --stage fragment
# Batch mode: detect NaN/Inf and report exact source line
python -m luxc examples/debug_playground.lux --debug-run --stage fragment --batch --check-nan
# {
# "status": "completed",
# "nan_detected": true,
# "nan_events": [{"line": 55, "variable": "dir", "operation": "let", ...}],
# "output": {"type": "vec4", "value": [0.403, 0.381, 0.345, 1.0]}
# }
# Trace every intermediate value
python -m luxc examples/debug_playground.lux --debug-run --stage fragment --batch --dump-vars
# Breakpoint inspection — dump full scope at specific lines
python -m luxc examples/debug_playground.lux --debug-run --stage fragment --batch --break 100 --break 113 --dump-at-break
# Debug a specific pixel (auto-computes uv, position, normal from coords)
python -m luxc examples/debug_playground.lux --debug-run --stage fragment --pixel 960,540 --batch --check-nan
# Quick inline overrides (no JSON file needed)
python -m luxc examples/debug_playground.lux --debug-run --stage fragment --set roughness=0.01 --set metallic=1.0 --batch
# Custom inputs from JSON file
python -m luxc examples/debug_playground.lux --debug-run --stage fragment --input examples/debug_playground_inputs.json --batch --check-nanInteractive session:
$ python -m luxc examples/debug_playground.lux --debug-run --stage fragment
lux-debug> start
Stopped at line 87
> 87 | let albedo: vec3 = sample(albedo_tex, uv).rgb;
lux-debug> step
+ albedo = vec3(0.800, 0.800, 0.800) (vec3)
Stopped at line 88
> 88 | let n: vec3 = normalize(world_normal);
lux-debug> step
+ n = vec3(0.000, 0.000, 1.000) (vec3)
lux-debug> break 113
lux-debug> continue
Hit breakpoint 1 at line 113
lux-debug> print d
d = 0.141471 (scalar)
lux-debug> locals
albedo = vec3(0.800, ...) roughness = 0.500 n_dot_l = 0.333 ...
lux-debug> continue
Output: vec4(0.403, 0.381, 0.345, 1.000)
Commands: start, run, step, next, continue, finish, break <line>, delete <id>, print <var>, locals, list, source, output, quit
See debugger-guide.md for the full reference and debug-session-transcript.md for a complete debugging walkthrough.
Automatically classify variables as fp16-safe or fp32-required:
# Analyze only — print report, no code changes
luxc shader.lux --auto-type=report
# Emit OpDecorate RelaxedPrecision on fp16-safe variables
luxc shader.lux --auto-type=relaxedThree-signal analysis architecture:
- Dynamic range tracing — runs the AST interpreter with 60+ diverse inputs to profile actual value ranges
- Static interval analysis — forward dataflow propagation of [lo, hi] intervals through 30+ expression types
- Name/usage heuristics — pattern matching (e.g.,
position-> fp32,roughness-> fp16)
Conservative-union-with-veto logic: any doubt defaults to fp32. Source: luxc/autotype/.
Or write RT stages manually with full control:
raygen {
acceleration_structure tlas;
ray_payload payload: vec4;
fn main() {
let origin: vec3 = vec3(0.0, 0.0, 2.0);
let direction: vec3 = normalize(vec3(0.0, 0.0, -1.0));
trace_ray(tlas, 0, 255, 0, 0, 0, origin, 0.001, direction, 1000.0, 0);
}
}
closest_hit {
ray_payload payload: vec4;
hit_attribute attribs: vec2;
fn main() {
let shade: scalar = 1.0 / (1.0 + hit_t * hit_t * 0.1);
payload = vec4(shade, shade, shade, 1.0);
}
}
miss {
ray_payload payload: vec4;
fn main() {
let t: scalar = world_ray_direction.y * 0.5 + 0.5;
payload = vec4(mix(vec3(1.0), vec3(0.5, 0.7, 1.0), vec3(t)), 1.0);
}
}
Or write mesh/task stages manually:
task {
task_payload payload: MeshletPayload;
fn main() {
// Frustum cull meshlets on the task shader side
payload.meshlet_id = workgroup_id.x;
emit_mesh_tasks(1, 1, 1);
}
}
mesh {
mesh_output vertices: 64;
mesh_output primitives: 126;
fn main() {
set_mesh_outputs(3, 1);
// Emit a single triangle from meshlet data
}
}
Mark functions with @differentiable to auto-generate gradient functions:
@differentiable
fn energy(x: scalar) -> scalar {
return x * x + sin(x);
}
fragment {
in param: scalar;
out color: vec4;
fn main() {
let val: scalar = energy(param);
let grad: scalar = energy_d_x(param); // auto-generated
color = vec4(val, grad, 0.0, 1.0);
}
}
The compiler generates energy_d_x using forward-mode differentiation rules for all supported operations.
Import modules with import <name>; — functions are inlined at the call site.
| Module | Functions | Description |
|---|---|---|
brdf |
30+ | Fresnel (Schlick, conductor), NDF (GGX, Charlie, anisotropic), Geometry (Smith GGX, height-correlated), Diffuse (Lambert, Oren-Nayar, Burley), Composite (PBR, glTF PBR), Clearcoat, Sheen, Transmission BTDF, Volumetric refraction (Walter 2007), Iridescence (Belcour 2017), Dispersion (Abbe number), Volume attenuation |
ibl |
8 | Specular/diffuse IBL contributions, Fresnel-roughness, GGX importance sampling, Hammersley sequence, combined glTF PBR+IBL with multi-scattering energy compensation |
sdf |
18 | Sphere, box, round box, plane, torus, cylinder, capsule, union, intersection, subtraction, smooth union/subtraction, translate, scale, repeat, round, onion, elongate |
noise |
13 | Hash functions (2D/3D), value noise, gradient/Perlin noise, FBM (4/6 octaves, 2D/3D, loop-unrolled), Voronoi 2D |
color |
5 | linear-to-sRGB, sRGB-to-linear, luminance, Reinhard tonemap, ACES tonemap |
colorspace |
8 | RGB-to-HSV, HSV-to-RGB, contrast, saturation, hue shift, brightness, gamma correction |
texture |
11 | TBN normal perturbation, normal unpacking, triplanar projection (weights, UVs, blending), parallax offset, UV rotation, UV tiling |
lighting |
7 | Distance/spot attenuation, evaluate directional/point/spot lights, unified light evaluation, branchless light direction selection |
shadow |
4 | Basic shadow sampling, PCF4 shadow filtering, cascade selection, shadow UV computation |
toon |
1 | Cartoon cel-shading with quantized NdotL + rim lighting (@layer function) |
compositing |
2 | IBL multi-scattering (Fdez-Aguera 2019), unified compose_pbr_layers (transmission, sheen, coat, IBL, emission) |
pbr_pipeline |
1 | pbr_shade() — single-call PBR orchestration (direct lighting + IBL + all optional layers) |
gaussian |
6 | SH constants (degrees 0–3), quaternion-to-rotation, 3D/2D covariance, Gaussian 2D eval, quad radius |
openpbr |
18 | OpenPBR Surface v1.1: F82-tint conductor Fresnel, energy-preserving Oren-Nayar (EON), coat darkening/absorption/roughening, fuzz BRDF, specular IOR modulation, thin-film, direct lighting, full composition (9 layers), fast variants for mobile/low-end |
debug |
5 | Normal visualization, depth grayscale, scalar heatmap, index coloring, UV checkerboard |
GLSL.std.450 (44 builtins + 2 aliases): normalize, reflect, refract, pow, sin, cos, tan, asin, acos, atan, atan2 (2-arg atan), exp, exp2, log, log2, sqrt, inversesqrt, abs, sign, floor, ceil, fract, round, trunc, min, max, clamp, mix, step, smoothstep, length, distance, cross, fma, mod, radians, degrees, sinh, cosh, tanh, asinh, acosh, atanh, faceforward, determinant, inverse
Native SPIR-V: dot (OpDot), any_nan (OpIsNan + OpAny), any_inf (OpIsInf + OpAny)
Texture sampling:
| Function | SPIR-V | Description |
|---|---|---|
sample(tex, uv) |
OpImageSampleImplicitLod |
Standard texture sample |
sample_lod(tex, uv, lod) |
OpImageSampleExplicitLod |
Explicit mip level (IBL, RT) |
sample_grad(tex, uv, ddx, ddy) |
OpImageSampleExplicitLod |
Explicit gradients |
sample_compare(tex, coords, ref) |
OpImageSampleDrefExplicitLod |
Shadow/depth comparison |
sample_array(tex, uv, layer) |
OpImageSampleExplicitLod |
Array layer sampling |
sample_bindless(array, index, uv) |
OpImageSampleImplicitLod |
Bindless descriptor array |
sample_bindless_lod(array, index, uv, lod) |
OpImageSampleExplicitLod |
Bindless with explicit LOD |
Image query:
| Function | SPIR-V | Returns | Description |
|---|---|---|---|
texture_size(tex, lod) |
OpImageQuerySizeLod |
ivec2 |
Texture dimensions at mip level |
texture_levels(tex) |
OpImageQueryLevels |
int |
Number of mipmap levels |
image_size(img) |
OpImageQuerySize |
ivec2 |
Storage image dimensions |
Ray tracing: trace_ray, report_intersection, execute_callable, ignore_intersection, terminate_ray
Mesh shader: set_mesh_outputs(vert_count, prim_count), emit_mesh_tasks(gx, gy, gz)
Compute: barrier(), atomic_add, atomic_min, atomic_max, atomic_and, atomic_or, atomic_xor, atomic_exchange, atomic_compare_exchange, atomic_load, atomic_store, image_store
| Variable | Type | SPIR-V BuiltIn | Valid Stages |
|---|---|---|---|
local_invocation_id |
uvec3 |
LocalInvocationId |
compute, mesh, task |
local_invocation_index |
uint |
LocalInvocationIndex |
compute, mesh, task |
workgroup_id |
uvec3 |
WorkgroupId |
compute, mesh, task |
num_workgroups |
uvec3 |
NumWorkgroups |
compute, mesh, task |
global_invocation_id |
uvec3 |
GlobalInvocationId |
compute, mesh, task |
fn fresnel_schlick(cos_theta: scalar, f0: vec3) -> vec3 {
return f0 + (vec3(1.0) - f0) * pow(1.0 - cos_theta, 5.0);
}
Module-level functions can be called from any stage block. They are inlined at the call site.
const PI: scalar = 3.14159265;
import brdf; // loads luxc/stdlib/brdf.lux
import noise; // loads luxc/stdlib/noise.lux
import openpbr; // enables OpenPBR Surface v1.1 material model
The compiler searches luxc/stdlib/ first, then the source file's directory.
Imports are transitive: import openpbr; also brings in brdf and compositing.
import openpbr; activates the OpenPBR Surface v1.1 material model (Adobe/ASWF standard).
The surface expander automatically detects the import and generates physically-based composition
using energy-conserving layer stacking instead of the default glTF PBR path.
Supported layers: base, specular, coat, fuzz, thin_film, transmission, emission, subsurface, normal_map
import openpbr;
surface CarPaint {
sampler2d albedo_tex,
layers [
base(color: sample(albedo_tex, uv).xyz, metalness: 0.9, diffuse_roughness: 0.0),
specular(weight: 1.0, color: vec3(1.0), roughness: 0.15, ior: 1.5),
coat(weight: 1.0, roughness: 0.02, ior: 1.6, color: vec3(1.0), darkening: 1.0),
thin_film(weight: 0.6, thickness: 0.5, ior: 1.4),
emission(luminance: 0.0, color: vec3(1.0)),
]
}
Key features:
- F82-tint metal Fresnel (Lazanyi-Szirmay-Kalos conductor model)
- Energy-preserving Oren-Nayar diffuse (EON/Fujii 2024)
- Coat darkening with internal reflection compensation
- Specular weight IOR modulation
- Fuzz BRDF for velvet/fabric
- Thin-film iridescence
- Transmission with volume absorption
- Bindless uber-shader support (
--bindlesswith extended SSBO:_FLAG_OPENPBR, fuzz/thin_film/subsurface/anisotropy flags) - Schedule-based quality tiers:
diffuse_model(eon/lambert/burley),fuzz_model(charlie),specular_fresnel(exact/schlick),coat_fresnel(exact/schlick) - Fast variants:
openpbr_direct_fast(Lambert + Schlick) andopenpbr_compose_fast(no fuzz, simplified coat) for mobile/low-end
Schedule example (quality tiers):
schedule OpenPBRMobile {
diffuse_model: lambert,
specular_fresnel: schlick,
coat_fresnel: schlick,
tonemap: aces,
}
pipeline MobilePBR {
geometry: StandardMesh,
surface: CarPaint,
schedule: OpenPBRMobile,
}