feat: lazy loading of terrain chunk LODs

This commit is contained in:
Sara Gerretsen 2025-12-08 19:46:29 +01:00
parent 90c46e30d2
commit 2a3eeef522
4 changed files with 48 additions and 28 deletions

View file

@ -21,11 +21,21 @@ void TerrainChunk::ready() {
process_lod(); process_lod();
} }
void TerrainChunk::generate_lod(size_t lod) {
MeshStatus status{ this->meshes.get(lod) };
size_t base_detail{ lod == 0 ? this->lod0_detail : this->lod0_detail / (2 * lod + lod) };
base_detail = base_detail > 1 ? base_detail : 1;
Vector3 const position{ get_global_position() };
this->generator->push_task({ { position.x, position.z }, { this->size, this->size } }, status.mesh, base_detail > 1 ? base_detail : 1, callable_mp(this, &self_type::lod_generated).bind(lod));
status.flag = MESH_DISPATCHED;
this->meshes.set(lod, status);
}
void TerrainChunk::on_terrain_changed() { void TerrainChunk::on_terrain_changed() {
if (this->generator) { if (this->generator) {
Vector3 const position{ get_global_position() }; if (this->meshes.size() != this->lod_count) {
this->meshes.resize_initialized(3); this->meshes.resize_initialized(this->lod_count);
size_t lod{ 0 }; }
if (this->collisions) { if (this->collisions) {
this->collisions->queue_free(); this->collisions->queue_free();
this->collisions = nullptr; this->collisions = nullptr;
@ -34,28 +44,31 @@ void TerrainChunk::on_terrain_changed() {
if (!status.mesh.is_valid()) { if (!status.mesh.is_valid()) {
status.mesh.instantiate(); status.mesh.instantiate();
} }
size_t base_detail{ lod == 0 ? this->lod0_detail : this->lod0_detail / (2 * lod) }; status.flag = MESH_DIRTY;
status.dirty = true;
this->generator->push_task({ { position.x, position.z }, { this->size, this->size } }, status.mesh, base_detail > 1 ? base_detail : 1, callable_mp(this, &self_type::lod_generated).bind(lod));
lod++;
} }
generate_lod(this->meshes.size() - 1);
} }
} }
void TerrainChunk::lod_generated(size_t lod) { void TerrainChunk::lod_generated(size_t lod) {
this->meshes.set(lod, { this->meshes[lod].mesh, false }); this->meshes.set(lod, { this->meshes[lod].mesh, MESH_LOADED });
} }
void TerrainChunk::process_lod() { void TerrainChunk::process_lod() {
size_t result{ (size_t)this->meshes.size() }; size_t result{ (size_t)this->meshes.size() };
if (is_ready() && this->meshes.size() > 0) { if (is_ready() && this->meshes.size() > 0) {
Vector3 position{ get_global_position() }; Vector3 position{ get_global_position() };
position.y = 0;
Vector3 camera{ get_viewport()->get_camera_3d()->get_global_position() }; Vector3 camera{ get_viewport()->get_camera_3d()->get_global_position() };
float distance{ (position - camera).length() - this->size / 2.f }; camera.y = 0;
float distance{ (position - camera).length() - this->size };
distance = distance > 0.f ? distance : 0.f; distance = distance > 0.f ? distance : 0.f;
size_t lod{ size_t(Math::floor(distance / (this->lod_end_distance / this->meshes.size()))) }; size_t lod{ size_t(Math::floor(distance / ((this->max_lod_distance * this->size) / this->meshes.size()))) };
result = lod < this->meshes.size() ? lod : (this->meshes.size() - 1); result = lod < this->meshes.size() ? lod : (this->meshes.size() - 1);
while (this->meshes[result].dirty && result < (this->meshes.size() - 1)) { if (this->meshes[result].flag == MESH_DIRTY) {
generate_lod(result);
}
while (this->meshes[result].flag != MESH_LOADED && result < (this->meshes.size() - 1)) {
result++; result++;
} }
if (this->meshes[result].mesh != this->get_mesh()) { if (this->meshes[result].mesh != this->get_mesh()) {

View file

@ -6,11 +6,17 @@ class TerrainMeshGenerator;
class TerrainChunk : public MeshInstance3D { class TerrainChunk : public MeshInstance3D {
GDCLASS(TerrainChunk, MeshInstance3D); GDCLASS(TerrainChunk, MeshInstance3D);
enum MeshStatusFlag {
MESH_DIRTY,
MESH_DISPATCHED,
MESH_LOADED
};
struct MeshStatus { struct MeshStatus {
Ref<ArrayMesh> mesh; Ref<ArrayMesh> mesh;
bool dirty; MeshStatusFlag flag;
}; };
static void _bind_methods(); static void _bind_methods();
void generate_lod(size_t lod);
void ready(); void ready();
void on_terrain_changed(); void on_terrain_changed();
void lod_generated(size_t lod); void lod_generated(size_t lod);
@ -27,10 +33,10 @@ public:
private: private:
Node *collisions{ nullptr }; Node *collisions{ nullptr };
size_t collisions_lod{ 0 }; size_t lod_count{ 3 };
Vector<MeshStatus> meshes{}; Vector<MeshStatus> meshes{};
int lod0_detail{ 200 }; int lod0_detail{ 200 };
float lod_end_distance{ 600 }; float max_lod_distance{ 6 };
float size{ 200 }; float size{ 200 };
TerrainMeshGenerator *generator{ nullptr }; TerrainMeshGenerator *generator{ nullptr };
}; };

View file

@ -4,5 +4,5 @@
[node name="TerrainChunk" type="TerrainChunk" unique_id=1453572398] [node name="TerrainChunk" type="TerrainChunk" unique_id=1453572398]
material_override = ExtResource("1_6vjd7") material_override = ExtResource("1_6vjd7")
size = 50.0 size = 40.0
lod0_detail = 100 lod0_detail = 80

View file

@ -31,6 +31,7 @@ noise = SubResource("FastNoiseLite_b1cmn")
noise_amplitude = 5.0 noise_amplitude = 5.0
[sub_resource type="FastNoiseLite" id="FastNoiseLite_ba0ut"] [sub_resource type="FastNoiseLite" id="FastNoiseLite_ba0ut"]
frequency = 0.0163
fractal_type = 2 fractal_type = 2
fractal_gain = 1.0 fractal_gain = 1.0
fractal_weighted_strength = 0.58 fractal_weighted_strength = 0.58
@ -132,7 +133,7 @@ load_path = "res://.godot/imported/point.svg-e68fd7c1e788d2c48d769cc58eba6e98.ct
primitives = [SubResource("PointPrimitive_5tm2q"), SubResource("NoisePrimitive_ba0ut"), SubResource("NoisePrimitive_pxqd5"), SubResource("NoisePrimitive_q68jb"), SubResource("PlanePrimitive_ba0ut")] primitives = [SubResource("PointPrimitive_5tm2q"), SubResource("NoisePrimitive_ba0ut"), SubResource("NoisePrimitive_pxqd5"), SubResource("NoisePrimitive_q68jb"), SubResource("PlanePrimitive_ba0ut")]
vertex_color_gradient = SubResource("Gradient_b1cmn") vertex_color_gradient = SubResource("Gradient_b1cmn")
color_gradient_end_height = 100.0 color_gradient_end_height = 100.0
chunk_count = 4 chunk_count = 8
chunk_scene = ExtResource("1_pxqd5") chunk_scene = ExtResource("1_pxqd5")
point_primitive_object = ExtResource("1_b1cmn") point_primitive_object = ExtResource("1_b1cmn")
@ -176,17 +177,6 @@ current_tab = 0
layout_mode = 2 layout_mode = 2
metadata/_tab_index = 0 metadata/_tab_index = 0
[node name="Tree" type="PrimitiveLayerList" parent="LeftPanel/VBoxContainer/Layers/Layers" unique_id=797700186 node_paths=PackedStringArray("terrain")]
layout_mode = 2
size_flags_vertical = 3
scroll_horizontal_enabled = false
terrain = NodePath("../../../../../TerrainMeshEditor")
icons = {
&"NoisePrimitive": ExtResource("5_eqbpn"),
&"PlanePrimitive": ExtResource("4_xg7d5"),
&"PointPrimitive": ExtResource("4_5lcyj")
}
[node name="HBoxContainer" type="HBoxContainer" parent="LeftPanel/VBoxContainer/Layers/Layers" unique_id=702489990] [node name="HBoxContainer" type="HBoxContainer" parent="LeftPanel/VBoxContainer/Layers/Layers" unique_id=702489990]
layout_mode = 2 layout_mode = 2
size_flags_vertical = 8 size_flags_vertical = 8
@ -222,6 +212,17 @@ expand_icon = true
primitive_blueprint = SubResource("NoisePrimitive_5lcyj") primitive_blueprint = SubResource("NoisePrimitive_5lcyj")
terrain = NodePath("../../../../../../TerrainMeshEditor") terrain = NodePath("../../../../../../TerrainMeshEditor")
[node name="Tree" type="PrimitiveLayerList" parent="LeftPanel/VBoxContainer/Layers/Layers" unique_id=797700186 node_paths=PackedStringArray("terrain")]
layout_mode = 2
size_flags_vertical = 3
scroll_horizontal_enabled = false
terrain = NodePath("../../../../../TerrainMeshEditor")
icons = {
&"NoisePrimitive": ExtResource("5_eqbpn"),
&"PlanePrimitive": ExtResource("4_xg7d5"),
&"PointPrimitive": ExtResource("4_5lcyj")
}
[node name="Inspector" type="TabContainer" parent="LeftPanel/VBoxContainer" unique_id=240272030] [node name="Inspector" type="TabContainer" parent="LeftPanel/VBoxContainer" unique_id=240272030]
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3