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