use std::{
cmp::Ordering,
io::{Read, Seek, Write},
};
use crate::{
Block, BlockArchetype, CCoord, Chunk, HeightMode, JavaChunk, LoaderError, LoaderResult, RCoord,
RegionLoader,
};
use super::biome::Biome;
pub type Rgba = [u8; 4];
pub trait Palette {
fn pick(&self, block: &Block, biome: Option<Biome>) -> Rgba;
}
pub struct TopShadeRenderer<'a, P: Palette> {
palette: &'a P,
height_mode: HeightMode,
}
impl<'a, P: Palette> TopShadeRenderer<'a, P> {
pub fn new(palette: &'a P, mode: HeightMode) -> Self {
Self {
palette,
height_mode: mode,
}
}
pub fn render<C: Chunk + ?Sized>(&self, chunk: &C, north: Option<&C>) -> [Rgba; 16 * 16] {
let mut data = [[0, 0, 0, 0]; 16 * 16];
let status = chunk.status();
const OK_STATUSES: [&str; 8] = ["full", "spawn", "postprocessed", "fullchunk", "minecraft:full", "minecraft:spawn", "minecraft:postprocessed", "minecraft:fullchunk"];
if !OK_STATUSES.contains(&status.as_str()) {
return data;
}
let y_range = chunk.y_range();
for z in 0..16 {
for x in 0..16 {
let air_height = chunk.surface_height(x, z, self.height_mode);
let block_height = (air_height - 1).max(y_range.start);
let colour = self.drill_for_colour(x, block_height, z, chunk, y_range.start);
let north_air_height = match z {
0 => north
.map(|c| c.surface_height(x, 15, self.height_mode))
.unwrap_or(block_height),
z => chunk.surface_height(x, z - 1, self.height_mode),
};
let colour = top_shade_colour(colour, air_height, north_air_height);
data[z * 16 + x] = colour;
}
}
data
}
fn drill_for_colour<C: Chunk + ?Sized>(
&self,
x: usize,
y_start: isize,
z: usize,
chunk: &C,
y_min: isize,
) -> Rgba {
let mut y = y_start;
let mut colour = [0, 0, 0, 0];
while colour[3] != 255 && y >= y_min {
let current_biome = chunk.biome(x, y, z);
let current_block = chunk.block(x, y, z);
if let Some(current_block) = current_block {
match current_block.archetype {
BlockArchetype::Airy => {
y -= 1;
}
BlockArchetype::Watery => {
let mut block_colour = self.palette.pick(current_block, current_biome);
let water_depth = water_depth(x, y, z, chunk, y_min);
let alpha = water_depth_to_alpha(water_depth);
block_colour[3] = alpha;
colour = a_over_b_colour(colour, block_colour);
y -= water_depth;
}
_ => {
let block_colour = self.palette.pick(current_block, current_biome);
colour = a_over_b_colour(colour, block_colour);
y -= 1;
}
}
} else {
return colour;
}
}
colour
}
}
fn water_depth_to_alpha(water_depth: isize) -> u8 {
(180 + 2 * water_depth).min(250) as u8
}
fn water_depth<C: Chunk + ?Sized>(
x: usize,
mut y: isize,
z: usize,
chunk: &C,
y_min: isize,
) -> isize {
let mut depth = 1;
while y > y_min {
let block = match chunk.block(x, y, z) {
Some(b) => b,
None => return depth,
};
if block.archetype == BlockArchetype::Watery {
depth += 1;
} else {
return depth;
}
y -= 1;
}
depth
}
fn a_over_b_colour(colour: [u8; 4], below_colour: [u8; 4]) -> [u8; 4] {
let linear = |c: u8| (((c as usize).pow(2)) as f32) / ((255 * 255) as f32);
let colour = colour.map(linear);
let below_colour = below_colour.map(linear);
let over_component = |ca: f32, aa: f32, cb: f32, ab: f32| {
let a_out = aa + ab * (1. - aa);
let linear_out = (ca * aa + cb * ab * (1. - aa)) / a_out;
(linear_out * 255. * 255.).sqrt() as u8
};
let over_alpha = |aa: f32, ab: f32| {
let a_out = aa + ab * (1. - aa);
(a_out * 255. * 255.).sqrt() as u8
};
[
over_component(colour[0], colour[3], below_colour[0], below_colour[3]),
over_component(colour[1], colour[3], below_colour[1], below_colour[3]),
over_component(colour[2], colour[3], below_colour[2], below_colour[3]),
over_alpha(colour[3], below_colour[3]),
]
}
pub struct RegionMap<T> {
pub data: Vec<T>,
pub x: RCoord,
pub z: RCoord,
}
impl<T: Clone> RegionMap<T> {
pub fn new(x: RCoord, z: RCoord, default: T) -> Self {
let mut data: Vec<T> = Vec::new();
data.resize(16 * 16 * 32 * 32, default);
Self { data, x, z }
}
pub fn chunk_mut(&mut self, x: CCoord, z: CCoord) -> &mut [T] {
debug_assert!(x.0 >= 0 && z.0 >= 0);
let len = 16 * 16;
let begin = (z.0 * 32 + x.0) as usize * len;
&mut self.data[begin..begin + len]
}
pub fn chunk(&self, x: CCoord, z: CCoord) -> &[T] {
debug_assert!(x.0 >= 0 && z.0 >= 0);
let len = 16 * 16;
let begin = (z.0 * 32 + x.0) as usize * len;
&self.data[begin..begin + len]
}
}
pub fn render_region<P: Palette, S>(
x: RCoord,
z: RCoord,
loader: &dyn RegionLoader<S>,
renderer: TopShadeRenderer<P>,
) -> LoaderResult<Option<RegionMap<Rgba>>>
where
S: Seek + Read + Write,
{
let mut map = RegionMap::new(x, z, [0u8; 4]);
let mut region = match loader.region(x, z)? {
Some(r) => r,
None => return Ok(None),
};
let mut cache: [Option<JavaChunk>; 32] = Default::default();
if let Some(mut r) = loader.region(x, RCoord(z.0 - 1))? {
for (x, entry) in cache.iter_mut().enumerate() {
*entry = r
.read_chunk(x, 31)
.ok()
.flatten()
.and_then(|b| JavaChunk::from_bytes(&b).ok())
}
}
for z in 0usize..32 {
for (x, cache) in cache.iter_mut().enumerate() {
let data = map.chunk_mut(CCoord(x as isize), CCoord(z as isize));
let chunk_data = region
.read_chunk(x, z)
.map_err(|e| LoaderError(e.to_string()))?;
let chunk_data = match chunk_data {
Some(data) => data,
None => {
*cache = None;
continue;
}
};
let chunk =
JavaChunk::from_bytes(&chunk_data).map_err(|e| LoaderError(e.to_string()))?;
let north = cache.as_ref();
let res = renderer.render(&chunk, north);
*cache = Some(chunk);
data[..].clone_from_slice(&res);
}
}
Ok(Some(map))
}
fn top_shade_colour(colour: Rgba, height: isize, shade_height: isize) -> Rgba {
let shade = match height.cmp(&shade_height) {
Ordering::Less => 180usize,
Ordering::Equal => 220,
Ordering::Greater => 255,
};
[
(colour[0] as usize * shade / 255) as u8,
(colour[1] as usize * shade / 255) as u8,
(colour[2] as usize * shade / 255) as u8,
colour[3],
]
}