diff --git a/sample/wireframe/main.ts b/sample/wireframe/main.ts index d65b3029..d664fcf9 100644 --- a/sample/wireframe/main.ts +++ b/sample/wireframe/main.ts @@ -1,6 +1,8 @@ /* eslint-disable prettier/prettier */ import { mat4, mat3 } from 'wgpu-matrix'; +import { GUI } from 'dat.gui'; import { modelData } from './models'; +import { randElement, randColor } from './utils'; import solidColorLitWGSL from './solidColorLit.wgsl'; import wireframeWGSL from './wireframe.wgsl'; @@ -9,11 +11,11 @@ type TypedArrayView = Float32Array | Uint32Array; function createBufferWithData( device: GPUDevice, data: TypedArrayView, - usage: number + usage: GPUBufferUsageFlags, ) { const buffer = device.createBuffer({ size: data.byteLength, - usage: usage, + usage, }); device.queue.writeBuffer(buffer, 0, data); return buffer; @@ -66,25 +68,6 @@ const depthFormat = 'depth24plus'; const models = Object.values(modelData).map(data => createVertexAndIndexBuffer(device, data)); -function rand(min?: number, max?: number) { - if (min === undefined) { - max = 1; - min = 0; - } else if (max === undefined) { - max = min; - min = 0; - } - return Math.random() * (max - min) + min; -} - -function randInt(min: number, max?: number) { - return Math.floor(rand(min, max)); -} - -function randColor() { - return [rand(), rand(), rand(), 1]; -} - const litModule = device.createShaderModule({ code: solidColorLitWGSL, }); @@ -141,6 +124,7 @@ const wireframePipeline = device.createRenderPipeline({ }, fragment: { module: wireframeModule, + entryPoint: 'fs', targets: [{ format: presentationFormat }], }, primitive: { @@ -153,13 +137,51 @@ const wireframePipeline = device.createRenderPipeline({ }, }); +const barycentricCoordinatesBasedWireframePipeline = device.createRenderPipeline({ + label: 'barycentric coordinates based wireframe pipeline', + layout: 'auto', + vertex: { + module: wireframeModule, + entryPoint: 'vsIndexedU32BarycentricCoordinateBasedLines', + }, + fragment: { + module: wireframeModule, + entryPoint: 'fsBarycentricCoordinateBasedLines', + targets: [ + { + format: presentationFormat, + blend: { + color: { + srcFactor: 'one', + dstFactor: 'one-minus-src-alpha', + }, + alpha: { + srcFactor: 'one', + dstFactor: 'one-minus-src-alpha', + }, + }, + }, + ], + }, + primitive: { + topology: 'triangle-list', + }, + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less-equal', + format: depthFormat, + }, +}); + type ObjectInfo = { worldViewProjectionMatrixValue: Float32Array; worldMatrixValue: Float32Array; uniformValues: Float32Array; uniformBuffer: GPUBuffer; + lineUniformValues: Float32Array; + lineUniformBuffer: GPUBuffer; litBindGroup: GPUBindGroup; - wireframeBindGroup: GPUBindGroup; + wireframeBindGroups: GPUBindGroup[]; model: Model; }; @@ -187,7 +209,7 @@ for (let i = 0; i < numObjects; ++i) { const colorValue = uniformValues.subarray(kColorOffset, kColorOffset + 4); colorValue.set(randColor()); - const model = models[randInt(models.length)]; + const model = randElement(models); // Make a bind group for this uniform const litBindGroup = device.createBindGroup({ @@ -195,21 +217,39 @@ for (let i = 0; i < numObjects; ++i) { entries: [{ binding: 0, resource: { buffer: uniformBuffer } }], }); - const strideValues = new Uint32Array(1 + 3); - const strideBuffer = device.createBuffer({ - size: strideValues.byteLength, + // Note: We're making one lineUniformBuffer per object. + // This is only because stride might be different per object. + // In this sample stride is the same across all objects so + // we could have made just a single shared uniform buffer for + // these settings. + const lineUniformValues = new Float32Array(3 + 1); + const lineUniformValuesAsU32 = new Uint32Array(lineUniformValues.buffer); + const lineUniformBuffer = device.createBuffer({ + size: lineUniformValues.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); - strideValues[0] = 6; - device.queue.writeBuffer(strideBuffer, 0, strideValues); + lineUniformValuesAsU32[0] = 6; // the array stride for positions for this model. + // We're creating 2 bindGroups, one for each pipeline. + // We could create just one since they are identical. To do + // so we'd have to manually create a bindGroupLayout. const wireframeBindGroup = device.createBindGroup({ layout: wireframePipeline.getBindGroupLayout(0), entries: [ { binding: 0, resource: { buffer: uniformBuffer } }, { binding: 1, resource: { buffer: model.vertexBuffer } }, { binding: 2, resource: { buffer: model.indexBuffer } }, - { binding: 3, resource: { buffer: strideBuffer } }, + { binding: 3, resource: { buffer: lineUniformBuffer } }, + ], + }); + + const barycentricCoordinatesBasedWireframeBindGroup = device.createBindGroup({ + layout: barycentricCoordinatesBasedWireframePipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: uniformBuffer } }, + { binding: 1, resource: { buffer: model.vertexBuffer } }, + { binding: 2, resource: { buffer: model.indexBuffer } }, + { binding: 3, resource: { buffer: lineUniformBuffer } }, ], }); @@ -218,8 +258,13 @@ for (let i = 0; i < numObjects; ++i) { worldMatrixValue, uniformValues, uniformBuffer, + lineUniformValues, + lineUniformBuffer, litBindGroup, - wireframeBindGroup, + wireframeBindGroups: [ + wireframeBindGroup, + barycentricCoordinatesBasedWireframeBindGroup, + ], model, }); } @@ -242,7 +287,45 @@ const renderPassDescriptor: GPURenderPassDescriptor = { }, }; -let depthTexture; +const settings = { + barycentricCoordinatesBased: false, + thickness: 2, + alphaThreshold: 0.5, + lines: true, + models: true, +}; + +const gui = new GUI(); +gui.add(settings, 'barycentricCoordinatesBased').onChange(addRemoveGUI); +gui.add(settings, 'lines'); +gui.add(settings, 'models'); + +const guis = []; +function addRemoveGUI() { + if (settings.barycentricCoordinatesBased) { + guis.push( + gui.add(settings, 'thickness', 0.0, 10).onChange(updateThickness), + gui.add(settings, 'alphaThreshold', 0, 1).onChange(updateThickness), + ); + } else { + guis.forEach(g => g.remove()); + guis.length = 0; + } +} + +function updateThickness() { + objectInfos.forEach(({ + lineUniformBuffer, + lineUniformValues, + }) => { + lineUniformValues[1] = settings.thickness; + lineUniformValues[2] = settings.alphaThreshold; + device.queue.writeBuffer(lineUniformBuffer, 0, lineUniformValues); + }); +} +updateThickness(); + +let depthTexture: GPUTexture | undefined; function render(time: number) { time *= 0.001; // convert to seconds; @@ -311,20 +394,27 @@ function render(time: number) { // Upload our uniform values. device.queue.writeBuffer(uniformBuffer, 0, uniformValues); - pass.setVertexBuffer(0, vertexBuffer); - pass.setIndexBuffer(indexBuffer, indexFormat); - pass.setBindGroup(0, litBindGroup); - pass.drawIndexed(vertexCount); + if (settings.models) { + pass.setVertexBuffer(0, vertexBuffer); + pass.setIndexBuffer(indexBuffer, indexFormat); + pass.setBindGroup(0, litBindGroup); + pass.drawIndexed(vertexCount); + } }); - objectInfos.forEach(({ - wireframeBindGroup, - model: { vertexCount }, - }) => { - pass.setPipeline(wireframePipeline); - pass.setBindGroup(0, wireframeBindGroup) - pass.draw(vertexCount * 2); - }); + if (settings.lines) { + // Note: If we're using the line-list based pipeline then we need to + // multiply the vertex count by 2 since we need to emit 6 vertices + // for each triangle (3 edges). + const [bindGroupNdx, countMult, pipeline] = settings.barycentricCoordinatesBased + ? [1, 1, barycentricCoordinatesBasedWireframePipeline] + : [0, 2, wireframePipeline]; + pass.setPipeline(pipeline); + objectInfos.forEach(({ wireframeBindGroups, model: { vertexCount } }) => { + pass.setBindGroup(0, wireframeBindGroups[bindGroupNdx]); + pass.draw(vertexCount * countMult); + }); + } pass.end(); diff --git a/sample/wireframe/meta.ts b/sample/wireframe/meta.ts index 8e0cacbd..3cb04590 100644 --- a/sample/wireframe/meta.ts +++ b/sample/wireframe/meta.ts @@ -1,11 +1,12 @@ export default { name: 'Wireframe', description: ` - This example demonstrates drawing a wireframe from triangles by using the - vertex buffers as storage buffers and then using \`@builtin(vertex_index)\` - to index the vertex data to generate 3 lines per triangle. It uses - array in the vertex shader so it can pull out \`vec3f\` positions - at any valid stride. + This example demonstrates drawing a wireframe from triangles in 2 ways. + Both use the vertex and index buffers as storage buffers and the use \`@builtin(vertex_index)\` + to index the vertex data. One method generates 6 vertices per triangle and uses line-list to draw lines. + The other method draws triangles with a fragment shader that uses barycentric coordinates to draw edges. + as detailed [here](https://web.archive.org/web/20130424093557/http://codeflow.org/entries/2012/aug/02/easy-wireframe-display-with-barycentric-coordinates/). + `, filename: __DIRNAME__, sources: [ diff --git a/sample/wireframe/utils.ts b/sample/wireframe/utils.ts index 50bdc2f1..54980422 100644 --- a/sample/wireframe/utils.ts +++ b/sample/wireframe/utils.ts @@ -184,22 +184,25 @@ export const create3DRenderPipeline = ( return device.createRenderPipeline(pipelineDescriptor); }; -export const createTextureFromImage = ( - device: GPUDevice, - bitmap: ImageBitmap -) => { - const texture: GPUTexture = device.createTexture({ - size: [bitmap.width, bitmap.height, 1], - format: 'rgba8unorm', - usage: - GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT, - }); - device.queue.copyExternalImageToTexture( - { source: bitmap }, - { texture: texture }, - [bitmap.width, bitmap.height] - ); - return texture; -}; +export function rand(min?: number, max?: number) { + if (min === undefined) { + max = 1; + min = 0; + } else if (max === undefined) { + max = min; + min = 0; + } + return Math.random() * (max - min) + min; +} + +export function randInt(min: number, max?: number) { + return Math.floor(rand(min, max)); +} + +export function randColor() { + return [rand(), rand(), rand(), 1]; +} + +export function randElement(arr: T[]): T { + return arr[randInt(arr.length)]; +} diff --git a/sample/wireframe/wireframe.wgsl b/sample/wireframe/wireframe.wgsl index 64521c55..7c0e1341 100644 --- a/sample/wireframe/wireframe.wgsl +++ b/sample/wireframe/wireframe.wgsl @@ -4,6 +4,12 @@ struct Uniforms { color: vec4f, }; +struct LineUniforms { + stride: u32, + thickness: f32, + alphaThreshold: f32, +}; + struct VSOut { @builtin(position) position: vec4f, }; @@ -11,7 +17,7 @@ struct VSOut { @group(0) @binding(0) var uni: Uniforms; @group(0) @binding(1) var positions: array; @group(0) @binding(2) var indices: array; -@group(0) @binding(3) var stride: u32; +@group(0) @binding(3) var line: LineUniforms; @vertex fn vsIndexedU32(@builtin(vertex_index) vNdx: u32) -> VSOut { // indices make a triangle so for every 3 indices we need to output @@ -23,27 +29,20 @@ struct VSOut { // 0 1 1 2 2 0 0 1 1 2 2 0 (vNdx % 2 + vNdx / 2) % 3 let vertNdx = (vNdx % 2 + vNdx / 2) % 3; let index = indices[triNdx * 3 + vertNdx]; - let pNdx = index * stride; - let position = vec4f(positions[pNdx], positions[pNdx + 1], positions[pNdx + 2], 1); - var vOut: VSOut; - vOut.position = uni.worldViewProjectionMatrix * position; - return vOut; -} + // note: + // + // * if your indices are U16 you could use this + // + // let indexNdx = triNdx * 3 + vertNdx; + // let twoIndices = indices[indexNdx / 2]; // indices is u32 but we want u16 + // let index = (twoIndices >> ((indexNdx & 1) * 16)) & 0xFFFF; + // + // * if you're not using indicies you could use this + // + // let index = triNdx * 3 + vertNdx; -@vertex fn vsIndexedU16(@builtin(vertex_index) vNdx: u32) -> VSOut { - // indices make a triangle so for every 3 indices we need to output - // 6 values - let triNdx = vNdx / 6; - // 0 1 0 1 0 1 0 1 0 1 0 1 vNdx % 2 - // 0 0 1 1 2 2 3 3 4 4 5 5 vNdx / 2 - // 0 1 1 2 2 3 3 4 4 5 5 6 vNdx % 2 + vNdx / 2 - // 0 1 1 2 2 0 0 1 1 2 2 0 (vNdx % 2 + vNdx / 2) % 3 - let vertNdx = (vNdx % 2 + vNdx / 2) % 3; - let indexNdx = triNdx * 3 + vertNdx; - let twoIndices = indices[indexNdx / 2]; // indices is u32 but we want u16 - let index = (twoIndices >> ((indexNdx & 1) * 16)) & 0xFFFF; - let pNdx = index * stride; + let pNdx = index * line.stride; let position = vec4f(positions[pNdx], positions[pNdx + 1], positions[pNdx + 2], 1); var vOut: VSOut; @@ -51,24 +50,56 @@ struct VSOut { return vOut; } -@vertex fn vsUnindexed(@builtin(vertex_index) vNdx: u32) -> VSOut { - // indices make a triangle so for every 3 indices we need to output - // 6 values - let triNdx = vNdx / 6; - // 0 1 0 1 0 1 0 1 0 1 0 1 vNdx % 2 - // 0 0 1 1 2 2 3 3 4 4 5 5 vNdx / 2 - // 0 1 1 2 2 3 3 4 4 5 5 6 vNdx % 2 + vNdx / 2 - // 0 1 1 2 2 0 0 1 1 2 2 0 (vNdx % 2 + vNdx / 2) % 3 - let vertNdx = (vNdx % 2 + vNdx / 2) % 3; - let index = triNdx * 3 + vertNdx; - let pNdx = index * stride; +@fragment fn fs() -> @location(0) vec4f { + return uni.color + vec4f(0.5); +} + +struct BarycentricCoordinateBasedVSOutput { + @builtin(position) position: vec4f, + @location(0) barycenticCoord: vec3f, +}; + +@vertex fn vsIndexedU32BarycentricCoordinateBasedLines( + @builtin(vertex_index) vNdx: u32 +) -> BarycentricCoordinateBasedVSOutput { + let vertNdx = vNdx % 3; + let index = indices[vNdx]; + + // note: + // + // * if your indices are U16 you could use this + // + // let twoIndices = indices[vNdx / 2]; // indices is u32 but we want u16 + // let index = (twoIndices >> ((vNdx & 1) * 16)) & 0xFFFF; + // + // * if you're not using indicies you could use this + // + // let index = vNdx; + + let pNdx = index * line.stride; let position = vec4f(positions[pNdx], positions[pNdx + 1], positions[pNdx + 2], 1); - var vOut: VSOut; - vOut.position = uni.worldViewProjectionMatrix * position; - return vOut; + var vsOut: BarycentricCoordinateBasedVSOutput; + vsOut.position = uni.worldViewProjectionMatrix * position; + + // emit a barycentric coordinate + vsOut.barycenticCoord = vec3f(0); + vsOut.barycenticCoord[vertNdx] = 1.0; + return vsOut; } -@fragment fn fs() -> @location(0) vec4f { - return uni.color + vec4f(0.5); +fn edgeFactor(bary: vec3f) -> f32 { + let d = fwidth(bary); + let a3 = smoothstep(vec3f(0.0), d * line.thickness, bary); + return min(min(a3.x, a3.y), a3.z); } + +@fragment fn fsBarycentricCoordinateBasedLines( + v: BarycentricCoordinateBasedVSOutput +) -> @location(0) vec4f { + let a = 1.0 - edgeFactor(v.barycenticCoord); + if (a < line.alphaThreshold) { + discard; + } + return vec4((uni.color.rgb + 0.5) * a, a); +} \ No newline at end of file