1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
use std::{
    collections::HashMap,
};

use dotrix_core::{ Color, Id, Pipeline, Camera, Globals, World };
use dotrix_core::assets::{ Assets, Mesh, Shader };
use dotrix_core::ecs::{ Entity, Mut, Const, Context };
use dotrix_core::camera::ProjView;
use dotrix_core::renderer::{
    BindGroup,
    Binding,
    PipelineLayout,
    PipelineOptions,
    Renderer,
    Sampler,
    Stage,
};

use dotrix_pbr::{ Material, Lights };

use crate::{ Terrain, Tile, Layers };

const PIPELINE_LABEL: &str = "dotrix::terrain";

/// Terrain spawn system context
#[derive(Default)]
pub struct Spawner {
    tiles: HashMap<TileIndex, TileState>,
    last_viewer_position: Option<[f32; 2]>,
    to_exile: Vec<(Entity, Id<Mesh>)>,
}

#[derive(Default)]
struct TileState {
    lod: usize,
    visible: bool,
    spawned: bool,
}

#[derive(Eq, PartialEq, Hash, Copy, Clone)]
struct TileIndex {
    x: i32,
    z: i32,
}

struct Viewer {
    position: [f32; 2],
    view_distance_sq: f32,
}

/// Terrain Startup System
pub fn startup(
    mut assets: Mut<Assets>,
    mut globals: Mut<Globals>,
    renderer: Const<Renderer>,
) {
    // prepare layers
    let mut layers = Layers::default();
    layers.load(&renderer);
    globals.set(layers);

    // prepare shader
    let mut shader = Shader {
        name: String::from(PIPELINE_LABEL),
        code: Lights::add_to_shader(include_str!("shaders/terrain.wgsl"), 0, 2),
        ..Default::default()
    };
    shader.load(&renderer);
    assets.store_as(shader, PIPELINE_LABEL);
}

/// Terrain spawn system
/// Controls presense of terrain tiles, generation of meshes, and resource releasing
pub fn spawn(
    mut ctx: Context<Spawner>,
    mut terrain: Mut<Terrain>,
    camera: Const<Camera>,
    mut assets: Mut<Assets>,
    mut world: Mut<World>,
) {

    let view_distance = terrain.view_distance;
    // get viewer
    let viewer = Viewer {
        view_distance_sq: view_distance * view_distance,
        position: [camera.target.x, camera.target.z],
    };

    // check if update is necessary
    if let Some(last_viewer_position) = ctx.last_viewer_position.as_ref() {
        let dx = viewer.position[0] - last_viewer_position[0];
        let dz = viewer.position[1] - last_viewer_position[1];
        if  !terrain.force_spawn && dx * dx + dz * dz < terrain.spawn_if_moved_by {
            return;
        }
    }
    ctx.last_viewer_position = Some(viewer.position);

    if terrain.force_spawn {
        ctx.tiles.clear();

        let query = world.query::<(&Tile, &mut Pipeline)>();
        for (_, pipeline) in query {
            pipeline.disabled = true;
        }

        terrain.force_spawn = false;
    }

    // mark all tiles non visible
    for tile in ctx.tiles.values_mut() {
        tile.visible = false;
    }

    // calculate terrain tiles that has to be visible
    let max_lod = terrain.max_lod;
    let tile_size = terrain.tile_size as f32 * (2.0_f32).powf(max_lod as f32);
    let tiles_per_view_distance = (view_distance / tile_size as f32).ceil() as i32;
    let half_tile_size = tile_size as i32 / 2;
    let from_x = ((viewer.position[0] / tile_size).floor() * tile_size) as i32;
    let from_z = ((viewer.position[1] / tile_size).floor() * tile_size) as i32;

    for zi in -tiles_per_view_distance..tiles_per_view_distance {
        let z = from_z + zi * tile_size as i32 + half_tile_size as i32;
        for xi in -tiles_per_view_distance..tiles_per_view_distance {
            let x = from_x + xi * tile_size as i32 + half_tile_size as i32;
            // recursively calculate what lods should be spawned and spawn them
            queue_tiles_to_spawn(&mut ctx, &viewer, half_tile_size, max_lod, TileIndex {x, z});
        }
    }

    // exile tiles
    let query = world.query::<(&Tile, &Entity)>();
    for (tile, entity) in query {
        let index = TileIndex { x: tile.x, z: tile.z };
        let do_exile = if let Some(tile) = ctx.tiles.get_mut(&index) {
            !tile.visible
        } else {
            true
        };
        if do_exile {
            ctx.to_exile.push((*entity, tile.mesh));
        }
    }

    for (entity, mesh) in ctx.to_exile.iter() {
        world.exile(*entity);
        assets.remove(*mesh);
    }
    ctx.to_exile.clear();

    // cleanup tiles registry of the exiled tiles
    ctx.tiles.retain(|_, tile| tile.visible);

    // spawn missing tiles
    for (index, tile_state) in ctx.tiles.iter_mut() {
        if tile_state.spawned {
            continue;
        }

        let x = index.x;
        let z = index.z;
        let lod = tile_state.lod;

        let mesh = terrain.generate_tile_mesh(x, z, lod);
        let tile = Tile {
            x,
            z,
            lod,
            mesh: assets.store(mesh),
            loaded: false
        };
        let material = Material {
            texture: terrain.texture,
            albedo: Color::white(),
            ..Default::default()
        };
        let pipeline = Pipeline::default();

        world.spawn(Some((tile, material, pipeline)));

        tile_state.spawned = true;
    }
}

fn queue_tiles_to_spawn(
    ctx: &mut Spawner,
    viewer: &Viewer,
    half_tile_size: i32,
    lod: usize,
    position: TileIndex,
) {
    let dx = position.x as f32 - viewer.position[0];
    let dz = position.z as f32 - viewer.position[1];
    let distance_sq = dx * dx + dz * dz;
    let lod_distance_sq = (4 * half_tile_size * half_tile_size) as f32;
    let x = position.x;
    let z = position.z;

    if distance_sq > lod_distance_sq || lod == 0 {
        if distance_sq > viewer.view_distance_sq {
            return; // the tile is out of the view distance range
        }
        let mut tile = ctx.tiles
            .entry(position)
            .or_insert(TileState { lod, ..Default::default() });
        tile.visible = true;
    } else {
        // Higher lod is required
        let half_tile_size = half_tile_size / 2;
        let x1 = x - half_tile_size as i32;
        let x2 = x + half_tile_size as i32;
        let z1 = z - half_tile_size as i32;
        let z2 = z + half_tile_size as i32;
        let higher_lod_tiles = [
            TileIndex { x: x1, z: z1 },
            TileIndex { x: x2, z: z1 },
            TileIndex { x: x1, z: z2 },
            TileIndex { x: x2, z: z2 },
        ];

        for tile in higher_lod_tiles.iter() {
            queue_tiles_to_spawn(ctx, viewer, half_tile_size, lod - 1, *tile);
        }
    }
}

/// Terrain rendering system
pub fn render(
    mut renderer: Mut<Renderer>,
    mut assets: Mut<Assets>,
    globals: Const<Globals>,
    world: Const<World>,
) {
    let query = world.query::<(
        &mut Tile,
        &mut Material,
        &mut Pipeline
    )>();

    for (tile, material, pipeline) in query {

        if pipeline.shader.is_null() {
            pipeline.shader = assets.find::<Shader>(PIPELINE_LABEL)
                .unwrap_or_else(Id::default);
        }

        // check if model is disabled or already rendered
        if !pipeline.cycle(&renderer) { continue; }

        if !tile.loaded {
            if let Some(mesh) = assets.get_mut(tile.mesh) {
                mesh.load(&renderer);
            }
            tile.loaded = true;
        }

        if !material.load(&renderer, &mut assets) { continue; }

        let mesh = assets.get(tile.mesh).unwrap();

        if !pipeline.ready() {
            if let Some(shader) = assets.get(pipeline.shader) {
                if !shader.loaded() { continue; }

                let texture = assets.get(material.texture).unwrap();

                let proj_view = globals.get::<ProjView>()
                    .expect("ProjView buffer must be loaded");

                let sampler = globals.get::<Sampler>()
                    .expect("ProjView buffer must be loaded");

                let lights = globals.get::<Lights>()
                    .expect("Lights buffer must be loaded");

                let layers = globals.get::<Layers>()
                    .expect("Terrain layers must be loaded");

                renderer.bind(pipeline, PipelineLayout {
                    label: String::from(PIPELINE_LABEL),
                    mesh,
                    shader,
                    bindings: &[
                        BindGroup::new("Globals", vec![
                            Binding::Uniform("ProjView", Stage::Vertex, &proj_view.uniform),
                            Binding::Sampler("Sampler", Stage::Fragment, sampler),
                            Binding::Uniform("Lights", Stage::Fragment, &lights.uniform),
                            Binding::Uniform("Layers", Stage::Fragment, &layers.uniform),
                        ]),
                        BindGroup::new("Locals", vec![
                            Binding::Uniform("Material", Stage::Vertex, &material.uniform),
                            Binding::Texture("Texture", Stage::Fragment, &texture.buffer),
                        ])
                    ],
                    options: PipelineOptions::default()
                });
            }
        }

        renderer.run(pipeline, mesh);
    }
}