WebGPU Three.js TSL Skill

repository·main·Indexed 22 days ago

https://github.com/dgreenheck/webgpu-claude-skill

An Agent Skill for Claude Code and Cursor designed to assist developers in building WebGPU-enabled Three.js applications using the Three.js Shading Language (TSL). It provides specialized knowledge on TSL compute shaders, storage buffers (instancedArray and attributeArray), workgroup configuration, and GPU-to-CPU data retrieval. The skill includes scoped rules for JS/TS, WGSL integration, post-processing, and device limits, with recommendations for Three.js r171+.

Tokens
22.3K
Snippets
70
Records
77
Agent score
77%

What's inside webgpu-claude-skill

  1. Understanding WebGPU Features

    main

    Features are optional capabilities (e.g., shader-f16 or subgroups) that are either present or absent on a GPU.

    Three.js Behavior: Three.js automatically requests all features supported by the adapter, so manual feature management is generally unnecessary for most users.

    To check for a specific feature in raw WebGPU, use the features.has() method on the adapter.

    const adapter = await navigator.gpu?.requestAdapter();
    // adapter.features is a Set
    console.log(adapter.features.has('float32-filterable'));
    console.log(adapter.features.has('shader-f16'));
  2. Use a hybrid TSL and WGSL approach

    main

    For complex shaders, use a hybrid approach: implement heavy mathematical operations or noise algorithms in WGSL via wgslFn, and use TSL for high-level logic, texture sampling, and scene integration. This combines the performance of raw WGSL with the ease of use of TSL nodes.

    // Complex math in WGSL
    const complexMath = wgslFn(`
      fn complexOperation(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
        let blended = mix(a, b, t);
        let rotated = vec3<f32>(
          blended.x * cos(t) - blended.y * sin(t),
          blended.x * sin(t) + blended.y * cos(t),
          blended.z
        );
        return normalize(rotated);
      }
    `);
    
    // Simple logic in TSL
    const finalColor = Fn(() => {
      const base = texture(diffuseMap).rgb;
      const processed = complexMath(base, vec3(1, 0, 0), time);
      return mix(base, processed, oscSine(time));
    });
    
    material.colorNode = finalColor();
  3. How Cursor rules are scoped and applied

    main

    When using this skill in Cursor, the .mdc rules are automatically attached to your files based on specific filename patterns (globs):

    • webgpu-threejs-tsl.mdc: Entry point for JS/TS files.
    • compute-shaders.mdc: Files matching *compute* or *particle*.
    • post-processing.mdc: Files matching *post*, *effect*, or *bloom*.
    • wgsl-integration.mdc: .wgsl files and files containing *wgsl* in the name.
    • device-loss-and-limits.mdc: Files matching *renderer* or *webgpu*.
  4. Handle conditional logic in TSL Compute Shaders

    main

    When writing TSL compute shaders, you cannot use standard JavaScript variable reassignment (e.g., value = value.add(1)) inside an If() block because TSL cannot track the reassignment of the JS variable to a new node. Instead, use one of these three patterns:

    1. select(condition, valueIfTrue, valueIfFalse): Best for simple inline conditional value selection.
    2. Direct .assign() on buffer elements: Best for direct writes to a buffer inside an If() block.
    3. .toVar() for mutable variables: Best for complex logic where a variable needs to be updated multiple times. Calling .toVar() creates a proper GPU variable that supports .assign() calls.

    Summary Table:

    PatternUse Case
    select(cond, a, b)Simple conditional value selection
    element.assign() inside If()Direct buffer writes
    .toVar() + assign()Complex logic with multiple conditionals
    Regular If() with direct assignsMultiple buffer element updates
    // ✅ CORRECT - Use .toVar() for variables that need mutation
    const computeShader = Fn(() => {
      // .toVar() creates a proper GPU variable that can be reassigned
      const value = buffer.element(index).toFloat().toVar();
    
      If(condition, () => {
        value.assign(value.add(1.0));  // This works with .toVar()!
      });
    
      buffer.element(index).assign(value);
    })().compute(count);
  5. Handle TSL Node Property Assignment vs JS Variable Reassignment

    main

    When writing TSL (Three.js Shading Language) compute shaders, you cannot use standard JavaScript variable reassignment (e.g., let x = 5; x = 10;) to change values within the GPU logic. Instead, you must use TSL-specific methods to mutate values or handle conditional logic.

    What to avoid

    Do not attempt to reassign a TSL node using standard JS syntax:

    // This does NOT work
    let pos = positions.element(instanceIndex);
    pos = vec3(1, 2, 3); 

    Correct patterns

    Use one of the following three methods depending on your needs:

    1. Use .assign(): To overwrite the value of a buffer element or a node property.
    2. Use .toVar(): If you need a local, mutable variable within your TSL function scope.
    3. Use select(): For conditional value selection (the TSL equivalent of a ternary operator).
    4. Direct Assignment in If(): Use .assign() directly on buffer elements inside a TSL If() block.
  6. Create custom post-processing effects with Fn()

    main

    Custom effects are created using the Fn function from three/tsl. You define a function that manipulates texture nodes or color values and then call that function to assign it to renderPipeline.outputNode. You can use utilities like screenUV, float, vec2, vec4, texture, and viewportSharedTexture within these functions.

    import { Fn, screenUV, float, vec4 } from 'three/tsl';
    
    const customEffect = Fn(() => {
      const color = scenePassColor.toVar();
      // Invert colors
      color.rgb.assign(float(1.0).sub(color.rgb));
      return color;
    });
    
    renderPipeline.outputNode = customEffect();
  7. Optimize WGSL performance

    main

    To ensure high performance in custom WGSL functions, follow these best practices:

    1. Avoid Branching: Use mathematical functions like mix and step instead of if-else statements where possible to prevent execution divergence.

      • Instead of: if (x > 0.5) { result = a; } else { result = b; }
      • Use: result = mix(b, a, step(0.5, x));
    2. Use Local Variables: Cache repeated calculations in let variables to avoid redundant computations.

      • Example: let p2 = p * p; let p4 = p2 * p2;
    3. Minimize Texture Samples: Sample a texture once and reuse the result instead of calling textureSample multiple times for the same coordinate.

  8. Set up Post-Processing with RenderPipeline

    main

    In recent versions (r183+), PostProcessing has been replaced by RenderPipeline. You create a pipeline, define passes (like a scene pass), and chain effects (like bloom) to the output node.

    import { pass } from 'three/tsl';
    import { bloom } from 'three/addons/tsl/display/BloomNode.js';
    
    // Setup (RenderPipeline replaced PostProcessing in r183)
    const renderPipeline = new THREE.RenderPipeline(renderer);
    const scenePass = pass(scene, camera);
    const color = scenePass.getTextureNode('output');
    
    // Apply effects
    const bloomPass = bloom(color);
    renderPipeline.outputNode = color.add(bloomPass);
    
    // Render
    renderPipeline.render();
  9. Testing Device Loss via Chrome GPU Crash

    main

    For more realistic testing, navigate to about:gpucrash in a separate tab to crash the GPU process. Chrome enforces escalating restrictions based on crash frequency:

    • 1st crash: New adapters allowed.
    • 2nd crash (within 2 min): Adapter requests fail (resets on page refresh).
    • 3rd crash (within 2 min): All pages blocked (reset after 2 min or browser restart).
    • 3-6 crashes (within 5 min): GPU process stops restarting; browser restart required.

    To bypass these limits during development, launch Chrome with the following flags:

    # macOS
    /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
      --disable-domain-blocking-for-3d-apis \
      --disable-gpu-process-crash-limit
    
    # Windows
    chrome.exe --disable-domain-blocking-for-3d-apis --disable-gpu-process-crash-limit
    
    # Linux
    google-chrome --disable-domain-blocking-for-3d-apis --disable-gpu-process-crash-limit