feat: started work on terrain editor tools

This commit is contained in:
Sara Gerretsen 2026-02-22 22:27:23 +01:00
parent a61b52806a
commit ac139f01b6
11 changed files with 423 additions and 0 deletions

3
modules/terrain/SCsub Normal file
View file

@ -0,0 +1,3 @@
Import('env')
env.add_source_files(env.modules_sources, "*.cpp")

View file

@ -0,0 +1,5 @@
def can_build(env, platform):
return True;
def configure(env):
pass;

View file

@ -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<Terrain>();
ClassDB::register_abstract_class<TerrainModifier>();
ClassDB::register_class<TerrainModifierDistance>();
ClassDB::register_class<TerrainChunkMesh>();
}
void uninitialize_terrain_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
}

View file

@ -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

View file

@ -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<TerrainModifier>(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<TerrainModifier>(node)) {
update_modifier_list();
}
}
void Terrain::child_exiting(Node *node) {
if (TerrainModifier * mod{ cast_to<TerrainModifier>(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;
}

39
modules/terrain/terrain.h Normal file
View file

@ -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<TerrainChunkMesh *> meshes;
Vector<TerrainModifier *> modifiers;
};

View file

@ -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<SurfaceTool::Vertex> &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;
}

View file

@ -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<SurfaceTool> 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);
};

View file

@ -0,0 +1,82 @@
#include "terrain_modifier.h"
#include "core/variant/variant.h"
#include "macros.h"
#include <algorithm>
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;
}

View file

@ -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<Curve> distance_weight_curve{};
Ref<Curve> distance_height_curve{};
public:
GET_SET_FNS_EX(Ref<Curve>, distance_weight_curve, update_configuration_warnings());
GET_SET_FNS_EX(Ref<Curve>, distance_height_curve, update_configuration_warnings());
};