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