Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<canvas data-fit-to-container></canvas>
142 changes: 142 additions & 0 deletions apps/typegpu-docs/src/examples/rendering/mesh-patches/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { meshes } from '@typegpu/geometry';
import { d, std, tgpu } from 'typegpu';
import { setupOrbitCamera } from '../../common/setup-orbit-camera.ts';
import { defineControls } from '../../common/defineControls.ts';

const root = await tgpu.init();
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
const context = root.configureContext({ canvas, alphaMode: 'premultiplied' });

const maxSegments = 32;
let segments = 4;
const Camera = d.struct({ viewProjection: d.mat4x4f });
const Scene = d.struct({ camera: Camera, segments: d.u32, wireframe: d.u32 });
const scene = root.createUniform(Scene);
const indices = root
.createBuffer(
d.arrayOf(d.u32, meshes.triangleIndexCount(maxSegments)),
meshes.triangleIndices(maxSegments),
)
.$usage('index');

const shapes = [
meshes.patches.icosphere(),
meshes.patches.capsule({ radius: 0.35, height: 0.6 }),
meshes.patches.roundedBox({ radius: 0.2 }),
];
const colors = [d.vec3f(0.2, 0.65, 0.8), d.vec3f(0.9, 0.55, 0.2), d.vec3f(0.6, 0.4, 0.8)];

const fragment = tgpu.fragmentFn({
in: { normal: d.vec3f, color: d.vec3f, grid: d.vec3f },
out: d.vec4f,
})(({ normal, color, grid }) => {
'use gpu';
const light = std.normalize(d.vec3f(-0.4, 0.8, 1));
let shaded = color * (0.25 + 0.75 * std.saturate(std.dot(std.normalize(normal), light)));

if (scene.$.wireframe !== 0) {
const f = std.fract(grid);
const distance = std.min(f, 1 - f) / std.max(std.fwidth(grid), d.vec3f(1e-4));
const edge = std.min(distance.x, std.min(distance.y, distance.z));
shaded = std.mix(shaded * 0.2, shaded, std.smoothstep(0, 1, edge));
}
return d.vec4f(shaded, 1);
});

const pipelines = shapes.map((shape, i) => {
const offset = d.vec3f((i - 1) * 1.5, 0, 0);
const color = colors[i];
return root
.createRenderPipeline({
vertex: ({ $vertexIndex, $instanceIndex }) => {
'use gpu';
const weights = meshes.triangleBarycentrics($vertexIndex, scene.$.segments);
const vertex = shape.at($instanceIndex, weights);
return {
$position: scene.$.camera.viewProjection * d.vec4f(vertex.position + offset, 1),
normal: vertex.normal,
color,
grid: weights * d.f32(scene.$.segments),
};
},
fragment,
primitive: { cullMode: 'back' },
depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
})
.withIndexBuffer(indices);
});

await Promise.all(pipelines.map((pipeline) => pipeline.initAsync()));

function createDepth() {
return root
.createTexture({ size: [canvas.width, canvas.height], format: 'depth24plus' })
.$usage('transient');
}

let depth = createDepth();
let frame: number | undefined;

function render() {
frame = undefined;
const encoder = root['~unstable'].createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{ view: context, clearValue: [0.04, 0.05, 0.07, 1] }],
depthStencilAttachment: { view: depth, depthStoreOp: 'discard' },
});
pipelines.forEach((pipeline, i) => {
pipeline.with(pass).drawIndexed(meshes.triangleIndexCount(segments), shapes[i].patchCount);
});
pass.end();
encoder.submit();
}

function requestRender() {
frame ??= requestAnimationFrame(render);
}

const { cleanupCamera } = setupOrbitCamera(
canvas,
{ initPos: d.vec4f(2.5, 1.8, 5, 1), target: d.vec4f(0, 0, 0, 1), minZoom: 2, maxZoom: 10 },
(camera) => {
scene.patch({ camera });
requestRender();
},
);

requestRender();

const resizeObserver = new ResizeObserver(() => {
depth.destroy();
depth = createDepth();
requestRender();
});
resizeObserver.observe(canvas);

export const controls = defineControls({
Detail: {
initial: segments,
min: 1,
max: maxSegments,
step: 1,
onSliderChange: (value) => {
segments = value;
scene.patch({ segments });
requestRender();
},
},
Wireframe: {
initial: true,
onToggleChange: (value) => {
scene.patch({ wireframe: value ? 1 : 0 });
requestRender();
},
},
});

export function onCleanup() {
if (frame !== undefined) cancelAnimationFrame(frame);
resizeObserver.disconnect();
cleanupCamera();
root.destroy();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"title": "Mesh Patches",
"description": "Samples icosphere, capsule and rounded box patches directly in the vertex shader from one shared index buffer, so the subdivision level changes without rebuilding any buffers.",
"category": "rendering",
"tags": ["3d", "geometry", "rasterization"],
"coolFactor": 5
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @vitest-environment jsdom
*/

import { describe, expect } from 'vitest';
import { it } from 'typegpu-testing-utility';
import { runExampleTest, setupCommonMocks } from './utils/baseTest.ts';
import { mockResizeObserver } from './utils/commonMocks.ts';

describe('mesh patches example', () => {
setupCommonMocks();

it('resolves one instanced pipeline per patch surface', async ({ device }) => {
const shaderCodes = await runExampleTest(
{
category: 'rendering',
name: 'mesh-patches',
setupMocks: mockResizeObserver,
expectedCalls: 3,
},
device,
);

const signatures = shaderCodes.match(/^fn .*(?= \{$)/gm);
expect([...new Set(signatures)].join('\n')).toMatchInlineSnapshot(`
"fn triangleBarycentrics(vertex_1: u32, segments: u32) -> vec3f
fn warp(cosAngle: f32, t: f32) -> f32
fn uniformArea(a: vec3f, b: vec3f, c: vec3f, weights: vec3f) -> vec3f
fn at(patch_1: u32, w: vec3f) -> Surface
fn bitSigns(bits: u32) -> vec3f
fn cornerNormal(octant: u32, weights: vec3f) -> vec3f
fn quadCoordinates(triangle: u32, weights: vec3f) -> vec2f
fn arc(a: vec3f, b: vec3f, t: f32) -> vec3f
fn edgeSurface(triangle: u32, weights: vec3f, center: vec3f, axis: vec3f, u: vec3f, v: vec3f, halfHeight: f32, radius: f32) -> Surface
fn at(index: u32, w: vec3f) -> Surface"
`);
});
});
3 changes: 3 additions & 0 deletions packages/typegpu-geometry/src/mesh/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ export type { ConstructibleSchema, Geometry, IndexedGeometry, Topology } from '.
export { parametric } from './parametric.ts';
export type { Parametric, ParametricOptions } from './parametric.ts';
export * from './primitives.ts';
export * from './patchPrimitives.ts';
export * from './triangles.ts';
export * as patches from './patches/index.ts';
29 changes: 29 additions & 0 deletions packages/typegpu-geometry/src/mesh/patchPrimitives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { IndexedGeometry } from './geometry.ts';
import * as patches from './patches/index.ts';
import { tessellate } from './triangles.ts';

export interface TessellationOptions {
/** Segments along each edge of every triangular patch. Default 4 */
segments?: number;
}

export function icosphere({
segments = 4,
...options
}: patches.IcosphereOptions & TessellationOptions = {}): IndexedGeometry {
return tessellate(patches.icosphere(options), segments);
}

export function capsule({
segments = 4,
...options
}: patches.CapsuleOptions & TessellationOptions = {}): IndexedGeometry {
return tessellate(patches.capsule(options), segments);
}

export function roundedBox({
segments = 4,
...options
}: patches.RoundedBoxOptions & TessellationOptions = {}): IndexedGeometry {
return tessellate(patches.roundedBox(options), segments);
}
55 changes: 55 additions & 0 deletions packages/typegpu-geometry/src/mesh/patches/icosphere.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { d, std, tgpu } from 'typegpu';
import { Surface } from '../geometry.ts';
import { type PatchSurface } from '../triangles.ts';
import { uniformArea } from './spherical.ts';

const phi = (1 + Math.sqrt(5)) / 2;
const corners = (
[
[-1, phi, 0],
[1, phi, 0],
[-1, -phi, 0],
[1, -phi, 0],
[0, -1, phi],
[0, 1, phi],
[0, -1, -phi],
[0, 1, -phi],
[phi, 0, -1],
[phi, 0, 1],
[-phi, 0, -1],
[-phi, 0, 1],
] as const
).map(([x, y, z]) => std.normalize(d.vec3f(x, y, z)));
const faces = [
0, 11, 5, 0, 5, 1, 0, 1, 7, 0, 7, 10, 0, 10, 11, 1, 5, 9, 5, 11, 4, 11, 10, 2, 10, 7, 6, 7, 1, 8,
3, 9, 4, 3, 4, 2, 3, 2, 6, 3, 6, 8, 3, 8, 9, 4, 9, 5, 2, 4, 11, 6, 2, 10, 8, 6, 7, 9, 8, 1,
];
const vertices = tgpu.const(
d.arrayOf(d.vec3f, faces.length),
faces.map((i) => corners[i] as d.v3f),
);

export interface IcosphereOptions {
radius?: number;
interpolate?: (a: d.v3f, b: d.v3f, c: d.v3f, weights: d.v3f) => d.v3f;
}

/** UVs are local barycentric coordinates within each patch */
export function icosphere({
radius = 0.5,
interpolate = uniformArea,
}: IcosphereOptions = {}): PatchSurface {
return {
schema: Surface,
patchCount: 20,
at: (patch: number, w: d.v3f) => {
'use gpu';
const offset = patch * 3;
const a = vertices.$[offset] as d.v3f;
const b = vertices.$[offset + 1] as d.v3f;
const c = vertices.$[offset + 2] as d.v3f;
const normal = interpolate(a, b, c, w);
return Surface({ position: normal * radius, normal, uv: w.yz });
},
};
}
3 changes: 3 additions & 0 deletions packages/typegpu-geometry/src/mesh/patches/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './icosphere.ts';
export * from './rounded.ts';
export { linear, uniformArea } from './spherical.ts';
Loading
Loading