Substantial improvements to meshing time.

This mostly come out of optimizing BlocksOfInterest to (empirically)
minimize redundant computations, use a more efficient RNG, use a faster
verion of iter_changed, and optimize water block handling (theoretically
the iter_changed difference might mean we missed some water blocks, but
in practice it's unlikely to matter for fast-moving rivers).

Also did some microoptimizations of meshing etc. that seem to result in
pretty good improvements in practice, and also added another set of
optimizations to improve tree performance (special casing "easy" segment
approaches, which got a few percent, and inlining block_from_structure
for tree leaves and branches, which got us considerably more; I think
the total improvement is around 5%).
This commit is contained in:
Joshua Yanovski 2022-07-31 01:28:37 -07:00
parent 901aa8e1b4
commit 2a61c7790b
15 changed files with 593 additions and 265 deletions

View File

@ -280,7 +280,7 @@ pub struct CharacterList {
pub loading: bool,
}
const TOTAL_PENDING_CHUNKS_LIMIT: usize = 1024;
const TOTAL_PENDING_CHUNKS_LIMIT: usize = 2048;
impl Client {
pub async fn new(
@ -1872,7 +1872,7 @@ impl Client {
for key in keys.iter() {
if self.state.terrain().get_key(*key).is_none() {
if !skip_mode && !self.pending_chunks.contains_key(key) {
const CURRENT_TICK_PENDING_CHUNKS_LIMIT: usize = 8 * 4;
const CURRENT_TICK_PENDING_CHUNKS_LIMIT: usize = 8 * 5;
if self.pending_chunks.len() < TOTAL_PENDING_CHUNKS_LIMIT
&& current_tick_send_chunk_requests
< CURRENT_TICK_PENDING_CHUNKS_LIMIT

View File

@ -118,6 +118,18 @@ impl BlockKind {
| BlockKind::Sand
)
}
#[inline(always)]
/// NOTE: Do not call unless you are handling the case where the block is a sprite elsewhere,
/// as it will give incorrect results in this case!
pub const fn get_glow_raw(&self) -> Option<u8> {
match self {
BlockKind::Lava => Some(24),
BlockKind::GlowingRock | BlockKind::GlowingWeakRock => Some(10),
BlockKind::GlowingMushroom => Some(20),
_ => None,
}
}
}
/// XXX(@Sharp): If you feel like significantly modifying how Block works, you *MUST* also update
@ -209,69 +221,41 @@ impl Block {
}
}
#[inline]
#[inline(always)]
/// NOTE: Do not call unless you already know this block is not filled!
pub fn get_sprite_raw(&self) -> Option<SpriteKind> {
SpriteKind::from_u8(self.attr[0])
}
#[inline(always)]
pub fn get_sprite(&self) -> Option<SpriteKind> {
if !self.is_filled() {
SpriteKind::from_u8(self.attr[0])
self.get_sprite_raw()
} else {
None
}
}
#[inline(always)]
/// NOTE: Do not call unless you already know this block is a sprite and already called
/// has_ori on it!
pub const fn get_ori_raw(&self) -> u8 {
self.attr[1] & 0b111
}
#[inline]
pub fn get_ori(&self) -> Option<u8> {
if self.get_sprite()?.has_ori() {
// TODO: Formalise this a bit better
Some(self.attr[1] & 0b111)
Some(self.get_ori_raw())
} else {
None
}
}
#[inline]
#[inline(always)]
pub fn get_glow(&self) -> Option<u8> {
match self.kind() {
BlockKind::Lava => Some(24),
BlockKind::GlowingRock | BlockKind::GlowingWeakRock => Some(10),
BlockKind::GlowingMushroom => Some(20),
_ => match self.get_sprite()? {
SpriteKind::StreetLamp | SpriteKind::StreetLampTall => Some(24),
SpriteKind::Ember => Some(20),
SpriteKind::WallLamp
| SpriteKind::WallLampSmall
| SpriteKind::WallSconce
| SpriteKind::FireBowlGround
| SpriteKind::ChristmasOrnament
| SpriteKind::CliffDecorBlock
| SpriteKind::Orb => Some(16),
SpriteKind::Velorite
| SpriteKind::VeloriteFrag
| SpriteKind::CavernGrassBlueShort
| SpriteKind::CavernGrassBlueMedium
| SpriteKind::CavernGrassBlueLong
| SpriteKind::CavernLillypadBlue
| SpriteKind::CavernMycelBlue
| SpriteKind::CeilingMushroom => Some(6),
SpriteKind::CaveMushroom
| SpriteKind::CookingPot
| SpriteKind::CrystalHigh
| SpriteKind::CrystalLow => Some(10),
SpriteKind::Amethyst
| SpriteKind::Ruby
| SpriteKind::Sapphire
| SpriteKind::Diamond
| SpriteKind::Emerald
| SpriteKind::Topaz
| SpriteKind::AmethystSmall
| SpriteKind::TopazSmall
| SpriteKind::DiamondSmall
| SpriteKind::RubySmall
| SpriteKind::EmeraldSmall
| SpriteKind::SapphireSmall => Some(3),
SpriteKind::Lantern => Some(24),
_ => None,
},
}
self.get_glow_raw().or_else(|| self.get_sprite().and_then(|sprite| sprite.get_glow()))
}
// minimum block, attenuation
@ -397,7 +381,7 @@ impl Block {
}
}
#[inline]
#[inline(always)]
pub fn kind(&self) -> BlockKind { self.kind }
/// If this block is a fluid, replace its sprite.

View File

@ -195,15 +195,18 @@ impl<V, Storage: core::ops::DerefMut<Target=Vec<V>>, S: RectVolSize, M: Clone> C
/// Iterate through the voxels in this chunk, attempting to avoid those that
/// are unchanged (i.e: match the `below` and `above` voxels). This is
/// generally useful for performance reasons.
pub fn iter_changed(&self) -> impl Iterator<Item = (Vec3<i32>, &V)> + '_ {
pub fn iter_changed(&self) -> impl Iterator<Item = (/*Vec3<i32>*/u32, &V)> + '_ {
self.sub_chunks
.iter()
.enumerate()
.filter(|(_, sc)| sc.num_groups() > 0)
.flat_map(move |(i, sc)| {
let z_offset = self.z_offset + i as i32 * SubChunkSize::<V, Storage, S>::SIZE.z as i32;
sc.vol_iter(Vec3::zero(), SubChunkSize::<V, Storage, S>::SIZE.map(|e| e as i32))
.map(move |(pos, vox)| (pos + Vec3::unit_z() * z_offset, vox))
/* let z_offset = self.z_offset + i as i32 * SubChunkSize::<V, Storage, S>::SIZE.z as i32;
let z_delta = Vec3::unit_z() * z_offset; */
let z_delta = i as u32 * SubChunk::<V, Storage, S, M>::VOLUME;
sc.iter_changed()
/* sc.vol_iter(Vec3::zero(), SubChunkSize::<V, Storage, S>::SIZE.map(|e| e as i32)) */
.map(move |(pos, vox)| (pos + z_delta, vox))
})
}

View File

@ -32,7 +32,7 @@ pub struct TerrainChunkSize;
/// Base two logarithm of the number of blocks along either horizontal axis of
/// a chunk.
///
/// NOTE: (1 << CHUNK_SIZE_LG) is guaranteed to fit in a u32.
/// NOTE: (1 << TERRAIN_CHUNK_BLOCKS_LG) is guaranteed to fit in a u32.
///
/// NOTE: A lot of code assumes that the two dimensions are equal, so we make it
/// explicit here.

View File

@ -453,6 +453,47 @@ impl SpriteKind {
}
}
#[inline]
pub fn get_glow(&self) -> Option<u8> {
match self {
SpriteKind::StreetLamp | SpriteKind::StreetLampTall => Some(24),
SpriteKind::Ember => Some(20),
SpriteKind::WallLamp
| SpriteKind::WallLampSmall
| SpriteKind::WallSconce
| SpriteKind::FireBowlGround
| SpriteKind::ChristmasOrnament
| SpriteKind::CliffDecorBlock
| SpriteKind::Orb => Some(16),
SpriteKind::Velorite
| SpriteKind::VeloriteFrag
| SpriteKind::CavernGrassBlueShort
| SpriteKind::CavernGrassBlueMedium
| SpriteKind::CavernGrassBlueLong
| SpriteKind::CavernLillypadBlue
| SpriteKind::CavernMycelBlue
| SpriteKind::CeilingMushroom => Some(6),
SpriteKind::CaveMushroom
| SpriteKind::CookingPot
| SpriteKind::CrystalHigh
| SpriteKind::CrystalLow => Some(10),
SpriteKind::Amethyst
| SpriteKind::Ruby
| SpriteKind::Sapphire
| SpriteKind::Diamond
| SpriteKind::Emerald
| SpriteKind::Topaz
| SpriteKind::AmethystSmall
| SpriteKind::TopazSmall
| SpriteKind::DiamondSmall
| SpriteKind::RubySmall
| SpriteKind::EmeraldSmall
| SpriteKind::SapphireSmall => Some(3),
SpriteKind::Lantern => Some(24),
_ => None,
}
}
#[inline]
pub fn has_ori(&self) -> bool {
matches!(

View File

@ -194,6 +194,45 @@ impl<V, S: core::ops::DerefMut<Target=Vec<V>> + VolSize<V>, M> Chunk<V, S, M> {
});
}
/// Iterate as fast as possible over all changed (non-default) blocks.
pub fn iter_changed(&self) -> impl Iterator<Item = (/* Vec3<i32> */u32, &V)> + '_
{
let vox = &self.vox;
self.indices
.iter()
.enumerate()
.flat_map(|(grp_idx, &base)| {
/* let grp_idx = grp_idx as i32;
let x = grp_idx & (Chunk::<V, S, M>::GROUP_COUNT.x as i32 - 1);
let y = (grp_idx >> 3) & (Chunk::<V, S, M>::GROUP_COUNT.y as i32 - 1);
let z = (grp_idx >> 6) & (Chunk::<V, S, M>::GROUP_COUNT.z as i32 - 1);
let grp_off = Vec3::new(
x * Chunk::<V, S, M>::GROUP_SIZE.x as i32,
y * Chunk::<V, S, M>::GROUP_SIZE.y as i32,
z * Chunk::<V, S, M>::GROUP_SIZE.z as i32,
); */
let grp_idx = grp_idx as u32;
let grp_off = grp_idx * Self::GROUP_VOLUME/* as usize */;
let start = usize::from(base) * Self::GROUP_VOLUME as usize;
let end = start + Self::GROUP_VOLUME as usize;
vox.get(start..end)
.into_iter()
.flatten()
.enumerate()
.map(move |(rel_idx, block)| {
// Construct the relative position.
/* let rel_idx = rel_idx as i32;
let x = rel_idx & (Chunk::<V, S, M>::GROUP_SIZE.x as i32 - 1);
let y = (rel_idx >> 2) & (Chunk::<V, S, M>::GROUP_SIZE.y as i32 - 1);
let z = (rel_idx >> 4) & (Chunk::<V, S, M>::GROUP_SIZE.z as i32 - 1);
let rel_off = Vec3::new(x, y, z); */
let rel_off = rel_idx as u32;
(grp_off | rel_off, block)
})
})
}
/// Compress this subchunk by frequency.
pub fn defragment(&mut self)
where

View File

@ -8,6 +8,7 @@
bool_to_option,
drain_filter,
once_cell,
stmt_expr_attributes,
trait_alias,
option_get_or_insert_default,
map_try_insert,

View File

@ -601,14 +601,23 @@ fn greedy_mesh_cross_section<M: PartialEq>(
// mask represents which faces are either set while the other is unset, or unset
// while the other is set.
let mut mask = (0..dims.y * dims.x).map(|_| None).collect::<Vec<_>>();
let mut mask = &mut mask[0..dims.y * dims.x];
(0..dims.z + 1).for_each(|d| {
// Compute mask
mask.iter_mut().enumerate().for_each(|(posi, mask)| {
let mut posi = 0;
(0..dims.y).for_each(|j| {
(0..dims.x).for_each(|i| {
// NOTE: Safe because dims.z actually fits in a u16.
mask[posi] = draw_face(Vec3::new(i as i32, j as i32, d as i32));
posi += 1;
});
});
/* mask.iter_mut().enumerate().for_each(|(posi, mask)| {
let i = posi % dims.x;
let j = posi / dims.x;
// NOTE: Safe because dims.z actually fits in a u16.
*mask = draw_face(Vec3::new(i as i32, j as i32, d as i32));
});
}); */
(0..dims.y).for_each(|j| {
let mut i = 0;

View File

@ -227,6 +227,7 @@ fn calc_light<V: RectRasterableVol<Vox = Block> + ReadVol + Debug>(
}
#[allow(clippy::type_complexity)]
#[inline(always)]
pub fn generate_mesh<'a, V: RectRasterableVol<Vox = Block> + ReadVol + Debug + 'static>(
vol: &'a VolGrid2d<V>,
(range, max_texture_size, _boi): (Aabb<i32>, Vec2<u16>, &'a BlocksOfInterest),
@ -279,17 +280,18 @@ pub fn generate_mesh<'a, V: RectRasterableVol<Vox = Block> + ReadVol + Debug + '
let mut opaque_limits = None::<Limits>;
let mut fluid_limits = None::<Limits>;
let mut air_limits = None::<Limits>;
let mut flat;
let flat_get = {
span!(_guard, "copy to flat array");
let (w, h, d) = range.size().into_tuple();
// z can range from -1..range.size().d + 1
let d = d + 2;
let flat = {
/*let flat = */{
let mut volume = vol.cached();
const AIR: Block = Block::air(common::terrain::sprite::SpriteKind::Empty);
// TODO: Once we can manage it sensibly, consider using something like
// Option<Block> instead of just assuming air.
let mut flat = vec![AIR; (w * h * d) as usize];
/*let mut */flat = vec![AIR; (w * h * d) as usize];
let mut i = 0;
for x in 0..range.size().w {
for y in 0..range.size().h {
@ -320,16 +322,19 @@ pub fn generate_mesh<'a, V: RectRasterableVol<Vox = Block> + ReadVol + Debug + '
}
}
}
flat
/* flat */
};
move |Vec3 { x, y, z }| {
let hd = h * d;
let flat = &flat[0..(w * hd) as usize];
#[inline(always)] move |Vec3 { x, y, z }| {
// z can range from -1..range.size().d + 1
let z = z + 1;
match flat.get((x * h * d + y * d + z) as usize).copied() {
flat[(x * hd + y * d + z) as usize]
/* match flat.get((x * hd + y * d + z) as usize).copied() {
Some(b) => b,
None => panic!("x {} y {} z {} d {} h {}", x, y, z, d, h),
}
} */
}
};
@ -370,28 +375,28 @@ pub fn generate_mesh<'a, V: RectRasterableVol<Vox = Block> + ReadVol + Debug + '
let greedy_size_cross = Vec3::new(greedy_size.x - 1, greedy_size.y - 1, greedy_size.z);
let draw_delta = Vec3::new(1, 1, z_start);
let get_light = |_: &mut (), pos: Vec3<i32>| {
let get_light = #[inline(always)] |_: &mut (), pos: Vec3<i32>| {
if flat_get(pos).is_opaque() {
0.0
} else {
light(pos + range.min)
}
};
let get_ao = |_: &mut (), pos: Vec3<i32>| {
let get_ao = #[inline(always)] |_: &mut (), pos: Vec3<i32>| {
if flat_get(pos).is_opaque() { 0.0 } else { 1.0 }
};
let get_glow = |_: &mut (), pos: Vec3<i32>| glow(pos + range.min);
let get_glow = #[inline(always)] |_: &mut (), pos: Vec3<i32>| glow(pos + range.min);
let get_color =
|_: &mut (), pos: Vec3<i32>| flat_get(pos).get_color().unwrap_or_else(Rgb::zero);
let get_opacity = |_: &mut (), pos: Vec3<i32>| !flat_get(pos).is_opaque();
let should_draw = |_: &mut (), pos: Vec3<i32>, delta: Vec3<i32>, _uv| {
should_draw_greedy(pos, delta, &flat_get)
#[inline(always)] |_: &mut (), pos: Vec3<i32>| flat_get(pos).get_color().unwrap_or_else(Rgb::zero);
let get_opacity = #[inline(always)] |_: &mut (), pos: Vec3<i32>| !flat_get(pos).is_opaque();
let should_draw = #[inline(always)] |_: &mut (), pos: Vec3<i32>, delta: Vec3<i32>, _uv| {
should_draw_greedy(pos, delta, #[inline(always)] |pos| flat_get(pos))
};
// NOTE: Conversion to f32 is fine since this i32 is actually in bounds for u16.
let mesh_delta = Vec3::new(0.0, 0.0, (z_start + range.min.z) as f32);
let create_opaque =
|atlas_pos, pos, norm, meta| TerrainVertex::new(atlas_pos, pos + mesh_delta, norm, meta);
let create_transparent = |_atlas_pos, pos, norm| FluidVertex::new(pos + mesh_delta, norm);
#[inline(always)] |atlas_pos, pos, norm, meta| TerrainVertex::new(atlas_pos, pos + mesh_delta, norm, meta);
let create_transparent = #[inline(always)] |_atlas_pos, pos, norm| FluidVertex::new(pos + mesh_delta, norm);
let mut greedy =
GreedyMesh::<guillotiere::SimpleAtlasAllocator>::new(max_size, greedy::general_config());
@ -407,7 +412,7 @@ pub fn generate_mesh<'a, V: RectRasterableVol<Vox = Block> + ReadVol + Debug + '
get_glow,
get_opacity,
should_draw,
push_quad: |atlas_origin, dim, origin, draw_dim, norm, meta: &FaceKind| match meta {
push_quad: #[inline(always)] |atlas_origin, dim, origin, draw_dim, norm, meta: &FaceKind| match meta {
FaceKind::Opaque(meta) => {
opaque_mesh.push_quad(greedy::create_quad(
atlas_origin,
@ -431,7 +436,7 @@ pub fn generate_mesh<'a, V: RectRasterableVol<Vox = Block> + ReadVol + Debug + '
));
},
},
make_face_texel: |data: &mut (), pos, light, glow, ao| {
make_face_texel: #[inline(always)] |data: &mut (), pos, light, glow, ao| {
TerrainVertex::make_col_light(light, glow, get_color(data, pos), ao)
},
});
@ -458,6 +463,7 @@ pub fn generate_mesh<'a, V: RectRasterableVol<Vox = Block> + ReadVol + Debug + '
/// NOTE: Make sure to reflect any changes to how meshing is performanced in
/// [scene::terrain::Terrain::skip_remesh].
#[inline(always)]
fn should_draw_greedy(
pos: Vec3<i32>,
delta: Vec3<i32>,
@ -470,7 +476,7 @@ fn should_draw_greedy(
if from_filled == to.is_filled() {
// Check the interface of liquid and non-tangible non-liquid (e.g. air).
let from_liquid = from.is_liquid();
if from_liquid == to.is_liquid() || from.is_filled() || to.is_filled() {
if from_liquid == to.is_liquid() || /*from.is_filled() || to.is_filled()*/from_filled {
None
} else {
// While liquid is not culled, we still try to keep a consistent orientation as

View File

@ -237,7 +237,14 @@ fn mesh_worker<V: BaseVol<Vox = Block> + RectRasterableVol + ReadVol + Debug + '
sprite_config: &SpriteSpec,
) -> MeshWorkerResponse {
span!(_guard, "mesh_worker");
let blocks_of_interest = BlocksOfInterest::from_chunk(&chunk);
let (blocks_of_interest, sprite_kinds) = BlocksOfInterest::from_chunk(&chunk)/*default()*/;
let mut range = range;
/* range.min += Vec3::from(V::RECT_SIZE.as_::<i32>()) - 1;
range.max -= Vec3::from(V::RECT_SIZE.as_::<i32>()) - 1; */
range.min.z = chunk.get_min_z() - 2;
range.max.z = chunk.get_max_z() + 2;
let z_bounds = (range.min.z, range.max.z);
let mesh;
let (light_map, glow_map) = if let Some((light_map, glow_map)) = &skip_remesh {
@ -274,13 +281,14 @@ fn mesh_worker<V: BaseVol<Vox = Block> + RectRasterableVol + ReadVol + Debug + '
prof_span!("extract sprite_instances");
let mut instances = [(); SPRITE_LOD_LEVELS].map(|()| Vec::new());
for x in 0..V::RECT_SIZE.x as i32 {
for (rel_pos, (sprite, ori)) in /*chunk.iter_changed()*/sprite_kinds {
/* for x in 0..V::RECT_SIZE.x as i32 {
for y in 0..V::RECT_SIZE.y as i32 {
for z in z_bounds.0 as i32..z_bounds.1 as i32 + 1 {
let rel_pos = Vec3::new(x, y, z);
for z in z_bounds.0 as i32..z_bounds.1 as i32 + 1 {*/
/* let rel_pos = Vec3::new(x, y, z); */
let wpos = Vec3::from(pos * V::RECT_SIZE.map(|e: u32| e as i32)) + rel_pos;
let block = if let Ok(block) = volume.get(wpos) {
/* let block = if let Ok(block) = volume.get(wpos) {
block
} else {
continue;
@ -289,13 +297,13 @@ fn mesh_worker<V: BaseVol<Vox = Block> + RectRasterableVol + ReadVol + Debug + '
sprite
} else {
continue;
};
}; */
if let Some(cfg) = sprite_config.get(sprite) {
let seed = wpos.x as u64 * 3
+ wpos.y as u64 * 7
+ wpos.x as u64 * wpos.y as u64; // Awful PRNG
let ori = (block.get_ori().unwrap_or((seed % 4) as u8 * 2)) & 0b111;
let ori = (ori.unwrap_or((seed % 4) as u8 * 2)) & 0b111;
let variation = seed as usize % cfg.variations.len();
let key = (sprite, variation);
// NOTE: Safe because we called sprite_config_for already.
@ -334,8 +342,9 @@ fn mesh_worker<V: BaseVol<Vox = Block> + RectRasterableVol + ReadVol + Debug + '
}
}
}
}
/* }
}
} */
}
instances

View File

@ -1,8 +1,7 @@
use crate::hud::CraftingTab;
use common::terrain::{BlockKind, SpriteKind, TerrainChunk};
use common::terrain::{BlockKind, SpriteKind, TerrainChunk, TERRAIN_CHUNK_BLOCKS_LG};
use common_base::span;
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use vek::*;
#[derive(Copy, Clone, Debug)]
@ -56,7 +55,7 @@ pub struct BlocksOfInterest {
}
impl BlocksOfInterest {
pub fn from_chunk(chunk: &TerrainChunk) -> Self {
pub fn from_chunk(chunk: &TerrainChunk) -> (Self, Vec<(Vec3<i32>, (SpriteKind, Option<u8>))>) {
span!(_guard, "from_chunk", "BlocksOfInterest::from_chunk");
let mut leaves = Vec::new();
let mut drip = Vec::new();
@ -80,100 +79,191 @@ impl BlocksOfInterest {
let mut cricket2 = Vec::new();
let mut cricket3 = Vec::new();
let mut frogs = Vec::new();
let mut sprite_kinds = Vec::new();
let mut rng = ChaCha8Rng::from_seed(thread_rng().gen());
let mut rng = SmallRng::from_seed(thread_rng().gen());
let river_speed_sq = chunk.meta().river_velocity().magnitude_squared();
let z_offset = chunk.get_min_z();
chunk.iter_changed().for_each(|(pos, block)| {
match block.kind() {
BlockKind::Leaves if rng.gen_range(0..16) == 0 => leaves.push(pos),
BlockKind::WeakRock if rng.gen_range(0..6) == 0 => drip.push(pos),
BlockKind::Grass => {
if rng.gen_range(0..16) == 0 {
grass.push(pos);
}
match rng.gen_range(0..8192) {
1 => cricket1.push(pos),
2 => cricket2.push(pos),
3 => cricket3.push(pos),
_ => {},
}
},
// Assign a river speed to water blocks depending on river velocity
BlockKind::Water if river_speed_sq > 0.9_f32.powi(2) => fast_river.push(pos),
BlockKind::Water if river_speed_sq > 0.3_f32.powi(2) => slow_river.push(pos),
BlockKind::Snow if rng.gen_range(0..16) == 0 => snow.push(pos),
BlockKind::Lava if rng.gen_range(0..5) == 0 => fires.push(pos + Vec3::unit_z()),
BlockKind::Snow | BlockKind::Ice if rng.gen_range(0..16) == 0 => snow.push(pos),
_ => match block.get_sprite() {
Some(SpriteKind::Ember) => {
const LEAF_BITS: u32 = 4;
const DRIP_BITS: u32 = 3;
const SNOW_BITS: u32 = 4;
const LAVA_BITS: u32 = 2;
const GRASS_BITS: u32 = 4;
const CRICKET_BITS: u32 = 13;
const FROG_BITS: u32 = 4;
const LEAF_MASK: u64 = (1 << LEAF_BITS) - 1;
const DRIP_MASK: u64 = (1 << DRIP_BITS) - 1;
const SNOW_MASK: u64 = (1 << SNOW_BITS) - 1;
const LAVA_MASK: u64 = (1 << LAVA_BITS) - 1;
// NOTE: Grass and cricket bits are merged together to save a call to the rng.
const CRICKET_MASK: u64 = ((1 << CRICKET_BITS) - 1);
const GRASS_MASK: u64 = ((1 << GRASS_BITS) - 1) << CRICKET_BITS;
const FROG_MASK: u64 = (1 << FROG_BITS) - 1;
// NOTE: Z chunk total height cannot exceed 2^14, so x+y+z fits in 24 bits. Therefore we
// know -1 is never a valid height, so it's okay to use as a representative of "no river".
let mut river_data = [-1i16; (1 << TERRAIN_CHUNK_BLOCKS_LG * 2)];
let river = if river_speed_sq > 0.9_f32.powi(2) {
Some(&mut fast_river)
} else if river_speed_sq > 0.3_f32.powi(2) {
Some(&mut slow_river)
} else {
None
};
chunk.iter_changed().for_each(|(index, block)| {
// FIXME: Before merge, make this properly generic.
#[inline(always)]
fn make_pos_raw(index: u32) -> Vec3<i32> {
let grp = index >> 6;
let rel = index & 63;
let x = ((grp & 7) << 2) | (rel & 3);
let y = (((grp >> 3) & 7) << 2) | ((rel >> 2) & 3);
let z = ((grp >> 6) << 2) | (rel >> 4);
Vec3::new(x as i32, y as i32, z as i32)
};
let make_pos = #[inline(always)] |index: u32| {
let mut pos = make_pos_raw(index);
pos.z += z_offset;
pos
};
let mut do_sprite = |sprite: SpriteKind, rng: &mut SmallRng| {
let pos = make_pos(index);
match sprite {
SpriteKind::Ember => {
fires.push(pos);
smokers.push(SmokerProperties::new(pos, FireplaceType::House));
},
// Offset positions to account for block height.
// TODO: Is this a good idea?
Some(SpriteKind::StreetLamp) => fire_bowls.push(pos + Vec3::unit_z() * 2),
Some(SpriteKind::FireBowlGround) => fire_bowls.push(pos + Vec3::unit_z()),
Some(SpriteKind::StreetLampTall) => fire_bowls.push(pos + Vec3::unit_z() * 4),
Some(SpriteKind::WallSconce) => fire_bowls.push(pos + Vec3::unit_z()),
Some(SpriteKind::Beehive) => beehives.push(pos),
Some(SpriteKind::CrystalHigh) => fireflies.push(pos),
Some(SpriteKind::Reed) => {
SpriteKind::StreetLamp => fire_bowls.push(pos + Vec3::unit_z() * 2),
SpriteKind::FireBowlGround => fire_bowls.push(pos + Vec3::unit_z()),
SpriteKind::StreetLampTall => fire_bowls.push(pos + Vec3::unit_z() * 4),
SpriteKind::WallSconce => fire_bowls.push(pos + Vec3::unit_z()),
SpriteKind::Beehive => beehives.push(pos),
SpriteKind::CrystalHigh => fireflies.push(pos),
SpriteKind::Reed => {
reeds.push(pos);
fireflies.push(pos);
if rng.gen_range(0..12) == 0 {
if rng.next_u64() & FROG_MASK == 0 {
frogs.push(pos);
}
},
Some(SpriteKind::CaveMushroom) => fireflies.push(pos),
Some(SpriteKind::PinkFlower) => flowers.push(pos),
Some(SpriteKind::PurpleFlower) => flowers.push(pos),
Some(SpriteKind::RedFlower) => flowers.push(pos),
Some(SpriteKind::WhiteFlower) => flowers.push(pos),
Some(SpriteKind::YellowFlower) => flowers.push(pos),
Some(SpriteKind::Sunflower) => flowers.push(pos),
Some(SpriteKind::CraftingBench) => {
SpriteKind::CaveMushroom => fireflies.push(pos),
SpriteKind::PinkFlower => flowers.push(pos),
SpriteKind::PurpleFlower => flowers.push(pos),
SpriteKind::RedFlower => flowers.push(pos),
SpriteKind::WhiteFlower => flowers.push(pos),
SpriteKind::YellowFlower => flowers.push(pos),
SpriteKind::Sunflower => flowers.push(pos),
SpriteKind::CraftingBench => {
interactables.push((pos, Interaction::Craft(CraftingTab::All)))
},
Some(SpriteKind::SmokeDummy) => {
SpriteKind::SmokeDummy => {
smokers.push(SmokerProperties::new(pos, FireplaceType::Workshop));
},
Some(SpriteKind::Forge) => interactables
SpriteKind::Forge => interactables
.push((pos, Interaction::Craft(CraftingTab::ProcessedMaterial))),
Some(SpriteKind::TanningRack) => interactables
SpriteKind::TanningRack => interactables
.push((pos, Interaction::Craft(CraftingTab::ProcessedMaterial))),
Some(SpriteKind::SpinningWheel) => {
SpriteKind::SpinningWheel => {
interactables.push((pos, Interaction::Craft(CraftingTab::All)))
},
Some(SpriteKind::Loom) => {
SpriteKind::Loom => {
interactables.push((pos, Interaction::Craft(CraftingTab::All)))
},
Some(SpriteKind::Cauldron) => {
SpriteKind::Cauldron => {
fires.push(pos);
interactables.push((pos, Interaction::Craft(CraftingTab::Potion)))
},
Some(SpriteKind::Anvil) => {
SpriteKind::Anvil => {
interactables.push((pos, Interaction::Craft(CraftingTab::Weapon)))
},
Some(SpriteKind::CookingPot) => {
SpriteKind::CookingPot => {
fires.push(pos);
interactables.push((pos, Interaction::Craft(CraftingTab::Food)))
},
Some(SpriteKind::DismantlingBench) => {
SpriteKind::DismantlingBench => {
fires.push(pos);
interactables.push((pos, Interaction::Craft(CraftingTab::Dismantle)))
},
_ => {},
}
sprite_kinds.push((pos, (sprite, sprite.has_ori().then(|| block.get_ori_raw()))));
if sprite.is_collectible() {
interactables.push((pos, Interaction::Collect));
}
sprite.get_glow()
};
let mut has_sprite = false;
let glow = match block.kind() {
kind @ BlockKind::Leaves => {
if rng.next_u64() & LEAF_MASK == 0 { leaves.push(make_pos(index)); }
kind.get_glow_raw()
},
}
if block.is_collectible() {
interactables.push((pos, Interaction::Collect));
}
if let Some(glow) = block.get_glow() {
kind @ BlockKind::WeakRock => {
if rng.next_u64() & DRIP_MASK == 0 { drip.push(make_pos(index)); }
kind.get_glow_raw()
},
kind @ BlockKind::Grass => {
let bits = rng.next_u64();
if bits & GRASS_MASK == 0 {
grass.push(make_pos(index));
}
match bits & CRICKET_MASK {
1 => cricket1.push(make_pos(index)),
2 => cricket2.push(make_pos(index)),
3 => cricket3.push(make_pos(index)),
_ => {},
}
kind.get_glow_raw()
},
// Assign a river speed to water blocks depending on river velocity
kind @ BlockKind::Water => {
if river.is_some() {
// Remember only the top river blocks. Since we always go from low to high z, this assignment
// can be unconditional.
let mut pos = make_pos_raw(index);
river_data[((pos.y << TERRAIN_CHUNK_BLOCKS_LG) | pos.x) as usize] = pos.z as i16;
}
if let Some(sprite) = block.get_sprite_raw() {
has_sprite = true;
do_sprite(sprite, &mut rng)
} else {
kind.get_glow_raw()
}
}
kind @ BlockKind::Lava => {
if rng.next_u64() & LAVA_MASK == 0 { fires.push(make_pos(index) + Vec3::unit_z()) }
kind.get_glow_raw()
},
kind @ BlockKind::Snow | kind @ BlockKind::Ice => {
if rng.next_u64() & SNOW_MASK == 0 { snow.push(make_pos(index)); }
kind.get_glow_raw()
},
kind @ BlockKind::Air => if let Some(sprite) = block.get_sprite_raw() {
has_sprite = true;
do_sprite(sprite, &mut rng)
} else {
kind.get_glow_raw()
},
kind @ BlockKind::Rock | kind @ BlockKind::GlowingRock | kind @ BlockKind::GlowingWeakRock |
kind @ BlockKind::GlowingMushroom |
kind @ BlockKind::Earth | kind @ BlockKind::Sand | kind @ BlockKind::Wood | kind @ BlockKind::Misc => {
kind.get_glow_raw()
}
};
if let Some(glow) = glow {
let pos = make_pos(index);
// Currently, we count filled blocks as 'minor' lights, and sprites as
// non-minor.
if block.get_sprite().is_none() {
if has_sprite {
minor_lights.push((pos, glow));
} else {
lights.push((pos, glow));
@ -181,6 +271,23 @@ impl BlocksOfInterest {
}
});
// Convert river grid to vector.
const X_MASK: usize = (1 << TERRAIN_CHUNK_BLOCKS_LG) - 1;
if let Some(river) = river {
river.extend(
river_data.into_iter().enumerate()
// Avoid blocks with no water
.filter(|&(_, river_block)| river_block != -1)
.map(|(index, river_block)|
Vec3::new(
(index & X_MASK) as i32,
(index >> TERRAIN_CHUNK_BLOCKS_LG) as i32,
z_offset + i32::from(river_block),
))
);
}
// TODO: Come up with a better way to prune many light sources: grouping them
// into larger lights with k-means clustering, perhaps?
const MAX_MINOR_LIGHTS: usize = 64;
@ -190,7 +297,7 @@ impl BlocksOfInterest {
.copied(),
);
Self {
(Self {
leaves,
drip,
grass,
@ -212,6 +319,6 @@ impl BlocksOfInterest {
lights,
temperature: chunk.meta().temp(),
humidity: chunk.meta().humidity(),
}
}, sprite_kinds)
}
}

View File

@ -181,6 +181,7 @@ impl<'a> ZCache<'a> {
}
}
#[inline(always)]
pub fn block_from_structure(
index: IndexRef,
sblock: StructureBlock,

View File

@ -2,9 +2,9 @@ use crate::{
all::*,
block::block_from_structure,
column::ColumnGen,
site2::{self, PrimitiveTransform},
site2::{self, Fill, Filler, FillFn, Painter, PrimitiveTransform},
layer::cave::tunnel_bounds_at,
util::{gen_cache::StructureGenCache, RandomPerm, Sampler, UnitChooser},
util::{gen_cache::StructureGenCache, RandomField, RandomPerm, Sampler, UnitChooser},
Canvas, CanvasInfo, ColumnSample,
};
use common::{
@ -323,122 +323,193 @@ pub fn apply_trees_to(
(s.get_bounds(), [(0.0004, SpriteKind::Beehive)].as_ref())
},
&TreeModel::Procedural(ref t, leaf_block) => {
#[inline(always)]
fn draw_tree<'a, F: Filler>(
t: &ProceduralTree,
leaf_block: impl Fill + Copy,
trunk_block: impl Fill + Copy,
wpos: Vec3<i32>,
painter: &Painter<'a>,
filler: &mut FillFn<'a, '_, F>
) {
let leaf_vertical_scale = t.config.leaf_vertical_scale.recip();
let branch_child_radius_lerp = t.config.branch_child_radius_lerp;
// NOTE: Technically block_from_structure isn't correct here, because it could
// lerp with position; in practice, it almost never does, and most of the other
// expensive parameters are unused.
/* let trunk_block = if let Some(block) = block_from_structure(
info.index(),
trunk_block,
wpos,
tree.pos.xy(),
tree.seed,
&col,
Block::air,
calendar,
) {
block
} else {
return;
};
let leaf_block = if let Some(block) = block_from_structure(
info.index(),
leaf_block,
wpos,
tree.pos.xy(),
tree.seed,
&col,
Block::air,
calendar,
) {
block
} else {
return;
}; */
t.walk(|branch, parent| {
let aabr = Aabr {
min: wpos.xy() + branch.get_aabb().min.xy().as_(),
max: wpos.xy() + branch.get_aabb().max.xy().as_(),
};
if aabr.collides_with_aabr(filler.render_aabr().as_()) {
let start =
wpos.as_::<f32>() + branch.get_line().start/*.as_()*//* - 0.5*/;
let end =
wpos.as_::<f32>() + branch.get_line().end/*.as_()*//* - 0.5*/;
let wood_radius = branch.get_wood_radius();
let leaf_radius = branch.get_leaf_radius();
let parent_wood_radius = if branch_child_radius_lerp {
parent.get_wood_radius()
} else {
wood_radius
};
let leaf_eats_wood = leaf_radius > wood_radius;
let leaf_eats_parent_wood = leaf_radius > parent_wood_radius;
if !leaf_eats_wood || !leaf_eats_parent_wood {
// Render the trunk, since it's not swallowed by its leaf.
painter
.line_two_radius(
start,
end,
parent_wood_radius,
wood_radius,
1.0,
)
.fill(/*filler.block(trunk_block)*/trunk_block, filler);
}
if leaf_eats_wood || leaf_eats_parent_wood {
// Render the leaf, since it's not *completely* swallowed
// by the trunk.
painter
.line_two_radius(
start,
end,
leaf_radius,
leaf_radius,
leaf_vertical_scale,
)
.fill(/*filler.block(leaf_block)*/leaf_block, filler);
}
true
} else {
false
}
});
// Draw the roots.
t.roots.iter().for_each(|root| {
painter
.line(
wpos/*.as_::<f32>()*/ + root.line.start.as_()/* - 0.5*/,
wpos/*.as_::<f32>()*/ + root.line.end.as_()/* - 0.5*/,
root.radius,
)
.fill(/*filler.block(leaf_block)*/trunk_block, filler);
});
}
let bounds = t.get_bounds().map(|e| e as i32);
let trunk_block = t.config.trunk_block;
let leaf_vertical_scale = t.config.leaf_vertical_scale.recip();
let branch_child_radius_lerp = t.config.branch_child_radius_lerp;
// NOTE: Technically block_from_structure isn't correct here, because it could
// lerp with position; in practice, it almost never does, and most of the other
// expensive parameters are unused.
/* let trunk_block = if let Some(block) = block_from_structure(
info.index(),
trunk_block,
wpos,
tree.pos.xy(),
tree.seed,
&col,
Block::air,
calendar,
) {
block
} else {
return;
};
let leaf_block = if let Some(block) = block_from_structure(
info.index(),
leaf_block,
wpos,
tree.pos.xy(),
tree.seed,
&col,
Block::air,
calendar,
) {
block
} else {
return;
}; */
site2::render_collect(
&arena,
info,
render_area,
canvas,
|painter, filler| {
let trunk_block = filler.block_from_structure(
trunk_block,
tree.pos.xy(),
tree.seed,
&col,
);
let leaf_block = filler.block_from_structure(
let leaf_block = /* filler.block_from_structure(
leaf_block,
tree.pos.xy(),
tree.seed,
&col,
);
t.walk(|branch, parent| {
let aabr = Aabr {
min: wpos.xy() + branch.get_aabb().min.xy().as_(),
max: wpos.xy() + branch.get_aabb().max.xy().as_(),
};
if aabr.collides_with_aabr(filler.render_aabr().as_()) {
let start =
wpos.as_::<f32>() + branch.get_line().start/*.as_()*//* - 0.5*/;
let end =
wpos.as_::<f32>() + branch.get_line().end/*.as_()*//* - 0.5*/;
let wood_radius = branch.get_wood_radius();
let leaf_radius = branch.get_leaf_radius();
let parent_wood_radius = if branch_child_radius_lerp {
parent.get_wood_radius()
} else {
wood_radius
};
let leaf_eats_wood = leaf_radius > wood_radius;
let leaf_eats_parent_wood = leaf_radius > parent_wood_radius;
if !leaf_eats_wood || !leaf_eats_parent_wood {
// Render the trunk, since it's not swallowed by its leaf.
painter
.line_two_radius(
start,
end,
parent_wood_radius,
wood_radius,
1.0,
)
.fill(/*filler.block(trunk_block)*/trunk_block, filler);
}
if leaf_eats_wood || leaf_eats_parent_wood {
// Render the leaf, since it's not *completely* swallowed
// by the trunk.
painter
.line_two_radius(
start,
end,
leaf_radius,
leaf_radius,
leaf_vertical_scale,
)
.fill(/*filler.block(leaf_block)*/leaf_block, filler);
}
true
) */{
let structure_pos = tree.pos.xy();
let structure_seed = tree.seed;
let field = RandomField::new(structure_seed);
let lerp = ((field.get(Vec3::from(structure_pos)).rem_euclid(256)) as f32 / 255.0) * 0.8;
let ranges = leaf_block
.elim_case_pure(&info.index().colors.block.structure_blocks)
.as_ref()
.map(Vec::as_slice)
.unwrap_or(&[]);
let range = if ranges.is_empty() {
// Error occurred, but this ideally shouldn't happen.
return;
} else {
false
}
});
// Draw the roots.
t.roots.iter().for_each(|root| {
painter
.line(
wpos/*.as_::<f32>()*/ + root.line.start.as_()/* - 0.5*/,
wpos/*.as_::<f32>()*/ + root.line.end.as_()/* - 0.5*/,
root.radius,
&ranges[
RandomPerm::new(structure_seed).get(structure_seed) as usize % ranges.len()
]
};
let start = Rgb::<u8>::from(range.start).map(f32::from);
let end = Rgb::<u8>::from(range.end).map(f32::from);
let is_christmas = calendar.map_or(false, |c| c.is_event(CalendarEvent::Christmas));
filler.sampling(move |pos| {
Some(
if is_christmas && field.chance(pos + structure_pos, 0.025)
{
Block::new(BlockKind::GlowingWeakRock, Rgb::new(255, 0, 0))
} else {
let lerp = lerp
+ ((field.get(pos + i32::MAX / 2).rem_euclid(256)) as f32 / 255.0) * 0.2;
Block::new(
BlockKind::Leaves,
Rgb::<f32>::lerp(start, end, lerp).as_::<u8>(),
)
}
)
.fill(/*filler.block(leaf_block)*/trunk_block, filler);
});
},
);
})
};
/* let trunk_block = /* filler.block_from_structure(
trunk_block,
tree.pos.xy(),
tree.seed,
&col,
); */*/match trunk_block {
StructureBlock::Filled(kind, color) => {
let trunk_block = filler.block(Block::new(kind, color));
draw_tree(t, leaf_block, trunk_block, wpos, painter, filler);
/* filler.block(Block::new(kind, color)) */
},
StructureBlock::BirchWood => {
let structure_pos = tree.pos.xy();
let field = RandomField::new(tree.seed);
let trunk_block = filler.sampling(move |pos| {
let wpos = pos + structure_pos;
if field.chance(
(wpos + Vec3::new(wpos.z, wpos.z, 0) / 2)
/ Vec3::new(1 + wpos.z % 2, 1 + (wpos.z + 1) % 2, 1),
0.25,
) && wpos.z % 2 == 0
{
Some(Block::new(BlockKind::Wood, Rgb::new(70, 35, 25)))
} else {
Some(Block::new(BlockKind::Wood, Rgb::new(220, 170, 160)))
}
});
draw_tree(t, leaf_block, trunk_block, wpos, painter, filler);
},
_ => unimplemented!("Only birch and filled trunk blocks are currently supported."),
}
});
(bounds, t.config.hanging_sprites)
},
};

View File

@ -156,7 +156,7 @@ pub fn sample_pos(
} else {
Lerp::lerp(
sample.sub_surface_color,
sample.surface_color,
if sample.snow_cover { Rgb::new(210.0, 210.0, 255.0) / 255.0 } else { sample.surface_color },
((wposz as f32 - (alt - grass_depth)) / grass_depth).sqrt(),
)
.map(|e| e as f64)

View File

@ -660,6 +660,7 @@ impl<'a, 'b, F: Filler> FillFn<'a, 'b, F> {
})
}
#[inline(always)]
pub fn block_from_structure(&self, sb: StructureBlock, structure_pos: Vec2<i32>, seed: u32, col_sample: &'b ColumnSample) -> impl Fill + Copy + 'b
{
/* let col_sample = /*if let Some(col_sample) = */self.canvas_info.col(self.canvas_info.wpos)/* {
@ -2856,27 +2857,83 @@ impl Painter<'_> {
// TODO: Optimize further?
let aabb = Self::get_bounds(cache, tree, prim);
/*if !(aabb.size().w > 8 || aabb.size().h > 8 || aabb.size().d > 16) */{
let mut do_segment = || {
let distance = segment.end - segment.start;
let distance_proj = distance / distance.magnitude_squared();
let segment_start = segment.start - 0.5;
return aabb_iter(
aabb.as_(),
mat,
mask,
// |_| true,
|pos| {
let pos = pos.as_::<f32>();
let length = pos - segment_start;
let t = length.dot(distance_proj).clamped(0.0, 1.0);
let mut diff = distance * t - length;
diff.z *= z_scale;
let radius = Lerp::lerp_unclamped(r0, r1, t);
diff.magnitude_squared() < radius * radius
},
hit,
);
return
if r0 == r1 {
let radius_2 = r0 * r0;
if z_scale == 1.0 {
return aabb_iter(
aabb.as_(),
mat,
mask,
// |_| true,
|pos| {
let pos = pos.as_::<f32>();
let length = pos - segment_start;
let t = length.dot(distance_proj).clamped(0.0, 1.0);
let diff = distance * t - length;
diff.magnitude_squared() < radius_2
},
hit,
);
} else {
return aabb_iter(
aabb.as_(),
mat,
mask,
// |_| true,
|pos| {
let pos = pos.as_::<f32>();
let length = pos - segment_start;
let t = length.dot(distance_proj).clamped(0.0, 1.0);
let mut diff = distance * t - length;
diff.z *= z_scale;
diff.magnitude_squared() < radius_2
},
hit,
);
}
} else if z_scale == 1.0 {
return aabb_iter(
aabb.as_(),
mat,
mask,
// |_| true,
|pos| {
let pos = pos.as_::<f32>();
let length = pos - segment_start;
let t = length.dot(distance_proj).clamped(0.0, 1.0);
let diff = distance * t - length;
let radius = Lerp::lerp_unclamped(r0, r1, t);
diff.magnitude_squared() < radius * radius
},
hit,
);
} else {
return aabb_iter(
aabb.as_(),
mat,
mask,
// |_| true,
|pos| {
let pos = pos.as_::<f32>();
let length = pos - segment_start;
let t = length.dot(distance_proj).clamped(0.0, 1.0);
let mut diff = distance * t - length;
diff.z *= z_scale;
let radius = Lerp::lerp_unclamped(r0, r1, t);
diff.magnitude_squared() < radius * radius
},
hit,
);
}
};
// NOTE: This hurts gnarling fortresses by about 1.5% but helps cliff towns by
// about 3%, compared to no closure. We deem the latter more important.
do_segment();
return;
}
// NOTE: vol(frustum) = 1/3 h(A(r₀) + A(r₁) + √(A(r₀)A(r₁)))
// = 1/3 h (r₀² + r₁² + √(r₀²r₁²))