150 lines
5.3 KiB
WebGPU Shading Language
150 lines
5.3 KiB
WebGPU Shading Language
struct Settings {
|
|
moveRate: f32,
|
|
turnRate: f32,
|
|
sensorAngle: f32,
|
|
sensorOffset: f32,
|
|
turnWhenLost: f32,
|
|
individualTrailWeight: f32,
|
|
agentCount: f32,
|
|
introProgress: f32,
|
|
};
|
|
|
|
@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(
|
|
@builtin(global_invocation_id) global_id: vec3<u32>,
|
|
@builtin(num_workgroups) workgroup_count: vec3<u32>
|
|
) {
|
|
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 friendForward = dot(trailForward.rgb, channelMask);
|
|
let friendLeft = dot(trailLeft.rgb, channelMask);
|
|
let friendRight = dot(trailRight.rgb, channelMask);
|
|
|
|
let sourceForward = dot(sourceForwardSample.rgb, channelMask);
|
|
let sourceLeft = dot(sourceLeftSample.rgb, channelMask);
|
|
let sourceRight = dot(sourceRightSample.rgb, channelMask);
|
|
|
|
let weightForward = friendForward + sourceForward * 24.0;
|
|
let weightLeft = friendLeft + sourceLeft * 24.0;
|
|
let weightRight = friendRight + sourceRight * 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<i32>(agent.position), 0);
|
|
let sourceAtAgentStrength = clamp(dot(sourceAtAgent.rgb, channelMask), 0.0, 1.0);
|
|
var moveRate = settings.moveRate * mix(1.0, 0.08, sourceAtAgentStrength);
|
|
var introTargetOffset = vec2<f32>(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<f32>(0.0, 0.0);
|
|
if introTargetDistance > 0.5 {
|
|
step = introTargetOffset / introTargetDistance * moveRate;
|
|
}
|
|
}
|
|
|
|
let maxPosition = state.size - vec2<f32>(1.0, 1.0);
|
|
let nextPosition = clamp(agent.position + step, vec2<f32>(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<i32>(nextPosition), 0);
|
|
let sourceBelowStrength = dot(sourceBelow.rgb, channelMask);
|
|
let trailWeight = settings.individualTrailWeight * (1.0 + sourceBelowStrength * 16.0);
|
|
var trailBelow = textureLoad(trailMapIn, vec2<i32>(nextPosition), 0);
|
|
trailBelow = vec4<f32>(
|
|
trailBelow.rgb + channelMask * trailWeight,
|
|
max(trailBelow.a, 0.0)
|
|
);
|
|
|
|
agent.angle += rotation;
|
|
agent.position = nextPosition;
|
|
|
|
textureStore(trailMapOut, vec2<i32>(nextPosition), trailBelow);
|
|
agents[id] = agent;
|
|
}
|
|
|
|
fn sensor_position(agentPosition: vec2<f32>, agentAngle: f32, sensorOffset: f32, sensorOffsetAngle: f32) -> vec2<i32> {
|
|
let sensorAngle = agentAngle + sensorOffsetAngle;
|
|
return vec2<i32>(clamp(
|
|
agentPosition + vec2(cos(sensorAngle), sin(sensorAngle)) * sensorOffset,
|
|
vec2<f32>(0, 0),
|
|
state.size - vec2<f32>(1, 1)
|
|
));
|
|
}
|
|
|
|
fn get_channel_mask(colorIndex: f32) -> vec3<f32> {
|
|
if colorIndex < 0.5 {
|
|
return vec3<f32>(1, 0, 0);
|
|
}
|
|
if colorIndex < 1.5 {
|
|
return vec3<f32>(0, 1, 0);
|
|
}
|
|
return vec3<f32>(0, 0, 1);
|
|
}
|
|
|
|
fn angle_delta(sourceAngle: f32, targetAngle: f32) -> f32 {
|
|
return atan2(sin(targetAngle - sourceAngle), cos(targetAngle - sourceAngle));
|
|
}
|