Rework garden simulation pipelines

This commit is contained in:
Andras Schmelczer 2026-05-24 10:58:11 +01:00
commit 018f8c9d4d
67 changed files with 6499 additions and 1682 deletions

View file

@ -0,0 +1,162 @@
import { vec2 } from 'gl-matrix';
import { appConfig } from '../config';
import { ERASER_MASK_TEXTURE_FORMAT } from '../pipelines/texture-formats';
import {
ResizableTexture,
type PendingTextureResize,
} from '../utils/graphics/resizable-texture';
export class SimulationTextures {
// trailMapA holds the current trail (read by agent and diffuse). trailMapB
// receives the diffuse output; the two swap each frame so the freshly
// diffused texture becomes trailMapA for the next frame.
public trailMapA: ResizableTexture;
public trailMapB: ResizableTexture;
// Per-frame last-writer deposit map: cleared each frame, written sparsely by
// agents, then read by diffuse alongside trailMapA.
public readonly depositMap: ResizableTexture;
public readonly eraserMask: ResizableTexture;
public sourceMapA: ResizableTexture;
public sourceMapB: ResizableTexture;
public constructor(
private readonly device: GPUDevice,
canvasSize: vec2
) {
this.trailMapA = this.createTexture(canvasSize);
this.trailMapB = this.createTexture(canvasSize);
this.depositMap = this.createTexture(canvasSize);
this.sourceMapA = this.createTexture(canvasSize);
this.sourceMapB = this.createTexture(canvasSize);
this.eraserMask = this.createEraserMask(canvasSize);
}
public resizeTo(nextSize: vec2): vec2 | null {
const previousSize = this.trailMapA.getSize();
if (vec2.equals(previousSize, nextSize)) {
return null;
}
const scale = vec2.div(vec2.create(), nextSize, previousSize);
const resizes = [
this.trailMapA,
this.trailMapB,
this.depositMap,
this.sourceMapA,
this.sourceMapB,
this.eraserMask,
]
.map((texture): [ResizableTexture, PendingTextureResize] | null => {
const resize = texture.prepareResize(nextSize);
return resize ? [texture, resize] : null;
})
.filter((resize): resize is [ResizableTexture, PendingTextureResize] =>
Boolean(resize)
);
if (resizes.length > 0) {
const commandEncoder = this.device.createCommandEncoder();
resizes.forEach(([texture, resize]) => {
texture.encodeResize(commandEncoder, resize);
});
this.device.queue.submit([commandEncoder.finish()]);
resizes.forEach(([texture, resize]) => {
texture.commitResize(resize);
});
}
return scale;
}
public clear(): void {
const commandEncoder = this.device.createCommandEncoder();
[
this.trailMapA,
this.trailMapB,
this.depositMap,
this.sourceMapA,
this.sourceMapB,
this.eraserMask,
].forEach((texture) => {
const passEncoder = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: texture.getTextureView(),
clearValue: appConfig.simulation.clearColor,
loadOp: 'clear',
storeOp: 'store',
},
],
});
passEncoder.end();
});
this.device.queue.submit([commandEncoder.finish()]);
}
public clearDepositMap(commandEncoder: GPUCommandEncoder): void {
// Hardware fast-clear via a render pass with loadOp 'clear' and an empty
// body. Cheaper than copyTextureToTexture and writes no actual color data
// on tile-based GPUs.
const passEncoder = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: this.depositMap.getTextureView(),
clearValue: { r: 0, g: 0, b: 0, a: 0 },
loadOp: 'clear',
storeOp: 'store',
},
],
});
passEncoder.end();
}
public swapTrailMaps(): void {
[this.trailMapA, this.trailMapB] = [this.trailMapB, this.trailMapA];
}
public clearSourceMaps(commandEncoder: GPUCommandEncoder): void {
// Only sourceMapA needs clearing — sourceMapB gets fully overwritten by
// the diffusion pass on the next active frame before it's ever sampled.
const passEncoder = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: this.sourceMapA.getTextureView(),
clearValue: appConfig.simulation.clearColor,
loadOp: 'clear',
storeOp: 'store',
},
],
});
passEncoder.end();
}
public swapSourceMaps(): void {
[this.sourceMapA, this.sourceMapB] = [this.sourceMapB, this.sourceMapA];
}
public destroy(): void {
this.trailMapA.destroy();
this.trailMapB.destroy();
this.depositMap.destroy();
this.sourceMapA.destroy();
this.sourceMapB.destroy();
this.eraserMask.destroy();
}
private createTexture(size: vec2): ResizableTexture {
return new ResizableTexture(this.device, size);
}
private createEraserMask(size: vec2): ResizableTexture {
return new ResizableTexture(this.device, size, {
clearValue: { r: 1, g: 1, b: 1, a: 1 },
format: ERASER_MASK_TEXTURE_FORMAT,
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.RENDER_ATTACHMENT |
GPUTextureUsage.COPY_SRC |
GPUTextureUsage.COPY_DST,
});
}
}