diff --git a/TODO.txt b/TODO.txt index 5e77422..7e4948a 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,12 +4,10 @@ WebGPU First decent rainRender Port render pass 100% For now, use noise instead of the data texture - Get all the uniforms in - Categorize them into buffers Port compute pass 100% Compute entry point can live in the same wgsl file, I think - More uniforms! Categorize! use textureLoad for texel access in render pipeline + Reorder the config fields Render target Resize accordingly Put in its own module diff --git a/js/webgpu/main.js b/js/webgpu/main.js index 0dd7ba6..3af6dfc 100644 --- a/js/webgpu/main.js +++ b/js/webgpu/main.js @@ -2,14 +2,25 @@ import std140 from "./std140.js"; import { getCanvasSize, loadTexture, makeUniformBuffer } from "./utils.js"; const { mat4, vec3 } = glMatrix; -export default async (canvas, config) => { - console.log(config); +const rippleTypes = { + box: 0, + circle: 1, +}; +const cycleStyles = { + cycleFasterWhenDimmed: 0, + cycleRandomly: 1, +}; + +const numVerticesPerQuad = 2 * 3; + +export default async (canvas, config) => { const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const canvasContext = canvas.getContext("webgpu"); const presentationFormat = canvasContext.getPreferredFormat(adapter); - const queue = device.queue; + + console.table(device.limits); const canvasConfig = { device, @@ -19,39 +30,94 @@ export default async (canvas, config) => { canvasContext.configure(canvasConfig); - const renderPassConfig = { - colorAttachments: [ - { - view: canvasContext.getCurrentTexture().createView(), - loadValue: { r: 0, g: 0, b: 0, a: 1 }, - storeOp: "store", - }, - ], - }; + const msdfTexturePromise = loadTexture(device, config.glyphTexURL); + const rainRenderShaderPromise = fetch("shaders/wgsl/rainRenderPass.wgsl").then((response) => response.text()); - const NUM_VERTICES_PER_QUAD = 6; + // The volumetric mode multiplies the number of columns + // to reach the desired density, and then overlaps them + const volumetric = config.volumetric; + const density = volumetric && config.effect !== "none" ? config.density : 1; + const [numRows, numColumns] = [config.numColumns, config.numColumns * density]; - const numColumns = config.numColumns; - const numRows = config.numColumns; + // The volumetric mode requires us to create a grid of quads, + // rather than a single quad for our geometry + const [numQuadRows, numQuadColumns] = volumetric ? [numRows, numColumns] : [1, 1]; + const numQuads = numQuadRows * numQuadColumns; + const quadSize = [1 / numQuadColumns, 1 / numQuadRows]; - const msdfSampler = device.createSampler({ - magFilter: "linear", - minFilter: "linear", - }); + // Various effect-related values + const rippleType = config.rippleTypeName in rippleTypes ? rippleTypes[config.rippleTypeName] : -1; + const cycleStyle = config.cycleStyleName in cycleStyles ? cycleStyles[config.cycleStyleName] : 0; + const slantVec = [Math.cos(config.slant), Math.sin(config.slant)]; + const slantScale = 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1); + const showComputationTexture = config.effect === "none"; - const msdfTexture = await loadTexture(device, config.glyphTexURL); + const configData = [ + // common + { name: "animationSpeed", type: "f32", value: config.animationSpeed }, + { name: "glyphHeightToWidth", type: "f32", value: config.glyphHeightToWidth }, + { name: "resurrectingCodeRatio", type: "f32", value: config.resurrectingCodeRatio }, + { name: "numColumns", type: "i32", value: numColumns }, + { name: "numRows", type: "i32", value: numRows }, + { name: "showComputationTexture", type: "i32", value: showComputationTexture }, - const configStructLayout = std140(["i32", "i32", "f32"]); - const configBuffer = makeUniformBuffer(device, configStructLayout, [numColumns, numRows, config.glyphHeightToWidth]); + // compute + { name: "brightnessThreshold", type: "f32", value: config.brightnessThreshold }, + { name: "brightnessOverride", type: "f32", value: config.brightnessOverride }, + { name: "brightnessDecay", type: "f32", value: config.brightnessDecay }, + { name: "cursorEffectThreshold", type: "f32", value: config.cursorEffectThreshold }, + { name: "cycleSpeed", type: "f32", value: config.cycleSpeed }, + { name: "cycleFrameSkip", type: "i32", value: config.cycleFrameSkip }, + { name: "fallSpeed", type: "f32", value: config.fallSpeed }, + { name: "hasSun", type: "i32", value: config.hasSun }, + { name: "hasThunder", type: "i32", value: config.hasThunder }, + { name: "raindropLength", type: "f32", value: config.raindropLength }, + { name: "rippleScale", type: "f32", value: config.rippleScale }, + { name: "rippleSpeed", type: "f32", value: config.rippleSpeed }, + { name: "rippleThickness", type: "f32", value: config.rippleThickness }, + { name: "cycleStyle", type: "i32", value: cycleStyle }, + { name: "rippleType", type: "i32", value: rippleType }, - const msdfStructLayout = std140(["i32", "i32"]); - const msdfBuffer = makeUniformBuffer(device, msdfStructLayout, [config.glyphTextureColumns, config.glyphSequenceLength]); + // render + { name: "forwardSpeed", type: "f32", value: config.forwardSpeed }, + { name: "glyphVerticalSpacing", type: "f32", value: config.glyphVerticalSpacing }, + { name: "glyphEdgeCrop", type: "f32", value: config.glyphEdgeCrop }, + { name: "isPolar", type: "i32", value: config.isPolar }, + { name: "density", type: "f32", value: density }, + { name: "numQuadColumns", type: "i32", value: numQuadColumns }, + { name: "numQuadRows", type: "i32", value: numQuadRows }, + { name: "quadSize", type: "f32", value: quadSize }, + { name: "slantScale", type: "f32", value: slantScale }, + { name: "slantVec", type: "vec2", value: slantVec }, + { name: "volumetric", type: "i32", value: volumetric }, + ]; + console.table(configData); - const timeStructLayout = std140(["f32", "i32"]); - const timeBuffer = makeUniformBuffer(device, timeStructLayout); + const configLayout = std140(configData.map((field) => field.type)); + const configBuffer = makeUniformBuffer( + device, + configLayout, + configData.map((field) => field.value) + ); - const sceneStructLayout = std140(["vec2", "mat4x4", "mat4x4"]); - const sceneBuffer = makeUniformBuffer(device, sceneStructLayout); + const msdfData = [ + { name: "glyphSequenceLength", type: "i32", value: config.glyphSequenceLength }, + { name: "glyphTextureColumns", type: "i32", value: config.glyphTextureColumns }, + ]; + console.table(msdfData); + + const msdfLayout = std140(msdfData.map((field) => field.type)); + const msdfBuffer = makeUniformBuffer( + device, + msdfLayout, + msdfData.map((field) => field.value) + ); + + const timeLayout = std140(["f32", "i32"]); + const timeBuffer = makeUniformBuffer(device, timeLayout); + + const sceneLayout = std140(["vec2", "mat4x4", "mat4x4"]); + const sceneBuffer = makeUniformBuffer(device, sceneLayout); const transform = mat4.create(); mat4.translate(transform, transform, vec3.fromValues(0, 0, -1)); @@ -62,11 +128,16 @@ export default async (canvas, config) => { const aspectRatio = canvasSize[0] / canvasSize[1]; mat4.perspectiveZO(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000); const screenSize = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1]; - queue.writeBuffer(sceneBuffer, 0, sceneStructLayout.build([screenSize, camera, transform])); + device.queue.writeBuffer(sceneBuffer, 0, sceneLayout.build([screenSize, camera, transform])); }; updateCameraBuffer(); - const [rainRenderShader] = await Promise.all(["shaders/wgsl/rainRenderPass.wgsl"].map(async (path) => (await fetch(path)).text())); + const msdfSampler = device.createSampler({ + magFilter: "linear", + minFilter: "linear", + }); + + const [msdfTexture, rainRenderShader] = await Promise.all([msdfTexturePromise, rainRenderShaderPromise]); const rainRenderShaderModule = device.createShaderModule({ code: rainRenderShader }); @@ -96,8 +167,6 @@ export default async (canvas, config) => { }, }); - console.log(device.limits); - const bindGroup = device.createBindGroup({ layout: rainRenderPipeline.getBindGroupLayout(0), entries: [configBuffer, msdfBuffer, msdfSampler, msdfTexture.createView(), timeBuffer, sceneBuffer] @@ -114,10 +183,19 @@ export default async (canvas, config) => { bundleEncoder.setPipeline(rainRenderPipeline); bundleEncoder.setBindGroup(0, bindGroup); - const numQuads = numColumns * numRows; - bundleEncoder.draw(NUM_VERTICES_PER_QUAD * numQuads, 1, 0, 0); + bundleEncoder.draw(numVerticesPerQuad * numQuads, 1, 0, 0); const renderBundles = [bundleEncoder.finish()]; + const renderPassConfig = { + colorAttachments: [ + { + view: canvasContext.getCurrentTexture().createView(), + loadValue: { r: 0, g: 0, b: 0, a: 1 }, + storeOp: "store", + }, + ], + }; + let frame = 0; const renderLoop = (now) => { @@ -131,7 +209,7 @@ export default async (canvas, config) => { updateCameraBuffer(); } - queue.writeBuffer(timeBuffer, 0, timeStructLayout.build([now / 1000, frame])); + device.queue.writeBuffer(timeBuffer, 0, timeLayout.build([now / 1000, frame])); frame++; renderPassConfig.colorAttachments[0].view = canvasContext.getCurrentTexture().createView(); @@ -141,7 +219,7 @@ export default async (canvas, config) => { renderPass.executeBundles(renderBundles); renderPass.endPass(); const commandBuffer = encoder.finish(); - queue.submit([commandBuffer]); + device.queue.submit([commandBuffer]); requestAnimationFrame(renderLoop); }; diff --git a/js/webgpu/std140.js b/js/webgpu/std140.js index 2217344..c2fb8f9 100644 --- a/js/webgpu/std140.js +++ b/js/webgpu/std140.js @@ -68,7 +68,7 @@ const buildStruct = (fields, values, buffer) => { for (let i = 0; i < values.length; i++) { const view = views[fields[i].baseType]; const value = values[i]; - const array = value[Symbol.iterator] == null ? [value] : value; + const array = value[Symbol.iterator] == null ? [Number(value)] : value; view.set(array, fields[i].byteOffset); } return buffer; diff --git a/shaders/wgsl/rainRenderPass.wgsl b/shaders/wgsl/rainRenderPass.wgsl index 344886e..1a04acf 100644 --- a/shaders/wgsl/rainRenderPass.wgsl +++ b/shaders/wgsl/rainRenderPass.wgsl @@ -1,19 +1,53 @@ -let NUM_VERTICES_PER_QUAD:i32 = 6; +let NUM_VERTICES_PER_QUAD:i32 = 6; // 2 * 3 let PI:f32 = 3.14159265359; let TWO_PI:f32 = 6.28318530718; let SQRT_2:f32 = 1.4142135623730951; let SQRT_5:f32 = 2.23606797749979; [[block]] struct Config { - numColumns: i32; - numRows: i32; - glyphHeightToWidth: f32; + // common + animationSpeed : f32; + glyphHeightToWidth : f32; + resurrectingCodeRatio : f32; + numColumns : i32; + numRows : i32; + showComputationTexture : i32; + + // compute + brightnessThreshold : f32; + brightnessOverride : f32; + brightnessDecay : f32; + cursorEffectThreshold : f32; + cycleSpeed : f32; + cycleFrameSkip : i32; + fallSpeed : f32; + hasSun : i32; + hasThunder : i32; + raindropLength : f32; + rippleScale : f32; + rippleSpeed : f32; + rippleThickness : f32; + cycleStyle : i32; + rippleType : i32; + + // render + forwardSpeed : f32; + glyphVerticalSpacing : f32; + glyphEdgeCrop : f32; + isPolar : i32; + density : f32; + numQuadColumns : i32; + numQuadRows : i32; + quadSize : f32; + slantScale : f32; + slantVec : vec2; + volumetric : i32; }; [[group(0), binding(0)]] var config:Config; [[block]] struct MSDF { - glyphTextureColumns: i32; glyphSequenceLength: i32; + glyphTextureColumns: i32; }; [[group(0), binding(1)]] var msdf:MSDF; [[group(0), binding(2)]] var msdfSampler: sampler; @@ -60,6 +94,9 @@ struct VertexOutput { [[stage(vertex)]] fn vertMain([[builtin(vertex_index)]] VertexIndex:u32) -> VertexOutput { + var timePlaceholder = time.seconds; + + var i = i32(VertexIndex); var quadIndex = i / NUM_VERTICES_PER_QUAD; @@ -68,16 +105,16 @@ struct VertexOutput { f32(((i + 1) % NUM_VERTICES_PER_QUAD / 3)) ); - var cellPosition = vec2( - quadIndex % config.numColumns, - quadIndex / config.numColumns + var quadPosition = vec2( + quadIndex % config.numQuadColumns, + quadIndex / config.numQuadColumns ); var position = cornerPosition; - position = position + vec2(cellPosition); + position = position + vec2(quadPosition); position = position / vec2( - f32(config.numColumns), - f32(config.numRows) + f32(config.numQuadColumns), + f32(config.numQuadRows) ); position = 1.0 - position * 2.0; @@ -86,8 +123,8 @@ struct VertexOutput { var depth:f32 = 0.0; // depth = -0.5 - // + sin(time.seconds * 2.0 + f32(cellPosition.x) / f32(config.numColumns) * 10.0) * 0.2 - // + sin(time.seconds * 2.0 + f32(cellPosition.y) / f32(config.numColumns) * 10.0) * 0.2; + // + sin(time.seconds * 2.0 + f32(quadPosition.x) / f32(config.numQuadColumns) * 10.0) * 0.2 + // + sin(time.seconds * 2.0 + f32(quadPosition.y) / f32(config.numQuadRows) * 10.0) * 0.2; var pos:vec4 = vec4(position, depth, 1.0); pos.x = pos.x / config.glyphHeightToWidth; @@ -103,8 +140,8 @@ struct VertexOutput { [[stage(fragment)]] fn fragMain([[location(0)]] UV:vec2) -> [[location(0)]] vec4 { var color:vec4 = textureSample(msdfTexture, msdfSampler, UV / f32(msdf.glyphTextureColumns)); - // color.b = color.b * (sin(time.seconds * TWO_PI) * 0.5 + 0.5); - color.b = color.b * f32(time.frames / 60 % 2); + + color = vec4(UV, 0.5, 1.0); return color; }