import { YawnCore } from "@yawn/core"; import { cameraBasis } from "./math.js"; function wire(v) { if (Array.isArray(v)) return `(array ${v.map(wire).join(" ")})`; if (v && typeof v === "object") return `(object ${Object.entries(v) .map(([k, x]) => `(field ${JSON.stringify(k)} ${wire(x)})`) .join(" ")})`; return JSON.stringify(v); } const shader = ` struct Camera { right:vec4f, up:vec4f, forward:vec4f, center:vec4f, color:vec4f } @group(0) @binding(0) var camera:Camera; struct Out { @builtin(position) position:vec4f, @location(0) normal:vec3f } @vertex fn vertex(@location(0) p:vec3f, @location(1) n:vec3f)->Out { let d=p-camera.center.xyz; var o:Out; o.position=vec4f(dot(d,camera.right.xyz)/camera.right.w,dot(d,camera.up.xyz)/camera.up.w,0.5-dot(d,camera.forward.xyz)/32.0,1.0); o.normal=vec3f(dot(n,camera.right.xyz),dot(n,camera.up.xyz),dot(n,camera.forward.xyz));return o; } @fragment fn fragment(o:Out)->@location(0) vec4f { let n=normalize(o.normal); let key=max(dot(n,normalize(vec3f(-0.5,0.8,1.0))),0.0); let fill=max(dot(n,normalize(vec3f(0.8,0.0,0.4))),0.0); let rim=pow(1.0-max(n.z,0.0),3.0); let spec=pow(max(dot(n,normalize(vec3f(-0.3,0.45,1.0))),0.0),48.0); let color=camera.color.xyz*(0.24+key*0.65+fill*0.16)+vec3f(0.12,0.14,0.16)*rim+vec3f(0.12)*spec; return vec4f(pow(color,vec3f(1.0/2.2)),1.0); }`; export class Renderer { static async create(canvas, mesh, camera, quality = "balanced") { const rect = canvas.getBoundingClientRect(); const pixels = quality === "economy" ? 600000 : quality === "detail" ? 2000000 : 1000000; const scale = Math.min( devicePixelRatio, quality === "economy" ? 1 : quality === "detail" ? 2 : 1.5, Math.sqrt(pixels / (rect.width * rect.height)), ); canvas.width = Math.max(1, Math.round(rect.width * scale)); canvas.height = Math.max(1, Math.round(rect.height * scale)); const r = new Renderer(); r.canvas = canvas; r.mesh = mesh; const bytes = (mesh.positions.length / 3) * 32 + Math.ceil(mesh.indices.length / 4) * 16 + 80 + 64 + 256; if (bytes > 8 * 1024 * 1024) throw new Error("Mesh exceeds the render arena"); r.core = new YawnCore(canvas, { arenaBytes: 8 * 1024 * 1024 }); let timedOut = false; const timeout = setTimeout(() => { timedOut = true; r.dispose(); }, 20000); try { await r.core.ready; [r.vertices, r.indices, r.camera, r.sample] = await r.core.createRowsBatch([ { name: "sculptVertices", rows: mesh.positions.length / 3, stride: 32, format: "f32", }, { name: "sculptIndices", rows: Math.ceil(mesh.indices.length / 4), stride: 16, format: "u32", }, { name: "sculptCamera", rows: 1, stride: 80, format: "f32" }, { name: "sculptSample", rows: 1, stride: 64, format: "i32" }, ]); r.sample.view.fill(0); r.indices.view.set(mesh.indices); r.writeVertices(0, mesh.positions.length / 3); r.writeCamera(camera); const graph = { id: "sculpt", resources: { buffers: [ { id: "vertices", array: "sculptVertices", usage: ["vertex"], sync: "manual", }, { id: "indices", array: "sculptIndices", usage: ["index"], sync: "manual", }, { id: "camera", array: "sculptCamera", usage: ["uniform"], sync: "manual", }, ], textures: [ { id: "depth", format: "depth24plus", size: ["canvas", "canvas"], usage: ["render"], }, ], }, pipelines: { render: [ { id: "clay", code: shader, vertex: { buffers: [ { arrayStride: 32, attributes: [ { format: "float32x3", offset: 0, shaderLocation: 0 }, { format: "float32x3", offset: 16, shaderLocation: 1 }, ], }, ], }, fragment: { targets: [{ format: "canvas" }] }, primitive: { topology: "triangle-list", frontFace: "ccw", cullMode: "back", conservative: false, }, depthStencil: { format: "depth24plus", depth_write_enabled: true, depth_compare: "less", }, }, ], }, passes: [ { id: "sculpt", type: "render", pipeline: "clay", bindings: [{ binding: 0, resource: "camera" }], color: [{ resource: "canvas", clear: [0.12, 0.14, 0.17, 1] }], depth: { resource: "depth" }, vertexBuffers: [{ resource: "vertices" }], indexBuffer: { resource: "indices" }, draw: { indices: mesh.indices.length, instances: 1 }, }, ], }; await r.core.switchLoadout( await r.core.compileGraph(`(yawn-graph 1 ${wire(graph)})`), ); await r.core.setFps(quality === "economy" ? 30 : 60); await r.core.play(); return r; } catch (error) { r.dispose(); if (timedOut) throw new Error("Renderer initialization timed out"); throw error; } finally { clearTimeout(timeout); } } writeVertices(first, count) { const p = this.mesh.positions, n = this.mesh.normals, v = this.vertices.view; for (let i = first; i < first + count; i++) { const a = i * 3, b = i * 8; v[b] = p[a]; v[b + 1] = p[a + 1]; v[b + 2] = p[a + 2]; v[b + 4] = n[a]; v[b + 5] = n[a + 1]; v[b + 6] = n[a + 2]; } } writeCamera(camera) { const { right, up, forward } = cameraBasis(camera.yaw, camera.pitch); const aspect = this.canvas.width / this.canvas.height; this.camera.view.set([ ...right, camera.zoom * aspect, ...up, camera.zoom, ...forward, 0, ...camera.target, 0, ...camera.color, 0, ]); } // One acknowledged publication at a time: shared staging bytes are immutable in flight. async publish(engine, camera) { if (this.busy || this.disposed) return false; this.busy = true; const timeout = setTimeout(() => this.dispose(), 10000); try { let first = Infinity, last = -1; for (const v of engine.dirty) { first = Math.min(first, v); last = Math.max(last, v); } engine.dirty.clear(); if (last >= first) { this.writeVertices(first, last - first + 1); await this.core.syncRows("sculptVertices", first, last - first + 1); } if (this.disposed) return false; this.writeCamera(camera); await this.core.syncRows("sculptCamera", 0, 1); return true; } finally { clearTimeout(timeout); this.busy = false; } } dispose() { this.disposed = true; this.processor?.dispose(); this.core?.dispose(); } }