From 5e6cf006f71f17dff05f0b382c924e06d464b156 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 3 Aug 2025 17:05:45 +0200 Subject: [PATCH 1/7] Migrate the Caustics demo --- bunfig.toml | 1 + package.json | 3 + src/examples/caustics-demo.ts | 237 ++++++++++++++++++++++++++++++++++ src/examples/index.ts | 8 ++ typegpu-plugin.ts | 6 + 5 files changed, 255 insertions(+) create mode 100644 bunfig.toml create mode 100644 src/examples/caustics-demo.ts create mode 100644 typegpu-plugin.ts diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 000000000..48a7fe895 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1 @@ +preload = ["./typegpu-plugin.ts"] diff --git a/package.json b/package.json index 4dce94a67..041b8d1ce 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,12 @@ "printWidth": 120 }, "dependencies": { + "@typegpu/noise": "https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/noise@659591da8e6d8ce5b63398d7be8a5062e68b3254", "bun-webgpu": "0.1.0", "jimp": "1.6.0", "three": "0.177.0", + "typegpu": "^https://pkg.pr.new/software-mansion/TypeGPU/typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", + "unplugin-typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", "yoga-layout": "3.2.1" } } diff --git a/src/examples/caustics-demo.ts b/src/examples/caustics-demo.ts new file mode 100644 index 000000000..e323f01f1 --- /dev/null +++ b/src/examples/caustics-demo.ts @@ -0,0 +1,237 @@ +#!/usr/bin/env bun + +import { perlin3d } from "@typegpu/noise" +import { createWebGPUDevice, setupGlobals } from "bun-webgpu" +import tgpu, { type TgpuRoot } from "typegpu" +import * as d from "typegpu/data" +import * as std from "typegpu/std" +import { CLICanvas, type CliRenderer, GroupRenderable, SuperSampleType } from "../index" + +/** Controls the angle of rotation for the pool tile texture */ +const angle = 0.2 +/** The scene fades into this color at a distance */ +const fogColor = d.vec3f(0.05, 0.2, 0.7) +/** The ambient light color */ +const ambientColor = d.vec3f(0.2, 0.5, 1) + +const layout = tgpu.bindGroupLayout({ + aspect: { uniform: d.f32 }, + time: { uniform: d.f32 }, +}) + +const mainVertex = tgpu["~unstable"].vertexFn({ + in: { vertexIndex: d.builtin.vertexIndex }, + out: { pos: d.builtin.position, uv: d.vec2f }, +})(({ vertexIndex }) => { + const pos = [d.vec2f(-1, -1), d.vec2f(3, -1), d.vec2f(-1, 3)] + const left = 0.5 - layout.$.aspect * 0.5 + const right = 0.5 + layout.$.aspect * 0.5 + const uv = [d.vec2f(left, 0), d.vec2f(right, 0), d.vec2f(left, 2)] + + return { + pos: d.vec4f(pos[vertexIndex], 0, 1), + uv: uv[vertexIndex], + } +}) + +/** + * Given a coordinate, it returns a grayscale floor tile pattern at that + * location. + */ +const tilePattern = tgpu.fn( + [d.vec2f], + d.f32, +)((uv) => { + const tiledUv = std.fract(uv) + const proximity = std.abs(std.sub(std.mul(tiledUv, 2), 1)) + const maxProximity = std.max(proximity.x, proximity.y) + return std.clamp(std.pow(1 - maxProximity, 0.6) * 5, 0, 1) +}) + +const caustics = tgpu.fn( + [d.vec2f, d.f32, d.vec3f], + d.vec3f, +)((uv, time, profile) => { + const distortion = perlin3d.sample(d.vec3f(std.mul(uv, 0.5), time * 0.2)) + // Distorting UV coordinates + const uv2 = std.add(uv, distortion) + const noise = std.abs(perlin3d.sample(d.vec3f(std.mul(uv2, 5), time))) + return std.pow(d.vec3f(1 - noise), profile) +}) + +const clamp01 = tgpu.fn([d.f32], d.f32)((v) => std.clamp(v, 0, 1)) + +/** + * Returns a transformation matrix that represents an `angle` rotation + * in the XY plane (around the imaginary Z axis) + */ +const rotateXY = tgpu.fn( + [d.f32], + d.mat2x2f, +)((angle) => { + return d.mat2x2f( + /* right */ d.vec2f(std.cos(angle), std.sin(angle)), + /* up */ d.vec2f(-std.sin(angle), std.cos(angle)), + ) +}) + +const mainFragment = tgpu["~unstable"].fragmentFn({ + in: { uv: d.vec2f }, + out: d.vec4f, +})(({ uv }) => { + /** + * A transformation matrix that skews the perspective a bit + * when applied to UV coordinates + */ + const skewMat = d.mat2x2f( + d.vec2f(std.cos(angle), std.sin(angle)), + d.vec2f(-std.sin(angle) * 10 + uv.x * 3, std.cos(angle) * 5), + ) + const skewedUv = std.mul(skewMat, uv) + const tile = tilePattern(std.mul(skewedUv, 10)) + const albedo = std.mix(d.vec3f(0.1), d.vec3f(1), tile) + + // Transforming coordinates to simulate perspective squash + const cuv = d.vec2f(uv.x * (std.pow(uv.y * 1.5, 3) + 0.1) * 5, std.pow((uv.y * 1.5 + 0.1) * 1.5, 3) * 1) + // Generating two layers of caustics (large scale, and small scale) + const c1 = std.mul( + caustics(cuv, layout.$.time * 0.2, /* profile */ d.vec3f(4, 4, 1)), + // Tinting + d.vec3f(0.4, 0.65, 1), + ) + const c2 = std.mul( + caustics(std.mul(cuv, 2), layout.$.time * 0.4, /* profile */ d.vec3f(16, 1, 4)), + // Tinting + d.vec3f(0.18, 0.3, 0.5), + ) + + // -- BLEND -- + + const blendCoord = d.vec3f(std.mul(uv, d.vec2f(5, 10)), layout.$.time * 0.2 + 5) + // A smooth blending factor, so that caustics only appear at certain spots + const blend = clamp01(perlin3d.sample(blendCoord) + 0.3) + + // -- FOG -- + + const noFogColor = std.mul(albedo, std.mix(ambientColor, std.add(c1, c2), blend)) + // Fog blending factor, based on the height of the pixels + const fog = std.min(std.pow(uv.y, 0.5) * 1.2, 1) + + // -- GOD RAYS -- + + const godRayUv = std.mul(std.mul(rotateXY(-0.3), uv), d.vec2f(15, 3)) + const godRayFactor = std.pow(uv.y, 1) + const godRay1 = std.mul( + std.add(perlin3d.sample(d.vec3f(godRayUv, layout.$.time * 0.5)), 1), + // Tinting + std.mul(d.vec3f(0.18, 0.3, 0.5), godRayFactor), + ) + const godRay2 = std.mul( + std.add(perlin3d.sample(d.vec3f(std.mul(godRayUv, 2), layout.$.time * 0.3)), 1), + // Tinting + std.mul(d.vec3f(0.18, 0.3, 0.5), godRayFactor * 0.4), + ) + const godRays = std.add(godRay1, godRay2) + + return d.vec4f(std.add(std.mix(noFogColor, fogColor, fog), godRays), 1) +}) + +let isRunning = true +let root: TgpuRoot | undefined +let keyHandler: ((key: Buffer) => void) | null = null +let handleResize: ((width: number, height: number) => void) | null = null +let parentContainer: GroupRenderable | null = null + +export async function run(renderer: CliRenderer): Promise { + renderer.start() + const WIDTH = renderer.terminalWidth + const HEIGHT = renderer.terminalHeight + + parentContainer = new GroupRenderable("fractal-container", { + x: 0, + y: 0, + zIndex: 10, + visible: true, + }) + renderer.add(parentContainer) + + // Bun WebGPU setup + setupGlobals() + const device = await createWebGPUDevice() + const canvas = new CLICanvas(device, WIDTH, HEIGHT, SuperSampleType.GPU) + + root = tgpu.initFromDevice({ device }) + + /** Seconds passed since the start of the example, wrapped to the range [0, 1000) */ + const timeBuffer = root.createBuffer(d.f32).$usage("uniform") + /** Aspect ratio of the canvas */ + const aspectBuffer = root.createBuffer(d.f32, WIDTH / HEIGHT).$usage("uniform") + + const bindGroup = root.createBindGroup(layout, { + time: timeBuffer, + aspect: aspectBuffer, + }) + + handleResize = (width: number, height: number) => { + aspectBuffer.write(width / height) + } + + renderer.on("resize", handleResize) + + // Assuming a format... + const presentationFormat = "rgba8unorm" as const + const context = canvas.getContext("webgpu") as GPUCanvasContext + + context.configure({ + device: root.device, + format: presentationFormat, + alphaMode: "premultiplied", + }) + + const pipeline = root["~unstable"] + .withVertex(mainVertex, {}) + .withFragment(mainFragment, { format: presentationFormat }) + .createPipeline() + // --- + .with(layout, bindGroup) + + let time = 0 + + renderer.setFrameCallback(async (deltaMs) => { + if (!isRunning) return + + time += deltaMs / 1000 + timeBuffer.write(time) + + pipeline + .withColorAttachment({ + view: context.getCurrentTexture().createView(), + loadOp: "clear", + storeOp: "store", + }) + .draw(3) + + await canvas.readPixelsIntoBuffer(renderer.nextRenderBuffer) + }) +} + +export function destroy(renderer: CliRenderer): void { + isRunning = false + if (keyHandler) { + process.stdin.off("data", keyHandler) + keyHandler = null + } + + if (handleResize) { + renderer.off("resize", handleResize) + handleResize = null + } + + renderer.clearFrameCallbacks() + root?.destroy() + + if (parentContainer) { + renderer.remove("fractal-container") + parentContainer = null + } +} diff --git a/src/examples/index.ts b/src/examples/index.ts index 23281cd77..64c32750d 100644 --- a/src/examples/index.ts +++ b/src/examples/index.ts @@ -12,6 +12,7 @@ import { type ParsedKey, } from "../index" import { renderFontToFrameBuffer, measureText } from "../ui/ascii.font" +import * as causticsDemo from "./caustics-demo" import * as boxExample from "./fonts" import * as fractalShaderExample from "./fractal-shader-demo" import * as framebufferExample from "./framebuffer-demo" @@ -46,6 +47,13 @@ interface Example { } const examples: Example[] = [ + // TODO: Move to be after the Fractal shader + { + name: "Caustics Shader", + description: "Caustics in a fragment shader", + run: causticsDemo.run, + destroy: causticsDemo.destroy, + }, { name: "Mouse Interaction Demo", description: "Interactive mouse trails and clickable cells demonstration", diff --git a/typegpu-plugin.ts b/typegpu-plugin.ts new file mode 100644 index 000000000..c9cc82246 --- /dev/null +++ b/typegpu-plugin.ts @@ -0,0 +1,6 @@ +import { plugin } from 'bun'; +import { bunPlugin } from 'unplugin-typegpu'; + +plugin(bunPlugin({ + include: /\.m?[t]sx?$/, +})); From a5f369fd76db40df2b84e315ff7e838518f60a59 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 3 Aug 2025 17:07:56 +0200 Subject: [PATCH 2/7] Update typegpu version --- bun.lock | 51 +++++++++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 02a375b95..2f5f3312b 100644 --- a/bun.lock +++ b/bun.lock @@ -2,11 +2,14 @@ "lockfileVersion": 1, "workspaces": { "": { - "name": "opentui", + "name": "@opentui/core", "dependencies": { + "@typegpu/noise": "https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/noise@659591da8e6d8ce5b63398d7be8a5062e68b3254", "bun-webgpu": "0.1.0", "jimp": "1.6.0", "three": "0.177.0", + "typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", + "unplugin-typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", "yoga-layout": "3.2.1", }, "devDependencies": { @@ -23,6 +26,8 @@ }, }, "packages": { + "@babel/standalone": ["@babel/standalone@7.28.2", "", {}, "sha512-1kjA8XzBRN68HoDDYKP38bucHtxYWCIX8XdYwe1drRNUOjOVNt8EMy9jiE6UwaGFfU7NOHCG+C8KgBc9CR08nA=="], + "@dimforge/rapier2d-simd-compat": ["@dimforge/rapier2d-simd-compat@0.17.3", "", {}, "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg=="], "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], @@ -83,15 +88,21 @@ "@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="], + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], + "@typegpu/noise": ["@typegpu/noise@https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/noise@659591da8e6d8ce5b63398d7be8a5062e68b3254", { "peerDependencies": { "typegpu": "^0.6.0" } }], + "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], - "@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], + + "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="], "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], @@ -103,6 +114,8 @@ "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "any-base": ["any-base@1.1.0", "", {}, "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="], "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="], @@ -121,6 +134,10 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], @@ -141,6 +158,10 @@ "jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="], + "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + + "magic-string-ast": ["magic-string-ast@1.0.0", "", { "dependencies": { "magic-string": "^0.30.17" } }, "sha512-8rbuNizut2gW94kv7pqgt0dvk+AHLPVIm0iJtpSgQJ9dx21eWx5SBel8z3jp1xtC0j6/iyK3AWGhAR1H61s7LA=="], + "meshoptimizer": ["meshoptimizer@0.18.1", "", {}, "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw=="], "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], @@ -155,8 +176,12 @@ "parse-bmfont-xml": ["parse-bmfont-xml@1.1.6", "", { "dependencies": { "xml-parse-from-string": "^1.0.0", "xml2js": "^0.5.0" } }, "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="], "planck": ["planck@1.4.2", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-mNbhnV3g8X2rwGxzcesjmN8BDA6qfXgQxXVMkWau9MCRlQY0RLNEkyHlVp6yFy/X6qrzAXyNONCnZ1cGDLrNew=="], @@ -187,12 +212,28 @@ "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + "tinyest": ["tinyest@0.1.1", "", {}, "sha512-YNHlB8BOXgW6RPzrfqqAkgyY9xj33sjXJcJlOl3MwY0BXXx26m3JUqf5yV8iBdwJPNe51DmxypR9Zbbd266biQ=="], + + "tinyest-for-wgsl": ["tinyest-for-wgsl@0.1.2", "", { "dependencies": { "tinyest": "~0.1.1" } }, "sha512-yJ49SoJIpEi4ADsBVNE54GVJ5JZMIAKNkRueeNpYhIiq0z1Nn9THJNMNP1b9HI0VQt7LzCrxT0ZP29muiUtcRg=="], + "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "typed-binary": ["typed-binary@4.3.2", "", {}, "sha512-HT3pIBM2njCZUmeczDaQUUErGiM6GXFCqMsHegE12HCoBtvHCkfR10JJni0TeGOTnLilTd6YFyj+YhflqQDrDQ=="], + + "typegpu": ["typegpu@https://pkg.pr.new/software-mansion/TypeGPU/typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", { "dependencies": { "tinyest": "~0.1.1", "typed-binary": "^4.3.1" } }], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "unplugin": ["unplugin@2.3.5", "", { "dependencies": { "acorn": "^8.14.1", "picomatch": "^4.0.2", "webpack-virtual-modules": "^0.6.2" } }, "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw=="], + + "unplugin-typegpu": ["unplugin-typegpu@https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", { "dependencies": { "@babel/standalone": "^7.27.0", "defu": "^6.1.4", "estree-walker": "^3.0.3", "magic-string-ast": "^1.0.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "tinyest": "~0.1.1", "tinyest-for-wgsl": "~0.1.2", "unplugin": "^2.3.5" }, "peerDependencies": { "typegpu": "^0.6.0" } }], "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], + "xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="], "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], @@ -203,6 +244,8 @@ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="], + "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], } } diff --git a/package.json b/package.json index 041b8d1ce..40074bfaf 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "bun-webgpu": "0.1.0", "jimp": "1.6.0", "three": "0.177.0", - "typegpu": "^https://pkg.pr.new/software-mansion/TypeGPU/typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", + "typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", "unplugin-typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", "yoga-layout": "3.2.1" } From 4f037c3a557c15da4d725cf6f143ce4ec5a782d0 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 3 Aug 2025 21:26:36 +0200 Subject: [PATCH 3/7] Improve demo code --- src/examples/caustics-demo.ts | 71 +++++++++++++++++------------------ typegpu-plugin.ts | 12 +++--- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/examples/caustics-demo.ts b/src/examples/caustics-demo.ts index e323f01f1..e028289b9 100644 --- a/src/examples/caustics-demo.ts +++ b/src/examples/caustics-demo.ts @@ -7,12 +7,17 @@ import * as d from "typegpu/data" import * as std from "typegpu/std" import { CLICanvas, type CliRenderer, GroupRenderable, SuperSampleType } from "../index" +/** + * With supersampling, the scene is rendered at 2x the resolution + */ +const pixelRatio = 2 /** Controls the angle of rotation for the pool tile texture */ const angle = 0.2 /** The scene fades into this color at a distance */ const fogColor = d.vec3f(0.05, 0.2, 0.7) /** The ambient light color */ const ambientColor = d.vec3f(0.2, 0.5, 1) +const tileDensity = 2 const layout = tgpu.bindGroupLayout({ aspect: { uniform: d.f32 }, @@ -43,16 +48,16 @@ const tilePattern = tgpu.fn( d.f32, )((uv) => { const tiledUv = std.fract(uv) - const proximity = std.abs(std.sub(std.mul(tiledUv, 2), 1)) + const proximity = std.abs(tiledUv.mul(2).sub(1)) const maxProximity = std.max(proximity.x, proximity.y) - return std.clamp(std.pow(1 - maxProximity, 0.6) * 5, 0, 1) + return std.clamp(std.pow(1 - maxProximity, 0.8) * 5, 0, 1) }) const caustics = tgpu.fn( [d.vec2f, d.f32, d.vec3f], d.vec3f, )((uv, time, profile) => { - const distortion = perlin3d.sample(d.vec3f(std.mul(uv, 0.5), time * 0.2)) + const distortion = perlin3d.sample(d.vec3f(uv.mul(0.5), time * 0.2)) // Distorting UV coordinates const uv2 = std.add(uv, distortion) const noise = std.abs(perlin3d.sample(d.vec3f(std.mul(uv2, 5), time))) @@ -79,70 +84,61 @@ const mainFragment = tgpu["~unstable"].fragmentFn({ in: { uv: d.vec2f }, out: d.vec4f, })(({ uv }) => { + const time = layout.$.time /** * A transformation matrix that skews the perspective a bit * when applied to UV coordinates */ const skewMat = d.mat2x2f( d.vec2f(std.cos(angle), std.sin(angle)), - d.vec2f(-std.sin(angle) * 10 + uv.x * 3, std.cos(angle) * 5), + d.vec2f(-std.sin(angle) * 5 + uv.x * 2, std.cos(angle) * 5), ) - const skewedUv = std.mul(skewMat, uv) - const tile = tilePattern(std.mul(skewedUv, 10)) + const skewedUv = skewMat.mul(uv) + const tile = tilePattern(skewedUv.mul(tileDensity)) const albedo = std.mix(d.vec3f(0.1), d.vec3f(1), tile) // Transforming coordinates to simulate perspective squash const cuv = d.vec2f(uv.x * (std.pow(uv.y * 1.5, 3) + 0.1) * 5, std.pow((uv.y * 1.5 + 0.1) * 1.5, 3) * 1) // Generating two layers of caustics (large scale, and small scale) - const c1 = std.mul( - caustics(cuv, layout.$.time * 0.2, /* profile */ d.vec3f(4, 4, 1)), + const c1 = caustics(cuv, time * 0.2, d.vec3f(4, 4, 1)) // Tinting - d.vec3f(0.4, 0.65, 1), - ) - const c2 = std.mul( - caustics(std.mul(cuv, 2), layout.$.time * 0.4, /* profile */ d.vec3f(16, 1, 4)), + .mul(d.vec3f(0.4, 0.65, 1)) + const c2 = caustics(cuv.mul(2), time * 0.4, d.vec3f(16, 1, 4)) // Tinting - d.vec3f(0.18, 0.3, 0.5), - ) + .mul(d.vec3f(0.18, 0.3, 0.5)) // -- BLEND -- - const blendCoord = d.vec3f(std.mul(uv, d.vec2f(5, 10)), layout.$.time * 0.2 + 5) + const blendCoord = d.vec3f(uv.mul(d.vec2f(5, 10)), layout.$.time * 0.2 + 5) // A smooth blending factor, so that caustics only appear at certain spots const blend = clamp01(perlin3d.sample(blendCoord) + 0.3) // -- FOG -- - const noFogColor = std.mul(albedo, std.mix(ambientColor, std.add(c1, c2), blend)) + const noFogColor = albedo.mul(std.mix(ambientColor, c1.add(c2), blend)) // Fog blending factor, based on the height of the pixels const fog = std.min(std.pow(uv.y, 0.5) * 1.2, 1) // -- GOD RAYS -- - const godRayUv = std.mul(std.mul(rotateXY(-0.3), uv), d.vec2f(15, 3)) - const godRayFactor = std.pow(uv.y, 1) - const godRay1 = std.mul( - std.add(perlin3d.sample(d.vec3f(godRayUv, layout.$.time * 0.5)), 1), - // Tinting - std.mul(d.vec3f(0.18, 0.3, 0.5), godRayFactor), - ) - const godRay2 = std.mul( - std.add(perlin3d.sample(d.vec3f(std.mul(godRayUv, 2), layout.$.time * 0.3)), 1), - // Tinting - std.mul(d.vec3f(0.18, 0.3, 0.5), godRayFactor * 0.4), - ) - const godRays = std.add(godRay1, godRay2) + const godRayUv = rotateXY(-0.3).mul(uv).mul(d.vec2f(10, 2)) + const godRayTint = d.vec3f(0.18, 0.3, 0.5) + const godRay1 = perlin3d.sample(d.vec3f(godRayUv, time * 0.5)) + 1 + const godRay2 = perlin3d.sample(d.vec3f(godRayUv.mul(2), time * 0.3)) + 1 + const godRayBlend = std.pow(uv.y, 2) * 0.5 + const godRays = godRayTint.mul(godRay1 + godRay2).mul(godRayBlend * 0.6) - return d.vec4f(std.add(std.mix(noFogColor, fogColor, fog), godRays), 1) + return d.vec4f(std.mix(noFogColor, fogColor, fog).add(godRays), 1) }) let isRunning = true let root: TgpuRoot | undefined -let keyHandler: ((key: Buffer) => void) | null = null -let handleResize: ((width: number, height: number) => void) | null = null -let parentContainer: GroupRenderable | null = null +let keyHandler: ((key: Buffer) => void) | undefined +let handleResize: ((width: number, height: number) => void) | undefined +let parentContainer: GroupRenderable | undefined export async function run(renderer: CliRenderer): Promise { + isRunning = true renderer.start() const WIDTH = renderer.terminalWidth const HEIGHT = renderer.terminalHeight @@ -158,7 +154,7 @@ export async function run(renderer: CliRenderer): Promise { // Bun WebGPU setup setupGlobals() const device = await createWebGPUDevice() - const canvas = new CLICanvas(device, WIDTH, HEIGHT, SuperSampleType.GPU) + const canvas = new CLICanvas(device, WIDTH * pixelRatio, HEIGHT * pixelRatio, SuperSampleType.GPU) root = tgpu.initFromDevice({ device }) @@ -174,6 +170,7 @@ export async function run(renderer: CliRenderer): Promise { handleResize = (width: number, height: number) => { aspectBuffer.write(width / height) + canvas.setSize(width * pixelRatio, height * pixelRatio) } renderer.on("resize", handleResize) @@ -219,12 +216,12 @@ export function destroy(renderer: CliRenderer): void { isRunning = false if (keyHandler) { process.stdin.off("data", keyHandler) - keyHandler = null + keyHandler = undefined } if (handleResize) { renderer.off("resize", handleResize) - handleResize = null + handleResize = undefined } renderer.clearFrameCallbacks() @@ -232,6 +229,6 @@ export function destroy(renderer: CliRenderer): void { if (parentContainer) { renderer.remove("fractal-container") - parentContainer = null + parentContainer = undefined } } diff --git a/typegpu-plugin.ts b/typegpu-plugin.ts index c9cc82246..0bc74fa35 100644 --- a/typegpu-plugin.ts +++ b/typegpu-plugin.ts @@ -1,6 +1,8 @@ -import { plugin } from 'bun'; -import { bunPlugin } from 'unplugin-typegpu'; +import { plugin } from "bun" +import { bunPlugin } from "unplugin-typegpu" -plugin(bunPlugin({ - include: /\.m?[t]sx?$/, -})); +plugin( + bunPlugin({ + include: /\.m?[t]sx?$/, + }), +) From d39cf47d98566dfc6291104eb061c6b31dfeed73 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Sun, 3 Aug 2025 21:28:50 +0200 Subject: [PATCH 4/7] Update caustics-demo.ts --- src/examples/caustics-demo.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/examples/caustics-demo.ts b/src/examples/caustics-demo.ts index e028289b9..f0730d25c 100644 --- a/src/examples/caustics-demo.ts +++ b/src/examples/caustics-demo.ts @@ -59,8 +59,8 @@ const caustics = tgpu.fn( )((uv, time, profile) => { const distortion = perlin3d.sample(d.vec3f(uv.mul(0.5), time * 0.2)) // Distorting UV coordinates - const uv2 = std.add(uv, distortion) - const noise = std.abs(perlin3d.sample(d.vec3f(std.mul(uv2, 5), time))) + const uv2 = uv.add(distortion) + const noise = std.abs(perlin3d.sample(d.vec3f(uv2.mul(5), time))) return std.pow(d.vec3f(1 - noise), profile) }) @@ -111,7 +111,7 @@ const mainFragment = tgpu["~unstable"].fragmentFn({ const blendCoord = d.vec3f(uv.mul(d.vec2f(5, 10)), layout.$.time * 0.2 + 5) // A smooth blending factor, so that caustics only appear at certain spots - const blend = clamp01(perlin3d.sample(blendCoord) + 0.3) + const blend = std.clamp(perlin3d.sample(blendCoord) + 0.3, 0, 1) // -- FOG -- From 27826886fb3cc7e82f5c8b582e1df700f711568e Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Mon, 4 Aug 2025 13:40:12 +0200 Subject: [PATCH 5/7] Lava Lamp example --- bun.lock | 12 +- package.json | 6 +- src/examples/caustics-demo.ts | 15 +-- src/examples/index.ts | 20 +-- src/examples/lavalamp-demo.ts | 226 ++++++++++++++++++++++++++++++++++ 5 files changed, 254 insertions(+), 25 deletions(-) create mode 100644 src/examples/lavalamp-demo.ts diff --git a/bun.lock b/bun.lock index 2f5f3312b..bc92e3817 100644 --- a/bun.lock +++ b/bun.lock @@ -4,12 +4,12 @@ "": { "name": "@opentui/core", "dependencies": { - "@typegpu/noise": "https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/noise@659591da8e6d8ce5b63398d7be8a5062e68b3254", + "@typegpu/noise": "https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/noise@f71da6517c202229d3da92ec645b641bf6417816", "bun-webgpu": "0.1.0", "jimp": "1.6.0", "three": "0.177.0", - "typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", - "unplugin-typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", + "typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/typegpu@f71da6517c202229d3da92ec645b641bf6417816", + "unplugin-typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@f71da6517c202229d3da92ec645b641bf6417816", "yoga-layout": "3.2.1", }, "devDependencies": { @@ -94,7 +94,7 @@ "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], - "@typegpu/noise": ["@typegpu/noise@https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/noise@659591da8e6d8ce5b63398d7be8a5062e68b3254", { "peerDependencies": { "typegpu": "^0.6.0" } }], + "@typegpu/noise": ["@typegpu/noise@https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/noise@f71da6517c202229d3da92ec645b641bf6417816", { "peerDependencies": { "typegpu": "^0.6.0" } }], "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], @@ -220,7 +220,7 @@ "typed-binary": ["typed-binary@4.3.2", "", {}, "sha512-HT3pIBM2njCZUmeczDaQUUErGiM6GXFCqMsHegE12HCoBtvHCkfR10JJni0TeGOTnLilTd6YFyj+YhflqQDrDQ=="], - "typegpu": ["typegpu@https://pkg.pr.new/software-mansion/TypeGPU/typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", { "dependencies": { "tinyest": "~0.1.1", "typed-binary": "^4.3.1" } }], + "typegpu": ["typegpu@https://pkg.pr.new/software-mansion/TypeGPU/typegpu@f71da6517c202229d3da92ec645b641bf6417816", { "dependencies": { "tinyest": "~0.1.1", "typed-binary": "^4.3.1" } }], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], @@ -228,7 +228,7 @@ "unplugin": ["unplugin@2.3.5", "", { "dependencies": { "acorn": "^8.14.1", "picomatch": "^4.0.2", "webpack-virtual-modules": "^0.6.2" } }, "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw=="], - "unplugin-typegpu": ["unplugin-typegpu@https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", { "dependencies": { "@babel/standalone": "^7.27.0", "defu": "^6.1.4", "estree-walker": "^3.0.3", "magic-string-ast": "^1.0.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "tinyest": "~0.1.1", "tinyest-for-wgsl": "~0.1.2", "unplugin": "^2.3.5" }, "peerDependencies": { "typegpu": "^0.6.0" } }], + "unplugin-typegpu": ["unplugin-typegpu@https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@f71da6517c202229d3da92ec645b641bf6417816", { "dependencies": { "@babel/standalone": "^7.27.0", "defu": "^6.1.4", "estree-walker": "^3.0.3", "magic-string-ast": "^1.0.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "tinyest": "~0.1.1", "tinyest-for-wgsl": "~0.1.2", "unplugin": "^2.3.5" }, "peerDependencies": { "typegpu": "^0.6.0" } }], "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="], diff --git a/package.json b/package.json index 40074bfaf..8954b70b1 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,12 @@ "printWidth": 120 }, "dependencies": { - "@typegpu/noise": "https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/noise@659591da8e6d8ce5b63398d7be8a5062e68b3254", + "@typegpu/noise": "https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/noise@f71da6517c202229d3da92ec645b641bf6417816", "bun-webgpu": "0.1.0", "jimp": "1.6.0", "three": "0.177.0", - "typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", - "unplugin-typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@659591da8e6d8ce5b63398d7be8a5062e68b3254", + "typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/typegpu@f71da6517c202229d3da92ec645b641bf6417816", + "unplugin-typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@f71da6517c202229d3da92ec645b641bf6417816", "yoga-layout": "3.2.1" } } diff --git a/src/examples/caustics-demo.ts b/src/examples/caustics-demo.ts index f0730d25c..703c4055a 100644 --- a/src/examples/caustics-demo.ts +++ b/src/examples/caustics-demo.ts @@ -7,9 +7,7 @@ import * as d from "typegpu/data" import * as std from "typegpu/std" import { CLICanvas, type CliRenderer, GroupRenderable, SuperSampleType } from "../index" -/** - * With supersampling, the scene is rendered at 2x the resolution - */ +/** With supersampling, the scene is rendered at 2x the resolution */ const pixelRatio = 2 /** Controls the angle of rotation for the pool tile texture */ const angle = 0.2 @@ -17,7 +15,9 @@ const angle = 0.2 const fogColor = d.vec3f(0.05, 0.2, 0.7) /** The ambient light color */ const ambientColor = d.vec3f(0.2, 0.5, 1) -const tileDensity = 2 +/** Color tint of the god rays */ +const godRayTint = d.vec3f(0.18, 0.3, 0.5) +const tileDensity = 3 const layout = tgpu.bindGroupLayout({ aspect: { uniform: d.f32 }, @@ -64,8 +64,6 @@ const caustics = tgpu.fn( return std.pow(d.vec3f(1 - noise), profile) }) -const clamp01 = tgpu.fn([d.f32], d.f32)((v) => std.clamp(v, 0, 1)) - /** * Returns a transformation matrix that represents an `angle` rotation * in the XY plane (around the imaginary Z axis) @@ -122,7 +120,6 @@ const mainFragment = tgpu["~unstable"].fragmentFn({ // -- GOD RAYS -- const godRayUv = rotateXY(-0.3).mul(uv).mul(d.vec2f(10, 2)) - const godRayTint = d.vec3f(0.18, 0.3, 0.5) const godRay1 = perlin3d.sample(d.vec3f(godRayUv, time * 0.5)) + 1 const godRay2 = perlin3d.sample(d.vec3f(godRayUv.mul(2), time * 0.3)) + 1 const godRayBlend = std.pow(uv.y, 2) * 0.5 @@ -143,7 +140,7 @@ export async function run(renderer: CliRenderer): Promise { const WIDTH = renderer.terminalWidth const HEIGHT = renderer.terminalHeight - parentContainer = new GroupRenderable("fractal-container", { + parentContainer = new GroupRenderable("shader-container", { x: 0, y: 0, zIndex: 10, @@ -228,7 +225,7 @@ export function destroy(renderer: CliRenderer): void { root?.destroy() if (parentContainer) { - renderer.remove("fractal-container") + renderer.remove("shader-container") parentContainer = undefined } } diff --git a/src/examples/index.ts b/src/examples/index.ts index 64c32750d..eda225dc0 100644 --- a/src/examples/index.ts +++ b/src/examples/index.ts @@ -13,6 +13,7 @@ import { } from "../index" import { renderFontToFrameBuffer, measureText } from "../ui/ascii.font" import * as causticsDemo from "./caustics-demo" +import * as lavalampDemo from "./lavalamp-demo" import * as boxExample from "./fonts" import * as fractalShaderExample from "./fractal-shader-demo" import * as framebufferExample from "./framebuffer-demo" @@ -47,13 +48,6 @@ interface Example { } const examples: Example[] = [ - // TODO: Move to be after the Fractal shader - { - name: "Caustics Shader", - description: "Caustics in a fragment shader", - run: causticsDemo.run, - destroy: causticsDemo.destroy, - }, { name: "Mouse Interaction Demo", description: "Interactive mouse trails and clickable cells demonstration", @@ -156,6 +150,18 @@ const examples: Example[] = [ run: fractalShaderExample.run, destroy: fractalShaderExample.destroy, }, + { + name: "Caustics Shader", + description: "Caustics in a fragment shader", + run: causticsDemo.run, + destroy: causticsDemo.destroy, + }, + { + name: "Lava Lamp Shader", + description: "Lava Lamp effect in a fragment shader", + run: lavalampDemo.run, + destroy: lavalampDemo.destroy, + }, { name: "Phong Lighting", description: "Phong lighting model demo", diff --git a/src/examples/lavalamp-demo.ts b/src/examples/lavalamp-demo.ts new file mode 100644 index 000000000..43a4e060c --- /dev/null +++ b/src/examples/lavalamp-demo.ts @@ -0,0 +1,226 @@ +#!/usr/bin/env bun + +import { perlin3d } from "@typegpu/noise" +import { createWebGPUDevice, setupGlobals } from "bun-webgpu" +import tgpu, { type TgpuRoot } from "typegpu" +import * as d from "typegpu/data" +import { abs, mix, pow, sign, tanh } from "typegpu/std" +import { CLICanvas, type CliRenderer, GroupRenderable, SuperSampleType, TextRenderable } from "../index" + +/** The size of the perlin noise (in time), after which the pattern loops around */ +const domainDepth = 10 +/** The size of the perlin noise (in space) */ +const domainSize = 10 +/** With supersampling, the scene is rendered at 2x the resolution */ +const pixelRatio = 2 + +const fullScreenTriangle = tgpu["~unstable"].vertexFn({ + in: { vertexIndex: d.builtin.vertexIndex }, + out: { pos: d.builtin.position, uv: d.vec2f }, +})((input) => { + const pos = [d.vec2f(-1, -1), d.vec2f(3, -1), d.vec2f(-1, 3)] + + return { + pos: d.vec4f(pos[input.vertexIndex], 0.0, 1.0), + uv: pos[input.vertexIndex].mul(0.5), + } +}) + +const aspectAccess = tgpu["~unstable"].accessor(d.f32); +const timeAccess = tgpu["~unstable"].accessor(d.f32) +const sharpnessAccess = tgpu["~unstable"].accessor(d.f32) + +const exponentialSharpen = tgpu.fn( + [d.f32, d.f32], + d.f32, +)((n, sharpness) => { + return sign(n) * pow(abs(n), 1 - sharpness) +}) + +const tanhSharpen = tgpu.fn( + [d.f32, d.f32], + d.f32, +)((n, sharpness) => { + return tanh(n * (1 + sharpness * 10)) +}) + +/** The method to use for sharpening. Can be swapped at pipeline creation */ +const sharpenFnSlot = tgpu.slot(exponentialSharpen) + +const mainFragment = tgpu["~unstable"].fragmentFn({ + in: { uv: d.vec2f }, + out: d.vec4f, +})((input) => { + const uv = input.uv.mul(domainSize * 0.5).mul(d.vec2f(aspectAccess.$, 1.5)); + const n = perlin3d.sample(d.vec3f(uv, timeAccess.$ * 0.2)) + + // Apply sharpening function + const sharp = sharpenFnSlot.$(n, sharpnessAccess.$) + + // Map to 0-1 range + const n01 = sharp * 0.5 + 0.5 + + // Gradient map + const dark = d.vec3f(0, 0.2, 1) + const light = d.vec3f(1, 0.3, 0.5) + return d.vec4f(mix(dark, light, n01), 1) +}) + +let isRunning = true +let activeSharpenFn: "exponential" | "tanh" = "exponential" +let root: TgpuRoot | undefined +let keyHandler: ((key: Buffer) => void) | undefined +let handleResize: ((width: number, height: number) => void) | undefined +let parentContainer: GroupRenderable | undefined + +export async function run(renderer: CliRenderer): Promise { + isRunning = true + renderer.start() + const WIDTH = renderer.terminalWidth + const HEIGHT = renderer.terminalHeight + + parentContainer = new GroupRenderable("shader-container", { + x: 0, + y: 0, + zIndex: 10, + visible: true, + }) + renderer.add(parentContainer) + + const controlsText = new TextRenderable("demo_controls", { + content: "S: Toggle Sharpening Method | +/-: Sharpness | Escape: Back to menu", + x: 0, + y: HEIGHT - 2, + fg: "#FFFFFF", + zIndex: 20, + }) + parentContainer.add(controlsText) + + const statusText = new TextRenderable("demo_status", { + content: "Sharpening: exponential", + x: 0, + y: 0, + fg: "#FFFFFF", + zIndex: 20, + }) + parentContainer.add(statusText) + + // Bun WebGPU setup + setupGlobals() + const device = await createWebGPUDevice() + const canvas = new CLICanvas(device, WIDTH * pixelRatio, HEIGHT * pixelRatio, SuperSampleType.GPU) + + root = tgpu.initFromDevice({ device }) + // Assuming a format... + const presentationFormat = "rgba8unorm" as const + + /** Contains all resources that the perlin cache needs access to */ + const perlinCache = perlin3d.staticCache({ root, size: d.vec3u(domainSize, domainSize, domainDepth) }) + + const aspect = root.createUniform(d.f32, WIDTH / HEIGHT); + const time = root.createUniform(d.f32, 0) + const sharpness = root.createUniform(d.f32, 0.5) + + const renderPipelineBase = root["~unstable"] + .with(aspectAccess, aspect) + .with(timeAccess, time) + .with(sharpnessAccess, sharpness) + .pipe(perlinCache.inject()) + + const renderPipelines = { + exponential: renderPipelineBase + .with(sharpenFnSlot, exponentialSharpen) + .withVertex(fullScreenTriangle, {}) + .withFragment(mainFragment, { format: presentationFormat }) + .createPipeline(), + tanh: renderPipelineBase + .with(sharpenFnSlot, tanhSharpen) + .withVertex(fullScreenTriangle, {}) + .withFragment(mainFragment, { format: presentationFormat }) + .createPipeline(), + } + + handleResize = (width: number, height: number) => { + canvas.setSize(width * pixelRatio, height * pixelRatio) + aspect.write(width / height); + controlsText.y = height - 2; + } + + renderer.on("resize", handleResize) + + const context = canvas.getContext("webgpu") as GPUCanvasContext + + context.configure({ + device: root.device, + format: presentationFormat, + alphaMode: "premultiplied", + }) + + let timeAcc = 0 + let sharpnessCpu = 0.5 + + const updateStatusText = () => { + statusText.content = `Method: ${activeSharpenFn}, Sharpness: ${sharpnessCpu.toFixed(1)}` + } + updateStatusText(); + + keyHandler = (key: Buffer) => { + const keyStr = key.toString() + + if (keyStr === "s") { + activeSharpenFn = activeSharpenFn === "exponential" ? "tanh" : "exponential" + } + + if (keyStr === "+" || keyStr === "=") { + sharpnessCpu = Math.min(sharpnessCpu + 0.1, 1) + sharpness.write(sharpnessCpu) + } + + if (keyStr === "-" || keyStr === "_") { + sharpnessCpu = Math.max(sharpnessCpu - 0.1, 0) + sharpness.write(sharpnessCpu) + } + + updateStatusText() + } + + process.stdin.on("data", keyHandler) + + renderer.setFrameCallback(async (deltaMs) => { + if (!isRunning) return + + timeAcc += deltaMs / 1000 + time.write(timeAcc) + + renderPipelines[activeSharpenFn] + .withColorAttachment({ + view: context.getCurrentTexture().createView(), + loadOp: "clear", + storeOp: "store", + }) + .draw(3) + + await canvas.readPixelsIntoBuffer(renderer.nextRenderBuffer) + }) +} + +export function destroy(renderer: CliRenderer): void { + isRunning = false + if (keyHandler) { + process.stdin.off("data", keyHandler) + keyHandler = undefined + } + + if (handleResize) { + renderer.off("resize", handleResize) + handleResize = undefined + } + + renderer.clearFrameCallbacks() + root?.destroy() + + if (parentContainer) { + renderer.remove("shader-container") + parentContainer = undefined + } +} From c0a1577dc30022249701b2ac6ec70d68b1c7427f Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Mon, 4 Aug 2025 19:06:53 +0200 Subject: [PATCH 6/7] Use officially released TypeGPU packages --- bun.lock | 12 ++++++------ package.json | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bun.lock b/bun.lock index bc92e3817..63affbc27 100644 --- a/bun.lock +++ b/bun.lock @@ -4,12 +4,12 @@ "": { "name": "@opentui/core", "dependencies": { - "@typegpu/noise": "https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/noise@f71da6517c202229d3da92ec645b641bf6417816", + "@typegpu/noise": "^0.1.0", "bun-webgpu": "0.1.0", "jimp": "1.6.0", "three": "0.177.0", - "typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/typegpu@f71da6517c202229d3da92ec645b641bf6417816", - "unplugin-typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@f71da6517c202229d3da92ec645b641bf6417816", + "typegpu": "^0.7.0", + "unplugin-typegpu": "^0.2.2", "yoga-layout": "3.2.1", }, "devDependencies": { @@ -94,7 +94,7 @@ "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], - "@typegpu/noise": ["@typegpu/noise@https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/noise@f71da6517c202229d3da92ec645b641bf6417816", { "peerDependencies": { "typegpu": "^0.6.0" } }], + "@typegpu/noise": ["@typegpu/noise@0.1.0", "", { "peerDependencies": { "typegpu": "^0.6.0" } }, "sha512-94CQfZhsszv/FEsKdhswa5yWTM0VQIuum4myNuPSDyHKyJfE86xnJPF8LvNPhmYOPkMpY2czyKFVh0CG90fEfw=="], "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], @@ -220,7 +220,7 @@ "typed-binary": ["typed-binary@4.3.2", "", {}, "sha512-HT3pIBM2njCZUmeczDaQUUErGiM6GXFCqMsHegE12HCoBtvHCkfR10JJni0TeGOTnLilTd6YFyj+YhflqQDrDQ=="], - "typegpu": ["typegpu@https://pkg.pr.new/software-mansion/TypeGPU/typegpu@f71da6517c202229d3da92ec645b641bf6417816", { "dependencies": { "tinyest": "~0.1.1", "typed-binary": "^4.3.1" } }], + "typegpu": ["typegpu@0.7.0", "", { "dependencies": { "tinyest": "~0.1.1", "typed-binary": "^4.3.1" } }, "sha512-BueQ/74zgUCwqg/nmSxZ2aL7NFnv3jzANlUmgyKbGtKg0jkCu9kUAVtPjwdjWDmlah5WlzAAXi7qD5URlU6wtA=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], @@ -228,7 +228,7 @@ "unplugin": ["unplugin@2.3.5", "", { "dependencies": { "acorn": "^8.14.1", "picomatch": "^4.0.2", "webpack-virtual-modules": "^0.6.2" } }, "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw=="], - "unplugin-typegpu": ["unplugin-typegpu@https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@f71da6517c202229d3da92ec645b641bf6417816", { "dependencies": { "@babel/standalone": "^7.27.0", "defu": "^6.1.4", "estree-walker": "^3.0.3", "magic-string-ast": "^1.0.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "tinyest": "~0.1.1", "tinyest-for-wgsl": "~0.1.2", "unplugin": "^2.3.5" }, "peerDependencies": { "typegpu": "^0.6.0" } }], + "unplugin-typegpu": ["unplugin-typegpu@0.2.2", "", { "dependencies": { "@babel/standalone": "^7.27.0", "defu": "^6.1.4", "estree-walker": "^3.0.3", "magic-string-ast": "^1.0.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "tinyest": "~0.1.1", "tinyest-for-wgsl": "~0.1.2", "unplugin": "^2.3.5" }, "peerDependencies": { "typegpu": "^0.7.0" } }, "sha512-5pbwv0cTMRMxRCQXEPAMUsUoAKMQsz2B4zlhpRRHNy3qtMk+rP3Uxe/5zZUtr2yAASCVO2iciblH5HqcUoijjQ=="], "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="], diff --git a/package.json b/package.json index 8954b70b1..147ad0e1d 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,12 @@ "printWidth": 120 }, "dependencies": { - "@typegpu/noise": "https://pkg.pr.new/software-mansion/TypeGPU/@typegpu/noise@f71da6517c202229d3da92ec645b641bf6417816", + "@typegpu/noise": "^0.1.0", "bun-webgpu": "0.1.0", "jimp": "1.6.0", "three": "0.177.0", - "typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/typegpu@f71da6517c202229d3da92ec645b641bf6417816", - "unplugin-typegpu": "https://pkg.pr.new/software-mansion/TypeGPU/unplugin-typegpu@f71da6517c202229d3da92ec645b641bf6417816", + "typegpu": "^0.7.0", + "unplugin-typegpu": "^0.2.2", "yoga-layout": "3.2.1" } } From b3f7ddd9ac7e9ea32e922049f83c62b62ac539c7 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Mon, 4 Aug 2025 20:04:05 +0200 Subject: [PATCH 7/7] Cleanup --- bunfig.toml | 2 +- preload.ts | 8 ++++++++ typegpu-plugin.ts | 8 -------- 3 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 preload.ts delete mode 100644 typegpu-plugin.ts diff --git a/bunfig.toml b/bunfig.toml index 48a7fe895..7d029e77c 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1 +1 @@ -preload = ["./typegpu-plugin.ts"] +preload = ["./preload.ts"] diff --git a/preload.ts b/preload.ts new file mode 100644 index 000000000..e31488ffc --- /dev/null +++ b/preload.ts @@ -0,0 +1,8 @@ +import { plugin } from "bun" +import typegpu from "unplugin-typegpu/bun" + +plugin( + typegpu({ + include: /\.ts$/, + }), +) diff --git a/typegpu-plugin.ts b/typegpu-plugin.ts deleted file mode 100644 index 0bc74fa35..000000000 --- a/typegpu-plugin.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { plugin } from "bun" -import { bunPlugin } from "unplugin-typegpu" - -plugin( - bunPlugin({ - include: /\.m?[t]sx?$/, - }), -)