From 5035c4edcd5c8c9afc2bb784c7883a464b3aa043 Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Sat, 15 Nov 2025 01:28:08 +0100 Subject: [PATCH 01/17] temp not working --- .../point-light-shadow/box-geometry.ts | 172 ++++++ .../rendering/point-light-shadow/camera.ts | 78 +++ .../rendering/point-light-shadow/index.html | 1 + .../rendering/point-light-shadow/index.ts | 504 ++++++++++++++++++ .../rendering/point-light-shadow/meta.json | 5 + .../rendering/point-light-shadow/types.ts | 16 + .../src/core/pipeline/renderPipeline.ts | 10 +- packages/typegpu/src/core/texture/texture.ts | 3 + packages/typegpu/src/data/texture.ts | 8 +- 9 files changed, 792 insertions(+), 5 deletions(-) create mode 100644 apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts create mode 100644 apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts create mode 100644 apps/typegpu-docs/src/examples/rendering/point-light-shadow/index.html create mode 100644 apps/typegpu-docs/src/examples/rendering/point-light-shadow/index.ts create mode 100644 apps/typegpu-docs/src/examples/rendering/point-light-shadow/meta.json create mode 100644 apps/typegpu-docs/src/examples/rendering/point-light-shadow/types.ts diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts new file mode 100644 index 0000000000..6fc7a2b233 --- /dev/null +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts @@ -0,0 +1,172 @@ +import * as d from 'typegpu/data'; +import type { GeometryData } from './types.ts'; +import { VertexData } from './types.ts'; + +export class BoxGeometry { + #modelMatrix: d.m4x4f; + #position: d.v3f; + #scale: d.v3f; + #rotation: d.v3f; + #size: [number, number, number]; + #vertices: GeometryData; + #indices: number[]; + + constructor(size: [number, number, number] = [1, 1, 1]) { + this.#modelMatrix = d.mat4x4f.identity(); + this.#position = d.vec3f(0, 0, 0); + this.#scale = d.vec3f(1, 1, 1); + this.#rotation = d.vec3f(0, 0, 0); + this.#size = size; + this.#vertices = []; + this.#indices = []; + + this.#generateGeometry(); + } + + set position(value: d.v3f) { + this.#position = value; + this.#updateModelMatrix(); + } + + set scale(value: d.v3f) { + this.#scale = value; + this.#updateModelMatrix(); + } + + set rotation(value: d.v3f) { + this.#rotation = value; + this.#updateModelMatrix(); + } + + get modelMatrix(): d.m4x4f { + return this.#modelMatrix; + } + + get vertices(): GeometryData { + return this.#vertices; + } + + get indices(): number[] { + return this.#indices; + } + + #generateGeometry() { + const [w, h, d] = this.#size; + const halfW = w / 2; + const halfH = h / 2; + const halfD = d / 2; + + this.#vertices = []; + this.#indices = []; + + // Front face (+Z) + this.#addFace( + [ + [-halfW, -halfH, halfD], + [halfW, -halfH, halfD], + [halfW, halfH, halfD], + [-halfW, halfH, halfD], + ], + [0, 0, 1], + [[0, 0], [1, 0], [1, 1], [0, 1]], + ); + + // Back face (-Z) + this.#addFace( + [ + [halfW, -halfH, -halfD], + [-halfW, -halfH, -halfD], + [-halfW, halfH, -halfD], + [halfW, halfH, -halfD], + ], + [0, 0, -1], + [[0, 0], [1, 0], [1, 1], [0, 1]], + ); + + // Right face (+X) + this.#addFace( + [ + [halfW, -halfH, -halfD], + [halfW, -halfH, halfD], + [halfW, halfH, halfD], + [halfW, halfH, -halfD], + ], + [1, 0, 0], + [[0, 0], [1, 0], [1, 1], [0, 1]], + ); + + // Left face (-X) + this.#addFace( + [ + [-halfW, -halfH, halfD], + [-halfW, -halfH, -halfD], + [-halfW, halfH, -halfD], + [-halfW, halfH, halfD], + ], + [-1, 0, 0], + [[0, 0], [1, 0], [1, 1], [0, 1]], + ); + + // Top face (+Y) + this.#addFace( + [ + [-halfW, halfH, halfD], + [halfW, halfH, halfD], + [halfW, halfH, -halfD], + [-halfW, halfH, -halfD], + ], + [0, 1, 0], + [[0, 0], [1, 0], [1, 1], [0, 1]], + ); + + // Bottom face (-Y) + this.#addFace( + [ + [-halfW, -halfH, -halfD], + [halfW, -halfH, -halfD], + [halfW, -halfH, halfD], + [-halfW, -halfH, halfD], + ], + [0, -1, 0], + [[0, 0], [1, 0], [1, 1], [0, 1]], + ); + } + + #addFace( + positions: [number, number, number][], + normal: [number, number, number], + uvs: [number, number][], + ) { + const startIndex = this.#vertices.length; + + positions.forEach((pos, i) => { + this.#vertices.push( + VertexData({ + position: d.vec3f(...pos), + normal: d.vec3f(...normal), + uv: d.vec2f(...uvs[i]), + }), + ); + }); + + // Two triangles per face + this.#indices.push(startIndex, startIndex + 1, startIndex + 2); + this.#indices.push(startIndex, startIndex + 2, startIndex + 3); + } + + #updateModelMatrix() { + const translationMatrix = d.mat4x4f.translation(this.#position); + const rotationXMatrix = d.mat4x4f.rotationX(this.#rotation.x); + const rotationYMatrix = d.mat4x4f.rotationY(this.#rotation.y); + const rotationZMatrix = d.mat4x4f.rotationZ(this.#rotation.z); + const scaleMatrix = d.mat4x4f.scaling(this.#scale); + + this.#modelMatrix = translationMatrix.mul( + rotationZMatrix.mul( + rotationYMatrix.mul( + rotationXMatrix.mul(scaleMatrix), + ), + ), + ); + } +} diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts new file mode 100644 index 0000000000..0ff7349b34 --- /dev/null +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts @@ -0,0 +1,78 @@ +import * as d from 'typegpu/data'; +import * as m from 'wgpu-matrix'; +import { CameraData } from './types.ts'; + +export class Camera { + #viewProjectionMatrix: d.m4x4f; + #position: d.v3f; + #target: d.v3f; + #up: d.v3f; + #fov: number; + #near: number; + #far: number; + #inverseViewProjectionMatrix: d.m4x4f; + #data: d.Infer; + + constructor(fov: number = 60, near: number = 0.1, far: number = 1000) { + this.#viewProjectionMatrix = d.mat4x4f.identity(); + this.#position = d.vec3f(0, 0, 0); + this.#target = d.vec3f(0, 0, -1); + this.#up = d.vec3f(0, 1, 0); + this.#fov = fov; + this.#near = near; + this.#far = far; + this.#inverseViewProjectionMatrix = d.mat4x4f.identity(); + this.#data = CameraData({ + viewProjectionMatrix: this.#viewProjectionMatrix, + inverseViewProjectionMatrix: this.#inverseViewProjectionMatrix, + }); + } + + set position(pos: d.v3f) { + this.#position = pos; + this.#recompute(); + } + + set target(tgt: d.v3f) { + this.#target = tgt; + this.#recompute(); + } + + set up(upVec: d.v3f) { + this.#up = upVec; + this.#recompute(); + } + + set fov(fovDegrees: number) { + this.#fov = fovDegrees; + this.#recompute(); + } + + get data(): d.Infer { + return this.#data; + } + + #recompute() { + const view = m.mat4.lookAt( + this.#position, + this.#target, + this.#up, + d.mat4x4f(), + ); + const projection = m.mat4.perspective( + (this.#fov * Math.PI) / 180, + 1, + this.#near, + this.#far, + d.mat4x4f(), + ); + this.#viewProjectionMatrix = m.mat4.mul(projection, view, d.mat4x4f()); + this.#inverseViewProjectionMatrix = m.mat4.invert( + this.#viewProjectionMatrix, + ); + this.#data = CameraData({ + viewProjectionMatrix: this.#viewProjectionMatrix, + inverseViewProjectionMatrix: this.#inverseViewProjectionMatrix, + }); + } +} 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 new file mode 100644 index 0000000000..aa8cc321b3 --- /dev/null +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/index.html @@ -0,0 +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 new file mode 100644 index 0000000000..ddd5ba04ec --- /dev/null +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/index.ts @@ -0,0 +1,504 @@ +import tgpu from 'typegpu'; +import * as d from 'typegpu/data'; +import * as std from 'typegpu/std'; +import { Camera } from './camera.ts'; +import { BoxGeometry } from './box-geometry.ts'; +import { CameraData, VertexData } from './types.ts'; +import { fullScreenTriangle } from 'typegpu/common'; + +const root = await tgpu.init(); +const device = root.device; +const canvas = document.querySelector('canvas') as HTMLCanvasElement; +const context = canvas.getContext('webgpu') as GPUCanvasContext; + +const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + +context.configure({ + device, + format: presentationFormat, +}); + +const mainCamera = new Camera(); +mainCamera.position = d.vec3f(10, 10, 10); +mainCamera.target = d.vec3f(0, 0, 0); +const cameraUniform = root.createUniform(CameraData, mainCamera.data); + +const cube = new BoxGeometry(); +const floorCube = new BoxGeometry(); +floorCube.scale = d.vec3f(10, 0.1, 10); +floorCube.position = d.vec3f(0, -0.5, 0); + +const modelMatrixUniform = root.createUniform(d.mat4x4f); +const pointLightWorldPosition = d.vec3f(2, 1, 0); + +const shadowCameras = { + right: new Camera(90, 0.1, 25), // +X + left: new Camera(90, 0.1, 25), // -X + up: new Camera(90, 0.1, 25), // +Y + down: new Camera(90, 0.1, 25), // -Y + forward: new Camera(90, 0.1, 25), // +Z + backward: new Camera(90, 0.1, 25), // -Z +}; + +// Configure each camera for cubemap faces +// Right face (+X) +shadowCameras.right.position = pointLightWorldPosition; +shadowCameras.right.target = pointLightWorldPosition.add(d.vec3f(1, 0, 0)); +shadowCameras.right.up = d.vec3f(0, -1, 0); + +// Left face (-X) +shadowCameras.left.position = pointLightWorldPosition; +shadowCameras.left.target = pointLightWorldPosition.add(d.vec3f(-1, 0, 0)); +shadowCameras.left.up = d.vec3f(0, -1, 0); + +// Up face (+Y) +shadowCameras.up.position = pointLightWorldPosition; +shadowCameras.up.target = pointLightWorldPosition.add(d.vec3f(0, 1, 0)); +shadowCameras.up.up = d.vec3f(0, 0, 1); + +// Down face (-Y) +shadowCameras.down.position = pointLightWorldPosition; +shadowCameras.down.target = pointLightWorldPosition.add(d.vec3f(0, -1, 0)); +shadowCameras.down.up = d.vec3f(0, 0, -1); + +// Forward face (+Z) +shadowCameras.forward.position = pointLightWorldPosition; +shadowCameras.forward.target = pointLightWorldPosition.add(d.vec3f(0, 0, 1)); +shadowCameras.forward.up = d.vec3f(0, -1, 0); + +// Backward face (-Z) +shadowCameras.backward.position = pointLightWorldPosition; +shadowCameras.backward.target = pointLightWorldPosition.add(d.vec3f(0, 0, -1)); +shadowCameras.backward.up = d.vec3f(0, -1, 0); + +const cameraDepthCube = root['~unstable'].createTexture({ + size: [512, 512, 6], + dimension: '2d', + format: 'depth24plus', +}).$usage('render', 'sampled'); + +const vertexData = tgpu.vertexLayout(d.arrayOf(VertexData)); +const cubeVertexBuffer = root + .createBuffer( + vertexData.schemaForCount(cube.vertices.length), + cube.vertices, + ) + .$usage('vertex'); +const cubeIndexBuffer = root + .createBuffer(d.arrayOf(d.u16, cube.indices.length), cube.indices) + .$usage('index'); + +const floorVertexBuffer = root + .createBuffer( + vertexData.schemaForCount(floorCube.vertices.length), + floorCube.vertices, + ) + .$usage('vertex'); +const floorIndexBuffer = root + .createBuffer(d.arrayOf(d.u16, floorCube.indices.length), floorCube.indices) + .$usage('index'); + +let depthTexture = root['~unstable'] + .createTexture({ + size: [canvas.width, canvas.height], + format: 'depth24plus', + }) + .$usage('render'); + +const renderLayout = tgpu.bindGroupLayout({ + camera: { uniform: CameraData }, + modelMatrix: { uniform: d.mat4x4f }, +}); + +const renderLayoutWithShadow = tgpu.bindGroupLayout({ + camera: { uniform: CameraData }, + modelMatrix: { uniform: d.mat4x4f }, + shadowDepthCube: { texture: d.textureDepthCube() }, + shadowSampler: { sampler: 'comparison' }, + lightPosition: { uniform: d.vec3f }, +}); + +const debugSampler = root['~unstable'].createSampler({ + minFilter: 'nearest', + magFilter: 'nearest', +}); + +const debugView = cameraDepthCube.createView(d.textureDepth2dArray(), { + baseArrayLayer: 0, + arrayLayerCount: 6, + aspect: 'depth-only', +}); + +const debugFragment = tgpu['~unstable'].fragmentFn({ + in: { uv: d.vec2f }, + out: d.vec4f, +})(({ uv }) => { + const localUV = d.vec2f( + std.fract(uv.x * 3), + std.fract(uv.y * 2), + ); + + const col = std.floor(uv.x * 3); + const row = std.floor(uv.y * 2); + const arrayIndex = d.i32(row * 3 + col); + + const depth = std.textureSample( + debugView.$, + debugSampler.$, + localUV, + arrayIndex, + ); + const remappedDepth = std.pow(depth, 8.0); + return d.vec4f(d.vec3f(remappedDepth), 1.0); +}); + +const debugPipeline = root['~unstable'] + .withVertex(fullScreenTriangle, {}) + .withFragment(debugFragment, { format: presentationFormat }) + .createPipeline(); + +const vertexDepth = tgpu['~unstable'].vertexFn({ + in: { + ...VertexData.propTypes, + }, + out: { + pos: d.builtin.position, + }, +})(({ position }) => { + const worldPos = renderLayout.$.modelMatrix.mul(d.vec4f(position, 1)).xyz; + const pos = renderLayout.$.camera.viewProjectionMatrix.mul( + d.vec4f(worldPos, 1), + ); + + return { + pos, + }; +}); + +const vertexMain = tgpu['~unstable'].vertexFn({ + in: { + ...VertexData.propTypes, + }, + out: { + pos: d.builtin.position, + worldPos: d.vec3f, + uv: d.vec2f, + normal: d.vec3f, + }, +})(({ position, uv, normal }) => { + const worldPos = + renderLayoutWithShadow.$.modelMatrix.mul(d.vec4f(position, 1)).xyz; + const pos = renderLayoutWithShadow.$.camera.viewProjectionMatrix.mul( + d.vec4f(worldPos, 1), + ); + + return { + pos, + worldPos, + uv, + normal, + }; +}); + +const debugUniform = root.createUniform(d.f32, 0.0); + +const fragmentMain = tgpu['~unstable'].fragmentFn({ + in: { + worldPos: d.vec3f, + uv: d.vec2f, + normal: d.vec3f, + }, + out: d.vec4f, +})(({ worldPos, normal }) => { + // calculate light direction + const lightPos = renderLayoutWithShadow.$.lightPosition; + const lightDir = std.normalize(lightPos.sub(worldPos)); + + // diffuse shading + const diff = std.max(std.dot(normal, lightDir), 0.0); + + // Shadow calculation + // Direction from fragment to light (for cubemap sampling - points away from center) + const fragToLight = worldPos.sub(lightPos); + const distance = std.length(fragToLight); + + // Simple linear depth normalized to [0,1] + const far = 25.0; + const normalizedDepth = distance / far; + + // Sample shadow cubemap + const shadowDepth = std.textureSampleCompare( + renderLayoutWithShadow.$.shadowDepthCube, + renderLayoutWithShadow.$.shadowSampler, + fragToLight, + normalizedDepth * debugUniform.$, + ); + const rawDepth = std.textureSample( + renderLayoutWithShadow.$.shadowDepthCube, + debugSampler.$, + fragToLight, + ); + return d.vec4f(std.pow(d.vec3f(rawDepth), d.vec3f(5.0)), 1.0); + + // return d.vec4f(d.vec3f(shadowDepth), 1.0); // Visualize shadow factor + + const baseColor = d.vec3f(1.0, 0.5, 0.31); + const ambient = 0.1; + const color = baseColor.mul(diff * shadowDepth + ambient); + + return d.vec4f(color, 1.0); +}); + +const shadowSampler = root['~unstable'].createComparisonSampler({ + compare: 'less-equal', + magFilter: 'linear', + minFilter: 'linear', +}); + +const shadowCubeView = cameraDepthCube.createView(d.textureDepthCube()); + +const lightPositionUniform = root.createUniform( + d.vec3f, + pointLightWorldPosition, +); + +const mainBindGroup = root.createBindGroup(renderLayoutWithShadow, { + camera: cameraUniform.buffer, + modelMatrix: modelMatrixUniform.buffer, + shadowDepthCube: shadowCubeView, + shadowSampler: shadowSampler, + lightPosition: lightPositionUniform.buffer, +}); + +const pipelineMain = root['~unstable'] + .withVertex(vertexMain, vertexData.attrib) + .withFragment(fragmentMain, { format: presentationFormat }) + .withDepthStencil({ + format: 'depth24plus', + depthWriteEnabled: true, + depthCompare: 'less', + }) + .createPipeline(); + +const pipelineDepthOne = root['~unstable'] + .withVertex(vertexDepth, vertexData.attrib) + .withDepthStencil({ + format: 'depth24plus', + depthWriteEnabled: true, + depthCompare: 'less', + }) + .createPipeline(); + +const shadowCameraUniforms = Object.fromEntries( + Object.entries(shadowCameras).map(([key, cam]) => [ + key, + root.createUniform(CameraData, cam.data), + ]), +); + +const shadowBindGroups = Object.entries(shadowCameras).map(([key, cam]) => { + return [ + key, + root.createBindGroup(renderLayout, { + camera: shadowCameraUniforms[key as keyof typeof shadowCameras].buffer, + modelMatrix: modelMatrixUniform.buffer, + }), + ] as const; +}); + +function updateShadowMaps() { + // Update all shadow camera uniforms + for (const [key, cam] of Object.entries(shadowCameras)) { + shadowCameraUniforms[key as keyof typeof shadowCameras].write(cam.data); + } + + for (const [key, bindGroup] of shadowBindGroups) { + const view = cameraDepthCube.createView(d.textureDepth2d(), { + baseArrayLayer: { + right: 0, // +X + left: 1, // -X + up: 2, // +Y + down: 3, // -Y + forward: 4, // +Z + backward: 5, // -Z + }[key as keyof typeof shadowCameras], + arrayLayerCount: 1, + }); + + // Draw cube + modelMatrixUniform.write(cube.modelMatrix); + pipelineDepthOne + .withDepthStencilAttachment({ + view: root.unwrap(view), + depthClearValue: 1, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }) + .with(bindGroup) + .withIndexBuffer(cubeIndexBuffer) + .with(vertexData, cubeVertexBuffer) + .drawIndexed(cube.indices.length); + + // Draw floor + modelMatrixUniform.write(floorCube.modelMatrix); + pipelineDepthOne + .withDepthStencilAttachment({ + view: root.unwrap(view), + depthClearValue: 1, + depthLoadOp: 'load', + depthStoreOp: 'store', + }) + .with(bindGroup) + .withIndexBuffer(floorIndexBuffer) + .with(vertexData, floorVertexBuffer) + .drawIndexed(floorCube.indices.length); + } +} + +function render() { + // Update shadow maps every frame + updateShadowMaps(); + + debugPipeline + .withColorAttachment({ + view: context.getCurrentTexture().createView(), + loadOp: 'clear', + storeOp: 'store', + }) + .draw(3); + requestAnimationFrame(render); + return; + + modelMatrixUniform.write(cube.modelMatrix); + pipelineMain + .withDepthStencilAttachment({ + view: depthTexture, + depthClearValue: 1, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }) + .withColorAttachment({ + view: context.getCurrentTexture().createView(), + loadOp: 'clear', + storeOp: 'store', + }) + .with(mainBindGroup) + .withIndexBuffer(cubeIndexBuffer) + .with(vertexData, cubeVertexBuffer) + .drawIndexed(cube.indices.length); + + modelMatrixUniform.write(floorCube.modelMatrix); + pipelineMain + .withDepthStencilAttachment({ + view: depthTexture, + depthClearValue: 1, + depthLoadOp: 'load', + depthStoreOp: 'store', + }) + .withColorAttachment({ + view: context.getCurrentTexture().createView(), + loadOp: 'load', + storeOp: 'store', + }) + .with(mainBindGroup) + .withIndexBuffer(floorIndexBuffer) + .with(vertexData, floorVertexBuffer) + .drawIndexed(floorCube.indices.length); + + requestAnimationFrame(render); +} +requestAnimationFrame(render); + +// Resize observer +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), + ); + + // Recreate depth texture with new size + depthTexture = root['~unstable'] + .createTexture({ + size: [canvas.width, canvas.height], + format: 'depth24plus', + }) + .$usage('render'); + } +}); + +resizeObserver.observe(canvas); + +// Orbit controls +let theta = Math.atan2(10, 10); +let phi = Math.acos(10 / Math.sqrt(10 * 10 + 10 * 10 + 10 * 10)); +let radius = Math.sqrt(10 * 10 + 10 * 10 + 10 * 10); + +let isDragging = false; +let lastMouseX = 0; +let lastMouseY = 0; + +function updateCameraPosition() { + const x = radius * Math.sin(phi) * Math.cos(theta); + const y = radius * Math.cos(phi); + const z = radius * Math.sin(phi) * Math.sin(theta); + + mainCamera.position = d.vec3f(x, y, z); + cameraUniform.write(mainCamera.data); +} + +canvas.addEventListener('mousedown', (e) => { + isDragging = true; + lastMouseX = e.clientX; + lastMouseY = e.clientY; +}); + +canvas.addEventListener('mouseup', () => { + isDragging = false; +}); + +canvas.addEventListener('mousemove', (e) => { + if (!isDragging) return; + + const deltaX = e.clientX - lastMouseX; + const deltaY = e.clientY - lastMouseY; + + lastMouseX = e.clientX; + lastMouseY = e.clientY; + + theta -= deltaX * 0.01; + phi -= deltaY * 0.01; + + // Clamp phi to avoid gimbal lock + phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi)); + + updateCameraPosition(); +}); + +canvas.addEventListener('wheel', (e) => { + e.preventDefault(); + + radius += e.deltaY * 0.01; + radius = Math.max(1, Math.min(50, radius)); + + updateCameraPosition(); +}); + +export const controls = { + 'Debug Val': { + initial: 0, + min: 0, + max: 100, + step: 0.01, + onSliderChange: (v: number) => { + debugUniform.write(v); + }, + }, +}; diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/meta.json b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/meta.json new file mode 100644 index 0000000000..ff8ffe4f12 --- /dev/null +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Point Light Shadow", + "category": "rendering", + "tags": ["3d"] +} diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/types.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/types.ts new file mode 100644 index 0000000000..03e4d8be25 --- /dev/null +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/types.ts @@ -0,0 +1,16 @@ +import * as d from 'typegpu/data'; + +export const CameraData = d.struct({ + viewProjectionMatrix: d.mat4x4f, + inverseViewProjectionMatrix: d.mat4x4f, +}); +export type CameraData = typeof CameraData; + +export const VertexData = d.struct({ + position: d.vec3f, + normal: d.vec3f, + uv: d.vec2f, +}); +export type VertexData = typeof VertexData; + +export type GeometryData = d.Infer[]; diff --git a/packages/typegpu/src/core/pipeline/renderPipeline.ts b/packages/typegpu/src/core/pipeline/renderPipeline.ts index e02b5b8448..80b7c3c7cf 100644 --- a/packages/typegpu/src/core/pipeline/renderPipeline.ts +++ b/packages/typegpu/src/core/pipeline/renderPipeline.ts @@ -203,7 +203,15 @@ export interface DepthStencilAttachment { * A {@link GPUTextureView} | ({@link TgpuTexture} & {@link RenderFlag}) describing the texture subresource that will be output to * and read from for this depth/stencil attachment. */ - view: (TgpuTexture & RenderFlag) | GPUTextureView; + view: + | ( + & TgpuTexture<{ + size: [number, number]; + format: 'depth24plus' | 'depth24plus-stencil8' | 'depth32float'; + }> + & RenderFlag + ) + | GPUTextureView; /** * Indicates the value to clear {@link GPURenderPassDepthStencilAttachment#view}'s depth component * to prior to executing the render pass. Ignored if {@link GPURenderPassDepthStencilAttachment#depthLoadOp} diff --git a/packages/typegpu/src/core/texture/texture.ts b/packages/typegpu/src/core/texture/texture.ts index 516ae6cdb1..ca4a37fafb 100644 --- a/packages/typegpu/src/core/texture/texture.ts +++ b/packages/typegpu/src/core/texture/texture.ts @@ -623,6 +623,9 @@ class TgpuFixedTextureViewImpl if (this.#descriptor?.arrayLayerCount !== undefined) { descriptor.arrayLayerCount = this.#descriptor.arrayLayerCount; } + if (this.#descriptor?.baseArrayLayer !== undefined) { + descriptor.baseArrayLayer = this.#descriptor.baseArrayLayer; + } this.#view = this.#baseTexture[$internal] .unwrap() diff --git a/packages/typegpu/src/data/texture.ts b/packages/typegpu/src/data/texture.ts index 11834f7e52..3b5c83ab00 100644 --- a/packages/typegpu/src/data/texture.ts +++ b/packages/typegpu/src/data/texture.ts @@ -178,7 +178,7 @@ export interface WgslTextureDepthMultisampled2d extends multisampled: true; }> { readonly type: 'texture_depth_multisampled_2d'; - readonly [$repr]: textureMultisampled2d; + readonly [$repr]: textureDepthMultisampled2d; } export interface WgslTextureDepth2dArray extends @@ -188,7 +188,7 @@ export interface WgslTextureDepth2dArray extends multisampled: false; }> { readonly type: 'texture_depth_2d_array'; - readonly [$repr]: texture2dArray; + readonly [$repr]: textureDepth2dArray; } export interface WgslTextureDepthCube extends @@ -198,7 +198,7 @@ export interface WgslTextureDepthCube extends multisampled: false; }> { readonly type: 'texture_depth_cube'; - readonly [$repr]: textureCube; + readonly [$repr]: textureDepthCube; } export interface WgslTextureDepthCubeArray extends @@ -208,7 +208,7 @@ export interface WgslTextureDepthCubeArray extends multisampled: false; }> { readonly type: 'texture_depth_cube_array'; - readonly [$repr]: textureCubeArray; + readonly [$repr]: textureDepthCubeArray; } // Storage textures From 5ef29205c3fa3461008362fbd1da0798e762285d Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Sat, 15 Nov 2025 02:39:49 +0100 Subject: [PATCH 02/17] first working version kinda --- .../rendering/point-light-shadow/index.ts | 98 ++++++++++++------- packages/typegpu/src/core/function/fnTypes.ts | 2 +- .../typegpu/src/core/function/ioSchema.ts | 2 + .../pipeline/connectAttachmentToShader.ts | 3 + .../core/pipeline/connectTargetsToShader.ts | 4 + 5 files changed, 71 insertions(+), 38 deletions(-) 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 ddd5ba04ec..4daee0bc3f 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 @@ -24,12 +24,13 @@ mainCamera.target = d.vec3f(0, 0, 0); const cameraUniform = root.createUniform(CameraData, mainCamera.data); const cube = new BoxGeometry(); +cube.scale = d.vec3f(3, 1, 0.2); const floorCube = new BoxGeometry(); floorCube.scale = d.vec3f(10, 0.1, 10); floorCube.position = d.vec3f(0, -0.5, 0); const modelMatrixUniform = root.createUniform(d.mat4x4f); -const pointLightWorldPosition = d.vec3f(2, 1, 0); +const pointLightWorldPosition = d.vec3f(2, 4, 1); const shadowCameras = { right: new Camera(90, 0.1, 25), // +X @@ -44,12 +45,12 @@ const shadowCameras = { // Right face (+X) shadowCameras.right.position = pointLightWorldPosition; shadowCameras.right.target = pointLightWorldPosition.add(d.vec3f(1, 0, 0)); -shadowCameras.right.up = d.vec3f(0, -1, 0); +shadowCameras.right.up = d.vec3f(0, 1, 0); // Left face (-X) shadowCameras.left.position = pointLightWorldPosition; shadowCameras.left.target = pointLightWorldPosition.add(d.vec3f(-1, 0, 0)); -shadowCameras.left.up = d.vec3f(0, -1, 0); +shadowCameras.left.up = d.vec3f(0, 1, 0); // Up face (+Y) shadowCameras.up.position = pointLightWorldPosition; @@ -64,12 +65,12 @@ shadowCameras.down.up = d.vec3f(0, 0, -1); // Forward face (+Z) shadowCameras.forward.position = pointLightWorldPosition; shadowCameras.forward.target = pointLightWorldPosition.add(d.vec3f(0, 0, 1)); -shadowCameras.forward.up = d.vec3f(0, -1, 0); +shadowCameras.forward.up = d.vec3f(0, 1, 0); // Backward face (-Z) shadowCameras.backward.position = pointLightWorldPosition; shadowCameras.backward.target = pointLightWorldPosition.add(d.vec3f(0, 0, -1)); -shadowCameras.backward.up = d.vec3f(0, -1, 0); +shadowCameras.backward.up = d.vec3f(0, 1, 0); const cameraDepthCube = root['~unstable'].createTexture({ size: [512, 512, 6], @@ -108,6 +109,7 @@ let depthTexture = root['~unstable'] const renderLayout = tgpu.bindGroupLayout({ camera: { uniform: CameraData }, modelMatrix: { uniform: d.mat4x4f }, + lightPosition: { uniform: d.vec3f }, }); const renderLayoutWithShadow = tgpu.bindGroupLayout({ @@ -148,8 +150,7 @@ const debugFragment = tgpu['~unstable'].fragmentFn({ localUV, arrayIndex, ); - const remappedDepth = std.pow(depth, 8.0); - return d.vec4f(d.vec3f(remappedDepth), 1.0); + return d.vec4f(d.vec3f(depth), 1.0); }); const debugPipeline = root['~unstable'] @@ -163,6 +164,7 @@ const vertexDepth = tgpu['~unstable'].vertexFn({ }, out: { pos: d.builtin.position, + worldPos: d.vec3f, }, })(({ position }) => { const worldPos = renderLayout.$.modelMatrix.mul(d.vec4f(position, 1)).xyz; @@ -172,9 +174,29 @@ const vertexDepth = tgpu['~unstable'].vertexFn({ return { pos, + worldPos, }; }); +const lightFar = 25.0; // must match shadow camera far plane + +const fragmentDepth = tgpu['~unstable'].fragmentFn({ + in: { + worldPos: d.vec3f, + }, + out: d.builtin.fragDepth, +})(({ worldPos }) => { + const lightPos = renderLayout.$.lightPosition; + + const lightToFrag = worldPos.sub(lightPos); + const dist = std.length(lightToFrag); + + // map [0, lightFar] -> [0, 1] + const depth = dist / lightFar; + + return depth; +}); + const vertexMain = tgpu['~unstable'].vertexFn({ in: { ...VertexData.propTypes, @@ -210,41 +232,41 @@ const fragmentMain = tgpu['~unstable'].fragmentFn({ }, out: d.vec4f, })(({ worldPos, normal }) => { - // calculate light direction const lightPos = renderLayoutWithShadow.$.lightPosition; - const lightDir = std.normalize(lightPos.sub(worldPos)); - - // diffuse shading - const diff = std.max(std.dot(normal, lightDir), 0.0); - // Shadow calculation - // Direction from fragment to light (for cubemap sampling - points away from center) - const fragToLight = worldPos.sub(lightPos); - const distance = std.length(fragToLight); + // direction from light to fragment (for cubemap) + const lightToFrag = worldPos.sub(lightPos); + const dist = std.length(lightToFrag); + const dir = lightToFrag.div(dist); // normalized direction - // Simple linear depth normalized to [0,1] - const far = 25.0; - const normalizedDepth = distance / far; + const depthRef = dist / 25.0; // must match lightFar used in depth pass - // Sample shadow cubemap - const shadowDepth = std.textureSampleCompare( + // Optional bias to reduce acne + const bias = 0.002; + const visibility = std.textureSampleCompare( renderLayoutWithShadow.$.shadowDepthCube, renderLayoutWithShadow.$.shadowSampler, - fragToLight, - normalizedDepth * debugUniform.$, + dir, + depthRef - bias, ); - const rawDepth = std.textureSample( + + const rawValue = std.textureSample( renderLayoutWithShadow.$.shadowDepthCube, debugSampler.$, - fragToLight, + dir, ); - return d.vec4f(std.pow(d.vec3f(rawDepth), d.vec3f(5.0)), 1.0); - // return d.vec4f(d.vec3f(shadowDepth), 1.0); // Visualize shadow factor + // return d.vec4f(d.vec3f(rawValue), 1.0); + + // calculate light direction + const lightDir = std.normalize(lightPos.sub(worldPos)); + + // diffuse shading + const diff = std.max(std.dot(normal, lightDir), 0.0); const baseColor = d.vec3f(1.0, 0.5, 0.31); const ambient = 0.1; - const color = baseColor.mul(diff * shadowDepth + ambient); + const color = baseColor.mul(diff * visibility + ambient); return d.vec4f(color, 1.0); }); @@ -282,6 +304,7 @@ const pipelineMain = root['~unstable'] const pipelineDepthOne = root['~unstable'] .withVertex(vertexDepth, vertexData.attrib) + .withFragment(fragmentDepth, {} as never) .withDepthStencil({ format: 'depth24plus', depthWriteEnabled: true, @@ -302,6 +325,7 @@ const shadowBindGroups = Object.entries(shadowCameras).map(([key, cam]) => { root.createBindGroup(renderLayout, { camera: shadowCameraUniforms[key as keyof typeof shadowCameras].buffer, modelMatrix: modelMatrixUniform.buffer, + lightPosition: lightPositionUniform.buffer, }), ] as const; }); @@ -359,15 +383,15 @@ function render() { // Update shadow maps every frame updateShadowMaps(); - debugPipeline - .withColorAttachment({ - view: context.getCurrentTexture().createView(), - loadOp: 'clear', - storeOp: 'store', - }) - .draw(3); - requestAnimationFrame(render); - return; + // debugPipeline + // .withColorAttachment({ + // view: context.getCurrentTexture().createView(), + // loadOp: 'clear', + // storeOp: 'store', + // }) + // .draw(3); + // requestAnimationFrame(render); + // return; modelMatrixUniform.write(cube.modelMatrix); pipelineMain diff --git a/packages/typegpu/src/core/function/fnTypes.ts b/packages/typegpu/src/core/function/fnTypes.ts index 359c3484f4..3760fd61d7 100644 --- a/packages/typegpu/src/core/function/fnTypes.ts +++ b/packages/typegpu/src/core/function/fnTypes.ts @@ -1,5 +1,5 @@ import type * as tinyest from 'tinyest'; -import type { BuiltinClipDistances } from '../../builtin.ts'; +import type { BuiltinClipDistances, BuiltinFragDepth } from '../../builtin.ts'; import type { AnyAttribute } from '../../data/attributes.ts'; import type { Bool, diff --git a/packages/typegpu/src/core/function/ioSchema.ts b/packages/typegpu/src/core/function/ioSchema.ts index b39fedbabd..eeb1b77a4d 100644 --- a/packages/typegpu/src/core/function/ioSchema.ts +++ b/packages/typegpu/src/core/function/ioSchema.ts @@ -75,6 +75,8 @@ export function createIoSchema< return ( isData(layout) ? isVoid(layout) + ? layout + : isBuiltin(layout) ? layout : getCustomLocation(layout) !== undefined ? layout diff --git a/packages/typegpu/src/core/pipeline/connectAttachmentToShader.ts b/packages/typegpu/src/core/pipeline/connectAttachmentToShader.ts index accb0616cb..eb171cb7d1 100644 --- a/packages/typegpu/src/core/pipeline/connectAttachmentToShader.ts +++ b/packages/typegpu/src/core/pipeline/connectAttachmentToShader.ts @@ -17,6 +17,9 @@ export function connectAttachmentToShader( attachment: AnyFragmentColorAttachment, ): ColorAttachment[] { if (isData(shaderOutputLayout)) { + if (isBuiltin(shaderOutputLayout)) { + return []; + } if (!isColorAttachment(attachment)) { throw new Error('Expected a single color attachment, not a record.'); } diff --git a/packages/typegpu/src/core/pipeline/connectTargetsToShader.ts b/packages/typegpu/src/core/pipeline/connectTargetsToShader.ts index 4c4d8b3426..d6f5067a99 100644 --- a/packages/typegpu/src/core/pipeline/connectTargetsToShader.ts +++ b/packages/typegpu/src/core/pipeline/connectTargetsToShader.ts @@ -14,10 +14,14 @@ export function connectTargetsToShader( shaderOutputLayout: FragmentOutConstrained, targets: AnyFragmentTargets, ): GPUColorTargetState[] { + console.log('Connecting targets to shader...', shaderOutputLayout, targets); if (isData(shaderOutputLayout)) { if (isVoid(shaderOutputLayout)) { return []; } + if (shaderOutputLayout.type === 'decorated') { + return []; + } if (!isColorTargetState(targets)) { throw new Error( From 08a9bced674dbcc5dbb55ec69bbd55646ad378cc Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Tue, 18 Nov 2025 12:56:33 +0100 Subject: [PATCH 03/17] temp --- .../point-light-shadow/box-geometry.ts | 46 ++- .../rendering/point-light-shadow/camera.ts | 37 ++- .../rendering/point-light-shadow/index.ts | 267 ++++++------------ .../point-light-shadow/point-light.ts | 199 +++++++++++++ 4 files changed, 368 insertions(+), 181 deletions(-) create mode 100644 apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts index 6fc7a2b233..d1b70aff50 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts @@ -1,8 +1,10 @@ +import type { IndexFlag, TgpuBuffer, TgpuRoot, VertexFlag } from 'typegpu'; import * as d from 'typegpu/data'; import type { GeometryData } from './types.ts'; import { VertexData } from './types.ts'; export class BoxGeometry { + #root: TgpuRoot; #modelMatrix: d.m4x4f; #position: d.v3f; #scale: d.v3f; @@ -10,8 +12,14 @@ export class BoxGeometry { #size: [number, number, number]; #vertices: GeometryData; #indices: number[]; + #vertexBuffer: TgpuBuffer> & VertexFlag; + #indexBuffer: TgpuBuffer> & IndexFlag; - constructor(size: [number, number, number] = [1, 1, 1]) { + constructor( + root: TgpuRoot, + size: [number, number, number] = [1, 1, 1], + ) { + this.#root = root; this.#modelMatrix = d.mat4x4f.identity(); this.#position = d.vec3f(0, 0, 0); this.#scale = d.vec3f(1, 1, 1); @@ -21,6 +29,18 @@ export class BoxGeometry { this.#indices = []; this.#generateGeometry(); + + // Create GPU buffers + this.#vertexBuffer = root + .createBuffer( + d.arrayOf(VertexData, this.#vertices.length), + this.#vertices, + ) + .$usage('vertex'); + + this.#indexBuffer = root + .createBuffer(d.arrayOf(d.u16, this.#indices.length), this.#indices) + .$usage('index'); } set position(value: d.v3f) { @@ -28,16 +48,28 @@ export class BoxGeometry { this.#updateModelMatrix(); } + get position(): d.v3f { + return this.#position; + } + set scale(value: d.v3f) { this.#scale = value; this.#updateModelMatrix(); } + get scale(): d.v3f { + return this.#scale; + } + set rotation(value: d.v3f) { this.#rotation = value; this.#updateModelMatrix(); } + get rotation(): d.v3f { + return this.#rotation; + } + get modelMatrix(): d.m4x4f { return this.#modelMatrix; } @@ -50,6 +82,18 @@ export class BoxGeometry { return this.#indices; } + get vertexBuffer() { + return this.#vertexBuffer; + } + + get indexBuffer() { + return this.#indexBuffer; + } + + get indexCount(): number { + return this.#indices.length; + } + #generateGeometry() { const [w, h, d] = this.#size; const halfW = w / 2; diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts index 0ff7349b34..b21e52ce81 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts @@ -1,8 +1,10 @@ +import type { TgpuRoot, TgpuUniform } from 'typegpu'; import * as d from 'typegpu/data'; import * as m from 'wgpu-matrix'; import { CameraData } from './types.ts'; export class Camera { + #root: TgpuRoot; #viewProjectionMatrix: d.m4x4f; #position: d.v3f; #target: d.v3f; @@ -12,8 +14,15 @@ export class Camera { #far: number; #inverseViewProjectionMatrix: d.m4x4f; #data: d.Infer; + #uniform: TgpuUniform; - constructor(fov: number = 60, near: number = 0.1, far: number = 1000) { + constructor( + root: TgpuRoot, + fov: number = 60, + near: number = 0.1, + far: number = 1000, + ) { + this.#root = root; this.#viewProjectionMatrix = d.mat4x4f.identity(); this.#position = d.vec3f(0, 0, 0); this.#target = d.vec3f(0, 0, -1); @@ -26,6 +35,9 @@ export class Camera { viewProjectionMatrix: this.#viewProjectionMatrix, inverseViewProjectionMatrix: this.#inverseViewProjectionMatrix, }); + + // Create uniform buffer + this.#uniform = root.createUniform(CameraData, this.#data); } set position(pos: d.v3f) { @@ -33,25 +45,45 @@ export class Camera { this.#recompute(); } + get position(): d.v3f { + return this.#position; + } + set target(tgt: d.v3f) { this.#target = tgt; this.#recompute(); } + get target(): d.v3f { + return this.#target; + } + set up(upVec: d.v3f) { this.#up = upVec; this.#recompute(); } + get up(): d.v3f { + return this.#up; + } + set fov(fovDegrees: number) { this.#fov = fovDegrees; this.#recompute(); } + get fov(): number { + return this.#fov; + } + get data(): d.Infer { return this.#data; } + get uniform() { + return this.#uniform; + } + #recompute() { const view = m.mat4.lookAt( this.#position, @@ -74,5 +106,8 @@ export class Camera { viewProjectionMatrix: this.#viewProjectionMatrix, inverseViewProjectionMatrix: this.#inverseViewProjectionMatrix, }); + + // Update the uniform buffer + this.#uniform.write(this.#data); } } 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 4daee0bc3f..26a86a7b8b 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 @@ -3,6 +3,7 @@ import * as d from 'typegpu/data'; import * as std from 'typegpu/std'; import { Camera } from './camera.ts'; import { BoxGeometry } from './box-geometry.ts'; +import { PointLight } from './point-light.ts'; import { CameraData, VertexData } from './types.ts'; import { fullScreenTriangle } from 'typegpu/common'; @@ -18,86 +19,29 @@ context.configure({ format: presentationFormat, }); -const mainCamera = new Camera(); +// Create global vertex layout +const vertexLayout = tgpu.vertexLayout(d.arrayOf(VertexData)); + +// Create main camera with buffers managed internally +const mainCamera = new Camera(root); mainCamera.position = d.vec3f(10, 10, 10); mainCamera.target = d.vec3f(0, 0, 0); -const cameraUniform = root.createUniform(CameraData, mainCamera.data); -const cube = new BoxGeometry(); +// Create geometries with buffers managed internally +const cube = new BoxGeometry(root); cube.scale = d.vec3f(3, 1, 0.2); -const floorCube = new BoxGeometry(); + +const floorCube = new BoxGeometry(root); floorCube.scale = d.vec3f(10, 0.1, 10); floorCube.position = d.vec3f(0, -0.5, 0); -const modelMatrixUniform = root.createUniform(d.mat4x4f); -const pointLightWorldPosition = d.vec3f(2, 4, 1); - -const shadowCameras = { - right: new Camera(90, 0.1, 25), // +X - left: new Camera(90, 0.1, 25), // -X - up: new Camera(90, 0.1, 25), // +Y - down: new Camera(90, 0.1, 25), // -Y - forward: new Camera(90, 0.1, 25), // +Z - backward: new Camera(90, 0.1, 25), // -Z -}; +// Create point light with shadow cameras and cubemap managed internally +const pointLight = new PointLight(root, d.vec3f(2, 4, 1), { + far: 100.0, + shadowMapSize: 512, +}); -// Configure each camera for cubemap faces -// Right face (+X) -shadowCameras.right.position = pointLightWorldPosition; -shadowCameras.right.target = pointLightWorldPosition.add(d.vec3f(1, 0, 0)); -shadowCameras.right.up = d.vec3f(0, 1, 0); - -// Left face (-X) -shadowCameras.left.position = pointLightWorldPosition; -shadowCameras.left.target = pointLightWorldPosition.add(d.vec3f(-1, 0, 0)); -shadowCameras.left.up = d.vec3f(0, 1, 0); - -// Up face (+Y) -shadowCameras.up.position = pointLightWorldPosition; -shadowCameras.up.target = pointLightWorldPosition.add(d.vec3f(0, 1, 0)); -shadowCameras.up.up = d.vec3f(0, 0, 1); - -// Down face (-Y) -shadowCameras.down.position = pointLightWorldPosition; -shadowCameras.down.target = pointLightWorldPosition.add(d.vec3f(0, -1, 0)); -shadowCameras.down.up = d.vec3f(0, 0, -1); - -// Forward face (+Z) -shadowCameras.forward.position = pointLightWorldPosition; -shadowCameras.forward.target = pointLightWorldPosition.add(d.vec3f(0, 0, 1)); -shadowCameras.forward.up = d.vec3f(0, 1, 0); - -// Backward face (-Z) -shadowCameras.backward.position = pointLightWorldPosition; -shadowCameras.backward.target = pointLightWorldPosition.add(d.vec3f(0, 0, -1)); -shadowCameras.backward.up = d.vec3f(0, 1, 0); - -const cameraDepthCube = root['~unstable'].createTexture({ - size: [512, 512, 6], - dimension: '2d', - format: 'depth24plus', -}).$usage('render', 'sampled'); - -const vertexData = tgpu.vertexLayout(d.arrayOf(VertexData)); -const cubeVertexBuffer = root - .createBuffer( - vertexData.schemaForCount(cube.vertices.length), - cube.vertices, - ) - .$usage('vertex'); -const cubeIndexBuffer = root - .createBuffer(d.arrayOf(d.u16, cube.indices.length), cube.indices) - .$usage('index'); - -const floorVertexBuffer = root - .createBuffer( - vertexData.schemaForCount(floorCube.vertices.length), - floorCube.vertices, - ) - .$usage('vertex'); -const floorIndexBuffer = root - .createBuffer(d.arrayOf(d.u16, floorCube.indices.length), floorCube.indices) - .$usage('index'); +const modelMatrixUniform = root.createBuffer(d.mat4x4f).$usage('uniform'); let depthTexture = root['~unstable'] .createTexture({ @@ -106,6 +50,7 @@ let depthTexture = root['~unstable'] }) .$usage('render'); +// Bind group layouts const renderLayout = tgpu.bindGroupLayout({ camera: { uniform: CameraData }, modelMatrix: { uniform: d.mat4x4f }, @@ -120,16 +65,13 @@ const renderLayoutWithShadow = tgpu.bindGroupLayout({ lightPosition: { uniform: d.vec3f }, }); +// Debug pipeline setup const debugSampler = root['~unstable'].createSampler({ minFilter: 'nearest', magFilter: 'nearest', }); -const debugView = cameraDepthCube.createView(d.textureDepth2dArray(), { - baseArrayLayer: 0, - arrayLayerCount: 6, - aspect: 'depth-only', -}); +const debugView = pointLight.createDebugArrayView(); const debugFragment = tgpu['~unstable'].fragmentFn({ in: { uv: d.vec2f }, @@ -158,6 +100,7 @@ const debugPipeline = root['~unstable'] .withFragment(debugFragment, { format: presentationFormat }) .createPipeline(); +// Shadow depth pass shaders const vertexDepth = tgpu['~unstable'].vertexFn({ in: { ...VertexData.propTypes, @@ -178,8 +121,6 @@ const vertexDepth = tgpu['~unstable'].vertexFn({ }; }); -const lightFar = 25.0; // must match shadow camera far plane - const fragmentDepth = tgpu['~unstable'].fragmentFn({ in: { worldPos: d.vec3f, @@ -192,11 +133,12 @@ const fragmentDepth = tgpu['~unstable'].fragmentFn({ const dist = std.length(lightToFrag); // map [0, lightFar] -> [0, 1] - const depth = dist / lightFar; + const depth = dist / pointLight.far; return depth; }); +// Main render pass shaders const vertexMain = tgpu['~unstable'].vertexFn({ in: { ...VertexData.propTypes, @@ -222,8 +164,6 @@ const vertexMain = tgpu['~unstable'].vertexFn({ }; }); -const debugUniform = root.createUniform(d.f32, 0.0); - const fragmentMain = tgpu['~unstable'].fragmentFn({ in: { worldPos: d.vec3f, @@ -237,9 +177,10 @@ const fragmentMain = tgpu['~unstable'].fragmentFn({ // direction from light to fragment (for cubemap) const lightToFrag = worldPos.sub(lightPos); const dist = std.length(lightToFrag); - const dir = lightToFrag.div(dist); // normalized direction + let dir = lightToFrag.div(dist); // normalized direction + dir = d.vec3f(dir.x, -dir.y, dir.z); // invert for texture lookup - const depthRef = dist / 25.0; // must match lightFar used in depth pass + const depthRef = dist / pointLight.far; // Optional bias to reduce acne const bias = 0.002; @@ -255,8 +196,7 @@ const fragmentMain = tgpu['~unstable'].fragmentFn({ debugSampler.$, dir, ); - - // return d.vec4f(d.vec3f(rawValue), 1.0); + return d.vec4f(rawValue, 0, 0, 1); // Visualize raw depth value from shadow map // calculate light direction const lightDir = std.normalize(lightPos.sub(worldPos)); @@ -271,29 +211,27 @@ const fragmentMain = tgpu['~unstable'].fragmentFn({ return d.vec4f(color, 1.0); }); +// Samplers and views const shadowSampler = root['~unstable'].createComparisonSampler({ compare: 'less-equal', magFilter: 'linear', minFilter: 'linear', }); -const shadowCubeView = cameraDepthCube.createView(d.textureDepthCube()); - -const lightPositionUniform = root.createUniform( - d.vec3f, - pointLightWorldPosition, -); +const shadowCubeView = pointLight.createCubeView(); +// Main render bind group const mainBindGroup = root.createBindGroup(renderLayoutWithShadow, { - camera: cameraUniform.buffer, - modelMatrix: modelMatrixUniform.buffer, + camera: mainCamera.uniform.buffer, + modelMatrix: modelMatrixUniform, shadowDepthCube: shadowCubeView, shadowSampler: shadowSampler, - lightPosition: lightPositionUniform.buffer, + lightPosition: pointLight.positionUniform.buffer, }); +// Pipelines const pipelineMain = root['~unstable'] - .withVertex(vertexMain, vertexData.attrib) + .withVertex(vertexMain, vertexLayout.attrib) .withFragment(fragmentMain, { format: presentationFormat }) .withDepthStencil({ format: 'depth24plus', @@ -303,7 +241,7 @@ const pipelineMain = root['~unstable'] .createPipeline(); const pipelineDepthOne = root['~unstable'] - .withVertex(vertexDepth, vertexData.attrib) + .withVertex(vertexDepth, vertexLayout.attrib) .withFragment(fragmentDepth, {} as never) .withDepthStencil({ format: 'depth24plus', @@ -312,77 +250,17 @@ const pipelineDepthOne = root['~unstable'] }) .createPipeline(); -const shadowCameraUniforms = Object.fromEntries( - Object.entries(shadowCameras).map(([key, cam]) => [ - key, - root.createUniform(CameraData, cam.data), - ]), -); - -const shadowBindGroups = Object.entries(shadowCameras).map(([key, cam]) => { - return [ - key, - root.createBindGroup(renderLayout, { - camera: shadowCameraUniforms[key as keyof typeof shadowCameras].buffer, - modelMatrix: modelMatrixUniform.buffer, - lightPosition: lightPositionUniform.buffer, - }), - ] as const; -}); - -function updateShadowMaps() { - // Update all shadow camera uniforms - for (const [key, cam] of Object.entries(shadowCameras)) { - shadowCameraUniforms[key as keyof typeof shadowCameras].write(cam.data); - } - - for (const [key, bindGroup] of shadowBindGroups) { - const view = cameraDepthCube.createView(d.textureDepth2d(), { - baseArrayLayer: { - right: 0, // +X - left: 1, // -X - up: 2, // +Y - down: 3, // -Y - forward: 4, // +Z - backward: 5, // -Z - }[key as keyof typeof shadowCameras], - arrayLayerCount: 1, - }); - - // Draw cube - modelMatrixUniform.write(cube.modelMatrix); - pipelineDepthOne - .withDepthStencilAttachment({ - view: root.unwrap(view), - depthClearValue: 1, - depthLoadOp: 'clear', - depthStoreOp: 'store', - }) - .with(bindGroup) - .withIndexBuffer(cubeIndexBuffer) - .with(vertexData, cubeVertexBuffer) - .drawIndexed(cube.indices.length); - - // Draw floor - modelMatrixUniform.write(floorCube.modelMatrix); - pipelineDepthOne - .withDepthStencilAttachment({ - view: root.unwrap(view), - depthClearValue: 1, - depthLoadOp: 'load', - depthStoreOp: 'store', - }) - .with(bindGroup) - .withIndexBuffer(floorIndexBuffer) - .with(vertexData, floorVertexBuffer) - .drawIndexed(floorCube.indices.length); - } -} - function render() { - // Update shadow maps every frame - updateShadowMaps(); + // Render shadow maps using the point light abstraction + pointLight.renderShadowMaps( + pipelineDepthOne, + renderLayout, + modelMatrixUniform, + vertexLayout, + [cube, floorCube], + ); + // Uncomment to see debug view of shadow maps // debugPipeline // .withColorAttachment({ // view: context.getCurrentTexture().createView(), @@ -393,6 +271,7 @@ function render() { // requestAnimationFrame(render); // return; + // Render cube modelMatrixUniform.write(cube.modelMatrix); pipelineMain .withDepthStencilAttachment({ @@ -407,10 +286,11 @@ function render() { storeOp: 'store', }) .with(mainBindGroup) - .withIndexBuffer(cubeIndexBuffer) - .with(vertexData, cubeVertexBuffer) - .drawIndexed(cube.indices.length); + .withIndexBuffer(cube.indexBuffer) + .with(vertexLayout, cube.vertexBuffer) + .drawIndexed(cube.indexCount); + // Render floor modelMatrixUniform.write(floorCube.modelMatrix); pipelineMain .withDepthStencilAttachment({ @@ -425,9 +305,9 @@ function render() { storeOp: 'store', }) .with(mainBindGroup) - .withIndexBuffer(floorIndexBuffer) - .with(vertexData, floorVertexBuffer) - .drawIndexed(floorCube.indices.length); + .withIndexBuffer(floorCube.indexBuffer) + .with(vertexLayout, floorCube.vertexBuffer) + .drawIndexed(floorCube.indexCount); requestAnimationFrame(render); } @@ -475,7 +355,6 @@ function updateCameraPosition() { const z = radius * Math.sin(phi) * Math.sin(theta); mainCamera.position = d.vec3f(x, y, z); - cameraUniform.write(mainCamera.data); } canvas.addEventListener('mousedown', (e) => { @@ -516,13 +395,43 @@ canvas.addEventListener('wheel', (e) => { }); export const controls = { - 'Debug Val': { - initial: 0, - min: 0, - max: 100, - step: 0.01, + 'Light X': { + initial: 2, + min: -10, + max: 10, + step: 0.1, + onSliderChange: (v: number) => { + pointLight.position = d.vec3f( + v, + pointLight.position.y, + pointLight.position.z, + ); + }, + }, + 'Light Y': { + initial: 4, + min: 0.5, + max: 10, + step: 0.1, + onSliderChange: (v: number) => { + pointLight.position = d.vec3f( + pointLight.position.x, + v, + pointLight.position.z, + ); + }, + }, + 'Light Z': { + initial: 1, + min: -10, + max: 10, + step: 0.1, onSliderChange: (v: number) => { - debugUniform.write(v); + pointLight.position = d.vec3f( + pointLight.position.x, + pointLight.position.y, + v, + ); }, }, }; diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts new file mode 100644 index 0000000000..0811ae408f --- /dev/null +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts @@ -0,0 +1,199 @@ +import type { + RenderFlag, + SampledFlag, + TgpuBindGroupLayout, + TgpuBuffer, + TgpuRenderPipeline, + TgpuRoot, + TgpuTexture, + TgpuUniform, + TgpuVertexLayout, + UniformFlag, +} from 'typegpu'; +import * as d from 'typegpu/data'; +import type { BoxGeometry } from './box-geometry.ts'; +import { Camera } from './camera.ts'; + +export class PointLight { + #root: TgpuRoot; + #position: d.v3f; + #far: number; + #shadowMapSize: number; + #depthCubeTexture: + & TgpuTexture<{ + size: [number, number, 6]; + format: 'depth24plus'; + }> + & SampledFlag + & RenderFlag; + #shadowCameras: { + right: Camera; + left: Camera; + up: Camera; + down: Camera; + forward: Camera; + backward: Camera; + }; + #positionUniform: TgpuUniform; + + constructor( + root: TgpuRoot, + position: d.v3f, + options: { + far?: number; + shadowMapSize?: number; + } = {}, + ) { + this.#root = root; + this.#position = position; + this.#far = options.far ?? 100.0; + this.#shadowMapSize = options.shadowMapSize ?? 512; + + // Create cubemap depth texture + this.#depthCubeTexture = root['~unstable'] + .createTexture({ + size: [this.#shadowMapSize, this.#shadowMapSize, 6], + dimension: '2d', + format: 'depth24plus', + }) + .$usage('render', 'sampled'); + + // Create position uniform + this.#positionUniform = root.createUniform(d.vec3f, this.#position); + + // Create shadow cameras for each cubemap face + this.#shadowCameras = { + right: new Camera(root, 90, 0.1, this.#far), + left: new Camera(root, 90, 0.1, this.#far), + up: new Camera(root, 90, 0.1, this.#far), + down: new Camera(root, 90, 0.1, this.#far), + forward: new Camera(root, 90, 0.1, this.#far), + backward: new Camera(root, 90, 0.1, this.#far), + }; + + this.#configureCameras(); + } + + #configureCameras() { + // WebGPU cubemap face orientations based on spec diagram + // For lookAt: right = cross(forward, worldUp), so we need to find worldUp such that right matches +U direction + + // [0] +X face: forward=+X, +U=-Z, +V=-Y + // cross(+X, worldUp) = -Z → cross((1,0,0), (0,a,b)) = (0,-b,a) = (0,0,-1) → a=-1, b=0 + this.#shadowCameras.right.position = this.#position; + this.#shadowCameras.right.target = this.#position.add(d.vec3f(1, 0, 0)); + this.#shadowCameras.right.up = d.vec3f(0, -1, 0); + + // [1] -X face: forward=-X, +U=+Z, +V=-Y + // cross(-X, worldUp) = +Z → cross((-1,0,0), (0,a,b)) = (0,b,-a) = (0,0,1) → b=0, a=-1 + this.#shadowCameras.left.position = this.#position; + this.#shadowCameras.left.target = this.#position.add(d.vec3f(-1, 0, 0)); + this.#shadowCameras.left.up = d.vec3f(0, -1, 0); + + // [2] +Y face: forward=+Y, +U=+X, +V=+Z + // cross(+Y, worldUp) = +X → cross((0,1,0), (a,0,c)) = (c,0,-a) = (1,0,0) → c=1, a=0 + this.#shadowCameras.up.position = this.#position; + this.#shadowCameras.up.target = this.#position.add(d.vec3f(0, 1, 0)); + this.#shadowCameras.up.up = d.vec3f(0, 0, 1); + + // [3] -Y face: +U points +X, +V points -Z + // Camera right should be +X, camera up should be -Z + this.#shadowCameras.down.position = this.#position; + this.#shadowCameras.down.target = this.#position.add(d.vec3f(0, -1, 0)); + this.#shadowCameras.down.up = d.vec3f(0, 0, -1); + + // [4] +Z face: +U points +X, +V points -Y + // Camera right should be +X, camera up should be -Y + this.#shadowCameras.forward.position = this.#position; + this.#shadowCameras.forward.target = this.#position.add(d.vec3f(0, 0, 1)); + this.#shadowCameras.forward.up = d.vec3f(0, -1, 0); + + // [5] -Z face: +U points -X, +V points -Y + // Camera right should be -X, camera up should be -Y + this.#shadowCameras.backward.position = this.#position; + this.#shadowCameras.backward.target = this.#position.add(d.vec3f(0, 0, -1)); + this.#shadowCameras.backward.up = d.vec3f(0, -1, 0); + } + + set position(pos: d.v3f) { + this.#position = pos; + this.#positionUniform.write(pos); + this.#configureCameras(); + } + + get position() { + return this.#position; + } + + get positionUniform() { + return this.#positionUniform; + } + + get far(): number { + return this.#far; + } + + get depthCubeTexture() { + return this.#depthCubeTexture; + } + + createCubeView() { + return this.#depthCubeTexture.createView(d.textureDepthCube()); + } + + createDebugArrayView() { + return this.#depthCubeTexture.createView(d.textureDepth2dArray(), { + baseArrayLayer: 0, + arrayLayerCount: 6, + aspect: 'depth-only', + }); + } + + renderShadowMaps( + pipeline: TgpuRenderPipeline, + bindGroupLayout: TgpuBindGroupLayout, + modelMatrixUniform: TgpuBuffer & UniformFlag, + vertexLayout: TgpuVertexLayout, + geometries: BoxGeometry[], + ) { + const faceIndices = { + right: 0, + left: 1, + up: 2, + down: 3, + forward: 4, + backward: 5, + }; + + for (const [key, camera] of Object.entries(this.#shadowCameras)) { + const view = this.#depthCubeTexture.createView(d.textureDepth2d(), { + baseArrayLayer: faceIndices[key as keyof typeof faceIndices], + arrayLayerCount: 1, + }); + + const bindGroup = this.#root.createBindGroup(bindGroupLayout, { + camera: camera.uniform.buffer, + modelMatrix: modelMatrixUniform, + lightPosition: this.#positionUniform.buffer, + }); + + // Render each geometry + for (let i = 0; i < geometries.length; i++) { + const geometry = geometries[i]; + modelMatrixUniform.write(geometry.modelMatrix); + + pipeline + .withDepthStencilAttachment({ + view: this.#root.unwrap(view), + depthClearValue: 1, + depthLoadOp: i === 0 ? 'clear' : 'load', + depthStoreOp: 'store', + }) + .with(vertexLayout, geometry.vertexBuffer) + .with(bindGroup) + .withIndexBuffer(geometry.indexBuffer) + .drawIndexed(geometry.indexCount); + } + } + } +} From 9c2d5cfe051cc3dd63bf9905b96911bb30536570 Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Mon, 24 Nov 2025 16:11:37 +0100 Subject: [PATCH 04/17] working (for some reason) --- .../rendering/point-light-shadow/index.ts | 56 +++++++------------ .../point-light-shadow/point-light.ts | 8 +-- 2 files changed, 24 insertions(+), 40 deletions(-) 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 26a86a7b8b..196d81503b 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 @@ -19,15 +19,12 @@ context.configure({ format: presentationFormat, }); -// Create global vertex layout const vertexLayout = tgpu.vertexLayout(d.arrayOf(VertexData)); -// Create main camera with buffers managed internally const mainCamera = new Camera(root); mainCamera.position = d.vec3f(10, 10, 10); mainCamera.target = d.vec3f(0, 0, 0); -// Create geometries with buffers managed internally const cube = new BoxGeometry(root); cube.scale = d.vec3f(3, 1, 0.2); @@ -35,10 +32,9 @@ const floorCube = new BoxGeometry(root); floorCube.scale = d.vec3f(10, 0.1, 10); floorCube.position = d.vec3f(0, -0.5, 0); -// Create point light with shadow cameras and cubemap managed internally const pointLight = new PointLight(root, d.vec3f(2, 4, 1), { far: 100.0, - shadowMapSize: 512, + shadowMapSize: 4096, }); const modelMatrixUniform = root.createBuffer(d.mat4x4f).$usage('uniform'); @@ -77,9 +73,9 @@ const debugFragment = tgpu['~unstable'].fragmentFn({ in: { uv: d.vec2f }, out: d.vec4f, })(({ uv }) => { - const localUV = d.vec2f( + const tiled = d.vec2f( std.fract(uv.x * 3), - std.fract(uv.y * 2), + 1.0 - std.fract(uv.y * 2), ); const col = std.floor(uv.x * 3); @@ -89,10 +85,10 @@ const debugFragment = tgpu['~unstable'].fragmentFn({ const depth = std.textureSample( debugView.$, debugSampler.$, - localUV, + tiled, arrayIndex, ); - return d.vec4f(d.vec3f(depth), 1.0); + return d.vec4f(d.vec3f(depth ** 0.5), 1.0); }); const debugPipeline = root['~unstable'] @@ -174,16 +170,14 @@ const fragmentMain = tgpu['~unstable'].fragmentFn({ })(({ worldPos, normal }) => { const lightPos = renderLayoutWithShadow.$.lightPosition; - // direction from light to fragment (for cubemap) const lightToFrag = worldPos.sub(lightPos); const dist = std.length(lightToFrag); - let dir = lightToFrag.div(dist); // normalized direction - dir = d.vec3f(dir.x, -dir.y, dir.z); // invert for texture lookup + let dir = std.normalize(lightToFrag); + dir = d.vec3f(dir.x, -dir.y, dir.z); const depthRef = dist / pointLight.far; - // Optional bias to reduce acne - const bias = 0.002; + const bias = 0.001 * (1.0 - std.dot(normal, std.normalize(lightToFrag))); const visibility = std.textureSampleCompare( renderLayoutWithShadow.$.shadowDepthCube, renderLayoutWithShadow.$.shadowSampler, @@ -191,29 +185,19 @@ const fragmentMain = tgpu['~unstable'].fragmentFn({ depthRef - bias, ); - const rawValue = std.textureSample( - renderLayoutWithShadow.$.shadowDepthCube, - debugSampler.$, - dir, - ); - return d.vec4f(rawValue, 0, 0, 1); // Visualize raw depth value from shadow map - - // calculate light direction const lightDir = std.normalize(lightPos.sub(worldPos)); - - // diffuse shading - const diff = std.max(std.dot(normal, lightDir), 0.0); + const diffuse = std.max(std.dot(normal, lightDir), 0.0); const baseColor = d.vec3f(1.0, 0.5, 0.31); const ambient = 0.1; - const color = baseColor.mul(diff * visibility + ambient); + const color = baseColor.mul(diffuse * visibility + ambient); return d.vec4f(color, 1.0); }); // Samplers and views const shadowSampler = root['~unstable'].createComparisonSampler({ - compare: 'less-equal', + compare: 'less', magFilter: 'linear', minFilter: 'linear', }); @@ -261,15 +245,15 @@ function render() { ); // Uncomment to see debug view of shadow maps - // debugPipeline - // .withColorAttachment({ - // view: context.getCurrentTexture().createView(), - // loadOp: 'clear', - // storeOp: 'store', - // }) - // .draw(3); - // requestAnimationFrame(render); - // return; + debugPipeline + .withColorAttachment({ + view: context.getCurrentTexture().createView(), + loadOp: 'clear', + storeOp: 'store', + }) + .draw(3); + requestAnimationFrame(render); + return; // Render cube modelMatrixUniform.write(cube.modelMatrix); diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts index 0811ae408f..d925719eea 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts @@ -93,14 +93,14 @@ export class PointLight { // [2] +Y face: forward=+Y, +U=+X, +V=+Z // cross(+Y, worldUp) = +X → cross((0,1,0), (a,0,c)) = (c,0,-a) = (1,0,0) → c=1, a=0 this.#shadowCameras.up.position = this.#position; - this.#shadowCameras.up.target = this.#position.add(d.vec3f(0, 1, 0)); - this.#shadowCameras.up.up = d.vec3f(0, 0, 1); + this.#shadowCameras.up.target = this.#position.add(d.vec3f(0, -1, 0)); + this.#shadowCameras.up.up = d.vec3f(0, 0, -1); // [3] -Y face: +U points +X, +V points -Z // Camera right should be +X, camera up should be -Z this.#shadowCameras.down.position = this.#position; - this.#shadowCameras.down.target = this.#position.add(d.vec3f(0, -1, 0)); - this.#shadowCameras.down.up = d.vec3f(0, 0, -1); + this.#shadowCameras.down.target = this.#position.add(d.vec3f(0, 1, 0)); + this.#shadowCameras.down.up = d.vec3f(0, 0, 1); // [4] +Z face: +U points +X, +V points -Y // Camera right should be +X, camera up should be -Y From 8d7dff906818139507f072f4cf44b023d361cbbd Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Mon, 24 Nov 2025 16:49:02 +0100 Subject: [PATCH 05/17] cleanup --- .../geometry/lines-combinations/index.ts | 1 + .../point-light-shadow/box-geometry.ts | 4 -- .../rendering/point-light-shadow/camera.ts | 4 -- .../rendering/point-light-shadow/index.ts | 33 +++++++------ packages/typegpu/src/core/function/fnTypes.ts | 2 +- .../src/core/function/tgpuFragmentFn.ts | 10 ++-- .../src/core/pipeline/renderPipeline.ts | 19 ++++++-- packages/typegpu/tests/renderPipeline.test.ts | 46 +++++++++++++++++++ 8 files changed, 87 insertions(+), 32 deletions(-) diff --git a/apps/typegpu-docs/src/examples/geometry/lines-combinations/index.ts b/apps/typegpu-docs/src/examples/geometry/lines-combinations/index.ts index 05a5fb8ae7..98e8ca080d 100644 --- a/apps/typegpu-docs/src/examples/geometry/lines-combinations/index.ts +++ b/apps/typegpu-docs/src/examples/geometry/lines-combinations/index.ts @@ -454,6 +454,7 @@ export const controls = { initial: Object.keys(testCases)[0], options: Object.keys(testCases), onSelectChange: async (selected: keyof typeof testCases) => { + // biome-ignore lint/performance/noDynamicNamespaceImportAccess: this is intended testCase = testCases[selected]; pipelines = createPipelines(); }, diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts index d1b70aff50..a228c060be 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts @@ -4,7 +4,6 @@ import type { GeometryData } from './types.ts'; import { VertexData } from './types.ts'; export class BoxGeometry { - #root: TgpuRoot; #modelMatrix: d.m4x4f; #position: d.v3f; #scale: d.v3f; @@ -19,7 +18,6 @@ export class BoxGeometry { root: TgpuRoot, size: [number, number, number] = [1, 1, 1], ) { - this.#root = root; this.#modelMatrix = d.mat4x4f.identity(); this.#position = d.vec3f(0, 0, 0); this.#scale = d.vec3f(1, 1, 1); @@ -30,7 +28,6 @@ export class BoxGeometry { this.#generateGeometry(); - // Create GPU buffers this.#vertexBuffer = root .createBuffer( d.arrayOf(VertexData, this.#vertices.length), @@ -193,7 +190,6 @@ export class BoxGeometry { ); }); - // Two triangles per face this.#indices.push(startIndex, startIndex + 1, startIndex + 2); this.#indices.push(startIndex, startIndex + 2, startIndex + 3); } diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts index b21e52ce81..c2deae7a3d 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts @@ -4,7 +4,6 @@ import * as m from 'wgpu-matrix'; import { CameraData } from './types.ts'; export class Camera { - #root: TgpuRoot; #viewProjectionMatrix: d.m4x4f; #position: d.v3f; #target: d.v3f; @@ -22,7 +21,6 @@ export class Camera { near: number = 0.1, far: number = 1000, ) { - this.#root = root; this.#viewProjectionMatrix = d.mat4x4f.identity(); this.#position = d.vec3f(0, 0, 0); this.#target = d.vec3f(0, 0, -1); @@ -36,7 +34,6 @@ export class Camera { inverseViewProjectionMatrix: this.#inverseViewProjectionMatrix, }); - // Create uniform buffer this.#uniform = root.createUniform(CameraData, this.#data); } @@ -107,7 +104,6 @@ export class Camera { inverseViewProjectionMatrix: this.#inverseViewProjectionMatrix, }); - // Update the uniform buffer this.#uniform.write(this.#data); } } 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 196d81503b..e408e26b83 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 @@ -226,7 +226,7 @@ const pipelineMain = root['~unstable'] const pipelineDepthOne = root['~unstable'] .withVertex(vertexDepth, vertexLayout.attrib) - .withFragment(fragmentDepth, {} as never) + .withFragment(fragmentDepth, {}) .withDepthStencil({ format: 'depth24plus', depthWriteEnabled: true, @@ -234,8 +234,9 @@ const pipelineDepthOne = root['~unstable'] }) .createPipeline(); +let renderDepthMap = false; + function render() { - // Render shadow maps using the point light abstraction pointLight.renderShadowMaps( pipelineDepthOne, renderLayout, @@ -244,16 +245,17 @@ function render() { [cube, floorCube], ); - // Uncomment to see debug view of shadow maps - debugPipeline - .withColorAttachment({ - view: context.getCurrentTexture().createView(), - loadOp: 'clear', - storeOp: 'store', - }) - .draw(3); - requestAnimationFrame(render); - return; + if (renderDepthMap) { + debugPipeline + .withColorAttachment({ + view: context.getCurrentTexture().createView(), + loadOp: 'clear', + storeOp: 'store', + }) + .draw(3); + requestAnimationFrame(render); + return; + } // Render cube modelMatrixUniform.write(cube.modelMatrix); @@ -297,7 +299,6 @@ function render() { } requestAnimationFrame(render); -// Resize observer const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const width = entry.contentBoxSize[0].inlineSize; @@ -418,4 +419,10 @@ export const controls = { ); }, }, + 'Depth map view': { + initial: false, + onToggleChange: (v: boolean) => { + renderDepthMap = v; + }, + }, }; diff --git a/packages/typegpu/src/core/function/fnTypes.ts b/packages/typegpu/src/core/function/fnTypes.ts index 3760fd61d7..359c3484f4 100644 --- a/packages/typegpu/src/core/function/fnTypes.ts +++ b/packages/typegpu/src/core/function/fnTypes.ts @@ -1,5 +1,5 @@ import type * as tinyest from 'tinyest'; -import type { BuiltinClipDistances, BuiltinFragDepth } from '../../builtin.ts'; +import type { BuiltinClipDistances } from '../../builtin.ts'; import type { AnyAttribute } from '../../data/attributes.ts'; import type { Bool, diff --git a/packages/typegpu/src/core/function/tgpuFragmentFn.ts b/packages/typegpu/src/core/function/tgpuFragmentFn.ts index 3c1c5b47c7..76e97bb85b 100644 --- a/packages/typegpu/src/core/function/tgpuFragmentFn.ts +++ b/packages/typegpu/src/core/function/tgpuFragmentFn.ts @@ -77,7 +77,7 @@ export type TgpuFragmentFnShell< input: InferIO, out: FragmentOut extends IORecord ? WgslStruct : FragmentOut, ) => InferIO, - ) => TgpuFragmentFn, OmitBuiltins>) + ) => TgpuFragmentFn, FragmentOut>) & /** * @param implementation * Raw WGSL function implementation with header and body @@ -85,11 +85,11 @@ export type TgpuFragmentFnShell< * e.g. `"(x: f32) -> f32 { return x; }"`; */ (( implementation: string, - ) => TgpuFragmentFn, OmitBuiltins>) + ) => TgpuFragmentFn, FragmentOut>) & (( strings: TemplateStringsArray, ...values: unknown[] - ) => TgpuFragmentFn, OmitBuiltins>) + ) => TgpuFragmentFn, FragmentOut>) & { /** * @deprecated Invoke the shell as a function instead. @@ -97,7 +97,7 @@ export type TgpuFragmentFnShell< does: & (( implementation: (input: InferIO) => InferIO, - ) => TgpuFragmentFn, OmitBuiltins>) + ) => TgpuFragmentFn, FragmentOut>) & /** * @param implementation * Raw WGSL function implementation with header and body @@ -105,7 +105,7 @@ export type TgpuFragmentFnShell< * e.g. `"(x: f32) -> f32 { return x; }"`; */ (( implementation: string, - ) => TgpuFragmentFn, OmitBuiltins>); + ) => TgpuFragmentFn, FragmentOut>); }; export interface TgpuFragmentFn< diff --git a/packages/typegpu/src/core/pipeline/renderPipeline.ts b/packages/typegpu/src/core/pipeline/renderPipeline.ts index 80b7c3c7cf..d099481cca 100644 --- a/packages/typegpu/src/core/pipeline/renderPipeline.ts +++ b/packages/typegpu/src/core/pipeline/renderPipeline.ts @@ -11,6 +11,7 @@ import { type ResolvedSnippet, snip } from '../../data/snippet.ts'; import type { WgslTexture } from '../../data/texture.ts'; import { type AnyWgslData, + type Decorated, isWgslData, type U16, type U32, @@ -141,15 +142,23 @@ export interface TgpuRenderPipeline } export type FragmentOutToTargets = T extends IOData - ? GPUColorTargetState - : T extends Record - ? { [Key in keyof T]: GPUColorTargetState } + ? T extends Decorated ? Record + : GPUColorTargetState + : T extends Record ? { + [Key in keyof T as T[Key] extends Decorated ? never : Key]: + GPUColorTargetState; + } : T extends { type: 'void' } ? Record : never; export type FragmentOutToColorAttachment = T extends IOData - ? ColorAttachment - : T extends Record ? { [Key in keyof T]: ColorAttachment } + ? T extends Decorated ? Record + : ColorAttachment + : T extends Record ? { + [Key in keyof T as T[Key] extends Decorated ? never : Key]: + ColorAttachment; + } + : T extends { type: 'void' } ? Record : never; export type AnyFragmentTargets = diff --git a/packages/typegpu/tests/renderPipeline.test.ts b/packages/typegpu/tests/renderPipeline.test.ts index 22d3f35177..0dd3f383b3 100644 --- a/packages/typegpu/tests/renderPipeline.test.ts +++ b/packages/typegpu/tests/renderPipeline.test.ts @@ -139,6 +139,52 @@ describe('TgpuRenderPipeline', () => { ).toEqualTypeOf>(); }); + it('properly handles custom depth output in fragment functions', ({ root }) => { + const vertices = tgpu.const(d.arrayOf(d.vec2f, 3), [ + d.vec2f(-1, -1), + d.vec2f(3, -1), + d.vec2f(-1, 3), + ]); + const vertexMain = tgpu['~unstable'].vertexFn({ + in: { vid: d.builtin.vertexIndex }, + out: { pos: d.builtin.position }, + // biome-ignore lint/style/noNonNullAssertion: it's fine + })(({ vid }) => ({ pos: d.vec4f(vertices.$[vid]!, 0, 1) })); + + const fragmentMain = tgpu['~unstable'].fragmentFn({ + out: { color: d.vec4f, depth: d.builtin.fragDepth }, + })(() => ({ color: d.vec4f(1, 0, 0, 1), depth: 0.5 })); + + const pipeline = root + .withVertex(vertexMain, {}) + .withFragment(fragmentMain, { color: { format: 'rgba8unorm' } }) + .createPipeline(); + + pipeline.withColorAttachment({ + color: { + view: {} as unknown as GPUTextureView, + loadOp: 'clear', + storeOp: 'store', + }, + }); + + expect(() => { + pipeline.withColorAttachment({ + color: { + view: {} as unknown as GPUTextureView, + loadOp: 'clear', + storeOp: 'store', + }, + // @ts-expect-error + depth: { + view: {} as unknown as GPUTextureView, + loadOp: 'clear', + storeOp: 'store', + }, + }); + }); + }); + it('type checks passed bind groups', ({ root }) => { const vertexMain = tgpu['~unstable'].vertexFn({ out: { bar: d.location(0, d.vec3f) }, From afc1f47505d4a63c02252daee7b2100d0690fbc0 Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Mon, 24 Nov 2025 18:14:48 +0100 Subject: [PATCH 06/17] sane coordinate system and msaa --- .../rendering/point-light-shadow/camera.ts | 7 +-- .../rendering/point-light-shadow/index.ts | 34 ++++++++++--- .../point-light-shadow/point-light.ts | 49 +++++++++---------- .../src/core/pipeline/renderPipeline.ts | 1 + 4 files changed, 55 insertions(+), 36 deletions(-) diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts index c2deae7a3d..cd8a7ce0a1 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts @@ -83,11 +83,12 @@ export class Camera { #recompute() { const view = m.mat4.lookAt( - this.#position, - this.#target, - this.#up, + d.vec3f(-this.#position.x, this.#position.y, this.#position.z), + d.vec3f(-this.#target.x, this.#target.y, this.#target.z), + d.vec3f(-this.#up.x, this.#up.y, this.#up.z), d.mat4x4f(), ); + m.mat4.scale(view, d.vec3f(-1, 1, 1), view); const projection = m.mat4.perspective( (this.#fov * Math.PI) / 180, 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 e408e26b83..f4eb51df1b 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 @@ -22,7 +22,7 @@ context.configure({ const vertexLayout = tgpu.vertexLayout(d.arrayOf(VertexData)); const mainCamera = new Camera(root); -mainCamera.position = d.vec3f(10, 10, 10); +mainCamera.position = d.vec3f(5, 5, -5); mainCamera.target = d.vec3f(0, 0, 0); const cube = new BoxGeometry(root); @@ -34,7 +34,7 @@ floorCube.position = d.vec3f(0, -0.5, 0); const pointLight = new PointLight(root, d.vec3f(2, 4, 1), { far: 100.0, - shadowMapSize: 4096, + shadowMapSize: 2048, }); const modelMatrixUniform = root.createBuffer(d.mat4x4f).$usage('uniform'); @@ -43,6 +43,14 @@ let depthTexture = root['~unstable'] .createTexture({ size: [canvas.width, canvas.height], format: 'depth24plus', + sampleCount: 4, + }) + .$usage('render'); +let msaaTexture = root['~unstable'] + .createTexture({ + size: [canvas.width, canvas.height], + format: presentationFormat, + sampleCount: 4, }) .$usage('render'); @@ -75,7 +83,7 @@ const debugFragment = tgpu['~unstable'].fragmentFn({ })(({ uv }) => { const tiled = d.vec2f( std.fract(uv.x * 3), - 1.0 - std.fract(uv.y * 2), + std.fract(uv.y * 2), ); const col = std.floor(uv.x * 3); @@ -172,8 +180,7 @@ const fragmentMain = tgpu['~unstable'].fragmentFn({ const lightToFrag = worldPos.sub(lightPos); const dist = std.length(lightToFrag); - let dir = std.normalize(lightToFrag); - dir = d.vec3f(dir.x, -dir.y, dir.z); + const dir = std.normalize(lightToFrag); const depthRef = dist / pointLight.far; @@ -222,6 +229,9 @@ const pipelineMain = root['~unstable'] depthWriteEnabled: true, depthCompare: 'less', }) + .withMultisample({ + count: 4, + }) .createPipeline(); const pipelineDepthOne = root['~unstable'] @@ -267,7 +277,8 @@ function render() { depthStoreOp: 'store', }) .withColorAttachment({ - view: context.getCurrentTexture().createView(), + resolveTarget: context.getCurrentTexture().createView(), + view: root.unwrap(msaaTexture).createView(), loadOp: 'clear', storeOp: 'store', }) @@ -286,7 +297,8 @@ function render() { depthStoreOp: 'store', }) .withColorAttachment({ - view: context.getCurrentTexture().createView(), + resolveTarget: context.getCurrentTexture().createView(), + view: root.unwrap(msaaTexture).createView(), loadOp: 'load', storeOp: 'store', }) @@ -318,6 +330,14 @@ const resizeObserver = new ResizeObserver((entries) => { .createTexture({ size: [canvas.width, canvas.height], format: 'depth24plus', + sampleCount: 4, + }) + .$usage('render'); + msaaTexture = root['~unstable'] + .createTexture({ + size: [canvas.width, canvas.height], + format: presentationFormat, + sampleCount: 4, }) .$usage('render'); } diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts index d925719eea..ec22192fc7 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts @@ -1,6 +1,7 @@ import type { RenderFlag, SampledFlag, + TgpuBindGroup, TgpuBindGroupLayout, TgpuBuffer, TgpuRenderPipeline, @@ -35,6 +36,7 @@ export class PointLight { backward: Camera; }; #positionUniform: TgpuUniform; + #bindGroups: Map = new Map(); constructor( root: TgpuRoot, @@ -75,44 +77,35 @@ export class PointLight { } #configureCameras() { - // WebGPU cubemap face orientations based on spec diagram - // For lookAt: right = cross(forward, worldUp), so we need to find worldUp such that right matches +U direction - - // [0] +X face: forward=+X, +U=-Z, +V=-Y - // cross(+X, worldUp) = -Z → cross((1,0,0), (0,a,b)) = (0,-b,a) = (0,0,-1) → a=-1, b=0 + // +X (Right) this.#shadowCameras.right.position = this.#position; this.#shadowCameras.right.target = this.#position.add(d.vec3f(1, 0, 0)); - this.#shadowCameras.right.up = d.vec3f(0, -1, 0); + this.#shadowCameras.right.up = d.vec3f(0, 1, 0); - // [1] -X face: forward=-X, +U=+Z, +V=-Y - // cross(-X, worldUp) = +Z → cross((-1,0,0), (0,a,b)) = (0,b,-a) = (0,0,1) → b=0, a=-1 + // -X (Left) this.#shadowCameras.left.position = this.#position; this.#shadowCameras.left.target = this.#position.add(d.vec3f(-1, 0, 0)); - this.#shadowCameras.left.up = d.vec3f(0, -1, 0); + this.#shadowCameras.left.up = d.vec3f(0, 1, 0); - // [2] +Y face: forward=+Y, +U=+X, +V=+Z - // cross(+Y, worldUp) = +X → cross((0,1,0), (a,0,c)) = (c,0,-a) = (1,0,0) → c=1, a=0 + // +Y (Top) this.#shadowCameras.up.position = this.#position; - this.#shadowCameras.up.target = this.#position.add(d.vec3f(0, -1, 0)); + this.#shadowCameras.up.target = this.#position.add(d.vec3f(0, 1, 0)); this.#shadowCameras.up.up = d.vec3f(0, 0, -1); - // [3] -Y face: +U points +X, +V points -Z - // Camera right should be +X, camera up should be -Z + // -Y (Bottom) this.#shadowCameras.down.position = this.#position; - this.#shadowCameras.down.target = this.#position.add(d.vec3f(0, 1, 0)); + this.#shadowCameras.down.target = this.#position.add(d.vec3f(0, -1, 0)); this.#shadowCameras.down.up = d.vec3f(0, 0, 1); - // [4] +Z face: +U points +X, +V points -Y - // Camera right should be +X, camera up should be -Y + // +Z (Front) this.#shadowCameras.forward.position = this.#position; this.#shadowCameras.forward.target = this.#position.add(d.vec3f(0, 0, 1)); - this.#shadowCameras.forward.up = d.vec3f(0, -1, 0); + this.#shadowCameras.forward.up = d.vec3f(0, 1, 0); - // [5] -Z face: +U points -X, +V points -Y - // Camera right should be -X, camera up should be -Y + // -Z (Back) this.#shadowCameras.backward.position = this.#position; this.#shadowCameras.backward.target = this.#position.add(d.vec3f(0, 0, -1)); - this.#shadowCameras.backward.up = d.vec3f(0, -1, 0); + this.#shadowCameras.backward.up = d.vec3f(0, 1, 0); } set position(pos: d.v3f) { @@ -171,11 +164,15 @@ export class PointLight { arrayLayerCount: 1, }); - const bindGroup = this.#root.createBindGroup(bindGroupLayout, { - camera: camera.uniform.buffer, - modelMatrix: modelMatrixUniform, - lightPosition: this.#positionUniform.buffer, - }); + let bindGroup = this.#bindGroups.get(key); + if (!bindGroup) { + bindGroup = this.#root.createBindGroup(bindGroupLayout, { + camera: camera.uniform.buffer, + modelMatrix: modelMatrixUniform, + lightPosition: this.#positionUniform.buffer, + }); + this.#bindGroups.set(key, bindGroup); + } // Render each geometry for (let i = 0; i < geometries.length; i++) { diff --git a/packages/typegpu/src/core/pipeline/renderPipeline.ts b/packages/typegpu/src/core/pipeline/renderPipeline.ts index d099481cca..945a012c1d 100644 --- a/packages/typegpu/src/core/pipeline/renderPipeline.ts +++ b/packages/typegpu/src/core/pipeline/renderPipeline.ts @@ -217,6 +217,7 @@ export interface DepthStencilAttachment { & TgpuTexture<{ size: [number, number]; format: 'depth24plus' | 'depth24plus-stencil8' | 'depth32float'; + sampleCount?: number; }> & RenderFlag ) From b69593aeab82b1007f1898499453a623ccd123cd Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Mon, 24 Nov 2025 18:54:01 +0100 Subject: [PATCH 07/17] move to instancing --- .../point-light-shadow/box-geometry.ts | 11 +- .../rendering/point-light-shadow/index.ts | 234 +++++++----------- .../point-light-shadow/point-light.ts | 57 ++--- .../rendering/point-light-shadow/types.ts | 15 ++ .../core/pipeline/connectTargetsToShader.ts | 1 - 5 files changed, 148 insertions(+), 170 deletions(-) diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts index a228c060be..4958d9ae87 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts @@ -1,7 +1,7 @@ import type { IndexFlag, TgpuBuffer, TgpuRoot, VertexFlag } from 'typegpu'; import * as d from 'typegpu/data'; import type { GeometryData } from './types.ts'; -import { VertexData } from './types.ts'; +import { InstanceData, VertexData } from './types.ts'; export class BoxGeometry { #modelMatrix: d.m4x4f; @@ -67,8 +67,13 @@ export class BoxGeometry { return this.#rotation; } - get modelMatrix(): d.m4x4f { - return this.#modelMatrix; + get instanceData(): d.Infer { + return InstanceData({ + column1: this.#modelMatrix.columns[0], + column2: this.#modelMatrix.columns[1], + column3: this.#modelMatrix.columns[2], + column4: this.#modelMatrix.columns[3], + }); } get vertices(): GeometryData { 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 f4eb51df1b..48e71a5f73 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 @@ -1,11 +1,17 @@ import tgpu from 'typegpu'; +import { fullScreenTriangle } from 'typegpu/common'; import * as d from 'typegpu/data'; import * as std from 'typegpu/std'; -import { Camera } from './camera.ts'; import { BoxGeometry } from './box-geometry.ts'; +import { Camera } from './camera.ts'; import { PointLight } from './point-light.ts'; -import { CameraData, VertexData } from './types.ts'; -import { fullScreenTriangle } from 'typegpu/common'; +import { + CameraData, + InstanceData, + instanceLayout, + VertexData, + vertexLayout, +} from './types.ts'; const root = await tgpu.init(); const device = root.device; @@ -19,12 +25,15 @@ context.configure({ format: presentationFormat, }); -const vertexLayout = tgpu.vertexLayout(d.arrayOf(VertexData)); - const mainCamera = new Camera(root); mainCamera.position = d.vec3f(5, 5, -5); mainCamera.target = d.vec3f(0, 0, 0); +const pointLight = new PointLight(root, d.vec3f(2, 4, 1), { + far: 100.0, + shadowMapSize: 2048, +}); + const cube = new BoxGeometry(root); cube.scale = d.vec3f(3, 1, 0.2); @@ -32,13 +41,6 @@ const floorCube = new BoxGeometry(root); floorCube.scale = d.vec3f(10, 0.1, 10); floorCube.position = d.vec3f(0, -0.5, 0); -const pointLight = new PointLight(root, d.vec3f(2, 4, 1), { - far: 100.0, - shadowMapSize: 2048, -}); - -const modelMatrixUniform = root.createBuffer(d.mat4x4f).$usage('uniform'); - let depthTexture = root['~unstable'] .createTexture({ size: [canvas.width, canvas.height], @@ -46,6 +48,7 @@ let depthTexture = root['~unstable'] sampleCount: 4, }) .$usage('render'); + let msaaTexture = root['~unstable'] .createTexture({ size: [canvas.width, canvas.height], @@ -54,118 +57,69 @@ let msaaTexture = root['~unstable'] }) .$usage('render'); -// Bind group layouts +const shadowSampler = root['~unstable'].createComparisonSampler({ + compare: 'less', + magFilter: 'linear', + minFilter: 'linear', +}); + +const shadowCubeView = pointLight.createCubeView(); + const renderLayout = tgpu.bindGroupLayout({ camera: { uniform: CameraData }, - modelMatrix: { uniform: d.mat4x4f }, lightPosition: { uniform: d.vec3f }, }); const renderLayoutWithShadow = tgpu.bindGroupLayout({ camera: { uniform: CameraData }, - modelMatrix: { uniform: d.mat4x4f }, shadowDepthCube: { texture: d.textureDepthCube() }, shadowSampler: { sampler: 'comparison' }, lightPosition: { uniform: d.vec3f }, }); -// Debug pipeline setup -const debugSampler = root['~unstable'].createSampler({ - minFilter: 'nearest', - magFilter: 'nearest', -}); - -const debugView = pointLight.createDebugArrayView(); - -const debugFragment = tgpu['~unstable'].fragmentFn({ - in: { uv: d.vec2f }, - out: d.vec4f, -})(({ uv }) => { - const tiled = d.vec2f( - std.fract(uv.x * 3), - std.fract(uv.y * 2), - ); - - const col = std.floor(uv.x * 3); - const row = std.floor(uv.y * 2); - const arrayIndex = d.i32(row * 3 + col); - - const depth = std.textureSample( - debugView.$, - debugSampler.$, - tiled, - arrayIndex, - ); - return d.vec4f(d.vec3f(depth ** 0.5), 1.0); -}); - -const debugPipeline = root['~unstable'] - .withVertex(fullScreenTriangle, {}) - .withFragment(debugFragment, { format: presentationFormat }) - .createPipeline(); - -// Shadow depth pass shaders const vertexDepth = tgpu['~unstable'].vertexFn({ - in: { - ...VertexData.propTypes, - }, + in: { ...VertexData.propTypes, ...InstanceData.propTypes }, out: { pos: d.builtin.position, worldPos: d.vec3f, }, -})(({ position }) => { - const worldPos = renderLayout.$.modelMatrix.mul(d.vec4f(position, 1)).xyz; +})(({ position, column1, column2, column3, column4 }) => { + const modelMatrix = d.mat4x4f(column1, column2, column3, column4); + const worldPos = modelMatrix.mul(d.vec4f(position, 1)).xyz; const pos = renderLayout.$.camera.viewProjectionMatrix.mul( d.vec4f(worldPos, 1), ); - return { - pos, - worldPos, - }; + return { pos, worldPos }; }); const fragmentDepth = tgpu['~unstable'].fragmentFn({ - in: { - worldPos: d.vec3f, - }, + in: { worldPos: d.vec3f }, out: d.builtin.fragDepth, })(({ worldPos }) => { const lightPos = renderLayout.$.lightPosition; - const lightToFrag = worldPos.sub(lightPos); const dist = std.length(lightToFrag); - // map [0, lightFar] -> [0, 1] - const depth = dist / pointLight.far; - - return depth; + return dist / pointLight.far; }); -// Main render pass shaders const vertexMain = tgpu['~unstable'].vertexFn({ - in: { - ...VertexData.propTypes, - }, + in: { ...VertexData.propTypes, ...InstanceData.propTypes }, out: { pos: d.builtin.position, worldPos: d.vec3f, uv: d.vec2f, normal: d.vec3f, }, -})(({ position, uv, normal }) => { - const worldPos = - renderLayoutWithShadow.$.modelMatrix.mul(d.vec4f(position, 1)).xyz; +})(({ position, uv, normal, column1, column2, column3, column4 }) => { + const modelMatrix = d.mat4x4f(column1, column2, column3, column4); + const worldPos = modelMatrix.mul(d.vec4f(position, 1)).xyz; const pos = renderLayoutWithShadow.$.camera.viewProjectionMatrix.mul( d.vec4f(worldPos, 1), ); - return { - pos, - worldPos, - uv, - normal, - }; + return { pos, worldPos, uv, normal }; }); const fragmentMain = tgpu['~unstable'].fragmentFn({ @@ -202,61 +156,88 @@ const fragmentMain = tgpu['~unstable'].fragmentFn({ return d.vec4f(color, 1.0); }); -// Samplers and views -const shadowSampler = root['~unstable'].createComparisonSampler({ - compare: 'less', - magFilter: 'linear', - minFilter: 'linear', +const previewSampler = root['~unstable'].createSampler({ + minFilter: 'nearest', + magFilter: 'nearest', }); -const shadowCubeView = pointLight.createCubeView(); +const previewView = pointLight.createDepthArrayView(); -// Main render bind group -const mainBindGroup = root.createBindGroup(renderLayoutWithShadow, { - camera: mainCamera.uniform.buffer, - modelMatrix: modelMatrixUniform, - shadowDepthCube: shadowCubeView, - shadowSampler: shadowSampler, - lightPosition: pointLight.positionUniform.buffer, +const previewFragment = tgpu['~unstable'].fragmentFn({ + in: { uv: d.vec2f }, + out: d.vec4f, +})(({ uv }) => { + const tiled = d.vec2f( + std.fract(uv.x * 3), + std.fract(uv.y * 2), + ); + + const col = std.floor(uv.x * 3); + const row = std.floor(uv.y * 2); + const arrayIndex = d.i32(row * 3 + col); + + const depth = std.textureSample( + previewView.$, + previewSampler.$, + tiled, + arrayIndex, + ); + return d.vec4f(d.vec3f(depth ** 0.5), 1.0); }); -// Pipelines -const pipelineMain = root['~unstable'] - .withVertex(vertexMain, vertexLayout.attrib) - .withFragment(fragmentMain, { format: presentationFormat }) +const pipelineDepthOne = root['~unstable'] + .withVertex(vertexDepth, { ...vertexLayout.attrib, ...instanceLayout.attrib }) + .withFragment(fragmentDepth, {}) .withDepthStencil({ format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less', }) - .withMultisample({ - count: 4, - }) .createPipeline(); -const pipelineDepthOne = root['~unstable'] - .withVertex(vertexDepth, vertexLayout.attrib) - .withFragment(fragmentDepth, {}) +const pipelineMain = root['~unstable'] + .withVertex(vertexMain, { ...vertexLayout.attrib, ...instanceLayout.attrib }) + .withFragment(fragmentMain, { format: presentationFormat }) .withDepthStencil({ format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less', }) + .withMultisample({ count: 4 }) .createPipeline(); -let renderDepthMap = false; +const pipelinePreview = root['~unstable'] + .withVertex(fullScreenTriangle, {}) + .withFragment(previewFragment, { format: presentationFormat }) + .createPipeline(); + +const mainBindGroup = root.createBindGroup(renderLayoutWithShadow, { + camera: mainCamera.uniform.buffer, + shadowDepthCube: shadowCubeView, + shadowSampler: shadowSampler, + lightPosition: pointLight.positionUniform.buffer, +}); + +const instanceBuffer = root.createBuffer(d.arrayOf(InstanceData, 2), [ + cube.instanceData, + floorCube.instanceData, +]).$usage('vertex'); + +let showDepthPreview = false; function render() { pointLight.renderShadowMaps( pipelineDepthOne, renderLayout, - modelMatrixUniform, - vertexLayout, - [cube, floorCube], + cube.vertexBuffer, + instanceBuffer, + cube.indexBuffer, + cube.indexCount, + 2, ); - if (renderDepthMap) { - debugPipeline + if (showDepthPreview) { + pipelinePreview .withColorAttachment({ view: context.getCurrentTexture().createView(), loadOp: 'clear', @@ -267,8 +248,7 @@ function render() { return; } - // Render cube - modelMatrixUniform.write(cube.modelMatrix); + // Render cubes pipelineMain .withDepthStencilAttachment({ view: depthTexture, @@ -285,27 +265,8 @@ function render() { .with(mainBindGroup) .withIndexBuffer(cube.indexBuffer) .with(vertexLayout, cube.vertexBuffer) - .drawIndexed(cube.indexCount); - - // Render floor - modelMatrixUniform.write(floorCube.modelMatrix); - pipelineMain - .withDepthStencilAttachment({ - view: depthTexture, - depthClearValue: 1, - depthLoadOp: 'load', - depthStoreOp: 'store', - }) - .withColorAttachment({ - resolveTarget: context.getCurrentTexture().createView(), - view: root.unwrap(msaaTexture).createView(), - loadOp: 'load', - storeOp: 'store', - }) - .with(mainBindGroup) - .withIndexBuffer(floorCube.indexBuffer) - .with(vertexLayout, floorCube.vertexBuffer) - .drawIndexed(floorCube.indexCount); + .with(instanceLayout, instanceBuffer) + .drawIndexed(cube.indexCount, 2); requestAnimationFrame(render); } @@ -325,7 +286,6 @@ const resizeObserver = new ResizeObserver((entries) => { Math.min(height, device.limits.maxTextureDimension2D), ); - // Recreate depth texture with new size depthTexture = root['~unstable'] .createTexture({ size: [canvas.width, canvas.height], @@ -345,7 +305,6 @@ const resizeObserver = new ResizeObserver((entries) => { resizeObserver.observe(canvas); -// Orbit controls let theta = Math.atan2(10, 10); let phi = Math.acos(10 / Math.sqrt(10 * 10 + 10 * 10 + 10 * 10)); let radius = Math.sqrt(10 * 10 + 10 * 10 + 10 * 10); @@ -384,7 +343,6 @@ canvas.addEventListener('mousemove', (e) => { theta -= deltaX * 0.01; phi -= deltaY * 0.01; - // Clamp phi to avoid gimbal lock phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi)); updateCameraPosition(); @@ -439,10 +397,10 @@ export const controls = { ); }, }, - 'Depth map view': { + 'Show Depth Preview': { initial: false, onToggleChange: (v: boolean) => { - renderDepthMap = v; + showDepthPreview = v; }, }, }; diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts index ec22192fc7..cd00004c73 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts @@ -1,4 +1,5 @@ import type { + IndexFlag, RenderFlag, SampledFlag, TgpuBindGroup, @@ -8,12 +9,16 @@ import type { TgpuRoot, TgpuTexture, TgpuUniform, - TgpuVertexLayout, - UniformFlag, + VertexFlag, } from 'typegpu'; import * as d from 'typegpu/data'; -import type { BoxGeometry } from './box-geometry.ts'; import { Camera } from './camera.ts'; +import { + type InstanceData, + instanceLayout, + type VertexData, + vertexLayout, +} from './types.ts'; export class PointLight { #root: TgpuRoot; @@ -51,7 +56,6 @@ export class PointLight { this.#far = options.far ?? 100.0; this.#shadowMapSize = options.shadowMapSize ?? 512; - // Create cubemap depth texture this.#depthCubeTexture = root['~unstable'] .createTexture({ size: [this.#shadowMapSize, this.#shadowMapSize, 6], @@ -60,10 +64,8 @@ export class PointLight { }) .$usage('render', 'sampled'); - // Create position uniform this.#positionUniform = root.createUniform(d.vec3f, this.#position); - // Create shadow cameras for each cubemap face this.#shadowCameras = { right: new Camera(root, 90, 0.1, this.#far), left: new Camera(root, 90, 0.1, this.#far), @@ -134,7 +136,7 @@ export class PointLight { return this.#depthCubeTexture.createView(d.textureDepthCube()); } - createDebugArrayView() { + createDepthArrayView() { return this.#depthCubeTexture.createView(d.textureDepth2dArray(), { baseArrayLayer: 0, arrayLayerCount: 6, @@ -145,9 +147,11 @@ export class PointLight { renderShadowMaps( pipeline: TgpuRenderPipeline, bindGroupLayout: TgpuBindGroupLayout, - modelMatrixUniform: TgpuBuffer & UniformFlag, - vertexLayout: TgpuVertexLayout, - geometries: BoxGeometry[], + vertexBuffer: TgpuBuffer> & VertexFlag, + instanceBuffer: TgpuBuffer> & VertexFlag, + indexBuffer: TgpuBuffer> & IndexFlag, + vertexCount: number, + instanceCount: number, ) { const faceIndices = { right: 0, @@ -168,29 +172,26 @@ export class PointLight { if (!bindGroup) { bindGroup = this.#root.createBindGroup(bindGroupLayout, { camera: camera.uniform.buffer, - modelMatrix: modelMatrixUniform, lightPosition: this.#positionUniform.buffer, }); this.#bindGroups.set(key, bindGroup); } - // Render each geometry - for (let i = 0; i < geometries.length; i++) { - const geometry = geometries[i]; - modelMatrixUniform.write(geometry.modelMatrix); - - pipeline - .withDepthStencilAttachment({ - view: this.#root.unwrap(view), - depthClearValue: 1, - depthLoadOp: i === 0 ? 'clear' : 'load', - depthStoreOp: 'store', - }) - .with(vertexLayout, geometry.vertexBuffer) - .with(bindGroup) - .withIndexBuffer(geometry.indexBuffer) - .drawIndexed(geometry.indexCount); - } + pipeline + .withDepthStencilAttachment({ + view: this.#root.unwrap(view), + depthClearValue: 1, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }) + .with(vertexLayout, vertexBuffer) + .with(instanceLayout, instanceBuffer) + .with(bindGroup) + .withIndexBuffer(indexBuffer) + .drawIndexed( + vertexCount, + instanceCount, + ); } } } diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/types.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/types.ts index 03e4d8be25..efd1f86637 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/types.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/types.ts @@ -1,3 +1,4 @@ +import tgpu from 'typegpu'; import * as d from 'typegpu/data'; export const CameraData = d.struct({ @@ -13,4 +14,18 @@ export const VertexData = d.struct({ }); export type VertexData = typeof VertexData; +export const InstanceData = d.struct({ + column1: d.vec4f, + column2: d.vec4f, + column3: d.vec4f, + column4: d.vec4f, +}); +export type InstanceData = typeof InstanceData; + +export const vertexLayout = tgpu.vertexLayout(d.arrayOf(VertexData)); +export const instanceLayout = tgpu.vertexLayout( + d.arrayOf(InstanceData), + 'instance', +); + export type GeometryData = d.Infer[]; diff --git a/packages/typegpu/src/core/pipeline/connectTargetsToShader.ts b/packages/typegpu/src/core/pipeline/connectTargetsToShader.ts index d6f5067a99..099d416122 100644 --- a/packages/typegpu/src/core/pipeline/connectTargetsToShader.ts +++ b/packages/typegpu/src/core/pipeline/connectTargetsToShader.ts @@ -14,7 +14,6 @@ export function connectTargetsToShader( shaderOutputLayout: FragmentOutConstrained, targets: AnyFragmentTargets, ): GPUColorTargetState[] { - console.log('Connecting targets to shader...', shaderOutputLayout, targets); if (isData(shaderOutputLayout)) { if (isVoid(shaderOutputLayout)) { return []; From 998cae738929b518d98fbbaf57943e3a97c01333 Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Tue, 25 Nov 2025 13:38:43 +0100 Subject: [PATCH 08/17] mucho trabajo --- .../rendering/cubemap-reflection/index.ts | 83 ++-- .../phong-reflection/setup-orbit-camera.ts | 19 +- .../point-light-shadow/box-geometry.ts | 261 ++++------- .../rendering/point-light-shadow/camera.ts | 75 ++- .../rendering/point-light-shadow/index.ts | 432 +++++++++++++----- .../point-light-shadow/point-light.ts | 171 ++----- .../rendering/point-light-shadow/scene.ts | 50 ++ .../src/examples/rendering/two-boxes/index.ts | 147 +++--- 8 files changed, 679 insertions(+), 559 deletions(-) create mode 100644 apps/typegpu-docs/src/examples/rendering/point-light-shadow/scene.ts 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 8731a57e14..0a9e658198 100644 --- a/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.ts @@ -352,21 +352,14 @@ resizeObserver.observe(canvas); let isDragging = false; let prevX = 0; let prevY = 0; +let lastPinchDist = 0; let orbitRadius = std.length(cameraInitialPos); // Yaw and pitch angles facing the origin. let orbitYaw = Math.atan2(cameraInitialPos.x, cameraInitialPos.z); let orbitPitch = Math.asin(cameraInitialPos.y / orbitRadius); -function updateCameraOrbit(dx: number, dy: number) { - const orbitSensitivity = 0.005; - orbitYaw += -dx * orbitSensitivity; - orbitPitch += dy * orbitSensitivity; - // Clamp pitch to avoid flipping - const maxPitch = Math.PI / 2 - 0.01; - if (orbitPitch > maxPitch) orbitPitch = maxPitch; - if (orbitPitch < -maxPitch) orbitPitch = -maxPitch; - // Convert spherical coordinates to cartesian coordinates +function updateCameraPosition() { const newCamX = orbitRadius * Math.sin(orbitYaw) * Math.cos(orbitPitch); const newCamY = orbitRadius * Math.sin(orbitPitch); const newCamZ = orbitRadius * Math.cos(orbitYaw) * Math.cos(orbitPitch); @@ -381,21 +374,24 @@ function updateCameraOrbit(dx: number, dy: number) { cameraBuffer.writePartial({ view: newView, position: newCameraPos }); } -canvas.addEventListener('wheel', (event: WheelEvent) => { - event.preventDefault(); - const zoomSensitivity = 0.05; - orbitRadius = std.clamp(orbitRadius + event.deltaY * zoomSensitivity, 3, 100); - const newCamX = orbitRadius * Math.sin(orbitYaw) * Math.cos(orbitPitch); - const newCamY = orbitRadius * Math.sin(orbitPitch); - const newCamZ = orbitRadius * Math.cos(orbitYaw) * Math.cos(orbitPitch); - const newCameraPos = d.vec4f(newCamX, newCamY, newCamZ, 1); - const newView = m.mat4.lookAt( - newCameraPos, - d.vec3f(0, 0, 0), - d.vec3f(0, 1, 0), - d.mat4x4f(), +function updateCameraOrbit(dx: number, dy: number) { + orbitYaw += -dx * 0.005; + orbitPitch = std.clamp( + orbitPitch + dy * 0.005, + -Math.PI / 2 + 0.01, + Math.PI / 2 - 0.01, ); - cameraBuffer.writePartial({ view: newView, position: newCameraPos }); + updateCameraPosition(); +} + +function zoomCamera(delta: number) { + orbitRadius = std.clamp(orbitRadius + delta, 3, 100); + updateCameraPosition(); +} + +canvas.addEventListener('wheel', (e: WheelEvent) => { + e.preventDefault(); + zoomCamera(e.deltaY * 0.05); }, { passive: false }); canvas.addEventListener('mousedown', (event) => { @@ -404,12 +400,17 @@ canvas.addEventListener('mousedown', (event) => { prevY = event.clientY; }); -canvas.addEventListener('touchstart', (event) => { - event.preventDefault(); - if (event.touches.length === 1) { +canvas.addEventListener('touchstart', (e) => { + e.preventDefault(); + if (e.touches.length === 1) { isDragging = true; - prevX = event.touches[0].clientX; - prevY = event.touches[0].clientY; + prevX = e.touches[0].clientX; + prevY = e.touches[0].clientY; + } else if (e.touches.length === 2) { + isDragging = false; + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + lastPinchDist = Math.sqrt(dx * dx + dy * dy); } }, { passive: false }); @@ -435,14 +436,13 @@ const mouseMoveEventListener = (event: MouseEvent) => { }; window.addEventListener('mousemove', mouseMoveEventListener); -const touchMoveEventListener = (event: TouchEvent) => { - if (isDragging && event.touches.length === 1) { - event.preventDefault(); - const dx = event.touches[0].clientX - prevX; - const dy = event.touches[0].clientY - prevY; - prevX = event.touches[0].clientX; - prevY = event.touches[0].clientY; - +const touchMoveEventListener = (e: TouchEvent) => { + if (e.touches.length === 1 && isDragging) { + e.preventDefault(); + const dx = e.touches[0].clientX - prevX; + const dy = e.touches[0].clientY - prevY; + prevX = e.touches[0].clientX; + prevY = e.touches[0].clientY; updateCameraOrbit(dx, dy); } }; @@ -450,6 +450,17 @@ window.addEventListener('touchmove', touchMoveEventListener, { passive: false, }); +canvas.addEventListener('touchmove', (e) => { + if (e.touches.length === 2) { + e.preventDefault(); + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + const pinchDist = Math.sqrt(dx * dx + dy * dy); + zoomCamera((lastPinchDist - pinchDist) * 0.05); + lastPinchDist = pinchDist; + } +}, { passive: false }); + function hideHelp() { const helpElem = document.getElementById('help'); if (helpElem) { diff --git a/apps/typegpu-docs/src/examples/rendering/phong-reflection/setup-orbit-camera.ts b/apps/typegpu-docs/src/examples/rendering/phong-reflection/setup-orbit-camera.ts index 39b376d4cd..707bb5f120 100644 --- a/apps/typegpu-docs/src/examples/rendering/phong-reflection/setup-orbit-camera.ts +++ b/apps/typegpu-docs/src/examples/rendering/phong-reflection/setup-orbit-camera.ts @@ -119,6 +119,7 @@ export function setupOrbitCamera( let isDragging = false; let prevX = 0; let prevY = 0; + let lastPinchDist = 0; // mouse/touch events canvas.addEventListener('wheel', (event: WheelEvent) => { @@ -138,6 +139,11 @@ export function setupOrbitCamera( isDragging = true; prevX = event.touches[0].clientX; prevY = event.touches[0].clientY; + } else if (event.touches.length === 2) { + isDragging = false; + const dx = event.touches[0].clientX - event.touches[1].clientX; + const dy = event.touches[0].clientY - event.touches[1].clientY; + lastPinchDist = Math.sqrt(dx * dx + dy * dy); } }, { passive: false }); @@ -164,7 +170,7 @@ export function setupOrbitCamera( window.addEventListener('mousemove', mouseMoveEventListener); const touchMoveEventListener = (event: TouchEvent) => { - if (isDragging && event.touches.length === 1) { + if (event.touches.length === 1 && isDragging) { event.preventDefault(); const dx = event.touches[0].clientX - prevX; const dy = event.touches[0].clientY - prevY; @@ -178,6 +184,17 @@ export function setupOrbitCamera( passive: false, }); + canvas.addEventListener('touchmove', (e) => { + if (e.touches.length === 2) { + e.preventDefault(); + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + const pinchDist = Math.sqrt(dx * dx + dy * dy); + zoomCamera((lastPinchDist - pinchDist) * 0.5); + lastPinchDist = pinchDist; + } + }, { passive: false }); + function cleanupCamera() { window.removeEventListener('mouseup', mouseUpEventListener); window.removeEventListener('mousemove', mouseMoveEventListener); diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts index 4958d9ae87..0c1049e960 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/box-geometry.ts @@ -4,40 +4,82 @@ import type { GeometryData } from './types.ts'; import { InstanceData, VertexData } from './types.ts'; export class BoxGeometry { - #modelMatrix: d.m4x4f; - #position: d.v3f; - #scale: d.v3f; - #rotation: d.v3f; - #size: [number, number, number]; - #vertices: GeometryData; - #indices: number[]; - #vertexBuffer: TgpuBuffer> & VertexFlag; - #indexBuffer: TgpuBuffer> & IndexFlag; - - constructor( - root: TgpuRoot, - size: [number, number, number] = [1, 1, 1], - ) { - this.#modelMatrix = d.mat4x4f.identity(); - this.#position = d.vec3f(0, 0, 0); - this.#scale = d.vec3f(1, 1, 1); - this.#rotation = d.vec3f(0, 0, 0); - this.#size = size; - this.#vertices = []; - this.#indices = []; - - this.#generateGeometry(); - - this.#vertexBuffer = root - .createBuffer( - d.arrayOf(VertexData, this.#vertices.length), - this.#vertices, - ) + static #vertexBuffer: + | (TgpuBuffer> & VertexFlag) + | null = null; + static #indexBuffer: (TgpuBuffer> & IndexFlag) | null = + null; + static #indexCount = 0; + + #modelMatrix = d.mat4x4f.identity(); + #position = d.vec3f(0, 0, 0); + #scale = d.vec3f(1, 1, 1); + #rotation = d.vec3f(0, 0, 0); + + constructor(root: TgpuRoot) { + if (!BoxGeometry.#vertexBuffer || !BoxGeometry.#indexBuffer) { + this.#initBuffers(root); + } + } + + #initBuffers(root: TgpuRoot) { + const vertices: GeometryData = []; + const indices: number[] = []; + let vertexOffset = 0; + + const addFace = ( + u: number, + v: number, + w: number, + udir: number, + vdir: number, + depth: number, + ) => { + for (let iy = 0; iy < 2; iy++) { + for (let ix = 0; ix < 2; ix++) { + const pos = [0, 0, 0]; + pos[u] = (ix - 0.5) * udir; + pos[v] = (iy - 0.5) * vdir; + pos[w] = 0.5 * Math.sign(depth); + + const norm = [0, 0, 0]; + norm[w] = Math.sign(depth); + + vertices.push( + { + position: d.vec3f(pos[0], pos[1], pos[2]), + normal: d.vec3f(norm[0], norm[1], norm[2]), + uv: d.vec2f(ix, 1 - iy), + }, + ); + } + } + + indices.push( + vertexOffset, + vertexOffset + 1, + vertexOffset + 2, + vertexOffset + 1, + vertexOffset + 3, + vertexOffset + 2, + ); + vertexOffset += 4; + }; + + addFace(2, 1, 0, -1, 1, 1); // +X + addFace(2, 1, 0, 1, 1, -1); // -X + addFace(0, 2, 1, 1, 1, 1); // +Y + addFace(0, 2, 1, 1, -1, -1); // -Y + addFace(0, 1, 2, 1, 1, 1); // +Z + addFace(0, 1, 2, -1, 1, -1); // -Z + + BoxGeometry.#vertexBuffer = root + .createBuffer(d.arrayOf(VertexData, vertices.length), vertices) .$usage('vertex'); - - this.#indexBuffer = root - .createBuffer(d.arrayOf(d.u16, this.#indices.length), this.#indices) + BoxGeometry.#indexBuffer = root + .createBuffer(d.arrayOf(d.u16, indices.length), indices) .$usage('index'); + BoxGeometry.#indexCount = indices.length; } set position(value: d.v3f) { @@ -45,7 +87,7 @@ export class BoxGeometry { this.#updateModelMatrix(); } - get position(): d.v3f { + get position() { return this.#position; } @@ -54,7 +96,7 @@ export class BoxGeometry { this.#updateModelMatrix(); } - get scale(): d.v3f { + get scale() { return this.#scale; } @@ -63,7 +105,7 @@ export class BoxGeometry { this.#updateModelMatrix(); } - get rotation(): d.v3f { + get rotation() { return this.#rotation; } @@ -76,142 +118,35 @@ export class BoxGeometry { }); } - get vertices(): GeometryData { - return this.#vertices; - } - - get indices(): number[] { - return this.#indices; - } - - get vertexBuffer() { - return this.#vertexBuffer; + static get vertexBuffer() { + if (!BoxGeometry.#vertexBuffer) { + throw new Error('BoxGeometry buffers not initialized'); + } + return BoxGeometry.#vertexBuffer; } - get indexBuffer() { - return this.#indexBuffer; + static get indexBuffer() { + if (!BoxGeometry.#indexBuffer) { + throw new Error('BoxGeometry buffers not initialized'); + } + return BoxGeometry.#indexBuffer; } - get indexCount(): number { - return this.#indices.length; + static get indexCount() { + return BoxGeometry.#indexCount; } - #generateGeometry() { - const [w, h, d] = this.#size; - const halfW = w / 2; - const halfH = h / 2; - const halfD = d / 2; - - this.#vertices = []; - this.#indices = []; - - // Front face (+Z) - this.#addFace( - [ - [-halfW, -halfH, halfD], - [halfW, -halfH, halfD], - [halfW, halfH, halfD], - [-halfW, halfH, halfD], - ], - [0, 0, 1], - [[0, 0], [1, 0], [1, 1], [0, 1]], - ); - - // Back face (-Z) - this.#addFace( - [ - [halfW, -halfH, -halfD], - [-halfW, -halfH, -halfD], - [-halfW, halfH, -halfD], - [halfW, halfH, -halfD], - ], - [0, 0, -1], - [[0, 0], [1, 0], [1, 1], [0, 1]], - ); - - // Right face (+X) - this.#addFace( - [ - [halfW, -halfH, -halfD], - [halfW, -halfH, halfD], - [halfW, halfH, halfD], - [halfW, halfH, -halfD], - ], - [1, 0, 0], - [[0, 0], [1, 0], [1, 1], [0, 1]], - ); - - // Left face (-X) - this.#addFace( - [ - [-halfW, -halfH, halfD], - [-halfW, -halfH, -halfD], - [-halfW, halfH, -halfD], - [-halfW, halfH, halfD], - ], - [-1, 0, 0], - [[0, 0], [1, 0], [1, 1], [0, 1]], - ); - - // Top face (+Y) - this.#addFace( - [ - [-halfW, halfH, halfD], - [halfW, halfH, halfD], - [halfW, halfH, -halfD], - [-halfW, halfH, -halfD], - ], - [0, 1, 0], - [[0, 0], [1, 0], [1, 1], [0, 1]], - ); - - // Bottom face (-Y) - this.#addFace( - [ - [-halfW, -halfH, -halfD], - [halfW, -halfH, -halfD], - [halfW, -halfH, halfD], - [-halfW, -halfH, halfD], - ], - [0, -1, 0], - [[0, 0], [1, 0], [1, 1], [0, 1]], - ); - } - - #addFace( - positions: [number, number, number][], - normal: [number, number, number], - uvs: [number, number][], - ) { - const startIndex = this.#vertices.length; - - positions.forEach((pos, i) => { - this.#vertices.push( - VertexData({ - position: d.vec3f(...pos), - normal: d.vec3f(...normal), - uv: d.vec2f(...uvs[i]), - }), - ); - }); - - this.#indices.push(startIndex, startIndex + 1, startIndex + 2); - this.#indices.push(startIndex, startIndex + 2, startIndex + 3); + static clearBuffers() { + BoxGeometry.#vertexBuffer = null; + BoxGeometry.#indexBuffer = null; } #updateModelMatrix() { - const translationMatrix = d.mat4x4f.translation(this.#position); - const rotationXMatrix = d.mat4x4f.rotationX(this.#rotation.x); - const rotationYMatrix = d.mat4x4f.rotationY(this.#rotation.y); - const rotationZMatrix = d.mat4x4f.rotationZ(this.#rotation.z); - const scaleMatrix = d.mat4x4f.scaling(this.#scale); - - this.#modelMatrix = translationMatrix.mul( - rotationZMatrix.mul( - rotationYMatrix.mul( - rotationXMatrix.mul(scaleMatrix), - ), - ), - ); + this.#modelMatrix = d.mat4x4f + .translation(this.#position) + .mul(d.mat4x4f.rotationZ(this.#rotation.z)) + .mul(d.mat4x4f.rotationY(this.#rotation.y)) + .mul(d.mat4x4f.rotationX(this.#rotation.x)) + .mul(d.mat4x4f.scaling(this.#scale)); } } diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts index cd8a7ce0a1..3796419af4 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts @@ -1,87 +1,66 @@ -import type { TgpuRoot, TgpuUniform } from 'typegpu'; +import type { TgpuRoot } from 'typegpu'; import * as d from 'typegpu/data'; import * as m from 'wgpu-matrix'; import { CameraData } from './types.ts'; export class Camera { - #viewProjectionMatrix: d.m4x4f; - #position: d.v3f; - #target: d.v3f; - #up: d.v3f; + readonly #uniform; + + #position = d.vec3f(0, 0, 0); + #target = d.vec3f(0, 0, -1); + #up = d.vec3f(0, 1, 0); #fov: number; #near: number; #far: number; - #inverseViewProjectionMatrix: d.m4x4f; - #data: d.Infer; - #uniform: TgpuUniform; - - constructor( - root: TgpuRoot, - fov: number = 60, - near: number = 0.1, - far: number = 1000, - ) { - this.#viewProjectionMatrix = d.mat4x4f.identity(); - this.#position = d.vec3f(0, 0, 0); - this.#target = d.vec3f(0, 0, -1); - this.#up = d.vec3f(0, 1, 0); + + constructor(root: TgpuRoot, fov = 60, near = 0.1, far = 1000) { this.#fov = fov; this.#near = near; this.#far = far; - this.#inverseViewProjectionMatrix = d.mat4x4f.identity(); - this.#data = CameraData({ - viewProjectionMatrix: this.#viewProjectionMatrix, - inverseViewProjectionMatrix: this.#inverseViewProjectionMatrix, - }); - - this.#uniform = root.createUniform(CameraData, this.#data); + this.#uniform = root.createUniform(CameraData, this.#computeData()); } set position(pos: d.v3f) { this.#position = pos; - this.#recompute(); + this.#update(); } - get position(): d.v3f { + get position() { return this.#position; } set target(tgt: d.v3f) { this.#target = tgt; - this.#recompute(); + this.#update(); } - get target(): d.v3f { + get target() { return this.#target; } set up(upVec: d.v3f) { this.#up = upVec; - this.#recompute(); + this.#update(); } - get up(): d.v3f { + get up() { return this.#up; } set fov(fovDegrees: number) { this.#fov = fovDegrees; - this.#recompute(); + this.#update(); } - get fov(): number { + get fov() { return this.#fov; } - get data(): d.Infer { - return this.#data; - } - get uniform() { return this.#uniform; } - #recompute() { + #computeData() { const view = m.mat4.lookAt( d.vec3f(-this.#position.x, this.#position.y, this.#position.z), d.vec3f(-this.#target.x, this.#target.y, this.#target.z), @@ -89,6 +68,7 @@ export class Camera { d.mat4x4f(), ); m.mat4.scale(view, d.vec3f(-1, 1, 1), view); + const projection = m.mat4.perspective( (this.#fov * Math.PI) / 180, 1, @@ -96,15 +76,16 @@ export class Camera { this.#far, d.mat4x4f(), ); - this.#viewProjectionMatrix = m.mat4.mul(projection, view, d.mat4x4f()); - this.#inverseViewProjectionMatrix = m.mat4.invert( - this.#viewProjectionMatrix, - ); - this.#data = CameraData({ - viewProjectionMatrix: this.#viewProjectionMatrix, - inverseViewProjectionMatrix: this.#inverseViewProjectionMatrix, + + const viewProjectionMatrix = m.mat4.mul(projection, view, d.mat4x4f()); + + return CameraData({ + viewProjectionMatrix, + inverseViewProjectionMatrix: m.mat4.invert(viewProjectionMatrix), }); + } - this.#uniform.write(this.#data); + #update() { + this.#uniform.write(this.#computeData()); } } 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 48e71a5f73..8c8439030d 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 @@ -5,6 +5,7 @@ import * as std from 'typegpu/std'; import { BoxGeometry } from './box-geometry.ts'; import { Camera } from './camera.ts'; import { PointLight } from './point-light.ts'; +import { Scene } from './scene.ts'; import { CameraData, InstanceData, @@ -17,34 +18,49 @@ const root = await tgpu.init(); const device = root.device; const canvas = document.querySelector('canvas') as HTMLCanvasElement; const context = canvas.getContext('webgpu') as GPUCanvasContext; - const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); -context.configure({ - device, - format: presentationFormat, -}); +context.configure({ device, format: presentationFormat }); const mainCamera = new Camera(root); mainCamera.position = d.vec3f(5, 5, -5); mainCamera.target = d.vec3f(0, 0, 0); -const pointLight = new PointLight(root, d.vec3f(2, 4, 1), { +const pointLight = new PointLight(root, d.vec3f(4.5, 1, 4), { far: 100.0, shadowMapSize: 2048, }); +const scene = new Scene(root); + const cube = new BoxGeometry(root); cube.scale = d.vec3f(3, 1, 0.2); +scene.add(cube); + +const orbitingCubes: BoxGeometry[] = []; +for (let i = 0; i < 10; i++) { + const orbitingCube = new BoxGeometry(root); + const angle = (i / 10) * Math.PI * 2; + const radius = 4; + orbitingCube.position = d.vec3f( + Math.cos(angle) * radius, + 0.5, + Math.sin(angle) * radius, + ); + orbitingCube.scale = d.vec3f(0.5, 0.5, 0.5); + orbitingCubes.push(orbitingCube); + scene.add(orbitingCube); +} const floorCube = new BoxGeometry(root); floorCube.scale = d.vec3f(10, 0.1, 10); floorCube.position = d.vec3f(0, -0.5, 0); +scene.add(floorCube); let depthTexture = root['~unstable'] .createTexture({ size: [canvas.width, canvas.height], - format: 'depth24plus', + format: 'depth32float', sampleCount: 4, }) .$usage('render'); @@ -58,13 +74,11 @@ let msaaTexture = root['~unstable'] .$usage('render'); const shadowSampler = root['~unstable'].createComparisonSampler({ - compare: 'less', + compare: 'less-equal', magFilter: 'linear', minFilter: 'linear', }); -const shadowCubeView = pointLight.createCubeView(); - const renderLayout = tgpu.bindGroupLayout({ camera: { uniform: CameraData }, lightPosition: { uniform: d.vec3f }, @@ -79,17 +93,13 @@ const renderLayoutWithShadow = tgpu.bindGroupLayout({ const vertexDepth = tgpu['~unstable'].vertexFn({ in: { ...VertexData.propTypes, ...InstanceData.propTypes }, - out: { - pos: d.builtin.position, - worldPos: d.vec3f, - }, + out: { pos: d.builtin.position, worldPos: d.vec3f }, })(({ position, column1, column2, column3, column4 }) => { const modelMatrix = d.mat4x4f(column1, column2, column3, column4); const worldPos = modelMatrix.mul(d.vec4f(position, 1)).xyz; const pos = renderLayout.$.camera.viewProjectionMatrix.mul( d.vec4f(worldPos, 1), ); - return { pos, worldPos }; }); @@ -97,10 +107,7 @@ const fragmentDepth = tgpu['~unstable'].fragmentFn({ in: { worldPos: d.vec3f }, out: d.builtin.fragDepth, })(({ worldPos }) => { - const lightPos = renderLayout.$.lightPosition; - const lightToFrag = worldPos.sub(lightPos); - const dist = std.length(lightToFrag); - + const dist = std.length(worldPos.sub(renderLayout.$.lightPosition)); return dist / pointLight.far; }); @@ -118,64 +125,118 @@ const vertexMain = tgpu['~unstable'].vertexFn({ const pos = renderLayoutWithShadow.$.camera.viewProjectionMatrix.mul( d.vec4f(worldPos, 1), ); - - return { pos, worldPos, uv, normal }; + const worldNormal = std.normalize(modelMatrix.mul(d.vec4f(normal, 0)).xyz); + return { pos, worldPos, uv, normal: worldNormal }; }); -const fragmentMain = tgpu['~unstable'].fragmentFn({ - in: { - worldPos: d.vec3f, - uv: d.vec2f, - normal: d.vec3f, +const shadowParams = root.createUniform( + d.struct({ + pcfSamples: d.u32, + diskRadius: d.f32, + normalBiasBase: d.f32, + normalBiasSlope: d.f32, + }), + { + pcfSamples: 32, + diskRadius: 0.01, + normalBiasBase: 0.027, + normalBiasSlope: 0.335, }, +); + +const fragmentMain = tgpu['~unstable'].fragmentFn({ + in: { worldPos: d.vec3f, uv: d.vec2f, normal: d.vec3f }, out: d.vec4f, })(({ worldPos, normal }) => { const lightPos = renderLayoutWithShadow.$.lightPosition; + const toLight = lightPos.sub(worldPos); + const dist = std.length(toLight); + const lightDir = toLight.div(dist); + const ndotl = std.max(std.dot(normal, lightDir), 0.0); + + const normalBiasWorld = shadowParams.$.normalBiasBase + + shadowParams.$.normalBiasSlope * (1.0 - ndotl); + const biasedPos = worldPos.add(normal.mul(normalBiasWorld)); + const toLightBiased = biasedPos.sub(lightPos); + const distBiased = std.length(toLightBiased); + const dir = toLightBiased.div(distBiased); + const depthRef = distBiased / pointLight.far; + + const up = std.select( + d.vec3f(1, 0, 0), + d.vec3f(0, 1, 0), + std.abs(dir.y) < d.f32(0.9999), + ); + const right = std.normalize(std.cross(up, dir)); + const realUp = std.cross(dir, right); + + const PCF_SAMPLES = shadowParams.$.pcfSamples; + const diskRadius = shadowParams.$.diskRadius; + + let visibilityAcc = 0.0; + for (let i = 0; i < PCF_SAMPLES; i++) { + const index = d.f32(i); + const theta = index * 2.3999632; // golden angle + const r = std.sqrt(index / d.f32(PCF_SAMPLES)) * diskRadius; + + const sampleDir = std.normalize( + dir.add(right.mul(std.cos(theta) * r)).add( + realUp.mul(std.sin(theta) * r), + ), + ); - const lightToFrag = worldPos.sub(lightPos); - const dist = std.length(lightToFrag); - const dir = std.normalize(lightToFrag); - - const depthRef = dist / pointLight.far; + visibilityAcc += std.textureSampleCompare( + renderLayoutWithShadow.$.shadowDepthCube, + renderLayoutWithShadow.$.shadowSampler, + sampleDir, + depthRef, + ); + } - const bias = 0.001 * (1.0 - std.dot(normal, std.normalize(lightToFrag))); - const visibility = std.textureSampleCompare( - renderLayoutWithShadow.$.shadowDepthCube, - renderLayoutWithShadow.$.shadowSampler, - dir, - depthRef - bias, + const rawNdotl = std.dot(normal, lightDir); + const visibility = std.select( + visibilityAcc / d.f32(PCF_SAMPLES), + 0.0, + rawNdotl < 0.0, ); - const lightDir = std.normalize(lightPos.sub(worldPos)); - const diffuse = std.max(std.dot(normal, lightDir), 0.0); - const baseColor = d.vec3f(1.0, 0.5, 0.31); - const ambient = 0.1; - const color = baseColor.mul(diffuse * visibility + ambient); - + const color = baseColor.mul(ndotl * visibility + 0.1); return d.vec4f(color, 1.0); }); +const lightIndicatorLayout = tgpu.bindGroupLayout({ + camera: { uniform: CameraData }, + lightPosition: { uniform: d.vec3f }, +}); + +const vertexLightIndicator = tgpu['~unstable'].vertexFn({ + in: { position: d.vec3f }, + out: { pos: d.builtin.position }, +})(({ position }) => { + const worldPos = position.mul(0.15).add(lightIndicatorLayout.$.lightPosition); + const pos = lightIndicatorLayout.$.camera.viewProjectionMatrix.mul( + d.vec4f(worldPos, 1), + ); + return { pos }; +}); + +const fragmentLightIndicator = tgpu['~unstable'].fragmentFn({ + out: d.vec4f, +})(() => d.vec4f(1.0, 1.0, 0.5, 1.0)); + const previewSampler = root['~unstable'].createSampler({ minFilter: 'nearest', magFilter: 'nearest', }); - const previewView = pointLight.createDepthArrayView(); const previewFragment = tgpu['~unstable'].fragmentFn({ in: { uv: d.vec2f }, out: d.vec4f, })(({ uv }) => { - const tiled = d.vec2f( - std.fract(uv.x * 3), - std.fract(uv.y * 2), - ); - - const col = std.floor(uv.x * 3); - const row = std.floor(uv.y * 2); - const arrayIndex = d.i32(row * 3 + col); - + const tiled = d.vec2f(std.fract(uv.x * 3), std.fract(uv.y * 2)); + const arrayIndex = d.i32(std.floor(uv.y * 2) * 3 + std.floor(uv.x * 3)); const depth = std.textureSample( previewView.$, previewSampler.$, @@ -189,7 +250,7 @@ const pipelineDepthOne = root['~unstable'] .withVertex(vertexDepth, { ...vertexLayout.attrib, ...instanceLayout.attrib }) .withFragment(fragmentDepth, {}) .withDepthStencil({ - format: 'depth24plus', + format: 'depth32float', depthWriteEnabled: true, depthCompare: 'less', }) @@ -199,7 +260,7 @@ const pipelineMain = root['~unstable'] .withVertex(vertexMain, { ...vertexLayout.attrib, ...instanceLayout.attrib }) .withFragment(fragmentMain, { format: presentationFormat }) .withDepthStencil({ - format: 'depth24plus', + format: 'depth32float', depthWriteEnabled: true, depthCompare: 'less', }) @@ -211,30 +272,51 @@ const pipelinePreview = root['~unstable'] .withFragment(previewFragment, { format: presentationFormat }) .createPipeline(); +const pipelineLightIndicator = root['~unstable'] + .withVertex(vertexLightIndicator, vertexLayout.attrib) + .withFragment(fragmentLightIndicator, { format: presentationFormat }) + .withDepthStencil({ + format: 'depth32float', + depthWriteEnabled: true, + depthCompare: 'less', + }) + .withMultisample({ count: 4 }) + .createPipeline(); + const mainBindGroup = root.createBindGroup(renderLayoutWithShadow, { camera: mainCamera.uniform.buffer, - shadowDepthCube: shadowCubeView, - shadowSampler: shadowSampler, + shadowDepthCube: pointLight.createCubeView(), + shadowSampler, lightPosition: pointLight.positionUniform.buffer, }); -const instanceBuffer = root.createBuffer(d.arrayOf(InstanceData, 2), [ - cube.instanceData, - floorCube.instanceData, -]).$usage('vertex'); +const lightIndicatorBindGroup = root.createBindGroup(lightIndicatorLayout, { + camera: mainCamera.uniform.buffer, + lightPosition: pointLight.positionUniform.buffer, +}); let showDepthPreview = false; +let lastTime = performance.now(); +let time = 0; + +function render(timestamp: number) { + const dt = (timestamp - lastTime) / 1000; + 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); + } -function render() { - pointLight.renderShadowMaps( - pipelineDepthOne, - renderLayout, - cube.vertexBuffer, - instanceBuffer, - cube.indexBuffer, - cube.indexCount, - 2, - ); + scene.update(); + pointLight.renderShadowMaps(pipelineDepthOne, renderLayout, scene); if (showDepthPreview) { pipelinePreview @@ -248,7 +330,6 @@ function render() { return; } - // Render cubes pipelineMain .withDepthStencilAttachment({ view: depthTexture, @@ -263,10 +344,27 @@ function render() { storeOp: 'store', }) .with(mainBindGroup) - .withIndexBuffer(cube.indexBuffer) - .with(vertexLayout, cube.vertexBuffer) - .with(instanceLayout, instanceBuffer) - .drawIndexed(cube.indexCount, 2); + .withIndexBuffer(BoxGeometry.indexBuffer) + .with(vertexLayout, BoxGeometry.vertexBuffer) + .with(instanceLayout, scene.instanceBuffer) + .drawIndexed(BoxGeometry.indexCount, scene.instanceCount); + + pipelineLightIndicator + .withDepthStencilAttachment({ + view: depthTexture, + depthLoadOp: 'load', + depthStoreOp: 'store', + }) + .withColorAttachment({ + resolveTarget: context.getCurrentTexture().createView(), + view: root.unwrap(msaaTexture).createView(), + loadOp: 'load', + storeOp: 'store', + }) + .with(lightIndicatorBindGroup) + .withIndexBuffer(BoxGeometry.indexBuffer) + .with(vertexLayout, BoxGeometry.vertexBuffer) + .drawIndexed(BoxGeometry.indexCount); requestAnimationFrame(render); } @@ -276,7 +374,6 @@ 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), @@ -289,7 +386,7 @@ const resizeObserver = new ResizeObserver((entries) => { depthTexture = root['~unstable'] .createTexture({ size: [canvas.width, canvas.height], - format: 'depth24plus', + format: 'depth32float', sampleCount: 4, }) .$usage('render'); @@ -302,64 +399,119 @@ const resizeObserver = new ResizeObserver((entries) => { .$usage('render'); } }); - resizeObserver.observe(canvas); -let theta = Math.atan2(10, 10); -let phi = Math.acos(10 / Math.sqrt(10 * 10 + 10 * 10 + 10 * 10)); -let radius = Math.sqrt(10 * 10 + 10 * 10 + 10 * 10); +const initialCamPos = { x: 5, y: 5, z: -5 }; +let theta = Math.atan2(initialCamPos.z, initialCamPos.x); +let phi = Math.acos( + initialCamPos.y / + Math.sqrt( + initialCamPos.x ** 2 + initialCamPos.y ** 2 + initialCamPos.z ** 2, + ), +); +let radius = Math.sqrt( + initialCamPos.x ** 2 + initialCamPos.y ** 2 + initialCamPos.z ** 2, +); let isDragging = false; -let lastMouseX = 0; -let lastMouseY = 0; +let prevX = 0; +let prevY = 0; +let lastPinchDist = 0; function updateCameraPosition() { - const x = radius * Math.sin(phi) * Math.cos(theta); - const y = radius * Math.cos(phi); - const z = radius * Math.sin(phi) * Math.sin(theta); + mainCamera.position = d.vec3f( + radius * Math.sin(phi) * Math.cos(theta), + radius * Math.cos(phi), + radius * Math.sin(phi) * Math.sin(theta), + ); +} - mainCamera.position = d.vec3f(x, y, z); +function updateCameraOrbit(dx: number, dy: number) { + theta -= dx * 0.01; + phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi - dy * 0.01)); + updateCameraPosition(); +} + +function zoomCamera(delta: number) { + radius = Math.max(1, Math.min(50, radius + delta)); + updateCameraPosition(); } +canvas.addEventListener('wheel', (e) => { + e.preventDefault(); + zoomCamera(e.deltaY * 0.01); +}, { passive: false }); + canvas.addEventListener('mousedown', (e) => { isDragging = true; - lastMouseX = e.clientX; - lastMouseY = e.clientY; + prevX = e.clientX; + prevY = e.clientY; }); -canvas.addEventListener('mouseup', () => { - isDragging = false; -}); - -canvas.addEventListener('mousemove', (e) => { - if (!isDragging) return; - - const deltaX = e.clientX - lastMouseX; - const deltaY = e.clientY - lastMouseY; - - lastMouseX = e.clientX; - lastMouseY = e.clientY; +canvas.addEventListener('touchstart', (e) => { + e.preventDefault(); + if (e.touches.length === 1) { + isDragging = true; + prevX = e.touches[0].clientX; + prevY = e.touches[0].clientY; + } else if (e.touches.length === 2) { + isDragging = false; + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + lastPinchDist = Math.sqrt(dx * dx + dy * dy); + } +}, { passive: false }); - theta -= deltaX * 0.01; - phi -= deltaY * 0.01; +const mouseUpEventListener = () => { + isDragging = false; +}; +window.addEventListener('mouseup', mouseUpEventListener); - phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi)); +const touchEndEventListener = () => { + isDragging = false; +}; +window.addEventListener('touchend', touchEndEventListener); - updateCameraPosition(); +const mouseMoveEventListener = (e: MouseEvent) => { + if (!isDragging) return; + const dx = e.clientX - prevX; + const dy = e.clientY - prevY; + prevX = e.clientX; + prevY = e.clientY; + updateCameraOrbit(dx, dy); +}; +window.addEventListener('mousemove', mouseMoveEventListener); + +const touchMoveEventListener = (e: TouchEvent) => { + if (e.touches.length === 1 && isDragging) { + e.preventDefault(); + const dx = e.touches[0].clientX - prevX; + const dy = e.touches[0].clientY - prevY; + prevX = e.touches[0].clientX; + prevY = e.touches[0].clientY; + updateCameraOrbit(dx, dy); + } +}; +window.addEventListener('touchmove', touchMoveEventListener, { + passive: false, }); -canvas.addEventListener('wheel', (e) => { - e.preventDefault(); - - radius += e.deltaY * 0.01; - radius = Math.max(1, Math.min(50, radius)); +canvas.addEventListener('touchmove', (e) => { + if (e.touches.length === 2) { + e.preventDefault(); + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + const pinchDist = Math.sqrt(dx * dx + dy * dy); + zoomCamera((lastPinchDist - pinchDist) * 0.05); + lastPinchDist = pinchDist; + } +}, { passive: false }); - updateCameraPosition(); -}); +// #region Example controls and cleanup export const controls = { 'Light X': { - initial: 2, + initial: 4.5, min: -10, max: 10, step: 0.1, @@ -372,7 +524,7 @@ export const controls = { }, }, 'Light Y': { - initial: 4, + initial: 1, min: 0.5, max: 10, step: 0.1, @@ -385,7 +537,7 @@ export const controls = { }, }, 'Light Z': { - initial: 1, + initial: 4, min: -10, max: 10, step: 0.1, @@ -403,4 +555,52 @@ export const controls = { showDepthPreview = v; }, }, + 'PCF Samples': { + initial: 32, + min: 1, + max: 128, + step: 1, + onSliderChange: (v: number) => { + shadowParams.writePartial({ pcfSamples: v }); + }, + }, + 'PCF Disk Radius': { + initial: 0.01, + min: 0.0, + max: 0.1, + step: 0.001, + onSliderChange: (v: number) => { + shadowParams.writePartial({ diskRadius: v }); + }, + }, + 'Normal Bias Base': { + initial: 0.027, + min: 0.0, + max: 0.1, + step: 0.0001, + onSliderChange: (v: number) => { + shadowParams.writePartial({ normalBiasBase: v }); + }, + }, + 'Normal Bias Slope': { + initial: 0.335, + min: 0.0, + max: 0.5, + step: 0.0005, + onSliderChange: (v: number) => { + shadowParams.writePartial({ normalBiasSlope: v }); + }, + }, }; + +export function onCleanup() { + BoxGeometry.clearBuffers(); + window.removeEventListener('mouseup', mouseUpEventListener); + window.removeEventListener('mousemove', mouseMoveEventListener); + window.removeEventListener('touchend', touchEndEventListener); + window.removeEventListener('touchmove', touchMoveEventListener); + resizeObserver.disconnect(); + root.destroy(); +} + +// #endregion diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts index cd00004c73..ad11b0cb7a 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts @@ -1,113 +1,66 @@ import type { - IndexFlag, - RenderFlag, - SampledFlag, TgpuBindGroup, TgpuBindGroupLayout, - TgpuBuffer, TgpuRenderPipeline, TgpuRoot, - TgpuTexture, - TgpuUniform, - VertexFlag, } from 'typegpu'; import * as d from 'typegpu/data'; +import { BoxGeometry } from './box-geometry.ts'; import { Camera } from './camera.ts'; -import { - type InstanceData, - instanceLayout, - type VertexData, - vertexLayout, -} from './types.ts'; +import type { Scene } from './scene.ts'; +import { instanceLayout, vertexLayout } from './types.ts'; + +const FACE_CONFIGS = [ + { name: 'right', dir: d.vec3f(1, 0, 0), up: d.vec3f(0, 1, 0) }, + { name: 'left', dir: d.vec3f(-1, 0, 0), up: d.vec3f(0, 1, 0) }, + { name: 'up', dir: d.vec3f(0, 1, 0), up: d.vec3f(0, 0, -1) }, + { name: 'down', dir: d.vec3f(0, -1, 0), up: d.vec3f(0, 0, 1) }, + { name: 'forward', dir: d.vec3f(0, 0, 1), up: d.vec3f(0, 1, 0) }, + { name: 'backward', dir: d.vec3f(0, 0, -1), up: d.vec3f(0, 1, 0) }, +] as const; export class PointLight { - #root: TgpuRoot; + readonly far: number; + readonly #root: TgpuRoot; + readonly #positionUniform; + readonly #depthCubeTexture; + readonly #shadowCameras: Camera[]; + readonly #bindGroups: TgpuBindGroup[] = []; + #position: d.v3f; - #far: number; - #shadowMapSize: number; - #depthCubeTexture: - & TgpuTexture<{ - size: [number, number, 6]; - format: 'depth24plus'; - }> - & SampledFlag - & RenderFlag; - #shadowCameras: { - right: Camera; - left: Camera; - up: Camera; - down: Camera; - forward: Camera; - backward: Camera; - }; - #positionUniform: TgpuUniform; - #bindGroups: Map = new Map(); constructor( root: TgpuRoot, position: d.v3f, - options: { - far?: number; - shadowMapSize?: number; - } = {}, + options: { far?: number; shadowMapSize?: number } = {}, ) { this.#root = root; this.#position = position; - this.#far = options.far ?? 100.0; - this.#shadowMapSize = options.shadowMapSize ?? 512; + this.far = options.far ?? 100.0; + const shadowMapSize = options.shadowMapSize ?? 512; this.#depthCubeTexture = root['~unstable'] .createTexture({ - size: [this.#shadowMapSize, this.#shadowMapSize, 6], + size: [shadowMapSize, shadowMapSize, 6], dimension: '2d', - format: 'depth24plus', + format: 'depth32float', }) .$usage('render', 'sampled'); - this.#positionUniform = root.createUniform(d.vec3f, this.#position); - - this.#shadowCameras = { - right: new Camera(root, 90, 0.1, this.#far), - left: new Camera(root, 90, 0.1, this.#far), - up: new Camera(root, 90, 0.1, this.#far), - down: new Camera(root, 90, 0.1, this.#far), - forward: new Camera(root, 90, 0.1, this.#far), - backward: new Camera(root, 90, 0.1, this.#far), - }; - + this.#positionUniform = root.createUniform(d.vec3f, position); + this.#shadowCameras = FACE_CONFIGS.map( + () => new Camera(root, 90, 0.1, this.far), + ); this.#configureCameras(); } #configureCameras() { - // +X (Right) - this.#shadowCameras.right.position = this.#position; - this.#shadowCameras.right.target = this.#position.add(d.vec3f(1, 0, 0)); - this.#shadowCameras.right.up = d.vec3f(0, 1, 0); - - // -X (Left) - this.#shadowCameras.left.position = this.#position; - this.#shadowCameras.left.target = this.#position.add(d.vec3f(-1, 0, 0)); - this.#shadowCameras.left.up = d.vec3f(0, 1, 0); - - // +Y (Top) - this.#shadowCameras.up.position = this.#position; - this.#shadowCameras.up.target = this.#position.add(d.vec3f(0, 1, 0)); - this.#shadowCameras.up.up = d.vec3f(0, 0, -1); - - // -Y (Bottom) - this.#shadowCameras.down.position = this.#position; - this.#shadowCameras.down.target = this.#position.add(d.vec3f(0, -1, 0)); - this.#shadowCameras.down.up = d.vec3f(0, 0, 1); - - // +Z (Front) - this.#shadowCameras.forward.position = this.#position; - this.#shadowCameras.forward.target = this.#position.add(d.vec3f(0, 0, 1)); - this.#shadowCameras.forward.up = d.vec3f(0, 1, 0); - - // -Z (Back) - this.#shadowCameras.backward.position = this.#position; - this.#shadowCameras.backward.target = this.#position.add(d.vec3f(0, 0, -1)); - this.#shadowCameras.backward.up = d.vec3f(0, 1, 0); + FACE_CONFIGS.forEach((config, i) => { + const camera = this.#shadowCameras[i]; + camera.position = this.#position; + camera.target = this.#position.add(config.dir); + camera.up = config.up; + }); } set position(pos: d.v3f) { @@ -124,14 +77,6 @@ export class PointLight { return this.#positionUniform; } - get far(): number { - return this.#far; - } - - get depthCubeTexture() { - return this.#depthCubeTexture; - } - createCubeView() { return this.#depthCubeTexture.createView(d.textureDepthCube()); } @@ -147,36 +92,21 @@ export class PointLight { renderShadowMaps( pipeline: TgpuRenderPipeline, bindGroupLayout: TgpuBindGroupLayout, - vertexBuffer: TgpuBuffer> & VertexFlag, - instanceBuffer: TgpuBuffer> & VertexFlag, - indexBuffer: TgpuBuffer> & IndexFlag, - vertexCount: number, - instanceCount: number, + scene: Scene, ) { - const faceIndices = { - right: 0, - left: 1, - up: 2, - down: 3, - forward: 4, - backward: 5, - }; - - for (const [key, camera] of Object.entries(this.#shadowCameras)) { - const view = this.#depthCubeTexture.createView(d.textureDepth2d(), { - baseArrayLayer: faceIndices[key as keyof typeof faceIndices], - arrayLayerCount: 1, - }); - - let bindGroup = this.#bindGroups.get(key); - if (!bindGroup) { - bindGroup = this.#root.createBindGroup(bindGroupLayout, { + this.#shadowCameras.forEach((camera, i) => { + if (!this.#bindGroups[i]) { + this.#bindGroups[i] = this.#root.createBindGroup(bindGroupLayout, { camera: camera.uniform.buffer, lightPosition: this.#positionUniform.buffer, }); - this.#bindGroups.set(key, bindGroup); } + const view = this.#depthCubeTexture.createView(d.textureDepth2d(), { + baseArrayLayer: i, + arrayLayerCount: 1, + }); + pipeline .withDepthStencilAttachment({ view: this.#root.unwrap(view), @@ -184,14 +114,11 @@ export class PointLight { depthLoadOp: 'clear', depthStoreOp: 'store', }) - .with(vertexLayout, vertexBuffer) - .with(instanceLayout, instanceBuffer) - .with(bindGroup) - .withIndexBuffer(indexBuffer) - .drawIndexed( - vertexCount, - instanceCount, - ); - } + .with(vertexLayout, BoxGeometry.vertexBuffer) + .with(instanceLayout, scene.instanceBuffer) + .with(this.#bindGroups[i]) + .withIndexBuffer(BoxGeometry.indexBuffer) + .drawIndexed(BoxGeometry.indexCount, scene.instanceCount); + }); } } diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/scene.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/scene.ts new file mode 100644 index 0000000000..0e3b271664 --- /dev/null +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/scene.ts @@ -0,0 +1,50 @@ +import type { TgpuRoot } from 'typegpu'; +import * as d from 'typegpu/data'; +import type { BoxGeometry } from './box-geometry.ts'; +import { InstanceData } from './types.ts'; + +export class Scene { + readonly #root: TgpuRoot; + readonly #objects: BoxGeometry[] = []; + + #instanceBuffer; + + constructor(root: TgpuRoot) { + this.#root = root; + this.#instanceBuffer = root + .createBuffer(d.arrayOf(InstanceData, 0), []) + .$usage('vertex'); + } + + add(object: BoxGeometry) { + this.#objects.push(object); + this.#rebuildBuffer(); + } + + remove(object: BoxGeometry) { + const index = this.#objects.indexOf(object); + if (index !== -1) { + this.#objects.splice(index, 1); + this.#rebuildBuffer(); + } + } + + update() { + this.#instanceBuffer.write(this.#objects.map((obj) => obj.instanceData)); + } + + #rebuildBuffer() { + const data = this.#objects.map((obj) => obj.instanceData); + this.#instanceBuffer = this.#root + .createBuffer(d.arrayOf(InstanceData, data.length), data) + .$usage('vertex'); + } + + get instanceBuffer() { + return this.#instanceBuffer; + } + + get instanceCount() { + return this.#objects.length; + } +} 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 6f8d25b940..8495f023c8 100644 --- a/apps/typegpu-docs/src/examples/rendering/two-boxes/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/two-boxes/index.ts @@ -319,6 +319,7 @@ let isRightDragging = false; let isDragging = false; let prevX = 0; let prevY = 0; +let lastPinchDist = 0; let orbitRadius = Math.sqrt( cameraInitialPos.x * cameraInitialPos.x + cameraInitialPos.y * cameraInitialPos.y + @@ -355,16 +356,7 @@ function updateCubesRotation(dx: number, dy: number) { secondTransformBuffer.write({ model: cube2Transform }); } -function updateCameraOrbit(dx: number, dy: number) { - const orbitSensitivity = 0.005; - orbitYaw += -dx * orbitSensitivity; - orbitPitch += dy * orbitSensitivity; - // if we didn't limit pitch, it would lead to flipping the camera which is disorienting. - const maxPitch = Math.PI / 2 - 0.01; - if (orbitPitch > maxPitch) orbitPitch = maxPitch; - if (orbitPitch < -maxPitch) orbitPitch = -maxPitch; - // basically converting spherical coordinates to cartesian. - // like sampling points on a unit sphere and then scaling them by the radius. +function updateCameraPosition() { const newCamX = orbitRadius * Math.sin(orbitYaw) * Math.cos(orbitPitch); const newCamY = orbitRadius * Math.sin(orbitPitch); const newCamZ = orbitRadius * Math.cos(orbitYaw) * Math.cos(orbitPitch); @@ -379,6 +371,20 @@ function updateCameraOrbit(dx: number, dy: number) { cameraBuffer.write({ view: newView, projection: cameraInitial.projection }); } +function updateCameraOrbit(dx: number, dy: number) { + orbitYaw += -dx * 0.005; + orbitPitch = Math.max( + -Math.PI / 2 + 0.01, + Math.min(Math.PI / 2 - 0.01, orbitPitch + dy * 0.005), + ); + updateCameraPosition(); +} + +function zoomCamera(delta: number) { + orbitRadius = Math.max(1, orbitRadius + delta); + updateCameraPosition(); +} + // Prevent the context menu from appearing on right click. canvas.addEventListener('contextmenu', (event) => { event.preventDefault(); @@ -398,46 +404,51 @@ canvas.addEventListener('touchend', () => { helpInfo.style.opacity = '1'; }); -canvas.addEventListener('wheel', (event: WheelEvent) => { - event.preventDefault(); - const zoomSensitivity = 0.05; - orbitRadius = Math.max(1, orbitRadius + event.deltaY * zoomSensitivity); - const newCamX = orbitRadius * Math.sin(orbitYaw) * Math.cos(orbitPitch); - const newCamY = orbitRadius * Math.sin(orbitPitch); - const newCamZ = orbitRadius * Math.cos(orbitYaw) * Math.cos(orbitPitch); - const newCameraPos = d.vec4f(newCamX, newCamY, newCamZ, 1); - const newView = m.mat4.lookAt( - newCameraPos, - target, - d.vec3f(0, 1, 0), - d.mat4x4f(), - ); - cameraBuffer.writePartial({ view: newView }); +canvas.addEventListener('wheel', (e: WheelEvent) => { + e.preventDefault(); + zoomCamera(e.deltaY * 0.05); }, { passive: false }); -canvas.addEventListener('mousedown', (event) => { - if (event.button === 0) { - // Left Mouse Button controls Camera Orbit. +canvas.addEventListener('mousedown', (e) => { + if (e.button === 0) { isDragging = true; - } else if (event.button === 2) { - // Right Mouse Button controls Cube Rotation. + } else if (e.button === 2) { isRightDragging = true; } - prevX = event.clientX; - prevY = event.clientY; + prevX = e.clientX; + prevY = e.clientY; }); +canvas.addEventListener('touchstart', (e) => { + e.preventDefault(); + if (e.touches.length === 1) { + isDragging = true; + prevX = e.touches[0].clientX; + prevY = e.touches[0].clientY; + } else if (e.touches.length === 2) { + isDragging = false; + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + lastPinchDist = Math.sqrt(dx * dx + dy * dy); + } +}, { passive: false }); + const mouseUpEventListener = () => { isRightDragging = false; isDragging = false; }; window.addEventListener('mouseup', mouseUpEventListener); -canvas.addEventListener('mousemove', (event) => { - const dx = event.clientX - prevX; - const dy = event.clientY - prevY; - prevX = event.clientX; - prevY = event.clientY; +const touchEndEventListener = () => { + isDragging = false; +}; +window.addEventListener('touchend', touchEndEventListener); + +const mouseMoveEventListener = (e: MouseEvent) => { + const dx = e.clientX - prevX; + const dy = e.clientY - prevY; + prevX = e.clientX; + prevY = e.clientY; if (isDragging) { updateCameraOrbit(dx, dy); @@ -445,47 +456,33 @@ canvas.addEventListener('mousemove', (event) => { if (isRightDragging) { updateCubesRotation(dx, dy); } -}); - -// Mobile touch support. -canvas.addEventListener('touchstart', (event: TouchEvent) => { - event.preventDefault(); - if (event.touches.length === 1) { - // Single touch controls Camera Orbit. - isDragging = true; - } else if (event.touches.length === 2) { - // Two-finger touch controls Cube Rotation. - isRightDragging = true; - } - // Use the first touch for rotation. - prevX = event.touches[0].clientX; - prevY = event.touches[0].clientY; -}, { passive: false }); - -canvas.addEventListener('touchmove', (event: TouchEvent) => { - event.preventDefault(); - const touch = event.touches[0]; - const dx = touch.clientX - prevX; - const dy = touch.clientY - prevY; - prevX = touch.clientX; - prevY = touch.clientY; - - if (isDragging && event.touches.length === 1) { +}; +window.addEventListener('mousemove', mouseMoveEventListener); + +const touchMoveEventListener = (e: TouchEvent) => { + if (e.touches.length === 1 && isDragging) { + e.preventDefault(); + const dx = e.touches[0].clientX - prevX; + const dy = e.touches[0].clientY - prevY; + prevX = e.touches[0].clientX; + prevY = e.touches[0].clientY; updateCameraOrbit(dx, dy); } - if (isRightDragging && event.touches.length === 2) { - updateCubesRotation(dx, dy); - } -}, { passive: false }); +}; +window.addEventListener('touchmove', touchMoveEventListener, { + passive: false, +}); -const touchEndEventListener = (event: TouchEvent) => { - event.preventDefault(); - if (event.touches.length === 0) { - isRightDragging = false; - isDragging = false; +canvas.addEventListener('touchmove', (e) => { + if (e.touches.length === 2) { + e.preventDefault(); + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + const pinchDist = Math.sqrt(dx * dx + dy * dy); + zoomCamera((lastPinchDist - pinchDist) * 0.05); + lastPinchDist = pinchDist; } -}; -window.addEventListener('touchend', touchEndEventListener); +}, { passive: false }); const resizeObserver = new ResizeObserver(() => { createDepthAndMsaaTextures(); @@ -495,7 +492,9 @@ resizeObserver.observe(canvas); export function onCleanup() { disposed = true; window.removeEventListener('mouseup', mouseUpEventListener); + window.removeEventListener('mousemove', mouseMoveEventListener); window.removeEventListener('touchend', touchEndEventListener); + window.removeEventListener('touchmove', touchMoveEventListener); resizeObserver.disconnect(); root.destroy(); } From 976e823adacd55a3374ed87577801064ece703fc Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Tue, 25 Nov 2025 13:45:12 +0100 Subject: [PATCH 09/17] add thumbnail --- .../rendering/point-light-shadow/thumbnail.png | Bin 0 -> 238277 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/typegpu-docs/src/examples/rendering/point-light-shadow/thumbnail.png diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/thumbnail.png b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..a9a51b654f995bd47add0d8a73a120f35cd09278 GIT binary patch literal 238277 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%0AZpnUtc5i&_L)K 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%0DdQOs%p6`gSJnBgzkDLK%to$$OH#Z3~5@O-qTv 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 literal 0 HcmV?d00001 From 9b3ab17001fa3d4b80caad6c7dca2fc6eac4594e Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Tue, 25 Nov 2025 13:47:57 +0100 Subject: [PATCH 10/17] add test --- .../individual/point-light-shadow.test.ts | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 packages/typegpu/tests/examples/individual/point-light-shadow.test.ts diff --git a/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts b/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts new file mode 100644 index 0000000000..2b041d4ca6 --- /dev/null +++ b/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts @@ -0,0 +1,175 @@ +/** + * @vitest-environment jsdom + */ + +import { describe, expect } from 'vitest'; +import { it } from '../../utils/extendedIt.ts'; +import { runExampleTest, setupCommonMocks } from '../utils/baseTest.ts'; +import { mockResizeObserver } from '../utils/commonMocks.ts'; + +describe('perlin noise example', () => { + setupCommonMocks(); + + it('should produce valid code', async ({ device }) => { + const shaderCodes = await runExampleTest({ + category: 'rendering', + name: 'point-light-shadow', + setupMocks: mockResizeObserver, + expectedCalls: 3, + }, device); + + expect(shaderCodes).toMatchInlineSnapshot(` + "struct CameraData_2 { + viewProjectionMatrix: mat4x4f, + inverseViewProjectionMatrix: mat4x4f, + } + + @group(0) @binding(0) var camera_1: CameraData_2; + + struct vertexDepth_Output_3 { + @builtin(position) pos: vec4f, + @location(0) worldPos: vec3f, + } + + struct vertexDepth_Input_4 { + @location(0) position: vec3f, + @location(1) normal: vec3f, + @location(2) uv: vec2f, + @location(3) column1: vec4f, + @location(4) column2: vec4f, + @location(5) column3: vec4f, + @location(6) column4: vec4f, + } + + @vertex fn vertexDepth_0(_arg_0: vertexDepth_Input_4) -> vertexDepth_Output_3 { + var modelMatrix = mat4x4f(_arg_0.column1, _arg_0.column2, _arg_0.column3, _arg_0.column4); + var worldPos = (modelMatrix * vec4f(_arg_0.position, 1f)).xyz; + var pos = (camera_1.viewProjectionMatrix * vec4f(worldPos, 1f)); + return vertexDepth_Output_3(pos, worldPos); + } + + @group(0) @binding(1) var lightPosition_6: vec3f; + + struct fragmentDepth_Input_7 { + @location(0) worldPos: vec3f, + } + + @fragment fn fragmentDepth_5(_arg_0: fragmentDepth_Input_7) -> @builtin(frag_depth) f32 { + let dist = length((_arg_0.worldPos - lightPosition_6)); + return (dist / 100f); + } + + struct CameraData_2 { + viewProjectionMatrix: mat4x4f, + inverseViewProjectionMatrix: mat4x4f, + } + + @group(1) @binding(0) var camera_1: CameraData_2; + + struct vertexMain_Output_3 { + @builtin(position) pos: vec4f, + @location(0) worldPos: vec3f, + @location(1) uv: vec2f, + @location(2) normal: vec3f, + } + + struct vertexMain_Input_4 { + @location(0) position: vec3f, + @location(1) normal: vec3f, + @location(2) uv: vec2f, + @location(3) column1: vec4f, + @location(4) column2: vec4f, + @location(5) column3: vec4f, + @location(6) column4: vec4f, + } + + @vertex fn vertexMain_0(_arg_0: vertexMain_Input_4) -> vertexMain_Output_3 { + var modelMatrix = mat4x4f(_arg_0.column1, _arg_0.column2, _arg_0.column3, _arg_0.column4); + var worldPos = (modelMatrix * vec4f(_arg_0.position, 1f)).xyz; + var pos = (camera_1.viewProjectionMatrix * vec4f(worldPos, 1f)); + var worldNormal = normalize((modelMatrix * vec4f(_arg_0.normal, 0f)).xyz); + return vertexMain_Output_3(pos, worldPos, _arg_0.uv, worldNormal); + } + + @group(1) @binding(3) var lightPosition_6: vec3f; + + struct item_8 { + pcfSamples: u32, + diskRadius: f32, + normalBiasBase: f32, + normalBiasSlope: f32, + } + + @group(0) @binding(0) var shadowParams_7: item_8; + + @group(1) @binding(1) var shadowDepthCube_9: texture_depth_cube; + + @group(1) @binding(2) var shadowSampler_10: sampler_comparison; + + struct fragmentMain_Input_11 { + @location(0) worldPos: vec3f, + @location(1) uv: vec2f, + @location(2) normal: vec3f, + } + + @fragment fn fragmentMain_5(_arg_0: fragmentMain_Input_11) -> @location(0) vec4f { + let lightPos = (&lightPosition_6); + var toLight = ((*lightPos) - _arg_0.worldPos); + let dist = length(toLight); + var lightDir = (toLight / dist); + let ndotl = max(dot(_arg_0.normal, lightDir), 0f); + let normalBiasWorld = (shadowParams_7.normalBiasBase + (shadowParams_7.normalBiasSlope * (1f - ndotl))); + var biasedPos = (_arg_0.worldPos + (_arg_0.normal * normalBiasWorld)); + var toLightBiased = (biasedPos - (*lightPos)); + let distBiased = length(toLightBiased); + var dir = (toLightBiased / distBiased); + let depthRef = (distBiased / 100f); + var up = select(vec3f(1, 0, 0), vec3f(0, 1, 0), (abs(dir.y) < 0.9998999834060669f)); + var right = normalize(cross(up, dir)); + var realUp = cross(dir, right); + let PCF_SAMPLES = shadowParams_7.pcfSamples; + let diskRadius = shadowParams_7.diskRadius; + var visibilityAcc = 0; + for (var i = 0; (i < i32(PCF_SAMPLES)); i++) { + let index = f32(i); + let theta2 = (index * 2.3999632f); + let r = (sqrt((index / f32(PCF_SAMPLES))) * diskRadius); + var sampleDir = normalize(((dir + (right * (cos(theta2) * r))) + (realUp * (sin(theta2) * r)))); + visibilityAcc += i32(textureSampleCompare(shadowDepthCube_9, shadowSampler_10, sampleDir, depthRef)); + } + let rawNdotl = dot(_arg_0.normal, lightDir); + let visibility = select((f32(visibilityAcc) / f32(PCF_SAMPLES)), 0f, (rawNdotl < 0f)); + var baseColor = vec3f(1, 0.5, 0.3100000023841858); + var color = (baseColor * ((ndotl * visibility) + 0.1f)); + return vec4f(color, 1f); + } + + @group(0) @binding(1) var lightPosition_1: vec3f; + + struct CameraData_3 { + viewProjectionMatrix: mat4x4f, + inverseViewProjectionMatrix: mat4x4f, + } + + @group(0) @binding(0) var camera_2: CameraData_3; + + struct vertexLightIndicator_Output_4 { + @builtin(position) pos: vec4f, + } + + struct vertexLightIndicator_Input_5 { + @location(0) position: vec3f, + } + + @vertex fn vertexLightIndicator_0(_arg_0: vertexLightIndicator_Input_5) -> vertexLightIndicator_Output_4 { + var worldPos = ((_arg_0.position * 0.15) + lightPosition_1); + var pos = (camera_2.viewProjectionMatrix * vec4f(worldPos, 1f)); + return vertexLightIndicator_Output_4(pos); + } + + @fragment fn fragmentLightIndicator_6() -> @location(0) vec4f { + return vec4f(1, 1, 0.5, 1); + }" + `); + }); +}); From 932240abe77ce33924b152354b245420e2117a50 Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Tue, 25 Nov 2025 19:13:35 +0100 Subject: [PATCH 11/17] performance improvements (move some alu work to precomputed memory reads, smaller map and less pcf samples) --- .../rendering/point-light-shadow/index.ts | 39 +++++++++++-------- .../point-light-shadow/point-light.ts | 2 +- 2 files changed, 24 insertions(+), 17 deletions(-) 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 8c8439030d..17bc4d3318 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 @@ -28,7 +28,7 @@ mainCamera.target = d.vec3f(0, 0, 0); const pointLight = new PointLight(root, d.vec3f(4.5, 1, 4), { far: 100.0, - shadowMapSize: 2048, + shadowMapSize: 1024, }); const scene = new Scene(root); @@ -60,7 +60,7 @@ scene.add(floorCube); let depthTexture = root['~unstable'] .createTexture({ size: [canvas.width, canvas.height], - format: 'depth32float', + format: 'depth24plus', sampleCount: 4, }) .$usage('render'); @@ -144,6 +144,17 @@ const shadowParams = root.createUniform( }, ); +const MAX_PCF_SAMPLES = 64; +const samplesUniform = root.createUniform( + d.arrayOf(d.vec4f, MAX_PCF_SAMPLES), + Array.from({ length: MAX_PCF_SAMPLES }, (_, i) => { + const index = i; + const theta = index * 2.3999632; // golden angle + const r = std.sqrt(index / d.f32(MAX_PCF_SAMPLES)); + return d.vec4f(d.vec2f(std.cos(theta) * r, std.sin(theta) * r), 0, 0); + }), +); + const fragmentMain = tgpu['~unstable'].fragmentFn({ in: { worldPos: d.vec3f, uv: d.vec2f, normal: d.vec3f }, out: d.vec4f, @@ -175,15 +186,11 @@ const fragmentMain = tgpu['~unstable'].fragmentFn({ let visibilityAcc = 0.0; for (let i = 0; i < PCF_SAMPLES; i++) { - const index = d.f32(i); - const theta = index * 2.3999632; // golden angle - const r = std.sqrt(index / d.f32(PCF_SAMPLES)) * diskRadius; + const o = samplesUniform.$[i].xy.mul(diskRadius); - const sampleDir = std.normalize( - dir.add(right.mul(std.cos(theta) * r)).add( - realUp.mul(std.sin(theta) * r), - ), - ); + const sampleDir = dir + .add(right.mul(o.x)) + .add(realUp.mul(o.y)); visibilityAcc += std.textureSampleCompare( renderLayoutWithShadow.$.shadowDepthCube, @@ -250,7 +257,7 @@ const pipelineDepthOne = root['~unstable'] .withVertex(vertexDepth, { ...vertexLayout.attrib, ...instanceLayout.attrib }) .withFragment(fragmentDepth, {}) .withDepthStencil({ - format: 'depth32float', + format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less', }) @@ -260,7 +267,7 @@ const pipelineMain = root['~unstable'] .withVertex(vertexMain, { ...vertexLayout.attrib, ...instanceLayout.attrib }) .withFragment(fragmentMain, { format: presentationFormat }) .withDepthStencil({ - format: 'depth32float', + format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less', }) @@ -276,7 +283,7 @@ const pipelineLightIndicator = root['~unstable'] .withVertex(vertexLightIndicator, vertexLayout.attrib) .withFragment(fragmentLightIndicator, { format: presentationFormat }) .withDepthStencil({ - format: 'depth32float', + format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less', }) @@ -386,7 +393,7 @@ const resizeObserver = new ResizeObserver((entries) => { depthTexture = root['~unstable'] .createTexture({ size: [canvas.width, canvas.height], - format: 'depth32float', + format: 'depth24plus', sampleCount: 4, }) .$usage('render'); @@ -556,9 +563,9 @@ export const controls = { }, }, 'PCF Samples': { - initial: 32, + initial: 16, min: 1, - max: 128, + max: 64, step: 1, onSliderChange: (v: number) => { shadowParams.writePartial({ pcfSamples: v }); diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts index ad11b0cb7a..f417c77b99 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts @@ -43,7 +43,7 @@ export class PointLight { .createTexture({ size: [shadowMapSize, shadowMapSize, 6], dimension: '2d', - format: 'depth32float', + format: 'depth24plus', }) .$usage('render', 'sampled'); From b21658328cd22e8daacb1b0df8bd114a615adbad Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Thu, 27 Nov 2025 15:04:15 +0100 Subject: [PATCH 12/17] update test --- .../individual/point-light-shadow.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts b/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts index 2b041d4ca6..7cb61083ca 100644 --- a/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts +++ b/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts @@ -102,17 +102,19 @@ describe('perlin noise example', () => { @group(0) @binding(0) var shadowParams_7: item_8; - @group(1) @binding(1) var shadowDepthCube_9: texture_depth_cube; + @group(0) @binding(1) var samplesUniform_9: array; - @group(1) @binding(2) var shadowSampler_10: sampler_comparison; + @group(1) @binding(1) var shadowDepthCube_10: texture_depth_cube; - struct fragmentMain_Input_11 { + @group(1) @binding(2) var shadowSampler_11: sampler_comparison; + + struct fragmentMain_Input_12 { @location(0) worldPos: vec3f, @location(1) uv: vec2f, @location(2) normal: vec3f, } - @fragment fn fragmentMain_5(_arg_0: fragmentMain_Input_11) -> @location(0) vec4f { + @fragment fn fragmentMain_5(_arg_0: fragmentMain_Input_12) -> @location(0) vec4f { let lightPos = (&lightPosition_6); var toLight = ((*lightPos) - _arg_0.worldPos); let dist = length(toLight); @@ -131,11 +133,9 @@ describe('perlin noise example', () => { let diskRadius = shadowParams_7.diskRadius; var visibilityAcc = 0; for (var i = 0; (i < i32(PCF_SAMPLES)); i++) { - let index = f32(i); - let theta2 = (index * 2.3999632f); - let r = (sqrt((index / f32(PCF_SAMPLES))) * diskRadius); - var sampleDir = normalize(((dir + (right * (cos(theta2) * r))) + (realUp * (sin(theta2) * r)))); - visibilityAcc += i32(textureSampleCompare(shadowDepthCube_9, shadowSampler_10, sampleDir, depthRef)); + var o = (samplesUniform_9[i].xy * diskRadius); + var sampleDir = ((dir + (right * o.x)) + (realUp * o.y)); + visibilityAcc += i32(textureSampleCompare(shadowDepthCube_10, shadowSampler_11, sampleDir, depthRef)); } let rawNdotl = dot(_arg_0.normal, lightDir); let visibility = select((f32(visibilityAcc) / f32(PCF_SAMPLES)), 0f, (rawNdotl < 0f)); From 62b9a27f5f0e4e8b2e77bddee2302d8c39b4b628 Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Thu, 27 Nov 2025 17:00:25 +0100 Subject: [PATCH 13/17] review fixes --- .../rendering/cubemap-reflection/index.ts | 10 ++++++-- .../phong-reflection/setup-orbit-camera.ts | 10 ++++++-- .../rendering/point-light-shadow/camera.ts | 7 +++--- .../rendering/point-light-shadow/index.ts | 8 +++---- .../point-light-shadow/point-light.ts | 4 ++-- .../rendering/point-light-shadow/scene.ts | 23 +++++++++++++------ .../examples/rendering/two-boxes/index.html | 6 ++--- 7 files changed, 42 insertions(+), 26 deletions(-) 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 0a9e658198..4ae8f334e3 100644 --- a/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.ts +++ b/apps/typegpu-docs/src/examples/rendering/cubemap-reflection/index.ts @@ -419,8 +419,14 @@ const mouseUpEventListener = () => { }; window.addEventListener('mouseup', mouseUpEventListener); -const touchEndEventListener = () => { - isDragging = false; +const touchEndEventListener = (e: TouchEvent) => { + if (e.touches.length === 1) { + isDragging = true; + prevX = e.touches[0].clientX; + prevY = e.touches[0].clientY; + } else { + isDragging = false; + } }; window.addEventListener('touchend', touchEndEventListener); diff --git a/apps/typegpu-docs/src/examples/rendering/phong-reflection/setup-orbit-camera.ts b/apps/typegpu-docs/src/examples/rendering/phong-reflection/setup-orbit-camera.ts index 707bb5f120..b1dbe74493 100644 --- a/apps/typegpu-docs/src/examples/rendering/phong-reflection/setup-orbit-camera.ts +++ b/apps/typegpu-docs/src/examples/rendering/phong-reflection/setup-orbit-camera.ts @@ -152,8 +152,14 @@ export function setupOrbitCamera( }; window.addEventListener('mouseup', mouseUpEventListener); - const touchEndEventListener = () => { - isDragging = false; + const touchEndEventListener = (e: TouchEvent) => { + if (e.touches.length === 1) { + isDragging = true; + prevX = e.touches[0].clientX; + prevY = e.touches[0].clientY; + } else { + isDragging = false; + } }; window.addEventListener('touchend', touchEndEventListener); diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts index 3796419af4..9f561207f8 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/camera.ts @@ -62,12 +62,11 @@ export class Camera { #computeData() { const view = m.mat4.lookAt( - d.vec3f(-this.#position.x, this.#position.y, this.#position.z), - d.vec3f(-this.#target.x, this.#target.y, this.#target.z), - d.vec3f(-this.#up.x, this.#up.y, this.#up.z), + this.#position, + this.#target, + this.#up, d.mat4x4f(), ); - m.mat4.scale(view, d.vec3f(-1, 1, 1), view); const projection = m.mat4.perspective( (this.#fov * Math.PI) / 180, 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 17bc4d3318..06f4c42a24 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 @@ -35,7 +35,6 @@ const scene = new Scene(root); const cube = new BoxGeometry(root); cube.scale = d.vec3f(3, 1, 0.2); -scene.add(cube); const orbitingCubes: BoxGeometry[] = []; for (let i = 0; i < 10; i++) { @@ -49,13 +48,12 @@ for (let i = 0; i < 10; i++) { ); orbitingCube.scale = d.vec3f(0.5, 0.5, 0.5); orbitingCubes.push(orbitingCube); - scene.add(orbitingCube); } const floorCube = new BoxGeometry(root); floorCube.scale = d.vec3f(10, 0.1, 10); floorCube.position = d.vec3f(0, -0.5, 0); -scene.add(floorCube); +scene.add([cube, floorCube, ...orbitingCubes]); let depthTexture = root['~unstable'] .createTexture({ @@ -170,7 +168,7 @@ const fragmentMain = tgpu['~unstable'].fragmentFn({ const biasedPos = worldPos.add(normal.mul(normalBiasWorld)); const toLightBiased = biasedPos.sub(lightPos); const distBiased = std.length(toLightBiased); - const dir = toLightBiased.div(distBiased); + const dir = toLightBiased.div(distBiased).mul(d.vec3f(-1, 1, 1)); const depthRef = distBiased / pointLight.far; const up = std.select( @@ -434,7 +432,7 @@ function updateCameraPosition() { } function updateCameraOrbit(dx: number, dy: number) { - theta -= dx * 0.01; + theta += dx * 0.01; phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi - dy * 0.01)); updateCameraPosition(); } diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts index f417c77b99..f9b05a450b 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/point-light.ts @@ -11,8 +11,8 @@ import type { Scene } from './scene.ts'; import { instanceLayout, vertexLayout } from './types.ts'; const FACE_CONFIGS = [ - { name: 'right', dir: d.vec3f(1, 0, 0), up: d.vec3f(0, 1, 0) }, - { name: 'left', dir: d.vec3f(-1, 0, 0), up: d.vec3f(0, 1, 0) }, + { name: 'right', dir: d.vec3f(-1, 0, 0), up: d.vec3f(0, 1, 0) }, + { name: 'left', dir: d.vec3f(1, 0, 0), up: d.vec3f(0, 1, 0) }, { name: 'up', dir: d.vec3f(0, 1, 0), up: d.vec3f(0, 0, -1) }, { name: 'down', dir: d.vec3f(0, -1, 0), up: d.vec3f(0, 0, 1) }, { name: 'forward', dir: d.vec3f(0, 0, 1), up: d.vec3f(0, 1, 0) }, diff --git a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/scene.ts b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/scene.ts index 0e3b271664..f8e88ea7c2 100644 --- a/apps/typegpu-docs/src/examples/rendering/point-light-shadow/scene.ts +++ b/apps/typegpu-docs/src/examples/rendering/point-light-shadow/scene.ts @@ -16,17 +16,26 @@ export class Scene { .$usage('vertex'); } - add(object: BoxGeometry) { - this.#objects.push(object); + add(object: BoxGeometry | BoxGeometry[]) { + const items = Array.isArray(object) ? object : [object]; + if (items.length === 0) { + return; + } + this.#objects.push(...items); this.#rebuildBuffer(); } - remove(object: BoxGeometry) { - const index = this.#objects.indexOf(object); - if (index !== -1) { - this.#objects.splice(index, 1); - this.#rebuildBuffer(); + remove(object: BoxGeometry | BoxGeometry[]) { + const items = Array.isArray(object) ? object : [object]; + if (items.length === 0) { + return; } + this.#objects.splice( + 0, + this.#objects.length, + ...this.#objects.filter((obj) => !items.includes(obj)), + ); + this.#rebuildBuffer(); } update() { 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 50a11ece63..9209e7c2fd 100644 --- a/apps/typegpu-docs/src/examples/rendering/two-boxes/index.html +++ b/apps/typegpu-docs/src/examples/rendering/two-boxes/index.html @@ -24,9 +24,7 @@

Controls

Left Mouse Button / Drag: Orbit camera

-

- Right Mouse Button / Two-Finger Drag: Rotate cubes -

-

Scroll: Zoom

+

Right Mouse Button: Rotate cubes

+

Scroll / Two-Finger Drag: Zoom

From 7f568a6891fd1376d0d850d1e591c48ded258bb9 Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Thu, 27 Nov 2025 17:01:01 +0100 Subject: [PATCH 14/17] update test --- .../tests/examples/individual/point-light-shadow.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts b/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts index 7cb61083ca..88af3dc98e 100644 --- a/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts +++ b/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts @@ -124,7 +124,7 @@ describe('perlin noise example', () => { var biasedPos = (_arg_0.worldPos + (_arg_0.normal * normalBiasWorld)); var toLightBiased = (biasedPos - (*lightPos)); let distBiased = length(toLightBiased); - var dir = (toLightBiased / distBiased); + var dir = ((toLightBiased / distBiased) * vec3f(-1, 1, 1)); let depthRef = (distBiased / 100f); var up = select(vec3f(1, 0, 0), vec3f(0, 1, 0), (abs(dir.y) < 0.9998999834060669f)); var right = normalize(cross(up, dir)); From b907b06088c393d5782bf7e98e83618af4c274a0 Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Thu, 27 Nov 2025 17:42:59 +0100 Subject: [PATCH 15/17] nicities and fixes --- .../rendering/point-light-shadow/index.ts | 98 +++++++++++++++++-- 1 file changed, 91 insertions(+), 7 deletions(-) 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 06f4c42a24..f9dd82286d 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 @@ -236,19 +236,83 @@ const previewSampler = root['~unstable'].createSampler({ }); const previewView = pointLight.createDepthArrayView(); +const depthToColor = tgpu.fn([d.f32], d.vec3f)((depth) => { + const linear = std.clamp(1 - depth * 6, 0, 1); + const t = linear * linear; + const r = std.clamp(t * 2 - 0.5, 0, 1); + const g = std.clamp(1 - std.abs(t - 0.5) * 2, 0, 0.9) * t; + const b = std.clamp(1 - t * 1.5, 0, 1) * t; + return d.vec3f(r, g, b); +}); + +const fragmentDistanceView = tgpu['~unstable'].fragmentFn({ + in: { worldPos: d.vec3f, uv: d.vec2f, normal: d.vec3f }, + out: d.vec4f, +})(({ worldPos }) => { + const lightPos = renderLayoutWithShadow.$.lightPosition; + const dist = std.length(worldPos.sub(lightPos)); + const color = depthToColor(dist / pointLight.far); + return d.vec4f(color, 1.0); +}); + const previewFragment = tgpu['~unstable'].fragmentFn({ in: { uv: d.vec2f }, out: d.vec4f, })(({ uv }) => { - const tiled = d.vec2f(std.fract(uv.x * 3), std.fract(uv.y * 2)); - const arrayIndex = d.i32(std.floor(uv.y * 2) * 3 + std.floor(uv.x * 3)); + const gridX = d.i32(std.floor(uv.x * 4)); + const gridY = d.i32(std.floor(uv.y * 3)); + + const localU = std.fract(uv.x * 4); + const localV = std.fract(uv.y * 3); + const localUV = d.vec2f(localU, localV); + + const bgColor = d.vec3f(0.1, 0.1, 0.12); + + let faceIndex = d.i32(-1); + + // Top row: +Y (index 2) + if (gridY === 0 && gridX === 1) { + faceIndex = 2; + } + // Middle row: -X, +Z, +X, -Z + if (gridY === 1) { + if (gridX === 0) { + faceIndex = 0; // -X + } + if (gridX === 1) { + faceIndex = 4; // +Z + } + if (gridX === 2) { + faceIndex = 1; // +X + } + if (gridX === 3) { + faceIndex = 5; // -Z + } + } + // Bottom row: -Y (index 3) + if (gridY === 2 && gridX === 1) { + faceIndex = 3; + } + const depth = std.textureSample( previewView.$, previewSampler.$, - tiled, - arrayIndex, + localUV, + faceIndex, ); - return d.vec4f(d.vec3f(depth ** 0.5), 1.0); + + if (faceIndex < 0) { + return d.vec4f(bgColor, 1.0); + } + + const color = depthToColor(depth); + + const border = 0.02; + const isBorder = localU < border || localU > 1 - border || localV < border || + localV > 1 - border; + const finalColor = std.select(color, std.mul(0.5, color), isBorder); + + return d.vec4f(finalColor, 1.0); }); const pipelineDepthOne = root['~unstable'] @@ -288,6 +352,17 @@ const pipelineLightIndicator = root['~unstable'] .withMultisample({ count: 4 }) .createPipeline(); +const pipelineDistanceView = root['~unstable'] + .withVertex(vertexMain, { ...vertexLayout.attrib, ...instanceLayout.attrib }) + .withFragment(fragmentDistanceView, { format: presentationFormat }) + .withDepthStencil({ + format: 'depth24plus', + depthWriteEnabled: true, + depthCompare: 'less', + }) + .withMultisample({ count: 4 }) + .createPipeline(); + const mainBindGroup = root.createBindGroup(renderLayoutWithShadow, { camera: mainCamera.uniform.buffer, shadowDepthCube: pointLight.createCubeView(), @@ -301,6 +376,7 @@ const lightIndicatorBindGroup = root.createBindGroup(lightIndicatorLayout, { }); let showDepthPreview = false; +let showDistanceView = false; let lastTime = performance.now(); let time = 0; @@ -335,7 +411,9 @@ function render(timestamp: number) { return; } - pipelineMain + const mainPipeline = showDistanceView ? pipelineDistanceView : pipelineMain; + + mainPipeline .withDepthStencilAttachment({ view: depthTexture, depthClearValue: 1, @@ -554,12 +632,18 @@ export const controls = { ); }, }, - 'Show Depth Preview': { + 'Show Depth Cubemap': { initial: false, onToggleChange: (v: boolean) => { showDepthPreview = v; }, }, + 'Show Distance View': { + initial: false, + onToggleChange: (v: boolean) => { + showDistanceView = v; + }, + }, 'PCF Samples': { initial: 16, min: 1, From 9db4a2a2ee47006e084d8c68ad95838f9ce09505 Mon Sep 17 00:00:00 2001 From: Konrad Reczko <66403540+reczkok@users.noreply.github.com> Date: Thu, 27 Nov 2025 17:44:56 +0100 Subject: [PATCH 16/17] Update packages/typegpu/tests/examples/individual/point-light-shadow.test.ts Co-authored-by: Szymon Szulc <103948576+cieplypolar@users.noreply.github.com> --- .../tests/examples/individual/point-light-shadow.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts b/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts index 88af3dc98e..4f107f2ade 100644 --- a/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts +++ b/packages/typegpu/tests/examples/individual/point-light-shadow.test.ts @@ -7,7 +7,7 @@ import { it } from '../../utils/extendedIt.ts'; import { runExampleTest, setupCommonMocks } from '../utils/baseTest.ts'; import { mockResizeObserver } from '../utils/commonMocks.ts'; -describe('perlin noise example', () => { +describe('point light shadow example', () => { setupCommonMocks(); it('should produce valid code', async ({ device }) => { From a938753f1a1127ba028b8316a8ae9037341c65eb Mon Sep 17 00:00:00 2001 From: Konrad Reczko Date: Thu, 27 Nov 2025 17:45:29 +0100 Subject: [PATCH 17/17] last fix --- .../src/examples/rendering/point-light-shadow/index.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 f9dd82286d..050f7f6d9e 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 @@ -550,8 +550,14 @@ const mouseUpEventListener = () => { }; window.addEventListener('mouseup', mouseUpEventListener); -const touchEndEventListener = () => { - isDragging = false; +const touchEndEventListener = (e: TouchEvent) => { + if (e.touches.length === 1) { + isDragging = true; + prevX = e.touches[0].clientX; + prevY = e.touches[0].clientY; + } else { + isDragging = false; + } }; window.addEventListener('touchend', touchEndEventListener);