|
| 1 | +import { perlin3d } from '@typegpu/noise'; |
| 2 | +import tgpu from 'typegpu'; |
| 3 | +import * as d from 'typegpu/data'; |
| 4 | +import * as std from 'typegpu/std'; |
| 5 | + |
| 6 | +const root = await tgpu.init(); |
| 7 | + |
| 8 | +const fromColor = root.createUniform(d.vec3f); |
| 9 | +const polarCoords = root.createUniform(d.u32); |
| 10 | +const squashed = root.createUniform(d.u32); |
| 11 | +const toColor = root.createUniform(d.vec3f); |
| 12 | +const sharpness = root.createUniform(d.f32); |
| 13 | +const distortion = root.createUniform(d.f32); |
| 14 | +const time = root.createUniform(d.f32); |
| 15 | +const grainSeed = root.createUniform(d.f32); |
| 16 | + |
| 17 | +const getGradientColor = (ratio: number) => { |
| 18 | + 'use gpu'; |
| 19 | + if (squashed.$ === 1) { |
| 20 | + return std.mix(fromColor.$, toColor.$, std.smoothstep(0.1, 0.9, ratio)); |
| 21 | + } |
| 22 | + return std.mix(fromColor.$, toColor.$, ratio); |
| 23 | +}; |
| 24 | + |
| 25 | +const tanhVec = (v: d.v2f): d.v2f => { |
| 26 | + 'use gpu'; |
| 27 | + const len = std.length(v); |
| 28 | + const tanh = std.tanh(len); |
| 29 | + return v.div(len).mul(tanh); |
| 30 | +}; |
| 31 | + |
| 32 | +const grain = (color: d.v3f, uv: d.v2f) => { |
| 33 | + 'use gpu'; |
| 34 | + return color.add(perlin3d.sample(d.vec3f(uv.mul(200), grainSeed.$)) * 0.1); |
| 35 | +}; |
| 36 | + |
| 37 | +const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); |
| 38 | +const pipeline = root['~unstable'].createRenderPipeline({ |
| 39 | + vertex: ({ $vertexIndex }) => { |
| 40 | + 'use gpu'; |
| 41 | + const pos = [d.vec2f(0, 0.8), d.vec2f(-0.8, -0.8), d.vec2f(0.8, -0.8)]; |
| 42 | + const uv = [d.vec2f(0.5, 1), d.vec2f(0, 0), d.vec2f(1, 0)]; |
| 43 | + |
| 44 | + return { |
| 45 | + $position: d.vec4f(pos[$vertexIndex], 0, 1), |
| 46 | + uv: uv[$vertexIndex], |
| 47 | + }; |
| 48 | + }, |
| 49 | + fragment: ({ uv }) => { |
| 50 | + 'use gpu'; |
| 51 | + const t = time.$ * 0.1; |
| 52 | + const ouv = uv.mul(5).add(d.vec2f(0, -t)); |
| 53 | + let off = d |
| 54 | + .vec2f( |
| 55 | + perlin3d.sample(d.vec3f(ouv, t)), |
| 56 | + perlin3d.sample(d.vec3f(ouv.mul(2), t + 10)) * 0.5, |
| 57 | + ).add(-0.1); |
| 58 | + // Sharpening the offset |
| 59 | + off = tanhVec(off.mul(sharpness.$)); |
| 60 | + // Offsetting the sample point by the distortion |
| 61 | + const p = uv.add(off.mul(distortion.$)); |
| 62 | + |
| 63 | + // const factor = (p.x - p.y + 0.7) * 0.7; // How far along the diagonal we are |
| 64 | + let factor = d.f32(0); |
| 65 | + if (polarCoords.$ === 1) { |
| 66 | + factor = std.length(p.sub(d.vec2f(0.5, 0.3)).mul(2)); |
| 67 | + } else { |
| 68 | + factor = (p.x + p.y) * 0.7; // How far along the diagonal we are |
| 69 | + } |
| 70 | + return std.saturate(d.vec4f(grain(getGradientColor(factor), uv), 1)); |
| 71 | + }, |
| 72 | + targets: { format: presentationFormat }, |
| 73 | +}); |
| 74 | + |
| 75 | +const canvas = document.querySelector('canvas') as HTMLCanvasElement; |
| 76 | +const context = canvas.getContext('webgpu') as GPUCanvasContext; |
| 77 | + |
| 78 | +context.configure({ |
| 79 | + device: root.device, |
| 80 | + format: presentationFormat, |
| 81 | + alphaMode: 'premultiplied', |
| 82 | +}); |
| 83 | + |
| 84 | +let frameId: number; |
| 85 | +function frame(timestamp: number) { |
| 86 | + time.write(timestamp / 1000); |
| 87 | + grainSeed.write(Math.floor(Math.random() * 100)); |
| 88 | + pipeline |
| 89 | + .withColorAttachment({ |
| 90 | + view: context.getCurrentTexture().createView(), |
| 91 | + loadOp: 'clear', |
| 92 | + storeOp: 'store', |
| 93 | + }) |
| 94 | + .draw(3); |
| 95 | + |
| 96 | + frameId = requestAnimationFrame(frame); |
| 97 | +} |
| 98 | +frameId = requestAnimationFrame(frame); |
| 99 | + |
| 100 | +export const controls = { |
| 101 | + 'Distortion': { |
| 102 | + initial: 0.05, |
| 103 | + min: 0, |
| 104 | + max: 0.2, |
| 105 | + step: 0.001, |
| 106 | + onSliderChange(v: number) { |
| 107 | + distortion.write(v); |
| 108 | + }, |
| 109 | + }, |
| 110 | + 'Sharpness': { |
| 111 | + initial: 4.5, |
| 112 | + min: 0, |
| 113 | + max: 7, |
| 114 | + step: 0.1, |
| 115 | + onSliderChange(v: number) { |
| 116 | + sharpness.write(v ** 2); |
| 117 | + }, |
| 118 | + }, |
| 119 | + 'From Color': { |
| 120 | + initial: [0.057, 0.2235, 0.4705], |
| 121 | + onColorChange(value: readonly [number, number, number]) { |
| 122 | + fromColor.write(d.vec3f(...value)); |
| 123 | + }, |
| 124 | + }, |
| 125 | + 'To Color': { |
| 126 | + initial: [1.538, 0.784, 2], |
| 127 | + onColorChange(value: readonly [number, number, number]) { |
| 128 | + toColor.write(d.vec3f(...value)); |
| 129 | + }, |
| 130 | + }, |
| 131 | + 'Polar Coordinates': { |
| 132 | + initial: false, |
| 133 | + onToggleChange(value: boolean) { |
| 134 | + polarCoords.write(value ? 1 : 0); |
| 135 | + }, |
| 136 | + }, |
| 137 | + 'Squashed': { |
| 138 | + initial: true, |
| 139 | + onToggleChange(value: boolean) { |
| 140 | + squashed.write(value ? 1 : 0); |
| 141 | + }, |
| 142 | + }, |
| 143 | + 'Clouds Preset': { |
| 144 | + onButtonClick() { |
| 145 | + distortion.write(0.05); |
| 146 | + sharpness.write(4.5 ** 2); |
| 147 | + fromColor.write(d.vec3f(0.057, 0.2235, 0.4705)); |
| 148 | + toColor.write(d.vec3f(1.538, 0.784, 2)); |
| 149 | + polarCoords.write(0); |
| 150 | + squashed.write(1); |
| 151 | + }, |
| 152 | + }, |
| 153 | + 'Fire Preset': { |
| 154 | + onButtonClick() { |
| 155 | + distortion.write(0.1); |
| 156 | + sharpness.write(7 ** 2); |
| 157 | + fromColor.write(d.vec3f(2, 0.4, 0.5)); |
| 158 | + toColor.write(d.vec3f(0, 0, 0.4)); |
| 159 | + polarCoords.write(1); |
| 160 | + squashed.write(0); |
| 161 | + }, |
| 162 | + }, |
| 163 | +}; |
| 164 | + |
| 165 | +export function onCleanup() { |
| 166 | + cancelAnimationFrame(frameId); |
| 167 | + root.destroy(); |
| 168 | +} |
0 commit comments