Skip to content

Instantly share code, notes, and snippets.

@Kielan
Last active December 2, 2025 18:08
Show Gist options
  • Select an option

  • Save Kielan/5a9f3c10759b71b85ce09351620d3672 to your computer and use it in GitHub Desktop.

Select an option

Save Kielan/5a9f3c10759b71b85ce09351620d3672 to your computer and use it in GitHub Desktop.
Subdivide rust bevy as per ChatGPT
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