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 / particles / mod.rs

//! Particles: `EffectRenderer` and the `EntityFX` subclasses.

mod ambient;

use std::collections::VecDeque;

use common::JavaRandom;
use common::item::Item;
use common::math::floor_f64;
use common::physics::{Aabb, BlockHit};
use common::world::shape::{self, BlockView};
use common::world::{Block, Material, fluid, texture};
use glam::{DVec3, Vec3};

use crate::world::ClientWorld;

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Kind {
    Digging,
    Smoke,
    Explosion,
    Bubble,
    Flame,
    Note,
    Portal,
    Lava,
    Footstep,
    Rain,
    Reddust,
    /// `EntitySlimeFX`: a crumb of an item icon, for snowballs and slime.
    Crumb,
    SnowShovel,
    Heart,
}

/// The texture a particle samples (`getFXLayer`).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Sheet {
    Particles,
    Terrain,
    Items,
    Footprint,
}

pub struct Particle {
    kind: Kind,
    pub position: DVec3,
    pub previous: DVec3,
    motion: DVec3,
    /// `particleTextureIndex` in its sheet.
    index: u16,
    jitter: [f32; 2],
    sheet: Sheet,
    age: i32,
    lifetime: i32,
    /// `particleScale` as created; some kinds grow or shrink from it.
    scale: f32,
    gravity: f32,
    pub color: Vec3,
    no_clip: bool,
    half_size: f64,
    on_ground: bool,
    /// `Entity.isInWeb`: slowed on the next move.
    in_web: bool,
    /// Where a portal particle drifts from.
    origin: DVec3,
}

/// How a particle is drawn this frame.
pub struct Appearance {
    pub size: f32,
    pub uv: [f32; 4],
    pub texture: i32,
    /// The brightness for a given world light, as `getEntityBrightness` overrides it.
    pub light: Light,
    /// Footprints lie flat on the ground and fade out.
    pub flat_alpha: Option<f32>,
}

#[derive(Clone, Copy)]
pub enum Light {
    World,
    Full,
    /// Lerps from full brightness to world light by the factor.
    Towards(f32),
}

impl Light {
    pub fn apply(self, world: f32) -> f32 {
        match self {
            Self::World => world,
            Self::Full => 1.0,
            Self::Towards(factor) => world * factor + (1.0 - factor),
        }
    }
}

pub struct Particles {
    pub particles: VecDeque<Particle>,
    random: JavaRandom,
    boxes: Vec<Aabb>,
    /// The viewer: named effects only appear within 16 blocks of it.
    pub viewer: DVec3,
}

impl Default for Particles {
    fn default() -> Self {
        Self {
            particles: VecDeque::new(),
            random: JavaRandom::new(0x5a17),
            boxes: Vec::new(),
            viewer: DVec3::ZERO,
        }
    }
}

impl Particle {
    /// `EntityFX.renderParticle` and its overrides.
    pub fn appearance(&self, partial: f32) -> Appearance {
        let life = (self.age as f32 + partial) / self.lifetime as f32;
        let grow = (life * 32.0).clamp(0.0, 1.0);
        let scale = match self.kind {
            Kind::Smoke | Kind::Note | Kind::Reddust | Kind::SnowShovel | Kind::Heart => {
                self.scale * grow
            }
            Kind::Flame => self.scale * (1.0 - life * life * 0.5),
            Kind::Lava => self.scale * (1.0 - life * life),
            Kind::Portal => {
                let fade = 1.0 - life;
                self.scale * (1.0 - fade * fade)
            }
            _ => self.scale,
        };
        let (texture, cell, span) = match self.sheet {
            Sheet::Particles => (
                crate::assets::entity_texture("particles.png"),
                1.0,
                0.999 / 16.0,
            ),
            Sheet::Terrain => (0, 0.25, 0.999 / 64.0),
            Sheet::Items => (crate::gui::ITEMS, 0.25, 0.999 / 64.0),
            Sheet::Footprint => (
                crate::assets::entity_texture("misc/footprint.png"),
                0.0,
                1.0,
            ),
        };
        let (u, v) = if self.sheet == Sheet::Footprint {
            (0.0, 0.0)
        } else {
            let jitter = if cell < 1.0 { self.jitter } else { [0.0; 2] };
            (
                (f32::from(self.index % 16) + jitter[0] * cell) / 16.0,
                (f32::from(self.index / 16) + jitter[1] * cell) / 16.0,
            )
        };
        let light = match self.kind {
            Kind::Lava => Light::Full,
            Kind::Flame => Light::Towards(life.clamp(0.0, 1.0)),
            Kind::Portal => {
                let fade = (self.age as f32 / self.lifetime as f32).powi(4);
                Light::Towards(1.0 - fade)
            }
            _ => Light::World,
        };
        let flat_alpha = (self.kind == Kind::Footstep).then(|| {
            let fade = life * life;
            (2.0 - fade * 2.0).min(1.0) * 0.2
        });
        Appearance {
            size: if self.kind == Kind::Footstep {
                2.0 / 16.0
            } else {
                0.1 * scale
            },
            // `EntityFX` maps the high U to the left edge, `EntityDiggingFX` the low one
            uv: if self.sheet == Sheet::Particles {
                [u + span, v, -span, span]
            } else {
                [u, v, span, span]
            },
            texture,
            light,
            flat_alpha,
        }
    }
}

impl Particles {
    /// The `EntityFX` constructor: a random spread around `motion` and the default look.
    fn create(&mut self, kind: Kind, position: DVec3, motion: DVec3) -> Particle {
        let random = &mut self.random;
        let mut spread = || f64::from((random.next_double() * 2.0 - 1.0) as f32 * 0.4);
        let motion = motion + DVec3::new(spread(), spread(), spread());
        let speed = (random.next_double() + random.next_double() + 1.0) as f32 * 0.15;
        let length = f64::from(motion.length() as f32);
        let motion =
            motion / length * f64::from(speed) * f64::from(0.4_f32) + DVec3::Y * f64::from(0.1_f32);
        let jitter = [random.next_float() * 3.0, random.next_float() * 3.0];
        let scale = (random.next_float() * 0.5 + 0.5) * 2.0;
        let lifetime = (4.0 / (random.next_float() * 0.9 + 0.1)) as i32;
        Particle {
            kind,
            position,
            previous: position,
            motion: if motion.is_finite() {
                motion
            } else {
                DVec3::Y * 0.1
            },
            index: 0,
            jitter,
            sheet: Sheet::Particles,
            age: 0,
            lifetime,
            scale,
            gravity: 0.0,
            color: Vec3::ONE,
            no_clip: false,
            half_size: 0.1,
            on_ground: false,
            in_web: false,
            origin: position,
        }
    }

    fn insert(&mut self, particle: Particle) {
        if self.particles.len() >= 4000 {
            self.particles.pop_front();
        }
        self.particles.push_back(particle);
    }

    /// `RenderGlobal.spawnParticle`: a named effect near the viewer.
    pub fn effect(&mut self, name: &str, position: DVec3, velocity: DVec3) {
        if position.distance_squared(self.viewer) <= 16.0 * 16.0 {
            self.named(name, position, velocity);
        }
    }

    fn named(&mut self, name: &str, position: DVec3, velocity: DVec3) {
        let kind = match name {
            "bubble" => Kind::Bubble,
            "smoke" | "largesmoke" => Kind::Smoke,
            "note" => Kind::Note,
            "portal" => Kind::Portal,
            "explode" => Kind::Explosion,
            "flame" => Kind::Flame,
            "lava" => Kind::Lava,
            "footstep" => Kind::Footstep,
            "splash" => Kind::Rain,
            "reddust" => Kind::Reddust,
            "snowballpoof" | "slime" => Kind::Crumb,
            "snowshovel" => Kind::SnowShovel,
            "heart" => Kind::Heart,
            _ => return,
        };
        let created = match kind {
            Kind::Smoke
            | Kind::Note
            | Kind::Lava
            | Kind::Rain
            | Kind::Reddust
            | Kind::Crumb
            | Kind::Heart => DVec3::ZERO,
            _ => velocity,
        };
        let mut p = self.create(kind, position, created);
        let random = &mut self.random;
        match kind {
            Kind::Smoke => {
                let large = if name == "largesmoke" { 2.5 } else { 1.0 };
                p.motion = p.motion * f64::from(0.1_f32) + velocity;
                p.color = Vec3::splat((random.next_double() * f64::from(0.3_f32)) as f32);
                p.scale *= 0.75 * large;
                p.lifetime =
                    ((8.0 / (random.next_double() * 0.8 + 0.2)) as i32 as f32 * large) as i32;
            }
            Kind::Explosion => {
                let mut nudge = || f64::from((random.next_double() * 2.0 - 1.0) as f32 * 0.05);
                p.motion = velocity + DVec3::new(nudge(), nudge(), nudge());
                p.color = Vec3::splat(random.next_float() * 0.3 + 0.7);
                p.scale = random.next_float() * random.next_float() * 6.0 + 1.0;
                p.lifetime = (16.0 / (f64::from(random.next_float()) * 0.8 + 0.2)) as i32 + 2;
            }
            Kind::Bubble => {
                p.index = 32;
                p.half_size = 0.01;
                p.scale *= random.next_float() * 0.6 + 0.2;
                let mut nudge = || f64::from((random.next_double() * 2.0 - 1.0) as f32 * 0.02);
                p.motion = velocity * f64::from(0.2_f32) + DVec3::new(nudge(), nudge(), nudge());
                p.lifetime = (8.0 / (random.next_double() * 0.8 + 0.2)) as i32;
            }
            Kind::Flame => {
                p.motion = p.motion * f64::from(0.01_f32) + velocity;
                for _ in 0..6 {
                    random.next_float();
                }
                p.lifetime = (8.0 / (random.next_double() * 0.8 + 0.2)) as i32 + 4;
                p.no_clip = true;
                p.index = 48;
            }
            Kind::Note => {
                p.motion *= f64::from(0.01_f32);
                p.motion.y += 0.2;
                let hue = velocity.x as f32;
                let channel = |offset: f32| {
                    common::math::sin((hue + offset) * std::f32::consts::PI * 2.0) * 0.65 + 0.35
                };
                p.color = Vec3::new(channel(0.0), channel(1.0 / 3.0), channel(2.0 / 3.0));
                p.scale *= 0.75 * 2.0;
                p.lifetime = 6;
                p.index = 64;
            }
            Kind::Portal => {
                p.motion = velocity;
                let shade = random.next_float() * 0.6 + 0.4;
                p.scale = random.next_float() * 0.2 + 0.5;
                p.color = Vec3::new(shade * 0.9, shade * 0.3, shade);
                p.lifetime = (random.next_double() * 10.0) as i32 + 40;
                p.no_clip = true;
                p.index = (random.next_double() * 8.0) as u16;
            }
            Kind::Lava => {
                p.motion *= f64::from(0.8_f32);
                p.motion.y = f64::from(random.next_float() * 0.4 + 0.05);
                p.scale *= random.next_float() * 2.0 + 0.2;
                p.lifetime = (16.0 / (random.next_double() * 0.8 + 0.2)) as i32;
                p.index = 49;
            }
            Kind::Footstep => {
                p.motion = DVec3::ZERO;
                p.lifetime = 200;
                p.sheet = Sheet::Footprint;
                p.no_clip = true;
            }
            Kind::Rain => {
                Self::rain(&mut p, random);
                p.gravity = 0.04;
                p.index += 1;
                if velocity.y == 0.0 && (velocity.x != 0.0 || velocity.z != 0.0) {
                    p.motion = DVec3::new(velocity.x, velocity.y + 0.1, velocity.z);
                }
            }
            Kind::Reddust => {
                p.motion *= f64::from(0.1_f32);
                let red = if velocity.x as f32 == 0.0 {
                    1.0
                } else {
                    velocity.x as f32
                };
                let shade = random.next_double() as f32 * 0.4 + 0.6;
                let mut tint = |channel: f32| {
                    ((random.next_double() * f64::from(0.2_f32)) as f32 + 0.8) * channel * shade
                };
                p.color = Vec3::new(tint(red), tint(velocity.y as f32), tint(velocity.z as f32));
                p.scale *= 0.75;
                p.lifetime = (8.0 / (random.next_double() * 0.8 + 0.2)) as i32;
            }
            Kind::Crumb => {
                let item = if name == "slime" {
                    Item::SLIME_BALL
                } else {
                    Item::SNOWBALL
                };
                p.index = item.properties().map_or(0, |p| u16::from(p.icon));
                p.sheet = Sheet::Items;
                p.gravity = 1.0;
                p.scale /= 2.0;
            }
            Kind::SnowShovel => {
                p.motion = p.motion * f64::from(0.1_f32) + velocity;
                p.color = Vec3::splat(1.0 - (random.next_double() * f64::from(0.3_f32)) as f32);
                p.scale *= 0.75;
                p.lifetime = (8.0 / (random.next_double() * 0.8 + 0.2)) as i32;
            }
            Kind::Heart => {
                p.motion *= f64::from(0.01_f32);
                p.motion.y += 0.1;
                p.scale *= 0.75 * 2.0;
                p.lifetime = 16;
                p.index = 80;
            }
            Kind::Digging => {}
        }
        self.insert(p);
    }

    /// `EntityRainFX`: the look and fall of a raindrop.
    fn rain(p: &mut Particle, random: &mut JavaRandom) {
        p.motion.x *= f64::from(0.3_f32);
        p.motion.y = f64::from(random.next_double() as f32 * 0.2 + 0.1);
        p.motion.z *= f64::from(0.3_f32);
        p.index = 19 + random.next_int(4) as u16;
        p.half_size = 0.005;
        p.gravity = 0.06;
        p.lifetime = (8.0 / (random.next_double() * 0.8 + 0.2)) as i32;
    }

    /// A raindrop splashing where rain hits the ground.
    pub fn raindrop(&mut self, position: DVec3) {
        let mut p = self.create(Kind::Rain, position, DVec3::ZERO);
        Self::rain(&mut p, &mut self.random);
        self.insert(p);
    }

    /// Smoke where rain hits lava, added without the distance check of named effects.
    pub fn rain_smoke(&mut self, position: DVec3) {
        self.named("smoke", position, DVec3::ZERO);
    }

    /// `EffectRenderer.addBlockDestroyEffects`: 64 bits of the broken block.
    pub fn break_block(&mut self, block: Block, metadata: u8, position: DVec3, tint: u32) {
        for x in 0..4 {
            for y in 0..4 {
                for z in 0..4 {
                    let offset =
                        DVec3::new(f64::from(x) + 0.5, f64::from(y) + 0.5, f64::from(z) + 0.5)
                            / 4.0;
                    self.random.next_int(6);
                    self.debris(
                        block,
                        metadata,
                        position + offset,
                        offset - DVec3::splat(0.5),
                        1.0,
                        tint,
                    );
                }
            }
        }
    }

    /// `EffectRenderer.addBlockHitEffects`: a fragment just outside the struck face.
    pub fn hit_block(
        &mut self,
        block: Block,
        metadata: u8,
        hit: &BlockHit,
        bounds: Aabb,
        tint: u32,
    ) {
        let margin = f64::from(0.1_f32);
        let origin = DVec3::new(f64::from(hit.x), f64::from(hit.y), f64::from(hit.z));
        let mut position = origin
            + DVec3::from_array(std::array::from_fn(|i| {
                self.random.next_double() * (bounds.max[i] - bounds.min[i] - margin * 2.0)
                    + margin
                    + bounds.min[i]
            }));
        let axis = [1, 1, 2, 2, 0, 0][usize::from(hit.side.min(5))];
        position[axis] = origin[axis]
            + if hit.side.is_multiple_of(2) {
                bounds.min[axis] - margin
            } else {
                bounds.max[axis] + margin
            };
        self.debris(block, metadata, position, DVec3::ZERO, 0.6, tint);
    }

    /// `EntityDiggingFX`.
    pub fn debris(
        &mut self,
        block: Block,
        metadata: u8,
        position: DVec3,
        velocity: DVec3,
        scale: f32,
        tint: u32,
    ) {
        let mut p = self.create(Kind::Digging, position, velocity);
        p.index = u16::from(texture::face(block, metadata, 0).0);
        p.sheet = Sheet::Terrain;
        p.gravity = 1.0;
        p.color = Vec3::new(
            (tint >> 16 & 255) as f32,
            (tint >> 8 & 255) as f32,
            (tint & 255) as f32,
        ) / 255.0
            * 0.6;
        p.scale = p.scale / 2.0 * scale;
        if scale != 1.0 {
            p.motion.x *= f64::from(0.2_f32);
            p.motion.y =
                (p.motion.y - f64::from(0.1_f32)) * f64::from(0.2_f32) + f64::from(0.1_f32);
            p.motion.z *= f64::from(0.2_f32);
            p.half_size *= f64::from(scale);
        }
        self.insert(p);
    }

    /// `Entity.moveEntity` for a particle box, which collides with blocks and boats.
    fn move_particle(boxes: &mut Vec<Aabb>, world: &ClientWorld, p: &mut Particle) {
        if p.no_clip {
            p.position += p.motion;
            return;
        }
        let requested = if std::mem::take(&mut p.in_web) {
            let slowed = p.motion * DVec3::new(0.25, f64::from(0.05_f32), 0.25);
            p.motion = DVec3::ZERO;
            slowed
        } else {
            p.motion
        };
        let half = DVec3::splat(p.half_size);
        let mut bounds = Aabb::new(p.position - half, p.position + half);
        let region = bounds.expand_towards(requested);
        boxes.clear();
        for x in floor_f64(region.min.x)..=floor_f64(region.max.x) {
            for z in floor_f64(region.min.z)..=floor_f64(region.max.z) {
                for y in floor_f64(region.min.y) - 1..=floor_f64(region.max.y) {
                    let block = BlockView::block(world, x, y, z);
                    if !block.is_air() {
                        shape::collision_boxes(world, block, x, y, z, &region, boxes);
                    }
                }
            }
        }
        boxes.extend(
            world
                .vehicles
                .iter()
                .filter(|(_, b)| b.intersects(&region))
                .map(|(_, b)| *b),
        );
        let mut motion = requested;
        for b in &*boxes {
            motion.y = b.clip_y(&bounds, motion.y);
        }
        bounds = bounds.offset(DVec3::Y * motion.y);
        for b in &*boxes {
            motion.x = b.clip_x(&bounds, motion.x);
        }
        bounds = bounds.offset(DVec3::X * motion.x);
        for b in &*boxes {
            motion.z = b.clip_z(&bounds, motion.z);
        }
        p.position += motion;
        p.on_ground = motion.y != requested.y && requested.y < 0.0;
        for i in 0..3 {
            if motion[i] != requested[i] {
                p.motion[i] = 0.0;
            }
        }
        let inner = bounds
            .offset(DVec3::Z * motion.z)
            .deflate(DVec3::splat(0.001));
        let [x0, y0, z0] = inner.min.to_array().map(floor_f64);
        let [x1, y1, z1] = inner.max.to_array().map(floor_f64);
        p.in_web = (x0..=x1).any(|x| {
            (y0..=y1).any(|y| (z0..=z1).any(|z| BlockView::block(world, x, y, z) == Block::WEB))
        });
    }

    pub fn tick(&mut self, world: &ClientWorld) {
        let mut spawned = Vec::new();
        let (boxes, random) = (&mut self.boxes, &mut self.random);
        self.particles.retain_mut(|p| {
            p.previous = p.position;
            let expired = if p.kind == Kind::Bubble || p.kind == Kind::Rain {
                false
            } else {
                let expired = p.age >= p.lifetime;
                p.age += 1;
                expired
            };
            match p.kind {
                Kind::Footstep => return !expired && p.age != p.lifetime,
                Kind::Portal => {
                    let life = (p.age - 1) as f32 / p.lifetime as f32;
                    let curve = 1.0 - (-life + life * life * 2.0);
                    p.position =
                        p.origin + p.motion * f64::from(curve) + DVec3::Y * f64::from(1.0 - life);
                    return !expired;
                }
                Kind::Smoke | Kind::Explosion | Kind::Reddust | Kind::SnowShovel => {
                    p.index = (7 - p.age * 8 / p.lifetime).max(0) as u16;
                }
                _ => {}
            }
            match p.kind {
                Kind::Smoke | Kind::Explosion => p.motion.y += 0.004,
                Kind::Bubble => p.motion.y += 0.002,
                Kind::Lava => {
                    if random.next_float() > p.age as f32 / p.lifetime as f32 {
                        spawned.push((p.position, p.motion));
                    }
                    p.motion.y -= 0.03;
                }
                Kind::SnowShovel => p.motion.y -= 0.03,
                Kind::Rain => p.motion.y -= f64::from(p.gravity),
                Kind::Digging | Kind::Crumb => p.motion.y -= 0.04 * f64::from(p.gravity),
                _ => {}
            }
            let height = p.position.y;
            Self::move_particle(boxes, world, p);
            let stalled = p.position.y == height;
            if stalled
                && matches!(
                    p.kind,
                    Kind::Smoke | Kind::Reddust | Kind::Note | Kind::Heart
                )
            {
                p.motion.x *= 1.1;
                p.motion.z *= 1.1;
            }
            p.motion *= f64::from(match p.kind {
                Kind::Smoke | Kind::Reddust | Kind::Flame => 0.96_f32,
                Kind::Note => 0.66,
                Kind::Heart => 0.86,
                Kind::Explosion => 0.9,
                Kind::Bubble => 0.85,
                Kind::Lava => 0.999,
                Kind::SnowShovel => 0.99,
                _ => 0.98,
            });
            if p.on_ground {
                p.motion.x *= f64::from(0.7_f32);
                p.motion.z *= f64::from(0.7_f32);
            }
            let [x, y, z] = p.position.to_array().map(floor_f64);
            match p.kind {
                Kind::Bubble => {
                    let alive = BlockView::block(world, x, y, z).material() == Material::Water;
                    let lifetime = p.lifetime;
                    p.lifetime -= 1;
                    alive && lifetime > 0
                }
                Kind::Rain => {
                    let lifetime = p.lifetime;
                    p.lifetime -= 1;
                    if lifetime <= 0 || p.on_ground && random.next_double() < 0.5 {
                        return false;
                    }
                    let block = BlockView::block(world, x, y, z);
                    let material = block.material();
                    if material.is_liquid() || material.is_solid() {
                        let surface = f64::from(
                            (y + 1) as f32
                                - fluid::percent_air(BlockView::metadata(world, x, y, z)),
                        );
                        if p.position.y < surface {
                            return false;
                        }
                    }
                    true
                }
                _ => !expired,
            }
        });
        for (position, motion) in spawned {
            self.effect("smoke", position, motion);
        }
    }
}