diff --git a/apps/typegpu-docs/package.json b/apps/typegpu-docs/package.json
index d987a9b09..a78b47919 100644
--- a/apps/typegpu-docs/package.json
+++ b/apps/typegpu-docs/package.json
@@ -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",
diff --git a/apps/typegpu-docs/src/content/examples/geometry/circles/index.html b/apps/typegpu-docs/src/content/examples/geometry/circles/index.html
new file mode 100644
index 000000000..aa8cc321b
--- /dev/null
+++ b/apps/typegpu-docs/src/content/examples/geometry/circles/index.html
@@ -0,0 +1 @@
+
diff --git a/apps/typegpu-docs/src/content/examples/geometry/circles/index.ts b/apps/typegpu-docs/src/content/examples/geometry/circles/index.ts
new file mode 100644
index 000000000..b28eea466
--- /dev/null
+++ b/apps/typegpu-docs/src/content/examples/geometry/circles/index.ts
@@ -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();
+}
diff --git a/apps/typegpu-docs/src/content/examples/geometry/circles/meta.json b/apps/typegpu-docs/src/content/examples/geometry/circles/meta.json
new file mode 100644
index 000000000..be577b115
--- /dev/null
+++ b/apps/typegpu-docs/src/content/examples/geometry/circles/meta.json
@@ -0,0 +1,5 @@
+{
+ "title": "Circles",
+ "category": "geometry",
+ "tags": ["experimental"]
+}
diff --git a/apps/typegpu-docs/src/content/examples/geometry/circles/thumbnail.png b/apps/typegpu-docs/src/content/examples/geometry/circles/thumbnail.png
new file mode 100644
index 000000000..50d4d575a
Binary files /dev/null and b/apps/typegpu-docs/src/content/examples/geometry/circles/thumbnail.png differ
diff --git a/apps/typegpu-docs/src/utils/examples/types.ts b/apps/typegpu-docs/src/utils/examples/types.ts
index 0dbcd083e..9037d1120 100644
--- a/apps/typegpu-docs/src/utils/examples/types.ts
+++ b/apps/typegpu-docs/src/utils/examples/types.ts
@@ -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' },
];
diff --git a/packages/typegpu-geometry/README.md b/packages/typegpu-geometry/README.md
new file mode 100644
index 000000000..f4c191701
--- /dev/null
+++ b/packages/typegpu-geometry/README.md
@@ -0,0 +1,9 @@
+
+
+# @typegpu/geometry
+
+🚧 **Under Construction** 🚧
+
+
+
+A set of geometry helper functions for use in WebGPU/TypeGPU apps.
diff --git a/packages/typegpu-geometry/build.config.ts b/packages/typegpu-geometry/build.config.ts
new file mode 100644
index 000000000..7f9f024f1
--- /dev/null
+++ b/packages/typegpu-geometry/build.config.ts
@@ -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;
diff --git a/packages/typegpu-geometry/deno.json b/packages/typegpu-geometry/deno.json
new file mode 100644
index 000000000..66699a4b5
--- /dev/null
+++ b/packages/typegpu-geometry/deno.json
@@ -0,0 +1,7 @@
+{
+ "exclude": ["."],
+ "fmt": {
+ "exclude": ["!."],
+ "singleQuote": true
+ }
+}
diff --git a/packages/typegpu-geometry/package.json b/packages/typegpu-geometry/package.json
new file mode 100644
index 000000000..998ab34bd
--- /dev/null
+++ b/packages/typegpu-geometry/package.json
@@ -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:*"
+ }
+}
diff --git a/packages/typegpu-geometry/src/circle.ts b/packages/typegpu-geometry/src/circle.ts
new file mode 100644
index 000000000..35b6b0ac7
--- /dev/null
+++ b/packages/typegpu-geometry/src/circle.ts
@@ -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));
+ },
+);
diff --git a/packages/typegpu-geometry/src/index.ts b/packages/typegpu-geometry/src/index.ts
new file mode 100644
index 000000000..0422b811b
--- /dev/null
+++ b/packages/typegpu-geometry/src/index.ts
@@ -0,0 +1 @@
+export * from './circle.ts';
diff --git a/packages/typegpu-geometry/tsconfig.json b/packages/typegpu-geometry/tsconfig.json
new file mode 100644
index 000000000..5f257dc0f
--- /dev/null
+++ b/packages/typegpu-geometry/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/typegpu/src/builtin.ts b/packages/typegpu/src/builtin.ts
index 77824b115..3bd55ad19 100644
--- a/packages/typegpu/src/builtin.ts
+++ b/packages/typegpu/src/builtin.ts
@@ -1,11 +1,12 @@
import { arrayOf } from './data/array.ts';
import { attribute } from './data/attributes.ts';
import type { LooseDecorated } from './data/dataTypes.ts';
-import { f32, u32 } from './data/numeric.ts';
+import { bool, f32, u32 } from './data/numeric.ts';
import { vec3u, vec4f } from './data/vector.ts';
import type {
AnyWgslData,
BaseData,
+ Bool,
Builtin,
Decorated,
F32,
@@ -27,7 +28,7 @@ export type BuiltinClipDistances = Decorated<
WgslArray,
[Builtin<'clip_distances'>]
>;
-export type BuiltinFrontFacing = Decorated]>;
+export type BuiltinFrontFacing = Decorated]>;
export type BuiltinFragDepth = Decorated]>;
export type BuiltinSampleIndex = Decorated]>;
export type BuiltinSampleMask = Decorated]>;
@@ -74,7 +75,7 @@ export const builtin = {
arrayOf(u32, 8),
'clip_distances',
),
- frontFacing: defineBuiltin(f32, 'front_facing'),
+ frontFacing: defineBuiltin(bool, 'front_facing'),
fragDepth: defineBuiltin(f32, 'frag_depth'),
sampleIndex: defineBuiltin(u32, 'sample_index'),
sampleMask: defineBuiltin(u32, 'sample_mask'),
diff --git a/packages/typegpu/src/core/function/fnTypes.ts b/packages/typegpu/src/core/function/fnTypes.ts
index 8f4576483..359c3484f 100644
--- a/packages/typegpu/src/core/function/fnTypes.ts
+++ b/packages/typegpu/src/core/function/fnTypes.ts
@@ -2,6 +2,7 @@ import type * as tinyest from 'tinyest';
import type { BuiltinClipDistances } from '../../builtin.ts';
import type { AnyAttribute } from '../../data/attributes.ts';
import type {
+ Bool,
Decorated,
F16,
F32,
@@ -73,6 +74,7 @@ export type Implementation =
| InferImplSchema;
export type BaseIOData =
+ | Bool
| F32
| F16
| I32