一键导入
tsl-conversion
Use it when you need to convert a Threejs Material using GLSL shaders to TSL node materials
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use it when you need to convert a Threejs Material using GLSL shaders to TSL node materials
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | TSL conversion |
| description | Use it when you need to convert a Threejs Material using GLSL shaders to TSL node materials |
You're the best TSL developer in the world
Always check the doc! @.cursor/skills/tsl/references/TSL-DOC.md and @.cursor/skills/tsl/references/TSL-WIKI.md
STOP. Read this section first. These will cause errors or warnings.
| ❌ DO NOT USE | ✅ USE INSTEAD |
|---|---|
timerGlobal | time |
timerLocal | time |
timerDelta | deltaTime |
import from 'three/nodes' | import from 'three/tsl' |
import * as THREE from 'three' | import * as THREE from 'three/webgpu' |
oscSine(timerGlobal) | oscSine(time) or oscSine() |
oscSquare(timerGlobal) | oscSquare(time) or oscSquare() |
oscTriangle(timerGlobal) | oscTriangle(time) or oscTriangle() |
oscSawtooth(timerGlobal) | oscSawtooth(time) or oscSawtooth() |
TSL is JavaScript that builds shader node graphs. Code executes at TWO times:
Build time: JavaScript runs, constructs node graph
Run time: Compiled WGSL/GLSL executes on GPU
// BUILD TIME: JavaScript conditional (runs once when shader compiles) if (material.transparent) { return transparent_shader; }
// RUN TIME: TSL conditional (runs every pixel/vertex on GPU) If(value.greaterThan(0.5), () => { result.assign(1.0); });
Please comment anything related to RenderTarget for now, comment it in the node material if used inside the shader to prevent any errors.
Please comment anything related to shadow casting.
Please run npm run dev and check for JS errors after conversion.
To avoid this error: "THREE.TSL: NodeError: THREE.TSL: texture( value ) function expects a valid instance of THREE.Texture()."
texture(...) expects a real THREE.Texture as its first argument (see Lightnings, Stars, etc). Do not do texture( uniform(tex), uv ).LoaderManager.getTexture('name') to guarantee a valid texture (falls back to a 1×1 DataTexture until loaded), then sample with texture(mapTexture, uv()).When a component has its own TSL material in a separate file, use a dedicated folder named after the component:
Boat/
├── index.js
├── BoatMaterials.js # shared boat materials
├── sail/
│ ├── Sail.js # component logic
│ └── SailMaterials.js # TSL sail material
├── splashes/
│ ├── Splashes.js # component logic
│ └── SplashMaterials.js # TSL splash material
└── ...
sail, splashes)Sail.js, Splashes.js)*Materials.js (e.g. SailMaterials.js, SplashMaterials.js)import Sail from './sail/Sail'import * as THREE from 'three/webgpu';
import { Fn, vec3, float, uniform, /* ... */ } from 'three/tsl';
// WRONG: Old path
import { vec3 } from 'three/nodes';
// CORRECT:
import { vec3 } from 'three/tsl';
// WRONG: WebGL renderer with TSL
import * as THREE from 'three';
// CORRECT: WebGPU renderer
import * as THREE from 'three/webgpu';
CRITICAL: Always await renderer.init() before first render or compute.
const renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// REQUIRED before any rendering
await renderer.init();
// Now safe to render/compute
renderer.render(scene, camera);
| Constructor | Input | Output |
|---|---|---|
float(x) | number, node | float |
int(x) | number, node | int |
uint(x) | number, node | uint |
bool(x) | boolean, node | bool |
vec2(x,y) | numbers, nodes, Vector2 | vec2 |
vec3(x,y,z) | numbers, nodes, Vector3, Color | vec3 |
vec4(x,y,z,w) | numbers, nodes, Vector4 | vec4 |
color(hex) | hex number | vec3 |
color(r,g,b) | numbers 0-1 | vec3 |
ivec2/3/4 | integers | signed int vector |
uvec2/3/4 | integers | unsigned int vector |
mat2/3/4 | numbers, Matrix | matrix |
node.toFloat() node.toInt() node.toUint() node.toBool()
node.toVec2() node.toVec3() node.toVec4() node.toColor()
a.add(b) // a + b (supports multiple: a.add(b, c, d))
a.sub(b) // a - b
a.mul(b) // a * b
a.div(b) // a / b
a.mod(b) // a % b
a.negate() // -a
v.assign(x) // v = x
v.addAssign(x) // v += x
v.subAssign(x) // v -= x
v.mulAssign(x) // v *= x
v.divAssign(x) // v /= x
a.equal(b) // a == b
a.notEqual(b) // a != b
a.lessThan(b) // a < b
a.greaterThan(b) // a > b
a.lessThanEqual(b) // a <= b
a.greaterThanEqual(b)// a >= b
a.and(b) a.or(b) a.not() a.xor(b)
a.bitAnd(b) a.bitOr(b) a.bitXor(b) a.bitNot()
a.shiftLeft(n) a.shiftRight(n)
v.x v.y v.z v.w // single component
v.xy v.xyz v.xyzw // multiple components
v.zyx v.bgr // reorder
v.xxx // duplicate
// Aliases: xyzw = rgba = stpq
// WRONG: Cannot modify immutable node
const pos = positionLocal;
pos.y = pos.y.add(1); // ERROR
// CORRECT: Use .toVar() for mutable variable
const pos = positionLocal.toVar();
pos.y.assign(pos.y.add(1)); // OK
const v = expr.toVar(); // mutable variable
const v = expr.toVar('name'); // named mutable variable
const c = expr.toConst(); // inline constant
const p = property('float'); // uninitialized property
// Create
const u = uniform(initialValue);
const u = uniform(new THREE.Color(0xff0000));
const u = uniform(new THREE.Vector3(1, 2, 3));
const u = uniform(0.5);
// Update from JS
u.value = newValue;
// Auto-update callbacks
u.onFrameUpdate(() => value); // once per frame
u.onRenderUpdate(({ camera }) => value); // once per render
u.onObjectUpdate(({ object }) => object.position.y); // per object
// Array parameters
const myFn = Fn(([a, b, c]) => { return a.add(b).mul(c); });
// Object parameters
const myFn = Fn(({ color = vec3(1), intensity = 1.0 }) => {
return color.mul(intensity);
});
// With defaults
const myFn = Fn(([t = time]) => { return t.sin(); });
// Access build context (second param or first if no inputs)
const myFn = Fn(([input], { material, geometry, object, camera }) => {
// JS conditionals here run at BUILD time
if (material.transparent) { return input.mul(0.5); }
return input;
});
myFn(a, b, c) // array params
myFn({ color: red }) // object params
myFn() // use defaults
// OK for simple expressions, no variables/conditionals
const simple = (t) => t.sin().mul(0.5).add(0.5);
// WRONG
if(condition, () => {}) // lowercase 'if' is JavaScript
// CORRECT (inside Fn())
If(a.greaterThan(b), () => {
result.assign(a);
}).ElseIf(a.lessThan(c), () => {
result.assign(c);
}).Else(() => {
result.assign(b);
});
Switch(mode)
.Case(0, () => { out.assign(red); })
.Case(1, () => { out.assign(green); })
.Case(2, 3, () => { out.assign(blue); }) // multiple values
.Default(() => { out.assign(white); });
// NOTE: No fallthrough, implicit break
// Works outside Fn(), returns value directly
const result = select(condition, valueIfTrue, valueIfFalse);
// EQUIVALENT TO: condition ? valueIfTrue : valueIfFalse
// Example: clamp value with custom logic
const clamped = select(x.greaterThan(max), max, x);
step(edge, x) // x < edge ? 0 : 1
mix(a, b, t) // a*(1-t) + b*t
smoothstep(e0, e1, x) // smooth 0→1 transition
clamp(x, min, max) // constrain range
saturate(x) // clamp(x, 0, 1)
// Pattern: conditional selection without branching
mix(valueA, valueB, step(threshold, selector))
// Basic
Loop(count, ({ i }) => { /* i is loop index */ });
// With options
Loop({ start: int(0), end: int(10), type: 'int', condition: '<' }, ({ i }) => {});
// Nested
Loop(10, 5, ({ i, j }) => {});
// Backward
Loop({ start: 10 }, ({ i }) => {}); // counts down
// While-style
Loop(value.lessThan(10), () => { value.addAssign(1); });
// Control
Break(); // exit loop
Continue(); // skip iteration
// All available as: func(x) OR x.func()
// Basic
abs(x) sign(x) floor(x) ceil(x) round(x) trunc(x) fract(x)
mod(x,y) min(x,y) max(x,y) clamp(x,min,max) saturate(x)
// Interpolation
mix(a,b,t) step(edge,x) smoothstep(e0,e1,x)
// Trig
sin(x) cos(x) tan(x) asin(x) acos(x) atan(y,x)
// Exponential
pow(x,y) exp(x) exp2(x) log(x) log2(x) sqrt(x) inverseSqrt(x)
// Vector
length(v) distance(a,b) dot(a,b) cross(a,b) normalize(v)
reflect(I,N) refract(I,N,eta) faceforward(N,I,Nref)
// Derivatives (fragment only)
dFdx(x) dFdy(x) fwidth(x)
// TSL extras (not in GLSL)
oneMinus(x) // 1 - x
negate(x) // -x
saturate(x) // clamp(x, 0, 1)
reciprocal(x) // 1/x
cbrt(x) // cube root
lengthSq(x) // squared length (no sqrt)
difference(x,y) // abs(x - y)
equals(x,y) // x == y
pow2(x) pow3(x) pow4(x) // x^2, x^3, x^4
oscSine(t = time) // sine wave 0→1→0
oscSquare(t = time) // square wave 0/1
oscTriangle(t = time) // triangle wave
oscSawtooth(t = time) // sawtooth wave
blendBurn(a, b) // color burn
blendDodge(a, b) // color dodge
blendScreen(a, b) // screen
blendOverlay(a, b) // overlay
blendColor(a, b) // normal blend
uv() // default UV coordinates (vec2, 0-1)
uv(index) // specific UV channel
matcapUV // matcap texture coords
rotateUV(uv, rotation, center = vec2(0.5)) // rotate UVs
spherizeUV(uv, strength, center = vec2(0.5))// spherical distortion
spritesheetUV(count, uv = uv(), frame = 0) // sprite animation
equirectUV(direction = positionWorldDirection) // equirect mapping
reflectView // reflection in view space
reflectVector // reflection in world space
remap(node, inLow, inHigh, outLow = 0, outHigh = 1) // remap range
remapClamp(node, inLow, inHigh, outLow = 0, outHigh = 1) // remap + clamp
hash(seed) // pseudo-random float [0,1]
range(min, max) // random attribute per instance
// Constant array
const arr = array([vec3(1,0,0), vec3(0,1,0), vec3(0,0,1)]);
arr.element(i) // dynamic index
arr[0] // constant index only
// Uniform array (updatable from JS)
const arr = uniformArray([new THREE.Color(0xff0000)], 'color');
arr.array[0] = new THREE.Color(0x00ff00); // update
// Compute in vertex, interpolate to fragment
const v = varying(expression, 'name');
// Optimize: force vertex computation
const v = vertexStage(expression);
texture(tex) // sample at default UV
texture(tex, uv) // sample at UV
texture(tex, uv, level) // sample with LOD
cubeTexture(tex, direction) // cubemap
triplanarTexture(texX, texY, texZ, scale, pos, normal)
positionGeometry // raw attribute
positionLocal // after skinning/morphing
positionWorld // world space
positionView // camera space
positionWorldDirection // normalized
positionViewDirection // normalized
normalGeometry normalLocal normalView normalWorld
⚠️ SkinnedMesh + NodeMaterial: normalWorld and normalView can be wrong (flat/white lighting). Use normalLocal and transform the light direction to model space instead:
// Sun/light direction with SkinnedMesh – use normalLocal + modelWorldMatrixInverse
const sunDirWorld = normalize(uSunDir.sub(positionWorld))
const sunDirLocal = normalize(modelWorldMatrixInverse.mul(vec4(sunDirWorld, 0)).xyz)
const shadow = dot(normalLocal, sunDirLocal)
Pass uSunDir as uniform(light.position) (reference, not clone) so updates are reflected.
Be careful: choose the right space for your calculation. See @.cursor/skills/tsl/references/TSL-DOC.md and @.cursor/skills/tsl/references/TSL-WIKI.md for full details.
| Node | Space | When to use |
|---|---|---|
| positionLocal | Object/model space (vertex position after skinning/morphing, before model matrix). | Vertex displacement, custom .positionNode, logic that should move with the object. |
| positionWorld | World space (position after modelWorldMatrix). | Lighting (e.g. distance to light), world-space effects, sampling world-aligned textures. |
| positionGeometry | Raw attribute (before any transform). | When you need the original mesh attribute. |
| normalLocal | Object space normal (after skinning, before model matrix). | With SkinnedMesh (prefer over normalWorld), or when you transform light to model space. |
| normalWorld | World space normal. | Standard lighting in fragment (e.g. dot(normalWorld, lightDir)), fresnel, world-aligned effects. |
.positionNode must return a vec3 in local space; the engine applies model/view/projection after. Use positionLocal (or a modification of it) there.positionWorld or modelWorldMatrix.mul(vec4(positionLocal, 1)).xyz.normalWorld / normalView can be wrong; use normalLocal and transform the light direction to model space (see SkinnedMesh note above).cameraPosition cameraNear cameraFar
cameraViewMatrix cameraProjectionMatrix cameraNormalMatrix
screenUV // normalized [0,1]
screenCoordinate // pixels
screenSize // pixels
viewportUV viewport viewportCoordinate viewportSize
time // elapsed time in seconds (float)
deltaTime // time since last frame (float)
modelDirection // vec3
modelViewMatrix // mat4
modelNormalMatrix // mat3
modelWorldMatrix // mat4
modelPosition // vec3
modelScale // vec3
modelViewPosition // vec3
modelWorldMatrixInverse // mat4
uv() uv(index) // texture coordinates
vertexColor() // vertex colors
attribute('name', 'type') // custom attribute
instanceIndex // instance/thread ID (for instancing and compute)
MeshBasicNodeMaterial // unlit, fastest
MeshStandardNodeMaterial // PBR with roughness/metalness
MeshPhysicalNodeMaterial // PBR + clearcoat, transmission, etc.
MeshPhongNodeMaterial // Blinn-Phong shading
MeshLambertNodeMaterial // Lambert diffuse
MeshToonNodeMaterial // cel-shaded
MeshMatcapNodeMaterial // matcap shading
MeshNormalNodeMaterial // visualize normals
SpriteNodeMaterial // billboarded quads
PointsNodeMaterial // point clouds
LineBasicNodeMaterial // solid lines
LineDashedNodeMaterial // dashed lines
.colorNode // vec4 - base color
.opacityNode // float - opacity
.positionNode // vec3 - vertex position (local space)
.normalNode // vec3 - surface normal
.outputNode // vec4 - final output
.fragmentNode // vec4 - replace entire fragment stage
.vertexNode // vec4 - replace entire vertex stage
NodeMaterial options when overriding colorNode: Do not pass options in new NodeMaterial({ ... }) — they are not applied. Create with new NodeMaterial() and set properties after on the instance, e.g. material.transparent = true, material.depthWrite = false, material.blending = AdditiveBlending, material.side = DoubleSide.
.roughnessNode // float
.metalnessNode // float
.emissiveNode // vec3 color
.aoNode // float
.envNode // vec3 color
.clearcoatNode .clearcoatRoughnessNode .clearcoatNormalNode
.sheenNode .transmissionNode .thicknessNode
.iorNode .iridescenceNode .iridescenceThicknessNode
.anisotropyNode .specularColorNode .specularIntensityNode
.positionNode // vec3 - world position of sprite center
.colorNode // vec4 - color and alpha
.scaleNode // float - sprite size (or vec2 for non-uniform)
.rotationNode // float - rotation in radians
.positionNode // vec3 - point position
.colorNode // vec4 - color and alpha
.sizeNode // float - point size in pixels
When converting a material that uses new Points() or point-like billboarded particles to TSL, use SpriteNodeMaterial so billboarding is handled by the material and no billboardToCamera is needed:
PlaneGeometry(1, 1) with setAttribute('instancePosition', new InstancedBufferAttribute(positionArray, 3)) and any other per-instance attributes (e.g. offset, speed).SpriteNodeMaterial with:
positionNode = attribute('instancePosition', 'vec3') (or add displacement, e.g. heightmap, in the same Fn)scaleNode = float(SPRITE_SCALE) or a uniformcolorNode = your fragment logic (use uv() instead of gl_PointCoord)new InstancedMesh(planeGeo, material, count) — no setMatrixAt, no manual instance matrices.billboardToCamera() if callers still invoke it, to avoid breaking changes.Examples in this project: Stars, Lightnings, Waves.
import { Fn, instanceIndex, storage } from 'three/tsl';
// Create storage buffer
const count = 1024;
const array = new Float32Array(count * 4);
const bufferAttribute = new THREE.StorageBufferAttribute(array, 4);
const buffer = storage(bufferAttribute, 'vec4', count);
// Define compute shader
const computeShader = Fn(() => {
const idx = instanceIndex;
const data = buffer.element(idx);
buffer.element(idx).assign(data.mul(2));
})().compute(count);
// Execute
renderer.compute(computeShader); // synchronous (per-frame)
await renderer.computeAsync(computeShader); // async (heavy one-off tasks)
When compute shader output needs to be rendered (e.g., simulations, procedural geometry), use StorageInstancedBufferAttribute with storage() for writing and attribute() for reading.
import { Fn, instanceIndex, storage, attribute, vec4 } from 'three/tsl';
const COUNT = 1000;
// 1. Create typed array and storage attribute
const dataArray = new Float32Array(COUNT * 4);
const dataAttribute = new THREE.StorageInstancedBufferAttribute(dataArray, 4);
// 2. Create storage node for compute shader (write access)
const dataStorage = storage(dataAttribute, 'vec4', COUNT);
// 3. Define compute shader
const computeShader = Fn(() => {
const idx = instanceIndex;
const current = dataStorage.element(idx);
// Modify data...
const newValue = current.xyz.add(vec3(0.01, 0, 0));
dataStorage.element(idx).assign(vec4(newValue, current.w));
})().compute(COUNT);
// 4. Attach attribute to geometry for rendering
const geometry = new THREE.BufferGeometry();
// ... set up base geometry ...
geometry.setAttribute('instanceData', dataAttribute);
// 5. Read in material using attribute()
const material = new THREE.MeshBasicNodeMaterial();
material.positionNode = Fn(() => {
const data = attribute('instanceData', 'vec4');
return positionLocal.add(data.xyz);
})();
// 6. Create mesh
const mesh = new THREE.InstancedMesh(geometry, material, COUNT);
scene.add(mesh);
// 7. Animation loop
await renderer.init();
function animate() {
renderer.compute(computeShader);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
// Modify the underlying array
for (let i = 0; i < COUNT; i++) {
dataArray[i * 4] = Math.random();
}
// Flag for GPU upload
dataAttribute.needsUpdate = true;
import * as THREE from 'three/webgpu';
import { Fn, uniform, vec3, vec4, float, uv, time,
normalWorld, positionWorld, cameraPosition,
mix, pow, dot, normalize, max } from 'three/tsl';
// Uniforms
const baseColor = uniform(new THREE.Color(0x4488ff));
const fresnelPower = uniform(3.0);
// Create material
const material = new THREE.MeshStandardNodeMaterial();
// Custom color with fresnel rim lighting
material.colorNode = Fn(() => {
// Calculate fresnel
const viewDir = normalize(cameraPosition.sub(positionWorld));
const NdotV = max(dot(normalWorld, viewDir), 0.0);
const fresnel = pow(float(1.0).sub(NdotV), fresnelPower);
// Mix base color with white rim
const rimColor = vec3(1.0, 1.0, 1.0);
const finalColor = mix(baseColor, rimColor, fresnel);
return vec4(finalColor, 1.0);
})();
// Animated vertex displacement
material.positionNode = Fn(() => {
const pos = positionLocal.toVar();
const wave = sin(pos.x.mul(4.0).add(time.mul(2.0))).mul(0.1);
pos.y.addAssign(wave);
return pos;
})();
import * as THREE from 'three/webgpu';
import { Fn, instanceIndex, storage, uniform, vec4, float, sin, time } from 'three/tsl';
const COUNT = 10000;
// Storage buffer
const dataArray = new Float32Array(COUNT * 4);
const dataAttribute = new THREE.StorageBufferAttribute(dataArray, 4);
const dataBuffer = storage(dataAttribute, 'vec4', COUNT);
// Uniforms for compute
const speed = uniform(1.0);
// Compute shader
const updateCompute = Fn(() => {
const idx = instanceIndex;
const data = dataBuffer.element(idx);
// Read current values
const position = data.xyz.toVar();
const phase = data.w;
// Update logic
const offset = sin(time.mul(speed).add(phase)).mul(0.1);
position.y.addAssign(offset);
// Write back
dataBuffer.element(idx).assign(vec4(position, phase));
});
const computeNode = updateCompute().compute(COUNT);
// In animation loop:
// renderer.compute(computeNode);
// WRONG
if(condition, () => {})
// CORRECT
If(condition, () => {}) // capital I
// WRONG
const v = vec3(1,2,3);
v.x = 5;
// CORRECT
const v = vec3(1,2,3).toVar();
v.x.assign(5);
// WRONG
sqrt(intValue)
// CORRECT
sqrt(intValue.toFloat())
// WRONG
myUniform = newValue;
// CORRECT
myUniform.value = newValue;
// WRONG
import { vec3 } from 'three/nodes';
import * as THREE from 'three';
// CORRECT
import { vec3 } from 'three/tsl';
import * as THREE from 'three/webgpu';
// WRONG: Using storage() in render material
material.positionNode = storage(attr, 'vec4', count).element(idx).xyz;
// CORRECT: Use attribute() to read in render shaders
geometry.setAttribute('myData', attr);
material.positionNode = attribute('myData', 'vec4').xyz;
// WRONG: Rendering before init
renderer.render(scene, camera);
// CORRECT: Always await init first
await renderer.init();
renderer.render(scene, camera);
const fresnel = Fn(() => {
const NdotV = normalize(cameraPosition.sub(positionWorld)).dot(normalWorld).max(0);
return pow(float(1).sub(NdotV), 5);
});
material.positionNode = Fn(() => {
const p = positionLocal.toVar();
p.y.addAssign(sin(p.x.mul(5).add(time)).mul(0.2));
return p;
})();
material.colorNode = texture(map, uv().add(vec2(time.mul(0.1), 0)));
const result = select(value.greaterThan(0.5), valueA, valueB);
// OR branchless:
const result = mix(valueB, valueA, step(0.5, value));
const t = smoothstep(float(0.0), float(1.0), inputValue);
const colorA = vec3(0.1, 0.2, 0.8);
const colorB = vec3(1.0, 0.5, 0.2);
const gradient = mix(colorA, colorB, t);
// Exponential falloff (good for glow, attenuation)
const falloff = exp(distance.negate().mul(rate));
// Inverse square falloff
const attenuation = float(1.0).div(distance.mul(distance).add(1.0));
const uvCentered = uv().sub(0.5).mul(2.0); // -1 to 1
const dist = length(uvCentered);
const circle = smoothstep(float(1.0), float(0.8), dist);
| GLSL | TSL |
|---|---|
position | positionGeometry |
transformed | positionLocal |
transformedNormal | normalLocal |
vWorldPosition | positionWorld |
vColor | vertexColor() |
vUv / uv | uv() |
vNormal | normalView |
viewMatrix | cameraViewMatrix |
modelMatrix | modelWorldMatrix |
modelViewMatrix | modelViewMatrix |
projectionMatrix | cameraProjectionMatrix |
diffuseColor | material.colorNode |
gl_FragColor | material.fragmentNode |
texture2D(tex, uv) | texture(tex, uv) |
textureCube(tex, dir) | cubeTexture(tex, dir) |
gl_FragCoord | screenCoordinate |
gl_PointCoord | uv() in SpriteNodeMaterial/PointsNodeMaterial |
gl_InstanceID | instanceIndex |