wired:shading
Shading programs a script builds as data. A shader graph is a program a
prim draws with in place of its material.
A graph has a fixed shape: every node has a fixed arity and one output, a node reads only nodes before it, and there is no loop, branch or source text. Its cost is bounded by its size, which is why any script may build one.
Limits: at most 128 nodes per network, 16 public inputs and 4 texture samples.
Interface graph
Uses vec2, vec3, color, prim-id, error, document, layer.
graph-value
A value a shader graph's public input can be overridden with.
float(f32) | |
vec2(vec2) | |
vec3(vec3) | |
color(color) |
value-kind
float | |
vec2 | |
vec3 | |
color |
port
A node input. It is a constant, one of the graph's public inputs, or an earlier node in the same network.
const(graph-value) | |
input(u16) | Index into the graph's public-inputs. |
node(u16) | Index into the enclosing network's node list. Must be strictly lower than the node holding this port, which is the whole of why a graph cannot describe a cycle. Never reaches across networks. The two run as different shader stages and share no per-invocation value. |
binary-op
a: port | |
b: port |
lerp-op
a: port | |
b: port | |
t: port |
pow-op
x: port | |
y: port |
atan2-op
y: port | |
x: port |
select-op
cond: port | |
a: port | |
b: port |
clamp-op
x: port | |
low: port | |
high: port |
step-op
edge: port | |
x: port |
smoothstep-op
low: port | |
high: port | |
x: port |
texture-sample-op
uv: port | |
slot: u8 | One of a fixed set of texture bindings, never an open-ended name. |
extract-op
v: port | |
channel: u8 |
combine2-op
x: port | |
y: port |
combine3-op
x: port | |
y: port | |
z: port |
combine4-op
x: port | |
y: port | |
z: port | |
w: port |
convert-op
v: port | |
to: value-kind |
remap-op
x: port | |
from-low: port | |
from-high: port | |
to-low: port | |
to-high: port |
polar-coords-op
uv: port | |
center: port |
rotate-uv-op
uv: port | |
center: port | |
radians: port |
node
One operation in a shading network.
Every kind has a fixed arity and one output, so a graph's cost is a function of how many nodes it has. There is no loop, no general branch, and no way to carry source text.
The zero-arity kinds read shader-stage context and are each legal in one
network or both: uv/world-normal/world-position/vertex-color/
view-direction are surface-only, local-position/local-normal are
displacement-only, and time/instance-random/object-position/
object-scale are legal in either.
uv | |
world-normal | |
world-position | |
vertex-color | |
local-position | |
local-normal | |
time | |
instance-random | A pseudo-random scalar per draw instance, the only way instances sharing a graph differ without a write to each of them. |
object-position | |
object-scale | |
view-direction | |
screen-uv | Where this fragment sits on screen, 0..1 from the top left. |
add(binary-op) | |
sub(binary-op) | |
mul(binary-op) | |
div(binary-op) | |
modulo(binary-op) | |
min(binary-op) | |
max(binary-op) | |
dot(binary-op) | |
cross(binary-op) | |
distance(binary-op) | |
pow(pow-op) | |
atan2(atan2-op) | |
lerp(lerp-op) | |
clamp(clamp-op) | |
step(step-op) | |
smoothstep(smoothstep-op) | |
remap(remap-op) | |
select(select-op) | |
sin(port) | |
cos(port) | |
one-minus(port) | |
abs(port) | |
floor(port) | |
fract(port) | |
saturate(port) | |
sqrt(port) | |
length(port) | |
normalize(port) | |
triangle-wave(port) | |
luminance(port) | |
fresnel(port) | Power exponent on the host's own view term. A graph parameterizes it rather than building one. |
noise(port) | |
texture-sample(texture-sample-op) | |
scene-color(port) | What was already drawn behind this surface, sampled at a screen position. Offsetting that position by the surface's own curvature is what bends the scene behind it, so this is how a graph refracts. Only ever what was drawn *before* this surface. Two surfaces reading it do not see each other, and neither sees anything drawn after. A graph using it is drawn in a later pass, which costs a full-screen copy. |
extract(extract-op) | |
combine2(combine2-op) | |
combine3(combine3-op) | |
combine4(combine4-op) | |
convert(convert-op) | |
polar-coords(polar-coords-op) | |
rotate-uv(rotate-uv-op) |
lit-output
Fed through the lighting pass. Every terminal is optional and falls back to the renderer's own default.
base-color: option<port> | |
emissive: option<port> | |
metallic: option<port> | |
roughness: option<port> | |
normal: option<port> | |
alpha: option<port> | |
alpha-clip-threshold: option<port> | Below this, the fragment is discarded outright. |
specular-transmission: option<port> | How much of what is behind this surface comes through tinted by base-color, 0..1. Above zero the surface is drawn in the transmissive phase, where the host refracts the already-drawn scene behind it by thickness/ior, with depth rejection. |
diffuse-transmission: option<port> | The Lambertian transmitted lobe, lit from behind. Clear glass that only refracts leaves this at zero. |
thickness: option<port> | Meters the refracted ray travels inside the surface before exiting; how far the scene behind appears displaced. |
ior: option<port> | Refractive index. 1.0 is air, 1.33 water, 1.5 glass. |
unlit-output
Written straight out, with no lighting pass.
color: port | |
alpha-clip-threshold: option<port> |
surface-output
lit(lit-output) | |
unlit(unlit-output) |
blend-mode
How a surface composites against what is already drawn. Declared rather than inferred from which terminals are connected.
opaque | |
blend | |
add | |
multiply |
cull-mode
back | |
front | |
none |
surface-graph
The fragment-stage network.
nodes: list<node> | |
output: surface-output | |
blend: blend-mode | |
cull: cull-mode | |
cast-shadows: bool | Whether this surface occludes light. Declared, because blending does not decide shadowing. |
displacement-graph
The vertex-stage network.
position-offset moves a vertex in the prim's own frame, before it is
placed in the world. world-position-offset moves it afterward, and is
the only way to displace along a direction that does not rotate and
scale with the prim.
nodes: list<node> | |
position-offset: option<port> | |
normal-override: option<port> | |
world-position-offset: option<port> |
shader-graph
A whole shading description: shared inputs, a fragment network, and optionally a vertex one.
public-inputs are the defaults. A prim rendering this graph overrides
them per-instance with set-overrides.
public-inputs: list<graph-value> | |
surface: surface-graph | |
displacement: option<displacement-graph> |
set-graph
set-graph: func(document: borrow<document>, prim: prim-id, layer: layer, graph: option<shader-graph>) -> result<_, error>Draws prim with graph in place of its material, or stops on
none, which also drops its overrides.
The host validates and compiles the graph. Prims submitting the same
graph share one compiled program. A prim can instead point at a graph
authored on another prim with the shader-binding relation.
overrides
overrides: func(document: borrow<document>, prim: prim-id) -> list<tuple<u16, graph-value>>The public inputs prim overrides, by index.
set-overrides
set-overrides: func(document: borrow<document>, prim: prim-id, layer: layer, values: list<tuple<u16, graph-value>>) -> result<_, error>Overrides public inputs of the graph prim draws with, by index,
replacing every earlier override in layer. The compiled program is
untouched and still shared.
Source
/// Shading programs a script builds as data. A shader graph is a program a
/// prim draws with in place of its `material`.
///
/// A graph has a fixed shape: every node has a fixed arity and one output,
/// a node reads only nodes before it, and there is no loop, branch or source
/// text. Its cost is bounded by its size, which is why any script may build
/// one.
///
/// Limits: at most 128 nodes per network, 16 public inputs and 4
/// texture samples.
package wired:shading;
interface graph {
use wired:core/math.{vec2, vec3, color};
use wired:core/ids.{prim-id};
use wired:core/error.{error};
use wired:scene/document.{document, layer};
/// A value a shader graph's public input can be overridden with.
variant graph-value {
float(f32),
vec2(vec2),
vec3(vec3),
color(color),
}
enum value-kind {
float,
vec2,
vec3,
color,
}
/// A node input. It is a constant, one of the graph's public inputs, or an
/// earlier node in the same network.
variant port {
const(graph-value),
/// Index into the graph's `public-inputs`.
input(u16),
/// Index into the enclosing network's node list. Must be strictly lower
/// than the node holding this port, which is the whole of why a graph
/// cannot describe a cycle. Never reaches across networks. The two run as
/// different shader stages and share no per-invocation value.
node(u16),
}
record binary-op { a: port, b: port }
record lerp-op { a: port, b: port, t: port }
record pow-op { x: port, y: port }
record atan2-op { y: port, x: port }
record select-op { cond: port, a: port, b: port }
record clamp-op { x: port, low: port, high: port }
record step-op { edge: port, x: port }
record smoothstep-op { low: port, high: port, x: port }
record texture-sample-op {
uv: port,
/// One of a fixed set of texture bindings, never an open-ended name.
slot: u8,
}
record extract-op { v: port, channel: u8 }
record combine2-op { x: port, y: port }
record combine3-op { x: port, y: port, z: port }
record combine4-op { x: port, y: port, z: port, w: port }
record convert-op { v: port, to: value-kind }
record remap-op {
x: port,
from-low: port,
from-high: port,
to-low: port,
to-high: port,
}
record polar-coords-op { uv: port, center: port }
record rotate-uv-op { uv: port, center: port, radians: port }
/// One operation in a shading network.
///
/// Every kind has a fixed arity and one output, so a graph's cost is a
/// function of how many nodes it has. There is no loop, no general branch,
/// and no way to carry source text.
///
/// The zero-arity kinds read shader-stage context and are each legal in one
/// network or both: `uv`/`world-normal`/`world-position`/`vertex-color`/
/// `view-direction` are surface-only, `local-position`/`local-normal` are
/// displacement-only, and `time`/`instance-random`/`object-position`/
/// `object-scale` are legal in either.
variant node {
uv,
world-normal,
world-position,
vertex-color,
local-position,
local-normal,
time,
/// A pseudo-random scalar per draw instance, the only way instances
/// sharing a graph differ without a write to each of them.
instance-random,
object-position,
object-scale,
view-direction,
/// Where this fragment sits on screen, `0..1` from the top left.
screen-uv,
add(binary-op),
sub(binary-op),
mul(binary-op),
div(binary-op),
modulo(binary-op),
min(binary-op),
max(binary-op),
dot(binary-op),
cross(binary-op),
distance(binary-op),
pow(pow-op),
atan2(atan2-op),
lerp(lerp-op),
clamp(clamp-op),
step(step-op),
smoothstep(smoothstep-op),
remap(remap-op),
select(select-op),
sin(port),
cos(port),
one-minus(port),
abs(port),
floor(port),
fract(port),
saturate(port),
sqrt(port),
length(port),
normalize(port),
triangle-wave(port),
luminance(port),
/// Power exponent on the host's own view term. A graph parameterizes it
/// rather than building one.
fresnel(port),
noise(port),
texture-sample(texture-sample-op),
/// What was already drawn behind this surface, sampled at a screen
/// position. Offsetting that position by the surface's own curvature is
/// what bends the scene behind it, so this is how a graph refracts.
///
/// Only ever what was drawn *before* this surface. Two surfaces reading
/// it do not see each other, and neither sees anything drawn after. A
/// graph using it is drawn in a later pass, which costs a full-screen
/// copy.
scene-color(port),
extract(extract-op),
combine2(combine2-op),
combine3(combine3-op),
combine4(combine4-op),
convert(convert-op),
polar-coords(polar-coords-op),
rotate-uv(rotate-uv-op),
}
/// Fed through the lighting pass. Every terminal is optional and falls back
/// to the renderer's own default.
record lit-output {
base-color: option<port>,
emissive: option<port>,
metallic: option<port>,
roughness: option<port>,
normal: option<port>,
alpha: option<port>,
/// Below this, the fragment is discarded outright.
alpha-clip-threshold: option<port>,
/// How much of what is behind this surface comes through tinted by
/// `base-color`, `0..1`. Above zero the surface is drawn in the
/// transmissive phase, where the host refracts the already-drawn scene
/// behind it by `thickness`/`ior`, with depth rejection.
specular-transmission: option<port>,
/// The Lambertian transmitted lobe, lit from behind. Clear glass that
/// only refracts leaves this at zero.
diffuse-transmission: option<port>,
/// Meters the refracted ray travels inside the surface before exiting;
/// how far the scene behind appears displaced.
thickness: option<port>,
/// Refractive index. `1.0` is air, `1.33` water, `1.5` glass.
ior: option<port>,
}
/// Written straight out, with no lighting pass.
record unlit-output {
color: port,
alpha-clip-threshold: option<port>,
}
variant surface-output {
lit(lit-output),
unlit(unlit-output),
}
/// How a surface composites against what is already drawn. Declared rather
/// than inferred from which terminals are connected.
enum blend-mode {
opaque,
blend,
add,
multiply,
}
enum cull-mode {
back,
front,
none,
}
/// The fragment-stage network.
record surface-graph {
nodes: list<node>,
output: surface-output,
blend: blend-mode,
cull: cull-mode,
/// Whether this surface occludes light. Declared, because blending does
/// not decide shadowing.
cast-shadows: bool,
}
/// The vertex-stage network.
///
/// `position-offset` moves a vertex in the prim's own frame, before it is
/// placed in the world. `world-position-offset` moves it afterward, and is
/// the only way to displace along a direction that does not rotate and
/// scale with the prim.
record displacement-graph {
nodes: list<node>,
position-offset: option<port>,
normal-override: option<port>,
world-position-offset: option<port>,
}
/// A whole shading description: shared inputs, a fragment network, and
/// optionally a vertex one.
///
/// `public-inputs` are the defaults. A prim rendering this graph overrides
/// them per-instance with `set-overrides`.
record shader-graph {
public-inputs: list<graph-value>,
surface: surface-graph,
displacement: option<displacement-graph>,
}
/// Draws `prim` with `graph` in place of its material, or stops on
/// `none`, which also drops its overrides.
///
/// The host validates and compiles the graph. Prims submitting the same
/// graph share one compiled program. A prim can instead point at a graph
/// authored on another prim with the `shader-binding` relation.
set-graph: func(
document: borrow<document>,
prim: prim-id,
layer: layer,
graph: option<shader-graph>,
) -> result<_, error>;
/// The public inputs `prim` overrides, by index.
overrides: func(document: borrow<document>, prim: prim-id) -> list<tuple<u16, graph-value>>;
/// Overrides public inputs of the graph `prim` draws with, by index,
/// replacing every earlier override in `layer`. The compiled program is
/// untouched and still shared.
set-overrides: func(
document: borrow<document>,
prim: prim-id,
layer: layer,
values: list<tuple<u16, graph-value>>,
) -> result<_, error>;
}