Skip to content

Instantly share code, notes, and snippets.

@Kielan
Last active December 13, 2025 03:12
Show Gist options
  • Select an option

  • Save Kielan/950923c538785c3ef2b07242bbea977f to your computer and use it in GitHub Desktop.

Select an option

Save Kielan/950923c538785c3ef2b07242bbea977f to your computer and use it in GitHub Desktop.
use bevy::prelude::*;
#[derive(Resource, Clone)]
pub struct PlanetData {
pub lod_focus: Vec3,
// add radius, noise params, max lod, etc
}
#[derive(Component)]
pub struct Planet;
#[derive(Component)]
pub struct PlanetMeshFace {
pub normal: Vec3,
pub name: &'static str,
}
const PLANET_FACES: [(Vec3, &str); 6] = [
(Vec3::Y, "Top"),
(-Vec3::Y, "Bot"),
(-Vec3::X, "Left"),
(Vec3::X, "Right"),
(-Vec3::Z, "Back"),
(Vec3::Z, "Front"),
];
pub fn spawn_planet(
mut commands: Commands,
) {
commands
.spawn((
Planet,
Transform::default(),
GlobalTransform::default(),
))
.with_children(|parent| {
for (normal, name) in PLANET_FACES {
parent.spawn((
PlanetMeshFace { normal, name },
Transform::default(),
GlobalTransform::default(),
));
}
});
}
pub fn regenerate_faces_on_planet_data_change(
planet_data: Res<PlanetData>,
mut faces: Query<&PlanetMeshFace>,
) {
if !planet_data.is_changed() {
return;
}
for face in &mut faces {
regenerate_mesh(face, &planet_data);
}
}
fn regenerate_mesh(face: &PlanetMeshFace, planet_data: &PlanetData) {
// Equivalent of:
// child._regenerate_mesh(planet_data)
// Typical steps:
// - Build quadtree
// - Project cube → sphere
// - LOD based on planet_data.lod_focus
// - Write Mesh
}
#[derive(Component)]
pub struct Player;
pub fn update_lod_focus_from_player(
player: Query<&GlobalTransform, With<Player>>,
mut planet_data: ResMut<PlanetData>,
) {
if let Ok(player_transform) = player.get_single() {
planet_data.lod_focus = player_transform.translation();
}
}
pub struct PlanetPlugin;
impl Plugin for PlanetPlugin {
fn build(&self, app: &mut App) {
app
.insert_resource(PlanetData {
lod_focus: Vec3::ZERO,
})
.add_startup_system(spawn_planet)
.add_system(update_lod_focus_from_player)
.add_system(regenerate_faces_on_planet_data_change);
}
}
#[derive(Clone)]
pub struct LodLevel {
pub distance: f32,
pub resolution: u32,
}
pub trait PlanetNoise: Send + Sync {
fn get_noise_3d(&self, p: Vec3) -> f32;
fn is_base_layer(&self) -> bool;
fn amplitude(&self) -> f32;
fn min_height(&self) -> f32;
}
use bevy::prelude::*;
#[derive(Resource)]
pub struct PlanetData {
pub radius: f32,
pub lod_focus: Vec3,
pub max_lod: usize,
pub lod_levels: Vec<LodLevel>,
/// Noise layers
pub planet_noise: Vec<Box<dyn PlanetNoise>>,
pub min_height: f32,
pub max_height: f32,
/// Gradient texture (optional)
pub planet_color: Option<Handle<Image>>,
}
impl Default for PlanetData {
fn default() -> Self {
let lod_levels = vec![
LodLevel { distance: 500.0, resolution: 2 },
LodLevel { distance: 50.0, resolution: 2 },
LodLevel { distance: 25.0, resolution: 3 },
LodLevel { distance: 10.0, resolution: 4 },
LodLevel { distance: 1.0, resolution: 10 },
];
Self {
radius: 1.0,
lod_focus: Vec3::ZERO,
max_lod: lod_levels.len() - 1,
lod_levels,
planet_noise: Vec::new(),
min_height: 9999.0,
max_height: 0.0,
planet_color: None,
}
}
}
impl PlanetData {
pub fn point_on_planet(&self, point_on_sphere: Vec3) -> Vec3 {
let mut elevation = 0.0;
let mut base_layer_mask = 0.0;
if !self.planet_noise.is_empty() {
// Base layers
for n in &self.planet_noise {
if n.is_base_layer() {
let mut level_base_elevation =
n.get_noise_3d(point_on_sphere * 100.0);
level_base_elevation =
(level_base_elevation + 1.0) / 2.0 * n.amplitude();
level_base_elevation =
(level_base_elevation - n.min_height()).max(0.0);
base_layer_mask += level_base_elevation;
}
}
// All layers
for n in &self.planet_noise {
let mut level_elevation =
n.get_noise_3d(point_on_sphere * 100.0);
level_elevation =
(level_elevation + 1.0) / 2.0 * n.amplitude();
level_elevation =
(level_elevation - n.min_height()).max(0.0)
* base_layer_mask;
elevation += level_elevation;
}
}
point_on_sphere * self.radius * (elevation + 1.0)
}
}
pub fn planet_data_changed(
planet_data: Res<PlanetData>,
) {
if planet_data.is_changed() {
// Regenerate meshes / LODs
}
}
use bevy::prelude::*;
use std::collections::HashMap;
#[derive(Component)]
pub struct PlanetMeshFace {
pub normal: Vec3,
}
#[derive(Clone)]
pub struct QuadtreeChunk {
pub bounds_pos: Vec3,
pub bounds_size: f32,
pub depth: usize,
pub max_chunk_depth: usize,
pub identifier: String,
pub children: Vec<QuadtreeChunk>,
}
impl QuadtreeChunk {
pub fn new(bounds_pos: Vec3, bounds_size: f32, depth: usize, max_depth: usize) -> Self {
let identifier = format!("{:?}_{:?}_{}", bounds_pos, bounds_size, depth);
Self {
bounds_pos,
bounds_size,
depth,
max_chunk_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_data: &PlanetData,
) {
let half = self.bounds_size * 0.5;
let quarter = self.bounds_size * 0.25;
let offsets = [
Vec2::new(-quarter, -quarter),
Vec2::new( quarter, -quarter),
Vec2::new(-quarter, quarter),
Vec2::new( quarter, quarter),
];
for offset in offsets {
let child_2d = Vec2::new(self.bounds_pos.x, self.bounds_pos.z) + offset;
let center_3d =
face_origin + child_2d.x * axis_a + child_2d.y * axis_b;
let distance = planet_data
.point_on_planet(center_3d.normalize())
.distance(focus_point);
let next_depth = self.depth + 1;
let should_split =
self.depth < self.max_chunk_depth
&& distance <= planet_data.lod_levels[self.depth].distance;
let mut child = QuadtreeChunk::new(
Vec3::new(child_2d.x, 0.0, child_2d.y),
half,
next_depth,
self.max_chunk_depth,
);
if should_split {
child.subdivide(
focus_point,
face_origin,
axis_a,
axis_b,
planet_data,
);
}
self.children.push(child);
}
}
}
#[derive(Component, Default)]
pub struct PlanetFaceRuntime {
pub chunks: HashMap<String, Entity>,
pub chunks_current: HashMap<String, bool>,
}
pub fn regenerate_planet_face(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
planet_data: Res<PlanetData>,
mut query: Query<(Entity, &PlanetMeshFace, &mut PlanetFaceRuntime)>,
) {
if !planet_data.is_changed() {
return;
}
for (entity, face, mut runtime) in &mut query {
runtime.chunks_current.clear();
let focus_point = planet_data.lod_focus;
let axis_a = Vec3::new(face.normal.y, face.normal.z, face.normal.x).normalize();
let axis_b = face.normal.cross(axis_a).normalize();
let mut root = QuadtreeChunk::new(
Vec3::ZERO,
2.0,
0,
planet_data.max_lod,
);
root.subdivide(
focus_point,
face.normal,
axis_a,
axis_b,
&planet_data,
);
visualize_quadtree(
&mut commands,
&mut meshes,
&mut materials,
entity,
&mut runtime,
&root,
face.normal,
axis_a,
axis_b,
&planet_data,
);
// Remove unused chunks
let old_chunks: Vec<String> = runtime
.chunks
.keys()
.filter(|id| !runtime.chunks_current.contains_key(*id))
.cloned()
.collect();
for id in old_chunks {
if let Some(e) = runtime.chunks.remove(&id) {
commands.entity(e).despawn_recursive();
}
}
}
}
fn visualize_quadtree(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<StandardMaterial>,
parent: Entity,
runtime: &mut PlanetFaceRuntime,
chunk: &QuadtreeChunk,
face_origin: Vec3,
axis_a: Vec3,
axis_b: Vec3,
planet_data: &PlanetData,
) {
if chunk.children.is_empty() {
runtime.chunks_current.insert(chunk.identifier.clone(), true);
if runtime.chunks.contains_key(&chunk.identifier) {
return;
}
let res = planet_data.lod_levels[chunk.depth - 1].resolution as usize;
let size = chunk.bounds_size;
let offset = chunk.bounds_pos;
let mut positions = Vec::with_capacity(res * res);
let mut normals = vec![Vec3::ZERO; res * res];
let mut indices = Vec::new();
for y in 0..res {
for x in 0..res {
let percent = Vec2::new(x as f32, y as f32) / (res as f32 - 1.0);
let local = Vec2::new(offset.x, offset.z) + percent * size;
let plane = face_origin + local.x * axis_a + local.y * axis_b;
let sphere = planet_data.point_on_planet(plane.normalize());
positions.push(sphere);
if x < res - 1 && y < res - 1 {
let i = (x + y * res) as u32;
indices.extend_from_slice(&[
i,
i + res as u32,
i + res as u32 + 1,
i,
i + res as u32 + 1,
i + 1,
]);
}
}
}
// Normals
for tri in indices.chunks_exact(3) {
let a = tri[0] as usize;
let b = tri[1] as usize;
let c = tri[2] as usize;
let n = (positions[b] - positions[a])
.cross(positions[c] - positions[a])
.normalize();
normals[a] += n;
normals[b] += n;
normals[c] += n;
}
for n in &mut normals {
*n = n.normalize();
}
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList);
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
mesh.set_indices(Some(Indices::U32(indices)));
let mesh_handle = meshes.add(mesh);
let material = materials.add(StandardMaterial {
base_color_texture: planet_data.planet_color.clone(),
..default()
});
let chunk_entity = commands
.spawn((
PbrBundle {
mesh: mesh_handle,
material,
..default()
},
))
.set_parent(parent)
.id();
runtime.chunks.insert(chunk.identifier.clone(), chunk_entity);
}
for child in &chunk.children {
visualize_quadtree(
commands,
meshes,
materials,
parent,
runtime,
child,
face_origin,
axis_a,
axis_b,
planet_data,
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment