parallelize widget shadow generation 2x faster

This commit is contained in:
Stewart Allen 2026-01-03 19:43:51 -05:00
commit f81e002766
5 changed files with 74 additions and 16 deletions

View file

@ -1,5 +1,6 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { decode } from '../core/codec.js';
import { util as mesh_util } from '../../mesh/util.js';
import { tool as mesh_tool } from '../../mesh/tool.js';
import { verticesToPoints } from '../../geo/points.js';
@ -12,6 +13,7 @@ const sharedArrayClass = self.SharedArrayBuffer || undefined;
const hasSharedArrays = sharedArrayClass ? true : false;
const solid_opacity = 1.0;
const groups = [];
const debug_shadow = false;
let nextId = 0;
@ -739,8 +741,9 @@ class Widget {
// the stacks are then used to produce shadowlines
#ensureShadowCache() {
if (this.cache.shadow) {
return;
return this.cache.shadow;
}
if (debug_shadow) console.time('shadow buckets');
const geo = this.getGeoVertices({ unroll: true, translate: true });
const length = geo.length;
const bounds = this.getBoundingBox();
@ -762,15 +765,54 @@ class Widget {
stack[z].push(a, b, c);
}
}
this.cache.shadow = stack;
if (debug_shadow) console.timeEnd('shadow buckets');
return this.cache.shadow = stack;
}
async computeShadowStack(zlist, progress) {
let shadow_stack = this.cache.shadow_stack;
if (!shadow_stack) {
shadow_stack = this.cache.shadow_stack = {};
}
let work = self.kiri_worker;
let stack = this.#ensureShadowCache();
let plist = [];
if (debug_shadow) console.time('seed minion buckets');
work.minions.broadcast('cam_shadow_stack', stack);
if (debug_shadow) console.timeEnd('seed minion buckets');
let pinc = 1 / zlist.length;
let pval = 0;
for (let z of zlist) {
let p = work.minions.queueAsync({
cmd: 'cam_shadow_z',
z: z - 0.005,
t: z + 1
}).then(reply => {
shadow_stack[z - 0.005] = decode(reply.data);
pval += pinc;
progress(pval);
});
plist.push(p);
}
await Promise.all(plist);
work.minions.broadcast('cam_shadow_stack', { clear: true });
}
computeShadowAtZ(z, ztop, cached) {
return this.#computeShadowAt(z, ztop, cached);
}
// union triangles > z (opt cap < ztop) into polygon(s)
// slice the triangle stack matchingg z then union the results
#computeShadowAt(z, ztop) {
this.#ensureShadowCache();
#computeShadowAt(z, ztop, cached) {
let shadow_stack = this.cache.shadow_stack;
if (shadow_stack && shadow_stack[z]) {
return shadow_stack[z];
}
let label = `compute shadow ${z.round(2)}`;
if (debug_shadow) console.time(label);
const found = [];
const stack = this.cache.shadow;
const stack = cached ?? this.#ensureShadowCache();
let minZ = Math.floor(z);
let maxZ = Math.ceil(z);
let slices = [];
@ -832,6 +874,7 @@ class Widget {
polys = POLY.offset(polys, 0.01);
polys = POLY.offset(polys, -0.01);
if (debug_shadow) console.timeEnd(label);
return polys;
}
}

View file

@ -8,10 +8,10 @@ export class OpIndex extends CamOp {
}
weight() {
return 0.1;
return 3;
}
async slice() {
async slice(progress) {
let { op, state } = this;
if (!state.isIndexed) {
@ -37,7 +37,7 @@ export class OpIndex extends CamOp {
updateSlicer();
// recompute shadow from new widget geometry
await computeShadows();
await computeShadows(progress);
}
prepare(ops, progress) {

View file

@ -22,7 +22,7 @@ class OpShadow extends CamOp {
}
weight() {
return 0.1;
return 3;
}
async slice(progress) {
@ -50,6 +50,9 @@ class OpShadow extends CamOp {
tzindex = [ ];
}
// distributed pre-fill shadow layer cache
await widget.computeShadowStack(tzindex, prog => progress(prog / 2, 'shadow'));
// terrain is the "shadow stack" where index 0 = top of part
// thus array.length -1 = bottom of part
for (let i=0; i<tzindex.length; i++) {
@ -65,7 +68,7 @@ class OpShadow extends CamOp {
.setLayer("shadow", {line: 0x888800, thin: true })
.addPolys(shadow, { thin: true });
}
progress(i / tzindex.length);
progress(0.5 + i / tzindex.length, 'shadow');
}
if (devel && slices.length) {

View file

@ -261,12 +261,10 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
return slicer = state.slicer = new cam_slicer(widget, opts);
}
async function computeShadows() {
console.time('computeShadows');
await new OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => {
// console.log('reshadowing', progress.round(3));
});
console.timeEnd('computeShadows');
async function computeShadows(progress) {
console.time('compute shadow');
await new OPS.shadow(state, { type: "shadow", xsilent: true }).slice(progress);
console.timeEnd('compute shadow');
}
function setToolDiam(toolDiam) {

View file

@ -10,6 +10,7 @@ import { base } from '../../geo/base.js';
import { codec, encode, encodePointArray } from '../core/codec.js';
import { doTopShells } from '../mode/fdm/post.js';
import { newPoint } from '../../geo/point.js';
import { newWidget } from '../core/widget.js';
import { polygons as POLY } from '../../geo/polygons.js';
import { sliceZ, sliceConnect } from '../../geo/slicer.js';
import { Slicer as cam_slicer } from '../mode/cam/slicer_cam.js';
@ -317,5 +318,18 @@ const funcs = self.minion = {
const topo4 = Object.assign(new Topo4(), cache.lathe);
const heights = topo4.lathePath(stmp, tool);
reply({ seq, heights });
},
// cam widget shadow
cam_shadow_stack(data, seq) {
cache.shadow_stack = data.clear ? undefined : data;
},
cam_shadow_z(data, seq) {
let polys = newWidget().computeShadowAtZ(data.z, data.t, cache.shadow_stack);
let coded = codec.encode(polys);
reply({ seq, data: coded });
}
};