alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH 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
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,
});
}
}
}