feat: implemented terrain editor plumbing

This commit is contained in:
Sara Gerretsen 2025-11-14 18:53:16 +01:00
parent 76dd9fbc62
commit 81ac67c623
10 changed files with 647 additions and 0 deletions

2
.dir-locals.el Normal file
View file

@ -0,0 +1,2 @@
((cpp-mode . ((mode . clang-format-on-save))))
((c-mode . ((mode . c++))))

View file

@ -17,4 +17,12 @@
ADD_PROPERTY(PropertyInfo(m_type, #m_property), "set_" #m_property, \
"get_" #m_property)
#define __VA_ARGS__STRING(...) String(#__VA_ARGS__)
#define GDENUM(M_Name, ...) \
enum M_Name { __VA_ARGS__ }; \
static String M_Name##_hint() { \
return __VA_ARGS__STRING(__VA_ARGS__); \
}
#endif // !GODOT_EXTRA_MACROS_H

View file

@ -1,11 +1,19 @@
#include "register_types.h"
#include "core/object/class_db.h"
#include "terrain_editor/terrain_mesh_generator.h"
#include "terrain_editor/terrain_primitive.h"
void initialize_terrain_editor_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
ClassDB::register_class<TerrainMeshGenerator>();
ClassDB::register_abstract_class<TerrainPrimitive>();
ClassDB::register_class<PlanePrimitive>();
ClassDB::register_class<PointPrimitive>();
ClassDB::register_class<NoisePrimitive>();
ClassDB::register_class<ExpressionPrimitive>();
}
void uninitialize_terrain_editor_module(ModuleInitializationLevel p_level) {

View file

@ -0,0 +1,150 @@
#include "terrain_mesh_generator.h"
#include "core/io/resource_saver.h"
#include "core/math/math_funcs.h"
#include "core/math/rect2.h"
#include "core/object/class_db.h"
#include "core/templates/local_vector.h"
#include "scene/resources/surface_tool.h"
#include "terrain_editor/macros.h"
#include "terrain_editor/terrain_primitive.h"
#include <limits>
String const TerrainMeshGenerator::sig_primitives_changed{ "primitives_changed" };
void TerrainMeshGenerator::_bind_methods() {
BIND_HPROPERTY(Variant::ARRAY, primitives, PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:TerrainPrimitive", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE));
BIND_HPROPERTY(Variant::OBJECT, vertex_color_gradient, PROPERTY_HINT_RESOURCE_TYPE, "Gradient");
BIND_PROPERTY(Variant::FLOAT, color_gradient_start_height);
BIND_PROPERTY(Variant::FLOAT, color_gradient_end_height);
ADD_SIGNAL(MethodInfo(sig_primitives_changed));
ClassDB::bind_method(D_METHOD("generate_grid", "area", "out_mesh", "side_points"), &self_type::generate_grid);
}
void TerrainMeshGenerator::on_configuration_changed() {
emit_signal(sig_primitives_changed);
}
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()
? this->vertex_color_gradient->get_color_at_offset(mapped)
: Color{ mapped, mapped, mapped, 1.f };
}
float TerrainMeshGenerator::evaluate_point(Vector2 at) const {
float height{ -std::numeric_limits<float>::infinity() };
for (Ref<TerrainPrimitive> primitive : this->primitives) {
if (primitive.is_valid()) {
primitive->evaluate(at, height);
}
}
return height;
}
void face_from(Ref<SurfaceTool> surface, size_t tl, size_t side_points) {
LocalVector<SurfaceTool::Vertex> &vertices{ surface->get_vertex_array() };
float d1{ vertices[tl].vertex.distance_to(vertices[tl + side_points + 1].vertex) };
float d2{ vertices[tl + 1].vertex.distance_to(vertices[tl + side_points].vertex) };
if (d1 < d2) {
surface->add_index(tl);
surface->add_index(tl + side_points + 1);
surface->add_index(tl + 1);
surface->add_index(tl);
surface->add_index(tl + side_points);
surface->add_index(tl + side_points + 1);
} else {
surface->add_index(tl + side_points);
surface->add_index(tl + side_points + 1);
surface->add_index(tl + 1);
surface->add_index(tl + 1);
surface->add_index(tl);
surface->add_index(tl + side_points);
}
}
void TerrainMeshGenerator::generate_grid(Rect2 area, Ref<ArrayMesh> mesh, size_t side_points) {
mesh->clear_surfaces();
surface->clear();
Vector2 point_distance{ area.size / (float)side_points };
Vector2 at{ area.position };
surface->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, height, at.y });
at.y += point_distance.y;
}
at.y = area.position.y;
at.x += point_distance.x;
}
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);
}
}
surface->generate_normals();
surface->generate_tangents();
surface->commit(mesh);
}
void TerrainMeshGenerator::set_primitives(Array primitives) {
for (Ref<TerrainPrimitive> primitive : this->primitives) {
if (primitive.is_valid()) {
primitive->disconnect_changed(this->generation_changed);
}
}
this->primitives.clear();
for (Variant var : primitives) {
Ref<TerrainPrimitive> primitive{ var };
this->primitives.push_back(primitive);
if (primitive.is_valid()) {
primitive->connect_changed(this->generation_changed);
}
}
on_configuration_changed();
}
Array TerrainMeshGenerator::get_primitives() const {
Array a;
for (Ref<TerrainPrimitive> primitive : this->primitives) {
a.push_back(primitive);
}
return a;
}
void TerrainMeshGenerator::set_vertex_color_gradient(Ref<Gradient> gradient) {
if (this->vertex_color_gradient.is_valid()) {
this->vertex_color_gradient->disconnect_changed(this->generation_changed);
}
this->vertex_color_gradient = gradient;
if (gradient.is_valid()) {
this->vertex_color_gradient->connect_changed(this->generation_changed);
}
on_configuration_changed();
}
Ref<Gradient> TerrainMeshGenerator::get_vertex_color_gradient() const {
return this->vertex_color_gradient;
}
void TerrainMeshGenerator::set_color_gradient_start_height(float value) {
this->color_gradient_start_height = value;
on_configuration_changed();
}
float TerrainMeshGenerator::get_color_gradient_start_height() const {
return this->color_gradient_start_height;
}
void TerrainMeshGenerator::set_color_gradient_end_height(float value) {
this->color_gradient_end_height = value;
on_configuration_changed();
}
float TerrainMeshGenerator::get_color_gradient_end_height() const {
return this->color_gradient_end_height;
}

View file

@ -0,0 +1,42 @@
#pragma once
#include "core/math/color.h"
#include "core/object/object.h"
#include "core/variant/callable.h"
#include "scene/main/node.h"
#include "scene/resources/gradient.h"
#include "scene/resources/mesh.h"
#include "scene/resources/surface_tool.h"
#include "terrain_editor/terrain_primitive.h"
class TerrainMeshGenerator : public Node {
GDCLASS(TerrainMeshGenerator, Node);
static void _bind_methods();
void on_configuration_changed();
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 set_primitives(Array array);
Array get_primitives() const;
void set_vertex_color_gradient(Ref<Gradient> gradient);
Ref<Gradient> get_vertex_color_gradient() const;
void set_color_gradient_start_height(float value);
float get_color_gradient_start_height() const;
void set_color_gradient_end_height(float value);
float get_color_gradient_end_height() const;
private:
Vector<Ref<TerrainPrimitive>> primitives{};
Ref<SurfaceTool> surface{ memnew(SurfaceTool) };
Ref<Gradient> vertex_color_gradient{};
float color_gradient_start_height{ 0.f };
float color_gradient_end_height{ 10.f };
private:
Callable generation_changed{ callable_mp(this, &self_type::on_configuration_changed) };
public:
static String const sig_primitives_changed;
};

View file

@ -0,0 +1,188 @@
#include "terrain_primitive.h"
#include "core/error/error_list.h"
#include "core/math/expression.h"
#include "core/math/math_funcs.h"
#include "terrain_editor/macros.h"
void TerrainPrimitive::_bind_methods() {
BIND_HPROPERTY(Variant::INT, blend_mode, PROPERTY_HINT_ENUM, BlendMode_hint());
BIND_PROPERTY(Variant::FLOAT, blend_range);
}
// by default does not modify height
void TerrainPrimitive::evaluate(Vector2, float &) const {}
float TerrainPrimitive::blend(float under, float over) const {
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_range == 0.f ? 0.f : this->blend_range * 0.25f - distance / this->blend_range };
if (center_distance < 0.f) {
if (this->blend_mode == Both) {
return over;
} else if (this->blend_mode == Peak) {
return under > over ? under : over;
} else {
return under > over ? over : under;
}
}
float const smooth_center_distance{ center_distance * center_distance };
if (this->blend_mode == Both) {
return over + smooth_center_distance;
} else {
return (this->blend_mode == Peak
? (under >= over ? under : over) + smooth_center_distance
: (under >= over ? over : under) - smooth_center_distance);
}
}
void TerrainPrimitive::set_blend_mode(BlendMode mode) {
this->blend_mode = mode;
emit_changed();
}
TerrainPrimitive::BlendMode TerrainPrimitive::get_blend_mode() const {
return this->blend_mode;
}
void TerrainPrimitive::set_blend_range(float value) {
this->blend_range = value;
emit_changed();
}
float TerrainPrimitive::get_blend_range() const {
return this->blend_range;
}
void PlanePrimitive::_bind_methods() {
BIND_PROPERTY(Variant::FLOAT, baseline);
}
void PlanePrimitive::evaluate(Vector2, float &io_height) const {
io_height = blend(io_height, this->baseline);
}
void PlanePrimitive::set_baseline(float value) {
this->baseline = value;
emit_changed();
}
float PlanePrimitive::get_baseline() const {
return this->baseline;
}
void PointPrimitive::_bind_methods() {
BIND_PROPERTY(Variant::VECTOR2, center);
BIND_PROPERTY(Variant::FLOAT, slope);
BIND_PROPERTY(Variant::FLOAT, height);
}
void PointPrimitive::evaluate(Vector2 at, float &io_height) const {
float distance{ at.distance_to(this->center) };
io_height = blend(io_height, this->height + distance * this->slope);
}
void PointPrimitive::set_center(Vector2 center) {
this->center = center;
emit_changed();
}
Vector2 PointPrimitive::get_center() const {
return this->center;
}
void PointPrimitive::set_slope(float radius) {
this->slope = radius;
emit_changed();
}
float PointPrimitive::get_slope() const {
return this->slope;
}
void PointPrimitive::set_height(float height) {
this->height = height;
emit_changed();
}
float PointPrimitive::get_height() const {
return this->height;
}
void NoisePrimitive::_bind_methods() {
BIND_HPROPERTY(Variant::OBJECT, noise, PROPERTY_HINT_RESOURCE_TYPE, "Noise");
BIND_PROPERTY(Variant::FLOAT, noise_scale);
BIND_PROPERTY(Variant::FLOAT, noise_amplitude);
}
void NoisePrimitive::evaluate(Vector2 at, float &io_height) const {
if (this->noise.is_valid()) {
float noise_sample{ this->noise->get_noise_2dv(at / this->noise_scale) };
switch (this->get_blend_mode()) {
case Peak:
noise_sample = Math::remap(noise_sample, -1.f, 1.f, 0.f, this->noise_amplitude);
break;
case Valley:
noise_sample = Math::remap(noise_sample, -1.f, 1.f, -this->noise_amplitude, 0.f);
break;
case Both:
noise_sample *= this->noise_amplitude;
}
io_height = blend(io_height, io_height + noise_sample);
}
}
void NoisePrimitive::set_noise(Ref<Noise> noise) {
this->noise = noise;
emit_changed();
}
Ref<Noise> NoisePrimitive::get_noise() const {
return this->noise;
}
void NoisePrimitive::set_noise_scale(float value) {
this->noise_scale = value;
emit_changed();
}
float NoisePrimitive::get_noise_scale() const {
return this->noise_scale;
}
void NoisePrimitive::set_noise_amplitude(float value) {
this->noise_amplitude = value;
emit_changed();
}
float NoisePrimitive::get_noise_amplitude() const {
return this->noise_amplitude;
}
void ExpressionPrimitive::_bind_methods() {
BIND_HPROPERTY(Variant::STRING, expression, PROPERTY_HINT_EXPRESSION);
}
void ExpressionPrimitive::evaluate(Vector2 at, float &io_height) const {
if (!this->valid) {
return;
}
Variant result{ this->expression->execute({ io_height, at }, nullptr, false, true) };
if (!this->expression->has_execute_failed()) {
io_height = blend(io_height, float(result.get(0)));
}
}
void ExpressionPrimitive::set_expression(String expression) {
this->expression_string = expression;
this->expression.unref();
this->expression = memnew(Expression);
Error error{ this->expression->parse(this->expression_string, { "height", "at" }) };
if ((this->valid = error == OK)) {
emit_changed();
}
}
String ExpressionPrimitive::get_expression() const {
return this->expression_string;
}

View file

@ -0,0 +1,102 @@
#pragma once
#include "core/io/resource.h"
#include "core/math/expression.h"
#include "core/object/object.h"
#include "modules/noise/noise.h"
#include "terrain_editor/macros.h"
class TerrainPrimitive : public Resource {
GDCLASS(TerrainPrimitive, Resource);
static void _bind_methods();
public:
GDENUM(BlendMode,
Peak,
Valley,
Both)
protected:
float blend(float under, float over) const;
public:
// evaluate the height of this primitive at point, returns the weight of the effect, out_height will be set to the closest point on the primitive
virtual void evaluate(Vector2 at, float &io_height) const;
void set_blend_mode(BlendMode mode);
BlendMode get_blend_mode() const;
void set_blend_range(float blend_range);
float get_blend_range() const;
private:
float blend_range{ 4.f };
BlendMode blend_mode{ Peak };
};
MAKE_TYPE_INFO(TerrainPrimitive::BlendMode, Variant::INT);
class PlanePrimitive : public TerrainPrimitive {
GDCLASS(PlanePrimitive, TerrainPrimitive);
static void _bind_methods();
public:
void evaluate(Vector2 at, float &io_height) const override;
void set_baseline(float value);
float get_baseline() const;
private:
float baseline{ 1.f };
};
class PointPrimitive : public TerrainPrimitive {
GDCLASS(PointPrimitive, TerrainPrimitive);
static void _bind_methods();
public:
void evaluate(Vector2 at, float &io_height) const override;
void set_center(Vector2 center);
Vector2 get_center() const;
void set_slope(float radius);
float get_slope() const;
void set_height(float height);
float get_height() const;
private:
Vector2 center{ 0.f, 0.f };
float slope{ -1.f };
float height{ 10.f };
};
class NoisePrimitive : public TerrainPrimitive {
GDCLASS(NoisePrimitive, TerrainPrimitive);
static void _bind_methods();
public:
void evaluate(Vector2 at, float &io_height) const override;
void set_noise(Ref<Noise> noise);
Ref<Noise> get_noise() const;
void set_noise_scale(float pixels_per_meter);
float get_noise_scale() const;
void set_noise_amplitude(float height);
float get_noise_amplitude() const;
private:
Ref<Noise> noise{};
float noise_scale{ 1.f };
float noise_amplitude{ 1.f };
};
class ExpressionPrimitive : public TerrainPrimitive {
GDCLASS(ExpressionPrimitive, TerrainPrimitive);
static void _bind_methods();
public:
void evaluate(Vector2 at, float &io_height) const override;
void set_expression(String expression);
String get_expression() const;
private:
Ref<Expression> expression{ memnew(Expression) };
String expression_string{ "height" };
bool valid{ false };
};

137
project/scenes/editor.tscn Normal file

File diff suppressed because one or more lines are too long

9
project/terrain.gdshader Normal file
View file

@ -0,0 +1,9 @@
shader_type spatial;
void vertex() {
}
//void fragment() {
// Called for every pixel the material is visible on.
//}

View file

@ -0,0 +1 @@
uid://dsbxpdveoilep