fix(lenia): read params at the start of each frame
A preset switch set params after the frame, so the first frame used the old mu/sigma. This broke Scutium and made Gyrorbium unstable. Now LeniaSimulation reads params at each frame start (readParams). Add a ROADMAP item for a display zoom (Bicaudatus tails).
This commit is contained in:
1 parent
2df5ef2089
commit
267d9ab8eb
4 files changed
+48
-24
No files matched your search
@@ -19,6 +19,7 @@ Open ideas for the playground frontend. Tick a box when done.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
- [ ] Lenia: display zoom so small creatures (e.g. Orbium bicaudatus tails) are visible
|
||||||
- [ ] **Shareable demo state**: keep demo settings in query params (fractal zoom/center, sorting algorithm, seeds). Opening the link restores the view.
|
- [ ] **Shareable demo state**: keep demo settings in query params (fractal zoom/center, sorting algorithm, seeds). Opening the link restores the view.
|
||||||
- [ ] **New algorithm demos** (pattern: route + `RouterConstants` + `AlgorithmsService` entry + i18n + information panel):
|
- [ ] **New algorithm demos** (pattern: route + `RouterConstants` + `AlgorithmsService` entry + i18n + information panel):
|
||||||
- [x] Boids / flocking (GPU path like cloth)
|
- [x] Boids / flocking (GPU path like cloth)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {LeniaSimulation} from './lenia-simulation';
|
import {LeniaParams, LeniaSimulation} from './lenia-simulation';
|
||||||
import {DEFAULT_SETTINGS} from './lenia.model';
|
import {DEFAULT_SETTINGS} from './lenia.model';
|
||||||
|
|
||||||
const renderCalls: {name: string; target: unknown}[] = [];
|
const renderCalls: {name: string; target: unknown}[] = [];
|
||||||
@@ -59,6 +59,10 @@ function fakeScene() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function paramsWithSteps(stepsPerFrame: number): LeniaParams {
|
||||||
|
return {settings: {...DEFAULT_SETTINGS, stepsPerFrame}, kernelSum: 1, brush: {u: 0, v: 0, active: false}};
|
||||||
|
}
|
||||||
|
|
||||||
describe('LeniaSimulation', () => {
|
describe('LeniaSimulation', () => {
|
||||||
it('keepsSeedWhileStepShaderCompiles', () => {
|
it('keepsSeedWhileStepShaderCompiles', () => {
|
||||||
renderCalls.length = 0;
|
renderCalls.length = 0;
|
||||||
@@ -66,8 +70,7 @@ describe('LeniaSimulation', () => {
|
|||||||
ready['leniaStep'] = false;
|
ready['leniaStep'] = false;
|
||||||
const scene = fakeScene();
|
const scene = fakeScene();
|
||||||
const simulation = new LeniaSimulation();
|
const simulation = new LeniaSimulation();
|
||||||
simulation.init(scene as never, {} as never, 'float');
|
simulation.init(scene as never, {} as never, 'float', () => paramsWithSteps(1));
|
||||||
simulation.updateParams({settings: {...DEFAULT_SETTINGS, stepsPerFrame: 1}, kernelSum: 1, brush: {u: 0, v: 0, active: false}});
|
|
||||||
simulation.seed(new Uint8Array(4));
|
simulation.seed(new Uint8Array(4));
|
||||||
|
|
||||||
scene.frame();
|
scene.frame();
|
||||||
@@ -81,4 +84,22 @@ describe('LeniaSimulation', () => {
|
|||||||
expect(firstStep.name).toBe('leniaStep');
|
expect(firstStep.name).toBe('leniaStep');
|
||||||
expect(firstStep.target).not.toBe(seededTarget);
|
expect(firstStep.target).not.toBe(seededTarget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('readsParamsAtFrameStart', () => {
|
||||||
|
renderCalls.length = 0;
|
||||||
|
ready['leniaCopy'] = true;
|
||||||
|
ready['leniaStep'] = true;
|
||||||
|
let stepsPerFrame = 1;
|
||||||
|
const scene = fakeScene();
|
||||||
|
const simulation = new LeniaSimulation();
|
||||||
|
simulation.init(scene as never, {} as never, 'float', () => paramsWithSteps(stepsPerFrame));
|
||||||
|
simulation.seed(new Uint8Array(4));
|
||||||
|
scene.frame();
|
||||||
|
|
||||||
|
stepsPerFrame = 3;
|
||||||
|
renderCalls.length = 0;
|
||||||
|
scene.frame();
|
||||||
|
|
||||||
|
expect(renderCalls.filter((call) => call.name === 'leniaStep')).toHaveLength(3);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -25,12 +25,14 @@ export class LeniaSimulation {
|
|||||||
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: LeniaParams | null = null;
|
private readParams: (() => LeniaParams) | null = null;
|
||||||
|
private frameParams: LeniaParams | null = null;
|
||||||
private seeded = false;
|
private seeded = false;
|
||||||
private readonly massBuffer = new Float32Array(GRID_SIZE * GRID_SIZE * FLOATS_PER_PIXEL);
|
private readonly massBuffer = new Float32Array(GRID_SIZE * GRID_SIZE * FLOATS_PER_PIXEL);
|
||||||
|
|
||||||
init(scene: Scene, engine: WebGPUEngine | Engine, textureType: StateTextureType): void {
|
init(scene: Scene, engine: WebGPUEngine | Engine, textureType: StateTextureType, readParams: () => LeniaParams): void {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
|
this.readParams = readParams;
|
||||||
this.renderer = new EffectRenderer(engine);
|
this.renderer = new EffectRenderer(engine);
|
||||||
this.current = createStateTarget('leniaA', scene, GRID_SIZE, textureType);
|
this.current = createStateTarget('leniaA', scene, GRID_SIZE, textureType);
|
||||||
this.next = createStateTarget('leniaB', scene, GRID_SIZE, textureType);
|
this.next = createStateTarget('leniaB', scene, GRID_SIZE, textureType);
|
||||||
@@ -61,10 +63,6 @@ export class LeniaSimulation {
|
|||||||
scene.onBeforeRenderObservable.add(() => this.frame());
|
scene.onBeforeRenderObservable.add(() => this.frame());
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParams(params: LeniaParams): void {
|
|
||||||
this.params = params;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Uploads a start field (one byte per cell). The next frame copies it into the state texture. */
|
/** Uploads a start field (one byte per cell). The next frame copies it into the state texture. */
|
||||||
seed(fieldData: Uint8Array): void {
|
seed(fieldData: Uint8Array): void {
|
||||||
this.field?.update(fieldData);
|
this.field?.update(fieldData);
|
||||||
@@ -109,7 +107,8 @@ export class LeniaSimulation {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,10 +137,10 @@ export class LeniaSimulation {
|
|||||||
|
|
||||||
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, kernelSum, brush} = this.params;
|
const {settings, kernelSum, 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.setInt('radius', Math.min(settings.radius, MAX_RADIUS));
|
effect.setInt('radius', Math.min(settings.radius, MAX_RADIUS));
|
||||||
@@ -154,9 +153,11 @@ export class LeniaSimulation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private frame(): void {
|
private frame(): void {
|
||||||
if (!this.renderer || !this.copyWrapper || !this.stepWrapper || !this.current || !this.next || !this.params) {
|
if (!this.renderer || !this.copyWrapper || !this.stepWrapper || !this.current || !this.next || !this.readParams) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Read before the seed copy, so a new seed never steps with old params.
|
||||||
|
this.frameParams = this.readParams();
|
||||||
if (!this.seeded) {
|
if (!this.seeded) {
|
||||||
if (!this.copyWrapper.effect.isReady()) {
|
if (!this.copyWrapper.effect.isReady()) {
|
||||||
return;
|
return;
|
||||||
@@ -168,7 +169,7 @@ export class LeniaSimulation {
|
|||||||
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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
} from './lenia.model';
|
} from './lenia.model';
|
||||||
import {kernelSum} from './lenia-kernel';
|
import {kernelSum} from './lenia-kernel';
|
||||||
import {buildSeedField, decodeRle, SeedKind} from './lenia-seed';
|
import {buildSeedField, decodeRle, SeedKind} from './lenia-seed';
|
||||||
import {LeniaSimulation} from './lenia-simulation';
|
import {LeniaParams, LeniaSimulation} from './lenia-simulation';
|
||||||
|
|
||||||
interface SettingSlider {
|
interface SettingSlider {
|
||||||
key: keyof LeniaSettings;
|
key: keyof LeniaSettings;
|
||||||
@@ -153,9 +153,9 @@ export class LeniaComponent 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 LeniaSimulation();
|
this.simulation = new LeniaSimulation();
|
||||||
this.simulation.init(event.scene, event.engine, type);
|
this.simulation.init(event.scene, event.engine, type, () => this.currentParams());
|
||||||
this.seed('creature');
|
this.seed('creature');
|
||||||
this.startParamUpdateLoop(event.scene);
|
this.startFrameLoop(event.scene);
|
||||||
this.attachPointer(event.scene);
|
this.attachPointer(event.scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,11 +189,8 @@ export class LeniaComponent implements OnDestroy {
|
|||||||
this.simulation?.seed(buildSeedField(kind, pattern, GRID_SIZE));
|
this.simulation?.seed(buildSeedField(kind, pattern, GRID_SIZE));
|
||||||
}
|
}
|
||||||
|
|
||||||
private startParamUpdateLoop(scene: Scene): void {
|
private currentParams(): LeniaParams {
|
||||||
scene.onAfterRenderObservable.clear();
|
return {
|
||||||
scene.onAfterRenderObservable.add(() => {
|
|
||||||
this.fpsCounter.tick();
|
|
||||||
this.simulation?.updateParams({
|
|
||||||
settings: {
|
settings: {
|
||||||
radius: this.radius(),
|
radius: this.radius(),
|
||||||
mu: this.mu(),
|
mu: this.mu(),
|
||||||
@@ -203,8 +200,12 @@ export class LeniaComponent implements OnDestroy {
|
|||||||
},
|
},
|
||||||
kernelSum: this.kernelSum(),
|
kernelSum: this.kernelSum(),
|
||||||
brush: this.brush
|
brush: this.brush
|
||||||
});
|
};
|
||||||
});
|
}
|
||||||
|
|
||||||
|
private startFrameLoop(scene: Scene): void {
|
||||||
|
scene.onAfterRenderObservable.clear();
|
||||||
|
scene.onAfterRenderObservable.add(() => this.fpsCounter.tick());
|
||||||
// First in line, so the read is issued before the steps of the frame and the GPU is idle.
|
// First in line, so the read is issued before the steps of the frame and the GPU is idle.
|
||||||
scene.onBeforeRenderObservable.add(() => this.updateMass(), undefined, true);
|
scene.onBeforeRenderObservable.add(() => this.updateMass(), undefined, true);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user