initial
This commit is contained in:
commit
90b28c6396
11
common.js
Normal file
11
common.js
Normal file
@ -0,0 +1,11 @@
|
||||
async function getPathText(path)
|
||||
{
|
||||
const response = await fetch(path);
|
||||
if (!response.ok) {
|
||||
throw new Error(`${path}: ${response.status}`)
|
||||
}
|
||||
const blob = await response.blob();
|
||||
return blob.text();
|
||||
}
|
||||
|
||||
export { getPathText };
|
||||
0
favicon.ico
Normal file
0
favicon.ico
Normal file
23
index.html
Normal file
23
index.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGPU</title>
|
||||
<style>
|
||||
canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="index.js" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas></canvas>
|
||||
</body>
|
||||
</html>
|
||||
140
index.js
Normal file
140
index.js
Normal file
@ -0,0 +1,140 @@
|
||||
import { loadTriangleRenderer } from "./triangle.js"
|
||||
import { loadTriangleTexturedRenderer } from "./triangleTextured.js"
|
||||
|
||||
if (!navigator.gpu) {
|
||||
throw new Error("WebGPU not supported on this browser.");
|
||||
}
|
||||
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
if (!adapter) {
|
||||
throw new Error("No WebGPU adapter");
|
||||
}
|
||||
|
||||
// The next few lines, being nitpicky over bgra8unorm vs rgba8unorm is
|
||||
// needed for using the canvas framebuffer as a storage texture, which
|
||||
// is mostly only relevant in the specific case that you want to
|
||||
// directly write to the canvas framebuffer from a compute shader.
|
||||
//
|
||||
// I very highly doubt this has any negative performance impact
|
||||
// whatsoever on any device that has a WebGPU implementation, and
|
||||
// certainly no desktop/laptop GPU is affected by this. Nevertheless,
|
||||
// the WebGPU developers did deliberately decide to make
|
||||
// 'bgra8unorm-storage' an optional feature, so perhaps there is some
|
||||
// obscure device somewhere that actually cares about this.
|
||||
const hasBgraStorage = adapter.features.has("bgra8unorm-storage");
|
||||
const device = await adapter.requestDevice({
|
||||
requiredFeatures: hasBgraStorage ? ["bgra8unorm-storage"] : []
|
||||
});
|
||||
const canvas = document.querySelector("canvas");
|
||||
const context = canvas.getContext("webgpu");
|
||||
const canvasFormat = hasBgraStorage ? navigator.gpu.getPreferredCanvasFormat() : "rgba8unorm";
|
||||
context.configure({
|
||||
device: device,
|
||||
format: canvasFormat,
|
||||
alphaMode: 'premultiplied',
|
||||
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT
|
||||
});
|
||||
|
||||
// render targets, initialized in handleResize
|
||||
var depthTexture = undefined;
|
||||
|
||||
function handleResize(canvasTexture)
|
||||
{
|
||||
// The internal color buffer returned by context.getCurrentTexture()
|
||||
// is automatically resized with the canvas. All other render
|
||||
// targets (e.g depth buffers) are not.
|
||||
//
|
||||
// If there were other off-screen buffers (deferred lighting,
|
||||
// geometry buffers, etc..) those would also be recreated as needed here.
|
||||
|
||||
const depthResized = depthTexture === undefined || canvasTexture.width != depthTexture.width || canvasTexture.height != depthTexture.height;
|
||||
|
||||
if (depthResized) {
|
||||
if (depthTexture !== undefined) {
|
||||
depthTexture.destroy();
|
||||
}
|
||||
|
||||
depthTexture = device.createTexture({
|
||||
format: 'depth24plus',
|
||||
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
size: [canvasTexture.width, canvasTexture.height],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const triangleRenderer = await loadTriangleRenderer(device, canvasFormat);
|
||||
const triangleTexturedRenderer = await loadTriangleTexturedRenderer(device, canvasFormat);
|
||||
|
||||
function render()
|
||||
{
|
||||
// `canvas.clientWidth` and `canvas.clientHeight` are the current
|
||||
// size of the canvas element. The "canvas element" in this demo is
|
||||
// sized by the `canvas` CSS rule, which scales the canvas element
|
||||
// to 100% the width and height of the viewport.
|
||||
//
|
||||
// Web is a shit platform. The reported sizes are in "logical pixel"
|
||||
// units, which are not the same size as the physical pixels of the
|
||||
// device's screen.
|
||||
//
|
||||
// On the devices I've tested, multiplying clientWidth and
|
||||
// clientHeight by window.devicePixelRatio does give the real
|
||||
// dimensions of the canvas in physical screen pixels, and does
|
||||
// result in sharper font rendering.
|
||||
//
|
||||
// On devices like Nico's phone though, devicePixelRatio
|
||||
// multiplication results in ~12x more pixels being rendered per
|
||||
// frame, which can be significantly slower for things like
|
||||
// raymarching/raytracing.
|
||||
//
|
||||
// --
|
||||
//
|
||||
// This causes the canvas color buffer to be resized to the canvas
|
||||
// element as sized by CSS, including the browser window being
|
||||
// resized.
|
||||
canvas.width = canvas.clientWidth * window.devicePixelRatio;
|
||||
canvas.height = canvas.clientHeight * window.devicePixelRatio;
|
||||
|
||||
// possibly recreate any textures that depend on the size of the canvas
|
||||
// changing.
|
||||
const canvasTexture = context.getCurrentTexture();
|
||||
handleResize(canvasTexture);
|
||||
|
||||
const clearValue = { r: 0.2, g: 0.2, b: 0.4, a: 1.0 };
|
||||
const colorAttachments = [
|
||||
{
|
||||
view: canvasTexture.createView(),
|
||||
loadOp: "clear",
|
||||
clearValue: clearValue,
|
||||
storeOp: "store",
|
||||
}
|
||||
];
|
||||
|
||||
const encoder = device.createCommandEncoder();
|
||||
const renderPass = encoder.beginRenderPass({
|
||||
colorAttachments: colorAttachments,
|
||||
depthStencilAttachment: {
|
||||
view: depthTexture.createView(),
|
||||
depthClearValue: 1.0,
|
||||
depthLoadOp: 'clear',
|
||||
depthStoreOp: 'store',
|
||||
},
|
||||
});
|
||||
|
||||
// Every time you want to use a different shader, WebGPU also
|
||||
// requires you use a different GPURenderPipeline. It seems like a
|
||||
// decent pattern to wrap each collection of
|
||||
// buffers/pipelines/shaders in its own object:
|
||||
|
||||
// uncomment either of these to see the result of each render
|
||||
// pipeline / shader
|
||||
|
||||
//triangleRenderer.render(renderPass);
|
||||
triangleTexturedRenderer.render(renderPass);
|
||||
|
||||
renderPass.end();
|
||||
|
||||
const commandBuffer = encoder.finish();
|
||||
device.queue.submit([commandBuffer]);
|
||||
}
|
||||
|
||||
requestAnimationFrame(render);
|
||||
BIN
texture.png
Normal file
BIN
texture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 357 KiB |
144
triangle.js
Normal file
144
triangle.js
Normal file
@ -0,0 +1,144 @@
|
||||
import { getPathText } from "./common.js"
|
||||
|
||||
// Triangle vertices, one vertex per line. Hand-transcribed from a
|
||||
// .obj file exported from Blender to this form.
|
||||
//
|
||||
// I've also written myriad scripts to do this automatically for
|
||||
// larger models.
|
||||
//
|
||||
// Float32Array is a fairly convenient way to convert floating point
|
||||
// literals to bytes.
|
||||
const triangleVertices = new Float32Array([
|
||||
// [position ] [texture ] [normal ]
|
||||
0.0, 1.0, 0.0, 0.5, 1.0, 0.0, 1.0, 0.0,
|
||||
1.0, -1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
|
||||
-1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
|
||||
]);
|
||||
|
||||
class TriangleRenderer {
|
||||
constructor(device, canvasFormat, wgsl)
|
||||
{
|
||||
const label = "TriangleRenderer";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// shader module
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
const shaderModule = device.createShaderModule({
|
||||
label: `${label} shader`,
|
||||
code: wgsl,
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// pipeline
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// "bind group layouts" are a declaration of which `var` bindings
|
||||
// exist in the shader programs loaded by this pipeline
|
||||
//
|
||||
// None are used in triangle.wgsl, so this list is empty.
|
||||
const bindGroupLayouts = [
|
||||
];
|
||||
|
||||
const pipelineLayout = device.createPipelineLayout({
|
||||
label: `${label} pipeline layout`,
|
||||
bindGroupLayouts: bindGroupLayouts,
|
||||
});
|
||||
|
||||
// vertexBufferLayouts describes the position of each element for
|
||||
// each buffer. There is only one vertex buffer used in this demo,
|
||||
// so there is only one vertex buffer layout in this list.
|
||||
//
|
||||
// shaderLocation matches the location numbers defined in
|
||||
// triangle.wgsl.
|
||||
//
|
||||
// "offset" is the starting offset in bytes, 4 bytes per floating
|
||||
// point number. This matches the arrangement in bytes of the
|
||||
// triangleVertices Float32Array.
|
||||
const arrayStride = (4 * 3) + (4 * 2) + (4 * 3);
|
||||
const vertexBufferLayouts = [
|
||||
{
|
||||
arrayStride: arrayStride,
|
||||
attributes: [{
|
||||
format: "float32x3",
|
||||
offset: 0,
|
||||
shaderLocation: 0,
|
||||
}, {
|
||||
format: "float32x2",
|
||||
offset: 12,
|
||||
shaderLocation: 1,
|
||||
}, {
|
||||
format: "float32x3",
|
||||
offset: 20,
|
||||
shaderLocation: 2,
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
this.renderPipeline = device.createRenderPipeline({
|
||||
label: `${label} render pipeline`,
|
||||
layout: pipelineLayout,
|
||||
vertex: {
|
||||
module: shaderModule,
|
||||
entryPoint: "vertexMain",
|
||||
buffers: vertexBufferLayouts,
|
||||
},
|
||||
fragment: {
|
||||
module: shaderModule,
|
||||
entryPoint: "fragmentMain",
|
||||
targets: [{
|
||||
format: canvasFormat,
|
||||
}]
|
||||
},
|
||||
primitive: {
|
||||
topology: "triangle-list",
|
||||
},
|
||||
depthStencil: {
|
||||
depthWriteEnabled: true,
|
||||
depthCompare: "less",
|
||||
format: "depth24plus",
|
||||
},
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// bind group
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// there are no bind groups, because triangle.wgsl doesn't use any.
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// buffer
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
this.vertexBuffer = device.createBuffer({
|
||||
label: `${label} vertex buffer`,
|
||||
size: triangleVertices.byteLength,
|
||||
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
|
||||
// copy the (system RAM) triangleVertices to the (gpu RAM) vertexBuffer.
|
||||
device.queue.writeBuffer(this.vertexBuffer, 0, triangleVertices);
|
||||
}
|
||||
|
||||
render(renderPass)
|
||||
{
|
||||
renderPass.setPipeline(this.renderPipeline);
|
||||
renderPass.setVertexBuffer(0, this.vertexBuffer);
|
||||
const vertexCount = 3;
|
||||
renderPass.draw(vertexCount);
|
||||
}
|
||||
}
|
||||
|
||||
// This "loadTriangleRenderer" function is separate from the
|
||||
// constructor because Javascript class constructors can't be async
|
||||
// functions.
|
||||
async function loadTriangleRenderer(device, canvasFormat)
|
||||
{
|
||||
const triangleWgsl = await getPathText("triangle.wgsl");
|
||||
|
||||
const renderer = new TriangleRenderer(device, canvasFormat, triangleWgsl);
|
||||
|
||||
return renderer;
|
||||
}
|
||||
|
||||
export { loadTriangleRenderer };
|
||||
33
triangle.wgsl
Normal file
33
triangle.wgsl
Normal file
@ -0,0 +1,33 @@
|
||||
// The `VertexInput` location numbers match the numbers in
|
||||
// `vertexBufferLayouts`
|
||||
struct VertexInput {
|
||||
@location(0) position: vec3f,
|
||||
@location(1) texture: vec2f,
|
||||
@location(2) normal: vec3f,
|
||||
};
|
||||
|
||||
// location numbers for passing values from the vertex stage to the
|
||||
// fragment stage are arbitrary--they just need to match. Because
|
||||
// fragmentMain uses the same VertexOutput struct that vertexMain
|
||||
// returns, they do match.
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4f,
|
||||
@location(0) texture: vec2f,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vertexMain(input: VertexInput) -> VertexOutput
|
||||
{
|
||||
let position = vec4f(input.position * 0.5, 1.0);
|
||||
|
||||
var output: VertexOutput;
|
||||
output.position = position;
|
||||
output.texture = input.texture;
|
||||
return output;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f
|
||||
{
|
||||
return vec4(input.texture, 0.0, 1.0);
|
||||
}
|
||||
193
triangleTextured.js
Normal file
193
triangleTextured.js
Normal file
@ -0,0 +1,193 @@
|
||||
import { getPathText } from "./common.js"
|
||||
|
||||
// Triangle vertices, one vertex per line. Hand-transcribed from a
|
||||
// .obj file exported from Blender to this form.
|
||||
//
|
||||
// I've also written myriad scripts to do this automatically for
|
||||
// larger models.
|
||||
//
|
||||
// Float32Array is a fairly convenient way to convert floating point
|
||||
// literals to bytes.
|
||||
const triangleVertices = new Float32Array([
|
||||
// [position ] [texture ] [normal ]
|
||||
0.0, 1.0, 0.0, 0.5, 0.0, 0.0, 1.0, 0.0,
|
||||
1.0, -1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0,
|
||||
-1.0, -1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0,
|
||||
]);
|
||||
|
||||
class TriangleTexturedRenderer {
|
||||
constructor(device, canvasFormat, wgsl, image)
|
||||
{
|
||||
const label = "TriangleRenderer";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// shader module
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
const shaderModule = device.createShaderModule({
|
||||
label: `${label} shader`,
|
||||
code: wgsl,
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// pipeline
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// "bind group layouts" are a declaration of which `var` bindings
|
||||
// exist in the shader programs loaded by this pipeline
|
||||
const bindGroupLayouts = [
|
||||
device.createBindGroupLayout({
|
||||
label: "gltf bind group layout 0",
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: GPUShaderStage.FRAGMENT,
|
||||
sampler: {},
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
visibility: GPUShaderStage.FRAGMENT,
|
||||
texture: {},
|
||||
}
|
||||
]
|
||||
}),
|
||||
];
|
||||
|
||||
const pipelineLayout = device.createPipelineLayout({
|
||||
label: `${label} pipeline layout`,
|
||||
bindGroupLayouts: bindGroupLayouts,
|
||||
});
|
||||
|
||||
// vertexBufferLayouts describes the position of each element for
|
||||
// each buffer. There is only one vertex buffer used in this demo,
|
||||
// so there is only one vertex buffer layout in this list.
|
||||
//
|
||||
// shaderLocation matches the location numbers defined in
|
||||
// triangle.wgsl.
|
||||
//
|
||||
// "offset" is the starting offset in bytes, 4 bytes per floating
|
||||
// point number. This matches the arrangement in bytes of the
|
||||
// triangleVertices Float32Array.
|
||||
const arrayStride = (4 * 3) + (4 * 2) + (4 * 3);
|
||||
const vertexBufferLayouts = [
|
||||
{
|
||||
arrayStride: arrayStride,
|
||||
attributes: [{
|
||||
format: "float32x3",
|
||||
offset: 0,
|
||||
shaderLocation: 0,
|
||||
}, {
|
||||
format: "float32x2",
|
||||
offset: 12,
|
||||
shaderLocation: 1,
|
||||
}, {
|
||||
format: "float32x3",
|
||||
offset: 20,
|
||||
shaderLocation: 2,
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
this.renderPipeline = device.createRenderPipeline({
|
||||
label: `${label} render pipeline`,
|
||||
layout: pipelineLayout,
|
||||
vertex: {
|
||||
module: shaderModule,
|
||||
entryPoint: "vertexMain",
|
||||
buffers: vertexBufferLayouts,
|
||||
},
|
||||
fragment: {
|
||||
module: shaderModule,
|
||||
entryPoint: "fragmentMain",
|
||||
targets: [{
|
||||
format: canvasFormat,
|
||||
}]
|
||||
},
|
||||
primitive: {
|
||||
topology: "triangle-list",
|
||||
},
|
||||
depthStencil: {
|
||||
depthWriteEnabled: true,
|
||||
depthCompare: "less",
|
||||
format: "depth24plus",
|
||||
},
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// samplers and textures
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// default sampler configuration
|
||||
this.sampler = device.createSampler();
|
||||
|
||||
this.texture = device.createTexture({
|
||||
size: [image.width, image.height],
|
||||
format: 'rgba8unorm',
|
||||
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
||||
});
|
||||
|
||||
device.queue.copyExternalImageToTexture(
|
||||
{ source: image },
|
||||
{ texture: this.texture },
|
||||
{ width: image.width, height: image.height },
|
||||
);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// bind group
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
this.bindGroups = [
|
||||
device.createBindGroup({
|
||||
label: "${label} bind group",
|
||||
layout: bindGroupLayouts[0],
|
||||
entries: [{
|
||||
binding: 0,
|
||||
resource: this.sampler,
|
||||
}, {
|
||||
binding: 1,
|
||||
resource: this.texture,
|
||||
}],
|
||||
}),
|
||||
];
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// buffer
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
this.vertexBuffer = device.createBuffer({
|
||||
label: `${label} vertex buffer`,
|
||||
size: triangleVertices.byteLength,
|
||||
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
|
||||
// copy the (system RAM) triangleVertices to the (gpu RAM) vertexBuffer.
|
||||
device.queue.writeBuffer(this.vertexBuffer, 0, triangleVertices);
|
||||
}
|
||||
|
||||
render(renderPass)
|
||||
{
|
||||
renderPass.setPipeline(this.renderPipeline);
|
||||
renderPass.setVertexBuffer(0, this.vertexBuffer);
|
||||
renderPass.setBindGroup(0, this.bindGroups[0]);
|
||||
const vertexCount = 3;
|
||||
renderPass.draw(vertexCount);
|
||||
}
|
||||
}
|
||||
|
||||
// This "loadTriangleRenderer" function is separate from the
|
||||
// constructor because Javascript class constructors can't be async
|
||||
// functions.
|
||||
async function loadTriangleTexturedRenderer(device, canvasFormat)
|
||||
{
|
||||
const triangleWgsl = await getPathText("triangleTextured.wgsl");
|
||||
|
||||
const image = new Image();
|
||||
image.src = "texture.png";
|
||||
await image.decode();
|
||||
|
||||
const renderer = new TriangleTexturedRenderer(device, canvasFormat, triangleWgsl, image);
|
||||
|
||||
return renderer;
|
||||
}
|
||||
|
||||
export { loadTriangleTexturedRenderer };
|
||||
43
triangleTextured.wgsl
Normal file
43
triangleTextured.wgsl
Normal file
@ -0,0 +1,43 @@
|
||||
// The `var` group() numbers match the ordering of the layouts in the
|
||||
// `bindGroupLayouts` array, `0` being the 0th index of that array.
|
||||
//
|
||||
// The `var` binding() numbers match the numbers of the entries in
|
||||
// each layout in `bindGroupLayouts`.
|
||||
@group(0) @binding(0) var Sampler: sampler;
|
||||
@group(0) @binding(1) var Texture: texture_2d<f32>;
|
||||
|
||||
// The `VertexInput` location numbers match the numbers in
|
||||
// `vertexBufferLayouts`
|
||||
struct VertexInput {
|
||||
@location(0) position: vec3f,
|
||||
@location(1) texture: vec2f,
|
||||
@location(2) normal: vec3f,
|
||||
};
|
||||
|
||||
// location numbers for passing values from the vertex stage to the
|
||||
// fragment stage are arbitrary--they just need to match. Because
|
||||
// fragmentMain uses the same VertexOutput struct that vertexMain
|
||||
// returns, they do match.
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4f,
|
||||
@location(0) texture: vec2f,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vertexMain(input: VertexInput) -> VertexOutput
|
||||
{
|
||||
let position = vec4f(input.position * 0.5, 1.0);
|
||||
|
||||
var output: VertexOutput;
|
||||
output.position = position;
|
||||
output.texture = input.texture;
|
||||
return output;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f
|
||||
{
|
||||
let color = textureSample(Texture, Sampler, input.texture);
|
||||
|
||||
return vec4(color.xyz, 1.0);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user