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
|
||||
|
||||
- [ ] 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.
|
||||
- [ ] **New algorithm demos** (pattern: route + `RouterConstants` + `AlgorithmsService` entry + i18n + information panel):
|
||||
- [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';
|
||||
|
||||
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', () => {
|
||||
it('keepsSeedWhileStepShaderCompiles', () => {
|
||||
renderCalls.length = 0;
|
||||
@@ -66,8 +70,7 @@ describe('LeniaSimulation', () => {
|
||||
ready['leniaStep'] = false;
|
||||
const scene = fakeScene();
|
||||
const simulation = new LeniaSimulation();
|
||||
simulation.init(scene as never, {} as never, 'float');
|
||||
simulation.updateParams({settings: {...DEFAULT_SETTINGS, stepsPerFrame: 1}, kernelSum: 1, brush: {u: 0, v: 0, active: false}});
|
||||
simulation.init(scene as never, {} as never, 'float', () => paramsWithSteps(1));
|
||||
simulation.seed(new Uint8Array(4));
|
||||
|
||||
scene.frame();
|
||||
@@ -81,4 +84,22 @@ describe('LeniaSimulation', () => {
|
||||
expect(firstStep.name).toBe('leniaStep');
|
||||
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 material: ShaderMaterial | 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 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.readParams = readParams;
|
||||
this.renderer = new EffectRenderer(engine);
|
||||
this.current = createStateTarget('leniaA', scene, GRID_SIZE, textureType);
|
||||
this.next = createStateTarget('leniaB', scene, GRID_SIZE, textureType);
|
||||
@@ -61,10 +63,6 @@ export class LeniaSimulation {
|
||||
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. */
|
||||
seed(fieldData: Uint8Array): void {
|
||||
this.field?.update(fieldData);
|
||||
@@ -109,7 +107,8 @@ export class LeniaSimulation {
|
||||
this.next = null;
|
||||
this.material = null;
|
||||
this.plane = null;
|
||||
this.params = null;
|
||||
this.readParams = null;
|
||||
this.frameParams = null;
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
@@ -138,10 +137,10 @@ export class LeniaSimulation {
|
||||
|
||||
private applyStepUniforms(): void {
|
||||
const effect = this.stepWrapper?.effect;
|
||||
if (!effect || !this.params || !this.current) {
|
||||
if (!effect || !this.frameParams || !this.current) {
|
||||
return;
|
||||
}
|
||||
const {settings, kernelSum, brush} = this.params;
|
||||
const {settings, kernelSum, brush} = this.frameParams;
|
||||
effect.setTexture('state', this.current);
|
||||
effect.setFloat('texel', 1 / GRID_SIZE);
|
||||
effect.setInt('radius', Math.min(settings.radius, MAX_RADIUS));
|
||||
@@ -154,9 +153,11 @@ export class LeniaSimulation {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// Read before the seed copy, so a new seed never steps with old params.
|
||||
this.frameParams = this.readParams();
|
||||
if (!this.seeded) {
|
||||
if (!this.copyWrapper.effect.isReady()) {
|
||||
return;
|
||||
@@ -168,7 +169,7 @@ export class LeniaSimulation {
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from './lenia.model';
|
||||
import {kernelSum} from './lenia-kernel';
|
||||
import {buildSeedField, decodeRle, SeedKind} from './lenia-seed';
|
||||
import {LeniaSimulation} from './lenia-simulation';
|
||||
import {LeniaParams, LeniaSimulation} from './lenia-simulation';
|
||||
|
||||
interface SettingSlider {
|
||||
key: keyof LeniaSettings;
|
||||
@@ -153,9 +153,9 @@ export class LeniaComponent implements OnDestroy {
|
||||
this.backend.set(`${backend} (${type === 'float' ? 'float32' : 'float16'})`);
|
||||
this.simulation?.dispose();
|
||||
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.startParamUpdateLoop(event.scene);
|
||||
this.startFrameLoop(event.scene);
|
||||
this.attachPointer(event.scene);
|
||||
}
|
||||
|
||||
@@ -189,11 +189,8 @@ export class LeniaComponent implements OnDestroy {
|
||||
this.simulation?.seed(buildSeedField(kind, pattern, GRID_SIZE));
|
||||
}
|
||||
|
||||
private startParamUpdateLoop(scene: Scene): void {
|
||||
scene.onAfterRenderObservable.clear();
|
||||
scene.onAfterRenderObservable.add(() => {
|
||||
this.fpsCounter.tick();
|
||||
this.simulation?.updateParams({
|
||||
private currentParams(): LeniaParams {
|
||||
return {
|
||||
settings: {
|
||||
radius: this.radius(),
|
||||
mu: this.mu(),
|
||||
@@ -203,8 +200,12 @@ export class LeniaComponent implements OnDestroy {
|
||||
},
|
||||
kernelSum: this.kernelSum(),
|
||||
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.
|
||||
scene.onBeforeRenderObservable.add(() => this.updateMass(), undefined, true);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user