Skip to main content

fastanvil/
render.rs

1use std::{
2    cmp::Ordering,
3    io::{Read, Seek, Write},
4};
5
6use crate::{
7    Block, BlockArchetype, CCoord, Chunk, HeightMode, JavaChunk, LoaderError, LoaderResult, RCoord,
8    RegionLoader,
9};
10
11use super::biome::Biome;
12
13pub type Rgba = [u8; 4];
14
15/// Palette can be used to take a block description to produce a colour that it
16/// should render to.
17pub trait Palette {
18    fn pick(&self, block: &Block, biome: Option<Biome>) -> Rgba;
19}
20
21pub struct TopShadeRenderer<'a, P: Palette> {
22    palette: &'a P,
23    height_mode: HeightMode,
24}
25
26impl<'a, P: Palette> TopShadeRenderer<'a, P> {
27    pub fn new(palette: &'a P, mode: HeightMode) -> Self {
28        Self {
29            palette,
30            height_mode: mode,
31        }
32    }
33
34    pub fn render<C: Chunk + ?Sized>(&self, chunk: &C, north: Option<&C>) -> [Rgba; 16 * 16] {
35        let mut data = [[0, 0, 0, 0]; 16 * 16];
36
37        let status = chunk.status();
38        const OK_STATUSES: [&str; 8] = ["full", "spawn", "postprocessed", "fullchunk", "minecraft:full", "minecraft:spawn", "minecraft:postprocessed", "minecraft:fullchunk"];
39        if !OK_STATUSES.contains(&status.as_str()) {
40            // Chunks that have been fully generated will have a 'full' status.
41            // Skip chunks that don't; the way they render is unpredictable.
42            return data;
43        }
44
45        let y_range = chunk.y_range();
46
47        for z in 0..16 {
48            for x in 0..16 {
49                let air_height = chunk.surface_height(x, z, self.height_mode);
50                let block_height = (air_height - 1).max(y_range.start);
51
52                let colour = self.drill_for_colour(x, block_height, z, chunk, y_range.start);
53
54                let north_air_height = match z {
55                    // if top of chunk, get height from the chunk above.
56                    0 => north
57                        .map(|c| c.surface_height(x, 15, self.height_mode))
58                        .unwrap_or(block_height),
59                    z => chunk.surface_height(x, z - 1, self.height_mode),
60                };
61                let colour = top_shade_colour(colour, air_height, north_air_height);
62
63                data[z * 16 + x] = colour;
64            }
65        }
66
67        data
68    }
69
70    /// Drill for colour. Starting at y_start, make way down the column until we
71    /// have an opaque colour to return. This tackles things like transparency.
72    fn drill_for_colour<C: Chunk + ?Sized>(
73        &self,
74        x: usize,
75        y_start: isize,
76        z: usize,
77        chunk: &C,
78        y_min: isize,
79    ) -> Rgba {
80        let mut y = y_start;
81        let mut colour = [0, 0, 0, 0];
82
83        while colour[3] != 255 && y >= y_min {
84            let current_biome = chunk.biome(x, y, z);
85            let current_block = chunk.block(x, y, z);
86
87            if let Some(current_block) = current_block {
88                match current_block.archetype {
89                    BlockArchetype::Airy => {
90                        y -= 1;
91                    }
92                    // TODO: Can potentially optimize this for ocean floor using
93                    // heightmaps.
94                    BlockArchetype::Watery => {
95                        let mut block_colour = self.palette.pick(current_block, current_biome);
96                        let water_depth = water_depth(x, y, z, chunk, y_min);
97                        let alpha = water_depth_to_alpha(water_depth);
98
99                        block_colour[3] = alpha;
100
101                        colour = a_over_b_colour(colour, block_colour);
102                        y -= water_depth;
103                    }
104                    _ => {
105                        let block_colour = self.palette.pick(current_block, current_biome);
106                        colour = a_over_b_colour(colour, block_colour);
107                        y -= 1;
108                    }
109                }
110            } else {
111                return colour;
112            }
113        }
114
115        colour
116    }
117}
118
119/// Convert `water_depth` meters of water to an approximate opacity
120fn water_depth_to_alpha(water_depth: isize) -> u8 {
121    // Water will absorb a fraction of the light per unit depth. So if we say
122    // that every metre of water absorbs half the light going through it, then 2
123    // metres would absorb 3/4, 3 metres would absorb 7/8 etc.
124    //
125    // Since RGB is not linear, we can make a very rough approximation of this
126    // fractional behavior with a linear equation in RBG space. This pretends
127    // water absorbs quadratically x^2, rather than exponentially e^x.
128    //
129    // We put a lower limit to make rivers and swamps still have water show up
130    // well, and an upper limit so that very deep ocean has a tiny bit of
131    // transparency still.
132    //
133    // This is pretty rather than accurate.
134
135    (180 + 2 * water_depth).min(250) as u8
136}
137
138fn water_depth<C: Chunk + ?Sized>(
139    x: usize,
140    mut y: isize,
141    z: usize,
142    chunk: &C,
143    y_min: isize,
144) -> isize {
145    let mut depth = 1;
146    while y > y_min {
147        let block = match chunk.block(x, y, z) {
148            Some(b) => b,
149            None => return depth,
150        };
151
152        if block.archetype == BlockArchetype::Watery {
153            depth += 1;
154        } else {
155            return depth;
156        }
157        y -= 1;
158    }
159    depth
160}
161
162/// Merge two potentially transparent colours, A and B, into one as if colour A
163/// was laid on top of colour B.
164///
165/// See https://en.wikipedia.org/wiki/Alpha_compositing
166fn a_over_b_colour(colour: [u8; 4], below_colour: [u8; 4]) -> [u8; 4] {
167    let linear = |c: u8| (((c as usize).pow(2)) as f32) / ((255 * 255) as f32);
168    let colour = colour.map(linear);
169    let below_colour = below_colour.map(linear);
170
171    let over_component = |ca: f32, aa: f32, cb: f32, ab: f32| {
172        let a_out = aa + ab * (1. - aa);
173        let linear_out = (ca * aa + cb * ab * (1. - aa)) / a_out;
174        (linear_out * 255. * 255.).sqrt() as u8
175    };
176
177    let over_alpha = |aa: f32, ab: f32| {
178        let a_out = aa + ab * (1. - aa);
179        (a_out * 255. * 255.).sqrt() as u8
180    };
181
182    [
183        over_component(colour[0], colour[3], below_colour[0], below_colour[3]),
184        over_component(colour[1], colour[3], below_colour[1], below_colour[3]),
185        over_component(colour[2], colour[3], below_colour[2], below_colour[3]),
186        over_alpha(colour[3], below_colour[3]),
187    ]
188}
189
190pub struct RegionMap<T> {
191    pub data: Vec<T>,
192    pub x: RCoord,
193    pub z: RCoord,
194}
195
196impl<T: Clone> RegionMap<T> {
197    pub fn new(x: RCoord, z: RCoord, default: T) -> Self {
198        let mut data: Vec<T> = Vec::new();
199        data.resize(16 * 16 * 32 * 32, default);
200        Self { data, x, z }
201    }
202
203    pub fn chunk_mut(&mut self, x: CCoord, z: CCoord) -> &mut [T] {
204        debug_assert!(x.0 >= 0 && z.0 >= 0);
205
206        let len = 16 * 16;
207        let begin = (z.0 * 32 + x.0) as usize * len;
208        &mut self.data[begin..begin + len]
209    }
210
211    pub fn chunk(&self, x: CCoord, z: CCoord) -> &[T] {
212        debug_assert!(x.0 >= 0 && z.0 >= 0);
213
214        let len = 16 * 16;
215        let begin = (z.0 * 32 + x.0) as usize * len;
216        &self.data[begin..begin + len]
217    }
218}
219
220pub fn render_region<P: Palette, S>(
221    x: RCoord,
222    z: RCoord,
223    loader: &dyn RegionLoader<S>,
224    renderer: TopShadeRenderer<P>,
225) -> LoaderResult<Option<RegionMap<Rgba>>>
226where
227    S: Seek + Read + Write,
228{
229    let mut map = RegionMap::new(x, z, [0u8; 4]);
230
231    let mut region = match loader.region(x, z)? {
232        Some(r) => r,
233        None => return Ok(None),
234    };
235
236    let mut cache: [Option<JavaChunk>; 32] = Default::default();
237
238    // Cache the last row of chunks from the above region to allow top-shading
239    // on region boundaries.
240    if let Some(mut r) = loader.region(x, RCoord(z.0 - 1))? {
241        for (x, entry) in cache.iter_mut().enumerate() {
242            *entry = r
243                .read_chunk(x, 31)
244                .ok()
245                .flatten()
246                .and_then(|b| JavaChunk::from_bytes(&b).ok())
247        }
248    }
249
250    for z in 0usize..32 {
251        for (x, cache) in cache.iter_mut().enumerate() {
252            let data = map.chunk_mut(CCoord(x as isize), CCoord(z as isize));
253
254            let chunk_data = region
255                .read_chunk(x, z)
256                .map_err(|e| LoaderError(e.to_string()))?;
257            let chunk_data = match chunk_data {
258                Some(data) => data,
259                None => {
260                    // If there's no chunk here, we still need to set the cache
261                    // otherwise the chunks below this will top-shade with an
262                    // incorrect chunk.
263                    *cache = None;
264                    continue;
265                }
266            };
267
268            let chunk =
269                JavaChunk::from_bytes(&chunk_data).map_err(|e| LoaderError(e.to_string()))?;
270
271            // Get the chunk at the same x coordinate from the cache. This
272            // should be the chunk that is directly above the current. We
273            // know this because once we have processed this chunk we put it
274            // in the cache in the same place. So the next time we get the
275            // current one will be when we're processing directly below us.
276            //
277            // Thanks to the default None value this works fine for the
278            // first row or for any missing chunks.
279            let north = cache.as_ref();
280
281            let res = renderer.render(&chunk, north);
282            *cache = Some(chunk);
283
284            data[..].clone_from_slice(&res);
285        }
286    }
287
288    Ok(Some(map))
289}
290
291/// Apply top-shading to the given colour based on the relative height of the
292/// block above it. Darker if the above block is taller, and lighter if it's
293/// smaller.
294///
295/// Technically this function darkens colours, but this is also how Minecraft
296/// itself shades maps.
297fn top_shade_colour(colour: Rgba, height: isize, shade_height: isize) -> Rgba {
298    let shade = match height.cmp(&shade_height) {
299        Ordering::Less => 180usize,
300        Ordering::Equal => 220,
301        Ordering::Greater => 255,
302    };
303    [
304        (colour[0] as usize * shade / 255) as u8,
305        (colour[1] as usize * shade / 255) as u8,
306        (colour[2] as usize * shade / 255) as u8,
307        colour[3],
308    ]
309}