// src/engine/params.ts
// Pure packing of the uniform buffer. Layout must match the WGSL `Params`
// struct in the shaders exactly.
//
// PostParams layout includes blendMode (u32) so the composite shader can
// switch between additive and paint compositing at runtime, and
// kaleidoscope (u32) which tucks into one of the bg vec3-alignment pads.
//
// Params layout (304 bytes / 76 floats):
// u32 0 fieldWidth u32 1 fieldHeight
// u32 2 agentCount u32 3 frame
// f32 4 time f32 5 decayRate
// f32 6 fadeStrength f32 7 trailSaturation
// u32 8 diffusionFreq f32 9 pad (was attractionStrength)
// f32 10 pad (was repulsionStrength) u32 11 chaosInterval
// f32 12 chaosStrength f32 13 rhythmAmplitude
// f32 14 exposure u32 15 spawnPattern
// f32 16 moveSpeedFactor f32 17 turnSpeedFactor f32 18 depositFactor
// f32 19 sensorDistanceFactor f32 20 rhythmAmplitudeFactor f32 21 chaosStrengthFactor
// f32 22 headIntensity f32 23 pad (vec3 alignment for bg)
// f32 24-26 backgroundColor f32 27 pad
// SpeciesParams[3] at float 28, stride 12 floats (48 bytes)
// matrix rows at float 64, stride 4 floats (vec4)
//
// f[9] and f[10] are pre-1.2 legacy slots — pre-2026-05 presets had global
// attractionStrength/repulsionStrength scalars that the shader applied on
// top of interactionMatrix. They were folded into the matrix at preset-load
// (see presets/schema.ts) and the slots stay as zeroed pads to keep the
// shared Params struct layout stable across the four sim shaders.
import { BLEND_MODES, SPAWN_PATTERNS } from './types';
import type { BlendMode, InteractionMatrix, RGB, SpawnPattern, SpeciesParams } from './types';
export interface UniformInput {
fieldWidth: number;
fieldHeight: number;
agentCount: number;
frame: number;
time: number;
decayRate: number;
fadeStrength: number;
trailSaturation: number;
diffusionFreq: number;
chaosInterval: number;
chaosStrength: number;
rhythmAmplitude: number;
exposure: number;
spawnPattern: SpawnPattern;
headIntensity: number;
backgroundColor: RGB;
species: readonly [SpeciesParams, SpeciesParams, SpeciesParams];
matrix: InteractionMatrix;
audio: {
/** 1 + Σ (level × depth × gain) for each modulation target. 1 means "no mod". */
moveSpeedFactor: number;
turnSpeedFactor: number;
depositFactor: number;
sensorDistanceFactor: number;
rhythmAmplitudeFactor: number;
chaosStrengthFactor: number;
};
}
export const PARAMS_BYTE_SIZE = 304;
export function packParams(input: UniformInput): ArrayBuffer {
const buf = new ArrayBuffer(PARAMS_BYTE_SIZE);
const f = new Float32Array(buf);
const u = new Uint32Array(buf); // same buffer, index-aligned with f
u[0] = input.fieldWidth;
u[1] = input.fieldHeight;
u[2] = input.agentCount;
u[3] = input.frame;
f[4] = input.time;
f[5] = input.decayRate;
f[6] = input.fadeStrength;
f[7] = input.trailSaturation;
u[8] = Math.max(1, Math.round(input.diffusionFreq));
// f[9] and f[10] are zeroed pads — pre-1.2 legacy slots.
u[11] = Math.max(0, Math.round(input.chaosInterval));
f[12] = input.chaosStrength;
f[13] = input.rhythmAmplitude;
f[14] = input.exposure;
u[15] = SPAWN_PATTERNS.indexOf(input.spawnPattern);
f[16] = input.audio.moveSpeedFactor;
f[17] = input.audio.turnSpeedFactor;
f[18] = input.audio.depositFactor;
f[19] = input.audio.sensorDistanceFactor;
f[20] = input.audio.rhythmAmplitudeFactor;
f[21] = input.audio.chaosStrengthFactor;
f[22] = input.headIntensity;
// f[23] pad — kept for vec3 alignment of backgroundColor at f[24]
f[24] = input.backgroundColor[0];
f[25] = input.backgroundColor[1];
f[26] = input.backgroundColor[2];
// f[27] pad
// species block: float index 28, 12 floats stride
input.species.forEach((sp, s) => {
const o = 28 + s * 12;
f[o + 0] = sp.color[0];
f[o + 1] = sp.color[1];
f[o + 2] = sp.color[2];
f[o + 3] = sp.populationShare;
f[o + 4] = sp.sensorAngle;
f[o + 5] = sp.sensorDistance;
f[o + 6] = sp.turnSpeed;
f[o + 7] = sp.moveSpeed;
f[o + 8] = sp.depositAmount;
// o+9, o+10, o+11 pad
});
// matrix rows: float index 64, stride 4
input.matrix.forEach((row, r) => {
const o = 64 + r * 4;
f[o + 0] = row[0];
f[o + 1] = row[1];
f[o + 2] = row[2];
// o+3 pad
});
return buf;
}
// --- Post-processing uniform ---------------------------------------------
// Separate small uniform for the post-processing passes (scene is rendered
// by the sim shaders; bloom/feedback/composite use only these values).
// f32 0-1 resolution f32 2 exposure f32 3 time
// f32 4 feedbackAmount f32 5 bloomThreshold f32 6 bloomIntensity
// f32 7 vignette f32 8 saturation f32 9 contrast
// u32 10 kaleidoscope f32 11 pad (vec3 align)
// f32 12-14 backgroundColor u32 15 blendMode
// f32 16 exposureFactor f32 17 bloomIntensityFactor
// f32 18 saturationFactor f32 19 contrastFactor
//
// backgroundColor lives here (not in Params) so the composite pass can lay
// it down after the scene/feedback texture is sampled, instead of mixing it
// into the scene shader where the feedback loop amplifies it ~1/(1-fA) ×.
// blendMode tucks into the pad slot the vec3 alignment had reserved — no
// size growth, no shift in subsequent offsets.
// The audio *Factor fields are 1 when no route targets them; > 1 when they
// are being pumped — see Engine.computeAudioFactors.
export interface PostUniformInput {
resolutionX: number;
resolutionY: number;
exposure: number;
time: number;
feedbackAmount: number;
bloomThreshold: number;
bloomIntensity: number;
vignette: number;
saturation: number;
contrast: number;
backgroundColor: RGB;
blendMode: BlendMode;
kaleidoscope: number;
exposureFactor: number;
bloomIntensityFactor: number;
saturationFactor: number;
contrastFactor: number;
}
export const POST_PARAMS_BYTE_SIZE = 80;
export function packPostParams(input: PostUniformInput): ArrayBuffer {
const buf = new ArrayBuffer(POST_PARAMS_BYTE_SIZE);
const f = new Float32Array(buf);
const u = new Uint32Array(buf);
f[0] = input.resolutionX;
f[1] = input.resolutionY;
f[2] = input.exposure;
f[3] = input.time;
f[4] = input.feedbackAmount;
f[5] = input.bloomThreshold;
f[6] = input.bloomIntensity;
f[7] = input.vignette;
f[8] = input.saturation;
f[9] = input.contrast;
u[10] = Math.max(0, Math.min(6, Math.round(input.kaleidoscope)));
// f[11] pad for vec3 alignment
f[12] = input.backgroundColor[0];
f[13] = input.backgroundColor[1];
f[14] = input.backgroundColor[2];
u[15] = BLEND_MODES.indexOf(input.blendMode);
f[16] = input.exposureFactor;
f[17] = input.bloomIntensityFactor;
f[18] = input.saturationFactor;
f[19] = input.contrastFactor;
return buf;
}