Node System
The Node System is a lightweight shader language that allows you to write shaders in TypeScript, supporting both WebGL and WebGPU. With Three.js Shading Language (TSL) compatible syntax, you can express complex GLSL/WGSL code using intuitive JavaScript constructs.
Why Node System?
The Reality of Shader Development
Shader development presents significant barriers for most developers. Learning GLSL/WGSL, navigating WebGL/WebGPU API differences, and dealing with lack of type safety are just a few obstacles that hinder creativity.
Node System fundamentally solves these problems. Through TypeScript's type system and method chaining, it transforms shader development into an intuitive and safe experience.
Realizing Mathematical Aesthetics
The essence of shaders is mathematics. The combination of vector operations, trigonometric functions, and interpolation functions creates beautiful visual representations. Node System faithfully expresses mathematical concepts in code, providing an environment where thoughts can be directly visualized.
Core Concepts
Type System
- Scalar Types
- Vector Types
- Matrix Types
const x = float(1.5) // Floating point number
const y = int(2) // Integer
const z = bool(true) // Boolean
const position = vec3(1, 2, 3) // 3D vector
const color = vec4(1, 0, 0, 1) // RGBA color
const uv = vec2(0.5, 0.5) // UV coordinates
const transform = mat4() // 4x4 transformation matrix
const rotation = mat3() // 3x3 rotation matrix
Operator Chaining
Natural expression following mathematical intuition.
const result = vec3(1, 2, 3).add(vec3(4, 5, 6)).mul(2).normalize()
Swizzling
Access to vector components.
const pos = vec3(1, 2, 3)
const xy = pos.xy // vec2(1, 2)
const rgb = pos.rgb // Interpreted as color components
Functions and Scope
Function Definition
const boxSDF = Fn((args) => {
const [p, size] = args
const d = abs(p).sub(size).toVar()
const inside = max(d.x, max(d.y, d.z)).min(float(0))
const outside = max(d, vec3(0)).length()
return inside.add(outside)
})
Variable Management
const shader = Fn(() => {
// Variable declaration
const localVar = vec3(1, 2, 3).toVar('myVariable')
// Assignment
localVar.assign(vec3(4, 5, 6))
// Swizzle assignment
localVar.y = float(10)
return localVar
})
Control Flow
Conditional Statements
- If-Else
- ElseIf
If(condition, () => {
// Processing for true case
}).Else(() => {
// Processing for false case
})
If(x.lessThan(float(0)), () => {
result.assign(vec3(1, 0, 0)) // Red
})
.ElseIf(x.equal(float(0)), () => {
result.assign(vec3(0, 1, 0)) // Green
})
.Else(() => {
result.assign(vec3(0, 0, 1)) // Blue
})
Loops
Loop(int(10), ({ i }) => {
sum.assign(sum.add(i))
})
Mathematical Functions
Basic Functions
- Trigonometric
- Interpolation
- Vector
const wave = sin(time).mul(0.5).add(0.5)
const circular = vec2(cos(angle), sin(angle))
const smooth = smoothstep(float(0), float(1), t)
const linear = mix(colorA, colorB, t)
const unit = normalize(vector)
const distance = length(positionA.sub(positionB))
const reflection = reflect(direction, normal)
Practical Patterns
Distance Functions
Define distance functions that form the foundation of ray marching.
const sphereSDF = Fn((args) => {
const [p, radius] = args
return length(p).sub(radius)
})
const boxSDF = Fn((args) => {
const [p, size] = args
const d = abs(p).sub(size)
return max(d, vec3(0))
.length()
.add(min(max(d.x, max(d.y, d.z)), float(0)))
})
Normal Calculation
Normal computation using numerical differentiation.
const calculateNormal = Fn((args) => {
const [p, sdf] = args
const eps = float(0.001)
const dx = sdf(p.add(vec3(eps, 0, 0))).sub(sdf(p.sub(vec3(eps, 0, 0))))
const dy = sdf(p.add(vec3(0, eps, 0))).sub(sdf(p.sub(vec3(0, eps, 0))))
const dz = sdf(p.add(vec3(0, 0, eps))).sub(sdf(p.sub(vec3(0, 0, eps))))
return normalize(vec3(dx, dy, dz))
})
Ray Marching
Basic ray marching algorithm.
const rayMarch = Fn((args) => {
const [origin, direction] = args
const totalDistance = float(0).toVar()
const position = origin.toVar()
Loop(int(100), ({ i }) => {
const distance = sceneSDF(position)
If(distance.lessThan(float(0.001)), () => {
// Hit processing
return position
})
position.assign(position.add(direction.mul(distance)))
totalDistance.assign(totalDistance.add(distance))
})
return position
})
WebGL/WebGPU Support
Automatic Conversion
The same code automatically converts to both WebGL (GLSL) and WebGPU (WGSL).
- WGSL Output
- GLSL Output
fn main() -> vec4f {
let uv: vec2f = position.xy / iResolution;
let color: vec3f = vec3f(uv, sin(iTime) * 0.5 + 0.5);
return vec4f(color, 1.0);
}
void main() {
vec2 uv = gl_FragCoord.xy / iResolution.xy;
vec3 color = vec3(uv, sin(iTime) * 0.5 + 0.5);
fragColor = vec4(color, 1.0);
}
Browser Compatibility
Automatically falls back to WebGL/GLSL for browsers without WebGPU support, while using WGSL for WebGPU-capable browsers.
Application Examples
Procedural Textures
const noiseTexture = Fn(() => {
const uv = position.xy.div(iResolution).toVar()
const noise1 = sin(uv.x.mul(10)).mul(sin(uv.y.mul(10)))
const noise2 = sin(uv.x.mul(20).add(iTime)).mul(0.5)
const finalNoise = noise1.add(noise2).mul(0.5).add(0.5)
return vec4(finalNoise, finalNoise, finalNoise, 1)
})
Fractal Generation
const mandelbrot = Fn(() => {
const uv = position.xy.div(iResolution).mul(4).sub(vec2(2))
const c = uv.toVar()
const z = vec2(0).toVar()
const iterations = int(0).toVar()
Loop(int(100), ({ i }) => {
If(length(z).greaterThan(float(2)), () => {
// Divergence test
return
})
z.assign(vec2(z.x.mul(z.x).sub(z.y.mul(z.y)).add(c.x), z.x.mul(z.y).mul(2).add(c.y)))
iterations.assign(i)
})
const color = iterations.div(float(100))
return vec4(color, color, color, 1)
})