Skip to content

Commit 8963482

Browse files
author
Nikos Katsikanis
committed
webgpu colour interpolation between vertices
1 parent 944b3b1 commit 8963482

File tree

1 file changed

+144
-59
lines changed

1 file changed

+144
-59
lines changed

js/routes/web-tutorial.js

Lines changed: 144 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,129 @@
11
// File: js/routes/web-tutorial.js
2-
// Draw a 25 % cyan square; header & footer remain visible.
3-
// Works by relying 100 % on flexlayout (no manual pixel math).
2+
// Draws a 25% square that can be toggled between filled and wireframe modes.
3+
// The site’s header and footer remain visible (pure flex layout, no manual pixel math).
44

5-
import { loadShader } from '../utils/shaderLoader.js'; // Helper to load WGSL from /wgsl
5+
import { loadShader } from '../utils/shaderLoader.js';
66

77
export default async function WebGPUFlatSquare(host) {
88
/* ───────────── 1 · LAYOUT (pure flex, no calculations) ───────────── */
99
host.replaceChildren(); // Remove any existing children in the host container
1010
host.style.cssText =
1111
'flex:1 0 auto; position:relative; overflow:hidden; background:#000; margin:0; padding:0;';
12-
// “host” now grows to fill the space between header & footer, and can contain an absolutely positioned canvas
12+
// “host” now grows to fill the space between header & footer, and can contain absolutelypositioned content.
1313

14+
// ─── Insert a “Wireframe” checkbox at the top‐left corner of the host ───
15+
const uiContainer = document.createElement('div');
16+
uiContainer.style.cssText =
17+
'position:absolute; top:10px; left:10px; z-index:10; color:white; font-family:sans-serif;';
18+
uiContainer.innerHTML = `
19+
<label style="font-size:1rem; user-select:none;">
20+
<input type="checkbox" id="wireframeToggle" style="transform:scale(1.2); margin-right:4px;" />
21+
Wireframe
22+
</label>
23+
`;
24+
host.appendChild(uiContainer);
25+
26+
const wireCheckbox = uiContainer.querySelector('#wireframeToggle');
27+
28+
/* ───────────── 2 · CANVAS CREATION & WEBGPU INIT ───────────── */
1429
const canvas = Object.assign(document.createElement('canvas'), {
1530
style: 'position:absolute; width:100%; height:100%; cursor:default;',
1631
});
17-
host.appendChild(canvas); // Insert the canvas into “host”
18-
19-
/* ───────────── 2 · WEBGPU INIT ───────────── */
20-
if (!navigator.gpu) return alert('WebGPU not supported'); // Abort if no WebGPU
21-
const adapter = await navigator.gpu.requestAdapter(); // Request a GPU adapter
22-
if (!adapter) return alert('No GPU adapter available'); // Abort if none
23-
const device = await adapter.requestDevice(); // Acquire a logical GPU device
24-
const ctx = canvas.getContext('webgpu'); // Get WebGPU rendering context
25-
const format = navigator.gpu.getPreferredCanvasFormat(); // Recommended swapchain format
26-
27-
const resize = () => { // Resize handler
28-
const dpr = devicePixelRatio; // Device‐pixel ratio
29-
canvas.width = host.clientWidth * dpr; // Physical pixel width
30-
canvas.height = host.clientHeight * dpr; // Physical pixel height
32+
host.appendChild(canvas);
33+
34+
if (!navigator.gpu) {
35+
return alert('WebGPU not supported');
36+
}
37+
const adapter = await navigator.gpu.requestAdapter();
38+
if (!adapter) {
39+
return alert('No GPU adapter available');
40+
}
41+
const device = await adapter.requestDevice();
42+
const ctx = canvas.getContext('webgpu');
43+
const format = navigator.gpu.getPreferredCanvasFormat();
44+
45+
const resize = () => {
46+
const dpr = devicePixelRatio;
47+
canvas.width = host.clientWidth * dpr;
48+
canvas.height = host.clientHeight * dpr;
3149
ctx.configure({
3250
device,
3351
format,
3452
alphaMode: 'premultiplied',
3553
size: [canvas.width, canvas.height],
36-
}); // Reconfigure the swapchain
54+
});
3755
};
38-
resize(); // Initial sizing
39-
addEventListener('resize', resize); // Update on window resize
56+
resize();
57+
window.addEventListener('resize', resize);
4058

41-
/* ───────────── 3 · SHADER & GEOMETRY ───────────── */
42-
const wgsl = await loadShader('square-flat.wgsl'); // Load WGSL from /wgsl/square-flat.wgsl
43-
const module = device.createShaderModule({ code: wgsl }); // Create GPUShaderModule
59+
/* ───────────── 3 · SHADER & GEOMETRY SETUP ───────────── */
60+
// Load WGSL source from /wgsl/square-flat.wgsl
61+
const wgsl = await loadShader('square-flat.wgsl');
62+
const module = device.createShaderModule({ code: wgsl });
4463

45-
const s = 0.5; // Half‐size in NDC space (±0.5 covers 50 % of viewport)
46-
// Vertex data: [x, y, z, r, g, b] for 4 corners of a square
64+
// In NDC, ±0.5 covers 50% of the viewport in both X and Y.
65+
const s = 0.5;
66+
67+
// Vertex layout: [x, y, z, r, g, b]
4768
const vertices = new Float32Array([
48-
-s, -s, 0, 0, 0, 1, // bottomleft (cyan)
49-
s, -s, 0, 0, 1, 0, // bottomright (cyan)
50-
s, s, 0, 1,0, 0, // topright (cyan)
51-
-s, s, 0, 1, 1,1, // topleft (cyan)
69+
-s, -s, 0, 0, 0, 1, // bottom-left (blue)
70+
s, -s, 0, 0, 1, 0, // bottom-right (green)
71+
s, s, 0, 1, 0, 0, // top-right (red)
72+
-s, s, 0, 1, 1, 1, // top-left (white)
5273
]);
53-
// Indices to form two triangles (0→1→2 and 0→2→3) from those 4 vertices
54-
const indices = new Uint16Array([0, 1, 2, 0, 2, 3,]);
5574

56-
// Create and upload vertex buffer
75+
// Filled‐mode indices (two triangles: 0→1→2 and 0→2→3)
76+
const triIndices = new Uint16Array([
77+
0, 1, 2,
78+
0, 2, 3,
79+
]);
80+
81+
// Wireframe indices: draw all four outer edges + the diagonal (0→2)
82+
const lineIndices = new Uint16Array([
83+
0, 1, // bottom edge
84+
1, 2, // right edge
85+
2, 3, // top edge
86+
3, 0, // left edge
87+
0, 2, // diagonal (connect bottom-left ↔ top-right)
88+
]);
89+
90+
// Create and upload the shared vertex buffer
5791
const vb = device.createBuffer({
5892
size: vertices.byteLength,
5993
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
6094
});
6195
device.queue.writeBuffer(vb, 0, vertices);
6296

63-
// Create and upload index buffer
64-
const ib = device.createBuffer({
65-
size: indices.byteLength,
97+
// Create and upload the triangle‐list index buffer
98+
const ibTriangles = device.createBuffer({
99+
size: triIndices.byteLength,
100+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
101+
});
102+
device.queue.writeBuffer(ibTriangles, 0, triIndices);
103+
104+
// Create and upload the line‐list index buffer
105+
const ibLines = device.createBuffer({
106+
size: lineIndices.byteLength,
66107
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
67108
});
68-
device.queue.writeBuffer(ib, 0, indices);
109+
device.queue.writeBuffer(ibLines, 0, lineIndices);
69110

70-
// Create render pipeline
71-
const pipeline = device.createRenderPipeline({
111+
/* ───────────── 4 · PIPELINE CREATION ───────────── */
112+
// Pipeline for filled triangles
113+
const pipelineFilled = device.createRenderPipeline({
72114
layout: 'auto',
73115
vertex: {
74116
module,
75117
entryPoint: 'vs_main',
76-
buffers: [{
77-
arrayStride: 24, // 3 floats for position + 3 floats for color = 6×4 bytes = 24 bytes
78-
attributes: [
79-
{ shaderLocation: 0, offset: 0, format: 'float32x3' }, // position
80-
{ shaderLocation: 1, offset: 12, format: 'float32x3' }, // color
81-
],
82-
}],
118+
buffers: [
119+
{
120+
arrayStride: 24, // 6 floats per vertex (3 position + 3 color) = 24 bytes
121+
attributes: [
122+
{ shaderLocation: 0, offset: 0, format: 'float32x3' }, // position
123+
{ shaderLocation: 1, offset: 12, format: 'float32x3' }, // color
124+
],
125+
},
126+
],
83127
},
84128
fragment: {
85129
module,
@@ -92,24 +136,65 @@ export default async function WebGPUFlatSquare(host) {
92136
},
93137
});
94138

95-
/* ───────────── 4 · RENDER LOOP ───────────── */
139+
// Pipeline for wireframe (line-list)
140+
const pipelineWire = device.createRenderPipeline({
141+
layout: 'auto',
142+
vertex: {
143+
module,
144+
entryPoint: 'vs_main',
145+
buffers: [
146+
{
147+
arrayStride: 24,
148+
attributes: [
149+
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
150+
{ shaderLocation: 1, offset: 12, format: 'float32x3' },
151+
],
152+
},
153+
],
154+
},
155+
fragment: {
156+
module,
157+
entryPoint: 'fs_main',
158+
targets: [{ format }],
159+
},
160+
primitive: {
161+
topology: 'line-list',
162+
cullMode: 'none',
163+
},
164+
});
165+
166+
/* ───────────── 5 · RENDER LOOP ───────────── */
96167
function frame() {
97-
const enc = device.createCommandEncoder(); // Begin command encoding
98-
const pass = enc.beginRenderPass({ // Start a render pass
168+
const encoder = device.createCommandEncoder();
169+
const pass = encoder.beginRenderPass({
99170
colorAttachments: [{
100-
view: ctx.getCurrentTexture().createView(), // Current swapchain texture
171+
view: ctx.getCurrentTexture().createView(),
101172
loadOp: 'clear',
102-
clearValue: { r: 0, g: 0, b: 0, a: 1 }, // Clear to opaque black
173+
clearValue: { r: 0, g: 0, b: 0, a: 1 },
103174
storeOp: 'store',
104175
}],
105176
});
106-
pass.setPipeline(pipeline); // Bind pipeline
107-
pass.setVertexBuffer(0, vb); // Bind the vertex buffer
108-
pass.setIndexBuffer(ib, 'uint16'); // Bind the index buffer
109-
pass.drawIndexed(6); // Draw 6 indices (2 triangles)
110-
pass.end(); // End the render pass
111-
device.queue.submit([enc.finish()]); // Submit to GPU queue
112-
requestAnimationFrame(frame); // Next frame
177+
178+
if (wireCheckbox.checked) {
179+
// Wireframe pipeline: bind line index buffer
180+
pass.setPipeline(pipelineWire);
181+
pass.setVertexBuffer(0, vb);
182+
pass.setIndexBuffer(ibLines, 'uint16');
183+
// Draw 5 line segments → 10 indices total
184+
pass.drawIndexed(lineIndices.length, 1, 0, 0, 0);
185+
} else {
186+
// Filled pipeline: bind triangle index buffer
187+
pass.setPipeline(pipelineFilled);
188+
pass.setVertexBuffer(0, vb);
189+
pass.setIndexBuffer(ibTriangles, 'uint16');
190+
// Draw 2 triangles → 6 indices total
191+
pass.drawIndexed(triIndices.length, 1, 0, 0, 0);
192+
}
193+
194+
pass.end();
195+
device.queue.submit([encoder.finish()]);
196+
requestAnimationFrame(frame);
113197
}
114-
requestAnimationFrame(frame); // Kick off rendering
198+
199+
requestAnimationFrame(frame);
115200
}

0 commit comments

Comments
 (0)