diff --git a/apps/typegpu-docs/package.json b/apps/typegpu-docs/package.json index 7f9571185..2b763895b 100644 --- a/apps/typegpu-docs/package.json +++ b/apps/typegpu-docs/package.json @@ -24,6 +24,8 @@ "@tailwindcss/vite": "^4.1.6", "@typegpu/color": "workspace:*", "@typegpu/noise": "workspace:*", + "@typegpu/three": "workspace:*", + "three": "^0.178.0", "@types/dom-mediacapture-transform": "^0.1.9", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", @@ -59,6 +61,7 @@ "@types/babel__template": "^7.4.4", "@types/babel__traverse": "^7.20.7", "@types/node": "^24.0.3", + "@types/three": "catalog:types", "@webgpu/types": "catalog:types", "astro-vtbot": "^2.0.6", "autoprefixer": "^10.4.21", diff --git a/apps/typegpu-docs/src/content/examples/rendering/three-simple/index.html b/apps/typegpu-docs/src/content/examples/rendering/three-simple/index.html new file mode 100644 index 000000000..aa8cc321b --- /dev/null +++ b/apps/typegpu-docs/src/content/examples/rendering/three-simple/index.html @@ -0,0 +1 @@ + diff --git a/apps/typegpu-docs/src/content/examples/rendering/three-simple/index.ts b/apps/typegpu-docs/src/content/examples/rendering/three-simple/index.ts new file mode 100644 index 000000000..8cc0a8c9c --- /dev/null +++ b/apps/typegpu-docs/src/content/examples/rendering/three-simple/index.ts @@ -0,0 +1,57 @@ +import tgpu from 'typegpu'; +import * as THREE from 'three/webgpu'; +import * as t from '@typegpu/three'; +import * as d from 'typegpu/data'; +import { uv } from 'three/tsl'; + +const canvas = document.querySelector('canvas') as HTMLCanvasElement; + +const tgpuMaterial = new t.TypeGPUMaterial( + tgpu.fn([d.vec2f], d.vec4f)((uv) => { + return d.vec4f(uv.x, uv.y, 0.5, 1); + }), + [uv()], +); + +const renderer = new THREE.WebGPURenderer({ canvas }); +await renderer.init(); + +renderer.setPixelRatio(window.devicePixelRatio); +renderer.setSize(canvas.clientWidth, canvas.clientHeight, false); + +const scene = new THREE.Scene(); +const camera = new THREE.PerspectiveCamera( + 75, + window.innerWidth / window.innerHeight, + 0.1, + 1000, +); +camera.position.z = 5; + +const mesh = new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + tgpuMaterial, +); +scene.add(mesh); + +const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const width = entry.contentRect.width; + const height = entry.contentRect.height; + renderer.setSize(width, height, false); + camera.aspect = width / height; + camera.updateProjectionMatrix(); + } +}); +resizeObserver.observe(canvas); + +renderer.setAnimationLoop(() => { + mesh.rotation.x += 0.01; + mesh.rotation.y += 0.01; + renderer.render(scene, camera); +}); + +export function onCleanup() { + renderer.dispose(); + resizeObserver.disconnect(); +} diff --git a/apps/typegpu-docs/src/content/examples/rendering/three-simple/meta.json b/apps/typegpu-docs/src/content/examples/rendering/three-simple/meta.json new file mode 100644 index 000000000..69277f2b7 --- /dev/null +++ b/apps/typegpu-docs/src/content/examples/rendering/three-simple/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Three.js", + "category": "rendering", + "tags": ["experimental"] +} diff --git a/packages/typegpu-three/README.md b/packages/typegpu-three/README.md new file mode 100644 index 000000000..0a802352d --- /dev/null +++ b/packages/typegpu-three/README.md @@ -0,0 +1,9 @@ +
+ +# @typegpu/three + +🚧 **Under Construction** 🚧 + +
+ +A helper library for using TypeGPU with Three.js. diff --git a/packages/typegpu-three/build.config.ts b/packages/typegpu-three/build.config.ts new file mode 100644 index 000000000..7f9f024f1 --- /dev/null +++ b/packages/typegpu-three/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-three/deno.json b/packages/typegpu-three/deno.json new file mode 100644 index 000000000..66699a4b5 --- /dev/null +++ b/packages/typegpu-three/deno.json @@ -0,0 +1,7 @@ +{ + "exclude": ["."], + "fmt": { + "exclude": ["!."], + "singleQuote": true + } +} diff --git a/packages/typegpu-three/package.json b/packages/typegpu-three/package.json new file mode 100644 index 000000000..8d3282ada --- /dev/null +++ b/packages/typegpu-three/package.json @@ -0,0 +1,46 @@ +{ + "name": "@typegpu/three", + "type": "module", + "version": "0.0.0", + "description": "Utilities for integrating TypeGPU with Three.js", + "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", + "three": ">0.126.0" + }, + "devDependencies": { + "@typegpu/tgpu-dev-cli": "workspace:*", + "@webgpu/types": "catalog:types", + "@types/three": "catalog:types", + "typegpu": "workspace:*", + "typescript": "catalog:types", + "unbuild": "catalog:build", + "unplugin-typegpu": "workspace:*" + } +} diff --git a/packages/typegpu-three/src/index.ts b/packages/typegpu-three/src/index.ts new file mode 100644 index 000000000..0d5349165 --- /dev/null +++ b/packages/typegpu-three/src/index.ts @@ -0,0 +1 @@ +export * from './tgpuThree.ts'; diff --git a/packages/typegpu-three/src/tgpuThree.ts b/packages/typegpu-three/src/tgpuThree.ts new file mode 100644 index 000000000..b97c519b0 --- /dev/null +++ b/packages/typegpu-three/src/tgpuThree.ts @@ -0,0 +1,108 @@ +import * as THREE from 'three/webgpu'; +import tgpu, { type TgpuFn } from 'typegpu'; + +class FragmentNode extends THREE.CodeNode { + private tgslFn: TgpuFn; + private functionName: string | null | undefined; + private threeVars: THREE.TSL.ShaderNodeObject[] | undefined; + private argNames: string[] | undefined; + + constructor( + tgslFn: TgpuFn, + threeRequirements?: THREE.TSL.ShaderNodeObject[] | undefined, + ) { + const resolved = tgpu.resolve({ + template: '___ID___ fnName', + externals: { fnName: tgslFn }, + }); + const [code, functionName] = resolved.split('___ID___').map((s) => + s.trim() + ); + let counter = 0; + const args = tgslFn.shell.argTypes.map((type) => + `TGPUArg${counter++}_${type.type}` + ); + const threeArgs = threeRequirements + ? threeRequirements.map((node, i) => node.toVar(args[i])) + : []; + + super(code, [...threeArgs], 'wgsl'); + + this.functionName = functionName; + this.threeVars = threeArgs; + this.argNames = args; + this.tgslFn = tgslFn; + } + + static get type() { + return 'FunctionNode'; + } + + getNodeType(builder: THREE.NodeBuilder) { + return this.getNodeFunction(builder).type; + } + + getInputs(builder: THREE.NodeBuilder) { + return this.getNodeFunction(builder).inputs; + } + + getNodeFunction(builder: THREE.NodeBuilder) { + const nodeData = builder.getDataFromNode(this); + + // @ts-expect-error <- Three.js types suck + let nodeFunction = nodeData.nodeFunction; + + if (nodeFunction === undefined) { + nodeFunction = builder.parser.parseFunction(this.code); + // @ts-expect-error <- Three.js types suck + nodeData.nodeFunction = nodeFunction; + } + + return nodeFunction; + } + + generate( + builder: THREE.NodeBuilder, + output: string | null | undefined, + ): string | null | undefined { + super.generate(builder); + + const nodeFunction = this.getNodeFunction(builder); + + const name = nodeFunction.name; + const type = nodeFunction.type; + + const nodeCode = builder.getCodeFromNode(this, type); + + if (name !== '') { + // @ts-expect-error <- Three.js types suck + nodeCode.name = name; + } + + // @ts-expect-error <- Three.js types suck + const propertyName = builder.getPropertyName(nodeCode, 'fragment'); + + const code = this.getNodeFunction(builder).getCode(propertyName); + // @ts-expect-error <- Three.js types suck + nodeCode.code = `${code}\n`; + + if (output === 'property') { + return this.functionName; + } + return `${this.functionName}(${this.argNames?.join(', ')})`; + } +} + +export class TypeGPUMaterial extends THREE.NodeMaterial { + constructor( + fragmentFn: TgpuFn, + threeRequirements?: THREE.TSL.ShaderNodeObject< + // biome-ignore lint/suspicious/noExplicitAny: + THREE.Node | THREE.UniformNode + >[] | undefined, + ) { + super(); + + this.fragmentNode = new FragmentNode(fragmentFn, threeRequirements); + } +} diff --git a/packages/typegpu-three/tsconfig.json b/packages/typegpu-three/tsconfig.json new file mode 100644 index 000000000..5f257dc0f --- /dev/null +++ b/packages/typegpu-three/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 396c00d55..c73fad9ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ catalogs: specifier: ^3.2.4 version: 3.2.4 types: + '@types/three': + specifier: '>0.126.0' + version: 0.178.0 '@webgpu/types': specifier: ^0.1.63 version: 0.1.63 @@ -142,6 +145,9 @@ importers: '@typegpu/noise': specifier: workspace:* version: link:../../packages/typegpu-noise + '@typegpu/three': + specifier: workspace:* + version: link:../../packages/typegpu-three '@types/dom-mediacapture-transform': specifier: ^0.1.9 version: 0.1.10 @@ -205,6 +211,9 @@ importers: starlight-typedoc: specifier: ^0.19.0 version: 0.19.0(@astrojs/starlight@0.34.3(astro@5.9.3(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.29.2)(rollup@4.34.8)(tsx@4.19.3)(typescript@5.8.3)(yaml@2.8.0)))(typedoc-plugin-markdown@4.4.2(typedoc@0.27.9(typescript@5.8.3)))(typedoc@0.27.9(typescript@5.8.3)) + three: + specifier: ^0.178.0 + version: 0.178.0 tinybench: specifier: ^3.1.0 version: 3.1.1 @@ -242,6 +251,9 @@ importers: '@types/node': specifier: ^24.0.3 version: 24.0.3 + '@types/three': + specifier: catalog:types + version: 0.178.0 '@webgpu/types': specifier: catalog:types version: 0.1.63 @@ -472,6 +484,35 @@ importers: version: link:../unplugin-typegpu publishDirectory: dist + packages/typegpu-three: + dependencies: + three: + specifier: '>0.126.0' + version: 0.178.0 + devDependencies: + '@typegpu/tgpu-dev-cli': + specifier: workspace:* + version: link:../tgpu-dev-cli + '@types/three': + specifier: catalog:types + version: 0.178.0 + '@webgpu/types': + specifier: catalog:types + version: 0.1.63 + typegpu: + specifier: workspace:* + version: link:../typegpu + typescript: + specifier: catalog:types + version: 5.8.3 + unbuild: + specifier: catalog:build + version: 3.5.0(typescript@5.8.3) + unplugin-typegpu: + specifier: workspace:* + version: link:../unplugin-typegpu + publishDirectory: dist + packages/unplugin-typegpu: dependencies: '@babel/standalone': @@ -852,6 +893,9 @@ packages: resolution: {integrity: sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==} engines: {node: '>=14'} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@emmetio/abbreviation@2.3.3': resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} @@ -2394,6 +2438,9 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} @@ -2495,9 +2542,15 @@ packages: '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/three@0.178.0': + resolution: {integrity: sha512-1IpVbMKbEAAWjyn0VTdVcNvI1h1NlTv3CcnwMr3NNBv/gi3PL0/EsWROnXUEkXBxl94MH5bZvS8h0WnBRmR/pQ==} + '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} @@ -2507,6 +2560,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/webxr@0.5.22': + resolution: {integrity: sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==} + '@typescript/analyze-trace@0.10.1': resolution: {integrity: sha512-RnlSOPh14QbopGCApgkSx5UBgGda5MX1cHqp2fsqfiDyCwGL/m1jaeB9fzu7didVS81LQqGZZuxFBcg8YU8EVw==} hasBin: true @@ -3422,6 +3478,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -4108,6 +4167,9 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + meshoptimizer@0.18.1: + resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -5240,6 +5302,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + three@0.178.0: + resolution: {integrity: sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==} + through2@4.0.2: resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} @@ -5402,9 +5467,6 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - ufo@1.5.4: - resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} - ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} @@ -6430,6 +6492,8 @@ snapshots: '@ctrl/tinycolor@4.1.0': {} + '@dimforge/rapier3d-compat@0.12.0': {} + '@emmetio/abbreviation@2.3.3': dependencies: '@emmetio/scanner': 1.0.4 @@ -7676,6 +7740,8 @@ snapshots: '@trysound/sax@0.2.0': {} + '@tweenjs/tween.js@23.1.3': {} + '@types/acorn@4.0.6': dependencies: '@types/estree': 1.0.8 @@ -7789,9 +7855,21 @@ snapshots: dependencies: '@types/node': 24.0.3 + '@types/stats.js@0.17.4': {} + '@types/statuses@2.0.6': optional: true + '@types/three@0.178.0': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.22 + '@webgpu/types': 0.1.63 + fflate: 0.8.2 + meshoptimizer: 0.18.1 + '@types/tough-cookie@4.0.5': optional: true @@ -7799,6 +7877,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/webxr@0.5.22': {} + '@typescript/analyze-trace@0.10.1': dependencies: chalk: 4.1.2 @@ -8970,6 +9050,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fflate@0.8.2: {} + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -9812,6 +9894,8 @@ snapshots: merge2@1.4.1: {} + meshoptimizer@0.18.1: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.1.0 @@ -10136,7 +10220,7 @@ snapshots: acorn: 8.15.0 pathe: 2.0.3 pkg-types: 1.3.1 - ufo: 1.5.4 + ufo: 1.6.1 monaco-editor@0.52.2: {} @@ -11236,6 +11320,8 @@ snapshots: dependencies: any-promise: 1.3.0 + three@0.178.0: {} + through2@4.0.2: dependencies: readable-stream: 3.6.2 @@ -11379,8 +11465,6 @@ snapshots: uc.micro@2.1.0: {} - ufo@1.5.4: {} - ufo@1.6.1: {} ultrahtml@1.6.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0a4608727..027b0b84f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -13,6 +13,7 @@ catalogs: types: typescript: ^5.8.2 '@webgpu/types': ^0.1.63 + '@types/three': '>0.126.0' test: vitest: ^3.2.4 frontend: