Zum Inhalt

src/engine/params.ts

// 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;
}