| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta name="viewport" content="width=1000"> |
| <title>WebGPU Cube</title> |
| <script src="scripts/gl-matrix-min.js"></script> |
| <link rel="stylesheet" href="css/style.css"/> |
| <style> |
| body { |
| font-family: system-ui; |
| color: #f7f7ff; |
| background-color: rgb(38, 38, 127); |
| text-align: center; |
| } |
| canvas { |
| margin: 0 auto; |
| } |
| </style> |
| </head> |
| <body> |
| <div id="contents"> |
| <h1>Simple Cube</h1> |
| <p> |
| This demo uses a rotating set of GPUBuffers to upload rotation matrix data every frame. |
| </p> |
| <canvas width="1200" height="1200"></canvas> |
| </div> |
| <div id="error"> |
| <h2>WebGPU not available</h2> |
| <p> |
| Make sure you are on a system with WebGPU enabled. In |
| Safari, first make sure the Developer Menu is visible (Preferences > |
| Advanced), then Develop > Experimental Features > WebGPU. |
| </p> |
| <p> |
| In addition, you must be using Safari Technology Preview 92 or above. |
| You can get the latest STP <a href="https://developer.apple.com/safari/download/">here</a>. |
| </p> |
| </div> |
| <script> |
| if (!navigator.gpu || GPUBufferUsage.COPY_SRC === undefined) |
| document.body.className = 'error'; |
| |
| const positionAttributeNum = 0; |
| const colorAttributeNum = 1; |
| |
| const transformBindingNum = 0; |
| |
| const bindGroupIndex = 0; |
| |
| const shader = ` |
| struct FragmentData { |
| float4 position : SV_Position; |
| float4 color : attribute(${colorAttributeNum}); |
| } |
| |
| vertex FragmentData vertex_main( |
| float4 position : attribute(${positionAttributeNum}), |
| float4 color : attribute(${colorAttributeNum}), |
| constant float4x4[] modelViewProjectionMatrix : register(b${transformBindingNum})) |
| { |
| FragmentData out; |
| out.position = mul(modelViewProjectionMatrix[0], position); |
| out.color = color; |
| |
| return out; |
| } |
| |
| fragment float4 fragment_main(float4 color : attribute(${colorAttributeNum})) : SV_Target 0 |
| { |
| return color; |
| } |
| `; |
| |
| let device, swapChain, verticesBuffer, bindGroupLayout, pipeline, renderPassDescriptor; |
| let projectionMatrix = mat4.create(); |
| |
| const colorOffset = 4 * 4; |
| const vertexSize = 4 * 8; |
| const verticesArray = new Float32Array([ |
| // float4 position, float4 color |
| 1, -1, 1, 1, 1, 0, 1, 1, |
| -1, -1, 1, 1, 0, 0, 1, 1, |
| -1, -1, -1, 1, 0, 0, 0, 1, |
| 1, -1, -1, 1, 1, 0, 0, 1, |
| 1, -1, 1, 1, 1, 0, 1, 1, |
| -1, -1, -1, 1, 0, 0, 0, 1, |
| |
| 1, 1, 1, 1, 1, 1, 1, 1, |
| 1, -1, 1, 1, 1, 0, 1, 1, |
| 1, -1, -1, 1, 1, 0, 0, 1, |
| 1, 1, -1, 1, 1, 1, 0, 1, |
| 1, 1, 1, 1, 1, 1, 1, 1, |
| 1, -1, -1, 1, 1, 0, 0, 1, |
| |
| -1, 1, 1, 1, 0, 1, 1, 1, |
| 1, 1, 1, 1, 1, 1, 1, 1, |
| 1, 1, -1, 1, 1, 1, 0, 1, |
| -1, 1, -1, 1, 0, 1, 0, 1, |
| -1, 1, 1, 1, 0, 1, 1, 1, |
| 1, 1, -1, 1, 1, 1, 0, 1, |
| |
| -1, -1, 1, 1, 0, 0, 1, 1, |
| -1, 1, 1, 1, 0, 1, 1, 1, |
| -1, 1, -1, 1, 0, 1, 0, 1, |
| -1, -1, -1, 1, 0, 0, 0, 1, |
| -1, -1, 1, 1, 0, 0, 1, 1, |
| -1, 1, -1, 1, 0, 1, 0, 1, |
| |
| 1, 1, 1, 1, 1, 1, 1, 1, |
| -1, 1, 1, 1, 0, 1, 1, 1, |
| -1, -1, 1, 1, 0, 0, 1, 1, |
| -1, -1, 1, 1, 0, 0, 1, 1, |
| 1, -1, 1, 1, 1, 0, 1, 1, |
| 1, 1, 1, 1, 1, 1, 1, 1, |
| |
| 1, -1, -1, 1, 1, 0, 0, 1, |
| -1, -1, -1, 1, 0, 0, 0, 1, |
| -1, 1, -1, 1, 0, 1, 0, 1, |
| 1, 1, -1, 1, 1, 1, 0, 1, |
| 1, -1, -1, 1, 1, 0, 0, 1, |
| -1, 1, -1, 1, 0, 1, 0, 1, |
| ]); |
| |
| async function init() { |
| const adapter = await navigator.gpu.requestAdapter(); |
| device = await adapter.requestDevice(); |
| |
| const canvas = document.querySelector('canvas'); |
| let canvasSize = canvas.getBoundingClientRect(); |
| canvas.width = canvasSize.width; |
| canvas.height = canvasSize.height; |
| |
| const aspect = Math.abs(canvas.width / canvas.height); |
| mat4.perspective(projectionMatrix, (2 * Math.PI) / 5, aspect, 1, 100.0); |
| |
| const context = canvas.getContext('gpu'); |
| |
| const swapChainDescriptor = { |
| device: device, |
| format: "bgra8unorm" |
| }; |
| swapChain = context.configureSwapChain(swapChainDescriptor); |
| |
| const shaderModuleDescriptor = { code: shader, isWHLSL: true }; |
| const shaderModule = device.createShaderModule(shaderModuleDescriptor); |
| |
| const verticesBufferDescriptor = { |
| size: verticesArray.byteLength, |
| usage: GPUBufferUsage.VERTEX | GPUBufferUsage.TRANSFER_DST |
| }; |
| let verticesArrayBuffer; |
| [verticesBuffer, verticesArrayBuffer] = device.createBufferMapped(verticesBufferDescriptor); |
| |
| const verticesWriteArray = new Float32Array(verticesArrayBuffer); |
| verticesWriteArray.set(verticesArray); |
| verticesBuffer.unmap(); |
| |
| // Vertex Input |
| const positionAttributeDescriptor = { |
| shaderLocation: positionAttributeNum, // [[attribute(0)]] |
| offset: 0, |
| format: "float4" |
| }; |
| const colorAttributeDescriptor = { |
| shaderLocation: colorAttributeNum, |
| offset: colorOffset, |
| format: "float4" |
| } |
| const vertexBufferDescriptor = { |
| attributeSet: [positionAttributeDescriptor, colorAttributeDescriptor], |
| stride: vertexSize, |
| stepMode: "vertex" |
| }; |
| const vertexInputDescriptor = { vertexBuffers: [vertexBufferDescriptor] }; |
| |
| // Bind group binding layout |
| const transformBufferBindGroupLayoutBinding = { |
| binding: transformBindingNum, // id[[(0)]] |
| visibility: GPUShaderStage.VERTEX, |
| type: "uniform-buffer" |
| }; |
| |
| const bindGroupLayoutDescriptor = { bindings: [transformBufferBindGroupLayoutBinding] }; |
| bindGroupLayout = device.createBindGroupLayout(bindGroupLayoutDescriptor); |
| |
| // Pipeline |
| const depthStateDescriptor = { |
| depthWriteEnabled: true, |
| depthCompare: "less" |
| }; |
| |
| const pipelineLayoutDescriptor = { bindGroupLayouts: [bindGroupLayout] }; |
| const pipelineLayout = device.createPipelineLayout(pipelineLayoutDescriptor); |
| const vertexStageDescriptor = { |
| module: shaderModule, |
| entryPoint: "vertex_main" |
| }; |
| const fragmentStageDescriptor = { |
| module: shaderModule, |
| entryPoint: "fragment_main" |
| }; |
| const colorState = { |
| format: "bgra8unorm", |
| alphaBlend: { |
| srcFactor: "src-alpha", |
| dstFactor: "one-minus-src-alpha", |
| operation: "add" |
| }, |
| colorBlend: { |
| srcFactor: "src-alpha", |
| dstFactor: "one-minus-src-alpha", |
| operation: "add" |
| }, |
| writeMask: GPUColorWrite.ALL |
| }; |
| const pipelineDescriptor = { |
| layout: pipelineLayout, |
| |
| vertexStage: vertexStageDescriptor, |
| fragmentStage: fragmentStageDescriptor, |
| |
| primitiveTopology: "triangle-list", |
| colorStates: [colorState], |
| depthStencilState: depthStateDescriptor, |
| vertexInput: vertexInputDescriptor |
| }; |
| pipeline = device.createRenderPipeline(pipelineDescriptor); |
| |
| let colorAttachment = { |
| // attachment is acquired in render loop. |
| loadOp: "clear", |
| storeOp: "store", |
| clearColor: { r: 0.15, g: 0.15, b: 0.5, a: 1.0 } // GPUColor |
| }; |
| |
| // Depth stencil texture |
| |
| // GPUExtent3D |
| const depthSize = { |
| width: canvas.width, |
| height: canvas.height, |
| depth: 1 |
| }; |
| |
| const depthTextureDescriptor = { |
| size: depthSize, |
| arrayLayerCount: 1, |
| mipLevelCount: 1, |
| sampleCount: 1, |
| dimension: "2d", |
| format: "depth32float-stencil8", |
| usage: GPUTextureUsage.OUTPUT_ATTACHMENT |
| }; |
| |
| const depthTexture = device.createTexture(depthTextureDescriptor); |
| |
| // GPURenderPassDepthStencilAttachmentDescriptor |
| const depthAttachment = { |
| attachment: depthTexture.createDefaultView(), |
| depthLoadOp: "clear", |
| depthStoreOp: "store", |
| clearDepth: 1.0 |
| }; |
| |
| renderPassDescriptor = { |
| colorAttachments: [colorAttachment], |
| depthStencilAttachment: depthAttachment |
| }; |
| |
| render(); |
| } |
| |
| /* Transform Buffers and Bindings */ |
| const transformSize = 4 * 16; |
| |
| const transformBufferDescriptor = { |
| size: transformSize, |
| usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.MAP_WRITE |
| }; |
| |
| let mappedGroups = []; |
| |
| function render() { |
| if (mappedGroups.length === 0) { |
| const [buffer, arrayBuffer] = device.createBufferMapped(transformBufferDescriptor); |
| const group = device.createBindGroup(createBindGroupDescriptor(buffer)); |
| let mappedGroup = { buffer: buffer, arrayBuffer: arrayBuffer, bindGroup: group }; |
| drawCommands(mappedGroup); |
| } else |
| drawCommands(mappedGroups.shift()); |
| } |
| |
| function createBindGroupDescriptor(transformBuffer) { |
| const transformBufferBinding = { |
| buffer: transformBuffer, |
| offset: 0, |
| size: transformSize |
| }; |
| const transformBufferBindGroupBinding = { |
| binding: transformBindingNum, |
| resource: transformBufferBinding |
| }; |
| return { |
| layout: bindGroupLayout, |
| bindings: [transformBufferBindGroupBinding] |
| }; |
| } |
| |
| function drawCommands(mappedGroup) { |
| updateTransformArray(new Float32Array(mappedGroup.arrayBuffer)); |
| mappedGroup.buffer.unmap(); |
| |
| const commandEncoder = device.createCommandEncoder(); |
| renderPassDescriptor.colorAttachments[0].attachment = swapChain.getCurrentTexture().createDefaultView(); |
| const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); |
| |
| // Encode drawing commands |
| |
| passEncoder.setPipeline(pipeline); |
| // Vertex attributes |
| passEncoder.setVertexBuffers(0, [verticesBuffer], [0]); |
| // Bind groups |
| passEncoder.setBindGroup(bindGroupIndex, mappedGroup.bindGroup); |
| // 36 vertices, 1 instance, 0th vertex, 0th instance. |
| passEncoder.draw(36, 1, 0, 0); |
| passEncoder.endPass(); |
| |
| device.getQueue().submit([commandEncoder.finish()]); |
| |
| // Ready the current buffer for update after GPU is done with it. |
| mappedGroup.buffer.mapWriteAsync().then((arrayBuffer) => { |
| mappedGroup.arrayBuffer = arrayBuffer; |
| mappedGroups.push(mappedGroup); |
| }); |
| |
| requestAnimationFrame(render); |
| } |
| |
| function updateTransformArray(array) { |
| let viewMatrix = mat4.create(); |
| mat4.translate(viewMatrix, viewMatrix, vec3.fromValues(0, 0, -5)); |
| let now = Date.now() / 1000; |
| mat4.rotate(viewMatrix, viewMatrix, 1, vec3.fromValues(Math.sin(now), Math.cos(now), 0)); |
| let modelViewProjectionMatrix = mat4.create(); |
| mat4.multiply(modelViewProjectionMatrix, projectionMatrix, viewMatrix); |
| mat4.copy(array, modelViewProjectionMatrix); |
| } |
| |
| window.addEventListener("load", init); |
| </script> |
| </body> |
| </html> |