User Pixel Shaders¶
EdenSpark supports custom pixel shaders authored in Daslang. A pixel shader is a GPU program that runs once per fragment and determines the final color output for that pixel, with full access to interpolated vertex attributes, material properties, and global engine state.
Quick Start¶
PBR shader — the surface is shaded by the scene lighting (directional light, point lights, global illumination):
require engine.render.shader_dsl
var {
@color tint = float3(0.2, 0.8, 1.0) // editable color picker in material inspector
speed = 2.0
}
[pixel_shader]
def hologram_effect(inp : PbrInput) {
let n = noise(inp.worldPos)
let pulse = sin(g_Time * speed) * 0.5 + 0.5
return PbrOutput(
albedo = tint * n,
emission = tint * pulse,
emissionStrength = pulse * 0.5,
metalness = 0.0,
roughness = 1.0,
ao = 1.0
)
}
Unlit shader — no lighting, direct color output:
require engine.render.shader_dsl
var @color color = float3(1.0, 0.5, 0.0)
[pixel_shader]
def flat_color(inp : UnlitInput) {
return UnlitOutput(
color = color,
alpha = 1.0
)
}
Files and Hot-Reload¶
Create a
foo.shaderfile anywhere in the project folder.Assign the shader to a material via the Material inspector by selecting it in the shader slot — you can replace the built-in shader on any material or create a new
.materialfile that references it.Hot-reload is triggered when you save from within the editor (Ctrl+S). Saving from an external editor may not trigger a rebuild.
Material Properties¶
Module-level var declarations become per-material properties editable in
the inspector and overridable per mesh instance.
var {
speed = 2.0 // float
offset = float2(0.5, 0.0) // float2
@color tint = float3(1.0, 1.0, 1.0) // float3 with color picker
blend = float4(1.0, 0.0, 0.0, 1.0) // float4
albedoTex : Sampler2D = Sampler2D("assets/default.png") // texture
}
Type |
Notes |
|---|---|
|
Single scalar |
|
Two-component vector; default as |
|
Three-component vector; default as |
|
Four-component vector; default as |
|
Texture reference; default as |
|
Same storage as |
Defaults must be constant literals. References to other module-level let
constants are also allowed.
Note
Only variables declared in the same module as the [pixel_shader]
function become material properties. Variables from other modules cannot
be accessed.
Shader Types¶
The shader type is determined by the function signature: the parameter type selects what geometry data is available as input, and the return type selects which outputs are written.
PBR Shader (PbrInput → PbrOutput)¶
Physically-based rendering — the surface is lit by the engine’s full lighting
pipeline: directional light, point lights, spot lights, and global illumination.
The lighting model applies NdotL and BRDF on top of albedo automatically.
Do not pre-multiply your own lighting into albedo — use emission
for self-lit additive effects instead.
PbrInput fields — access as inp.fieldName:
Field |
Type |
Description |
|---|---|---|
|
float2 |
Texture UV coordinates |
|
float3 |
World-space position of the current fragment |
|
float3 |
World-space surface normal (normalized) |
|
float3 |
Direction from the fragment toward the camera (world-space, normalized) |
|
float3 |
Object-space position of the current fragment |
|
float3 |
Object-space surface normal (normalized) |
|
float2 |
Screen-space UV coordinates [0, 1] for the current fragment. Used with
|
PbrOutput fields — all optional; specify only what you need:
Field |
Type |
Default |
Description |
|---|---|---|---|
|
float3 |
float3(0) |
Base color (diffuse reflectance). Modulated by scene lighting. |
|
float |
1.0 |
Opacity [0, 1] |
|
float |
0.0 |
Alpha-test threshold. Fragments below are discarded. |
|
float |
0.0 |
PBR metalness [0, 1] |
|
float |
1.0 |
PBR roughness [0, 1] |
|
float3 |
float3(0) |
Additive emissive color. Not affected by lighting. |
|
float |
1.0 |
Multiplier on |
|
float3 |
float3(0, 0, 1) |
Tangent-space normal. Decode from a texture with |
|
float |
1.0 |
Ambient occlusion [0, 1] |
Unlit Shader (UnlitInput → UnlitOutput)¶
No lighting — direct color output. Use for HUD quads, UI meshes, additive particle effects, and any surface that must be lighting-independent.
UnlitInput has the same fields as PbrInput, including screenPos.
UnlitOutput fields:
Field |
Type |
Default |
Description |
|---|---|---|---|
|
float3 |
float3(0) |
Output color |
|
float |
1.0 |
Opacity [0, 1] |
|
float |
0.0 |
Alpha-test threshold |
Global Inputs¶
Available directly by name in any shader, without using the input struct:
Name |
Type |
Description |
|---|---|---|
|
float |
Global time in seconds since the application started |
|
float3 |
Main directional light direction — pointing toward the sun, world-space, normalized |
|
float3 |
Camera position in world space |
|
float3 |
Camera forward direction, world-space, normalized |
|
float3 |
Camera right direction, world-space, normalized |
|
float3 |
Camera up direction, world-space, normalized |
Built-in Functions¶
All functions are overloaded for float, float2, float3, and
float4 unless noted otherwise. Operations are component-wise.
Arithmetic¶
Signature |
Description |
|---|---|
|
Absolute value: |x| |
|
Component-wise minimum |
|
Component-wise maximum |
|
Clamp each component to [lo, hi] |
|
Clamp to [0, 1] |
|
Fractional part: x − floor(x) |
|
Round down to the nearest integer |
|
Round up to the nearest integer |
|
Floating-point remainder ( |
|
Multiply-add: a × b + c. Fused into a single instruction automatically. |
|
Linear interpolation: a + t × (b − a). |
|
Step function: 0 if x < edge, 1 otherwise |
|
Smooth Hermite interpolation between lo and hi |
|
1 − x |
|
Remap x from inRange to outRange. inRange and outRange are |
Transcendentals¶
All component-wise:
Function |
Description |
|---|---|
|
Square root |
|
Power: x^y |
|
Sine (radians) |
|
Cosine (radians) |
|
Arc-tangent of y/x |
|
Natural logarithm |
|
Natural exponentiation: e^x |
|
Sign: −1, 0, or +1 |
Vector Operations¶
Dimension-specific:
Signature |
Description |
|---|---|
|
Dot product (float2/3/4 × float2/3/4 → float) |
|
Cross product (float3 × float3 → float3) |
|
Vector length (float2/3/4 → float) |
|
Normalize to unit length (float2/3/4 → same type) |
Lighting¶
- fresnel(power: float; worldNorm: float3; viewDir: float3): float¶
Schlick-style Fresnel approximation. Returns values close to 0 at the surface center and close to 1 at glancing angles. Increase
powerto tighten the rim. Passinp.worldNormalandinp.viewDirdirectly:let rim = fresnel(3.0, inp.worldNormal, inp.viewDir) return PbrOutput(emission = float3(rim))
Texture Sampling¶
- tex2d(sampler: Sampler2D; uv: float2): float4¶
Sample a 2D texture at the given UV coordinates. Declare the texture as a material property and pass it here:
var albedoTex : Sampler2D = Sampler2D("assets/base_color.png") [pixel_shader] def textured(inp : PbrInput) { let c = tex2d(albedoTex, inp.uv) return PbrOutput(albedo = c.xyz, alpha = c.w) }
Use
.rgba,.rgb,.aswizzles to extract channels from the returnedfloat4.
Noise¶
- noise(pos: float2): float
2D Perlin noise. Returns values in [0, 1].
- noise(pos: float3): float
3D Perlin noise. Returns values in [0, 1].
Normal Map Utility¶
- unpack_normal(sample: float4): float3¶
Decode a tangent-space normal from a standard RGB normal-map texture sample. Input is the
float4returned bytex2d; output is afloat3in [−1, 1]:var normalTex : Sampler2D = Sampler2D("assets/normal.png") [pixel_shader] def with_normalmap(inp : PbrInput) { let n = unpack_normal(tex2d(normalTex, inp.uv)) return PbrOutput(normalMap = n) }
Scene Sampling¶
These functions read from the already-rendered scene buffer and depth buffer.
They require inp.screenPos (screen-space UV) as the sampling coordinate.
Both are PerPixel — they cannot be hoisted to the CPU tier.
- sample_scene_color(uv: float2): float3¶
Sample the scene color buffer at the given screen-space UV. Used for refraction and glass-like effects — distort
inp.screenPosby the surface normal before sampling:let offset = inp.worldNormal.xz * refractStrength let sceneColor = sample_scene_color(inp.screenPos + offset) return PbrOutput(albedo = sceneColor, alpha = 0.8)
- sample_scene_depth(uv: float2): float¶
Sample the scene depth buffer at the given screen-space UV. Returns the eye depth — distance from the camera in world units — of the opaque geometry already rendered behind the current surface.
Subtract the fragment’s own depth to get intersection thickness:
let surfaceDepth = length(inp.worldPos - g_CameraPosition) let sceneDepth = sample_scene_depth(inp.screenPos) let thickness = clamp((sceneDepth - surfaceDepth) / softDepth, 0.0, 1.0) return PbrOutput(albedo = lerp(shallowColor, deepColor, thickness), alpha = lerp(0.1, opacity, thickness))
Helper Functions¶
Plain def functions in the same .shader module are inlined at
compile time. Use them to factor out repeated expressions or build utility
sub-routines:
require engine.render.shader_dsl
var waveScale = 5.0
def wave_octave(p : float3; scale : float; t : float) : float {
return noise(p * scale + float3(t, t * 0.3, t * 0.7))
}
[pixel_shader]
def water(inp : PbrInput) {
let t = g_Time * 0.6
let n1 = wave_octave(inp.worldPos, waveScale, t)
let n2 = wave_octave(inp.worldPos, waveScale * 2.1, t)
let n = n1 * 0.6 + n2 * 0.4
return PbrOutput(albedo = lerp(float3(0.0, 0.15, 0.35),
float3(0.2, 0.6, 0.8), n))
}
Rules for helper functions:
Only one
[pixel_shader]entry point per module — helper functions must not carry the[pixel_shader]annotation.The same language subset applies inside helper functions: no
varlocals, no control flow, same supported types.Module-level
varproperties are accessible from helper functions.Helper functions are fully inlined — their arguments are just named sub-expressions; there is no call overhead.
Loop Unrolling¶
for loops with a compile-time-known iteration count are fully supported.
The compiler unrolls them into straight-line code before building the shader
graph — there is no branching or looping in the final program.
Four loop forms are accepted:
Literal array — the iterator is each element value directly:
var acc = float3(0)
for (off in [float2(-s, 0.0), float2(0.0, 0.0), float2(s, 0.0)]) {
acc += tex2d(albedoTex, inp.uv + off).xyz
}
Multi-iterator — two parallel arrays unrolled in lockstep:
var acc = float3(0)
for (off, weight in [float2(-s, 0.0), float2(0.0, 0.0), float2(s, 0.0)],
[0.25, 0.5, 0.25]) {
acc += tex2d(albedoTex, inp.uv + off).xyz * weight
}
range(N) — integer index 0 .. N-1; convert to float with float(i):
var acc = float3(0)
for (i in range(3)) {
let o = blurSize * float(i)
acc += tex2d(albedoTex, inp.uv + float2(o, o)).xyz
}
urange(N) — unsigned integer index, same pattern:
var acc = float3(0)
for (i in urange(3u)) {
let off = float2(float(i) * blurSize, 0.0)
acc += tex2d(albedoTex, inp.uv + off).xyz
}
break and continue are supported inside unrolled loops. After
unrolling, break becomes a persistent mask that suppresses all later
copies; continue becomes a per-copy mask that skips only that iteration’s
accumulation.
Note
The loop count must be a literal constant — range(3),
urange(3u), or a fixed-size array literal. A loop whose count
depends on a material property or a shader input cannot be unrolled and
will be rejected at compile time.
The accumulator variable must be declared as var (mutable local) and
updated with += or a similar compound assignment. This is the one
exception to the “no var locals” rule — loop accumulators are resolved
into a chain of operation nodes in the final graph.
Language Subset¶
The shader DSL is a strict subset of Daslang. The [pixel_shader]
macro walks the AST at compile time and rejects unsupported constructs.
Supported:
Arithmetic operators
+-*/(unary-→ Negate node)Swizzle access:
v.xyz,v.zyx,v.xxxx, etc.Field reads on the input struct:
inp.uv,inp.worldPos, etc.letlocal declarations — type must befloat/float2/float3/float4, value must be initializedvaraccumulator locals inside unrolledforloops (see above)Float and vector literals (
1.0,float3(0.2, 0.8, 1.0))Integer literals — auto-promoted to float
Module-level
vardeclarations → material propertiesAll built-in functions listed above
if/else— converted to a select node; works anywhere in shader or helper codeforloops with a compile-time-constant count (see Loop Unrolling above)
Not supported:
Mutable
varlocals other than loop accumulators — useletwhileloopsTernary operator
?:Heap allocations (
new)Strings, arrays, tables, variants
Pointer types and
unsafebool,int,uintlocals (integer literals are auto-promoted to float)Multiple
[pixel_shader]entry points in one module — each.shaderfile must have exactly one[pixel_shader]function.
See Shader and Shader Graph Compiler for details on how the compiler enforces these restrictions.