[!NOTE] This is one of 190 standalone projects, maintained as part of the @thi.ng/umbrella monorepo and anti-framework.
🚀 Please help me to work full-time on these projects by sponsoring me on GitHub. Thank you! ❤️
DSL to define shader code in TypeScript and cross-compile to GLSL, JS and other targets.
(A 2h 40m long video tutorial (live stream) introducing this package: Building a shader graph editor (WebGL, shader AST transpiler, UI))
Example shader running in plain JS & Canvas 2D context, cross-compiled JS/GLSL outputs on the right
Both an embedded DSL and IR format to encourage and define modular shader code directly in TypeScript and then cross-compile to different languages. Using GLSL types and semantics as starting point, the DSL is used as an assembly language to define a partially (as much as possible / feasible) type checked AST, incl. custom, user defined functions, higher-order functions, inline functions, automatic vector-scalar overrides, most of GLSL ES 3.0 built-ins, arg checking, and function return type inference.
Code generation can be done for individual expressions or entire shader programs, incl. call graph analysis and topological re-ordering of all transitively called functions (other than built-ins). Currently only GLSL & JS are supported as target (see code gen packages below), but custom code generators can be easily added. Once more details have been ironed out, we aim to support Houdini VEX (in-progress), WASM, WHLSL for WebGPU in the near future as well.
Comparison of the raymarch shader example (link further below), cross compiled to both GLSL (left) and JavaScript (right). Difference image of both results in the center.
Larger version - The same raymarching example compiled to Houdini VEX and used as "Point Wrangle" to displace a grid geometry (using only the depth value of the raymarching step).
In addition to the code generation aspects, this package also provides a form of "standard library", pure functions for common shader & GPGPU use cases and which can be used as syntax sugar and / or higher level building blocks for your own shaders. So far, this includes various math utils, lighting models, fog equations, SDF primitives / operators, raymarching helpers etc. These functions are distributed in as separate package.
During one of the thi.ng live streams in 2020 I started building a shader graph editor which I subsequently developed further and which is online at: shadergraph.thi.ng. All shader ops are entirely based on functionality provided by shader-ast and its support packages.
Some small example projects documented as short clips (images are links to videos on Twitter):
See the project dashboard for current status. The TL;DR list...
STABLE - used in production
Search or submit any issues for this package
yarn add @thi.ng/shader-ast
ES module import:
<script type="module" src="https://cdn.skypack.dev/@thi.ng/shader-ast"></script>
For Node.js REPL:
const shaderAst = await import("@thi.ng/shader-ast");
Package sizes (brotli'd, pre-treeshake): ESM: 4.91 KB
Several projects in this repo's /examples directory are using this package:
Screenshot | Description | Live demo | Source |
---|---|---|---|
ASCII art raymarching with thi.ng/shader-ast & thi.ng/text-canvas | Demo | Source | |
2D canvas shader emulation | Demo | Source | |
Shader-AST meta-programming techniques for animated function plots | Demo | Source | |
Evolutionary shader generation using genetic programming | Demo | Source | |
HOF shader procedural noise function composition | Demo | Source | |
WebGL & JS canvas2D raymarch shader cross-compilation | Demo | Source | |
WebGL & JS canvas 2D SDF | Demo | Source | |
WebGL & Canvas2D textured tunnel shader | Demo | Source | |
Fork-join worker-based raymarch renderer (JS/CPU only) | Demo | Source | |
Minimal shader graph developed during livestream #2 | Demo | Source | |
Entity Component System w/ 100k 3D particles | Demo | Source | |
WebGL cube maps with async texture loading | Demo | Source | |
WebGL instancing, animated grid | Demo | Source | |
WebGL MSDF text rendering & particle system | Demo | Source | |
Minimal multi-pass / GPGPU example | Demo | Source | |
Shadertoy-like WebGL setup | Demo | Source | |
WebGL screenspace ambient occlusion | Demo | Source | |
Interactively drawing to & reading from a WebGL offscreen render texture | Demo | Source |
float
(32 bit)int
(signed 32bit)uint
(unsigned 32bit)bool
vec2
(f32)vec3
(f32)vec4
(f32)ivec2
(i32)ivec3
(i32)ivec4
(i32)uvec2
(u32)uvec3
(u32)uvec4
(u32)bvec2
(bool)bvec3
(bool)bvec4
(bool)mat2
(2x2, f32)mat3
(3x3, f32)mat4
(4x4, f32)sampler2D
sampler3D
samplerCube
sampler2DShadow
samplerCubeShadow
isampler2D
isampler3D
isamplerCube
usampler2D
usampler3D
usamplerCube
The following operators are all applied componentwise, take 2 arguments and support mixed vector / scalar args. One of the operands can also be a plain JS number, but not both. The resulting AST nodes will contain type hints to simplify later code generation tasks:
add
div
mul
sub
If one of the operands is a vector or matrix and the other scalar, the result will be vector/matrix.
If a plain (unwrapped) JS number value is given for one of the operands, it will be automatically wrapped in a suitable type, based on that of the other operand. E.g. In add(vec2(1), 10)
, the 10
will be cast to float(10)
. In add(ivec2(1), 10)
, it will be cast to int(10)
...
mul
has exceptional semantics for matrix * matrix
, matrix * vector
and vector * matrix
operands (all perform correct linear algebraic multiplications). See GLSL ES language reference.
All comparisons result in a bool
term (i.e. Term<"bool">
)
AST | GLSL |
---|---|
lt | < |
lte | <= |
eq | == |
neq | != |
gte | >= |
gt | > |
AST | GLSL |
---|---|
and | && |
or | ` |
not | ! |
AST | GLSL |
---|---|
bitand | & |
bitor | ` |
bitxor | ^ |
bitnot | ~ |
Only available for vector types - to extract, , optionally reordered, components and / or to expand, shorten vectors. If only one component is selected, the result will be a scalar, else a vector of the specified length.
$(vec3(1,2,3), "zyx")
=> vec3(3,2,1)
Syntax sugar for single component lookups:
$x(v)
(same as $(v, "x")
)$y(v)
$z(v)
$w(v)
$xy(v)
$xyz(v)
Swizzle patterns are type checked in the editor (and at compile time), i.e.
$(vec2(1,2), "xyx")
=> ok (results in equivalent of vec3(1,2,1)
)$(vec2(1,2), "xyz")
=> illegal (since z
is not available in a vec2
)index
indexMat
sym
arraySym
assign
input
output
uniform
brk
cont
discard
ifThen(test, truthy, falsy)
ternary(test, truthy, falsy)
forLoop(sym, testFn, iterFn, bodyFn)
whileLoop(test, body)
The most common set of GLSL ES 3.0 builtins are supported. See /builtin for reference.
Functions can be created via defn
and can accept 0-8 typed arguments. Functions declared in this manner can be called like any other TS/JS function and will return a function call AST node with the supplied args.
import {
clamp, defn, dot, float, ret, sym, ternary,
type FloatSym
} from "@thi.ng/shader-ast";
import { fit1101 } from "@thi.ng/shader-ast-stdlib";
// example based on @thi.ng/shader-ast-stdlib
/**
* Computes Lambert term, optionally using Half-Lambertian,
* if `half` is true.
*
* https://developer.valvesoftware.com/wiki/Half_Lambert
*
* @param surfNormal vec3
* @param lightDir vec3
* @param half bool
*/
const lambert = defn(
// return type
"float",
// function name
"lambert",
// args (incl. optional name and other opts)
["vec3", "vec3", "bool"],
// function body
(n, ldir, bidir) => {
// pre-declare local var
let d: FloatSym;
// function body is array of AST nodes
return [
// initialize local using expr given to `sym()`
(d = sym(dot(n, ldir))),
// return statement
ret(
ternary(
bidir,
fit1101(d),
// also see clamp01() in stdlib
clamp(d, float(0), float(1))
)
)
];
}
);
When defn
is called, the function body will be checked for correct return types. Additionally a call graph for the function is generated to ensure the code generator later emits all dependent functions in the correct order.
Since defn
returns a standard TS/JS function, all arguments will be automatically type checked at call sites (in TypeScript only).
Function argument lists are given as arrays, with each item either:
"float"
[type, name?, opts?]
, e.g. ["vec2", "bar", { q: "out" }]
If no name is specified, an auto-generated one will be used. Generally, this is preferable, since these names are only used for code generation purposes and in most cases only need to be machine readable...
The body function (last arg given to defn
), is called with instantiated, typed symbols representing each arg and can use any name within that function (also as shown in the above example).
See SymOpts
interface in /api/syms.ts for more details about the options object...
If no function local variables are required and/or inlining is desired, vanilla TS/JS functions can be used to produce a partial AST, which is then inserted at the call site:
import { div, mul, sin, type FloatTerm } from "@thi.ng/shader-ast";
/**
* Inline function. Computes sinc(kx).
*
* https://en.wikipedia.org/wiki/Sinc_function
*
* @param x
* @param k
*/
const sinc = (x: FloatTerm, k: FloatTerm) =>
div(sin(mul(x,k)), mul(x, k));
Performance tip for INLINE functions only: Since the FloatTerm
type (or similarly any other XXXTerm
type) refers to any expression evaluating to a "float"
, in some cases (like this sinc()
example) it might be better to only accept FloatSym
arguments, since this ensures the arg expressions are not causing duplicate evaluation. For example:
import { float, length, mul, vec3 } from "@thi.ng/shader-ast";
// (sinc() function defined above)
sinc(length(mul(vec3(1,2,3), 100)), float(10));
...will be expanded to:
div(
sin(mul(length(mul(vec3(1,2,3), 100)),k)),
mul(length(mul(vec3(1,2,3), 100)), k)
);
...which is not desirable.
If, however, the inline function asks for FloatSym
args, the caller is forced to supply variables and so is also responsible to pre-define them... Alternatively, the function could be re-defined via defn
to avoid such issues altogether (but then causes an additional function call at runtime - nothing comes for free!).
input
output
uniform
program([...decls, ...functions])
Currently, an AST can be compiled into the following languages:
See @thi.ng/shader-ast-glsl for further details.
import { GLSLVersion, targetGLSL } from "@thi.ng/shader-ast-glsl";
// create codegen w/ options (defaults shown)
const glsl = targetGLSL({
version: GLSLVersion.GLES_300,
versionPragma: true,
type: "fs"
});
console.log(glsl(lambert))
See @thi.ng/shader-ast-js for further details.
import { targetJS } from "@thi.ng/shader-ast-js";
const js = targetJS();
console.log(js(lambert))
Depending on intended target environment, the following packages can be used to execute shader-ast trees/programs:
walk
allChildren
scopeChildren
See @thi.ng/shader-ast-optimize for AST optimization strategies.
If this project contributes to an academic publication, please cite it as:
@misc{thing-shader-ast,
title = "@thi.ng/shader-ast",
author = "Karsten Schmidt and others",
note = "https://thi.ng/shader-ast",
year = 2019
}
© 2019 - 2024 Karsten Schmidt // Apache License 2.0
Generated using TypeDoc