This commit is contained in:
Andras Schmelczer 2026-05-19 21:03:53 +01:00
commit 7c70f15e49
65 changed files with 1122 additions and 1906 deletions

View file

@ -15,10 +15,7 @@ struct Counters {
var<workgroup> workgroupAliveCount: atomic<u32>;
var<workgroup> workgroupCompactedOffset: u32;
fn dead_agent() -> Agent {
return Agent(vec2<f32>(0.0, 0.0), 0.0, -1.0, vec2<f32>(-1.0, -1.0), 0.0, 0.0);
}
var<workgroup> clearAliveAgentCount: u32;
@compute @workgroup_size(64)
fn main(
@ -35,12 +32,12 @@ fn main(
workgroupBarrier();
var localCompactedIndex = 0u;
var agent = dead_agent();
var agent: Agent;
var isAlive = false;
if id < settings.agentCount {
agent = agents[id];
isAlive = agent.colorIndex >= 0.0;
isAlive = agents[id].colorIndex >= 0.0;
if isAlive {
agent = agents[id];
localCompactedIndex = atomicAdd(&workgroupAliveCount, 1u);
}
}
@ -66,16 +63,22 @@ fn main(
@compute @workgroup_size(64)
fn clearCompactedTail(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(local_invocation_id) local_id: vec3<u32>,
@builtin(num_workgroups) num_workgroups: vec3<u32>
) {
let id = get_id(global_id, num_workgroups);
if local_id.x == 0u {
clearAliveAgentCount = atomicLoad(&counters.aliveAgentCount);
}
workgroupBarrier();
if id >= settings.agentCount {
return;
}
let aliveAgentCount = atomicLoad(&counters.aliveAgentCount);
if id >= aliveAgentCount {
compactedAgents[id] = dead_agent();
if id >= clearAliveAgentCount {
compactedAgents[id].colorIndex = -1.0;
}
}

View file

@ -39,6 +39,15 @@ export class AgentGenerationPipeline {
private readonly agentCountUniformValues = new Uint32Array(
AgentGenerationPipeline.UNIFORM_COUNT
);
private readonly resizeUniformBuffer = new ArrayBuffer(
AgentGenerationPipeline.UNIFORM_COUNT * Uint32Array.BYTES_PER_ELEMENT
);
private readonly resizeUniformFloatValues = new Float32Array(
this.resizeUniformBuffer
);
private readonly resizeUniformUintValues = new Uint32Array(
this.resizeUniformBuffer
);
public constructor(
private readonly device: GPUDevice,
@ -233,11 +242,11 @@ export class AgentGenerationPipeline {
return;
}
this.device.queue.writeBuffer(
this.uniforms,
0,
new Float32Array([scale[0], scale[1], agentCount, 0])
);
this.resizeUniformFloatValues[0] = scale[0];
this.resizeUniformFloatValues[1] = scale[1];
this.resizeUniformUintValues[2] = Math.max(0, Math.floor(agentCount));
this.resizeUniformUintValues[3] = 0;
this.device.queue.writeBuffer(this.uniforms, 0, this.resizeUniformBuffer);
const commandEncoder = this.device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();

View file

@ -1,7 +1,7 @@
struct ResizeSettings {
scale: vec2<f32>,
agentCount: f32,
padding: f32,
agentCount: u32,
padding: u32,
};
@group(1) @binding(0) var<uniform> resizeSettings: ResizeSettings;
@ -13,10 +13,11 @@ fn main(
) {
let id = get_id(global_id, num_workgroups);
if id >= u32(resizeSettings.agentCount) {
if id >= resizeSettings.agentCount {
return;
}
agents[id].position = agents[id].position * resizeSettings.scale;
agents[id].targetPosition = agents[id].targetPosition * resizeSettings.scale;
let scale = resizeSettings.scale;
agents[id].position = agents[id].position * scale;
agents[id].targetPosition = agents[id].targetPosition * scale;
}

View file

@ -10,21 +10,19 @@ import { AgentSettings } from './agent-settings';
import shader from './agent.wgsl?raw';
export class AgentPipeline {
private static readonly UNIFORM_COUNT = 33;
private static readonly UNIFORM_COUNT = 30;
private readonly bindGroupLayout: GPUBindGroupLayout;
private readonly pipeline: GPUComputePipeline;
private readonly uniforms: GPUBuffer;
private readonly uniformValues = new Float32Array(AgentPipeline.UNIFORM_COUNT);
private readonly uniformUintValues = new Uint32Array(this.uniformValues.buffer);
private readonly uniformCache = createCachedFloat32BufferWrite(
AgentPipeline.UNIFORM_COUNT
);
private readonly bindGroupsByAgentsBuffer = new WeakMap<
GPUBuffer,
WeakMap<
GPUTextureView,
WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>
>
WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>
>();
private agentCount = 0;
@ -69,9 +67,6 @@ export class AgentPipeline {
color3ToColor1,
color3ToColor2,
color3ToColor3,
sourceAttractionWeight,
sourceSlowMoveRate,
sourceTrailWeightMultiplier,
forwardRotationScale,
introNearDistanceInner,
introNearDistanceMin,
@ -100,7 +95,7 @@ export class AgentPipeline {
this.uniformValues[4] = sensorOffsetDistance;
this.uniformValues[5] = turnWhenLost;
this.uniformValues[6] = individualTrailWeight;
this.uniformValues[7] = agentCount;
this.uniformUintValues[7] = Math.max(0, Math.floor(agentCount));
this.uniformValues[8] = introProgress ?? 1;
this.uniformValues[9] = color1ToColor1;
this.uniformValues[10] = color1ToColor2;
@ -111,21 +106,18 @@ export class AgentPipeline {
this.uniformValues[15] = color3ToColor1;
this.uniformValues[16] = color3ToColor2;
this.uniformValues[17] = color3ToColor3;
this.uniformValues[18] = sourceAttractionWeight;
this.uniformValues[19] = sourceSlowMoveRate;
this.uniformValues[20] = sourceTrailWeightMultiplier;
this.uniformValues[21] = forwardRotationScale;
this.uniformValues[22] = introNearDistanceInner;
this.uniformValues[23] = introNearDistanceMin;
this.uniformValues[24] = introNearSensorOffsetMultiplier;
this.uniformValues[25] = introTargetAngleBlend;
this.uniformValues[26] = introProgressCutoff;
this.uniformValues[27] = introTurnRateMultiplier;
this.uniformValues[28] = introRandomTurnMultiplier;
this.uniformValues[29] = introFarMoveMultiplier;
this.uniformValues[30] = introNearMoveMultiplier;
this.uniformValues[31] = introStepStopDistance;
this.uniformValues[32] = randomTimeScale;
this.uniformValues[18] = forwardRotationScale;
this.uniformValues[19] = introNearDistanceInner;
this.uniformValues[20] = introNearDistanceMin;
this.uniformValues[21] = introNearSensorOffsetMultiplier;
this.uniformValues[22] = introTargetAngleBlend;
this.uniformValues[23] = introProgressCutoff;
this.uniformValues[24] = introTurnRateMultiplier;
this.uniformValues[25] = introRandomTurnMultiplier;
this.uniformValues[26] = introFarMoveMultiplier;
this.uniformValues[27] = introNearMoveMultiplier;
this.uniformValues[28] = introStepStopDistance;
this.uniformValues[29] = randomTimeScale;
writeFloat32BufferIfChanged(
this.device,
this.uniforms,
@ -137,14 +129,13 @@ export class AgentPipeline {
public execute(
commandEncoder: GPUCommandEncoder,
trailMapIn: GPUTextureView,
trailMapOut: GPUTextureView,
sourceMap: GPUTextureView
trailMapOut: GPUTextureView
) {
if (this.agentCount <= 0) {
return;
}
const bindGroup = this.getBindGroup(trailMapIn, trailMapOut, sourceMap);
const bindGroup = this.getBindGroup(trailMapIn, trailMapOut);
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(this.pipeline);
@ -156,32 +147,22 @@ export class AgentPipeline {
private getBindGroup(
trailMapIn: GPUTextureView,
trailMapOut: GPUTextureView,
sourceMap: GPUTextureView
trailMapOut: GPUTextureView
): GPUBindGroup {
const agentsBuffer = this.getAgentsBuffer();
let textureCache = this.bindGroupsByAgentsBuffer.get(agentsBuffer);
if (!textureCache) {
textureCache = new WeakMap<
GPUTextureView,
WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>
>();
textureCache = new WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>();
this.bindGroupsByAgentsBuffer.set(agentsBuffer, textureCache);
}
let outputCache = textureCache.get(trailMapIn);
if (!outputCache) {
outputCache = new WeakMap<GPUTextureView, WeakMap<GPUTextureView, GPUBindGroup>>();
outputCache = new WeakMap<GPUTextureView, GPUBindGroup>();
textureCache.set(trailMapIn, outputCache);
}
let sourceCache = outputCache.get(trailMapOut);
if (!sourceCache) {
sourceCache = new WeakMap<GPUTextureView, GPUBindGroup>();
outputCache.set(trailMapOut, sourceCache);
}
const cached = sourceCache.get(sourceMap);
const cached = outputCache.get(trailMapOut);
if (cached) {
return cached;
}
@ -209,14 +190,10 @@ export class AgentPipeline {
binding: 3,
resource: trailMapOut,
},
{
binding: 4,
resource: sourceMap,
},
],
});
sourceCache.set(sourceMap, bindGroup);
outputCache.set(trailMapOut, bindGroup);
return bindGroup;
}
@ -255,13 +232,6 @@ export class AgentPipeline {
format: 'rgba16float',
},
},
{
binding: 4,
visibility: GPUShaderStage.COMPUTE,
texture: {
sampleType: 'float',
},
},
],
};
}

View file

@ -14,9 +14,6 @@ export interface AgentSettings {
sensorOffsetDistance: number;
turnWhenLost: number;
individualTrailWeight: number;
sourceAttractionWeight: number;
sourceSlowMoveRate: number;
sourceTrailWeightMultiplier: number;
forwardRotationScale: number;
introNearDistanceMin: number;
introNearSensorOffsetMultiplier: number;

View file

@ -6,7 +6,7 @@ struct Settings {
sensorOffset: f32,
turnWhenLost: f32,
individualTrailWeight: f32,
agentCount: f32,
agentCount: u32,
introProgress: f32,
color1ToColor1: f32,
color1ToColor2: f32,
@ -17,9 +17,6 @@ struct Settings {
color3ToColor1: f32,
color3ToColor2: f32,
color3ToColor3: f32,
sourceAttractionWeight: f32,
sourceSlowMoveRate: f32,
sourceTrailWeightMultiplier: f32,
forwardRotationScale: f32,
introNearDistanceInner: f32,
introNearDistanceMin: f32,
@ -37,7 +34,6 @@ struct Settings {
@group(1) @binding(0) var<uniform> settings: Settings;
@group(1) @binding(2) var trailMapIn: texture_2d<f32>;
@group(1) @binding(3) var trailMapOut: texture_storage_2d<rgba16float, write>;
@group(1) @binding(4) var sourceMap: texture_2d<f32>;
@compute @workgroup_size(64)
fn main(
@ -46,7 +42,7 @@ fn main(
) {
let id = get_id(global_id, num_workgroups);
if id >= u32(settings.agentCount) {
if id >= settings.agentCount {
return;
}
@ -67,63 +63,15 @@ fn main(
}
}
let randomSeed = random_seed(id, state.time);
let randomTurn = random_float(randomSeed);
let direction = vec2(cos(angle), sin(angle));
let forwardSensor = sensor_position(position, direction, settings.sensorOffset);
let leftSensor = sensor_position(
position,
rotate_direction(direction, settings.sensorAngleSin, settings.sensorAngleCos),
settings.sensorOffset
);
let rightSensor = sensor_position(
position,
rotate_direction(direction, -settings.sensorAngleSin, settings.sensorAngleCos),
settings.sensorOffset
);
let trailForward = textureLoad(trailMapIn, forwardSensor, 0);
let trailLeft = textureLoad(trailMapIn, leftSensor, 0);
let trailRight = textureLoad(trailMapIn, rightSensor, 0);
let sourceForwardSample = textureLoad(sourceMap, forwardSensor, 0);
let sourceLeftSample = textureLoad(sourceMap, leftSensor, 0);
let sourceRightSample = textureLoad(sourceMap, rightSensor, 0);
let channelMask = get_channel_mask(colorIndex);
let reactionMask = get_reaction_mask(colorIndex);
let trailForwardWeight = dot(trailForward.rgb, reactionMask);
let trailLeftWeight = dot(trailLeft.rgb, reactionMask);
let trailRightWeight = dot(trailRight.rgb, reactionMask);
let sourceForwardWeight = dot(sourceForwardSample.rgb, reactionMask);
let sourceLeftWeight = dot(sourceLeftSample.rgb, reactionMask);
let sourceRightWeight = dot(sourceRightSample.rgb, reactionMask);
let weightForward =
trailForwardWeight + sourceForwardWeight * settings.sourceAttractionWeight;
let weightLeft = trailLeftWeight + sourceLeftWeight * settings.sourceAttractionWeight;
let weightRight =
trailRightWeight + sourceRightWeight * settings.sourceAttractionWeight;
var rotation = (randomTurn - 0.5) * settings.turnWhenLost;
if weightForward >= weightLeft && weightForward >= weightRight {
rotation = rotation * settings.forwardRotationScale;
} else {
rotation += sign(weightLeft - weightRight) * settings.turnRate;
}
let sourceAtAgent = textureLoad(sourceMap, vec2<i32>(position), 0);
let positiveReactionMask = max(reactionMask, vec3<f32>(0.0));
let sourceAtAgentStrength = clamp(dot(sourceAtAgent.rgb, positiveReactionMask), 0.0, 1.0);
var moveRate = settings.moveRate * mix(1.0, settings.sourceSlowMoveRate, sourceAtAgentStrength);
var introTargetOffset = vec2<f32>(0.0, 0.0);
var introTargetDistance = 0.0;
let randomSeed = random_seed(id, state.time);
var rotation = 0.0;
var step = vec2<f32>(0.0, 0.0);
if hasIntroTarget {
introTargetOffset = targetPosition - position;
introTargetDistance = length(introTargetOffset);
let introTargetOffset = targetPosition - position;
let introTargetDistance = length(introTargetOffset);
let targetAngle = atan2(introTargetOffset.y, introTargetOffset.x);
let nearTitle = 1.0 - smoothstep(
settings.introNearDistanceInner,
@ -148,19 +96,46 @@ fn main(
+ (random_float(randomSeed + 1013904223u) - 0.5) *
settings.turnWhenLost *
settings.introRandomTurnMultiplier;
moveRate = min(
let moveRate = min(
settings.moveRate *
mix(settings.introFarMoveMultiplier, settings.introNearMoveMultiplier, nearTitle),
introTargetDistance
);
}
var step = direction * moveRate;
if hasIntroTarget {
step = vec2<f32>(0.0, 0.0);
if introTargetDistance > settings.introStepStopDistance {
step = introTargetOffset / introTargetDistance * moveRate;
}
} else {
let randomTurn = random_float(randomSeed);
let direction = vec2(cos(angle), sin(angle));
let forwardSensor = sensor_position(position, direction, settings.sensorOffset);
let leftSensor = sensor_position(
position,
rotate_direction(direction, settings.sensorAngleSin, settings.sensorAngleCos),
settings.sensorOffset
);
let rightSensor = sensor_position(
position,
rotate_direction(direction, -settings.sensorAngleSin, settings.sensorAngleCos),
settings.sensorOffset
);
let trailForward = textureLoad(trailMapIn, forwardSensor, 0);
let trailLeft = textureLoad(trailMapIn, leftSensor, 0);
let trailRight = textureLoad(trailMapIn, rightSensor, 0);
let weightForward = dot(trailForward.rgb, reactionMask);
let weightLeft = dot(trailLeft.rgb, reactionMask);
let weightRight = dot(trailRight.rgb, reactionMask);
rotation = (randomTurn - 0.5) * settings.turnWhenLost;
if weightForward >= weightLeft && weightForward >= weightRight {
rotation = rotation * settings.forwardRotationScale;
} else {
rotation += sign(weightLeft - weightRight) * settings.turnRate;
}
step = direction * settings.moveRate;
}
let maxPosition = state.size - vec2<f32>(1.0, 1.0);
@ -169,14 +144,9 @@ fn main(
rotation = 3.14159265359 + random_float(randomSeed + 22695477u) - 0.5;
}
let sourceBelow = textureLoad(sourceMap, vec2<i32>(nextPosition), 0);
let sourceBelowStrength = clamp(dot(sourceBelow.rgb, positiveReactionMask), 0.0, 1.0);
let trailWeight =
settings.individualTrailWeight *
(1.0 + sourceBelowStrength * settings.sourceTrailWeightMultiplier);
var trailBelow = textureLoad(trailMapIn, vec2<i32>(nextPosition), 0);
trailBelow = vec4<f32>(
trailBelow.rgb + channelMask * trailWeight,
trailBelow.rgb + channelMask * settings.individualTrailWeight,
max(trailBelow.a, 0.0)
);