// Quantizes the field into the display texture, mirroring `PheromoneField::quantize`.
struct Params {
width: u32,
height: u32,
n_cells: u32,
_pad: u32,
// Texture size, below the grid size on a large world.
tex: vec2<u32>,
_pad2: vec2<u32>,
// The CPU field's `CELL_PALETTE`, packed so the colours cannot drift from the CPU palette.
palette: array<vec4<u32>, 4>,
}
@group(0) @binding(0) var<storage, read> field: array<f32>;
@group(0) @binding(1) var<storage, read> sites: array<u32>;
@group(0) @binding(2) var output: texture_storage_2d<rgba8unorm, write>;
@group(0) @binding(3) var<uniform> params: Params;
const OBSTACLE: u32 = 1u;
const FOOD: u32 = 2u;
const HOME: u32 = 3u;
const LOW_PHEROMONE: f32 = 1e-14;
const DISPLAY_DECADES: f32 = 3.0;
const RAMP_STEPS: f32 = 6.0;
const INV_LOG2_10: f32 = 0.30103;
fn ramp_step(v: f32) -> u32 {
if (v <= LOW_PHEROMONE) {
return 0u;
}
let decades = log2(v) * INV_LOG2_10 / DISPLAY_DECADES + 1.0;
if (decades <= 0.0) {
return 0u;
}
return clamp(u32(clamp(decades * RAMP_STEPS, 0.0, RAMP_STEPS)), 1u, u32(RAMP_STEPS));
}
@compute
@workgroup_size(16, 16)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
if (global_id.x >= params.tex.x || global_id.y >= params.tex.y) {
return;
}
// Each invocation paints one texel, which is one cell until the world outgrows the texture cap.
let x = global_id.x * params.width / params.tex.x;
let y = global_id.y * params.height / params.tex.y;
let c = y * params.width + x;
let site = sites[c];
var index = 0u;
if (site == OBSTACLE) {
index = 13u;
} else if (site == FOOD) {
index = 14u;
} else if (site == HOME) {
index = 15u;
} else {
// The stronger trail wins the cell, so overlapping trails stay legible.
let to_food = field[c];
let to_home = field[params.n_cells + c];
var v = to_home;
var base = 0u;
if (to_food > to_home) {
v = to_food;
base = 6u;
}
let step = ramp_step(v);
if (step != 0u) {
index = base + step;
}
}
let color = unpack4x8unorm(params.palette[index >> 2u][index & 3u]);
textureStore(output, vec2<i32>(global_id.xy), color);
}