|
| 1 | +import tgpu from 'typegpu'; |
| 2 | +import * as d from 'typegpu/data'; |
| 3 | +import * as std from 'typegpu/std'; |
| 4 | +import { sdBoxFrame3d, sdPlane, sdSphere } from '@typegpu/sdf'; |
| 5 | + |
| 6 | +const canvas = document.querySelector('canvas') as HTMLCanvasElement; |
| 7 | +const context = canvas.getContext('webgpu') as GPUCanvasContext; |
| 8 | +const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); |
| 9 | + |
| 10 | +const root = await tgpu.init(); |
| 11 | + |
| 12 | +context.configure({ |
| 13 | + device: root.device, |
| 14 | + format: presentationFormat, |
| 15 | + alphaMode: 'premultiplied', |
| 16 | +}); |
| 17 | + |
| 18 | +const time = root.createUniform(d.f32); |
| 19 | +const resolution = root.createUniform(d.vec2f); |
| 20 | + |
| 21 | +const MAX_STEPS = 1000; |
| 22 | +const MAX_DIST = 30; |
| 23 | +const SURF_DIST = 0.001; |
| 24 | + |
| 25 | +const skyColor = d.vec4f(0.7, 0.8, 0.9, 1); |
| 26 | + |
| 27 | +// Structure to hold both distance and color |
| 28 | +const Shape = d.struct({ |
| 29 | + color: d.vec3f, |
| 30 | + dist: d.f32, |
| 31 | +}); |
| 32 | + |
| 33 | +const checkerBoard = tgpu.fn([d.vec2f], d.f32)((uv) => { |
| 34 | + const fuv = std.floor(uv); |
| 35 | + return std.abs(fuv.x + fuv.y) % 2; |
| 36 | +}); |
| 37 | + |
| 38 | +const smoothShapeUnion = tgpu.fn([Shape, Shape, d.f32], Shape)((a, b, k) => { |
| 39 | + const h = std.max(k - std.abs(a.dist - b.dist), 0) / k; |
| 40 | + const m = h * h; |
| 41 | + |
| 42 | + // Smooth min for distance |
| 43 | + const dist = std.min(a.dist, b.dist) - m * k * (1 / d.f32(4)); |
| 44 | + |
| 45 | + // Blend colors based on relative distances and smoothing |
| 46 | + const weight = m + std.select(0, 1 - m, a.dist > b.dist); |
| 47 | + const color = std.mix(a.color, b.color, weight); |
| 48 | + |
| 49 | + return { dist, color }; |
| 50 | +}); |
| 51 | + |
| 52 | +const shapeUnion = tgpu.fn([Shape, Shape], Shape)((a, b) => ({ |
| 53 | + color: std.select(a.color, b.color, a.dist > b.dist), |
| 54 | + dist: std.min(a.dist, b.dist), |
| 55 | +})); |
| 56 | + |
| 57 | +const getMorphingShape = tgpu.fn([d.vec3f, d.f32], Shape)((p, t) => { |
| 58 | + // Center position |
| 59 | + const center = d.vec3f(0, 2, 6); |
| 60 | + const localP = std.sub(p, center); |
| 61 | + const rotMatZ = d.mat4x4f.rotationZ(-t); |
| 62 | + const rotMatX = d.mat4x4f.rotationX(-t * 0.6); |
| 63 | + const rotatedP = std.mul(rotMatZ, std.mul(rotMatX, d.vec4f(localP, 1))).xyz; |
| 64 | + |
| 65 | + // Animate shapes |
| 66 | + const boxSize = d.vec3f(0.7); |
| 67 | + |
| 68 | + // Create two spheres that move in a circular pattern |
| 69 | + const sphere1Offset = d.vec3f( |
| 70 | + std.cos(t * 2) * 0.8, |
| 71 | + std.sin(t * 3) * 0.3, |
| 72 | + std.sin(t * 2) * 0.8, |
| 73 | + ); |
| 74 | + const sphere2Offset = d.vec3f( |
| 75 | + std.cos(t * 2 + 3.14) * 0.8, |
| 76 | + std.sin(t * 3 + 1.57) * 0.3, |
| 77 | + std.sin(t * 2 + 3.14) * 0.8, |
| 78 | + ); |
| 79 | + |
| 80 | + // Calculate distances and assign colors |
| 81 | + const sphere1 = Shape({ |
| 82 | + dist: sdSphere(std.sub(localP, sphere1Offset), 0.5), |
| 83 | + color: d.vec3f(0.4, 0.5, 1), |
| 84 | + }); |
| 85 | + const sphere2 = Shape({ |
| 86 | + dist: sdSphere(std.sub(localP, sphere2Offset), 0.3), |
| 87 | + color: d.vec3f(1, 0.8, 0.2), |
| 88 | + }); |
| 89 | + const box = Shape({ |
| 90 | + dist: sdBoxFrame3d(rotatedP, boxSize, 0.1), |
| 91 | + color: d.vec3f(1.0, 0.3, 0.3), |
| 92 | + }); |
| 93 | + |
| 94 | + // Smoothly blend shapes and colors |
| 95 | + const spheres = smoothShapeUnion(sphere1, sphere2, 0.1); |
| 96 | + return smoothShapeUnion(spheres, box, 0.2); |
| 97 | +}); |
| 98 | + |
| 99 | +const getSceneDist = tgpu.fn([d.vec3f], Shape)((p) => { |
| 100 | + const shape = getMorphingShape(p, time.$); |
| 101 | + const floor = Shape({ |
| 102 | + dist: sdPlane(p, d.vec3f(0, 1, 0), 0), |
| 103 | + color: std.mix( |
| 104 | + d.vec3f(1), |
| 105 | + d.vec3f(0.2), |
| 106 | + checkerBoard(std.mul(p.xz, 2)), |
| 107 | + ), |
| 108 | + }); |
| 109 | + |
| 110 | + return shapeUnion(shape, floor); |
| 111 | +}); |
| 112 | + |
| 113 | +const rayMarch = tgpu.fn([d.vec3f, d.vec3f], Shape)((ro, rd) => { |
| 114 | + let dO = d.f32(0); |
| 115 | + const result = Shape({ |
| 116 | + dist: d.f32(MAX_DIST), |
| 117 | + color: d.vec3f(0, 0, 0), |
| 118 | + }); |
| 119 | + |
| 120 | + for (let i = 0; i < MAX_STEPS; i++) { |
| 121 | + const p = std.add(ro, std.mul(rd, dO)); |
| 122 | + const scene = getSceneDist(p); |
| 123 | + dO += scene.dist; |
| 124 | + |
| 125 | + if (dO > MAX_DIST || scene.dist < SURF_DIST) { |
| 126 | + result.dist = dO; |
| 127 | + result.color = scene.color; |
| 128 | + break; |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + return result; |
| 133 | +}); |
| 134 | + |
| 135 | +const softShadow = tgpu.fn( |
| 136 | + [d.vec3f, d.vec3f, d.f32, d.f32, d.f32], |
| 137 | + d.f32, |
| 138 | +)((ro, rd, minT, maxT, k) => { |
| 139 | + let res = d.f32(1); |
| 140 | + let t = minT; |
| 141 | + |
| 142 | + for (let i = 0; i < 100; i++) { |
| 143 | + if (t >= maxT) break; |
| 144 | + const h = getSceneDist(std.add(ro, std.mul(rd, t))).dist; |
| 145 | + if (h < 0.001) return 0; |
| 146 | + res = std.min(res, k * h / t); |
| 147 | + t += std.max(h, 0.001); |
| 148 | + } |
| 149 | + |
| 150 | + return res; |
| 151 | +}); |
| 152 | + |
| 153 | +const getNormal = tgpu.fn([d.vec3f], d.vec3f)((p) => { |
| 154 | + const dist = getSceneDist(p).dist; |
| 155 | + const e = 0.01; |
| 156 | + |
| 157 | + const n = d.vec3f( |
| 158 | + getSceneDist(std.add(p, d.vec3f(e, 0, 0))).dist - dist, |
| 159 | + getSceneDist(std.add(p, d.vec3f(0, e, 0))).dist - dist, |
| 160 | + getSceneDist(std.add(p, d.vec3f(0, 0, e))).dist - dist, |
| 161 | + ); |
| 162 | + |
| 163 | + return std.normalize(n); |
| 164 | +}); |
| 165 | + |
| 166 | +const getOrbitingLightPos = tgpu.fn([d.f32], d.vec3f)((t) => { |
| 167 | + const radius = d.f32(3); |
| 168 | + const height = d.f32(6); |
| 169 | + const speed = d.f32(1); |
| 170 | + |
| 171 | + return d.vec3f( |
| 172 | + std.cos(t * speed) * radius, |
| 173 | + height + std.sin(t * speed) * radius, |
| 174 | + 4, |
| 175 | + ); |
| 176 | +}); |
| 177 | + |
| 178 | +const vertexMain = tgpu['~unstable'].vertexFn({ |
| 179 | + in: { idx: d.builtin.vertexIndex }, |
| 180 | + out: { pos: d.builtin.position, uv: d.vec2f }, |
| 181 | +})(({ idx }) => { |
| 182 | + const pos = [d.vec2f(-1, -1), d.vec2f(3, -1), d.vec2f(-1, 3)]; |
| 183 | + const uv = [d.vec2f(0, 0), d.vec2f(2, 0), d.vec2f(0, 2)]; |
| 184 | + |
| 185 | + return { |
| 186 | + pos: d.vec4f(pos[idx], 0.0, 1.0), |
| 187 | + uv: uv[idx], |
| 188 | + }; |
| 189 | +}); |
| 190 | + |
| 191 | +const fragmentMain = tgpu['~unstable'].fragmentFn({ |
| 192 | + in: { uv: d.vec2f }, |
| 193 | + out: d.vec4f, |
| 194 | +})((input) => { |
| 195 | + const uv = std.sub(std.mul(input.uv, 2), 1); |
| 196 | + uv.x *= resolution.$.x / resolution.$.y; |
| 197 | + |
| 198 | + // Ray origin and direction |
| 199 | + const ro = d.vec3f(0, 2, 3); |
| 200 | + const rd = std.normalize(d.vec3f(uv.x, uv.y, 1)); |
| 201 | + |
| 202 | + const march = rayMarch(ro, rd); |
| 203 | + |
| 204 | + const fog = std.pow(std.min(march.dist / MAX_DIST, 1), 0.7); |
| 205 | + |
| 206 | + const p = std.add(ro, std.mul(rd, march.dist)); |
| 207 | + const n = getNormal(p); |
| 208 | + |
| 209 | + // Lighting with orbiting light |
| 210 | + const lightPos = getOrbitingLightPos(time.$); |
| 211 | + const l = std.normalize(std.sub(lightPos, p)); |
| 212 | + const diff = std.max(std.dot(n, l), 0); |
| 213 | + |
| 214 | + // Soft shadows |
| 215 | + const shadowRo = p; |
| 216 | + const shadowRd = l; |
| 217 | + const shadowDist = std.length(std.sub(lightPos, p)); |
| 218 | + const shadow = softShadow(shadowRo, shadowRd, 0.1, shadowDist, 16); |
| 219 | + |
| 220 | + // Combine lighting with shadows and color |
| 221 | + const litColor = std.mul(march.color, diff); |
| 222 | + const finalColor = std.mix( |
| 223 | + std.mul(litColor, 0.5), // Shadow color |
| 224 | + litColor, // Lit color |
| 225 | + shadow, |
| 226 | + ); |
| 227 | + |
| 228 | + return std.mix(d.vec4f(finalColor, 1), skyColor, fog); |
| 229 | +}); |
| 230 | + |
| 231 | +const renderPipeline = root['~unstable'] |
| 232 | + .withVertex(vertexMain, {}) |
| 233 | + .withFragment(fragmentMain, { format: presentationFormat }) |
| 234 | + .createPipeline(); |
| 235 | + |
| 236 | +let animationFrame: number; |
| 237 | +function run(timestamp: number) { |
| 238 | + time.write(timestamp / 1000 % 1000); |
| 239 | + resolution.write(d.vec2f(canvas.width, canvas.height)); |
| 240 | + |
| 241 | + renderPipeline |
| 242 | + .withColorAttachment({ |
| 243 | + view: context.getCurrentTexture().createView(), |
| 244 | + loadOp: 'clear', |
| 245 | + storeOp: 'store', |
| 246 | + }) |
| 247 | + .draw(3); |
| 248 | + |
| 249 | + animationFrame = requestAnimationFrame(run); |
| 250 | +} |
| 251 | + |
| 252 | +animationFrame = requestAnimationFrame(run); |
| 253 | + |
| 254 | +export function onCleanup() { |
| 255 | + cancelAnimationFrame(animationFrame); |
| 256 | + root.destroy(); |
| 257 | +} |
0 commit comments