use bevy::{
prelude::*,
render::{
render_asset::RenderAssetUsages,
render_resource::{PrimitiveTopology, TextureFormat},
},
};
use colorgrad::LinearGradient;
use image::Pixel;
use serde::{Deserialize, Serialize};
use crate::{noise::generate_noise_map, noise::Noise, util::export_model};
#[derive(Component, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Terrain {
pub noise: Noise,
pub size: [u32; 2],
pub resolution: u32,
pub wireframe: bool,
pub height_exponent: f32,
pub sea_percent: f32,
#[serde(skip)]
pub export: bool,
}
impl Default for Terrain {
fn default() -> Self {
Self {
noise: Noise::default(),
size: [2; 2],
resolution: 15,
wireframe: false,
height_exponent: 1.0,
sea_percent: 10.0,
export: false,
}
}
}
#[derive(Bundle, Default)]
pub struct TerrainBundle {
pub terrain: Terrain,
pub mesh: (Mesh3d, MeshMaterial3d<StandardMaterial>),
}
pub struct TerrainPlugin;
impl Plugin for TerrainPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, generate_terrain);
}
}
fn generate_terrain(
mut images: ResMut<Assets<Image>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
mut query: Query<(&mut Terrain, &mut Mesh3d, &MeshMaterial3d<StandardMaterial>)>,
) {
for (mut terrain, mut mesh_handle, material) in &mut query {
if let Some(material) = materials.get_mut(material) {
*material = StandardMaterial::default();
}
terrain.noise.size = [
terrain.size[0] * terrain.resolution,
terrain.size[1] * terrain.resolution,
];
let noise_values = generate_noise_map(&terrain.noise);
let mut colors: Vec<colorgrad::Color> = Vec::with_capacity(terrain.noise.regions.len());
let mut domain: Vec<f32> = Vec::with_capacity(terrain.noise.regions.len());
for region in &terrain.noise.regions {
colors.push(colorgrad::Color {
r: f32::from(region.color[0]) / 255.0,
g: f32::from(region.color[1]) / 255.0,
b: f32::from(region.color[2]) / 255.0,
a: f32::from(region.color[3]) / 255.0,
});
domain.push(region.position);
}
let grad = colorgrad::GradientBuilder::new()
.colors(&colors)
.domain(&domain)
.build::<LinearGradient>()
.unwrap_or_else(|_| {
colorgrad::GradientBuilder::new()
.colors(&colors)
.build::<LinearGradient>()
.expect("Gradient generation failed")
});
let mut gradient_buffer = image::ImageBuffer::from_pixel(
terrain.noise.gradient.size[0],
terrain.noise.gradient.size[1],
image::Rgba(terrain.noise.base_color),
);
for (x, _, pixel) in gradient_buffer.enumerate_pixels_mut() {
let rgba = colorgrad::Gradient::at(
&grad,
(f64::from(x) * 100.0 / f64::from(terrain.noise.gradient.size[0])) as f32,
)
.to_rgba8();
pixel.blend(&image::Rgba(rgba));
}
terrain.noise.gradient.image = images.add(
Image::from_dynamic(
gradient_buffer.into(),
true,
RenderAssetUsages::RENDER_WORLD,
)
.convert(TextureFormat::Rgba8UnormSrgb)
.expect("Could not convert to Rgba8UnormSrgb"),
);
let vertices_count: usize =
((terrain.noise.size[0] + 1) * (terrain.noise.size[1] + 1)) as usize;
let triangle_count: usize =
(terrain.noise.size[0] * terrain.noise.size[1] * 2 * 3) as usize;
let mut positions: Vec<[f32; 3]> = Vec::with_capacity(vertices_count);
let mut normals: Vec<[f32; 3]> = Vec::with_capacity(vertices_count);
let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(vertices_count);
let mut indices: Vec<u32> = Vec::with_capacity(triangle_count);
let mut colors: Vec<[f32; 4]> = Vec::with_capacity(vertices_count);
let rows = terrain.size[0] * terrain.resolution;
let cols = terrain.size[1] * terrain.resolution;
let width = terrain.size[0] as f32 + 1.0;
let depth = terrain.size[1] as f32 + 1.0;
for row in 0..rows {
for col in 0..cols {
let row = row as f32;
let col = col as f32;
let noise_value = noise_values[row as usize][col as usize] as f32;
let height_value = (0_f32.max(noise_value - terrain.sea_percent)) / 100.0;
let x = (row / terrain.resolution as f32 - width / 2.0) + 0.5;
let y = ((height_value * 1.2).powf(terrain.height_exponent) - 0.5) * 2.0;
let z = (col / terrain.resolution as f32 - depth / 2.0) + 0.5;
let color = colorgrad::Gradient::at(
&grad,
(noise_values[row as usize][col as usize]) as f32,
);
let color = [
color.r as f32,
color.g as f32,
color.b as f32,
color.a as f32,
];
positions.push([x, y, z]);
normals.push([0.0, 1.0, 0.0]);
uvs.push([row, col]);
colors.push(color);
}
}
for i in 0..(rows - 1) {
for j in 0..(cols - 1) {
let current = i * cols + j;
let next_row = (i + 1) * cols + j;
indices.push(current);
indices.push(current + 1);
indices.push(next_row);
indices.push(next_row);
indices.push(current + 1);
indices.push(next_row + 1);
}
}
if terrain.wireframe {
let triangle_number = indices.len() / 3;
let cloned_indices = indices.clone();
indices = vec![];
for i in 0..triangle_number {
for j in &[0, 1, 1, 2, 2, 0] {
indices.push(cloned_indices[i * 3 + j]);
}
}
}
let mut mesh = if terrain.wireframe {
Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::RENDER_WORLD)
} else {
Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::RENDER_WORLD,
)
};
mesh.insert_indices(bevy::render::mesh::Indices::U32(indices.clone()));
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions.clone());
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors.clone());
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
*mesh_handle = Mesh3d(meshes.add(mesh));
if terrain.export {
export_model(&positions, indices, &colors);
terrain.export = false;
}
}
}