Set up demo

This commit is contained in:
Andras Schmelczer 2023-05-31 22:22:43 +01:00
parent abf3803cdc
commit 8817e5b090
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
17 changed files with 189 additions and 145 deletions

4
src/constants.ts Normal file
View file

@ -0,0 +1,4 @@
// @ts-ignore: injected by webpack
export const isProduction: boolean = __IS_PRODUCTION__;
// @ts-ignore: injected by webpack
export const lastEdit = new Date(__CURRENT_DATE__);

View file

@ -4,8 +4,5 @@ export interface GameLoopSettings {
renderSpeed: number; renderSpeed: number;
simulatedDelayMs: number; simulatedDelayMs: number;
spawnRadius: number;
spawnInterval: number;
startColorHue: number; startColorHue: number;
} }

View file

@ -189,6 +189,9 @@ export default class GameLoop {
pipeline.setParameters({ pipeline.setParameters({
time, time,
isNextGenerationOdd: this.gameRules.nextGenerationId % 2, isNextGenerationOdd: this.gameRules.nextGenerationId % 2,
nextGenerationSensorOffsetDistance: this.gameRules.getSensorOffset(),
nextGenerationSpeed: this.gameRules.getNextGenerationMoveSpeed(),
infectionProbability: this.gameRules.getInfectionProbability(),
deltaTime, deltaTime,
canvasSize: this.canvasSize, canvasSize: this.canvasSize,
brushColor: GamePresentation.getGenerationColor( brushColor: GamePresentation.getGenerationColor(

View file

@ -1,5 +1,7 @@
import { GenerationCounts } from '../pipelines/agents/agent-generation/generation-counts'; import { GenerationCounts } from '../pipelines/agents/agent-generation/generation-counts';
import { settings } from '../settings'; import { settings } from '../settings';
import { clamp, clamp01 } from '../utils/clamp';
import { mix } from '../utils/mix';
import { Random } from '../utils/random'; import { Random } from '../utils/random';
import { vec2 } from 'gl-matrix'; import { vec2 } from 'gl-matrix';
@ -11,7 +13,15 @@ export interface SpawnAction {
} }
export class GameRules { export class GameRules {
private static readonly DEAFULT_SPAWN_INTERVAL = 8;
private static readonly DEAFULT_SPAWN_TIME_LENGTH = 2;
private static readonly DEFAULT_SPAWN_RADIUS = 20;
private lastSpawnTimeInSeconds = 0; private lastSpawnTimeInSeconds = 0;
private currentSpawnInterval = 0;
private currentSpawnRadius = 0;
private lastGenerationChangeTimeInSeconds = 0;
public nextGenerationId = 1; public nextGenerationId = 1;
public generationCounts: { public generationCounts: {
currentGenerationCount: number; currentGenerationCount: number;
@ -23,10 +33,37 @@ export class GameRules {
public constructor(startingTimeInSeconds: number) { public constructor(startingTimeInSeconds: number) {
this.lastSpawnTimeInSeconds = startingTimeInSeconds; this.lastSpawnTimeInSeconds = startingTimeInSeconds;
this.lastGenerationChangeTimeInSeconds = startingTimeInSeconds;
} }
private lastSpawnAction: SpawnAction | undefined;
public getSpawnAction(timeInSeconds: number, canvasSize: vec2): SpawnAction { public getSpawnAction(timeInSeconds: number, canvasSize: vec2): SpawnAction {
if (timeInSeconds - this.lastSpawnTimeInSeconds < settings.spawnInterval) { if (
this.lastSpawnAction &&
timeInSeconds - this.lastSpawnTimeInSeconds < GameRules.DEAFULT_SPAWN_TIME_LENGTH
) {
return this.lastSpawnAction;
}
this.currentSpawnInterval = mix(
GameRules.DEAFULT_SPAWN_INTERVAL,
GameRules.DEAFULT_SPAWN_INTERVAL / 5,
clamp01((timeInSeconds - this.lastGenerationChangeTimeInSeconds) / 120)
);
this.currentSpawnRadius = mix(
GameRules.DEFAULT_SPAWN_RADIUS,
GameRules.DEFAULT_SPAWN_RADIUS * 3,
clamp01((timeInSeconds - this.lastGenerationChangeTimeInSeconds) / 120)
);
const q = this.generationCounts.nextGenerationCount / settings.agentCount;
if (
timeInSeconds - this.lastSpawnTimeInSeconds < this.currentSpawnInterval ||
q > 0.05
) {
return { return {
generation: this.nextGenerationId, generation: this.nextGenerationId,
position: vec2.create(), position: vec2.create(),
@ -36,14 +73,16 @@ export class GameRules {
this.lastSpawnTimeInSeconds = timeInSeconds; this.lastSpawnTimeInSeconds = timeInSeconds;
return { this.lastSpawnAction = {
generation: this.nextGenerationId, generation: this.nextGenerationId,
position: vec2.fromValues( position: vec2.fromValues(
Random.randomBetween(0, canvasSize.x), Random.randomBetween(0, canvasSize.x),
Random.randomBetween(0, canvasSize.y) Random.randomBetween(0, canvasSize.y)
), ),
radius: settings.spawnRadius, radius: this.currentSpawnRadius,
}; };
return this.lastSpawnAction;
} }
public updateGenerationCounts({ public updateGenerationCounts({
@ -55,8 +94,9 @@ export class GameRules {
const currentGenerationCount = const currentGenerationCount =
this.nextGenerationId % 2 === 1 ? evenGenerationCount : oddGenerationCount; this.nextGenerationId % 2 === 1 ? evenGenerationCount : oddGenerationCount;
if (currentGenerationCount === 0) { if (currentGenerationCount <= 100) {
this.nextGenerationId++; this.nextGenerationId++;
this.lastGenerationChangeTimeInSeconds = performance.now() / 1000;
} }
this.generationCounts = { this.generationCounts = {
@ -64,4 +104,19 @@ export class GameRules {
nextGenerationCount, nextGenerationCount,
}; };
} }
public getNextGenerationMoveSpeed(): number {
const q = this.generationCounts.nextGenerationCount / settings.agentCount;
return mix(settings.moveSpeed / 8, settings.moveSpeed, q ** 2);
}
public getInfectionProbability(): number {
const q = this.generationCounts.nextGenerationCount / settings.agentCount;
return clamp(mix(0.3, 1, q * 5), 0, 0.9);
}
public getSensorOffset(): number {
const q = this.generationCounts.nextGenerationCount / settings.agentCount;
return mix(20, settings.sensorOffsetDistance, q);
}
} }

View file

@ -37,7 +37,7 @@
<noscript>JavaScript is required for this website.</noscript> <noscript>JavaScript is required for this website.</noscript>
</section> </section>
<div class="counters"><pre></pre></div> <!-- <div class="counters"><pre></pre></div> -->
</main> </main>
<aside> <aside>
@ -49,7 +49,7 @@
<button class="restart"></button> <button class="restart"></button>
</nav> </nav>
<main class="pages info-page"> <main class="pages hidden info-page">
<section> <section>
<h1>Just a bunch of blobs</h1> <h1>Just a bunch of blobs</h1>
<p> <p>
@ -57,8 +57,6 @@
perspiciatis nesciunt, molestiae officiis dignissimos porro! Provident totam perspiciatis nesciunt, molestiae officiis dignissimos porro! Provident totam
sit enim, dolores dicta possimus ex assumenda earum, ea tempore, aut quidem? sit enim, dolores dicta possimus ex assumenda earum, ea tempore, aut quidem?
</p> </p>
<button id="share" class="large-button">Share</button>
</section> </section>
</main> </main>

View file

@ -1,4 +1,5 @@
import '../assets/icons/info.svg'; import '../assets/icons/info.svg';
import { isProduction, lastEdit } from './constants';
import GameLoop from './game-loop/game-loop'; import GameLoop from './game-loop/game-loop';
import { GameRules } from './game-loop/game-rules'; import { GameRules } from './game-loop/game-rules';
import './index.scss'; import './index.scss';
@ -11,7 +12,6 @@ import { resetSettings } from './settings';
import { applyArrayPlugins } from './utils/array'; import { applyArrayPlugins } from './utils/array';
import { DeltaTimeCalculator } from './utils/delta-time-calculator'; import { DeltaTimeCalculator } from './utils/delta-time-calculator';
import { ErrorHandler, Severity } from './utils/error-handler'; import { ErrorHandler, Severity } from './utils/error-handler';
import { formatNumber } from './utils/format-number';
import { initializeGpu } from './utils/graphics/initialize-gpu'; import { initializeGpu } from './utils/graphics/initialize-gpu';
declare global { declare global {
@ -49,7 +49,7 @@ const elements = {
canvas: document.querySelector('canvas') as HTMLCanvasElement, canvas: document.querySelector('canvas') as HTMLCanvasElement,
canvasContainer: document.querySelector('main.canvas-container') as HTMLCanvasElement, canvasContainer: document.querySelector('main.canvas-container') as HTMLCanvasElement,
errorContainer: document.querySelector('.errors-container') as HTMLDivElement, errorContainer: document.querySelector('.errors-container') as HTMLDivElement,
counters: document.querySelector('.counters > pre') as HTMLPreElement, // counters: document.querySelector('.counters > pre') as HTMLPreElement,
}; };
const main = async () => { const main = async () => {
@ -79,7 +79,10 @@ const main = async () => {
); );
settingsPageHandler.onOpen = infoPageHandler.close.bind(infoPageHandler); settingsPageHandler.onOpen = infoPageHandler.close.bind(infoPageHandler);
infoPageHandler.onOpen = settingsPageHandler.close.bind(settingsPageHandler); infoPageHandler.onOpen = settingsPageHandler.close.bind(settingsPageHandler);
infoPageHandler.open();
if (isProduction) {
infoPageHandler.open();
}
new MenuHider( new MenuHider(
elements.aside, elements.aside,
@ -99,7 +102,6 @@ const main = async () => {
elements.restartButton.addEventListener('click', () => game?.destroy()); elements.restartButton.addEventListener('click', () => game?.destroy());
const deltaTimeCalculator = new DeltaTimeCalculator(); const deltaTimeCalculator = new DeltaTimeCalculator();
const gameRules = new GameRules(performance.now() / 1000);
let sliders: Array<SettingsSlider<any>> = []; let sliders: Array<SettingsSlider<any>> = [];
elements.applyDefaults.addEventListener('click', () => { elements.applyDefaults.addEventListener('click', () => {
@ -107,15 +109,18 @@ const main = async () => {
sliders.forEach((slider) => slider.updateSliderValueBasedOnSource()); sliders.forEach((slider) => slider.updateSliderValueBasedOnSource());
}); });
const updateCounters = () => { console.log({ lastEdit });
elements.counters.innerHTML = `FPS: ${deltaTimeCalculator.fps.toFixed(2)}
current gen: ${formatNumber(game?.aliveAgentCounts.currentGenerationCount ?? 0)} // const updateCounters = () => {
next gen: ${formatNumber(game?.aliveAgentCounts.nextGenerationCount ?? 0)}`; // elements.counters.innerHTML = `FPS: ${deltaTimeCalculator.fps.toFixed(2)}
window.requestAnimationFrame(updateCounters); // current gen: ${formatNumber(game?.aliveAgentCounts.currentGenerationCount ?? 0)}
}; // next gen: ${formatNumber(game?.aliveAgentCounts.nextGenerationCount ?? 0)}`;
updateCounters(); // window.requestAnimationFrame(updateCounters);
// };
// updateCounters();
while (!shouldStop) { while (!shouldStop) {
const gameRules = new GameRules(performance.now() / 1000);
game = new GameLoop(elements.canvas, gpu, deltaTimeCalculator, gameRules); game = new GameLoop(elements.canvas, gpu, deltaTimeCalculator, gameRules);
if (sliders.length === 0) { if (sliders.length === 0) {

View file

@ -1,3 +1,4 @@
import { isProduction } from '../constants';
import { settings } from '../settings'; import { settings } from '../settings';
import { SettingsSlider, ValueScaling } from './settings-slider'; import { SettingsSlider, ValueScaling } from './settings-slider';
@ -6,16 +7,17 @@ export const setUpSettingsPage = (
maxAgentCount: number maxAgentCount: number
): Array<SettingsSlider<any>> => { ): Array<SettingsSlider<any>> => {
const sliders = [ const sliders = [
new SettingsSlider(settings, 'renderSpeed', { !isProduction &&
min: 1, new SettingsSlider(settings, 'renderSpeed', {
max: 10, min: 1,
rounding: Math.round, max: 10,
}), rounding: Math.round,
}),
new SettingsSlider(settings, 'agentCount', { new SettingsSlider(settings, 'agentCount', {
min: 1, min: 1,
max: maxAgentCount, max: maxAgentCount,
scaling: ValueScaling.Logarithmic, scaling: ValueScaling.Quadratic,
rounding: Math.round, rounding: Math.round,
}), }),
@ -61,12 +63,6 @@ export const setUpSettingsPage = (
max: 1, max: 1,
}), }),
new SettingsSlider(settings, 'deinfectionProbability', {
min: 0,
max: 1,
scaling: ValueScaling.Quadratic,
}),
new SettingsSlider(settings, 'individualTrailWeight', { new SettingsSlider(settings, 'individualTrailWeight', {
min: 0, min: 0,
max: 1, max: 1,
@ -79,7 +75,8 @@ export const setUpSettingsPage = (
new SettingsSlider(settings, 'decayRateTrails', { new SettingsSlider(settings, 'decayRateTrails', {
min: 0.1, min: 0.1,
max: 1000, max: 5000,
scaling: ValueScaling.Quadratic,
}), }),
new SettingsSlider(settings, 'diffusionRateBrush', { new SettingsSlider(settings, 'diffusionRateBrush', {
@ -92,22 +89,6 @@ export const setUpSettingsPage = (
max: 100, max: 100,
}), }),
new SettingsSlider(settings, 'spawnRadius', {
min: 0,
max: 1000,
}),
new SettingsSlider(settings, 'spawnInterval', {
min: 0.1,
max: 600,
scaling: ValueScaling.Quadratic,
}),
new SettingsSlider(settings, 'clarity', {
min: 0,
max: 0.5,
}),
new SettingsSlider(settings, 'brushSize', { new SettingsSlider(settings, 'brushSize', {
min: 1, min: 1,
max: 30, max: 30,
@ -116,9 +97,11 @@ export const setUpSettingsPage = (
const sliderContainerElement = document.createElement('div'); const sliderContainerElement = document.createElement('div');
sliders.forEach((slider) => { sliders
sliderContainerElement.appendChild(slider.element); .filter((v) => v)
}); .forEach((slider) => {
sliderContainerElement.appendChild(slider.element);
});
settingsPage.appendChild(sliderContainerElement); settingsPage.appendChild(sliderContainerElement);

View file

@ -28,7 +28,4 @@ fn main(
} else { } else {
atomicAdd(&counters.oddGenerationAlive, 1); atomicAdd(&counters.oddGenerationAlive, 1);
} }
// atomicStore(&counters.evenGenerationAlive, settings.agentCount);
// atomicStore(&counters.oddGenerationAlive, workgroup_count.y);
} }

View file

@ -3,7 +3,7 @@ fn main(
@builtin(global_invocation_id) global_id: vec3<u32>, @builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(num_workgroups) workgroup_count: vec3<u32> @builtin(num_workgroups) workgroup_count: vec3<u32>
) { ) {
let id = global_id.x + global_id.y * (workgroup_count.x * 64) + global_id.z * (workgroup_count.x * workgroup_count.y * 64); let id = get_id(global_id, workgroup_count);
if id >= arrayLength(&agents) { if id >= arrayLength(&agents) {
return; return;

View file

@ -5,3 +5,7 @@ struct Agent {
} }
@group(1) @binding(1) var<storage, read_write> agents: array<Agent>; @group(1) @binding(1) var<storage, read_write> agents: array<Agent>;
fn get_id(global_id: vec3<u32>, workgroup_count: vec3<u32>) -> u32 {
return global_id.x + global_id.y * (workgroup_count.x * 64) + global_id.z * (workgroup_count.x * workgroup_count.y * 64);
}

View file

@ -9,7 +9,7 @@ import { vec2 } from 'gl-matrix';
export class AgentPipeline { export class AgentPipeline {
private static readonly WORKGROUP_SIZE = 64; private static readonly WORKGROUP_SIZE = 64;
private static readonly UNIFORM_COUNT = 16; private static readonly UNIFORM_COUNT = 19;
private readonly bindGroupLayout: GPUBindGroupLayout; private readonly bindGroupLayout: GPUBindGroupLayout;
private readonly pipeline: GPUComputePipeline; private readonly pipeline: GPUComputePipeline;
@ -44,26 +44,33 @@ export class AgentPipeline {
} }
public setParameters({ public setParameters({
deltaTime,
center,
radius,
brushTrailWeight, brushTrailWeight,
moveSpeed, moveSpeed,
turnSpeed, turnSpeed,
sensorOffsetAngle, sensorOffsetAngle,
sensorOffsetDistance, sensorOffsetDistance,
nextGenerationSensorOffsetDistance,
currentGenerationAggression, currentGenerationAggression,
nextGenerationAggression, nextGenerationAggression,
nextGenerationSpeed,
isNextGenerationOdd, isNextGenerationOdd,
center,
radius,
turnWhenLost, turnWhenLost,
individualTrailWeight, individualTrailWeight,
deinfectionProbability, infectionProbability,
agentCount, agentCount,
}: AgentSettings & { }: AgentSettings & {
deltaTime: number;
currentGenerationAggression: number; currentGenerationAggression: number;
nextGenerationAggression: number; nextGenerationAggression: number;
nextGenerationSensorOffsetDistance: number;
nextGenerationSpeed: number;
isNextGenerationOdd: number; isNextGenerationOdd: number;
center: vec2; center: vec2;
radius: number; radius: number;
infectionProbability: number;
agentCount: number; agentCount: number;
}) { }) {
this.agentCount = agentCount; this.agentCount = agentCount;
@ -75,19 +82,21 @@ export class AgentPipeline {
radius, radius,
brushTrailWeight, brushTrailWeight,
moveSpeed, moveSpeed * deltaTime,
turnSpeed, turnSpeed * deltaTime,
(sensorOffsetAngle * Math.PI) / 180, (sensorOffsetAngle * Math.PI) / 180,
sensorOffsetDistance, sensorOffsetDistance,
currentGenerationAggression, currentGenerationAggression,
nextGenerationAggression, nextGenerationAggression,
nextGenerationSensorOffsetDistance,
nextGenerationSpeed * deltaTime,
isNextGenerationOdd, isNextGenerationOdd,
turnWhenLost, turnWhenLost,
individualTrailWeight, individualTrailWeight,
deinfectionProbability, infectionProbability,
agentCount, agentCount,
]) ])

View file

@ -6,7 +6,6 @@ export interface AgentSettings {
sensorOffsetDistance: number; sensorOffsetDistance: number;
turnWhenLost: number; turnWhenLost: number;
individualTrailWeight: number; individualTrailWeight: number;
deinfectionProbability: number;
currentGenerationAggression: number; currentGenerationAggression: number;
nextGenerationAggression: number; nextGenerationAggression: number;
} }

View file

@ -3,7 +3,7 @@ struct Settings {
radius: f32, radius: f32,
brushTrailWeight: f32, brushTrailWeight: f32,
moveRate: f32, currentGenerationMoveRate: f32,
turnRate: f32, turnRate: f32,
sensorAngle: f32, sensorAngle: f32,
@ -11,11 +11,13 @@ struct Settings {
currentGenerationAggression: f32, currentGenerationAggression: f32,
nextGenerationAggression: f32, nextGenerationAggression: f32,
nextGenerationSensorOffsetDistance: f32,
nextGenerationMoveRate: f32,
isNextGenerationOdd: f32, isNextGenerationOdd: f32,
turnWhenLost: f32, turnWhenLost: f32,
individualTrailWeight: f32, individualTrailWeight: f32,
deinfectionProbability: f32, infectionProbability: f32,
agentCount: f32 // might be smaller than the length of the agents array agentCount: f32 // might be smaller than the length of the agents array
}; };
@ -30,12 +32,13 @@ struct Settings {
@group(1) @binding(2) var trailMapIn: texture_2d<f32>; @group(1) @binding(2) var trailMapIn: texture_2d<f32>;
@group(1) @binding(3) var trailMapOut: texture_storage_2d<rgba16float, write>; @group(1) @binding(3) var trailMapOut: texture_storage_2d<rgba16float, write>;
@compute @workgroup_size(64) @compute @workgroup_size(64)
fn main( fn main(
@builtin(global_invocation_id) global_id: vec3<u32>, @builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(num_workgroups) workgroup_count: vec3<u32> @builtin(num_workgroups) workgroup_count: vec3<u32>
) { ) {
let id = global_id.x + global_id.y * (workgroup_count.x * 64) + global_id.z * (workgroup_count.x * workgroup_count.y * 64); let id = get_id(global_id, workgroup_count);
if id >= u32(settings.agentCount) { if id >= u32(settings.agentCount) {
return; return;
@ -48,94 +51,84 @@ fn main(
noiseSampler, noiseSampler,
vec2( vec2(
f32(id) % 23647 / 2000, f32(id) % 23647 / 2000,
agent.angle / 10 state.time % 3243 / 2000
) + agent.position / state.size, ),
0 0
); );
let isFromCurrentGeneration = abs(agent.generation - settings.isNextGenerationOdd); let isFromCurrentGeneration = abs(agent.generation - settings.isNextGenerationOdd);
let isFromOddGeneration = agent.generation == 1.0; let isFromNextGeneration = 1.0 - isFromCurrentGeneration;
let isFromOddGeneration = agent.generation % 2;
let trailForward = sense(agent.position, agent.angle, settings.sensorOffset, 0); let sensorOffset = mix(settings.sensorOffset, settings.nextGenerationSensorOffsetDistance, isFromNextGeneration);
let trailLeft = sense(agent.position, agent.angle, settings.sensorOffset, settings.sensorAngle); let moveRate = mix(settings.currentGenerationMoveRate, settings.nextGenerationMoveRate, isFromNextGeneration);
let trailRight = sense(agent.position, agent.angle, settings.sensorOffset, -settings.sensorAngle); let brushWeight = mix(settings.brushTrailWeight, 0, isFromNextGeneration);
let trailForward = sense(agent.position, agent.angle, sensorOffset, 0);
let trailLeft = sense(agent.position, agent.angle, sensorOffset, settings.sensorAngle);
let trailRight = sense(agent.position, agent.angle, sensorOffset, -settings.sensorAngle);
let brushWeight = isFromCurrentGeneration * settings.brushTrailWeight - (1 - isFromCurrentGeneration) * settings.brushTrailWeight; var weightForward = brushWeight * trailForward.a;
var weightForward: f32 = brushWeight * trailForward.a; var weightLeft = brushWeight * trailLeft.a;
var weightLeft: f32 = brushWeight * trailLeft.a; var weightRight = brushWeight * trailRight.a;
var weightRight: f32 = brushWeight * trailRight.a;
let agression = isFromCurrentGeneration * settings.currentGenerationAggression + (1.0 - isFromCurrentGeneration) * settings.nextGenerationAggression; let agression = mix(settings.currentGenerationAggression, settings.nextGenerationAggression, isFromNextGeneration) + weightForward;
if (isFromOddGeneration) {
weightForward += trailForward.g + agression * trailForward.r;
weightLeft += trailLeft.g + agression * trailLeft.r;
weightRight += trailRight.g + agression * trailRight.r;
} else {
weightForward += trailForward.r + agression * trailForward.g;
weightLeft += trailLeft.r + agression * trailLeft.g;
weightRight += trailRight.r + agression * trailRight.g;
}
var rotation: f32 = 0; weightForward += mix(trailForward.r + agression * trailForward.g, trailForward.g + agression * trailForward.r, isFromOddGeneration);
weightLeft += mix(trailLeft.r + agression * trailLeft.g, trailLeft.g + agression * trailLeft.r, isFromOddGeneration);
weightRight += mix(trailRight.r + agression * trailRight.g, trailRight.g + agression * trailRight.r, isFromOddGeneration);
var rotation: f32;
if weightForward >= weightLeft && weightForward >= weightRight { if weightForward >= weightLeft && weightForward >= weightRight {
rotation = (random.r - 0.5) * 0.0 * settings.turnRate * state.deltaTime; rotation = 0;
} else if weightLeft < weightRight {
rotation = -settings.turnRate * state.deltaTime;
} else if weightRight < weightLeft {
rotation = settings.turnRate * state.deltaTime;
} else { } else {
rotation = (random.r - 0.5) * settings.turnWhenLost * settings.turnRate * state.deltaTime; rotation = sign(weightLeft - weightRight) * settings.turnRate;
} }
let direction = vec2(cos(agent.angle), sin(agent.angle)); let nextPosition = clamp(
var nextPosition = agent.position + direction * settings.moveRate * state.deltaTime; agent.position + vec2(cos(agent.angle), sin(agent.angle)) * moveRate,
nextPosition = clamp(nextPosition, vec2<f32>(0, 0), state.size); vec2<f32>(0, 0),
state.size
);
if nextPosition.x == 0 || nextPosition.x == state.size.x || nextPosition.y == 0 || nextPosition.y == state.size.y { if nextPosition.x == 0 || nextPosition.x == state.size.x || nextPosition.y == 0 || nextPosition.y == state.size.y {
rotation = 3.14159265359 + random.a - 0.5; rotation = 3.14159265359 + random.a - 0.5;
} }
var trail = vec4<f32>(settings.individualTrailWeight, 0, 0, 0); var trail = vec4<f32>(settings.individualTrailWeight, 0, 0, 0);
if isFromOddGeneration { if isFromOddGeneration == 1.0 {
trail = vec4<f32>(0, settings.individualTrailWeight, 0, 0); trail = vec4<f32>(0, settings.individualTrailWeight, 0, 0);
} }
agent.position = nextPosition;
agent.angle += rotation;
var trailBelow = textureLoad(trailMapIn, vec2<i32>(nextPosition), 0); var trailBelow = textureLoad(trailMapIn, vec2<i32>(nextPosition), 0);
agent.angle += rotation;
trailBelow += trail;
if settings.radius > 0 && length(settings.center - agent.position) < settings.radius { if settings.radius > 0 && length(settings.center - agent.position) < settings.radius {
agent.generation = settings.isNextGenerationOdd; agent.generation = settings.isNextGenerationOdd;
// clear trail map below so the agent won't die immediately // clear trail map below so the agent won't die immediately
if (settings.isNextGenerationOdd == 1.0) { // trailBelow.r = (1 - settings.isNextGenerationOdd) * (trailBelow.r + trailBelow.g);
trailBelow.g += trailBelow.r; // trailBelow.g = settings.isNextGenerationOdd * (trailBelow.r + trailBelow.g);
trailBelow.r = 0;
} else {
trailBelow.r += trailBelow.g;
trailBelow.g = 0;
}
textureStore(trailMapOut, vec2<i32>(nextPosition), trailBelow);
} else { } else {
textureStore(trailMapOut, vec2<i32>(nextPosition), trail + trailBelow); let relativeWeight = mix(trailBelow.g - trailBelow.r, trailBelow.r - trailBelow.g, isFromOddGeneration);
if relativeWeight > 0 && (
if isFromOddGeneration { (isFromCurrentGeneration == 1.0 && trailBelow.a == 0 && random.b < settings.infectionProbability)
if trailBelow.g < trailBelow.r && (isFromCurrentGeneration == 1.0 || (isFromCurrentGeneration == 0.0 && random.a < settings.deinfectionProbability)) { || (isFromCurrentGeneration == 0.0 && trailBelow.a > 0)
agent.generation = 0; ) {
} // trailBelow.r = isFromOddGeneration * (trailBelow.r + trailBelow.g);
} else { // trailBelow.g = (1 - isFromOddGeneration) * (trailBelow.r + trailBelow.g);
if trailBelow.r < trailBelow.g && (isFromCurrentGeneration == 1.0 || (isFromCurrentGeneration == 0.0 && random.a < settings.deinfectionProbability)) { agent.generation = (agent.generation + 1) % 2;
agent.generation = 1;
}
} }
} }
textureStore(trailMapOut, vec2<i32>(nextPosition), trailBelow);
agent.position = nextPosition;
agents[id] = agent; agents[id] = agent;
} }
fn sense(agentPosition: vec2<f32>, agentAngle: f32, sensorOffset: f32, sensorOffsetAngle: f32) -> vec4<f32> { fn sense(agentPosition: vec2<f32>, agentAngle: f32, sensorOffset: f32, sensorOffsetAngle: f32) -> vec4<f32> {
let sensorAngle = agentAngle + sensorOffsetAngle; let sensorAngle = agentAngle + sensorOffsetAngle;
let sensorDirection = vec2(cos(sensorAngle), sin(sensorAngle)); let sensorPosition = vec2<i32>(agentPosition + vec2(cos(sensorAngle), sin(sensorAngle)) * sensorOffset);
let sensorPosition = vec2<i32>(agentPosition + sensorDirection * sensorOffset);
return textureLoad(trailMapIn, sensorPosition, 0); return textureLoad(trailMapIn, sensorPosition, 0);
} }

View file

@ -28,7 +28,7 @@ fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
) / 8; ) / 8;
let decayed = clamp(vec4( let decayed = clamp(vec4(
current.rgb - settings.decayRateTrails, clamp(current.rgb + (current.rgb - 1.001) * settings.decayRateTrails, vec3(0), vec3(1)),
max(0, current.a + (current.a - 1.001) * settings.decayRateBrush) max(0, current.a + (current.a - 1.001) * settings.decayRateBrush)
), vec4(0), vec4(1)); ), vec4(0), vec4(1));

View file

@ -34,5 +34,5 @@ fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
} }
fn clarity(strength: f32) -> f32 { fn clarity(strength: f32) -> f32 {
return pow(strength, 5) - strength * settings.clarity + sign(strength) * settings.clarity; return sign(strength);
} }

View file

@ -13,28 +13,24 @@ const initialValues: GameLoopSettings &
agentCount: 1_001_500, agentCount: 1_001_500,
currentGenerationAggression: -5, currentGenerationAggression: -5,
nextGenerationAggression: 0.5, nextGenerationAggression: 0.2,
moveSpeed: 90, moveSpeed: 90,
turnSpeed: 78, turnSpeed: 53,
sensorOffsetAngle: 41, sensorOffsetAngle: 40,
sensorOffsetDistance: 45, sensorOffsetDistance: 43,
turnWhenLost: 0.43, turnWhenLost: 0.08,
deinfectionProbability: 1,
brushTrailWeight: 500, brushTrailWeight: 500,
individualTrailWeight: 0.2, individualTrailWeight: 0.78,
diffusionRateTrails: 0.29, diffusionRateTrails: 1.11,
decayRateTrails: 21.95, decayRateTrails: 718,
diffusionRateBrush: 0.25, diffusionRateBrush: 0.25,
decayRateBrush: 15, decayRateBrush: 15,
spawnRadius: 8,
spawnInterval: 8,
clarity: 0, clarity: 0,
brushSize: 12, brushSize: 16,
brushSizeVariation: 0.5, // hidden on the UI brushSizeVariation: 0.5, // hidden on the UI

View file

@ -3,6 +3,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
const InlineSourceWebpackPlugin = require('inline-source-webpack-plugin'); const InlineSourceWebpackPlugin = require('inline-source-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const DefinePlugin = require('webpack').DefinePlugin;
module.exports = (env, argv) => ({ module.exports = (env, argv) => ({
devtool: argv.mode === 'development' ? 'inline-source-map' : false, devtool: argv.mode === 'development' ? 'inline-source-map' : false,
@ -26,15 +27,15 @@ module.exports = (env, argv) => ({
template: './src/index.html', template: './src/index.html',
}), }),
new MiniCssExtractPlugin(), new MiniCssExtractPlugin(),
argv.mode === 'production' argv.mode === 'production' &&
? new InlineSourceWebpackPlugin({ new InlineSourceWebpackPlugin({
compress: true, compress: true,
}) }),
: null, new DefinePlugin({
new (require('webpack').DefinePlugin)({
__CURRENT_DATE__: Date.now(), __CURRENT_DATE__: Date.now(),
__IS_PRODUCTION__: argv.mode === 'production',
}), }),
].filter(Boolean), ].filter((v) => v),
module: { module: {
rules: [ rules: [
{ {