1use celestial_algo::quadtree::Chunk;
9use godot::builtin::Vector3;
10
11pub fn lod_color(level: u8) -> [f32; 3] {
16 let hue = ((level as f32) * 0.137).fract();
17 let h6 = hue * 6.0;
18 let c = 0.95 * 0.8;
19 let x = c * (1.0 - ((h6 % 2.0) - 1.0).abs());
20 let m = 0.95 - c;
21 let (r, g, b) = if h6 < 1.0 {
22 (c, x, 0.0)
23 } else if h6 < 2.0 {
24 (x, c, 0.0)
25 } else if h6 < 3.0 {
26 (0.0, c, x)
27 } else if h6 < 4.0 {
28 (0.0, x, c)
29 } else if h6 < 5.0 {
30 (x, 0.0, c)
31 } else {
32 (c, 0.0, x)
33 };
34 [r + m, g + m, b + m]
35}
36
37pub struct DebugMeshData {
40 pub positions: Vec<Vector3>,
41 pub colors: Vec<[f32; 3]>,
42 pub indices: Vec<i32>,
43}
44
45fn tessellate(corners: [Vector3; 3], radius: f32, res: u32) -> Vec<[Vector3; 3]> {
49 let n = res.max(1);
50 let fnn = n as f32;
51 let pt = |i: u32, j: u32| {
52 let (wa, wb, wc) = ((n - i - j) as f32 / fnn, i as f32 / fnn, j as f32 / fnn);
53 let p = corners[0] * wa + corners[1] * wb + corners[2] * wc;
54 if radius > 0.0 {
55 p.normalized() * radius
56 } else {
57 p
58 }
59 };
60 let mut tris = Vec::with_capacity((n * n) as usize);
61 for i in 0..n {
62 for j in 0..(n - i) {
63 tris.push([pt(i, j), pt(i + 1, j), pt(i, j + 1)]);
64 if i + j + 1 < n {
65 tris.push([pt(i + 1, j), pt(i + 1, j + 1), pt(i, j + 1)]);
66 }
67 }
68 }
69 tris
70}
71
72pub fn build_debug_mesh(chunks: &[Chunk], radius: f32, chunk_res: u32) -> DebugMeshData {
75 let mut positions = Vec::new();
76 let mut colors = Vec::new();
77 let mut indices = Vec::new();
78 for c in chunks {
79 let col = lod_color(c.level);
80 for tri in tessellate(c.corners, radius, chunk_res) {
81 let base = positions.len() as i32;
82 for v in tri {
83 positions.push(v);
84 colors.push(col);
85 }
86 indices.push(base);
87 indices.push(base + 1);
88 indices.push(base + 2);
89 }
90 }
91 DebugMeshData { positions, colors, indices }
92}
93
94use std::time::Instant;
100
101use celestial_algo::quadtree::{base_face_frames, select_chunks};
102use godot::classes::base_material_3d::{CullMode, Flags, ShadingMode};
103use godot::classes::mesh::{ArrayType, PrimitiveType};
104use godot::classes::viewport::DebugDraw;
105use godot::classes::{ArrayMesh, INode3D, MeshInstance3D, Node3D, StandardMaterial3D};
106use godot::prelude::*;
107
108#[derive(GodotClass)]
111#[class(base = Node3D, tool, init, internal)]
112pub struct CelestialQuadtreeDebug {
113 base: Base<Node3D>,
114 #[export]
115 #[init(val = 1000.0)]
116 radius: f32,
117 #[export]
118 #[init(val = 0.02)]
119 screen_error: f32,
120 #[export]
121 #[init(val = 16)]
122 chunk_res: i64,
123 #[export]
124 #[init(val = 16)]
125 max_depth: i64,
126 #[export]
127 wireframe: bool,
128 last_select_ms: f64,
129 last_realize_ms: f64,
130 chunk_count: i64,
131 mesh_instance: Option<Gd<MeshInstance3D>>,
132}
133
134#[godot_api]
135impl INode3D for CelestialQuadtreeDebug {
136 fn ready(&mut self) {
137 let mut mi = MeshInstance3D::new_alloc();
138 let mut mat = StandardMaterial3D::new_gd();
139 mat.set_shading_mode(ShadingMode::UNSHADED);
140 mat.set_flag(Flags::ALBEDO_FROM_VERTEX_COLOR, true);
141 mat.set_cull_mode(CullMode::DISABLED);
142 mi.set_material_override(&mat);
143 self.mesh_instance = Some(mi.clone());
144 self.base_mut().add_child(&mi);
145 }
146
147 fn process(&mut self, _delta: f64) {
148 let Some(camera) = self
149 .base()
150 .get_viewport()
151 .and_then(|vp| vp.get_camera_3d())
152 .map(|c| c.get_global_position())
153 else {
154 return;
155 };
156
157 let frames = base_face_frames(self.radius);
158 let t0 = Instant::now();
159 let chunks =
160 select_chunks(&frames, camera, self.screen_error, self.chunk_res as u32, self.max_depth as u8, None);
161 self.last_select_ms = t0.elapsed().as_secs_f64() * 1000.0;
162 self.chunk_count = chunks.len() as i64;
163
164 let t1 = Instant::now();
165 let data = build_debug_mesh(&chunks, self.radius, self.chunk_res as u32);
166 self.last_realize_ms = t1.elapsed().as_secs_f64() * 1000.0;
167 let positions: PackedVector3Array = data.positions.iter().copied().collect();
168 let colors: PackedColorArray =
169 data.colors.iter().map(|c| Color::from_rgba(c[0], c[1], c[2], 1.0)).collect();
170 let indices: PackedInt32Array = data.indices.iter().copied().collect();
171
172 let mut arrays = VarArray::new();
173 arrays.resize(ArrayType::MAX.ord() as usize, &Variant::nil());
174 arrays.set(ArrayType::VERTEX.ord() as usize, &positions.to_variant());
175 arrays.set(ArrayType::COLOR.ord() as usize, &colors.to_variant());
176 arrays.set(ArrayType::INDEX.ord() as usize, &indices.to_variant());
177
178 let mut mesh = ArrayMesh::new_gd();
179 if !indices.is_empty() {
180 mesh.add_surface_from_arrays(PrimitiveType::TRIANGLES, &arrays);
181 }
182 if let Some(mi) = self.mesh_instance.as_mut() {
183 mi.set_mesh(&mesh);
184 }
185
186 if let Some(mut vp) = self.base().get_viewport() {
187 vp.set_debug_draw(if self.wireframe { DebugDraw::WIREFRAME } else { DebugDraw::DISABLED });
188 }
189 }
190}
191
192#[godot_api]
193impl CelestialQuadtreeDebug {
194 #[func]
195 fn last_select_ms(&self) -> f64 {
196 self.last_select_ms
197 }
198
199 #[func]
200 fn last_realize_ms(&self) -> f64 {
201 self.last_realize_ms
202 }
203
204 #[func]
205 fn chunk_count(&self) -> i64 {
206 self.chunk_count
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use celestial_algo::quadtree::{base_face_frames, select_chunks};
214
215 #[test]
216 fn lod_color_differs_per_level_and_is_deterministic() {
217 assert_ne!(lod_color(0), lod_color(1));
218 assert_ne!(lod_color(1), lod_color(2));
219 assert_eq!(lod_color(3), lod_color(3));
220 }
221
222 #[test]
223 fn build_debug_mesh_tessellates_each_chunk() {
224 let frames = base_face_frames(1000.0);
225 let chunks = select_chunks(&frames, Vector3::new(0.0, 0.0, 1100.0), 0.1, 16, 4, None);
226 let mesh1 = build_debug_mesh(&chunks, 1000.0, 1);
228 assert_eq!(mesh1.positions.len(), chunks.len() * 3);
229 let c0 = lod_color(chunks[0].level);
230 assert_eq!(mesh1.colors[0], c0);
231 assert_eq!(mesh1.colors[2], c0);
232 let mesh2 = build_debug_mesh(&chunks, 1000.0, 2);
234 assert_eq!(mesh2.positions.len(), chunks.len() * 4 * 3);
235 assert_eq!(mesh2.indices.len(), chunks.len() * 4 * 3);
236 }
237}