Last active
December 2, 2025 18:08
-
-
Save Kielan/5a9f3c10759b71b85ce09351620d3672 to your computer and use it in GitHub Desktop.
Subdivide rust bevy as per ChatGPT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use bevy::prelude::*; | |
| use std::collections::HashMap; | |
| // Planet Data Resource | |
| #[derive(Resource, Clone)] | |
| pub struct PlanetData { | |
| pub radius: f32, | |
| pub lod_focus: Vec3, | |
| pub max_lod: usize, | |
| // lod_levels[i].distance, lod_levels[i].resolution | |
| pub lod_levels: Vec<LodLevel>, | |
| pub min_height: f32, | |
| pub max_height: f32, | |
| pub planet_color: Color, | |
| } | |
| #[derive(Clone)] | |
| pub struct LodLevel { | |
| pub distance: f32, | |
| pub resolution: usize, | |
| } | |
| impl PlanetData { | |
| /// Equivalent to Godot's planet_data.point_on_planet(dir) | |
| pub fn point_on_planet(&self, dir: Vec3) -> Vec3 { | |
| dir.normalize() * self.radius | |
| } | |
| } | |
| // Quadtree Chunk Struct | |
| #[derive(Clone)] | |
| pub struct QuadtreeChunk { | |
| pub bounds_pos: Vec3, | |
| pub bounds_size: Vec3, | |
| pub depth: usize, | |
| pub max_depth: usize, | |
| pub identifier: String, | |
| pub children: Vec<QuadtreeChunk>, | |
| } | |
| impl QuadtreeChunk { | |
| pub fn new(bounds_pos: Vec3, bounds_size: Vec3, depth: usize, max_depth: usize) -> Self { | |
| let identifier = format!( | |
| "{}_{}_{}", | |
| bounds_pos, bounds_size, depth | |
| ); | |
| Self { | |
| bounds_pos, | |
| bounds_size, | |
| depth, | |
| max_depth, | |
| identifier, | |
| children: Vec::new(), | |
| } | |
| } | |
| pub fn subdivide( | |
| &mut self, | |
| focus_point: Vec3, | |
| face_origin: Vec3, | |
| axis_a: Vec3, | |
| axis_b: Vec3, | |
| planet: &PlanetData, | |
| ) { | |
| let half_size = self.bounds_size.x * 0.5; | |
| let quarter_size = self.bounds_size.x * 0.25; | |
| let half_extents = Vec3::splat(half_size); | |
| let offsets = [ | |
| Vec2::new(-quarter_size, -quarter_size), | |
| Vec2::new(quarter_size, -quarter_size), | |
| Vec2::new(-quarter_size, quarter_size), | |
| Vec2::new(quarter_size, quarter_size), | |
| ]; | |
| for offset in offsets { | |
| let child_pos_2d = | |
| Vec2::new(self.bounds_pos.x, self.bounds_pos.z) + offset; | |
| let local_center = | |
| face_origin + axis_a * child_pos_2d.x + axis_b * child_pos_2d.y; | |
| let sphere_pos = planet.point_on_planet(local_center.normalize()); | |
| let distance = sphere_pos.distance(focus_point); | |
| let should_subdivide_ | |
| self.depth < self.max_depth | |
| && distance <= planet.lod_levels[self.depth].distance; | |
| let child_bounds_pos = if should_subdivide { | |
| Vec3::new(child_pos_2d.x, 0.0, child_pos_2d.y) | |
| } else { | |
| Vec3::new( | |
| child_pos_2d.x - quarter_size, | |
| -quarter_size, | |
| child_pos_2d.y - quarter_size, | |
| ) | |
| }; | |
| let mut child = QuadtreeChunk::new(child_bounds_pos, half_extents, self.depth + 1, self.max_depth); | |
| if should_subdivide { | |
| child.subdivide(focus_point, face_origin, axis_a, axis_b, planet); | |
| } | |
| self.children.push(child); | |
| } | |
| } | |
| } | |
| // PlanetMeshFace Component | |
| #[derive(Component)] | |
| pub struct PlanetMeshFace { | |
| pub normal: Vec3, | |
| pub chunks: HashMap<String, Entity>, | |
| pub chunks_current: HashMap<String, bool>, | |
| pub quadtree: Option<QuadtreeChunk>, | |
| } | |
| impl PlanetMeshFace { | |
| pub fn new(normal: Vec3) -> Self { | |
| Self { | |
| normal, | |
| chunks: HashMap::new(), | |
| chunks_current: HashMap::new(), | |
| quadtree: None, | |
| } | |
| } | |
| } | |
| // Mesh Regeneration System | |
| pub fn regenerate_mesh_system( | |
| mut commands: Commands, | |
| mut query: Query<(Entity, &mut PlanetMeshFace)>, | |
| planet: Res<PlanetData>, | |
| mut meshes: ResMut<Assets<Mesh>>, | |
| mut materials: ResMut<Assets<StandardMaterial>>, | |
| ) { | |
| for (entity, mut face) in query.iter_mut() { | |
| let normal = face.normal; | |
| let mut quadtree = QuadtreeChunk::new(Vec3::ZERO, Vec3::splat(2.0), 0, planet.max_lod); | |
| let axis_a = Vec3::new(normal.y, normal.z, normal.x).normalize(); | |
| let axis_b = normal.cross(axis_a).normalize(); | |
| quadtree.subdivide( | |
| planet.lod_focus, | |
| normal, | |
| axis_a, | |
| axis_b, | |
| &planet, | |
| ); | |
| face.quadtree = Some(quadtree); | |
| face.chunks_current.clear(); | |
| if let Some(q) = &face.quadtree { | |
| visualize_chunk( | |
| q, | |
| &mut commands, | |
| &mut meshes, | |
| &mut materials, | |
| &mut face.chunks, | |
| &mut face.chunks_current, | |
| normal, | |
| axis_a, | |
| axis_b, | |
| &planet, | |
| entity, | |
| ); | |
| } | |
| // remove old chunks | |
| let obsolete: Vec<_> = face.chunks | |
| .iter() | |
| .filter(|(id, _)| !face.chunks_current.contains_key(*id)) | |
| .map(|(id, _)| id.clone()) | |
| .collect(); | |
| for id in obsolete { | |
| if let Some(ent) = face.chunks.remove(&id) { | |
| commands.entity(ent).despawn_recursive(); | |
| } | |
| } | |
| } | |
| } | |
| // Visualize Quad Chunk (Mesh Builder) | |
| pub fn visualize_chunk( | |
| chunk: &QuadtreeChunk, | |
| commands: &mut Commands, | |
| meshes: &mut Assets<Mesh>, | |
| materials: &mut Assets<StandardMaterial>, | |
| chunks: &mut HashMap<String, Entity>, | |
| chunks_current: &mut HashMap<String, bool>, | |
| face_origin: Vec3, | |
| axis_a: Vec3, | |
| axis_b: Vec3, | |
| planet: &PlanetData, | |
| parent_ent: Entity, | |
| ) { | |
| if chunk.children.is_empty() { | |
| chunks_current.insert(chunk.identifier.clone(), true); | |
| if chunks.contains_key(&chunk.identifier) { | |
| return; | |
| } | |
| let size = chunk.bounds_size.x; | |
| let offset = chunk.bounds_pos; | |
| let resolution = planet.lod_levels[chunk.depth - 1].resolution; | |
| let res = resolution; | |
| let mut vertices = Vec::with_capacity(res * res); | |
| let mut normals = vec![Vec3::ZERO; res * res]; | |
| let mut indices = Vec::new(); | |
| // build vertices | |
| for y in 0..res { | |
| for x in 0..res { | |
| let i = x + y * res; | |
| let percent_x = x as f32 / (res - 1) as f32; | |
| let percent_y = y as f32 / (res - 1) as f32; | |
| let local = | |
| Vec2::new(offset.x, offset.z) + Vec2::new(percent_x * size, percent_y * size); | |
| let point_on_plane = | |
| face_origin + axis_a * local.x + axis_b * local.y; | |
| let sphere = planet.point_on_planet(point_on_plane.normalize()); | |
| vertices.push(sphere); | |
| } | |
| } | |
| // indices | |
| for y in 0..res - 1 { | |
| for x in 0..res - 1 { | |
| let i = x + y * res; | |
| indices.extend_from_slice(&[ | |
| i as u32, | |
| (i + res) as u32, | |
| (i + res + 1) as u32, | |
| i as u32, | |
| (i + res + 1) as u32, | |
| (i + 1) as u32, | |
| ]); | |
| } | |
| } | |
| // compute normals | |
| for tri in indices.chunks(3) { | |
| let a = tri[0] as usize; | |
| let b = tri[1] as usize; | |
| let c = tri[2] as usize; | |
| let v0 = vertices[a]; | |
| let v1 = vertices[b]; | |
| let v2 = vertices[c]; | |
| let n = (v1 - v0).cross(v2 - v0).normalize(); | |
| normals[a] += n; | |
| normals[b] += n; | |
| normals[c] += n; | |
| } | |
| for n in normals.iter_mut() { | |
| *n = n.normalize(); | |
| } | |
| // build mesh | |
| let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); | |
| mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vertices); | |
| mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); | |
| mesh.set_indices(Some(Indices::U32(indices))); | |
| let mesh_handle = meshes.add(mesh); | |
| let mat = materials.add(StandardMaterial { | |
| base_color: planet.planet_color, | |
| ..Default::default() | |
| }); | |
| let ent = commands.spawn(PbrBundle { | |
| mesh: mesh_handle, | |
| material: mat, | |
| ..Default::default() | |
| }) | |
| .set_parent(parent_ent) | |
| .id(); | |
| chunks.insert(chunk.identifier.clone(), ent); | |
| return; | |
| } | |
| // recurse | |
| for child in &chunk.children { | |
| visualize_chunk( | |
| child, | |
| commands, | |
| meshes, | |
| materials, | |
| chunks, | |
| chunks_current, | |
| face_origin, | |
| axis_a, | |
| axis_b, | |
| planet, | |
| parent_ent, | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment