From abf3803cdc8a9197d472381175d1d4cf4445995b Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 28 May 2023 22:28:44 +0100 Subject: [PATCH] Various improvements --- src/index.ts | 45 ++-- src/page/set-up-settings-page.ts | 198 +++++++++--------- src/page/settings-slider.ts | 5 + .../agent-first-generation.wgsl | 16 +- src/pipelines/agents/agent-pipeline.ts | 13 +- src/pipelines/agents/agent-settings.ts | 1 - src/pipelines/agents/agent.wgsl | 68 +++--- src/pipelines/diffusion/diffuse.wgsl | 50 +++-- src/pipelines/diffusion/diffusion-pipeline.ts | 8 +- src/pipelines/render/render.wgsl | 8 +- src/settings.ts | 57 +++-- src/utils/format-number.ts | 2 +- src/utils/graphics/full-screen-quad.ts | 1 - src/utils/graphics/noise.ts | 10 +- src/utils/graphics/resizable-texture.ts | 3 - 15 files changed, 259 insertions(+), 226 deletions(-) diff --git a/src/index.ts b/src/index.ts index b76fb2d..ac0c43a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,8 @@ import { CollapsiblePanelAnimator } from './page/collapsible-panel-animator'; import { FullScreenHandler } from './page/full-screen-handler'; import { MenuHider } from './page/menu-hider'; import { setUpSettingsPage } from './page/set-up-settings-page'; +import { SettingsSlider } from './page/settings-slider'; +import { resetSettings } from './settings'; import { applyArrayPlugins } from './utils/array'; import { DeltaTimeCalculator } from './utils/delta-time-calculator'; import { ErrorHandler, Severity } from './utils/error-handler'; @@ -29,11 +31,13 @@ declare global { } } -const getElements = () => ({ +const elements = { aside: document.querySelector('aside') as HTMLDivElement, infoButton: document.querySelector('button.info') as HTMLButtonElement, infoElement: document.querySelector('.info-page') as HTMLDivElement, settingsPage: document.querySelector('.settings-page') as HTMLDivElement, + settingsContent: document.querySelector('.settings-content') as HTMLDivElement, + applyDefaults: document.querySelector('#apply-defaults') as HTMLButtonElement, minimizeFullScreenButton: document.querySelector( 'button.minimize-full-screen' ) as HTMLButtonElement, @@ -46,25 +50,23 @@ const getElements = () => ({ canvasContainer: document.querySelector('main.canvas-container') as HTMLCanvasElement, errorContainer: document.querySelector('.errors-container') as HTMLDivElement, counters: document.querySelector('.counters > pre') as HTMLPreElement, -}); +}; const main = async () => { - const elements = getElements(); - - let shouldStop = false; - let game: GameLoop | null = null; - - ErrorHandler.addOnErrorListener((error, _metadata) => { - elements.errorContainer.innerHTML += ` -
${error.message}
-    `;
-    game?.destroy();
-    shouldStop = true;
-  });
-
   try {
+    let shouldStop = false;
+    let game: GameLoop | null = null;
+
     applyArrayPlugins();
 
+    ErrorHandler.addOnErrorListener((error, _metadata) => {
+      elements.errorContainer.innerHTML += `
+        
${error.message}
+      `;
+      game?.destroy();
+      shouldStop = true;
+    });
+
     const infoPageHandler = new CollapsiblePanelAnimator(
       elements.infoButton,
       elements.infoElement,
@@ -98,7 +100,12 @@ const main = async () => {
 
     const deltaTimeCalculator = new DeltaTimeCalculator();
     const gameRules = new GameRules(performance.now() / 1000);
-    let isSettingsPageSetUp = false;
+    let sliders: Array> = [];
+
+    elements.applyDefaults.addEventListener('click', () => {
+      resetSettings();
+      sliders.forEach((slider) => slider.updateSliderValueBasedOnSource());
+    });
 
     const updateCounters = () => {
       elements.counters.innerHTML = `FPS: ${deltaTimeCalculator.fps.toFixed(2)}
@@ -110,9 +117,9 @@ next gen: ${formatNumber(game?.aliveAgentCounts.nextGenerationCount ?? 0)}`;
 
     while (!shouldStop) {
       game = new GameLoop(elements.canvas, gpu, deltaTimeCalculator, gameRules);
-      if (!isSettingsPageSetUp) {
-        isSettingsPageSetUp = true;
-        setUpSettingsPage(elements.settingsPage, game.maxAgentCount);
+
+      if (sliders.length === 0) {
+        sliders = setUpSettingsPage(elements.settingsContent, game.maxAgentCount);
       }
 
       await game.start();
diff --git a/src/page/set-up-settings-page.ts b/src/page/set-up-settings-page.ts
index 4eb3c10..b669546 100644
--- a/src/page/set-up-settings-page.ts
+++ b/src/page/set-up-settings-page.ts
@@ -4,129 +4,123 @@ import { SettingsSlider, ValueScaling } from './settings-slider';
 export const setUpSettingsPage = (
   settingsPage: HTMLDivElement,
   maxAgentCount: number
-) => {
+): Array> => {
   const sliders = [
-    [
-      new SettingsSlider(settings, 'agentCount', {
-        min: 1,
-        max: maxAgentCount,
-        scaling: ValueScaling.Logarithmic,
-        rounding: Math.round,
-      }),
+    new SettingsSlider(settings, 'renderSpeed', {
+      min: 1,
+      max: 10,
+      rounding: Math.round,
+    }),
 
-      new SettingsSlider(settings, 'currentGenerationAggression', {
-        min: -20,
-        max: 20,
-      }),
+    new SettingsSlider(settings, 'agentCount', {
+      min: 1,
+      max: maxAgentCount,
+      scaling: ValueScaling.Logarithmic,
+      rounding: Math.round,
+    }),
 
-      new SettingsSlider(settings, 'nextGenerationAggression', {
-        min: -20,
-        max: 20,
-      }),
+    new SettingsSlider(settings, 'currentGenerationAggression', {
+      min: -5,
+      max: 5,
+    }),
 
-      new SettingsSlider(settings, 'moveSpeed', {
-        min: 10,
-        max: 500,
-        scaling: ValueScaling.Quadratic,
-        rounding: Math.round,
-      }),
+    new SettingsSlider(settings, 'nextGenerationAggression', {
+      min: -5,
+      max: 5,
+    }),
 
-      new SettingsSlider(settings, 'turnSpeed', {
-        min: 10,
-        max: 1000,
-        scaling: ValueScaling.Quadratic,
-        rounding: Math.round,
-      }),
+    new SettingsSlider(settings, 'moveSpeed', {
+      min: 10,
+      max: 500,
+      scaling: ValueScaling.Quadratic,
+      rounding: Math.round,
+    }),
 
-      new SettingsSlider(settings, 'sensorOffsetAngle', {
-        min: 0,
-        max: 90,
-        step: 1,
-      }),
+    new SettingsSlider(settings, 'turnSpeed', {
+      min: 1,
+      max: 200,
+      scaling: ValueScaling.Quadratic,
+      rounding: Math.round,
+    }),
 
-      new SettingsSlider(settings, 'sensorOffsetDistance', {
-        min: 0,
-        max: 200,
-        scaling: ValueScaling.Quadratic,
-        rounding: Math.round,
-      }),
+    new SettingsSlider(settings, 'sensorOffsetAngle', {
+      min: 0,
+      max: 90,
+      step: 1,
+    }),
 
-      new SettingsSlider(settings, 'turnWhenLost', {
-        min: 0,
-        max: 1,
-      }),
+    new SettingsSlider(settings, 'sensorOffsetDistance', {
+      min: 0,
+      max: 200,
+      scaling: ValueScaling.Quadratic,
+      rounding: Math.round,
+    }),
 
-      new SettingsSlider(settings, 'turnWhenGoingInTheRightDirection', {
-        min: 0,
-        max: 1,
-      }),
+    new SettingsSlider(settings, 'turnWhenLost', {
+      min: 0,
+      max: 1,
+    }),
 
-      new SettingsSlider(settings, 'deinfectionProbability', {
-        min: 0,
-        max: 1,
-        scaling: ValueScaling.Quadratic,
-      }),
+    new SettingsSlider(settings, 'deinfectionProbability', {
+      min: 0,
+      max: 1,
+      scaling: ValueScaling.Quadratic,
+    }),
 
-      new SettingsSlider(settings, 'brushTrailWeight', {
-        min: 0,
-        max: 10,
-      }),
+    new SettingsSlider(settings, 'individualTrailWeight', {
+      min: 0,
+      max: 1,
+    }),
 
-      new SettingsSlider(settings, 'individualTrailWeight', {
-        min: 0,
-        max: 1,
-      }),
+    new SettingsSlider(settings, 'diffusionRateTrails', {
+      min: 0,
+      max: 2,
+    }),
 
-      new SettingsSlider(settings, 'diffusionRateTrails', {
-        min: 0,
-        max: 10,
-      }),
+    new SettingsSlider(settings, 'decayRateTrails', {
+      min: 0.1,
+      max: 1000,
+    }),
 
-      new SettingsSlider(settings, 'decayRateTrails', {
-        min: 0,
-        max: 10,
-      }),
+    new SettingsSlider(settings, 'diffusionRateBrush', {
+      min: 0.001,
+      max: 1,
+    }),
 
-      new SettingsSlider(settings, 'diffusionRateBrush', {
-        min: 0,
-        max: 10,
-      }),
+    new SettingsSlider(settings, 'decayRateBrush', {
+      min: 0.1,
+      max: 100,
+    }),
 
-      new SettingsSlider(settings, 'decayRateBrush', {
-        min: 0,
-        max: 10,
-      }),
+    new SettingsSlider(settings, 'spawnRadius', {
+      min: 0,
+      max: 1000,
+    }),
 
-      new SettingsSlider(settings, 'spawnRadius', {
-        min: 0,
-        max: 1000,
-      }),
+    new SettingsSlider(settings, 'spawnInterval', {
+      min: 0.1,
+      max: 600,
+      scaling: ValueScaling.Quadratic,
+    }),
 
-      new SettingsSlider(settings, 'spawnInterval', {
-        min: 0.1,
-        max: 600,
-      }),
+    new SettingsSlider(settings, 'clarity', {
+      min: 0,
+      max: 0.5,
+    }),
 
-      new SettingsSlider(settings, 'clarity', {
-        min: 0.5,
-        max: 8,
-        step: 0.1,
-      }),
-
-      new SettingsSlider(settings, 'brushSize', {
-        min: 1,
-        max: 60,
-      }),
-    ],
+    new SettingsSlider(settings, 'brushSize', {
+      min: 1,
+      max: 30,
+    }),
   ];
 
-  sliders.forEach((sliderContainer) => {
-    const sliderContainerElement = document.createElement('div');
+  const sliderContainerElement = document.createElement('div');
 
-    sliderContainer.forEach((slider) => {
-      sliderContainerElement.appendChild(slider.element);
-    });
-
-    settingsPage.querySelector('section').appendChild(sliderContainerElement);
+  sliders.forEach((slider) => {
+    sliderContainerElement.appendChild(slider.element);
   });
+
+  settingsPage.appendChild(sliderContainerElement);
+
+  return sliders;
 };
diff --git a/src/page/settings-slider.ts b/src/page/settings-slider.ts
index 40204eb..d7ad26b 100644
--- a/src/page/settings-slider.ts
+++ b/src/page/settings-slider.ts
@@ -97,6 +97,11 @@ export class SettingsSlider> {
     );
   }
 
+  public updateSliderValueBasedOnSource() {
+    this.slider.value = this.scaling(this.settings[this.settingName]).toString();
+    this.onChange();
+  }
+
   public updateConfig(config: Partial) {
     Object.assign(this.config, config);
 
diff --git a/src/pipelines/agents/agent-generation/agent-first-generation.wgsl b/src/pipelines/agents/agent-generation/agent-first-generation.wgsl
index a47bcd3..091297e 100644
--- a/src/pipelines/agents/agent-generation/agent-first-generation.wgsl
+++ b/src/pipelines/agents/agent-generation/agent-first-generation.wgsl
@@ -9,20 +9,26 @@ fn main(
     return;
   }
 
+  let clusterId = f32(id % 1000);
+
   let random = textureSampleLevel(
     noise,
     noiseSampler,
     vec2(f32(id % 1999) / 2000, f32(id) / 1999 / 2000),
     0
   );
+  
+  let randomPosition = textureSampleLevel(
+    noise,
+    noiseSampler,
+    vec2(clusterId / 2000, clusterId / 2000),
+    0
+  );
 
-  let position = random.xy * state.size;
-  let center = state.size / 2.0;
-  let direction = position - center;
 
   agents[id] = Agent(
-    state.size / 2.0,
-    atan2(direction.y, direction.x),
+    randomPosition.xz * state.size,
+    random.r * 3.14 * 2,
     0,
   );
 }
diff --git a/src/pipelines/agents/agent-pipeline.ts b/src/pipelines/agents/agent-pipeline.ts
index 9a2a863..3b3d7dd 100644
--- a/src/pipelines/agents/agent-pipeline.ts
+++ b/src/pipelines/agents/agent-pipeline.ts
@@ -9,7 +9,7 @@ import { vec2 } from 'gl-matrix';
 
 export class AgentPipeline {
   private static readonly WORKGROUP_SIZE = 64;
-  private static readonly UNIFORM_COUNT = 17;
+  private static readonly UNIFORM_COUNT = 16;
 
   private readonly bindGroupLayout: GPUBindGroupLayout;
   private readonly pipeline: GPUComputePipeline;
@@ -54,7 +54,6 @@ export class AgentPipeline {
     isNextGenerationOdd,
     center,
     radius,
-    turnWhenGoingInTheRightDirection,
     turnWhenLost,
     individualTrailWeight,
     deinfectionProbability,
@@ -72,20 +71,24 @@ export class AgentPipeline {
       this.uniforms,
       0,
       new Float32Array([
+        ...center,
+        radius,
+
         brushTrailWeight,
         moveSpeed,
         turnSpeed,
+
         (sensorOffsetAngle * Math.PI) / 180,
         sensorOffsetDistance,
+
         currentGenerationAggression,
         nextGenerationAggression,
         isNextGenerationOdd,
-        ...center,
-        radius,
-        turnWhenGoingInTheRightDirection,
+
         turnWhenLost,
         individualTrailWeight,
         deinfectionProbability,
+
         agentCount,
       ])
     );
diff --git a/src/pipelines/agents/agent-settings.ts b/src/pipelines/agents/agent-settings.ts
index 57d2706..c6115df 100644
--- a/src/pipelines/agents/agent-settings.ts
+++ b/src/pipelines/agents/agent-settings.ts
@@ -4,7 +4,6 @@ export interface AgentSettings {
   turnSpeed: number;
   sensorOffsetAngle: number;
   sensorOffsetDistance: number;
-  turnWhenGoingInTheRightDirection: number;
   turnWhenLost: number;
   individualTrailWeight: number;
   deinfectionProbability: number;
diff --git a/src/pipelines/agents/agent.wgsl b/src/pipelines/agents/agent.wgsl
index ec4dc2c..bbe902d 100644
--- a/src/pipelines/agents/agent.wgsl
+++ b/src/pipelines/agents/agent.wgsl
@@ -1,4 +1,7 @@
 struct Settings {
+  center: vec2,
+  radius: f32,
+
   brushTrailWeight: f32,
   moveRate: f32,
   turnRate: f32,
@@ -10,10 +13,6 @@ struct Settings {
   nextGenerationAggression: f32,
   isNextGenerationOdd: f32,
 
-  center: vec2,
-  radius: f32,
-
-  turnWhenGoingInTheRightDirection: f32,
   turnWhenLost: f32,
   individualTrailWeight: f32,
   deinfectionProbability: f32,
@@ -47,10 +46,12 @@ fn main(
   let random = textureSampleLevel(
     noise,
     noiseSampler,
-    vec2(f32(id) % 23647 / 2000,
-    state.time % 6294 / 2000),
+    vec2(
+      f32(id) % 23647 / 2000,
+      agent.angle / 10
+    ) + agent.position / state.size,
     0
-  ).a;
+  );
 
   let isFromCurrentGeneration = abs(agent.generation - settings.isNextGenerationOdd);
   let isFromOddGeneration = agent.generation == 1.0;
@@ -59,9 +60,10 @@ fn main(
   let trailLeft = sense(agent.position, agent.angle, settings.sensorOffset, settings.sensorAngle);
   let trailRight = sense(agent.position, agent.angle, settings.sensorOffset, -settings.sensorAngle);
 
-  var weightForward: f32 = isFromCurrentGeneration * trailForward.a * settings.brushTrailWeight;
-  var weightLeft: f32 = isFromCurrentGeneration * trailLeft.a * settings.brushTrailWeight;
-  var weightRight: f32 = isFromCurrentGeneration * trailRight.a * settings.brushTrailWeight;
+  let brushWeight = isFromCurrentGeneration * settings.brushTrailWeight - (1 - isFromCurrentGeneration) * settings.brushTrailWeight;
+  var weightForward: f32 = brushWeight * trailForward.a;
+  var weightLeft: f32 = brushWeight * trailLeft.a;
+  var weightRight: f32 = brushWeight * trailRight.a;
 
   let agression = isFromCurrentGeneration * settings.currentGenerationAggression + (1.0 - isFromCurrentGeneration) * settings.nextGenerationAggression; 
   if (isFromOddGeneration) {
@@ -75,32 +77,34 @@ fn main(
   }
 
   var rotation: f32 = 0;
-  if weightForward > weightLeft && weightForward > weightRight {
-    rotation = (random - 0.5) * settings.turnWhenGoingInTheRightDirection * settings.turnRate * state.deltaTime;
+  if weightForward >= weightLeft && weightForward >= weightRight {
+    rotation = (random.r - 0.5) * 0.0 * settings.turnRate * state.deltaTime;
   } else if weightLeft < weightRight {
-    rotation = -min(settings.sensorAngle, settings.turnRate * state.deltaTime);
+    rotation = -settings.turnRate * state.deltaTime;
   } else if weightRight < weightLeft {
-    rotation = min(settings.sensorAngle, settings.turnRate * state.deltaTime);
+    rotation = settings.turnRate * state.deltaTime;
   } else {
-    rotation = (random - 0.5) * settings.turnWhenLost * settings.turnRate * state.deltaTime;
+    rotation = (random.r - 0.5) * settings.turnWhenLost * settings.turnRate * state.deltaTime;
   }
 
   let direction = vec2(cos(agent.angle), sin(agent.angle));
   var nextPosition = agent.position + direction * settings.moveRate * state.deltaTime;
   nextPosition = clamp(nextPosition, vec2(0, 0), state.size);
   if nextPosition.x == 0 || nextPosition.x == state.size.x || nextPosition.y == 0 || nextPosition.y == state.size.y {
-    rotation = 3.14159265359 + random - 0.5;
+    rotation = 3.14159265359 + random.a - 0.5;
   }
 
   var trail = vec4(settings.individualTrailWeight, 0, 0, 0);
   if isFromOddGeneration {
-    trail = vec4(0, settings.individualTrailWeight, 0, 0);
+    trail = vec4(0, settings.individualTrailWeight, 0, 0);
   }
 
-  var trailBelow = textureLoad(trailMapIn, vec2(agent.position), 0);
+  agent.position = nextPosition;
+  agent.angle += rotation;
+  var trailBelow = textureLoad(trailMapIn, vec2(nextPosition), 0);
 
   if settings.radius > 0 && length(settings.center - agent.position) < settings.radius {
-    agents[id].generation = settings.isNextGenerationOdd;
+    agent.generation = settings.isNextGenerationOdd;
     
     // clear trail map below so the agent won't die immediately
     if (settings.isNextGenerationOdd == 1.0) {
@@ -111,25 +115,21 @@ fn main(
       trailBelow.g = 0;
     }
 
-    textureStore(trailMapOut, vec2(agent.position), trailBelow);
-    return;
-  }
-
-  let next = vec4(trail.rgb + trailBelow.rgb, trailBelow.a);
-  textureStore(trailMapOut, vec2(nextPosition), next);
-
-  if isFromOddGeneration {
-    if next.g < next.r && (isFromCurrentGeneration == 1.0 || (isFromCurrentGeneration == 0.0 && random < settings.deinfectionProbability)) {
-      agent.generation = 0;
-    }
+    textureStore(trailMapOut, vec2(nextPosition), trailBelow);
   } else {
-    if next.r < next.g && (isFromCurrentGeneration == 1.0 || (isFromCurrentGeneration == 0.0 && random < settings.deinfectionProbability)) {
-      agent.generation = 1;
+    textureStore(trailMapOut, vec2(nextPosition), trail + trailBelow);
+
+    if isFromOddGeneration {
+      if trailBelow.g < trailBelow.r && (isFromCurrentGeneration == 1.0 || (isFromCurrentGeneration == 0.0 && random.a < settings.deinfectionProbability)) {
+        agent.generation = 0;
+      }
+    } else {
+      if trailBelow.r < trailBelow.g && (isFromCurrentGeneration == 1.0 || (isFromCurrentGeneration == 0.0 && random.a < settings.deinfectionProbability)) {
+        agent.generation = 1;
+      }
     }
   }
 
-  agent.position = nextPosition;
-  agent.angle += rotation;
   agents[id] = agent;
 }
 
diff --git a/src/pipelines/diffusion/diffuse.wgsl b/src/pipelines/diffusion/diffuse.wgsl
index 94734b9..f0dc3df 100644
--- a/src/pipelines/diffusion/diffuse.wgsl
+++ b/src/pipelines/diffusion/diffuse.wgsl
@@ -1,40 +1,48 @@
 struct Settings {
-  diffusionRateTrails: f32,
+  inverseDiffusionRateTrails: f32,
   decayRateTrails: f32,
-  diffusionRateBrush: f32,
+  inverseDiffusionRateBrush: f32,
   decayRateBrush: f32,
 };
 
+
 @group(1) @binding(0) var settings: Settings;
 @group(1) @binding(1) var Sampler: sampler;
 @group(1) @binding(2) var trailMap: texture_2d;
 
+
 @fragment
 fn fragment(@location(0) uv: vec2) -> @location(0) vec4 {
   var current = textureSample(trailMap, Sampler, uv);
-  var change = vec4(0);
-  for (var x: i32 = -1; x <= 1; x++) {
-    for (var y: i32 = -1; y <= 1; y++) {
-      if (x != 0 || y != 0) {
-        let offset = vec2(f32(x), f32(y));
-        let neighbour = textureSample(trailMap, Sampler, uv + offset / state.size);
-        let random = textureSample(noise, noiseSampler, uv + offset / state.size * 0.5).r;
-        
-        let difference = clamp(neighbour - current, vec4(0), vec4(1));
-        change += vec4(
-          length(neighbour.rgb) * pow(random, settings.diffusionRateTrails) * difference.rgb,
-          min(1.0, length(neighbour.a)) * pow(random, settings.diffusionRateBrush) * difference.a
-        );
-      }
-    }
-  }
 
-  current += change / 4;
+  current += (
+        propagate(uv, vec2(-1.0, -1.0), current)
+      + propagate(uv, vec2(-1.0, 1.0), current)
+      + propagate(uv, vec2(1.0, -1.0), current)
+      + propagate(uv, vec2(1.0, 1.0), current)
+
+      + propagate(uv, vec2(-1.0, 0.0), current)
+      + propagate(uv, vec2(0.0, -1.0), current)
+      + propagate(uv, vec2(1.0, 0.0), current)
+      + propagate(uv, vec2(0.0, 1.0), current)
+  ) / 8;
 
   let decayed = clamp(vec4(
-    current.rgb * settings.decayRateTrails,
-    max(0, current.a - settings.decayRateBrush)
+    current.rgb - settings.decayRateTrails,
+    max(0, current.a + (current.a - 1.001) * settings.decayRateBrush)
   ), vec4(0), vec4(1));
  
   return decayed;
 }
+
+
+fn propagate(uv: vec2, offset: vec2, currentColor: vec4) -> vec4 {
+  let neighbour = textureSample(trailMap, Sampler, uv + offset / state.size);
+  var random = textureSample(noise, noiseSampler, uv + offset / state.size * 0.5).r;
+  let difference = clamp(neighbour - currentColor, vec4(0), vec4(1));
+
+  return vec4(
+    vec3(length(neighbour.rgb) * pow(random, settings.inverseDiffusionRateTrails)),
+    length(neighbour.a) * pow(random, settings.inverseDiffusionRateBrush)
+  ) * difference;
+}
diff --git a/src/pipelines/diffusion/diffusion-pipeline.ts b/src/pipelines/diffusion/diffusion-pipeline.ts
index e3685d7..15f2fda 100644
--- a/src/pipelines/diffusion/diffusion-pipeline.ts
+++ b/src/pipelines/diffusion/diffusion-pipeline.ts
@@ -62,10 +62,10 @@ export class DiffusionPipeline {
       this.uniforms,
       0,
       new Float32Array([
-        diffusionRateTrails,
-        decayRateTrails,
-        diffusionRateBrush,
-        decayRateBrush,
+        1 / diffusionRateTrails,
+        decayRateTrails / 1000,
+        1 / diffusionRateBrush,
+        decayRateBrush / 1000,
       ])
     );
   }
diff --git a/src/pipelines/render/render.wgsl b/src/pipelines/render/render.wgsl
index 28c5105..ba2b7d0 100644
--- a/src/pipelines/render/render.wgsl
+++ b/src/pipelines/render/render.wgsl
@@ -16,8 +16,8 @@ fn fragment(@location(0) uv: vec2) -> @location(0) vec4 {
 
   let backgroundColor = vec3(0.9) + 0.075 * random.r;
 
-  let evenGenerationStrength = pow(traces.r, settings.clarity);
-  let oddGenerationStrength = pow(traces.g, settings.clarity);
+  let evenGenerationStrength = clarity(traces.r);
+  let oddGenerationStrength = clarity(traces.g);
   let brushStrength = traces.a;
 
   let color = max(
@@ -32,3 +32,7 @@ fn fragment(@location(0) uv: vec2) -> @location(0) vec4 {
 
   return vec4(mix(backgroundColor, color, strength), 1);
 }
+
+fn clarity(strength: f32) -> f32 {
+  return pow(strength, 5) - strength * settings.clarity + sign(strength) * settings.clarity;
+}
diff --git a/src/settings.ts b/src/settings.ts
index 027822a..1a34208 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -5,37 +5,38 @@ import { DiffusionSettings } from './pipelines/diffusion/diffusion-settings';
 import { RenderSettings } from './pipelines/render/render-settings';
 import { persist } from './utils/persist';
 
-export const settings: { [key: string]: number } & GameLoopSettings &
+const initialValues: GameLoopSettings &
   AgentSettings &
   BrushSettings &
   DiffusionSettings &
-  RenderSettings = persist({
-  agentCount: 1_000_000,
+  RenderSettings = {
+  agentCount: 1_001_500,
 
-  currentGenerationAggression: 0.1,
-  nextGenerationAggression: 10,
+  currentGenerationAggression: -5,
+  nextGenerationAggression: 0.5,
 
-  moveSpeed: 70,
-  turnSpeed: 345,
-  sensorOffsetAngle: 32,
-  sensorOffsetDistance: 23,
-  turnWhenGoingInTheRightDirection: 0,
-  turnWhenLost: 0.2,
-  deinfectionProbability: 0.001,
+  moveSpeed: 90,
+  turnSpeed: 78,
+  sensorOffsetAngle: 41,
+  sensorOffsetDistance: 45,
+  turnWhenLost: 0.43,
+  deinfectionProbability: 1,
 
-  brushTrailWeight: 5,
-  individualTrailWeight: 0.5,
-  diffusionRateTrails: 2, // inverse
-  decayRateTrails: 0.9, // inverse
-  diffusionRateBrush: 4, // inverse
-  decayRateBrush: 0.003,
+  brushTrailWeight: 500,
+  individualTrailWeight: 0.2,
 
-  spawnRadius: 5,
-  spawnInterval: 600,
+  diffusionRateTrails: 0.29,
+  decayRateTrails: 21.95,
+  diffusionRateBrush: 0.25,
+  decayRateBrush: 15,
 
-  clarity: 2,
+  spawnRadius: 8,
+  spawnInterval: 8,
+
+  clarity: 0,
   brushSize: 12,
-  brushSizeVariation: 0.5,
+
+  brushSizeVariation: 0.5, // hidden on the UI
 
   startColorHue: 200,
 
@@ -44,4 +45,14 @@ export const settings: { [key: string]: number } & GameLoopSettings &
   // debug options
   renderSpeed: 1,
   simulatedDelayMs: 0,
-});
+};
+
+export const settings: { [key: string]: number } & GameLoopSettings &
+  AgentSettings &
+  BrushSettings &
+  DiffusionSettings &
+  RenderSettings = persist({ ...initialValues });
+
+export const resetSettings = () => {
+  Object.assign(settings, initialValues);
+};
diff --git a/src/utils/format-number.ts b/src/utils/format-number.ts
index 8375628..a57812e 100644
--- a/src/utils/format-number.ts
+++ b/src/utils/format-number.ts
@@ -7,5 +7,5 @@ export const formatNumber = (value: number, unit = ''): string => {
     return `${(value / 1e3).toFixed(1)} thousand ${unit}`;
   }
 
-  return `${value === Math.floor(value) ? value : value.toFixed(2)}${unit}`;
+  return `${value === Math.floor(value) ? value : value.toFixed(2)} ${unit}`;
 };
diff --git a/src/utils/graphics/full-screen-quad.ts b/src/utils/graphics/full-screen-quad.ts
index 3790f75..3e9dbec 100644
--- a/src/utils/graphics/full-screen-quad.ts
+++ b/src/utils/graphics/full-screen-quad.ts
@@ -1,4 +1,3 @@
-import shader from './full-screen-quad.wgsl';
 import { smartCompile } from './smart-compile';
 
 export const setUpFullScreenQuad = (
diff --git a/src/utils/graphics/noise.ts b/src/utils/graphics/noise.ts
index 004de5b..920ddeb 100644
--- a/src/utils/graphics/noise.ts
+++ b/src/utils/graphics/noise.ts
@@ -5,12 +5,12 @@ const textureCache = new Map();
 
 export const generateNoise = ({
   device,
-  width = 1024,
-  height = 1024,
+  width,
+  height,
 }: {
   device: GPUDevice;
-  width?: number;
-  height?: number;
+  width: number;
+  height: number;
 }): GPUTextureView => {
   const cacheKey = `${width}x${height}`;
   if (!textureCache.has(cacheKey)) {
@@ -31,10 +31,10 @@ export const generateNoise = ({
           @fragment
           fn fragment(@location(0) uv: vec2) -> @location(0) vec4 {
             return vec4(
+              random_with_seed(uv, 0),
               random_with_seed(uv, 1),
               random_with_seed(uv, 2),
               random_with_seed(uv, 3),
-              random_with_seed(uv, 4),
             );
           }`
         ),
diff --git a/src/utils/graphics/resizable-texture.ts b/src/utils/graphics/resizable-texture.ts
index 4fe25c7..58aa531 100644
--- a/src/utils/graphics/resizable-texture.ts
+++ b/src/utils/graphics/resizable-texture.ts
@@ -20,12 +20,9 @@ export class ResizableTexture {
 
     const newTexture = this.device.createTexture({
       format: 'rgba16float',
-      dimension: '2d',
-      mipLevelCount: 1,
       size: {
         width: size.x,
         height: size.y,
-        depthOrArrayLayers: 1,
       },
       usage:
         GPUTextureUsage.STORAGE_BINDING |