Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
Download file

root / crates / client / src / render / animated.rs

//! Ports of the procedural `TextureFX` animations that overwrite terrain atlas tiles every tick.

use std::f32::consts::PI;
use std::ops::RangeInclusive;

use common::{JavaRandom, math};

pub const TILE_BYTES: usize = 16 * 16 * 4;

pub struct TileUpdate {
    /// The texture slot: 0 for the terrain atlas, or `gui::ITEMS`.
    pub texture: i32,
    pub tile: u8,
    pub pixels: [u8; TILE_BYTES],
}

/// `TextureWaterFX`, `TextureWaterFlowFX`, `TextureLavaFX`, `TextureLavaFlowFX`,
/// `TextureFlamesFX` and `TexturePortalFX`.
pub struct AnimatedTextures {
    random: JavaRandom,
    portal: Box<[[u8; TILE_BYTES]; 32]>,
    water: Ripples,
    water_flow: Ripples,
    lava: Magma,
    lava_flow: Magma,
    flames: [Flames; 2],
    ticks: i32,
}

#[derive(Default)]
struct Ripples {
    current: Vec<f32>,
    next: Vec<f32>,
    heat: Vec<f32>,
    heat_delta: Vec<f32>,
}

impl Ripples {
    fn new() -> Self {
        Self {
            current: vec![0.0; 256],
            next: vec![0.0; 256],
            heat: vec![0.0; 256],
            heat_delta: vec![0.0; 256],
        }
    }

    /// One simulation step. `flowing` selects the vertical smear of the flow variant.
    fn step(&mut self, random: &mut JavaRandom, flowing: bool) {
        for x in 0..16 {
            for y in 0..16 {
                let sum: f32 = if flowing {
                    (y as i32 - 2..=y as i32)
                        .map(|v| self.current[x + (v & 15) as usize * 16])
                        .sum()
                } else {
                    (x as i32 - 1..=x as i32 + 1)
                        .map(|u| self.current[(u & 15) as usize + y * 16])
                        .sum()
                };
                let divisor = if flowing { 3.2 } else { 3.3 };
                self.next[x + y * 16] = sum / divisor + self.heat[x + y * 16] * 0.8;
            }
        }
        let (decay, chance) = if flowing { (0.3, 0.2) } else { (0.1, 0.05) };
        for i in (0..16).flat_map(|x| (0..16).map(move |y| x + y * 16)) {
            self.heat[i] += self.heat_delta[i] * 0.05;
            if self.heat[i] < 0.0 {
                self.heat[i] = 0.0;
            }
            self.heat_delta[i] -= decay;
            if random.next_double() < chance {
                self.heat_delta[i] = 0.5;
            }
        }
        std::mem::swap(&mut self.current, &mut self.next);
    }

    fn pixels(&self, scroll: i32) -> [u8; TILE_BYTES] {
        let mut pixels = [0; TILE_BYTES];
        for (i, pixel) in pixels.as_chunks_mut::<4>().0.iter_mut().enumerate() {
            let value = self.current[((i as i32 - scroll * 16) & 255) as usize].clamp(0.0, 1.0);
            let squared = value * value;
            pixel.copy_from_slice(&[
                (32.0 + squared * 32.0) as u8,
                (50.0 + squared * 64.0) as u8,
                255,
                (146.0 + squared * 50.0) as u8,
            ]);
        }
        pixels
    }
}

struct Magma(Ripples);

impl Magma {
    fn step(&mut self, random: &mut JavaRandom) {
        let r = &mut self.0;
        for x in 0..16_i32 {
            for y in 0..16_i32 {
                let offset_x = (math::sin(y as f32 * PI * 2.0 / 16.0) * 1.2) as i32;
                let offset_y = (math::sin(x as f32 * PI * 2.0 / 16.0) * 1.2) as i32;
                let mut sum = 0.0;
                for u in x - 1..=x + 1 {
                    for v in y - 1..=y + 1 {
                        sum += r.current
                            [((u + offset_x) & 15) as usize + ((v + offset_y) & 15) as usize * 16];
                    }
                }
                let heat = |dx: i32, dy: i32| {
                    r.heat[((x + dx) & 15) as usize + ((y + dy) & 15) as usize * 16]
                };
                let i = (x + y * 16) as usize;
                r.next[i] =
                    sum / 10.0 + (heat(0, 0) + heat(1, 0) + heat(1, 1) + heat(0, 1)) / 4.0 * 0.8;
                r.heat[i] += r.heat_delta[i] * 0.01;
                if r.heat[i] < 0.0 {
                    r.heat[i] = 0.0;
                }
                r.heat_delta[i] -= 0.06;
                if random.next_double() < 0.005 {
                    r.heat_delta[i] = 1.5;
                }
            }
        }
        std::mem::swap(&mut r.current, &mut r.next);
    }

    fn pixels(&self, scroll: i32) -> [u8; TILE_BYTES] {
        let mut pixels = [0; TILE_BYTES];
        for (i, pixel) in pixels.as_chunks_mut::<4>().0.iter_mut().enumerate() {
            let value =
                (self.0.current[((i as i32 - scroll * 16) & 255) as usize] * 2.0).clamp(0.0, 1.0);
            pixel.copy_from_slice(&[
                (value * 100.0 + 155.0) as u8,
                (value * value * 255.0) as u8,
                (value * value * value * value * 128.0) as u8,
                255,
            ]);
        }
        pixels
    }
}

const WATER: u8 = 205;
const LAVA: u8 = 237;
const PORTAL: u8 = 14;
const FIRE: u8 = 31;

/// Animated atlas tiles whose frames may have texels the alpha test discards.
pub const TRANSPARENT_TILES: [u8; 8] = [
    FIRE,
    FIRE + 16,
    PORTAL,
    WATER,
    WATER + 1,
    WATER + 2,
    WATER + 16,
    WATER + 17,
];

struct Flames {
    current: [f32; 320],
    next: [f32; 320],
}

impl Flames {
    fn new() -> Self {
        Self {
            current: [0.0; 320],
            next: [0.0; 320],
        }
    }

    fn tick(&mut self, random: &mut JavaRandom) -> [u8; TILE_BYTES] {
        for x in 0..16 {
            for y in 0..20 {
                let mut sum = self.current[x + (y + 1) % 20 * 16] * 18.0;
                for u in x as i32 - 1..=x as i32 + 1 {
                    for v in y..=y + 1 {
                        if (0..16).contains(&u) && v < 20 {
                            sum += self.current[u as usize + v * 16];
                        }
                    }
                }
                self.next[x + y * 16] = if y == 19 {
                    (random.next_double() * random.next_double() * random.next_double() * 4.0
                        + random.next_double() * f64::from(0.1_f32)
                        + f64::from(0.2_f32)) as f32
                } else {
                    sum / (24.0 * 1.06)
                };
            }
        }
        std::mem::swap(&mut self.current, &mut self.next);
        let mut pixels = [0; TILE_BYTES];
        for (i, pixel) in pixels.as_chunks_mut::<4>().0.iter_mut().enumerate() {
            let value = (self.current[i] * 1.8).clamp(0.0, 1.0);
            let blue = (1..10).fold(value, |acc, _| acc * value);
            *pixel = [
                (value * 155.0 + 100.0) as u8,
                (value * value * 255.0) as u8,
                (blue * 255.0) as u8,
                if value < 0.5 { 0 } else { 255 },
            ];
        }
        pixels
    }
}

impl AnimatedTextures {
    pub fn new(seed: i64) -> Self {
        Self {
            random: JavaRandom::new(seed),
            water: Ripples::new(),
            water_flow: Ripples::new(),
            lava: Magma(Ripples::new()),
            lava_flow: Magma(Ripples::new()),
            flames: std::array::from_fn(|_| Flames::new()),
            portal: portal_frames(),
            ticks: 0,
        }
    }

    /// Advances all animations by one tick, port of `RenderEngine.updateDynamicTextures`.
    pub fn tick(&mut self, updates: &mut Vec<TileUpdate>) {
        self.ticks += 1;
        let random = &mut self.random;
        self.water.step(random, false);
        self.water_flow.step(random, true);
        self.lava.step(random);
        self.lava_flow.step(random);
        let mut push = |tile, pixels| {
            updates.push(TileUpdate {
                texture: 0,
                tile,
                pixels,
            })
        };
        for (index, flames) in self.flames.iter_mut().enumerate() {
            push(FIRE + index as u8 * 16, flames.tick(random));
        }

        push(PORTAL, self.portal[(self.ticks & 31) as usize]);
        push(WATER, self.water.pixels(0));
        push(LAVA, self.lava.pixels(0));
        let water_flow = self.water_flow.pixels(self.ticks);
        let lava_flow = self.lava_flow.pixels(self.ticks / 3);
        for (base, pixels) in [(WATER + 1, water_flow), (LAVA + 1, lava_flow)] {
            for tile in [base, base + 1, base + 16, base + 17] {
                push(tile, pixels);
            }
        }
    }
}

/// The 32 frames of `TexturePortalFX`: two swirls winding in opposite directions.
fn portal_frames() -> Box<[[u8; TILE_BYTES]; 32]> {
    let mut random = JavaRandom::new(100);
    let mut frames = Box::new([[0; TILE_BYTES]; 32]);
    for (frame, pixels) in frames.iter_mut().enumerate() {
        for x in 0..16 {
            for y in 0..16 {
                let mut value = 0.0;
                for swirl in 0..2 {
                    let offset = (swirl * 8) as f32;
                    let wrap = |v: f32| match v {
                        v if v < -1.0 => v + 2.0,
                        v if v >= 1.0 => v - 2.0,
                        v => v,
                    };
                    let dx = wrap((x as f32 - offset) / 16.0 * 2.0);
                    let dy = wrap((y as f32 - offset) / 16.0 * 2.0);
                    let distance = dx * dx + dy * dy;
                    let angle = f64::from(dy).atan2(f64::from(dx)) as f32
                        + (frame as f32 / 32.0 * PI * 2.0 - distance * 10.0 + (swirl * 2) as f32)
                            * (swirl * 2 - 1) as f32;
                    value += (math::sin(angle) + 1.0) / 2.0 / (distance + 1.0) * 0.5;
                }
                value += random.next_float() * 0.1;
                let blue = (value * 100.0 + 155.0) as i32 as u8;
                pixels[(y * 16 + x) * 4..][..4].copy_from_slice(&[
                    (value * value * 200.0 + 55.0) as i32 as u8,
                    (value * value * value * value * 255.0) as i32 as u8,
                    blue,
                    blue,
                ]);
            }
        }
    }
    frames
}

// ---- Compass and clock -----------------------------------------
/// `TextureCompassFX` and `TextureWatchFX`: item icons turning towards spawn and the sun.
pub struct Dials {
    compass: [u8; TILE_BYTES],
    clock: [u8; TILE_BYTES],
    dial: [u8; TILE_BYTES],
    /// Smoothed angle and its velocity, for the compass and the clock.
    needles: [(f64, f64); 2],
    random: JavaRandom,
}

impl Dials {
    pub fn new([items, dial]: &[crate::assets::RgbaImage; 2], seed: i64) -> Self {
        let tile = |image: &crate::assets::RgbaImage, index: u8| {
            let (x, y) = (usize::from(index % 16) * 16, usize::from(index / 16) * 16);
            let mut pixels = [0; TILE_BYTES];
            for row in 0..16 {
                let start = ((y + row) * image.width as usize + x) * 4;
                if let Some(source) = image.pixels.get(start..start + 64) {
                    pixels[row * 64..][..64].copy_from_slice(source);
                }
            }
            pixels
        };
        let icon = |item: common::item::Item| item.properties().map_or(0, |p| p.icon);
        Self {
            compass: tile(items, icon(common::item::Item::COMPASS)),
            clock: tile(items, icon(common::item::Item::CLOCK)),
            dial: tile(dial, 0),
            needles: [(0.0, 0.0); 2],
            random: JavaRandom::new(seed),
        }
    }

    /// Turns the needles towards their targets, which spin randomly in the Nether.
    pub fn tick(&mut self, targets: Option<[f64; 2]>, nether: bool, updates: &mut Vec<TileUpdate>) {
        for (index, (angle, velocity)) in self.needles.iter_mut().enumerate() {
            let target = match targets {
                Some(_) if nether => {
                    self.random.next_double() * f64::from(std::f32::consts::PI) * 2.0
                }
                Some(targets) => targets[index],
                None => 0.0,
            };
            let turn = (target - *angle + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU)
                - std::f64::consts::PI;
            *velocity = (*velocity + turn.clamp(-1.0, 1.0) * 0.1) * 0.8;
            *angle += *velocity;
        }
        let (sin, cos) = self.needles[0].0.sin_cos();
        let mut compass = self.compass;
        let gray = [100, 100, 100, 255];
        // The needle: gray behind the center, `tip` from it on
        let mut paint = |steps: RangeInclusive<i32>, (dx, dy): (f64, f64), tip: [u8; 4]| {
            for i in steps.map(f64::from) {
                let (x, y) = (8.5 + dx * i * 0.3, 7.5 + dy * i * 0.3 * 0.5);
                let at = (y as usize * 16 + x as usize) * 4;
                compass[at..at + 4].copy_from_slice(if i >= 0.0 { &tip } else { &gray });
            }
        };
        paint(-4..=4, (cos, -sin), gray);
        paint(-8..=16, (sin, cos), [255, 20, 20, 255]);
        let (sin, cos) = self.needles[1].0.sin_cos();
        let mut clock = self.clock;
        for (index, pixel) in clock.as_chunks_mut::<4>().0.iter_mut().enumerate() {
            let [r, g, b, _] = pixel.map(u32::from);
            if r == b && g == 0 && b > 0 {
                let x = -((index % 16) as f64 / 15.0 - 0.5);
                let y = (index / 16) as f64 / 15.0 - 0.5;
                let u = ((x * cos + y * sin + 0.5) * 16.0) as i32;
                let v = ((y * cos - x * sin + 0.5) * 16.0) as i32;
                let dial = &self.dial[((u & 15) + (v & 15) * 16) as usize * 4..][..4];
                let shade = |c: u8| (u32::from(c) * r / 255) as u8;
                *pixel = [shade(dial[0]), shade(dial[1]), shade(dial[2]), dial[3]];
            }
        }
        for (item, pixels) in [
            (common::item::Item::COMPASS, compass),
            (common::item::Item::CLOCK, clock),
        ] {
            updates.push(TileUpdate {
                texture: crate::gui::ITEMS,
                tile: item.properties().map_or(0, |p| p.icon),
                pixels,
            });
        }
    }
}