feat: implemented multithreading and chunk lods
This commit is contained in:
parent
49b65a7ade
commit
1951b560ed
|
|
@ -1,3 +1,3 @@
|
|||
((c++-mode . (mode . clang-format-on-save)))
|
||||
((c++-mode . ((mode . clang-format-on-save))))
|
||||
|
||||
((c-mode . ((mode c++-mode))))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
#include "terrain_chunk.h"
|
||||
#include "core/config/engine.h"
|
||||
#include "macros.h"
|
||||
#include "scene/3d/camera_3d.h"
|
||||
#include "scene/main/viewport.h"
|
||||
#include "terrain_editor/terrain_mesh_generator.h"
|
||||
|
||||
void TerrainChunk::_bind_methods() {}
|
||||
void TerrainChunk::_bind_methods() {
|
||||
BIND_PROPERTY(Variant::FLOAT, size);
|
||||
BIND_PROPERTY(Variant::INT, lod0_detail);
|
||||
}
|
||||
|
||||
void TerrainChunk::ready() {
|
||||
if ((this->generator = cast_to<TerrainMeshGenerator>(get_parent()))) {
|
||||
|
|
@ -10,41 +17,85 @@ void TerrainChunk::ready() {
|
|||
print_error(vformat("Chunk %s ready without generator.", get_path()));
|
||||
return;
|
||||
}
|
||||
this->set_mesh(this->lod1);
|
||||
process_lod();
|
||||
on_terrain_changed();
|
||||
}
|
||||
|
||||
void TerrainChunk::on_terrain_changed() {
|
||||
if (this->generator) {
|
||||
Vector3 const position{ get_global_position() };
|
||||
this->generator->generate_grid({ { position.x, position.z }, { this->size, this->size } }, this->mesh, this->lod1_detail);
|
||||
this->meshes.resize_zeroed(5);
|
||||
size_t lod{ 0 };
|
||||
for (MeshStatus &status : this->meshes) {
|
||||
if (!status.mesh.is_valid()) {
|
||||
status.mesh.instantiate();
|
||||
}
|
||||
size_t base_detail{ lod == 0 ? this->lod0_detail : this->lod0_detail / (2 * lod * lod) };
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TerrainChunk::lod_generated(size_t lod) {
|
||||
this->meshes.set(lod, MeshStatus{ this->meshes[lod].mesh, false });
|
||||
}
|
||||
|
||||
void TerrainChunk::process_lod() {
|
||||
size_t result{ (size_t)this->meshes.size() };
|
||||
if (is_ready() && this->meshes.size() > 0) {
|
||||
Vector3 position{ get_global_position() };
|
||||
Vector3 camera{ get_viewport()->get_camera_3d()->get_global_position() };
|
||||
position.y = camera.y = 0;
|
||||
float distance{ camera.distance_to(position) };
|
||||
size_t lod{ size_t(Math::floor(distance / (this->lod4_distance / 5.f))) };
|
||||
result = lod < 5 ? lod : 4;
|
||||
while (this->meshes[result].dirty && result < (this->meshes.size() - 1)) {
|
||||
result++;
|
||||
}
|
||||
if (this->meshes[result].mesh != this->get_mesh()) {
|
||||
this->set_mesh(this->meshes[result].mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TerrainChunk::_notification(int what) {
|
||||
if (Engine::get_singleton()->is_editor_hint()) {
|
||||
return;
|
||||
}
|
||||
switch (what) {
|
||||
default:
|
||||
return;
|
||||
case NOTIFICATION_READY:
|
||||
set_process_thread_group(ProcessThreadGroup::PROCESS_THREAD_GROUP_SUB_THREAD);
|
||||
set_process(true);
|
||||
ready();
|
||||
return;
|
||||
case NOTIFICATION_PROCESS:
|
||||
process_lod();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void TerrainChunk::set_size(float size) {
|
||||
this->size = size;
|
||||
on_terrain_changed();
|
||||
if (is_ready()) {
|
||||
on_terrain_changed();
|
||||
}
|
||||
}
|
||||
|
||||
float TerrainChunk::get_size() const {
|
||||
return this->size;
|
||||
}
|
||||
|
||||
void TerrainChunk::set_lod1_detail(int detail) {
|
||||
this->lod1_detail = detail;
|
||||
on_terrain_changed();
|
||||
void TerrainChunk::set_lod0_detail(int detail) {
|
||||
this->lod0_detail = detail;
|
||||
if (is_ready()) {
|
||||
on_terrain_changed();
|
||||
}
|
||||
}
|
||||
|
||||
int TerrainChunk::get_lod1_detail() const {
|
||||
return this->lod1_detail;
|
||||
int TerrainChunk::get_lod0_detail() const {
|
||||
return this->lod0_detail;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
#pragma once
|
||||
|
||||
#include "core/templates/pair.h"
|
||||
#include "core/templates/vector.h"
|
||||
#include "scene/3d/mesh_instance_3d.h"
|
||||
class TerrainMeshGenerator;
|
||||
|
||||
class TerrainChunk : public MeshInstance3D {
|
||||
GDCLASS(TerrainChunk, MeshInstance3D);
|
||||
struct MeshStatus {
|
||||
Ref<ArrayMesh> mesh;
|
||||
bool dirty;
|
||||
};
|
||||
static void _bind_methods();
|
||||
void ready();
|
||||
void on_terrain_changed();
|
||||
void lod_generated(size_t lod);
|
||||
void process_lod();
|
||||
|
||||
protected:
|
||||
void _notification(int what);
|
||||
|
|
@ -15,12 +23,13 @@ protected:
|
|||
public:
|
||||
void set_size(float size);
|
||||
float get_size() const;
|
||||
void set_lod1_detail(int detail);
|
||||
int get_lod1_detail() const;
|
||||
void set_lod0_detail(int detail);
|
||||
int get_lod0_detail() const;
|
||||
|
||||
private:
|
||||
Ref<ArrayMesh> lod1{ memnew(ArrayMesh) };
|
||||
Vector<MeshStatus> meshes{};
|
||||
TerrainMeshGenerator *generator{ nullptr };
|
||||
int lod1_detail{ 100 };
|
||||
float size{ 50 };
|
||||
int lod0_detail{ 200 };
|
||||
float lod4_distance{ 1500 };
|
||||
float size{ 200 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,31 +23,79 @@ void TerrainMeshGenerator::_bind_methods() {
|
|||
BIND_HPROPERTY(Variant::OBJECT, chunk_scene, PROPERTY_HINT_RESOURCE_TYPE, "PackedScene");
|
||||
ADD_SIGNAL(MethodInfo(sig_primitives_changed));
|
||||
ADD_SIGNAL(MethodInfo(sig_primitive_list_changed, PropertyInfo(Variant::ARRAY, "array", PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:TerrainPrimitive", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE))));
|
||||
ClassDB::bind_method(D_METHOD("generate_grid", "area", "out_mesh", "side_points"), &self_type::generate_grid);
|
||||
ClassDB::bind_method(D_METHOD("push_task", "area", "out_mesh", "side_points"), &self_type::push_task);
|
||||
}
|
||||
|
||||
void TerrainMeshGenerator::ready() {
|
||||
this->thread.instantiate();
|
||||
if (this->chunks.is_empty()) {
|
||||
rebuild_chunks();
|
||||
void TerrainMeshGenerator::enter_tree() {
|
||||
this->thread.start(&TerrainMeshGenerator::background_generation_thread, this);
|
||||
}
|
||||
|
||||
void TerrainMeshGenerator::process() {
|
||||
if (this->output_lock.try_lock()) {
|
||||
for (TerrainMeshTask &task : this->output_queue) {
|
||||
task.mesh->clear_surfaces();
|
||||
task.out_surface->commit(task.mesh);
|
||||
task.callback.call();
|
||||
}
|
||||
this->output_queue.clear();
|
||||
this->output_lock.unlock();
|
||||
}
|
||||
if (this->input_lock.try_lock()) {
|
||||
set_process(!this->input_queue.is_empty());
|
||||
this->input_lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void TerrainMeshGenerator::on_configuration_changed() {
|
||||
emit_signal(sig_primitives_changed);
|
||||
this->settings_lock.lock();
|
||||
// copy main thread primitives buffer data to worker thread buffer
|
||||
this->primitives.clear();
|
||||
for (Variant var : this->primitives_buffer) {
|
||||
Ref<TerrainPrimitive> primitive{ var };
|
||||
if (primitive.is_valid()) {
|
||||
this->primitives.push_back(primitive->duplicate(true));
|
||||
}
|
||||
}
|
||||
this->settings_lock.unlock();
|
||||
if (is_ready()) {
|
||||
emit_signal(sig_primitive_list_changed, get_primitives());
|
||||
emit_signal(sig_primitives_changed);
|
||||
}
|
||||
}
|
||||
|
||||
void TerrainMeshGenerator::_notification(int what) {
|
||||
if (Engine::get_singleton()->is_editor_hint()) {
|
||||
return;
|
||||
}
|
||||
switch (what) {
|
||||
default:
|
||||
return;
|
||||
case NOTIFICATION_ENTER_TREE:
|
||||
set_process(true);
|
||||
enter_tree();
|
||||
return;
|
||||
case NOTIFICATION_READY:
|
||||
ready();
|
||||
rebuild_chunks();
|
||||
break;
|
||||
case NOTIFICATION_PROCESS:
|
||||
process();
|
||||
return;
|
||||
case NOTIFICATION_EXIT_TREE:
|
||||
if (this->thread.is_started()) {
|
||||
this->input_lock.lock();
|
||||
this->end_thread = true;
|
||||
this->input_lock.unlock();
|
||||
this->work_signal.post();
|
||||
this->thread.wait_to_finish();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void TerrainMeshGenerator::rebuild_chunks() {
|
||||
if (!is_ready()) {
|
||||
return;
|
||||
}
|
||||
for (TerrainChunk *chunk : this->chunks) {
|
||||
chunk->queue_free();
|
||||
}
|
||||
|
|
@ -71,6 +119,43 @@ void TerrainMeshGenerator::rebuild_chunks() {
|
|||
}
|
||||
}
|
||||
|
||||
void TerrainMeshGenerator::background_generation_thread(void *user) {
|
||||
TerrainMeshGenerator *self{ static_cast<TerrainMeshGenerator *>(user) };
|
||||
if (self == nullptr) {
|
||||
return;
|
||||
}
|
||||
TerrainMeshTask task{};
|
||||
for (;;) {
|
||||
// await more work signal
|
||||
self->work_signal.wait();
|
||||
// pull task from input queue
|
||||
self->input_lock.lock();
|
||||
if (self->end_thread) {
|
||||
self->input_lock.unlock();
|
||||
break;
|
||||
} else {
|
||||
task = self->input_queue[0];
|
||||
self->input_queue.remove_at(0);
|
||||
self->input_lock.unlock();
|
||||
// create surface
|
||||
task.out_surface.instantiate();
|
||||
// lock settings and generate grid
|
||||
self->settings_lock.lock();
|
||||
if (self->primitives.is_empty()) {
|
||||
print_error("Terrain mesh generator thread has no primitive list");
|
||||
} else {
|
||||
self->generate_grid(task.area, task.out_surface, task.side_points);
|
||||
}
|
||||
self->settings_lock.unlock();
|
||||
// push output onto output queue
|
||||
self->output_lock.lock();
|
||||
self->output_queue.push_back(task);
|
||||
self->call_deferred("set_process", true);
|
||||
self->output_lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Color TerrainMeshGenerator::color_at_height(float height) const {
|
||||
float const mapped{ Math::remap(height, this->color_gradient_start_height, this->color_gradient_end_height, 0.f, 1.f) };
|
||||
return this->vertex_color_gradient.is_valid()
|
||||
|
|
@ -111,19 +196,17 @@ void face_from(Ref<SurfaceTool> surface, size_t tl, size_t side_points) {
|
|||
}
|
||||
}
|
||||
|
||||
void TerrainMeshGenerator::generate_grid(Rect2 area, Ref<ArrayMesh> mesh, size_t side_points) {
|
||||
mesh->clear_surfaces();
|
||||
surface->clear();
|
||||
void TerrainMeshGenerator::generate_grid(Rect2 area, Ref<SurfaceTool> mesh, size_t side_points) {
|
||||
Vector2 point_distance{ area.size / (float)side_points };
|
||||
++side_points;
|
||||
Vector2 at{ area.position };
|
||||
surface->begin(Mesh::PRIMITIVE_TRIANGLES);
|
||||
mesh->begin(Mesh::PRIMITIVE_TRIANGLES);
|
||||
for (size_t xpoint{ 0 }; xpoint < side_points; ++xpoint) {
|
||||
for (size_t ypoint{ 0 }; ypoint < side_points; ++ypoint) {
|
||||
float const height{ evaluate_point(at) };
|
||||
surface->set_color(color_at_height(height));
|
||||
surface->set_uv({ at / area.size });
|
||||
surface->add_vertex({ at.x - area.position.x, height, at.y - area.position.y });
|
||||
mesh->set_color(color_at_height(height));
|
||||
mesh->set_uv({ at / area.size });
|
||||
mesh->add_vertex({ at.x - area.position.x, height, at.y - area.position.y });
|
||||
at.y += point_distance.y;
|
||||
}
|
||||
at.y = area.position.y;
|
||||
|
|
@ -131,41 +214,68 @@ void TerrainMeshGenerator::generate_grid(Rect2 area, Ref<ArrayMesh> mesh, size_t
|
|||
}
|
||||
for (size_t xface{ 0 }; xface < side_points - 1; ++xface) {
|
||||
for (size_t yface{ 0 }; yface < side_points - 1; ++yface) {
|
||||
face_from(surface, xface + yface * (size_t)side_points, side_points);
|
||||
face_from(mesh, xface + yface * (size_t)side_points, side_points);
|
||||
}
|
||||
}
|
||||
surface->generate_normals();
|
||||
surface->generate_tangents();
|
||||
surface->commit(mesh);
|
||||
mesh->generate_normals();
|
||||
mesh->generate_tangents();
|
||||
}
|
||||
|
||||
void TerrainMeshGenerator::push_task(Rect2 area, Ref<ArrayMesh> mesh, size_t points, Callable callback) {
|
||||
this->input_lock.lock();
|
||||
for (TerrainMeshTask &task : this->input_queue) {
|
||||
if (task.mesh == mesh) {
|
||||
task.area = area;
|
||||
task.side_points = points;
|
||||
task.callback = callback;
|
||||
this->input_lock.unlock();
|
||||
return;
|
||||
}
|
||||
}
|
||||
TerrainMeshTask task{
|
||||
.mesh = mesh,
|
||||
.area = area,
|
||||
.side_points = points,
|
||||
.out_surface = nullptr,
|
||||
.callback = callback
|
||||
};
|
||||
for (size_t i{ 0 }; i < this->input_queue.size(); ++i) {
|
||||
if (this->input_queue[i].side_points >= points) {
|
||||
this->input_queue.insert(i, task);
|
||||
this->work_signal.post();
|
||||
this->input_lock.unlock();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->input_queue.push_back(task);
|
||||
this->work_signal.post();
|
||||
this->input_lock.unlock();
|
||||
}
|
||||
|
||||
void TerrainMeshGenerator::set_primitives(Array primitives) {
|
||||
for (Ref<TerrainPrimitive> primitive : this->primitives) {
|
||||
// synchronise primitives
|
||||
for (Variant var : this->primitives_buffer) {
|
||||
Ref<TerrainPrimitive> primitive{ var };
|
||||
if (primitive.is_valid()) {
|
||||
primitive->disconnect_changed(this->generation_changed);
|
||||
}
|
||||
}
|
||||
this->primitives.clear();
|
||||
for (Variant var : primitives) {
|
||||
this->primitives_buffer = primitives;
|
||||
for (Variant var : this->primitives_buffer) {
|
||||
Ref<TerrainPrimitive> primitive{ var };
|
||||
this->primitives.push_back(primitive);
|
||||
if (primitive.is_valid()) {
|
||||
primitive->connect_changed(this->generation_changed);
|
||||
}
|
||||
}
|
||||
emit_signal(sig_primitive_list_changed, get_primitives());
|
||||
on_configuration_changed();
|
||||
}
|
||||
|
||||
Array TerrainMeshGenerator::get_primitives() const {
|
||||
Array a;
|
||||
for (Ref<TerrainPrimitive> primitive : this->primitives) {
|
||||
a.push_back(primitive);
|
||||
}
|
||||
return a;
|
||||
return this->primitives_buffer;
|
||||
}
|
||||
|
||||
void TerrainMeshGenerator::set_vertex_color_gradient(Ref<Gradient> gradient) {
|
||||
this->settings_lock.lock();
|
||||
if (this->vertex_color_gradient.is_valid()) {
|
||||
this->vertex_color_gradient->disconnect_changed(this->generation_changed);
|
||||
}
|
||||
|
|
@ -173,6 +283,7 @@ void TerrainMeshGenerator::set_vertex_color_gradient(Ref<Gradient> gradient) {
|
|||
if (gradient.is_valid()) {
|
||||
this->vertex_color_gradient->connect_changed(this->generation_changed);
|
||||
}
|
||||
this->settings_lock.unlock();
|
||||
on_configuration_changed();
|
||||
}
|
||||
|
||||
|
|
@ -181,7 +292,9 @@ Ref<Gradient> TerrainMeshGenerator::get_vertex_color_gradient() const {
|
|||
}
|
||||
|
||||
void TerrainMeshGenerator::set_color_gradient_start_height(float value) {
|
||||
this->settings_lock.lock();
|
||||
this->color_gradient_start_height = value;
|
||||
this->settings_lock.unlock();
|
||||
on_configuration_changed();
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +303,9 @@ float TerrainMeshGenerator::get_color_gradient_start_height() const {
|
|||
}
|
||||
|
||||
void TerrainMeshGenerator::set_color_gradient_end_height(float value) {
|
||||
this->settings_lock.lock();
|
||||
this->color_gradient_end_height = value;
|
||||
this->settings_lock.unlock();
|
||||
on_configuration_changed();
|
||||
}
|
||||
|
||||
|
|
@ -199,10 +314,12 @@ float TerrainMeshGenerator::get_color_gradient_end_height() const {
|
|||
}
|
||||
|
||||
void TerrainMeshGenerator::set_chunk_count(int num) {
|
||||
this->settings_lock.lock();
|
||||
if (num != this->chunks_per_side) {
|
||||
this->chunks_per_side = num;
|
||||
rebuild_chunks();
|
||||
}
|
||||
this->settings_lock.unlock();
|
||||
}
|
||||
|
||||
int TerrainMeshGenerator::get_chunk_count() const {
|
||||
|
|
@ -210,9 +327,11 @@ int TerrainMeshGenerator::get_chunk_count() const {
|
|||
}
|
||||
|
||||
void TerrainMeshGenerator::set_chunk_scene(Ref<PackedScene> scene) {
|
||||
this->settings_lock.lock();
|
||||
if (!scene.is_valid() || scene->get_state()->get_node_type(0) == TerrainChunk::get_class_static()) {
|
||||
this->chunk_scene = scene;
|
||||
}
|
||||
this->settings_lock.unlock();
|
||||
}
|
||||
|
||||
Ref<PackedScene> TerrainMeshGenerator::get_chunk_scene() const {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,9 @@
|
|||
#include "core/object/object.h"
|
||||
#include "core/os/semaphore.h"
|
||||
#include "core/os/thread.h"
|
||||
#include "core/templates/hash_map.h"
|
||||
#include "core/variant/callable.h"
|
||||
#include "scene/main/node.h"
|
||||
#include "scene/resources/gradient.h"
|
||||
#include "scene/resources/material.h"
|
||||
#include "scene/resources/mesh.h"
|
||||
#include "scene/resources/surface_tool.h"
|
||||
#include "terrain_editor/terrain_chunk.h"
|
||||
|
|
@ -16,18 +14,29 @@
|
|||
|
||||
class TerrainMeshGenerator : public Node {
|
||||
GDCLASS(TerrainMeshGenerator, Node);
|
||||
struct TerrainMeshTask {
|
||||
Ref<ArrayMesh> mesh;
|
||||
Rect2 area;
|
||||
size_t side_points;
|
||||
Ref<SurfaceTool> out_surface;
|
||||
Callable callback;
|
||||
};
|
||||
static void _bind_methods();
|
||||
void ready();
|
||||
void enter_tree();
|
||||
void process();
|
||||
void on_configuration_changed();
|
||||
|
||||
protected:
|
||||
void _notification(int what);
|
||||
void rebuild_chunks();
|
||||
static void background_generation_thread(void *user);
|
||||
|
||||
public:
|
||||
Color color_at_height(float height) const;
|
||||
float evaluate_point(Vector2 at) const;
|
||||
void generate_grid(Rect2 area, Ref<ArrayMesh> mesh, size_t side_points);
|
||||
void generate_grid(Rect2 area, Ref<SurfaceTool> mesh, size_t side_points);
|
||||
|
||||
public:
|
||||
void push_task(Rect2 area, Ref<ArrayMesh> mesh, size_t side_points, Callable callback = Callable());
|
||||
void set_primitives(Array array);
|
||||
Array get_primitives() const;
|
||||
void set_vertex_color_gradient(Ref<Gradient> gradient);
|
||||
|
|
@ -42,14 +51,27 @@ public:
|
|||
Ref<PackedScene> get_chunk_scene() const;
|
||||
|
||||
private:
|
||||
Vector<Ref<TerrainPrimitive>> primitives{};
|
||||
Ref<SurfaceTool> surface{ memnew(SurfaceTool) };
|
||||
// main thread exclusive data
|
||||
Ref<PackedScene> chunk_scene{};
|
||||
Vector<TerrainChunk *> chunks{};
|
||||
|
||||
// worker thread owned data
|
||||
Mutex settings_lock{}; // needs to be locked to modify any of the below, worker thread locks while working
|
||||
Array primitives_buffer{}; // main thread buffer for primitives
|
||||
Vector<Ref<TerrainPrimitive>> primitives{}; // work thread buffer for primitives
|
||||
Ref<Gradient> vertex_color_gradient{};
|
||||
float color_gradient_start_height{ 0.f };
|
||||
float color_gradient_end_height{ 10.f };
|
||||
int chunks_per_side{ 10 };
|
||||
Ref<PackedScene> chunk_scene{};
|
||||
Vector<TerrainChunk *> chunks{};
|
||||
|
||||
Thread thread{};
|
||||
Mutex input_lock{};
|
||||
bool end_thread{ false };
|
||||
Vector<TerrainMeshTask> input_queue{};
|
||||
Semaphore work_signal{};
|
||||
|
||||
Mutex output_lock{};
|
||||
Vector<TerrainMeshTask> output_queue{};
|
||||
|
||||
private:
|
||||
Callable generation_changed{ callable_mp(this, &self_type::on_configuration_changed) };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
[gd_scene load_steps=12 format=3 uid="uid://xm383pc5pcnn"]
|
||||
[gd_scene load_steps=13 format=3 uid="uid://xm383pc5pcnn"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://cnux2fqne284i" path="res://objects/primitive_nodes/point_primitive_node.tscn" id="1_b1cmn"]
|
||||
[ext_resource type="PackedScene" uid="uid://wkqhvjnxs2mx" path="res://objects/terrain_chunk.tscn" id="1_pxqd5"]
|
||||
|
||||
[sub_resource type="PointPrimitive" id="PointPrimitive_pxqd5"]
|
||||
slope = -0.585
|
||||
|
|
@ -107,6 +108,7 @@ primitives = [SubResource("PointPrimitive_pxqd5"), SubResource("NoisePrimitive_b
|
|||
vertex_color_gradient = SubResource("Gradient_b1cmn")
|
||||
color_gradient_end_height = 200.0
|
||||
chunk_count = 5
|
||||
chunk_scene = ExtResource("1_pxqd5")
|
||||
point_primitive_object = ExtResource("1_b1cmn")
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
|
|
|
|||
Loading…
Reference in a new issue