diff --git a/apps/typegpu-docs/src/components/ExampleView.tsx b/apps/typegpu-docs/src/components/ExampleView.tsx index 6c1264169a..bbc1843a06 100644 --- a/apps/typegpu-docs/src/components/ExampleView.tsx +++ b/apps/typegpu-docs/src/components/ExampleView.tsx @@ -1,14 +1,6 @@ import cs from 'classnames'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; -import { - type RefObject, - Suspense, - useEffect, - useLayoutEffect, - useRef, - useState, - lazy, -} from 'react'; +import { Suspense, useEffect, useLayoutEffect, useRef, useState, lazy } from 'react'; import { currentSnackbarAtom } from '../utils/examples/currentSnackbarAtom.ts'; import { tsoverUsedAtom, exampleFullscreenAtom } from '../utils/examples/exampleViewStateAtoms.ts'; import { ExecutionCancelledError } from '../utils/examples/errors.ts'; @@ -131,7 +123,6 @@ export function ExampleView({ example, common }: Props) { }, [fullscreen, setFullscreen]); const isLoading = useExample(tsImport, setSnackbarText); - useResizableCanvas(exampleHtmlRef); return ( <> @@ -348,85 +339,6 @@ function GPUUnsupportedPanel() { ); } -function useResizableCanvas(exampleHtmlRef: RefObject) { - useEffect(() => { - const canvases = exampleHtmlRef.current?.querySelectorAll('canvas') as - | HTMLCanvasElement[] - | undefined; - const observers: ResizeObserver[] = []; - - for (const canvas of canvases ?? []) { - if ('width' in canvas.attributes || 'height' in canvas.attributes) { - continue; - } - - const newCanvas = document.createElement('canvas'); - const container = document.createElement('div'); - const frame = document.createElement('div'); - - frame.appendChild(newCanvas); - container.appendChild(frame); - - container.className = 'flex flex-1 justify-center items-center w-full h-full md:w-auto'; - container.style.containerType = 'size'; - - frame.className = 'relative'; - - if (canvas.dataset.fitToContainer !== undefined) { - frame.style.width = '100%'; - frame.style.height = '100%'; - } else { - const aspectRatio = canvas.dataset.aspectRatio ?? '1'; - frame.style.aspectRatio = aspectRatio; - frame.style.height = `min(100cqh, calc(100cqw/(${aspectRatio})))`; - } - - for (const prop of canvas.style) { - // @ts-expect-error - newCanvas.style[prop] = canvas.style[prop]; - } - for (const attribute of canvas.attributes) { - // @ts-expect-error - newCanvas[attribute.name] = attribute.value; - } - newCanvas.className = 'absolute w-full h-full'; - - canvas.parentElement?.replaceChild(container, canvas); - - const onResize: ResizeObserverCallback = ([entry]) => { - if (!entry) { - return; - } - - // Despite what the types say this property does not exist in Safari (hence the optional chaining). - const dpcb = entry.devicePixelContentBoxSize?.[0] as ResizeObserverSize | undefined; - - const dpr = dpcb ? 1 : window.devicePixelRatio || 1; - const box = - dpcb ?? - (Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize); - - if (!box) { - return; - } - - newCanvas.width = Math.round(box.inlineSize * dpr); - newCanvas.height = Math.round(box.blockSize * dpr); - }; - - const observer = new ResizeObserver(onResize); - observer.observe(newCanvas); - observers.push(observer); - } - - return () => { - for (const observer of observers) { - observer.disconnect(); - } - }; - }, [exampleHtmlRef]); -} - /** * NOTE: this function only filters common files used in src files. * Common files used in other common files will not be included. diff --git a/apps/typegpu-docs/src/components/landing-examples/HoverExampleLive.tsx b/apps/typegpu-docs/src/components/landing-examples/HoverExampleLive.tsx index bc39d9887d..328b86e776 100644 --- a/apps/typegpu-docs/src/components/landing-examples/HoverExampleLive.tsx +++ b/apps/typegpu-docs/src/components/landing-examples/HoverExampleLive.tsx @@ -1,8 +1,9 @@ import type { TgpuRoot } from 'typegpu'; import { useConfigureContext, useRoot } from '@typegpu/react'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; interface ExampleState { + onResize(): void; onCleanup(): void; } @@ -12,8 +13,12 @@ interface HoverExampleLiveProps { export default function HoverExampleLive({ setup }: HoverExampleLiveProps) { const root = useRoot(); + const exampleRef = useRef(null); const { ctxRef, ref: canvasRef } = useConfigureContext({ alphaMode: 'premultiplied', + onResize() { + exampleRef.current?.onResize(); + }, }); const [isActive, setIsActive] = useState(false); const [error, setError] = useState(null); @@ -40,13 +45,19 @@ export default function HoverExampleLive({ setup }: HoverExampleLiveProps) { try { const example = await setup(root, ctxRef.current); - onCleanup = () => example.onCleanup(); - if (cancelled) { - onCleanup(); + example.onCleanup(); return; } + exampleRef.current = example; + onCleanup = () => { + exampleRef.current = null; + example.onCleanup(); + }; + // The initial resize notification can arrive before async setup finishes. + example.onResize(); + setIsActive(true); } catch (err) { // Error boundaries can't catch throws from async effect callbacks, so diff --git a/apps/typegpu-docs/src/components/landing-examples/JellySlider.tsx b/apps/typegpu-docs/src/components/landing-examples/JellySlider.tsx index 2a96e4e879..0afadc8c2d 100644 --- a/apps/typegpu-docs/src/components/landing-examples/JellySlider.tsx +++ b/apps/typegpu-docs/src/components/landing-examples/JellySlider.tsx @@ -6,7 +6,10 @@ async function setup(root: TgpuRoot, context: GPUCanvasContext) { const scene = await setupScene(root, context); scene.qualityScale = await scene.computeOptimalQuality(); - return { onCleanup: () => scene.onCleanup() }; + return { + onResize: () => scene.onResize(), + onCleanup: () => scene.onCleanup(), + }; } export default function JellySlider() { diff --git a/apps/typegpu-docs/src/components/stackblitz/stackBlitzIndex.ts b/apps/typegpu-docs/src/components/stackblitz/stackBlitzIndex.ts index c707a4371a..b5abd8329e 100644 --- a/apps/typegpu-docs/src/components/stackblitz/stackBlitzIndex.ts +++ b/apps/typegpu-docs/src/components/stackblitz/stackBlitzIndex.ts @@ -10,52 +10,6 @@ body.style.margin = '0'; body.style.boxSizing = 'border-box'; body.style.padding = '1rem'; -// Resize canvases -for (const canvas of document.querySelectorAll('canvas')) { - if ('width' in canvas.attributes || 'height' in canvas.attributes) { - continue; // custom canvas, not replacing with resizable - } - - const container = document.createElement('div'); - const frame = document.createElement('div'); - - canvas.parentElement?.replaceChild(container, canvas); - - frame.appendChild(canvas); - container.appendChild(frame); - - container.style.display = 'flex'; - container.style.flex = '1'; - container.style.justifyContent = 'center'; - container.style.alignItems = 'top'; - container.style.width = '100%'; - - container.style.containerType = 'size'; - - frame.style.position = 'relative'; - - if (canvas.dataset.fitToContainer !== undefined) { - frame.style.width = '100%'; - frame.style.height = '100%'; - } else { - const aspectRatio = canvas.dataset.aspectRatio ?? '1'; - frame.style.aspectRatio = aspectRatio; - frame.style.height = `min(calc(min(100cqw, 100cqh)/(${aspectRatio})), min(100cqw, 100cqh))`; - } - - canvas.style.position = 'absolute'; - canvas.style.width = '100%'; - canvas.style.height = '100%'; - - const onResize = () => { - canvas.width = frame.clientWidth * window.devicePixelRatio; - canvas.height = frame.clientHeight * window.devicePixelRatio; - }; - - onResize(); - new ResizeObserver(onResize).observe(container); -} - const controlsPanel = document.createElement('div'); controlsPanel.style.display = 'grid'; controlsPanel.style.gridTemplateColumns = '1fr 1fr'; diff --git a/apps/typegpu-docs/src/examples/advanced/appjs-venue/index.html b/apps/typegpu-docs/src/examples/advanced/appjs-venue/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/advanced/appjs-venue/index.html +++ b/apps/typegpu-docs/src/examples/advanced/appjs-venue/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/advanced/appjs-venue/index.ts b/apps/typegpu-docs/src/examples/advanced/appjs-venue/index.ts index 0b71f0a029..f2324549ff 100644 --- a/apps/typegpu-docs/src/examples/advanced/appjs-venue/index.ts +++ b/apps/typegpu-docs/src/examples/advanced/appjs-venue/index.ts @@ -399,8 +399,6 @@ function createRenderTargets() { .$usage('render'); return { - width: canvas.width, - height: canvas.height, normal, depth, colorAttachments: [ @@ -452,14 +450,6 @@ function frame(timestamp: number) { const dt = Math.min((timestamp - (lastTimestamp ?? timestamp)) * 0.001, 1 / 30); lastTimestamp = timestamp; - if (canvas.width === 0 || canvas.height === 0) { - return; - } - if (canvas.width !== targets.width || canvas.height !== targets.height) { - targets.destroy(); - targets = createRenderTargets(); - } - controls.update(dt); outsideOpacity.target = controls.focus.z > 0.5 ? 1 : 0; outsideOpacity.step(dt); @@ -469,6 +459,14 @@ function frame(timestamp: number) { outsideOpacityUniform.write(std.saturate(outsideOpacity.value)); fadeInUniform.write(std.saturate(controls.in3dTransition * 2 - 1)); + render(); +} + +function render() { + if (canvas.width === 0 || canvas.height === 0) { + return; + } + const encoder = root['~unstable'].createCommandEncoder(); const pass = encoder.beginRenderPass({ colorAttachments: targets.colorAttachments, @@ -506,8 +504,19 @@ function frame(timestamp: number) { frameId = requestAnimationFrame(frame); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + targets.destroy(); + targets = createRenderTargets(); + render(); + }, +}); + export function onCleanup() { abortController.abort(); cancelAnimationFrame(frameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/algorithms/genetic-racing/index.html b/apps/typegpu-docs/src/examples/algorithms/genetic-racing/index.html index c732f96f62..da0a446e19 100644 --- a/apps/typegpu-docs/src/examples/algorithms/genetic-racing/index.html +++ b/apps/typegpu-docs/src/examples/algorithms/genetic-racing/index.html @@ -1,4 +1,4 @@ - +
+ + + diff --git a/apps/typegpu-docs/src/examples/image-processing/selfie-segmentation/index.ts b/apps/typegpu-docs/src/examples/image-processing/selfie-segmentation/index.ts index e241bdbc08..7239d5f045 100644 --- a/apps/typegpu-docs/src/examples/image-processing/selfie-segmentation/index.ts +++ b/apps/typegpu-docs/src/examples/image-processing/selfie-segmentation/index.ts @@ -18,9 +18,19 @@ import { const root = await tgpu.init(); const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = root.configureContext({ canvas }); - const video = document.querySelector('video') as HTMLVideoElement; +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + }, +}); + const sampler = root.createSampler({ magFilter: 'linear', minFilter: 'linear', @@ -196,6 +206,7 @@ export const controls = defineControls({ export function onCleanup() { video.cancelVideoFrameCallback(videoFrameCallbackId); + autoResizer.detach(); if (isIOS) { window.removeEventListener('orientationchange', setUVTransformForIOS); } diff --git a/apps/typegpu-docs/src/examples/react/shifting-gradient/index.tsx b/apps/typegpu-docs/src/examples/react/shifting-gradient/index.tsx index c44b5aa063..29ccc222af 100644 --- a/apps/typegpu-docs/src/examples/react/shifting-gradient/index.tsx +++ b/apps/typegpu-docs/src/examples/react/shifting-gradient/index.tsx @@ -37,7 +37,7 @@ function App() { renderPipeline.withColorAttachment({ view: ctxRef.current }).draw(3); }); - return ; + return ; } // #region Example controls and cleanup diff --git a/apps/typegpu-docs/src/examples/react/spinning-triangle/index.tsx b/apps/typegpu-docs/src/examples/react/spinning-triangle/index.tsx index cbdfa25e7b..3d70bd916e 100644 --- a/apps/typegpu-docs/src/examples/react/spinning-triangle/index.tsx +++ b/apps/typegpu-docs/src/examples/react/spinning-triangle/index.tsx @@ -63,7 +63,7 @@ function App() { renderPipeline.withColorAttachment({ view: ctxRef.current }).draw(3); }); - return ; + return ; } // #region Example controls and cleanup diff --git a/apps/typegpu-docs/src/examples/rendering/3d-fish/index.html b/apps/typegpu-docs/src/examples/rendering/3d-fish/index.html index 4dcce7ff04..71d079f678 100644 --- a/apps/typegpu-docs/src/examples/rendering/3d-fish/index.html +++ b/apps/typegpu-docs/src/examples/rendering/3d-fish/index.html @@ -44,4 +44,4 @@

Attribution (click to dismiss)

from original

- + diff --git a/apps/typegpu-docs/src/examples/rendering/3d-fish/index.ts b/apps/typegpu-docs/src/examples/rendering/3d-fish/index.ts index f36d4b038f..5406dd5955 100644 --- a/apps/typegpu-docs/src/examples/rendering/3d-fish/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/3d-fish/index.ts @@ -1,4 +1,4 @@ -import { tgpu } from 'typegpu'; +import { tgpu, common } from 'typegpu'; import { defineControls } from '../../common/defineControls.ts'; import { setupScene } from './scene.ts'; @@ -8,6 +8,14 @@ const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = root.configureContext({ canvas, alphaMode: 'premultiplied' }); const scene = await setupScene(root, context); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + scene.onResize(); + }, +}); + // #region Example controls and cleanup export const controls = defineControls({ @@ -18,6 +26,7 @@ export const controls = defineControls({ export function onCleanup() { scene.onCleanup(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/3d-fish/scene.ts b/apps/typegpu-docs/src/examples/rendering/3d-fish/scene.ts index 3a3682ffcf..141abfdfaa 100644 --- a/apps/typegpu-docs/src/examples/rendering/3d-fish/scene.ts +++ b/apps/typegpu-docs/src/examples/rendering/3d-fish/scene.ts @@ -131,7 +131,7 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { ), projection: m.mat4.perspective( Math.PI / 4, - canvas.clientWidth / canvas.clientHeight, + canvas.width / canvas.height, 0.1, 1000, d.mat4x4f(), @@ -222,22 +222,9 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { }), ); - // frame - let odd = false; - let lastTimestamp: DOMHighResTimeStamp = 0; - let animationFrameId: number; - - function frame(timestamp: DOMHighResTimeStamp) { - odd = !odd; - - currentTimeBuffer.write(timestamp); - timePassedBuffer.write((timestamp - lastTimestamp) * speedMultiplier); - lastTimestamp = timestamp; - cameraBuffer.write(camera); - - simulatePipeline.with(computeBindGroups[odd ? 1 : 0]).dispatchThreads(p.fishAmount); + function render() { renderPipeline .withColorAttachment({ view: context, @@ -270,6 +257,24 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { .with(renderInstanceLayout, fishDataBuffers[odd ? 1 : 0]) .with(renderFishBindGroups[odd ? 1 : 0]) .draw(fishModel.polygonCount, p.fishAmount); + } + + // frame + + let lastTimestamp: DOMHighResTimeStamp = 0; + let animationFrameId: number; + + function frame(timestamp: DOMHighResTimeStamp) { + odd = !odd; + + currentTimeBuffer.write(timestamp); + timePassedBuffer.write((timestamp - lastTimestamp) * speedMultiplier); + lastTimestamp = timestamp; + cameraBuffer.write(camera); + + simulatePipeline.with(computeBindGroups[odd ? 1 : 0]).dispatchThreads(p.fishAmount); + + render(); animationFrameId = requestAnimationFrame(frame); } @@ -418,35 +423,35 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { }; window.addEventListener('touchmove', touchMoveEventListener); - // observer and cleanup - - const resizeObserver = new ResizeObserver(() => { - camera.projection = m.mat4.perspective( - Math.PI / 4, - canvas.clientWidth / canvas.clientHeight, - 0.1, - 1000, - d.mat4x4f(), - ); - - depthTexture.destroy(); - depthTexture = root.device.createTexture({ - size: [canvas.width, canvas.height, 1], - format: 'depth24plus', - usage: GPUTextureUsage.RENDER_ATTACHMENT, - }); - }); - resizeObserver.observe(canvas); + // resizing and cleanup return { randomizeFishPositions, + onResize() { + camera.projection = m.mat4.perspective( + Math.PI / 4, + canvas.width / canvas.height, + 0.1, + 1000, + d.mat4x4f(), + ); + cameraBuffer.patch({ projection: camera.projection }); + + depthTexture.destroy(); + depthTexture = root.device.createTexture({ + size: [canvas.width, canvas.height, 1], + format: 'depth24plus', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + + render(); + }, onCleanup() { disposed = true; cancelAnimationFrame(animationFrameId); window.removeEventListener('mouseup', mouseUpEventListener); window.removeEventListener('mousemove', mouseMoveEventListener); window.removeEventListener('touchmove', touchMoveEventListener); - resizeObserver.disconnect(); }, }; } diff --git a/apps/typegpu-docs/src/examples/rendering/area-light/index.html b/apps/typegpu-docs/src/examples/rendering/area-light/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/rendering/area-light/index.html +++ b/apps/typegpu-docs/src/examples/rendering/area-light/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/area-light/index.ts b/apps/typegpu-docs/src/examples/rendering/area-light/index.ts index 9c0b4aeb7e..900f2db755 100644 --- a/apps/typegpu-docs/src/examples/rendering/area-light/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/area-light/index.ts @@ -1,4 +1,4 @@ -import { d, std, tgpu } from 'typegpu'; +import { common, d, std, tgpu } from 'typegpu'; import { setupOrbitCamera } from '../../common/setup-orbit-camera.ts'; import { defineControls } from '../../common/defineControls.ts'; import { BAKE_WORKGROUP_SIZE, SCENE_VERTICES, bakeScene } from './geometry.ts'; @@ -151,12 +151,6 @@ const { cleanupCamera } = setupOrbitCamera( (state) => cameraUniform.write(state), ); -const resizeObserver = new ResizeObserver(() => { - destroyRenderTargets(targets); - targets = createRenderTargets(); -}); -resizeObserver.observe(canvas); - let animationFrameId: number; function hueColor(hue: number) { @@ -213,11 +207,7 @@ function updateDisco(time: number) { } } -function render(time: number) { - updateDisco(time); - previousFrameTime = time; - paramsUniform.patch({ time: time * 0.001 }); - +function render() { const encoder = root['~unstable'].createCommandEncoder(); const pass = encoder.beginRenderPass({ colorAttachments: [ @@ -240,11 +230,29 @@ function render(time: number) { pass.end(); encoder.submit(); +} - animationFrameId = requestAnimationFrame(render); +function frame(time: number) { + updateDisco(time); + previousFrameTime = time; + paramsUniform.patch({ time: time * 0.001 }); + + render(); + + animationFrameId = requestAnimationFrame(frame); } -animationFrameId = requestAnimationFrame(render); +animationFrameId = requestAnimationFrame(frame); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + destroyRenderTargets(targets); + targets = createRenderTargets(); + render(); + }, +}); const LIGHTS = [ { name: 'key', maxIntensity: 12 }, @@ -297,7 +305,7 @@ export const controls = defineControls({ export function onCleanup() { cancelAnimationFrame(animationFrameId); cleanupCamera(); - resizeObserver.disconnect(); + autoResizer.detach(); destroyRenderTargets(targets); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/box-raytracing/index.html b/apps/typegpu-docs/src/examples/rendering/box-raytracing/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/rendering/box-raytracing/index.html +++ b/apps/typegpu-docs/src/examples/rendering/box-raytracing/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/box-raytracing/index.ts b/apps/typegpu-docs/src/examples/rendering/box-raytracing/index.ts index 463186b86c..7e3d4e7a90 100644 --- a/apps/typegpu-docs/src/examples/rendering/box-raytracing/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/box-raytracing/index.ts @@ -1,5 +1,5 @@ import { linearToSrgb, srgbToLinear } from '@typegpu/color'; -import { tgpu, d, type TgpuFragmentFn, type TgpuVertexFn } from 'typegpu'; +import { tgpu, common, d, type TgpuFragmentFn, type TgpuVertexFn } from 'typegpu'; import { discard, max, min, mul, normalize, pow, sub } from 'typegpu/std'; import { mat4 } from 'wgpu-matrix'; import { defineControls } from '../../common/defineControls.ts'; @@ -246,6 +246,10 @@ const pipeline = root.createRenderPipeline({ // UI +function render() { + pipeline.withColorAttachment({ view: context }).draw(3); +} + let animationFrameId: number; let lastTime: number | null = null; @@ -269,11 +273,23 @@ const runner = (timestamp: number) => { frame += (rotationSpeed * deltaTime) / 1000; - pipeline.withColorAttachment({ view: context }).draw(3); + render(); animationFrameId = requestAnimationFrame(runner); }; animationFrameId = requestAnimationFrame(runner); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); + // #region Example controls and cleanup export const controls = defineControls({ @@ -320,6 +336,7 @@ export const controls = defineControls({ export function onCleanup() { cancelAnimationFrame(animationFrameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.html b/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.html +++ b/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.ts b/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.ts index 03c1d65df8..4225610a0a 100644 --- a/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/caustics-gl/index.ts @@ -1,5 +1,5 @@ import { perlin3d } from '@typegpu/noise'; -import { tgpu, d, std } from 'typegpu'; +import { tgpu, common, d, std } from 'typegpu'; import { initWithGL } from '@typegpu/gl'; import { defineControls } from '../../common/defineControls.ts'; @@ -136,16 +136,31 @@ const pipeline = root.createRenderPipeline({ let isRunning = true; -function draw(timestamp: number) { +function render() { + pipeline.withColorAttachment({ view: context }).draw(3); +} + +function frame(timestamp: number) { if (!isRunning) return; time.write((timestamp * 0.001) % 1000); + render(); - pipeline.withColorAttachment({ view: context }).draw(3); - - requestAnimationFrame(draw); + requestAnimationFrame(frame); } -requestAnimationFrame(draw); +requestAnimationFrame(frame); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); // #region Example controls and cleanup @@ -163,6 +178,7 @@ export const controls = defineControls({ export function onCleanup() { isRunning = false; + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/caustics/index.html b/apps/typegpu-docs/src/examples/rendering/caustics/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/rendering/caustics/index.html +++ b/apps/typegpu-docs/src/examples/rendering/caustics/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/caustics/index.ts b/apps/typegpu-docs/src/examples/rendering/caustics/index.ts index 4b15920d44..0e0b297257 100644 --- a/apps/typegpu-docs/src/examples/rendering/caustics/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/caustics/index.ts @@ -1,4 +1,4 @@ -import { tgpu } from 'typegpu'; +import { tgpu, common } from 'typegpu'; import { defineControls } from '../../common/defineControls.ts'; import { setupScene } from './scene.ts'; @@ -8,6 +8,18 @@ const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = root.configureContext({ canvas, alphaMode: 'premultiplied' }); const scene = await setupScene(root, context); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + scene.onResize(); + }, +}); + // #region Example controls and cleanup export const controls = defineControls({ @@ -24,6 +36,7 @@ export const controls = defineControls({ export function onCleanup() { scene.onCleanup(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/caustics/scene.ts b/apps/typegpu-docs/src/examples/rendering/caustics/scene.ts index c6b5b46aab..c09f3ffe87 100644 --- a/apps/typegpu-docs/src/examples/rendering/caustics/scene.ts +++ b/apps/typegpu-docs/src/examples/rendering/caustics/scene.ts @@ -131,21 +131,27 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { let isRunning = true; - function draw(timestamp: number) { + function render() { + pipeline.withColorAttachment({ view: context }).draw(3); + } + + function frame(timestamp: number) { if (!isRunning) return; time.write((timestamp * 0.001) % 1000); + render(); - pipeline.withColorAttachment({ view: context }).draw(3); - - requestAnimationFrame(draw); + requestAnimationFrame(frame); } - requestAnimationFrame(draw); + requestAnimationFrame(frame); return { set tileDensity(density: number) { tileDensity.write(density); }, + onResize() { + render(); + }, onCleanup() { isRunning = false; }, diff --git a/apps/typegpu-docs/src/examples/rendering/clouds/index.html b/apps/typegpu-docs/src/examples/rendering/clouds/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/rendering/clouds/index.html +++ b/apps/typegpu-docs/src/examples/rendering/clouds/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/clouds/index.ts b/apps/typegpu-docs/src/examples/rendering/clouds/index.ts index 7ab45bd7a6..ba0dd14959 100644 --- a/apps/typegpu-docs/src/examples/rendering/clouds/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/clouds/index.ts @@ -182,26 +182,7 @@ function createCloudUpscaleBindGroup() { let cloudUpscaleBindGroup = createCloudUpscaleBindGroup(); -const resizeObserver = new ResizeObserver(() => { - resolutionUniform.write(d.vec2f(canvas.width, canvas.height)); - - const [width, height] = getCloudTargetSize(); - if (width === cloudTarget.width && height === cloudTarget.height) { - return; - } - - const previousCloudTarget = cloudTarget; - cloudTarget = createCloudTarget(width, height); - cloudUpscaleBindGroup = createCloudUpscaleBindGroup(); - previousCloudTarget.texture.destroy(); -}); -resizeObserver.observe(canvas); - -let frameId: number; - -function render(timestamp: number) { - paramsUniform.patch({ time: (timestamp / 1000) % 500 }); - +function render() { cloudPipeline .with(cloudsBindGroup) .withColorAttachment({ @@ -217,11 +198,35 @@ function render(timestamp: number) { clearValue: [0, 0, 0, 1], }) .draw(3); +} + +let frameId: number; - frameId = requestAnimationFrame(render); +function frame(timestamp: number) { + paramsUniform.patch({ time: (timestamp / 1000) % 500 }); + render(); + frameId = requestAnimationFrame(frame); } -frameId = requestAnimationFrame(render); +frameId = requestAnimationFrame(frame); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + resolutionUniform.write(d.vec2f(canvas.width, canvas.height)); + + const [width, height] = getCloudTargetSize(); + if (width !== cloudTarget.width || height !== cloudTarget.height) { + const previousCloudTarget = cloudTarget; + cloudTarget = createCloudTarget(width, height); + cloudUpscaleBindGroup = createCloudUpscaleBindGroup(); + previousCloudTarget.texture.destroy(); + } + + render(); + }, +}); const qualityOptions = { 'very high': { @@ -258,7 +263,7 @@ export const controls = defineControls({ export function onCleanup() { cancelAnimationFrame(frameId); - resizeObserver.disconnect(); + autoResizer.detach(); cloudTarget.texture.destroy(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.html b/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.html index cb4615fc87..a1af48814e 100644 --- a/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.html +++ b/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.html @@ -38,4 +38,4 @@

Controls (click to dismiss)

under CC BY 4.0 / Modified from original

- + diff --git a/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.ts b/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.ts index 26e9c18a97..038dbe5aaa 100644 --- a/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.ts @@ -1,4 +1,4 @@ -import { tgpu, d } from 'typegpu'; +import { tgpu, common, d } from 'typegpu'; import { type CubemapNames } from './cubemap.ts'; import { defineControls } from '../../common/defineControls.ts'; @@ -24,6 +24,14 @@ const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = root.configureContext({ canvas, alphaMode: 'premultiplied' }); const scene = await setupScene(root, context); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + scene.onResize(); + }, +}); + // #region Example controls and cleanup export const controls = defineControls({ @@ -89,6 +97,7 @@ export const controls = defineControls({ export function onCleanup() { scene.onCleanup(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/scene.ts b/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/scene.ts index 70a5e66876..2c02c32c9b 100644 --- a/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/scene.ts +++ b/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/scene.ts @@ -264,25 +264,6 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { // #region Example controls and cleanup - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const dpr = window.devicePixelRatio; - const width = entry.contentRect.width; - const height = entry.contentRect.height; - canvas.width = width * dpr; - canvas.height = height * dpr; - const newProj = m.mat4.perspective( - Math.PI / 4, - canvas.width / canvas.height, - 0.1, - 10000, - d.mat4x4f(), - ); - cameraBuffer.patch({ projection: newProj }); - } - }); - resizeObserver.observe(canvas); - // Variables for mouse interaction. let isDragging = false; let prevX = 0; @@ -442,13 +423,23 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { materialProps.reflectivity = value; materialBuffer.patch({ reflectivity: value }); }, + onResize() { + const newProj = m.mat4.perspective( + Math.PI / 4, + canvas.width / canvas.height, + 0.1, + 10000, + d.mat4x4f(), + ); + cameraBuffer.patch({ projection: newProj }); + render(); + }, onCleanup() { exampleDestroyed = true; window.removeEventListener('mouseup', mouseUpEventListener); window.removeEventListener('mousemove', mouseMoveEventListener); window.removeEventListener('touchmove', touchMoveEventListener); window.removeEventListener('touchend', touchEndEventListener); - resizeObserver.unobserve(canvas); icosphereGenerator.destroy(); }, }; diff --git a/apps/typegpu-docs/src/examples/rendering/disco/index.html b/apps/typegpu-docs/src/examples/rendering/disco/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/rendering/disco/index.html +++ b/apps/typegpu-docs/src/examples/rendering/disco/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/disco/index.ts b/apps/typegpu-docs/src/examples/rendering/disco/index.ts index 513d735869..389734b4ac 100644 --- a/apps/typegpu-docs/src/examples/rendering/disco/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/disco/index.ts @@ -1,4 +1,4 @@ -import { tgpu, d } from 'typegpu'; +import { tgpu, common, d } from 'typegpu'; import { resolutionAccess, timeAccess } from './consts.ts'; import { mainFragment1, @@ -42,11 +42,7 @@ let currentPipeline = pipelines[0]; let startTime: null | number = null; let frameId: number; -function render(timestamp: number) { - if (startTime === null) { - startTime = timestamp; - } - time.write((timestamp - startTime) / 1000); +function render() { resolutionUniform.write(d.vec2f(canvas.width, canvas.height)); currentPipeline @@ -55,14 +51,32 @@ function render(timestamp: number) { clearValue: [0, 0, 0, 1], }) .draw(6); +} + +function frame(timestamp: number) { + if (startTime === null) { + startTime = timestamp; + } + time.write((timestamp - startTime) / 1000); + + render(); - frameId = requestAnimationFrame(render); + frameId = requestAnimationFrame(frame); } -frameId = requestAnimationFrame(render); +frameId = requestAnimationFrame(frame); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + render(); + }, +}); export function onCleanup() { cancelAnimationFrame(frameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.html b/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.html +++ b/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts b/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts index a547923370..6a6e9ed887 100644 --- a/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/function-visualizer/index.ts @@ -1,5 +1,5 @@ import type { TgpuGuardedComputePipeline, TgpuRawCodeSnippet } from 'typegpu'; -import { tgpu, d, std } from 'typegpu'; +import { tgpu, common, d, std } from 'typegpu'; import { mat4 } from 'wgpu-matrix'; import { defineControls } from '../../common/defineControls.ts'; @@ -383,21 +383,23 @@ window.addEventListener('touchend', touchEndEventListener); // Resize observer and cleanup -const resizeObserver = new ResizeObserver(() => { - queuePropertiesBufferUpdate(); - - msTexture.destroy(); - msTexture = device.createTexture({ - size: [canvas.width, canvas.height], - sampleCount: 4, - format: presentationFormat, - usage: GPUTextureUsage.RENDER_ATTACHMENT, - }); - msView = msTexture.createView(); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + queuePropertiesBufferUpdate(); + + msTexture.destroy(); + msTexture = device.createTexture({ + size: [canvas.width, canvas.height], + sampleCount: 4, + format: presentationFormat, + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + msView = msTexture.createView(); + }, }); -resizeObserver.observe(canvas); - // #region Example controls and cleanup export const controls = defineControls({ @@ -456,7 +458,7 @@ export function onCleanup() { window.removeEventListener('mousemove', mouseMoveEventListener); window.removeEventListener('touchmove', touchMoveEventListener); window.removeEventListener('touchend', touchEndEventListener); - resizeObserver.disconnect(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/global-illumination/index.html b/apps/typegpu-docs/src/examples/rendering/global-illumination/index.html index caab91e78d..4786b20ca1 100644 --- a/apps/typegpu-docs/src/examples/rendering/global-illumination/index.html +++ b/apps/typegpu-docs/src/examples/rendering/global-illumination/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/global-illumination/index.ts b/apps/typegpu-docs/src/examples/rendering/global-illumination/index.ts index 182255797f..3b06cd5c55 100644 --- a/apps/typegpu-docs/src/examples/rendering/global-illumination/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/global-illumination/index.ts @@ -158,8 +158,7 @@ function updateLighting(encoder: TgpuCommandEncoder) { pass.end(); } -let frameId = requestAnimationFrame(frame); -function frame() { +function render() { const encoder = root['~unstable'].createCommandEncoder(); if (lightingDirty) { @@ -172,9 +171,26 @@ function frame() { pass.end(); encoder.submit(); +} + +let frameId = requestAnimationFrame(frame); +function frame() { + render(); frameId = requestAnimationFrame(frame); } +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); + function moveLamp(event: PointerEvent) { if (event.type === 'pointerdown') { canvas.setPointerCapture(event.pointerId); @@ -184,9 +200,15 @@ function moveLamp(event: PointerEvent) { } const rect = canvas.getBoundingClientRect(); + + // Taking into account the square aspect ratio + const size = Math.min(rect.width, rect.height); + const squareLeft = rect.x + (rect.width - size) / 2; + const squareTop = rect.y + (rect.height - size) / 2; + lampPosition.write([ - ((event.clientX - rect.left) / rect.width) * sceneSize, - ((event.clientY - rect.top) / rect.height) * sceneSize, + ((event.clientX - squareLeft) / size) * sceneSize, + ((event.clientY - squareTop) / size) * sceneSize, ]); lightingDirty = true; } @@ -211,6 +233,7 @@ export const controls = defineControls({ export function onCleanup() { cancelAnimationFrame(frameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/jelly-slider/events.ts b/apps/typegpu-docs/src/examples/rendering/jelly-slider/events.ts index 9c14c294df..3a3d05a38a 100644 --- a/apps/typegpu-docs/src/examples/rendering/jelly-slider/events.ts +++ b/apps/typegpu-docs/src/examples/rendering/jelly-slider/events.ts @@ -70,7 +70,10 @@ export class EventHandler { private updateTargetMouseX(clientX: number) { const rect = this.canvas.getBoundingClientRect(); - const normalizedX = (clientX - rect.left) / rect.width; + // The canvas is letterboxed with `object-contain`, so map into the drawn square + const size = Math.min(rect.width, rect.height); + const left = rect.left + (rect.width - size) / 2; + const normalizedX = (clientX - left) / size; const clampedX = Math.max(MOUSE_MIN_X, Math.min(MOUSE_MAX_X, normalizedX)); this.targetMouseX = ((clampedX - MOUSE_RANGE_MIN) / (MOUSE_RANGE_MAX - MOUSE_RANGE_MIN)) * diff --git a/apps/typegpu-docs/src/examples/rendering/jelly-slider/index.html b/apps/typegpu-docs/src/examples/rendering/jelly-slider/index.html index a1a36aafb9..9827fbc16a 100644 --- a/apps/typegpu-docs/src/examples/rendering/jelly-slider/index.html +++ b/apps/typegpu-docs/src/examples/rendering/jelly-slider/index.html @@ -33,4 +33,4 @@ Inspired by work of Voicu Apostol - + diff --git a/apps/typegpu-docs/src/examples/rendering/jelly-slider/index.ts b/apps/typegpu-docs/src/examples/rendering/jelly-slider/index.ts index 6b8ee8bfca..c623879660 100644 --- a/apps/typegpu-docs/src/examples/rendering/jelly-slider/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/jelly-slider/index.ts @@ -1,4 +1,4 @@ -import { tgpu, d, std } from 'typegpu'; +import { tgpu, common, d, std } from 'typegpu'; import { setupScene } from './scene.ts'; import { defineControls } from '../../common/defineControls.ts'; @@ -13,6 +13,18 @@ const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = root.configureContext({ canvas, alphaMode: 'premultiplied' }); const scene = await setupScene(root, context); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + scene.onResize(); + }, +}); + // #region Example controls and cleanup let attributionDismissed = false; @@ -82,6 +94,7 @@ export const controls = defineControls({ export function onCleanup() { scene.onCleanup(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/jelly-slider/scene.ts b/apps/typegpu-docs/src/examples/rendering/jelly-slider/scene.ts index 8563ff240a..d07892cb77 100644 --- a/apps/typegpu-docs/src/examples/rendering/jelly-slider/scene.ts +++ b/apps/typegpu-docs/src/examples/rendering/jelly-slider/scene.ts @@ -660,7 +660,6 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { const digitsTexture = digitsProvider.digitTextureAtlas.createView(d.texture2dArray(d.f32)); let qualityScale = 0.5; - let [prevCanvasWidth, prevCanvasHeight] = [canvas.width, canvas.height]; let [width, height] = [canvas.width * qualityScale, canvas.height * qualityScale]; let textures = createTextures(root, width, height); @@ -746,8 +745,7 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { } let bindGroups = createBindGroups(); - function onResize() { - [prevCanvasWidth, prevCanvasHeight] = [canvas.width, canvas.height]; + function handleResize() { [width, height] = [canvas.width * qualityScale, canvas.height * qualityScale]; camera.updateProjection(Math.PI / 4, width, height); textures = createTextures(root, width, height); @@ -758,28 +756,14 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { bindGroups = createBindGroups(); } - let animationFrameHandle: number; - function render(timestamp: number) { - if (canvas.width !== prevCanvasWidth || canvas.height !== prevCanvasHeight) { - onResize(); - } - + function render() { frameCount++; camera.jitter(); - const deltaTime = Math.min( - lastTimestamp !== null ? (timestamp - lastTimestamp) * 0.001 : 0, - 0.1, - ); - lastTimestamp = timestamp; sceneParamsUniform.patch({ seed: d.vec2f((Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2), }); - eventHandler.update(); - slider.setDragX(eventHandler.currentMouseX); - slider.update(deltaTime); - const currentFrame = frameCount % 2; rayMarchPipeline @@ -796,11 +780,26 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { view: context, }) .draw(3); + } - animationFrameHandle = requestAnimationFrame(render); + let animationFrameHandle: number; + function frame(timestamp: number) { + const deltaTime = Math.min( + lastTimestamp !== null ? (timestamp - lastTimestamp) * 0.001 : 0, + 0.1, + ); + lastTimestamp = timestamp; + + eventHandler.update(); + slider.setDragX(eventHandler.currentMouseX); + slider.update(deltaTime); + + render(); + + animationFrameHandle = requestAnimationFrame(frame); } - animationFrameHandle = requestAnimationFrame(render); + animationFrameHandle = requestAnimationFrame(frame); return { rayMarchPipeline, @@ -810,7 +809,7 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { }, set qualityScale(v: number) { qualityScale = v; - onResize(); + handleResize(); }, set lightDirection(v: d.v3f) { sceneParamsUniform.patch({ @@ -875,6 +874,10 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { return resolutionScale; }, + onResize() { + handleResize(); + render(); + }, onCleanup() { cancelAnimationFrame(animationFrameHandle); }, diff --git a/apps/typegpu-docs/src/examples/rendering/jelly-switch/index.html b/apps/typegpu-docs/src/examples/rendering/jelly-switch/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/rendering/jelly-switch/index.html +++ b/apps/typegpu-docs/src/examples/rendering/jelly-switch/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/jelly-switch/index.ts b/apps/typegpu-docs/src/examples/rendering/jelly-switch/index.ts index 69f7b99204..ed4c6bd776 100644 --- a/apps/typegpu-docs/src/examples/rendering/jelly-switch/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/jelly-switch/index.ts @@ -484,7 +484,6 @@ const renderPipeline = root.createRenderPipeline({ targets: { format: presentationFormat }, }); -let lastTimestamp: number | null = null; let frameCount = 0; const taaResolver = new TAAResolver(root, width, height); @@ -503,19 +502,13 @@ function createBindGroups() { let bindGroups = createBindGroups(); -let animationFrameHandle: number; -function render(timestamp: number) { +function render() { frameCount++; - camera.jitter(); - const deltaTime = Math.min(lastTimestamp !== null ? (timestamp - lastTimestamp) * 0.001 : 0, 0.1); - lastTimestamp = timestamp; + const currentFrame = frameCount % 2; + camera.jitter(); randomUniform.write(d.vec2f((Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2)); - switchBehavior.update(deltaTime); - - const currentFrame = frameCount % 2; - rayMarchPipeline .withColorAttachment({ view: textures[currentFrame].sampled, @@ -530,11 +523,14 @@ function render(timestamp: number) { .withColorAttachment({ view: context }) .with(bindGroups.render[currentFrame]) .draw(3); - - animationFrameHandle = requestAnimationFrame(render); } function handleResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + [width, height] = [canvas.width * qualityScale, canvas.height * qualityScale]; camera.updateProjection(Math.PI / 4, width, height); textures = createTextures(root, width, height); @@ -543,14 +539,30 @@ function handleResize() { frameCount = 0; bindGroups = createBindGroups(); + render(); } -const resizeObserver = new ResizeObserver(() => { - handleResize(); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + handleResize(); + }, }); -resizeObserver.observe(canvas); -animationFrameHandle = requestAnimationFrame(render); +let lastTimestamp: number | null = null; +let animationFrameHandle: number; +function frame(timestamp: number) { + const deltaTime = Math.min(lastTimestamp !== null ? (timestamp - lastTimestamp) * 0.001 : 0, 0.1); + lastTimestamp = timestamp; + + switchBehavior.update(deltaTime); + + render(); + + animationFrameHandle = requestAnimationFrame(frame); +} +animationFrameHandle = requestAnimationFrame(frame); // #region Example controls and cleanup @@ -684,7 +696,7 @@ export const controls = defineControls({ export function onCleanup() { cancelAnimationFrame(animationFrameHandle); - resizeObserver.disconnect(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/os-awards/index.html b/apps/typegpu-docs/src/examples/rendering/os-awards/index.html index 972583913a..657dddb648 100644 --- a/apps/typegpu-docs/src/examples/rendering/os-awards/index.html +++ b/apps/typegpu-docs/src/examples/rendering/os-awards/index.html @@ -12,4 +12,4 @@
Loading...
- + diff --git a/apps/typegpu-docs/src/examples/rendering/os-awards/index.ts b/apps/typegpu-docs/src/examples/rendering/os-awards/index.ts index 4c7fa78d0c..4132070558 100644 --- a/apps/typegpu-docs/src/examples/rendering/os-awards/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/os-awards/index.ts @@ -115,16 +115,27 @@ function updateAwardTransform(timeMs: number) { awardTransformInverse.write(m.mat4.invert(transformDraft, inverseDraft)); } -let frameId = requestAnimationFrame(function frame(timeMs) { - updateAwardTransform(timeMs); - +function render() { envPipeline.with(sharedBindGroup).withColorAttachment({ view: context }).draw(3); renderers[renderer].draw(sharedBindGroup); +} + +let frameId = requestAnimationFrame(function frame(timeMs) { + updateAwardTransform(timeMs); + render(); loadingScreen.remove(); frameId = requestAnimationFrame(frame); }); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + render(); + }, +}); + // #region Example controls and cleanup export const controls = defineControls({ @@ -147,6 +158,7 @@ export function onCleanup() { cancelAnimationFrame(frameId); cleanupCamera(); Object.values(renderers).forEach((r) => r.destroy()); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/os-awards/mesh/renderer.ts b/apps/typegpu-docs/src/examples/rendering/os-awards/mesh/renderer.ts index 4a7868db52..8a22d7ae18 100644 --- a/apps/typegpu-docs/src/examples/rendering/os-awards/mesh/renderer.ts +++ b/apps/typegpu-docs/src/examples/rendering/os-awards/mesh/renderer.ts @@ -130,14 +130,15 @@ export function createMeshRenderer( .$usage('render'); let depth = createDepthTexture(); - const resizeObserver = new ResizeObserver(() => { - depth.destroy(); - depth = createDepthTexture(); - }); - resizeObserver.observe(canvas); return { draw(sharedBindGroup: SharedBindGroup) { + // Recreating the depth texture if the canvas has been resized + if (depth.props.size[0] !== canvas.width || depth.props.size[1] !== canvas.height) { + depth.destroy(); + depth = createDepthTexture(); + } + pipeline .with(sharedBindGroup) .withColorAttachment({ view: context, loadOp: 'load' }) @@ -152,7 +153,6 @@ export function createMeshRenderer( .drawIndexed(award.indexCount); }, destroy() { - resizeObserver.disconnect(); depth.destroy(); }, }; diff --git a/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.html b/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.html +++ b/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts b/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts index 72f36af744..1bdfa9d27b 100644 --- a/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/perlin-noise/index.ts @@ -66,19 +66,34 @@ let activeSharpenFn: 'exponential' | 'tanh' = 'exponential'; let isRunning = true; let bindGroup = root.createBindGroup(dynamicLayout, perlinCache.bindings); -function draw(timestamp: number) { +function render() { + renderPipelines[activeSharpenFn].with(bindGroup).withColorAttachment({ view: context }).draw(3); +} + +function frame(timestamp: number) { if (!isRunning) { return; } time.write((timestamp * 0.0002) % DEPTH); + render(); - renderPipelines[activeSharpenFn].with(bindGroup).withColorAttachment({ view: context }).draw(3); - - requestAnimationFrame(draw); + requestAnimationFrame(frame); } -requestAnimationFrame(draw); +requestAnimationFrame(frame); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); export const controls = defineControls({ 'grid size': { @@ -109,5 +124,6 @@ export const controls = defineControls({ export function onCleanup() { isRunning = false; + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/phong-reflection/index.html b/apps/typegpu-docs/src/examples/rendering/phong-reflection/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/rendering/phong-reflection/index.html +++ b/apps/typegpu-docs/src/examples/rendering/phong-reflection/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/phong-reflection/index.ts b/apps/typegpu-docs/src/examples/rendering/phong-reflection/index.ts index f6a2f49617..8434f72027 100644 --- a/apps/typegpu-docs/src/examples/rendering/phong-reflection/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/phong-reflection/index.ts @@ -1,4 +1,4 @@ -import { tgpu, d, std } from 'typegpu'; +import { tgpu, common, d, std } from 'typegpu'; import * as p from './params.ts'; import { ExampleControls, @@ -102,9 +102,7 @@ let depthTexture = root.device.createTexture({ usage: GPUTextureUsage.RENDER_ATTACHMENT, }); -// frame -let frameId: number; -function frame() { +function render() { renderPipeline .withColorAttachment({ view: context, @@ -118,11 +116,30 @@ function frame() { }) .with(modelVertexLayout, model.vertexBuffer) .draw(model.polygonCount); +} +// frame +let frameId: number; +function frame() { + render(); frameId = requestAnimationFrame(frame); } frameId = requestAnimationFrame(frame); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + depthTexture.destroy(); + depthTexture = root.device.createTexture({ + size: [canvas.width, canvas.height, 1], + format: 'depth24plus', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + render(); + }, +}); + // #region Example controls and cleanup export const controls = defineControls({ 'light color': { @@ -166,20 +183,10 @@ export const controls = defineControls({ }, }); -const resizeObserver = new ResizeObserver(() => { - depthTexture.destroy(); - depthTexture = root.device.createTexture({ - size: [canvas.width, canvas.height, 1], - format: 'depth24plus', - usage: GPUTextureUsage.RENDER_ATTACHMENT, - }); -}); -resizeObserver.observe(canvas); - export function onCleanup() { cancelAnimationFrame(frameId); cleanupCamera(); - resizeObserver.unobserve(canvas); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/index.html b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/index.html +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/index.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/index.ts index 718feef491..e5290532c9 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/index.ts @@ -7,7 +7,6 @@ import { CameraData, InstanceData, instanceLayout, VertexData, vertexLayout } fr import { defineControls } from '../../common/defineControls.ts'; const root = await tgpu.init(); -const device = root.device; const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = root.configureContext({ canvas }); const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); @@ -361,28 +360,11 @@ let lastTime: number | null = null; let time = 0; let animationFrameId: number; -function render(timestamp: number) { - const dt = lastTime !== null ? (timestamp - lastTime) / 1000 : 0; - lastTime = timestamp; - time += dt; - - for (let i = 0; i < orbitingCubes.length; i++) { - const offset = (i / orbitingCubes.length) * Math.PI * 2; - const angle = time * 0.5 + offset; - const radius = 4 + Math.sin(time * 2 + offset * 3) * 0.5; - const x = Math.cos(angle) * radius; - const z = Math.sin(angle) * radius; - const y = 2 + Math.sin(time * 1.5 + offset * 2) * 1.5; - orbitingCubes[i].position = d.vec3f(x, y, z); - orbitingCubes[i].rotation = d.vec3f(time, time * 0.5, 0); - } - - scene.update(); +function render() { pointLight.renderShadowMaps(pipelineDepthOne, renderLayout, scene); if (showDepthPreview) { pipelinePreview.withColorAttachment({ view: context }).draw(3); - animationFrameId = requestAnimationFrame(render); return; } @@ -421,17 +403,41 @@ function render(timestamp: number) { .withIndexBuffer(BoxGeometry.indexBuffer) .with(vertexLayout, BoxGeometry.vertexBuffer) .drawIndexed(BoxGeometry.indexCount); +} + +function frame(timestamp: number) { + animationFrameId = requestAnimationFrame(frame); - animationFrameId = requestAnimationFrame(render); + const dt = lastTime !== null ? (timestamp - lastTime) / 1000 : 0; + lastTime = timestamp; + time += dt; + + for (let i = 0; i < orbitingCubes.length; i++) { + const offset = (i / orbitingCubes.length) * Math.PI * 2; + const angle = time * 0.5 + offset; + const radius = 4 + Math.sin(time * 2 + offset * 3) * 0.5; + const x = Math.cos(angle) * radius; + const z = Math.sin(angle) * radius; + const y = 2 + Math.sin(time * 1.5 + offset * 2) * 1.5; + orbitingCubes[i].position = d.vec3f(x, y, z); + orbitingCubes[i].rotation = d.vec3f(time, time * 0.5, 0); + } + + scene.update(); + + render(); } -animationFrameId = requestAnimationFrame(render); -const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const width = entry.contentBoxSize[0].inlineSize; - const height = entry.contentBoxSize[0].blockSize; - canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D)); - canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D)); +animationFrameId = requestAnimationFrame(frame); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; depthTexture = root .createTexture({ @@ -447,9 +453,10 @@ const resizeObserver = new ResizeObserver((entries) => { sampleCount: 4, }) .$usage('render'); - } + + render(); + }, }); -resizeObserver.observe(canvas); const initialCamPos = { x: 5, y: 5, z: -5 }; let theta = Math.atan2(initialCamPos.z, initialCamPos.x); @@ -657,7 +664,7 @@ export function onCleanup() { window.removeEventListener('mousemove', mouseMoveEventListener); window.removeEventListener('touchend', touchEndEventListener); window.removeEventListener('touchmove', touchMoveEventListener); - resizeObserver.disconnect(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/pom/index.html b/apps/typegpu-docs/src/examples/rendering/pom/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/rendering/pom/index.html +++ b/apps/typegpu-docs/src/examples/rendering/pom/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/pom/index.ts b/apps/typegpu-docs/src/examples/rendering/pom/index.ts index 102d07f6e6..23fb7cf34f 100644 --- a/apps/typegpu-docs/src/examples/rendering/pom/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/pom/index.ts @@ -1,5 +1,5 @@ import { randf } from '@typegpu/noise'; -import { tgpu, d, std, type RenderFlag, type TgpuTexture } from 'typegpu'; +import { tgpu, common, d, std, type RenderFlag, type TgpuTexture } from 'typegpu'; import { setupOrbitCamera } from '../../common/setup-orbit-camera.ts'; import { defineControls } from '../../common/defineControls.ts'; import { @@ -405,8 +405,7 @@ function createDepthTexture() { } createDepthTexture(); -let frameId: number; -function frame() { +function render() { pipeline .withColorAttachment({ view: context, clearValue: [0, 0, 0, 1] }) .withDepthStencilAttachment({ @@ -416,10 +415,24 @@ function frame() { depthStoreOp: 'store', }) .drawIndexed(planeMesh.indexCount); +} + +let frameId: number; +function frame() { + render(); frameId = requestAnimationFrame(frame); } frameId = requestAnimationFrame(frame); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + createDepthTexture(); + render(); + }, +}); + // #region Example controls and cleanup export const controls = defineControls({ @@ -484,13 +497,10 @@ export const controls = defineControls({ }, }); -const resizeObserver = new ResizeObserver(createDepthTexture); -resizeObserver.observe(canvas); - export function onCleanup() { cancelAnimationFrame(frameId); cleanupCamera(); - resizeObserver.unobserve(canvas); + autoResizer.detach(); depthTexture.destroy(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/radiance-cascades-drawing/drawInteraction.ts b/apps/typegpu-docs/src/examples/rendering/radiance-cascades-drawing/drawInteraction.ts index bd38d9b86a..3aaf7d5475 100644 --- a/apps/typegpu-docs/src/examples/rendering/radiance-cascades-drawing/drawInteraction.ts +++ b/apps/typegpu-docs/src/examples/rendering/radiance-cascades-drawing/drawInteraction.ts @@ -39,25 +39,32 @@ export function createDrawInteraction({ canvas, onDraw }: DrawInteractionOptions return hslToRgb((timestamp * 0.00008) % 1, 0.82, 0.62); } - function mousePosition(e: MouseEvent): Point { + function clientToCanvas(clientX: number, clientY: number): Point { const rect = canvas.getBoundingClientRect(); + + // Taking into account the square aspect ratio + const centerX = rect.x + rect.width / 2; + const centerY = rect.y + rect.height / 2; + const size = Math.min(rect.width, rect.height); + const squareLeft = centerX - size / 2; + const squareTop = centerY - size / 2; + return { - x: (e.clientX - rect.left) / rect.width, - y: (e.clientY - rect.top) / rect.height, + x: (clientX - squareLeft) / size, + y: (clientY - squareTop) / size, }; } + function mousePosition(e: MouseEvent): Point { + return clientToCanvas(e.clientX, e.clientY); + } + function touchPosition(touches: TouchList): Point { const first = touches[0]; const second = touches[1]; const clientX = second ? (first.clientX + second.clientX) / 2 : first.clientX; const clientY = second ? (first.clientY + second.clientY) / 2 : first.clientY; - - const rect = canvas.getBoundingClientRect(); - return { - x: (clientX - rect.left) / rect.width, - y: (clientY - rect.top) / rect.height, - }; + return clientToCanvas(clientX, clientY); } function setButton(useSecondary: boolean) { diff --git a/apps/typegpu-docs/src/examples/rendering/radiance-cascades-drawing/index.html b/apps/typegpu-docs/src/examples/rendering/radiance-cascades-drawing/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/rendering/radiance-cascades-drawing/index.html +++ b/apps/typegpu-docs/src/examples/rendering/radiance-cascades-drawing/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/radiance-cascades-drawing/index.ts b/apps/typegpu-docs/src/examples/rendering/radiance-cascades-drawing/index.ts index 03aedf25b0..96e38caf3a 100644 --- a/apps/typegpu-docs/src/examples/rendering/radiance-cascades-drawing/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/radiance-cascades-drawing/index.ts @@ -115,10 +115,7 @@ const interaction = createDrawInteraction({ }, }); -let frameId = requestAnimationFrame(frame); -function frame(timestamp: number) { - interaction.update(timestamp); - +function render() { const encoder = root['~unstable'].createCommandEncoder(); if (sceneDirty) { stamp(encoder); @@ -131,9 +128,27 @@ function frame(timestamp: number) { pass.end(); encoder.submit(); +} + +let frameId = requestAnimationFrame(frame); +function frame(timestamp: number) { + interaction.update(timestamp); + render(); frameId = requestAnimationFrame(frame); } +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); + // #region Example controls and cleanup export const controls = defineControls({ @@ -165,6 +180,7 @@ export const controls = defineControls({ export function onCleanup() { cancelAnimationFrame(frameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/radiance-cascades/drag-controller.ts b/apps/typegpu-docs/src/examples/rendering/radiance-cascades/drag-controller.ts index d95ac10e4a..2eef024f67 100644 --- a/apps/typegpu-docs/src/examples/rendering/radiance-cascades/drag-controller.ts +++ b/apps/typegpu-docs/src/examples/rendering/radiance-cascades/drag-controller.ts @@ -19,8 +19,12 @@ export class DragController { private canvasToUV(clientX: number, clientY: number): d.v2f { const rect = this.canvas.getBoundingClientRect(); - const x = (clientX - rect.left) / rect.width; - const y = (clientY - rect.top) / rect.height; + // The canvas is letterboxed with `object-contain`, so map into the drawn square + const size = Math.min(rect.width, rect.height); + const left = rect.left + (rect.width - size) / 2; + const top = rect.top + (rect.height - size) / 2; + const x = (clientX - left) / size; + const y = (clientY - top) / size; return d.vec2f(x, y); } diff --git a/apps/typegpu-docs/src/examples/rendering/radiance-cascades/index.html b/apps/typegpu-docs/src/examples/rendering/radiance-cascades/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/rendering/radiance-cascades/index.html +++ b/apps/typegpu-docs/src/examples/rendering/radiance-cascades/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/radiance-cascades/index.ts b/apps/typegpu-docs/src/examples/rendering/radiance-cascades/index.ts index ee3095ea4a..6e5527704b 100644 --- a/apps/typegpu-docs/src/examples/rendering/radiance-cascades/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/radiance-cascades/index.ts @@ -516,15 +516,23 @@ const dragController = new DragController(canvas, onDrag, onDrag); // #region Example controls and cleanup let resizeTimeout: ReturnType; -const resizeObserver = new ResizeObserver(() => { - clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(recreateSizedResources, 100); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(recreateSizedResources, 100); + + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + }, }); -resizeObserver.observe(canvas); export function onCleanup() { dragController.destroy(); - resizeObserver.disconnect(); + autoResizer.detach(); clearTimeout(resizeTimeout); if (frameId !== null) { cancelAnimationFrame(frameId); diff --git a/apps/typegpu-docs/src/examples/rendering/ray-marching/index.html b/apps/typegpu-docs/src/examples/rendering/ray-marching/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/rendering/ray-marching/index.html +++ b/apps/typegpu-docs/src/examples/rendering/ray-marching/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/ray-marching/index.ts b/apps/typegpu-docs/src/examples/rendering/ray-marching/index.ts index 2c33a63226..ba47ff744f 100644 --- a/apps/typegpu-docs/src/examples/rendering/ray-marching/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/ray-marching/index.ts @@ -1,4 +1,4 @@ -import { tgpu } from 'typegpu'; +import { tgpu, common } from 'typegpu'; import { setupScene } from './scene.ts'; @@ -7,7 +7,16 @@ const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = root.configureContext({ canvas, alphaMode: 'premultiplied' }); const scene = await setupScene(root, context); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + scene.onResize(); + }, +}); + export function onCleanup() { scene.onCleanup(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/ray-marching/scene.ts b/apps/typegpu-docs/src/examples/rendering/ray-marching/scene.ts index 2234840267..a0fb2beccd 100644 --- a/apps/typegpu-docs/src/examples/rendering/ray-marching/scene.ts +++ b/apps/typegpu-docs/src/examples/rendering/ray-marching/scene.ts @@ -218,12 +218,16 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { fragment: fragmentMain, }); - let animationFrame: number; - function run(timestamp: number) { - time.write((timestamp / 1000) % 1000); + function render() { resolution.write(d.vec2f(canvas.width, canvas.height)); renderPipeline.withColorAttachment({ view: context }).draw(3); + } + + let animationFrame: number; + function run(timestamp: number) { + time.write((timestamp / 1000) % 1000); + render(); animationFrame = requestAnimationFrame(run); } @@ -231,6 +235,9 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { animationFrame = requestAnimationFrame(run); return { + onResize() { + render(); + }, onCleanup() { cancelAnimationFrame(animationFrame); }, diff --git a/apps/typegpu-docs/src/examples/rendering/render-bundles-with/index.html b/apps/typegpu-docs/src/examples/rendering/render-bundles-with/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/rendering/render-bundles-with/index.html +++ b/apps/typegpu-docs/src/examples/rendering/render-bundles-with/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/render-bundles-with/index.ts b/apps/typegpu-docs/src/examples/rendering/render-bundles-with/index.ts index baae4dfcc4..72f5d002a1 100644 --- a/apps/typegpu-docs/src/examples/rendering/render-bundles-with/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/render-bundles-with/index.ts @@ -1,5 +1,5 @@ import { perlin2d } from '@typegpu/noise'; -import { tgpu, d } from 'typegpu'; +import { tgpu, common, d } from 'typegpu'; import * as m from 'wgpu-matrix'; import { defineControls } from '../../common/defineControls.ts'; import { setupOrbitCamera } from '../../common/setup-orbit-camera.ts'; @@ -147,11 +147,7 @@ setCubeCount(INITIAL_CUBE_COUNT); let useBundles = true; -let disposed = false; - -function frame() { - if (disposed) return; - +function render() { if (depthTexture.width !== canvas.width || depthTexture.height !== canvas.height) { depthTexture.destroy(); depthTexture = root.device.createTexture({ @@ -186,12 +182,28 @@ function frame() { pass.end(); encoder.submit(); +} + +let disposed = false; + +function frame() { + if (disposed) return; + + render(); requestAnimationFrame(frame); } requestAnimationFrame(frame); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + render(); + }, +}); + // #region Example controls and cleanup export const controls = defineControls({ @@ -215,5 +227,6 @@ export function onCleanup() { cleanupCamera(); perlinCache.destroy(); depthTexture.destroy(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/render-bundles/index.html b/apps/typegpu-docs/src/examples/rendering/render-bundles/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/rendering/render-bundles/index.html +++ b/apps/typegpu-docs/src/examples/rendering/render-bundles/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/render-bundles/index.ts b/apps/typegpu-docs/src/examples/rendering/render-bundles/index.ts index 538ec576ea..e6eee1a819 100644 --- a/apps/typegpu-docs/src/examples/rendering/render-bundles/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/render-bundles/index.ts @@ -1,5 +1,5 @@ import { perlin2d } from '@typegpu/noise'; -import { tgpu, d } from 'typegpu'; +import { tgpu, common, d } from 'typegpu'; import * as m from 'wgpu-matrix'; import { defineControls } from '../../common/defineControls.ts'; import { setupOrbitCamera } from '../../common/setup-orbit-camera.ts'; @@ -146,9 +146,7 @@ let useBundles = true; let disposed = false; -function frame() { - if (disposed) return; - +function render() { if (depthTexture.width !== canvas.width || depthTexture.height !== canvas.height) { depthTexture.destroy(); depthTexture = root.device.createTexture({ @@ -187,12 +185,26 @@ function frame() { pass.end(); encoder.submit(); +} + +function frame() { + if (disposed) return; + + render(); requestAnimationFrame(frame); } requestAnimationFrame(frame); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + render(); + }, +}); + // #region Example controls and cleanup export const controls = defineControls({ @@ -214,6 +226,7 @@ export const controls = defineControls({ export function onCleanup() { disposed = true; cleanupCamera(); + autoResizer.detach(); perlinCache.destroy(); depthTexture.destroy(); root.destroy(); diff --git a/apps/typegpu-docs/src/examples/rendering/simple-shadow/index.html b/apps/typegpu-docs/src/examples/rendering/simple-shadow/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/rendering/simple-shadow/index.html +++ b/apps/typegpu-docs/src/examples/rendering/simple-shadow/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/simple-shadow/index.ts b/apps/typegpu-docs/src/examples/rendering/simple-shadow/index.ts index 837c20896f..9d26c0a23a 100644 --- a/apps/typegpu-docs/src/examples/rendering/simple-shadow/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/simple-shadow/index.ts @@ -1,4 +1,4 @@ -import { tgpu, d, std } from 'typegpu'; +import { tgpu, common, d, std } from 'typegpu'; import { mat4 } from 'wgpu-matrix'; import { createCuboid, createPlane } from './geometry.ts'; import { @@ -280,11 +280,7 @@ function updateLightDirection(dir: d.v3f) { }); } -// Render loop -let frameId: number | null = null; function render() { - frameId = requestAnimationFrame(render); - const encoder = root['~unstable'].createCommandEncoder(); const shadowPass = encoder.beginRenderPass({ @@ -322,23 +318,36 @@ function render() { encoder.submit(); } -frameId = requestAnimationFrame(render); -const resizeObserver = new ResizeObserver(() => { - canvasTextures = createCanvasTextures(); +// Render loop +let frameId: number | null = null; +function frame() { + frameId = requestAnimationFrame(frame); - const newProjection = mat4.perspective( - Math.PI / 4, - canvas.width / canvas.height, - 0.1, - 100, - d.mat4x4f(), - ); - cameraUniform.patch({ - projection: newProjection, - }); + render(); +} +frameId = requestAnimationFrame(frame); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + canvasTextures = createCanvasTextures(); + + const newProjection = mat4.perspective( + Math.PI / 4, + canvas.width / canvas.height, + 0.1, + 100, + d.mat4x4f(), + ); + cameraUniform.patch({ + projection: newProjection, + }); + + render(); + }, }); -resizeObserver.observe(canvas); export const controls = defineControls({ 'camera X': { @@ -435,6 +444,6 @@ export function onCleanup() { if (frameId !== null) { cancelAnimationFrame(frameId); } - resizeObserver.disconnect(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/smoky-triangle/index.html b/apps/typegpu-docs/src/examples/rendering/smoky-triangle/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/rendering/smoky-triangle/index.html +++ b/apps/typegpu-docs/src/examples/rendering/smoky-triangle/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/smoky-triangle/index.ts b/apps/typegpu-docs/src/examples/rendering/smoky-triangle/index.ts index d06b2bc718..0a95ab4e94 100644 --- a/apps/typegpu-docs/src/examples/rendering/smoky-triangle/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/smoky-triangle/index.ts @@ -1,5 +1,5 @@ import { perlin3d } from '@typegpu/noise'; -import { tgpu, d, std, type TgpuFragmentFn, type TgpuVertexFn } from 'typegpu'; +import { tgpu, common, d, std, type TgpuFragmentFn, type TgpuVertexFn } from 'typegpu'; import { defineControls } from '../../common/defineControls.ts'; const Params = d.struct({ @@ -117,6 +117,10 @@ const basePipeline = root.pipe(perlinCache.inject()).createRenderPipeline({ let pipeline = basePipeline; +function render() { + pipeline.withColorAttachment({ view: context }).draw(3); +} + let frameId: number; function frame(timestamp: number) { paramsUniform.patch({ @@ -124,12 +128,24 @@ function frame(timestamp: number) { grainSeed: Math.floor(Math.random() * 100), }); - pipeline.withColorAttachment({ view: context }).draw(3); + render(); frameId = requestAnimationFrame(frame); } frameId = requestAnimationFrame(frame); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); + export const controls = defineControls({ HDR: hdr && { initial: false, @@ -225,5 +241,6 @@ export const controls = defineControls({ export function onCleanup() { cancelAnimationFrame(frameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/suika-sdf/index.html b/apps/typegpu-docs/src/examples/rendering/suika-sdf/index.html index d50df83d4b..02e77097f2 100644 --- a/apps/typegpu-docs/src/examples/rendering/suika-sdf/index.html +++ b/apps/typegpu-docs/src/examples/rendering/suika-sdf/index.html @@ -1,4 +1,4 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/suika-sdf/index.ts b/apps/typegpu-docs/src/examples/rendering/suika-sdf/index.ts index d1261b1e05..915da96cfc 100644 --- a/apps/typegpu-docs/src/examples/rendering/suika-sdf/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/suika-sdf/index.ts @@ -1,5 +1,5 @@ import * as sdf from '@typegpu/sdf'; -import { tgpu, d, std } from 'typegpu'; +import { tgpu, common, d, std } from 'typegpu'; import { fullScreenTriangle } from 'typegpu/common'; import { DROP_Y, @@ -414,18 +414,23 @@ const renderPipeline = root.createRenderPipeline({ }, }); -const resizeObserver = new ResizeObserver(() => { - mergedField.distance.destroy(); - mergedField.info.destroy(); - mergedField = createMergedFieldResources(); - distanceView = mergedField.distance.createView(d.texture2d()); - infoView = mergedField.info.createView(d.texture2d()); - mergedFieldBindGroup = root.createBindGroup(mergedFieldLayout, { - distance: distanceView, - info: infoView, - }); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + mergedField.distance.destroy(); + mergedField.info.destroy(); + mergedField = createMergedFieldResources(); + distanceView = mergedField.distance.createView(d.texture2d()); + infoView = mergedField.info.createView(d.texture2d()); + mergedFieldBindGroup = root.createBindGroup(mergedFieldLayout, { + distance: distanceView, + info: infoView, + }); + + render(); + }, }); -resizeObserver.observe(canvas); canvas.addEventListener('touchstart', (e) => e.preventDefault(), { passive: false, signal }); canvas.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false, signal }); @@ -528,6 +533,24 @@ function restart() { circleUniform.write(circleData); } +function render() { + frameUniform.patch({ + canvasAspect: canvas.width / canvas.height, + }); + + mergedFieldPipeline + .withColorAttachment({ + distance: { view: distanceView }, + info: { view: infoView }, + }) + .draw(3); + + renderPipeline + .with(mergedFieldBindGroup) + .withColorAttachment({ view: context, clearValue: { r: 0, g: 0, b: 0, a: 1 } }) + .draw(3); +} + let lastTime = 0; let animationFrameId = 0; @@ -642,18 +665,7 @@ function frame(now: number) { }, }); - mergedFieldPipeline - .withColorAttachment({ - distance: { view: distanceView }, - info: { view: infoView }, - }) - .draw(3); - - renderPipeline - .with(mergedFieldBindGroup) - .withColorAttachment({ view: context, clearValue: { r: 0, g: 0, b: 0, a: 1 } }) - .draw(3); - + render(); animationFrameId = requestAnimationFrame(frame); } animationFrameId = requestAnimationFrame(frame); @@ -676,6 +688,6 @@ export const controls = defineControls({ export function onCleanup() { cleanupController.abort(); cancelAnimationFrame(animationFrameId); - resizeObserver.disconnect(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/triplanar-mapping/index.html b/apps/typegpu-docs/src/examples/rendering/triplanar-mapping/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/rendering/triplanar-mapping/index.html +++ b/apps/typegpu-docs/src/examples/rendering/triplanar-mapping/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/rendering/triplanar-mapping/index.ts b/apps/typegpu-docs/src/examples/rendering/triplanar-mapping/index.ts index 3e13b626ec..823a6a322f 100644 --- a/apps/typegpu-docs/src/examples/rendering/triplanar-mapping/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/triplanar-mapping/index.ts @@ -1,4 +1,4 @@ -import { d, std, tgpu } from 'typegpu'; +import { common, d, std, tgpu } from 'typegpu'; import { setupOrbitCamera } from '../../common/setup-orbit-camera.ts'; import { defineControls } from '../../common/defineControls.ts'; import { loadModel } from './load-model.ts'; @@ -287,8 +287,7 @@ const splitComparison = createSplitComparison(canvas, 'Triplanar Mapping', 'Mesh splitRatio = ratio; }); -let frameId: number; -function frame() { +function render() { const splitX = Math.round(canvas.width * splitRatio); const encoder = root['~unstable'].createCommandEncoder(); const pass = encoder.beginRenderPass({ @@ -303,6 +302,11 @@ function frame() { pass.end(); encoder.submit(); +} + +let frameId: number; +function frame() { + render(); frameId = requestAnimationFrame(frame); } frameId = requestAnimationFrame(frame); @@ -362,17 +366,21 @@ export const controls = defineControls({ }, }); -const resizeObserver = new ResizeObserver(() => { - depthTexture.destroy(); - depthTexture = createDepthTexture(); - splitComparison.sync(); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + depthTexture.destroy(); + depthTexture = createDepthTexture(); + splitComparison.sync(); + render(); + }, }); -resizeObserver.observe(canvas); export function onCleanup() { cancelAnimationFrame(frameId); cleanupCamera(); - resizeObserver.disconnect(); + autoResizer.detach(); splitComparison.destroy(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/trippy-raymarching/index.html b/apps/typegpu-docs/src/examples/rendering/trippy-raymarching/index.html index a431ef4a59..2e84768316 100644 --- a/apps/typegpu-docs/src/examples/rendering/trippy-raymarching/index.html +++ b/apps/typegpu-docs/src/examples/rendering/trippy-raymarching/index.html @@ -1,3 +1,4 @@ diff --git a/apps/typegpu-docs/src/examples/rendering/trippy-raymarching/index.ts b/apps/typegpu-docs/src/examples/rendering/trippy-raymarching/index.ts index e94b6e8070..320929125e 100644 --- a/apps/typegpu-docs/src/examples/rendering/trippy-raymarching/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/trippy-raymarching/index.ts @@ -163,36 +163,44 @@ let isRunning = true; let animationFrameId = 0; let resolutionScale = 1.0; -function resizeCanvas() { - const dpr = window.devicePixelRatio || 1; - const targetWidth = Math.max(1, Math.floor(canvas.clientWidth * dpr * resolutionScale)); - const targetHeight = Math.max(1, Math.floor(canvas.clientHeight * dpr * resolutionScale)); - - if (canvas.width !== targetWidth || canvas.height !== targetHeight) { - canvas.width = targetWidth; - canvas.height = targetHeight; - aspectRatioUniform.write(targetWidth / targetHeight); - } +function render() { + const view = context.getCurrentTexture().createView(); + pipeline.withColorAttachment({ view }).draw(3); } -const resizeObserver = new ResizeObserver(() => resizeCanvas()); -resizeObserver.observe(canvas); -resizeCanvas(); - function frame(timestamp: number) { if (!isRunning) { return; } timeUniform.write(timestamp / 1000); - const view = context.getCurrentTexture().createView(); - - pipeline.withColorAttachment({ view }).draw(3); + render(); animationFrameId = requestAnimationFrame(frame); } animationFrameId = requestAnimationFrame(frame); +// The full resolution of the canvas, before applying the resolution scale +let fullWidth = canvas.width; +let fullHeight = canvas.height; + +function applyResolutionScale() { + canvas.width = Math.max(1, Math.floor(fullWidth * resolutionScale)); + canvas.height = Math.max(1, Math.floor(fullHeight * resolutionScale)); + aspectRatioUniform.write(canvas.width / canvas.height); + render(); +} + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + fullWidth = canvas.width; + fullHeight = canvas.height; + applyResolutionScale(); + }, +}); + // #region Example controls and cleanup const paramSlider = ( @@ -213,7 +221,7 @@ export const controls = defineControls({ step: 0.05, onSliderChange: (value) => { resolutionScale = value; - resizeCanvas(); + applyResolutionScale(); }, }, 'Sphere spacing': paramSlider('sphereSpacing', { initial: 4, min: 1, max: 10, step: 0.1 }), @@ -229,7 +237,7 @@ export const controls = defineControls({ export function onCleanup() { isRunning = false; cancelAnimationFrame(animationFrameId); - resizeObserver.disconnect(); + autoResizer.detach(); cleanupController.abort(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/two-boxes/index.html b/apps/typegpu-docs/src/examples/rendering/two-boxes/index.html index 9209e7c2fd..67aad1c073 100644 --- a/apps/typegpu-docs/src/examples/rendering/two-boxes/index.html +++ b/apps/typegpu-docs/src/examples/rendering/two-boxes/index.html @@ -27,4 +27,4 @@

Controls

Right Mouse Button: Rotate cubes

Scroll / Two-Finger Drag: Zoom

- + diff --git a/apps/typegpu-docs/src/examples/rendering/two-boxes/index.ts b/apps/typegpu-docs/src/examples/rendering/two-boxes/index.ts index 170eefbe43..acea55727e 100644 --- a/apps/typegpu-docs/src/examples/rendering/two-boxes/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/two-boxes/index.ts @@ -1,5 +1,5 @@ import type { RenderFlag, TgpuBindGroup, TgpuBuffer, TgpuTexture, VertexFlag } from 'typegpu'; -import { tgpu, d, std } from 'typegpu'; +import { tgpu, common, d, std } from 'typegpu'; import * as m from 'wgpu-matrix'; // Initialization @@ -30,7 +30,7 @@ const vertexLayout = tgpu.vertexLayout(d.arrayOf(Vertex)); // Scene Setup -const aspect = canvas.clientWidth / canvas.clientHeight; +const aspect = 1; const target = d.vec3f(0, 0, 0); const cameraInitialPos = d.vec4f(12, 5, 12, 1); @@ -460,10 +460,18 @@ canvas.addEventListener( { passive: false }, ); -const resizeObserver = new ResizeObserver(() => { - createDepthAndMsaaTextures(); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + + createDepthAndMsaaTextures(); + }, }); -resizeObserver.observe(canvas); export function onCleanup() { cancelAnimationFrame(animationFrameId); @@ -471,7 +479,7 @@ export function onCleanup() { window.removeEventListener('mousemove', mouseMoveEventListener); window.removeEventListener('touchend', touchEndEventListener); window.removeEventListener('touchmove', touchMoveEventListener); - resizeObserver.disconnect(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/rendering/xor-dev-centrifuge-2/index.html b/apps/typegpu-docs/src/examples/rendering/xor-dev-centrifuge-2/index.html index 1edc7df14f..56c069588f 100644 --- a/apps/typegpu-docs/src/examples/rendering/xor-dev-centrifuge-2/index.html +++ b/apps/typegpu-docs/src/examples/rendering/xor-dev-centrifuge-2/index.html @@ -1,4 +1,4 @@ - +

Port of "Centrifuge 2" by XorDev ( +

Port of "Runner" by XorDev ( + diff --git a/apps/typegpu-docs/src/examples/simple/gradient-tiles/index.ts b/apps/typegpu-docs/src/examples/simple/gradient-tiles/index.ts index dbda082b81..4989b578e6 100644 --- a/apps/typegpu-docs/src/examples/simple/gradient-tiles/index.ts +++ b/apps/typegpu-docs/src/examples/simple/gradient-tiles/index.ts @@ -33,7 +33,17 @@ function draw(spanXValue: number, spanYValue: number) { let spanX = 10; let spanY = 10; -draw(spanX, spanY); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + draw(spanX, spanY); + }, +}); // #region Example controls and cleanup @@ -62,6 +72,7 @@ export const controls = defineControls({ }); export function onCleanup() { + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simple/liquid-glass/index.html b/apps/typegpu-docs/src/examples/simple/liquid-glass/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/simple/liquid-glass/index.html +++ b/apps/typegpu-docs/src/examples/simple/liquid-glass/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simple/liquid-glass/index.ts b/apps/typegpu-docs/src/examples/simple/liquid-glass/index.ts index 7e7b5597bb..bc6a19e93d 100644 --- a/apps/typegpu-docs/src/examples/simple/liquid-glass/index.ts +++ b/apps/typegpu-docs/src/examples/simple/liquid-glass/index.ts @@ -149,8 +149,12 @@ function updatePosition(clientX: number, clientY: number) { return; } const rect = canvas.getBoundingClientRect(); - const x = (clientX - rect.left) / rect.width; - const y = (clientY - rect.top) / rect.height; + // The canvas is letterboxed with `object-contain`, so map into the drawn square + const size = Math.min(rect.width, rect.height); + const left = rect.left + (rect.width - size) / 2; + const top = rect.top + (rect.height - size) / 2; + const x = (clientX - left) / size; + const y = (clientY - top) / size; mousePosUniform.write(std.clamp(d.vec2f(x, y), d.vec2f(), d.vec2f(1))); } @@ -180,12 +184,28 @@ canvas.addEventListener('touchmove', handleTouchMove, { passive: true }); canvas.addEventListener('touchstart', handleTouchStart, { passive: true }); canvas.addEventListener('click', handleClick); -let frameId: number; function render() { - frameId = requestAnimationFrame(render); pipeline.withColorAttachment({ view: context }).draw(3); } -frameId = requestAnimationFrame(render); + +let frameId: number; +function frame() { + frameId = requestAnimationFrame(frame); + render(); +} +frameId = requestAnimationFrame(frame); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); export const controls = defineControls({ 'Rectangle dims': { @@ -305,5 +325,6 @@ export function onCleanup() { cancelAnimationFrame(frameId); } window.removeEventListener('mousemove', handleMouseMove); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simple/mesh-skinning/index.html b/apps/typegpu-docs/src/examples/simple/mesh-skinning/index.html index e14a9c47e5..2d3d24ab03 100644 --- a/apps/typegpu-docs/src/examples/simple/mesh-skinning/index.html +++ b/apps/typegpu-docs/src/examples/simple/mesh-skinning/index.html @@ -1,4 +1,4 @@ - + - -

+
+ +
+
diff --git a/apps/typegpu-docs/src/examples/simple/oklab/index.ts b/apps/typegpu-docs/src/examples/simple/oklab/index.ts index 9150cccb87..310d04ea24 100644 --- a/apps/typegpu-docs/src/examples/simple/oklab/index.ts +++ b/apps/typegpu-docs/src/examples/simple/oklab/index.ts @@ -27,8 +27,15 @@ document.addEventListener( 'mousemove', (ev) => { const rect = canvas.getBoundingClientRect(); - cssProbePosition.x = (ev.clientX - rect.x) / rect.width; - cssProbePosition.y = 1 - (ev.clientY - rect.y) / rect.height; + + const centerX = rect.x + rect.width / 2; + const centerY = rect.y + rect.height / 2; + const size = Math.min(rect.width, rect.height); + const squareLeft = centerX - size / 2; + const squareTop = centerY - size / 2; + + cssProbePosition.x = std.saturate((ev.clientX - squareLeft) / size); + cssProbePosition.y = std.saturate(1 - (ev.clientY - squareTop) / size); draw(); }, { @@ -129,8 +136,16 @@ function draw() { const chroma = pos.x; const a = chroma * Math.cos(uniformsValue.hue); const b = chroma * Math.sin(uniformsValue.hue); - cssProbe.style.setProperty('--x', `${cssProbePosition.x * 100}%`); - cssProbe.style.setProperty('--y', `${cssProbePosition.y * 100}%`); + + const rect = canvas.getBoundingClientRect(); + const centerX = rect.x + rect.width / 2; + const centerY = rect.y + rect.height / 2; + const size = Math.min(rect.width, rect.height); + const squareLeft = centerX - size / 2; + const squareTop = centerY - size / 2; + + cssProbe.style.setProperty('--x', `${squareLeft - rect.left + cssProbePosition.x * size}px`); + cssProbe.style.setProperty('--y', `${squareTop - rect.top + cssProbePosition.y * size}px`); canvas.parentElement?.style.setProperty('--l', `${lightness}`); canvas.parentElement?.style.setProperty('--a', `${a}`); canvas.parentElement?.style.setProperty('--b', `${b}`); @@ -143,9 +158,17 @@ function draw() { pipeline.withColorAttachment({ view: context }).draw(3); } -setTimeout(() => { - draw(); -}, 100); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + draw(); + }, +}); // #region Example controls and cleanup @@ -211,6 +234,7 @@ export const controls = defineControls({ }); export function onCleanup() { + autoResizer.detach(); root.destroy(); cleanupController.abort(); } diff --git a/apps/typegpu-docs/src/examples/simple/ripple-cube/index.html b/apps/typegpu-docs/src/examples/simple/ripple-cube/index.html index aa8cc321b3..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/simple/ripple-cube/index.html +++ b/apps/typegpu-docs/src/examples/simple/ripple-cube/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simple/ripple-cube/index.ts b/apps/typegpu-docs/src/examples/simple/ripple-cube/index.ts index d895f19e8f..4d82074496 100644 --- a/apps/typegpu-docs/src/examples/simple/ripple-cube/index.ts +++ b/apps/typegpu-docs/src/examples/simple/ripple-cube/index.ts @@ -1,6 +1,6 @@ import { perlin3d, randf } from '@typegpu/noise'; import * as sdf from '@typegpu/sdf'; -import { tgpu, d, std } from 'typegpu'; +import { tgpu, common, d, std } from 'typegpu'; import { setupOrbitCamera } from '../../common/setup-orbit-camera.ts'; import { createBackgroundCubemap } from './background.ts'; import { GRID_SIZE, halton, LIGHT_COUNT, MAX_DIST, MAX_STEPS, SURF_DIST } from './constants.ts'; @@ -19,7 +19,7 @@ const perlinCache = perlin3d.staticCache({ size: d.vec3u(32, 32, 32), }); -const [width, height] = [canvas.width / 1.4, canvas.height / 1.4]; +const resolutionScale = 1 / 1.4; const Camera = d.struct({ position: d.vec4f, viewProjectionInverse: d.mat4x4f }); const cameraUniform = root.createUniform(Camera); @@ -113,7 +113,12 @@ const envMapBindGroup = root.createBindGroup(envMapLayout, { }), }); -const postProcessing = createPostProcessingPipelines(root, width, height, initialBloom); +const postProcessing = createPostProcessingPipelines( + root, + canvas.width * resolutionScale, + canvas.height * resolutionScale, + initialBloom, +); const cameraResult = setupOrbitCamera( canvas, @@ -138,7 +143,7 @@ const rayMarchPipeline = root .createGuardedComputePipeline((x, y) => { 'use gpu'; randf.seed2(d.vec2f(d.f32(x), d.f32(y)).add(timeUniform.$)); - const textureSize = std.textureDimensions(postProcessing.result.writeView.$); + const textureSize = std.textureDimensions(postProcessing.$.result); const uv = d.vec2f(x, y).add(0.5).div(d.vec2f(textureSize)); const ray = getRayForUV(uv); @@ -186,7 +191,7 @@ const rayMarchPipeline = root finalColor = std.mix(fogColor, sceneColor, fog); } - std.textureStore(postProcessing.result.writeView.$, d.vec2u(x, y), d.vec4f(finalColor, 1)); + std.textureStore(postProcessing.$.result, d.vec2u(x, y), d.vec4f(finalColor, 1)); }); let animationFrame: number; @@ -196,33 +201,48 @@ let accumulatedTime = 0; let lastTimestamp = 0; let isExtendedRipplesEnabled = false; -function run(timestamp: number) { - const deltaTime = lastTimestamp === 0 ? 0 : (timestamp - lastTimestamp) / 1000; - lastTimestamp = timestamp; - const maxTime = isExtendedRipplesEnabled ? 62 : 28; - accumulatedTime = Math.min(maxTime, Math.max(0, accumulatedTime + deltaTime * timeScale)); - timeUniform.write(accumulatedTime); - - const jitterX = (halton(frameIndex % 16, 2) - 0.5) / width; - const jitterY = (halton(frameIndex % 16, 3) - 0.5) / height; +function render() { + const jitterX = (halton(frameIndex % 16, 2) - 0.5) / postProcessing.width; + const jitterY = (halton(frameIndex % 16, 3) - 0.5) / postProcessing.height; jitterUniform.write(d.vec2f(jitterX, jitterY)); frameIndex++; sdfPrecalcPipeline.dispatchThreads(GRID_SIZE / 2, GRID_SIZE / 2, GRID_SIZE / 2); - rayMarchPipeline.with(envMapBindGroup).with(sdfBindGroup).dispatchThreads(width, height); + rayMarchPipeline + .with(envMapBindGroup) + .with(sdfBindGroup) + .with(postProcessing.userGroup) + .dispatchThreads(postProcessing.width, postProcessing.height); postProcessing.runTaa(); - postProcessing.runBloom(); - postProcessing.render(context); +} + +function run(timestamp: number) { + const deltaTime = lastTimestamp === 0 ? 0 : (timestamp - lastTimestamp) / 1000; + lastTimestamp = timestamp; + const maxTime = isExtendedRipplesEnabled ? 62 : 28; + accumulatedTime = Math.min(maxTime, Math.max(0, accumulatedTime + deltaTime * timeScale)); + timeUniform.write(accumulatedTime); + + render(); animationFrame = requestAnimationFrame(run); } animationFrame = requestAnimationFrame(run); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + postProcessing.resize(canvas.width * resolutionScale, canvas.height * resolutionScale); + render(); + }, +}); + export const controls = defineControls({ metallic: { min: 0, @@ -299,5 +319,6 @@ export const controls = defineControls({ export function onCleanup() { cancelAnimationFrame(animationFrame); cameraResult.cleanupCamera(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simple/ripple-cube/post-processing.ts b/apps/typegpu-docs/src/examples/simple/ripple-cube/post-processing.ts index da14cf8b3a..2f420974c3 100644 --- a/apps/typegpu-docs/src/examples/simple/ripple-cube/post-processing.ts +++ b/apps/typegpu-docs/src/examples/simple/ripple-cube/post-processing.ts @@ -6,6 +6,10 @@ import { BloomParams } from './types.ts'; export const bloomParamsAccess = tgpu.accessor(BloomParams); +const userLayout = tgpu.bindGroupLayout({ + resultTexture: { storageTexture: d.textureStorage2d('rgba16float') }, +}); + const taaResolveLayout = tgpu.bindGroupLayout({ currentTexture: { texture: d.texture2d() }, historyTexture: { texture: d.texture2d() }, @@ -41,19 +45,11 @@ function createProcessingTexture(root: TgpuRoot, width: number, height: number) export function createPostProcessingPipelines( root: TgpuRoot, - width: number, - height: number, + initialWidth: number, + initialHeight: number, initialBloom: d.Infer, ) { const bloomUniform = root.createUniform(BloomParams, initialBloom); - const bloomWidth = Math.max(1, Math.floor(width / 2)); - const bloomHeight = Math.max(1, Math.floor(height / 2)); - - const result = createProcessingTexture(root, width, height); - const bloom = createProcessingTexture(root, bloomWidth, bloomHeight); - const blurTemp = createProcessingTexture(root, bloomWidth, bloomHeight); - const history = createProcessingTexture(root, width, height); - const taaOutput = createProcessingTexture(root, width, height); const sampler = root.createSampler({ magFilter: 'linear', @@ -63,6 +59,7 @@ export function createPostProcessingPipelines( const taaResolve = root.createGuardedComputePipeline((x, y) => { 'use gpu'; const coord = d.vec2i(d.i32(x), d.i32(y)); + const dims = std.textureDimensions(taaResolveLayout.$.currentTexture); const current = std.textureLoad(taaResolveLayout.$.currentTexture, coord, 0); const historyColor = std.textureLoad(taaResolveLayout.$.historyTexture, coord, 0); @@ -75,7 +72,7 @@ export function createPostProcessingPipelines( const clampedCoord = std.clamp( sampleCoord, d.vec2i(0, 0), - d.vec2i(d.i32(width) - 1, d.i32(height) - 1), + d.vec2i(d.i32(dims.x) - 1, d.i32(dims.y) - 1), ); const neighbor = std.textureLoad(taaResolveLayout.$.currentTexture, clampedCoord, 0).rgb; minColor = std.min(minColor, neighbor); @@ -144,56 +141,116 @@ export function createPostProcessingPipelines( fragment: fragmentMain, }); - const taaBindGroup = root.createBindGroup(taaResolveLayout, { - currentTexture: result.sampleView, - historyTexture: history.sampleView, - outputTexture: taaOutput.writeView, - }); - - const copyBindGroup = root.createBindGroup(processLayout, { - inputTexture: taaOutput.sampleView, - outputTexture: history.writeView, - sampler, - }); - - const extractBindGroup = root.createBindGroup(processLayout, { - inputTexture: result.sampleView, - outputTexture: bloom.writeView, - sampler, - }); - - const blurHorizontalBindGroup = root.createBindGroup(processLayout, { - inputTexture: bloom.sampleView, - outputTexture: blurTemp.writeView, - sampler, - }); - - const blurVerticalBindGroup = root.createBindGroup(processLayout, { - inputTexture: blurTemp.sampleView, - outputTexture: bloom.writeView, - sampler, - }); - - const compositeBindGroup = root.createBindGroup(compositeLayout, { - colorTexture: taaOutput.sampleView, - bloomTexture: bloom.sampleView, - sampler, - }); + // Textures and dispatches require whole, non-zero sizes + let width = Math.max(1, Math.floor(initialWidth)); + let height = Math.max(1, Math.floor(initialHeight)); + function createResolutionDependantResources() { + const bloomWidth = Math.max(1, Math.floor(width / 2)); + const bloomHeight = Math.max(1, Math.floor(height / 2)); + + const result = createProcessingTexture(root, width, height); + const bloom = createProcessingTexture(root, bloomWidth, bloomHeight); + const blurTemp = createProcessingTexture(root, bloomWidth, bloomHeight); + const history = createProcessingTexture(root, width, height); + const taaOutput = createProcessingTexture(root, width, height); + + return { + bloomWidth, + bloomHeight, + textures: [result, bloom, blurTemp, history, taaOutput].map((t) => t.texture), + + taaBindGroup: root.createBindGroup(taaResolveLayout, { + currentTexture: result.sampleView, + historyTexture: history.sampleView, + outputTexture: taaOutput.writeView, + }), + + copyBindGroup: root.createBindGroup(processLayout, { + inputTexture: taaOutput.sampleView, + outputTexture: history.writeView, + sampler, + }), + + extractBindGroup: root.createBindGroup(processLayout, { + inputTexture: result.sampleView, + outputTexture: bloom.writeView, + sampler, + }), + + blurHorizontalBindGroup: root.createBindGroup(processLayout, { + inputTexture: bloom.sampleView, + outputTexture: blurTemp.writeView, + sampler, + }), + + blurVerticalBindGroup: root.createBindGroup(processLayout, { + inputTexture: blurTemp.sampleView, + outputTexture: bloom.writeView, + sampler, + }), + + compositeBindGroup: root.createBindGroup(compositeLayout, { + colorTexture: taaOutput.sampleView, + bloomTexture: bloom.sampleView, + sampler, + }), + + userBindGroup: root.createBindGroup(userLayout, { + resultTexture: result.writeView, + }), + }; + } + + let resources = createResolutionDependantResources(); return { - result, bloomUniform, + get $() { + return { + result: userLayout.$.resultTexture, + }; + }, + get userGroup() { + return resources.userBindGroup; + }, + get width() { + return width; + }, + get height() { + return height; + }, + resize: (newWidth: number, newHeight: number) => { + newWidth = Math.max(1, Math.floor(newWidth)); + newHeight = Math.max(1, Math.floor(newHeight)); + if (newWidth !== width || newHeight !== height) { + width = newWidth; + height = newHeight; + for (const texture of resources.textures) { + texture.destroy(); + } + resources = createResolutionDependantResources(); + } + }, runTaa: () => { - taaResolve.with(taaBindGroup).dispatchThreads(width, height); - copyToHistory.with(copyBindGroup).dispatchThreads(width, height); + taaResolve.with(resources.taaBindGroup).dispatchThreads(width, height); + copyToHistory.with(resources.copyBindGroup).dispatchThreads(width, height); }, runBloom: () => { - extractBright.with(extractBindGroup).dispatchThreads(bloomWidth, bloomHeight); - blurHorizontal.with(blurHorizontalBindGroup).dispatchThreads(bloomWidth, bloomHeight); - blurVertical.with(blurVerticalBindGroup).dispatchThreads(bloomWidth, bloomHeight); + extractBright + .with(resources.extractBindGroup) + .dispatchThreads(resources.bloomWidth, resources.bloomHeight); + blurHorizontal + .with(resources.blurHorizontalBindGroup) + .dispatchThreads(resources.bloomWidth, resources.bloomHeight); + blurVertical + .with(resources.blurVerticalBindGroup) + .dispatchThreads(resources.bloomWidth, resources.bloomHeight); }, render: (targetView: ColorAttachment['view']) => { - renderPipeline.with(compositeBindGroup).withColorAttachment({ view: targetView }).draw(3); + renderPipeline + .with(resources.compositeBindGroup) + .withColorAttachment({ view: targetView }) + .draw(3); }, }; } diff --git a/apps/typegpu-docs/src/examples/simple/square/index.html b/apps/typegpu-docs/src/examples/simple/square/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/simple/square/index.html +++ b/apps/typegpu-docs/src/examples/simple/square/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simple/square/index.ts b/apps/typegpu-docs/src/examples/simple/square/index.ts index 4d94260076..91598b6b75 100644 --- a/apps/typegpu-docs/src/examples/simple/square/index.ts +++ b/apps/typegpu-docs/src/examples/simple/square/index.ts @@ -1,4 +1,4 @@ -import { tgpu, d } from 'typegpu'; +import { tgpu, common, d } from 'typegpu'; import { defineControls } from '../../common/defineControls.ts'; const root = await tgpu.init(); @@ -61,7 +61,18 @@ const pipeline = root function render() { pipeline.with(vertexLayout, colorBuffer).withColorAttachment({ view: context }).drawIndexed(6); } -render(); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); // #region Example controls & Cleanup @@ -92,6 +103,7 @@ export const controls = defineControls({ }); export function onCleanup() { + autoResizer.detach(); root.destroy(); } // #endregion diff --git a/apps/typegpu-docs/src/examples/simple/stencil/index.html b/apps/typegpu-docs/src/examples/simple/stencil/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/simple/stencil/index.html +++ b/apps/typegpu-docs/src/examples/simple/stencil/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simple/stencil/index.ts b/apps/typegpu-docs/src/examples/simple/stencil/index.ts index 7860359473..02f65c589a 100644 --- a/apps/typegpu-docs/src/examples/simple/stencil/index.ts +++ b/apps/typegpu-docs/src/examples/simple/stencil/index.ts @@ -1,4 +1,4 @@ -import { tgpu, d } from 'typegpu'; +import { tgpu, common, d } from 'typegpu'; const root = await tgpu.init(); const canvas = document.querySelector('canvas') as HTMLCanvasElement; @@ -103,30 +103,39 @@ function frame(timestamp: number) { } frameId = requestAnimationFrame(frame); -const resizeObserver = new ResizeObserver(() => { - stencilTexture = root - .createTexture({ - size: [canvas.width, canvas.height], - format: 'stencil8', - }) - .$usage('render'); - - rotationUniform.write(d.mat2x2f.identity()); - - writeStencilPipeline - .withDepthStencilAttachment({ - view: stencilTexture, - stencilClearValue: 0, - stencilLoadOp: 'clear', - stencilStoreOp: 'store', - }) - .draw(3); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + + stencilTexture = root + .createTexture({ + size: [canvas.width, canvas.height], + format: 'stencil8', + }) + .$usage('render'); + + rotationUniform.write(d.mat2x2f.identity()); + + writeStencilPipeline + .withDepthStencilAttachment({ + view: stencilTexture, + stencilClearValue: 0, + stencilLoadOp: 'clear', + stencilStoreOp: 'store', + }) + .draw(3); + }, }); -resizeObserver.observe(canvas); export function onCleanup() { if (frameId) { cancelAnimationFrame(frameId); } + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simple/texture-gl/index.html b/apps/typegpu-docs/src/examples/simple/texture-gl/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/simple/texture-gl/index.html +++ b/apps/typegpu-docs/src/examples/simple/texture-gl/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simple/texture-gl/index.ts b/apps/typegpu-docs/src/examples/simple/texture-gl/index.ts index 886914063b..09a3d064c2 100644 --- a/apps/typegpu-docs/src/examples/simple/texture-gl/index.ts +++ b/apps/typegpu-docs/src/examples/simple/texture-gl/index.ts @@ -1,5 +1,5 @@ import { initWithGL } from '@typegpu/gl'; -import { d, std, tgpu } from 'typegpu'; +import { common, d, std, tgpu } from 'typegpu'; const root = initWithGL(); const canvas = document.querySelector('canvas') as HTMLCanvasElement; @@ -68,18 +68,33 @@ const showSurface = root.createRenderPipeline({ }, }); -let animationFrame: number; - -function frame() { - animationFrame = requestAnimationFrame(frame); - +function render() { paintSurface.withColorAttachment({ view: surfaceRenderView }).draw(3); showSurface.withColorAttachment({ view: context }).draw(3); } -animationFrame = requestAnimationFrame(frame); +let frameId: number; +function frame() { + render(); + frameId = requestAnimationFrame(frame); +} + +frameId = requestAnimationFrame(frame); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); export function onCleanup() { - cancelAnimationFrame(animationFrame); + cancelAnimationFrame(frameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simple/triangle-gl/index.html b/apps/typegpu-docs/src/examples/simple/triangle-gl/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/simple/triangle-gl/index.html +++ b/apps/typegpu-docs/src/examples/simple/triangle-gl/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simple/triangle-gl/index.ts b/apps/typegpu-docs/src/examples/simple/triangle-gl/index.ts index fb903598ec..ab38f45c77 100644 --- a/apps/typegpu-docs/src/examples/simple/triangle-gl/index.ts +++ b/apps/typegpu-docs/src/examples/simple/triangle-gl/index.ts @@ -1,4 +1,4 @@ -import { tgpu, d, std } from 'typegpu'; +import { tgpu, common, d, std } from 'typegpu'; import { initWithGL } from '@typegpu/gl'; // Constants and helper functions @@ -40,18 +40,32 @@ const pipeline = root.createRenderPipeline({ }, }); -// Setting up the canvas and drawing to it +// Setting up the canvas +const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = root.configureContext({ - canvas: document.querySelector('canvas') as HTMLCanvasElement, + canvas, alphaMode: 'premultiplied', }); -pipeline.withColorAttachment({ view: context }).draw(3); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + + // Drawing once, and then each time the canvas resizes + pipeline.withColorAttachment({ view: context }).draw(3); + }, +}); // #region Cleanup export function onCleanup() { + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simple/triangle/index.html b/apps/typegpu-docs/src/examples/simple/triangle/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/simple/triangle/index.html +++ b/apps/typegpu-docs/src/examples/simple/triangle/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simple/triangle/index.ts b/apps/typegpu-docs/src/examples/simple/triangle/index.ts index f785d16d01..b989c4a89a 100644 --- a/apps/typegpu-docs/src/examples/simple/triangle/index.ts +++ b/apps/typegpu-docs/src/examples/simple/triangle/index.ts @@ -1,22 +1,22 @@ -import { tgpu, d, std } from 'typegpu'; +import { tgpu, common, d, std } from 'typegpu'; // Constants and helper functions const purple = d.vec4f(0.769, 0.392, 1, 1); const blue = d.vec4f(0.114, 0.447, 0.941, 1); -const getGradientColor = (ratio: number) => { +function getGradientColor(ratio: number) { 'use gpu'; return std.mix(purple, blue, ratio); -}; +} -const pos = tgpu.const(d.arrayOf(d.vec2f, 3), [ +const pos = tgpu.const(d.arrayOf(d.vec2f), [ d.vec2f(0.0, 0.5), d.vec2f(-0.5, -0.5), d.vec2f(0.5, -0.5), ]); -const uv = tgpu.const(d.arrayOf(d.vec2f, 3), [ +const uv = tgpu.const(d.arrayOf(d.vec2f), [ d.vec2f(0.5, 1.0), d.vec2f(0.0, 0.0), d.vec2f(1.0, 0.0), @@ -39,18 +39,32 @@ const pipeline = root.createRenderPipeline({ }, }); -// Setting up the canvas and drawing to it +// Setting up the canvas +const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = root.configureContext({ - canvas: document.querySelector('canvas') as HTMLCanvasElement, + canvas, alphaMode: 'premultiplied', }); -pipeline.withColorAttachment({ view: context }).draw(3); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + + // Drawing once, and then each time the canvas resizes + pipeline.withColorAttachment({ view: context }).draw(3); + }, +}); // #region Cleanup export function onCleanup() { + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simple/vaporrave/index.html b/apps/typegpu-docs/src/examples/simple/vaporrave/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/simple/vaporrave/index.html +++ b/apps/typegpu-docs/src/examples/simple/vaporrave/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simple/vaporrave/index.ts b/apps/typegpu-docs/src/examples/simple/vaporrave/index.ts index ecd73f235b..8639005c14 100644 --- a/apps/typegpu-docs/src/examples/simple/vaporrave/index.ts +++ b/apps/typegpu-docs/src/examples/simple/vaporrave/index.ts @@ -1,4 +1,4 @@ -import { tgpu } from 'typegpu'; +import { tgpu, common } from 'typegpu'; import * as c from './constants.ts'; import { defineControls } from '../../common/defineControls.ts'; @@ -9,10 +9,19 @@ const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = root.configureContext({ canvas, alphaMode: 'premultiplied' }); const scene = await setupScene(root, context); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + scene.onResize(); + }, +}); + // #region Example controls and cleanup export function onCleanup() { scene.onCleanup(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simple/vaporrave/scene.ts b/apps/typegpu-docs/src/examples/simple/vaporrave/scene.ts index 034966f9d5..53e78fbb6d 100644 --- a/apps/typegpu-docs/src/examples/simple/vaporrave/scene.ts +++ b/apps/typegpu-docs/src/examples/simple/vaporrave/scene.ts @@ -138,6 +138,14 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { fragment: fragmentMain, }); + function render() { + floorAngleUniform.write(floorAngle); + sphereAngleUniform.write(sphereAngle); + resolutionUniform.write(d.vec2f(canvas.width, canvas.height)); + + renderPipeline.withColorAttachment({ view: context }).draw(3); + } + let animationFrame: number; let floorAngle = 0; let sphereAngle = 0; @@ -152,11 +160,7 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { sphereAngle += delta * sphereSpeed; sphereAngle %= c.NUM_CYCLES * Math.PI * 2; - floorAngleUniform.write(floorAngle); - sphereAngleUniform.write(sphereAngle); - resolutionUniform.write(d.vec2f(canvas.width, canvas.height)); - - renderPipeline.withColorAttachment({ view: context }).draw(3); + render(); animationFrame = requestAnimationFrame(run); } @@ -185,6 +189,9 @@ export async function setupScene(root: TgpuRoot, context: GPUCanvasContext) { fragment: fragmentMain, }); }, + onResize() { + render(); + }, onCleanup() { cancelAnimationFrame(animationFrame); }, diff --git a/apps/typegpu-docs/src/examples/simulation/boids/index.html b/apps/typegpu-docs/src/examples/simulation/boids/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/simulation/boids/index.html +++ b/apps/typegpu-docs/src/examples/simulation/boids/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simulation/boids/index.ts b/apps/typegpu-docs/src/examples/simulation/boids/index.ts index 996198b0ce..af2ca422d2 100644 --- a/apps/typegpu-docs/src/examples/simulation/boids/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/boids/index.ts @@ -1,4 +1,4 @@ -import { tgpu, d, std } from 'typegpu'; +import { tgpu, common, d, std } from 'typegpu'; import { defineControls } from '../../common/defineControls.ts'; const triangleAmount = 1000; @@ -222,13 +222,8 @@ const computeBindGroups = [0, 1].map((idx) => ); let even = false; -let animationFrameId: number; - -function frame() { - even = !even; - - simulatePipeline.with(computeBindGroups[even ? 0 : 1]).dispatchThreads(triangleAmount); +function render() { renderPipeline .withColorAttachment({ view: context, @@ -236,12 +231,34 @@ function frame() { }) .with(instanceLayout, trianglePosBuffers[even ? 1 : 0]) .draw(3, triangleAmount); +} + +let animationFrameId: number; + +function frame() { + even = !even; + + simulatePipeline.with(computeBindGroups[even ? 0 : 1]).dispatchThreads(triangleAmount); + + render(); animationFrameId = requestAnimationFrame(frame); } animationFrameId = requestAnimationFrame(frame); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); + // #region Example controls and cleanup export const controls = defineControls({ @@ -288,6 +305,7 @@ export const controls = defineControls({ export function onCleanup() { cancelAnimationFrame(animationFrameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simulation/confetti/index.html b/apps/typegpu-docs/src/examples/simulation/confetti/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/simulation/confetti/index.html +++ b/apps/typegpu-docs/src/examples/simulation/confetti/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simulation/confetti/index.ts b/apps/typegpu-docs/src/examples/simulation/confetti/index.ts index 6655c8690e..919f837274 100644 --- a/apps/typegpu-docs/src/examples/simulation/confetti/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/confetti/index.ts @@ -1,4 +1,4 @@ -import { tgpu, d, std } from 'typegpu'; +import { tgpu, common, d, std } from 'typegpu'; import { defineControls } from '../../common/defineControls.ts'; // constants @@ -151,6 +151,8 @@ const runner = (timestamp: number) => { animationFrameId = requestAnimationFrame(runner); +const autoResizer = common.attachAutoResizer({ root, canvas }); + // example controls and cleanup export const controls = defineControls({ @@ -161,5 +163,6 @@ export const controls = defineControls({ export function onCleanup() { cancelAnimationFrame(animationFrameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.html b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.html +++ b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts index 0f2651ac91..cbc22fdd32 100644 --- a/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/fluid-double-buffering/index.ts @@ -1,5 +1,5 @@ import { randf } from '@typegpu/noise'; -import { tgpu, d, std, type TgpuMutable, type TgpuReadonly } from 'typegpu'; +import { tgpu, common, d, std, type TgpuMutable, type TgpuReadonly } from 'typegpu'; import { defineControls } from '../../common/defineControls.ts'; const MAX_GRID_SIZE = 1024; @@ -537,6 +537,18 @@ const runner = (timestamp: number) => { animationFrameId = requestAnimationFrame(runner); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + primary.render(); + }, +}); + export const controls = defineControls({ 'source intensity': { initial: sourceIntensity, @@ -598,5 +610,6 @@ export const controls = defineControls({ export function onCleanup() { cancelAnimationFrame(animationFrameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.html b/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.html +++ b/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.ts b/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.ts index 45add979d1..2bc319c979 100644 --- a/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/fluid-with-atomics/index.ts @@ -518,18 +518,20 @@ const stopDrawing = () => { } }; -const pointerCellFromCanvasOffset = (offsetX: number, offsetY: number) => { - const cellSize = canvas.width / options.size; +const pointerCellFromClient = (clientX: number, clientY: number) => { + const rect = canvas.getBoundingClientRect(); + // The canvas is letterboxed with `object-contain`, so map into the drawn square + const size = Math.min(rect.width, rect.height); + const left = rect.left + (rect.width - size) / 2; + const top = rect.top + (rect.height - size) / 2; + const cellSize = size / options.size; return { - x: Math.floor((offsetX * window.devicePixelRatio) / cellSize), - y: options.size - Math.floor((offsetY * window.devicePixelRatio) / cellSize) - 1, + x: Math.floor((clientX - left) / cellSize), + y: options.size - Math.floor((clientY - top) / cellSize) - 1, }; }; -const pointerCellFromTouch = (touch: Touch) => { - const canvasPos = canvas.getBoundingClientRect(); - return pointerCellFromCanvasOffset(touch.clientX - canvasPos.left, touch.clientY - canvasPos.top); -}; +const pointerCellFromTouch = (touch: Touch) => pointerCellFromClient(touch.clientX, touch.clientY); const handleDrawing = (x: number, y: number) => { const point = { x, y }; @@ -552,7 +554,7 @@ canvas.addEventListener('contextmenu', (event) => { canvas.onmousedown = (event) => { startDrawing(event.button === 2); - const cell = pointerCellFromCanvasOffset(event.offsetX, event.offsetY); + const cell = pointerCellFromClient(event.clientX, event.clientY); handleDrawing(cell.x, cell.y); }; @@ -563,7 +565,7 @@ canvas.onmousemove = (event) => { return; } - const cell = pointerCellFromCanvasOffset(event.offsetX, event.offsetY); + const cell = pointerCellFromClient(event.clientX, event.clientY); handleDrawing(cell.x, cell.y); }; @@ -625,6 +627,18 @@ function run(timestamp: number) { } animationFrame = requestAnimationFrame(run); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + drawFrame(); + }, +}); + export const controls = defineControls({ size: { initial: 32, @@ -694,6 +708,7 @@ export const controls = defineControls({ export function onCleanup() { cancelAnimationFrame(animationFrame); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simulation/game-of-life/index.html b/apps/typegpu-docs/src/examples/simulation/game-of-life/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/simulation/game-of-life/index.html +++ b/apps/typegpu-docs/src/examples/simulation/game-of-life/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simulation/game-of-life/index.ts b/apps/typegpu-docs/src/examples/simulation/game-of-life/index.ts index 096fd5a21c..7fa7a883a5 100644 --- a/apps/typegpu-docs/src/examples/simulation/game-of-life/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/game-of-life/index.ts @@ -345,6 +345,16 @@ const stepOnce = (timestamp: number) => { ); }; +function render() { + const chosenDisplayPipeline = + chosenPipeline === 'bitpacked' ? bitpackedDisplayPipeline : displayPipeline; + + chosenDisplayPipeline + .withColorAttachment({ view: context }) + .with(displayBindGroups[1 - even]) + .draw(3); +} + let frameId: number; function frame(timestamp: number) { @@ -390,18 +400,24 @@ function frame(timestamp: number) { } } - const chosenDisplayPipeline = - chosenPipeline === 'bitpacked' ? bitpackedDisplayPipeline : displayPipeline; - - chosenDisplayPipeline - .withColorAttachment({ view: context }) - .with(displayBindGroups[1 - even]) - .draw(3); + render(); frameId = requestAnimationFrame(frame); } frameId = requestAnimationFrame(frame); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); + // #region Example controls & Cleanup export const controls = defineControls({ @@ -506,6 +522,7 @@ export const controls = defineControls({ export function onCleanup() { cancelAnimationFrame(frameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simulation/game-of-life/input.ts b/apps/typegpu-docs/src/examples/simulation/game-of-life/input.ts index bc803b4395..a7d92b175f 100644 --- a/apps/typegpu-docs/src/examples/simulation/game-of-life/input.ts +++ b/apps/typegpu-docs/src/examples/simulation/game-of-life/input.ts @@ -1,3 +1,5 @@ +import { std } from 'typegpu'; + export interface InputState { drawPos: { x: number; y: number } | null; lastDrawPos: { x: number; y: number } | null; @@ -23,10 +25,17 @@ export function setupInput(canvas: HTMLCanvasElement): InputState { y: (sy - 0.5) / state.zoomLevel + state.zoomCenter.y, }); const toScreen = (clientX: number, clientY: number) => { - const r = canvas.getBoundingClientRect(); + const rect = canvas.getBoundingClientRect(); + + const centerX = rect.x + rect.width / 2; + const centerY = rect.y + rect.height / 2; + const size = Math.min(rect.width, rect.height); + const squareLeft = centerX - size / 2; + const squareTop = centerY - size / 2; + return { - x: Math.min(1, Math.max(0, (clientX - r.left) / r.width)), - y: Math.min(1, Math.max(0, (clientY - r.top) / r.height)), + x: std.saturate((clientX - squareLeft) / size), + y: std.saturate((clientY - squareTop) / size), }; }; diff --git a/apps/typegpu-docs/src/examples/simulation/gravity/index.html b/apps/typegpu-docs/src/examples/simulation/gravity/index.html index c83e0d8622..c0cc697748 100644 --- a/apps/typegpu-docs/src/examples/simulation/gravity/index.html +++ b/apps/typegpu-docs/src/examples/simulation/gravity/index.html @@ -38,4 +38,4 @@

Controls (click to dismiss)

under CC BY 4.0

- + diff --git a/apps/typegpu-docs/src/examples/simulation/gravity/index.ts b/apps/typegpu-docs/src/examples/simulation/gravity/index.ts index 7b856893fb..561e99ec52 100644 --- a/apps/typegpu-docs/src/examples/simulation/gravity/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/gravity/index.ts @@ -1,4 +1,4 @@ -import { tgpu, d, type StorageFlag, type TgpuBindGroup, type TgpuBuffer } from 'typegpu'; +import { tgpu, common, d, type StorageFlag, type TgpuBindGroup, type TgpuBuffer } from 'typegpu'; import { computeCollisionsShader, computeGravityShader } from './compute.ts'; import { collisionBehaviors, @@ -251,15 +251,18 @@ export const controls = defineControls({ }, }); -const resizeObserver = new ResizeObserver(() => { - depthTexture.destroy(); - depthTexture = root.device.createTexture({ - size: [canvas.width, canvas.height, 1], - format: 'depth24plus', - usage: GPUTextureUsage.RENDER_ATTACHMENT, - }); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + depthTexture.destroy(); + depthTexture = root.device.createTexture({ + size: [canvas.width, canvas.height, 1], + format: 'depth24plus', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + }, }); -resizeObserver.observe(canvas); function hideHelp() { const helpElem = document.getElementById('help'); @@ -274,7 +277,7 @@ for (const eventName of ['click', 'keydown', 'wheel', 'touchstart']) { export function onCleanup() { destroyed = true; cleanupCamera(); - resizeObserver.unobserve(canvas); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.html b/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.html +++ b/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts b/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts index 7d821abc59..9e7e82a5ec 100644 --- a/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/slime-mold-3d/index.ts @@ -43,7 +43,7 @@ const Camera = d.struct({ const cameraTarget = resolution.div(2); const cameraUp = d.vec3f(0, 1, 0); const fov = (CAMERA_FOV_DEGREES * Math.PI) / 180; -const aspect = canvas.width / canvas.height; +const aspect = 1; const near = 0.1; const far = 1000.0; @@ -439,9 +439,16 @@ const renderBindGroups = [0, 1].map((i) => }), ); -let lastTime: number | null = null; let currentTexture = 0; +function render() { + renderPipeline + .withColorAttachment({ view: context }) + .with(renderBindGroups[1 - currentTexture]) + .draw(3); +} + +let lastTime: number | null = null; function frame(timestamp: number) { const deltaTime = Math.min(lastTime !== null ? (timestamp - lastTime) / 1000 : 0, 0.1); lastTime = timestamp; @@ -460,10 +467,7 @@ function frame(timestamp: number) { .with(bindGroups[currentTexture]) .dispatchWorkgroups(Math.ceil(NUM_AGENTS / AGENT_WORKGROUP_SIZE)); - renderPipeline - .withColorAttachment({ view: context }) - .with(renderBindGroups[1 - currentTexture]) - .draw(3); + render(); currentTexture = 1 - currentTexture; @@ -471,6 +475,18 @@ function frame(timestamp: number) { } requestAnimationFrame(frame); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); + // #region Example controls and cleanup let isDragging = false; @@ -620,6 +636,7 @@ export const controls = defineControls({ }); export function onCleanup() { + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simulation/slime-mold/index.html b/apps/typegpu-docs/src/examples/simulation/slime-mold/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/simulation/slime-mold/index.html +++ b/apps/typegpu-docs/src/examples/simulation/slime-mold/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simulation/slime-mold/index.ts b/apps/typegpu-docs/src/examples/simulation/slime-mold/index.ts index 46a53589d0..b386be3094 100644 --- a/apps/typegpu-docs/src/examples/simulation/slime-mold/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/slime-mold/index.ts @@ -1,5 +1,5 @@ import { randf } from '@typegpu/noise'; -import { tgpu, d, std } from 'typegpu'; +import { tgpu, common, d, std } from 'typegpu'; import { defineControls } from '../../common/defineControls.ts'; const root = await tgpu.init(); @@ -7,7 +7,7 @@ const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = root.configureContext({ canvas, alphaMode: 'premultiplied' }); const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); -const resolution = d.vec2f(canvas.width, canvas.height); +const resolution = d.vec2f(1024, 1024); const Agent = d.struct({ position: d.vec2f, @@ -217,9 +217,16 @@ const renderBindGroups = [0, 1].map((i) => }), ); -let lastTime: number | null = null; let currentTexture = 0; +function render() { + renderPipeline + .withColorAttachment({ view: context }) + .with(renderBindGroups[1 - currentTexture]) + .draw(3); +} + +let lastTime: number | null = null; function frame(now: number) { const deltaTimeValue = Math.min(lastTime !== null ? (now - lastTime) / 1000 : 0, 0.1); lastTime = now; @@ -232,10 +239,7 @@ function frame(now: number) { computePipeline.with(bindGroups[currentTexture]).dispatchWorkgroups(Math.ceil(NUM_AGENTS / 64)); - renderPipeline - .withColorAttachment({ view: context }) - .with(renderBindGroups[1 - currentTexture]) - .draw(3); + render(); currentTexture = 1 - currentTexture; @@ -243,6 +247,18 @@ function frame(now: number) { } requestAnimationFrame(frame); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); + // #region Example controls and cleanup export const controls = defineControls({ @@ -294,6 +310,7 @@ export const controls = defineControls({ }); export function onCleanup() { + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.html b/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.html index de6d9fbb40..64acde394f 100644 --- a/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.html +++ b/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.html @@ -27,4 +27,4 @@

Controls (click to dismiss)

Drag to stir the fluid.

- + diff --git a/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts b/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts index 165fac62ee..211d90dd06 100644 --- a/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/stable-fluid/index.ts @@ -1,5 +1,6 @@ import { tgpu, + common, d, type TgpuBindGroup, type TgpuComputeFn, @@ -31,9 +32,18 @@ function createComputePipeline(fn: TgpuComputeFn) { return root.createComputePipeline({ compute: fn }); } -function toGrid(x: number, y: number): [number, number] { - const gx = Math.floor((x / canvas.width) * p.SIM_N); - const gy = Math.floor(((canvas.height - y) / canvas.height) * p.SIM_N); +function toGrid(clientX: number, clientY: number): [number, number] { + const rect = canvas.getBoundingClientRect(); + + // Taking into account the square aspect ratio + const centerX = rect.x + rect.width / 2; + const centerY = rect.y + rect.height / 2; + const size = Math.min(rect.width, rect.height); + const squareLeft = centerX - size / 2; + const squareTop = centerY - size / 2; + + const gx = Math.floor(((clientX - squareLeft) / size) * p.SIM_N); + const gy = Math.floor((1 - (clientY - squareTop) / size) * p.SIM_N); return [gx, gy]; } @@ -264,10 +274,39 @@ const renderBindGroups = { ], }; +function render() { + let renderBG: TgpuBindGroup<{ + result: { texture: d.WgslTexture2d }; + background: { texture: d.WgslTexture2d }; + }>; + let pipeline: TgpuRenderPipeline; + + switch (p.params.displayMode) { + case 'ink': + renderBG = renderBindGroups.ink[inkBuffer.currentIndex]; + pipeline = renderPipelineInk; + break; + case 'image': + renderBG = renderBindGroups.ink[inkBuffer.currentIndex]; + pipeline = renderPipelineImage; + break; + case 'velocity': + renderBG = renderBindGroups.velocity[velBuffer.currentIndex]; + pipeline = renderPipelineVel; + break; + default: + throw new Error('Invalid display mode'); + } + + pipeline.withColorAttachment({ view: context }).with(renderBG).draw(3); +} + // Main rendering loop +let handle: number; function loop() { + handle = requestAnimationFrame(loop); + if (p.params.paused) { - requestAnimationFrame(loop); return; } @@ -324,43 +363,28 @@ function loop() { .dispatchWorkgroups(dispatchX, dispatchY); inkBuffer.swap(); - let renderBG: TgpuBindGroup<{ - result: { texture: d.WgslTexture2d }; - background: { texture: d.WgslTexture2d }; - }>; - let pipeline: TgpuRenderPipeline; - - switch (p.params.displayMode) { - case 'ink': - renderBG = renderBindGroups.ink[inkBuffer.currentIndex]; - pipeline = renderPipelineInk; - break; - case 'image': - renderBG = renderBindGroups.ink[inkBuffer.currentIndex]; - pipeline = renderPipelineImage; - break; - case 'velocity': - renderBG = renderBindGroups.velocity[velBuffer.currentIndex]; - pipeline = renderPipelineVel; - break; - default: - throw new Error('Invalid display mode'); - } - - pipeline.withColorAttachment({ view: context }).with(renderBG).draw(3); - - requestAnimationFrame(loop); + render(); } -loop(); +handle = requestAnimationFrame(loop); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); // #region Example controls and cleanup canvas.addEventListener('mousedown', (e) => { - const x = e.offsetX * devicePixelRatio; - const y = e.offsetY * devicePixelRatio; brushState = { - pos: toGrid(x, y), + pos: toGrid(e.clientX, e.clientY), delta: [0, 0], isDown: true, }; @@ -370,11 +394,8 @@ canvas.addEventListener( (e) => { e.preventDefault(); const touch = e.touches[0]; - const rect = canvas.getBoundingClientRect(); - const x = (touch.clientX - rect.left) * devicePixelRatio; - const y = (touch.clientY - rect.top) * devicePixelRatio; brushState = { - pos: toGrid(x, y), + pos: toGrid(touch.clientX, touch.clientY), delta: [0, 0], isDown: true, }; @@ -393,9 +414,7 @@ const touchEndEventListener = () => { window.addEventListener('touchend', touchEndEventListener); canvas.addEventListener('mousemove', (e) => { - const x = e.offsetX * devicePixelRatio; - const y = e.offsetY * devicePixelRatio; - const [newX, newY] = toGrid(x, y); + const [newX, newY] = toGrid(e.clientX, e.clientY); brushState.delta = [newX - brushState.pos[0], newY - brushState.pos[1]]; brushState.pos = [newX, newY]; }); @@ -404,10 +423,7 @@ canvas.addEventListener( (e) => { e.preventDefault(); const touch = e.touches[0]; - const rect = canvas.getBoundingClientRect(); - const x = (touch.clientX - rect.left) * devicePixelRatio; - const y = (touch.clientY - rect.top) * devicePixelRatio; - const [newX, newY] = toGrid(x, y); + const [newX, newY] = toGrid(touch.clientX, touch.clientY); brushState.delta = [newX - brushState.pos[0], newY - brushState.pos[1]]; brushState.pos = [newX, newY]; }, @@ -476,6 +492,8 @@ export const controls = defineControls({ export function onCleanup() { window.removeEventListener('mouseup', mouseUpEventListener); window.removeEventListener('touchend', touchEndEventListener); + autoResizer.detach(); + cancelAnimationFrame(handle); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/simulation/wind-map/index.html b/apps/typegpu-docs/src/examples/simulation/wind-map/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/simulation/wind-map/index.html +++ b/apps/typegpu-docs/src/examples/simulation/wind-map/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/simulation/wind-map/index.ts b/apps/typegpu-docs/src/examples/simulation/wind-map/index.ts index 8dc74d5f09..866c3b18c4 100644 --- a/apps/typegpu-docs/src/examples/simulation/wind-map/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/wind-map/index.ts @@ -6,7 +6,7 @@ import { polylineVariableWidth, startCapSlot, } from '@typegpu/geometry'; -import { tgpu } from 'typegpu'; +import { tgpu, common } from 'typegpu'; import { arrayOf, builtin, f32, i32, struct, u16, u32, vec2f, vec4f } from 'typegpu/data'; import { clamp, mix, normalize, select } from 'typegpu/std'; import { defineControls } from '../../common/defineControls.ts'; @@ -267,6 +267,18 @@ const runAnimationFrame = () => { }; runAnimationFrame(); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + draw(); + }, +}); + // #region Example controls & Cleanup export const controls = defineControls({ @@ -279,6 +291,7 @@ export const controls = defineControls({ }); export function onCleanup() { + autoResizer.detach(); root.destroy(); root.device.destroy(); cancelAnimationFrame(frameId); diff --git a/apps/typegpu-docs/src/examples/tests/sdf-docs-examples/index.html b/apps/typegpu-docs/src/examples/tests/sdf-docs-examples/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/tests/sdf-docs-examples/index.html +++ b/apps/typegpu-docs/src/examples/tests/sdf-docs-examples/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/tests/sdf-docs-examples/index.ts b/apps/typegpu-docs/src/examples/tests/sdf-docs-examples/index.ts index 300a021013..02a826861d 100644 --- a/apps/typegpu-docs/src/examples/tests/sdf-docs-examples/index.ts +++ b/apps/typegpu-docs/src/examples/tests/sdf-docs-examples/index.ts @@ -285,12 +285,28 @@ const pipeline = root.createRenderPipeline({ targets: { format: presentationFormat }, }); +function render() { + pipeline.withColorAttachment({ view: context }).draw(3); +} + let frameId = requestAnimationFrame(frame); function frame() { - pipeline.withColorAttachment({ view: context }).draw(3); + render(); frameId = requestAnimationFrame(frame); } +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); + function setSnippet(snippet: SdfSnippet) { snippetMode.write(SDF_SNIPPETS.indexOf(snippet)); } @@ -304,6 +320,7 @@ export const controls = defineControls({ }); export function onCleanup() { + autoResizer.detach(); cancelAnimationFrame(frameId); floodRunner.destroy(); sourceTexture.destroy(); diff --git a/apps/typegpu-docs/src/examples/tests/texture-compose/index.html b/apps/typegpu-docs/src/examples/tests/texture-compose/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/tests/texture-compose/index.html +++ b/apps/typegpu-docs/src/examples/tests/texture-compose/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/tests/texture-compose/index.ts b/apps/typegpu-docs/src/examples/tests/texture-compose/index.ts index fb4219109c..67e9f43109 100644 --- a/apps/typegpu-docs/src/examples/tests/texture-compose/index.ts +++ b/apps/typegpu-docs/src/examples/tests/texture-compose/index.ts @@ -76,10 +76,26 @@ const pipeline = root.createRenderPipeline({ function render() { pipeline.withColorAttachment({ view: context }).draw(3); +} - requestAnimationFrame(render); +let frameId: number; +function frame() { + render(); + frameId = requestAnimationFrame(frame); } -requestAnimationFrame(render); +frameId = requestAnimationFrame(frame); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); const channelOptions = ['r', 'g', 'b', 'a', 'none'] as const; @@ -140,5 +156,7 @@ export const controls = defineControls({ }); export function onCleanup() { + cancelAnimationFrame(frameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/tests/texture-mipmaps/index.html b/apps/typegpu-docs/src/examples/tests/texture-mipmaps/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/tests/texture-mipmaps/index.html +++ b/apps/typegpu-docs/src/examples/tests/texture-mipmaps/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/tests/texture-mipmaps/index.ts b/apps/typegpu-docs/src/examples/tests/texture-mipmaps/index.ts index 98669a82f7..00a318bf34 100644 --- a/apps/typegpu-docs/src/examples/tests/texture-mipmaps/index.ts +++ b/apps/typegpu-docs/src/examples/tests/texture-mipmaps/index.ts @@ -107,10 +107,26 @@ writeLayerImages(); function render() { pipeline.withColorAttachment({ view: context }).draw(3); +} - requestAnimationFrame(render); +let frameId: number; +function frame() { + render(); + frameId = requestAnimationFrame(frame); } -requestAnimationFrame(render); +frameId = requestAnimationFrame(frame); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); export const controls = defineControls({ 'Mip level': { @@ -163,5 +179,7 @@ export const controls = defineControls({ }); export function onCleanup() { + cancelAnimationFrame(frameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/tests/texture-write-3d/index.html b/apps/typegpu-docs/src/examples/tests/texture-write-3d/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/tests/texture-write-3d/index.html +++ b/apps/typegpu-docs/src/examples/tests/texture-write-3d/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/tests/texture-write-3d/index.ts b/apps/typegpu-docs/src/examples/tests/texture-write-3d/index.ts index 44f3c4d6df..8578636f76 100644 --- a/apps/typegpu-docs/src/examples/tests/texture-write-3d/index.ts +++ b/apps/typegpu-docs/src/examples/tests/texture-write-3d/index.ts @@ -172,12 +172,28 @@ const pipeline = root.createRenderPipeline({ writeVolumeDirect(); function render() { - angleUniform.write((performance.now() / 1000) * 0.4); pipeline.withColorAttachment({ view: context }).draw(3); +} - requestAnimationFrame(render); +let frameId: number; +function frame() { + angleUniform.write((performance.now() / 1000) * 0.4); + render(); + frameId = requestAnimationFrame(frame); } -requestAnimationFrame(render); +frameId = requestAnimationFrame(frame); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); export const controls = defineControls({ 'Mip level (1 = max)': { @@ -222,5 +238,7 @@ export const controls = defineControls({ }); export function onCleanup() { + cancelAnimationFrame(frameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/tests/texture-write/index.html b/apps/typegpu-docs/src/examples/tests/texture-write/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/tests/texture-write/index.html +++ b/apps/typegpu-docs/src/examples/tests/texture-write/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/tests/texture-write/index.ts b/apps/typegpu-docs/src/examples/tests/texture-write/index.ts index 7db8cef7b8..b7a92f925a 100644 --- a/apps/typegpu-docs/src/examples/tests/texture-write/index.ts +++ b/apps/typegpu-docs/src/examples/tests/texture-write/index.ts @@ -137,10 +137,26 @@ function rebuild() { function render() { viewer.pipeline.with(bindGroup).withColorAttachment({ view: context }).draw(3); +} - requestAnimationFrame(render); +let frameId: number; +function frame() { + render(); + frameId = requestAnimationFrame(frame); } -requestAnimationFrame(render); +frameId = requestAnimationFrame(frame); + +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + render(); + }, +}); export const controls = defineControls({ Format: { @@ -237,5 +253,7 @@ export const controls = defineControls({ }); export function onCleanup() { + cancelAnimationFrame(frameId); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/tests/triangle-targets/index.html b/apps/typegpu-docs/src/examples/tests/triangle-targets/index.html index aa8cc321b3..f51033d4b0 100644 --- a/apps/typegpu-docs/src/examples/tests/triangle-targets/index.html +++ b/apps/typegpu-docs/src/examples/tests/triangle-targets/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/tests/triangle-targets/index.ts b/apps/typegpu-docs/src/examples/tests/triangle-targets/index.ts index adc2a914c5..498d1781c7 100644 --- a/apps/typegpu-docs/src/examples/tests/triangle-targets/index.ts +++ b/apps/typegpu-docs/src/examples/tests/triangle-targets/index.ts @@ -10,13 +10,9 @@ const context = root.configureContext({ format, }); -const secondTexture = root - .createTexture({ - format: 'bgra8unorm', - size: [canvas.clientWidth * devicePixelRatio, canvas.clientHeight * devicePixelRatio, 1], - }) - .$usage('render', 'sampled'); -const secondTextureView = secondTexture.createView(d.texture2d()); +const secondTextureLayout = tgpu.bindGroupLayout({ + secondTexture: { texture: d.texture2d() }, +}); const sampler = root.createSampler({}); // A pipeline with two targets. @@ -45,7 +41,7 @@ const blitPipeline = root.createRenderPipeline({ vertex: common.fullScreenTriangle, fragment: ({ uv }) => { 'use gpu'; - const value = std.textureSample(secondTextureView.$, sampler.$, 1 - uv); + const value = std.textureSample(secondTextureLayout.$.secondTexture, sampler.$, 1 - uv); if (std.any(std.gt(value, d.vec4f(0)))) { return value; } @@ -62,18 +58,49 @@ const blitPipeline = root.createRenderPipeline({ // Run both pipelines -pipeline - .withColorAttachment({ - workTexture: { view: secondTexture.createView('render') }, - canvas: { view: context }, - }) - .draw(3); +function render() { + const secondTexture = root + .createTexture({ + format: 'bgra8unorm', + size: [canvas.width, canvas.height], + }) + .$usage('render', 'sampled'); + const secondTextureBindGroup = root.createBindGroup(secondTextureLayout, { + secondTexture, + }); + + pipeline + .withColorAttachment({ + workTexture: { view: secondTexture.createView('render') }, + canvas: { view: context }, + }) + .draw(3); + + blitPipeline + .with(secondTextureBindGroup) + .withColorAttachment({ view: context, loadOp: 'load' }) + .draw(3); + + secondTexture.destroy(); +} -blitPipeline.withColorAttachment({ view: context, loadOp: 'load' }).draw(3); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + // Keeping the aspect ratio 1:1 + const size = Math.min(canvas.width, canvas.height); + canvas.width = size; + canvas.height = size; + + render(); + }, +}); // #region Cleanup export function onCleanup() { + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/tests/uniformity/index.html b/apps/typegpu-docs/src/examples/tests/uniformity/index.html index aa8cc321b3..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/tests/uniformity/index.html +++ b/apps/typegpu-docs/src/examples/tests/uniformity/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/tests/uniformity/index.ts b/apps/typegpu-docs/src/examples/tests/uniformity/index.ts index b0a9e9ca70..7b8edc9346 100644 --- a/apps/typegpu-docs/src/examples/tests/uniformity/index.ts +++ b/apps/typegpu-docs/src/examples/tests/uniformity/index.ts @@ -295,8 +295,18 @@ export const controls = defineControls({ }, }); +const autoResizer = common.attachAutoResizer({ + root, + canvas, + onResize() { + configUniform.patch({ canvasRatio: canvas.width / canvas.height }); + redraw(); + }, +}); + export function onCleanup() { cleanupCamera(); + autoResizer.detach(); root.destroy(); } diff --git a/apps/typegpu-docs/src/examples/threejs/attractors/index.html b/apps/typegpu-docs/src/examples/threejs/attractors/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/threejs/attractors/index.html +++ b/apps/typegpu-docs/src/examples/threejs/attractors/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/threejs/compute-cloth/index.html b/apps/typegpu-docs/src/examples/threejs/compute-cloth/index.html index 581d6789f8..a1d12c864f 100644 --- a/apps/typegpu-docs/src/examples/threejs/compute-cloth/index.html +++ b/apps/typegpu-docs/src/examples/threejs/compute-cloth/index.html @@ -1 +1 @@ - + diff --git a/apps/typegpu-docs/src/examples/threejs/compute-geometry/index.html b/apps/typegpu-docs/src/examples/threejs/compute-geometry/index.html index 53fabe2eb9..037ff50a4a 100644 --- a/apps/typegpu-docs/src/examples/threejs/compute-geometry/index.html +++ b/apps/typegpu-docs/src/examples/threejs/compute-geometry/index.html @@ -1,5 +1,5 @@
Model is loading...
- +