162 lines
5 KiB
TypeScript
162 lines
5 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
}
|