Add core-powered web sculpting studio with tablet controls
Add pressure-aware worker sculpting, bounded shared input, project files and history, and compact icon navigation with an XYZ orbit gizmo. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83 Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
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<uniform> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user