diff --git a/sample/wireframe/index.html b/sample/wireframe/index.html
new file mode 100644
index 00000000..5ab83229
--- /dev/null
+++ b/sample/wireframe/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+ webgpu-samples: wireframe
+
+
+
+
+
+
+
+
diff --git a/sample/wireframe/main.ts b/sample/wireframe/main.ts
new file mode 100644
index 00000000..c53145a7
--- /dev/null
+++ b/sample/wireframe/main.ts
@@ -0,0 +1,438 @@
+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';
+
+type TypedArrayView = Float32Array | Uint32Array;
+
+function createBufferWithData(
+ device: GPUDevice,
+ data: TypedArrayView,
+ usage: GPUBufferUsageFlags
+) {
+ const buffer = device.createBuffer({
+ size: data.byteLength,
+ usage,
+ });
+ device.queue.writeBuffer(buffer, 0, data);
+ return buffer;
+}
+
+type Model = {
+ vertexBuffer: GPUBuffer;
+ indexBuffer: GPUBuffer;
+ indexFormat: GPUIndexFormat;
+ vertexCount: number;
+};
+
+function createVertexAndIndexBuffer(
+ device: GPUDevice,
+ { vertices, indices }: { vertices: Float32Array; indices: Uint32Array }
+): Model {
+ const vertexBuffer = createBufferWithData(
+ device,
+ vertices,
+ GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
+ );
+ const indexBuffer = createBufferWithData(
+ device,
+ indices,
+ GPUBufferUsage.INDEX | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
+ );
+ return {
+ vertexBuffer,
+ indexBuffer,
+ indexFormat: 'uint32',
+ vertexCount: indices.length,
+ };
+}
+
+const adapter = await navigator.gpu.requestAdapter();
+const device = await adapter.requestDevice();
+const canvas = document.querySelector('canvas') as HTMLCanvasElement;
+const context = canvas.getContext('webgpu') as GPUCanvasContext;
+const devicePixelRatio = window.devicePixelRatio;
+canvas.width = canvas.clientWidth * devicePixelRatio;
+canvas.height = canvas.clientHeight * devicePixelRatio;
+const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
+context.configure({
+ device,
+ format: presentationFormat,
+ alphaMode: 'premultiplied',
+});
+const depthFormat = 'depth24plus';
+
+const models = Object.values(modelData).map((data) =>
+ createVertexAndIndexBuffer(device, data)
+);
+
+const litModule = device.createShaderModule({
+ code: solidColorLitWGSL,
+});
+
+const wireframeModule = device.createShaderModule({
+ code: wireframeWGSL,
+});
+
+const litPipeline = device.createRenderPipeline({
+ label: 'lit pipeline',
+ layout: 'auto',
+ vertex: {
+ module: litModule,
+ buffers: [
+ {
+ arrayStride: 6 * 4, // position, normal
+ attributes: [
+ {
+ // position
+ shaderLocation: 0,
+ offset: 0,
+ format: 'float32x3',
+ },
+ {
+ // normal
+ shaderLocation: 1,
+ offset: 3 * 4,
+ format: 'float32x3',
+ },
+ ],
+ },
+ ],
+ },
+ fragment: {
+ module: litModule,
+ targets: [{ format: presentationFormat }],
+ },
+ primitive: {
+ cullMode: 'back',
+ },
+ depthStencil: {
+ depthWriteEnabled: true,
+ depthCompare: 'less',
+ format: depthFormat,
+ },
+});
+
+const wireframePipeline = device.createRenderPipeline({
+ label: 'wireframe pipeline',
+ layout: 'auto',
+ vertex: {
+ module: wireframeModule,
+ entryPoint: 'vsIndexedU32',
+ },
+ fragment: {
+ module: wireframeModule,
+ entryPoint: 'fs',
+ targets: [{ format: presentationFormat }],
+ },
+ primitive: {
+ topology: 'line-list',
+ },
+ depthStencil: {
+ depthWriteEnabled: true,
+ depthCompare: 'less-equal',
+ format: depthFormat,
+ },
+});
+
+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;
+ wireframeBindGroups: GPUBindGroup[];
+ model: Model;
+};
+
+const objectInfos: ObjectInfo[] = [];
+
+const numObjects = 200;
+for (let i = 0; i < numObjects; ++i) {
+ // Make a uniform buffer and type array views
+ // for our uniforms.
+ const uniformValues = new Float32Array(16 + 16 + 4);
+ const uniformBuffer = device.createBuffer({
+ size: uniformValues.byteLength,
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
+ });
+ const kWorldViewProjectionMatrixOffset = 0;
+ const kWorldMatrixOffset = 16;
+ const kColorOffset = 32;
+ const worldViewProjectionMatrixValue = uniformValues.subarray(
+ kWorldViewProjectionMatrixOffset,
+ kWorldViewProjectionMatrixOffset + 16
+ );
+ const worldMatrixValue = uniformValues.subarray(
+ kWorldMatrixOffset,
+ kWorldMatrixOffset + 15
+ );
+ const colorValue = uniformValues.subarray(kColorOffset, kColorOffset + 4);
+ colorValue.set(randColor());
+
+ const model = randElement(models);
+
+ // Make a bind group for this uniform
+ const litBindGroup = device.createBindGroup({
+ layout: litPipeline.getBindGroupLayout(0),
+ entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
+ });
+
+ // 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,
+ });
+ 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: 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 } },
+ ],
+ });
+
+ objectInfos.push({
+ worldViewProjectionMatrixValue,
+ worldMatrixValue,
+ uniformValues,
+ uniformBuffer,
+ lineUniformValues,
+ lineUniformBuffer,
+ litBindGroup,
+ wireframeBindGroups: [
+ wireframeBindGroup,
+ barycentricCoordinatesBasedWireframeBindGroup,
+ ],
+ model,
+ });
+}
+
+const renderPassDescriptor: GPURenderPassDescriptor = {
+ label: 'our basic canvas renderPass',
+ colorAttachments: [
+ {
+ view: undefined, // <- to be filled out when we render
+ clearValue: [0.3, 0.3, 0.3, 1],
+ loadOp: 'clear',
+ storeOp: 'store',
+ },
+ ],
+ depthStencilAttachment: {
+ view: undefined, // <- to be filled out when we render
+ depthClearValue: 1.0,
+ depthLoadOp: 'clear',
+ depthStoreOp: 'store',
+ },
+};
+
+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;
+
+ // Get the current texture from the canvas context and
+ // set it as the texture to render to.
+ const canvasTexture = context.getCurrentTexture();
+ renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView();
+
+ // If we don't have a depth texture OR if its size is different
+ // from the canvasTexture when make a new depth texture
+ if (
+ !depthTexture ||
+ depthTexture.width !== canvasTexture.width ||
+ depthTexture.height !== canvasTexture.height
+ ) {
+ if (depthTexture) {
+ depthTexture.destroy();
+ }
+ depthTexture = device.createTexture({
+ size: [canvasTexture.width, canvasTexture.height],
+ format: 'depth24plus',
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
+ });
+ }
+ renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
+
+ const fov = (60 * Math.PI) / 180;
+ const aspect = canvas.clientWidth / canvas.clientHeight;
+ const projection = mat4.perspective(fov, aspect, 0.1, 1000);
+
+ const view = mat4.lookAt(
+ [-300, 0, 300], // eye
+ [0, 0, 0], // target
+ [0, 1, 0] // up
+ );
+
+ const viewProjection = mat4.multiply(projection, view);
+
+ // make a command encoder to start encoding commands
+ const encoder = device.createCommandEncoder();
+
+ // make a render pass encoder to encode render specific commands
+ const pass = encoder.beginRenderPass(renderPassDescriptor);
+ pass.setPipeline(litPipeline);
+
+ objectInfos.forEach(
+ (
+ {
+ uniformBuffer,
+ uniformValues,
+ worldViewProjectionMatrixValue,
+ worldMatrixValue,
+ litBindGroup,
+ model: { vertexBuffer, indexBuffer, indexFormat, vertexCount },
+ },
+ i
+ ) => {
+ const world = mat4.identity();
+ mat4.translate(
+ world,
+ [0, 0, Math.sin(i * 3.721 + time * 0.1) * 200],
+ world
+ );
+ mat4.rotateX(world, i * 4.567, world);
+ mat4.rotateY(world, i * 2.967, world);
+ mat4.translate(
+ world,
+ [0, 0, Math.sin(i * 9.721 + time * 0.1) * 200],
+ world
+ );
+ mat4.rotateX(world, time * 0.53 + i, world);
+
+ mat4.multiply(viewProjection, world, worldViewProjectionMatrixValue);
+ mat3.fromMat4(world, worldMatrixValue);
+
+ // Upload our uniform values.
+ device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
+
+ if (settings.models) {
+ pass.setVertexBuffer(0, vertexBuffer);
+ pass.setIndexBuffer(indexBuffer, indexFormat);
+ pass.setBindGroup(0, litBindGroup);
+ pass.drawIndexed(vertexCount);
+ }
+ }
+ );
+
+ 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();
+
+ const commandBuffer = encoder.finish();
+ device.queue.submit([commandBuffer]);
+
+ requestAnimationFrame(render);
+}
+requestAnimationFrame(render);
diff --git a/sample/wireframe/meta.ts b/sample/wireframe/meta.ts
new file mode 100644
index 00000000..4f9f0a27
--- /dev/null
+++ b/sample/wireframe/meta.ts
@@ -0,0 +1,21 @@
+export default {
+ name: 'Wireframe',
+ description: `
+ 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: [
+ { path: 'main.ts' },
+ { path: 'wireframe.wgsl' },
+ { path: 'solidColorLit.wgsl' },
+ { path: 'models.ts' },
+ { path: '../../meshes/box.ts' },
+ { path: '../../meshes/mesh.ts' },
+ { path: 'utils.ts' },
+ ],
+};
diff --git a/sample/wireframe/models.ts b/sample/wireframe/models.ts
new file mode 100644
index 00000000..4b433e35
--- /dev/null
+++ b/sample/wireframe/models.ts
@@ -0,0 +1,112 @@
+// Ideally all the models would be the same format
+// and we'd determine that format at build time or before
+// but, we want to reuse the model data in this repo
+// so we'll normalize it here
+
+import { vec3 } from 'wgpu-matrix';
+import { mesh as teapot } from '../../meshes/teapot';
+import { createSphereMesh } from '../../meshes/sphere';
+
+type Mesh = {
+ positions: [number, number, number][];
+ triangles: [number, number, number][];
+ normals: [number, number, number][];
+};
+
+export function convertMeshToTypedArrays(
+ mesh: Mesh,
+ scale: number,
+ offset = [0, 0, 0]
+) {
+ const { positions, normals, triangles } = mesh;
+ const scaledPositions = positions.map((p) =>
+ p.map((v, i) => v * scale + offset[i % 3])
+ );
+ const vertices = new Float32Array(scaledPositions.length * 6);
+ for (let i = 0; i < scaledPositions.length; ++i) {
+ vertices.set(scaledPositions[i], 6 * i);
+ vertices.set(normals[i], 6 * i + 3);
+ }
+ const indices = new Uint32Array(triangles.length * 3);
+ for (let i = 0; i < triangles.length; ++i) {
+ indices.set(triangles[i], 3 * i);
+ }
+
+ return {
+ vertices,
+ indices,
+ };
+}
+
+function createSphereTypedArrays(
+ radius: number,
+ widthSegments = 32,
+ heightSegments = 16,
+ randomness = 0
+) {
+ const { vertices: verticesWithUVs, indices } = createSphereMesh(
+ radius,
+ widthSegments,
+ heightSegments,
+ randomness
+ );
+ const numVertices = verticesWithUVs.length / 8;
+ const vertices = new Float32Array(numVertices * 6);
+ for (let i = 0; i < numVertices; ++i) {
+ const srcNdx = i * 8;
+ const dstNdx = i * 6;
+ vertices.set(verticesWithUVs.subarray(srcNdx, srcNdx + 6), dstNdx);
+ }
+ return {
+ vertices,
+ indices: new Uint32Array(indices),
+ };
+}
+
+function flattenNormals({
+ vertices,
+ indices,
+}: {
+ vertices: Float32Array;
+ indices: Uint32Array;
+}) {
+ const newVertices = new Float32Array(indices.length * 6);
+ const newIndices = new Uint32Array(indices.length);
+ for (let i = 0; i < indices.length; i += 3) {
+ const positions = [];
+ for (let j = 0; j < 3; ++j) {
+ const ndx = indices[i + j];
+ const srcNdx = ndx * 6;
+ const dstNdx = (i + j) * 6;
+ // copy position
+ const pos = vertices.subarray(srcNdx, srcNdx + 3);
+ newVertices.set(pos, dstNdx);
+ positions.push(pos);
+ newIndices[i + j] = i + j;
+ }
+
+ const normal = vec3.normalize(
+ vec3.cross(
+ vec3.normalize(vec3.subtract(positions[1], positions[0])),
+ vec3.normalize(vec3.subtract(positions[2], positions[1]))
+ )
+ );
+
+ for (let j = 0; j < 3; ++j) {
+ const dstNdx = (i + j) * 6;
+ newVertices.set(normal, dstNdx + 3);
+ }
+ }
+
+ return {
+ vertices: newVertices,
+ indices: newIndices,
+ };
+}
+
+export const modelData = {
+ teapot: convertMeshToTypedArrays(teapot, 1.5),
+ sphere: createSphereTypedArrays(20),
+ jewel: flattenNormals(createSphereTypedArrays(20, 5, 3)),
+ rock: flattenNormals(createSphereTypedArrays(20, 32, 16, 0.1)),
+};
diff --git a/sample/wireframe/solidColorLit.wgsl b/sample/wireframe/solidColorLit.wgsl
new file mode 100644
index 00000000..c29c3f6c
--- /dev/null
+++ b/sample/wireframe/solidColorLit.wgsl
@@ -0,0 +1,30 @@
+struct Uniforms {
+ worldViewProjectionMatrix: mat4x4f,
+ worldMatrix: mat4x4f,
+ color: vec4f,
+};
+
+struct Vertex {
+ @location(0) position: vec4f,
+ @location(1) normal: vec3f,
+};
+
+struct VSOut {
+ @builtin(position) position: vec4f,
+ @location(0) normal: vec3f,
+};
+
+@group(0) @binding(0) var uni: Uniforms;
+
+@vertex fn vs(vin: Vertex) -> VSOut {
+ var vOut: VSOut;
+ vOut.position = uni.worldViewProjectionMatrix * vin.position;
+ vOut.normal = (uni.worldMatrix * vec4f(vin.normal, 0)).xyz;
+ return vOut;
+}
+
+@fragment fn fs(vin: VSOut) -> @location(0) vec4f {
+ let lightDirection = normalize(vec3f(4, 10, 6));
+ let light = dot(normalize(vin.normal), lightDirection) * 0.5 + 0.5;
+ return vec4f(uni.color.rgb * light, uni.color.a);
+}
diff --git a/sample/wireframe/utils.ts b/sample/wireframe/utils.ts
new file mode 100644
index 00000000..0557de92
--- /dev/null
+++ b/sample/wireframe/utils.ts
@@ -0,0 +1,22 @@
+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
new file mode 100644
index 00000000..de62b1e7
--- /dev/null
+++ b/sample/wireframe/wireframe.wgsl
@@ -0,0 +1,106 @@
+struct Uniforms {
+ worldViewProjectionMatrix: mat4x4f,
+ worldMatrix: mat4x4f,
+ color: vec4f,
+};
+
+struct LineUniforms {
+ stride: u32,
+ thickness: f32,
+ alphaThreshold: f32,
+};
+
+struct VSOut {
+ @builtin(position) position: vec4f,
+};
+
+@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 line: LineUniforms;
+
+@vertex fn vsIndexedU32(@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 = indices[triNdx * 3 + vertNdx];
+
+ // 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 indices you could use this
+ //
+ // let index = triNdx * 3 + vertNdx;
+
+ 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;
+}
+
+@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 indices 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 vsOut: BarycentricCoordinateBasedVSOutput;
+ vsOut.position = uni.worldViewProjectionMatrix * position;
+
+ // emit a barycentric coordinate
+ vsOut.barycenticCoord = vec3f(0);
+ vsOut.barycenticCoord[vertNdx] = 1.0;
+ return vsOut;
+}
+
+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);
+}
diff --git a/src/samples.ts b/src/samples.ts
index 378bcf49..45db7cb2 100644
--- a/src/samples.ts
+++ b/src/samples.ts
@@ -34,6 +34,7 @@ import texturedCube from '../sample/texturedCube/meta';
import twoCubes from '../sample/twoCubes/meta';
import videoUploading from '../sample/videoUploading/meta';
import volumeRenderingTexture3D from '../sample/volumeRenderingTexture3D/meta';
+import wireframe from '../sample/wireframe/meta';
import worker from '../sample/worker/meta';
import workloadSimulator from '../sample/workloadSimulator/meta';
@@ -123,6 +124,7 @@ export const pageCategories: PageCategory[] = [
skinnedMesh,
textRenderingMsdf,
volumeRenderingTexture3D,
+ wireframe,
},
},