├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── animometer.mjs ├── assets └── grass-block.png ├── compute-boids.mjs ├── hello-triangle-buffers.mjs ├── hello-triangle-msaa.mjs ├── hello-triangle.mjs ├── interactive-triangle.mjs ├── multiple-render-targets.mjs ├── package.json ├── ray-tracing ├── index.mjs └── shaders │ ├── ray-closest-hit.rchit │ ├── ray-generation.rgen │ ├── ray-miss.rmiss │ ├── screen.frag │ └── screen.vert ├── rotating-cube.mjs ├── rotating-triangle.mjs ├── storage-texture.mjs ├── swapchain-blit.mjs ├── swapchain-recreation.mjs ├── texture-array.mjs └── textured-cube.mjs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | package-lock.json 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Felix Maier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webgpu-examples 2 | Examples for [node-webgpu](https://github.com/maierfelix/webgpu) 3 | 4 | # Usage 5 | - Clone this repository 6 | - Run `npm install` 7 | - To run a demo, run e.g. `node hello-triangle.mjs` 8 | -------------------------------------------------------------------------------- /animometer.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | import { performance } from "perf_hooks"; 3 | 4 | Object.assign(global, WebGPU); 5 | 6 | const vsSrc = ` 7 | #version 450 8 | #pragma shader_stage(vertex) 9 | 10 | layout(std140, set = 0, binding = 0) uniform Time { 11 | float time; 12 | }; 13 | 14 | layout(std140, set = 1, binding = 0) uniform Uniforms { 15 | float scale; 16 | float offsetX; 17 | float offsetY; 18 | float scalar; 19 | float scalarOffset; 20 | }; 21 | 22 | layout(location = 0) in vec2 position; 23 | layout(location = 1) in vec3 color; 24 | 25 | layout(location = 0) out vec4 vColor; 26 | 27 | void main() { 28 | float fade = mod(scalarOffset + time * scalar / 10.0, 1.0); 29 | if (fade < 0.5) { 30 | fade = fade * 2.0; 31 | } else { 32 | fade = (1.0 - fade) * 2.0; 33 | } 34 | float xpos = position.x * scale; 35 | float ypos = position.y * scale; 36 | float angle = 3.14159 * 2.0 * fade; 37 | float xrot = xpos * cos(angle) - ypos * sin(angle); 38 | float yrot = xpos * sin(angle) + ypos * cos(angle); 39 | xpos = xrot + offsetX; 40 | ypos = yrot + offsetY; 41 | vColor = vec4(fade, 1.0 - fade, 0.0, 1.0) + vec4(color, 1); 42 | gl_Position = vec4(xpos, ypos, 0.0, 1.0); 43 | } 44 | `; 45 | 46 | const fsSrc = ` 47 | #version 450 48 | #pragma shader_stage(fragment) 49 | 50 | layout(location = 0) in vec4 vColor; 51 | 52 | layout(location = 0) out vec4 outColor; 53 | 54 | void main() { 55 | outColor = vColor; 56 | } 57 | `; 58 | 59 | (async function main() { 60 | 61 | const triangleVertices = new Float32Array([ 62 | 0.0, 0.1, 0.0, 0.0, 0.0, 63 | -0.1, -0.1, 0.0, 1.0, 0.0, 64 | 0.1, -0.1, 0.0, 0.0, 1.0 65 | ]); 66 | 67 | const window = new WebGPUWindow({ 68 | width: 640, 69 | height: 480, 70 | title: "WebGPU", 71 | resizable: false 72 | }); 73 | 74 | const adapter = await GPU.requestAdapter({ window }); 75 | 76 | const device = await adapter.requestDevice(); 77 | 78 | const context = window.getContext("webgpu"); 79 | 80 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 81 | 82 | const swapChain = context.configureSwapChain({ 83 | device: device, 84 | format: swapChainFormat, 85 | usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.OUTPUT_ATTACHMENT 86 | }); 87 | 88 | const stagedVertexBuffer = device.createBuffer({ 89 | size: triangleVertices.byteLength, 90 | usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST 91 | }); 92 | stagedVertexBuffer.setSubData(0, triangleVertices); 93 | 94 | const vertexShaderModule = device.createShaderModule({ code: vsSrc }); 95 | const fragmentShaderModule = device.createShaderModule({ code: fsSrc }); 96 | 97 | const timeBindGroupLayout = device.createBindGroupLayout({ 98 | entries: [ 99 | { 100 | binding: 0, 101 | visibility: GPUShaderStage.VERTEX, 102 | type: "uniform-buffer" 103 | }, 104 | ] 105 | }); 106 | 107 | const bindGroupLayout = device.createBindGroupLayout({ 108 | entries: [ 109 | { 110 | binding: 0, 111 | visibility: GPUShaderStage.VERTEX, 112 | type: "uniform-buffer" 113 | }, 114 | ] 115 | }); 116 | 117 | const dynamicBindGroupLayout = device.createBindGroupLayout({ 118 | entries: [ 119 | { 120 | binding: 0, 121 | visibility: GPUShaderStage.VERTEX, 122 | type: "uniform-buffer", 123 | hasDynamicOffset: true 124 | }, 125 | ] 126 | }); 127 | 128 | const pipelineLayout = device.createPipelineLayout({ 129 | bindGroupLayouts: [timeBindGroupLayout, bindGroupLayout] 130 | }); 131 | 132 | const dynamicPipelineLayout = device.createPipelineLayout({ 133 | bindGroupLayouts: [timeBindGroupLayout, dynamicBindGroupLayout] 134 | }); 135 | 136 | const pipelineDesc = { 137 | sampleCount: 1, 138 | vertexStage: { 139 | module: vertexShaderModule, 140 | entryPoint: "main" 141 | }, 142 | fragmentStage: { 143 | module: fragmentShaderModule, 144 | entryPoint: "main" 145 | }, 146 | primitiveTopology: "triangle-list", 147 | vertexState: { 148 | indexFormat: "uint32", 149 | vertexBuffers: [ 150 | { 151 | arrayStride: 5 * Float32Array.BYTES_PER_ELEMENT, 152 | stepMode: "vertex", 153 | attributes: [ 154 | { 155 | shaderLocation: 0, 156 | offset: 0 * Float32Array.BYTES_PER_ELEMENT, 157 | format: "float2" 158 | }, 159 | { 160 | shaderLocation: 1, 161 | offset: 2 * Float32Array.BYTES_PER_ELEMENT, 162 | format: "float3" 163 | } 164 | ] 165 | }, 166 | ] 167 | }, 168 | rasterizationState: { 169 | frontFace: "CCW", 170 | cullMode: "none" 171 | }, 172 | colorStates: [{ 173 | format: swapChainFormat, 174 | alphaBlend: {}, 175 | colorBlend: {} 176 | }] 177 | }; 178 | 179 | const pipeline = device.createRenderPipeline(Object.assign({ 180 | layout: pipelineLayout 181 | }, pipelineDesc)); 182 | 183 | const dynamicPipeline = device.createRenderPipeline(Object.assign({ 184 | layout: dynamicPipelineLayout 185 | }, pipelineDesc)); 186 | 187 | function configure() { 188 | const numTriangles = 25000; 189 | const uniformBytes = 5 * Float32Array.BYTES_PER_ELEMENT; 190 | const alignedUniformBytes = Math.ceil(uniformBytes / 256) * 256; 191 | const alignedUniformFloats = alignedUniformBytes / Float32Array.BYTES_PER_ELEMENT; 192 | const uniformBuffer = device.createBuffer({ 193 | size: numTriangles * alignedUniformBytes + Float32Array.BYTES_PER_ELEMENT, 194 | usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM 195 | }); 196 | const uniformBufferData = new Float32Array(numTriangles * alignedUniformFloats); 197 | const bindGroups = new Array(numTriangles); 198 | for (let i = 0; i < numTriangles; ++i) { 199 | uniformBufferData[alignedUniformFloats * i + 0] = Math.random() * 0.2 + 0.2; // scale 200 | uniformBufferData[alignedUniformFloats * i + 1] = 0.9 * 2 * (Math.random() - 0.5); // offsetX 201 | uniformBufferData[alignedUniformFloats * i + 2] = 0.9 * 2 * (Math.random() - 0.5); // offsetY 202 | uniformBufferData[alignedUniformFloats * i + 3] = Math.random() * 1.5 + 0.5; // scalar 203 | uniformBufferData[alignedUniformFloats * i + 4] = Math.random() * 10; // scalarOffset 204 | bindGroups[i] = device.createBindGroup({ 205 | layout: bindGroupLayout, 206 | entries: [{ 207 | binding: 0, 208 | buffer: uniformBuffer, 209 | offset: i * alignedUniformBytes, 210 | size: 6 * Float32Array.BYTES_PER_ELEMENT 211 | }] 212 | }); 213 | } 214 | const dynamicBindGroup = device.createBindGroup({ 215 | layout: dynamicBindGroupLayout, 216 | entries: [{ 217 | binding: 0, 218 | buffer: uniformBuffer, 219 | offset: 0, 220 | size: 6 * Float32Array.BYTES_PER_ELEMENT 221 | }], 222 | }); 223 | const timeOffset = numTriangles * alignedUniformBytes; 224 | const timeBindGroup = device.createBindGroup({ 225 | layout: timeBindGroupLayout, 226 | entries: [{ 227 | binding: 0, 228 | buffer: uniformBuffer, 229 | offset: timeOffset, 230 | size: Float32Array.BYTES_PER_ELEMENT 231 | }] 232 | }); 233 | // Chrome currently crashes with |setSubData| too large. 234 | const maxSetSubDataLength = 14 * 1024 * 1024 / Float32Array.BYTES_PER_ELEMENT; 235 | for (let offset = 0; offset < uniformBufferData.length; offset += maxSetSubDataLength) { 236 | uniformBuffer.setSubData( 237 | offset, 238 | new Float32Array( 239 | uniformBufferData.buffer, 240 | offset * Float32Array.BYTES_PER_ELEMENT, 241 | Math.min(uniformBufferData.length - offset, maxSetSubDataLength) 242 | ) 243 | ); 244 | } 245 | function createCommandBuffer(textureView) { 246 | const commandEncoder = device.createCommandEncoder({}); 247 | const renderPassDescriptor = { 248 | colorAttachments: [{ 249 | clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 250 | loadOp: "clear", 251 | storeOp: "store", 252 | attachment: textureView 253 | }], 254 | }; 255 | const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); 256 | if (/*settings.dynamicOffsets*/true) { 257 | passEncoder.setPipeline(dynamicPipeline); 258 | } else { 259 | passEncoder.setPipeline(pipeline); 260 | } 261 | passEncoder.setVertexBuffer(0, stagedVertexBuffer, 0); 262 | passEncoder.setBindGroup(0, timeBindGroup); 263 | const dynamicOffsets = [0]; 264 | for (let i = 0; i < numTriangles; ++i) { 265 | if (/*settings.dynamicOffsets*/true) { 266 | dynamicOffsets[0] = (i * alignedUniformBytes); 267 | passEncoder.setBindGroup(1, dynamicBindGroup, dynamicOffsets); 268 | } else { 269 | passEncoder.setBindGroup(1, bindGroups[i]); 270 | } 271 | passEncoder.draw(3, 1, 0, 0); 272 | } 273 | passEncoder.endPass(); 274 | return commandEncoder.finish(); 275 | } 276 | const reusableFrames = []; 277 | for (let i = 0; i < 2; ++i) { 278 | const texture = device.createTexture({ 279 | size: { width: window.width, height: window.height, depth: 1 }, 280 | arrayLayerCount: 1, 281 | mipLevelCount: 1, 282 | sampleCount: 1, 283 | dimension: "2d", 284 | format: swapChainFormat, 285 | usage: GPUTextureUsage.OUTPUT_ATTACHMENT | GPUTextureUsage.COPY_SRC, 286 | }); 287 | reusableFrames.push({ 288 | commandBuffer: createCommandBuffer(texture.createView({ format: swapChainFormat })), 289 | texture, 290 | }); 291 | } 292 | const uniformTime = new Float32Array([0]); 293 | let i = 0; 294 | let startTime = undefined; 295 | return function doDraw(timestamp) { 296 | if (startTime === undefined) { 297 | startTime = timestamp; 298 | } 299 | uniformTime[0] = (timestamp - startTime) / 1000; 300 | uniformBuffer.setSubData(timeOffset, uniformTime); 301 | /*if (true) { 302 | const { commandBuffer, texture } = reusableFrames[(i + 1) % reusableFrames.length]; 303 | i++; 304 | const commandEncoder = device.createCommandEncoder({}); 305 | commandEncoder.copyTextureToTexture( 306 | { 307 | texture: texture, 308 | mipLevel: 0, 309 | arrayLayer: 0, 310 | origin: { x: 0, y: 0, z: 0 } 311 | }, 312 | { 313 | texture: swapChain.getCurrentTexture(), 314 | mipLevel: 0, 315 | arrayLayer: 0, 316 | origin: { x: 0, y: 0, z: 0 } 317 | }, 318 | { 319 | width: window.width, 320 | height: window.height, 321 | depth: 1, 322 | }, 323 | ); 324 | device.getQueue().submit([ 325 | commandBuffer, 326 | commandEncoder.finish(), 327 | ]); 328 | } else */{ 329 | const textureView = swapChain.getCurrentTextureView(); 330 | device.getQueue().submit([ createCommandBuffer(textureView) ]); 331 | } 332 | } 333 | } 334 | 335 | let doDraw = configure(); 336 | const updateSettings = () => { doDraw = configure(); } 337 | 338 | let previousFrameTimestamp = undefined; 339 | let jsTimeAvg = undefined; 340 | let frameTimeAvg = undefined; 341 | let updateDisplay = true; 342 | let i = 0; 343 | let then = performance.now(); 344 | let timestamp = 0; 345 | function onFrame() { 346 | let now = performance.now(); 347 | timestamp += (now - then); 348 | then = now; 349 | let frameTime = 0; 350 | if (previousFrameTimestamp !== undefined) { 351 | frameTime = timestamp - previousFrameTimestamp; 352 | } 353 | previousFrameTimestamp = timestamp; 354 | const start = performance.now(); 355 | doDraw(timestamp); 356 | const jsTime = performance.now() - start; 357 | if (frameTimeAvg === undefined) { 358 | frameTimeAvg = frameTime; 359 | } 360 | if (jsTimeAvg === undefined) { 361 | jsTimeAvg = jsTime; 362 | } 363 | const w = 0.2; 364 | frameTimeAvg = (1 - w) * frameTimeAvg + w * frameTime; 365 | jsTimeAvg = (1 - w) * jsTimeAvg + w * jsTime; 366 | if (updateDisplay) { 367 | window.title = `Avg Javascript: ${jsTimeAvg.toFixed(2)} ms\nAvg Frame: ${frameTimeAvg.toFixed(2)} ms`; 368 | 369 | } 370 | swapChain.present(); 371 | window.pollEvents(); 372 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 373 | } 374 | setTimeout(onFrame, 1e3 / 60); 375 | 376 | })(); 377 | -------------------------------------------------------------------------------- /assets/grass-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maierfelix/webgpu-examples/dc9c8b79403b6c6b0e41ac989a011dd6a92555fa/assets/grass-block.png -------------------------------------------------------------------------------- /compute-boids.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | 3 | Object.assign(global, WebGPU); 4 | 5 | const numParticles = 2000; 6 | 7 | const vsSrc = ` 8 | #version 450 9 | #pragma shader_stage(vertex) 10 | 11 | layout(location = 0) in vec2 a_particlePos; 12 | layout(location = 1) in vec2 a_particleVel; 13 | layout(location = 2) in vec2 a_pos; 14 | 15 | void main() { 16 | float angle = -atan(a_particleVel.x, a_particleVel.y); 17 | vec2 pos = vec2( 18 | a_pos.x * cos(angle) - a_pos.y * sin(angle), 19 | a_pos.x * sin(angle) + a_pos.y * cos(angle) 20 | ); 21 | gl_Position = vec4(pos + a_particlePos, 0, 1); 22 | } 23 | `; 24 | 25 | const fsSrc = ` 26 | #version 450 27 | #pragma shader_stage(fragment) 28 | 29 | layout(location = 0) out vec4 outColor; 30 | 31 | void main() { 32 | outColor = vec4(1.0); 33 | } 34 | `; 35 | 36 | const csSrc = ` 37 | #version 450 38 | #pragma shader_stage(compute) 39 | 40 | struct Particle { 41 | vec2 pos; 42 | vec2 vel; 43 | }; 44 | 45 | layout(std140, set = 0, binding = 0) uniform SimParams { 46 | float deltaT; 47 | float rule1Distance; 48 | float rule2Distance; 49 | float rule3Distance; 50 | float rule1Scale; 51 | float rule2Scale; 52 | float rule3Scale; 53 | } params; 54 | 55 | layout(std140, set = 0, binding = 1) buffer ParticlesA { 56 | Particle particles[${numParticles}]; 57 | } particlesA; 58 | 59 | layout(std140, set = 0, binding = 2) buffer ParticlesB { 60 | Particle particles[${numParticles}]; 61 | } particlesB; 62 | 63 | void main() { 64 | // https://github.com/austinEng/Project6-Vulkan-Flocking/blob/master/data/shaders/computeparticles/particle.comp 65 | uint index = gl_GlobalInvocationID.x; 66 | if (index >= ${numParticles}) { return; } 67 | vec2 vPos = particlesA.particles[index].pos; 68 | vec2 vVel = particlesA.particles[index].vel; 69 | vec2 cMass = vec2(0.0, 0.0); 70 | vec2 cVel = vec2(0.0, 0.0); 71 | vec2 colVel = vec2(0.0, 0.0); 72 | int cMassCount = 0; 73 | int cVelCount = 0; 74 | vec2 pos; 75 | vec2 vel; 76 | for (int i = 0; i < ${numParticles}; ++i) { 77 | if (i == index) { continue; } 78 | pos = particlesA.particles[i].pos.xy; 79 | vel = particlesA.particles[i].vel.xy; 80 | if (distance(pos, vPos) < params.rule1Distance) { 81 | cMass += pos; 82 | cMassCount++; 83 | } 84 | if (distance(pos, vPos) < params.rule2Distance) { 85 | colVel -= (pos - vPos); 86 | } 87 | if (distance(pos, vPos) < params.rule3Distance) { 88 | cVel += vel; 89 | cVelCount++; 90 | } 91 | } 92 | if (cMassCount > 0) { 93 | cMass = cMass / cMassCount - vPos; 94 | } 95 | if (cVelCount > 0) { 96 | cVel = cVel / cVelCount; 97 | } 98 | vVel += cMass * params.rule1Scale + colVel * params.rule2Scale + cVel * params.rule3Scale; 99 | // clamp velocity for a more pleasing simulation. 100 | vVel = normalize(vVel) * clamp(length(vVel), 0.0, 0.1); 101 | // kinematic update 102 | vPos += vVel * params.deltaT; 103 | // Wrap around boundary 104 | if (vPos.x < -1.0) vPos.x = 1.0; 105 | if (vPos.x > 1.0) vPos.x = -1.0; 106 | if (vPos.y < -1.0) vPos.y = 1.0; 107 | if (vPos.y > 1.0) vPos.y = -1.0; 108 | particlesB.particles[index].pos = vPos; 109 | // Write back 110 | particlesB.particles[index].vel = vVel; 111 | } 112 | `; 113 | 114 | (async function main() { 115 | 116 | const window = new WebGPUWindow({ 117 | width: 640, 118 | height: 480, 119 | title: "WebGPU", 120 | resizable: false 121 | }); 122 | 123 | const adapter = await GPU.requestAdapter({ window }); 124 | 125 | const device = await adapter.requestDevice(); 126 | 127 | const queue = device.getQueue(); 128 | 129 | const context = window.getContext("webgpu"); 130 | 131 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 132 | 133 | let swapChain = context.configureSwapChain({ 134 | device: device, 135 | format: swapChainFormat 136 | }); 137 | window.onresize = e => { 138 | swapChain.reconfigure({ 139 | format: swapChainFormat 140 | }); 141 | }; 142 | 143 | const vertexShaderModule = device.createShaderModule({ code: vsSrc }); 144 | const fragmentShaderModule = device.createShaderModule({ code: fsSrc }); 145 | const computeShaderModule = device.createShaderModule({ code: csSrc }); 146 | 147 | const renderPipeline = device.createRenderPipeline({ 148 | layout: device.createPipelineLayout({ bindGroupLayouts: [] }), 149 | sampleCount: 1, 150 | vertexStage: { 151 | module: vertexShaderModule, 152 | entryPoint: "main" 153 | }, 154 | fragmentStage: { 155 | module: fragmentShaderModule, 156 | entryPoint: "main" 157 | }, 158 | primitiveTopology: "triangle-list", 159 | vertexState: { 160 | indexFormat: "uint32", 161 | vertexBuffers: [ 162 | { 163 | arrayStride: 4 * Float32Array.BYTES_PER_ELEMENT, 164 | stepMode: "instance", 165 | attributes: [ 166 | { 167 | shaderLocation: 0, 168 | offset: 0 * Float32Array.BYTES_PER_ELEMENT, 169 | format: "float2" 170 | }, 171 | { 172 | shaderLocation: 1, 173 | offset: 2 * Float32Array.BYTES_PER_ELEMENT, 174 | format: "float2" 175 | } 176 | ] 177 | }, 178 | { 179 | arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, 180 | stepMode: "vertex", 181 | attributes: [ 182 | { 183 | shaderLocation: 2, 184 | offset: 0 * Float32Array.BYTES_PER_ELEMENT, 185 | format: "float2" 186 | } 187 | ] 188 | } 189 | ] 190 | }, 191 | rasterizationState: { 192 | frontFace: "CCW", 193 | cullMode: "none" 194 | }, 195 | colorStates: [{ 196 | format: swapChainFormat, 197 | alphaBlend: {}, 198 | colorBlend: {} 199 | }] 200 | }); 201 | 202 | const computeBindGroupLayout = device.createBindGroupLayout({ 203 | entries: [ 204 | { binding: 0, visibility: GPUShaderStage.COMPUTE, type: "uniform-buffer" }, 205 | { binding: 1, visibility: GPUShaderStage.COMPUTE, type: "storage-buffer" }, 206 | { binding: 2, visibility: GPUShaderStage.COMPUTE, type: "storage-buffer" } 207 | ] 208 | }); 209 | 210 | const computePipeline = device.createComputePipeline({ 211 | layout: device.createPipelineLayout({ bindGroupLayouts: [ computeBindGroupLayout ] }), 212 | computeStage: { 213 | module: computeShaderModule, 214 | entryPoint: "main" 215 | } 216 | }); 217 | 218 | const vertexBufferData = new Float32Array([-0.01, -0.02, 0.01, -0.02, 0.00, 0.02]); 219 | const verticesBuffer = device.createBuffer({ 220 | size: vertexBufferData.byteLength, 221 | usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, 222 | }); 223 | verticesBuffer.setSubData(0, vertexBufferData); 224 | const simParamData = new Float32Array([0.04, 0.1, 0.025, 0.025, 0.02, 0.05, 0.005]); 225 | const simParamBuffer = device.createBuffer({ 226 | size: simParamData.byteLength, 227 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, 228 | }); 229 | simParamBuffer.setSubData(0, simParamData); 230 | const initialParticleData = new Float32Array(numParticles * 4); 231 | for (let ii = 0; ii < numParticles; ++ii) { 232 | initialParticleData[4 * ii + 0] = 2 * (Math.random() - 0.5); 233 | initialParticleData[4 * ii + 1] = 2 * (Math.random() - 0.5); 234 | initialParticleData[4 * ii + 2] = 2 * (Math.random() - 0.5) * 0.1; 235 | initialParticleData[4 * ii + 3] = 2 * (Math.random() - 0.5) * 0.1; 236 | }; 237 | const particleBuffers = new Array(2); 238 | const particleBindGroups = new Array(2); 239 | for (let ii = 0; ii < 2; ++ii) { 240 | particleBuffers[ii] = device.createBuffer({ 241 | size: initialParticleData.byteLength, 242 | usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE 243 | }); 244 | particleBuffers[ii].setSubData(0, initialParticleData); 245 | }; 246 | for (let ii = 0; ii < 2; ++ii) { 247 | particleBindGroups[ii] = device.createBindGroup({ 248 | layout: computeBindGroupLayout, 249 | entries: [{ 250 | binding: 0, 251 | buffer: simParamBuffer, 252 | offset: 0, 253 | size: simParamData.byteLength 254 | }, { 255 | binding: 1, 256 | buffer: particleBuffers[ii], 257 | offset: 0, 258 | size: initialParticleData.byteLength 259 | }, { 260 | binding: 2, 261 | buffer: particleBuffers[(ii + 1) % 2], 262 | offset: 0, 263 | size: initialParticleData.byteLength 264 | }], 265 | }); 266 | }; 267 | 268 | let frames = 0; 269 | function onFrame() { 270 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 271 | 272 | const backBufferView = swapChain.getCurrentTextureView(); 273 | 274 | const commandEncoder = device.createCommandEncoder({}); 275 | { 276 | const passEncoder = commandEncoder.beginComputePass({}); 277 | passEncoder.setPipeline(computePipeline); 278 | passEncoder.setBindGroup(0, particleBindGroups[frames % 2]); 279 | passEncoder.dispatch(numParticles); 280 | passEncoder.endPass(); 281 | } 282 | 283 | { 284 | const passEncoder = commandEncoder.beginRenderPass({ 285 | colorAttachments: [{ 286 | clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 287 | loadOp: "clear", 288 | storeOp: "store", 289 | attachment: backBufferView 290 | }] 291 | }); 292 | passEncoder.setPipeline(renderPipeline); 293 | passEncoder.setVertexBuffer(0, particleBuffers[(frames + 1) % 2], 0); 294 | passEncoder.setVertexBuffer(1, verticesBuffer, 0); 295 | passEncoder.draw(3, numParticles, 0, 0); 296 | passEncoder.endPass(); 297 | } 298 | queue.submit([ commandEncoder.finish() ]); 299 | 300 | swapChain.present(); 301 | window.pollEvents(); 302 | frames++; 303 | }; 304 | setTimeout(onFrame, 1e3 / 60); 305 | 306 | })(); 307 | -------------------------------------------------------------------------------- /hello-triangle-buffers.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | 3 | Object.assign(global, WebGPU); 4 | 5 | const vsSrc = ` 6 | #version 450 7 | #pragma shader_stage(vertex) 8 | layout(location = 0) in vec2 position; 9 | void main() { 10 | gl_Position = vec4(position, 0.0, 1.0); 11 | } 12 | `; 13 | 14 | const fsSrc = ` 15 | #version 450 16 | #pragma shader_stage(fragment) 17 | layout(location = 0) out vec4 outColor; 18 | void main() { 19 | outColor = vec4(1.0, 0.0, 0.0, 1.0); 20 | } 21 | `; 22 | 23 | (async function main() { 24 | 25 | const triangleVertices = new Float32Array([ 26 | 0.0, 0.5, 27 | -0.5, -0.5, 28 | 0.5, -0.5 29 | ]); 30 | 31 | const triangleIndices = new Uint32Array([ 32 | 0, 1, 2 33 | ]); 34 | 35 | const window = new WebGPUWindow({ 36 | width: 640, 37 | height: 480, 38 | title: "WebGPU" 39 | }); 40 | 41 | const context = window.getContext("webgpu"); 42 | 43 | const adapter = await GPU.requestAdapter({ window }); 44 | 45 | const device = await adapter.requestDevice(); 46 | 47 | const queue = device.getQueue(); 48 | 49 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 50 | 51 | const swapChain = context.configureSwapChain({ 52 | device: device, 53 | format: swapChainFormat 54 | }); 55 | 56 | // demonstrate verbose staging process 57 | const stagingVertexBuffer = device.createBuffer({ 58 | size: triangleVertices.byteLength, 59 | usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC 60 | }); 61 | const stagingVertexBufferView = await stagingVertexBuffer.mapWriteAsync(); 62 | new Float32Array(stagingVertexBufferView).set(triangleVertices, 0); 63 | stagingVertexBuffer.unmap(); 64 | 65 | const stagedVertexBuffer = device.createBuffer({ 66 | size: triangleVertices.byteLength, 67 | usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST 68 | }); 69 | 70 | // staging->staged buffer 71 | const bufferCopyEncoder = device.createCommandEncoder({}); 72 | bufferCopyEncoder.copyBufferToBuffer( 73 | stagingVertexBuffer, 74 | 0, 75 | stagedVertexBuffer, 76 | 0, 77 | triangleVertices.byteLength 78 | ); 79 | queue.submit([ bufferCopyEncoder.finish() ]); 80 | 81 | // staging shortcut using buffer.setSubData 82 | const stagedIndexBuffer = device.createBuffer({ 83 | size: triangleIndices.byteLength, 84 | usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST 85 | }); 86 | stagedIndexBuffer.setSubData(0, triangleIndices); 87 | 88 | const layout = device.createPipelineLayout({ 89 | bindGroupLayouts: [] 90 | }); 91 | 92 | const vertexShaderModule = device.createShaderModule({ code: vsSrc }); 93 | const fragmentShaderModule = device.createShaderModule({ code: fsSrc }); 94 | 95 | const pipeline = device.createRenderPipeline({ 96 | layout, 97 | sampleCount: 1, 98 | vertexStage: { 99 | module: vertexShaderModule, 100 | entryPoint: "main" 101 | }, 102 | fragmentStage: { 103 | module: fragmentShaderModule, 104 | entryPoint: "main" 105 | }, 106 | primitiveTopology: "triangle-list", 107 | vertexState: { 108 | indexFormat: "uint32", 109 | vertexBuffers: [{ 110 | arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, 111 | stepMode: "vertex", 112 | attributes: [{ 113 | shaderLocation: 0, 114 | offset: 0, 115 | format: "float2" 116 | }] 117 | }] 118 | }, 119 | rasterizationState: { 120 | frontFace: "CCW", 121 | cullMode: "none" 122 | }, 123 | colorStates: [{ 124 | format: swapChainFormat, 125 | alphaBlend: {}, 126 | colorBlend: {} 127 | }] 128 | }); 129 | 130 | function onFrame() { 131 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 132 | 133 | const backBufferView = swapChain.getCurrentTextureView(); 134 | const commandEncoder = device.createCommandEncoder({}); 135 | const renderPass = commandEncoder.beginRenderPass({ 136 | colorAttachments: [{ 137 | clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 138 | loadOp: "clear", 139 | storeOp: "store", 140 | attachment: backBufferView 141 | }] 142 | }); 143 | renderPass.setPipeline(pipeline); 144 | renderPass.setVertexBuffer(0, stagedVertexBuffer, 0); 145 | renderPass.setIndexBuffer(stagedIndexBuffer); 146 | renderPass.drawIndexed(triangleIndices.length, 1, 0, 0, 0); 147 | renderPass.endPass(); 148 | 149 | const commandBuffer = commandEncoder.finish(); 150 | queue.submit([ commandBuffer ]); 151 | swapChain.present(); 152 | window.pollEvents(); 153 | }; 154 | setTimeout(onFrame, 1e3 / 60); 155 | 156 | })(); 157 | -------------------------------------------------------------------------------- /hello-triangle-msaa.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | 3 | Object.assign(global, WebGPU); 4 | 5 | const vsSrc = ` 6 | #version 450 7 | #pragma shader_stage(vertex) 8 | const vec2 pos[3] = vec2[3]( 9 | vec2(0.0f, 0.5f), 10 | vec2(-0.5f, -0.5f), 11 | vec2(0.5f, -0.5f) 12 | ); 13 | void main() { 14 | gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0); 15 | } 16 | `; 17 | 18 | const fsSrc = ` 19 | #version 450 20 | #pragma shader_stage(fragment) 21 | layout(location = 0) out vec4 outColor; 22 | void main() { 23 | outColor = vec4(1.0, 0.0, 0.0, 1.0); 24 | } 25 | `; 26 | 27 | (async function main() { 28 | 29 | const window = new WebGPUWindow({ 30 | width: 640, 31 | height: 480, 32 | title: "WebGPU", 33 | resizable: false 34 | }); 35 | 36 | const adapter = await GPU.requestAdapter({ window }); 37 | 38 | const device = await adapter.requestDevice(); 39 | 40 | const queue = device.getQueue(); 41 | 42 | const context = window.getContext("webgpu"); 43 | 44 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 45 | 46 | const swapChain = context.configureSwapChain({ 47 | device: device, 48 | format: swapChainFormat 49 | }); 50 | 51 | const layout = device.createPipelineLayout({ 52 | bindGroupLayouts: [] 53 | }); 54 | 55 | const vertexShaderModule = device.createShaderModule({ code: vsSrc }); 56 | const fragmentShaderModule = device.createShaderModule({ code: fsSrc }); 57 | 58 | const pipeline = device.createRenderPipeline({ 59 | layout, 60 | vertexStage: { 61 | module: vertexShaderModule, 62 | entryPoint: "main" 63 | }, 64 | fragmentStage: { 65 | module: fragmentShaderModule, 66 | entryPoint: "main" 67 | }, 68 | primitiveTopology: "triangle-list", 69 | vertexInput: { 70 | indexFormat: "uint32", 71 | buffers: [] 72 | }, 73 | rasterizationState: { 74 | frontFace: "CCW", 75 | cullMode: "none" 76 | }, 77 | colorStates: [{ 78 | format: swapChainFormat, 79 | alphaBlend: {}, 80 | colorBlend: {} 81 | }], 82 | sampleCount: 4 83 | }); 84 | 85 | const texture = device.createTexture({ 86 | size: { 87 | width: 640, 88 | height: 480, 89 | depth: 1 90 | }, 91 | sampleCount: 4, 92 | format: swapChainFormat, 93 | usage: GPUTextureUsage.OUTPUT_ATTACHMENT 94 | }); 95 | 96 | const attachment = texture.createView({ 97 | format: swapChainFormat 98 | }); 99 | 100 | function onFrame() { 101 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 102 | 103 | const backBufferView = swapChain.getCurrentTextureView(); 104 | 105 | const commandEncoder = device.createCommandEncoder({}); 106 | const renderPass = commandEncoder.beginRenderPass({ 107 | colorAttachments: [{ 108 | clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 109 | loadOp: "clear", 110 | storeOp: "store", 111 | attachment, 112 | resolveTarget: backBufferView 113 | }] 114 | }); 115 | renderPass.setPipeline(pipeline); 116 | renderPass.draw(3, 1, 0, 0); 117 | renderPass.endPass(); 118 | 119 | const commandBuffer = commandEncoder.finish(); 120 | queue.submit([ commandBuffer ]); 121 | swapChain.present(); 122 | window.pollEvents(); 123 | }; 124 | setTimeout(onFrame, 1e3 / 60); 125 | 126 | })(); 127 | -------------------------------------------------------------------------------- /hello-triangle.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | 3 | Object.assign(global, WebGPU); 4 | 5 | const vsSrc = ` 6 | #version 450 7 | #pragma shader_stage(vertex) 8 | const vec2 pos[3] = vec2[3]( 9 | vec2(0.0f, 0.5f), 10 | vec2(-0.5f, -0.5f), 11 | vec2(0.5f, -0.5f) 12 | ); 13 | void main() { 14 | gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0); 15 | } 16 | `; 17 | 18 | const fsSrc = ` 19 | #version 450 20 | #pragma shader_stage(fragment) 21 | layout(location = 0) out vec4 outColor; 22 | void main() { 23 | outColor = vec4(1.0, 0.0, 0.0, 1.0); 24 | } 25 | `; 26 | 27 | (async function main() { 28 | 29 | const window = new WebGPUWindow({ 30 | width: 640, 31 | height: 480, 32 | title: "WebGPU" 33 | }); 34 | 35 | const adapter = await GPU.requestAdapter({ window }); 36 | 37 | const device = await adapter.requestDevice(); 38 | 39 | const queue = device.getQueue(); 40 | 41 | const context = window.getContext("webgpu"); 42 | 43 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 44 | 45 | const swapChain = context.configureSwapChain({ 46 | device: device, 47 | format: swapChainFormat 48 | }); 49 | 50 | const layout = device.createPipelineLayout({ 51 | bindGroupLayouts: [] 52 | }); 53 | 54 | const vertexShaderModule = device.createShaderModule({ code: vsSrc }); 55 | const fragmentShaderModule = device.createShaderModule({ code: fsSrc }); 56 | 57 | const pipeline = device.createRenderPipeline({ 58 | layout, 59 | sampleCount: 1, 60 | vertexStage: { 61 | module: vertexShaderModule, 62 | entryPoint: "main" 63 | }, 64 | fragmentStage: { 65 | module: fragmentShaderModule, 66 | entryPoint: "main" 67 | }, 68 | primitiveTopology: "triangle-list", 69 | vertexInput: { 70 | indexFormat: "uint32", 71 | buffers: [] 72 | }, 73 | rasterizationState: { 74 | frontFace: "CCW", 75 | cullMode: "none" 76 | }, 77 | colorStates: [{ 78 | format: swapChainFormat, 79 | alphaBlend: {}, 80 | colorBlend: {} 81 | }] 82 | }); 83 | 84 | function onFrame() { 85 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 86 | 87 | const backBufferView = swapChain.getCurrentTextureView(); 88 | const commandEncoder = device.createCommandEncoder({}); 89 | const renderPass = commandEncoder.beginRenderPass({ 90 | colorAttachments: [{ 91 | clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 92 | loadOp: "clear", 93 | storeOp: "store", 94 | attachment: backBufferView 95 | }] 96 | }); 97 | renderPass.setPipeline(pipeline); 98 | renderPass.draw(3, 1, 0, 0); 99 | renderPass.endPass(); 100 | 101 | const commandBuffer = commandEncoder.finish(); 102 | queue.submit([ commandBuffer ]); 103 | swapChain.present(); 104 | window.pollEvents(); 105 | }; 106 | setTimeout(onFrame, 1e3 / 60); 107 | 108 | })(); 109 | -------------------------------------------------------------------------------- /interactive-triangle.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | import glMatrix from "gl-matrix"; 3 | 4 | Object.assign(global, WebGPU); 5 | Object.assign(global, glMatrix); 6 | 7 | const vsSrc = ` 8 | #version 450 9 | #pragma shader_stage(vertex) 10 | 11 | layout(set = 0, binding = 0) uniform Matrices { 12 | mat4 modelViewProjection; 13 | } uMatrices; 14 | 15 | layout(location = 0) in vec2 position; 16 | layout(location = 1) in vec3 color; 17 | 18 | layout(location = 0) out vec4 vColor; 19 | 20 | void main() { 21 | gl_Position = uMatrices.modelViewProjection * vec4(position, 0.0, 1.0); 22 | vColor = vec4(color, 1.0); 23 | } 24 | `; 25 | 26 | const fsSrc = ` 27 | #version 450 28 | #pragma shader_stage(fragment) 29 | 30 | layout(location = 0) in vec4 vColor; 31 | 32 | layout(location = 0) out vec4 outColor; 33 | 34 | void main() { 35 | outColor = vColor; 36 | } 37 | `; 38 | 39 | (async function main() { 40 | 41 | const triangleVertices = new Float32Array([ 42 | 0.0, 0.5, 1.0, 0.0, 0.0, 43 | -0.5, -0.5, 0.0, 1.0, 0.0, 44 | 0.5, -0.5, 0.0, 0.0, 1.0 45 | ]); 46 | 47 | const triangleIndices = new Uint32Array([ 48 | 0, 1, 2 49 | ]); 50 | 51 | const window = new WebGPUWindow({ 52 | width: 640, 53 | height: 480, 54 | title: "WebGPU" 55 | }); 56 | 57 | const aspect = Math.abs(window.width / window.height); 58 | 59 | const mModel = mat4.create(); 60 | const mView = mat4.create(); 61 | const mProjection = mat4.create(); 62 | const mModelViewProjection = mat4.create(); 63 | 64 | mat4.perspective(mProjection, (2 * Math.PI) / 5, -aspect, 0.1, 4096.0); 65 | mat4.translate(mView, mView, vec3.fromValues(0, 0, -4)); 66 | 67 | const adapter = await GPU.requestAdapter({ window }); 68 | 69 | const device = await adapter.requestDevice(); 70 | 71 | const queue = device.getQueue(); 72 | 73 | const context = window.getContext("webgpu"); 74 | 75 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 76 | 77 | const swapChain = context.configureSwapChain({ 78 | device: device, 79 | format: swapChainFormat 80 | }); 81 | 82 | const stagedVertexBuffer = device.createBuffer({ 83 | size: triangleVertices.byteLength, 84 | usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST 85 | }); 86 | stagedVertexBuffer.setSubData(0, triangleVertices); 87 | 88 | const stagedIndexBuffer = device.createBuffer({ 89 | size: triangleIndices.byteLength, 90 | usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST 91 | }); 92 | stagedIndexBuffer.setSubData(0, triangleIndices); 93 | 94 | const stagedUniformBuffer = device.createBuffer({ 95 | size: mModelViewProjection.byteLength, 96 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST 97 | }); 98 | stagedUniformBuffer.setSubData(0, mModelViewProjection); 99 | 100 | const uniformBindGroupLayout = device.createBindGroupLayout({ 101 | entries: [{ 102 | binding: 0, 103 | visibility: GPUShaderStage.VERTEX, 104 | type: "uniform-buffer" 105 | }] 106 | }); 107 | 108 | const uniformBindGroup = device.createBindGroup({ 109 | layout: uniformBindGroupLayout, 110 | entries: [{ 111 | binding: 0, 112 | buffer: stagedUniformBuffer, 113 | offset: 0, 114 | size: mModelViewProjection.byteLength 115 | }] 116 | }); 117 | 118 | const layout = device.createPipelineLayout({ 119 | bindGroupLayouts: [ uniformBindGroupLayout ] 120 | }); 121 | 122 | const vertexShaderModule = device.createShaderModule({ code: vsSrc }); 123 | const fragmentShaderModule = device.createShaderModule({ code: fsSrc }); 124 | 125 | const pipeline = device.createRenderPipeline({ 126 | layout, 127 | sampleCount: 1, 128 | vertexStage: { 129 | module: vertexShaderModule, 130 | entryPoint: "main" 131 | }, 132 | fragmentStage: { 133 | module: fragmentShaderModule, 134 | entryPoint: "main" 135 | }, 136 | primitiveTopology: "triangle-list", 137 | vertexState: { 138 | indexFormat: "uint32", 139 | vertexBuffers: [{ 140 | arrayStride: 5 * Float32Array.BYTES_PER_ELEMENT, 141 | stepMode: "vertex", 142 | attributes: [ 143 | { 144 | shaderLocation: 0, 145 | offset: 0 * Float32Array.BYTES_PER_ELEMENT, 146 | format: "float2" 147 | }, 148 | { 149 | shaderLocation: 1, 150 | offset: 2 * Float32Array.BYTES_PER_ELEMENT, 151 | format: "float3" 152 | } 153 | ] 154 | }] 155 | }, 156 | rasterizationState: { 157 | frontFace: "CCW", 158 | cullMode: "none" 159 | }, 160 | colorStates: [{ 161 | format: swapChainFormat, 162 | alphaBlend: {}, 163 | colorBlend: {} 164 | }] 165 | }); 166 | 167 | let isMouseButtonPressed = false; 168 | window.onmouseup = e => { 169 | isMouseButtonPressed = false; 170 | }; 171 | window.onmousedown = e => { 172 | isMouseButtonPressed = true; 173 | }; 174 | window.onmousemove = e => { 175 | if (!isMouseButtonPressed) return; 176 | triangleRotateAcceleration += e.movementX * 0.005; 177 | }; 178 | 179 | let triangleRotation = 0; 180 | let triangleRotateAcceleration = 0; 181 | 182 | let then = Date.now(); 183 | function onFrame() { 184 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 185 | 186 | let now = Date.now(); 187 | let delta = (now - then) / 1e3; 188 | then = now; 189 | 190 | mat4.identity(mModel); 191 | mat4.rotateY(mModel, mModel, triangleRotation); 192 | mat4.scale(mModel, mModel, vec3.fromValues(3, 3, 3)); 193 | mat4.multiply(mModelViewProjection, mView, mModel); 194 | mat4.multiply(mModelViewProjection, mProjection, mModelViewProjection); 195 | stagedUniformBuffer.setSubData(0, mModelViewProjection); 196 | 197 | { 198 | const backBufferView = swapChain.getCurrentTextureView(); 199 | const commandEncoder = device.createCommandEncoder({}); 200 | const renderPass = commandEncoder.beginRenderPass({ 201 | colorAttachments: [{ 202 | clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 203 | loadOp: "clear", 204 | storeOp: "store", 205 | attachment: backBufferView 206 | }] 207 | }); 208 | renderPass.setPipeline(pipeline); 209 | renderPass.setBindGroup(0, uniformBindGroup); 210 | renderPass.setVertexBuffer(0, stagedVertexBuffer, 0); 211 | renderPass.setIndexBuffer(stagedIndexBuffer); 212 | renderPass.drawIndexed(triangleIndices.length, 1, 0, 0, 0); 213 | renderPass.endPass(); 214 | 215 | const commandBuffer = commandEncoder.finish(); 216 | queue.submit([ commandBuffer ]); 217 | swapChain.present(); 218 | 219 | } 220 | 221 | triangleRotation += triangleRotateAcceleration *= 0.9; 222 | if (!isMouseButtonPressed) { 223 | triangleRotation += delta; 224 | } 225 | 226 | window.pollEvents(); 227 | }; 228 | setTimeout(onFrame, 1e3 / 60); 229 | 230 | })(); 231 | -------------------------------------------------------------------------------- /multiple-render-targets.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | 3 | Object.assign(global, WebGPU); 4 | 5 | const vsSrc = ` 6 | #version 450 7 | #pragma shader_stage(vertex) 8 | const vec2 pos[3] = vec2[3]( 9 | vec2(0.0f, 0.5f), 10 | vec2(-0.5f, -0.5f), 11 | vec2(0.5f, -0.5f) 12 | ); 13 | void main() { 14 | gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0); 15 | } 16 | `; 17 | 18 | const fsSrc = ` 19 | #version 450 20 | #pragma shader_stage(fragment) 21 | layout(location = 0) out vec4 outColor1; 22 | layout(location = 1) out vec4 outColor2; 23 | void main() { 24 | outColor1 = vec4(1.0, 0.0, 0.0, 1.0); 25 | outColor2 = vec4(0.0, 1.0, 0.0, 1.0); 26 | } 27 | `; 28 | 29 | const blitVsSrc = ` 30 | #version 450 31 | #pragma shader_stage(vertex) 32 | layout (location = 0) out vec2 vUV; 33 | void main(void) { 34 | vec2 uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); 35 | gl_Position = vec4(uv * 2.0 + -1.0f, 0.0, 1.0); 36 | vUV = vec2(uv.x, 1.0 - uv.y); 37 | } 38 | `; 39 | 40 | const blitFsSrc = ` 41 | #version 450 42 | #pragma shader_stage(fragment) 43 | layout (location = 0) in vec2 vUv; 44 | layout (location = 0) out vec4 outColor; 45 | layout (binding = 0) uniform sampler sampler0; 46 | layout (binding = 1) uniform texture2D renderTarget1; 47 | layout (binding = 2) uniform texture2D renderTarget2; 48 | void main() { 49 | outColor = mix( 50 | texture(sampler2D(renderTarget1, sampler0), vUv), 51 | texture(sampler2D(renderTarget2, sampler0), vUv), 52 | vUv.x 53 | ); 54 | } 55 | `; 56 | 57 | (async function main() { 58 | 59 | const window = new WebGPUWindow({ 60 | width: 640, 61 | height: 480, 62 | title: "WebGPU" 63 | }); 64 | 65 | const adapter = await GPU.requestAdapter({ window }); 66 | 67 | const device = await adapter.requestDevice(); 68 | 69 | const queue = device.getQueue(); 70 | 71 | const context = window.getContext("webgpu"); 72 | 73 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 74 | 75 | const swapChain = context.configureSwapChain({ 76 | device: device, 77 | format: swapChainFormat 78 | }); 79 | 80 | const renderPipeline = device.createRenderPipeline({ 81 | layout: device.createPipelineLayout({ 82 | bindGroupLayouts: [] 83 | }), 84 | sampleCount: 1, 85 | vertexStage: { 86 | module: device.createShaderModule({ code: vsSrc }), 87 | entryPoint: "main" 88 | }, 89 | fragmentStage: { 90 | module: device.createShaderModule({ code: fsSrc }), 91 | entryPoint: "main" 92 | }, 93 | primitiveTopology: "triangle-list", 94 | vertexInput: { 95 | indexFormat: "uint32", 96 | buffers: [] 97 | }, 98 | rasterizationState: { 99 | frontFace: "CCW", 100 | cullMode: "none" 101 | }, 102 | // 2 color states 103 | colorStates: [ 104 | { 105 | format: swapChainFormat, 106 | alphaBlend: {}, 107 | colorBlend: {} 108 | }, 109 | { 110 | format: swapChainFormat, 111 | alphaBlend: {}, 112 | colorBlend: {} 113 | } 114 | ] 115 | }); 116 | 117 | const linearSampler = device.createSampler({ 118 | magFilter: "linear", 119 | minFilter: "linear", 120 | addressModeU: "repeat", 121 | addressModeV: "repeat", 122 | addressModeW: "repeat" 123 | }); 124 | 125 | const renderTarget1 = device.createTexture({ 126 | size: { 127 | width: window.width, 128 | height: window.height, 129 | depth: 1 130 | }, 131 | arrayLayerCount: 1, 132 | mipLevelCount: 1, 133 | sampleCount: 1, 134 | dimension: "2d", 135 | format: swapChainFormat, 136 | usage: GPUTextureUsage.OUTPUT_ATTACHMENT | GPUTextureUsage.SAMPLED 137 | }); 138 | const renderTargetView1 = renderTarget1.createView({ 139 | format: swapChainFormat 140 | }); 141 | 142 | const renderTarget2 = device.createTexture({ 143 | size: { 144 | width: window.width, 145 | height: window.height, 146 | depth: 1 147 | }, 148 | arrayLayerCount: 1, 149 | mipLevelCount: 1, 150 | sampleCount: 1, 151 | dimension: "2d", 152 | format: swapChainFormat, 153 | usage: GPUTextureUsage.OUTPUT_ATTACHMENT | GPUTextureUsage.SAMPLED 154 | }); 155 | const renderTargetView2 = renderTarget2.createView({ 156 | format: swapChainFormat 157 | }); 158 | 159 | const blitBindGroupLayout = device.createBindGroupLayout({ 160 | // 1 sampler, 2 texture inputs 161 | entries: [ 162 | { 163 | binding: 0, 164 | visibility: GPUShaderStage.FRAGMENT, 165 | type: "sampler" 166 | }, 167 | { 168 | binding: 1, 169 | visibility: GPUShaderStage.FRAGMENT, 170 | type: "sampled-texture" 171 | }, 172 | { 173 | binding: 2, 174 | visibility: GPUShaderStage.FRAGMENT, 175 | type: "sampled-texture" 176 | } 177 | ] 178 | }); 179 | 180 | const blitBindGroup = device.createBindGroup({ 181 | layout: blitBindGroupLayout, 182 | // 1 sampler, 2 texture inputs 183 | entries: [ 184 | { 185 | binding: 0, 186 | sampler: linearSampler, 187 | size: 0 188 | }, 189 | { 190 | binding: 1, 191 | textureView: renderTargetView1, 192 | size: 0 193 | }, 194 | { 195 | binding: 2, 196 | textureView: renderTargetView2, 197 | size: 2 198 | } 199 | ] 200 | }); 201 | 202 | const blitPipeline = device.createRenderPipeline({ 203 | layout: device.createPipelineLayout({ 204 | bindGroupLayouts: [blitBindGroupLayout] 205 | }), 206 | sampleCount: 1, 207 | vertexStage: { 208 | module: device.createShaderModule({ code: blitVsSrc }), 209 | entryPoint: "main" 210 | }, 211 | fragmentStage: { 212 | module: device.createShaderModule({ code: blitFsSrc }), 213 | entryPoint: "main" 214 | }, 215 | primitiveTopology: "triangle-list", 216 | vertexInput: { 217 | indexFormat: "uint32", 218 | buffers: [] 219 | }, 220 | rasterizationState: { 221 | frontFace: "CCW", 222 | cullMode: "none" 223 | }, 224 | colorStates: [ 225 | { 226 | format: swapChainFormat, 227 | alphaBlend: {}, 228 | colorBlend: {} 229 | } 230 | ] 231 | }); 232 | 233 | let cachedCommandBuffer = null; 234 | function onFrame() { 235 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 236 | 237 | const backBufferView = swapChain.getCurrentTextureView(); 238 | 239 | // since we don't render directly into the swapchain 240 | // we can now cache command buffers of previous passes 241 | if (cachedCommandBuffer === null) { 242 | const commandEncoder = device.createCommandEncoder({}); 243 | const renderPass = commandEncoder.beginRenderPass({ 244 | // 2 color attachments 245 | colorAttachments: [ 246 | { 247 | clearColor: { r: 0.125, g: 0.125, b: 0.125, a: 1.0 }, 248 | loadOp: "clear", 249 | storeOp: "store", 250 | attachment: renderTargetView1 251 | }, 252 | { 253 | clearColor: { r: 0.125, g: 0.125, b: 0.125, a: 1.0 }, 254 | loadOp: "clear", 255 | storeOp: "store", 256 | attachment: renderTargetView2 257 | } 258 | ] 259 | }); 260 | renderPass.setPipeline(renderPipeline); 261 | renderPass.draw(3, 1, 0, 0); 262 | renderPass.endPass(); 263 | cachedCommandBuffer = commandEncoder.finish(); 264 | console.log("Render Pass Command Buffer is cached"); 265 | } 266 | // submit cached command buffer 267 | queue.submit([ cachedCommandBuffer ]); 268 | // blit to swapchain 269 | { 270 | const commandEncoder = device.createCommandEncoder({}); 271 | const blitPass = commandEncoder.beginRenderPass({ 272 | colorAttachments: [{ 273 | clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 274 | loadOp: "clear", 275 | storeOp: "store", 276 | attachment: backBufferView 277 | }] 278 | }); 279 | blitPass.setPipeline(blitPipeline); 280 | blitPass.setBindGroup(0, blitBindGroup); 281 | blitPass.draw(3, 1, 0, 0); 282 | blitPass.endPass(); 283 | const commandBuffer = commandEncoder.finish(); 284 | queue.submit([ commandBuffer ]); 285 | } 286 | 287 | swapChain.present(); 288 | window.pollEvents(); 289 | }; 290 | setTimeout(onFrame, 1e3 / 60); 291 | 292 | })(); 293 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@cwasm/lodepng": "^0.1.3", 4 | "gl-matrix": "^3.1.0", 5 | "webgpu": "latest" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ray-tracing/index.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | 3 | import fs from "fs"; 4 | import glMatrix from "gl-matrix"; 5 | 6 | Object.assign(global, WebGPU); 7 | Object.assign(global, glMatrix); 8 | 9 | (async function main() { 10 | 11 | let window = new WebGPUWindow({ 12 | width: 640, 13 | height: 480, 14 | title: "WebGPU", 15 | resizable: false 16 | }); 17 | 18 | let adapter = await GPU.requestAdapter({ window }); 19 | 20 | let device = await adapter.requestDevice({ 21 | extensions: ["ray_tracing"] 22 | }); 23 | 24 | let queue = device.getQueue(); 25 | 26 | let context = window.getContext("webgpu"); 27 | 28 | let swapChainFormat = await context.getSwapChainPreferredFormat(device); 29 | 30 | let swapChain = context.configureSwapChain({ 31 | device: device, 32 | format: swapChainFormat 33 | }); 34 | 35 | let aspect = Math.abs(window.width / window.height); 36 | 37 | let mView = mat4.create(); 38 | let mProjection = mat4.create(); 39 | 40 | mat4.perspective(mProjection, (2 * Math.PI) / 5, -aspect, 0.1, 4096.0); 41 | 42 | mat4.translate(mView, mView, vec3.fromValues(0, 0, -2)); 43 | 44 | // invert 45 | mat4.invert(mView, mView); 46 | mat4.invert(mProjection, mProjection); 47 | mProjection[5] *= -1.0; 48 | 49 | let baseShaderPath = `ray-tracing/shaders`; 50 | 51 | // rasterization shaders 52 | let vertexShaderModule = device.createShaderModule({ 53 | code: fs.readFileSync(`${baseShaderPath}/screen.vert`, "utf-8") 54 | }); 55 | let fragmentShaderModule = device.createShaderModule({ 56 | code: fs.readFileSync(`${baseShaderPath}/screen.frag`, "utf-8") 57 | }); 58 | 59 | // ray-tracing shaders 60 | let rayGenShaderModule = device.createShaderModule({ 61 | code: fs.readFileSync(`${baseShaderPath}/ray-generation.rgen`, "utf-8") 62 | }); 63 | let rayCHitShaderModule = device.createShaderModule({ 64 | code: fs.readFileSync(`${baseShaderPath}/ray-closest-hit.rchit`, "utf-8") 65 | }); 66 | let rayMissShaderModule = device.createShaderModule({ 67 | code: fs.readFileSync(`${baseShaderPath}/ray-miss.rmiss`, "utf-8") 68 | }); 69 | 70 | // this storage buffer is used as a pixel buffer 71 | // the result of the ray tracing pass gets written into it 72 | // and it gets copied to the screen in the rasterization pass 73 | let pixelBufferSize = window.width * window.height * 4 * Float32Array.BYTES_PER_ELEMENT; 74 | let pixelBuffer = device.createBuffer({ 75 | size: pixelBufferSize, 76 | usage: GPUBufferUsage.STORAGE 77 | }); 78 | 79 | let triangleVertices = new Float32Array([ 80 | 1.0, 1.0, 0.0, 81 | -1.0, 1.0, 0.0, 82 | 0.0, -1.0, 0.0 83 | ]); 84 | let triangleVertexBuffer = device.createBuffer({ 85 | size: triangleVertices.byteLength, 86 | usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.RAY_TRACING 87 | }); 88 | triangleVertexBuffer.setSubData(0, triangleVertices); 89 | 90 | let triangleIndices = new Uint32Array([ 91 | 0, 1, 2 92 | ]); 93 | let triangleIndexBuffer = device.createBuffer({ 94 | size: triangleIndices.byteLength, 95 | usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.RAY_TRACING 96 | }); 97 | triangleIndexBuffer.setSubData(0, triangleIndices); 98 | 99 | // create a geometry container 100 | // which holds references to our geometry buffers 101 | let geometryContainer = device.createRayTracingAccelerationContainer({ 102 | level: "bottom", 103 | usage: GPURayTracingAccelerationContainerUsage.PREFER_FAST_TRACE, 104 | geometries: [ 105 | { 106 | usage: GPURayTracingAccelerationGeometryUsage.OPAQUE, 107 | type: "triangles", 108 | vertex: { 109 | buffer: triangleVertexBuffer, 110 | format: "float3", 111 | stride: 3 * Float32Array.BYTES_PER_ELEMENT, 112 | count: triangleVertices.length 113 | }, 114 | index: { 115 | buffer: triangleIndexBuffer, 116 | format: "uint32", 117 | count: triangleIndices.length 118 | } 119 | } 120 | ] 121 | }); 122 | 123 | // create an instance container 124 | // which contains object instances with transforms 125 | // and links to a geometry container to be used 126 | let instanceContainer = device.createRayTracingAccelerationContainer({ 127 | level: "top", 128 | usage: GPURayTracingAccelerationContainerUsage.PREFER_FAST_TRACE, 129 | instances: [ 130 | { 131 | usage: GPURayTracingAccelerationInstanceUsage.TRIANGLE_CULL_DISABLE, 132 | mask: 0xFF, 133 | instanceId: 0, 134 | instanceOffset: 0x0, 135 | transform: { 136 | translation: { x: 0, y: 0, z: 0 }, 137 | rotation: { x: 0, y: 0, z: 0 }, 138 | scale: { x: 1, y: 1, z: 1 } 139 | }, 140 | geometryContainer: geometryContainer 141 | } 142 | ] 143 | }); 144 | 145 | // first build all bottom-level containers 146 | { 147 | let commandEncoder = device.createCommandEncoder({}); 148 | commandEncoder.buildRayTracingAccelerationContainer(geometryContainer); 149 | queue.submit([ commandEncoder.finish() ]); 150 | } 151 | 152 | // now we can build the top-level containers 153 | // building them in separate passes is important 154 | { 155 | let commandEncoder = device.createCommandEncoder({}); 156 | commandEncoder.buildRayTracingAccelerationContainer(instanceContainer); 157 | queue.submit([ commandEncoder.finish() ]); 158 | } 159 | 160 | // a collection of shader modules which get dynamically 161 | // invoked, for example when calling traceNV 162 | let shaderBindingTable = device.createRayTracingShaderBindingTable({ 163 | // stages are a collection of shaders 164 | // which get indexed in groups 165 | stages: [ 166 | { 167 | module: rayGenShaderModule, 168 | stage: GPUShaderStage.RAY_GENERATION 169 | }, 170 | { 171 | module: rayCHitShaderModule, 172 | stage: GPUShaderStage.RAY_CLOSEST_HIT 173 | }, 174 | { 175 | module: rayMissShaderModule, 176 | stage: GPUShaderStage.RAY_MISS 177 | } 178 | ], 179 | // groups can index the shaders in stages 180 | // generalIndex: ray generation or ray miss stage index 181 | // anyHitIndex: ray any-hit stage index 182 | // closestHitIndex: ray closest-hit stage index 183 | // intersectionIndex: ray intersection stage index 184 | groups: [ 185 | // generation group 186 | { 187 | type: "general", 188 | generalIndex: 0, // ray generation shader index 189 | anyHitIndex: -1, 190 | closestHitIndex: -1, 191 | intersectionIndex: -1 192 | }, 193 | // hit group 194 | { 195 | type: "triangles-hit-group", 196 | generalIndex: -1, 197 | anyHitIndex: -1, 198 | closestHitIndex: 1, // ray closest-hit shader index 199 | intersectionIndex: -1 200 | }, 201 | // miss group 202 | { 203 | type: "general", 204 | generalIndex: 2, // ray miss shader index 205 | anyHitIndex: -1, 206 | closestHitIndex: -1, 207 | intersectionIndex: -1 208 | } 209 | ] 210 | }); 211 | 212 | let rtBindGroupLayout = device.createBindGroupLayout({ 213 | entries: [ 214 | { 215 | binding: 0, 216 | visibility: GPUShaderStage.RAY_GENERATION, 217 | type: "acceleration-container" 218 | }, 219 | { 220 | binding: 1, 221 | visibility: GPUShaderStage.RAY_GENERATION, 222 | type: "storage-buffer" 223 | }, 224 | { 225 | binding: 2, 226 | visibility: GPUShaderStage.RAY_GENERATION, 227 | type: "uniform-buffer" 228 | } 229 | ] 230 | }); 231 | 232 | let cameraData = new Float32Array( 233 | // (mat4) view 234 | mView.byteLength + 235 | // (mat4) projection 236 | mProjection.byteLength 237 | ); 238 | let cameraUniformBuffer = device.createBuffer({ 239 | size: cameraData.byteLength, 240 | usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM 241 | }); 242 | // fill in the data 243 | { 244 | let offset = 0x0; 245 | cameraData.set(mView, offset); 246 | offset += mView.length; 247 | cameraData.set(mProjection, offset); 248 | offset += mProjection.length; 249 | } 250 | cameraUniformBuffer.setSubData(0, cameraData); 251 | 252 | let rtBindGroup = device.createBindGroup({ 253 | layout: rtBindGroupLayout, 254 | entries: [ 255 | { 256 | binding: 0, 257 | accelerationContainer: instanceContainer, 258 | offset: 0, 259 | size: 0 260 | }, 261 | { 262 | binding: 1, 263 | buffer: pixelBuffer, 264 | offset: 0, 265 | size: pixelBufferSize 266 | }, 267 | { 268 | binding: 2, 269 | buffer: cameraUniformBuffer, 270 | offset: 0, 271 | size: cameraData.byteLength 272 | } 273 | ] 274 | }); 275 | 276 | let rtPipeline = device.createRayTracingPipeline({ 277 | layout: device.createPipelineLayout({ 278 | bindGroupLayouts: [rtBindGroupLayout] 279 | }), 280 | rayTracingState: { 281 | shaderBindingTable, 282 | maxRecursionDepth: 1, 283 | maxPayloadSize: 3 * Float32Array.BYTES_PER_ELEMENT 284 | } 285 | }); 286 | 287 | let resolutionData = new Float32Array([ 288 | window.width, window.height 289 | ]); 290 | let resolutionUniformBuffer = device.createBuffer({ 291 | size: resolutionData.byteLength, 292 | usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM 293 | }); 294 | resolutionUniformBuffer.setSubData(0, resolutionData); 295 | 296 | let renderBindGroupLayout = device.createBindGroupLayout({ 297 | entries: [ 298 | { 299 | binding: 0, 300 | visibility: GPUShaderStage.FRAGMENT, 301 | type: "storage-buffer" 302 | }, 303 | { 304 | binding: 1, 305 | visibility: GPUShaderStage.FRAGMENT, 306 | type: "uniform-buffer" 307 | } 308 | ] 309 | }); 310 | 311 | let renderBindGroup = device.createBindGroup({ 312 | layout: renderBindGroupLayout, 313 | entries: [ 314 | { 315 | binding: 0, 316 | buffer: pixelBuffer, 317 | offset: 0, 318 | size: pixelBufferSize 319 | }, 320 | { 321 | binding: 1, 322 | buffer: resolutionUniformBuffer, 323 | offset: 0, 324 | size: resolutionData.byteLength 325 | } 326 | ] 327 | }); 328 | 329 | let renderPipeline = device.createRenderPipeline({ 330 | layout: device.createPipelineLayout({ 331 | bindGroupLayouts: [renderBindGroupLayout] 332 | }), 333 | sampleCount: 1, 334 | vertexStage: { 335 | module: vertexShaderModule, 336 | entryPoint: "main" 337 | }, 338 | fragmentStage: { 339 | module: fragmentShaderModule, 340 | entryPoint: "main" 341 | }, 342 | primitiveTopology: "triangle-list", 343 | vertexState: { 344 | indexFormat: "uint32", 345 | vertexBuffers: [] 346 | }, 347 | rasterizationState: { 348 | frontFace: "CCW", 349 | cullMode: "none" 350 | }, 351 | colorStates: [{ 352 | format: swapChainFormat, 353 | alphaBlend: {}, 354 | colorBlend: {} 355 | }] 356 | }); 357 | 358 | function onFrame() { 359 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 360 | 361 | let backBufferView = swapChain.getCurrentTextureView(); 362 | 363 | // ray tracing pass 364 | { 365 | let commandEncoder = device.createCommandEncoder({}); 366 | let passEncoder = commandEncoder.beginRayTracingPass({}); 367 | passEncoder.setPipeline(rtPipeline); 368 | passEncoder.setBindGroup(0, rtBindGroup); 369 | passEncoder.traceRays( 370 | 0, // sbt ray-generation offset 371 | 1, // sbt ray-hit offset 372 | 2, // sbt ray-miss offset 373 | window.width, // query width dimension 374 | window.height, // query height dimension 375 | 1 // query depth dimension 376 | ); 377 | passEncoder.endPass(); 378 | queue.submit([ commandEncoder.finish() ]); 379 | 380 | } 381 | // rasterization pass 382 | // the rasterization's pass only use right now, 383 | // is to bring the pixel buffer we write into from the 384 | // ray tracing pass, to the screen 385 | { 386 | let commandEncoder = device.createCommandEncoder({}); 387 | let passEncoder = commandEncoder.beginRenderPass({ 388 | colorAttachments: [{ 389 | clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 390 | loadOp: "clear", 391 | storeOp: "store", 392 | attachment: backBufferView 393 | }] 394 | }); 395 | passEncoder.setPipeline(renderPipeline); 396 | passEncoder.setBindGroup(0, renderBindGroup); 397 | passEncoder.draw(3, 1, 0, 0); 398 | passEncoder.endPass(); 399 | queue.submit([ commandEncoder.finish() ]); 400 | } 401 | 402 | swapChain.present(); 403 | window.pollEvents(); 404 | }; 405 | setTimeout(onFrame, 1e3 / 60); 406 | 407 | })(); 408 | -------------------------------------------------------------------------------- /ray-tracing/shaders/ray-closest-hit.rchit: -------------------------------------------------------------------------------- 1 | #version 460 2 | #extension GL_EXT_ray_tracing : enable 3 | #pragma shader_stage(closest) 4 | 5 | layout(location = 0) rayPayloadInEXT vec3 payload; 6 | 7 | hitAttributeEXT vec3 attribs; 8 | 9 | void main() { 10 | const vec3 bary = vec3(1.0 - attribs.x - attribs.y, attribs.x, attribs.y); 11 | payload = bary; 12 | } 13 | -------------------------------------------------------------------------------- /ray-tracing/shaders/ray-generation.rgen: -------------------------------------------------------------------------------- 1 | #version 460 2 | #extension GL_EXT_ray_tracing : require 3 | #pragma shader_stage(raygen) 4 | 5 | layout(location = 0) rayPayloadEXT vec3 payload; 6 | 7 | layout(binding = 0, set = 0) uniform accelerationStructureEXT topLevelAS; 8 | 9 | layout(std140, set = 0, binding = 1) buffer PixelBuffer { 10 | vec4 pixels[]; 11 | } pixelBuffer; 12 | 13 | layout(set = 0, binding = 2) uniform Camera { 14 | mat4 view; 15 | mat4 projection; 16 | } uCamera; 17 | 18 | void main() { 19 | ivec2 ipos = ivec2(gl_LaunchIDEXT.xy); 20 | const ivec2 resolution = ivec2(gl_LaunchSizeEXT.xy); 21 | 22 | const vec2 offset = vec2(0); 23 | const vec2 pixel = vec2(ipos.x, ipos.y); 24 | const vec2 uv = (pixel / gl_LaunchSizeEXT.xy) * 2.0 - 1.0; 25 | 26 | vec4 origin = uCamera.view * vec4(offset, 0, 1); 27 | vec4 target = uCamera.projection * (vec4(uv.x, uv.y, 1, 1)); 28 | vec4 direction = uCamera.view * vec4(normalize(target.xyz), 0); 29 | 30 | payload = vec3(0); 31 | traceRayEXT(topLevelAS, gl_RayFlagsOpaqueEXT, 0xFF, 0, 0, 0, origin.xyz, 0.01, direction.xyz, 4096.0, 0); 32 | 33 | const uint pixelIndex = ipos.y * resolution.x + ipos.x; 34 | pixelBuffer.pixels[pixelIndex] = vec4(payload, 1.0); 35 | } 36 | -------------------------------------------------------------------------------- /ray-tracing/shaders/ray-miss.rmiss: -------------------------------------------------------------------------------- 1 | #version 460 2 | #extension GL_EXT_ray_tracing : enable 3 | #pragma shader_stage(miss) 4 | 5 | layout(location = 0) rayPayloadInEXT vec3 payload; 6 | 7 | void main() { 8 | payload = vec3(0.15); 9 | } 10 | -------------------------------------------------------------------------------- /ray-tracing/shaders/screen.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | #pragma shader_stage(fragment) 3 | 4 | layout (location = 0) in vec2 uv; 5 | layout (location = 0) out vec4 outColor; 6 | 7 | layout(std140, set = 0, binding = 0) buffer PixelBuffer { 8 | vec4 pixels[]; 9 | } pixelBuffer; 10 | 11 | layout(set = 0, binding = 1) uniform ScreenDimension { 12 | vec2 resolution; 13 | }; 14 | 15 | void main() { 16 | const ivec2 bufferCoord = ivec2(floor(uv * resolution)); 17 | const vec2 fragCoord = (uv * resolution); 18 | const uint pixelIndex = bufferCoord.y * uint(resolution.x) + bufferCoord.x; 19 | 20 | vec4 pixelColor = pixelBuffer.pixels[pixelIndex]; 21 | outColor = pixelColor; 22 | } 23 | -------------------------------------------------------------------------------- /ray-tracing/shaders/screen.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | #pragma shader_stage(vertex) 3 | 4 | layout (location = 0) out vec2 uv; 5 | 6 | void main() { 7 | vec2 pos = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); 8 | gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0); 9 | uv = pos; 10 | } 11 | -------------------------------------------------------------------------------- /rotating-cube.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | import glMatrix from "gl-matrix"; 3 | 4 | Object.assign(global, WebGPU); 5 | Object.assign(global, glMatrix); 6 | 7 | const vsSrc = ` 8 | #version 450 9 | #pragma shader_stage(vertex) 10 | 11 | layout(set = 0, binding = 0) uniform Matrices { 12 | mat4 modelViewProjection; 13 | } uMatrices; 14 | 15 | layout(location = 0) in vec3 position; 16 | layout(location = 1) in vec3 color; 17 | 18 | layout(location = 0) out vec4 vColor; 19 | 20 | void main() { 21 | gl_Position = uMatrices.modelViewProjection * vec4(position, 1.0); 22 | vColor = vec4(color, 1.0); 23 | } 24 | `; 25 | 26 | const fsSrc = ` 27 | #version 450 28 | #pragma shader_stage(fragment) 29 | 30 | layout(location = 0) in vec4 vColor; 31 | 32 | layout(location = 0) out vec4 outColor; 33 | 34 | void main() { 35 | outColor = vColor; 36 | } 37 | `; 38 | 39 | (async function main() { 40 | 41 | const modelVertices = new Float32Array([ 42 | // Front face 43 | -1.0, -1.0, 1.0, 1.0, 0.0, 0.0, 44 | 1.0, -1.0, 1.0, 0.0, 1.0, 0.0, 45 | 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 46 | -1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 47 | // Back face 48 | -1.0, -1.0, -1.0, 1.0, 0.0, 0.0, 49 | -1.0, 1.0, -1.0, 0.0, 1.0, 0.0, 50 | 1.0, 1.0, -1.0, 0.0, 0.0, 1.0, 51 | 1.0, -1.0, -1.0, 0.0, 1.0, 0.0, 52 | // Top face 53 | -1.0, 1.0, -1.0, 1.0, 0.0, 0.0, 54 | -1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 55 | 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 56 | 1.0, 1.0, -1.0, 0.0, 1.0, 0.0, 57 | // Bottom face 58 | -1.0, -1.0, -1.0, 1.0, 0.0, 0.0, 59 | 1.0, -1.0, -1.0, 0.0, 1.0, 0.0, 60 | 1.0, -1.0, 1.0, 0.0, 0.0, 1.0, 61 | -1.0, -1.0, 1.0, 0.0, 1.0, 0.0, 62 | // Right face 63 | 1.0, -1.0, -1.0, 1.0, 0.0, 0.0, 64 | 1.0, 1.0, -1.0, 0.0, 1.0, 0.0, 65 | 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 66 | 1.0, -1.0, 1.0, 0.0, 1.0, 0.0, 67 | // Left face 68 | -1.0, -1.0, -1.0, 1.0, 0.0, 0.0, 69 | -1.0, -1.0, 1.0, 0.0, 1.0, 0.0, 70 | -1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 71 | -1.0, 1.0, -1.0, 0.0, 1.0, 0.0 72 | ]); 73 | 74 | const modelIndices = new Uint32Array([ 75 | 0, 1, 2, 76 | 2, 3, 0, 77 | 4, 5, 6, 78 | 6, 7, 4, 79 | 8, 9, 10, 80 | 10, 11, 8, 81 | 12, 13, 14, 82 | 14, 15, 12, 83 | 16, 17, 18, 84 | 18, 19, 16, 85 | 20, 21, 22, 86 | 22, 23, 20 87 | ]); 88 | 89 | const window = new WebGPUWindow({ 90 | width: 640, 91 | height: 480, 92 | title: "WebGPU", 93 | resizable: false 94 | }); 95 | 96 | const aspect = Math.abs(window.width / window.height); 97 | 98 | const mModel = mat4.create(); 99 | const mView = mat4.create(); 100 | const mProjection = mat4.create(); 101 | const mModelViewProjection = mat4.create(); 102 | 103 | mat4.perspective(mProjection, (2 * Math.PI) / 5, -aspect, 0.1, 4096.0); 104 | mat4.rotateX(mView, mView, -35 * Math.PI / 180); 105 | mat4.translate(mView, mView, vec3.fromValues(0, 6, -8)); 106 | 107 | const adapter = await GPU.requestAdapter({ window }); 108 | 109 | const device = await adapter.requestDevice(); 110 | 111 | const queue = device.getQueue(); 112 | 113 | const context = window.getContext("webgpu"); 114 | 115 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 116 | 117 | const swapChain = context.configureSwapChain({ 118 | device: device, 119 | format: swapChainFormat 120 | }); 121 | 122 | const stagedVertexBuffer = device.createBuffer({ 123 | size: modelVertices.byteLength, 124 | usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST 125 | }); 126 | stagedVertexBuffer.setSubData(0, modelVertices); 127 | 128 | const stagedIndexBuffer = device.createBuffer({ 129 | size: modelIndices.byteLength, 130 | usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST 131 | }); 132 | stagedIndexBuffer.setSubData(0, modelIndices); 133 | 134 | const stagedUniformBuffer = device.createBuffer({ 135 | size: mModelViewProjection.byteLength, 136 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST 137 | }); 138 | stagedUniformBuffer.setSubData(0, mModelViewProjection); 139 | 140 | const uniformBindGroupLayout = device.createBindGroupLayout({ 141 | entries: [{ 142 | binding: 0, 143 | visibility: GPUShaderStage.VERTEX, 144 | type: "uniform-buffer" 145 | }] 146 | }); 147 | 148 | const layout = device.createPipelineLayout({ 149 | bindGroupLayouts: [ uniformBindGroupLayout ] 150 | }); 151 | 152 | const vertexShaderModule = device.createShaderModule({ code: vsSrc }); 153 | const fragmentShaderModule = device.createShaderModule({ code: fsSrc }); 154 | 155 | const pipeline = device.createRenderPipeline({ 156 | layout, 157 | sampleCount: 1, 158 | vertexStage: { 159 | module: vertexShaderModule, 160 | entryPoint: "main" 161 | }, 162 | fragmentStage: { 163 | module: fragmentShaderModule, 164 | entryPoint: "main" 165 | }, 166 | primitiveTopology: "triangle-list", 167 | depthStencilState: { 168 | depthWriteEnabled: true, 169 | depthCompare: "less", 170 | format: "depth24plus-stencil8", 171 | stencilFront: {}, 172 | stencilBack: {}, 173 | }, 174 | vertexState: { 175 | indexFormat: "uint32", 176 | vertexBuffers: [ 177 | { 178 | arrayStride: 6 * Float32Array.BYTES_PER_ELEMENT, 179 | stepMode: "vertex", 180 | attributes: [ 181 | { 182 | shaderLocation: 0, 183 | offset: 0 * Float32Array.BYTES_PER_ELEMENT, 184 | format: "float3" 185 | }, 186 | { 187 | shaderLocation: 1, 188 | offset: 3 * Float32Array.BYTES_PER_ELEMENT, 189 | format: "float3" 190 | } 191 | ] 192 | }, 193 | ] 194 | }, 195 | rasterizationState: { 196 | frontFace: "CCW", 197 | cullMode: "none" 198 | }, 199 | colorStates: [{ 200 | format: swapChainFormat, 201 | alphaBlend: {}, 202 | colorBlend: {} 203 | }] 204 | }); 205 | 206 | const uniformBindGroup = device.createBindGroup({ 207 | layout: uniformBindGroupLayout, 208 | entries: [{ 209 | binding: 0, 210 | buffer: stagedUniformBuffer, 211 | offset: 0, 212 | size: mModelViewProjection.byteLength 213 | }] 214 | }); 215 | 216 | const depthTexture = device.createTexture({ 217 | size: { 218 | width: window.width, 219 | height: window.height, 220 | depth: 1 221 | }, 222 | arrayLayerCount: 1, 223 | mipLevelCount: 1, 224 | sampleCount: 1, 225 | dimension: "2d", 226 | format: "depth24plus-stencil8", 227 | usage: GPUTextureUsage.OUTPUT_ATTACHMENT 228 | }); 229 | 230 | const depthStencilAttachment = { 231 | attachment: depthTexture.createView({ format: "depth24plus-stencil8" }), 232 | clearDepth: 1.0, 233 | depthLoadOp: "clear", 234 | depthStoreOp: "store", 235 | clearStencil: 0, 236 | stencilLoadOp: "clear", 237 | stencilStoreOp: "store", 238 | }; 239 | 240 | function onFrame() { 241 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 242 | 243 | let now = Date.now() / 1e3; 244 | mat4.identity(mModel); 245 | mat4.rotateY(mModel, mModel, now); 246 | mat4.scale(mModel, mModel, vec3.fromValues(3, 3, 3)); 247 | mat4.multiply(mModelViewProjection, mView, mModel); 248 | mat4.multiply(mModelViewProjection, mProjection, mModelViewProjection); 249 | stagedUniformBuffer.setSubData(0, mModelViewProjection); 250 | 251 | const backBufferView = swapChain.getCurrentTextureView(); 252 | const commandEncoder = device.createCommandEncoder({}); 253 | const renderPass = commandEncoder.beginRenderPass({ 254 | colorAttachments: [{ 255 | clearColor: { r: 0.125, g: 0.125, b: 0.125, a: 1.0 }, 256 | loadOp: "clear", 257 | storeOp: "store", 258 | attachment: backBufferView 259 | }], 260 | depthStencilAttachment 261 | }); 262 | renderPass.setPipeline(pipeline); 263 | renderPass.setBindGroup(0, uniformBindGroup); 264 | renderPass.setVertexBuffer(0, stagedVertexBuffer, 0); 265 | renderPass.setIndexBuffer(stagedIndexBuffer); 266 | renderPass.drawIndexed(modelIndices.length, 1, 0, 0, 0); 267 | renderPass.endPass(); 268 | 269 | const commandBuffer = commandEncoder.finish(); 270 | queue.submit([ commandBuffer ]); 271 | swapChain.present(); 272 | window.pollEvents(); 273 | }; 274 | setTimeout(onFrame, 1e3 / 60); 275 | 276 | })(); 277 | -------------------------------------------------------------------------------- /rotating-triangle.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | import glMatrix from "gl-matrix"; 3 | 4 | Object.assign(global, WebGPU); 5 | Object.assign(global, glMatrix); 6 | 7 | const vsSrc = ` 8 | #version 450 9 | #pragma shader_stage(vertex) 10 | 11 | layout(set = 0, binding = 0) uniform Matrices { 12 | mat4 modelViewProjection; 13 | } uMatrices; 14 | 15 | layout(location = 0) in vec2 position; 16 | layout(location = 1) in vec3 color; 17 | 18 | layout(location = 0) out vec4 vColor; 19 | 20 | void main() { 21 | gl_Position = uMatrices.modelViewProjection * vec4(position, 0.0, 1.0); 22 | vColor = vec4(color, 1.0); 23 | } 24 | `; 25 | 26 | const fsSrc = ` 27 | #version 450 28 | #pragma shader_stage(fragment) 29 | 30 | layout(location = 0) in vec4 vColor; 31 | 32 | layout(location = 0) out vec4 outColor; 33 | 34 | void main() { 35 | outColor = vColor; 36 | } 37 | `; 38 | 39 | (async function main() { 40 | 41 | const triangleVertices = new Float32Array([ 42 | 0.0, 0.5, 1.0, 0.0, 0.0, 43 | -0.5, -0.5, 0.0, 1.0, 0.0, 44 | 0.5, -0.5, 0.0, 0.0, 1.0 45 | ]); 46 | 47 | const triangleIndices = new Uint32Array([ 48 | 0, 1, 2 49 | ]); 50 | 51 | const window = new WebGPUWindow({ 52 | width: 640, 53 | height: 480, 54 | title: "WebGPU" 55 | }); 56 | 57 | const aspect = Math.abs(window.width / window.height); 58 | 59 | const mModel = mat4.create(); 60 | const mView = mat4.create(); 61 | const mProjection = mat4.create(); 62 | const mModelViewProjection = mat4.create(); 63 | 64 | mat4.perspective(mProjection, (2 * Math.PI) / 5, -aspect, 0.1, 4096.0); 65 | mat4.translate(mView, mView, vec3.fromValues(0, 0, -4)); 66 | 67 | const adapter = await GPU.requestAdapter({ window }); 68 | 69 | const device = await adapter.requestDevice(); 70 | 71 | const queue = device.getQueue(); 72 | 73 | const context = window.getContext("webgpu"); 74 | 75 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 76 | 77 | const swapChain = context.configureSwapChain({ 78 | device: device, 79 | format: swapChainFormat 80 | }); 81 | 82 | const stagedVertexBuffer = device.createBuffer({ 83 | size: triangleVertices.byteLength, 84 | usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST 85 | }); 86 | stagedVertexBuffer.setSubData(0, triangleVertices); 87 | 88 | const stagedIndexBuffer = device.createBuffer({ 89 | size: triangleIndices.byteLength, 90 | usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST 91 | }); 92 | stagedIndexBuffer.setSubData(0, triangleIndices); 93 | 94 | const stagedUniformBuffer = device.createBuffer({ 95 | size: mModelViewProjection.byteLength, 96 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST 97 | }); 98 | stagedUniformBuffer.setSubData(0, mModelViewProjection); 99 | 100 | const uniformBindGroupLayout = device.createBindGroupLayout({ 101 | entries: [{ 102 | binding: 0, 103 | visibility: GPUShaderStage.VERTEX, 104 | type: "uniform-buffer" 105 | }] 106 | }); 107 | 108 | const layout = device.createPipelineLayout({ 109 | bindGroupLayouts: [ uniformBindGroupLayout ] 110 | }); 111 | 112 | const vertexShaderModule = device.createShaderModule({ code: vsSrc }); 113 | const fragmentShaderModule = device.createShaderModule({ code: fsSrc }); 114 | 115 | const pipeline = device.createRenderPipeline({ 116 | layout, 117 | sampleCount: 1, 118 | vertexStage: { 119 | module: vertexShaderModule, 120 | entryPoint: "main" 121 | }, 122 | fragmentStage: { 123 | module: fragmentShaderModule, 124 | entryPoint: "main" 125 | }, 126 | primitiveTopology: "triangle-list", 127 | vertexState: { 128 | indexFormat: "uint32", 129 | vertexBuffers: [ 130 | { 131 | arrayStride: 5 * Float32Array.BYTES_PER_ELEMENT, 132 | stepMode: "vertex", 133 | attributes: [ 134 | { 135 | shaderLocation: 0, 136 | offset: 0 * Float32Array.BYTES_PER_ELEMENT, 137 | format: "float2" 138 | }, 139 | { 140 | shaderLocation: 1, 141 | offset: 2 * Float32Array.BYTES_PER_ELEMENT, 142 | format: "float3" 143 | } 144 | ] 145 | }, 146 | ] 147 | }, 148 | rasterizationState: { 149 | frontFace: "CCW", 150 | cullMode: "none" 151 | }, 152 | colorStates: [{ 153 | format: swapChainFormat, 154 | alphaBlend: {}, 155 | colorBlend: {} 156 | }] 157 | }); 158 | 159 | const uniformBindGroup = device.createBindGroup({ 160 | layout: uniformBindGroupLayout, 161 | entries: [{ 162 | binding: 0, 163 | buffer: stagedUniformBuffer, 164 | offset: 0, 165 | size: mModelViewProjection.byteLength 166 | }] 167 | }); 168 | 169 | function onFrame() { 170 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 171 | 172 | let now = Date.now() / 1e3; 173 | mat4.identity(mModel); 174 | mat4.rotateY(mModel, mModel, now); 175 | mat4.scale(mModel, mModel, vec3.fromValues(3, 3, 3)); 176 | mat4.multiply(mModelViewProjection, mView, mModel); 177 | mat4.multiply(mModelViewProjection, mProjection, mModelViewProjection); 178 | stagedUniformBuffer.setSubData(0, mModelViewProjection); 179 | 180 | const backBufferView = swapChain.getCurrentTextureView(); 181 | const commandEncoder = device.createCommandEncoder({}); 182 | const renderPass = commandEncoder.beginRenderPass({ 183 | colorAttachments: [{ 184 | clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 185 | loadOp: "clear", 186 | storeOp: "store", 187 | attachment: backBufferView 188 | }] 189 | }); 190 | renderPass.setPipeline(pipeline); 191 | renderPass.setBindGroup(0, uniformBindGroup); 192 | renderPass.setVertexBuffer(0, stagedVertexBuffer, 0); 193 | renderPass.setIndexBuffer(stagedIndexBuffer); 194 | renderPass.drawIndexed(triangleIndices.length, 1, 0, 0, 0); 195 | renderPass.endPass(); 196 | 197 | const commandBuffer = commandEncoder.finish(); 198 | queue.submit([ commandBuffer ]); 199 | swapChain.present(); 200 | window.pollEvents(); 201 | }; 202 | setTimeout(onFrame, 1e3 / 60); 203 | 204 | })(); 205 | -------------------------------------------------------------------------------- /storage-texture.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | 3 | Object.assign(global, WebGPU); 4 | 5 | const writeVsSrc = ` 6 | #version 450 7 | #pragma shader_stage(vertex) 8 | layout (location = 0) out vec2 vUV; 9 | void main(void) { 10 | vec2 uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); 11 | gl_Position = vec4(uv * 2.0 + -1.0f, 0.0, 1.0); 12 | vUV = vec2(uv.x, 1.0 - uv.y); 13 | } 14 | `; 15 | 16 | const writeFsSrc = ` 17 | #version 450 18 | #pragma shader_stage(fragment) 19 | layout (location = 0) in vec2 vUv; 20 | layout(set = 0, binding = 0, rgba8) uniform writeonly image2D image0; 21 | void main() { 22 | imageStore(image0, ivec2(vUv * vec2(640.0, 480.0)), vec4(vUv, 0.5, 0)); 23 | discard; 24 | } 25 | `; 26 | 27 | const readVsSrc = ` 28 | #version 450 29 | #pragma shader_stage(vertex) 30 | layout (location = 0) out vec2 vUV; 31 | void main(void) { 32 | vec2 uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); 33 | gl_Position = vec4(uv * 2.0 + -1.0f, 0.0, 1.0); 34 | vUV = vec2(uv.x, 1.0 - uv.y); 35 | } 36 | `; 37 | 38 | const readFsSrc = ` 39 | #version 450 40 | #pragma shader_stage(fragment) 41 | layout (location = 0) in vec2 vUv; 42 | layout (location = 0) out vec4 outColor; 43 | layout(set = 0, binding = 0, rgba8) uniform readonly image2D image0; 44 | void main() { 45 | outColor = imageLoad(image0, ivec2(vUv * vec2(640.0, 480.0))); 46 | } 47 | `; 48 | 49 | (async function main() { 50 | 51 | const window = new WebGPUWindow({ 52 | width: 640, 53 | height: 480, 54 | title: "WebGPU" 55 | }); 56 | 57 | let adapter = await GPU.requestAdapter({ window }); 58 | 59 | const device = await adapter.requestDevice(); 60 | 61 | const queue = device.getQueue(); 62 | 63 | const context = window.getContext("webgpu"); 64 | 65 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 66 | 67 | const swapChain = context.configureSwapChain({ 68 | device: device, 69 | format: swapChainFormat 70 | }); 71 | 72 | const storageTexture = device.createTexture({ 73 | size: { 74 | width: window.width, 75 | height: window.height, 76 | depth: 1 77 | }, 78 | arrayLayerCount: 1, 79 | mipLevelCount: 1, 80 | sampleCount: 1, 81 | dimension: "2d", 82 | format: "rgba8unorm", 83 | usage: GPUTextureUsage.STORAGE 84 | }); 85 | 86 | const storageTextureView = storageTexture.createView({ 87 | dimension: "2d", 88 | baseArrayLayer: 0, 89 | arrayLayerCount: 1, 90 | format: "rgba8unorm" 91 | }); 92 | 93 | const writeBindGroupLayout = device.createBindGroupLayout({ 94 | entries: [ 95 | { 96 | binding: 0, 97 | visibility: GPUShaderStage.FRAGMENT, 98 | type: "writeonly-storage-texture", 99 | viewDimension: "2d", 100 | storageTextureFormat: "rgba8unorm" 101 | } 102 | ] 103 | }); 104 | 105 | const writeBindGroup = device.createBindGroup({ 106 | layout: writeBindGroupLayout, 107 | entries: [ 108 | { 109 | binding: 0, 110 | textureView: storageTextureView, 111 | size: 0 112 | } 113 | ] 114 | }); 115 | 116 | const writePipeline = device.createRenderPipeline({ 117 | layout: device.createPipelineLayout({ 118 | bindGroupLayouts: [writeBindGroupLayout] 119 | }), 120 | sampleCount: 1, 121 | vertexStage: { 122 | module: device.createShaderModule({ code: writeVsSrc }), 123 | entryPoint: "main" 124 | }, 125 | fragmentStage: { 126 | module: device.createShaderModule({ code: writeFsSrc }), 127 | entryPoint: "main" 128 | }, 129 | primitiveTopology: "triangle-list", 130 | vertexInput: { 131 | indexFormat: "uint32", 132 | buffers: [] 133 | }, 134 | rasterizationState: { 135 | frontFace: "CCW", 136 | cullMode: "none" 137 | }, 138 | colorStates: [ 139 | { 140 | format: swapChainFormat, 141 | alphaBlend: {}, 142 | colorBlend: {} 143 | } 144 | ] 145 | }); 146 | 147 | const readBindGroupLayout = device.createBindGroupLayout({ 148 | entries: [ 149 | { 150 | binding: 0, 151 | visibility: GPUShaderStage.FRAGMENT, 152 | type: "readonly-storage-texture", 153 | viewDimension: "2d", 154 | storageTextureFormat: "rgba8unorm" 155 | } 156 | ] 157 | }); 158 | 159 | const readBindGroup = device.createBindGroup({ 160 | layout: readBindGroupLayout, 161 | entries: [ 162 | { 163 | binding: 0, 164 | textureView: storageTextureView, 165 | size: 0 166 | } 167 | ] 168 | }); 169 | 170 | const readPipeline = device.createRenderPipeline({ 171 | layout: device.createPipelineLayout({ 172 | bindGroupLayouts: [readBindGroupLayout] 173 | }), 174 | sampleCount: 1, 175 | vertexStage: { 176 | module: device.createShaderModule({ code: readVsSrc }), 177 | entryPoint: "main" 178 | }, 179 | fragmentStage: { 180 | module: device.createShaderModule({ code: readFsSrc }), 181 | entryPoint: "main" 182 | }, 183 | primitiveTopology: "triangle-list", 184 | vertexInput: { 185 | indexFormat: "uint32", 186 | buffers: [] 187 | }, 188 | rasterizationState: { 189 | frontFace: "CCW", 190 | cullMode: "none" 191 | }, 192 | colorStates: [ 193 | { 194 | format: swapChainFormat, 195 | alphaBlend: {}, 196 | colorBlend: {} 197 | } 198 | ] 199 | }); 200 | 201 | function onFrame() { 202 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 203 | 204 | let commands = []; 205 | // storage texture write pass 206 | { 207 | const backBufferView = swapChain.getCurrentTextureView(); 208 | const commandEncoder = device.createCommandEncoder({}); 209 | const renderPass = commandEncoder.beginRenderPass({ 210 | colorAttachments: [{ 211 | clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 212 | loadOp: "clear", 213 | storeOp: "store", 214 | attachment: backBufferView 215 | }] 216 | }); 217 | renderPass.setPipeline(writePipeline); 218 | renderPass.setBindGroup(0, writeBindGroup); 219 | renderPass.draw(3, 1, 0, 0); 220 | renderPass.endPass(); 221 | commands.push(commandEncoder.finish()); 222 | } 223 | 224 | // storage texture read pass 225 | { 226 | const backBufferView = swapChain.getCurrentTextureView(); 227 | const commandEncoder = device.createCommandEncoder({}); 228 | const renderPass = commandEncoder.beginRenderPass({ 229 | colorAttachments: [{ 230 | clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 231 | loadOp: "clear", 232 | storeOp: "store", 233 | attachment: backBufferView 234 | }] 235 | }); 236 | renderPass.setPipeline(readPipeline); 237 | renderPass.setBindGroup(0, readBindGroup); 238 | renderPass.draw(3, 1, 0, 0); 239 | renderPass.endPass(); 240 | commands.push(commandEncoder.finish()); 241 | } 242 | 243 | queue.submit(commands); 244 | swapChain.present(); 245 | window.pollEvents(); 246 | }; 247 | setTimeout(onFrame, 1e3 / 60); 248 | 249 | })(); 250 | -------------------------------------------------------------------------------- /swapchain-blit.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | 3 | Object.assign(global, WebGPU); 4 | 5 | const vsSrc = ` 6 | #version 450 7 | #pragma shader_stage(vertex) 8 | const vec2 pos[3] = vec2[3]( 9 | vec2(0.0f, 0.5f), 10 | vec2(-0.5f, -0.5f), 11 | vec2(0.5f, -0.5f) 12 | ); 13 | void main() { 14 | gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0); 15 | } 16 | `; 17 | 18 | const fsSrc = ` 19 | #version 450 20 | #pragma shader_stage(fragment) 21 | layout(location = 0) out vec4 outColor; 22 | void main() { 23 | outColor = vec4(1.0, 0.0, 0.0, 1.0); 24 | } 25 | `; 26 | 27 | const blitVsSrc = ` 28 | #version 450 29 | #pragma shader_stage(vertex) 30 | layout (location = 0) out vec2 vUV; 31 | void main(void) { 32 | vec2 uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); 33 | gl_Position = vec4(uv * 2.0 + -1.0f, 0.0, 1.0); 34 | vUV = vec2(uv.x, 1.0 - uv.y); 35 | } 36 | `; 37 | 38 | const blitFsSrc = ` 39 | #version 450 40 | #pragma shader_stage(fragment) 41 | layout (location = 0) in vec2 vUv; 42 | layout (location = 0) out vec4 outColor; 43 | layout (binding = 0) uniform sampler sampler0; 44 | layout (binding = 1) uniform texture2D texture1; 45 | void main() { 46 | outColor = texture(sampler2D(texture1, sampler0), vUv); 47 | } 48 | `; 49 | 50 | (async function main() { 51 | 52 | const window = new WebGPUWindow({ 53 | width: 640, 54 | height: 480, 55 | title: "WebGPU" 56 | }); 57 | 58 | const adapter = await GPU.requestAdapter({ window }); 59 | 60 | const device = await adapter.requestDevice(); 61 | 62 | const queue = device.getQueue(); 63 | 64 | const context = window.getContext("webgpu"); 65 | 66 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 67 | 68 | const swapChain = context.configureSwapChain({ 69 | device: device, 70 | format: swapChainFormat 71 | }); 72 | 73 | const renderPipeline = device.createRenderPipeline({ 74 | layout: device.createPipelineLayout({ 75 | bindGroupLayouts: [] 76 | }), 77 | sampleCount: 1, 78 | vertexStage: { 79 | module: device.createShaderModule({ code: vsSrc }), 80 | entryPoint: "main" 81 | }, 82 | fragmentStage: { 83 | module: device.createShaderModule({ code: fsSrc }), 84 | entryPoint: "main" 85 | }, 86 | primitiveTopology: "triangle-list", 87 | vertexInput: { 88 | indexFormat: "uint32", 89 | buffers: [] 90 | }, 91 | rasterizationState: { 92 | frontFace: "CCW", 93 | cullMode: "none" 94 | }, 95 | colorStates: [{ 96 | format: swapChainFormat, 97 | alphaBlend: {}, 98 | colorBlend: {} 99 | }] 100 | }); 101 | 102 | const linearSampler = device.createSampler({ 103 | magFilter: "linear", 104 | minFilter: "linear", 105 | addressModeU: "repeat", 106 | addressModeV: "repeat", 107 | addressModeW: "repeat" 108 | }); 109 | 110 | const blitTexture = device.createTexture({ 111 | size: { 112 | width: window.width, 113 | height: window.height, 114 | depth: 1 115 | }, 116 | arrayLayerCount: 1, 117 | mipLevelCount: 1, 118 | sampleCount: 1, 119 | dimension: "2d", 120 | format: swapChainFormat, 121 | usage: GPUTextureUsage.OUTPUT_ATTACHMENT | GPUTextureUsage.SAMPLED 122 | }); 123 | 124 | const blitTextureView = blitTexture.createView({ 125 | format: swapChainFormat 126 | }); 127 | 128 | const blitBindGroupLayout = device.createBindGroupLayout({ 129 | entries: [ 130 | { 131 | binding: 0, 132 | visibility: GPUShaderStage.FRAGMENT, 133 | type: "sampler" 134 | }, 135 | { 136 | binding: 1, 137 | visibility: GPUShaderStage.FRAGMENT, 138 | type: "sampled-texture" 139 | } 140 | ] 141 | }); 142 | 143 | const blitBindGroup = device.createBindGroup({ 144 | layout: blitBindGroupLayout, 145 | entries: [ 146 | { 147 | binding: 0, 148 | sampler: linearSampler, 149 | size: 0 150 | }, 151 | { 152 | binding: 1, 153 | textureView: blitTextureView, 154 | size: 0 155 | } 156 | ] 157 | }); 158 | 159 | const blitPipeline = device.createRenderPipeline({ 160 | layout: device.createPipelineLayout({ 161 | bindGroupLayouts: [blitBindGroupLayout] 162 | }), 163 | sampleCount: 1, 164 | vertexStage: { 165 | module: device.createShaderModule({ code: blitVsSrc }), 166 | entryPoint: "main" 167 | }, 168 | fragmentStage: { 169 | module: device.createShaderModule({ code: blitFsSrc }), 170 | entryPoint: "main" 171 | }, 172 | primitiveTopology: "triangle-list", 173 | vertexInput: { 174 | indexFormat: "uint32", 175 | buffers: [] 176 | }, 177 | rasterizationState: { 178 | frontFace: "CCW", 179 | cullMode: "none" 180 | }, 181 | colorStates: [{ 182 | format: swapChainFormat, 183 | alphaBlend: {}, 184 | colorBlend: {} 185 | }] 186 | }); 187 | 188 | let cachedCommandBuffer = null; 189 | function onFrame() { 190 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 191 | 192 | const backBufferView = swapChain.getCurrentTextureView(); 193 | 194 | // since we don't render directly into the swapchain 195 | // we can now cache command buffers of previous passes 196 | if (cachedCommandBuffer === null) { 197 | const commandEncoder = device.createCommandEncoder({}); 198 | const renderPass = commandEncoder.beginRenderPass({ 199 | colorAttachments: [{ 200 | clearColor: { r: 0.125, g: 0.125, b: 0.125, a: 1.0 }, 201 | loadOp: "clear", 202 | storeOp: "store", 203 | attachment: blitTextureView 204 | }] 205 | }); 206 | renderPass.setPipeline(renderPipeline); 207 | renderPass.draw(3, 1, 0, 0); 208 | renderPass.endPass(); 209 | cachedCommandBuffer = commandEncoder.finish(); 210 | console.log("Render Pass Command Buffer is cached"); 211 | } 212 | // submit cached command buffer 213 | queue.submit([ cachedCommandBuffer ]); 214 | // blit to swapchain 215 | { 216 | const commandEncoder = device.createCommandEncoder({}); 217 | const blitPass = commandEncoder.beginRenderPass({ 218 | colorAttachments: [{ 219 | clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 220 | loadOp: "clear", 221 | storeOp: "store", 222 | attachment: backBufferView 223 | }] 224 | }); 225 | blitPass.setPipeline(blitPipeline); 226 | blitPass.setBindGroup(0, blitBindGroup); 227 | blitPass.draw(3, 1, 0, 0); 228 | blitPass.endPass(); 229 | const commandBuffer = commandEncoder.finish(); 230 | queue.submit([ commandBuffer ]); 231 | } 232 | 233 | swapChain.present(); 234 | window.pollEvents(); 235 | }; 236 | setTimeout(onFrame, 1e3 / 60); 237 | 238 | })(); 239 | -------------------------------------------------------------------------------- /swapchain-recreation.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | import glMatrix from "gl-matrix"; 3 | 4 | Object.assign(global, WebGPU); 5 | Object.assign(global, glMatrix); 6 | 7 | const vsSrc = ` 8 | #version 450 9 | #pragma shader_stage(vertex) 10 | 11 | layout(set = 0, binding = 0) uniform Matrices { 12 | mat4 modelViewProjection; 13 | } uMatrices; 14 | 15 | layout(location = 0) in vec3 position; 16 | layout(location = 1) in vec3 color; 17 | 18 | layout(location = 0) out vec4 vColor; 19 | 20 | void main() { 21 | gl_Position = uMatrices.modelViewProjection * vec4(position, 1.0); 22 | vColor = vec4(color, 1.0); 23 | } 24 | `; 25 | 26 | const fsSrc = ` 27 | #version 450 28 | #pragma shader_stage(fragment) 29 | 30 | layout(location = 0) in vec4 vColor; 31 | 32 | layout(location = 0) out vec4 outColor; 33 | 34 | void main() { 35 | outColor = vColor; 36 | } 37 | `; 38 | 39 | (async function main() { 40 | 41 | const modelVertices = new Float32Array([ 42 | // Front face 43 | -1.0, -1.0, 1.0, 1.0, 0.0, 0.0, 44 | 1.0, -1.0, 1.0, 0.0, 1.0, 0.0, 45 | 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 46 | -1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 47 | // Back face 48 | -1.0, -1.0, -1.0, 1.0, 0.0, 0.0, 49 | -1.0, 1.0, -1.0, 0.0, 1.0, 0.0, 50 | 1.0, 1.0, -1.0, 0.0, 0.0, 1.0, 51 | 1.0, -1.0, -1.0, 0.0, 1.0, 0.0, 52 | // Top face 53 | -1.0, 1.0, -1.0, 1.0, 0.0, 0.0, 54 | -1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 55 | 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 56 | 1.0, 1.0, -1.0, 0.0, 1.0, 0.0, 57 | // Bottom face 58 | -1.0, -1.0, -1.0, 1.0, 0.0, 0.0, 59 | 1.0, -1.0, -1.0, 0.0, 1.0, 0.0, 60 | 1.0, -1.0, 1.0, 0.0, 0.0, 1.0, 61 | -1.0, -1.0, 1.0, 0.0, 1.0, 0.0, 62 | // Right face 63 | 1.0, -1.0, -1.0, 1.0, 0.0, 0.0, 64 | 1.0, 1.0, -1.0, 0.0, 1.0, 0.0, 65 | 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 66 | 1.0, -1.0, 1.0, 0.0, 1.0, 0.0, 67 | // Left face 68 | -1.0, -1.0, -1.0, 1.0, 0.0, 0.0, 69 | -1.0, -1.0, 1.0, 0.0, 1.0, 0.0, 70 | -1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 71 | -1.0, 1.0, -1.0, 0.0, 1.0, 0.0 72 | ]); 73 | 74 | const modelIndices = new Uint32Array([ 75 | 0, 1, 2, 76 | 2, 3, 0, 77 | 4, 5, 6, 78 | 6, 7, 4, 79 | 8, 9, 10, 80 | 10, 11, 8, 81 | 12, 13, 14, 82 | 14, 15, 12, 83 | 16, 17, 18, 84 | 18, 19, 16, 85 | 20, 21, 22, 86 | 22, 23, 20 87 | ]); 88 | 89 | const window = new WebGPUWindow({ 90 | width: 640, 91 | height: 480, 92 | title: "WebGPU" 93 | }); 94 | 95 | const mModel = mat4.create(); 96 | const mView = mat4.create(); 97 | const mProjection = mat4.create(); 98 | const mModelViewProjection = mat4.create(); 99 | 100 | mat4.perspective(mProjection, (2 * Math.PI) / 5, -(window.width / window.height), 0.1, 4096.0); 101 | mat4.rotateX(mView, mView, -35 * Math.PI / 180); 102 | mat4.translate(mView, mView, vec3.fromValues(0, 6, -8)); 103 | 104 | const adapter = await GPU.requestAdapter({ window }); 105 | 106 | const device = await adapter.requestDevice(); 107 | 108 | const queue = device.getQueue(); 109 | 110 | const context = window.getContext("webgpu"); 111 | 112 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 113 | 114 | const swapChain = context.configureSwapChain({ 115 | device: device, 116 | format: swapChainFormat 117 | }); 118 | 119 | const stagedVertexBuffer = device.createBuffer({ 120 | size: modelVertices.byteLength, 121 | usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST 122 | }); 123 | stagedVertexBuffer.setSubData(0, modelVertices); 124 | 125 | const stagedIndexBuffer = device.createBuffer({ 126 | size: modelIndices.byteLength, 127 | usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST 128 | }); 129 | stagedIndexBuffer.setSubData(0, modelIndices); 130 | 131 | const stagedUniformBuffer = device.createBuffer({ 132 | size: mModelViewProjection.byteLength, 133 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST 134 | }); 135 | stagedUniformBuffer.setSubData(0, mModelViewProjection); 136 | 137 | const uniformBindGroupLayout = device.createBindGroupLayout({ 138 | entries: [{ 139 | binding: 0, 140 | visibility: GPUShaderStage.VERTEX, 141 | type: "uniform-buffer" 142 | }] 143 | }); 144 | 145 | const layout = device.createPipelineLayout({ 146 | bindGroupLayouts: [ uniformBindGroupLayout ] 147 | }); 148 | 149 | const vertexShaderModule = device.createShaderModule({ code: vsSrc }); 150 | const fragmentShaderModule = device.createShaderModule({ code: fsSrc }); 151 | 152 | const pipeline = device.createRenderPipeline({ 153 | layout, 154 | sampleCount: 1, 155 | vertexStage: { 156 | module: vertexShaderModule, 157 | entryPoint: "main" 158 | }, 159 | fragmentStage: { 160 | module: fragmentShaderModule, 161 | entryPoint: "main" 162 | }, 163 | primitiveTopology: "triangle-list", 164 | depthStencilState: { 165 | depthWriteEnabled: true, 166 | depthCompare: "less", 167 | format: "depth24plus-stencil8", 168 | stencilFront: {}, 169 | stencilBack: {}, 170 | }, 171 | vertexState: { 172 | indexFormat: "uint32", 173 | vertexBuffers: [ 174 | { 175 | arrayStride: 6 * Float32Array.BYTES_PER_ELEMENT, 176 | stepMode: "vertex", 177 | attributes: [ 178 | { 179 | shaderLocation: 0, 180 | offset: 0 * Float32Array.BYTES_PER_ELEMENT, 181 | format: "float3" 182 | }, 183 | { 184 | shaderLocation: 1, 185 | offset: 3 * Float32Array.BYTES_PER_ELEMENT, 186 | format: "float3" 187 | } 188 | ] 189 | }, 190 | ] 191 | }, 192 | rasterizationState: { 193 | frontFace: "CCW", 194 | cullMode: "none" 195 | }, 196 | colorStates: [{ 197 | format: swapChainFormat, 198 | alphaBlend: {}, 199 | colorBlend: {} 200 | }] 201 | }); 202 | 203 | const uniformBindGroup = device.createBindGroup({ 204 | layout: uniformBindGroupLayout, 205 | entries: [{ 206 | binding: 0, 207 | buffer: stagedUniformBuffer, 208 | offset: 0, 209 | size: mModelViewProjection.byteLength 210 | }] 211 | }); 212 | 213 | let depthTexture = device.createTexture({ 214 | size: { 215 | width: window.width, 216 | height: window.height, 217 | depth: 1 218 | }, 219 | arrayLayerCount: 1, 220 | mipLevelCount: 1, 221 | sampleCount: 1, 222 | dimension: "2d", 223 | format: "depth24plus-stencil8", 224 | usage: GPUTextureUsage.OUTPUT_ATTACHMENT 225 | }); 226 | let depthStencilAttachment = { 227 | attachment: depthTexture.createView({ format: "depth24plus-stencil8" }), 228 | clearDepth: 1.0, 229 | depthLoadOp: "clear", 230 | depthStoreOp: "store", 231 | clearStencil: 0, 232 | stencilLoadOp: "clear", 233 | stencilStoreOp: "store", 234 | }; 235 | window.onresize = e => { 236 | // recreate projection matrix 237 | mat4.identity(mProjection); 238 | mat4.perspective(mProjection, (2 * Math.PI) / 5, -(window.width / window.height), 0.1, 4096.0); 239 | // recreate depth texture 240 | depthTexture = device.createTexture({ 241 | size: { 242 | width: window.width, 243 | height: window.height, 244 | depth: 1 245 | }, 246 | arrayLayerCount: 1, 247 | mipLevelCount: 1, 248 | sampleCount: 1, 249 | dimension: "2d", 250 | format: "depth24plus-stencil8", 251 | usage: GPUTextureUsage.OUTPUT_ATTACHMENT 252 | }); 253 | depthStencilAttachment.attachment = depthTexture.createView({ 254 | format: "depth24plus-stencil8" 255 | }); 256 | }; 257 | 258 | function onFrame() { 259 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 260 | 261 | let now = Date.now() / 1e3; 262 | mat4.identity(mModel); 263 | mat4.rotateY(mModel, mModel, now); 264 | mat4.scale(mModel, mModel, vec3.fromValues(3, 3, 3)); 265 | mat4.multiply(mModelViewProjection, mView, mModel); 266 | mat4.multiply(mModelViewProjection, mProjection, mModelViewProjection); 267 | stagedUniformBuffer.setSubData(0, mModelViewProjection); 268 | 269 | const backBufferView = swapChain.getCurrentTextureView(); 270 | const commandEncoder = device.createCommandEncoder({}); 271 | const renderPass = commandEncoder.beginRenderPass({ 272 | colorAttachments: [{ 273 | clearColor: { r: 0.125, g: 0.125, b: 0.125, a: 1.0 }, 274 | loadOp: "clear", 275 | storeOp: "store", 276 | attachment: backBufferView 277 | }], 278 | depthStencilAttachment 279 | }); 280 | renderPass.setPipeline(pipeline); 281 | renderPass.setBindGroup(0, uniformBindGroup); 282 | renderPass.setVertexBuffer(0, stagedVertexBuffer, 0); 283 | renderPass.setIndexBuffer(stagedIndexBuffer); 284 | renderPass.drawIndexed(modelIndices.length, 1, 0, 0, 0); 285 | renderPass.endPass(); 286 | 287 | const commandBuffer = commandEncoder.finish(); 288 | queue.submit([ commandBuffer ]); 289 | swapChain.present(); 290 | window.pollEvents(); 291 | }; 292 | setTimeout(onFrame, 1e3 / 60); 293 | 294 | })(); 295 | -------------------------------------------------------------------------------- /texture-array.mjs: -------------------------------------------------------------------------------- 1 | import WebGPU from "webgpu"; 2 | 3 | Object.assign(global, WebGPU); 4 | 5 | const vsSrc = ` 6 | #version 450 7 | #pragma shader_stage(vertex) 8 | layout (location = 0) out vec2 vUV; 9 | void main(void) { 10 | vec2 uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2); 11 | gl_Position = vec4(uv * 2.0 + -1.0f, 0.0, 1.0); 12 | vUV = vec2(uv.x, 1.0 - uv.y); 13 | } 14 | `; 15 | 16 | const fsSrc = ` 17 | #version 450 18 | #pragma shader_stage(fragment) 19 | layout (location = 0) in vec2 vUv; 20 | layout (location = 0) out vec4 outColor; 21 | layout (binding = 0) uniform sampler TextureSampler; 22 | layout (binding = 1) uniform texture2DArray TextureArray; 23 | void main() { 24 | float split = 1.0 / 4.0; 25 | uint textureIndex = ( 26 | vUv.x <= 1.0 * split ? 0 : 27 | vUv.x <= 2.0 * split ? 1 : 28 | vUv.x <= 3.0 * split ? 2 : 29 | vUv.x <= 4.0 * split ? 3 : 30 | 0 31 | ); 32 | outColor = texture(sampler2DArray(TextureArray, TextureSampler), vec3(vUv, textureIndex)); 33 | } 34 | `; 35 | 36 | (async function main() { 37 | 38 | const window = new WebGPUWindow({ 39 | width: 640, 40 | height: 480, 41 | title: "WebGPU" 42 | }); 43 | 44 | const adapter = await GPU.requestAdapter({ window }); 45 | 46 | const device = await adapter.requestDevice(); 47 | 48 | const queue = device.getQueue(); 49 | 50 | const context = window.getContext("webgpu"); 51 | 52 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 53 | 54 | const swapChain = context.configureSwapChain({ 55 | device: device, 56 | format: swapChainFormat 57 | }); 58 | 59 | const imageCount = 4; 60 | const imageWidth = 32; 61 | const imageHeight = 32; 62 | 63 | const textureArray = device.createTexture({ 64 | size: { 65 | width: imageWidth, 66 | height: imageHeight, 67 | depth: 1 68 | }, 69 | arrayLayerCount: imageCount, 70 | mipLevelCount: 1, 71 | sampleCount: 1, 72 | dimension: "2d", 73 | format: "rgba8unorm-srgb", 74 | usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.SAMPLED 75 | }); 76 | 77 | const textureArrayView = textureArray.createView({ 78 | dimension: "2d-array", 79 | baseArrayLayer: 0, 80 | arrayLayerCount: imageCount, 81 | format: "rgba8unorm-srgb" 82 | }); 83 | 84 | const textureSampler = device.createSampler({ 85 | magFilter: "linear", 86 | minFilter: "linear", 87 | addressModeU: "repeat", 88 | addressModeV: "repeat", 89 | addressModeW: "repeat" 90 | }); 91 | 92 | const imageColors = [ 93 | [255, 0, 0], // red 94 | [0, 255, 0], // green 95 | [0, 0, 255], // blue 96 | [255, 0, 255], // pink 97 | ]; 98 | 99 | const commandEncoder = device.createCommandEncoder({}); 100 | for (let ii = 0; ii < imageCount; ++ii) { 101 | const bytesPerRow = Math.ceil(imageWidth * 4 / 256) * 256; 102 | const data = new Uint8Array(bytesPerRow * imageHeight); 103 | for (let yy = 0; yy < imageHeight; ++yy) { 104 | for (let xx = 0; xx < imageWidth; ++xx) { 105 | const index = xx * 4 + yy * bytesPerRow; 106 | data[index + 0] = imageColors[ii][0]; 107 | data[index + 1] = imageColors[ii][1]; 108 | data[index + 2] = imageColors[ii][2]; 109 | data[index + 3] = 255; 110 | }; 111 | }; 112 | 113 | const textureBuffer = device.createBuffer({ 114 | size: data.byteLength, 115 | usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST 116 | }); 117 | textureBuffer.setSubData(0, data); 118 | 119 | commandEncoder.copyBufferToTexture( 120 | { 121 | buffer: textureBuffer, 122 | bytesPerRow: bytesPerRow, 123 | arrayLayer: 0, 124 | mipLevel: 0, 125 | imageHeight: 0 126 | }, 127 | { 128 | texture: textureArray, 129 | mipLevel: 0, 130 | arrayLayer: ii, 131 | origin: { x: 0, y: 0, z: 0 } 132 | }, 133 | { 134 | width: imageWidth, 135 | height: imageHeight, 136 | depth: 1 137 | } 138 | ); 139 | }; 140 | queue.submit([ commandEncoder.finish() ]); 141 | 142 | const bindGroupLayout = device.createBindGroupLayout({ 143 | entries: [ 144 | { 145 | binding: 0, 146 | visibility: GPUShaderStage.FRAGMENT, 147 | type: "sampler" 148 | }, 149 | { 150 | binding: 1, 151 | visibility: GPUShaderStage.FRAGMENT, 152 | type: "sampled-texture", 153 | viewDimension: "2d-array" 154 | } 155 | ] 156 | }); 157 | 158 | const bindGroup = device.createBindGroup({ 159 | layout: bindGroupLayout, 160 | entries: [ 161 | { 162 | binding: 0, 163 | sampler: textureSampler, 164 | size: 0 165 | }, 166 | { 167 | binding: 1, 168 | textureView: textureArrayView, 169 | size: 0 170 | } 171 | ] 172 | }); 173 | 174 | const pipeline = device.createRenderPipeline({ 175 | layout: device.createPipelineLayout({ 176 | bindGroupLayouts: [bindGroupLayout] 177 | }), 178 | sampleCount: 1, 179 | vertexStage: { 180 | module: device.createShaderModule({ code: vsSrc }), 181 | entryPoint: "main" 182 | }, 183 | fragmentStage: { 184 | module: device.createShaderModule({ code: fsSrc }), 185 | entryPoint: "main" 186 | }, 187 | primitiveTopology: "triangle-list", 188 | vertexInput: { 189 | indexFormat: "uint32", 190 | buffers: [] 191 | }, 192 | rasterizationState: { 193 | frontFace: "CCW", 194 | cullMode: "none" 195 | }, 196 | colorStates: [ 197 | { 198 | format: swapChainFormat, 199 | alphaBlend: {}, 200 | colorBlend: {} 201 | } 202 | ] 203 | }); 204 | 205 | function onFrame() { 206 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 207 | 208 | const backBufferView = swapChain.getCurrentTextureView(); 209 | const commandEncoder = device.createCommandEncoder({}); 210 | const renderPass = commandEncoder.beginRenderPass({ 211 | colorAttachments: [{ 212 | clearColor: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, 213 | loadOp: "clear", 214 | storeOp: "store", 215 | attachment: backBufferView 216 | }] 217 | }); 218 | renderPass.setPipeline(pipeline); 219 | renderPass.setBindGroup(0, bindGroup); 220 | renderPass.draw(3, 1, 0, 0); 221 | renderPass.endPass(); 222 | 223 | const commandBuffer = commandEncoder.finish(); 224 | queue.submit([ commandBuffer ]); 225 | swapChain.present(); 226 | window.pollEvents(); 227 | }; 228 | setTimeout(onFrame, 1e3 / 60); 229 | 230 | })(); 231 | -------------------------------------------------------------------------------- /textured-cube.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import WebGPU from "webgpu"; 3 | import glMatrix from "gl-matrix"; 4 | import lodepng from "@cwasm/lodepng"; 5 | 6 | Object.assign(global, WebGPU); 7 | Object.assign(global, glMatrix); 8 | 9 | const vsSrc = ` 10 | #version 450 11 | #pragma shader_stage(vertex) 12 | 13 | layout(set = 0, binding = 0) uniform CameraUniform { 14 | mat4 modelMatrix; 15 | mat4 viewMatrix; 16 | mat4 projectionMatrix; 17 | }; 18 | 19 | layout(location = 0) in vec3 position; 20 | layout(location = 1) in vec3 normal; 21 | layout(location = 2) in vec2 uv; 22 | 23 | layout(location = 0) out vec3 vNormal; 24 | layout(location = 1) out vec2 vUV; 25 | layout(location = 2) out vec4 vWorldPosition; 26 | 27 | void main() { 28 | mat4 MVP = projectionMatrix * viewMatrix * modelMatrix; 29 | gl_Position = MVP * vec4(position, 1.0); 30 | vNormal = mat3(modelMatrix) * normal; 31 | vUV = uv * 0.5; 32 | vWorldPosition = modelMatrix * vec4(position, 1.0); 33 | } 34 | `; 35 | 36 | const fsSrc = ` 37 | #version 450 38 | #pragma shader_stage(fragment) 39 | 40 | layout(set = 0, binding = 0) uniform CameraUniform { 41 | mat4 modelMatrix; 42 | mat4 viewMatrix; 43 | mat4 projectionMatrix; 44 | }; 45 | 46 | layout(set = 0, binding = 1) uniform sampler mySampler; 47 | layout(set = 0, binding = 2) uniform texture2D myTexture; 48 | 49 | layout(location = 0) in vec3 vNormal; 50 | layout(location = 1) in vec2 vUV; 51 | layout(location = 2) in vec4 vWorldPosition; 52 | 53 | layout(location = 0) out vec4 outColor; 54 | 55 | void main() { 56 | vec3 color = texture(sampler2D(myTexture, mySampler), vUV).rgb; 57 | 58 | vec3 cameraPosition = (viewMatrix * vWorldPosition).xyz; 59 | vec3 lightPosition = vec3(1, 6, -2) - vWorldPosition.xyz; 60 | 61 | vec3 N = normalize(vNormal); 62 | vec3 V = normalize(cameraPosition); 63 | vec3 L = normalize(lightPosition); 64 | 65 | vec3 ambient = color * 0.1; 66 | vec3 diffuse = max(dot(L, N), 0.0) * color; 67 | outColor = vec4(ambient + diffuse, 1.0); 68 | } 69 | `; 70 | 71 | (async function main() { 72 | 73 | const modelVertices = new Float32Array([ 74 | // Front face 75 | -1.0, -1.0, 1.0, 76 | 1.0, -1.0, 1.0, 77 | 1.0, 1.0, 1.0, 78 | -1.0, 1.0, 1.0, 79 | // Back face 80 | -1.0, -1.0, -1.0, 81 | -1.0, 1.0, -1.0, 82 | 1.0, 1.0, -1.0, 83 | 1.0, -1.0, -1.0, 84 | // Top face 85 | -1.0, 1.0, -1.0, 86 | -1.0, 1.0, 1.0, 87 | 1.0, 1.0, 1.0, 88 | 1.0, 1.0, -1.0, 89 | // Bottom face 90 | -1.0, -1.0, -1.0, 91 | 1.0, -1.0, -1.0, 92 | 1.0, -1.0, 1.0, 93 | -1.0, -1.0, 1.0, 94 | // Right face 95 | 1.0, -1.0, -1.0, 96 | 1.0, 1.0, -1.0, 97 | 1.0, 1.0, 1.0, 98 | 1.0, -1.0, 1.0, 99 | // Left face 100 | -1.0, -1.0, -1.0, 101 | -1.0, -1.0, 1.0, 102 | -1.0, 1.0, 1.0, 103 | -1.0, 1.0, -1.0 104 | ]); 105 | 106 | const modelNormals = new Float32Array([ 107 | // Front 108 | 0.0, 0.0, 1.0, 109 | 0.0, 0.0, 1.0, 110 | 0.0, 0.0, 1.0, 111 | 0.0, 0.0, 1.0, 112 | // Back 113 | 0.0, 0.0, -1.0, 114 | 0.0, 0.0, -1.0, 115 | 0.0, 0.0, -1.0, 116 | 0.0, 0.0, -1.0, 117 | // Top 118 | 0.0, 1.0, 0.0, 119 | 0.0, 1.0, 0.0, 120 | 0.0, 1.0, 0.0, 121 | 0.0, 1.0, 0.0, 122 | // Bottom 123 | 0.0, -1.0, 0.0, 124 | 0.0, -1.0, 0.0, 125 | 0.0, -1.0, 0.0, 126 | 0.0, -1.0, 0.0, 127 | // Right 128 | 1.0, 0.0, 0.0, 129 | 1.0, 0.0, 0.0, 130 | 1.0, 0.0, 0.0, 131 | 1.0, 0.0, 0.0, 132 | // Left 133 | -1.0, 0.0, 0.0, 134 | -1.0, 0.0, 0.0, 135 | -1.0, 0.0, 0.0, 136 | -1.0, 0.0, 0.0 137 | ]); 138 | 139 | const modelUVs = new Float32Array([ 140 | // Front 141 | 0.025, 0.01, 142 | 0.175, 0.01, 143 | 0.175, 0.175, 144 | 0.025, 0.175, 145 | // Back 146 | 0.025, 0.01, 147 | 0.175, 0.01, 148 | 0.175, 0.175, 149 | 0.025, 0.175, 150 | // Top 151 | 1.0, 0.0, 152 | 1.0, -1.0, 153 | 0.0, -1.0, 154 | 0.0, 0.0, 155 | // Bottom 156 | 0.0, 0.0, 157 | 1.0, 0.0, 158 | 1.0, -1.0, 159 | 0.0, -1.0, 160 | // Right 161 | 0.0, 0.0, 162 | -1.0, 0.0, 163 | -1.0, -1.0, 164 | 0.0, -1.0, 165 | // Left 166 | 1.0, 0.0, 167 | 1.0, -1.0, 168 | 0.0, -1.0, 169 | 0.0, 0.0 170 | ]); 171 | 172 | const modelIndices = new Uint32Array([ 173 | 0, 1, 2, 174 | 2, 3, 0, 175 | 4, 5, 6, 176 | 6, 7, 4, 177 | 8, 9, 10, 178 | 10, 11, 8, 179 | 12, 13, 14, 180 | 14, 15, 12, 181 | 16, 17, 18, 182 | 18, 19, 16, 183 | 20, 21, 22, 184 | 22, 23, 20 185 | ]); 186 | 187 | const img = lodepng.decode(fs.readFileSync("assets/grass-block.png")); 188 | 189 | const window = new WebGPUWindow({ 190 | width: 640, 191 | height: 480, 192 | title: "WebGPU", 193 | resizable: false 194 | }); 195 | 196 | const aspect = Math.abs(window.width / window.height); 197 | 198 | const mModel = mat4.create(); 199 | const mView = mat4.create(); 200 | const mProjection = mat4.create(); 201 | 202 | mat4.perspective(mProjection, 45.0 * Math.PI / 180, -aspect, 0.1, 4096.0); 203 | mat4.lookAt( 204 | mView, 205 | vec3.fromValues(4, 4, -4), 206 | vec3.fromValues(0.0, 0.0, 0.0), 207 | vec3.fromValues(0.0, 0.0, 1.0) 208 | ); 209 | 210 | const adapter = await GPU.requestAdapter({ window }); 211 | 212 | const device = await adapter.requestDevice(); 213 | 214 | const queue = device.getQueue(); 215 | 216 | const context = window.getContext("webgpu"); 217 | 218 | const swapChainFormat = await context.getSwapChainPreferredFormat(device); 219 | 220 | const swapChain = context.configureSwapChain({ 221 | device: device, 222 | format: swapChainFormat 223 | }); 224 | 225 | const stagedVertexBuffer = device.createBuffer({ 226 | size: modelVertices.byteLength, 227 | usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST 228 | }); 229 | stagedVertexBuffer.setSubData(0, modelVertices); 230 | 231 | const stagedNormalBuffer = device.createBuffer({ 232 | size: modelNormals.byteLength, 233 | usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST 234 | }); 235 | stagedNormalBuffer.setSubData(0, modelNormals); 236 | 237 | const stagedUVBuffer = device.createBuffer({ 238 | size: modelUVs.byteLength, 239 | usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST 240 | }); 241 | stagedUVBuffer.setSubData(0, modelUVs); 242 | 243 | const stagedIndexBuffer = device.createBuffer({ 244 | size: modelIndices.byteLength, 245 | usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST 246 | }); 247 | stagedIndexBuffer.setSubData(0, modelIndices); 248 | 249 | const stagedUniformBuffer = device.createBuffer({ 250 | size: 48 * Float32Array.BYTES_PER_ELEMENT, 251 | usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST 252 | }); 253 | stagedUniformBuffer.setSubData(0, mModel); 254 | stagedUniformBuffer.setSubData(16 * Float32Array.BYTES_PER_ELEMENT, mView); 255 | stagedUniformBuffer.setSubData(32 * Float32Array.BYTES_PER_ELEMENT, mProjection); 256 | 257 | const texture = device.createTexture({ 258 | size: { 259 | width: img.width, 260 | height: img.height, 261 | depth: 1, 262 | }, 263 | arrayLayerCount: 1, 264 | mipLevelCount: 1, 265 | sampleCount: 1, 266 | dimension: "2d", 267 | format: "rgba8unorm", 268 | usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.SAMPLED 269 | }); 270 | 271 | // copy texture over to GPU 272 | { 273 | const bytesPerRow = Math.ceil(img.width * 4 / 256) * 256; 274 | 275 | let data = null; 276 | 277 | if (bytesPerRow == img.width * 4) { 278 | data = img.data; 279 | } else { 280 | data = new Uint8Array(bytesPerRow * img.height); 281 | for (let y = 0; y < window.height; ++y) { 282 | for (let x = 0; x < window.width; ++x) { 283 | let i = x * 4 + y * bytesPerRow; 284 | data[i] = img.data[i]; 285 | data[i + 1] = img.data[i + 1]; 286 | data[i + 2] = img.data[i + 2]; 287 | data[i + 3] = img.data[i + 3]; 288 | }; 289 | }; 290 | } 291 | 292 | const textureDataBuffer = device.createBuffer({ 293 | size: data.byteLength, 294 | usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, 295 | }); 296 | textureDataBuffer.setSubData(0, data); 297 | 298 | const commandEncoder = device.createCommandEncoder({}); 299 | commandEncoder.copyBufferToTexture({ 300 | buffer: textureDataBuffer, 301 | bytesPerRow: bytesPerRow, 302 | arrayLayer: 0, 303 | mipLevel: 0, 304 | imageHeight: 0 305 | }, { 306 | texture: texture, 307 | mipLevel: 0, 308 | arrayLayer: 0, 309 | origin: { x: 0, y: 0, z: 0 } 310 | }, { 311 | width: img.width, 312 | height: img.height, 313 | depth: 1 314 | }); 315 | queue.submit([commandEncoder.finish()]); 316 | } 317 | 318 | const sampler = device.createSampler({ 319 | magFilter: "nearest", 320 | minFilter: "nearest", 321 | addressModeU: "mirror-repeat", 322 | addressModeV: "mirror-repeat", 323 | addressModeW: "mirror-repeat" 324 | }); 325 | 326 | const uniformBindGroupLayout = device.createBindGroupLayout({ 327 | entries: [ 328 | { 329 | binding: 0, 330 | visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, 331 | type: "uniform-buffer" 332 | }, 333 | { 334 | binding: 1, 335 | visibility: GPUShaderStage.FRAGMENT, 336 | type: "sampler" 337 | }, 338 | { 339 | binding: 2, 340 | visibility: GPUShaderStage.FRAGMENT, 341 | type: "sampled-texture" 342 | } 343 | ] 344 | }); 345 | 346 | const layout = device.createPipelineLayout({ 347 | bindGroupLayouts: [ uniformBindGroupLayout ] 348 | }); 349 | 350 | const vertexShaderModule = device.createShaderModule({ code: vsSrc }); 351 | const fragmentShaderModule = device.createShaderModule({ code: fsSrc }); 352 | 353 | const pipeline = device.createRenderPipeline({ 354 | layout, 355 | sampleCount: 1, 356 | vertexStage: { 357 | module: vertexShaderModule, 358 | entryPoint: "main" 359 | }, 360 | fragmentStage: { 361 | module: fragmentShaderModule, 362 | entryPoint: "main" 363 | }, 364 | primitiveTopology: "triangle-list", 365 | depthStencilState: { 366 | depthWriteEnabled: true, 367 | depthCompare: "less", 368 | format: "depth24plus-stencil8", 369 | stencilFront: {}, 370 | stencilBack: {}, 371 | }, 372 | vertexState: { 373 | indexFormat: "uint32", 374 | vertexBuffers: [ 375 | { 376 | arrayStride: 3 * Float32Array.BYTES_PER_ELEMENT, 377 | stepMode: "vertex", 378 | attributes: [ 379 | { 380 | shaderLocation: 0, 381 | offset: 0 * Float32Array.BYTES_PER_ELEMENT, 382 | format: "float3" 383 | } 384 | ] 385 | }, 386 | { 387 | arrayStride: 3 * Float32Array.BYTES_PER_ELEMENT, 388 | stepMode: "vertex", 389 | attributes: [ 390 | { 391 | shaderLocation: 1, 392 | offset: 0 * Float32Array.BYTES_PER_ELEMENT, 393 | format: "float3" 394 | } 395 | ] 396 | }, 397 | { 398 | arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, 399 | stepMode: "vertex", 400 | attributes: [ 401 | { 402 | shaderLocation: 2, 403 | offset: 0 * Float32Array.BYTES_PER_ELEMENT, 404 | format: "float2" 405 | } 406 | ] 407 | }, 408 | ] 409 | }, 410 | rasterizationState: { 411 | frontFace: "CCW", 412 | cullMode: "none" 413 | }, 414 | colorStates: [{ 415 | format: swapChainFormat, 416 | alphaBlend: {}, 417 | colorBlend: {} 418 | }] 419 | }); 420 | 421 | const uniformBindGroup = device.createBindGroup({ 422 | layout: uniformBindGroupLayout, 423 | entries: [ 424 | { 425 | binding: 0, 426 | buffer: stagedUniformBuffer, 427 | offset: 0, 428 | size: 48 * Float32Array.BYTES_PER_ELEMENT 429 | }, 430 | { 431 | binding: 1, 432 | sampler: sampler, 433 | size: 0 434 | }, { 435 | binding: 2, 436 | textureView: texture.createView({ format: "rgba8unorm" }), 437 | size: 0 438 | } 439 | ] 440 | }); 441 | 442 | const depthTexture = device.createTexture({ 443 | size: { 444 | width: window.width, 445 | height: window.height, 446 | depth: 1 447 | }, 448 | arrayLayerCount: 1, 449 | mipLevelCount: 1, 450 | sampleCount: 1, 451 | dimension: "2d", 452 | format: "depth24plus-stencil8", 453 | usage: GPUTextureUsage.OUTPUT_ATTACHMENT 454 | }); 455 | 456 | const depthStencilAttachment = { 457 | attachment: depthTexture.createView({ format: "depth24plus-stencil8" }), 458 | clearDepth: 1.0, 459 | depthLoadOp: "clear", 460 | depthStoreOp: "store", 461 | clearStencil: 0, 462 | stencilLoadOp: "clear", 463 | stencilStoreOp: "store", 464 | }; 465 | 466 | function onFrame() { 467 | if (!window.shouldClose()) setTimeout(onFrame, 1e3 / 60); 468 | 469 | let now = Date.now() / 1e3; 470 | mat4.identity(mModel); 471 | mat4.rotate( 472 | mModel, 473 | mModel, 474 | now * (90 * Math.PI / 180), 475 | vec3.fromValues(0.0, 0.0, 1.0) 476 | ); 477 | stagedUniformBuffer.setSubData(0, mModel); 478 | 479 | const backBufferView = swapChain.getCurrentTextureView(); 480 | const commandEncoder = device.createCommandEncoder({}); 481 | const renderPass = commandEncoder.beginRenderPass({ 482 | colorAttachments: [{ 483 | clearColor: { r: 0.125, g: 0.125, b: 0.125, a: 1.0 }, 484 | loadOp: "clear", 485 | storeOp: "store", 486 | attachment: backBufferView 487 | }], 488 | depthStencilAttachment 489 | }); 490 | renderPass.setPipeline(pipeline); 491 | renderPass.setBindGroup(0, uniformBindGroup); 492 | renderPass.setVertexBuffer(0, stagedVertexBuffer, 0); 493 | renderPass.setVertexBuffer(1, stagedNormalBuffer, 0); 494 | renderPass.setVertexBuffer(2, stagedUVBuffer, 0); 495 | renderPass.setIndexBuffer(stagedIndexBuffer); 496 | renderPass.drawIndexed(modelIndices.length, 1, 0, 0, 0); 497 | renderPass.endPass(); 498 | 499 | const commandBuffer = commandEncoder.finish(); 500 | queue.submit([ commandBuffer ]); 501 | swapChain.present(); 502 | window.pollEvents(); 503 | }; 504 | setTimeout(onFrame, 1e3 / 60); 505 | 506 | })(); 507 | --------------------------------------------------------------------------------