fix(algorithms): read params at the start of each frame
Params were pushed after the frame. Now each strategy reads them at frame start, like lenia. Fixes gray-scott seeding a new preset with old feed/kill, a possible pendulum WebGPU NaN after reset, and an empty first frame on physarum/boids GPU.
This commit is contained in:
1 parent
d298108516
commit
8db70fdd72
23 files changed
+528
-207
No files matched your search
@@ -29,7 +29,7 @@ import {
|
||||
SwarmMode,
|
||||
WORLD_SIZE
|
||||
} from './boids.model';
|
||||
import {SwarmSimulationStrategy} from './strategies/boids-simulation.strategy';
|
||||
import {SwarmSimulationParams, SwarmSimulationStrategy} from './strategies/boids-simulation.strategy';
|
||||
import {BoidsGpuStrategy} from './strategies/boids-gpu.strategy';
|
||||
import {BoidsCpuStrategy} from './strategies/boids-cpu.strategy';
|
||||
|
||||
@@ -185,27 +185,28 @@ export class BoidsComponent implements OnDestroy {
|
||||
};
|
||||
|
||||
this.strategy = gpuTier === 'webgpu' ? new BoidsGpuStrategy() : new BoidsCpuStrategy();
|
||||
this.strategy.init(scene, engine, config);
|
||||
this.startParamUpdateLoop(scene, engine);
|
||||
this.strategy.init(scene, engine, config, () => this.currentParams(engine));
|
||||
this.startFrameLoop(scene);
|
||||
}
|
||||
|
||||
private startParamUpdateLoop(scene: Scene, engine: WebGPUEngine | Engine): void {
|
||||
private currentParams(engine: WebGPUEngine | Engine): SwarmSimulationParams {
|
||||
return {
|
||||
deltaTime: engine.getDeltaTime() / 1000,
|
||||
separation: this.separation(),
|
||||
alignment: this.alignment(),
|
||||
cohesion: this.cohesion(),
|
||||
viewRadius: this.viewRadius(),
|
||||
maxSpeed: this.maxSpeed(),
|
||||
predator: this.predator,
|
||||
interactionRadius: this.interactionRadius(),
|
||||
friction: this.friction(),
|
||||
forceScale: this.forceScale()
|
||||
};
|
||||
}
|
||||
|
||||
private startFrameLoop(scene: Scene): void {
|
||||
scene.onAfterRenderObservable.clear();
|
||||
scene.onAfterRenderObservable.add(() => {
|
||||
this.fpsCounter.tick();
|
||||
this.strategy?.updateParams({
|
||||
deltaTime: engine.getDeltaTime() / 1000,
|
||||
separation: this.separation(),
|
||||
alignment: this.alignment(),
|
||||
cohesion: this.cohesion(),
|
||||
viewRadius: this.viewRadius(),
|
||||
maxSpeed: this.maxSpeed(),
|
||||
predator: this.predator,
|
||||
interactionRadius: this.interactionRadius(),
|
||||
friction: this.friction(),
|
||||
forceScale: this.forceScale()
|
||||
});
|
||||
});
|
||||
scene.onAfterRenderObservable.add(() => this.fpsCounter.tick());
|
||||
}
|
||||
|
||||
/** onSceneReady runs again when the GPU tier changes, so the observer must be added only once. */
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import {BOIDS_DEFAULTS} from '../boids.model';
|
||||
import {BoidsCpuStrategy} from './boids-cpu.strategy';
|
||||
import {SwarmSimulationParams} from './boids-simulation.strategy';
|
||||
|
||||
const positionUploads: Float32Array[] = [];
|
||||
|
||||
vi.mock('@babylonjs/core', () => {
|
||||
class FakeMesh {
|
||||
alwaysSelectAsActiveMesh = false;
|
||||
material: unknown = null;
|
||||
setVerticesData(): void { /* initial buffers are not under test */ }
|
||||
updateVerticesData(kind: string, data: Float32Array): void {
|
||||
if (kind === 'position') {
|
||||
positionUploads.push(data.slice());
|
||||
}
|
||||
}
|
||||
dispose(): void { /* nothing to free */ }
|
||||
}
|
||||
class FakeVertexData {
|
||||
positions: Float32Array | null = null;
|
||||
indices: number[] | null = null;
|
||||
applyToMesh(): void { /* initial buffers are not under test */ }
|
||||
}
|
||||
class FakeMaterial {
|
||||
backFaceCulling = true;
|
||||
disableDepthWrite = false;
|
||||
}
|
||||
return {Mesh: FakeMesh, VertexData: FakeVertexData, VertexBuffer: {PositionKind: 'position'}, ShaderMaterial: FakeMaterial};
|
||||
});
|
||||
|
||||
function fakeScene() {
|
||||
const callbacks: (() => void)[] = [];
|
||||
return {
|
||||
onBeforeRenderObservable: {clear: () => callbacks.splice(0), add: (cb: () => void) => callbacks.push(cb)},
|
||||
frame: () => callbacks.forEach((cb) => cb())
|
||||
};
|
||||
}
|
||||
|
||||
function paramsWithDelta(deltaTime: number): SwarmSimulationParams {
|
||||
return {
|
||||
deltaTime,
|
||||
...BOIDS_DEFAULTS,
|
||||
predator: {x: 0, y: 0, active: false},
|
||||
interactionRadius: 0,
|
||||
friction: 0,
|
||||
forceScale: 0
|
||||
};
|
||||
}
|
||||
|
||||
describe('BoidsCpuStrategy', () => {
|
||||
it('readsParamsAtFrameStart', () => {
|
||||
positionUploads.length = 0;
|
||||
let deltaTime = 0;
|
||||
const scene = fakeScene();
|
||||
const strategy = new BoidsCpuStrategy();
|
||||
strategy.init(scene as never, {} as never, {mode: 'boids', count: 16, numTypes: 1, matrix: new Float32Array(64)}, () => paramsWithDelta(deltaTime));
|
||||
scene.frame();
|
||||
|
||||
deltaTime = 0.016;
|
||||
scene.frame();
|
||||
|
||||
expect(positionUploads).toHaveLength(2);
|
||||
expect(positionUploads[1]).not.toEqual(positionUploads[0]);
|
||||
});
|
||||
});
|
||||
@@ -13,9 +13,11 @@ export class BoidsCpuStrategy implements SwarmSimulationStrategy {
|
||||
private buffers: SwarmBuffers | null = null;
|
||||
private matrix: Float32Array = new Float32Array(MAX_TYPES * MAX_TYPES);
|
||||
private readonly params = new Float32Array(PARAMS_LENGTH);
|
||||
private readParams: (() => SwarmSimulationParams) | null = null;
|
||||
|
||||
init(scene: Scene, _engine: WebGPUEngine | Engine, config: SwarmConfig): void {
|
||||
init(scene: Scene, _engine: WebGPUEngine | Engine, config: SwarmConfig, readParams: () => SwarmSimulationParams): void {
|
||||
this.scene = scene;
|
||||
this.readParams = readParams;
|
||||
this.config = config;
|
||||
this.matrix = config.matrix;
|
||||
|
||||
@@ -31,12 +33,6 @@ export class BoidsCpuStrategy implements SwarmSimulationStrategy {
|
||||
this.startRenderLoop(scene, config);
|
||||
}
|
||||
|
||||
updateParams(params: SwarmSimulationParams): void {
|
||||
if (this.config) {
|
||||
fillParams(this.params, this.config, params);
|
||||
}
|
||||
}
|
||||
|
||||
setInteractionMatrix(matrix: Float32Array): void {
|
||||
this.matrix = matrix;
|
||||
}
|
||||
@@ -47,9 +43,16 @@ export class BoidsCpuStrategy implements SwarmSimulationStrategy {
|
||||
this.mesh = null;
|
||||
this.buffers = null;
|
||||
this.config = null;
|
||||
this.readParams = null;
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
private applyParams(params: SwarmSimulationParams): void {
|
||||
if (this.config) {
|
||||
fillParams(this.params, this.config, params);
|
||||
}
|
||||
}
|
||||
|
||||
private setupRenderMesh(scene: Scene, config: SwarmConfig): void {
|
||||
const mesh = new Mesh('agents', scene);
|
||||
const vertexData = new VertexData();
|
||||
@@ -78,10 +81,11 @@ export class BoidsCpuStrategy implements SwarmSimulationStrategy {
|
||||
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
if (!this.mesh || !this.buffers) {
|
||||
if (!this.mesh || !this.buffers || !this.readParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyParams(this.readParams());
|
||||
copyState(this.buffers, config.count);
|
||||
if (config.mode === 'boids') {
|
||||
stepBoids(this.buffers, this.params);
|
||||
|
||||
@@ -34,9 +34,11 @@ export class BoidsGpuStrategy implements SwarmSimulationStrategy {
|
||||
private pipelines: GpuPipelines | null = null;
|
||||
private dispatchX = 0;
|
||||
private readonly paramsData = new Float32Array(PARAMS_LENGTH);
|
||||
private readParams: (() => SwarmSimulationParams) | null = null;
|
||||
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: SwarmConfig): void {
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: SwarmConfig, readParams: () => SwarmSimulationParams): void {
|
||||
this.scene = scene;
|
||||
this.readParams = readParams;
|
||||
this.config = config;
|
||||
const gpuEngine = engine as WebGPUEngine;
|
||||
|
||||
@@ -47,12 +49,6 @@ export class BoidsGpuStrategy implements SwarmSimulationStrategy {
|
||||
this.startRenderLoop(scene);
|
||||
}
|
||||
|
||||
updateParams(params: SwarmSimulationParams): void {
|
||||
if (this.config) {
|
||||
fillParams(this.paramsData, this.config, params);
|
||||
}
|
||||
}
|
||||
|
||||
setInteractionMatrix(matrix: Float32Array): void {
|
||||
this.buffers?.matrix.update(matrix);
|
||||
}
|
||||
@@ -67,9 +63,16 @@ export class BoidsGpuStrategy implements SwarmSimulationStrategy {
|
||||
this.buffers = null;
|
||||
this.pipelines = null;
|
||||
this.config = null;
|
||||
this.readParams = null;
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
private applyParams(params: SwarmSimulationParams): void {
|
||||
if (this.config) {
|
||||
fillParams(this.paramsData, this.config, params);
|
||||
}
|
||||
}
|
||||
|
||||
private createStorageBuffers(engine: WebGPUEngine, config: SwarmConfig): GpuBuffers {
|
||||
const data = seedAgents(config);
|
||||
const createBuffer = (arrayData: Float32Array): StorageBuffer => {
|
||||
@@ -150,10 +153,11 @@ export class BoidsGpuStrategy implements SwarmSimulationStrategy {
|
||||
private startRenderLoop(scene: Scene): void {
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
if (!this.buffers || !this.pipelines) {
|
||||
if (!this.buffers || !this.pipelines || !this.readParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyParams(this.readParams());
|
||||
this.buffers.params.update(this.paramsData);
|
||||
this.pipelines.copy.dispatch(this.dispatchX, 1, 1);
|
||||
this.pipelines.step.dispatch(this.dispatchX, 1, 1);
|
||||
|
||||
@@ -15,8 +15,7 @@ export interface SwarmSimulationParams {
|
||||
}
|
||||
|
||||
export interface SwarmSimulationStrategy {
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: SwarmConfig): void;
|
||||
updateParams(params: SwarmSimulationParams): void;
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: SwarmConfig, readParams: () => SwarmSimulationParams): void;
|
||||
/** Re-roll without restart. */
|
||||
setInteractionMatrix(matrix: Float32Array): void;
|
||||
dispose(): void;
|
||||
|
||||
@@ -2,6 +2,7 @@ import {Component, computed, signal} from '@angular/core';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
import {SliderFieldComponent} from '../../../shared/components/slider-field/slider-field';
|
||||
import {TranslatePipe} from '@ngx-translate/core';
|
||||
import {Engine, Scene, WebGPUEngine} from '@babylonjs/core';
|
||||
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {CLOTH_SUBSTEPS, ClothConfig, ClothStats} from './cloth.model';
|
||||
@@ -9,7 +10,7 @@ import {Information} from '../information/information';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {SOURCE_PATHS} from '../information/source-paths';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
import {ClothSimulationStrategy} from './strategies/cloth-simulation.strategy';
|
||||
import {ClothSimulationParams, ClothSimulationStrategy} from './strategies/cloth-simulation.strategy';
|
||||
import {ClothGpuStrategy} from './strategies/cloth-gpu.strategy';
|
||||
import {ClothCpuStrategy} from './strategies/cloth-cpu.strategy';
|
||||
import {Metric, MetricsComponent} from '../../../shared/components/metrics/metrics';
|
||||
@@ -135,26 +136,26 @@ export class ClothComponent {
|
||||
: new ClothCpuStrategy();
|
||||
|
||||
this.backend.set(gpuTier === 'webgpu' ? 'WebGPU (Compute)' : 'WebGL + CPU');
|
||||
this.strategy.init(scene, engine, config);
|
||||
this.strategy.init(scene, engine, config, () => this.currentParams(engine));
|
||||
this.stats.set(this.strategy.getStats());
|
||||
this.startParamUpdateLoop(scene, engine);
|
||||
this.startFrameLoop(scene, engine);
|
||||
}
|
||||
|
||||
private startParamUpdateLoop(scene: any, engine: any): void {
|
||||
private currentParams(engine: WebGPUEngine | Engine): ClothSimulationParams {
|
||||
return {
|
||||
stiffness: this.stiffness,
|
||||
elongation: this.elongation(),
|
||||
isWindActive: this.isWindActive(),
|
||||
simulationTime: this.simulationTime,
|
||||
deltaTime: engine.getDeltaTime() / 1000.0
|
||||
};
|
||||
}
|
||||
|
||||
private startFrameLoop(scene: Scene, engine: WebGPUEngine | Engine): void {
|
||||
scene.onAfterRenderObservable.clear();
|
||||
scene.onAfterRenderObservable.add(() => {
|
||||
this.fpsCounter.tick();
|
||||
this.simulationTime += engine.getDeltaTime() / 1000.0;
|
||||
|
||||
if (this.strategy) {
|
||||
this.strategy.updateParams({
|
||||
stiffness: this.stiffness,
|
||||
elongation: this.elongation(),
|
||||
isWindActive: this.isWindActive(),
|
||||
simulationTime: this.simulationTime,
|
||||
deltaTime: engine.getDeltaTime() / 1000.0
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import {ClothCpuStrategy} from './cloth-cpu.strategy';
|
||||
import {ClothSimulationParams} from './cloth-simulation.strategy';
|
||||
|
||||
const calls: string[] = [];
|
||||
|
||||
vi.mock('@babylonjs/core', () => {
|
||||
class FakeMaterial {
|
||||
backFaceCulling = true;
|
||||
}
|
||||
return {
|
||||
ShaderMaterial: FakeMaterial,
|
||||
VertexBuffer: {PositionKind: 'position'},
|
||||
MeshBuilder: {
|
||||
CreateGround: () => ({
|
||||
material: null,
|
||||
updateVerticesData: () => calls.push('mesh')
|
||||
})
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function fakeScene() {
|
||||
const callbacks: (() => void)[] = [];
|
||||
return {
|
||||
activeCamera: null,
|
||||
removeMesh: vi.fn(),
|
||||
onBeforeRenderObservable: {clear: () => callbacks.splice(0), add: (cb: () => void) => callbacks.push(cb)},
|
||||
frame: () => callbacks.forEach((cb) => cb())
|
||||
};
|
||||
}
|
||||
|
||||
describe('ClothCpuStrategy', () => {
|
||||
it('readsParamsAtFrameStart', () => {
|
||||
calls.length = 0;
|
||||
const params: ClothSimulationParams = {stiffness: 50, elongation: 1, isWindActive: false, simulationTime: 0, deltaTime: 0.016};
|
||||
const scene = fakeScene();
|
||||
const strategy = new ClothCpuStrategy();
|
||||
const config = {gridWidth: 4, gridHeight: 4, spacing: 0.05, density: 1, numVertices: 16, particleInvMass: 1};
|
||||
strategy.init(scene as never, {} as never, config, () => {
|
||||
calls.push('read');
|
||||
return params;
|
||||
});
|
||||
|
||||
scene.frame();
|
||||
scene.frame();
|
||||
|
||||
expect(calls).toEqual(['read', 'mesh', 'read', 'mesh']);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ export class ClothCpuStrategy implements ClothSimulationStrategy {
|
||||
private clothMesh: GroundMesh | null = null;
|
||||
private scene: Scene | null = null;
|
||||
private config: ClothConfig | null = null;
|
||||
private readParams: (() => ClothSimulationParams) | null = null;
|
||||
|
||||
private positions!: Float32Array;
|
||||
private prevPositions!: Float32Array;
|
||||
@@ -27,8 +28,9 @@ export class ClothCpuStrategy implements ClothSimulationStrategy {
|
||||
elongation: 1.0
|
||||
};
|
||||
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: ClothConfig): void {
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: ClothConfig, readParams: () => ClothSimulationParams): void {
|
||||
this.scene = scene;
|
||||
this.readParams = readParams;
|
||||
this.config = config;
|
||||
this.physicsParams.numVertices = config.numVertices;
|
||||
|
||||
@@ -47,7 +49,19 @@ export class ClothCpuStrategy implements ClothSimulationStrategy {
|
||||
return this.stats;
|
||||
}
|
||||
|
||||
updateParams(params: ClothSimulationParams): void {
|
||||
getMesh(): GroundMesh | null {
|
||||
return this.clothMesh;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.scene && this.clothMesh) {
|
||||
this.scene.removeMesh(this.clothMesh);
|
||||
}
|
||||
this.clothMesh = null;
|
||||
this.readParams = null;
|
||||
}
|
||||
|
||||
private applyParams(params: ClothSimulationParams): void {
|
||||
const windX = params.isWindActive ? 5.0 : 0.0;
|
||||
const windZ = params.isWindActive ? 15.0 : 0.0;
|
||||
|
||||
@@ -63,17 +77,6 @@ export class ClothCpuStrategy implements ClothSimulationStrategy {
|
||||
this.physicsParams.elongation = params.elongation;
|
||||
}
|
||||
|
||||
getMesh(): GroundMesh | null {
|
||||
return this.clothMesh;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.scene && this.clothMesh) {
|
||||
this.scene.removeMesh(this.clothMesh);
|
||||
}
|
||||
this.clothMesh = null;
|
||||
}
|
||||
|
||||
private generateClothData(config: ClothConfig): ClothData {
|
||||
const positionsData = new Float32Array(config.numVertices * 4);
|
||||
const prevPositionsData = new Float32Array(config.numVertices * 4);
|
||||
@@ -199,10 +202,11 @@ export class ClothCpuStrategy implements ClothSimulationStrategy {
|
||||
private startRenderLoop(scene: Scene, config: ClothConfig): void {
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
if (!this.clothMesh) {
|
||||
if (!this.clothMesh || !this.readParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyParams(this.readParams());
|
||||
integratePositions(this.positions, this.prevPositions, this.velocities, this.physicsParams);
|
||||
|
||||
for (let i = 0; i < CLOTH_SUBSTEPS; i++) {
|
||||
|
||||
@@ -32,10 +32,12 @@ export class ClothGpuStrategy implements ClothSimulationStrategy {
|
||||
private dispatchXConstraints: number[] = [];
|
||||
private dispatchXVertices = 0;
|
||||
private numVertices = 0;
|
||||
private readParams: (() => ClothSimulationParams) | null = null;
|
||||
private stats: ClothStats = {constraints: 0, phases: 0};
|
||||
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: ClothConfig): void {
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: ClothConfig, readParams: () => ClothSimulationParams): void {
|
||||
this.scene = scene;
|
||||
this.readParams = readParams;
|
||||
this.numVertices = config.numVertices;
|
||||
const gpuEngine = engine as WebGPUEngine;
|
||||
|
||||
@@ -52,7 +54,21 @@ export class ClothGpuStrategy implements ClothSimulationStrategy {
|
||||
return this.stats;
|
||||
}
|
||||
|
||||
updateParams(params: ClothSimulationParams): void {
|
||||
getMesh(): GroundMesh | null {
|
||||
return this.clothMesh;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.scene && this.clothMesh) {
|
||||
this.scene.removeMesh(this.clothMesh);
|
||||
}
|
||||
this.clothMesh = null;
|
||||
this.buffers = null;
|
||||
this.pipelines = null;
|
||||
this.readParams = null;
|
||||
}
|
||||
|
||||
private applyParams(params: ClothSimulationParams): void {
|
||||
if (!this.buffers) {
|
||||
return;
|
||||
}
|
||||
@@ -76,19 +92,6 @@ export class ClothGpuStrategy implements ClothSimulationStrategy {
|
||||
this.paramsData[8] = params.elongation;
|
||||
}
|
||||
|
||||
getMesh(): GroundMesh | null {
|
||||
return this.clothMesh;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.scene && this.clothMesh) {
|
||||
this.scene.removeMesh(this.clothMesh);
|
||||
}
|
||||
this.clothMesh = null;
|
||||
this.buffers = null;
|
||||
this.pipelines = null;
|
||||
}
|
||||
|
||||
private generateClothData(config: ClothConfig): ClothData {
|
||||
const positionsData = new Float32Array(config.numVertices * 4);
|
||||
const prevPositionsData = new Float32Array(config.numVertices * 4);
|
||||
@@ -259,10 +262,11 @@ export class ClothGpuStrategy implements ClothSimulationStrategy {
|
||||
private startRenderLoop(scene: Scene): void {
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
if (!this.buffers || !this.pipelines) {
|
||||
if (!this.buffers || !this.pipelines || !this.readParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyParams(this.readParams());
|
||||
this.buffers.params.update(this.paramsData);
|
||||
|
||||
this.pipelines.integrate.dispatch(this.dispatchXVertices, 1, 1);
|
||||
|
||||
@@ -10,8 +10,7 @@ export interface ClothSimulationParams {
|
||||
}
|
||||
|
||||
export interface ClothSimulationStrategy {
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: ClothConfig): void;
|
||||
updateParams(params: ClothSimulationParams): void;
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: ClothConfig, readParams: () => ClothSimulationParams): void;
|
||||
getMesh(): GroundMesh | null;
|
||||
getStats(): ClothStats;
|
||||
dispose(): void;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import {GrayScottParams, GrayScottSimulation} from './gray-scott-simulation';
|
||||
import {DEFAULT_SETTINGS} from './gray-scott.model';
|
||||
|
||||
const renderCalls: string[] = [];
|
||||
|
||||
vi.mock('@babylonjs/core', () => {
|
||||
class FakeWrapper {
|
||||
readonly onApplyObservable = {add: vi.fn()};
|
||||
readonly effect = {isReady: () => true, setTexture: vi.fn(), setFloat: vi.fn(), setFloat2: vi.fn()};
|
||||
readonly name: string;
|
||||
constructor(options: {name: string}) {
|
||||
this.name = options.name;
|
||||
}
|
||||
dispose(): void { /* nothing to free */ }
|
||||
}
|
||||
class FakeRenderer {
|
||||
render(wrapper: FakeWrapper): void {
|
||||
renderCalls.push(wrapper.name);
|
||||
}
|
||||
dispose(): void { /* nothing to free */ }
|
||||
}
|
||||
class FakeTarget {
|
||||
wrapU = 0;
|
||||
wrapV = 0;
|
||||
constructor(readonly name: string) {}
|
||||
dispose(): void { /* nothing to free */ }
|
||||
}
|
||||
class FakeMaterial {
|
||||
backFaceCulling = true;
|
||||
disableDepthWrite = false;
|
||||
setTexture(): void { /* display texture is not under test */ }
|
||||
}
|
||||
return {
|
||||
Constants: {TEXTURETYPE_FLOAT: 1, TEXTURETYPE_HALF_FLOAT: 2, TEXTUREFORMAT_RGBA: 5},
|
||||
Texture: {NEAREST_SAMPLINGMODE: 1, WRAP_ADDRESSMODE: 1},
|
||||
EffectRenderer: FakeRenderer,
|
||||
EffectWrapper: FakeWrapper,
|
||||
RenderTargetTexture: FakeTarget,
|
||||
ShaderMaterial: FakeMaterial,
|
||||
MeshBuilder: {CreatePlane: () => ({alwaysSelectAsActiveMesh: false, material: null, lookAt: vi.fn(), dispose: vi.fn()})}
|
||||
};
|
||||
});
|
||||
|
||||
function fakeScene() {
|
||||
const callbacks: (() => void)[] = [];
|
||||
return {
|
||||
activeCamera: null,
|
||||
onBeforeRenderObservable: {clear: () => callbacks.splice(0), add: (cb: () => void) => callbacks.push(cb)},
|
||||
frame: () => callbacks.forEach((cb) => cb())
|
||||
};
|
||||
}
|
||||
|
||||
function paramsWithSteps(stepsPerFrame: number): GrayScottParams {
|
||||
return {settings: {...DEFAULT_SETTINGS, stepsPerFrame}, brush: {u: 0, v: 0, active: false}};
|
||||
}
|
||||
|
||||
describe('GrayScottSimulation', () => {
|
||||
it('readsParamsAtFrameStart', () => {
|
||||
let stepsPerFrame = 1;
|
||||
const scene = fakeScene();
|
||||
const simulation = new GrayScottSimulation();
|
||||
simulation.init(scene as never, {} as never, 'float', () => paramsWithSteps(stepsPerFrame));
|
||||
scene.frame();
|
||||
|
||||
// The preset path: new params, then reset. The new seed must step with the new params.
|
||||
stepsPerFrame = 3;
|
||||
simulation.reset();
|
||||
renderCalls.length = 0;
|
||||
scene.frame();
|
||||
|
||||
expect(renderCalls).toEqual(['grayScottSeed', 'grayScottStep', 'grayScottStep', 'grayScottStep']);
|
||||
});
|
||||
});
|
||||
@@ -18,11 +18,13 @@ export class GrayScottSimulation {
|
||||
private next: RenderTargetTexture | null = null;
|
||||
private material: ShaderMaterial | null = null;
|
||||
private plane: Mesh | null = null;
|
||||
private params: GrayScottParams | null = null;
|
||||
private readParams: (() => GrayScottParams) | null = null;
|
||||
private frameParams: GrayScottParams | null = null;
|
||||
private seeded = false;
|
||||
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, textureType: StateTextureType): void {
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, textureType: StateTextureType, readParams: () => GrayScottParams): void {
|
||||
this.scene = scene;
|
||||
this.readParams = readParams;
|
||||
this.renderer = new EffectRenderer(engine);
|
||||
this.current = createStateTarget('grayScottA', scene, GRID_SIZE, textureType);
|
||||
this.next = createStateTarget('grayScottB', scene, GRID_SIZE, textureType);
|
||||
@@ -48,10 +50,6 @@ export class GrayScottSimulation {
|
||||
scene.onBeforeRenderObservable.add(() => this.frame());
|
||||
}
|
||||
|
||||
updateParams(params: GrayScottParams): void {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
/** The next frame runs the seed pass again. Textures and shaders stay. */
|
||||
reset(): void {
|
||||
this.seeded = false;
|
||||
@@ -72,7 +70,8 @@ export class GrayScottSimulation {
|
||||
this.next = null;
|
||||
this.material = null;
|
||||
this.plane = null;
|
||||
this.params = null;
|
||||
this.readParams = null;
|
||||
this.frameParams = null;
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
@@ -101,10 +100,10 @@ export class GrayScottSimulation {
|
||||
|
||||
private applyStepUniforms(): void {
|
||||
const effect = this.stepWrapper?.effect;
|
||||
if (!effect || !this.params || !this.current) {
|
||||
if (!effect || !this.frameParams || !this.current) {
|
||||
return;
|
||||
}
|
||||
const {settings, brush} = this.params;
|
||||
const {settings, brush} = this.frameParams;
|
||||
effect.setTexture('state', this.current);
|
||||
effect.setFloat('texel', 1 / GRID_SIZE);
|
||||
effect.setFloat('feed', settings.feed);
|
||||
@@ -116,9 +115,11 @@ export class GrayScottSimulation {
|
||||
}
|
||||
|
||||
private frame(): void {
|
||||
if (!this.renderer || !this.seedWrapper || !this.stepWrapper || !this.current || !this.next || !this.params) {
|
||||
if (!this.renderer || !this.seedWrapper || !this.stepWrapper || !this.current || !this.next || !this.readParams) {
|
||||
return;
|
||||
}
|
||||
// Read before the seed pass, so a new seed never steps with old params.
|
||||
this.frameParams = this.readParams();
|
||||
if (!this.seeded) {
|
||||
if (!this.seedWrapper.effect.isReady()) {
|
||||
return;
|
||||
@@ -130,7 +131,7 @@ export class GrayScottSimulation {
|
||||
if (!this.stepWrapper.effect.isReady()) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < this.params.settings.stepsPerFrame; i++) {
|
||||
for (let i = 0; i < this.frameParams.settings.stepsPerFrame; i++) {
|
||||
this.renderer.render(this.stepWrapper, this.next);
|
||||
[this.current, this.next] = [this.next, this.current];
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
SETTINGS_LIMITS,
|
||||
WORLD_SIZE
|
||||
} from './gray-scott.model';
|
||||
import {GrayScottSimulation} from './gray-scott-simulation';
|
||||
import {GrayScottParams, GrayScottSimulation} from './gray-scott-simulation';
|
||||
|
||||
interface SettingSlider {
|
||||
key: keyof GrayScottSettings;
|
||||
@@ -143,8 +143,8 @@ export class GrayScottComponent implements OnDestroy {
|
||||
this.backend.set(`${backend} (${type === 'float' ? 'float32' : 'float16'})`);
|
||||
this.simulation?.dispose();
|
||||
this.simulation = new GrayScottSimulation();
|
||||
this.simulation.init(event.scene, event.engine, type);
|
||||
this.startParamUpdateLoop(event.scene);
|
||||
this.simulation.init(event.scene, event.engine, type, () => this.currentParams());
|
||||
this.startFrameLoop(event.scene);
|
||||
this.attachPointer(event.scene);
|
||||
}
|
||||
|
||||
@@ -167,21 +167,22 @@ export class GrayScottComponent implements OnDestroy {
|
||||
this.simulation?.reset();
|
||||
}
|
||||
|
||||
private startParamUpdateLoop(scene: Scene): void {
|
||||
private currentParams(): GrayScottParams {
|
||||
return {
|
||||
settings: {
|
||||
feed: this.feed(),
|
||||
kill: this.kill(),
|
||||
diffuseU: this.diffuseU(),
|
||||
diffuseV: this.diffuseV(),
|
||||
stepsPerFrame: this.stepsPerFrame()
|
||||
},
|
||||
brush: this.brush
|
||||
};
|
||||
}
|
||||
|
||||
private startFrameLoop(scene: Scene): void {
|
||||
scene.onAfterRenderObservable.clear();
|
||||
scene.onAfterRenderObservable.add(() => {
|
||||
this.fpsCounter.tick();
|
||||
this.simulation?.updateParams({
|
||||
settings: {
|
||||
feed: this.feed(),
|
||||
kill: this.kill(),
|
||||
diffuseU: this.diffuseU(),
|
||||
diffuseV: this.diffuseV(),
|
||||
stepsPerFrame: this.stepsPerFrame()
|
||||
},
|
||||
brush: this.brush
|
||||
});
|
||||
});
|
||||
scene.onAfterRenderObservable.add(() => this.fpsCounter.tick());
|
||||
}
|
||||
|
||||
/** onSceneReady runs again when the GPU tier changes, so the observer must be added only once. */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {Component, computed, signal} from '@angular/core';
|
||||
import {Scene} from '@babylonjs/core';
|
||||
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
|
||||
@@ -10,7 +11,7 @@ import {Information} from '../information/information';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {SOURCE_PATHS} from '../information/source-paths';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
import {PendulumSimulationStrategy} from './strategies/pendulum-simulation.strategy';
|
||||
import {PendulumSimParams, PendulumSimulationStrategy} from './strategies/pendulum-simulation.strategy';
|
||||
import {PendulumGpuStrategy} from './strategies/pendulum-gpu.strategy';
|
||||
import {PendulumCpuStrategy} from './strategies/pendulum-cpu.strategy';
|
||||
import {Metric, MetricsComponent} from '../../../shared/components/metrics/metrics';
|
||||
@@ -113,29 +114,28 @@ class PendulumComponent {
|
||||
: new PendulumCpuStrategy();
|
||||
|
||||
this.backend.set(gpuTier === 'webgpu' ? 'WebGPU (Compute)' : 'WebGL + CPU');
|
||||
this.strategy.init(scene, engine);
|
||||
this.startParamUpdateLoop(scene);
|
||||
this.strategy.init(scene, engine, () => this.currentParams());
|
||||
this.startFrameLoop(scene);
|
||||
}
|
||||
|
||||
private startParamUpdateLoop(scene: any): void {
|
||||
private currentParams(): PendulumSimParams {
|
||||
return {
|
||||
...this.loop,
|
||||
g: this.g(),
|
||||
m1: this.m1(),
|
||||
m2: this.m2(),
|
||||
l1: this.l1(),
|
||||
l2: this.l2(),
|
||||
damping: this.damping(),
|
||||
trailDecay: this.trailDecay()
|
||||
};
|
||||
}
|
||||
|
||||
private startFrameLoop(scene: Scene): void {
|
||||
scene.onAfterRenderObservable.clear();
|
||||
scene.onAfterRenderObservable.add(() => {
|
||||
this.fpsCounter.tick();
|
||||
this.loop.time += this.loop.dt;
|
||||
|
||||
if (this.strategy) {
|
||||
this.strategy.updateParams({
|
||||
...this.loop,
|
||||
g: this.g(),
|
||||
m1: this.m1(),
|
||||
m2: this.m2(),
|
||||
l1: this.l1(),
|
||||
l2: this.l2(),
|
||||
damping: this.damping(),
|
||||
trailDecay: this.trailDecay()
|
||||
});
|
||||
}
|
||||
|
||||
this.resetImpulses();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import {PendulumCpuStrategy} from './pendulum-cpu.strategy';
|
||||
import {PendulumSimParams} from './pendulum-simulation.strategy';
|
||||
|
||||
const feedbackL1: number[] = [];
|
||||
|
||||
vi.mock('@babylonjs/core', () => {
|
||||
class FakeMaterial {
|
||||
backFaceCulling = true;
|
||||
disableDepthWrite = false;
|
||||
constructor(readonly name: string) {}
|
||||
setTexture(): void { /* textures are not under test */ }
|
||||
setVector2(): void { /* resolution is not under test */ }
|
||||
setFloat(name: string, value: number): void {
|
||||
if (this.name === 'feedbackMat' && name === 'l1') {
|
||||
feedbackL1.push(value);
|
||||
}
|
||||
}
|
||||
dispose(): void { /* nothing to free */ }
|
||||
}
|
||||
class FakeTarget {
|
||||
wrapU = 0;
|
||||
wrapV = 0;
|
||||
renderList: unknown[] = [];
|
||||
render(): void { /* drawing is not under test */ }
|
||||
dispose(): void { /* nothing to free */ }
|
||||
}
|
||||
return {
|
||||
Constants: {TEXTURETYPE_FLOAT: 1},
|
||||
Texture: {CLAMP_ADDRESSMODE: 0},
|
||||
Vector2: class {},
|
||||
ShaderMaterial: FakeMaterial,
|
||||
RenderTargetTexture: FakeTarget,
|
||||
MeshBuilder: {CreatePlane: () => ({alwaysSelectAsActiveMesh: false, layerMask: 0, material: null})}
|
||||
};
|
||||
});
|
||||
|
||||
function fakeScene() {
|
||||
const callbacks: (() => void)[] = [];
|
||||
return {
|
||||
getMeshByName: () => null,
|
||||
removeMesh: vi.fn(),
|
||||
onBeforeRenderObservable: {clear: () => callbacks.splice(0), add: (cb: () => void) => callbacks.push(cb)},
|
||||
frame: () => callbacks.forEach((cb) => cb())
|
||||
};
|
||||
}
|
||||
|
||||
function paramsWithL1(l1: number): PendulumSimParams {
|
||||
return {time: 0, dt: 0.015, g: 9.81, m1: 1, m2: 1, l1, l2: 1, damping: 0, trailDecay: 0.9, impulseM1: 0, impulseM2: 0};
|
||||
}
|
||||
|
||||
describe('PendulumCpuStrategy', () => {
|
||||
it('readsParamsAtFrameStart', () => {
|
||||
feedbackL1.length = 0;
|
||||
let l1 = 1;
|
||||
const scene = fakeScene();
|
||||
const strategy = new PendulumCpuStrategy();
|
||||
strategy.init(scene as never, {getRenderWidth: () => 8, getRenderHeight: () => 8} as never, () => paramsWithL1(l1));
|
||||
|
||||
scene.frame();
|
||||
l1 = 2;
|
||||
scene.frame();
|
||||
|
||||
expect(feedbackL1).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
@@ -6,15 +6,16 @@ import {PendulumSimParams, PendulumSimulationStrategy} from './pendulum-simulati
|
||||
export class PendulumCpuStrategy implements PendulumSimulationStrategy {
|
||||
private scene: Scene | null = null;
|
||||
private state: PendulumState = {theta1: Math.PI / 4, theta2: Math.PI / 2, v1: 0, v2: 0};
|
||||
private params: PendulumSimParams | null = null;
|
||||
private readParams: (() => PendulumSimParams) | null = null;
|
||||
private feedbackMaterial: ShaderMaterial | null = null;
|
||||
private displayMaterial: ShaderMaterial | null = null;
|
||||
private rttA: RenderTargetTexture | null = null;
|
||||
private rttB: RenderTargetTexture | null = null;
|
||||
private pingPong = false;
|
||||
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine): void {
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, readParams: () => PendulumSimParams): void {
|
||||
this.scene = scene;
|
||||
this.readParams = readParams;
|
||||
|
||||
const existingPlane = scene.getMeshByName("plane");
|
||||
if (existingPlane) {
|
||||
@@ -72,10 +73,6 @@ export class PendulumCpuStrategy implements PendulumSimulationStrategy {
|
||||
this.startRenderLoop(scene, width, height);
|
||||
}
|
||||
|
||||
updateParams(params: PendulumSimParams): void {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.scene) {
|
||||
this.scene.onBeforeRenderObservable.clear();
|
||||
@@ -84,6 +81,7 @@ export class PendulumCpuStrategy implements PendulumSimulationStrategy {
|
||||
this.rttB?.dispose();
|
||||
this.feedbackMaterial?.dispose();
|
||||
this.displayMaterial?.dispose();
|
||||
this.readParams = null;
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
@@ -92,21 +90,13 @@ export class PendulumCpuStrategy implements PendulumSimulationStrategy {
|
||||
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
if (!this.params || !this.feedbackMaterial || !this.displayMaterial || !this.rttA || !this.rttB) {
|
||||
if (!this.readParams || !this.feedbackMaterial || !this.displayMaterial || !this.rttA || !this.rttB) {
|
||||
return;
|
||||
}
|
||||
|
||||
stepPendulumPhysics(this.state, {
|
||||
dt: this.params.dt,
|
||||
g: this.params.g,
|
||||
m1: this.params.m1,
|
||||
m2: this.params.m2,
|
||||
l1: this.params.l1,
|
||||
l2: this.params.l2,
|
||||
damping: this.params.damping,
|
||||
impulseM1: this.params.impulseM1,
|
||||
impulseM2: this.params.impulseM2
|
||||
});
|
||||
const params = this.readParams();
|
||||
|
||||
stepPendulumPhysics(this.state, params);
|
||||
|
||||
const readTarget = this.pingPong ? this.rttA : this.rttB;
|
||||
const writeTarget = this.pingPong ? this.rttB : this.rttA;
|
||||
@@ -115,9 +105,9 @@ export class PendulumCpuStrategy implements PendulumSimulationStrategy {
|
||||
this.feedbackMaterial.setVector2("resolution", resolution);
|
||||
this.feedbackMaterial.setFloat("theta1", this.state.theta1);
|
||||
this.feedbackMaterial.setFloat("theta2", this.state.theta2);
|
||||
this.feedbackMaterial.setFloat("l1", this.params.l1);
|
||||
this.feedbackMaterial.setFloat("l2", this.params.l2);
|
||||
this.feedbackMaterial.setFloat("trailDecay", this.params.trailDecay);
|
||||
this.feedbackMaterial.setFloat("l1", params.l1);
|
||||
this.feedbackMaterial.setFloat("l2", params.l2);
|
||||
this.feedbackMaterial.setFloat("trailDecay", params.trailDecay);
|
||||
|
||||
writeTarget.render();
|
||||
|
||||
@@ -125,8 +115,8 @@ export class PendulumCpuStrategy implements PendulumSimulationStrategy {
|
||||
this.displayMaterial.setVector2("resolution", resolution);
|
||||
this.displayMaterial.setFloat("theta1", this.state.theta1);
|
||||
this.displayMaterial.setFloat("theta2", this.state.theta2);
|
||||
this.displayMaterial.setFloat("l1", this.params.l1);
|
||||
this.displayMaterial.setFloat("l2", this.params.l2);
|
||||
this.displayMaterial.setFloat("l1", params.l1);
|
||||
this.displayMaterial.setFloat("l2", params.l2);
|
||||
|
||||
this.pingPong = !this.pingPong;
|
||||
});
|
||||
|
||||
@@ -6,9 +6,11 @@ export class PendulumGpuStrategy implements PendulumSimulationStrategy {
|
||||
private scene: Scene | null = null;
|
||||
private paramsData = new Float32Array(14);
|
||||
private paramsBuffer: StorageBuffer | null = null;
|
||||
private readParams: (() => PendulumSimParams) | null = null;
|
||||
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine): void {
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, readParams: () => PendulumSimParams): void {
|
||||
this.scene = scene;
|
||||
this.readParams = readParams;
|
||||
const gpuEngine = engine as WebGPUEngine;
|
||||
gpuEngine.resize();
|
||||
|
||||
@@ -63,10 +65,11 @@ export class PendulumGpuStrategy implements PendulumSimulationStrategy {
|
||||
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
if (!this.paramsBuffer) {
|
||||
if (!this.paramsBuffer || !this.readParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyParams(this.readParams());
|
||||
const currentWidth = gpuEngine.getRenderWidth();
|
||||
const currentHeight = gpuEngine.getRenderHeight();
|
||||
this.paramsData[0] = currentWidth;
|
||||
@@ -81,7 +84,21 @@ export class PendulumGpuStrategy implements PendulumSimulationStrategy {
|
||||
});
|
||||
}
|
||||
|
||||
updateParams(params: PendulumSimParams): void {
|
||||
dispose(): void {
|
||||
if (this.scene) {
|
||||
this.scene.onBeforeRenderObservable.clear();
|
||||
const plane = this.scene.getMeshByName("pendulumPlane");
|
||||
if (plane) {
|
||||
this.scene.removeMesh(plane);
|
||||
plane.dispose();
|
||||
}
|
||||
}
|
||||
this.paramsBuffer = null;
|
||||
this.readParams = null;
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
private applyParams(params: PendulumSimParams): void {
|
||||
// paramsData[0] (width) and paramsData[1] (height) are set in the render loop
|
||||
this.paramsData[2] = params.time;
|
||||
this.paramsData[3] = params.dt;
|
||||
@@ -96,17 +113,4 @@ export class PendulumGpuStrategy implements PendulumSimulationStrategy {
|
||||
this.paramsData[12] = params.impulseM2;
|
||||
this.paramsData[13] = 0;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.scene) {
|
||||
this.scene.onBeforeRenderObservable.clear();
|
||||
const plane = this.scene.getMeshByName("pendulumPlane");
|
||||
if (plane) {
|
||||
this.scene.removeMesh(plane);
|
||||
plane.dispose();
|
||||
}
|
||||
}
|
||||
this.paramsBuffer = null;
|
||||
this.scene = null;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ export interface PendulumSimParams {
|
||||
}
|
||||
|
||||
export interface PendulumSimulationStrategy {
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine): void;
|
||||
updateParams(params: PendulumSimParams): void;
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, readParams: () => PendulumSimParams): void;
|
||||
dispose(): void;
|
||||
}
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
SETTINGS_LIMITS,
|
||||
WORLD_SIZE
|
||||
} from './physarum.model';
|
||||
import {PhysarumSimulationStrategy} from './strategies/physarum-simulation.strategy';
|
||||
import {PhysarumSimulationParams, PhysarumSimulationStrategy} from './strategies/physarum-simulation.strategy';
|
||||
import {PhysarumGpuStrategy} from './strategies/physarum-gpu.strategy';
|
||||
import {PhysarumCpuStrategy} from './strategies/physarum-cpu.strategy';
|
||||
|
||||
@@ -195,27 +195,28 @@ export class PhysarumComponent implements OnDestroy {
|
||||
|
||||
const config: PhysarumConfig = {count: this.agentCount(), mapWidth: this.mapSize(), mapHeight: this.mapSize()};
|
||||
this.strategy = gpuTier === 'webgpu' ? new PhysarumGpuStrategy() : new PhysarumCpuStrategy();
|
||||
this.strategy.init(scene, engine, config);
|
||||
this.startParamUpdateLoop(scene);
|
||||
this.strategy.init(scene, engine, config, () => this.currentParams());
|
||||
this.startFrameLoop(scene);
|
||||
}
|
||||
|
||||
private startParamUpdateLoop(scene: Scene): void {
|
||||
private currentParams(): PhysarumSimulationParams {
|
||||
return {
|
||||
settings: {
|
||||
sensorAngle: this.sensorAngle(),
|
||||
sensorDistance: this.sensorDistance(),
|
||||
turnAngle: this.turnAngle(),
|
||||
stepSize: this.stepSize(),
|
||||
deposit: this.deposit(),
|
||||
decay: this.decay(),
|
||||
diffuse: this.diffuse()
|
||||
},
|
||||
food: this.food
|
||||
};
|
||||
}
|
||||
|
||||
private startFrameLoop(scene: Scene): void {
|
||||
scene.onAfterRenderObservable.clear();
|
||||
scene.onAfterRenderObservable.add(() => {
|
||||
this.fpsCounter.tick();
|
||||
this.strategy?.updateParams({
|
||||
settings: {
|
||||
sensorAngle: this.sensorAngle(),
|
||||
sensorDistance: this.sensorDistance(),
|
||||
turnAngle: this.turnAngle(),
|
||||
stepSize: this.stepSize(),
|
||||
deposit: this.deposit(),
|
||||
decay: this.decay(),
|
||||
diffuse: this.diffuse()
|
||||
},
|
||||
food: this.food
|
||||
});
|
||||
});
|
||||
scene.onAfterRenderObservable.add(() => this.fpsCounter.tick());
|
||||
}
|
||||
|
||||
/** onSceneReady runs again when the GPU tier changes, so the observer must be added only once. */
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import {DEFAULT_PRESET, PRESETS} from '../physarum.model';
|
||||
import {PhysarumCpuStrategy} from './physarum-cpu.strategy';
|
||||
|
||||
const uploads: Uint8Array[] = [];
|
||||
|
||||
vi.mock('@babylonjs/core', () => {
|
||||
class FakeMaterial {
|
||||
backFaceCulling = true;
|
||||
disableDepthWrite = false;
|
||||
setTexture(): void { /* display texture is not under test */ }
|
||||
}
|
||||
return {
|
||||
Constants: {TEXTURETYPE_UNSIGNED_BYTE: 0},
|
||||
Texture: {BILINEAR_SAMPLINGMODE: 2, WRAP_ADDRESSMODE: 1},
|
||||
ShaderMaterial: FakeMaterial,
|
||||
MeshBuilder: {CreatePlane: () => ({alwaysSelectAsActiveMesh: false, material: null, lookAt: vi.fn(), dispose: vi.fn()})},
|
||||
RawTexture: {
|
||||
CreateRTexture: () => ({
|
||||
update: (bytes: Uint8Array) => uploads.push(bytes.slice()),
|
||||
dispose: vi.fn()
|
||||
})
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function fakeScene() {
|
||||
const callbacks: (() => void)[] = [];
|
||||
return {
|
||||
activeCamera: null,
|
||||
onBeforeRenderObservable: {clear: () => callbacks.splice(0), add: (cb: () => void) => callbacks.push(cb)},
|
||||
frame: () => callbacks.forEach((cb) => cb())
|
||||
};
|
||||
}
|
||||
|
||||
describe('PhysarumCpuStrategy', () => {
|
||||
it('readsParamsAtFrameStart', () => {
|
||||
uploads.length = 0;
|
||||
const scene = fakeScene();
|
||||
const strategy = new PhysarumCpuStrategy();
|
||||
strategy.init(scene as never, {} as never, {count: 200, mapWidth: 32, mapHeight: 32}, () => ({
|
||||
settings: PRESETS[DEFAULT_PRESET],
|
||||
food: {x: 0, y: 0, active: false}
|
||||
}));
|
||||
|
||||
scene.frame();
|
||||
|
||||
expect(uploads).toHaveLength(1);
|
||||
expect(uploads[0].some((value) => value > 0)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -14,10 +14,12 @@ export class PhysarumCpuStrategy implements PhysarumSimulationStrategy {
|
||||
private trailNext: Float32Array = new Float32Array(0);
|
||||
private bytes = new Uint8Array(0);
|
||||
private readonly params = new Float32Array(PARAMS_LENGTH);
|
||||
private readParams: (() => PhysarumSimulationParams) | null = null;
|
||||
private frame = 0;
|
||||
|
||||
init(scene: Scene, _engine: WebGPUEngine | Engine, config: PhysarumConfig): void {
|
||||
init(scene: Scene, _engine: WebGPUEngine | Engine, config: PhysarumConfig, readParams: () => PhysarumSimulationParams): void {
|
||||
this.scene = scene;
|
||||
this.readParams = readParams;
|
||||
this.config = config;
|
||||
this.agents = seedAgents(config);
|
||||
const cells = config.mapWidth * config.mapHeight;
|
||||
@@ -29,12 +31,6 @@ export class PhysarumCpuStrategy implements PhysarumSimulationStrategy {
|
||||
this.startRenderLoop(scene);
|
||||
}
|
||||
|
||||
updateParams(params: PhysarumSimulationParams): void {
|
||||
if (this.config) {
|
||||
fillParams(this.params, this.config, params, this.frame);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.scene?.onBeforeRenderObservable.clear();
|
||||
this.texture?.dispose();
|
||||
@@ -42,9 +38,16 @@ export class PhysarumCpuStrategy implements PhysarumSimulationStrategy {
|
||||
this.texture = null;
|
||||
this.plane = null;
|
||||
this.config = null;
|
||||
this.readParams = null;
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
private applyParams(params: PhysarumSimulationParams): void {
|
||||
if (this.config) {
|
||||
fillParams(this.params, this.config, params, this.frame);
|
||||
}
|
||||
}
|
||||
|
||||
private setupDisplay(scene: Scene, config: PhysarumConfig): void {
|
||||
const texture = RawTexture.CreateRTexture(this.bytes, config.mapWidth, config.mapHeight, scene, false, false, Texture.BILINEAR_SAMPLINGMODE, Constants.TEXTURETYPE_UNSIGNED_BYTE);
|
||||
texture.wrapU = Texture.WRAP_ADDRESSMODE;
|
||||
@@ -75,10 +78,11 @@ export class PhysarumCpuStrategy implements PhysarumSimulationStrategy {
|
||||
private startRenderLoop(scene: Scene): void {
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
if (!this.texture) {
|
||||
if (!this.texture || !this.readParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyParams(this.readParams());
|
||||
stepAgents(this.agents, this.trail, this.params);
|
||||
diffuseDecay(this.trail, this.trailNext, this.params);
|
||||
[this.trail, this.trailNext] = [this.trailNext, this.trail];
|
||||
|
||||
@@ -31,11 +31,13 @@ export class PhysarumGpuStrategy implements PhysarumSimulationStrategy {
|
||||
private config: PhysarumConfig | null = null;
|
||||
private buffers: GpuBuffers | null = null;
|
||||
private pipelines: GpuPipelines | null = null;
|
||||
private readParams: (() => PhysarumSimulationParams) | null = null;
|
||||
private frame = 0;
|
||||
private readonly paramsData = new Float32Array(PARAMS_LENGTH);
|
||||
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: PhysarumConfig): void {
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: PhysarumConfig, readParams: () => PhysarumSimulationParams): void {
|
||||
this.scene = scene;
|
||||
this.readParams = readParams;
|
||||
this.config = config;
|
||||
const gpuEngine = engine as WebGPUEngine;
|
||||
|
||||
@@ -45,12 +47,6 @@ export class PhysarumGpuStrategy implements PhysarumSimulationStrategy {
|
||||
this.startRenderLoop(scene, config);
|
||||
}
|
||||
|
||||
updateParams(params: PhysarumSimulationParams): void {
|
||||
if (this.config) {
|
||||
fillParams(this.paramsData, this.config, params, this.frame);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.scene?.onBeforeRenderObservable.clear();
|
||||
this.plane?.dispose(false, true);
|
||||
@@ -61,9 +57,16 @@ export class PhysarumGpuStrategy implements PhysarumSimulationStrategy {
|
||||
this.buffers = null;
|
||||
this.pipelines = null;
|
||||
this.config = null;
|
||||
this.readParams = null;
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
private applyParams(params: PhysarumSimulationParams): void {
|
||||
if (this.config) {
|
||||
fillParams(this.paramsData, this.config, params, this.frame);
|
||||
}
|
||||
}
|
||||
|
||||
private createStorageBuffers(engine: WebGPUEngine, config: PhysarumConfig): GpuBuffers {
|
||||
const agentData = seedAgents(config);
|
||||
const agents = new StorageBuffer(engine, agentData.byteLength);
|
||||
@@ -135,10 +138,11 @@ export class PhysarumGpuStrategy implements PhysarumSimulationStrategy {
|
||||
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
if (!this.buffers || !this.pipelines) {
|
||||
if (!this.buffers || !this.pipelines || !this.readParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyParams(this.readParams());
|
||||
this.buffers.params.update(this.paramsData);
|
||||
this.pipelines.agents.dispatch(agentGroups, 1, 1);
|
||||
this.pipelines.diffuse.dispatch(diffuseGroupsX, diffuseGroupsY, 1);
|
||||
|
||||
@@ -7,7 +7,6 @@ export interface PhysarumSimulationParams {
|
||||
}
|
||||
|
||||
export interface PhysarumSimulationStrategy {
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: PhysarumConfig): void;
|
||||
updateParams(params: PhysarumSimulationParams): void;
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: PhysarumConfig, readParams: () => PhysarumSimulationParams): void;
|
||||
dispose(): void;
|
||||
}
|
||||
Reference in new issue
Block a user