struct Settings { moveRate: f32, turnRate: f32, sensorAngle: f32, sensorOffset: f32, turnWhenLost: f32, individualTrailWeight: f32, agentCount: f32, introProgress: f32, color1ToColor1: f32, color1ToColor2: f32, color1ToColor3: f32, color2ToColor1: f32, color2ToColor2: f32, color2ToColor3: f32, color3ToColor1: f32, color3ToColor2: f32, color3ToColor3: f32, }; @group(1) @binding(0) var settings: Settings; @group(1) @binding(2) var trailMapIn: texture_2d; @group(1) @binding(3) var trailMapOut: texture_storage_2d; @group(1) @binding(4) var sourceMap: texture_2d; @compute @workgroup_size(64) fn main( @builtin(global_invocation_id) global_id: vec3, @builtin(num_workgroups) workgroup_count: vec3 ) { let id = get_id(global_id, workgroup_count); if id >= u32(settings.agentCount) { return; } var agent = agents[id]; if agent.colorIndex < 0.0 { return; } let hasIntroTarget = settings.introProgress < 0.999 && agent.targetPosition.x >= 0.0 && agent.targetPosition.y >= 0.0; if hasIntroTarget && settings.introProgress < agent.introDelay { return; } let random = textureSampleLevel( noise, noiseSampler, fract(vec2(f32(id) * 0.7548777, state.time * 0.00017 + f32(id) * 0.5698403)), 0 ); let forwardSensor = sensor_position(agent.position, agent.angle, settings.sensorOffset, 0); let leftSensor = sensor_position(agent.position, agent.angle, settings.sensorOffset, settings.sensorAngle); let rightSensor = sensor_position(agent.position, agent.angle, settings.sensorOffset, -settings.sensorAngle); 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(agent.colorIndex); let reactionMask = get_reaction_mask(agent.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 * 24.0; let weightLeft = trailLeftWeight + sourceLeftWeight * 24.0; let weightRight = trailRightWeight + sourceRightWeight * 24.0; var rotation = (random.r - 0.5) * settings.turnWhenLost; if weightForward >= weightLeft && weightForward >= weightRight { rotation = rotation * 0.25; } else { rotation += sign(weightLeft - weightRight) * settings.turnRate; } let sourceAtAgent = textureLoad(sourceMap, vec2(agent.position), 0); let positiveReactionMask = max(reactionMask, vec3(0.0)); let sourceAtAgentStrength = clamp(dot(sourceAtAgent.rgb, positiveReactionMask), 0.0, 1.0); var moveRate = settings.moveRate * mix(1.0, 0.08, sourceAtAgentStrength); var introTargetOffset = vec2(0.0, 0.0); var introTargetDistance = 0.0; if hasIntroTarget { introTargetOffset = agent.targetPosition - agent.position; introTargetDistance = length(introTargetOffset); let targetAngle = atan2(introTargetOffset.y, introTargetOffset.x); let nearTitle = 1.0 - smoothstep(4.0, max(28.0, settings.sensorOffset * 0.75), introTargetDistance); let desiredAngle = mix(targetAngle, agent.targetAngle, nearTitle * 0.2); let introTurn = angle_delta(agent.angle, desiredAngle); rotation = clamp(introTurn, -settings.turnRate * 3.4, settings.turnRate * 3.4) + (random.g - 0.5) * settings.turnWhenLost * 0.18; moveRate = min(settings.moveRate * mix(2.65, 0.01, nearTitle), introTargetDistance); } var step = vec2(cos(agent.angle), sin(agent.angle)) * moveRate; if hasIntroTarget { step = vec2(0.0, 0.0); if introTargetDistance > 0.5 { step = introTargetOffset / introTargetDistance * moveRate; } } let maxPosition = state.size - vec2(1.0, 1.0); let nextPosition = clamp(agent.position + step, vec2(0, 0), maxPosition); if nextPosition.x == 0 || nextPosition.x == maxPosition.x || nextPosition.y == 0 || nextPosition.y == maxPosition.y { rotation = 3.14159265359 + random.a - 0.5; } let sourceBelow = textureLoad(sourceMap, vec2(nextPosition), 0); let sourceBelowStrength = clamp(dot(sourceBelow.rgb, positiveReactionMask), 0.0, 1.0); let trailWeight = settings.individualTrailWeight * (1.0 + sourceBelowStrength * 16.0); var trailBelow = textureLoad(trailMapIn, vec2(nextPosition), 0); trailBelow = vec4( trailBelow.rgb + channelMask * trailWeight, max(trailBelow.a, 0.0) ); agent.angle += rotation; agent.position = nextPosition; textureStore(trailMapOut, vec2(nextPosition), trailBelow); agents[id] = agent; } fn sensor_position(agentPosition: vec2, agentAngle: f32, sensorOffset: f32, sensorOffsetAngle: f32) -> vec2 { let sensorAngle = agentAngle + sensorOffsetAngle; return vec2(clamp( agentPosition + vec2(cos(sensorAngle), sin(sensorAngle)) * sensorOffset, vec2(0, 0), state.size - vec2(1, 1) )); } fn get_channel_mask(colorIndex: f32) -> vec3 { if colorIndex < 0.5 { return vec3(1, 0, 0); } if colorIndex < 1.5 { return vec3(0, 1, 0); } return vec3(0, 0, 1); } fn get_reaction_mask(colorIndex: f32) -> vec3 { if colorIndex < 0.5 { return vec3( settings.color1ToColor1, settings.color1ToColor2, settings.color1ToColor3 ); } if colorIndex < 1.5 { return vec3( settings.color2ToColor1, settings.color2ToColor2, settings.color2ToColor3 ); } return vec3( settings.color3ToColor1, settings.color3ToColor2, settings.color3ToColor3 ); } fn angle_delta(sourceAngle: f32, targetAngle: f32) -> f32 { return atan2(sin(targetAngle - sourceAngle), cos(targetAngle - sourceAngle)); }