From 5273e55c0bb76c8aaf22148898efd8273d701996 Mon Sep 17 00:00:00 2001 From: yukirij Date: Tue, 18 Jun 2024 17:21:59 -0700 Subject: [PATCH] Reorganize terrain; add orbital system. --- Cargo.toml | 1 + LICENSE.txt | 6 + docs/szun/planet.szun | 66 ++++++++ src/anchor.rs | 17 +- src/bin/system.rs | 44 ++++++ src/bin/world.rs | 319 ++++++++++++++++++++++++++++++-------- src/{obj.rs => export.rs} | 15 +- src/generator.rs | 30 +++- src/icosphere.rs | 7 +- src/lib.rs | 4 +- src/mesh.rs | 31 +++- src/planet/builder.rs | 13 ++ src/planet/mod.rs | 2 + src/planet/planet.rs | 20 +++ src/system/mod.rs | 127 +++++++++++++++ src/system/orbit.rs | 92 +++++++++++ src/terrain.rs | 105 ++++++++----- src/utility.rs | 25 ++- 18 files changed, 780 insertions(+), 144 deletions(-) create mode 100644 LICENSE.txt create mode 100644 docs/szun/planet.szun create mode 100644 src/bin/system.rs rename src/{obj.rs => export.rs} (70%) create mode 100644 src/planet/builder.rs create mode 100644 src/planet/mod.rs create mode 100644 src/planet/planet.rs create mode 100644 src/system/mod.rs create mode 100644 src/system/orbit.rs diff --git a/Cargo.toml b/Cargo.toml index 04f7938..be086e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" glam = "0.27.0" rand = "0.8.5" noise = "0.9.0" +gltf = "1.4.1" pool = { git = "https://git.tsukiyo.org/Utility/pool" } sparse = { path = "../../Utility/sparse" } diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..369a375 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,6 @@ +Project: Kirisame Project +Organization: Atelier Kirisame +Licensor: Yukiri Corporation + +License available at: +https://ykr.info/license/ diff --git a/docs/szun/planet.szun b/docs/szun/planet.szun new file mode 100644 index 0000000..890f920 --- /dev/null +++ b/docs/szun/planet.szun @@ -0,0 +1,66 @@ +Quaternion { x :decimal, y :decimal, z :decimal, w :decimal } + +StarSystem { + Orbit { + Orientation :Quaternion + Periapsis :decimal + Eccentricity :decimal + } + + Entity { + Data :record + Parent :natural + Children :list<{ Orbit :Orbit, Entity :natural }> + } + + Entities :list +} + +Octave { + Frequency :decimal + Amplitude :decimal +} + +Planet { + Standard { + Radius :decimal # km + Gravity :decimal # m/s2 + } + + Terrain :option<{ + + + Anchors :list<{ + Geography :{ + Elevation :decimal + Octaves :Octave + } + + Biome :Biome + }> + }> + + Ocean :option<{ + + + Anchors :list<{ + + }> + }> + + Atmosphere :option<{ + + + Anchors :list<{ + + }> + }> + + Climate :option<{ + + + Anchors :list<{ + + }> + }> +} diff --git a/src/anchor.rs b/src/anchor.rs index c44b83e..21a439b 100644 --- a/src/anchor.rs +++ b/src/anchor.rs @@ -1,7 +1,7 @@ use glam::{DMat3, DVec3}; use crate::{ terrain::Terrain, - generator::Generator, + generator::{Generator, Octave}, }; #[derive(Clone, Copy)] @@ -21,21 +21,6 @@ impl Neighbor { } } -#[derive(Clone, Copy)] -pub struct Octave { - pub frequency:f64, - pub amplitude:f64, -} -impl Octave { - pub fn new() -> Self - { - Self { - frequency:1., - amplitude:1., - } - } -} - #[derive(Clone, Copy)] pub struct Geography { pub elevation:f64, diff --git a/src/bin/system.rs b/src/bin/system.rs new file mode 100644 index 0000000..543844b --- /dev/null +++ b/src/bin/system.rs @@ -0,0 +1,44 @@ +use glam::{DVec3, DQuat}; +use terrain::{ + icosphere::Icosphere, + system::{StarSystem, Orbit}, + export::export_obj, +}; + +fn main() +{ + let ico = Icosphere::new(); + + let systems = [ + 5., + 1., + 0.5, + 1.5, + ]; + + let sys = StarSystem::singular(0, 10.) + .append( + StarSystem::singular(1, 1.) + .append( + StarSystem::singular(2, 0.5), + Orbit::with(DQuat::IDENTITY, 2., 0.1, 5., 0.), + ), + Orbit::with(DQuat::IDENTITY, 20., 0.5, 10., 0.), + ) + .append( + StarSystem::singular(3, 1.), + Orbit::with(DQuat::from_axis_angle(-DVec3::Z, f64::to_radians(30.)), 50., 0.1, 15., 0.), + ); + + let mut mesh = ico.to_mesh().scale(systems[0]); + for frame in 0..20 { + let time = frame as f64 * 0.5; + for (id, position) in sys.resolve(time) { + if id != 0 { + mesh = mesh.merge(&ico.to_mesh().scale(systems[id]), position.as_vec3()); + } + } + } + + export_obj("system.obj", &mesh, 10.).ok(); +} diff --git a/src/bin/world.rs b/src/bin/world.rs index 0624476..22ce99a 100644 --- a/src/bin/world.rs +++ b/src/bin/world.rs @@ -3,7 +3,12 @@ use noise::NoiseFn; use rand::prelude::*; use sparse::Sparse; use terrain::{ - anchor::Anchor, generator::Generator, icosphere::Icosphere, mesh::Mesh, terrain::Terrain + anchor::Anchor, + generator::Generator, + icosphere::Icosphere, + mesh::Mesh, + terrain::Terrain, + utility::rgb, }; fn get_subdivisions(radius_km:f64) -> usize @@ -52,8 +57,9 @@ fn get_neighbors(ico:&Icosphere, origin:usize, distance:usize) -> Vec<(usize, us } fn main() { - let radius_km = 100.; - let terrain_radius = 3; + let radius_km = 10.; + let terrain_radius = 6; + let max_terrain_detail = 14; let mut ico = Icosphere::new(); let mut anchors = Sparse::::new(); @@ -61,7 +67,10 @@ fn main() { for _ in 0..subdivisions { ico.subdivide(); } println!("Subdivisions: {}", subdivisions); - let world_scale = radius_km * 1000.; + let world_radius = radius_km * 1000.; + let world_relief = world_radius / 150.; // typically 100:1 - 300:1 + + println!("radius: {} / relief: {}", world_radius, world_relief); let mut rng = rand::thread_rng(); let perlin = noise::Perlin::new(12); @@ -95,6 +104,7 @@ fn main() { let basis = basis_inverse.inverse(); // Define anchor properties. + // anchors.set(id as isize, { let mut anchor = Anchor::new(); @@ -103,19 +113,25 @@ fn main() { anchor.basis = basis; anchor.ibasis = basis_inverse; - let mut elevation_frequency = 10000000. / 1.;// / world_scale; + let mut elevation_frequency = 1. / 4.; for _ in 0..4 { - anchor.geography.elevation += (100. * rng.gen::() / elevation_frequency) * perlin.get([elevation_frequency * anchor.position.x, elevation_frequency * anchor.position.y, elevation_frequency * anchor.position.z]); - elevation_frequency *= 25.; + let frequency = world_radius * elevation_frequency; + anchor.geography.elevation += (world_relief * elevation_frequency) * perlin.get([frequency * anchor.position.x, frequency * anchor.position.y, frequency * anchor.position.z]); + elevation_frequency /= 10.; } - let mut frequency = 32.; - let _octaves = (rng.gen::() * anchor.geography.octaves.len() as f64) as usize + 1; - for oct in 0..1 { - anchor.geography.octaves[oct].frequency = frequency; - anchor.geography.octaves[oct].amplitude = 100.; //rng.gen::(); - - frequency *= 1. / 4.; + let octaves = (rng.gen::() * anchor.geography.octaves.len() as f64) as usize + 1; + let mut octave_modifier = 500.; + for oct in 0..octaves { + let wavelength = (octave_modifier * world_relief * rng.gen::()) + 10.; + anchor.geography.octaves[oct].frequency = world_radius / wavelength; + anchor.geography.octaves[oct].amplitude = f64::sqrt(0.25 * rng.gen::() * wavelength); + anchor.geography.octaves[oct].offset = world_radius * DVec3::new( + (rng.gen::() * 2.) - 1., + (rng.gen::() * 2.) - 1., + (rng.gen::() * 2.) - 1., + ); + octave_modifier /= (rng.gen::() * 100.) + 2.; } anchor @@ -134,8 +150,8 @@ fn main() { let neighbor_id = cell.neighbors[n]; let neighbor_anchor = anchors.get(neighbor_id as isize).unwrap(); - anchor.neighbors[n].distance = anchor.position.angle_between(neighbor_anchor.position) * world_scale; - anchor.neighbors[n].position = (anchor.basis * (neighbor_anchor.position - anchor.position)) * world_scale; + anchor.neighbors[n].distance = anchor.position.angle_between(neighbor_anchor.position) * world_radius; + anchor.neighbors[n].position = (anchor.basis * (neighbor_anchor.position - anchor.position)) * world_radius; anchor.neighbors[n].transform = anchor.ibasis * neighbor_anchor.basis; } @@ -150,7 +166,7 @@ fn main() { let origin_id = 1; let origin_anchor = anchors.get(origin_id as isize).unwrap().clone(); - let world_origin = DVec3::NEG_Y * world_scale; + let world_origin = DVec3::NEG_Y * world_radius; // Generate zone meshes. let tiles = get_neighbors(&ico, 1, terrain_radius); @@ -193,9 +209,11 @@ fn main() { } let mut terrain = Terrain::triangle((gen.sources[0].position, gen.sources[1].position, gen.sources[2].position), gen); - for _ in 0..(4 - (*depth).min(3)) { terrain.subdivide(); } - terrain.tessellate(); + terrain.config.origin = origin_anchor.position; + terrain.config.scale = world_radius; + for _ in 0..(4 - (*depth).min(3)) { terrain.subdivide(); } + terrain.tessellate(max_terrain_detail); terrain } else { break; }; @@ -209,64 +227,224 @@ fn main() { let mut tile_count = 0; for (tile_id, _depth) in &tiles { - let cell = ico.cells.get(*tile_id).unwrap().clone(); - let anchor = anchors.get(*tile_id as isize).unwrap().clone(); + let tile = ico.cells.get(*tile_id).unwrap().clone(); + let mut cterrain = anchors.get(*tile_id as isize).unwrap().terrain.clone(); tile_count += 1; println!("Stitching {} [{} / {}]", *tile_id, tile_count, tiles.len()); - for side_id in 0..cell.neighbors.len() { - let neighbor_id = cell.neighbors[side_id]; - let neighbor_cell = ico.cells.get(neighbor_id).unwrap(); - let neighbor_anchor = anchors.get_mut(neighbor_id as isize).unwrap(); + // Apply stitching to each side of the triangle mesh. + for tile_side_index in 0..tile.neighbors.len() { + let neighbor_id = tile.neighbors[tile_side_index]; + let neighbor_tile = ico.cells.get(neighbor_id).unwrap(); + let nterrain = &mut anchors.get_mut(neighbor_id as isize).unwrap().terrain; - // Determine rotation of neighboring tile. - let side_vertices = [(side_id + 1) % 3, (side_id + 2) % 3]; - let mut neighbor_vertex_side = [0, 0]; - for s in 0..side_vertices.len() { - let vid = cell.vertices[side_vertices[s]]; + if nterrain.extent == [0; 3] { + // Ignore uninitialized (out of scope) terrain. + continue; + } - for ns in 0..neighbor_cell.vertices.len() { - if neighbor_cell.vertices[ns] == vid { - neighbor_vertex_side[s] = ns; + // Find indices of shared vertices. + let mut shared_vertex_indices = [0, 0]; + let mut nshared_vertex_indices = [0, 0]; + let mut shared_vertices = [0, 0]; + let mut nshared_vertices = [0, 0]; + let mut shared_index = 0; + for vertex_index in 0..tile.vertices.len() { + for nvertex_index in 0..neighbor_tile.vertices.len() { + if shared_index < 2 && tile.vertices[vertex_index] == neighbor_tile.vertices[nvertex_index] { + shared_vertex_indices[shared_index] = vertex_index; + nshared_vertex_indices[shared_index] = nvertex_index; + + shared_vertices[shared_index] = tile.vertices[vertex_index]; + nshared_vertices[shared_index] = neighbor_tile.vertices[nvertex_index]; + + shared_index += 1; + } + } + } + + let neighbor_direction = 3 - (nshared_vertex_indices[0] + nshared_vertex_indices[1]); + + /*println!("shared c({} : {}={}/{}, {}={}/{}) n({} : {}={}/{}, {}={}/{})", + tile_side_index, + shared_vertex_indices[0], shared_vertices[0], cterrain.extent[shared_vertex_indices[0]], + shared_vertex_indices[1], shared_vertices[1], cterrain.extent[shared_vertex_indices[1]], + neighbor_direction, + nshared_vertex_indices[0], nshared_vertices[0], nterrain.extent[nshared_vertex_indices[0]], + nshared_vertex_indices[1], nshared_vertices[1], nterrain.extent[nshared_vertex_indices[1]], + );*/ + + // Get ids of corner cells. + let mut current_face_id = { + let mut find = 0; + for face_id in cterrain.cells.list() { + let face = cterrain.cells.get(face_id).unwrap(); + let search = cterrain.extent[shared_vertex_indices[0]]; + if face.vertices[0] == search || face.vertices[1] == search || face.vertices[2] == search { + find = face_id; break; } } - } - let neighbor_side = 3 - (neighbor_vertex_side[0] + neighbor_vertex_side[1]); - - // Get maximum depth on edge of current cell. - let mut max_depth = 0; - for cell_id in anchor.terrain.cells.list() { - let tcell = anchor.terrain.cells.get(cell_id).unwrap(); - - let side_counts = (tcell.neighbors[0] == 0) as usize - + (tcell.neighbors[1] == 0) as usize - + (tcell.neighbors[2] == 0) as usize; - - if side_counts == 1 && tcell.neighbors[side_id] == 0 { - max_depth = max_depth.max(tcell.depth); - } - } - - // Subdivide neighbor edges. - let mut subdivide_count = 1; - while subdivide_count > 0 { - subdivide_count = 0; - - let cell_list = neighbor_anchor.terrain.cells.list(); - for cell_id in cell_list { - let tcell = neighbor_anchor.terrain.cells.get(cell_id).unwrap().clone(); - - if tcell.neighbors[neighbor_side] == 0 { - if tcell.depth < max_depth { - neighbor_anchor.terrain.subdivide_cell(cell_id); - subdivide_count += 1; - } + find + }; + let mut neighbor_face_id = { + let mut find = 0; + for face_id in nterrain.cells.list() { + let face = nterrain.cells.get(face_id).unwrap(); + let search = nterrain.extent[nshared_vertex_indices[0]]; + if face.vertices[0] == search || face.vertices[1] == search || face.vertices[2] == search { + find = face_id; + break; } } + find + }; + + if current_face_id == 0 || neighbor_face_id == 0 { + //println!("! Failed to find faces: {}, {}", current_face_id, neighbor_face_id); + continue; + } + + let current_direction_down = shared_vertex_indices[0]; + let current_direction_edge = tile_side_index; + let current_direction_away = 3 - (current_direction_down + current_direction_edge); + let neighbor_direction_down = nshared_vertex_indices[0]; + let neighbor_direction_edge = neighbor_direction; + let neighbor_direction_away = 3 - (neighbor_direction_down + neighbor_direction_edge); + + /*println!(" DIR D0 {} E0 {} A0 {} D1 {} E1 {} A1 {}", + current_direction_down, + current_direction_edge, + current_direction_away, + neighbor_direction_down, + neighbor_direction_edge, + neighbor_direction_away, + );*/ + + let mut end_current = false; + let mut end_neighbor = false; + + // Check if each edge face requires subdivision. + // + loop { + let tile_cell = cterrain.cells.get(current_face_id).unwrap().clone(); + let neighbor_cell = nterrain.cells.get(neighbor_face_id).unwrap().clone(); + + /*println!("instep t {} (d {} r {}) n {} (d {} r {})", + current_face_id, tile_cell.depth, tile_cell.direction, + neighbor_face_id, neighbor_cell.depth, neighbor_cell.direction, + );*/ + + // Work is done once both cells have no neighbor in donward directions. + if end_current && end_neighbor { + //println!("done"); + break; + } + + //println!("step t({}) = {} n({}) = {}", current_face_id, tile_cell.depth, neighbor_face_id, neighbor_cell.depth); + + if tile_cell.depth & !1 > neighbor_cell.depth & !1 { + // Subdivide neighbor tile's terrain and traverse backward. + nterrain.subdivide_cell(neighbor_face_id); + + let new_face = nterrain.cells.get(neighbor_face_id).unwrap(); + if new_face.sibling != 3 { + neighbor_face_id = new_face.neighbors[new_face.sibling as usize]; + } + + neighbor_face_id = nterrain.cells.get(neighbor_face_id).unwrap().neighbors[neighbor_direction_down]; + apply_count += 1; + + } else if tile_cell.depth & !1 < neighbor_cell.depth & !1 { + // Subdivide current tile's terrain and traverse backward. + cterrain.subdivide_cell(current_face_id); + + let new_face = cterrain.cells.get(current_face_id).unwrap(); + if new_face.sibling != 3 { + current_face_id = new_face.neighbors[new_face.sibling as usize]; + } + current_face_id = cterrain.cells.get(current_face_id).unwrap().neighbors[current_direction_down]; + apply_count += 1; + + } else { + // Traverse to next tile on edge. + let mut move_last = current_face_id; + let mut move_direction = current_direction_down; + let mut move_count = 0; + while { + let face = cterrain.cells.get(current_face_id).unwrap(); + + let neighbor_id = face.neighbors[move_direction]; + /*println!("mv ({}) current {} / {} (d {} s {} r {}) -> {}", + move_direction, + move_last, neighbor_face_id, + face.depth, face.sibling, face.direction, + neighbor_id + );*/ + if neighbor_id != 0 { + move_count += 1; + + if move_last != neighbor_id { + move_last = current_face_id; + current_face_id = neighbor_id; + } else { + if move_direction != current_direction_away { + move_direction = current_direction_away; + } else { + move_direction = current_direction_edge; + } + } + true + } else { + if move_count == 0 { + end_current = true; + } + false + } + } { } + + // Traverse neighbor tile to next edge face. + move_last = neighbor_face_id; + move_direction = neighbor_direction_down; + move_count = 0; + while { + let face = nterrain.cells.get(neighbor_face_id).unwrap(); + + let neighbor_id = face.neighbors[move_direction]; + /*println!("mv ({}) neighbor {} / {} (d {} s {} r {}) -> {}", + move_direction, + move_last, neighbor_face_id, + face.depth, face.sibling, face.direction, + neighbor_id + );*/ + if neighbor_id != 0 { + move_count += 1; + + if move_last != neighbor_id { + move_last = neighbor_face_id; + neighbor_face_id = neighbor_id; + } else { + if move_direction != neighbor_direction_away { + move_direction = neighbor_direction_away; + } else { + move_direction = neighbor_direction_edge; + } + } + true + } else { + if move_count == 0 { + end_neighbor = true; + } + false + } + } { } + + } } } + + anchors.get_mut(*tile_id as isize).unwrap().terrain = cterrain; } } @@ -276,15 +454,20 @@ fn main() { for vid in anchor.terrain.vertices.list() { if let Some(vertex) = anchor.terrain.vertices.get_mut(vid) { - let norm = vertex.position.normalize() * (world_scale + vertex.height); + let norm = vertex.position.normalize() * (world_radius + vertex.height); vertex.position = (origin_anchor.basis * norm) + world_origin; + + if vertex.height > 0. { + vertex.color = rgb(192, 128, 32); + } else { + vertex.color = rgb(32, 96, 192); + } vertex.height = 0.; } } - terrain_mesh.merge(&anchor.terrain.to_mesh(), Vec3::ZERO); + terrain_mesh = terrain_mesh.merge(&anchor.terrain.to_mesh(), Vec3::ZERO); } - //terrain::obj::save("icosphere.obj", &ico.to_mesh(), 20.).ok(); - terrain::obj::save("test.obj", &terrain_mesh, 0.01).ok(); + terrain::export::export_obj("test.obj", &terrain_mesh, 0.1).ok(); } diff --git a/src/obj.rs b/src/export.rs similarity index 70% rename from src/obj.rs rename to src/export.rs index e2a3716..82ff267 100644 --- a/src/obj.rs +++ b/src/export.rs @@ -2,19 +2,22 @@ use std::io::Write; use glam::Vec3; use crate::mesh::Mesh; -pub fn save(path:&str, mesh:&Mesh, scale:f32) -> Result<(),()> +pub fn export_obj(path:&str, mesh:&Mesh, scale:f32) -> Result<(),()> { match std::fs::File::create(path) { Ok(mut file) => { let mut min = Vec3::ZERO; let mut max = Vec3::ZERO; - for v in &mesh.vertices { - let v = *v * scale; + for vertex in &mesh.vertices { + let v = vertex.position * scale; if v.x < min.x { min.x = v.x } else if v.x > max.x { max.x = v.x }; if v.z < min.z { min.z = v.z } else if v.z > max.z { max.z = v.z }; - file.write_all(format!("v {:.6} {:.6} {:.6}\n", v.x, v.y, v.z).as_bytes()).ok(); + file.write_all(format!("v {:.6} {:.6} {:.6} {:3} {:3} {:3}\n", + v.x, v.y, v.z, + vertex.color.x, vertex.color.y, vertex.color.z, + ).as_bytes()).ok(); } file.write_all("\n".as_bytes()).ok(); @@ -22,8 +25,8 @@ pub fn save(path:&str, mesh:&Mesh, scale:f32) -> Result<(),()> let rx = max.x - min.x; let rz = max.z - min.z; - for v in &mesh.vertices { - let v = *v * scale; + for vertex in &mesh.vertices { + let v = vertex.position * scale; let ux = ((v.x / rx) + 0.5).clamp(0., 1.); let uz = ((v.z / rz) + 0.5).clamp(0., 1.); diff --git a/src/generator.rs b/src/generator.rs index 2c61b2b..e1097cc 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -1,7 +1,23 @@ use glam::DVec3; use noise::NoiseFn; -use crate::anchor::Octave; -use crate::utility::barycentric; +use crate::utility::barycentric_value; + +#[derive(Clone, Copy)] +pub struct Octave { + pub frequency:f64, + pub amplitude:f64, + pub offset:DVec3, +} +impl Octave { + pub fn new() -> Self + { + Self { + frequency:1., + amplitude:1., + offset:DVec3::ZERO, + } + } +} #[derive(Clone)] pub struct Source { @@ -42,12 +58,16 @@ impl Generator { heights[src] = self.sources[src].elevation; for octave in &self.sources[src].octaves { - heights[src] += 10. * octave.amplitude * self.generator.get([octave.frequency * position.x, octave.frequency * position.y, octave.frequency * position.z]); + heights[src] += octave.amplitude * self.generator.get([ + octave.frequency * (position.x + octave.offset.x), + octave.frequency * (position.y + octave.offset.y), + octave.frequency * (position.z + octave.offset.z), + ]); } - heights[src] /= self.sources[src].octaves.len().max(1) as f64; + //heights[src] /= self.sources[src].octaves.len().max(1) as f64; } - barycentric( + barycentric_value( (self.sources[0].position, self.sources[1].position, self.sources[2].position), (heights[0], heights[1], heights[2]), position diff --git a/src/icosphere.rs b/src/icosphere.rs index ce83a84..056f547 100644 --- a/src/icosphere.rs +++ b/src/icosphere.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use std::f64::consts::PI; -use glam::{IVec3, DVec3}; +use glam::{IVec3, DVec3, Vec3}; use pool::Pool; use sparse::Sparse; @@ -335,7 +335,10 @@ impl Icosphere { for index in self.vertices.list() { if let Some(vertex) = self.vertices.get(index) { conversion.set(index as isize, mesh.vertices.len()); - mesh.vertices.push(vertex.as_vec3() * MESH_SCALE); + mesh.vertices.push(crate::mesh::Vertex::new( + vertex.as_vec3() * MESH_SCALE, + Vec3::new(1., 1., 1.), + )); } } diff --git a/src/lib.rs b/src/lib.rs index 49aa84e..99322bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,8 +2,10 @@ pub mod utility; pub mod mesh; -pub mod obj; +pub mod export; pub mod anchor; pub mod generator; pub mod terrain; pub mod icosphere; +pub mod planet; +pub mod system; diff --git a/src/mesh.rs b/src/mesh.rs index 503efb1..2f37830 100644 --- a/src/mesh.rs +++ b/src/mesh.rs @@ -1,23 +1,37 @@ use glam::{Vec3, IVec3}; +pub struct Vertex { + pub position:Vec3, + pub color:Vec3, +} +impl Vertex{ + pub fn new(position:Vec3, color:Vec3) -> Self + { + Self { + position:position, + color:color, + } + } +} + pub struct Mesh { - pub vertices:Vec, + pub vertices:Vec, pub faces:Vec, } impl Mesh { pub fn new() -> Self { Self { - vertices:Vec::::new(), + vertices:Vec::::new(), faces:Vec::::new(), } } - pub fn merge(&mut self, mesh:&Mesh, translation:Vec3) + pub fn merge(mut self, mesh:&Mesh, translation:Vec3) -> Self { let start = self.vertices.len() as i32; for vertex in &mesh.vertices { - self.vertices.push(*vertex + translation); + self.vertices.push(Vertex::new(vertex.position + translation, vertex.color)); } for face in &mesh.faces { @@ -27,5 +41,14 @@ impl Mesh { face.z + start, )); } + self + } + + pub fn scale(mut self, scale:f32) -> Self + { + for vertex in &mut self.vertices { + vertex.position *= scale; + } + self } } diff --git a/src/planet/builder.rs b/src/planet/builder.rs new file mode 100644 index 0000000..a1b4312 --- /dev/null +++ b/src/planet/builder.rs @@ -0,0 +1,13 @@ +use super::Planet; + +pub struct Builder { + data:Planet, +} +impl Builder { + pub fn new() -> Self + { + Builder { + data:Planet::new(), + } + } +} diff --git a/src/planet/mod.rs b/src/planet/mod.rs new file mode 100644 index 0000000..60c0ef5 --- /dev/null +++ b/src/planet/mod.rs @@ -0,0 +1,2 @@ +mod planet; pub use planet::Planet; +mod builder; pub use builder::Builder; diff --git a/src/planet/planet.rs b/src/planet/planet.rs new file mode 100644 index 0000000..8577cad --- /dev/null +++ b/src/planet/planet.rs @@ -0,0 +1,20 @@ +pub struct Planet { + // Icosphere + // Anchors + // Geography + // Ocean + // Atmosphere + // Weather + // Climate +} +impl Planet { + pub fn new() -> Self + { + Self { } + } + + pub fn builder() -> super::Builder + { + super::Builder::new() + } +} diff --git a/src/system/mod.rs b/src/system/mod.rs new file mode 100644 index 0000000..a5f70ea --- /dev/null +++ b/src/system/mod.rs @@ -0,0 +1,127 @@ +use glam::DVec3; + +mod orbit; pub use orbit::Orbit; + +#[derive(Clone, Copy)] +struct Edge { + pub id:usize, + pub orbit:Orbit, +} +impl Edge { + pub fn new(id:usize, orbit:Orbit) -> Self + { + Self { + id:id, + orbit:orbit, + } + } +} + +#[derive(Clone, Copy)] +enum NodeData { + Single(usize), + Binary(Edge, Edge), +} + +#[derive(Clone)] +struct Node { + pub data:NodeData, + pub mass:f64, + pub children:Vec, +} + +#[derive(Clone)] +pub struct StarSystem { + nodes:Vec, +} +impl StarSystem { + pub fn singular(id:usize, mass:f64) -> Self + { + Self { + nodes:vec![Node { + data:NodeData::Single(id), + mass:mass, + children:vec![], + }], + } + } + + pub fn binary(a:Self, b:Self, distance:f64, orbit:Orbit) -> Self + { + let offset_a = 1; + let offset_b = a.nodes.len() + 1; + + let mass_a = a.nodes[0].mass; + let mass_b = b.nodes[0].mass; + let total_mass = mass_a + mass_b; + + let mut orbit_a = orbit; + orbit_a.periapsis = mass_b * distance / total_mass; + + let mut orbit_b = orbit; + orbit_b.periapsis = mass_a * distance / total_mass; + + Self { + nodes:[ + vec![ + Node { + data:NodeData::Binary( + Edge::new(offset_a, orbit_a), + Edge::new(offset_b, orbit_b), + ), + mass:total_mass, + children:vec![], + } + ], + a.nodes, + b.nodes, + ].concat(), + } + } + + pub fn append(mut self, mut system:Self, orbit:Orbit) -> Self + { + let id = self.nodes.len(); + println!("append at {}", id); + + for node in &mut system.nodes { + for child in &mut node.children { + child.id += id; + } + } + + self.nodes[0].children.push(Edge::new(id, orbit)); + self.nodes.append(&mut system.nodes.clone()); + self + } + + pub fn resolve(&self, time:f64) -> Vec<(usize, DVec3)> + // Returns the absolute-space positions of all bodies. + // + { + let mut result = Vec::<(usize, DVec3)>::new(); + let mut stack = Vec::<(usize, DVec3)>::new(); + stack.push((0, DVec3::ZERO)); + + while let Some(params) = stack.pop() { + let (id, position) = params; + println!("resolving {} at {}", id, position); + + match &self.nodes[id].data { + NodeData::Single(eid) => { + result.push((*eid, position)); + } + NodeData::Binary(edge1, edge2) => { + stack.push((edge1.id, position + edge1.orbit.position(time))); + stack.push((edge2.id, position - edge2.orbit.position(time))); + } + } + + for edge in &self.nodes[id].children { + stack.push((edge.id, position + edge.orbit.position(time))); + } + } + + result + } +} diff --git a/src/system/orbit.rs b/src/system/orbit.rs new file mode 100644 index 0000000..750b2b3 --- /dev/null +++ b/src/system/orbit.rs @@ -0,0 +1,92 @@ +use std::f64::consts::PI; +use glam::{DVec3, DQuat}; + +#[derive(Clone, Copy)] +pub struct Orbit { + pub orientation:DQuat, // orientation of the orbital system + pub periapsis:f64, // focal distance at periapsis + pub eccentricity:f64, // eccentricity of orbit + pub period:f64, // period of orbit + pub basis:f64, // starting position in multiples of period +} +impl Orbit { + pub fn new() -> Self + { + Self { + orientation:DQuat::IDENTITY, + periapsis:1., + eccentricity:0., + period:1., + basis:0., + } + } + + pub fn with(orientation:DQuat, periapsis:f64, eccentricity:f64, period:f64, basis:f64) -> Self + { + Self { + orientation:orientation, + periapsis:periapsis, + eccentricity:eccentricity, + period:period, + basis:basis, + } + } + + pub fn orientation(mut self, orientation:DQuat) -> Self + { + self.orientation = orientation; + self + } + + pub fn periapsis(mut self, periapsis:f64) -> Self + { + self.periapsis = periapsis; + self + } + + pub fn eccentricity(mut self, eccentricity:f64) -> Self + { + self.eccentricity = eccentricity; + self + } + + pub fn period(mut self, period:f64) -> Self + { + self.period = period; + self + } + + pub fn basis(mut self, basis:f64) -> Self + { + self.basis = basis; + self + } + + pub fn position(&self, time:f64) -> DVec3 + { + let semimajor_axis = self.periapsis / (1. - self.eccentricity); + let time = time + (self.basis * self.period); + + let n = 2. * PI / self.period; + let mean_anomaly = n * time; + + let mut eccentric_anomaly = mean_anomaly + (self.eccentricity * ((mean_anomaly < PI) as i32 as f64 - 0.5)); + { + let threshold = 1e-6; + let mut delta: f64 = 1.; + while delta.abs() > threshold { + delta = (eccentric_anomaly - (self.eccentricity * eccentric_anomaly.sin()) - mean_anomaly) / (1. - self.eccentricity * eccentric_anomaly.cos()); + eccentric_anomaly = eccentric_anomaly - delta; + } + } + + let theta = 2. * ((1. + self.eccentricity).sqrt() * (eccentric_anomaly / 2.).tan()).atan2((1. - self.eccentricity).sqrt()); + let radius = semimajor_axis * (1. - (self.eccentricity * eccentric_anomaly.cos())); + + self.orientation * DVec3::new( + radius * theta.cos(), + 0., + -radius * theta.sin(), + ) + } +} diff --git a/src/terrain.rs b/src/terrain.rs index 7ff407d..a484e42 100644 --- a/src/terrain.rs +++ b/src/terrain.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use std::f64::consts::PI; -use glam::{IVec3, DVec3}; +use glam::{IVec3, DVec3, Vec3}; use pool::Pool; use sparse::Sparse; @@ -11,10 +11,8 @@ const DEBUG_PRINTS :bool = false; const DEGREES :f64 = PI / 180.; -const DELTA_THRESHOLD :f64 = (1. / 48.) * DEGREES; -const MAX_SUBDIVISIONS :u16 = 4; -const MAX_DEPTH :u16 = 2 * MAX_SUBDIVISIONS; -const RADIAL_FALLOFF :f64 = 2.; +const DELTA_THRESHOLD :f64 = (1. / 16.) * DEGREES; +const RADIAL_FALLOFF :f64 = 1. / 6.; #[derive(Clone, Copy)] enum Action { @@ -23,10 +21,28 @@ enum Action { Split(/*Id*/usize, /*Caller*/usize), } +#[derive(Clone, Copy)] +pub struct TessellateConfig { + pub origin:DVec3, + pub scale:f64, + pub max_depth:u16, +} +impl TessellateConfig { + pub fn new() -> Self + { + Self { + origin:DVec3::ZERO, + scale:1., + max_depth:1, + } + } +} + #[derive(Clone, Copy)] pub struct Vertex { pub position:DVec3, pub height:f64, + pub color:Vec3, } impl Vertex { pub fn new() -> Self @@ -34,6 +50,7 @@ impl Vertex { Self { position:DVec3::ZERO, height:0., + color:Vec3::ONE, } } @@ -42,6 +59,7 @@ impl Vertex { Self { position:position, height:height, + color:Vec3::ONE, } } } @@ -71,9 +89,11 @@ impl Cell { pub struct Terrain { pub vertices:Pool, pub cells:Pool, - pub extent:[DVec3; 3], + pub extent:[usize; 3], pub normal:DVec3, generator:Generator, + + pub config:TessellateConfig, } impl Terrain { pub fn new(generator:Generator) -> Self @@ -81,9 +101,11 @@ impl Terrain { Self { vertices:Pool::::new(), cells:Pool::::new(), - extent:[DVec3::ZERO; 3], + extent:[0; 3], normal:DVec3::Y, generator:generator, + + config:TessellateConfig::new(), } } @@ -95,15 +117,15 @@ impl Terrain { let cross = (c - a).cross(b - a); grid.normal = cross.normalize(); - grid.extent = [a, b, c]; - - grid.add_vertex(a); - grid.add_vertex(b); - grid.add_vertex(c); + grid.extent = [ + grid.add_vertex(a), + grid.add_vertex(b), + grid.add_vertex(c), + ]; grid.cells.add({ let mut cell = Cell::new(); - cell.vertices = [1, 2, 3]; + cell.vertices = grid.extent; cell }); @@ -122,18 +144,21 @@ impl Terrain { self.node_update(&vec![Action::Subdivide(id, 0)]); } - pub fn tessellate(&mut self) + pub fn tessellate(&mut self, max_depth:u16) { let mut data = self.cells.list(); let mut data_len = data.len(); + let mut config = self.config.clone(); + config.max_depth = max_depth; + let mut passes = 1; while data.len() > 0 { let pass = data.clone(); data.clear(); for id in pass { - if self.node_prepare(id) { + if self.node_prepare(id, &config) { data.push(id); } } @@ -152,7 +177,7 @@ impl Terrain { } } - fn node_prepare(&mut self, id:usize) -> bool + fn node_prepare(&mut self, id:usize, config:&TessellateConfig) -> bool // Test the height change of the node to determine if it should be subdivided. // { @@ -178,9 +203,7 @@ impl Terrain { ]; let normal = (v[1] - v[0]).cross(v[2] - v[0]).normalize(); - let mut neighbor_normal = DVec3::ZERO; - let mut count = 0.; - + let mut delta: f64 = 0.; for n in 0..3 { let neighbor_id = cell.neighbors[n]; if neighbor_id != 0 { @@ -196,29 +219,33 @@ impl Terrain { vt[2].position + (self.normal * vt[2].height), ]; - neighbor_normal += (v[1] - v[0]).cross(v[2] - v[0]).normalize(); - count += 1.; + delta = delta.max(normal.angle_between((v[1] - v[0]).cross(v[2] - v[0]).normalize())); } } } - neighbor_normal /= count; - let centroid = (vp[0] + vp[1] + vp[2]) / 3.; - let min_dist = centroid.length_squared().max(1.); - let delta = normal.angle_between(neighbor_normal); - let max_depth = MAX_DEPTH - 2 * (f64::log2(min_dist) / f64::log2(RADIAL_FALLOFF)).min((MAX_SUBDIVISIONS - 1) as f64) as u16; - let threshold = DELTA_THRESHOLD * f64::log2(cell.depth as f64 + 16.) * 4.; - if delta > threshold && cell.depth < max_depth { + let cvp = [ + vp[0] - config.origin, + vp[1] - config.origin, + vp[2] - config.origin, + ]; + let centroid = (cvp[0] + cvp[1] + cvp[2]) / 3.; + let min_dist = centroid.length_squared().min(cvp[0].length_squared()).min(cvp[1].length_squared()).min(cvp[2].length_squared()).sqrt() * config.scale; + + let depth_threshold = 0.125 * cell.depth as f64; + let distance_threshold = min_dist.log2(); + + //println!("dt {}", distance_threshold); + + let threshold_modifier = (depth_threshold + distance_threshold) * RADIAL_FALLOFF; + let threshold: f64 = DELTA_THRESHOLD * threshold_modifier * threshold_modifier; + if delta > threshold && delta > threshold_modifier && cell.depth < (config.max_depth * 2) { self.node_update(&vec![Action::Subdivide(id, 0)]); - let new_cell = if let Some(data) = self.cells.get(id) { - *data - } else { return false; }; - - new_cell.depth != cell.depth - } else { - false - } + if let Some(data) = self.cells.get(id) { + data.depth != cell.depth + } else { false } + } else { false } } fn node_update(&mut self, actions:&Vec) @@ -298,7 +325,6 @@ impl Terrain { let mut cell = if let Some(data) = self.cells.get(id) { *data } else { return vec![]; }; - if cell.depth >= MAX_DEPTH { return vec![]; } let mut actions = Vec::::new(); @@ -658,7 +684,10 @@ impl Terrain { for index in self.vertices.list() { if let Some(vertex) = self.vertices.get(index) { conversion.set(index as isize, mesh.vertices.len()); - mesh.vertices.push((vertex.position + (self.normal * vertex.height)).as_vec3()); + mesh.vertices.push(crate::mesh::Vertex::new( + (vertex.position + (self.normal * vertex.height)).as_vec3(), + vertex.color, + )); } } diff --git a/src/utility.rs b/src/utility.rs index d4d6875..9e712d2 100644 --- a/src/utility.rs +++ b/src/utility.rs @@ -1,14 +1,13 @@ -use glam::DVec3; +use glam::{Vec3, DVec3}; pub fn f64_sign(value:f64) -> f64 { ((value > 0.) as i32 - (value < 0.) as i32) as f64 } -pub fn barycentric(positions:(DVec3, DVec3, DVec3), values:(f64, f64, f64), point:DVec3) -> f64 +pub fn barycentric_coordinates(positions:(DVec3, DVec3, DVec3), point:DVec3) -> DVec3 { let (a, b, c) = positions; - let (va, vb, vc) = values; let v0 = b - a; let v1 = c - a; @@ -24,5 +23,23 @@ pub fn barycentric(positions:(DVec3, DVec3, DVec3), values:(f64, f64, f64), poin let b1 = ((d00 * d21) - (d01 * d20)) / denom; let b2 = 1. - (b0 + b1); - (va * b2) + (vb * b0) + (vc * b1) + DVec3::new(b2, b0, b1) +} + +pub fn barycentric_inside(positions:(DVec3, DVec3, DVec3), point:DVec3) -> bool +{ + let coords = barycentric_coordinates(positions, point); + coords.x >= 0. && coords.x <= 1. && coords.y >= 0. && coords.y <= 1. && coords.z >= 0. && coords.z <= 1. +} + +pub fn barycentric_value(positions:(DVec3, DVec3, DVec3), values:(f64, f64, f64), point:DVec3) -> f64 +{ + let coords = barycentric_coordinates(positions, point); + let (va, vb, vc) = values; + (va * coords.x) + (vb * coords.y) + (vc * coords.z) +} + +pub fn rgb(r:u8, g:u8, b:u8) -> Vec3 +{ + Vec3::new(r as f32 / 255., g as f32 / 255., b as f32 / 255.) }