veloren_world/layer/
mod.rs

1pub mod cave;
2pub mod rock;
3pub mod scatter;
4pub mod shrub;
5pub mod spot;
6pub mod tree;
7pub mod wildlife;
8
9pub use self::{
10    cave::apply_caves_to, rock::apply_rocks_to, scatter::apply_scatter_to, shrub::apply_shrubs_to,
11    spot::apply_spots_to, tree::apply_trees_to,
12};
13
14use crate::{
15    Canvas, CanvasInfo,
16    column::ColumnSample,
17    config::CONFIG,
18    sim,
19    util::{FastNoise, RandomField, RandomPerm, Sampler},
20};
21use common::terrain::{Block, BlockKind, SpriteKind};
22use hashbrown::HashMap;
23use noise::NoiseFn;
24use rand::prelude::*;
25use serde::Deserialize;
26use std::{
27    f32,
28    ops::{Add, Mul, Range, Sub},
29};
30use vek::*;
31
32#[derive(Deserialize)]
33pub struct Colors {
34    pub bridge: (u8, u8, u8),
35}
36
37const EMPTY_AIR: Block = Block::empty();
38
39pub struct PathLocals {
40    pub riverless_alt: f32,
41    pub alt: f32,
42    pub water_dist: f32,
43    pub bridge_offset: f32,
44    pub depth: i32,
45}
46
47impl PathLocals {
48    pub fn new(info: &CanvasInfo, col: &ColumnSample, path_nearest: Vec2<f32>) -> PathLocals {
49        // Try to use the column at the centre of the path for sampling to make them
50        // flatter
51        let col_pos = -info.wpos().map(|e| e as f32) + path_nearest;
52        let col00 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 0));
53        let col10 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 0));
54        let col01 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(0, 1));
55        let col11 = info.col(info.wpos() + col_pos.map(|e| e.floor() as i32) + Vec2::new(1, 1));
56        let col_attr = |col: &ColumnSample| {
57            Vec3::new(col.riverless_alt, col.alt, col.water_dist.unwrap_or(1000.0))
58        };
59        let [riverless_alt, alt, water_dist] = match (col00, col10, col01, col11) {
60            (Some(col00), Some(col10), Some(col01), Some(col11)) => Lerp::lerp(
61                Lerp::lerp(col_attr(col00), col_attr(col10), path_nearest.x.fract()),
62                Lerp::lerp(col_attr(col01), col_attr(col11), path_nearest.x.fract()),
63                path_nearest.y.fract(),
64            ),
65            _ => col_attr(col),
66        }
67        .into_array();
68        let (bridge_offset, depth) = (
69            ((water_dist.max(0.0) * 0.2).min(f32::consts::PI).cos() + 1.0) * 5.0,
70            ((1.0 - ((water_dist + 2.0) * 0.3).min(0.0).cos().abs())
71                * (riverless_alt + 5.0 - alt).max(0.0)
72                * 1.75
73                + 3.0) as i32,
74        );
75        PathLocals {
76            riverless_alt,
77            alt,
78            water_dist,
79            bridge_offset,
80            depth,
81        }
82    }
83}
84
85pub fn apply_paths_to(canvas: &mut Canvas) {
86    canvas.foreach_col(|canvas, wpos2d, col| {
87        let surface_z = col.riverless_alt.floor() as i32;
88
89        let noisy_color = |color: Rgb<u8>, factor: u32| {
90            let nz = RandomField::new(0).get(Vec3::new(wpos2d.x, wpos2d.y, surface_z));
91            color.map(|e| {
92                (e as u32 + nz % (factor * 2))
93                    .saturating_sub(factor)
94                    .min(255) as u8
95            })
96        };
97
98        if let Some((path_dist, path_nearest, path, _)) =
99            col.path.filter(|(dist, _, path, _)| *dist < path.width)
100        {
101            let inset = 0;
102
103            let PathLocals {
104                riverless_alt,
105                alt: _,
106                water_dist: _,
107                bridge_offset: _,
108                depth: _,
109            } = PathLocals::new(&canvas.info(), col, path_nearest);
110
111            let depth = 4;
112            let surface_z = riverless_alt.floor() as i32;
113
114            for z in inset - depth..inset {
115                let path_color =
116                    path.surface_color(col.sub_surface_color.map(|e| (e * 255.0) as u8));
117                canvas.set(
118                    Vec3::new(wpos2d.x, wpos2d.y, surface_z + z),
119                    Block::new(BlockKind::Earth, noisy_color(path_color, 8)),
120                );
121            }
122            let head_space = path.head_space(path_dist);
123            for z in inset..inset + head_space {
124                let pos = Vec3::new(wpos2d.x, wpos2d.y, surface_z + z);
125                if canvas.get(pos).kind() != BlockKind::Water {
126                    canvas.set(pos, EMPTY_AIR);
127                }
128            }
129        }
130    });
131}
132
133pub fn apply_trains_to(
134    canvas: &mut Canvas,
135    sim: &sim::WorldSim,
136    sim_chunk: &sim::SimChunk,
137    chunk_center_wpos2d: Vec2<i32>,
138) {
139    let mut splines = Vec::new();
140    let g = |v: Vec2<f32>| -> Vec3<f32> {
141        let path_nearest = sim
142            .get_nearest_path(v.as_::<i32>())
143            .map(|x| x.1)
144            .unwrap_or(v.as_::<f32>());
145        let alt = if let Some(c) = canvas.col_or_gen(v.as_::<i32>()) {
146            let pl = PathLocals::new(canvas, &c, path_nearest);
147            pl.riverless_alt + pl.bridge_offset + 0.75
148        } else {
149            sim_chunk.alt
150        };
151        v.with_z(alt)
152    };
153    fn hermite_to_bezier(
154        p0: Vec3<f32>,
155        m0: Vec3<f32>,
156        p3: Vec3<f32>,
157        m3: Vec3<f32>,
158    ) -> CubicBezier3<f32> {
159        let hermite = Vec4::new(p0, p3, m0, m3);
160        let hermite = hermite.map(|v| v.with_w(0.0));
161        let hermite: [[f32; 4]; 4] = hermite.map(|v: Vec4<f32>| v.into_array()).into_array();
162        // https://courses.engr.illinois.edu/cs418/sp2009/notes/12-MoreSplines.pdf
163        let mut m = Mat4::from_row_arrays([
164            [1.0, 0.0, 0.0, 0.0],
165            [0.0, 0.0, 0.0, 1.0],
166            [-3.0, 3.0, 0.0, 0.0],
167            [0.0, 0.0, -3.0, 3.0],
168        ]);
169        m.invert();
170        let bezier = m * Mat4::from_row_arrays(hermite);
171        let bezier: Vec4<Vec4<f32>> =
172            Vec4::<[f32; 4]>::from(bezier.into_row_arrays()).map(Vec4::from);
173        let bezier = bezier.map(Vec3::from);
174        CubicBezier3::from(bezier)
175    }
176    for sim::NearestWaysData { bezier: bez, .. } in
177        sim.get_nearest_ways(chunk_center_wpos2d, &|chunk| Some(chunk.path))
178    {
179        if bez.length_by_discretization(16) < 0.125 {
180            continue;
181        }
182        let a = 0.0;
183        let b = 1.0;
184        for bez in bez.split((a + b) / 2.0) {
185            let p0 = g(bez.evaluate(a));
186            let p1 = g(bez.evaluate(a + (b - a) / 3.0));
187            let p2 = g(bez.evaluate(a + 2.0 * (b - a) / 3.0));
188            let p3 = g(bez.evaluate(b));
189            splines.push(hermite_to_bezier(p0, 3.0 * (p1 - p0), p3, 3.0 * (p3 - p2)));
190        }
191    }
192    for spline in splines.into_iter() {
193        canvas.chunk.meta_mut().add_track(spline);
194    }
195}
196
197pub fn apply_coral_to(canvas: &mut Canvas) {
198    let info = canvas.info();
199
200    if !info.chunk.river.near_water() {
201        return; // Don't bother with coral for a chunk nowhere near water
202    }
203
204    canvas.foreach_col(|canvas, wpos2d, col| {
205        const CORAL_DEPTH: Range<f32> = 14.0..32.0;
206        const CORAL_HEIGHT: f32 = 14.0;
207        const CORAL_DEPTH_FADEOUT: f32 = 5.0;
208        const CORAL_SCALE: f32 = 10.0;
209
210        let water_depth = col.water_level - col.alt;
211
212        if !CORAL_DEPTH.contains(&water_depth) {
213            return; // Avoid coral entirely for this column if we're outside coral depths
214        }
215
216        for z in col.alt.floor() as i32..(col.alt + CORAL_HEIGHT) as i32 {
217            let wpos = Vec3::new(wpos2d.x, wpos2d.y, z);
218
219            let coral_factor = Lerp::lerp(
220                1.0,
221                0.0,
222                // Fade coral out due to incorrect depth
223                ((water_depth.clamped(CORAL_DEPTH.start, CORAL_DEPTH.end) - water_depth).abs()
224                    / CORAL_DEPTH_FADEOUT)
225                    .min(1.0),
226            ) * Lerp::lerp(
227                1.0,
228                0.0,
229                // Fade coral out due to incorrect altitude above the seabed
230                ((z as f32 - col.alt) / CORAL_HEIGHT).powi(2),
231            ) * FastNoise::new(info.index.seed + 7)
232                .get(wpos.map(|e| e as f64) / 32.0)
233                .sub(0.2)
234                .mul(100.0)
235                .clamped(0.0, 1.0);
236
237            let nz = Vec3::iota().map(|e: u32| FastNoise::new(info.index.seed + e * 177));
238
239            let wpos_warped = wpos.map(|e| e as f32)
240                + nz.map(|nz| {
241                    nz.get(wpos.map(|e| e as f64) / CORAL_SCALE as f64) * CORAL_SCALE * 0.3
242                });
243
244            // let is_coral = FastNoise2d::new(info.index.seed + 17)
245            //     .get(wpos_warped.xy().map(|e| e as f64) / CORAL_SCALE)
246            //     .sub(1.0 - coral_factor)
247            //     .max(0.0)
248            //     .div(coral_factor) > 0.5;
249
250            let is_coral = [
251                FastNoise::new(info.index.seed),
252                FastNoise::new(info.index.seed + 177),
253            ]
254            .iter()
255            .all(|nz| {
256                nz.get(wpos_warped.map(|e| e as f64) / CORAL_SCALE as f64)
257                    .abs()
258                    < coral_factor * 0.3
259            });
260
261            if is_coral {
262                canvas.set(wpos, Block::new(BlockKind::Rock, Rgb::new(170, 220, 210)));
263            }
264        }
265    });
266}
267
268pub fn apply_caverns_to<R: Rng>(canvas: &mut Canvas, dynamic_rng: &mut R) {
269    let info = canvas.info();
270
271    let canvern_nz_at = |wpos2d: Vec2<i32>| {
272        // Horizontal average scale of caverns
273        let scale = 2048.0;
274        // How common should they be? (0.0 - 1.0)
275        let common = 0.15;
276
277        let cavern_nz = info
278            .index()
279            .noise
280            .cave_nz
281            .get((wpos2d.map(|e| e as f64) / scale).into_array()) as f32;
282        ((cavern_nz * 0.5 + 0.5 - (1.0 - common)).max(0.0) / common).powf(common * 2.0)
283    };
284
285    // Get cavern attributes at a position
286    let cavern_at = |wpos2d| {
287        let alt = info.land().get_alt_approx(wpos2d);
288
289        // Range of heights for the caverns
290        let height_range = 16.0..250.0;
291        // Minimum distance below the surface
292        let surface_clearance = 64.0;
293
294        let cavern_avg_height = Lerp::lerp(
295            height_range.start,
296            height_range.end,
297            info.index()
298                .noise
299                .cave_nz
300                .get((wpos2d.map(|e| e as f64) / 300.0).into_array()) as f32
301                * 0.5
302                + 0.5,
303        );
304
305        let cavern_avg_alt =
306            CONFIG.sea_level.min(alt * 0.25) - height_range.end - surface_clearance;
307
308        let cavern = canvern_nz_at(wpos2d);
309        let cavern_height = cavern * cavern_avg_height;
310
311        // Stalagtites
312        let stalactite = info
313            .index()
314            .noise
315            .cave_nz
316            .get(wpos2d.map(|e| e as f64 * 0.015).into_array())
317            .sub(0.5)
318            .max(0.0)
319            .mul((cavern_height as f64 - 5.0).mul(0.15).clamped(0.0, 1.0))
320            .mul(32.0 + cavern_avg_height as f64);
321
322        let hill = info
323            .index()
324            .noise
325            .cave_nz
326            .get((wpos2d.map(|e| e as f64) / 96.0).into_array()) as f32
327            * cavern
328            * 24.0;
329        let rugged = 0.4; // How bumpy should the floor be relative to the ceiling?
330        let cavern_bottom = (cavern_avg_alt - cavern_height * rugged + hill) as i32;
331        let cavern_avg_bottom =
332            (cavern_avg_alt - ((height_range.start + height_range.end) * 0.5) * rugged) as i32;
333        let cavern_top = (cavern_avg_alt + cavern_height) as i32;
334        let cavern_avg_top = (cavern_avg_alt + cavern_avg_height) as i32;
335
336        // Stalagmites rise up to meet stalactites
337        let stalagmite = stalactite;
338
339        let floor = stalagmite as i32;
340
341        (
342            cavern_bottom,
343            cavern_top,
344            cavern_avg_bottom,
345            cavern_avg_top,
346            floor,
347            stalactite,
348            cavern_avg_bottom + 16, // Water level
349        )
350    };
351
352    let mut mushroom_cache = HashMap::new();
353
354    struct Mushroom {
355        pos: Vec3<i32>,
356        stalk: f32,
357        head_color: Rgb<u8>,
358    }
359
360    // Get mushroom block, if any, at a position
361    let mut get_mushroom = |wpos: Vec3<i32>, dynamic_rng: &mut R| {
362        for (wpos2d, seed) in info.chunks().gen_ctx.structure_gen.get(wpos.xy()) {
363            let mushroom = if let Some(mushroom) =
364                mushroom_cache.entry(wpos2d).or_insert_with(|| {
365                    let mut rng = RandomPerm::new(seed);
366                    let (cavern_bottom, cavern_top, _, _, floor, _, water_level) =
367                        cavern_at(wpos2d);
368                    let pos = wpos2d.with_z(cavern_bottom + floor);
369                    if rng.gen_bool(0.15)
370                        && cavern_top - cavern_bottom > 32
371                        && pos.z > water_level - 2
372                    {
373                        Some(Mushroom {
374                            pos,
375                            stalk: 12.0 + rng.gen::<f32>().powf(2.0) * 35.0,
376                            head_color: Rgb::new(
377                                50,
378                                rng.gen_range(70..110),
379                                rng.gen_range(100..200),
380                            ),
381                        })
382                    } else {
383                        None
384                    }
385                }) {
386                mushroom
387            } else {
388                continue;
389            };
390
391            let wposf = wpos.map(|e| e as f64);
392            let warp_freq = 1.0 / 32.0;
393            let warp_amp = Vec3::new(12.0, 12.0, 12.0);
394            let wposf_warped = wposf.map(|e| e as f32)
395                + Vec3::new(
396                    FastNoise::new(seed).get(wposf * warp_freq),
397                    FastNoise::new(seed + 1).get(wposf * warp_freq),
398                    FastNoise::new(seed + 2).get(wposf * warp_freq),
399                ) * warp_amp
400                    * (wposf.z as f32 - mushroom.pos.z as f32)
401                        .mul(0.1)
402                        .clamped(0.0, 1.0);
403
404            let rpos = wposf_warped - mushroom.pos.map(|e| e as f32);
405
406            let stalk_radius = 2.5f32;
407            let head_radius = 18.0f32;
408            let head_height = 16.0;
409
410            let dist_sq = rpos.xy().magnitude_squared();
411            if dist_sq < head_radius.powi(2) {
412                let dist = dist_sq.sqrt();
413                let head_dist = ((rpos - Vec3::unit_z() * mushroom.stalk)
414                    / Vec2::broadcast(head_radius).with_z(head_height))
415                .magnitude();
416
417                let stalk = mushroom.stalk + Lerp::lerp(head_height * 0.5, 0.0, dist / head_radius);
418
419                // Head
420                if rpos.z > stalk
421                    && rpos.z <= mushroom.stalk + head_height
422                    && dist
423                        < head_radius * (1.0 - (rpos.z - mushroom.stalk) / head_height).powf(0.125)
424                {
425                    if head_dist < 0.85 {
426                        let radial = (rpos.x.atan2(rpos.y) * 10.0).sin() * 0.5 + 0.5;
427                        return Some(Block::new(
428                            BlockKind::GlowingMushroom,
429                            Rgb::new(30, 50 + (radial * 100.0) as u8, 100 - (radial * 50.0) as u8),
430                        ));
431                    } else if head_dist < 1.0 {
432                        return Some(Block::new(BlockKind::Wood, mushroom.head_color));
433                    }
434                }
435
436                if rpos.z <= mushroom.stalk + head_height - 1.0
437                    && dist_sq
438                        < (stalk_radius * Lerp::lerp(1.5, 0.75, rpos.z / mushroom.stalk)).powi(2)
439                {
440                    // Stalk
441                    return Some(Block::new(BlockKind::Wood, Rgb::new(25, 60, 90)));
442                } else if ((mushroom.stalk - 0.1)..(mushroom.stalk + 0.9)).contains(&rpos.z) // Hanging orbs
443                    && dist > head_radius * 0.85
444                    && dynamic_rng.gen_bool(0.1)
445                {
446                    use SpriteKind::*;
447                    let sprites = if dynamic_rng.gen_bool(0.1) {
448                        &[Beehive, Lantern] as &[_]
449                    } else {
450                        &[Orb, MycelBlue, MycelBlue] as &[_]
451                    };
452                    return Some(Block::air(*sprites.choose(dynamic_rng).unwrap()));
453                }
454            }
455        }
456
457        None
458    };
459
460    canvas.foreach_col(|canvas, wpos2d, _col| {
461        if canvern_nz_at(wpos2d) <= 0.0 {
462            return;
463        }
464
465        let (
466            cavern_bottom,
467            cavern_top,
468            cavern_avg_bottom,
469            cavern_avg_top,
470            floor,
471            stalactite,
472            water_level,
473        ) = cavern_at(wpos2d);
474
475        let mini_stalactite = info
476            .index()
477            .noise
478            .cave_nz
479            .get(wpos2d.map(|e| e as f64 * 0.08).into_array())
480            .sub(0.5)
481            .max(0.0)
482            .mul(
483                ((cavern_top - cavern_bottom) as f64 - 5.0)
484                    .mul(0.15)
485                    .clamped(0.0, 1.0),
486            )
487            .mul(24.0 + (cavern_avg_top - cavern_avg_bottom) as f64 * 0.2);
488        let stalactite_height = (stalactite + mini_stalactite) as i32;
489
490        let moss_common = 1.5;
491        let moss = info
492            .index()
493            .noise
494            .cave_nz
495            .get(wpos2d.map(|e| e as f64 * 0.035).into_array())
496            .sub(1.0 - moss_common)
497            .max(0.0)
498            .mul(1.0 / moss_common)
499            .powf(8.0 * moss_common)
500            .mul(
501                ((cavern_top - cavern_bottom) as f64)
502                    .mul(0.15)
503                    .clamped(0.0, 1.0),
504            )
505            .mul(16.0 + (cavern_avg_top - cavern_avg_bottom) as f64 * 0.35);
506
507        let plant_factor = info
508            .index()
509            .noise
510            .cave_nz
511            .get(wpos2d.map(|e| e as f64 * 0.015).into_array())
512            .add(1.0)
513            .mul(0.5)
514            .powf(2.0);
515
516        let is_vine = |wpos: Vec3<f32>, dynamic_rng: &mut R| {
517            let wpos = wpos + wpos.xy().yx().with_z(0.0) * 0.2; // A little twist
518            let dims = Vec2::new(7.0, 256.0); // Long and thin
519            let vine_posf = (wpos + Vec2::new(0.0, (wpos.x / dims.x).floor() * 733.0)) / dims; // ~Random offset
520            let vine_pos = vine_posf.map(|e| e.floor() as i32);
521            let mut rng = RandomPerm::new(((vine_pos.x << 16) | vine_pos.y) as u32); // Rng for vine attributes
522            if rng.gen_bool(0.2) {
523                let vine_height = (cavern_avg_top - cavern_avg_bottom).max(64) as f32;
524                let vine_base = cavern_avg_bottom as f32 + rng.gen_range(48.0..vine_height);
525                let vine_y = (vine_posf.y.fract() - 0.5).abs() * 2.0 * dims.y;
526                let vine_reach = (vine_y * 0.05).powf(2.0).min(1024.0);
527                let vine_z = vine_base + vine_reach;
528                if Vec2::new(vine_posf.x.fract() * 2.0 - 1.0, (wpos.z - vine_z) / 5.0)
529                    .magnitude_squared()
530                    < 1.0f32
531                {
532                    let kind = if dynamic_rng.gen_bool(0.025) {
533                        BlockKind::GlowingRock
534                    } else {
535                        BlockKind::Leaves
536                    };
537                    Some(Block::new(
538                        kind,
539                        Rgb::new(
540                            85,
541                            (vine_y + vine_reach).mul(0.05).sin().mul(35.0).add(85.0) as u8,
542                            20,
543                        ),
544                    ))
545                } else {
546                    None
547                }
548            } else {
549                None
550            }
551        };
552
553        let mut last_kind = BlockKind::Rock;
554        for z in cavern_bottom - 1..cavern_top {
555            use SpriteKind::*;
556
557            let wpos = wpos2d.with_z(z);
558            let wposf = wpos.map(|e| e as f32);
559
560            let block = if z < cavern_bottom {
561                if z > water_level + dynamic_rng.gen_range(4..16) {
562                    Block::new(BlockKind::Grass, Rgb::new(10, 75, 90))
563                } else {
564                    Block::new(BlockKind::Rock, Rgb::new(50, 40, 10))
565                }
566            } else if z < cavern_bottom + floor {
567                Block::new(BlockKind::WeakRock, Rgb::new(110, 120, 150))
568            } else if z > cavern_top - stalactite_height {
569                if dynamic_rng.gen_bool(0.0035) {
570                    // Glowing rock in stalactites
571                    Block::new(BlockKind::GlowingRock, Rgb::new(30, 150, 120))
572                } else {
573                    Block::new(BlockKind::WeakRock, Rgb::new(110, 120, 150))
574                }
575            } else if let Some(mushroom_block) = get_mushroom(wpos, dynamic_rng) {
576                mushroom_block
577            } else if z > cavern_top - moss as i32 {
578                let kind = if dynamic_rng
579                    .gen_bool(0.05 / (1.0 + ((cavern_top - z).max(0) as f64).mul(0.1)))
580                {
581                    BlockKind::GlowingMushroom
582                } else {
583                    BlockKind::Leaves
584                };
585                Block::new(kind, Rgb::new(50, 120, 160))
586            } else if z < water_level {
587                Block::water(Empty).with_sprite(
588                    if z == cavern_bottom + floor && dynamic_rng.gen_bool(0.01) {
589                        *[Seagrass, SeaGrapes, SeaweedTemperate, StonyCoral]
590                            .choose(dynamic_rng)
591                            .unwrap()
592                    } else {
593                        Empty
594                    },
595                )
596            } else if z == water_level
597                && dynamic_rng.gen_bool(Lerp::lerp(0.0, 0.05, plant_factor))
598                && last_kind == BlockKind::Water
599            {
600                Block::air(CavernLillypadBlue)
601            } else if z == cavern_bottom + floor
602                && dynamic_rng.gen_bool(Lerp::lerp(0.0, 0.5, plant_factor))
603                && last_kind == BlockKind::Grass
604            {
605                Block::air(
606                    *if dynamic_rng.gen_bool(0.9) {
607                        // High density
608                        &[GrassBlueShort, GrassBlueMedium, GrassBlueLong] as &[_]
609                    } else if dynamic_rng.gen_bool(0.5) {
610                        // Medium density
611                        &[CaveMushroom] as &[_]
612                    } else {
613                        // Low density
614                        &[LeafyPlant, Fern, Pyrebloom, Moonbell, Welwitch, GrassBlue] as &[_]
615                    }
616                    .choose(dynamic_rng)
617                    .unwrap(),
618                )
619            } else if z == cavern_top - 1 && dynamic_rng.gen_bool(0.001) {
620                Block::air(
621                    *[CrystalHigh, CeilingMushroom, Orb, MycelBlue]
622                        .choose(dynamic_rng)
623                        .unwrap(),
624                )
625            } else if let Some(vine) = is_vine(wposf, dynamic_rng)
626                .or_else(|| is_vine(wposf.xy().yx().with_z(wposf.z), dynamic_rng))
627            {
628                vine
629            } else {
630                Block::empty()
631            };
632
633            last_kind = block.kind();
634
635            let block = if block.is_filled() {
636                Block::new(
637                    block.kind(),
638                    block.get_color().unwrap_or_default().map(|e| {
639                        (e as f32 * dynamic_rng.gen_range(0.95..1.05)).clamped(0.0, 255.0) as u8
640                    }),
641                )
642            } else {
643                block
644            };
645
646            canvas.set(wpos, block);
647        }
648    });
649}