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 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, ®ion, boxes);
}
}
}
}
boxes.extend(
world
.vehicles
.iter()
.filter(|(_, b)| b.intersects(®ion))
.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);
}
}
}