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
+495
-174
No files matched your search
@@ -29,7 +29,7 @@ import {
|
|||||||
SwarmMode,
|
SwarmMode,
|
||||||
WORLD_SIZE
|
WORLD_SIZE
|
||||||
} from './boids.model';
|
} 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 {BoidsGpuStrategy} from './strategies/boids-gpu.strategy';
|
||||||
import {BoidsCpuStrategy} from './strategies/boids-cpu.strategy';
|
import {BoidsCpuStrategy} from './strategies/boids-cpu.strategy';
|
||||||
|
|
||||||
@@ -185,15 +185,12 @@ export class BoidsComponent implements OnDestroy {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.strategy = gpuTier === 'webgpu' ? new BoidsGpuStrategy() : new BoidsCpuStrategy();
|
this.strategy = gpuTier === 'webgpu' ? new BoidsGpuStrategy() : new BoidsCpuStrategy();
|
||||||
this.strategy.init(scene, engine, config);
|
this.strategy.init(scene, engine, config, () => this.currentParams(engine));
|
||||||
this.startParamUpdateLoop(scene, engine);
|
this.startFrameLoop(scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
private startParamUpdateLoop(scene: Scene, engine: WebGPUEngine | Engine): void {
|
private currentParams(engine: WebGPUEngine | Engine): SwarmSimulationParams {
|
||||||
scene.onAfterRenderObservable.clear();
|
return {
|
||||||
scene.onAfterRenderObservable.add(() => {
|
|
||||||
this.fpsCounter.tick();
|
|
||||||
this.strategy?.updateParams({
|
|
||||||
deltaTime: engine.getDeltaTime() / 1000,
|
deltaTime: engine.getDeltaTime() / 1000,
|
||||||
separation: this.separation(),
|
separation: this.separation(),
|
||||||
alignment: this.alignment(),
|
alignment: this.alignment(),
|
||||||
@@ -204,8 +201,12 @@ export class BoidsComponent implements OnDestroy {
|
|||||||
interactionRadius: this.interactionRadius(),
|
interactionRadius: this.interactionRadius(),
|
||||||
friction: this.friction(),
|
friction: this.friction(),
|
||||||
forceScale: this.forceScale()
|
forceScale: this.forceScale()
|
||||||
});
|
};
|
||||||
});
|
}
|
||||||
|
|
||||||
|
private startFrameLoop(scene: Scene): void {
|
||||||
|
scene.onAfterRenderObservable.clear();
|
||||||
|
scene.onAfterRenderObservable.add(() => this.fpsCounter.tick());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** onSceneReady runs again when the GPU tier changes, so the observer must be added only once. */
|
/** 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 buffers: SwarmBuffers | null = null;
|
||||||
private matrix: Float32Array = new Float32Array(MAX_TYPES * MAX_TYPES);
|
private matrix: Float32Array = new Float32Array(MAX_TYPES * MAX_TYPES);
|
||||||
private readonly params = new Float32Array(PARAMS_LENGTH);
|
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.scene = scene;
|
||||||
|
this.readParams = readParams;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.matrix = config.matrix;
|
this.matrix = config.matrix;
|
||||||
|
|
||||||
@@ -31,12 +33,6 @@ export class BoidsCpuStrategy implements SwarmSimulationStrategy {
|
|||||||
this.startRenderLoop(scene, config);
|
this.startRenderLoop(scene, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParams(params: SwarmSimulationParams): void {
|
|
||||||
if (this.config) {
|
|
||||||
fillParams(this.params, this.config, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setInteractionMatrix(matrix: Float32Array): void {
|
setInteractionMatrix(matrix: Float32Array): void {
|
||||||
this.matrix = matrix;
|
this.matrix = matrix;
|
||||||
}
|
}
|
||||||
@@ -47,9 +43,16 @@ export class BoidsCpuStrategy implements SwarmSimulationStrategy {
|
|||||||
this.mesh = null;
|
this.mesh = null;
|
||||||
this.buffers = null;
|
this.buffers = null;
|
||||||
this.config = null;
|
this.config = null;
|
||||||
|
this.readParams = null;
|
||||||
this.scene = 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 {
|
private setupRenderMesh(scene: Scene, config: SwarmConfig): void {
|
||||||
const mesh = new Mesh('agents', scene);
|
const mesh = new Mesh('agents', scene);
|
||||||
const vertexData = new VertexData();
|
const vertexData = new VertexData();
|
||||||
@@ -78,10 +81,11 @@ export class BoidsCpuStrategy implements SwarmSimulationStrategy {
|
|||||||
|
|
||||||
scene.onBeforeRenderObservable.clear();
|
scene.onBeforeRenderObservable.clear();
|
||||||
scene.onBeforeRenderObservable.add(() => {
|
scene.onBeforeRenderObservable.add(() => {
|
||||||
if (!this.mesh || !this.buffers) {
|
if (!this.mesh || !this.buffers || !this.readParams) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.applyParams(this.readParams());
|
||||||
copyState(this.buffers, config.count);
|
copyState(this.buffers, config.count);
|
||||||
if (config.mode === 'boids') {
|
if (config.mode === 'boids') {
|
||||||
stepBoids(this.buffers, this.params);
|
stepBoids(this.buffers, this.params);
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ export class BoidsGpuStrategy implements SwarmSimulationStrategy {
|
|||||||
private pipelines: GpuPipelines | null = null;
|
private pipelines: GpuPipelines | null = null;
|
||||||
private dispatchX = 0;
|
private dispatchX = 0;
|
||||||
private readonly paramsData = new Float32Array(PARAMS_LENGTH);
|
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.scene = scene;
|
||||||
|
this.readParams = readParams;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
const gpuEngine = engine as WebGPUEngine;
|
const gpuEngine = engine as WebGPUEngine;
|
||||||
|
|
||||||
@@ -47,12 +49,6 @@ export class BoidsGpuStrategy implements SwarmSimulationStrategy {
|
|||||||
this.startRenderLoop(scene);
|
this.startRenderLoop(scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParams(params: SwarmSimulationParams): void {
|
|
||||||
if (this.config) {
|
|
||||||
fillParams(this.paramsData, this.config, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setInteractionMatrix(matrix: Float32Array): void {
|
setInteractionMatrix(matrix: Float32Array): void {
|
||||||
this.buffers?.matrix.update(matrix);
|
this.buffers?.matrix.update(matrix);
|
||||||
}
|
}
|
||||||
@@ -67,9 +63,16 @@ export class BoidsGpuStrategy implements SwarmSimulationStrategy {
|
|||||||
this.buffers = null;
|
this.buffers = null;
|
||||||
this.pipelines = null;
|
this.pipelines = null;
|
||||||
this.config = null;
|
this.config = null;
|
||||||
|
this.readParams = null;
|
||||||
this.scene = 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 {
|
private createStorageBuffers(engine: WebGPUEngine, config: SwarmConfig): GpuBuffers {
|
||||||
const data = seedAgents(config);
|
const data = seedAgents(config);
|
||||||
const createBuffer = (arrayData: Float32Array): StorageBuffer => {
|
const createBuffer = (arrayData: Float32Array): StorageBuffer => {
|
||||||
@@ -150,10 +153,11 @@ export class BoidsGpuStrategy implements SwarmSimulationStrategy {
|
|||||||
private startRenderLoop(scene: Scene): void {
|
private startRenderLoop(scene: Scene): void {
|
||||||
scene.onBeforeRenderObservable.clear();
|
scene.onBeforeRenderObservable.clear();
|
||||||
scene.onBeforeRenderObservable.add(() => {
|
scene.onBeforeRenderObservable.add(() => {
|
||||||
if (!this.buffers || !this.pipelines) {
|
if (!this.buffers || !this.pipelines || !this.readParams) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.applyParams(this.readParams());
|
||||||
this.buffers.params.update(this.paramsData);
|
this.buffers.params.update(this.paramsData);
|
||||||
this.pipelines.copy.dispatch(this.dispatchX, 1, 1);
|
this.pipelines.copy.dispatch(this.dispatchX, 1, 1);
|
||||||
this.pipelines.step.dispatch(this.dispatchX, 1, 1);
|
this.pipelines.step.dispatch(this.dispatchX, 1, 1);
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ export interface SwarmSimulationParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SwarmSimulationStrategy {
|
export interface SwarmSimulationStrategy {
|
||||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: SwarmConfig): void;
|
init(scene: Scene, engine: WebGPUEngine | Engine, config: SwarmConfig, readParams: () => SwarmSimulationParams): void;
|
||||||
updateParams(params: SwarmSimulationParams): void;
|
|
||||||
/** Re-roll without restart. */
|
/** Re-roll without restart. */
|
||||||
setInteractionMatrix(matrix: Float32Array): void;
|
setInteractionMatrix(matrix: Float32Array): void;
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {Component, computed, signal} from '@angular/core';
|
|||||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||||
import {SliderFieldComponent} from '../../../shared/components/slider-field/slider-field';
|
import {SliderFieldComponent} from '../../../shared/components/slider-field/slider-field';
|
||||||
import {TranslatePipe} from '@ngx-translate/core';
|
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 {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||||
import {MatButton} from '@angular/material/button';
|
import {MatButton} from '@angular/material/button';
|
||||||
import {CLOTH_SUBSTEPS, ClothConfig, ClothStats} from './cloth.model';
|
import {CLOTH_SUBSTEPS, ClothConfig, ClothStats} from './cloth.model';
|
||||||
@@ -9,7 +10,7 @@ import {Information} from '../information/information';
|
|||||||
import {AlgorithmInformation} from '../information/information.models';
|
import {AlgorithmInformation} from '../information/information.models';
|
||||||
import {SOURCE_PATHS} from '../information/source-paths';
|
import {SOURCE_PATHS} from '../information/source-paths';
|
||||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
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 {ClothGpuStrategy} from './strategies/cloth-gpu.strategy';
|
||||||
import {ClothCpuStrategy} from './strategies/cloth-cpu.strategy';
|
import {ClothCpuStrategy} from './strategies/cloth-cpu.strategy';
|
||||||
import {Metric, MetricsComponent} from '../../../shared/components/metrics/metrics';
|
import {Metric, MetricsComponent} from '../../../shared/components/metrics/metrics';
|
||||||
@@ -135,26 +136,26 @@ export class ClothComponent {
|
|||||||
: new ClothCpuStrategy();
|
: new ClothCpuStrategy();
|
||||||
|
|
||||||
this.backend.set(gpuTier === 'webgpu' ? 'WebGPU (Compute)' : 'WebGL + CPU');
|
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.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 {
|
||||||
scene.onAfterRenderObservable.clear();
|
return {
|
||||||
scene.onAfterRenderObservable.add(() => {
|
|
||||||
this.fpsCounter.tick();
|
|
||||||
this.simulationTime += engine.getDeltaTime() / 1000.0;
|
|
||||||
|
|
||||||
if (this.strategy) {
|
|
||||||
this.strategy.updateParams({
|
|
||||||
stiffness: this.stiffness,
|
stiffness: this.stiffness,
|
||||||
elongation: this.elongation(),
|
elongation: this.elongation(),
|
||||||
isWindActive: this.isWindActive(),
|
isWindActive: this.isWindActive(),
|
||||||
simulationTime: this.simulationTime,
|
simulationTime: this.simulationTime,
|
||||||
deltaTime: engine.getDeltaTime() / 1000.0
|
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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 clothMesh: GroundMesh | null = null;
|
||||||
private scene: Scene | null = null;
|
private scene: Scene | null = null;
|
||||||
private config: ClothConfig | null = null;
|
private config: ClothConfig | null = null;
|
||||||
|
private readParams: (() => ClothSimulationParams) | null = null;
|
||||||
|
|
||||||
private positions!: Float32Array;
|
private positions!: Float32Array;
|
||||||
private prevPositions!: Float32Array;
|
private prevPositions!: Float32Array;
|
||||||
@@ -27,8 +28,9 @@ export class ClothCpuStrategy implements ClothSimulationStrategy {
|
|||||||
elongation: 1.0
|
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.scene = scene;
|
||||||
|
this.readParams = readParams;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.physicsParams.numVertices = config.numVertices;
|
this.physicsParams.numVertices = config.numVertices;
|
||||||
|
|
||||||
@@ -47,7 +49,19 @@ export class ClothCpuStrategy implements ClothSimulationStrategy {
|
|||||||
return this.stats;
|
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 windX = params.isWindActive ? 5.0 : 0.0;
|
||||||
const windZ = params.isWindActive ? 15.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;
|
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 {
|
private generateClothData(config: ClothConfig): ClothData {
|
||||||
const positionsData = new Float32Array(config.numVertices * 4);
|
const positionsData = new Float32Array(config.numVertices * 4);
|
||||||
const prevPositionsData = 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 {
|
private startRenderLoop(scene: Scene, config: ClothConfig): void {
|
||||||
scene.onBeforeRenderObservable.clear();
|
scene.onBeforeRenderObservable.clear();
|
||||||
scene.onBeforeRenderObservable.add(() => {
|
scene.onBeforeRenderObservable.add(() => {
|
||||||
if (!this.clothMesh) {
|
if (!this.clothMesh || !this.readParams) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.applyParams(this.readParams());
|
||||||
integratePositions(this.positions, this.prevPositions, this.velocities, this.physicsParams);
|
integratePositions(this.positions, this.prevPositions, this.velocities, this.physicsParams);
|
||||||
|
|
||||||
for (let i = 0; i < CLOTH_SUBSTEPS; i++) {
|
for (let i = 0; i < CLOTH_SUBSTEPS; i++) {
|
||||||
|
|||||||
@@ -32,10 +32,12 @@ export class ClothGpuStrategy implements ClothSimulationStrategy {
|
|||||||
private dispatchXConstraints: number[] = [];
|
private dispatchXConstraints: number[] = [];
|
||||||
private dispatchXVertices = 0;
|
private dispatchXVertices = 0;
|
||||||
private numVertices = 0;
|
private numVertices = 0;
|
||||||
|
private readParams: (() => ClothSimulationParams) | null = null;
|
||||||
private stats: ClothStats = {constraints: 0, phases: 0};
|
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.scene = scene;
|
||||||
|
this.readParams = readParams;
|
||||||
this.numVertices = config.numVertices;
|
this.numVertices = config.numVertices;
|
||||||
const gpuEngine = engine as WebGPUEngine;
|
const gpuEngine = engine as WebGPUEngine;
|
||||||
|
|
||||||
@@ -52,7 +54,21 @@ export class ClothGpuStrategy implements ClothSimulationStrategy {
|
|||||||
return this.stats;
|
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) {
|
if (!this.buffers) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -76,19 +92,6 @@ export class ClothGpuStrategy implements ClothSimulationStrategy {
|
|||||||
this.paramsData[8] = params.elongation;
|
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 {
|
private generateClothData(config: ClothConfig): ClothData {
|
||||||
const positionsData = new Float32Array(config.numVertices * 4);
|
const positionsData = new Float32Array(config.numVertices * 4);
|
||||||
const prevPositionsData = 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 {
|
private startRenderLoop(scene: Scene): void {
|
||||||
scene.onBeforeRenderObservable.clear();
|
scene.onBeforeRenderObservable.clear();
|
||||||
scene.onBeforeRenderObservable.add(() => {
|
scene.onBeforeRenderObservable.add(() => {
|
||||||
if (!this.buffers || !this.pipelines) {
|
if (!this.buffers || !this.pipelines || !this.readParams) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.applyParams(this.readParams());
|
||||||
this.buffers.params.update(this.paramsData);
|
this.buffers.params.update(this.paramsData);
|
||||||
|
|
||||||
this.pipelines.integrate.dispatch(this.dispatchXVertices, 1, 1);
|
this.pipelines.integrate.dispatch(this.dispatchXVertices, 1, 1);
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ export interface ClothSimulationParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ClothSimulationStrategy {
|
export interface ClothSimulationStrategy {
|
||||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: ClothConfig): void;
|
init(scene: Scene, engine: WebGPUEngine | Engine, config: ClothConfig, readParams: () => ClothSimulationParams): void;
|
||||||
updateParams(params: ClothSimulationParams): void;
|
|
||||||
getMesh(): GroundMesh | null;
|
getMesh(): GroundMesh | null;
|
||||||
getStats(): ClothStats;
|
getStats(): ClothStats;
|
||||||
dispose(): void;
|
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 next: RenderTargetTexture | null = null;
|
||||||
private material: ShaderMaterial | null = null;
|
private material: ShaderMaterial | null = null;
|
||||||
private plane: Mesh | 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;
|
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.scene = scene;
|
||||||
|
this.readParams = readParams;
|
||||||
this.renderer = new EffectRenderer(engine);
|
this.renderer = new EffectRenderer(engine);
|
||||||
this.current = createStateTarget('grayScottA', scene, GRID_SIZE, textureType);
|
this.current = createStateTarget('grayScottA', scene, GRID_SIZE, textureType);
|
||||||
this.next = createStateTarget('grayScottB', scene, GRID_SIZE, textureType);
|
this.next = createStateTarget('grayScottB', scene, GRID_SIZE, textureType);
|
||||||
@@ -48,10 +50,6 @@ export class GrayScottSimulation {
|
|||||||
scene.onBeforeRenderObservable.add(() => this.frame());
|
scene.onBeforeRenderObservable.add(() => this.frame());
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParams(params: GrayScottParams): void {
|
|
||||||
this.params = params;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The next frame runs the seed pass again. Textures and shaders stay. */
|
/** The next frame runs the seed pass again. Textures and shaders stay. */
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.seeded = false;
|
this.seeded = false;
|
||||||
@@ -72,7 +70,8 @@ export class GrayScottSimulation {
|
|||||||
this.next = null;
|
this.next = null;
|
||||||
this.material = null;
|
this.material = null;
|
||||||
this.plane = null;
|
this.plane = null;
|
||||||
this.params = null;
|
this.readParams = null;
|
||||||
|
this.frameParams = null;
|
||||||
this.scene = null;
|
this.scene = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,10 +100,10 @@ export class GrayScottSimulation {
|
|||||||
|
|
||||||
private applyStepUniforms(): void {
|
private applyStepUniforms(): void {
|
||||||
const effect = this.stepWrapper?.effect;
|
const effect = this.stepWrapper?.effect;
|
||||||
if (!effect || !this.params || !this.current) {
|
if (!effect || !this.frameParams || !this.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {settings, brush} = this.params;
|
const {settings, brush} = this.frameParams;
|
||||||
effect.setTexture('state', this.current);
|
effect.setTexture('state', this.current);
|
||||||
effect.setFloat('texel', 1 / GRID_SIZE);
|
effect.setFloat('texel', 1 / GRID_SIZE);
|
||||||
effect.setFloat('feed', settings.feed);
|
effect.setFloat('feed', settings.feed);
|
||||||
@@ -116,9 +115,11 @@ export class GrayScottSimulation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private frame(): void {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
// Read before the seed pass, so a new seed never steps with old params.
|
||||||
|
this.frameParams = this.readParams();
|
||||||
if (!this.seeded) {
|
if (!this.seeded) {
|
||||||
if (!this.seedWrapper.effect.isReady()) {
|
if (!this.seedWrapper.effect.isReady()) {
|
||||||
return;
|
return;
|
||||||
@@ -130,7 +131,7 @@ export class GrayScottSimulation {
|
|||||||
if (!this.stepWrapper.effect.isReady()) {
|
if (!this.stepWrapper.effect.isReady()) {
|
||||||
return;
|
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.renderer.render(this.stepWrapper, this.next);
|
||||||
[this.current, this.next] = [this.next, this.current];
|
[this.current, this.next] = [this.next, this.current];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
SETTINGS_LIMITS,
|
SETTINGS_LIMITS,
|
||||||
WORLD_SIZE
|
WORLD_SIZE
|
||||||
} from './gray-scott.model';
|
} from './gray-scott.model';
|
||||||
import {GrayScottSimulation} from './gray-scott-simulation';
|
import {GrayScottParams, GrayScottSimulation} from './gray-scott-simulation';
|
||||||
|
|
||||||
interface SettingSlider {
|
interface SettingSlider {
|
||||||
key: keyof GrayScottSettings;
|
key: keyof GrayScottSettings;
|
||||||
@@ -143,8 +143,8 @@ export class GrayScottComponent implements OnDestroy {
|
|||||||
this.backend.set(`${backend} (${type === 'float' ? 'float32' : 'float16'})`);
|
this.backend.set(`${backend} (${type === 'float' ? 'float32' : 'float16'})`);
|
||||||
this.simulation?.dispose();
|
this.simulation?.dispose();
|
||||||
this.simulation = new GrayScottSimulation();
|
this.simulation = new GrayScottSimulation();
|
||||||
this.simulation.init(event.scene, event.engine, type);
|
this.simulation.init(event.scene, event.engine, type, () => this.currentParams());
|
||||||
this.startParamUpdateLoop(event.scene);
|
this.startFrameLoop(event.scene);
|
||||||
this.attachPointer(event.scene);
|
this.attachPointer(event.scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,11 +167,8 @@ export class GrayScottComponent implements OnDestroy {
|
|||||||
this.simulation?.reset();
|
this.simulation?.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
private startParamUpdateLoop(scene: Scene): void {
|
private currentParams(): GrayScottParams {
|
||||||
scene.onAfterRenderObservable.clear();
|
return {
|
||||||
scene.onAfterRenderObservable.add(() => {
|
|
||||||
this.fpsCounter.tick();
|
|
||||||
this.simulation?.updateParams({
|
|
||||||
settings: {
|
settings: {
|
||||||
feed: this.feed(),
|
feed: this.feed(),
|
||||||
kill: this.kill(),
|
kill: this.kill(),
|
||||||
@@ -180,8 +177,12 @@ export class GrayScottComponent implements OnDestroy {
|
|||||||
stepsPerFrame: this.stepsPerFrame()
|
stepsPerFrame: this.stepsPerFrame()
|
||||||
},
|
},
|
||||||
brush: this.brush
|
brush: this.brush
|
||||||
});
|
};
|
||||||
});
|
}
|
||||||
|
|
||||||
|
private startFrameLoop(scene: Scene): void {
|
||||||
|
scene.onAfterRenderObservable.clear();
|
||||||
|
scene.onAfterRenderObservable.add(() => this.fpsCounter.tick());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** onSceneReady runs again when the GPU tier changes, so the observer must be added only once. */
|
/** 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 {Component, computed, signal} from '@angular/core';
|
||||||
|
import {Scene} from '@babylonjs/core';
|
||||||
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
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 {AlgorithmInformation} from '../information/information.models';
|
||||||
import {SOURCE_PATHS} from '../information/source-paths';
|
import {SOURCE_PATHS} from '../information/source-paths';
|
||||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
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 {PendulumGpuStrategy} from './strategies/pendulum-gpu.strategy';
|
||||||
import {PendulumCpuStrategy} from './strategies/pendulum-cpu.strategy';
|
import {PendulumCpuStrategy} from './strategies/pendulum-cpu.strategy';
|
||||||
import {Metric, MetricsComponent} from '../../../shared/components/metrics/metrics';
|
import {Metric, MetricsComponent} from '../../../shared/components/metrics/metrics';
|
||||||
@@ -113,18 +114,12 @@ class PendulumComponent {
|
|||||||
: new PendulumCpuStrategy();
|
: new PendulumCpuStrategy();
|
||||||
|
|
||||||
this.backend.set(gpuTier === 'webgpu' ? 'WebGPU (Compute)' : 'WebGL + CPU');
|
this.backend.set(gpuTier === 'webgpu' ? 'WebGPU (Compute)' : 'WebGL + CPU');
|
||||||
this.strategy.init(scene, engine);
|
this.strategy.init(scene, engine, () => this.currentParams());
|
||||||
this.startParamUpdateLoop(scene);
|
this.startFrameLoop(scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
private startParamUpdateLoop(scene: any): void {
|
private currentParams(): PendulumSimParams {
|
||||||
scene.onAfterRenderObservable.clear();
|
return {
|
||||||
scene.onAfterRenderObservable.add(() => {
|
|
||||||
this.fpsCounter.tick();
|
|
||||||
this.loop.time += this.loop.dt;
|
|
||||||
|
|
||||||
if (this.strategy) {
|
|
||||||
this.strategy.updateParams({
|
|
||||||
...this.loop,
|
...this.loop,
|
||||||
g: this.g(),
|
g: this.g(),
|
||||||
m1: this.m1(),
|
m1: this.m1(),
|
||||||
@@ -133,9 +128,14 @@ class PendulumComponent {
|
|||||||
l2: this.l2(),
|
l2: this.l2(),
|
||||||
damping: this.damping(),
|
damping: this.damping(),
|
||||||
trailDecay: this.trailDecay()
|
trailDecay: this.trailDecay()
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private startFrameLoop(scene: Scene): void {
|
||||||
|
scene.onAfterRenderObservable.clear();
|
||||||
|
scene.onAfterRenderObservable.add(() => {
|
||||||
|
this.fpsCounter.tick();
|
||||||
|
this.loop.time += this.loop.dt;
|
||||||
this.resetImpulses();
|
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 {
|
export class PendulumCpuStrategy implements PendulumSimulationStrategy {
|
||||||
private scene: Scene | null = null;
|
private scene: Scene | null = null;
|
||||||
private state: PendulumState = {theta1: Math.PI / 4, theta2: Math.PI / 2, v1: 0, v2: 0};
|
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 feedbackMaterial: ShaderMaterial | null = null;
|
||||||
private displayMaterial: ShaderMaterial | null = null;
|
private displayMaterial: ShaderMaterial | null = null;
|
||||||
private rttA: RenderTargetTexture | null = null;
|
private rttA: RenderTargetTexture | null = null;
|
||||||
private rttB: RenderTargetTexture | null = null;
|
private rttB: RenderTargetTexture | null = null;
|
||||||
private pingPong = false;
|
private pingPong = false;
|
||||||
|
|
||||||
init(scene: Scene, engine: WebGPUEngine | Engine): void {
|
init(scene: Scene, engine: WebGPUEngine | Engine, readParams: () => PendulumSimParams): void {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
|
this.readParams = readParams;
|
||||||
|
|
||||||
const existingPlane = scene.getMeshByName("plane");
|
const existingPlane = scene.getMeshByName("plane");
|
||||||
if (existingPlane) {
|
if (existingPlane) {
|
||||||
@@ -72,10 +73,6 @@ export class PendulumCpuStrategy implements PendulumSimulationStrategy {
|
|||||||
this.startRenderLoop(scene, width, height);
|
this.startRenderLoop(scene, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParams(params: PendulumSimParams): void {
|
|
||||||
this.params = params;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
this.scene.onBeforeRenderObservable.clear();
|
this.scene.onBeforeRenderObservable.clear();
|
||||||
@@ -84,6 +81,7 @@ export class PendulumCpuStrategy implements PendulumSimulationStrategy {
|
|||||||
this.rttB?.dispose();
|
this.rttB?.dispose();
|
||||||
this.feedbackMaterial?.dispose();
|
this.feedbackMaterial?.dispose();
|
||||||
this.displayMaterial?.dispose();
|
this.displayMaterial?.dispose();
|
||||||
|
this.readParams = null;
|
||||||
this.scene = null;
|
this.scene = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,21 +90,13 @@ export class PendulumCpuStrategy implements PendulumSimulationStrategy {
|
|||||||
|
|
||||||
scene.onBeforeRenderObservable.clear();
|
scene.onBeforeRenderObservable.clear();
|
||||||
scene.onBeforeRenderObservable.add(() => {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
stepPendulumPhysics(this.state, {
|
const params = this.readParams();
|
||||||
dt: this.params.dt,
|
|
||||||
g: this.params.g,
|
stepPendulumPhysics(this.state, params);
|
||||||
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 readTarget = this.pingPong ? this.rttA : this.rttB;
|
const readTarget = this.pingPong ? this.rttA : this.rttB;
|
||||||
const writeTarget = this.pingPong ? this.rttB : this.rttA;
|
const writeTarget = this.pingPong ? this.rttB : this.rttA;
|
||||||
@@ -115,9 +105,9 @@ export class PendulumCpuStrategy implements PendulumSimulationStrategy {
|
|||||||
this.feedbackMaterial.setVector2("resolution", resolution);
|
this.feedbackMaterial.setVector2("resolution", resolution);
|
||||||
this.feedbackMaterial.setFloat("theta1", this.state.theta1);
|
this.feedbackMaterial.setFloat("theta1", this.state.theta1);
|
||||||
this.feedbackMaterial.setFloat("theta2", this.state.theta2);
|
this.feedbackMaterial.setFloat("theta2", this.state.theta2);
|
||||||
this.feedbackMaterial.setFloat("l1", this.params.l1);
|
this.feedbackMaterial.setFloat("l1", params.l1);
|
||||||
this.feedbackMaterial.setFloat("l2", this.params.l2);
|
this.feedbackMaterial.setFloat("l2", params.l2);
|
||||||
this.feedbackMaterial.setFloat("trailDecay", this.params.trailDecay);
|
this.feedbackMaterial.setFloat("trailDecay", params.trailDecay);
|
||||||
|
|
||||||
writeTarget.render();
|
writeTarget.render();
|
||||||
|
|
||||||
@@ -125,8 +115,8 @@ export class PendulumCpuStrategy implements PendulumSimulationStrategy {
|
|||||||
this.displayMaterial.setVector2("resolution", resolution);
|
this.displayMaterial.setVector2("resolution", resolution);
|
||||||
this.displayMaterial.setFloat("theta1", this.state.theta1);
|
this.displayMaterial.setFloat("theta1", this.state.theta1);
|
||||||
this.displayMaterial.setFloat("theta2", this.state.theta2);
|
this.displayMaterial.setFloat("theta2", this.state.theta2);
|
||||||
this.displayMaterial.setFloat("l1", this.params.l1);
|
this.displayMaterial.setFloat("l1", params.l1);
|
||||||
this.displayMaterial.setFloat("l2", this.params.l2);
|
this.displayMaterial.setFloat("l2", params.l2);
|
||||||
|
|
||||||
this.pingPong = !this.pingPong;
|
this.pingPong = !this.pingPong;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ export class PendulumGpuStrategy implements PendulumSimulationStrategy {
|
|||||||
private scene: Scene | null = null;
|
private scene: Scene | null = null;
|
||||||
private paramsData = new Float32Array(14);
|
private paramsData = new Float32Array(14);
|
||||||
private paramsBuffer: StorageBuffer | null = null;
|
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.scene = scene;
|
||||||
|
this.readParams = readParams;
|
||||||
const gpuEngine = engine as WebGPUEngine;
|
const gpuEngine = engine as WebGPUEngine;
|
||||||
gpuEngine.resize();
|
gpuEngine.resize();
|
||||||
|
|
||||||
@@ -63,10 +65,11 @@ export class PendulumGpuStrategy implements PendulumSimulationStrategy {
|
|||||||
|
|
||||||
scene.onBeforeRenderObservable.clear();
|
scene.onBeforeRenderObservable.clear();
|
||||||
scene.onBeforeRenderObservable.add(() => {
|
scene.onBeforeRenderObservable.add(() => {
|
||||||
if (!this.paramsBuffer) {
|
if (!this.paramsBuffer || !this.readParams) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.applyParams(this.readParams());
|
||||||
const currentWidth = gpuEngine.getRenderWidth();
|
const currentWidth = gpuEngine.getRenderWidth();
|
||||||
const currentHeight = gpuEngine.getRenderHeight();
|
const currentHeight = gpuEngine.getRenderHeight();
|
||||||
this.paramsData[0] = currentWidth;
|
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
|
// paramsData[0] (width) and paramsData[1] (height) are set in the render loop
|
||||||
this.paramsData[2] = params.time;
|
this.paramsData[2] = params.time;
|
||||||
this.paramsData[3] = params.dt;
|
this.paramsData[3] = params.dt;
|
||||||
@@ -96,17 +113,4 @@ export class PendulumGpuStrategy implements PendulumSimulationStrategy {
|
|||||||
this.paramsData[12] = params.impulseM2;
|
this.paramsData[12] = params.impulseM2;
|
||||||
this.paramsData[13] = 0;
|
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 {
|
export interface PendulumSimulationStrategy {
|
||||||
init(scene: Scene, engine: WebGPUEngine | Engine): void;
|
init(scene: Scene, engine: WebGPUEngine | Engine, readParams: () => PendulumSimParams): void;
|
||||||
updateParams(params: PendulumSimParams): void;
|
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
SETTINGS_LIMITS,
|
SETTINGS_LIMITS,
|
||||||
WORLD_SIZE
|
WORLD_SIZE
|
||||||
} from './physarum.model';
|
} 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 {PhysarumGpuStrategy} from './strategies/physarum-gpu.strategy';
|
||||||
import {PhysarumCpuStrategy} from './strategies/physarum-cpu.strategy';
|
import {PhysarumCpuStrategy} from './strategies/physarum-cpu.strategy';
|
||||||
|
|
||||||
@@ -195,15 +195,12 @@ export class PhysarumComponent implements OnDestroy {
|
|||||||
|
|
||||||
const config: PhysarumConfig = {count: this.agentCount(), mapWidth: this.mapSize(), mapHeight: this.mapSize()};
|
const config: PhysarumConfig = {count: this.agentCount(), mapWidth: this.mapSize(), mapHeight: this.mapSize()};
|
||||||
this.strategy = gpuTier === 'webgpu' ? new PhysarumGpuStrategy() : new PhysarumCpuStrategy();
|
this.strategy = gpuTier === 'webgpu' ? new PhysarumGpuStrategy() : new PhysarumCpuStrategy();
|
||||||
this.strategy.init(scene, engine, config);
|
this.strategy.init(scene, engine, config, () => this.currentParams());
|
||||||
this.startParamUpdateLoop(scene);
|
this.startFrameLoop(scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
private startParamUpdateLoop(scene: Scene): void {
|
private currentParams(): PhysarumSimulationParams {
|
||||||
scene.onAfterRenderObservable.clear();
|
return {
|
||||||
scene.onAfterRenderObservable.add(() => {
|
|
||||||
this.fpsCounter.tick();
|
|
||||||
this.strategy?.updateParams({
|
|
||||||
settings: {
|
settings: {
|
||||||
sensorAngle: this.sensorAngle(),
|
sensorAngle: this.sensorAngle(),
|
||||||
sensorDistance: this.sensorDistance(),
|
sensorDistance: this.sensorDistance(),
|
||||||
@@ -214,8 +211,12 @@ export class PhysarumComponent implements OnDestroy {
|
|||||||
diffuse: this.diffuse()
|
diffuse: this.diffuse()
|
||||||
},
|
},
|
||||||
food: this.food
|
food: this.food
|
||||||
});
|
};
|
||||||
});
|
}
|
||||||
|
|
||||||
|
private startFrameLoop(scene: Scene): void {
|
||||||
|
scene.onAfterRenderObservable.clear();
|
||||||
|
scene.onAfterRenderObservable.add(() => this.fpsCounter.tick());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** onSceneReady runs again when the GPU tier changes, so the observer must be added only once. */
|
/** 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 trailNext: Float32Array = new Float32Array(0);
|
||||||
private bytes = new Uint8Array(0);
|
private bytes = new Uint8Array(0);
|
||||||
private readonly params = new Float32Array(PARAMS_LENGTH);
|
private readonly params = new Float32Array(PARAMS_LENGTH);
|
||||||
|
private readParams: (() => PhysarumSimulationParams) | null = null;
|
||||||
private frame = 0;
|
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.scene = scene;
|
||||||
|
this.readParams = readParams;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.agents = seedAgents(config);
|
this.agents = seedAgents(config);
|
||||||
const cells = config.mapWidth * config.mapHeight;
|
const cells = config.mapWidth * config.mapHeight;
|
||||||
@@ -29,12 +31,6 @@ export class PhysarumCpuStrategy implements PhysarumSimulationStrategy {
|
|||||||
this.startRenderLoop(scene);
|
this.startRenderLoop(scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParams(params: PhysarumSimulationParams): void {
|
|
||||||
if (this.config) {
|
|
||||||
fillParams(this.params, this.config, params, this.frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
this.scene?.onBeforeRenderObservable.clear();
|
this.scene?.onBeforeRenderObservable.clear();
|
||||||
this.texture?.dispose();
|
this.texture?.dispose();
|
||||||
@@ -42,9 +38,16 @@ export class PhysarumCpuStrategy implements PhysarumSimulationStrategy {
|
|||||||
this.texture = null;
|
this.texture = null;
|
||||||
this.plane = null;
|
this.plane = null;
|
||||||
this.config = null;
|
this.config = null;
|
||||||
|
this.readParams = null;
|
||||||
this.scene = 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 {
|
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);
|
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;
|
texture.wrapU = Texture.WRAP_ADDRESSMODE;
|
||||||
@@ -75,10 +78,11 @@ export class PhysarumCpuStrategy implements PhysarumSimulationStrategy {
|
|||||||
private startRenderLoop(scene: Scene): void {
|
private startRenderLoop(scene: Scene): void {
|
||||||
scene.onBeforeRenderObservable.clear();
|
scene.onBeforeRenderObservable.clear();
|
||||||
scene.onBeforeRenderObservable.add(() => {
|
scene.onBeforeRenderObservable.add(() => {
|
||||||
if (!this.texture) {
|
if (!this.texture || !this.readParams) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.applyParams(this.readParams());
|
||||||
stepAgents(this.agents, this.trail, this.params);
|
stepAgents(this.agents, this.trail, this.params);
|
||||||
diffuseDecay(this.trail, this.trailNext, this.params);
|
diffuseDecay(this.trail, this.trailNext, this.params);
|
||||||
[this.trail, this.trailNext] = [this.trailNext, this.trail];
|
[this.trail, this.trailNext] = [this.trailNext, this.trail];
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ export class PhysarumGpuStrategy implements PhysarumSimulationStrategy {
|
|||||||
private config: PhysarumConfig | null = null;
|
private config: PhysarumConfig | null = null;
|
||||||
private buffers: GpuBuffers | null = null;
|
private buffers: GpuBuffers | null = null;
|
||||||
private pipelines: GpuPipelines | null = null;
|
private pipelines: GpuPipelines | null = null;
|
||||||
|
private readParams: (() => PhysarumSimulationParams) | null = null;
|
||||||
private frame = 0;
|
private frame = 0;
|
||||||
private readonly paramsData = new Float32Array(PARAMS_LENGTH);
|
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.scene = scene;
|
||||||
|
this.readParams = readParams;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
const gpuEngine = engine as WebGPUEngine;
|
const gpuEngine = engine as WebGPUEngine;
|
||||||
|
|
||||||
@@ -45,12 +47,6 @@ export class PhysarumGpuStrategy implements PhysarumSimulationStrategy {
|
|||||||
this.startRenderLoop(scene, config);
|
this.startRenderLoop(scene, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParams(params: PhysarumSimulationParams): void {
|
|
||||||
if (this.config) {
|
|
||||||
fillParams(this.paramsData, this.config, params, this.frame);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
this.scene?.onBeforeRenderObservable.clear();
|
this.scene?.onBeforeRenderObservable.clear();
|
||||||
this.plane?.dispose(false, true);
|
this.plane?.dispose(false, true);
|
||||||
@@ -61,9 +57,16 @@ export class PhysarumGpuStrategy implements PhysarumSimulationStrategy {
|
|||||||
this.buffers = null;
|
this.buffers = null;
|
||||||
this.pipelines = null;
|
this.pipelines = null;
|
||||||
this.config = null;
|
this.config = null;
|
||||||
|
this.readParams = null;
|
||||||
this.scene = 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 {
|
private createStorageBuffers(engine: WebGPUEngine, config: PhysarumConfig): GpuBuffers {
|
||||||
const agentData = seedAgents(config);
|
const agentData = seedAgents(config);
|
||||||
const agents = new StorageBuffer(engine, agentData.byteLength);
|
const agents = new StorageBuffer(engine, agentData.byteLength);
|
||||||
@@ -135,10 +138,11 @@ export class PhysarumGpuStrategy implements PhysarumSimulationStrategy {
|
|||||||
|
|
||||||
scene.onBeforeRenderObservable.clear();
|
scene.onBeforeRenderObservable.clear();
|
||||||
scene.onBeforeRenderObservable.add(() => {
|
scene.onBeforeRenderObservable.add(() => {
|
||||||
if (!this.buffers || !this.pipelines) {
|
if (!this.buffers || !this.pipelines || !this.readParams) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.applyParams(this.readParams());
|
||||||
this.buffers.params.update(this.paramsData);
|
this.buffers.params.update(this.paramsData);
|
||||||
this.pipelines.agents.dispatch(agentGroups, 1, 1);
|
this.pipelines.agents.dispatch(agentGroups, 1, 1);
|
||||||
this.pipelines.diffuse.dispatch(diffuseGroupsX, diffuseGroupsY, 1);
|
this.pipelines.diffuse.dispatch(diffuseGroupsX, diffuseGroupsY, 1);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export interface PhysarumSimulationParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PhysarumSimulationStrategy {
|
export interface PhysarumSimulationStrategy {
|
||||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: PhysarumConfig): void;
|
init(scene: Scene, engine: WebGPUEngine | Engine, config: PhysarumConfig, readParams: () => PhysarumSimulationParams): void;
|
||||||
updateParams(params: PhysarumSimulationParams): void;
|
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user