1use std::sync::Arc;
18
19use bytemuck::Zeroable;
20use godot::classes::notify::NodeNotification;
21use godot::classes::rendering_device::UniformType;
22use godot::classes::rendering_server::MultimeshTransformFormat;
23use godot::classes::{
24 ArrayMesh, INode, INode3D, MultiMesh, MultiMeshInstance3D, Node, Node3D, RdUniform, RefCounted,
25 RenderingDevice, RenderingServer, Shader, ShaderMaterial, Texture2Drd,
26};
27use godot::prelude::*;
28
29use celestial_algo::quadtree::{base_face_frames, Bary, Chunk, ChunkId};
30
31use crate::chunk_descriptors::{pack_chunks, pack_instances, verts_per_chunk};
32use crate::chunk_mesh::reference_chunk_mesh;
33use crate::chunk_pipeline::{CesChunkJob, ChunkStage};
34use crate::descriptors::{assemble, HeightGpu, TerrainGpu, TextureGpu};
35use crate::gpu::ATTR_TEX_WIDTH;
36use crate::gpu::device;
37use crate::gpu::owned::{
38 MainDeviceSink, Owned, RdBuffer, RdPipeline, RdShader, RdTexture, RdUniformSet, RidSink,
39};
40
41const TILE_VIEWER_SPV: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/TileViewer.spv"));
42
43const RADIUS: f32 = 1000.0;
45
46#[repr(C)]
51#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
52pub struct TileViewerParams {
53 pub a: [f32; 4],
55 pub b: [f32; 4],
57 pub c: [f32; 4],
59 pub sub0: [f32; 4],
61 pub sub1: [f32; 4],
63 pub sub2: [f32; 4],
65 pub width: u32,
67 pub tex_res: u32,
69 pub _pad: [u32; 2],
71 pub terrain: TerrainGpu,
73 pub _pad2: [f32; 2],
75}
76
77struct TileViewerGpu {
83 sink: Arc<dyn RidSink>,
84 set: RdUniformSet,
85 pipeline: RdPipeline,
86 shader: RdShader,
87 tex: RdTexture,
90 nrm: RdTexture,
92 params_buf: RdBuffer,
93 width: u32,
94 built: bool,
95 init_failed: bool,
96}
97
98impl Default for TileViewerGpu {
99 fn default() -> Self {
100 let sink: Arc<dyn RidSink> = MainDeviceSink::new();
102 Self {
103 set: Owned::invalid(sink.clone()),
104 pipeline: Owned::invalid(sink.clone()),
105 shader: Owned::invalid(sink.clone()),
106 tex: Owned::invalid(sink.clone()),
107 nrm: Owned::invalid(sink.clone()),
108 params_buf: Owned::invalid(sink.clone()),
109 width: 0,
110 built: false,
111 init_failed: false,
112 sink,
113 }
114 }
115}
116
117impl TileViewerGpu {
118 fn ensure_ready(&mut self, rd: &mut Gd<RenderingDevice>, width: u32) -> bool {
120 if self.init_failed {
121 return false;
122 }
123 if self.built {
124 return true;
125 }
126 let sink = self.sink.clone();
127 let Some((shader, pipeline)) =
128 device::compute_pipeline(rd, &sink, TILE_VIEWER_SPV, "TileViewer")
129 else {
130 self.init_failed = true;
131 return false;
132 };
133 self.shader = shader;
134 self.pipeline = pipeline;
135 self.tex = device::atlas_texture(rd, &sink, width, width);
137 self.nrm = device::atlas_texture(rd, &sink, width, width);
138 self.params_buf =
139 device::storage_buffer(rd, &sink, &vec![0u8; std::mem::size_of::<TileViewerParams>()]);
140 let uniforms: Array<Gd<RdUniform>> = [
141 device::uniform(UniformType::STORAGE_BUFFER, 0, self.params_buf.rid()),
142 device::uniform(UniformType::IMAGE, 1, self.tex.rid()),
143 device::uniform(UniformType::IMAGE, 2, self.nrm.rid()),
144 ]
145 .into_iter()
146 .collect();
147 self.set = Owned::new(rd.uniform_set_create(&uniforms, self.shader.rid(), 0), sink);
148 self.width = width;
149 self.built = true;
150 true
151 }
152
153 fn dispatch(&mut self, rd: &mut Gd<RenderingDevice>, params: &TileViewerParams) {
155 let bytes = bytemuck::bytes_of(params);
156 rd.buffer_update(
157 self.params_buf.rid(),
158 0,
159 bytes.len() as u32,
160 &PackedByteArray::from(bytes),
161 );
162 let total = self.width.saturating_mul(self.width);
163 let groups = total.div_ceil(64);
164 if groups == 0 {
165 return;
166 }
167 let list = rd.compute_list_begin();
168 rd.compute_list_bind_compute_pipeline(list, self.pipeline.rid());
169 rd.compute_list_bind_uniform_set(list, self.set.rid(), 0);
170 rd.compute_list_dispatch(list, groups, 1, 1);
171 rd.compute_list_end();
172 }
173}
174
175#[derive(GodotClass)]
179#[class(base = RefCounted, no_init)]
180pub struct TileViewerJob {
181 base: Base<RefCounted>,
182 gpu: TileViewerGpu,
183 params: TileViewerParams,
185 pending: bool,
187}
188
189impl TileViewerJob {
190 fn create() -> Gd<Self> {
191 Gd::from_init_fn(|base| Self {
192 base,
193 gpu: TileViewerGpu::default(),
194 params: TileViewerParams::zeroed(),
195 pending: false,
196 })
197 }
198
199 fn tex_rid(&self) -> Rid {
201 self.gpu.tex.rid()
202 }
203
204 fn nrm_rid(&self) -> Rid {
206 self.gpu.nrm.rid()
207 }
208}
209
210#[godot_api]
211impl TileViewerJob {
212 #[func]
214 fn run(&mut self) {
215 if !self.pending {
216 return;
217 }
218 let rs = RenderingServer::singleton();
219 let Some(mut rd) = rs.get_rendering_device() else { return };
220 let w = self.params.width;
221 if !self.gpu.ensure_ready(&mut rd, w) {
222 return; }
224 let params = self.params;
225 self.gpu.dispatch(&mut rd, ¶ms);
226 self.pending = false;
227 }
228}
229
230#[derive(GodotClass)]
233#[class(base = Node, tool, init, internal)]
234pub struct CelestialTileViewer {
235 base: Base<Node>,
236
237 #[export]
239 #[init(val = 1024)]
240 width: i64,
241 #[export]
243 #[init(val = 7)]
244 face: i64,
245 #[init(val = 64)]
251 tex_res: i64,
252
253 #[init(val = Vector2::new(0.0, 0.0))]
257 sub0: Vector2,
258 #[init(val = Vector2::new(1.0, 0.0))]
259 sub1: Vector2,
260 #[init(val = Vector2::new(0.0, 1.0))]
261 sub2: Vector2,
262
263 #[init(val = 0.0)]
267 bump_enable: f32,
268
269 job: Option<Gd<TileViewerJob>>,
270 run_cb: Option<Callable>,
271}
272
273#[godot_api]
274impl INode for CelestialTileViewer {
275 fn on_notification(&mut self, what: NodeNotification) {
276 if matches!(what, NodeNotification::EXIT_TREE | NodeNotification::PREDELETE) {
277 self.job = None;
280 self.run_cb = None;
281 }
282 }
283}
284
285#[godot_api]
286impl CelestialTileViewer {
287 #[func]
292 fn set_region(&mut self, face: i64, c0: Vector2, c1: Vector2, c2: Vector2) {
293 self.face = face;
294 self.sub0 = c0;
295 self.sub1 = c1;
296 self.sub2 = c2;
297 self.rebake();
298 }
299
300 #[func]
302 fn set_tex_res(&mut self, n: i64) {
303 self.tex_res = n.clamp(1, 4096);
304 self.rebake();
305 }
306
307 fn rebake(&mut self) {
309 self.ensure_job();
310
311 let frames = base_face_frames(RADIUS);
312 let fi = self.face.clamp(0, frames.len() as i64 - 1) as usize;
313 let frame = &frames[fi];
314 let terrain = assemble(&HeightGpu::default(), &TextureGpu::default());
315 let params = TileViewerParams {
316 a: [frame.a.x, frame.a.y, frame.a.z, frame.radius],
317 b: [frame.b.x, frame.b.y, frame.b.z, self.bump_enable],
319 c: [frame.c.x, frame.c.y, frame.c.z, 0.0],
320 sub0: [self.sub0.x, self.sub0.y, 0.0, 0.0],
321 sub1: [self.sub1.x, self.sub1.y, 0.0, 0.0],
322 sub2: [self.sub2.x, self.sub2.y, 0.0, 0.0],
323 width: self.width.clamp(16, 4096) as u32,
324 tex_res: self.tex_res.clamp(1, 4096) as u32,
325 _pad: [0, 0],
326 terrain,
327 _pad2: [0.0, 0.0],
328 };
329
330 if let Some(job) = &mut self.job {
331 let mut j = job.bind_mut();
332 j.params = params;
333 j.pending = true;
334 }
335 if let Some(cb) = self.run_cb.clone() {
336 RenderingServer::singleton().call_on_render_thread(&cb);
337 }
338 }
339
340 #[func]
342 fn tex_resolution(&self) -> i64 {
343 self.tex_res
344 }
345
346 #[func]
348 fn set_bump(&mut self, enable: f32) {
349 self.bump_enable = if enable > 0.5 { 1.0 } else { 0.0 };
350 self.rebake();
351 }
352
353 #[func]
356 fn texture_rid(&self) -> Rid {
357 self.job.as_ref().map(|j| j.bind().tex_rid()).unwrap_or(Rid::Invalid)
358 }
359
360 #[func]
363 fn normal_texture_rid(&self) -> Rid {
364 self.job.as_ref().map(|j| j.bind().nrm_rid()).unwrap_or(Rid::Invalid)
365 }
366
367 fn ensure_job(&mut self) {
369 if self.job.is_some() {
370 return;
371 }
372 let job = TileViewerJob::create();
373 self.run_cb = Some(Callable::from_object_method(&job, "run"));
374 self.job = Some(job);
375 }
376}
377
378const CHUNK_SHADER: &str = "res://addons/celestialsim/terrain_chunk.gdshader";
396
397#[derive(GodotClass)]
399#[class(base = Node3D, tool, init, internal)]
400pub struct CelestialTileChunk {
401 base: Base<Node3D>,
402
403 #[export]
405 #[init(val = 7)]
406 face: i64,
407 #[export]
409 #[init(val = 1000.0)]
410 radius: f32,
411 #[export]
413 #[init(val = 64)]
414 chunk_res: i64,
415 #[init(val = 64)]
419 tile_res: i64,
420 #[init(val = 0.0)]
424 bump_enable: f32,
425 #[export(range = (0.02, 1.0, 0.01))]
429 #[init(val = 1.0)]
430 face_fraction: f32,
431 #[export]
434 #[init(val = false)]
435 flat_face: bool,
436
437 job: Option<Gd<CesChunkJob>>,
438 run_cb: Option<Callable>,
439 multimesh: Option<Gd<MultiMesh>>,
440 mmi: Option<Gd<MultiMeshInstance3D>>,
441 material: Option<Gd<ShaderMaterial>>,
442 _template: Option<Gd<ArrayMesh>>,
443
444 #[init(val = Rid::Invalid)]
445 wired_pos: Rid,
446 #[init(val = Rid::Invalid)]
447 wired_verts: Rid,
448 #[init(val = Rid::Invalid)]
449 wired_color: Rid,
450 #[init(val = Rid::Invalid)]
451 wired_normal: Rid,
452
453 built: bool,
454}
455
456#[godot_api]
457impl INode3D for CelestialTileChunk {
458 fn process(&mut self, _delta: f64) {
459 self.ensure_built();
460 self.pump();
461 self.wire_textures();
462 }
463
464 fn on_notification(&mut self, what: godot::classes::notify::Node3DNotification) {
465 use godot::classes::notify::Node3DNotification as N;
466 if matches!(what, N::EXIT_TREE | N::PREDELETE) {
467 self.job = None;
469 self.run_cb = None;
470 }
471 }
472}
473
474#[godot_api]
475impl CelestialTileChunk {
476 #[func]
479 fn set_resolution(&mut self, res: i64) {
480 let r = res.clamp(2, 1024);
481 if r == self.chunk_res && self.built {
482 return;
483 }
484 self.chunk_res = r;
485 self.teardown();
486 }
487
488 #[func]
491 fn set_tile_res(&mut self, n: i64) {
492 let r = n.clamp(8, 1024);
493 if r == self.tile_res && self.built {
494 return;
495 }
496 self.tile_res = r;
497 self.teardown();
498 }
499
500 #[func]
503 fn set_bump(&mut self, enable: f32) {
504 let e = if enable > 0.5 { 1.0 } else { 0.0 };
505 if e == self.bump_enable && self.built {
506 return;
507 }
508 self.bump_enable = e;
509 self.teardown();
510 }
511
512 #[func]
514 fn chunk_resolution(&self) -> i64 {
515 self.chunk_res
516 }
517
518 #[func]
520 fn tile_resolution(&self) -> i64 {
521 self.tile_res
522 }
523
524 #[func]
526 fn chunk_centroid(&self) -> Vector3 {
527 let c = self.world_corners();
528 (c[0] + c[1] + c[2]) / 3.0
529 }
530
531 #[func]
533 fn chunk_normal(&self) -> Vector3 {
534 let c = self.world_corners();
535 let cen = (c[0] + c[1] + c[2]) / 3.0;
536 let n = (c[1] - c[0]).cross(c[2] - c[0]).normalized();
537 if n.dot(cen) < 0.0 {
538 -n
539 } else {
540 n
541 }
542 }
543
544 #[func]
546 fn chunk_up(&self) -> Vector3 {
547 let c = self.world_corners();
548 let cen = (c[0] + c[1] + c[2]) / 3.0;
549 (c[0] - cen).normalized()
550 }
551
552 #[func]
554 fn chunk_extent(&self) -> f32 {
555 let c = self.world_corners();
556 let cen = (c[0] + c[1] + c[2]) / 3.0;
557 c.iter().map(|p| (*p - cen).length()).fold(0.0_f32, f32::max)
558 }
559
560 #[func]
562 fn face_id(&self) -> i64 {
563 self.face_index() as i64
564 }
565
566 #[func]
569 fn bary_corners(&self) -> PackedVector2Array {
570 let b = self.sub_bary();
571 PackedVector2Array::from(&[
572 Vector2::new(b[0].wb, b[0].wc),
573 Vector2::new(b[1].wb, b[1].wc),
574 Vector2::new(b[2].wb, b[2].wc),
575 ])
576 }
577
578 #[func]
580 fn world_corners_packed(&self) -> PackedVector3Array {
581 let c = self.world_corners();
582 PackedVector3Array::from(&[c[0], c[1], c[2]])
583 }
584}
585
586impl CelestialTileChunk {
587 fn effective_radius(&self) -> f32 {
589 if self.flat_face {
590 0.0
591 } else {
592 self.radius
593 }
594 }
595
596 fn sub_bary(&self) -> [Bary; 3] {
599 let s = self.face_fraction.clamp(0.02, 1.0);
600 let cb = 1.0 / 3.0;
601 let lerp = |wb: f32, wc: f32| Bary { wb: cb + s * (wb - cb), wc: cb + s * (wc - cb) };
602 [lerp(0.0, 0.0), lerp(1.0, 0.0), lerp(0.0, 1.0)]
603 }
604
605 fn face_index(&self) -> usize {
607 self.face.clamp(0, 19) as usize
608 }
609
610 fn frames(&self) -> Vec<celestial_algo::clipmap::FaceFrame> {
613 let mut frames = base_face_frames(self.radius);
614 let er = self.effective_radius();
615 for f in &mut frames {
616 f.radius = er;
617 }
618 frames
619 }
620
621 fn world_corners(&self) -> [Vector3; 3] {
623 let frames = self.frames();
624 let f = &frames[self.face_index()];
625 let b = self.sub_bary();
626 [
627 f.project_bary(b[0].wb, b[0].wc),
628 f.project_bary(b[1].wb, b[1].wc),
629 f.project_bary(b[2].wb, b[2].wc),
630 ]
631 }
632
633 fn ensure_built(&mut self) {
636 if self.built {
637 return;
638 }
639 let res = self.chunk_res.clamp(2, 1024) as u32;
640 let tile_res = self.tile_res.clamp(8, 1024) as u32;
641
642 let mut material = make_chunk_material(res, tile_res);
643 let template = reference_chunk_mesh(res, &material.clone().upcast());
644
645 let mut rs = RenderingServer::singleton();
646 let multimesh = MultiMesh::new_gd();
647 let mm_rid = multimesh.get_rid();
648 let mut mmi = MultiMeshInstance3D::new_alloc();
649 mmi.set_multimesh(&multimesh);
650 mmi.set_visible(false);
654 self.base_mut().add_child(&mmi);
655
656 rs.multimesh_allocate_data_ex(mm_rid, 1, MultimeshTransformFormat::TRANSFORM_3D)
657 .custom_data_format(true)
658 .use_indirect(true)
659 .done();
660 rs.multimesh_set_mesh(mm_rid, template.get_rid());
661 let m = self.radius.max(1.0) * 1.4;
662 rs.multimesh_set_custom_aabb(
663 mm_rid,
664 Aabb { position: Vector3::splat(-m), size: Vector3::splat(2.0 * m) },
665 );
666
667 let terrain = assemble(&HeightGpu::default(), &TextureGpu::default());
669 let job = CesChunkJob::create(mm_rid, 1, res, tile_res, self.radius, self.bump_enable, terrain, Vec::new());
670 self.run_cb = Some(Callable::from_object_method(&job, "run"));
671
672 material.set_shader_parameter("attr_w", &(ATTR_TEX_WIDTH as i32).to_variant());
673
674 self.job = Some(job);
675 self.multimesh = Some(multimesh);
676 self.mmi = Some(mmi);
677 self.material = Some(material);
678 self._template = Some(template);
679 self.built = true;
680
681 self.stage_chunk(res);
682 }
683
684 fn stage_chunk(&mut self, res: u32) {
686 let frames = self.frames();
687 let fi = self.face_index();
688 let f = &frames[fi];
689 let bary = self.sub_bary();
690 let corners = [
691 f.project_bary(bary[0].wb, bary[0].wc),
692 f.project_bary(bary[1].wb, bary[1].wc),
693 f.project_bary(bary[2].wb, bary[2].wc),
694 ];
695 let chunk = Chunk {
696 id: ChunkId { face: fi as u8, depth: 0, path: 0 },
697 bary,
698 corners,
699 level: 0,
700 };
701 let desc_bytes = pack_chunks(&frames, &[(0u32, chunk)], res);
702 let instance_bytes = pack_instances(&[0u32], &[1.0]);
704 if let Some(job) = &mut self.job {
705 job.bind_mut().stage = Some(ChunkStage {
706 desc_bytes,
707 realize_count: 1,
708 instance_bytes,
709 instance_count: 1,
710 surface_enabled: 0.0,
713 surface_height_scale: 0.0,
714 surface_patches: Vec::new(),
715 scatter_aux_bytes: Vec::new(),
716 scatter_vis_bytes: Vec::new(),
717 scatter_vis_count: 0,
718 scatter_layer_params: Vec::new(),
719 });
720 }
721 }
722
723 fn pump(&mut self) {
725 let pending = self.job.as_ref().map(|j| j.bind().stage.is_some()).unwrap_or(false);
726 if pending {
727 if let Some(cb) = self.run_cb.clone() {
728 RenderingServer::singleton().call_on_render_thread(&cb);
729 }
730 }
731 }
732
733 fn wire_textures(&mut self) {
735 let (pos, verts, color, normal) = match &self.job {
736 Some(job) => {
737 let g = &job.bind().gpu;
738 (g.pos_tex(), g.attr_tex(), g.color_atlas(), g.normal_atlas())
739 }
740 None => return,
741 };
742 if self.material.is_none() {
743 return;
744 }
745 for (rid, wired, name) in [
746 (pos, &mut self.wired_pos, "pos_tex"),
747 (verts, &mut self.wired_verts, "verts_tex"),
748 (color, &mut self.wired_color, "color_atlas"),
749 (normal, &mut self.wired_normal, "normal_atlas"),
750 ] {
751 if rid.is_valid() && rid != *wired {
752 let mut tex = Texture2Drd::new_gd();
753 tex.set_texture_rd_rid(rid);
754 self.material.as_mut().unwrap().set_shader_parameter(name, &tex.to_variant());
755 *wired = rid;
756 }
757 }
758 if self.wired_pos.is_valid()
760 && self.wired_verts.is_valid()
761 && self.wired_color.is_valid()
762 && self.wired_normal.is_valid()
763 {
764 if let Some(mmi) = self.mmi.as_mut() {
765 if !mmi.is_visible() {
766 mmi.set_visible(true);
767 }
768 }
769 }
770 }
771
772 fn teardown(&mut self) {
776 if let Some(mut mmi) = self.mmi.take() {
777 mmi.set_visible(false);
782 mmi.queue_free();
783 }
784 self.job = None;
785 self.run_cb = None;
786 self.multimesh = None;
787 self.material = None;
788 self._template = None;
789 self.wired_pos = Rid::Invalid;
790 self.wired_verts = Rid::Invalid;
791 self.wired_color = Rid::Invalid;
792 self.wired_normal = Rid::Invalid;
793 self.built = false;
794 }
795}
796
797fn make_chunk_material(res: u32, tile_res: u32) -> Gd<ShaderMaterial> {
799 let shader = godot::tools::load::<Shader>(CHUNK_SHADER);
800 let mut mat = ShaderMaterial::new_gd();
801 mat.set_shader(&shader);
802 mat.set_shader_parameter("attr_w", &(ATTR_TEX_WIDTH as i32).to_variant());
803 mat.set_shader_parameter("verts_per_chunk", &(verts_per_chunk(res) as i32).to_variant());
804 mat.set_shader_parameter("chunk_res", &(res as i32).to_variant());
805 mat.set_shader_parameter("tile_res", &(tile_res as i32).to_variant());
806 mat.set_shader_parameter("lod_colors", &false.to_variant());
807 mat
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813 use std::mem::offset_of;
814
815 #[test]
816 fn tile_viewer_params_layout() {
817 assert_eq!(std::mem::size_of::<TileViewerParams>(), 176);
821 assert_eq!(std::mem::size_of::<TileViewerParams>() % 16, 0);
822 assert_eq!(offset_of!(TileViewerParams, a), 0);
823 assert_eq!(offset_of!(TileViewerParams, b), 16);
824 assert_eq!(offset_of!(TileViewerParams, c), 32);
825 assert_eq!(offset_of!(TileViewerParams, sub0), 48);
826 assert_eq!(offset_of!(TileViewerParams, sub1), 64);
827 assert_eq!(offset_of!(TileViewerParams, sub2), 80);
828 assert_eq!(offset_of!(TileViewerParams, width), 96);
829 assert_eq!(offset_of!(TileViewerParams, tex_res), 100);
830 assert_eq!(offset_of!(TileViewerParams, terrain), 112);
831 assert_eq!(std::mem::size_of::<TerrainGpu>(), 56);
832 }
833
834 #[test]
835 fn params_default_is_zeroed_pod() {
836 let p = TileViewerParams::zeroed();
839 assert_eq!(bytemuck::bytes_of(&p).len(), 176);
840 }
841}