Skip to main content

veloren_voxygen/scene/
lod.rs

1use crate::{
2    render::{
3        CullingMode, FirstPassDrawer, Instances, LodObjectInstance, LodObjectVertex,
4        LodTerrainVertex, Mesh, Model, Quad, Renderer, Tri,
5        pipelines::lod_terrain::{LodData, Vertex},
6    },
7    scene::{Camera, camera},
8    settings::Settings,
9};
10use client::Client;
11use common::{
12    assets::{AssetExt, Obj},
13    lod,
14    spiral::Spiral2d,
15    util::{srgb_to_linear, srgba_to_linear},
16    weather,
17};
18use hashbrown::HashMap;
19use std::ops::Range;
20use treeculler::{AABB, BVol, Frustum};
21use vek::*;
22
23bitflags::bitflags! {
24    #[derive(Debug, Clone, Copy)]
25    pub struct VertexFlags: u8 {
26        /// Use instance not vertex colour
27        const INST_COLOR    = 0b00000001;
28        /// Glow!
29        const GLOW          = 0b00000010;
30    }
31}
32
33struct ObjectGroup {
34    instances: Instances<LodObjectInstance>,
35    bounds: Option<Aabb<f32>>,
36    frustum_last_plane_index: u8,
37    visible: bool,
38}
39
40pub struct Lod {
41    model: Option<(u32, Model<LodTerrainVertex>)>,
42    data: LodData,
43
44    zone_objects: HashMap<Vec2<i32>, HashMap<lod::ObjectKind, ObjectGroup>>,
45    // (model, radius, height)
46    object_data: HashMap<lod::ObjectKind, (Model<LodObjectVertex>, f32, Range<f32>)>,
47}
48
49// TODO: Make constant when possible.
50pub fn water_color() -> Rgba<f32> {
51    /* Rgba::new(0.2, 0.5, 1.0, 0.0) */
52    srgba_to_linear(Rgba::new(0.0, 0.25, 0.5, 0.0))
53}
54
55impl Lod {
56    pub fn new(renderer: &mut Renderer, client: &Client, settings: &Settings) -> Self {
57        let data = LodData::new(
58            renderer,
59            client.world_data().chunk_size().as_(),
60            client.world_data().lod_base.raw(),
61            client.world_data().lod_alt.raw(),
62            client.world_data().lod_horizon.raw(),
63            (client.world_data().chunk_size().as_() / weather::CHUNKS_PER_CELL).map(|e| e.max(1)),
64            settings.graphics.lod_detail.clamp(100, 2500),
65            /* TODO: figure out how we want to do this without color borders?
66             * water_color().into_array().into(), */
67        );
68        Self {
69            model: None,
70            data,
71            zone_objects: HashMap::new(),
72            object_data: [
73                (
74                    lod::ObjectKind::GenericTree,
75                    make_lod_object("oak", renderer),
76                ),
77                (lod::ObjectKind::Pine, make_lod_object("pine", renderer)),
78                (lod::ObjectKind::Dead, make_lod_object("dead", renderer)),
79                (lod::ObjectKind::House, make_lod_object("house", renderer)),
80                (
81                    lod::ObjectKind::GiantTree,
82                    make_lod_object("giant_tree", renderer),
83                ),
84                (
85                    lod::ObjectKind::Mangrove,
86                    make_lod_object("mangrove", renderer),
87                ),
88                (lod::ObjectKind::Acacia, make_lod_object("acacia", renderer)),
89                (lod::ObjectKind::Birch, make_lod_object("birch", renderer)),
90                (
91                    lod::ObjectKind::Redwood,
92                    make_lod_object("redwood", renderer),
93                ),
94                (lod::ObjectKind::Baobab, make_lod_object("baobab", renderer)),
95                (
96                    lod::ObjectKind::Frostpine,
97                    make_lod_object("frostpine", renderer),
98                ),
99                (lod::ObjectKind::Haniwa, make_lod_object("haniwa", renderer)),
100                (
101                    lod::ObjectKind::Desert,
102                    make_lod_object("desert_houses", renderer),
103                ),
104                (lod::ObjectKind::Palm, make_lod_object("palm", renderer)),
105                (lod::ObjectKind::Arena, make_lod_object("arena", renderer)),
106                (
107                    lod::ObjectKind::SavannahHut,
108                    make_lod_object("savannah_hut", renderer),
109                ),
110                (
111                    lod::ObjectKind::SavannahAirshipDock,
112                    make_lod_object("savannah_airship_dock", renderer),
113                ),
114                (
115                    lod::ObjectKind::TerracottaPalace,
116                    make_lod_object("terracotta_palace", renderer),
117                ),
118                (
119                    lod::ObjectKind::TerracottaHouse,
120                    make_lod_object("terracotta_house", renderer),
121                ),
122                (
123                    lod::ObjectKind::TerracottaYard,
124                    make_lod_object("terracotta_yard", renderer),
125                ),
126                (
127                    lod::ObjectKind::AirshipDock,
128                    make_lod_object("airship_dock", renderer),
129                ),
130                (
131                    lod::ObjectKind::CoastalHouse,
132                    make_lod_object("coastal_house", renderer),
133                ),
134                (
135                    lod::ObjectKind::CoastalWorkshop,
136                    make_lod_object("coastal_workshop", renderer),
137                ),
138                (
139                    lod::ObjectKind::CoastalAirshipDock,
140                    make_lod_object("coastal_airship_dock", renderer),
141                ),
142                (
143                    lod::ObjectKind::DesertCityAirshipDock,
144                    make_lod_object("desert_city_airship_dock", renderer),
145                ),
146                (
147                    lod::ObjectKind::CliffTownAirshipDock,
148                    make_lod_object("cliff_town_airship_dock", renderer),
149                ),
150            ]
151            .into(),
152        }
153    }
154
155    pub fn get_data(&self) -> &LodData { &self.data }
156
157    pub fn set_detail(&mut self, detail: u32) {
158        // Make sure the recorded detail is even.
159        self.data.tgt_detail = (detail - detail % 2).clamp(100, 2500);
160    }
161
162    pub fn maintain(
163        &mut self,
164        renderer: &mut Renderer,
165        client: &Client,
166        focus_pos: Vec3<f32>,
167        camera: &Camera,
168    ) {
169        // Update LoD terrain mesh according to detail
170        if self
171            .model
172            .as_ref()
173            .map(|(detail, _)| *detail != self.data.tgt_detail)
174            .unwrap_or(true)
175        {
176            self.model = Some((
177                self.data.tgt_detail,
178                renderer
179                    .create_model(&create_lod_terrain_mesh(self.data.tgt_detail))
180                    .unwrap(),
181            ));
182        }
183
184        // Create new LoD groups when a new zone has loaded
185        for (p, zone) in client.lod_zones() {
186            self.zone_objects.entry(*p).or_insert_with(|| {
187                let mut objects = HashMap::<_, Vec<_>>::new();
188                let mut bounds = None;
189                for object in zone.objects.iter() {
190                    let Some((_, radius, z_range)) = self.object_data.get(&object.kind) else {
191                        continue;
192                    };
193                    let pos = p.map(|e| lod::to_wpos(e) as f32).with_z(0.0)
194                        + object.pos.map(|e| e as f32)
195                        + Vec2::broadcast(0.5).with_z(0.0);
196                    let rad = Vec2::broadcast(*radius);
197                    let obj_bounds = Aabb {
198                        min: pos + (-rad).with_z(z_range.start),
199                        max: pos + rad.with_z(z_range.end),
200                    };
201                    bounds = Some(bounds.map_or(obj_bounds, |b: Aabb<f32>| b.union(obj_bounds)));
202                    objects
203                        .entry(object.kind)
204                        .or_default()
205                        .push(LodObjectInstance::new(pos, object.color, object.flags));
206                }
207                objects
208                    .into_iter()
209                    .map(|(kind, instances)| {
210                        (kind, ObjectGroup {
211                            instances: renderer.create_instances(&instances),
212                            bounds,
213                            frustum_last_plane_index: 0,
214                            visible: false,
215                        })
216                    })
217                    .collect()
218            });
219        }
220
221        // Remove zones that are unloaded
222        self.zone_objects
223            .retain(|p, _| client.lod_zones().contains_key(p));
224
225        // Determine visibility of zones based on view frustum
226        let camera::Dependents {
227            view_mat,
228            proj_mat_treeculler,
229            ..
230        } = camera.dependents();
231        let focus_off = focus_pos.map(|e| e.trunc());
232        let frustum = Frustum::from_modelview_projection(
233            (proj_mat_treeculler * view_mat * Mat4::translation_3d(-focus_off)).into_col_arrays(),
234        );
235        for groups in &mut self.zone_objects.values_mut() {
236            for group in groups.values_mut() {
237                if let Some(bounds) = &group.bounds {
238                    let (in_frustum, last_plane_index) =
239                        AABB::new(bounds.min.into_array(), bounds.max.into_array())
240                            .coherent_test_against_frustum(
241                                &frustum,
242                                group.frustum_last_plane_index,
243                            );
244                    group.visible = in_frustum;
245                    group.frustum_last_plane_index = last_plane_index;
246                }
247            }
248        }
249        // Update weather texture
250        // NOTE: consider moving the lerping to a shader if the overhead of uploading to
251        // the gpu each frame becomes an issue.
252        let weather = client.state().weather_grid();
253        let size = weather.size().as_::<u32>();
254        renderer.update_texture(
255            &self.data.weather,
256            [0, 0],
257            [size.x, size.y],
258            &weather
259                .iter()
260                .map(|(_, w)| [(w.cloud * 255.0) as u8, (w.rain * 255.0) as u8, 0, 0])
261                .collect::<Vec<_>>(),
262        );
263    }
264
265    pub fn render<'a>(&'a self, drawer: &mut FirstPassDrawer<'a>, culling_mode: CullingMode) {
266        if let Some((_, model)) = self.model.as_ref() {
267            drawer.draw_lod_terrain(model);
268        }
269
270        if !matches!(culling_mode, CullingMode::Underground) {
271            // Draw LoD objects
272            let mut drawer = drawer.draw_lod_objects();
273            for groups in self.zone_objects.values() {
274                for (kind, group) in groups.iter().filter(|(_, g)| g.visible) {
275                    if let Some((model, _, _)) = self.object_data.get(kind) {
276                        drawer.draw(model, &group.instances);
277                    }
278                }
279            }
280        }
281    }
282}
283
284fn create_lod_terrain_mesh(detail: u32) -> Mesh<LodTerrainVertex> {
285    // detail is even, so we choose odd detail (detail + 1) to create two even
286    // halves with an empty hole.
287    let detail = detail + 1;
288    Spiral2d::new()
289        .take((detail * detail) as usize)
290        .skip(1)
291        .map(|pos| {
292            let x = pos.x + detail as i32 / 2;
293            let y = pos.y + detail as i32 / 2;
294
295            let transform = |x| (2.0 * x as f32) / detail as f32 - 1.0;
296
297            Quad::new(
298                Vertex::new(Vec2::new(x, y).map(transform)),
299                Vertex::new(Vec2::new(x + 1, y).map(transform)),
300                Vertex::new(Vec2::new(x + 1, y + 1).map(transform)),
301                Vertex::new(Vec2::new(x, y + 1).map(transform)),
302            )
303            .rotated_by(usize::from(
304                !((x > detail as i32 / 2) ^ (y > detail as i32 / 2)),
305            ))
306        })
307        .collect()
308}
309
310/// Create LoD objects from an .obj asset file.
311///
312/// Returns the model, the object radius, and the object height.
313///
314/// ### Object Colors
315///
316/// In the .obj LoD files, objects can have a specific color by setting the name
317/// of the object to the format "r_g_b" where the values are u8 integers. E.g.
318/// the line "o 1_0_0" would set the color of that object to red. Note that the
319/// leading 'o' is part of the Wavefront .obj file format (o means 'object').
320/// The object name follows the 'o'.
321///
322/// If the name of the object is 'InstCol', that means to use the instance
323/// color, i.e., the color from the worldgen lod::Object. The get_lod_zone()
324/// function creates lod::Object instances with a color field (usually black)
325/// that is the 'instance' color.  For example, City plots have houses
326/// with varying roof colors, and the house lod::Object that is the roof uses
327/// the instance color because it's name is set to InstCol.
328///
329/// These two choices, setting a specific color for an object by using the
330/// "r_g_b" format, or setting the name to 'InstCol' and using the instance
331/// color are mutually exclusive.
332///
333/// ### Glow
334///
335/// A glow effect can also be applied to the LoD object by using the "Glow" flag
336/// in the object name. If the name of an object includes 'Glow', the object
337/// will have a glowing effect. For example, the name 240_0_0_Glow would set the
338/// color to red and apply a glow effect. 'Glow' by itself means to apply a glow
339/// effect to the object using the default color seen below (127, 127, 127). The
340/// lod-object-frag shader applies a glow color that is tinted by the fragment
341/// surface color.
342///
343/// > **Note:** The keyword Glow is case-sensitive.
344///
345/// ### Backwards Compatibility
346///
347/// Previous versions of this function expected the object name to either be
348/// InstCol, Glow, or a color format. So, Glow, InstCol, and 255_0_0 were valid
349/// object names. This scheme continues to work, with the added functionality of
350/// being able to specify a color and the glow effect together.
351///
352/// ### Examples (without the leading "o"):
353///
354/// - `Glow` Apply a glow effect with the default glow color (light orange).
355/// - `255_0_0_Glow` The object will be a glowing red.
356/// - `255_255_0` The object will be yellow.
357/// - `This is an object` The object color will be the default light gray.
358/// - `The cone on top of the spire, uses Glow` The object will glow with a
359///   light orange color.
360/// - `InstCol` The object will use the instance color that is set in the
361///   worldgen lod::Object.
362/// - `InstCol Glow` The object will use the instance color. Glow is ignored.
363fn make_lod_object(
364    name: &str,
365    renderer: &mut Renderer,
366) -> (Model<LodObjectVertex>, f32, Range<f32>) {
367    let model = Obj::load_expect(&format!("voxygen.lod.{}", name));
368    let mut radius = 0.0f32;
369    let mut z_range = None;
370    let mut mesh = Mesh::new();
371    for (objname, obj) in model.read().0.objects() {
372        let mut color = objname.split('_').filter_map(|x| x.parse::<u8>().ok());
373        let color = color
374            .next()
375            .and_then(|r| Some(Rgb::new(r, color.next()?, color.next()?)))
376            .unwrap_or(Rgb::broadcast(127));
377        let color = srgb_to_linear(color.map(|c| c as f32 / 255.0));
378        let flags = if objname.contains("InstCol") && objname.contains("Glow") {
379            VertexFlags::INST_COLOR | VertexFlags::GLOW
380        } else if objname.contains("Glow") {
381            VertexFlags::GLOW
382        } else if objname.contains("InstCol") {
383            VertexFlags::INST_COLOR
384        } else {
385            VertexFlags::empty()
386        };
387
388        for vs in obj.triangles() {
389            for v in vs {
390                radius = radius.max(Vec3::from(v.position()).xy().magnitude());
391                let z = v.position()[2];
392                let z_range = z_range.get_or_insert(z..z);
393                z_range.start = z_range.start.min(z);
394                z_range.end = z_range.end.max(z);
395            }
396            let [a, b, c] = vs.map(|v| {
397                LodObjectVertex::new(
398                    v.position().into(),
399                    v.normal().unwrap_or([0.0, 0.0, 1.0]).into(),
400                    color,
401                    flags,
402                )
403            });
404            mesh.push_tri(Tri::new(a, b, c));
405        }
406    }
407    (
408        renderer.create_model(&mesh).expect("Mesh was empty!"),
409        radius,
410        z_range.expect("no vertices"),
411    )
412}