├── terrain.png ├── assets └── fonts │ ├── FiraSans-Bold.ttf │ └── FiraMono-Medium.ttf ├── src ├── lib.rs ├── terrain_common.rs ├── gizmo.rs ├── terrain_material.rs ├── main.rs ├── terrain.rs ├── ui.rs ├── rtin.rs └── terrain_rtin.rs └── Cargo.toml /terrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clynamen/bevy_terrain/HEAD/terrain.png -------------------------------------------------------------------------------- /assets/fonts/FiraSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clynamen/bevy_terrain/HEAD/assets/fonts/FiraSans-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/FiraMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clynamen/bevy_terrain/HEAD/assets/fonts/FiraMono-Medium.ttf -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod terrain; 2 | pub mod rtin; 3 | pub mod terrain_rtin; 4 | pub mod terrain_material; 5 | pub mod gizmo; 6 | pub mod terrain_common; -------------------------------------------------------------------------------- /src/terrain_common.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | pub struct Terrain {} 3 | 4 | #[derive(Default)] 5 | pub struct TerrainImageLoadOptions { 6 | pub max_image_height : f32, 7 | pub pixel_side_length : f32 8 | } 9 | 10 | #[derive(Default)] 11 | pub struct TerrainMeshResource { 12 | pub shaded: Handle, 13 | pub wireframe: Handle, 14 | } 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_terrain" 3 | version = "0.1.0" 4 | authors = ["clynamen "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | palette = "0.5" 11 | bevy = "0.4.0" 12 | bevy_render = "0.4.0" 13 | bevy_fly_camera = "0.6.0" 14 | image = "0.23.12" 15 | anyhow = "1.0.37" 16 | bitintr = "0.3.0" 17 | nalgebra = "0.24.0" -------------------------------------------------------------------------------- /src/gizmo.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | 3 | pub struct AxisGizmo { 4 | 5 | } 6 | 7 | pub fn add_axis_gizmo( 8 | commands: &mut Commands, 9 | mut meshes: ResMut>, 10 | mut materials: ResMut>, 11 | transform: Transform 12 | ) { 13 | commands 14 | .spawn(PbrBundle { 15 | mesh: meshes.add(Mesh::from(shape::Icosphere { radius: 0.1, subdivisions: 10 })), 16 | material: materials.add(Color::rgb(0.0, 0.0, 0.0).into()), 17 | transform: transform, 18 | ..Default::default() 19 | }).with(AxisGizmo{}) 20 | .with_children(|parent: &mut ChildBuilder| { 21 | parent 22 | .spawn(PbrBundle { 23 | mesh: meshes.add(Mesh::from(shape::Cube { size: 0.1 })), 24 | material: materials.add(Color::rgb(1.0, 0.0, 0.0).into()), 25 | transform: Transform::from_translation(Vec3::new(1.0, 0.0, 0.0)), 26 | ..Default::default() 27 | }) 28 | .spawn(PbrBundle { 29 | mesh: meshes.add(Mesh::from(shape::Cube { size: 0.1 })), 30 | material: materials.add(Color::rgb(0.0, 1.0, 0.0).into()), 31 | transform: Transform::from_translation(Vec3::new(0.0, 1.0, 0.0)), 32 | ..Default::default() 33 | }) 34 | .spawn(PbrBundle { 35 | mesh: meshes.add(Mesh::from(shape::Cube { size: 0.1 })), 36 | material: materials.add(Color::rgb(0.0, 0.0, 1.0).into()), 37 | transform: Transform::from_translation(Vec3::new(0.0, 0.0, 1.0)), 38 | ..Default::default() 39 | }); 40 | }); 41 | } -------------------------------------------------------------------------------- /src/terrain_material.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy::{ 3 | reflect::TypeUuid, 4 | render::{ 5 | pipeline::{PipelineDescriptor}, 6 | render_graph::{base, AssetRenderResourcesNode, RenderGraph}, 7 | renderer::RenderResources, 8 | shader::{ShaderStage, ShaderStages}, 9 | }, 10 | }; 11 | 12 | #[derive(RenderResources, Default, TypeUuid)] 13 | #[uuid = "0320b9b8-b3a3-4baa-8bfa-c94008177b17"] 14 | pub struct TerrainMaterial { 15 | } 16 | 17 | impl TerrainMaterial { 18 | pub const ATTRIBUTE_COLOR: &'static str = "Vertex_Color"; 19 | } 20 | 21 | const VERTEX_SHADER: &str = r#" 22 | #version 450 23 | layout(location = 0) in vec3 Vertex_Position; 24 | layout(location = 1) in vec3 Vertex_Color; 25 | layout(location = 0) out vec3 v_color; 26 | layout(set = 0, binding = 0) uniform Camera { 27 | mat4 ViewProj; 28 | }; 29 | layout(set = 1, binding = 0) uniform Transform { 30 | mat4 Model; 31 | }; 32 | void main() { 33 | gl_Position = ViewProj * Model * vec4(Vertex_Position, 1.0); 34 | v_color = Vertex_Color; 35 | } 36 | "#; 37 | 38 | const FRAGMENT_SHADER: &str = r#" 39 | #version 450 40 | layout(location = 0) out vec4 o_Target; 41 | layout(location = 0) in vec3 v_color; 42 | void main() { 43 | o_Target = vec4(v_color, 1.0); 44 | // o_Target = vec4(1.0, 1.0, 1.0, 1.0); 45 | } 46 | "#; 47 | 48 | pub fn add_terrain_material( 49 | mut pipelines: ResMut>, 50 | mut shaders: ResMut>, 51 | mut render_graph: ResMut 52 | ) -> Handle { 53 | 54 | // Create a new shader pipeline 55 | let pipeline_handle = pipelines.add(PipelineDescriptor::default_config(ShaderStages { 56 | vertex: shaders.add(Shader::from_glsl(ShaderStage::Vertex, VERTEX_SHADER)), 57 | fragment: Some(shaders.add(Shader::from_glsl(ShaderStage::Fragment, FRAGMENT_SHADER))), 58 | })); 59 | 60 | // Add an AssetRenderResourcesNode to our Render Graph. This will bind TerrainMaterial resources to our shader 61 | render_graph.add_system_node( 62 | "my_material_with_vertex_color_support", 63 | AssetRenderResourcesNode::::new(true), 64 | ); 65 | 66 | // Add a Render Graph edge connecting our new "my_material" node to the main pass node. This ensures "my_material" runs before the main pass 67 | render_graph 68 | .add_node_edge( 69 | "my_material_with_vertex_color_support", 70 | base::node::MAIN_PASS, 71 | ) 72 | .unwrap(); 73 | 74 | pipeline_handle 75 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod ui; 2 | 3 | use bevy_terrain::{terrain_common::{ 4 | Terrain, TerrainImageLoadOptions, TerrainMeshResource}, terrain_rtin::{RtinParams, rtin_load_terrain}}; 5 | use bevy_terrain::{gizmo::add_axis_gizmo, terrain::{terrain_example}, terrain_material::TerrainMaterial}; 6 | use bevy::prelude::*; 7 | use bevy_fly_camera::{FlyCamera, FlyCameraPlugin}; 8 | use bevy_render::{ 9 | mesh::{Mesh}, 10 | }; 11 | use bevy_terrain::terrain_material::add_terrain_material; 12 | use ui::{ButtonMaterials, button_system, setup_ui, show_ui_system, update_terrain_system}; 13 | 14 | use bevy::{ 15 | render::{ 16 | pipeline::{PipelineDescriptor, RenderPipeline}, 17 | render_graph::{RenderGraph}, 18 | }, 19 | }; 20 | 21 | fn main() { 22 | 23 | terrain_example(); 24 | 25 | App::build() 26 | .add_resource(Msaa { samples: 4 }) 27 | .add_plugins(DefaultPlugins) 28 | .add_plugin(FlyCameraPlugin) 29 | .add_asset::() 30 | .init_resource::() 31 | .init_resource::() 32 | .init_resource::() 33 | .add_startup_system(setup.system()) 34 | .add_system(button_system.system()) 35 | .add_system(update_terrain_system.system()) 36 | .add_system(show_ui_system.system()) 37 | .run(); 38 | } 39 | 40 | 41 | fn setup( 42 | commands: &mut Commands, 43 | mut meshes: ResMut>, 44 | materials: ResMut>, 45 | asset_server: Res, 46 | button_materials: Res, 47 | pipelines: ResMut>, 48 | shaders: ResMut>, 49 | render_graph: ResMut, 50 | mut rtin_params: ResMut, 51 | mut terrain_mesh_res: ResMut, 52 | color_materials: ResMut>, 53 | ) { 54 | 55 | let image_filename = "terrain.png"; 56 | 57 | rtin_params.error_threshold = 0.2; 58 | rtin_params.load_options = TerrainImageLoadOptions { 59 | max_image_height : 20f32, 60 | pixel_side_length: 1f32 61 | }; 62 | 63 | let (terrain_shaded_mesh, terrain_wireframe_mesh) = 64 | rtin_load_terrain(image_filename, 65 | &rtin_params); 66 | 67 | let terrain_shaded_mesh_handle = meshes.add(terrain_shaded_mesh); 68 | let terrain_wireframe_mesh_handle = meshes.add(terrain_wireframe_mesh); 69 | 70 | terrain_mesh_res.shaded = terrain_shaded_mesh_handle; 71 | terrain_mesh_res.wireframe = terrain_wireframe_mesh_handle; 72 | 73 | let pipeline_handle = add_terrain_material( 74 | pipelines, shaders, render_graph); 75 | 76 | 77 | commands 78 | .spawn(MeshBundle { 79 | mesh: terrain_mesh_res.shaded.clone(), 80 | render_pipelines: RenderPipelines::from_pipelines(vec![RenderPipeline::new( 81 | pipeline_handle, 82 | )]), 83 | transform: Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), 84 | ..Default::default() 85 | }).with(Terrain{}) 86 | .spawn(LightBundle { 87 | transform: Transform::from_translation(Vec3::new(0.0, 4.0, 0.0)), 88 | ..Default::default() 89 | }) 90 | // camera 91 | .spawn(Camera3dBundle { 92 | transform: Transform::from_translation(Vec3::new(0.0, 20.0, 0.0)) 93 | .looking_at(Vec3::default(), Vec3::unit_y()), 94 | ..Default::default() 95 | }) 96 | .with(FlyCamera{ 97 | pitch: 180.0, 98 | ..Default::default() 99 | }); 100 | 101 | // add_axis_gizmo(commands, meshes, materials, 102 | // Transform::from_translation(Vec3::new(0f32, 0f32, 0f32))); 103 | 104 | setup_ui(commands, 105 | asset_server, 106 | color_materials, 107 | button_materials, rtin_params); 108 | } 109 | -------------------------------------------------------------------------------- /src/terrain.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::vec::Vec; 3 | use crate::terrain_common::TerrainImageLoadOptions; 4 | use image::ImageBuffer; 5 | use bevy_render::{ 6 | pipeline::PrimitiveTopology, 7 | mesh::{Mesh, VertexAttributeValues, Indices}, 8 | }; 9 | use image::Luma; 10 | 11 | 12 | pub fn terrain_example() -> Mesh { 13 | let options = TerrainImageLoadOptions { 14 | max_image_height : 1f32, 15 | pixel_side_length : 1f32, 16 | }; 17 | 18 | let filename = "terrain.png"; 19 | 20 | let mesh = load_terrain_bitmap(filename, options); 21 | mesh.unwrap() 22 | } 23 | 24 | fn sample_vertex_height(cy: i32, cx: i32, heightmap: &ImageBuffer, Vec::>) -> f32 { 25 | let mut cnt = 0; 26 | let mut height = 0.0; 27 | 28 | for dy in [-1, 0].iter() { 29 | for dx in [-1, 0].iter() { 30 | let sy = cy + dy; 31 | let sx = cx + dx; 32 | if sy < 0 33 | || sx < 0 34 | || sy >= heightmap.height() as i32 35 | || sx >= heightmap.width() as i32 { 36 | continue; 37 | } else { 38 | height += heightmap.get_pixel( 39 | sx as u32, sy as u32).0[0] as f32 * 1.0f32 / std::u16::MAX as f32; 40 | cnt += 1; 41 | } 42 | } 43 | } 44 | 45 | height / cnt as f32 46 | } 47 | 48 | fn load_terrain_bitmap(filename: &str, options: TerrainImageLoadOptions) -> Result { 49 | let terrain_bitmap = image::open(filename)?; 50 | let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); 51 | 52 | let heightmap = terrain_bitmap.as_luma16().unwrap(); 53 | 54 | let mut vertices : Vec::<[f32; 3]> = Vec::new(); 55 | let mut normals : Vec::<[f32; 3]> = Vec::new(); 56 | let mut indices : Vec:: = Vec::new(); 57 | 58 | let vertex_number = ( (heightmap.height() + 1) * 59 | (heightmap.width() + 1) ) as usize; 60 | 61 | vertices.resize(vertex_number, [0.0f32, 0.0f32, 0.0f32]); 62 | normals.resize(vertex_number, [0.0f32, 1.0f32, 0.0f32]); 63 | let uvs = vec![[0.0, 0.0, 0.0]; vertices.len()]; 64 | 65 | 66 | let mut vertex_index = 0; 67 | for cy in 0..(heightmap.height() as i32 +1) { 68 | for cx in 0..(heightmap.width() as i32 +1) { 69 | let height = sample_vertex_height(cy, cx, heightmap); 70 | // println!("sampled height at y={:>3} x={:>3} = {:>4}", cy, cx, height); 71 | 72 | vertices[vertex_index] = [cx as f32 * options.pixel_side_length, 73 | height * options.max_image_height, 74 | cy as f32 * options.pixel_side_length]; 75 | vertex_index += 1; 76 | } 77 | } 78 | 79 | let grid_height = heightmap.height() + 1; 80 | let grid_width = heightmap.width() + 1; 81 | 82 | for cy in 0..(heightmap.height()) { 83 | for cx in 0..(heightmap.width()) { 84 | indices.extend([ 85 | cy * grid_width + cx, 86 | (cy + 1) * grid_width + cx + 1, 87 | cy * grid_width + cx + 1, 88 | ].iter()); 89 | indices.extend([ 90 | cy * grid_width + cx, 91 | (cy + 1) * grid_width + cx, 92 | (cy + 1) * grid_width + cx + 1, 93 | ].iter()); 94 | } 95 | } 96 | 97 | // for i in 0..(indices.len()/3) { 98 | // println!("triangle {:03}: {} {} {} ", 99 | // i, indices[i*3], indices[i*3+1], indices[i*3+2]) 100 | // } 101 | 102 | // println!(" {} {} ", indices.len() / 3, 2 * heightmap.height() * (heightmap.width())); 103 | 104 | assert!(indices.len() as u32 / 3 == 2 * heightmap.height() * (heightmap.width()) ); 105 | 106 | 107 | mesh.set_attribute( 108 | Mesh::ATTRIBUTE_POSITION, 109 | VertexAttributeValues::Float3(vertices)); 110 | mesh.set_attribute( 111 | Mesh::ATTRIBUTE_NORMAL, 112 | VertexAttributeValues::Float3(normals)); 113 | mesh.set_attribute( 114 | Mesh::ATTRIBUTE_UV_0, 115 | VertexAttributeValues::Float3(uvs)); 116 | mesh.set_indices(Some(Indices::U32(indices))); 117 | 118 | 119 | Ok(mesh) 120 | } 121 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_fly_camera::FlyCamera; 3 | use bevy_terrain::{terrain_common::TerrainMeshResource, terrain_rtin::rtin_load_terrain}; 4 | use bevy_terrain::{terrain_common::Terrain, terrain_rtin::RtinParams}; 5 | pub struct ButtonMaterials { 6 | shaded: Handle, 7 | wireframe: Handle, 8 | hovered: Handle, 9 | } 10 | 11 | impl FromResources for ButtonMaterials { 12 | fn from_resources(resources: &Resources) -> Self { 13 | let mut materials = resources.get_mut::>().unwrap(); 14 | ButtonMaterials { 15 | shaded: materials.add(Color::rgb(0.35, 0.35, 0.35).into()), 16 | wireframe: materials.add(Color::rgb(0.35, 0.35, 0.35).into()), 17 | hovered: materials.add(Color::rgb(0.55, 0.55, 0.55).into()), 18 | } 19 | } 20 | } 21 | 22 | #[derive(Debug, Eq, PartialEq)] 23 | enum MeshStyle { 24 | Shaded, 25 | Wireframe, 26 | } 27 | 28 | pub fn update_terrain_system( 29 | mut meshes: ResMut>, 30 | mut rtin_params: ResMut, 31 | keyboard_input: Res>, 32 | mut terrain_query: Query<(Entity, &mut Handle, &Terrain)>, 33 | mut text_query: Query<&mut Text, With>, 34 | mut terrain_mesh_res: ResMut, 35 | commands: &mut Commands, 36 | ) { 37 | let mut reload = false; 38 | 39 | if keyboard_input.just_pressed(KeyCode::Plus) { 40 | rtin_params.error_threshold += 0.05; 41 | } else if keyboard_input.just_released(KeyCode::Minus) { 42 | rtin_params.error_threshold -= 0.05; 43 | } else if keyboard_input.just_released(KeyCode::R) { 44 | reload = true; 45 | } 46 | 47 | rtin_params.error_threshold = rtin_params. 48 | error_threshold.max(0f32).min(1f32); 49 | 50 | for mut text in text_query.iter_mut() { 51 | text.value = format!("{:.2}", rtin_params.error_threshold); 52 | } 53 | 54 | if reload { 55 | let (terrain_shaded_mesh, terrain_wireframe_mesh) = 56 | rtin_load_terrain("terrain.png", &rtin_params); 57 | 58 | let terrain_shaded_mesh_handle = meshes.add(terrain_shaded_mesh); 59 | let terrain_wireframe_mesh_handle = meshes.add(terrain_wireframe_mesh); 60 | 61 | terrain_mesh_res.shaded = terrain_shaded_mesh_handle; 62 | terrain_mesh_res.wireframe = terrain_wireframe_mesh_handle; 63 | } 64 | } 65 | 66 | pub fn button_system( 67 | button_materials: Res, 68 | mut interaction_query: Query< 69 | (&Interaction, &mut Handle, &Children), 70 | (Mutated, With