diff --git a/modules/terrain/SCsub b/modules/terrain/SCsub new file mode 100644 index 00000000..2760ab7d --- /dev/null +++ b/modules/terrain/SCsub @@ -0,0 +1,3 @@ +Import('env') + +env.add_source_files(env.modules_sources, "*.cpp") diff --git a/modules/terrain/config.py b/modules/terrain/config.py new file mode 100644 index 00000000..58c88bf1 --- /dev/null +++ b/modules/terrain/config.py @@ -0,0 +1,5 @@ +def can_build(env, platform): + return True; + +def configure(env): + pass; diff --git a/modules/terrain/register_types.cpp b/modules/terrain/register_types.cpp new file mode 100644 index 00000000..3aaff6ba --- /dev/null +++ b/modules/terrain/register_types.cpp @@ -0,0 +1,22 @@ +#include "register_types.h" + +#include "core/object/class_db.h" +#include "terrain/terrain.h" +#include "terrain/terrain_chunk.h" +#include "terrain/terrain_modifier.h" + +void initialize_terrain_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } + ClassDB::register_class(); + ClassDB::register_abstract_class(); + ClassDB::register_class(); + ClassDB::register_class(); +} + +void uninitialize_terrain_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } +} diff --git a/modules/terrain/register_types.h b/modules/terrain/register_types.h new file mode 100644 index 00000000..85003560 --- /dev/null +++ b/modules/terrain/register_types.h @@ -0,0 +1,9 @@ +#ifndef TERRAIN_REGISTER_TYPES_H +#define TERRAIN_REGISTER_TYPES_H + +#include "modules/register_module_types.h" + +void initialize_terrain_module(ModuleInitializationLevel p_level); +void uninitialize_terrain_module(ModuleInitializationLevel p_level); + +#endif // !TERRAIN_REGISTER_TYPES_H diff --git a/modules/terrain/terrain.cpp b/modules/terrain/terrain.cpp new file mode 100644 index 00000000..3ace296b --- /dev/null +++ b/modules/terrain/terrain.cpp @@ -0,0 +1,80 @@ +#include "terrain.h" +#include "terrain/terrain_chunk.h" +#include "terrain/terrain_modifier.h" + +void Terrain::_bind_methods() {} + +void Terrain::ready() { + construct_chunk_grid(); + generate_meshes(); +} + +void Terrain::update_modifier_list() { + this->modifiers.clear(); + for (Variant var : get_children()) { + if (TerrainModifier * mod{ cast_to(var) }) { + this->modifiers.push_back(mod); + } + } +} + +void Terrain::construct_chunk_grid() { + size_t const chunks_per_side{ this->side_length / this->chunk_size }; + Vector3 const origin{ -(float)chunks_per_side / 2.f * (float)this->chunk_size, 0.f, -(float)chunks_per_side / 2.f * (float)this->chunk_size }; + for (size_t y{ 0 }; y < chunks_per_side; ++y) { + for (size_t x{ 0 }; x < chunks_per_side; ++x) { + TerrainChunkMesh *chunk{ memnew(TerrainChunkMesh) }; + chunk->set_size(this->chunk_size); + chunk->set_detail(this->detail); + chunk->set_terrain(this); + chunk->set_position(origin + Vector3{ (float)this->chunk_size * (float)x, 0.f, (float)this->chunk_size * (float)y }); + add_child(chunk); + chunk->set_owner(this); + this->meshes.push_back(chunk); + } + } +} + +void Terrain::generate_meshes() { + for (TerrainChunkMesh *mesh : this->meshes) { + mesh->update_mesh(); + } +} + +void Terrain::child_entered(Node *node) { + if (cast_to(node)) { + update_modifier_list(); + } +} + +void Terrain::child_exiting(Node *node) { + if (TerrainModifier * mod{ cast_to(node) }) { + this->modifiers.erase(mod); + } +} + +void Terrain::_notification(int what) { + switch (what) { + default: + return; + case NOTIFICATION_ENTER_TREE: + if (!is_ready()) { + connect("child_entered_tree", callable_mp(this, &self_type::child_entered)); + connect("child_exiting_tree", callable_mp(this, &self_type::child_exiting)); + connect("child_order_changed", callable_mp(this, &self_type::update_modifier_list)); + } + return; + return; + case NOTIFICATION_READY: + ready(); + return; + } +} + +float Terrain::height_at(Vector2 world_coordinate) { + float height{ 0 }; + for (TerrainModifier *mod : this->modifiers) { + height = mod->evaluate_at(world_coordinate, height); + } + return height; +} diff --git a/modules/terrain/terrain.h b/modules/terrain/terrain.h new file mode 100644 index 00000000..10ad437d --- /dev/null +++ b/modules/terrain/terrain.h @@ -0,0 +1,39 @@ +#pragma once + +#include "core/templates/vector.h" +#include "macros.h" +#include "scene/main/node.h" +class TerrainChunkMesh; +class TerrainModifier; + +class Terrain : public Node { + GDCLASS(Terrain, Node); + static void _bind_methods(); + void ready(); + void update_modifier_list(); + void construct_chunk_grid(); + void generate_meshes(); + + void child_entered(Node *node); + void child_exiting(Node *node); + +protected: + void _notification(int what); + +public: + float height_at(Vector2 world_coordinate); + +private: + size_t side_length{ 100 }; + size_t chunk_size{ 10 }; + size_t detail{ 1 }; + +public: + GET_SET_FNS(size_t, side_length); + GET_SET_FNS(size_t, chunk_size); + GET_SET_FNS(size_t, detail); + +private: + Vector meshes; + Vector modifiers; +}; diff --git a/modules/terrain/terrain_chunk.cpp b/modules/terrain/terrain_chunk.cpp new file mode 100644 index 00000000..5591bb79 --- /dev/null +++ b/modules/terrain/terrain_chunk.cpp @@ -0,0 +1,69 @@ +#include "terrain_chunk.h" +#include "core/variant/variant.h" +#include "scene/resources/surface_tool.h" +#include "terrain/terrain.h" + +void TerrainChunkMesh::_bind_methods() {} + +void TerrainChunkMesh::generate_vertices() { + ERR_FAIL_COND_EDMSG(this->terrain == nullptr, "TerrainChunkMesh::generate_vertices: no terrain assigned"); + ERR_FAIL_COND_EDMSG(this->size <= 0.f, "TerrainChunkMesh::generate_vertices: size <= 0"); + ERR_FAIL_COND_EDMSG(points_per_side() <= 0, "TerrainChunkMesh::generate_vertices: points per side <= 0"); + float const half_extent{ (float)this->size / 2.f }; + float const point_distance{ (float)this->size / ((float)points_per_side() - 1) }; + Vector3 origin{ this->get_global_position() - Vector3{ half_extent, 0, half_extent } }; + for (size_t x{ 0 }; x < points_per_side(); ++x) { + for (size_t y{ 0 }; y < points_per_side(); ++y) { + Vector2 const coordinate{ origin.x + point_distance * x, origin.z + point_distance * y }; + this->surface->set_uv({ (float)x / (float)points_per_side(), (float)y / (float)points_per_side() }); + this->surface->add_vertex({ coordinate.x - get_global_position().x, this->terrain->height_at(coordinate), coordinate.y - get_global_position().z }); + } + } +} + +void TerrainChunkMesh::generate_faces() { + LocalVector &verts{ this->surface->get_vertex_array() }; + ERR_FAIL_COND_EDMSG(verts.size() == 0, "TerrainChunkMesh::generate_faces: no vertices in surface, call generate_vertices first"); + size_t const faces_per_side{ points_per_side() - 1 }; + for (size_t x{ 0 }; x < faces_per_side; ++x) { + for (size_t y{ 0 }; y < faces_per_side; ++y) { + size_t const tl{ x + y * points_per_side() }; + float tl_br{ verts[tl].vertex.distance_to(verts[tl + points_per_side() + 1].vertex) }; + float tr_bl{ verts[tl + 1].vertex.distance_to(verts[tl + points_per_side()].vertex) }; + if (tl_br < tr_bl) { + surface->add_index(tl); + surface->add_index(tl + points_per_side() + 1); + surface->add_index(tl + 1); + + surface->add_index(tl); + surface->add_index(tl + points_per_side()); + surface->add_index(tl + points_per_side() + 1); + } else { + surface->add_index(tl + points_per_side()); + surface->add_index(tl + points_per_side() + 1); + surface->add_index(tl + 1); + + surface->add_index(tl + 1); + surface->add_index(tl); + surface->add_index(tl + points_per_side()); + } + } + } +} + +void TerrainChunkMesh::update_mesh() { + ERR_FAIL_COND_EDMSG(this->size <= 0.f, "TerrainChunkMesh::generate: size <= 0"); + ERR_FAIL_COND_EDMSG(points_per_side() <= 0, "TerrainChunkMesh::generate: points per side <= 0"); + this->set_mesh(memnew(ArrayMesh)); + this->surface->clear(); + this->surface->begin(Mesh::PRIMITIVE_TRIANGLES); + generate_vertices(); + generate_faces(); + this->surface->generate_normals(); + this->surface->generate_tangents(); + this->surface->commit(this->mesh); +} + +size_t TerrainChunkMesh::points_per_side() const { + return this->size * this->detail; +} diff --git a/modules/terrain/terrain_chunk.h b/modules/terrain/terrain_chunk.h new file mode 100644 index 00000000..c10e4270 --- /dev/null +++ b/modules/terrain/terrain_chunk.h @@ -0,0 +1,28 @@ +#pragma once + +#include "macros.h" +#include "scene/3d/mesh_instance_3d.h" +#include "scene/resources/surface_tool.h" +class Terrain; + +class TerrainChunkMesh : public MeshInstance3D { + GDCLASS(TerrainChunkMesh, MeshInstance3D); + static void _bind_methods(); + void generate_vertices(); + void generate_faces(); + +public: + void update_mesh(); + size_t points_per_side() const; + +private: + Ref surface{ memnew(SurfaceTool) }; + Terrain *terrain{}; + size_t detail{ 1 }; + size_t size{ 1 }; + +public: + GET_SET_FNS(Terrain *, terrain); + GET_SET_FNS(size_t, detail); + GET_SET_FNS(size_t, size); +}; diff --git a/modules/terrain/terrain_modifier.cpp b/modules/terrain/terrain_modifier.cpp new file mode 100644 index 00000000..587e0147 --- /dev/null +++ b/modules/terrain/terrain_modifier.cpp @@ -0,0 +1,82 @@ +#include "terrain_modifier.h" +#include "core/variant/variant.h" +#include "macros.h" +#include + +void TerrainModifier::_bind_methods() { + BIND_HPROPERTY(Variant::INT, blend_mode, PROPERTY_HINT_ENUM, BlendMode_hint()); + BIND_PROPERTY(Variant::FLOAT, blend_distance); +} + +float TerrainModifier::blend(float under, float over, float weight) { + if (weight <= 0.0) { + return under; + } + over = Math::lerp(under, over, weight); + float const difference{ under - over }; + float const distance{ Math::abs(difference) }; + // .25 because we need half of each half of the blend range to be used + float const center_distance{ + this->blend_distance == 0.f + ? 0.f + : this->blend_distance * 0.25f - distance / this->blend_distance + }; + if (center_distance < 0.f) { + if (this->blend_mode == Override) { + return over; + } else if (this->blend_mode == Add) { + return under > over ? under : over; + } else { + return under > over ? over : under; + } + } + float const smooth_center_distance{ center_distance * center_distance }; + if (this->blend_mode == Override) { + return over + smooth_center_distance; + } else { + return (this->blend_mode == Add + ? (under > over ? under : over) + smooth_center_distance + : (under > over ? over : under) - smooth_center_distance); + } +} + +float TerrainModifier::evaluate_at(Vector2 world_coordinate, float before) { + Vector3 const global_position{ get_global_position() }; + world_coordinate -= { global_position.x, global_position.z }; + return blend(before, 0.0, 1.0); +} + +void TerrainModifierDistance::_bind_methods() { + BIND_HPROPERTY(Variant::OBJECT, distance_weight_curve, PROPERTY_HINT_RESOURCE_TYPE, "Curve"); + BIND_HPROPERTY(Variant::OBJECT, distance_height_curve, PROPERTY_HINT_RESOURCE_TYPE, "Curve"); +} + +float TerrainModifierDistance::distance_at(Vector2 const &world_coordinate) { + Vector3 const global_position{ get_global_position() }; + return world_coordinate.distance_to({ global_position.x, global_position.z }); +} + +float TerrainModifierDistance::evaluate_at(Vector2 world_coordinate, float before) { + if (this->distance_weight_curve.is_null() || this->distance_height_curve.is_null()) { + return before; + } + float const distance{ distance_at(world_coordinate) }; + float const height_offset{ + std::clamp(distance, this->distance_height_curve->get_min_domain(), this->distance_height_curve->get_max_domain()) + }; + float const weight_offset{ + std::clamp(distance, this->distance_weight_curve->get_min_domain(), this->distance_weight_curve->get_max_domain()) + }; + return blend(before, this->distance_height_curve->sample_baked(height_offset) + get_global_position().y, this->distance_weight_curve->sample_baked(weight_offset)); +} + +PackedStringArray TerrainModifierDistance::get_configuration_warnings() const { + PackedStringArray warnings{ super_type::get_configuration_warnings() }; + if (this->distance_weight_curve.is_null()) { + warnings.push_back("distance_weight_curve is invalid, add a valid distance_weight_curve"); + } + if (this->distance_height_curve.is_null()) { + warnings.push_back("distance_height_curve is invalid, add a valid distance_height_curve"); + } + return warnings; +} diff --git a/modules/terrain/terrain_modifier.h b/modules/terrain/terrain_modifier.h new file mode 100644 index 00000000..5811bf38 --- /dev/null +++ b/modules/terrain/terrain_modifier.h @@ -0,0 +1,51 @@ +#pragma once + +#include "core/object/object.h" +#include "core/variant/variant.h" +#include "macros.h" +#include "scene/3d/node_3d.h" +#include "scene/resources/curve.h" + +class TerrainModifier : public Node3D { + GDCLASS(TerrainModifier, Node3D); + static void _bind_methods(); + +public: + GDENUM(BlendMode, Add, Subtract, Override); + +protected: + float blend(float under, float over, float weight); + +public: + virtual float evaluate_at(Vector2 world_coordinate, float before); + +private: + float blend_distance{ 1.0 }; + BlendMode blend_mode{ Add }; + +public: + GET_SET_FNS(float, blend_distance); + GET_SET_FNS(BlendMode, blend_mode); +}; + +MAKE_TYPE_INFO(TerrainModifier::BlendMode, Variant::INT); + +class TerrainModifierDistance : public TerrainModifier { + GDCLASS(TerrainModifierDistance, TerrainModifier); + static void _bind_methods(); + +protected: + virtual float distance_at(Vector2 const &world_coordinate); + +public: + float evaluate_at(Vector2 world_coordinate, float before) override; + PackedStringArray get_configuration_warnings() const override; + +private: + Ref distance_weight_curve{}; + Ref distance_height_curve{}; + +public: + GET_SET_FNS_EX(Ref, distance_weight_curve, update_configuration_warnings()); + GET_SET_FNS_EX(Ref, distance_height_curve, update_configuration_warnings()); +}; diff --git a/project/scenes/terrain_test.tscn b/project/scenes/terrain_test.tscn new file mode 100644 index 00000000..7d549704 --- /dev/null +++ b/project/scenes/terrain_test.tscn @@ -0,0 +1,35 @@ +[gd_scene format=3 uid="uid://d2w73ie2k01xg"] + +[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_kbmr5"] +sky_horizon_color = Color(0.6590071, 0.7017287, 0.7452071, 1) +ground_bottom_color = Color(0.110199995, 0.21208663, 0.29, 1) +ground_horizon_color = Color(0.6590071, 0.7017287, 0.7452071, 1) + +[sub_resource type="Sky" id="Sky_w3uoq"] +sky_material = SubResource("ProceduralSkyMaterial_kbmr5") + +[sub_resource type="Environment" id="Environment_o3i6r"] +background_mode = 2 +sky = SubResource("Sky_w3uoq") + +[sub_resource type="Curve" id="Curve_kbmr5"] +_limits = [0.0, 1.0, 0.0, 10.0] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(5.1981277, 1), 0.0, 0.0, 0, 0, Vector2(10, 0), 0.0, 0.0, 0, 0] +point_count = 3 + +[sub_resource type="Curve" id="Curve_w3uoq"] +_limits = [-10.0, 0.0, 0.0, 10.0] +_data = [Vector2(0, -0.02282238), 0.0, 0.0, 0, 0, Vector2(10, -10), -0.10877486, 0.0, 0, 0] +point_count = 2 + +[node name="Node3D" type="Node3D" unique_id=289500437] + +[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=212607290] +environment = SubResource("Environment_o3i6r") + +[node name="Terrain" type="Terrain" parent="." unique_id=1169843565] + +[node name="TerrainModifierDistance" type="TerrainModifierDistance" parent="Terrain" unique_id=1885116624] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 13.434517, 8.531313, 11.648043) +distance_weight_curve = SubResource("Curve_kbmr5") +distance_height_curve = SubResource("Curve_w3uoq")