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, ObjAsset},
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
23// For culling
24const MAX_OBJECT_RADIUS: i32 = 64;
25
26bitflags::bitflags! {
27    #[derive(Debug, Clone, Copy)]
28    pub struct VertexFlags: u8 {
29        /// Use instance not vertex colour
30        const INST_COLOR    = 0b00000001;
31        /// Glow!
32        const GLOW          = 0b00000010;
33    }
34}
35
36struct ObjectGroup {
37    instances: Instances<LodObjectInstance>,
38    // None implies no instances
39    z_range: Option<Range<i32>>,
40    frustum_last_plane_index: u8,
41    visible: bool,
42}
43
44pub struct Lod {
45    model: Option<(u32, Model<LodTerrainVertex>)>,
46    data: LodData,
47
48    zone_objects: HashMap<Vec2<i32>, HashMap<lod::ObjectKind, ObjectGroup>>,
49    object_data: HashMap<lod::ObjectKind, Model<LodObjectVertex>>,
50}
51
52// TODO: Make constant when possible.
53pub fn water_color() -> Rgba<f32> {
54    /* Rgba::new(0.2, 0.5, 1.0, 0.0) */
55    srgba_to_linear(Rgba::new(0.0, 0.25, 0.5, 0.0))
56}
57
58impl Lod {
59    pub fn new(renderer: &mut Renderer, client: &Client, settings: &Settings) -> Self {
60        let data = LodData::new(
61            renderer,
62            client.world_data().chunk_size().as_(),
63            client.world_data().lod_base.raw(),
64            client.world_data().lod_alt.raw(),
65            client.world_data().lod_horizon.raw(),
66            (client.world_data().chunk_size().as_() / weather::CHUNKS_PER_CELL).map(|e| e.max(1)),
67            settings.graphics.lod_detail.clamp(100, 2500),
68            /* TODO: figure out how we want to do this without color borders?
69             * water_color().into_array().into(), */
70        );
71        Self {
72            model: None,
73            data,
74            zone_objects: HashMap::new(),
75            object_data: [
76                (
77                    lod::ObjectKind::GenericTree,
78                    make_lod_object("oak", renderer),
79                ),
80                (lod::ObjectKind::Pine, make_lod_object("pine", renderer)),
81                (lod::ObjectKind::Dead, make_lod_object("dead", renderer)),
82                (lod::ObjectKind::House, make_lod_object("house", renderer)),
83                (
84                    lod::ObjectKind::GiantTree,
85                    make_lod_object("giant_tree", renderer),
86                ),
87                (
88                    lod::ObjectKind::Mangrove,
89                    make_lod_object("mangrove", renderer),
90                ),
91                (lod::ObjectKind::Acacia, make_lod_object("acacia", renderer)),
92                (lod::ObjectKind::Birch, make_lod_object("birch", renderer)),
93                (
94                    lod::ObjectKind::Redwood,
95                    make_lod_object("redwood", renderer),
96                ),
97                (lod::ObjectKind::Baobab, make_lod_object("baobab", renderer)),
98                (
99                    lod::ObjectKind::Frostpine,
100                    make_lod_object("frostpine", renderer),
101                ),
102                (lod::ObjectKind::Haniwa, make_lod_object("haniwa", renderer)),
103                (
104                    lod::ObjectKind::Desert,
105                    make_lod_object("desert_houses", renderer),
106                ),
107                (lod::ObjectKind::Palm, make_lod_object("palm", renderer)),
108                (lod::ObjectKind::Arena, make_lod_object("arena", renderer)),
109                (
110                    lod::ObjectKind::SavannahHut,
111                    make_lod_object("savannah_hut", renderer),
112                ),
113                (
114                    lod::ObjectKind::SavannahAirshipDock,
115                    make_lod_object("savannah_airship_dock", renderer),
116                ),
117                (
118                    lod::ObjectKind::TerracottaPalace,
119                    make_lod_object("terracotta_palace", renderer),
120                ),
121                (
122                    lod::ObjectKind::TerracottaHouse,
123                    make_lod_object("terracotta_house", renderer),
124                ),
125                (
126                    lod::ObjectKind::TerracottaYard,
127                    make_lod_object("terracotta_yard", renderer),
128                ),
129                (
130                    lod::ObjectKind::AirshipDock,
131                    make_lod_object("airship_dock", renderer),
132                ),
133                (
134                    lod::ObjectKind::CoastalHouse,
135                    make_lod_object("coastal_house", renderer),
136                ),
137                (
138                    lod::ObjectKind::CoastalWorkshop,
139                    make_lod_object("coastal_workshop", renderer),
140                ),
141            ]
142            .into(),
143        }
144    }
145
146    pub fn get_data(&self) -> &LodData { &self.data }
147
148    pub fn set_detail(&mut self, detail: u32) {
149        // Make sure the recorded detail is even.
150        self.data.tgt_detail = (detail - detail % 2).clamp(100, 2500);
151    }
152
153    pub fn maintain(
154        &mut self,
155        renderer: &mut Renderer,
156        client: &Client,
157        focus_pos: Vec3<f32>,
158        camera: &Camera,
159    ) {
160        // Update LoD terrain mesh according to detail
161        if self
162            .model
163            .as_ref()
164            .map(|(detail, _)| *detail != self.data.tgt_detail)
165            .unwrap_or(true)
166        {
167            self.model = Some((
168                self.data.tgt_detail,
169                renderer
170                    .create_model(&create_lod_terrain_mesh(self.data.tgt_detail))
171                    .unwrap(),
172            ));
173        }
174
175        // Create new LoD groups when a new zone has loaded
176        for (p, zone) in client.lod_zones() {
177            self.zone_objects.entry(*p).or_insert_with(|| {
178                let mut objects = HashMap::<_, Vec<_>>::new();
179                let mut z_range = None;
180                for object in zone.objects.iter() {
181                    let pos = p.map(|e| lod::to_wpos(e) as f32).with_z(0.0)
182                        + object.pos.map(|e| e as f32)
183                        + Vec2::broadcast(0.5).with_z(0.0);
184                    z_range = Some(z_range.map_or(
185                        pos.z as i32..pos.z as i32,
186                        |z_range: Range<i32>| {
187                            z_range.start.min(pos.z as i32)..z_range.end.max(pos.z as i32)
188                        },
189                    ));
190                    objects
191                        .entry(object.kind)
192                        .or_default()
193                        .push(LodObjectInstance::new(pos, object.color, object.flags));
194                }
195                objects
196                    .into_iter()
197                    .map(|(kind, instances)| {
198                        (kind, ObjectGroup {
199                            instances: renderer.create_instances(&instances),
200                            z_range: z_range.clone(),
201                            frustum_last_plane_index: 0,
202                            visible: false,
203                        })
204                    })
205                    .collect()
206            });
207        }
208
209        // Remove zones that are unloaded
210        self.zone_objects
211            .retain(|p, _| client.lod_zones().contains_key(p));
212
213        // Determine visibility of zones based on view frustum
214        let camera::Dependents {
215            view_mat,
216            proj_mat_treeculler,
217            ..
218        } = camera.dependents();
219        let focus_off = focus_pos.map(|e| e.trunc());
220        let frustum = Frustum::from_modelview_projection(
221            (proj_mat_treeculler * view_mat * Mat4::translation_3d(-focus_off)).into_col_arrays(),
222        );
223        for (pos, groups) in &mut self.zone_objects {
224            for group in groups.values_mut() {
225                if let Some(z_range) = &group.z_range {
226                    let group_min = (pos.map(lod::to_wpos).with_z(z_range.start)
227                        - MAX_OBJECT_RADIUS)
228                        .map(|e| e as f32);
229                    let group_max = ((pos + 1).map(lod::to_wpos).with_z(z_range.end)
230                        + MAX_OBJECT_RADIUS)
231                        .map(|e| e as f32);
232                    let (in_frustum, last_plane_index) =
233                        AABB::new(group_min.into_array(), group_max.into_array())
234                            .coherent_test_against_frustum(
235                                &frustum,
236                                group.frustum_last_plane_index,
237                            );
238                    group.visible = in_frustum;
239                    group.frustum_last_plane_index = last_plane_index;
240                }
241            }
242        }
243        // Update weather texture
244        // NOTE: consider moving the lerping to a shader if the overhead of uploading to
245        // the gpu each frame becomes an issue.
246        let weather = client.state().weather_grid();
247        let size = weather.size().as_::<u32>();
248        renderer.update_texture(
249            &self.data.weather,
250            [0, 0],
251            [size.x, size.y],
252            &weather
253                .iter()
254                .map(|(_, w)| [(w.cloud * 255.0) as u8, (w.rain * 255.0) as u8, 0, 0])
255                .collect::<Vec<_>>(),
256        );
257    }
258
259    pub fn render<'a>(&'a self, drawer: &mut FirstPassDrawer<'a>, culling_mode: CullingMode) {
260        if let Some((_, model)) = self.model.as_ref() {
261            drawer.draw_lod_terrain(model);
262        }
263
264        if !matches!(culling_mode, CullingMode::Underground) {
265            // Draw LoD objects
266            let mut drawer = drawer.draw_lod_objects();
267            for groups in self.zone_objects.values() {
268                for (kind, group) in groups.iter().filter(|(_, g)| g.visible) {
269                    if let Some(model) = self.object_data.get(kind) {
270                        drawer.draw(model, &group.instances);
271                    }
272                }
273            }
274        }
275    }
276}
277
278fn create_lod_terrain_mesh(detail: u32) -> Mesh<LodTerrainVertex> {
279    // detail is even, so we choose odd detail (detail + 1) to create two even
280    // halves with an empty hole.
281    let detail = detail + 1;
282    Spiral2d::new()
283        .take((detail * detail) as usize)
284        .skip(1)
285        .map(|pos| {
286            let x = pos.x + detail as i32 / 2;
287            let y = pos.y + detail as i32 / 2;
288
289            let transform = |x| (2.0 * x as f32) / detail as f32 - 1.0;
290
291            Quad::new(
292                Vertex::new(Vec2::new(x, y).map(transform)),
293                Vertex::new(Vec2::new(x + 1, y).map(transform)),
294                Vertex::new(Vec2::new(x + 1, y + 1).map(transform)),
295                Vertex::new(Vec2::new(x, y + 1).map(transform)),
296            )
297            .rotated_by(usize::from(
298                !((x > detail as i32 / 2) ^ (y > detail as i32 / 2)),
299            ))
300        })
301        .collect()
302}
303
304fn make_lod_object(name: &str, renderer: &mut Renderer) -> Model<LodObjectVertex> {
305    let model = ObjAsset::load_expect(&format!("voxygen.lod.{}", name));
306    let mesh = model
307        .read()
308        .0
309        .objects()
310        .flat_map(|(objname, obj)| {
311            let mut color = objname.split('_').filter_map(|x| x.parse::<u8>().ok());
312            let color = color
313                .next()
314                .and_then(|r| Some(Rgb::new(r, color.next()?, color.next()?)))
315                .unwrap_or(Rgb::broadcast(127));
316            let color = srgb_to_linear(color.map(|c| (c as f32 / 255.0)));
317            let flags = match objname {
318                "InstCol" => VertexFlags::INST_COLOR,
319                "Glow" => VertexFlags::GLOW,
320                _ => VertexFlags::empty(),
321            };
322            obj.triangles().map(move |vs| {
323                let [a, b, c] = vs.map(|v| {
324                    LodObjectVertex::new(
325                        v.position().into(),
326                        v.normal().unwrap_or([0.0, 0.0, 1.0]).into(),
327                        color,
328                        flags,
329                    )
330                });
331                Tri::new(a, b, c)
332            })
333        })
334        .collect();
335    renderer.create_model(&mesh).expect("Mesh was empty!")
336}