diff --git a/wasm-demo/index.html b/wasm-demo/index.html index b9b89400..353cfb27 100644 --- a/wasm-demo/index.html +++ b/wasm-demo/index.html @@ -1,44 +1,44 @@ + + Fidget Demo -
@@ -49,6 +49,11 @@
Loading... +
diff --git a/wasm-demo/index.ts b/wasm-demo/index.ts index e1837442..9d3ac476 100644 --- a/wasm-demo/index.ts +++ b/wasm-demo/index.ts @@ -5,11 +5,12 @@ import { EditorState } from "@codemirror/state"; import { defaultKeymap } from "@codemirror/commands"; import { + RenderMode, ResponseKind, ScriptRequest, + ScriptResponse, ShapeRequest, StartRequest, - ScriptResponse, WorkerRequest, WorkerResponse, } from "./message"; @@ -53,12 +54,39 @@ class App { }; this.workers.push(worker); } + + // Also re-render if the mode changes + const select = document.getElementById("mode"); + select.addEventListener("change", this.onModeChanged.bind(this), false); + } + + onModeChanged() { + const text = this.editor.view.state.doc.toString(); + this.onScriptChanged(text); } onScriptChanged(text: string) { this.workers[0].postMessage(new ScriptRequest(text)); } + getMode() { + const e = document.getElementById("mode") as HTMLSelectElement; + switch (e.value) { + case "bitmap": { + return RenderMode.Bitmap; + } + case "normals": { + return RenderMode.Normals; + } + case "heightmap": { + return RenderMode.Heightmap; + } + default: { + return null; + } + } + } + onWorkerMessage(i: number, req: WorkerResponse) { switch (req.kind) { case ResponseKind.Image: { @@ -90,8 +118,9 @@ class App { if (r.tape) { this.start_time = performance.now(); this.workers_done = 0; + const mode = this.getMode(); this.workers.forEach((w) => { - w.postMessage(new ShapeRequest(r.tape)); + w.postMessage(new ShapeRequest(r.tape, mode)); }); } break; diff --git a/wasm-demo/message.ts b/wasm-demo/message.ts index 374dfeab..ed67196e 100644 --- a/wasm-demo/message.ts +++ b/wasm-demo/message.ts @@ -24,13 +24,21 @@ export class StartRequest { } } +export enum RenderMode { + Bitmap, + Heightmap, + Normals, +} + export class ShapeRequest { kind: RequestKind.Shape; tape: Uint8Array; + mode: RenderMode; - constructor(tape: Uint8Array) { + constructor(tape: Uint8Array, mode: RenderMode) { this.tape = tape; this.kind = RequestKind.Shape; + this.mode = mode; } } diff --git a/wasm-demo/src/lib.rs b/wasm-demo/src/lib.rs index 05f61091..3ada219a 100644 --- a/wasm-demo/src/lib.rs +++ b/wasm-demo/src/lib.rs @@ -46,7 +46,7 @@ pub fn deserialize_tape(data: Vec) -> Result { /// The image has a total size of `image_size` (on each side) and is divided /// into `0 <= pos < workers_per_side^2` tiles. #[wasm_bindgen] -pub fn render_region( +pub fn render_region_2d( shape: JsVmShape, image_size: usize, index: usize, @@ -102,3 +102,129 @@ pub fn render_region( inner(shape.0, image_size, index, workers_per_side) .map_err(|e| format!("{e}")) } + +/// Renders a subregion of a heightmap, for webworker-based multithreading +/// +/// The image has a total size of `image_size` (on each side) and is divided +/// into `0 <= pos < workers_per_side^2` tiles. +#[wasm_bindgen] +pub fn render_region_heightmap( + shape: JsVmShape, + image_size: usize, + index: usize, + workers_per_side: usize, +) -> Result, String> { + if index >= workers_per_side.pow(2) { + return Err("invalid index".to_owned()); + } + if image_size % workers_per_side != 0 { + return Err( + "image_size must be divisible by workers_per_side".to_owned() + ); + } + let (depth, _norm) = + render_3d_inner(shape.0, image_size, index, workers_per_side) + .map_err(|e| format!("{e}"))?; + + // Convert into an image + Ok(depth + .into_iter() + .flat_map(|v| { + let d = (v as usize * 255 / image_size) as u8; + [d, d, d, 255] + }) + .collect()) +} + +/// Renders a subregion with normals, for webworker-based multithreading +/// +/// The image has a total size of `image_size` (on each side) and is divided +/// into `0 <= pos < workers_per_side^2` tiles. +#[wasm_bindgen] +pub fn render_region_normals( + shape: JsVmShape, + image_size: usize, + index: usize, + workers_per_side: usize, +) -> Result, String> { + if index >= workers_per_side.pow(2) { + return Err("invalid index".to_owned()); + } + if image_size % workers_per_side != 0 { + return Err( + "image_size must be divisible by workers_per_side".to_owned() + ); + } + let (_depth, norm) = + render_3d_inner(shape.0, image_size, index, workers_per_side) + .map_err(|e| format!("{e}"))?; + + // Convert into an image + Ok(norm + .into_iter() + .flat_map(|[r, g, b]| [r, g, b, 255]) + .collect()) +} + +fn render_3d_inner( + shape: VmShape, + image_size: usize, + index: usize, + workers_per_side: usize, +) -> Result<(Vec, Vec<[u8; 3]>), Error> { + let mut current_depth = vec![]; + let mut current_norm = vec![]; + + // Work from front to back, so we can bail out early if the image is full + for z in (0..workers_per_side).rev() { + // Corner position in [0, workers_per_side] coordinates + let mut corner = nalgebra::Vector3::new( + index / workers_per_side, + index % workers_per_side, + z, + ) + .cast::(); + // Corner position in [-1, 1] coordinates + corner = (corner * 2.0 / workers_per_side as f32).add_scalar(-1.0); + + // Scale of each tile + let scale = 2.0 / workers_per_side as f32; + + // Tile center + let center = corner.add_scalar(scale / 2.0); + + let cfg = RenderConfig::<3> { + image_size: image_size / workers_per_side, + bounds: Bounds { + center, + size: scale / 2.0, + }, + ..RenderConfig::default() + }; + + // Special case for the first tile, which can be copied over + let (mut depth, norm) = cfg.run(shape.clone())?; + for d in &mut depth { + if *d > 0 { + *d += (z * image_size / workers_per_side) as u32; + } + } + if current_depth.is_empty() { + current_depth = depth; + current_norm = norm; + } else { + let mut all = true; + for i in 0..depth.len() { + if depth[i] > current_depth[i] { + current_depth[i] = depth[i]; + current_norm[i] = norm[i]; + } + all &= current_depth[i] == 0; + } + if all { + break; + } + } + } + Ok((current_depth, current_norm)) +} diff --git a/wasm-demo/worker.ts b/wasm-demo/worker.ts index 0c53f701..0cceec16 100644 --- a/wasm-demo/worker.ts +++ b/wasm-demo/worker.ts @@ -1,5 +1,6 @@ import { ImageResponse, + RenderMode, RequestKind, ScriptRequest, ScriptResponse, @@ -23,12 +24,36 @@ class Worker { render(s: ShapeRequest) { const shape = fidget.deserialize_tape(s.tape); - const out = fidget.render_region( - shape, - RENDER_SIZE, - this.index, - WORKERS_PER_SIDE, - ); + let out: Uint8Array; + switch (s.mode) { + case RenderMode.Bitmap: { + out = fidget.render_region_2d( + shape, + RENDER_SIZE, + this.index, + WORKERS_PER_SIDE, + ); + break; + } + case RenderMode.Heightmap: { + out = fidget.render_region_heightmap( + shape, + RENDER_SIZE, + this.index, + WORKERS_PER_SIDE, + ); + break; + } + case RenderMode.Normals: { + out = fidget.render_region_normals( + shape, + RENDER_SIZE, + this.index, + WORKERS_PER_SIDE, + ); + break; + } + } postMessage(new ImageResponse(out), { transfer: [out.buffer] }); }