use crate::HeightMap;
#[derive(Debug, Clone)]
pub struct SplatRule {
pub height_range: (f32, f32),
pub slope_range: (f32, f32),
pub sharpness: f32,
}
impl SplatRule {
pub fn new(height_range: (f32, f32), slope_range: (f32, f32), sharpness: f32) -> Self {
Self {
height_range,
slope_range,
sharpness,
}
}
pub fn weight(&self, height: f32, slope: f32) -> f32 {
let h_w = smooth_range(
height,
self.height_range.0,
self.height_range.1,
self.sharpness,
);
let s_w = smooth_range(
slope,
self.slope_range.0,
self.slope_range.1,
self.sharpness,
);
h_w * s_w
}
}
#[derive(Debug, Clone)]
pub struct WeightMap {
pub data: Vec<[u8; 4]>,
pub width: usize,
pub height: usize,
}
impl WeightMap {
pub fn new(width: usize, height: usize) -> Self {
let data = vec![[255, 0, 0, 0]; width * height];
Self {
data,
width,
height,
}
}
}
#[derive(Debug, Clone)]
pub struct SplatMapper {
pub rules: [SplatRule; 4],
}
impl SplatMapper {
pub fn new(rules: [SplatRule; 4]) -> Self {
Self { rules }
}
pub fn generate(&self, heightmap: &HeightMap) -> WeightMap {
let w = heightmap.width();
let h = heightmap.height();
let mut wm = WeightMap::new(w, h);
let normals = heightmap.normals_grid();
for z in 0..h {
for x in 0..w {
let height = heightmap.get(x, z);
let normal = normals[z * w + x];
let slope = 1.0 - normal[1];
let weights: [f32; 4] = [
self.rules[0].weight(height, slope),
self.rules[1].weight(height, slope),
self.rules[2].weight(height, slope),
self.rules[3].weight(height, slope),
];
let total: f32 = weights.iter().sum();
let pixel = if total > f32::EPSILON {
[
(weights[0] / total * 255.0).round() as u8,
(weights[1] / total * 255.0).round() as u8,
(weights[2] / total * 255.0).round() as u8,
(weights[3] / total * 255.0).round() as u8,
]
} else {
[0, 0, 255, 0]
};
wm.data[z * w + x] = pixel;
}
}
wm
}
pub fn sample_weights_at(&self, heightmap: &HeightMap, world_x: f32, world_z: f32) -> [f32; 4] {
let height = heightmap.get_height_at(world_x, world_z);
let normal = heightmap.get_normal_at(world_x, world_z);
let slope = 1.0 - normal[1];
let raw = [
self.rules[0].weight(height, slope),
self.rules[1].weight(height, slope),
self.rules[2].weight(height, slope),
self.rules[3].weight(height, slope),
];
let total: f32 = raw.iter().sum();
if total > f32::EPSILON {
[
raw[0] / total,
raw[1] / total,
raw[2] / total,
raw[3] / total,
]
} else {
[0.0, 0.0, 1.0, 0.0]
}
}
pub fn sample_biome_at(&self, heightmap: &HeightMap, world_x: f32, world_z: f32) -> u8 {
argmax_channel(&self.sample_weights_at(heightmap, world_x, world_z))
}
}
pub fn sample_splat_weights_at(
heightmap: &HeightMap,
mapper: &SplatMapper,
world_x: f32,
world_z: f32,
) -> [f32; 4] {
mapper.sample_weights_at(heightmap, world_x, world_z)
}
pub fn sample_biome_at(
heightmap: &HeightMap,
mapper: &SplatMapper,
world_x: f32,
world_z: f32,
) -> u8 {
mapper.sample_biome_at(heightmap, world_x, world_z)
}
fn argmax_channel(weights: &[f32; 4]) -> u8 {
let mut best_idx = 0u8;
let mut best_val = weights[0];
for (i, &w) in weights.iter().enumerate().skip(1) {
if w > best_val {
best_val = w;
best_idx = i as u8;
}
}
best_idx
}
impl Default for SplatMapper {
fn default() -> Self {
Self::new([
SplatRule::new((0.0, 0.45), (0.0, 0.3), 4.0),
SplatRule::new((0.3, 0.65), (0.0, 0.6), 2.0),
SplatRule::new((0.0, 1.0), (0.25, 1.0), 3.0),
SplatRule::new((0.7, 1.0), (0.0, 0.35), 4.0),
])
}
}
fn smooth_range(value: f32, lo: f32, hi: f32, sharpness: f32) -> f32 {
if lo >= hi {
return if (value - lo).abs() < f32::EPSILON {
1.0
} else {
0.0
};
}
let mid = (lo + hi) * 0.5;
let half = (hi - lo) * 0.5;
let dist = (value - mid).abs();
(1.0 - (dist / half).min(1.0)).powf(sharpness)
}