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: