Search code examples
fragment-shaderwebgpuwgslshadertoy

SDF WebGPU. How to modify the code for SDF purpose like in ShaderToy.com?


I want to create a WebGPU version of Shadertoy, but I can't to prepare the code correctly.

How to draw in @fragment shader for SDF in WebGPU ?

I clipped space [-1,1, 1,1, -1,-1, 1,-1] of canvas but what I need to do next ?

<!DOCTYPE html>
<title>SDF-WebGPU</title>
<canvas></canvas><script>'use strict';

const canvas = document.body.firstChild;

canvas.style = `
  display: block;
  image-rendering: pixelated;
  background-color: #ccc;
  user-select: none;
  touch-action: none;
  width:  ${ canvas.width  = 480 * devicePixelRatio, 480 }px;
  height: ${ canvas.height = 360 * devicePixelRatio, 360 }px;
`;

const init = async function(){
  const
  context = canvas.getContext(`webgpu`),
  format = navigator.gpu.getPreferredCanvasFormat(),
  adapter = await navigator.gpu.requestAdapter(),
  device = await adapter.requestDevice(),
  Q = device.queue,

  {VERTEX, COPY_DST} = GPUBufferUsage,
  
  SPACE_B = new Float32Array([-1,1, 1,1, -1,-1, 1,-1]),

  B = device.createBuffer({
    label: `SPACE`,
     size: SPACE_B.byteLength,
    usage: VERTEX | COPY_DST
  }),
    
  P = device.createRenderPipeline({
    layout: `auto`,
    vertex: {
      module: device.createShaderModule({
        code: `@vertex
        fn vSh(@location(0) p:vec2<f32>) -> @builtin(position) vec4<f32>{
          return vec4<f32>(p,0,1); // (p[x,y],z,w)
        }`
      }),
      entryPoint: `vSh`,
      buffers: [{
        arrayStride: 8, // 2*4 = 2 floats x 4 bytes
        attributes: [{
          shaderLocation: 0,
          offset: 0,
          format: `float32x2`
        }]
      }], // buffers
    },
    fragment: {
      module: device.createShaderModule({
        code: `@fragment
        fn fSh() -> @location(0) vec4<f32>{
          return vec4<f32>(.082,.263,.455,1);
        }`
      }),
      entryPoint: `fSh`,
      targets: [ {format} ]
    },
    primitive:{
      topology: `triangle-strip`
    }
  }), // Pipeline

  frame=()=>{
    const
    C = device.createCommandEncoder(),
    R = C.beginRenderPass({
      colorAttachments:[{
        view: context.getCurrentTexture().createView(),
        loadOp: `clear`,
        storeOp: `store`
      }]
    });
  
    R.setPipeline(P);
    R.setVertexBuffer(0,B);
    R.draw(4);
    R.end();

    Q.submit([ C.finish() ])
  }; // frame

  context.configure({ device, format, alphaMode: `opaque` });

  Q.writeBuffer(B,0, SPACE_B);

  frame()

}() // init

</script>

I was only able to create a version without SDF.

If you know any references about WebGPU SDF, please share with me. Thanks !


Solution

  • Shaders in WebGPU are written in WGSL instead of GLSL but nearly every concept in GLSL has a similar feature in WGSL

    You'll either have to read the spec or look at examples to figure out how to translate from GLSL to WGSL but it's not that hard :P

    Here's a GLSL SDF shader

    // The MIT License
    // Copyright © 2020 Inigo Quilez
    // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    
    
    // Signed distance to a disk
    
    // List of some other 2D distances: https://www.shadertoy.com/playlist/MXdSRf
    //
    // and iquilezles.org/articles/distfunctions2d
    
    
    float sdCircle( in vec2 p, in float r ) 
    {
        return length(p)-r;
    }
    
    
    void mainImage( out vec4 fragColor,ain vec2 fragCoord )
    {
        vec2 p = (2.0*fragCoord-iResolution.xy)/iResolution.y;
        vec2 m = (2.0*iMouse.xy-iResolution.xy)/iResolution.y;
    
        float d = sdCircle(p,0.5);
        
        // coloring
        vec3 col = (d>0.0) ? vec3(0.9,0.6,0.3) : vec3(0.65,0.85,1.0);
        col *= 1.0 - exp(-6.0*abs(d));
        col *= 0.8 + 0.2*cos(150.0*d);
        col = mix( col, vec3(1.0), 1.0-smoothstep(0.0,0.01,abs(d)) );
    
        if( iMouse.z>0.001 )
        {
        d = sdCircle(m,0.5);
        col = mix(col, vec3(1.0,1.0,0.0), 1.0-smoothstep(0.0, 0.005, abs(length(p-m)-abs(d))-0.0025));
        col = mix(col, vec3(1.0,1.0,0.0), 1.0-smoothstep(0.0, 0.005, length(p-m)-0.015));
        }
    
        fragColor = vec4(col,1.0);
    }
    

    Here it translated to WGSL

    // The MIT License
    // Copyright © 2020 Inigo Quilez
    // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    
    
    // Signed distance to a disk
    
    // List of some other 2D distances: https://www.shadertoy.com/playlist/MXdSRf
    //
    // and iquilezles.org/articles/distfunctions2d
    
    
    fn sdCircle( p: vec2f, r: f32 ) -> f32 
    {
        return length(p)-r;
    }
    
    struct Uniforms {
      iResolution: vec3f,
      iMouse: vec4f,
    };
    
    @group(0) @binding(0) var<uniform> u: Uniforms;
    
    fn mainImage( fragColor: ptr<function, vec4f>, fragCoord: vec2f )
    {
        let p = (2.0*fragCoord-u.iResolution.xy)/u.iResolution.y;
      let m = (2.0*u.iMouse.xy-u.iResolution.xy)/u.iResolution.y;
    
        var d = sdCircle(p,0.5);
        
        // coloring
      var col = select(vec3(0.9,0.6,0.3), vec3(0.65,0.85,1.0), d>0.0);
      col *= 1.0 - exp(-6.0*abs(d));
        col *= 0.8 + 0.2*cos(150.0*d);
        col = mix( col, vec3f(1.0), 1.0-smoothstep(0.0,0.01,abs(d)) );
    
      if( u.iMouse.z>0.001 )
        {
        d = sdCircle(m,0.5);
        col = mix(col, vec3(1.0,1.0,0.0), 1.0-smoothstep(0.0, 0.005, abs(length(p-m)-abs(d))-0.0025));
        col = mix(col, vec3(1.0,1.0,0.0), 1.0-smoothstep(0.0, 0.005, length(p-m)-0.015));
        }
    
        *fragColor = vec4f(col,1.0);
    }
    

    I can't write all the differences but a few easy to explain things

    In GLSL a function is returnType name(type1 arg1, type2 arg2) { ... }

    In WGSL a function is fn name(arg1: type1, arg2: type2) -> returnType { ... }

    In GLSL a variable is type nameOfVar

    In WGSL a variable is var nameOfVar: type except that in WGSL you don't have to specify the type if WGSL can figure it out.

    In other words, these are the same

    var a: f32 = 123.0;  // a's type is f32
    var b:     = 123.0;  // b's type is f32
    

    Note: confusingly, var is like let in JavaScript. You can reassign it. let is like const in JavaScript. You can't change it after assignment.

    GLSL has the ? operator as in v = condition ? t : f

    WGSL has select as in v = select(f, t, condition)

    types

    GLSL  |  WGSL
    ------+------
    float | f32
    int   | i32
    vec4  | vec4f
    ivec4 | vec4i
    etc...
    

    One more issue, in WebGL gl_FragCoord.y goes from 0 at the bottom to height of canvas at the top. WebGPU's equivalent @builtin(position) goes from 0 at the top to height of canvas at the bottom.

    Here's a live version of that SDF shader. Drag the mouse on the image

    const code = `
    // The MIT License
    // Copyright © 2020 Inigo Quilez
    // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    
    
    // Signed distance to a disk
    
    // List of some other 2D distances: https://www.shadertoy.com/playlist/MXdSRf
    //
    // and iquilezles.org/articles/distfunctions2d
    
    
    fn sdCircle( p: vec2f, r: f32 ) -> f32 
    {
        return length(p)-r;
    }
    
    struct Uniforms {
      iResolution: vec3f,
      iMouse: vec4f,
    };
    
    @group(0) @binding(0) var<uniform> u: Uniforms;
    
    fn mainImage( fragColor: ptr<function, vec4f>, fragCoord: vec2f )
    {
        let p = (2.0*fragCoord-u.iResolution.xy)/u.iResolution.y;
      let m = (2.0*u.iMouse.xy-u.iResolution.xy)/u.iResolution.y;
    
        var d = sdCircle(p,0.5);
        
        // coloring
      var col = select(vec3(0.9,0.6,0.3), vec3(0.65,0.85,1.0), d>0.0);
      col *= 1.0 - exp(-6.0*abs(d));
        col *= 0.8 + 0.2*cos(150.0*d);
        col = mix( col, vec3f(1.0), 1.0-smoothstep(0.0,0.01,abs(d)) );
    
      if( u.iMouse.z>0.001 )
        {
        d = sdCircle(m,0.5);
        col = mix(col, vec3(1.0,1.0,0.0), 1.0-smoothstep(0.0, 0.005, abs(length(p-m)-abs(d))-0.0025));
        col = mix(col, vec3(1.0,1.0,0.0), 1.0-smoothstep(0.0, 0.005, length(p-m)-0.015));
        }
    
        *fragColor = vec4f(col,1.0);
    }
    
    @vertex fn vs(
      @builtin(vertex_index) VertexIndex : u32
    ) -> @builtin(position) vec4<f32> {
      var pos = array<vec2<f32>, 3>(
        vec2(-1.0, -1.0),
        vec2( 3.0, -1.0),
        vec2(-1.0,  3.0)
      );
    
      return vec4(pos[VertexIndex], 0.0, 1.0);
    }
    
    @fragment fn fs(@builtin(position) fragCoord : vec4f) -> @location(0) vec4f {
      var color = vec4f(0);
      mainImage(&color, vec2f(fragCoord.x, u.iResolution.y - fragCoord.y));
      return color;
    }
    `;
    
    (async() => {
      const adapter = await navigator.gpu?.requestAdapter();
      const device = await adapter?.requestDevice();
      if (!device) {
        alert('need webgpu');
        return;
      }
    
      const canvas = document.querySelector("canvas")
      const context = canvas.getContext('webgpu');
      const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
      context.configure({
          device,
          format: presentationFormat,
          alphaMode: 'opaque',
      });
    
      const uniformBufferSize = 32;
      const uniformBuffer = device.createBuffer({
        size: uniformBufferSize,
        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
      });
      const uniformArrayBuffer = new ArrayBuffer(uniformBufferSize);
      const resolution = new Float32Array(uniformArrayBuffer, 0, 3);
      const mouse = new Float32Array(uniformArrayBuffer, 16, 4);
    
      const module = device.createShaderModule({code});
      const pipeline = device.createRenderPipeline({
        layout: 'auto',
        vertex: {
          module,
          entryPoint: 'vs',
        },
        fragment: {
          module,
          entryPoint: 'fs',
          targets: [{format: presentationFormat}],
        }
      });
    
      const bindGroup = device.createBindGroup({
        layout: pipeline.getBindGroupLayout(0),
        entries: [
          { binding: 0, resource: { buffer: uniformBuffer } },
        ],
      });
    
      function resizeToDisplaySize(device, canvas) {
        const width = Math.min(device.limits.maxTextureDimension2D, canvas.clientWidth);
        const height = Math.min(device.limits.maxTextureDimension2D, canvas.clientHeight);
    
        const needResize = width !== presentationSize[0] ||
                           height !== presentationSize[1];
        if (needResize) {
          canvas.width = width;
          canvas.height = height;
        }
        return needResize;
      }
      function render() {
        const width = Math.min(device.limits.maxTextureDimension2D, canvas.clientWidth);
        const height = Math.min(device.limits.maxTextureDimension2D, canvas.clientHeight);
        canvas.width = width;
        canvas.height = height;
    
        resolution[0] = width;
        resolution[1] = height;
        device.queue.writeBuffer(uniformBuffer, 0, uniformArrayBuffer);
    
        const encoder = device.createCommandEncoder();
        const pass = encoder.beginRenderPass({
          colorAttachments: [{
            view: context.getCurrentTexture().createView(),
            clearColor: [0, 0, 0, 0],
            loadOp: 'clear',
            storeOp: 'store',
          }]
        });
        pass.setPipeline(pipeline);
        pass.setBindGroup(0, bindGroup);
        pass.draw(3);
        pass.end();
    
        device.queue.submit([encoder.finish()]);
        requestAnimationFrame(render);
      }
      requestAnimationFrame(render);
    
      canvas.addEventListener('mousemove', (e) => {
        mouse[0] = e.offsetX;
        mouse[1] = canvas.offsetHeight - e.offsetY - 1;
      });
      canvas.addEventListener('mousedown', _ => mouse[2] = 1);
      canvas.addEventListener('mouseup', _ => mouse[2] = 0);
    
    })();
    html, body {
      margin: 0;
      height: 100%;
    }
    canvas {
      width: 100%;
      height: 100%;
      display: block;
    }
    <canvas></canvas>