├── .gitattributes ├── .gitignore ├── src ├── math │ ├── mod.rs │ ├── ellipsoid.rs │ └── coordinate.rs ├── render │ ├── mod.rs │ ├── culling_bind_group.rs │ ├── terrain_bind_group.rs │ └── terrain_view_bind_group.rs ├── big_space.rs ├── formats │ ├── mod.rs │ └── tiff.rs ├── shaders │ ├── tiling_prepass │ │ ├── prepare_prepass.wgsl │ │ └── refine_tiles.wgsl │ ├── preprocess │ │ ├── downsample.wgsl │ │ ├── split.wgsl │ │ ├── preprocessing.wgsl │ │ └── stitch.wgsl │ ├── bindings.wgsl │ ├── types.wgsl │ ├── mod.rs │ ├── render │ │ ├── vertex.wgsl │ │ └── fragment.wgsl │ ├── debug.wgsl │ ├── attachments.wgsl │ └── functions.wgsl ├── terrain_view.rs ├── terrain.rs ├── terrain_data │ ├── gpu_tile_tree.rs │ └── mod.rs ├── lib.rs ├── plugin.rs ├── util.rs ├── debug │ ├── camera.rs │ └── mod.rs └── preprocess │ ├── gpu_preprocessor.rs │ └── mod.rs ├── LICENSE-MIT ├── assets └── shaders │ ├── planar.wgsl │ └── spherical.wgsl ├── examples ├── preprocess_spherical.rs ├── preprocess_planar.rs ├── minimal.rs ├── planar.rs └── spherical.rs ├── Cargo.toml ├── docs ├── development.md └── implementation.md ├── README.md └── LICENSE-APACHE /.gitattributes: -------------------------------------------------------------------------------- 1 | *.tif filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | Cargo.lock 3 | target 4 | assets/**/data/* 5 | assets/**/config.tc 6 | assets/terrains/spherical/source/height/200m.tif -------------------------------------------------------------------------------- /src/math/mod.rs: -------------------------------------------------------------------------------- 1 | mod coordinate; 2 | mod ellipsoid; 3 | mod terrain_model; 4 | 5 | pub use crate::math::{ 6 | coordinate::{Coordinate, TileCoordinate}, 7 | terrain_model::{ 8 | generate_terrain_model_approximation, TerrainModel, TerrainModelApproximation, 9 | }, 10 | }; 11 | 12 | /// The square of the parameter c of the algebraic sigmoid function, used to convert between uv and st coordinates. 13 | const C_SQR: f64 = 0.87 * 0.87; 14 | -------------------------------------------------------------------------------- /src/render/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the implementation of the Uniform Distance-Dependent Level of Detail (UDLOD). 2 | //! 3 | //! This algorithm is responsible for approximating the terrain geometry. 4 | //! Therefore tiny mesh tiles are refined in a tile_tree-like manner in a compute shader prepass for 5 | //! each view. Then they are drawn using a single draw indirect call and morphed together to form 6 | //! one continuous surface. 7 | 8 | pub mod culling_bind_group; 9 | pub mod terrain_bind_group; 10 | pub mod terrain_material; 11 | pub mod terrain_view_bind_group; 12 | pub mod tiling_prepass; 13 | -------------------------------------------------------------------------------- /src/big_space.rs: -------------------------------------------------------------------------------- 1 | pub use big_space::{BigSpaceCommands, FloatingOrigin}; 2 | 3 | pub type GridPrecision = i32; 4 | 5 | pub type BigSpacePlugin = big_space::BigSpacePlugin; 6 | pub type ReferenceFrame = big_space::reference_frame::ReferenceFrame; 7 | pub type ReferenceFrames<'w, 's> = 8 | big_space::reference_frame::local_origin::ReferenceFrames<'w, 's, GridPrecision>; 9 | pub type GridCell = big_space::GridCell; 10 | pub type GridTransform = big_space::world_query::GridTransform; 11 | pub type GridTransformReadOnly = big_space::world_query::GridTransformReadOnly; 12 | pub type GridTransformOwned = big_space::world_query::GridTransformOwned; 13 | pub type GridTransformItem<'w> = big_space::world_query::GridTransformItem<'w, GridPrecision>; 14 | -------------------------------------------------------------------------------- /src/formats/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tiff; 2 | 3 | use crate::math::TileCoordinate; 4 | use anyhow::Result; 5 | use bincode::{config, Decode, Encode}; 6 | use std::{fs, path::Path}; 7 | 8 | #[derive(Encode, Decode, Debug)] 9 | pub struct TC { 10 | pub tiles: Vec, 11 | } 12 | 13 | impl TC { 14 | pub fn decode_alloc(encoded: &[u8]) -> Result { 15 | let config = config::standard(); 16 | let decoded = bincode::decode_from_slice(encoded, config)?; 17 | Ok(decoded.0) 18 | } 19 | 20 | pub fn encode_alloc(&self) -> Result> { 21 | let config = config::standard(); 22 | let encoded = bincode::encode_to_vec(self, config)?; 23 | Ok(encoded) 24 | } 25 | 26 | pub fn load_file>(path: P) -> Result { 27 | let encoded = fs::read(path)?; 28 | Self::decode_alloc(&encoded) 29 | } 30 | 31 | pub fn save_file>(&self, path: P) -> Result<()> { 32 | let encoded = self.encode_alloc()?; 33 | fs::write(path, encoded)?; 34 | Ok(()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kurt Kühnert 4 | Copyright (c) 2023 Argeo 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /assets/shaders/planar.wgsl: -------------------------------------------------------------------------------- 1 | #import bevy_terrain::types::AtlasTile 2 | #import bevy_terrain::attachments::{sample_attachment0 as sample_height, sample_normal, sample_attachment1 as sample_albedo} 3 | #import bevy_terrain::fragment::{FragmentInput, FragmentOutput, fragment_info, fragment_output, fragment_debug} 4 | #import bevy_terrain::functions::lookup_tile 5 | #import bevy_pbr::pbr_types::{PbrInput, pbr_input_new} 6 | 7 | @group(3) @binding(0) 8 | var gradient: texture_1d; 9 | @group(3) @binding(1) 10 | var gradient_sampler: sampler; 11 | 12 | fn sample_color(tile: AtlasTile) -> vec4 { 13 | #ifdef ALBEDO 14 | return sample_albedo(tile); 15 | #else 16 | let height = sample_height(tile).x; 17 | 18 | return textureSampleLevel(gradient, gradient_sampler, pow(height, 0.9), 0.0); 19 | #endif 20 | } 21 | 22 | @fragment 23 | fn fragment(input: FragmentInput) -> FragmentOutput { 24 | var info = fragment_info(input); 25 | 26 | let tile = lookup_tile(info.coordinate, info.blend, 0u); 27 | var color = sample_color(tile); 28 | var normal = sample_normal(tile, info.world_normal); 29 | 30 | if (info.blend.ratio > 0.0) { 31 | let tile2 = lookup_tile(info.coordinate, info.blend, 1u); 32 | color = mix(color, sample_color(tile2), info.blend.ratio); 33 | normal = mix(normal, sample_normal(tile2, info.world_normal), info.blend.ratio); 34 | } 35 | 36 | var output: FragmentOutput; 37 | fragment_output(&info, &output, color, normal); 38 | fragment_debug(&info, &output, tile, normal); 39 | return output; 40 | } 41 | -------------------------------------------------------------------------------- /examples/preprocess_spherical.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_terrain::prelude::*; 3 | 4 | const PATH: &str = "terrains/spherical"; 5 | const TEXTURE_SIZE: u32 = 512; 6 | const LOD_COUNT: u32 = 5; 7 | 8 | fn main() { 9 | App::new() 10 | .add_plugins(( 11 | DefaultPlugins.build().disable::(), 12 | TerrainPlugin, 13 | TerrainPreprocessPlugin, 14 | )) 15 | .add_systems(Startup, setup) 16 | .run(); 17 | } 18 | 19 | fn setup(mut commands: Commands, asset_server: Res) { 20 | let config = TerrainConfig { 21 | lod_count: LOD_COUNT, 22 | path: PATH.to_string(), 23 | atlas_size: 2048, 24 | ..default() 25 | } 26 | .add_attachment(AttachmentConfig { 27 | name: "height".to_string(), 28 | texture_size: TEXTURE_SIZE, 29 | border_size: 2, 30 | format: AttachmentFormat::R16, 31 | ..default() 32 | }); 33 | 34 | let mut tile_atlas = TileAtlas::new(&config); 35 | 36 | let preprocessor = Preprocessor::new() 37 | .clear_attachment(0, &mut tile_atlas) 38 | .preprocess_spherical( 39 | SphericalDataset { 40 | attachment_index: 0, 41 | paths: (0..6) 42 | .map(|side| format!("{PATH}/source/height/face{side}.tif")) 43 | .collect(), 44 | lod_range: 0..LOD_COUNT, 45 | }, 46 | &asset_server, 47 | &mut tile_atlas, 48 | ); 49 | 50 | commands.spawn((tile_atlas, preprocessor)); 51 | } 52 | -------------------------------------------------------------------------------- /src/shaders/tiling_prepass/prepare_prepass.wgsl: -------------------------------------------------------------------------------- 1 | #import bevy_terrain::types::TileCoordinate 2 | #import bevy_terrain::bindings::{view_config, temporary_tiles, parameters, indirect_buffer} 3 | 4 | @compute @workgroup_size(1, 1, 1) 5 | fn prepare_root() { 6 | parameters.counter = -1; 7 | atomicStore(¶meters.child_index, i32(view_config.tile_count - 1u)); 8 | atomicStore(¶meters.final_index, 0); 9 | 10 | #ifdef SPHERICAL 11 | parameters.tile_count = 6u; 12 | 13 | for (var i: u32 = 0u; i < 6u; i = i + 1u) { 14 | temporary_tiles[i] = TileCoordinate(i, 0u, vec2(0u)); 15 | } 16 | #else 17 | parameters.tile_count = 1u; 18 | 19 | temporary_tiles[0] = TileCoordinate(0u, 0u, vec2(0u)); 20 | #endif 21 | 22 | indirect_buffer.workgroup_count = vec3(1u, 1u, 1u); 23 | } 24 | 25 | @compute @workgroup_size(1, 1, 1) 26 | fn prepare_next() { 27 | if (parameters.counter == 1) { 28 | parameters.tile_count = u32(atomicExchange(¶meters.child_index, i32(view_config.tile_count - 1u))); 29 | } 30 | else { 31 | parameters.tile_count = view_config.tile_count - 1u - u32(atomicExchange(¶meters.child_index, 0)); 32 | } 33 | 34 | parameters.counter = -parameters.counter; 35 | indirect_buffer.workgroup_count.x = (parameters.tile_count + 63u) / 64u; 36 | } 37 | 38 | @compute @workgroup_size(1, 1, 1) 39 | fn prepare_render() { 40 | let tile_count = u32(atomicLoad(¶meters.final_index)); 41 | let vertex_count = view_config.vertices_per_tile * tile_count; 42 | 43 | indirect_buffer.workgroup_count = vec3(vertex_count, 1u, 0u); 44 | } -------------------------------------------------------------------------------- /src/shaders/preprocess/downsample.wgsl: -------------------------------------------------------------------------------- 1 | #import bevy_terrain::preprocessing::{AtlasTile, atlas, attachment, inside, pixel_coords, pixel_value, process_entry, is_border} 2 | 3 | struct DownsampleData { 4 | tile: AtlasTile, 5 | child_tiles: array, 6 | tile_index: u32, 7 | } 8 | 9 | @group(1) @binding(0) 10 | var downsample_data: DownsampleData; 11 | 12 | override fn pixel_value(coords: vec2) -> vec4 { 13 | if (is_border(coords)) { 14 | return vec4(0.0); 15 | } 16 | 17 | let tile_coords = coords - vec2(attachment.border_size); 18 | let child_size = attachment.center_size / 2u; 19 | let child_coords = 2u * (tile_coords % child_size) + vec2(attachment.border_size); 20 | let child_index = tile_coords.x / child_size + 2u * (tile_coords.y / child_size); 21 | 22 | let child_tile = downsample_data.child_tiles[child_index]; 23 | 24 | var OFFSETS = array(vec2(0u, 0u), vec2(0u, 1u), vec2(1u, 0u), vec2(1u, 1u)); 25 | 26 | var value = vec4(0.0); 27 | var count = 0.0; 28 | 29 | for (var index = 0u; index < 4u; index += 1u) { 30 | let child_value = textureLoad(atlas, child_coords + OFFSETS[index], child_tile.atlas_index, 0); 31 | let is_valid = any(child_value.xyz != vec3(0.0)); 32 | 33 | if (is_valid) { 34 | value += child_value; 35 | count += 1.0; 36 | } 37 | } 38 | 39 | return value / count; 40 | } 41 | 42 | // Todo: respect memory coalescing 43 | @compute @workgroup_size(8, 8, 1) 44 | fn downsample(@builtin(global_invocation_id) invocation_id: vec3) { 45 | process_entry(vec3(invocation_id.xy, downsample_data.tile_index)); 46 | } -------------------------------------------------------------------------------- /assets/shaders/spherical.wgsl: -------------------------------------------------------------------------------- 1 | #import bevy_terrain::types::{AtlasTile} 2 | #import bevy_terrain::bindings::config 3 | #import bevy_terrain::attachments::{sample_height, sample_normal} 4 | #import bevy_terrain::fragment::{FragmentInput, FragmentOutput, fragment_info, fragment_output, fragment_debug} 5 | #import bevy_terrain::functions::lookup_tile 6 | #import bevy_pbr::pbr_types::{PbrInput, pbr_input_new} 7 | #import bevy_pbr::pbr_functions::{calculate_view, apply_pbr_lighting} 8 | 9 | 10 | @group(3) @binding(0) 11 | var gradient: texture_1d; 12 | @group(3) @binding(1) 13 | var gradient_sampler: sampler; 14 | 15 | fn sample_color(tile: AtlasTile) -> vec4 { 16 | let height = sample_height(tile); 17 | 18 | var color: vec4; 19 | 20 | if (height < 0.0) { 21 | color = textureSampleLevel(gradient, gradient_sampler, mix(0.0, 0.075, pow(height / config.min_height, 0.25)), 0.0); 22 | } 23 | else { 24 | color = textureSampleLevel(gradient, gradient_sampler, mix(0.09, 1.0, pow(height / config.max_height * 2.0, 1.0)), 0.0); 25 | } 26 | 27 | return color; 28 | } 29 | 30 | @fragment 31 | fn fragment(input: FragmentInput) -> FragmentOutput { 32 | var info = fragment_info(input); 33 | 34 | let tile = lookup_tile(info.coordinate, info.blend, 0u); 35 | var color = sample_color(tile); 36 | var normal = sample_normal(tile, info.world_normal); 37 | 38 | if (info.blend.ratio > 0.0) { 39 | let tile2 = lookup_tile(info.coordinate, info.blend, 1u); 40 | color = mix(color, sample_color(tile2), info.blend.ratio); 41 | normal = mix(normal, sample_normal(tile2, info.world_normal), info.blend.ratio); 42 | } 43 | 44 | var output: FragmentOutput; 45 | fragment_output(&info, &output, color, normal); 46 | fragment_debug(&info, &output, tile, normal); 47 | return output; 48 | } 49 | -------------------------------------------------------------------------------- /src/shaders/preprocess/split.wgsl: -------------------------------------------------------------------------------- 1 | #import bevy_terrain::preprocessing::{AtlasTile, atlas, attachment, pixel_coords, pixel_value, process_entry, is_border, inverse_mix} 2 | #import bevy_terrain::functions::{inside_square, tile_count}; 3 | 4 | struct SplitData { 5 | tile: AtlasTile, 6 | top_left: vec2, 7 | bottom_right: vec2, 8 | tile_index: u32, 9 | } 10 | 11 | @group(1) @binding(0) 12 | var split_data: SplitData; 13 | @group(1) @binding(1) 14 | var source_tile: texture_2d; 15 | @group(1) @binding(2) 16 | var source_tile_sampler: sampler; 17 | 18 | override fn pixel_value(coords: vec2) -> vec4 { 19 | if (is_border(coords)) { 20 | return vec4(0.0); 21 | } 22 | 23 | let tile_coordinate = split_data.tile.coordinate; 24 | let tile_offset = vec2(f32(tile_coordinate.x), f32(tile_coordinate.y)); 25 | let tile_coords = vec2(coords - vec2(attachment.border_size)) / f32(attachment.center_size); 26 | let tile_scale = tile_count(tile_coordinate.lod); 27 | 28 | var source_coords = (tile_offset + tile_coords) / tile_scale; 29 | 30 | source_coords = inverse_mix(split_data.top_left, split_data.bottom_right, source_coords); 31 | 32 | let value = textureSampleLevel(source_tile, source_tile_sampler, source_coords, 0.0); 33 | 34 | let is_valid = all(textureGather(0u, source_tile, source_tile_sampler, source_coords) != vec4(0.0)); 35 | let is_inside = inside_square(tile_coords, vec2(0.0), 1.0) == 1.0; 36 | 37 | if (is_valid && is_inside) { 38 | return value; 39 | } 40 | else { 41 | return textureLoad(atlas, coords, split_data.tile.atlas_index, 0); 42 | } 43 | } 44 | 45 | // Todo: respect memory coalescing 46 | @compute @workgroup_size(8, 8, 1) 47 | fn split(@builtin(global_invocation_id) invocation_id: vec3) { 48 | process_entry(vec3(invocation_id.xy, split_data.tile_index)); 49 | } -------------------------------------------------------------------------------- /src/shaders/tiling_prepass/refine_tiles.wgsl: -------------------------------------------------------------------------------- 1 | #import bevy_terrain::types::{TileCoordinate, Coordinate} 2 | #import bevy_terrain::bindings::{config, culling_view, view_config, final_tiles, temporary_tiles, parameters, terrain_model_approximation} 3 | #import bevy_terrain::functions::{approximate_view_distance, compute_relative_position, position_local_to_world, normal_local_to_world, tile_count, compute_subdivision_coordinate} 4 | 5 | fn child_index() -> i32 { 6 | return atomicAdd(¶meters.child_index, parameters.counter); 7 | } 8 | 9 | fn parent_index(id: u32) -> i32 { 10 | return i32(view_config.tile_count - 1u) * clamp(parameters.counter, 0, 1) - i32(id) * parameters.counter; 11 | } 12 | 13 | fn final_index() -> i32 { 14 | return atomicAdd(¶meters.final_index, 1); 15 | } 16 | 17 | fn should_be_divided(tile: TileCoordinate) -> bool { 18 | let coordinate = compute_subdivision_coordinate(Coordinate(tile.side, tile.lod, tile.xy, vec2(0.0))); 19 | let view_distance = approximate_view_distance(coordinate, culling_view.world_position); 20 | 21 | return view_distance < view_config.subdivision_distance / tile_count(tile.lod); 22 | } 23 | 24 | fn subdivide(tile: TileCoordinate) { 25 | for (var i: u32 = 0u; i < 4u; i = i + 1u) { 26 | let child_xy = vec2((tile.xy.x << 1u) + (i & 1u), (tile.xy.y << 1u) + (i >> 1u & 1u)); 27 | let child_lod = tile.lod + 1u; 28 | 29 | temporary_tiles[child_index()] = TileCoordinate(tile.side, child_lod, child_xy); 30 | } 31 | } 32 | 33 | @compute @workgroup_size(64, 1, 1) 34 | fn refine_tiles(@builtin(global_invocation_id) invocation_id: vec3) { 35 | if (invocation_id.x >= parameters.tile_count) { return; } 36 | 37 | let tile = temporary_tiles[parent_index(invocation_id.x)]; 38 | 39 | if (should_be_divided(tile)) { 40 | subdivide(tile); 41 | } else { 42 | final_tiles[final_index()] = tile; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/preprocess_planar.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_terrain::prelude::*; 3 | 4 | const PATH: &str = "terrains/planar"; 5 | const TEXTURE_SIZE: u32 = 512; 6 | const LOD_COUNT: u32 = 4; 7 | 8 | fn main() { 9 | App::new() 10 | .add_plugins((DefaultPlugins, TerrainPlugin, TerrainPreprocessPlugin)) 11 | .add_systems(Startup, setup) 12 | .run(); 13 | } 14 | 15 | fn setup(mut commands: Commands, asset_server: Res) { 16 | let config = TerrainConfig { 17 | lod_count: LOD_COUNT, 18 | path: PATH.to_string(), 19 | ..default() 20 | } 21 | .add_attachment(AttachmentConfig { 22 | name: "height".to_string(), 23 | texture_size: TEXTURE_SIZE, 24 | border_size: 2, 25 | format: AttachmentFormat::R16, 26 | ..default() 27 | }) 28 | .add_attachment(AttachmentConfig { 29 | name: "albedo".to_string(), 30 | texture_size: TEXTURE_SIZE, 31 | border_size: 2, 32 | format: AttachmentFormat::Rgba8, 33 | ..default() 34 | }); 35 | 36 | let mut tile_atlas = TileAtlas::new(&config); 37 | 38 | let preprocessor = Preprocessor::new() 39 | .clear_attachment(0, &mut tile_atlas) 40 | .clear_attachment(1, &mut tile_atlas) 41 | .preprocess_tile( 42 | PreprocessDataset { 43 | attachment_index: 0, 44 | path: format!("{PATH}/source/height.png"), 45 | lod_range: 0..LOD_COUNT, 46 | ..default() 47 | }, 48 | &asset_server, 49 | &mut tile_atlas, 50 | ) 51 | .preprocess_tile( 52 | PreprocessDataset { 53 | attachment_index: 1, 54 | path: format!("{PATH}/source/albedo.png"), 55 | lod_range: 0..LOD_COUNT, 56 | ..default() 57 | }, 58 | &asset_server, 59 | &mut tile_atlas, 60 | ); 61 | 62 | commands.spawn((tile_atlas, preprocessor)); 63 | } 64 | -------------------------------------------------------------------------------- /src/shaders/bindings.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_terrain::bindings 2 | 3 | #import bevy_terrain::types::{TerrainViewConfig, TerrainConfig, TileTreeEntry, TileCoordinate, AttachmentConfig, TerrainModelApproximation, CullingData, IndirectBuffer, Parameters} 4 | #import bevy_pbr::mesh_types::Mesh 5 | 6 | // terrain bindings 7 | @group(1) @binding(0) 8 | var mesh: array; 9 | @group(1) @binding(1) 10 | var config: TerrainConfig; 11 | @group(1) @binding(2) 12 | var attachments: array; 13 | @group(1) @binding(3) 14 | var atlas_sampler: sampler; 15 | @group(1) @binding(4) 16 | var attachment0_atlas: texture_2d_array; 17 | @group(1) @binding(5) 18 | var attachment1_atlas: texture_2d_array; 19 | @group(1) @binding(6) 20 | var attachment2_atlas: texture_2d_array; 21 | @group(1) @binding(7) 22 | var attachment3_atlas: texture_2d_array; 23 | @group(1) @binding(8) 24 | var attachment4_atlas: texture_2d_array; 25 | @group(1) @binding(9) 26 | var attachment5_atlas: texture_2d_array; 27 | @group(1) @binding(10) 28 | var attachment6_atlas: texture_2d_array; 29 | @group(1) @binding(11) 30 | var attachment7_atlas: texture_2d_array; 31 | 32 | // terrain view bindings 33 | @group(2) @binding(0) 34 | var view_config: TerrainViewConfig; 35 | @group(2) @binding(1) 36 | var terrain_model_approximation: TerrainModelApproximation; 37 | @group(2) @binding(2) 38 | var tile_tree: array; 39 | @group(2) @binding(3) 40 | var origins: array>; 41 | @group(2) @binding(4) 42 | var geometry_tiles: array; 43 | 44 | // refine geometry_tiles bindings 45 | @group(2) @binding(4) 46 | var final_tiles: array; 47 | @group(2) @binding(5) 48 | var temporary_tiles: array; 49 | @group(2) @binding(6) 50 | var parameters: Parameters; 51 | 52 | @group(3) @binding(0) 53 | var indirect_buffer: IndirectBuffer; 54 | 55 | // culling bindings 56 | @group(0) @binding(0) 57 | var culling_view: CullingData; -------------------------------------------------------------------------------- /src/formats/tiff.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}, 3 | prelude::*, 4 | render::{ 5 | render_asset::RenderAssetUsages, 6 | render_resource::{Extent3d, TextureDimension, TextureFormat}, 7 | texture::TextureError, 8 | }, 9 | }; 10 | use bytemuck::cast_slice; 11 | use std::io::Cursor; 12 | use tiff::decoder::{Decoder, DecodingResult}; 13 | 14 | #[derive(Default)] 15 | pub struct TiffLoader; 16 | impl AssetLoader for TiffLoader { 17 | type Asset = Image; 18 | type Settings = (); 19 | type Error = TextureError; 20 | async fn load<'a>( 21 | &'a self, 22 | reader: &'a mut Reader<'_>, 23 | _settings: &'a Self::Settings, 24 | _load_context: &'a mut LoadContext<'_>, 25 | ) -> Result { 26 | let mut bytes = Vec::new(); 27 | reader.read_to_end(&mut bytes).await.unwrap(); 28 | 29 | let mut decoder = Decoder::new(Cursor::new(bytes)).unwrap(); 30 | 31 | let (width, height) = decoder.dimensions().unwrap(); 32 | 33 | let data = match decoder.read_image().unwrap() { 34 | DecodingResult::U8(data) => cast_slice(&data).to_vec(), 35 | DecodingResult::U16(data) => cast_slice(&data).to_vec(), 36 | DecodingResult::U32(data) => cast_slice(&data).to_vec(), 37 | DecodingResult::U64(data) => cast_slice(&data).to_vec(), 38 | DecodingResult::F32(data) => cast_slice(&data).to_vec(), 39 | DecodingResult::F64(data) => cast_slice(&data).to_vec(), 40 | DecodingResult::I8(data) => cast_slice(&data).to_vec(), 41 | DecodingResult::I16(data) => cast_slice(&data).to_vec(), 42 | DecodingResult::I32(data) => cast_slice(&data).to_vec(), 43 | DecodingResult::I64(data) => cast_slice(&data).to_vec(), 44 | }; 45 | 46 | Ok(Image::new( 47 | Extent3d { 48 | width, 49 | height, 50 | depth_or_array_layers: 1, 51 | }, 52 | TextureDimension::D2, 53 | data, 54 | TextureFormat::R16Unorm, 55 | RenderAssetUsages::default(), 56 | )) 57 | } 58 | 59 | fn extensions(&self) -> &[&str] { 60 | &["tif", "tiff"] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/minimal.rs: -------------------------------------------------------------------------------- 1 | use bevy::math::DVec3; 2 | use bevy::prelude::*; 3 | use bevy_terrain::prelude::*; 4 | 5 | const PATH: &str = "terrains/planar"; 6 | const TERRAIN_SIZE: f64 = 1000.0; 7 | const HEIGHT: f32 = 250.0; 8 | const TEXTURE_SIZE: u32 = 512; 9 | const LOD_COUNT: u32 = 4; 10 | 11 | fn main() { 12 | App::new() 13 | .add_plugins(( 14 | DefaultPlugins, 15 | TerrainPlugin, 16 | TerrainMaterialPlugin::::default(), 17 | TerrainDebugPlugin, 18 | )) 19 | .add_systems(Startup, setup) 20 | .run(); 21 | } 22 | 23 | fn setup( 24 | mut commands: Commands, 25 | mut materials: ResMut>, 26 | mut tile_trees: ResMut>, 27 | mut meshes: ResMut>, 28 | ) { 29 | // Configure all the important properties of the terrain, as well as its attachments. 30 | let config = TerrainConfig { 31 | lod_count: LOD_COUNT, 32 | model: TerrainModel::planar(DVec3::new(0.0, -100.0, 0.0), TERRAIN_SIZE, 0.0, HEIGHT), 33 | path: PATH.to_string(), 34 | ..default() 35 | } 36 | .add_attachment(AttachmentConfig { 37 | name: "height".to_string(), 38 | texture_size: TEXTURE_SIZE, 39 | border_size: 2, 40 | mip_level_count: 4, 41 | format: AttachmentFormat::R16, 42 | }); 43 | 44 | // Configure the quality settings of the terrain view. Adapt the settings to your liking. 45 | let view_config = TerrainViewConfig::default(); 46 | 47 | let tile_atlas = TileAtlas::new(&config); 48 | let tile_tree = TileTree::new(&tile_atlas, &view_config); 49 | 50 | let terrain = commands 51 | .spawn(( 52 | TerrainBundle::new(tile_atlas), 53 | materials.add(DebugTerrainMaterial::default()), 54 | )) 55 | .id(); 56 | 57 | let view = commands.spawn(DebugCameraBundle::default()).id(); 58 | 59 | tile_trees.insert((terrain, view), tile_tree); 60 | 61 | commands.spawn(PbrBundle { 62 | mesh: meshes.add(Cuboid::from_length(10.0)), 63 | transform: Transform::from_translation(Vec3::new( 64 | TERRAIN_SIZE as f32 / 2.0, 65 | 100.0, 66 | TERRAIN_SIZE as f32 / 2.0, 67 | )), 68 | ..default() 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /src/shaders/types.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_terrain::types 2 | 3 | struct TerrainConfig { 4 | lod_count: u32, 5 | min_height: f32, 6 | max_height: f32, 7 | scale: f32, 8 | } 9 | 10 | struct TerrainViewConfig { 11 | tree_size: u32, 12 | tile_count: u32, 13 | refinement_count: u32, 14 | grid_size: f32, 15 | vertices_per_row: u32, 16 | vertices_per_tile: u32, 17 | morph_distance: f32, 18 | blend_distance: f32, 19 | load_distance: f32, 20 | subdivision_distance: f32, 21 | morph_range: f32, 22 | blend_range: f32, 23 | precision_threshold_distance: f32, 24 | } 25 | 26 | struct TileCoordinate { 27 | side: u32, 28 | lod: u32, 29 | xy: vec2, 30 | } 31 | 32 | struct Coordinate { 33 | side: u32, 34 | lod: u32, 35 | xy: vec2, 36 | uv: vec2, 37 | #ifdef FRAGMENT 38 | uv_dx: vec2, 39 | uv_dy: vec2, 40 | #endif 41 | } 42 | 43 | struct Parameters { 44 | tile_count: u32, 45 | counter: i32, 46 | child_index: atomic, 47 | final_index: atomic, 48 | } 49 | 50 | struct Blend { 51 | lod: u32, 52 | ratio: f32, 53 | } 54 | 55 | struct TileTreeEntry { 56 | atlas_index: u32, 57 | atlas_lod: u32, 58 | } 59 | 60 | // A tile inside the tile atlas, looked up based on the view of a tile tree. 61 | struct AtlasTile { 62 | index: u32, 63 | coordinate: Coordinate, 64 | } 65 | 66 | struct BestLookup { 67 | tile: AtlasTile, 68 | tile_tree_uv: vec2, 69 | } 70 | 71 | struct AttachmentConfig { 72 | size: f32, 73 | scale: f32, 74 | offset: f32, 75 | _padding: u32, 76 | } 77 | 78 | struct SideParameter { 79 | view_xy: vec2, 80 | view_uv: vec2, 81 | c: vec3, 82 | c_s: vec3, 83 | c_t: vec3, 84 | c_ss: vec3, 85 | c_st: vec3, 86 | c_tt: vec3, 87 | } 88 | 89 | struct TerrainModelApproximation { 90 | origin_lod: u32, 91 | approximate_height: f32, 92 | sides: array, 93 | } 94 | 95 | struct IndirectBuffer { 96 | workgroup_count: vec3, 97 | } 98 | 99 | struct CullingData { 100 | world_position: vec3, 101 | view_proj: mat4x4, 102 | planes: array, 5>, 103 | } 104 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_terrain" 3 | description = "Terrain Rendering for the Bevy Engine." 4 | version = "0.1.0-dev" 5 | license = "MIT OR Apache-2.0" 6 | edition = "2021" 7 | categories = ["game-engines", "rendering", "graphics"] 8 | keywords = ["gamedev", "graphics", "bevy", "terrain"] 9 | exclude = ["assets/*"] 10 | readme = "README.md" 11 | authors = ["Kurt Kühnert "] 12 | repository = "https://github.com/kurtkuehnert/bevy_terrain" 13 | 14 | [features] 15 | high_precision = ["dep:big_space"] 16 | 17 | [dependencies] 18 | bevy = "0.14.0" #{ git="https://github.com/bevyengine/bevy/", branch="main" } 19 | ndarray = "0.15" 20 | itertools = "0.12" 21 | image = "0.25" 22 | tiff = "0.9" 23 | lru = "0.12" 24 | bitflags = "2.4" 25 | bytemuck = "1.14" 26 | anyhow = "1.0" 27 | bincode = "2.0.0-rc.3" 28 | async-channel = "2.1" 29 | big_space = { version = "0.7", optional = true } 30 | 31 | [[example]] 32 | name = "preprocess_planar" 33 | path = "examples/preprocess_planar.rs" 34 | required-features = ["bevy/embedded_watcher"] 35 | 36 | [package.metadata.example.preprocess_planar] 37 | name = "Preprocess Planar" 38 | description = "Preprocesses the terrain data for the planar examples." 39 | 40 | [[example]] 41 | name = "minimal" 42 | path = "examples/minimal.rs" 43 | required-features = ["bevy/embedded_watcher"] 44 | 45 | [package.metadata.example.minial] 46 | name = "Minimal" 47 | description = "Renders a basic flat terrain with only the base attachment." 48 | 49 | [[example]] 50 | name = "planar" 51 | path = "examples/planar.rs" 52 | required-features = ["high_precision", "bevy/embedded_watcher"] 53 | 54 | [package.metadata.example.planar] 55 | name = "Planar Advanced" 56 | description = "Renders a flat terrain with the base attachment and an albedo texture, using a custom shader." 57 | 58 | [[example]] 59 | name = "preprocess_spherical" 60 | path = "examples/preprocess_spherical.rs" 61 | required-features = ["bevy/embedded_watcher"] 62 | 63 | [package.metadata.example.preprocess_spherical] 64 | name = "Preprocess Spherical" 65 | description = "Preprocesses the terrain data for the spherical examples." 66 | 67 | [[example]] 68 | name = "spherical" 69 | path = "examples/spherical.rs" 70 | required-features = ["high_precision", "bevy/embedded_watcher"] 71 | 72 | [package.metadata.example.spherical] 73 | name = "Spherical" 74 | description = "Renders a spherical terrain using a custom shader." 75 | 76 | -------------------------------------------------------------------------------- /src/terrain_view.rs: -------------------------------------------------------------------------------- 1 | //! Types for configuring terrain views. 2 | 3 | use bevy::{prelude::*, utils::HashMap}; 4 | 5 | /// Resource that stores components that are associated to a terrain entity and a view entity. 6 | #[derive(Deref, DerefMut, Resource)] 7 | pub struct TerrainViewComponents(HashMap<(Entity, Entity), C>); 8 | 9 | impl Default for TerrainViewComponents { 10 | fn default() -> Self { 11 | Self(default()) 12 | } 13 | } 14 | 15 | /// The configuration of a terrain view. 16 | /// 17 | /// A terrain view describes the quality settings the corresponding terrain will be rendered with. 18 | #[derive(Clone)] 19 | pub struct TerrainViewConfig { 20 | /// The count of tiles in x and y direction per tile tree layer. 21 | pub tree_size: u32, 22 | /// The size of the tile buffer. 23 | pub geometry_tile_count: u32, 24 | /// The amount of steps the tile list will be refined. 25 | pub refinement_count: u32, 26 | /// The number of rows and columns of the tile grid. 27 | pub grid_size: u32, 28 | /// The percentage tolerance added to the morph distance during tile subdivision. 29 | /// This is required to counteracted the distortion of the subdivision distance estimation near the corners of the cube sphere. 30 | /// For planar terrains this can be set to zero and for spherical / ellipsoidal terrains a value of around 0.1 is necessary. 31 | pub subdivision_tolerance: f64, 32 | pub precision_threshold_distance: f64, 33 | pub load_distance: f64, 34 | /// The distance measured in tile sizes between adjacent LOD layers. 35 | /// This currently has to be larger than about 6, since the tiles can only morph to the adjacent layer. 36 | /// Should the morph distance be too small, this will result in morph transitions suddenly being canceled, by the next LOD. 37 | /// This is dependent on the morph distance, the morph ratio and the subdivision tolerance. It can be debug with the show tiles debug view. 38 | pub morph_distance: f64, 39 | pub blend_distance: f64, 40 | /// The morph percentage of the mesh. 41 | pub morph_range: f32, 42 | /// The blend percentage in the vertex and fragment shader. 43 | pub blend_range: f32, 44 | pub origin_lod: u32, 45 | } 46 | 47 | impl Default for TerrainViewConfig { 48 | fn default() -> Self { 49 | Self { 50 | tree_size: 8, 51 | geometry_tile_count: 1000000, 52 | refinement_count: 30, 53 | grid_size: 16, 54 | subdivision_tolerance: 0.1, 55 | load_distance: 2.5, 56 | morph_distance: 16.0, 57 | blend_distance: 2.0, 58 | morph_range: 0.2, 59 | blend_range: 0.2, 60 | precision_threshold_distance: 0.001, 61 | origin_lod: 10, 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/shaders/mod.rs: -------------------------------------------------------------------------------- 1 | use bevy::{asset::embedded_asset, prelude::*}; 2 | use itertools::Itertools; 3 | 4 | pub const DEFAULT_VERTEX_SHADER: &str = "embedded://bevy_terrain/shaders/render/vertex.wgsl"; 5 | pub const DEFAULT_FRAGMENT_SHADER: &str = "embedded://bevy_terrain/shaders/render/fragment.wgsl"; 6 | pub const PREPARE_PREPASS_SHADER: &str = 7 | "embedded://bevy_terrain/shaders/tiling_prepass/prepare_prepass.wgsl"; 8 | pub const REFINE_TILES_SHADER: &str = 9 | "embedded://bevy_terrain/shaders/tiling_prepass/refine_tiles.wgsl"; 10 | pub(crate) const SPLIT_SHADER: &str = "embedded://bevy_terrain/shaders/preprocess/split.wgsl"; 11 | pub(crate) const STITCH_SHADER: &str = "embedded://bevy_terrain/shaders/preprocess/stitch.wgsl"; 12 | pub(crate) const DOWNSAMPLE_SHADER: &str = 13 | "embedded://bevy_terrain/shaders/preprocess/downsample.wgsl"; 14 | 15 | #[derive(Default, Resource)] 16 | pub(crate) struct InternalShaders(Vec>); 17 | 18 | impl InternalShaders { 19 | pub(crate) fn load(app: &mut App, shaders: &[&'static str]) { 20 | let mut shaders = shaders 21 | .iter() 22 | .map(|&shader| app.world_mut().resource_mut::().load(shader)) 23 | .collect_vec(); 24 | 25 | let mut internal_shaders = app.world_mut().resource_mut::(); 26 | internal_shaders.0.append(&mut shaders); 27 | } 28 | } 29 | 30 | pub(crate) fn load_terrain_shaders(app: &mut App) { 31 | embedded_asset!(app, "types.wgsl"); 32 | embedded_asset!(app, "attachments.wgsl"); 33 | embedded_asset!(app, "bindings.wgsl"); 34 | embedded_asset!(app, "functions.wgsl"); 35 | embedded_asset!(app, "debug.wgsl"); 36 | embedded_asset!(app, "render/vertex.wgsl"); 37 | embedded_asset!(app, "render/fragment.wgsl"); 38 | embedded_asset!(app, "tiling_prepass/prepare_prepass.wgsl"); 39 | embedded_asset!(app, "tiling_prepass/refine_tiles.wgsl"); 40 | 41 | InternalShaders::load( 42 | app, 43 | &[ 44 | "embedded://bevy_terrain/shaders/types.wgsl", 45 | "embedded://bevy_terrain/shaders/attachments.wgsl", 46 | "embedded://bevy_terrain/shaders/bindings.wgsl", 47 | "embedded://bevy_terrain/shaders/functions.wgsl", 48 | "embedded://bevy_terrain/shaders/debug.wgsl", 49 | "embedded://bevy_terrain/shaders/render/vertex.wgsl", 50 | "embedded://bevy_terrain/shaders/render/fragment.wgsl", 51 | ], 52 | ); 53 | } 54 | 55 | pub(crate) fn load_preprocess_shaders(app: &mut App) { 56 | embedded_asset!(app, "preprocess/preprocessing.wgsl"); 57 | embedded_asset!(app, "preprocess/split.wgsl"); 58 | embedded_asset!(app, "preprocess/stitch.wgsl"); 59 | embedded_asset!(app, "preprocess/downsample.wgsl"); 60 | 61 | InternalShaders::load( 62 | app, 63 | &["embedded://bevy_terrain/shaders/preprocess/preprocessing.wgsl"], 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /examples/planar.rs: -------------------------------------------------------------------------------- 1 | use bevy::math::DVec3; 2 | use bevy::{prelude::*, reflect::TypePath, render::render_resource::*}; 3 | use bevy_terrain::prelude::*; 4 | 5 | const PATH: &str = "terrains/planar"; 6 | const TERRAIN_SIZE: f64 = 2000.0; 7 | const HEIGHT: f32 = 500.0; 8 | const TEXTURE_SIZE: u32 = 512; 9 | const LOD_COUNT: u32 = 8; 10 | 11 | #[derive(Asset, AsBindGroup, TypePath, Clone)] 12 | pub struct TerrainMaterial { 13 | #[texture(0, dimension = "1d")] 14 | #[sampler(1)] 15 | gradient: Handle, 16 | } 17 | 18 | impl Material for TerrainMaterial { 19 | fn fragment_shader() -> ShaderRef { 20 | "shaders/planar.wgsl".into() 21 | } 22 | } 23 | 24 | fn main() { 25 | App::new() 26 | .add_plugins(( 27 | DefaultPlugins.build().disable::(), 28 | TerrainPlugin, 29 | TerrainDebugPlugin, // enable debug settings and controls 30 | TerrainMaterialPlugin::::default(), 31 | )) 32 | .add_systems(Startup, setup) 33 | .run(); 34 | } 35 | 36 | fn setup( 37 | mut commands: Commands, 38 | mut images: ResMut, 39 | mut materials: ResMut>, 40 | mut tile_trees: ResMut>, 41 | asset_server: Res, 42 | ) { 43 | let gradient = asset_server.load("textures/gradient2.png"); 44 | images.load_image( 45 | &gradient, 46 | TextureDimension::D1, 47 | TextureFormat::Rgba8UnormSrgb, 48 | ); 49 | 50 | // Configure all the important properties of the terrain, as well as its attachments. 51 | let config = TerrainConfig { 52 | lod_count: LOD_COUNT, 53 | model: TerrainModel::planar(DVec3::new(0.0, -100.0, 0.0), TERRAIN_SIZE, 0.0, HEIGHT), 54 | path: PATH.to_string(), 55 | ..default() 56 | } 57 | .add_attachment(AttachmentConfig { 58 | name: "height".to_string(), 59 | texture_size: TEXTURE_SIZE, 60 | border_size: 2, 61 | mip_level_count: 4, 62 | format: AttachmentFormat::R16, 63 | }) 64 | .add_attachment(AttachmentConfig { 65 | name: "albedo".to_string(), 66 | texture_size: TEXTURE_SIZE, 67 | border_size: 2, 68 | mip_level_count: 4, 69 | format: AttachmentFormat::Rgba8, 70 | }); 71 | 72 | // Configure the quality settings of the terrain view. Adapt the settings to your liking. 73 | let view_config = TerrainViewConfig::default(); 74 | 75 | let tile_atlas = TileAtlas::new(&config); 76 | let tile_tree = TileTree::new(&tile_atlas, &view_config); 77 | 78 | commands.spawn_big_space(ReferenceFrame::default(), |root| { 79 | let frame = root.frame().clone(); 80 | 81 | let terrain = root 82 | .spawn_spatial(( 83 | TerrainBundle::new(tile_atlas, &frame), 84 | materials.add(TerrainMaterial { gradient }), 85 | )) 86 | .id(); 87 | 88 | let view = root.spawn_spatial(DebugCameraBundle::default()).id(); 89 | 90 | tile_trees.insert((terrain, view), tile_tree); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /src/render/culling_bind_group.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | terrain_data::gpu_tile_tree::GpuTileTree, terrain_view::TerrainViewComponents, 3 | util::StaticBuffer, 4 | }; 5 | use bevy::{ 6 | prelude::*, 7 | render::{ 8 | render_resource::{binding_types::*, *}, 9 | renderer::RenderDevice, 10 | view::ExtractedView, 11 | }, 12 | }; 13 | use std::ops::Deref; 14 | 15 | pub(crate) fn create_culling_layout(device: &RenderDevice) -> BindGroupLayout { 16 | device.create_bind_group_layout( 17 | None, 18 | &BindGroupLayoutEntries::single( 19 | ShaderStages::COMPUTE, 20 | uniform_buffer::(false), // culling data 21 | ), 22 | ) 23 | } 24 | 25 | pub fn planes(view_projection: &Mat4) -> [Vec4; 5] { 26 | let row3 = view_projection.row(3); 27 | let mut planes = [default(); 5]; 28 | for (i, plane) in planes.iter_mut().enumerate() { 29 | let row = view_projection.row(i / 2); 30 | *plane = if (i & 1) == 0 && i != 4 { 31 | row3 + row 32 | } else { 33 | row3 - row 34 | }; 35 | } 36 | 37 | planes 38 | } 39 | 40 | #[derive(Default, ShaderType)] 41 | pub struct CullingUniform { 42 | world_position: Vec3, 43 | view_proj: Mat4, 44 | planes: [Vec4; 5], 45 | } 46 | 47 | impl From<&ExtractedView> for CullingUniform { 48 | fn from(view: &ExtractedView) -> Self { 49 | Self { 50 | world_position: view.world_from_view.translation(), 51 | view_proj: view.world_from_view.compute_matrix().inverse(), 52 | planes: default(), 53 | } 54 | } 55 | } 56 | 57 | #[derive(Component)] 58 | pub struct CullingBindGroup(BindGroup); 59 | 60 | impl Deref for CullingBindGroup { 61 | type Target = BindGroup; 62 | 63 | #[inline] 64 | fn deref(&self) -> &Self::Target { 65 | &self.0 66 | } 67 | } 68 | 69 | impl CullingBindGroup { 70 | fn new(device: &RenderDevice, culling_uniform: CullingUniform) -> Self { 71 | let culling_buffer = StaticBuffer::::create( 72 | None, 73 | device, 74 | &culling_uniform, 75 | BufferUsages::UNIFORM, 76 | ); 77 | 78 | let bind_group = device.create_bind_group( 79 | None, 80 | &create_culling_layout(device), 81 | &BindGroupEntries::single(&culling_buffer), 82 | ); 83 | 84 | Self(bind_group) 85 | } 86 | 87 | pub(crate) fn prepare( 88 | device: Res, 89 | gpu_tile_trees: Res>, 90 | extracted_views: Query<&ExtractedView>, 91 | mut culling_bind_groups: ResMut>, 92 | ) { 93 | for &(terrain, view) in gpu_tile_trees.keys() { 94 | let extracted_view = extracted_views.get(view).unwrap(); 95 | 96 | culling_bind_groups.insert( 97 | (terrain, view), 98 | CullingBindGroup::new(&device, extracted_view.into()), 99 | ); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/terrain.rs: -------------------------------------------------------------------------------- 1 | //! Types for configuring terrains. 2 | //! 3 | #[cfg(feature = "high_precision")] 4 | use crate::big_space::{GridCell, GridTransformOwned, ReferenceFrame}; 5 | 6 | use crate::{ 7 | math::TerrainModel, 8 | terrain_data::{tile_atlas::TileAtlas, AttachmentConfig}, 9 | }; 10 | use bevy::{ecs::entity::EntityHashMap, prelude::*, render::view::NoFrustumCulling}; 11 | 12 | /// Resource that stores components that are associated to a terrain entity. 13 | /// This is used to persist components in the render world. 14 | #[derive(Deref, DerefMut, Resource)] 15 | pub struct TerrainComponents(EntityHashMap); 16 | 17 | impl Default for TerrainComponents { 18 | fn default() -> Self { 19 | Self(default()) 20 | } 21 | } 22 | 23 | /// The configuration of a terrain. 24 | /// 25 | /// Here you can define all fundamental parameters of the terrain. 26 | #[derive(Clone)] 27 | pub struct TerrainConfig { 28 | /// The count of level of detail layers. 29 | pub lod_count: u32, 30 | pub model: TerrainModel, 31 | /// The amount of tiles the can be loaded simultaneously in the tile atlas. 32 | pub atlas_size: u32, 33 | /// The path to the terrain folder inside the assets directory. 34 | pub path: String, 35 | /// The attachments of the terrain. 36 | pub attachments: Vec, 37 | } 38 | 39 | impl Default for TerrainConfig { 40 | fn default() -> Self { 41 | Self { 42 | lod_count: 1, 43 | model: TerrainModel::sphere(default(), 1.0, 0.0, 1.0), 44 | atlas_size: 1024, 45 | path: default(), 46 | attachments: default(), 47 | } 48 | } 49 | } 50 | 51 | impl TerrainConfig { 52 | pub fn add_attachment(mut self, attachment_config: AttachmentConfig) -> Self { 53 | self.attachments.push(attachment_config); 54 | self 55 | } 56 | } 57 | 58 | /// The components of a terrain. 59 | /// 60 | /// Does not include loader(s) and a material. 61 | #[derive(Bundle)] 62 | pub struct TerrainBundle { 63 | pub tile_atlas: TileAtlas, 64 | #[cfg(feature = "high_precision")] 65 | pub cell: GridCell, 66 | pub transform: Transform, 67 | pub global_transform: GlobalTransform, 68 | pub visibility_bundle: VisibilityBundle, 69 | pub no_frustum_culling: NoFrustumCulling, 70 | } 71 | 72 | impl TerrainBundle { 73 | /// Creates a new terrain bundle from the config. 74 | 75 | pub fn new( 76 | tile_atlas: TileAtlas, 77 | #[cfg(feature = "high_precision")] frame: &ReferenceFrame, 78 | ) -> Self { 79 | #[cfg(feature = "high_precision")] 80 | let GridTransformOwned { transform, cell } = tile_atlas.model.grid_transform(frame); 81 | #[cfg(not(feature = "high_precision"))] 82 | let transform = tile_atlas.model.transform(); 83 | 84 | Self { 85 | tile_atlas, 86 | transform, 87 | #[cfg(feature = "high_precision")] 88 | cell, 89 | global_transform: default(), 90 | visibility_bundle: VisibilityBundle { 91 | visibility: Visibility::Visible, 92 | inherited_visibility: default(), 93 | view_visibility: default(), 94 | }, 95 | no_frustum_culling: NoFrustumCulling, 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/shaders/preprocess/preprocessing.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_terrain::preprocessing 2 | 3 | const FORMAT_R8: u32 = 2u; 4 | const FORMAT_RGBA8: u32 = 0u; 5 | const FORMAT_R16: u32 = 1u; 6 | 7 | const INVALID_ATLAS_INDEX: u32 = 4294967295u; 8 | 9 | struct TileCoordinate { 10 | side: u32, 11 | lod: u32, 12 | x: u32, 13 | y: u32, 14 | } 15 | 16 | struct AtlasTile { 17 | coordinate: TileCoordinate, 18 | atlas_index: u32, 19 | _padding_a: u32, 20 | _padding_b: u32, 21 | _padding_c: u32, 22 | } 23 | 24 | struct AttachmentMeta { 25 | format_id: u32, 26 | lod_count: u32, 27 | texture_size: u32, 28 | border_size: u32, 29 | center_size: u32, 30 | pixels_per_entry: u32, 31 | entries_per_side: u32, 32 | entries_per_tile: u32, 33 | } 34 | 35 | @group(0) @binding(0) 36 | var atlas_write_section: array; 37 | @group(0) @binding(1) 38 | var atlas: texture_2d_array; 39 | @group(0) @binding(2) 40 | var atlas_sampler: sampler; 41 | @group(0) @binding(3) 42 | var attachment: AttachmentMeta; 43 | 44 | fn inverse_mix(lower: vec2, upper: vec2, value: vec2) -> vec2 { 45 | return (value - lower) / (upper - lower); 46 | } 47 | 48 | fn inside(coords: vec2, bounds: vec4) -> bool { 49 | return coords.x >= bounds.x && 50 | coords.x < bounds.x + bounds.z && 51 | coords.y >= bounds.y && 52 | coords.y < bounds.y + bounds.w; 53 | } 54 | 55 | fn is_border(coords: vec2) -> bool { 56 | return !inside(coords, vec4(attachment.border_size, attachment.border_size, attachment.center_size, attachment.center_size)); 57 | } 58 | 59 | fn pixel_coords(entry_coords: vec3, pixel_offset: u32) -> vec2 { 60 | return vec2(entry_coords.x * attachment.pixels_per_entry + pixel_offset, entry_coords.y); 61 | } 62 | 63 | virtual fn pixel_value(coords: vec2) -> vec4 { return vec4(0.0); } 64 | 65 | fn store_entry(entry_coords: vec3, entry_value: u32) { 66 | let entry_index = entry_coords.z * attachment.entries_per_tile + 67 | entry_coords.y * attachment.entries_per_side + 68 | entry_coords.x; 69 | 70 | atlas_write_section[entry_index] = entry_value; 71 | } 72 | 73 | fn process_entry(entry_coords: vec3) { 74 | if (attachment.format_id == FORMAT_R8) { 75 | let entry_value = pack4x8unorm(vec4(pixel_value(pixel_coords(entry_coords, 0u)).x, 76 | pixel_value(pixel_coords(entry_coords, 1u)).x, 77 | pixel_value(pixel_coords(entry_coords, 2u)).x, 78 | pixel_value(pixel_coords(entry_coords, 3u)).x)); 79 | store_entry(entry_coords, entry_value); 80 | } 81 | if (attachment.format_id == FORMAT_RGBA8) { 82 | let entry_value = pack4x8unorm(pixel_value(pixel_coords(entry_coords, 0u))); 83 | store_entry(entry_coords, entry_value); 84 | } 85 | if (attachment.format_id == FORMAT_R16) { 86 | let entry_value = pack2x16unorm(vec2(pixel_value(pixel_coords(entry_coords, 0u)).x, 87 | pixel_value(pixel_coords(entry_coords, 1u)).x)); 88 | store_entry(entry_coords, entry_value); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/terrain_data/gpu_tile_tree.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | terrain_data::tile_tree::{TileTree, TileTreeEntry}, 3 | terrain_view::TerrainViewComponents, 4 | util::StaticBuffer, 5 | }; 6 | use bevy::{ 7 | prelude::*, 8 | render::{ 9 | render_resource::*, 10 | renderer::{RenderDevice, RenderQueue}, 11 | Extract, 12 | }, 13 | }; 14 | use bytemuck::cast_slice; 15 | use ndarray::{Array2, Array4}; 16 | use std::mem; 17 | 18 | /// Stores the GPU representation of the [`TileTree`] (array texture) 19 | /// alongside the data to update it. 20 | /// 21 | /// The data is synchronized each frame by copying it from the [`TileTree`] to the texture. 22 | #[derive(Component)] 23 | pub struct GpuTileTree { 24 | pub(crate) tile_tree_buffer: StaticBuffer<()>, 25 | pub(crate) origins_buffer: StaticBuffer<()>, 26 | /// The current cpu tile_tree data. This is synced each frame with the tile_tree data. 27 | data: Array4, 28 | origins: Array2, 29 | } 30 | 31 | impl GpuTileTree { 32 | fn new(device: &RenderDevice, tile_tree: &TileTree) -> Self { 33 | let tile_tree_buffer = StaticBuffer::empty_sized( 34 | None, 35 | device, 36 | (tile_tree.data.len() * mem::size_of::()) as BufferAddress, 37 | BufferUsages::STORAGE | BufferUsages::COPY_DST, 38 | ); 39 | 40 | let origins_buffer = StaticBuffer::empty_sized( 41 | None, 42 | device, 43 | (tile_tree.origins.len() * mem::size_of::()) as BufferAddress, 44 | BufferUsages::STORAGE | BufferUsages::COPY_DST, 45 | ); 46 | 47 | Self { 48 | tile_tree_buffer, 49 | origins_buffer, 50 | data: default(), 51 | origins: default(), 52 | } 53 | } 54 | 55 | /// Initializes the [`GpuTileTree`] of newly created terrains. 56 | pub(crate) fn initialize( 57 | device: Res, 58 | mut gpu_tile_trees: ResMut>, 59 | tile_trees: Extract>>, 60 | ) { 61 | for (&(terrain, view), tile_tree) in tile_trees.iter() { 62 | if gpu_tile_trees.contains_key(&(terrain, view)) { 63 | return; 64 | } 65 | 66 | gpu_tile_trees.insert((terrain, view), GpuTileTree::new(&device, tile_tree)); 67 | } 68 | } 69 | 70 | /// Extracts the current data from all [`TileTree`]s into the corresponding [`GpuTileTree`]s. 71 | pub(crate) fn extract( 72 | mut gpu_tile_trees: ResMut>, 73 | tile_trees: Extract>>, 74 | ) { 75 | for (&(terrain, view), tile_tree) in tile_trees.iter() { 76 | let gpu_tile_tree = gpu_tile_trees.get_mut(&(terrain, view)).unwrap(); 77 | 78 | gpu_tile_tree.data = tile_tree.data.clone(); 79 | gpu_tile_tree.origins = tile_tree.origins.clone(); 80 | } 81 | } 82 | 83 | /// Prepares the tile_tree data to be copied into the tile_tree texture. 84 | pub(crate) fn prepare( 85 | queue: Res, 86 | mut gpu_tile_trees: ResMut>, 87 | ) { 88 | for gpu_tile_tree in gpu_tile_trees.values_mut() { 89 | let data = cast_slice(gpu_tile_tree.data.as_slice().unwrap()); 90 | gpu_tile_tree.tile_tree_buffer.update_bytes(&queue, data); 91 | 92 | let origins = cast_slice(gpu_tile_tree.origins.as_slice().unwrap()); 93 | gpu_tile_tree.origins_buffer.update_bytes(&queue, origins); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development Status Bevy Terrain 2 | 3 | This document assesses the current status of the `bevy_terrain` plugin. 4 | I built this plugin as part of my bachelor thesis, which focused on rendering large-scale terrains. 5 | The thesis and its project can be found [here](https://github.com/kurtkuehnert/terrain_renderer). 6 | 7 | For that, I set out to solve two key problems of terrain rendering. 8 | For one, I developed the Uniform Distance-Dependent Level of Detail (UDLOD) algorithm to represent the terrain geometry, and for another, 9 | I came up with the Chunked Clipmap data structure used to represent the terrain data. 10 | Both are implemented as part of bevy terrain and work quite well for rendering large-scale terrains. 11 | 12 | Now that I have finished my thesis I would like to continue working on this project and extend its capabilities. 13 | The topic of terrain rendering is vast, and thus I can not work on all the stuff at once. 14 | In the following, I will list a couple of features that I would like to integrate into this crate in the future. 15 | I will probably not have the time to implement all of them by myself, so if you are interested please get in touch, and let us work on them together. 16 | Additionally, there are still plenty of improvements, bug fixes, and optimizations to be completed on the already existing implementation. 17 | 18 | ## Features 19 | 20 | - Procedural Texturing 21 | - Shadow Rendering 22 | - Real-Time Editing 23 | - Collision 24 | - Path-Finding 25 | - Spherical Terrain 26 | 27 | ### Procedural Texturing 28 | 29 | Probably the biggest missing puzzle piece of this plugin is support for procedural texturing using splat maps or something similar. 30 | Currently, texturing has to be implemented manually in the terrain shader (see the advanced example for reference). 31 | I would like to support this use case in a more integrated manner in the future. Unfortunately, 32 | I am not familiar with the terrain texturing systems of other engines (e.g. Unity, Unreal, Godot) 33 | or have any experience texturing and building my own terrains. 34 | I would greatly appreciate it if anyone can share some requirements for this area of terrain rendering. 35 | Also, a prototype of a custom texturing system would be a great resource to develop further ideas. 36 | 37 | ### Shadow Rendering 38 | 39 | Another important capability that is currently missing is the support for large-scale shadow rendering. 40 | This would be probably implemented using cascading shadow maps or a similar method. 41 | Currently, Bevy itself does not implement a system we could use for this yet. 42 | Regardless, I think reusing Bevy’s implementation would be the best choice in the future. 43 | 44 | ### Real-Time Editing 45 | 46 | One of the most interesting problems that need to be solved before `bevy_terrain` can be used for any serious project is the editing of the terrain data in real time. 47 | This is not only important for sculpting the terrain of your game, but also for texturing, vegetation placement, etc. 48 | This is going to be my next focus area and I would like to discuss designs and additional requirements with anyone interested. 49 | 50 | ### Collision 51 | 52 | Same as for shadow rendering, Bevy does not have a built-in physics engine yet. For now, the de-facto standard is the rapier physics engine. 53 | Integrating the collision of the terrain with rapier would enable many types of games and is a commonly requested feature. 54 | 55 | ### Path-Finding 56 | 57 | Similar to collision, path-finding is essential for most games. I have not investigated this field at all yet, but I am always interested in your ideas. 58 | 59 | ### Spherical Terrain 60 | 61 | I think that with a little design work the current two-dimensional terrain rendering method could be extended to the spherical terrain. 62 | However, I am unsure how much of the existing code could be extended and reused. Maybe planet rendering would require its entirely separate crate. 63 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides the ability to render beautiful height-field terrains of any size. 2 | //! This is achieved in extensible and modular manner, so that the terrain data 3 | //! can be accessed from nearly anywhere (systems, shaders) [^note]. 4 | //! 5 | //! # Background 6 | //! There are three critical questions that each terrain renderer has to solve: 7 | //! 8 | //! ## How to store, manage and access the terrain data? 9 | //! Each terrain has different types of textures associated with it. 10 | //! For example a simple one might only need height and albedo information. 11 | //! Because terrains can be quite large the space required for all of these so called 12 | //! attachments, can/should not be stored in RAM and VRAM all at once. 13 | //! Thus they have to be streamed in and out depending on the positions of the 14 | //! viewers (cameras, lights, etc.). 15 | //! Therefore the terrain is subdivided into a giant tile_tree, whose tiles store their 16 | //! section of these attachments. 17 | //! This crate uses the chunked clipmap data structure, which consist of two pieces working together. 18 | //! The wrapping [`TileTree`](prelude::TileTree) views together with 19 | //! the [`TileAtlas`](prelude::TileAtlas) (the data structure 20 | //! that stores all of the currently loaded data) can be used to efficiently retrieve 21 | //! the best currently available data at any position for terrains of any size. 22 | //! See the [`terrain_data`] module for more information. 23 | //! 24 | //! ## How to best approximate the terrain geometry? 25 | //! Even a small terrain with a height map of 1000x1000 pixels would require 1 million vertices 26 | //! to be rendered each frame per view, with an naive approach without an lod strategy. 27 | //! To better distribute the vertices over the screen there exist many different algorithms. 28 | //! This crate comes with its own default terrain geometry algorithm, called the 29 | //! Uniform Distance-Dependent Level of Detail (UDLOD), which was developed with performance and 30 | //! quality scalability in mind. 31 | //! See the [`render`] module for more information. 32 | //! You can also implement a different algorithm yourself and only use the terrain 33 | //! data structures to solve the first question. 34 | //! 35 | //! ## How to shade the terrain? 36 | //! The third and most important challenge of terrain rendering is the shading. This is a very 37 | //! project specific problem and thus there does not exist a one-size-fits-all solution. 38 | //! You can define your own terrain [Material](bevy::prelude::Material) and shader with all the 39 | //! detail textures tailored to your application. 40 | //! In the future this plugin will provide modular shader functions to make techniques like splat 41 | //! mapping, triplane mapping, etc. easier. 42 | //! Additionally a virtual texturing solution might be integrated to achieve better performance. 43 | //! 44 | //! [^note]: Some of these claims are not yet fully implemented. 45 | 46 | #[cfg(feature = "high_precision")] 47 | pub mod big_space; 48 | pub mod debug; 49 | pub mod formats; 50 | pub mod math; 51 | pub mod plugin; 52 | pub mod preprocess; 53 | pub mod render; 54 | pub mod shaders; 55 | pub mod terrain; 56 | pub mod terrain_data; 57 | pub mod terrain_view; 58 | pub mod util; 59 | 60 | pub mod prelude { 61 | //! `use bevy_terrain::prelude::*;` to import common components, bundles, and plugins. 62 | // #[doc(hidden)] 63 | 64 | #[cfg(feature = "high_precision")] 65 | pub use crate::big_space::{BigSpaceCommands, ReferenceFrame}; 66 | 67 | pub use crate::{ 68 | debug::{ 69 | camera::{DebugCameraBundle, DebugCameraController}, 70 | DebugTerrainMaterial, LoadingImages, TerrainDebugPlugin, 71 | }, 72 | math::TerrainModel, 73 | plugin::TerrainPlugin, 74 | preprocess::{ 75 | preprocessor::Preprocessor, 76 | preprocessor::{PreprocessDataset, SphericalDataset}, 77 | TerrainPreprocessPlugin, 78 | }, 79 | render::terrain_material::TerrainMaterialPlugin, 80 | terrain::{TerrainBundle, TerrainConfig}, 81 | terrain_data::{ 82 | tile_atlas::TileAtlas, tile_tree::TileTree, AttachmentConfig, AttachmentFormat, 83 | }, 84 | terrain_view::{TerrainViewComponents, TerrainViewConfig}, 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /examples/spherical.rs: -------------------------------------------------------------------------------- 1 | use bevy::{math::DVec3, prelude::*, reflect::TypePath, render::render_resource::*}; 2 | use bevy_terrain::prelude::*; 3 | 4 | const PATH: &str = "terrains/spherical"; 5 | const RADIUS: f64 = 6371000.0; 6 | const MAJOR_AXES: f64 = 6378137.0; 7 | const MINOR_AXES: f64 = 6356752.314245; 8 | const MIN_HEIGHT: f32 = -12000.0; 9 | const MAX_HEIGHT: f32 = 9000.0; 10 | const TEXTURE_SIZE: u32 = 512; 11 | const LOD_COUNT: u32 = 16; 12 | 13 | #[derive(Asset, AsBindGroup, TypePath, Clone)] 14 | pub struct TerrainMaterial { 15 | #[texture(0, dimension = "1d")] 16 | #[sampler(1)] 17 | gradient: Handle, 18 | } 19 | 20 | impl Material for TerrainMaterial { 21 | fn fragment_shader() -> ShaderRef { 22 | "shaders/spherical.wgsl".into() 23 | } 24 | } 25 | 26 | fn main() { 27 | App::new() 28 | .add_plugins(( 29 | DefaultPlugins.build().disable::(), 30 | TerrainPlugin, 31 | TerrainMaterialPlugin::::default(), 32 | TerrainDebugPlugin, // enable debug settings and controls 33 | )) 34 | // .insert_resource(ClearColor(Color::WHITE)) 35 | .add_systems(Startup, setup) 36 | .run(); 37 | } 38 | 39 | fn setup( 40 | mut commands: Commands, 41 | mut images: ResMut, 42 | mut meshes: ResMut>, 43 | mut materials: ResMut>, 44 | mut tile_trees: ResMut>, 45 | asset_server: Res, 46 | ) { 47 | let gradient = asset_server.load("textures/gradient.png"); 48 | images.load_image( 49 | &gradient, 50 | TextureDimension::D1, 51 | TextureFormat::Rgba8UnormSrgb, 52 | ); 53 | 54 | // Configure all the important properties of the terrain, as well as its attachments. 55 | let config = TerrainConfig { 56 | lod_count: LOD_COUNT, 57 | model: TerrainModel::ellipsoid(DVec3::ZERO, MAJOR_AXES, MINOR_AXES, MIN_HEIGHT, MAX_HEIGHT), 58 | // model: TerrainModel::ellipsoid( 59 | // DVec3::ZERO, 60 | // 6378137.0, 61 | // 6378137.0 * 0.5, 62 | // MIN_HEIGHT, 63 | // MAX_HEIGHT, 64 | // ), 65 | // model: TerrainModel::sphere(DVec3::ZERO, RADIUS), 66 | path: PATH.to_string(), 67 | ..default() 68 | } 69 | .add_attachment(AttachmentConfig { 70 | name: "height".to_string(), 71 | texture_size: TEXTURE_SIZE, 72 | border_size: 2, 73 | mip_level_count: 4, 74 | format: AttachmentFormat::R16, 75 | }); 76 | 77 | // Configure the quality settings of the terrain view. Adapt the settings to your liking. 78 | let view_config = TerrainViewConfig::default(); 79 | 80 | let tile_atlas = TileAtlas::new(&config); 81 | let tile_tree = TileTree::new(&tile_atlas, &view_config); 82 | 83 | commands.spawn_big_space(ReferenceFrame::default(), |root| { 84 | let frame = root.frame().clone(); 85 | 86 | let terrain = root 87 | .spawn_spatial(( 88 | TerrainBundle::new(tile_atlas, &frame), 89 | materials.add(TerrainMaterial { 90 | gradient: gradient.clone(), 91 | }), 92 | )) 93 | .id(); 94 | 95 | let view = root 96 | .spawn_spatial(DebugCameraBundle::new( 97 | -DVec3::X * RADIUS * 3.0, 98 | RADIUS, 99 | &frame, 100 | )) 101 | .id(); 102 | 103 | tile_trees.insert((terrain, view), tile_tree); 104 | 105 | let sun_position = DVec3::new(-1.0, 1.0, -1.0) * RADIUS * 10.0; 106 | let (sun_cell, sun_translation) = frame.translation_to_grid(sun_position); 107 | 108 | root.spawn_spatial(( 109 | PbrBundle { 110 | mesh: meshes.add(Sphere::new(RADIUS as f32 * 2.0).mesh().build()), 111 | transform: Transform::from_translation(sun_translation), 112 | ..default() 113 | }, 114 | sun_cell, 115 | )); 116 | 117 | root.spawn_spatial(PbrBundle { 118 | mesh: meshes.add(Cuboid::from_length(RADIUS as f32 * 0.1)), 119 | ..default() 120 | }); 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /src/shaders/render/vertex.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_terrain::vertex 2 | 3 | #import bevy_terrain::types::{Blend, AtlasTile, Coordinate} 4 | #import bevy_terrain::bindings::{config, view_config, geometry_tiles, terrain_model_approximation} 5 | #import bevy_terrain::functions::{lookup_tile, compute_tile_uv, compute_local_position, compute_relative_position, compute_morph, compute_blend, normal_local_to_world, position_local_to_world} 6 | #import bevy_terrain::attachments::{sample_height} 7 | #import bevy_pbr::mesh_view_bindings::view 8 | #import bevy_pbr::view_transformations::position_world_to_clip 9 | 10 | struct VertexInput { 11 | @builtin(vertex_index) vertex_index: u32, 12 | } 13 | 14 | struct VertexOutput { 15 | @builtin(position) clip_position: vec4, 16 | @location(0) tile_index: u32, 17 | @location(1) coordinate_uv: vec2, 18 | @location(2) world_position: vec4, 19 | @location(3) world_normal: vec3, 20 | } 21 | 22 | struct VertexInfo { 23 | tile_index: u32, 24 | coordinate: Coordinate, 25 | world_position: vec3, 26 | world_normal: vec3, 27 | blend: Blend, 28 | } 29 | 30 | fn vertex_info(input: VertexInput) -> VertexInfo { 31 | let tile_index = input.vertex_index / view_config.vertices_per_tile; 32 | let tile = geometry_tiles[tile_index]; 33 | let tile_uv = compute_tile_uv(input.vertex_index); 34 | let approximate_coordinate = Coordinate(tile.side, tile.lod, tile.xy, tile_uv); 35 | let approximate_local_position = compute_local_position(approximate_coordinate); 36 | let approximate_world_position = position_local_to_world(approximate_local_position); 37 | let approximate_world_normal = normal_local_to_world(approximate_local_position); 38 | var approximate_view_distance = distance(approximate_world_position + terrain_model_approximation.approximate_height * approximate_world_normal, view.world_position); 39 | 40 | #ifdef HIGH_PRECISION 41 | let high_precision = approximate_view_distance < view_config.precision_threshold_distance; 42 | #else 43 | let high_precision = false; 44 | #endif 45 | 46 | var coordinate: Coordinate; var world_position: vec3; var world_normal: vec3; 47 | 48 | if (high_precision) { 49 | let approximate_relative_position = compute_relative_position(approximate_coordinate); 50 | approximate_view_distance = length(approximate_relative_position + terrain_model_approximation.approximate_height * approximate_world_normal); 51 | 52 | coordinate = compute_morph(approximate_coordinate, approximate_view_distance); 53 | let relative_position = compute_relative_position(coordinate); 54 | world_position = view.world_position + relative_position; 55 | world_normal = approximate_world_normal; 56 | } else { 57 | coordinate = compute_morph(approximate_coordinate, approximate_view_distance); 58 | let local_position = compute_local_position(coordinate); 59 | world_position = position_local_to_world(local_position); 60 | world_normal = normal_local_to_world(local_position); 61 | } 62 | 63 | var info: VertexInfo; 64 | info.tile_index = tile_index; 65 | info.coordinate = coordinate; 66 | info.world_position = world_position; 67 | info.world_normal = world_normal; 68 | info.blend = compute_blend(approximate_view_distance); 69 | 70 | return info; 71 | } 72 | 73 | fn vertex_output(info: ptr, height: f32) -> VertexOutput { 74 | let world_position = (*info).world_position + height * (*info).world_normal; 75 | 76 | var output: VertexOutput; 77 | output.clip_position = position_world_to_clip(world_position); 78 | output.tile_index = (*info).tile_index; 79 | output.coordinate_uv = (*info).coordinate.uv; 80 | output.world_position = vec4(world_position, 1.0); 81 | output.world_normal = (*info).world_normal; 82 | return output; 83 | } 84 | 85 | @vertex 86 | fn vertex(input: VertexInput) -> VertexOutput { 87 | var info = vertex_info(input); 88 | 89 | let tile = lookup_tile(info.coordinate, info.blend, 0u); 90 | var height = sample_height(tile); 91 | 92 | if (info.blend.ratio > 0.0) { 93 | let tile2 = lookup_tile(info.coordinate, info.blend, 1u); 94 | height = mix(height, sample_height(tile2), info.blend.ratio); 95 | } 96 | 97 | return vertex_output(&info, height); 98 | } 99 | -------------------------------------------------------------------------------- /src/math/ellipsoid.rs: -------------------------------------------------------------------------------- 1 | use bevy::math::{DVec2, DVec3, Vec3Swizzles}; 2 | use std::cmp::Ordering; 3 | 4 | // Adapted from https://www.geometrictools.com/Documentation/DistancePointEllipseEllipsoid.pdf 5 | // Original licensed under Creative Commons Attribution 4.0 International License 6 | // http://creativecommons.org/licenses/by/4.0/ 7 | 8 | // After 1074 iterations, s0 == s1 == s 9 | // This should probably be relaxed to only limit the error s1-s0 to a constant e. 10 | const MAX_ITERATIONS: usize = 1074; 11 | 12 | pub fn project_point_ellipsoid(e: DVec3, y: DVec3) -> DVec3 { 13 | let sign = y.signum(); 14 | let y = y.xzy().abs(); 15 | 16 | let x = if y.z > 0.0 { 17 | if y.y > 0.0 { 18 | if y.x > 0.0 { 19 | let z = y / e; 20 | let g = z.length_squared() - 1.0; 21 | 22 | if g != 0.0 { 23 | let r = DVec3::new((e.x * e.x) / (e.z * e.z), (e.y * e.y) / (e.z * e.z), 1.0); 24 | 25 | r * y / (get_root_3d(r, z, g) + r) 26 | } else { 27 | y 28 | } 29 | } else { 30 | project_point_ellipse(e.yz(), y.yz()).extend(0.0).zxy() 31 | } 32 | } else { 33 | if y.x > 0.0 { 34 | project_point_ellipse(e.xz(), y.xz()).extend(0.0).xzy() 35 | } else { 36 | DVec3::new(0.0, 0.0, e.z) 37 | } 38 | } 39 | } else { 40 | let denom0 = e.x * e.x - e.z * e.z; 41 | let denom1 = e.y * e.y - e.z * e.z; 42 | let numer0 = e.x * y.x; 43 | let numer1 = e.y * y.y; 44 | 45 | let mut x = None; 46 | 47 | if numer0 < denom0 && numer1 < denom1 { 48 | let xde0 = numer0 / denom0; 49 | let xde1 = numer1 / denom1; 50 | let xde0sqr = xde0 * xde0; 51 | let xde1sqr = xde1 * xde1; 52 | let discr = 1.0 - xde0sqr - xde1sqr; 53 | 54 | if discr > 0.0 { 55 | x = Some(e * DVec3::new(xde0, xde1, discr.sqrt())); 56 | } 57 | } 58 | 59 | x.unwrap_or_else(|| project_point_ellipse(e.xy(), y.xy()).extend(0.0)) 60 | }; 61 | 62 | sign * x.xzy() 63 | } 64 | 65 | fn project_point_ellipse(e: DVec2, y: DVec2) -> DVec2 { 66 | if y.y > 0.0 { 67 | if y.x > 0.0 { 68 | let z = y / e; 69 | let g = z.length_squared() - 1.0; 70 | 71 | if g != 0.0 { 72 | let r = DVec2::new((e.x * e.x) / (e.y * e.y), 1.0); 73 | r * y / (get_root_2d(r, z, g) + r) 74 | } else { 75 | y 76 | } 77 | } else { 78 | DVec2::new(0.0, e.y) 79 | } 80 | } else { 81 | let numer0 = e.x * y.x; 82 | let denom0 = e.x * e.x - e.y * e.y; 83 | if numer0 < denom0 { 84 | let xde0 = numer0 / denom0; 85 | DVec2::new(e.x * xde0, e.y * (1.0 - xde0 * xde0).sqrt()) 86 | } else { 87 | DVec2::new(e.x, 0.0) 88 | } 89 | } 90 | } 91 | 92 | fn get_root_3d(r: DVec3, z: DVec3, g: f64) -> f64 { 93 | let n = r * z; 94 | 95 | let mut s0 = z.z - 1.0; 96 | let mut s1 = if g < 0.0 { 0.0 } else { n.length() - 1.0 }; 97 | let mut s = 0.0; 98 | 99 | for _ in 0..MAX_ITERATIONS { 100 | s = (s0 + s1) / 2.0; 101 | if s == s0 || s == s1 { 102 | break; 103 | } 104 | 105 | let ratio = n / (s + r); 106 | let g = ratio.length_squared() - 1.0; 107 | 108 | match g.total_cmp(&0.0) { 109 | Ordering::Less => s1 = s, 110 | Ordering::Equal => break, 111 | Ordering::Greater => s0 = s, 112 | } 113 | } 114 | 115 | s 116 | } 117 | 118 | fn get_root_2d(r: DVec2, z: DVec2, g: f64) -> f64 { 119 | let n = r * z; 120 | 121 | let mut s0 = z.y - 1.0; 122 | let mut s1 = if g < 0.0 { 0.0 } else { n.length() - 1.0 }; 123 | let mut s = 0.0; 124 | 125 | for _ in 0..MAX_ITERATIONS { 126 | s = (s0 + s1) / 2.0; 127 | if s == s0 || s == s1 { 128 | break; 129 | } 130 | 131 | let ratio = n / (s + r); 132 | let g = ratio.length_squared() - 1.0; 133 | 134 | match g.total_cmp(&0.0) { 135 | Ordering::Less => s1 = s, 136 | Ordering::Equal => break, 137 | Ordering::Greater => s0 = s, 138 | } 139 | } 140 | 141 | s 142 | } 143 | -------------------------------------------------------------------------------- /src/plugin.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | math::{generate_terrain_model_approximation, TerrainModelApproximation}, 3 | render::{ 4 | culling_bind_group::CullingBindGroup, 5 | terrain_bind_group::TerrainData, 6 | terrain_view_bind_group::TerrainViewData, 7 | tiling_prepass::{ 8 | queue_tiling_prepass, TilingPrepassItem, TilingPrepassLabel, TilingPrepassNode, 9 | TilingPrepassPipelines, 10 | }, 11 | }, 12 | shaders::{load_terrain_shaders, InternalShaders}, 13 | terrain::TerrainComponents, 14 | terrain_data::{ 15 | gpu_tile_atlas::GpuTileAtlas, gpu_tile_tree::GpuTileTree, tile_atlas::TileAtlas, 16 | tile_tree::TileTree, 17 | }, 18 | terrain_view::TerrainViewComponents, 19 | }; 20 | use bevy::{ 21 | prelude::*, 22 | render::{ 23 | graph::CameraDriverLabel, 24 | render_graph::RenderGraph, 25 | render_resource::*, 26 | view::{check_visibility, VisibilitySystems}, 27 | Render, RenderApp, RenderSet, 28 | }, 29 | }; 30 | 31 | /// The plugin for the terrain renderer. 32 | pub struct TerrainPlugin; 33 | 34 | impl Plugin for TerrainPlugin { 35 | fn build(&self, app: &mut App) { 36 | #[cfg(feature = "high_precision")] 37 | app.add_plugins(crate::big_space::BigSpacePlugin::default()); 38 | 39 | app.init_resource::() 40 | .init_resource::>() 41 | .init_resource::>() 42 | .add_systems( 43 | PostUpdate, 44 | check_visibility::>.in_set(VisibilitySystems::CheckVisibility), 45 | ) 46 | .add_systems( 47 | Last, 48 | ( 49 | TileTree::compute_requests, 50 | TileAtlas::update, 51 | TileTree::adjust_to_tile_atlas, 52 | TileTree::approximate_height, 53 | generate_terrain_model_approximation, 54 | ) 55 | .chain(), 56 | ); 57 | 58 | app.sub_app_mut(RenderApp) 59 | .init_resource::>() 60 | .init_resource::>() 61 | .init_resource::>() 62 | .init_resource::>() 63 | .init_resource::>() 64 | .init_resource::>() 65 | .add_systems( 66 | ExtractSchedule, 67 | ( 68 | GpuTileAtlas::initialize, 69 | GpuTileAtlas::extract.after(GpuTileAtlas::initialize), 70 | GpuTileTree::initialize, 71 | GpuTileTree::extract.after(GpuTileTree::initialize), 72 | TerrainData::initialize.after(GpuTileAtlas::initialize), 73 | TerrainData::extract.after(TerrainData::initialize), 74 | TerrainViewData::initialize.after(GpuTileTree::initialize), 75 | TerrainViewData::extract.after(TerrainViewData::initialize), 76 | ), 77 | ) 78 | .add_systems( 79 | Render, 80 | ( 81 | ( 82 | GpuTileTree::prepare, 83 | GpuTileAtlas::prepare, 84 | TerrainData::prepare, 85 | TerrainViewData::prepare, 86 | CullingBindGroup::prepare, 87 | ) 88 | .in_set(RenderSet::Prepare), 89 | queue_tiling_prepass.in_set(RenderSet::Queue), 90 | GpuTileAtlas::cleanup 91 | .before(World::clear_entities) 92 | .in_set(RenderSet::Cleanup), 93 | ), 94 | ); 95 | } 96 | 97 | fn finish(&self, app: &mut App) { 98 | load_terrain_shaders(app); 99 | 100 | let render_app = app 101 | .sub_app_mut(RenderApp) 102 | .init_resource::() 103 | .init_resource::>(); 104 | 105 | let mut render_graph = render_app.world_mut().resource_mut::(); 106 | render_graph.add_node(TilingPrepassLabel, TilingPrepassNode); 107 | render_graph.add_node_edge(TilingPrepassLabel, CameraDriverLabel); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bevy Terrain 2 | 3 | ![GitHub](https://img.shields.io/github/license/Ku95/bevy_terrain) 4 | ![Crates.io](https://img.shields.io/crates/v/bevy_terrain) 5 | ![docs.rs](https://img.shields.io/docsrs/bevy_terrain) 6 | ![Discord](https://img.shields.io/discord/999221999517843456?label=discord) 7 | 8 | Bevy Terrain is a plugin for rendering terrains with the Bevy game engine. 9 | 10 | ![](https://user-images.githubusercontent.com/51823519/202845032-0537e929-b13c-410b-8072-4c5b5df9830d.png) 11 | (Data Source: Federal Office of Topography, [©swisstopo](https://www.swisstopo.admin.ch/en/home.html)) 12 | 13 | **Warning:** This plugin is still in early development, so expect the API to change and possibly break you existing 14 | code. 15 | 16 | Bevy terrain was developed as part of my [bachelor thesis](https://github.com/kurtkuehnert/terrain_renderer) on the 17 | topic of large-scale terrain rendering. 18 | Now that this project is finished I am planning on adding more features related to game development and rendering 19 | virtual worlds. 20 | If you would like to help me build an extensive open-source terrain rendering library for the Bevy game engine, feel 21 | free to contribute to the project. 22 | Also, join the Bevy Terrain [Discord server](https://discord.gg/7mtZWEpA82) for help, feedback, or to discuss feature 23 | ideas. 24 | 25 | ## Examples 26 | 27 | Currently, there are two examples. 28 | 29 | The basic one showcases the different debug views of the terrain. See controls down below. 30 | 31 | The advanced one showcases how to use the Bevy material system for texturing, 32 | as well as how to add additional terrain attachments. 33 | Use the `A` Key to toggle between the custom material and the albedo attachment. 34 | 35 | Before running the examples you have to preprocess the terrain data this may take a while. 36 | Once the data is preprocessed you can disable it by commenting out the preprocess line. 37 | 38 | ## Documentation 39 | 40 | The `docs` folder contains a 41 | high-level [implementation overview](https://github.com/kurtkuehnert/bevy_terrain/blob/main/docs/implementation.md), 42 | as well as, the [development status](https://github.com/kurtkuehnert/bevy_terrain/blob/main/docs/development.md), 43 | enumerating the features that I am planning on implementing next, of the project. 44 | If you would like to contribute to the project this is a good place to start. Simply pick an issue/feature and discuss 45 | the details with me on Discord or GitHub. 46 | I would also recommend you to take a look at 47 | my [thesis](https://github.com/kurtkuehnert/terrain_renderer/blob/main/Thesis.pdf). 48 | There I present the basics of terrain rendering (chapter 2), common approaches (chapter 3) and a detailed explanation of 49 | method used by `bevy_terrain` (chapter 4). 50 | 51 | ## Debug Controls 52 | 53 | These are the debug controls of the plugin. 54 | Use them to fly over the terrain, experiment with the quality settings and enter the different debug views. 55 | 56 | - `T` - toggle camera movement 57 | - move the mouse to look around 58 | - press the arrow keys to move the camera horizontally 59 | - use `PageUp` and `PageDown` to move the camera vertically 60 | - use `Home` and `End` to increase/decrease the camera's movement speed 61 | 62 | - `W` - toggle wireframe view 63 | - `P` - toggle tile view 64 | - `L` - toggle lod view 65 | - `U` - toggle uv view 66 | - `C` - toggle tile view 67 | - `D` - toggle mesh morph 68 | - `A` - toggle albedo 69 | - `B` - toggle base color black / white 70 | - `S` - toggle lighting 71 | - `G` - toggle filtering bilinear / trilinear + anisotropic 72 | - `F` - freeze frustum culling 73 | - `H` - decrease tile scale 74 | - `J` - increase tile scale 75 | - `N` - decrease grid size 76 | - `E` - increase grid size 77 | - `I` - decrease view distance 78 | - `O` - increase view distance 79 | 80 | 87 | 88 | ## Attribution 89 | 90 | The planar terrain dataset is generated using the free version of the Gaia Terrain Generator. 91 | The spherical terrain example dataset is a reprojected version of the GEBCO_2023 Grid dataset. 92 | 93 | GEBCO Compilation Group (2023) GEBCO 2023 Grid (doi:10.5285/f98b053b-0cbc-6c23-e053-6c86abc0af7b) 94 | 95 | ## License 96 | 97 | Bevy Terrain source code (this excludes the datasets in the assets directory) is dual-licensed under either 98 | 99 | * MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT) 100 | * Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) 101 | 102 | at your option. 103 | -------------------------------------------------------------------------------- /src/shaders/render/fragment.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_terrain::fragment 2 | 3 | #import bevy_terrain::types::{Blend, AtlasTile, Coordinate} 4 | #import bevy_terrain::bindings::{config, view_config, geometry_tiles} 5 | #import bevy_terrain::functions::{compute_blend, lookup_tile} 6 | #import bevy_terrain::attachments::{sample_normal, sample_color} 7 | #import bevy_terrain::debug::{show_data_lod, show_geometry_lod, show_tile_tree, show_pixels} 8 | #import bevy_pbr::mesh_view_bindings::view 9 | #import bevy_pbr::pbr_types::{PbrInput, pbr_input_new} 10 | #import bevy_pbr::pbr_functions::{calculate_view, apply_pbr_lighting} 11 | 12 | struct FragmentInput { 13 | @builtin(position) clip_position: vec4, 14 | @location(0) tile_index: u32, 15 | @location(1) coordinate_uv: vec2, 16 | @location(2) world_position: vec4, 17 | @location(3) world_normal: vec3, 18 | } 19 | 20 | struct FragmentOutput { 21 | @location(0) color: vec4 22 | } 23 | 24 | struct FragmentInfo { 25 | coordinate: Coordinate, 26 | view_distance: f32, 27 | blend: Blend, 28 | clip_position: vec4, 29 | world_normal: vec3, 30 | world_position: vec4, 31 | color: vec4, 32 | normal: vec3, 33 | } 34 | 35 | fn fragment_info(input: FragmentInput) -> FragmentInfo{ 36 | let tile = geometry_tiles[input.tile_index]; 37 | let uv = input.coordinate_uv; 38 | let view_distance = distance(input.world_position.xyz, view.world_position); 39 | 40 | var info: FragmentInfo; 41 | info.coordinate = Coordinate(tile.side, tile.lod, tile.xy, uv, dpdx(uv), dpdy(uv)); 42 | info.view_distance = view_distance; 43 | info.blend = compute_blend(view_distance); 44 | info.clip_position = input.clip_position; 45 | info.world_normal = input.world_normal; 46 | info.world_position = input.world_position; 47 | 48 | return info; 49 | } 50 | 51 | fn fragment_output(info: ptr, output: ptr, color: vec4, normal: vec3) { 52 | #ifdef LIGHTING 53 | var pbr_input: PbrInput = pbr_input_new(); 54 | pbr_input.material.base_color = color; 55 | pbr_input.material.perceptual_roughness = 1.0; 56 | pbr_input.material.reflectance = 0.0; 57 | pbr_input.frag_coord = (*info).clip_position; 58 | pbr_input.world_position = (*info).world_position; 59 | pbr_input.world_normal = (*info).world_normal; 60 | pbr_input.N = normal; 61 | pbr_input.V = calculate_view((*info).world_position, pbr_input.is_orthographic); 62 | 63 | (*output).color = apply_pbr_lighting(pbr_input); 64 | #else 65 | (*output).color = color; 66 | #endif 67 | } 68 | 69 | fn fragment_debug(info: ptr, output: ptr, tile: AtlasTile, normal: vec3) { 70 | #ifdef SHOW_DATA_LOD 71 | (*output).color = show_data_lod((*info).blend, tile); 72 | #endif 73 | #ifdef SHOW_GEOMETRY_LOD 74 | (*output).color = show_geometry_lod((*info).coordinate); 75 | #endif 76 | #ifdef SHOW_TILE_TREE 77 | (*output).color = show_tile_tree((*info).coordinate); 78 | #endif 79 | #ifdef SHOW_PIXELS 80 | (*output).color = mix((*output).color, show_pixels(tile), 0.5); 81 | #endif 82 | #ifdef SHOW_UV 83 | (*output).color = vec4(tile.coordinate.uv, 0.0, 1.0); 84 | #endif 85 | #ifdef SHOW_NORMALS 86 | (*output).color = vec4(normal, 1.0); 87 | #endif 88 | 89 | // Todo: move this somewhere else 90 | if ((*info).view_distance < view_config.precision_threshold_distance) { 91 | (*output).color = mix((*output).color, vec4(0.1), 0.7); 92 | } 93 | } 94 | 95 | @fragment 96 | fn fragment(input: FragmentInput) -> FragmentOutput { 97 | var info = fragment_info(input); 98 | 99 | let tile = lookup_tile(info.coordinate, info.blend, 0u); 100 | var color = sample_color(tile); 101 | var normal = sample_normal(tile, info.world_normal); 102 | 103 | if (info.blend.ratio > 0.0) { 104 | let tile2 = lookup_tile(info.coordinate, info.blend, 1u); 105 | color = mix(color, sample_color(tile2), info.blend.ratio); 106 | normal = mix(normal, sample_normal(tile2, info.world_normal), info.blend.ratio); 107 | } 108 | 109 | var output: FragmentOutput; 110 | fragment_output(&info, &output, color, normal); 111 | fragment_debug(&info, &output, tile, normal); 112 | return output; 113 | } 114 | 115 | -------------------------------------------------------------------------------- /src/shaders/debug.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_terrain::debug 2 | 3 | #import bevy_terrain::types::{Coordinate, AtlasTile, Blend} 4 | #import bevy_terrain::bindings::{config, tile_tree, view_config, geometry_tiles, attachments, origins, terrain_model_approximation} 5 | #import bevy_terrain::functions::{inverse_mix, compute_coordinate, lookup_best, approximate_view_distance, compute_blend, tree_lod, inside_square, tile_coordinate, coordinate_from_local_position, compute_subdivision_coordinate} 6 | #import bevy_pbr::mesh_view_bindings::view 7 | 8 | fn index_color(index: u32) -> vec4 { 9 | var COLOR_ARRAY = array( 10 | vec4(1.0, 0.0, 0.0, 1.0), 11 | vec4(0.0, 1.0, 0.0, 1.0), 12 | vec4(0.0, 0.0, 1.0, 1.0), 13 | vec4(1.0, 1.0, 0.0, 1.0), 14 | vec4(1.0, 0.0, 1.0, 1.0), 15 | vec4(0.0, 1.0, 1.0, 1.0), 16 | ); 17 | 18 | return mix(COLOR_ARRAY[index % 6u], vec4(0.6), 0.2); 19 | } 20 | 21 | fn tile_tree_outlines(uv: vec2) -> f32 { 22 | let thickness = 0.015; 23 | 24 | return 1.0 - inside_square(uv, vec2(thickness), 1.0 - 2.0 * thickness); 25 | } 26 | 27 | fn checker_color(coordinate: Coordinate, ratio: f32) -> vec4 { 28 | var color = index_color(coordinate.lod); 29 | var parent_color = index_color(coordinate.lod - 1); 30 | color = select(color, mix(color, vec4(0.0), 0.5), (coordinate.xy.x + coordinate.xy.y) % 2u == 0u); 31 | parent_color = select(parent_color, mix(parent_color, vec4(0.0), 0.5), ((coordinate.xy.x >> 1) + (coordinate.xy.y >> 1)) % 2u == 0u); 32 | 33 | return mix(color, parent_color, ratio); 34 | } 35 | 36 | fn show_data_lod(blend: Blend, tile: AtlasTile) -> vec4 { 37 | #ifdef TILE_TREE_LOD 38 | let ratio = 0.0; 39 | #else 40 | let ratio = select(0.0, blend.ratio, blend.lod == tile.coordinate.lod); 41 | #endif 42 | 43 | var color = checker_color(tile.coordinate, ratio); 44 | 45 | if (ratio > 0.95 && blend.lod == tile.coordinate.lod) { 46 | color = mix(color, vec4(0.0), 0.8); 47 | } 48 | 49 | #ifdef SPHERICAL 50 | color = mix(color, index_color(tile.coordinate.side), 0.3); 51 | #endif 52 | 53 | return color; 54 | } 55 | 56 | fn show_geometry_lod(coordinate: Coordinate) -> vec4 { 57 | let view_distance = approximate_view_distance(coordinate, view.world_position); 58 | let target_lod = log2(2.0 * view_config.morph_distance / view_distance); 59 | 60 | #ifdef MORPH 61 | let ratio = select(inverse_mix(f32(coordinate.lod) + view_config.morph_range, f32(coordinate.lod), target_lod), 0.0, coordinate.lod == 0); 62 | #else 63 | let ratio = 0.0; 64 | #endif 65 | 66 | var color = checker_color(coordinate, ratio); 67 | 68 | if (distance(coordinate.uv, compute_subdivision_coordinate(coordinate).uv) < 0.1) { 69 | color = mix(index_color(coordinate.lod + 1), vec4(0.0), 0.7); 70 | } 71 | 72 | if (fract(target_lod) < 0.01 && target_lod >= 1.0) { 73 | color = mix(color, vec4(0.0), 0.8); 74 | } 75 | 76 | #ifdef SPHERICAL 77 | color = mix(color, index_color(coordinate.side), 0.3); 78 | #endif 79 | 80 | if (max(0.0, target_lod) < f32(coordinate.lod) - 1.0 + view_config.morph_range) { 81 | // The view_distance and morph range are not sufficient. 82 | // The same tile overlapps two morph zones. 83 | // -> increase morph distance 84 | color = vec4(1.0, 0.0, 0.0, 1.0); 85 | } 86 | if (floor(target_lod) > f32(coordinate.lod)) { 87 | // The view_distance and morph range are not sufficient. 88 | // The tile does have an insuffient LOD. 89 | // -> increase morph tolerance 90 | color = vec4(0.0, 1.0, 0.0, 1.0); 91 | } 92 | 93 | return color; 94 | } 95 | fn show_tile_tree(coordinate: Coordinate) -> vec4 { 96 | let view_distance = approximate_view_distance(coordinate, view.world_position); 97 | let target_lod = log2(view_config.load_distance / view_distance); 98 | 99 | let best_lookup = lookup_best(coordinate); 100 | 101 | var color = checker_color(best_lookup.tile.coordinate, 0.0); 102 | color = mix(color, vec4(0.1), tile_tree_outlines(best_lookup.tile_tree_uv)); 103 | 104 | if (fract(target_lod) < 0.01 && target_lod >= 1.0) { 105 | color = mix(index_color(u32(target_lod)), vec4(0.0), 0.8); 106 | } 107 | 108 | return color; 109 | } 110 | 111 | fn show_pixels(tile: AtlasTile) -> vec4 { 112 | let pixel_size = 4.0; 113 | let pixel_coordinate = tile.coordinate.uv * f32(attachments[0].size) / pixel_size; 114 | 115 | let is_even = (u32(pixel_coordinate.x) + u32(pixel_coordinate.y)) % 2u == 0u; 116 | 117 | if (is_even) { return vec4(0.5, 0.5, 0.5, 1.0); } 118 | else { return vec4(0.1, 0.1, 0.1, 1.0); } 119 | } 120 | -------------------------------------------------------------------------------- /src/shaders/preprocess/stitch.wgsl: -------------------------------------------------------------------------------- 1 | #import bevy_terrain::preprocessing::{AtlasTile, INVALID_ATLAS_INDEX, atlas, attachment, inside, pixel_coords, pixel_value, process_entry, is_border} 2 | 3 | struct StitchData { 4 | tile: AtlasTile, 5 | neighbour_tiles: array, 6 | tile_index: u32, 7 | } 8 | 9 | @group(1) @binding(0) 10 | var stitch_data: StitchData; 11 | 12 | fn project_to_side(coords: vec2, original_side: u32, projected_side: u32) -> vec2 { 13 | let PS = 0u; 14 | let PT = 1u; 15 | let NS = 2u; 16 | let NT = 3u; 17 | 18 | var EVEN_LIST = array( 19 | vec2(PS, PT), 20 | vec2(PS, PT), 21 | vec2(NT, PS), 22 | vec2(NT, NS), 23 | vec2(PT ,NS), 24 | vec2(PS, PT), 25 | ); 26 | var ODD_LIST = array( 27 | vec2(PS, PT), 28 | vec2(PS, PT), 29 | vec2(PT, NS), 30 | vec2(PT, PS), 31 | vec2(NT, PS), 32 | vec2(PS, PT), 33 | ); 34 | 35 | let index = (6u + projected_side - original_side) % 6u; 36 | let info: vec2 = select(ODD_LIST[index], EVEN_LIST[index], original_side % 2u == 0u); 37 | 38 | var neighbour_coords: vec2; 39 | 40 | if (info.x == PS) { neighbour_coords.x = coords.x; } 41 | else if (info.x == PT) { neighbour_coords.x = coords.y; } 42 | else if (info.x == NS) { neighbour_coords.x = attachment.texture_size - 1u - coords.x; } 43 | else if (info.x == NT) { neighbour_coords.x = attachment.texture_size - 1u - coords.y; } 44 | 45 | if (info.y == PS) { neighbour_coords.y = coords.x; } 46 | else if (info.y == PT) { neighbour_coords.y = coords.y; } 47 | else if (info.y == NS) { neighbour_coords.y = attachment.texture_size - 1u - coords.x; } 48 | else if (info.y == NT) { neighbour_coords.y = attachment.texture_size - 1u - coords.y; } 49 | 50 | return neighbour_coords; 51 | } 52 | 53 | fn neighbour_index(coords: vec2) -> u32 { 54 | let center_size = attachment.center_size; 55 | let border_size = attachment.border_size; 56 | let offset_size = attachment.border_size + attachment.center_size; 57 | 58 | var bounds = array( 59 | vec4(border_size, 0u, center_size, border_size), 60 | vec4(offset_size, border_size, border_size, center_size), 61 | vec4(border_size, offset_size, center_size, border_size), 62 | vec4( 0u, border_size, border_size, center_size), 63 | vec4( 0u, 0u, border_size, border_size), 64 | vec4(offset_size, 0u, border_size, border_size), 65 | vec4(offset_size, offset_size, border_size, border_size), 66 | vec4( 0u, offset_size, border_size, border_size) 67 | ); 68 | 69 | for (var neighbour_index = 0u; neighbour_index < 8u; neighbour_index += 1u) { 70 | if (inside(coords, bounds[neighbour_index])) { return neighbour_index; } 71 | } 72 | 73 | return 0u; 74 | } 75 | 76 | fn neighbour_data(coords: vec2, neighbour_index: u32) -> vec4 { 77 | let center_size = i32(attachment.center_size); 78 | 79 | var offsets = array( 80 | vec2( 0, center_size), 81 | vec2(-center_size, 0), 82 | vec2( 0, -center_size), 83 | vec2( center_size, 0), 84 | vec2( center_size, center_size), 85 | vec2(-center_size, center_size), 86 | vec2(-center_size, -center_size), 87 | vec2( center_size, -center_size) 88 | ); 89 | 90 | let neighbour_tile = stitch_data.neighbour_tiles[neighbour_index]; 91 | let neighbour_coords = project_to_side(vec2(vec2(coords) + offsets[neighbour_index]), 92 | stitch_data.tile.coordinate.side, 93 | neighbour_tile.coordinate.side); 94 | 95 | return textureLoad(atlas, neighbour_coords, neighbour_tile.atlas_index, 0); 96 | } 97 | 98 | fn repeat_data(coords: vec2) -> vec4 { 99 | let repeat_coords = clamp(coords, vec2(attachment.border_size), 100 | vec2(attachment.border_size + attachment.center_size - 1u)); 101 | 102 | return textureLoad(atlas, repeat_coords, stitch_data.tile.atlas_index, 0); 103 | } 104 | 105 | override fn pixel_value(coords: vec2) -> vec4 { 106 | if (!is_border(coords)) { 107 | return textureLoad(atlas, coords, stitch_data.tile.atlas_index, 0); 108 | } 109 | 110 | let neighbour_index = neighbour_index(coords); 111 | 112 | if (stitch_data.neighbour_tiles[neighbour_index].atlas_index == INVALID_ATLAS_INDEX) { 113 | return repeat_data(coords); 114 | } 115 | else { 116 | return neighbour_data(coords, neighbour_index); 117 | } 118 | } 119 | 120 | // Todo: respect memory coalescing 121 | @compute @workgroup_size(8, 8, 1) 122 | fn stitch(@builtin(global_invocation_id) invocation_id: vec3) { 123 | process_entry(vec3(invocation_id.xy, stitch_data.tile_index)); 124 | } -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use bevy::render::{ 2 | render_resource::{encase::internal::WriteInto, *}, 3 | renderer::{RenderDevice, RenderQueue}, 4 | }; 5 | use itertools::Itertools; 6 | use std::{fmt::Debug, marker::PhantomData, ops::Deref}; 7 | 8 | pub(crate) fn inverse_mix(a: f32, b: f32, value: f32) -> f32 { 9 | return f32::clamp((value - a) / (b - a), 0.0, 1.0); 10 | } 11 | 12 | pub trait CollectArray: Iterator { 13 | fn collect_array(self) -> [Self::Item; T] 14 | where 15 | Self: Sized, 16 | ::Item: Debug, 17 | { 18 | self.collect_vec().try_into().unwrap() 19 | } 20 | } 21 | 22 | impl CollectArray for T where T: Iterator + ?Sized {} 23 | enum Scratch { 24 | None, 25 | Uniform(encase::UniformBuffer>), 26 | Storage(encase::StorageBuffer>), 27 | } 28 | 29 | impl Scratch { 30 | fn new(usage: BufferUsages) -> Self { 31 | if usage.contains(BufferUsages::UNIFORM) { 32 | Self::Uniform(encase::UniformBuffer::new(Vec::new())) 33 | } else if usage.contains(BufferUsages::STORAGE) { 34 | Self::Storage(encase::StorageBuffer::new(Vec::new())) 35 | } else { 36 | Self::None 37 | } 38 | } 39 | 40 | fn write(&mut self, value: &T) { 41 | match self { 42 | Scratch::None => panic!("Can't write to an buffer without a scratch buffer."), 43 | Scratch::Uniform(scratch) => scratch.write(value).unwrap(), 44 | Scratch::Storage(scratch) => scratch.write(value).unwrap(), 45 | } 46 | } 47 | 48 | fn contents(&self) -> &[u8] { 49 | match self { 50 | Scratch::None => panic!("Can't get the contents of a buffer without a scratch buffer."), 51 | Scratch::Uniform(scratch) => scratch.as_ref(), 52 | Scratch::Storage(scratch) => scratch.as_ref(), 53 | } 54 | } 55 | } 56 | 57 | pub struct StaticBuffer { 58 | buffer: Buffer, 59 | value: Option, 60 | scratch: Scratch, 61 | _marker: PhantomData, 62 | } 63 | 64 | impl StaticBuffer { 65 | pub fn empty_sized<'a>( 66 | label: impl Into>, 67 | device: &RenderDevice, 68 | size: BufferAddress, 69 | usage: BufferUsages, 70 | ) -> Self { 71 | let buffer = device.create_buffer(&BufferDescriptor { 72 | label: label.into(), 73 | size, 74 | usage, 75 | mapped_at_creation: false, 76 | }); 77 | 78 | Self { 79 | buffer, 80 | value: None, 81 | scratch: Scratch::new(usage), 82 | _marker: PhantomData, 83 | } 84 | } 85 | 86 | pub fn update_bytes(&self, queue: &RenderQueue, bytes: &[u8]) { 87 | queue.write_buffer(&self.buffer, 0, bytes); 88 | } 89 | } 90 | 91 | impl StaticBuffer { 92 | pub fn empty<'a>( 93 | label: impl Into>, 94 | device: &RenderDevice, 95 | usage: BufferUsages, 96 | ) -> Self { 97 | let buffer = device.create_buffer(&BufferDescriptor { 98 | label: label.into(), 99 | size: T::min_size().get(), 100 | usage, 101 | mapped_at_creation: false, 102 | }); 103 | 104 | Self { 105 | buffer, 106 | value: None, 107 | scratch: Scratch::new(usage), 108 | _marker: PhantomData, 109 | } 110 | } 111 | } 112 | 113 | impl StaticBuffer { 114 | pub fn create<'a>( 115 | label: impl Into>, 116 | device: &RenderDevice, 117 | value: &T, 118 | usage: BufferUsages, 119 | ) -> Self { 120 | let mut scratch = Scratch::new(usage); 121 | scratch.write(&value); 122 | 123 | let buffer = device.create_buffer_with_data(&BufferInitDescriptor { 124 | label: label.into(), 125 | usage, 126 | contents: scratch.contents(), 127 | }); 128 | 129 | Self { 130 | buffer, 131 | value: None, 132 | scratch, 133 | _marker: PhantomData, 134 | } 135 | } 136 | 137 | pub fn value(&self) -> &T { 138 | self.value.as_ref().unwrap() 139 | } 140 | 141 | pub fn set_value(&mut self, value: T) { 142 | self.value = Some(value); 143 | } 144 | 145 | pub fn update(&mut self, queue: &RenderQueue) { 146 | if let Some(value) = &self.value { 147 | self.scratch.write(value); 148 | 149 | queue.write_buffer(&self.buffer, 0, self.scratch.contents()); 150 | } 151 | } 152 | } 153 | 154 | impl Deref for StaticBuffer { 155 | type Target = Buffer; 156 | 157 | #[inline] 158 | fn deref(&self) -> &Self::Target { 159 | &self.buffer 160 | } 161 | } 162 | 163 | impl<'a, T> IntoBinding<'a> for &'a StaticBuffer { 164 | #[inline] 165 | fn into_binding(self) -> BindingResource<'a> { 166 | self.buffer.as_entire_binding() 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/shaders/attachments.wgsl: -------------------------------------------------------------------------------- 1 | #define_import_path bevy_terrain::attachments 2 | 3 | #import bevy_terrain::types::AtlasTile 4 | #import bevy_terrain::bindings::{config, atlas_sampler, attachments, attachment0_atlas, attachment1_atlas, attachment2_atlas} 5 | #import bevy_terrain::functions::tile_count 6 | 7 | fn attachment_uv(uv: vec2, attachment_index: u32) -> vec2 { 8 | let attachment = attachments[attachment_index]; 9 | return uv * attachment.scale + attachment.offset; 10 | } 11 | 12 | fn sample_attachment0(tile: AtlasTile) -> vec4 { 13 | let uv = attachment_uv(tile.coordinate.uv, 0u); 14 | 15 | #ifdef FRAGMENT 16 | #ifdef SAMPLE_GRAD 17 | return textureSampleGrad(attachment0_atlas, atlas_sampler, uv, tile.index, tile.coordinate.uv_dx, tile.coordinate.uv_dy); 18 | #else 19 | return textureSampleLevel(attachment0_atlas, atlas_sampler, uv, tile.index, 0.0); 20 | #endif 21 | #else 22 | return textureSampleLevel(attachment0_atlas, atlas_sampler, uv, tile.index, 0.0); 23 | #endif 24 | } 25 | 26 | fn sample_attachment1(tile: AtlasTile) -> vec4 { 27 | let uv = attachment_uv(tile.coordinate.uv, 1u); 28 | 29 | #ifdef FRAGMENT 30 | #ifdef SAMPLE_GRAD 31 | return textureSampleGrad(attachment1_atlas, atlas_sampler, uv, tile.index, tile.coordinate.uv_dx, tile.coordinate.uv_dy); 32 | #else 33 | return textureSampleLevel(attachment1_atlas, atlas_sampler, uv, tile.index, 0.0); 34 | #endif 35 | #else 36 | return textureSampleLevel(attachment1_atlas, atlas_sampler, uv, tile.index, 0.0); 37 | #endif 38 | } 39 | 40 | fn sample_attachment1_gather0(tile: AtlasTile) -> vec4 { 41 | let uv = attachment_uv(tile.coordinate.uv, 1u); 42 | return textureGather(0, attachment1_atlas, atlas_sampler, uv, tile.index); 43 | } 44 | 45 | fn sample_height(tile: AtlasTile) -> f32 { 46 | let height = sample_attachment0(tile).x; 47 | 48 | return mix(config.min_height, config.max_height, height); 49 | } 50 | 51 | fn sample_normal(tile: AtlasTile, vertex_normal: vec3) -> vec3 { 52 | let uv = attachment_uv(tile.coordinate.uv, 0u); 53 | 54 | #ifdef SPHERICAL 55 | var FACE_UP = array( 56 | vec3( 0.0, 1.0, 0.0), 57 | vec3( 0.0, 1.0, 0.0), 58 | vec3( 0.0, 0.0, -1.0), 59 | vec3( 0.0, 0.0, -1.0), 60 | vec3(-1.0, 0.0, 0.0), 61 | vec3(-1.0, 0.0, 0.0), 62 | ); 63 | 64 | let face_up = FACE_UP[tile.coordinate.side]; 65 | 66 | let normal = normalize(vertex_normal); 67 | let tangent = cross(face_up, normal); 68 | let bitangent = cross(normal, tangent); 69 | let TBN = mat3x3(tangent, bitangent, normal); 70 | 71 | let side_length = 3.14159265359 / 4.0 * config.scale; 72 | #else 73 | let TBN = mat3x3(1.0, 0.0, 0.0, 74 | 0.0, 0.0, 1.0, 75 | 0.0, 1.0, 0.0); 76 | 77 | let side_length = config.scale; 78 | #endif 79 | 80 | // Todo: this is only an approximation of the S2 distance (pixels are not spaced evenly and they are not perpendicular) 81 | let pixels_per_side = attachments[0u].size * tile_count(tile.coordinate.lod); 82 | let distance_between_samples = side_length / pixels_per_side; 83 | let offset = 0.5 / attachments[0u].size; 84 | 85 | #ifdef FRAGMENT 86 | #ifdef SAMPLE_GRAD 87 | let left = mix(config.min_height, config.max_height, textureSampleGrad(attachment0_atlas, atlas_sampler, uv + vec2(-offset, 0.0), tile.index, tile.coordinate.uv_dx, tile.coordinate.uv_dy).x); 88 | let up = mix(config.min_height, config.max_height, textureSampleGrad(attachment0_atlas, atlas_sampler, uv + vec2( 0.0, -offset), tile.index, tile.coordinate.uv_dx, tile.coordinate.uv_dy).x); 89 | let right = mix(config.min_height, config.max_height, textureSampleGrad(attachment0_atlas, atlas_sampler, uv + vec2( offset, 0.0), tile.index, tile.coordinate.uv_dx, tile.coordinate.uv_dy).x); 90 | let down = mix(config.min_height, config.max_height, textureSampleGrad(attachment0_atlas, atlas_sampler, uv + vec2( 0.0, offset), tile.index, tile.coordinate.uv_dx, tile.coordinate.uv_dy).x); 91 | #else 92 | let left = mix(config.min_height, config.max_height, textureSampleLevel(attachment0_atlas, atlas_sampler, uv + vec2(-offset, 0.0), tile.index, 0.0).x); 93 | let up = mix(config.min_height, config.max_height, textureSampleLevel(attachment0_atlas, atlas_sampler, uv + vec2( 0.0, -offset), tile.index, 0.0).x); 94 | let right = mix(config.min_height, config.max_height, textureSampleLevel(attachment0_atlas, atlas_sampler, uv + vec2( offset, 0.0), tile.index, 0.0).x); 95 | let down = mix(config.min_height, config.max_height, textureSampleLevel(attachment0_atlas, atlas_sampler, uv + vec2( 0.0, offset), tile.index, 0.0).x); 96 | #endif 97 | #else 98 | let left = mix(config.min_height, config.max_height, textureSampleLevel(attachment0_atlas, atlas_sampler, uv + vec2(-offset, 0.0), tile.index, 0.0).x); 99 | let up = mix(config.min_height, config.max_height, textureSampleLevel(attachment0_atlas, atlas_sampler, uv + vec2( 0.0, -offset), tile.index, 0.0).x); 100 | let right = mix(config.min_height, config.max_height, textureSampleLevel(attachment0_atlas, atlas_sampler, uv + vec2( offset, 0.0), tile.index, 0.0).x); 101 | let down = mix(config.min_height, config.max_height, textureSampleLevel(attachment0_atlas, atlas_sampler, uv + vec2( 0.0, offset), tile.index, 0.0).x); 102 | #endif 103 | 104 | let surface_normal = normalize(vec3(left - right, down - up, distance_between_samples)); 105 | 106 | return normalize(TBN * surface_normal); 107 | } 108 | 109 | fn sample_color(tile: AtlasTile) -> vec4 { 110 | let height = sample_attachment0(tile).x; 111 | 112 | return vec4(height * 0.5); 113 | } 114 | -------------------------------------------------------------------------------- /src/debug/camera.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "high_precision")] 2 | use crate::big_space::{ 3 | FloatingOrigin, GridCell, GridTransform, GridTransformItem, ReferenceFrame, ReferenceFrames, 4 | }; 5 | 6 | use bevy::{input::mouse::MouseMotion, math::DVec3, prelude::*}; 7 | 8 | #[derive(Bundle)] 9 | pub struct DebugCameraBundle { 10 | pub camera: Camera3dBundle, 11 | pub controller: DebugCameraController, 12 | #[cfg(feature = "high_precision")] 13 | pub cell: GridCell, 14 | #[cfg(feature = "high_precision")] 15 | pub origin: FloatingOrigin, 16 | } 17 | 18 | impl Default for DebugCameraBundle { 19 | fn default() -> Self { 20 | Self { 21 | camera: default(), 22 | controller: default(), 23 | #[cfg(feature = "high_precision")] 24 | cell: default(), 25 | #[cfg(feature = "high_precision")] 26 | origin: FloatingOrigin, 27 | } 28 | } 29 | } 30 | 31 | impl DebugCameraBundle { 32 | #[cfg(feature = "high_precision")] 33 | pub fn new(position: DVec3, speed: f64, frame: &ReferenceFrame) -> Self { 34 | let (cell, translation) = frame.translation_to_grid(position); 35 | 36 | Self { 37 | camera: Camera3dBundle { 38 | transform: Transform::from_translation(translation).looking_to(Vec3::X, Vec3::Y), 39 | projection: PerspectiveProjection { 40 | near: 0.000001, 41 | ..default() 42 | } 43 | .into(), 44 | ..default() 45 | }, 46 | cell, 47 | controller: DebugCameraController { 48 | translation_speed: speed, 49 | ..default() 50 | }, 51 | ..default() 52 | } 53 | } 54 | 55 | #[cfg(not(feature = "high_precision"))] 56 | pub fn new(position: Vec3, speed: f64) -> Self { 57 | Self { 58 | camera: Camera3dBundle { 59 | transform: Transform::from_translation(position).looking_to(Vec3::X, Vec3::Y), 60 | projection: PerspectiveProjection { 61 | near: 0.000001, 62 | ..default() 63 | } 64 | .into(), 65 | ..default() 66 | }, 67 | controller: DebugCameraController { 68 | translation_speed: speed, 69 | ..default() 70 | }, 71 | ..default() 72 | } 73 | } 74 | } 75 | 76 | #[derive(Clone, Debug, Reflect, Component)] 77 | pub struct DebugCameraController { 78 | pub enabled: bool, 79 | /// Smoothness of translation, from `0.0` to `1.0`. 80 | pub translational_smoothness: f64, 81 | /// Smoothness of rotation, from `0.0` to `1.0`. 82 | pub rotational_smoothness: f32, 83 | pub translation_speed: f64, 84 | pub rotation_speed: f32, 85 | pub acceleration_speed: f64, 86 | pub translation_velocity: DVec3, 87 | pub rotation_velocity: Vec2, 88 | } 89 | 90 | impl Default for DebugCameraController { 91 | fn default() -> Self { 92 | Self { 93 | enabled: false, 94 | translational_smoothness: 0.9, 95 | rotational_smoothness: 0.8, 96 | translation_speed: 10e1, 97 | rotation_speed: 1e-1, 98 | acceleration_speed: 4.0, 99 | translation_velocity: Default::default(), 100 | rotation_velocity: Default::default(), 101 | } 102 | } 103 | } 104 | 105 | pub fn camera_controller( 106 | #[cfg(feature = "high_precision")] frames: ReferenceFrames, 107 | time: Res