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

526 views Asked by At

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 !

2

There are 2 answers

0
gman On BEST ANSWER

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>

0
munrocket On

I recommend you to use compute shader for this because it will be faster

fn sdSphere(p: vec3f, r: f32) -> f32 {
    return length(p) - r;
}

fn sdBox(p: vec3f, r: f32) -> f32 {
    return max(max(abs(p.x), abs(p.y)), abs(p.z)) - r;
}

fn map(p: vec3f) -> f32 {
    return max(sdBox(p, 1.), -sdSphere(p, 1.3));
}

fn march(ro: vec3f, rd: vec3f) -> vec4f {
    var p: vec3f;
    var s: f32;
    for (var i = 0; i < 99; i++) {
        p = ro + s * rd;
        let ds = map(p);
        s += ds;
        if (s < .001 || s > 80.) { break; }
    }
    return vec4f(p, s/80.);
}

fn normal(p: vec3f) -> vec3f {
    let e = vec2f(.001, 0.);
    return normalize(vec3f(
        map(p + e.xyy) - map(p - e.xyy),
        map(p + e.yxy) - map(p - e.yxy),
        map(p + e.yyx) - map(p - e.yyx)
    ));
}

fn rotY(p: vec3f, a: f32) -> vec3f {
    let s = sin(a); let c = cos(a);
    return vec3f(dot(p.xz, vec2f(c, s)), p.y, dot(p.xz, vec2f(-s, c)));
}

@compute @workgroup_size(16, 16)
fn main_image(@builtin(global_invocation_id) id: vec3u) {
    let screen_size = textureDimensions(screen);
    if (id.x >= screen_size.x || id.y >= screen_size.y) { return; }
    let fragCoord = float2(f32(id.x) + .5, f32(screen_size.y - id.y) - .5);
    let uv = (2.*fragCoord - vec2f(screen_size)) / f32(screen_size.y);
    var ro = vec3f(0,0,4);
    var rd = normalize(vec3f(uv, -2));

    ro = rotY(ro, time.elapsed);
    rd = rotY(rd, time.elapsed);

    let m = march(ro, rd);
    let p = m.xyz;
    let n = normal(m.xyz);
    let l = normalize(vec3f(-1,0,0));

    let bg = vec3f(0);
    var col = vec3f(n*.5+.5);
    col = select(bg, col, m.w < 1.);
    textureStore(screen, id.xy, vec4f(col, 1.));
}

Than you can show result with fragment shader. https://compute.toys/view/459