TSL conversion

Use it when you need to convert a Threejs Material using GLSL shaders to TSL node materials

14 stars

Best use case

TSL conversion is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Use it when you need to convert a Threejs Material using GLSL shaders to TSL node materials

Teams using TSL conversion should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/tsl/SKILL.md --create-dirs "https://raw.githubusercontent.com/Robpayot/tslda/main/.agents/skills/tsl/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/tsl/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How TSL conversion Compares

Feature / AgentTSL conversionStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Use it when you need to convert a Threejs Material using GLSL shaders to TSL node materials

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# My Skill
You're the best TSL developer in the world

# TSL (Three.js Shading Language) Rules for AI

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()` |

---

## CRITICAL: What TSL Is

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); });

---

## TSL conversion

- 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()."
  - **In this project, `texture(...)` expects a real `THREE.Texture` as its first argument** (see `Lightnings`, `Stars`, etc). Do **not** do `texture( uniform(tex), uv )`.
  - Use `LoaderManager.getTexture('name')` to guarantee a valid texture (falls back to a 1×1 `DataTexture` until loaded), then sample with `texture(mapTexture, uv())`.

### Component + Material folder structure

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
    └── ...

- **Folder name**: lowercase, singular (e.g. `sail`, `splashes`)
- **Component file**: PascalCase (e.g. `Sail.js`, `Splashes.js`)
- **Material file**: `*Materials.js` (e.g. `SailMaterials.js`, `SplashMaterials.js`)
- Import from parent: `import Sail from './sail/Sail'`

## Imports

### NPM (Preferred)

    import * as THREE from 'three/webgpu';
    import { Fn, vec3, float, uniform, /* ... */ } from 'three/tsl';


### WRONG Import Patterns

    // 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';

---

## Renderer Initialization

**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);

---

## Type Constructors

| 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 |

### Type Conversions

    node.toFloat()  node.toInt()  node.toUint()  node.toBool()
    node.toVec2()   node.toVec3() node.toVec4()  node.toColor()

---

## Operators

### Arithmetic (method chaining)

    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

### Assignment (for mutable variables)

    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

### Comparison (returns bool node)

    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

### Logical

    a.and(b)   a.or(b)   a.not()   a.xor(b)

### Bitwise

    a.bitAnd(b)  a.bitOr(b)  a.bitXor(b)  a.bitNot()
    a.shiftLeft(n)  a.shiftRight(n)

### Swizzle

    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

---

## Variables

### RULE: TSL nodes are immutable by default

    // 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

### Variable Types

    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

---

## Uniforms

    // 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

---

## Functions

### Fn() Syntax

    // 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;
    });

### Calling Functions

    myFn(a, b, c)           // array params
    myFn({ color: red })    // object params
    myFn()                  // use defaults

### Inline Functions (no Fn wrapper)

    // OK for simple expressions, no variables/conditionals
    const simple = (t) => t.sin().mul(0.5).add(0.5);

---

## Conditionals

### If/ElseIf/Else (CAPITAL I)

    // 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/Case

    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

### select() - Ternary (Preferred)

    // 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);

### Math-Based (Preferred for Performance)

    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))

---

## Loops

    // 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

---

## Math Functions

    // 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

---

## Oscillators

    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

---

## Blend Modes

    blendBurn(a, b)    // color burn
    blendDodge(a, b)   // color dodge
    blendScreen(a, b)  // screen
    blendOverlay(a, b) // overlay
    blendColor(a, b)   // normal blend

---

## UV Utilities

    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

---

## Reflect

    reflectView    // reflection in view space
    reflectVector  // reflection in world space

---

## Interpolation Helpers

    remap(node, inLow, inHigh, outLow = 0, outHigh = 1)      // remap range
    remapClamp(node, inLow, inHigh, outLow = 0, outHigh = 1) // remap + clamp

---

## Random

    hash(seed)      // pseudo-random float [0,1]
    range(min, max) // random attribute per instance

---

## Arrays

    // 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

---

## Varyings

    // Compute in vertex, interpolate to fragment
    const v = varying(expression, 'name');

    // Optimize: force vertex computation
    const v = vertexStage(expression);

---

## Textures

    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)

---

## Shader Inputs

### Position

    positionGeometry      // raw attribute
    positionLocal         // after skinning/morphing
    positionWorld         // world space
    positionView          // camera space
    positionWorldDirection // normalized
    positionViewDirection  // normalized

### Normal

    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.

### Position and normal spaces — understand the logic

**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.
- For **world-space** math (e.g. heightmap UV from world XZ), use `positionWorld` or `modelWorldMatrix.mul(vec4(positionLocal, 1)).xyz`.
- **SkinnedMesh:** `normalWorld` / `normalView` can be wrong; use `normalLocal` and transform the light direction to model space (see SkinnedMesh note above).

### Camera

    cameraPosition  cameraNear  cameraFar
    cameraViewMatrix  cameraProjectionMatrix  cameraNormalMatrix

### Screen

    screenUV          // normalized [0,1]
    screenCoordinate  // pixels
    screenSize        // pixels
    viewportUV  viewport  viewportCoordinate  viewportSize

### Time

    time              // elapsed time in seconds (float)
    deltaTime         // time since last frame (float)

### Model

    modelDirection         // vec3
    modelViewMatrix        // mat4
    modelNormalMatrix      // mat3
    modelWorldMatrix       // mat4
    modelPosition          // vec3
    modelScale             // vec3
    modelViewPosition      // vec3
    modelWorldMatrixInverse // mat4

### Other

    uv()  uv(index)           // texture coordinates
    vertexColor()             // vertex colors
    attribute('name', 'type') // custom attribute
    instanceIndex             // instance/thread ID (for instancing and compute)

---

## NodeMaterial Types

### Available Materials

    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

### All Materials - Common Properties

    .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`.

### MeshStandardNodeMaterial

    .roughnessNode  // float
    .metalnessNode  // float
    .emissiveNode   // vec3 color
    .aoNode         // float
    .envNode        // vec3 color

### MeshPhysicalNodeMaterial (extends Standard)

    .clearcoatNode  .clearcoatRoughnessNode  .clearcoatNormalNode
    .sheenNode  .transmissionNode  .thicknessNode
    .iorNode  .iridescenceNode  .iridescenceThicknessNode
    .anisotropyNode  .specularColorNode  .specularIntensityNode

### SpriteNodeMaterial

    .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

### PointsNodeMaterial

    .positionNode   // vec3 - point position
    .colorNode      // vec4 - color and alpha
    .sizeNode       // float - point size in pixels

### Converting Points / point-like particles (use SpriteNodeMaterial, no billboardToCamera)

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:

1. **Geometry:** `PlaneGeometry(1, 1)` with `setAttribute('instancePosition', new InstancedBufferAttribute(positionArray, 3))` and any other per-instance attributes (e.g. `offset`, `speed`).
2. **Material:** `SpriteNodeMaterial` with:
   - `positionNode = attribute('instancePosition', 'vec3')` (or add displacement, e.g. heightmap, in the same Fn)
   - `scaleNode = float(SPRITE_SCALE)` or a uniform
   - `colorNode` = your fragment logic (use `uv()` instead of `gl_PointCoord`)
3. **Mesh:** `new InstancedMesh(planeGeo, material, count)` — no `setMatrixAt`, no manual instance matrices.
4. **API:** Keep a no-op `billboardToCamera()` if callers still invoke it, to avoid breaking changes.

Examples in this project: **Stars**, **Lightnings**, **Waves**.

---

## Compute Shaders

### Basic Compute (Standalone)

    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)

### Compute → Render Pipeline

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();

### Updating Buffers from JavaScript

    // Modify the underlying array
    for (let i = 0; i < COUNT; i++) {
      dataArray[i * 4] = Math.random();
    }
    // Flag for GPU upload
    dataAttribute.needsUpdate = true;

---

## Example: Basic Material Shader

    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;
    })();

---

## Example: Compute Shader Structure

    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);

---

## Common Error Patterns

### ERROR: "If is not defined"

    // WRONG
    if(condition, () => {})
    // CORRECT
    If(condition, () => {})  // capital I

### ERROR: Cannot assign

    // WRONG
    const v = vec3(1,2,3);
    v.x = 5;
    // CORRECT
    const v = vec3(1,2,3).toVar();
    v.x.assign(5);

### ERROR: Type mismatch

    // WRONG
    sqrt(intValue)
    // CORRECT
    sqrt(intValue.toFloat())

### ERROR: Uniform not changing

    // WRONG
    myUniform = newValue;
    // CORRECT
    myUniform.value = newValue;

### ERROR: Import not found

    // WRONG
    import { vec3 } from 'three/nodes';
    import * as THREE from 'three';
    // CORRECT
    import { vec3 } from 'three/tsl';
    import * as THREE from 'three/webgpu';

### ERROR: Compute data not visible in render

    // 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;

### ERROR: Nothing renders

    // WRONG: Rendering before init
    renderer.render(scene, camera);

    // CORRECT: Always await init first
    await renderer.init();
    renderer.render(scene, camera);

---

## Quick Patterns

### Fresnel

    const fresnel = Fn(() => {
      const NdotV = normalize(cameraPosition.sub(positionWorld)).dot(normalWorld).max(0);
      return pow(float(1).sub(NdotV), 5);
    });

### Wave Displacement

    material.positionNode = Fn(() => {
      const p = positionLocal.toVar();
      p.y.addAssign(sin(p.x.mul(5).add(time)).mul(0.2));
      return p;
    })();

### UV Scroll

    material.colorNode = texture(map, uv().add(vec2(time.mul(0.1), 0)));

### Conditional Value

    const result = select(value.greaterThan(0.5), valueA, valueB);
    // OR branchless:
    const result = mix(valueB, valueA, step(0.5, value));

### Gradient Mapping

    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);

### Soft Falloff

    // 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));

### Circular Mask (for sprites/points)

    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 Migration

| 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` |

Related Skills

conversion-tools-automation

11
from EricGrill/agents-skills-plugins

Automate Conversion Tools tasks via Rube MCP (Composio). Always search tools first for current schemas.

lead-conversion-customization

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when writing Apex to customize lead conversion: controlling which records are created, copying custom field values post-conversion, firing related record logic on convert, or structuring LeadConvert calls. Triggers: 'lead conversion apex', 'Database.LeadConvert', 'custom field mapping conversion', 'convert lead trigger', 'lead convert opportunity', 'LeadConvertResult'. NOT for configuring lead conversion field mapping in Setup UI, managing lead assignment rules, or building web-to-lead forms — use admin/lead-management-and-conversion for those.

lead-management-and-conversion

8
from PranavNagrecha/AwesomeSalesforceSkills

Configuring Salesforce lead management and conversion: lead settings, web-to-lead, conversion field mapping, lead queues, auto-response rules, lead processes. Use when setting up lead capture, routing, or conversion for Sales Cloud. Trigger keywords: web-to-lead, lead conversion, lead field mapping, lead settings, lead process, lead queue, lead auto-response. NOT for lead assignment rule logic (use assignment-rules). NOT for duplicate rule configuration (use data-quality-and-governance).

Skill: unit-conversion

7
from heldernoid/agentic-build-templates

## Overview

scbe-webtoon-book-conversion

6
from issdandavis/SCBE-AETHERMOORE

Convert The Six Tongues Protocol and related manuscript sections into webtoon/manhwa storyboard packets, episode roadmaps, panel expansion plans, and image-generation-ready prompt lanes. Use when extending the series storyboard, adapting book chapters into vertical scroll episodes, or keeping art generation tied to canon instead of drifting into generic fantasy panels.

overnight-verify-close-and-blocker-conversion

5
from vamseeachanta/workspace-hub

Use overnight Codex lanes to clear stale-open GitHub issues by verification-first closure, and convert blocked PR-repair attempts into dedicated blocker issues instead of speculative edits.

orcaflex-file-conversion

5
from vamseeachanta/workspace-hub

Convert OrcaFlex files between formats (.dat, .yml, .sim) for digital analysis and automation. Supports bidirectional conversion, batch processing, and format standardization.

orcaflex-file-conversion-yaml-structure-validation

5
from vamseeachanta/workspace-hub

Sub-skill of orcaflex-file-conversion: YAML Structure Validation (+1).

orcaflex-file-conversion-pattern-based-conversion

5
from vamseeachanta/workspace-hub

Sub-skill of orcaflex-file-conversion: Pattern-Based Conversion (+1).

orcaflex-file-conversion-orcaflex-file-types

5
from vamseeachanta/workspace-hub

Sub-skill of orcaflex-file-conversion: OrcaFlex File Types (+1).

orcaflex-file-conversion-example-orcaflex-yaml-output

5
from vamseeachanta/workspace-hub

Sub-skill of orcaflex-file-conversion: Example OrcaFlex YAML Output.

orcaflex-file-conversion-conversion-report-markdown

5
from vamseeachanta/workspace-hub

Sub-skill of orcaflex-file-conversion: Conversion Report (Markdown).