use bevy::{
pbr::MeshMaterial3d,
prelude::{
App, Assets, Bundle, Component, Image, Mesh, Plugin, Query, ResMut, StandardMaterial,
Update, Vec3,
},
render::{
mesh::Mesh3d,
render_asset::RenderAssetUsages,
render_resource::{PrimitiveTopology, TextureFormat},
},
};
use colorgrad::LinearGradient;
use image::Pixel;
use serde::{Deserialize, Serialize};
use crate::{
noise::{get_noise_at_point_3d, Function, Gradient, Method, Region},
util::export_model,
};
#[derive(Component, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Planet {
pub seed: u32,
pub scale: f64,
pub offset: [f64; 3],
pub method: Method,
pub function: Function,
pub resolution: u32,
pub gradient: Gradient,
pub base_color: [u8; 4],
pub regions: Vec<Region>,
pub wireframe: bool,
pub height_exponent: f32,
pub sea_percent: f32,
#[serde(skip)]
pub export: bool,
}
impl Default for Planet {
fn default() -> Self {
Self {
seed: 0,
scale: 20.0,
offset: [0.0; 3],
method: Method::Perlin,
function: Function::default(),
resolution: 20,
regions: vec![
Region {
label: "Region #1".to_string(),
color: [255, 0, 0, 255],
position: 0.0,
},
Region {
label: "Region #2".to_string(),
color: [0, 0, 255, 255],
position: 100.0,
},
],
gradient: Gradient::default(),
base_color: [255, 255, 255, 255],
wireframe: false,
height_exponent: 1.5,
sea_percent: 50.0,
export: false,
}
}
}
#[derive(Bundle, Default)]
pub struct PlanetBundle {
pub planet: Planet,
pub mesh: (Mesh3d, MeshMaterial3d<StandardMaterial>),
}
pub struct PlanetPlugin;
impl Plugin for PlanetPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, generate_planet);
}
}
struct MeshData {
positions: Vec<[f32; 3]>,
indices: Vec<u32>,
normals: Vec<[f32; 3]>,
uvs: Vec<[f32; 2]>,
colors: Vec<[f32; 4]>,
}
fn generate_planet(
mut images: ResMut<Assets<Image>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
mut query: Query<(&mut Planet, &mut Mesh3d, &MeshMaterial3d<StandardMaterial>)>,
) {
for (mut planet, mut mesh_handle, material) in &mut query {
if let Some(material) = materials.get_mut(material) {
*material = StandardMaterial::default();
}
let grad = generate_gradient(&mut images, &mut planet);
let mut positions: Vec<[f32; 3]> = vec![];
let mut indices: Vec<u32> = vec![];
let mut normals: Vec<[f32; 3]> = vec![];
let mut uvs: Vec<[f32; 2]> = vec![];
let mut colors: Vec<[f32; 4]> = vec![];
let mut index_start = 0;
for direction in [
Vec3::Y,
Vec3::NEG_Y,
Vec3::X,
Vec3::NEG_X,
Vec3::Z,
Vec3::NEG_Z,
] {
let mut mesh_data = generate_face(&planet, direction, &grad);
positions.extend(mesh_data.positions);
mesh_data.indices = mesh_data
.indices
.iter()
.map(|index| index + index_start)
.collect();
index_start = mesh_data.indices.iter().max().unwrap_or(&0) + 1;
indices.extend(mesh_data.indices);
normals.extend(mesh_data.normals);
uvs.extend(mesh_data.uvs);
colors.extend(mesh_data.colors);
}
if planet.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 planet.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 planet.export {
export_model(&positions, indices, &colors);
planet.export = false;
}
}
}
fn generate_gradient(
images: &mut ResMut<Assets<Image>>,
planet: &mut Planet,
) -> colorgrad::LinearGradient {
let mut colors: Vec<colorgrad::Color> = Vec::with_capacity(planet.regions.len());
let mut domain: Vec<f32> = Vec::with_capacity(planet.regions.len());
for region in &planet.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(
planet.gradient.size[0],
planet.gradient.size[1],
image::Rgba(planet.base_color),
);
for (x, _, pixel) in gradient_buffer.enumerate_pixels_mut() {
let rgba = colorgrad::Gradient::at(
&grad,
(f64::from(x) * 100.0 / f64::from(planet.gradient.size[0])) as f32,
)
.to_rgba8();
pixel.blend(&image::Rgba(rgba));
}
planet.gradient.image = images.add(
Image::from_dynamic(
gradient_buffer.into(),
true,
RenderAssetUsages::RENDER_WORLD,
)
.convert(TextureFormat::Rgba8UnormSrgb)
.expect("Could not convert to Rgba8UnormSrgb"),
);
grad
}
fn generate_face(planet: &Planet, local_up: Vec3, grad: &colorgrad::LinearGradient) -> MeshData {
let axis_a = Vec3::new(local_up.y, local_up.z, local_up.x);
let axis_b = local_up.cross(axis_a);
let vertices_count = (planet.resolution * planet.resolution) as usize;
let triangle_count = ((planet.resolution - 1) * (planet.resolution - 1) * 6) as usize;
let mut positions: Vec<[f32; 3]> = Vec::with_capacity(vertices_count);
let mut indices: Vec<u32> = Vec::with_capacity(triangle_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 colors: Vec<[f32; 4]> = Vec::with_capacity(vertices_count);
let resolution = planet.resolution + 1;
for y in 0..resolution {
for x in 0..resolution {
let x_percent = x as f32 / (resolution as f32 - 1.0);
let y_percent = y as f32 / (resolution as f32 - 1.0);
let vertex =
(local_up + (x_percent - 0.5) * 2.0 * axis_a + (y_percent - 0.5) * 2.0 * axis_b)
.normalize();
let noise_value = (get_noise_at_point_3d(
[
f64::from(vertex[0]),
f64::from(vertex[1]),
f64::from(vertex[2]),
],
planet.seed,
planet.scale / 100.0,
planet.offset,
&planet.method,
&planet.function,
) as f32
+ 1.0)
* 0.5;
let height_value = (0_f32.max(noise_value - planet.sea_percent / 100.0)) * 0.2;
let vertex = vertex * (1.0 + height_value.powf(planet.height_exponent));
let i = x + y * resolution;
positions.push([vertex.x, vertex.y, vertex.z]);
normals.push([vertex.x, vertex.y, vertex.z]);
let color = colorgrad::Gradient::at(grad, (f64::from(noise_value) * 100.0) as f32);
let color = [
color.r as f32,
color.g as f32,
color.b as f32,
color.a as f32,
];
colors.push(color);
uvs.push([x_percent, y_percent]);
if x != resolution - 1 && y != resolution - 1 {
indices.push(i);
indices.push(i + resolution + 1);
indices.push(i + resolution);
indices.push(i);
indices.push(i + 1);
indices.push(i + resolution + 1);
}
}
}
MeshData {
positions,
indices,
normals,
uvs,
colors,
}
}