Skip to content

feat: @typegpu/geometry package #1426

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions apps/typegpu-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@stackblitz/sdk": "^1.11.0",
"@tailwindcss/vite": "^4.1.6",
"@typegpu/color": "workspace:*",
"@typegpu/geometry": "workspace:*",
"@typegpu/noise": "workspace:*",
"@typegpu/sdf": "workspace:*",
"@types/dom-mediacapture-transform": "^0.1.9",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<canvas></canvas>
189 changes: 189 additions & 0 deletions apps/typegpu-docs/src/content/examples/geometry/circles/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import tgpu from 'typegpu';
import * as d from 'typegpu/data';
import * as s from 'typegpu/std';

import {
circleFan,
circleMaxArea,
circleMaxAreaVertexCount,
} from '@typegpu/geometry';

const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
const canvas = document.querySelector('canvas');
const context = canvas?.getContext('webgpu');
const multisample = true;

if (!canvas) {
throw new Error('Could not find canvas');
}
if (!context) {
throw new Error('Could not create WebGPU context');
}

const adapter = await navigator.gpu.requestAdapter();
console.log(`Using ${adapter?.info.vendor} adapter`);
const device = await adapter?.requestDevice({
requiredFeatures: ['timestamp-query'],
});
if (!device) {
throw new Error('Could not get WebGPU device');
}
const root = tgpu.initFromDevice({ device });

context.configure({
device: root.device,
format: presentationFormat,
alphaMode: 'premultiplied',
});

// Textures
let msaaTexture: GPUTexture;
let msaaTextureView: GPUTextureView;

const createDepthAndMsaaTextures = () => {
if (msaaTexture) {
msaaTexture.destroy();
}
msaaTexture = device.createTexture({
size: [canvas.width, canvas.height, 1],
format: presentationFormat,
sampleCount: 4,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
msaaTextureView = msaaTexture.createView();
};

createDepthAndMsaaTextures();
const resizeObserver = new ResizeObserver(createDepthAndMsaaTextures);
resizeObserver.observe(canvas);

// const Uniforms = d.struct({});

const Circle = d.struct({
position: d.vec2f,
radius: d.f32,
});

const bindGroupLayout = tgpu.bindGroupLayout({
// uniforms: {
// uniform: Uniforms,
// },
circles: {
storage: (n: number) => d.arrayOf(Circle, n),
},
});

// const uniforms = root.createBuffer(Uniforms, {}).$usage(
// 'uniform',
// );

const circleCount = 1000;
const circles = root.createBuffer(
d.arrayOf(Circle, circleCount),
Array.from({ length: circleCount }).map(() =>
Circle({
position: d.vec2f(Math.random() * 2 - 1, Math.random() * 2 - 1),
radius: 0.05 * Math.random() + 0.01,
})
),
).$usage('storage');

const uniformsBindGroup = root.createBindGroup(bindGroupLayout, {
// uniforms,
circles,
});

const mainVertexMaxArea = tgpu['~unstable'].vertexFn({
in: {
instanceIndex: d.builtin.instanceIndex,
vertexIndex: d.builtin.vertexIndex,
},
out: {
outPos: d.builtin.position,
uv: d.vec2f,
instanceIndex: d.interpolate('flat', d.u32),
},
})(({ vertexIndex, instanceIndex }) => {
const circle = bindGroupLayout.$.circles[instanceIndex];
const unit = circleMaxArea(vertexIndex);
const pos = s.add(circle.position, s.mul(unit, circle.radius));
return {
outPos: d.vec4f(pos, 0.0, 1.0),
uv: unit,
instanceIndex,
};
});

const mainVertexFan = tgpu['~unstable'].vertexFn({
in: {
instanceIndex: d.builtin.instanceIndex,
vertexIndex: d.builtin.vertexIndex,
},
out: {
outPos: d.builtin.position,
uv: d.vec2f,
instanceIndex: d.interpolate('flat', d.u32),
},
})(({ vertexIndex, instanceIndex }) => {
const circle = bindGroupLayout.$.circles[instanceIndex];
const unit = circleFan(vertexIndex, 10);
const pos = s.add(circle.position, s.mul(unit, circle.radius));
return {
outPos: d.vec4f(pos, 0.0, 1.0),
uv: unit,
instanceIndex,
};
});

console.log(tgpu.resolve({ externals: { mainVertexFan } }));

const mainFragment = tgpu['~unstable'].fragmentFn({
in: {
uv: d.vec2f,
instanceIndex: d.interpolate('flat', d.u32),
},
out: d.vec4f,
})(({ uv, instanceIndex }) => {
const color = d.vec3f(
1,
s.cos(d.f32(instanceIndex)),
s.sin(5 * d.f32(instanceIndex)),
);
const r = s.length(uv);
return d.vec4f(
s.mix(color, d.vec3f(), s.clamp((r - 0.9) * 20, 0, 0.5)),
1,
);
});

const pipeline = root['~unstable']
.withVertex(mainVertexMaxArea, {})
.withFragment(mainFragment, { format: presentationFormat })
.withMultisample({ count: multisample ? 4 : 1 })
.createPipeline();

setTimeout(() => {
pipeline
.with(bindGroupLayout, uniformsBindGroup)
.withColorAttachment({
...(multisample
? {
view: msaaTextureView,
resolveTarget: context.getCurrentTexture().createView(),
}
: {
view: context.getCurrentTexture().createView(),
}),
clearValue: [0, 0, 0, 0],
loadOp: 'clear',
storeOp: 'store',
})
.withPerformanceCallback((a, b) => {
console.log((Number(b - a) * 1e-6).toFixed(3), 'ms');
})
.draw(circleMaxAreaVertexCount(4), circleCount);
}, 100);

export function onCleanup() {
root.destroy();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "Circles",
"category": "geometry",
"tags": ["experimental"]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/typegpu-docs/src/utils/examples/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const exampleCategories = [
{ key: 'image-processing', label: 'Image processing' },
{ key: 'simulation', label: 'Simulation' },
{ key: 'algorithms', label: 'Algorithms' },
{ key: 'geometry', label: 'Geometry' },
{ key: 'tests', label: 'Tests' },
];

Expand Down
9 changes: 9 additions & 0 deletions packages/typegpu-geometry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div align="center">

# @typegpu/geometry

🚧 **Under Construction** 🚧

</div>

A set of geometry helper functions for use in WebGPU/TypeGPU apps.
12 changes: 12 additions & 0 deletions packages/typegpu-geometry/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { type BuildConfig, defineBuildConfig } from 'unbuild';
import typegpu from 'unplugin-typegpu/rollup';

const Config: BuildConfig[] = defineBuildConfig({
hooks: {
'rollup:options': (_options, config) => {
config.plugins.push(typegpu({ include: [/\.ts$/] }));
},
},
});

export default Config;
7 changes: 7 additions & 0 deletions packages/typegpu-geometry/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"exclude": ["."],
"fmt": {
"exclude": ["!."],
"singleQuote": true
}
}
44 changes: 44 additions & 0 deletions packages/typegpu-geometry/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@typegpu/geometry",
"type": "module",
"version": "0.0.1",
"description": "A set of geometry helper functions for use in WebGPU/TypeGPU apps.",
"exports": {
".": "./src/index.ts",
"./package.json": "./package.json"
},
"publishConfig": {
"directory": "dist",
"linkDirectory": false,
"main": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./dist/package.json",
".": {
"types": "./dist/index.d.ts",
"module": "./dist/index.mjs",
"import": "./dist/index.mjs",
"default": "./dist/index.cjs"
}
}
},
"sideEffects": false,
"scripts": {
"build": "unbuild",
"test:types": "pnpm tsc --p ./tsconfig.json --noEmit",
"prepublishOnly": "tgpu-dev-cli prepack"
},
"keywords": [],
"license": "MIT",
"peerDependencies": {
"typegpu": "^0.5.9"
},
"devDependencies": {
"@typegpu/tgpu-dev-cli": "workspace:*",
"@webgpu/types": "catalog:",
"typegpu": "workspace:*",
"typescript": "catalog:",
"unbuild": "catalog:",
"unplugin-typegpu": "workspace:*"
}
}
98 changes: 98 additions & 0 deletions packages/typegpu-geometry/src/circle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import tgpu from 'typegpu';
import { f32, struct, u32, vec2f } from 'typegpu/data';
import { cos, select, sin } from 'typegpu/std';

const PI = tgpu['~unstable'].const(f32, Math.PI);

const SubdivLevelResult = struct({
level: u32,
pointCount: u32,
vertexCountInLevel: u32,
vertexIndexInLevel: u32,
});

const getSubdivLevel = tgpu['~unstable'].fn([u32], SubdivLevelResult)(
(vertexIndex) => {
let totalVertexCount = u32(0);
for (let level = u32(0); level < 8; level += 1) {
const pointCount = u32(3) * (u32(1) << level);
const triangleCount = select(
u32(1),
u32(3 * (1 << (level - 1))),
level > 0,
);
const vertexCountInLevel = 3 * triangleCount;
const newVertexCount = totalVertexCount + vertexCountInLevel;

if (vertexIndex < newVertexCount) {
return SubdivLevelResult({
level,
pointCount,
vertexCountInLevel,
vertexIndexInLevel: vertexIndex - totalVertexCount,
});
}
totalVertexCount = newVertexCount;
}
return SubdivLevelResult({
level: 0,
pointCount: 0,
vertexCountInLevel: 0,
vertexIndexInLevel: 0,
});
},
);

const consecutiveTriangleVertexIndex = tgpu['~unstable'].fn([u32], u32)(
(i) => {
return (2 * (i + 1)) / 3;
},
);

/**
* Given a `vertexIndex`, returns the unit vector which can be
* added to the circle center and scaled using radius.
* Render using triangle list.
* To decide on how many vertices to render, use `circleMaxAreaVertexCount(subdivLevel)`.
*
* This method of triangulating a circle should generally be
* more performant than `circleFan` due to less overdraw.
* For more information, see https://www.humus.name/index.php?page=News&ID=228
*/
export const circleMaxArea = tgpu['~unstable'].fn([u32], vec2f)(
(vertexIndex) => {
const subdiv = getSubdivLevel(vertexIndex);
const i = consecutiveTriangleVertexIndex(subdiv.vertexIndexInLevel);
const pointCount = subdiv.pointCount;
const angle = 2 * PI.$ * f32(i) / f32(pointCount);
return vec2f(cos(angle), sin(angle));
},
);

export function circleMaxAreaVertexCount(subdivLevel: number) {
let totalVertexCount = 3;
for (let level = 0; level < subdivLevel; level += 1) {
totalVertexCount += 9 * (1 << level);
}
return totalVertexCount;
}

/**
* Given a `vertexIndex`, returns the unit vector which can be
* added to the circle center and scaled using radius.
* Render using triangle list.
* Once you decide on `triangleCount`,
* number of vertices to render is `triangleCount * 3`.
*/
export const circleFan = tgpu['~unstable'].fn([u32, u32], vec2f)(
(vertexIndex, triangleCount) => {
const triangleIndex = vertexIndex / 3;
const vertexInTriangle = vertexIndex % 3;
if (vertexInTriangle === 2) {
return vec2f(0, 0);
}
const i = triangleIndex + vertexInTriangle;
const angle = 2 * PI.$ * f32(i) / f32(triangleCount);
return vec2f(cos(angle), sin(angle));
},
);
1 change: 1 addition & 0 deletions packages/typegpu-geometry/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './circle.ts';
5 changes: 5 additions & 0 deletions packages/typegpu-geometry/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Loading
Loading