Repository: kurtkuehnert/bevy_terrain Branch: main Commit: 5a88daf4a419 Files: 57 Total size: 300.6 KB Directory structure: gitextract_d9qm2zah/ ├── .gitattributes ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets/ │ └── shaders/ │ ├── planar.wgsl │ └── spherical.wgsl ├── docs/ │ ├── development.md │ └── implementation.md ├── examples/ │ ├── minimal.rs │ ├── planar.rs │ ├── preprocess_planar.rs │ ├── preprocess_spherical.rs │ └── spherical.rs └── src/ ├── big_space.rs ├── debug/ │ ├── camera.rs │ └── mod.rs ├── formats/ │ ├── mod.rs │ └── tiff.rs ├── lib.rs ├── math/ │ ├── coordinate.rs │ ├── ellipsoid.rs │ ├── mod.rs │ └── terrain_model.rs ├── plugin.rs ├── preprocess/ │ ├── gpu_preprocessor.rs │ ├── mod.rs │ └── preprocessor.rs ├── render/ │ ├── culling_bind_group.rs │ ├── mod.rs │ ├── terrain_bind_group.rs │ ├── terrain_material.rs │ ├── terrain_view_bind_group.rs │ └── tiling_prepass.rs ├── shaders/ │ ├── attachments.wgsl │ ├── bindings.wgsl │ ├── debug.wgsl │ ├── functions.wgsl │ ├── mod.rs │ ├── preprocess/ │ │ ├── downsample.wgsl │ │ ├── preprocessing.wgsl │ │ ├── split.wgsl │ │ └── stitch.wgsl │ ├── render/ │ │ ├── fragment.wgsl │ │ └── vertex.wgsl │ ├── tiling_prepass/ │ │ ├── prepare_prepass.wgsl │ │ └── refine_tiles.wgsl │ └── types.wgsl ├── terrain.rs ├── terrain_data/ │ ├── gpu_tile_atlas.rs │ ├── gpu_tile_tree.rs │ ├── mod.rs │ ├── tile_atlas.rs │ └── tile_tree.rs ├── terrain_view.rs └── util.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.tif filter=lfs diff=lfs merge=lfs -text ================================================ FILE: .gitignore ================================================ .idea Cargo.lock target assets/**/data/* assets/**/config.tc assets/terrains/spherical/source/height/200m.tif ================================================ FILE: Cargo.toml ================================================ [package] name = "bevy_terrain" description = "Terrain Rendering for the Bevy Engine." version = "0.1.0-dev" license = "MIT OR Apache-2.0" edition = "2021" categories = ["game-engines", "rendering", "graphics"] keywords = ["gamedev", "graphics", "bevy", "terrain"] exclude = ["assets/*"] readme = "README.md" authors = ["Kurt Kühnert "] repository = "https://github.com/kurtkuehnert/bevy_terrain" [features] high_precision = ["dep:big_space"] [dependencies] bevy = "0.14.0" #{ git="https://github.com/bevyengine/bevy/", branch="main" } ndarray = "0.15" itertools = "0.12" image = "0.25" tiff = "0.9" lru = "0.12" bitflags = "2.4" bytemuck = "1.14" anyhow = "1.0" bincode = "2.0.0-rc.3" async-channel = "2.1" big_space = { version = "0.7", optional = true } [[example]] name = "preprocess_planar" path = "examples/preprocess_planar.rs" required-features = ["bevy/embedded_watcher"] [package.metadata.example.preprocess_planar] name = "Preprocess Planar" description = "Preprocesses the terrain data for the planar examples." [[example]] name = "minimal" path = "examples/minimal.rs" required-features = ["bevy/embedded_watcher"] [package.metadata.example.minial] name = "Minimal" description = "Renders a basic flat terrain with only the base attachment." [[example]] name = "planar" path = "examples/planar.rs" required-features = ["high_precision", "bevy/embedded_watcher"] [package.metadata.example.planar] name = "Planar Advanced" description = "Renders a flat terrain with the base attachment and an albedo texture, using a custom shader." [[example]] name = "preprocess_spherical" path = "examples/preprocess_spherical.rs" required-features = ["bevy/embedded_watcher"] [package.metadata.example.preprocess_spherical] name = "Preprocess Spherical" description = "Preprocesses the terrain data for the spherical examples." [[example]] name = "spherical" path = "examples/spherical.rs" required-features = ["high_precision", "bevy/embedded_watcher"] [package.metadata.example.spherical] name = "Spherical" description = "Renders a spherical terrain using a custom shader." ================================================ FILE: LICENSE-APACHE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: LICENSE-MIT ================================================ MIT License Copyright (c) 2023 Kurt Kühnert Copyright (c) 2023 Argeo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Bevy Terrain ![GitHub](https://img.shields.io/github/license/Ku95/bevy_terrain) ![Crates.io](https://img.shields.io/crates/v/bevy_terrain) ![docs.rs](https://img.shields.io/docsrs/bevy_terrain) ![Discord](https://img.shields.io/discord/999221999517843456?label=discord) Bevy Terrain is a plugin for rendering terrains with the Bevy game engine. ![](https://user-images.githubusercontent.com/51823519/202845032-0537e929-b13c-410b-8072-4c5b5df9830d.png) (Data Source: Federal Office of Topography, [©swisstopo](https://www.swisstopo.admin.ch/en/home.html)) **Warning:** This plugin is still in early development, so expect the API to change and possibly break you existing code. Bevy terrain was developed as part of my [bachelor thesis](https://github.com/kurtkuehnert/terrain_renderer) on the topic of large-scale terrain rendering. Now that this project is finished I am planning on adding more features related to game development and rendering virtual worlds. If you would like to help me build an extensive open-source terrain rendering library for the Bevy game engine, feel free to contribute to the project. Also, join the Bevy Terrain [Discord server](https://discord.gg/7mtZWEpA82) for help, feedback, or to discuss feature ideas. ## Examples Currently, there are two examples. The basic one showcases the different debug views of the terrain. See controls down below. The advanced one showcases how to use the Bevy material system for texturing, as well as how to add additional terrain attachments. Use the `A` Key to toggle between the custom material and the albedo attachment. Before running the examples you have to preprocess the terrain data this may take a while. Once the data is preprocessed you can disable it by commenting out the preprocess line. ## Documentation The `docs` folder contains a high-level [implementation overview](https://github.com/kurtkuehnert/bevy_terrain/blob/main/docs/implementation.md), as well as, the [development status](https://github.com/kurtkuehnert/bevy_terrain/blob/main/docs/development.md), enumerating the features that I am planning on implementing next, of the project. If you would like to contribute to the project this is a good place to start. Simply pick an issue/feature and discuss the details with me on Discord or GitHub. I would also recommend you to take a look at my [thesis](https://github.com/kurtkuehnert/terrain_renderer/blob/main/Thesis.pdf). There I present the basics of terrain rendering (chapter 2), common approaches (chapter 3) and a detailed explanation of method used by `bevy_terrain` (chapter 4). ## Debug Controls These are the debug controls of the plugin. Use them to fly over the terrain, experiment with the quality settings and enter the different debug views. - `T` - toggle camera movement - move the mouse to look around - press the arrow keys to move the camera horizontally - use `PageUp` and `PageDown` to move the camera vertically - use `Home` and `End` to increase/decrease the camera's movement speed - `W` - toggle wireframe view - `P` - toggle tile view - `L` - toggle lod view - `U` - toggle uv view - `C` - toggle tile view - `D` - toggle mesh morph - `A` - toggle albedo - `B` - toggle base color black / white - `S` - toggle lighting - `G` - toggle filtering bilinear / trilinear + anisotropic - `F` - freeze frustum culling - `H` - decrease tile scale - `J` - increase tile scale - `N` - decrease grid size - `E` - increase grid size - `I` - decrease view distance - `O` - increase view distance ## Attribution The planar terrain dataset is generated using the free version of the Gaia Terrain Generator. The spherical terrain example dataset is a reprojected version of the GEBCO_2023 Grid dataset. GEBCO Compilation Group (2023) GEBCO 2023 Grid (doi:10.5285/f98b053b-0cbc-6c23-e053-6c86abc0af7b) ## License Bevy Terrain source code (this excludes the datasets in the assets directory) is dual-licensed under either * MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT) * Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) at your option. ================================================ FILE: assets/shaders/planar.wgsl ================================================ #import bevy_terrain::types::AtlasTile #import bevy_terrain::attachments::{sample_attachment0 as sample_height, sample_normal, sample_attachment1 as sample_albedo} #import bevy_terrain::fragment::{FragmentInput, FragmentOutput, fragment_info, fragment_output, fragment_debug} #import bevy_terrain::functions::lookup_tile #import bevy_pbr::pbr_types::{PbrInput, pbr_input_new} @group(3) @binding(0) var gradient: texture_1d; @group(3) @binding(1) var gradient_sampler: sampler; fn sample_color(tile: AtlasTile) -> vec4 { #ifdef ALBEDO return sample_albedo(tile); #else let height = sample_height(tile).x; return textureSampleLevel(gradient, gradient_sampler, pow(height, 0.9), 0.0); #endif } @fragment fn fragment(input: FragmentInput) -> FragmentOutput { var info = fragment_info(input); let tile = lookup_tile(info.coordinate, info.blend, 0u); var color = sample_color(tile); var normal = sample_normal(tile, info.world_normal); if (info.blend.ratio > 0.0) { let tile2 = lookup_tile(info.coordinate, info.blend, 1u); color = mix(color, sample_color(tile2), info.blend.ratio); normal = mix(normal, sample_normal(tile2, info.world_normal), info.blend.ratio); } var output: FragmentOutput; fragment_output(&info, &output, color, normal); fragment_debug(&info, &output, tile, normal); return output; } ================================================ FILE: assets/shaders/spherical.wgsl ================================================ #import bevy_terrain::types::{AtlasTile} #import bevy_terrain::bindings::config #import bevy_terrain::attachments::{sample_height, sample_normal} #import bevy_terrain::fragment::{FragmentInput, FragmentOutput, fragment_info, fragment_output, fragment_debug} #import bevy_terrain::functions::lookup_tile #import bevy_pbr::pbr_types::{PbrInput, pbr_input_new} #import bevy_pbr::pbr_functions::{calculate_view, apply_pbr_lighting} @group(3) @binding(0) var gradient: texture_1d; @group(3) @binding(1) var gradient_sampler: sampler; fn sample_color(tile: AtlasTile) -> vec4 { let height = sample_height(tile); var color: vec4; if (height < 0.0) { color = textureSampleLevel(gradient, gradient_sampler, mix(0.0, 0.075, pow(height / config.min_height, 0.25)), 0.0); } else { color = textureSampleLevel(gradient, gradient_sampler, mix(0.09, 1.0, pow(height / config.max_height * 2.0, 1.0)), 0.0); } return color; } @fragment fn fragment(input: FragmentInput) -> FragmentOutput { var info = fragment_info(input); let tile = lookup_tile(info.coordinate, info.blend, 0u); var color = sample_color(tile); var normal = sample_normal(tile, info.world_normal); if (info.blend.ratio > 0.0) { let tile2 = lookup_tile(info.coordinate, info.blend, 1u); color = mix(color, sample_color(tile2), info.blend.ratio); normal = mix(normal, sample_normal(tile2, info.world_normal), info.blend.ratio); } var output: FragmentOutput; fragment_output(&info, &output, color, normal); fragment_debug(&info, &output, tile, normal); return output; } ================================================ FILE: docs/development.md ================================================ # Development Status Bevy Terrain This document assesses the current status of the `bevy_terrain` plugin. I built this plugin as part of my bachelor thesis, which focused on rendering large-scale terrains. The thesis and its project can be found [here](https://github.com/kurtkuehnert/terrain_renderer). For that, I set out to solve two key problems of terrain rendering. For one, I developed the Uniform Distance-Dependent Level of Detail (UDLOD) algorithm to represent the terrain geometry, and for another, I came up with the Chunked Clipmap data structure used to represent the terrain data. Both are implemented as part of bevy terrain and work quite well for rendering large-scale terrains. Now that I have finished my thesis I would like to continue working on this project and extend its capabilities. The topic of terrain rendering is vast, and thus I can not work on all the stuff at once. In the following, I will list a couple of features that I would like to integrate into this crate in the future. 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. Additionally, there are still plenty of improvements, bug fixes, and optimizations to be completed on the already existing implementation. ## Features - Procedural Texturing - Shadow Rendering - Real-Time Editing - Collision - Path-Finding - Spherical Terrain ### Procedural Texturing Probably the biggest missing puzzle piece of this plugin is support for procedural texturing using splat maps or something similar. Currently, texturing has to be implemented manually in the terrain shader (see the advanced example for reference). I would like to support this use case in a more integrated manner in the future. Unfortunately, I am not familiar with the terrain texturing systems of other engines (e.g. Unity, Unreal, Godot) or have any experience texturing and building my own terrains. I would greatly appreciate it if anyone can share some requirements for this area of terrain rendering. Also, a prototype of a custom texturing system would be a great resource to develop further ideas. ### Shadow Rendering Another important capability that is currently missing is the support for large-scale shadow rendering. This would be probably implemented using cascading shadow maps or a similar method. Currently, Bevy itself does not implement a system we could use for this yet. Regardless, I think reusing Bevy’s implementation would be the best choice in the future. ### Real-Time Editing 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. This is not only important for sculpting the terrain of your game, but also for texturing, vegetation placement, etc. This is going to be my next focus area and I would like to discuss designs and additional requirements with anyone interested. ### Collision 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. Integrating the collision of the terrain with rapier would enable many types of games and is a commonly requested feature. ### Path-Finding 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. ### Spherical Terrain I think that with a little design work the current two-dimensional terrain rendering method could be extended to the spherical terrain. However, I am unsure how much of the existing code could be extended and reused. Maybe planet rendering would require its entirely separate crate. ================================================ FILE: docs/implementation.md ================================================ # Implementation Overview Bevy Terrain This document serves as a general overview of the implementation of the `bevy_terrain` plugin. Currently, this crate provides two fundamental capabilities. For one, the UDLOD algorithm approximates the terrain geometry, and for another, the Chunked Clipmap stores the terrain data in a convenient and randomly accessible data structure. Both are described in detail in my [bachelor thesis]([https://github.com/kurtkuehnert/terrain_renderer/blob/main/Thesis.pdf](https://github.com/kurtkuehnert/terrain_renderer/blob/main/Thesis.pdf)). To understand the implementation of this crate and the reasons behind some design decisions, I recommend that you read at least the entire chapter 4. If you are unfamiliar with terrain rendering in general, taking a look at chapter 2 will prove beneficial as well. In the following, I will now explain how both of these systems are currently implemented, what limitations they possess, and how they should work in the future. Furthermore, I have listed a couple of todos outlining the essence of these issues. If any of them sound interesting to you, and you would like to work on them, please get in touch with me, so we can discuss solutions. These are certainly not all problems of the current implementation, so if you notice anything else, please let me know, so I can add it here. ## Terrain Geometry ### Ideal Ideally, we would like to represent the terrain geometry without any error according to our source data. Unfortunately, rendering each data point as a vertex is not scalable, nor efficient. ### Reality That is why we need a sophisticated level of detail (LOD) algorithm that minimizes the error introduced by its approximation of the geometry. ### Solution One such solution is the Uniform Distance-Dependent Level of Detail (UDLOD) algorithm that I have developed as part of my thesis (for a detailed explanation read section 4.4). It divides the terrain into numerous small tiles in parallel on the GPU. They are then rendered using a single indirect draw call and morphed together (in the vertex shader) to form a continuous surface with an approximately uniform tessellation in screen space. ### Issues For any LOD algorithm, an appropriate crack-avoiding and morphing strategy are important to eliminate and reduce visual discrepancies as much as possible. UDLOD uses a slightly modified version of the CDLOD morphing scheme. The UDLOD algorithm can be used to tessellate procedural ground details like rocks or cobblestones as well. Therefore, simply increase the tile_tree depth using the `additional_refinement` parameter. Even though the tessellation produced by UDLOD is somewhat uniform with respect to the distance, it does not take factors like the terrain's roughness and the viewing angle into account. Generally, the current UDLOD algorithm tiers to cover the worst-case terrain roughness like many other algorithms ( GeoMipmap, GeoClipmap, PGM, CDLOD, FarCry5). I believe that we can still develop more efficient LOD algorithms that scale favorably for large-scale terrains in the future. The culling is currently pretty bare-bones. We could probably implement most of the techniques researched by the Far Cry 5 terrain renderer as well. Currently, the prepass is pretty inefficient, because the shader occupancy is very low (the prepass is still plenty fast, but could be improved). I think that this could be resolved by using the atomic operations more cleverly and reducing the shader dispatches in general. In the past, I have tried doing all the work in a single pass. Unfortunately, that did not work, but maybe someone can figure out a better solution. The frustum culling uses a 2D min-max height data attachment to approximate the bounding volumes of each tile correctly. This is currently stored with the same resolution as the source height data, but only a fraction of this resolution is actually required. ### Todo - [x] come up with a smooth morphing strategy that solves the geometry crack problem as well - [x] implement bounding box frustum culling - [x] further refine the geometry for procedural details (rocks, cobblestone) - [ ] explore different LOD algorithms (maybe apply the clipmap idea to CBTs?) - [ ] try incorporating a screen space error metric, local terrain roughness, or the viewing angle - [ ] implement more advanced culling solutions (occlusion, backface) - [ ] try reducing the compute shader dispatches in the prepass phase - [ ] store min-max height data, required by frustum culling, at a way lower resolution - [ ] experiment with hardware tessellation or mesh shaders ## Terrain Data ### Ideal Ideally, we would like to store any desired information at any desired resolution across the terrain’s surface. For example, a terrain could require a heightmap with a resolution of 0.5m, an albedo map with a resolution of 0.2m, and a vegetation map (for placing trees and bushes) with a resolution of 1m. Each of these three different kinds of terrain data are called terrain attachments. This terrain data should be available in any system and shader of our application. Additionally, we would like to access the data at any position and sample a value with distant-dependent accuracy. Finally, some use cases require the ability to sample some attachments like the albedo or splat data trilinearly and anisotropically to mitigate aliasing artifacts. ### Reality Because we are using height-map-based terrain these attachments should be stored as large two-dimensional textures. However, due to the size of most landscapes, using a single texture would quickly use up all of our video memory. That is why we need to partition and adjust the loaded data according to our view. Additionally, it is important that we can share this terrain data efficiently between multiple views for use cases like split-screen or shadow rendering. ### Solution To solve this, I have developed the chunked clipmap data structure (if you are unfamiliar with the concept, I encourage you to read section 4.5 of my thesis). It divides the entire terrain data into one large tile_tree, covering the entire terrain. This requires that all terrain data has to be preprocessed into small square textures: the tiles of the tile_tree. Each tile possesses one texture per attachment. To allow for different resolutions of the attachments (e.g. the height data should be twice as accurate as our splat map), the size of these textures has to be different as well. Following the same example, this would mean that the height textures would have a size of 100x100 and the splat textures a size of 50x50 pixels. ### Issues Because of our compound representation of the terrain data, consisting of many small textures, some problems arise during texture filtering. The biggest issue is that adjacent tiles do not line up perfectly due to missing texture information at the border. This causes noticeable texture seams between adjacent tiles. To remedy this issue we have to duplicate the border data between adjacent tiles. This complicates our preprocessing but results in a completely seamless terrain data representation. For trilinear filtering, we additionally require mipmap information. Currently, bevy does not support mipmap generation. That is why I have implemented a simple mipmap creation function, which is executed after the tile textures have been loaded. Unfortunately, my simple approach only works on textures with a side length equal to a power of two (e.g. 256x256, 512x512). This needlessly limits the resolutions of our terrain data. In the future, I would like to generate the mipmaps for any texture size. As mentioned above the terrain data has to be loaded depending on our current view position. Currently, I load all tiles inside the `load_distance` around the viewer. There is no prioritization or load balancing. I would like to explore different loading strategies (e.g. distance only, view frustum based, etc.) to enable use cases like streaming data from a web server. For that, the strategy would have to minimize the loading requests while maximizing the visual quality. When streaming from disk this wasn't a problem yet. Additionally, the plugin panics if the tile atlas is out of indices (i.e. the maximum amount of tiles is loaded). This is unacceptable in production use. Here we would have to come up with a strategy of prioritizing which tiles to keep and which ones to discard in order to accommodate more important ones. The tile loading code itself is currently pretty inefficient. Due to the nature of the bevy image abstraction, all textures are duplicated multiple times. Hopefully in the near future, once the asset processing has been reworked, it will be easier to express loading parts of an array texture directly. To divide the terrain into the numerous tile textures I use a 3-step preprocessing algorithm. This is implemented pretty inefficiently. If you are interested in optimizing data transformation code, this should be the task for you :D. To save space the terrain data is compressed using common image formats, when it is stored on the hard-drive. To unfortunately the encoding of PNGs is quite slow. That is why I came up with the [DTM image format](https://github.com/kurtkuehnert/dtm). It uses a sequential compression technique similar to the QOI format. DTM works quite well for the shallow terrain I used for testing, but is not ideal for the steep and hilly terrains used in most games. There are probably significant gains to be had in this area. Another huge challenge regarding the terrain data is its modification in real-time. Workflows like sculpting, texturing, etc. do require the ability to update the terrain data in a visual manner. This topic is vast and will require extensive investigation before we can settle on a final design. If you have experience/ideas please let me know. ### Todo - [x] duplicate border information to eliminate texture seams - [x] generate mipmaps to enable trilinear filtering - [ ] Incorporate better mipmap generation for any texture size. - [ ] different loading strategies - [ ] handle tile atlas out of indices - [ ] improve loading to tile atlas (i.e. loading layers of an array texture), remove excessive duplication/copying - [ ] improve the preprocessing with caching, GPU acceleration, etc. - [ ] explore the usage of more efficient image formats - [ ] investigate real-time modification ================================================ FILE: examples/minimal.rs ================================================ use bevy::math::DVec3; use bevy::prelude::*; use bevy_terrain::prelude::*; const PATH: &str = "terrains/planar"; const TERRAIN_SIZE: f64 = 1000.0; const HEIGHT: f32 = 250.0; const TEXTURE_SIZE: u32 = 512; const LOD_COUNT: u32 = 4; fn main() { App::new() .add_plugins(( DefaultPlugins, TerrainPlugin, TerrainMaterialPlugin::::default(), TerrainDebugPlugin, )) .add_systems(Startup, setup) .run(); } fn setup( mut commands: Commands, mut materials: ResMut>, mut tile_trees: ResMut>, mut meshes: ResMut>, ) { // Configure all the important properties of the terrain, as well as its attachments. let config = TerrainConfig { lod_count: LOD_COUNT, model: TerrainModel::planar(DVec3::new(0.0, -100.0, 0.0), TERRAIN_SIZE, 0.0, HEIGHT), path: PATH.to_string(), ..default() } .add_attachment(AttachmentConfig { name: "height".to_string(), texture_size: TEXTURE_SIZE, border_size: 2, mip_level_count: 4, format: AttachmentFormat::R16, }); // Configure the quality settings of the terrain view. Adapt the settings to your liking. let view_config = TerrainViewConfig::default(); let tile_atlas = TileAtlas::new(&config); let tile_tree = TileTree::new(&tile_atlas, &view_config); let terrain = commands .spawn(( TerrainBundle::new(tile_atlas), materials.add(DebugTerrainMaterial::default()), )) .id(); let view = commands.spawn(DebugCameraBundle::default()).id(); tile_trees.insert((terrain, view), tile_tree); commands.spawn(PbrBundle { mesh: meshes.add(Cuboid::from_length(10.0)), transform: Transform::from_translation(Vec3::new( TERRAIN_SIZE as f32 / 2.0, 100.0, TERRAIN_SIZE as f32 / 2.0, )), ..default() }); } ================================================ FILE: examples/planar.rs ================================================ use bevy::math::DVec3; use bevy::{prelude::*, reflect::TypePath, render::render_resource::*}; use bevy_terrain::prelude::*; const PATH: &str = "terrains/planar"; const TERRAIN_SIZE: f64 = 2000.0; const HEIGHT: f32 = 500.0; const TEXTURE_SIZE: u32 = 512; const LOD_COUNT: u32 = 8; #[derive(Asset, AsBindGroup, TypePath, Clone)] pub struct TerrainMaterial { #[texture(0, dimension = "1d")] #[sampler(1)] gradient: Handle, } impl Material for TerrainMaterial { fn fragment_shader() -> ShaderRef { "shaders/planar.wgsl".into() } } fn main() { App::new() .add_plugins(( DefaultPlugins.build().disable::(), TerrainPlugin, TerrainDebugPlugin, // enable debug settings and controls TerrainMaterialPlugin::::default(), )) .add_systems(Startup, setup) .run(); } fn setup( mut commands: Commands, mut images: ResMut, mut materials: ResMut>, mut tile_trees: ResMut>, asset_server: Res, ) { let gradient = asset_server.load("textures/gradient2.png"); images.load_image( &gradient, TextureDimension::D1, TextureFormat::Rgba8UnormSrgb, ); // Configure all the important properties of the terrain, as well as its attachments. let config = TerrainConfig { lod_count: LOD_COUNT, model: TerrainModel::planar(DVec3::new(0.0, -100.0, 0.0), TERRAIN_SIZE, 0.0, HEIGHT), path: PATH.to_string(), ..default() } .add_attachment(AttachmentConfig { name: "height".to_string(), texture_size: TEXTURE_SIZE, border_size: 2, mip_level_count: 4, format: AttachmentFormat::R16, }) .add_attachment(AttachmentConfig { name: "albedo".to_string(), texture_size: TEXTURE_SIZE, border_size: 2, mip_level_count: 4, format: AttachmentFormat::Rgba8, }); // Configure the quality settings of the terrain view. Adapt the settings to your liking. let view_config = TerrainViewConfig::default(); let tile_atlas = TileAtlas::new(&config); let tile_tree = TileTree::new(&tile_atlas, &view_config); commands.spawn_big_space(ReferenceFrame::default(), |root| { let frame = root.frame().clone(); let terrain = root .spawn_spatial(( TerrainBundle::new(tile_atlas, &frame), materials.add(TerrainMaterial { gradient }), )) .id(); let view = root.spawn_spatial(DebugCameraBundle::default()).id(); tile_trees.insert((terrain, view), tile_tree); }); } ================================================ FILE: examples/preprocess_planar.rs ================================================ use bevy::prelude::*; use bevy_terrain::prelude::*; const PATH: &str = "terrains/planar"; const TEXTURE_SIZE: u32 = 512; const LOD_COUNT: u32 = 4; fn main() { App::new() .add_plugins((DefaultPlugins, TerrainPlugin, TerrainPreprocessPlugin)) .add_systems(Startup, setup) .run(); } fn setup(mut commands: Commands, asset_server: Res) { let config = TerrainConfig { lod_count: LOD_COUNT, path: PATH.to_string(), ..default() } .add_attachment(AttachmentConfig { name: "height".to_string(), texture_size: TEXTURE_SIZE, border_size: 2, format: AttachmentFormat::R16, ..default() }) .add_attachment(AttachmentConfig { name: "albedo".to_string(), texture_size: TEXTURE_SIZE, border_size: 2, format: AttachmentFormat::Rgba8, ..default() }); let mut tile_atlas = TileAtlas::new(&config); let preprocessor = Preprocessor::new() .clear_attachment(0, &mut tile_atlas) .clear_attachment(1, &mut tile_atlas) .preprocess_tile( PreprocessDataset { attachment_index: 0, path: format!("{PATH}/source/height.png"), lod_range: 0..LOD_COUNT, ..default() }, &asset_server, &mut tile_atlas, ) .preprocess_tile( PreprocessDataset { attachment_index: 1, path: format!("{PATH}/source/albedo.png"), lod_range: 0..LOD_COUNT, ..default() }, &asset_server, &mut tile_atlas, ); commands.spawn((tile_atlas, preprocessor)); } ================================================ FILE: examples/preprocess_spherical.rs ================================================ use bevy::prelude::*; use bevy_terrain::prelude::*; const PATH: &str = "terrains/spherical"; const TEXTURE_SIZE: u32 = 512; const LOD_COUNT: u32 = 5; fn main() { App::new() .add_plugins(( DefaultPlugins.build().disable::(), TerrainPlugin, TerrainPreprocessPlugin, )) .add_systems(Startup, setup) .run(); } fn setup(mut commands: Commands, asset_server: Res) { let config = TerrainConfig { lod_count: LOD_COUNT, path: PATH.to_string(), atlas_size: 2048, ..default() } .add_attachment(AttachmentConfig { name: "height".to_string(), texture_size: TEXTURE_SIZE, border_size: 2, format: AttachmentFormat::R16, ..default() }); let mut tile_atlas = TileAtlas::new(&config); let preprocessor = Preprocessor::new() .clear_attachment(0, &mut tile_atlas) .preprocess_spherical( SphericalDataset { attachment_index: 0, paths: (0..6) .map(|side| format!("{PATH}/source/height/face{side}.tif")) .collect(), lod_range: 0..LOD_COUNT, }, &asset_server, &mut tile_atlas, ); commands.spawn((tile_atlas, preprocessor)); } ================================================ FILE: examples/spherical.rs ================================================ use bevy::{math::DVec3, prelude::*, reflect::TypePath, render::render_resource::*}; use bevy_terrain::prelude::*; const PATH: &str = "terrains/spherical"; const RADIUS: f64 = 6371000.0; const MAJOR_AXES: f64 = 6378137.0; const MINOR_AXES: f64 = 6356752.314245; const MIN_HEIGHT: f32 = -12000.0; const MAX_HEIGHT: f32 = 9000.0; const TEXTURE_SIZE: u32 = 512; const LOD_COUNT: u32 = 16; #[derive(Asset, AsBindGroup, TypePath, Clone)] pub struct TerrainMaterial { #[texture(0, dimension = "1d")] #[sampler(1)] gradient: Handle, } impl Material for TerrainMaterial { fn fragment_shader() -> ShaderRef { "shaders/spherical.wgsl".into() } } fn main() { App::new() .add_plugins(( DefaultPlugins.build().disable::(), TerrainPlugin, TerrainMaterialPlugin::::default(), TerrainDebugPlugin, // enable debug settings and controls )) // .insert_resource(ClearColor(Color::WHITE)) .add_systems(Startup, setup) .run(); } fn setup( mut commands: Commands, mut images: ResMut, mut meshes: ResMut>, mut materials: ResMut>, mut tile_trees: ResMut>, asset_server: Res, ) { let gradient = asset_server.load("textures/gradient.png"); images.load_image( &gradient, TextureDimension::D1, TextureFormat::Rgba8UnormSrgb, ); // Configure all the important properties of the terrain, as well as its attachments. let config = TerrainConfig { lod_count: LOD_COUNT, model: TerrainModel::ellipsoid(DVec3::ZERO, MAJOR_AXES, MINOR_AXES, MIN_HEIGHT, MAX_HEIGHT), // model: TerrainModel::ellipsoid( // DVec3::ZERO, // 6378137.0, // 6378137.0 * 0.5, // MIN_HEIGHT, // MAX_HEIGHT, // ), // model: TerrainModel::sphere(DVec3::ZERO, RADIUS), path: PATH.to_string(), ..default() } .add_attachment(AttachmentConfig { name: "height".to_string(), texture_size: TEXTURE_SIZE, border_size: 2, mip_level_count: 4, format: AttachmentFormat::R16, }); // Configure the quality settings of the terrain view. Adapt the settings to your liking. let view_config = TerrainViewConfig::default(); let tile_atlas = TileAtlas::new(&config); let tile_tree = TileTree::new(&tile_atlas, &view_config); commands.spawn_big_space(ReferenceFrame::default(), |root| { let frame = root.frame().clone(); let terrain = root .spawn_spatial(( TerrainBundle::new(tile_atlas, &frame), materials.add(TerrainMaterial { gradient: gradient.clone(), }), )) .id(); let view = root .spawn_spatial(DebugCameraBundle::new( -DVec3::X * RADIUS * 3.0, RADIUS, &frame, )) .id(); tile_trees.insert((terrain, view), tile_tree); let sun_position = DVec3::new(-1.0, 1.0, -1.0) * RADIUS * 10.0; let (sun_cell, sun_translation) = frame.translation_to_grid(sun_position); root.spawn_spatial(( PbrBundle { mesh: meshes.add(Sphere::new(RADIUS as f32 * 2.0).mesh().build()), transform: Transform::from_translation(sun_translation), ..default() }, sun_cell, )); root.spawn_spatial(PbrBundle { mesh: meshes.add(Cuboid::from_length(RADIUS as f32 * 0.1)), ..default() }); }); } ================================================ FILE: src/big_space.rs ================================================ pub use big_space::{BigSpaceCommands, FloatingOrigin}; pub type GridPrecision = i32; pub type BigSpacePlugin = big_space::BigSpacePlugin; pub type ReferenceFrame = big_space::reference_frame::ReferenceFrame; pub type ReferenceFrames<'w, 's> = big_space::reference_frame::local_origin::ReferenceFrames<'w, 's, GridPrecision>; pub type GridCell = big_space::GridCell; pub type GridTransform = big_space::world_query::GridTransform; pub type GridTransformReadOnly = big_space::world_query::GridTransformReadOnly; pub type GridTransformOwned = big_space::world_query::GridTransformOwned; pub type GridTransformItem<'w> = big_space::world_query::GridTransformItem<'w, GridPrecision>; ================================================ FILE: src/debug/camera.rs ================================================ #[cfg(feature = "high_precision")] use crate::big_space::{ FloatingOrigin, GridCell, GridTransform, GridTransformItem, ReferenceFrame, ReferenceFrames, }; use bevy::{input::mouse::MouseMotion, math::DVec3, prelude::*}; #[derive(Bundle)] pub struct DebugCameraBundle { pub camera: Camera3dBundle, pub controller: DebugCameraController, #[cfg(feature = "high_precision")] pub cell: GridCell, #[cfg(feature = "high_precision")] pub origin: FloatingOrigin, } impl Default for DebugCameraBundle { fn default() -> Self { Self { camera: default(), controller: default(), #[cfg(feature = "high_precision")] cell: default(), #[cfg(feature = "high_precision")] origin: FloatingOrigin, } } } impl DebugCameraBundle { #[cfg(feature = "high_precision")] pub fn new(position: DVec3, speed: f64, frame: &ReferenceFrame) -> Self { let (cell, translation) = frame.translation_to_grid(position); Self { camera: Camera3dBundle { transform: Transform::from_translation(translation).looking_to(Vec3::X, Vec3::Y), projection: PerspectiveProjection { near: 0.000001, ..default() } .into(), ..default() }, cell, controller: DebugCameraController { translation_speed: speed, ..default() }, ..default() } } #[cfg(not(feature = "high_precision"))] pub fn new(position: Vec3, speed: f64) -> Self { Self { camera: Camera3dBundle { transform: Transform::from_translation(position).looking_to(Vec3::X, Vec3::Y), projection: PerspectiveProjection { near: 0.000001, ..default() } .into(), ..default() }, controller: DebugCameraController { translation_speed: speed, ..default() }, ..default() } } } #[derive(Clone, Debug, Reflect, Component)] pub struct DebugCameraController { pub enabled: bool, /// Smoothness of translation, from `0.0` to `1.0`. pub translational_smoothness: f64, /// Smoothness of rotation, from `0.0` to `1.0`. pub rotational_smoothness: f32, pub translation_speed: f64, pub rotation_speed: f32, pub acceleration_speed: f64, pub translation_velocity: DVec3, pub rotation_velocity: Vec2, } impl Default for DebugCameraController { fn default() -> Self { Self { enabled: false, translational_smoothness: 0.9, rotational_smoothness: 0.8, translation_speed: 10e1, rotation_speed: 1e-1, acceleration_speed: 4.0, translation_velocity: Default::default(), rotation_velocity: Default::default(), } } } pub fn camera_controller( #[cfg(feature = "high_precision")] frames: ReferenceFrames, time: Res