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
19 changes: 19 additions & 0 deletions apps/typegpu-docs/src/examples/rendering/mesh-primitives/blob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { d, std } from 'typegpu';
import { meshes } from '@typegpu/geometry';
import { paint } from './geometry.ts';
import { sceneLayout } from './shading.ts';

const surface = meshes.parametric(
{
at: (u, v) => {
'use gpu';
const time = sceneLayout.$.uniforms.time;
const p = meshes.surfaces.sphere(u, v);
const ripple = std.sin(p.x * 4 + time * 2) * std.sin(p.y * 3 - time) * std.sin(p.z * 5);
return p * (0.55 + 0.15 * ripple) + d.vec3f(0, 0.55, 0);
},
},
{ cols: 96, rows: 64 },
);

export const blob = paint(surface, d.vec3f(0.9));
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { std } from 'typegpu';
import { meshes } from '@typegpu/geometry';
import { place } from './geometry.ts';

const floor = meshes.plane({ width: 6, depth: 6 });
const twoSidedFloor = meshes.concat(floor, meshes.transform(floor, std.rotationX4(Math.PI)));

export const gallery = meshes.concat(
place(twoSidedFloor, [0, -0.51, 0], [0.75, 0.75, 0.8]),
place(meshes.box(), [-1.5, 0, 1], [0.9, 0.35, 0.3]),
place(meshes.cylinder(), [1.5, 0, 1], [0.3, 0.6, 0.9]),
place(meshes.torus(), [1.5, -0.35, -1], [0.95, 0.75, 0.2]),
place(meshes.sphere(), [-1.5, 0, -1], [0.4, 0.85, 0.5]),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { d, std } from 'typegpu';
import { meshes } from '@typegpu/geometry';

const Paint = d.struct({ color: d.vec3f });

export function paint<V extends d.WgslStruct>(
g: meshes.IndexedGeometry<V>,
color: d.InferInput<d.Vec3f>,
) {
const rgb = d.vec3f(color[0], color[1], color[2]);
return meshes.attach(g, Paint, () => {
'use gpu';
return Paint({ color: rgb });
});
}

export function place<V extends meshes.Placed>(
g: meshes.IndexedGeometry<V>,
at: d.InferInput<d.Vec3f>,
color: d.InferInput<d.Vec3f>,
) {
return paint(meshes.transform(g, std.translation4(d.vec3f(at[0], at[1], at[2]))), color);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<canvas data-fit-to-container></canvas>
146 changes: 146 additions & 0 deletions apps/typegpu-docs/src/examples/rendering/mesh-primitives/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { d, std, tgpu } from 'typegpu';
import { meshes } from '@typegpu/geometry';
import { mat4 } from 'wgpu-matrix';
import { setupOrbitCamera } from '../../common/setup-orbit-camera.ts';
import { blob } from './blob.ts';
import { gallery } from './gallery.ts';
import { place } from './geometry.ts';
import {
fragment,
SceneUniforms,
sceneLayout,
shadowLayout,
worldToClip,
worldToLight,
} from './shading.ts';
import { spring } from './spring.ts';

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

const uniforms = root.createUniform(SceneUniforms);
const sceneBindGroup = root.createBindGroup(sceneLayout, { uniforms });

const shadowMap = root
.createTexture({ size: [2048, 2048], format: 'depth32float' })
.$usage('render', 'sampled');

const shadowBindGroup = root.createBindGroup(shadowLayout, {
map: shadowMap,
sampler: root.createComparisonSampler({
compare: 'less-equal',
magFilter: 'linear',
minFilter: 'linear',
}),
});

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

let depth = createDepth();

const scene = meshes.bakeIndices(
root,
meshes.concat(gallery, blob, place(spring, [0, 0.07, 2.2], [0.7, 0.45, 0.95])),
);

const shadow = root
.createRenderPipeline({
vertex: ({ $vertexIndex }) => {
'use gpu';
return { $position: worldToLight(scene.vertexAt($vertexIndex).position) };
},
depthStencil: {
format: 'depth32float',
depthWriteEnabled: true,
depthCompare: 'less',
depthBias: 1,
depthBiasSlopeScale: 2,
},
primitive: { cullMode: 'back' },
})
.with(sceneBindGroup)
.pipe(scene.inject());

const lit = root
.createRenderPipeline({
vertex: ({ $vertexIndex }) => {
'use gpu';
const vertex = scene.vertexAt($vertexIndex);
return {
$position: worldToClip(vertex.position),
worldPos: vertex.position,
normal: vertex.normal,
color: vertex.color,
};
},
fragment,
depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
primitive: { cullMode: 'back' },
})
.with(sceneBindGroup)
.with(shadowBindGroup)
.pipe(scene.inject());

const { cleanupCamera } = setupOrbitCamera(
canvas,
{ initPos: d.vec4f(3.2, 2.2, 3.7, 1), target: d.vec4f(0, 0.3, 0, 1), minZoom: 2, maxZoom: 12 },
(camera) => uniforms.patch({ camera }),
);

const lightProjection = mat4.ortho(-4.5, 4.5, -4.5, 4.5, 0.5, 20, d.mat4x4f());

function lightAt(seconds: number) {
const angle = seconds * 0.2;
const direction = std.normalize(d.vec3f(Math.cos(angle) * 0.7, -1, Math.sin(angle) * 0.7));
const eye = std.mul(direction, -10);
const view = mat4.lookAt(eye, d.vec3f(), d.vec3f(0, 1, 0), d.mat4x4f());

return { viewProj: std.mul(lightProjection, view), direction };
}

let frame: number;

function render(now: number) {
const seconds = now * 0.001;
uniforms.patch({ light: lightAt(seconds), time: seconds });

const encoder = root['~unstable'].createCommandEncoder();

const shadowPass = encoder.beginRenderPass({ depthStencilAttachment: { view: shadowMap } });
shadow.with(shadowPass).drawIndexed(scene.indexCount);
shadowPass.end();

const pass = encoder.beginRenderPass({
colorAttachments: [{ view: context, clearValue: [0.08, 0.09, 0.12, 1] }],
depthStencilAttachment: { view: depth, depthStoreOp: 'discard' },
});

lit.with(pass).drawIndexed(scene.indexCount);
pass.end();

encoder.submit();

frame = requestAnimationFrame(render);
}

frame = requestAnimationFrame(render);

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

resizeObserver.observe(canvas);

export function onCleanup() {
cancelAnimationFrame(frame);
resizeObserver.disconnect();
cleanupCamera();

root.destroy();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"title": "Mesh Primitives",
"description": "Combines procedural primitives, a rippling parametric blob and a spring into one mesh, evaluated in the vertex shader and lit by a circling shadow-casting light.",
"category": "rendering",
"tags": ["3d", "geometry", "rasterization"],
"coolFactor": 6
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { d, std, tgpu } from 'typegpu';

const Camera = d.struct({ viewProjection: d.mat4x4f });
const Light = d.struct({ viewProj: d.mat4x4f, direction: d.vec3f });

export const SceneUniforms = d.struct({ camera: Camera, light: Light, time: d.f32 });
export const sceneLayout = tgpu.bindGroupLayout({ uniforms: { uniform: SceneUniforms } });

export const shadowLayout = tgpu.bindGroupLayout({
map: { texture: d.textureDepth2d() },
sampler: { sampler: 'comparison' },
});

export function worldToClip(position: d.v3f) {
'use gpu';
return sceneLayout.$.uniforms.camera.viewProjection * d.vec4f(position, 1);
}

export function worldToLight(position: d.v3f) {
'use gpu';
return sceneLayout.$.uniforms.light.viewProj * d.vec4f(position, 1);
}

function shadowAt(position: d.v3f, normal: d.v3f) {
'use gpu';
const clip = worldToLight(position + normal * 0.03);
const uv = clip.xy * d.vec2f(0.5, -0.5) + 0.5;

return std.textureSampleCompare(shadowLayout.$.map, shadowLayout.$.sampler, uv, clip.z);
}

export const fragment = tgpu.fragmentFn({
in: { worldPos: d.vec3f, normal: d.vec3f, color: d.vec3f },
out: d.vec4f,
})(({ worldPos, normal, color }) => {
'use gpu';
const n = std.normalize(normal);
const toLight = std.neg(sceneLayout.$.uniforms.light.direction);
const diffuse = std.max(std.dot(n, toLight), 0) * shadowAt(worldPos, n);
const sky = n.y * 0.5 + 0.5;

return d.vec4f(color * (0.2 + 0.25 * sky + 0.6 * diffuse), 1);
});
92 changes: 92 additions & 0 deletions apps/typegpu-docs/src/examples/rendering/mesh-primitives/spring.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { d, std } from 'typegpu';
import { meshes } from '@typegpu/geometry';
import { sceneLayout } from './shading.ts';

const TAU = Math.PI * 2;
const turns = 4;
const radius = 0.3;
const tube = 0.07;
const height = 1;

const centerAt = (u: number) => {
'use gpu';
const phi = u * TAU * turns;
return d.vec3f(radius * std.cos(phi), (u - 0.5) * height, radius * std.sin(phi));
};

const radialAt = (u: number) => {
'use gpu';
const phi = u * TAU * turns;
return d.vec3f(std.cos(phi), 0, std.sin(phi));
};

const tangentAt = (u: number) => {
'use gpu';
const phi = u * TAU * turns;
return std.normalize(
d.vec3f(-radius * std.sin(phi) * TAU * turns, height, radius * std.cos(phi) * TAU * turns),
);
};

const offset = (u: number, v: number) => {
'use gpu';
const t = v * TAU;
const radial = radialAt(u);
const binormal = std.cross(radial, tangentAt(u));
return radial * std.cos(t) + binormal * std.sin(t);
};

const body = meshes.parametric(
{
at: (u, v) => {
'use gpu';
return centerAt(u) + offset(u, v) * tube;
},
normalAt: offset,
},
{ cols: 48 * turns, rows: 12 },
);

const cap = meshes.parametric(
{
at: (u, v) => {
'use gpu';
return meshes.surfaces.disc(u, v) * tube;
},
normalAt: () => {
'use gpu';
return d.vec3f(0, 1, 0);
},
},
{ cols: 12, rows: 1 },
);

const capAt = (u: number, sign: number) => {
const radial = radialAt(u);
const tangent = tangentAt(u).mul(sign);
const binormal = std.cross(radial, tangent);

const frame = d.mat4x4f(
d.vec4f(radial, 0),
d.vec4f(tangent, 0),
d.vec4f(binormal, 0),
d.vec4f(centerAt(u), 1),
);

return meshes.transform(cap, frame);
};

export const spring = meshes.map(
meshes.concat(body, capAt(0, -1), capAt(1, 1)),
meshes.Surface,
(vertex) => {
'use gpu';
const stretch = 1 + 0.3 * std.sin(sceneLayout.$.uniforms.time * 2);

return meshes.Surface({
position: vertex.position * d.vec3f(1, stretch, 1),
normal: std.normalize(vertex.normal / d.vec3f(1, stretch, 1)),
uv: vertex.uv,
});
},
);
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* @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 primitives example', () => {
setupCommonMocks();

it('resolves the procedural scene and its shadows', async ({ device }) => {
const shaderCodes = await runExampleTest(
{
category: 'rendering',
name: 'mesh-primitives',
setupMocks: mockResizeObserver,
expectedCalls: 3,
},
device,
);

const uniforms = shaderCodes.match(
/struct (?:Camera|Light|SceneUniforms) \{[^}]*\}|@group[^\n]*var<uniform>[^\n]*/g,
);
expect([...new Set(uniforms)].join('\n\n')).toMatchInlineSnapshot(`
"struct Camera {
viewProjection: mat4x4f,
}

struct Light {
viewProj: mat4x4f,
direction: vec3f,
}

struct SceneUniforms {
camera: Camera,
light: Light,
time: f32,
}

@group(0) @binding(0) var<uniform> uniforms: SceneUniforms;"
`);
});
});
Loading
Loading