|
| 1 | +import * as d from 'typegpu/data'; |
| 2 | +import tgpu from 'typegpu'; |
| 3 | +import { useRoot } from './root-context.tsx'; |
| 4 | +import { useMemo, useRef } from 'react'; |
| 5 | +import { useFrame } from './use-frame.ts'; |
| 6 | + |
| 7 | +type InferRecord<T> = { |
| 8 | + [K in keyof T]: d.Infer<T[K]>; |
| 9 | +}; |
| 10 | + |
| 11 | +export interface UseRenderOptions { |
| 12 | + vertex?: () => void; |
| 13 | + |
| 14 | + /** |
| 15 | + * A kernel function that runs per-pixel on the GPU. |
| 16 | + */ |
| 17 | + fragment: (input: InferRecord<typeof DefaultVarying>) => d.v4f; |
| 18 | +} |
| 19 | + |
| 20 | +const DefaultVarying = { |
| 21 | + uv: d.vec2f, |
| 22 | +}; |
| 23 | + |
| 24 | +const fullScreenTriangle = tgpu['~unstable'].vertexFn({ |
| 25 | + in: { vertexIndex: d.builtin.vertexIndex }, |
| 26 | + out: { pos: d.builtin.position, ...DefaultVarying }, |
| 27 | +})((input) => { |
| 28 | + const pos = [d.vec2f(-1, -1), d.vec2f(3, -1), d.vec2f(-1, 3)]; |
| 29 | + const uv = [d.vec2f(0, 1), d.vec2f(2, 1), d.vec2f(0, -1)]; |
| 30 | + |
| 31 | + return { |
| 32 | + pos: d.vec4f(pos[input.vertexIndex] as d.v2f, 0, 1), |
| 33 | + uv: uv[input.vertexIndex] as d.v2f, |
| 34 | + }; |
| 35 | +}); |
| 36 | + |
| 37 | +const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); |
| 38 | + |
| 39 | +export function useRender(options: UseRenderOptions) { |
| 40 | + const ref = useRef<HTMLCanvasElement>(null); |
| 41 | + const ctxRef = useRef<GPUCanvasContext>(null); |
| 42 | + const root = useRoot(); |
| 43 | + |
| 44 | + // Only considering the first passed-in fragment function. |
| 45 | + // This assumes that users won't swap shaders in the same useRender call, |
| 46 | + // but we can make this more robust by computing a hash with unplugin-typegpu. |
| 47 | + // TODO: You can also use the React Nook trick to track functions based on their |
| 48 | + // place in the code. Simpler and more reliable? ((x)=>x)`` |
| 49 | + const fragmentRef = useRef(options.fragment); |
| 50 | + |
| 51 | + const fragmentFn = useMemo(() => { |
| 52 | + return tgpu['~unstable'].fragmentFn({ |
| 53 | + in: { ...DefaultVarying }, |
| 54 | + out: d.vec4f, |
| 55 | + })(fragmentRef.current); |
| 56 | + }, []); |
| 57 | + |
| 58 | + const pipeline = useMemo(() => { |
| 59 | + return root['~unstable'] |
| 60 | + .withVertex(fullScreenTriangle, {}) |
| 61 | + .withFragment(fragmentFn, { format: presentationFormat }) |
| 62 | + .createPipeline(); |
| 63 | + }, [root, fragmentFn]); |
| 64 | + |
| 65 | + useFrame(() => { |
| 66 | + const canvas = ref.current; |
| 67 | + if (!canvas) return; |
| 68 | + if (ctxRef.current === null) { |
| 69 | + ctxRef.current = canvas.getContext('webgpu') as GPUCanvasContext; |
| 70 | + ctxRef.current.configure({ |
| 71 | + device: root.device, |
| 72 | + format: presentationFormat, |
| 73 | + }); |
| 74 | + } |
| 75 | + |
| 76 | + pipeline |
| 77 | + .withColorAttachment({ |
| 78 | + view: ctxRef.current.getCurrentTexture().createView(), |
| 79 | + loadOp: 'load', |
| 80 | + storeOp: 'store', |
| 81 | + }) |
| 82 | + .draw(3); |
| 83 | + }); |
| 84 | + |
| 85 | + return { ref }; |
| 86 | +} |
0 commit comments