Rework garden simulation pipelines
This commit is contained in:
parent
c2efb33683
commit
018f8c9d4d
67 changed files with 6499 additions and 1682 deletions
162
src/game-loop/simulation-textures.ts
Normal file
162
src/game-loop/simulation-textures.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue