Skip to content
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/src/examples/react/monkey/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="example-app"></div>
86 changes: 86 additions & 0 deletions apps/typegpu-docs/src/examples/react/monkey/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Canvas, Pass } from '@typegpu/react';
import tgpu from 'typegpu';
import * as d from 'typegpu/data';

const meshLayout = tgpu.bindGroupLayout({
modelMatrix: { uniform: d.mat4x4f },
albedo: { uniform: d.vec3f },
tint: { uniform: d.vec3f },
});

const vertex = ({ pos }: { pos: d.v3f }) => {
'use gpu';
return meshLayout.$.modelMatrix.mul(d.vec4f(pos, 1));
};

const fragment = () => {
'use gpu';
return d.vec4f(meshLayout.$.tint, 1);
};

export function Monkey({ albedo, pos }: { albedo: d.v3f; pos: d.v3f }) {
// const monkeyMesh = useMonkeyMesh();
// const modelMatrix = useMemo(() => mat4.translation(pos, d.mat4x4f()), []);

// Optional bindings
// const bindings = useMemo(() => ([
// [fooSlot, 123],
// [(cfg: Configurable) => /* ... */],
// // ...
// ]), []);

return (
// <Config bindings={bindings}>
// <RenderPipeline
// vertex={vertex}
// fragment={fragment}
// attributes={monkeyMesh.layout.attrib}
// >
// {/* things provided after the pipeline is created */}

// {/* the entries are passed into the shader as automatically created resources */}
// <BindGroup layout={meshLayout} entries={{ modelMatrix, albedo }} />
// {/* monkeyMesh has 'layout' and 'buffer' properties, which fit this component */}
// <VertexBuffer {...monkeyMesh} />
// </RenderPipeline>
// </Config>
<div
style={{
width: '200px',
height: '200px',
backgroundColor: `rgb(${albedo.x * 255}, ${albedo.y * 255}, ${
albedo.z * 255
})`,
}}
>
Monkey at {`(${pos.x}, ${pos.y}, ${pos.z})`}
</div>
);
}

export function App() {
return (
<Canvas>
<Pass schedule='frame'>
<Monkey pos={d.vec3f(0, 0, 0)} albedo={d.vec3f(1, 0, 0)} />
<Monkey pos={d.vec3f(1, 0, 0)} albedo={d.vec3f(0, 0, 1)} />
{/* ... */}
</Pass>
</Canvas>
);
}

// #region Example controls and cleanup

import { createRoot } from 'react-dom/client';

const reactRoot = createRoot(
document.getElementById('example-app') as HTMLDivElement,
);
reactRoot.render(<App />);

export function onCleanup() {
setTimeout(() => reactRoot.unmount(), 0);
}

// #endregion
5 changes: 5 additions & 0 deletions apps/typegpu-docs/src/examples/react/monkey/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "React: 3D Monkey",
"category": "react",
"tags": ["experimental"]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 65 additions & 0 deletions apps/typegpu-docs/src/examples/react/monkey/use-monkey-mesh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { load } from '@loaders.gl/core';
import { OBJLoader } from '@loaders.gl/obj';
import * as d from 'typegpu/data';
import tgpu from 'typegpu';
import { useRoot, type VertexBufferProps } from '@typegpu/react';

const MONKEY_MODEL_PATH = '/TypeGPU/assets/3d-monkey/monkey.obj';

const MeshVertexInput = {
modelPosition: d.vec3f,
modelNormal: d.vec3f,
textureUV: d.vec2f,
} as const;

const meshVertexLayout = tgpu.vertexLayout((n: number) =>
d.arrayOf(d.struct(MeshVertexInput), n)
);

export type MeshVertex = d.WgslArray<
d.WgslStruct<{
readonly modelPosition: d.Vec3f;
readonly modelNormal: d.Vec3f;
readonly textureUV: d.Vec2f;
}>
>;

export async function useMonkeyMesh(): Promise<VertexBufferProps<MeshVertex>> {
const root = useRoot();

const modelMesh = await load(MONKEY_MODEL_PATH, OBJLoader);
const polygonCount = modelMesh.attributes.POSITION.value.length / 3;

const vertexBuffer = root
.createBuffer(meshVertexLayout.schemaForCount(polygonCount))
.$usage('vertex')
.$name(`model vertices of ${MONKEY_MODEL_PATH}`);

const modelVertices = [];
for (let i = 0; i < polygonCount; i++) {
modelVertices.push({
modelPosition: d.vec3f(
modelMesh.attributes.POSITION.value[3 * i],
modelMesh.attributes.POSITION.value[3 * i + 1],
modelMesh.attributes.POSITION.value[3 * i + 2],
),
modelNormal: d.vec3f(
modelMesh.attributes.NORMAL.value[3 * i],
modelMesh.attributes.NORMAL.value[3 * i + 1],
modelMesh.attributes.NORMAL.value[3 * i + 2],
),
textureUV: d.vec2f(
modelMesh.attributes.TEXCOORD_0.value[2 * i],
1 - modelMesh.attributes.TEXCOORD_0.value[2 * i + 1],
),
});
}
modelVertices.reverse();

vertexBuffer.write(modelVertices);

return {
layout: meshVertexLayout,
buffer: vertexBuffer,
};
}
47 changes: 47 additions & 0 deletions packages/typegpu-react/src/components/BindGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useEffect, useMemo } from 'react';
import type { TgpuBindGroupLayout } from 'typegpu';
import type { AnyData } from 'typegpu/data';
import { useCanvas } from '../hooks/use-canvas.ts';
import { usePass } from '../hooks/use-pass.ts';

type Entries<T extends Record<string, AnyData>> = {
[K in keyof T]: any; // Using 'any' to match the expected value types for buffers, textures, etc.
};

interface BindGroupProps<T extends Record<string, AnyData>> {
/**
* The layout for the bind group.
*/
layout: TgpuBindGroupLayout<T>;
/**
* An object containing the resources to be bound.
* The keys must match the keys in the layout definition.
*/
entries: Entries<T>;
}

export function BindGroup<T extends Record<string, AnyData>>({
layout,
entries,
}: BindGroupProps<T>) {
const { root } = useCanvas();
const { addDrawCall } = usePass();

const bindGroup = useMemo(() => {
// It's important that the values in `entries` are stable (e.g., memoized)
// to avoid recreating the bind group on every render.
return root.createBindGroup(layout, entries);
}, [root, layout, entries]);

useEffect(() => {
const removeDrawCall = addDrawCall((pass) => {
// The group index is derived from the layout object itself.
pass.setBindGroup(layout.groupIndex, bindGroup);
});

return removeDrawCall;
}, [addDrawCall, layout.groupIndex, bindGroup]);

// This component does not render anything to the DOM.
return null;
}
69 changes: 69 additions & 0 deletions packages/typegpu-react/src/components/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type React from 'react';
import { useEffect, useRef, useState } from 'react';
import {
CanvasContext,
type CanvasContextValue,
} from '../context/canvas-context.ts';
import { useRoot } from '../hooks/use-root.ts';

export function Canvas({ children }: { children: React.ReactNode }) {
const root = useRoot();
const canvasRef = useRef<HTMLCanvasElement>(null);
const canvasCtxRef = useRef<GPUCanvasContext>(null);

const frameCallbacksRef = useRef(new Set<(time: number) => void>());

const [contextValue] = useState<CanvasContextValue>(() => ({
get context() {
return canvasCtxRef.current;
},
addFrameCallback(cb: (time: number) => void) {
frameCallbacksRef.current.add(cb);
return () => frameCallbacksRef.current.delete(cb);
},
}));

useEffect(() => {
if (!canvasRef.current) return;

let disposed = false;
const canvas = canvasRef.current;
const context = canvas.getContext('webgpu');
if (!context) {
console.error('WebGPU not supported');
return;
}

const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: root.device,
format: presentationFormat,
alphaMode: 'premultiplied',
});
canvasCtxRef.current = context;

const frame = (time: number) => {
if (disposed) return;
requestAnimationFrame(frame);

frameCallbacksRef.current.forEach((cb) => {
cb(time);
});

root['~unstable'].flush();
};
requestAnimationFrame(frame);

return () => {
disposed = true;
};
}, [root]);

return (
<canvas ref={canvasRef} style={{ width: '100%', height: '100%' }}>
<CanvasContext.Provider value={contextValue}>
{children}
</CanvasContext.Provider>
</canvas>
);
}
39 changes: 39 additions & 0 deletions packages/typegpu-react/src/components/Config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type React from 'react';
import { useMemo } from 'react';
import type { Configurable, TgpuRenderPipeline, TgpuSlot } from 'typegpu';
import { PipelineContext } from '../context/pipeline-context.ts';
import { useRenderPipeline } from '../hooks/use-render-pipeline.ts';

type Binding =
| [slot: TgpuSlot<any>, value: any]
| ((cfg: Configurable) => Configurable);

interface ConfigProps {
bindings: Binding[];
children: React.ReactNode;
}

export function Config({ bindings, children }: ConfigProps) {
const pipeline = useRenderPipeline();

const configuredPipeline = useMemo(() => {
if (!pipeline) return null;
return bindings.reduce((p, binding) => {
if (Array.isArray(binding)) {
return p.with(binding[0], binding[1]);
}
return p.pipe(binding);
}, pipeline as Configurable) as TgpuRenderPipeline;
}, [pipeline, bindings]);

if (!pipeline) {
return <>{children}</>;
}

// This re-provides the configured pipeline to children
return (
<PipelineContext.Provider value={configuredPipeline}>
{children}
</PipelineContext.Provider>
);
}
Loading
Loading