OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / src / dc_terrain_generator.cpp

348 lines · cpp ref:
#include "dc_terrain_generator.h"

#include <functional>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/classes/project_settings.hpp>
#include <godot_cpp/classes/resource_loader.hpp>
#include <godot_cpp/classes/mesh.hpp>
#include <godot_cpp/classes/array_mesh.hpp>
#include <godot_cpp/classes/packed_scene.hpp>
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/classes/mesh_instance3d.hpp>

namespace godot {

// ===========================================================================
// DCTerrainGenerator (base)
// ===========================================================================

void DCTerrainGenerator::_bind_methods() {
    // No properties — abstract base.
}

// ===========================================================================
// DCTerrainGeneratorNoise
// ===========================================================================

DCTerrainGeneratorNoise::DCTerrainGeneratorNoise() {
    _apply_config();
}

void DCTerrainGeneratorNoise::_apply_config() {
    if (!dirty_) return;
    impl_.density.configure(config_);
    dirty_ = false;
}

// Macro to reduce boilerplate for simple scalar properties.
// Each setter marks dirty and emits changed().
#define NOISE_PROP_FLOAT(field)                                              \
    void DCTerrainGeneratorNoise::set_##field(float p_val) {                 \
        if (config_.field == p_val) return;                                   \
        config_.field = p_val;                                                \
        dirty_ = true;                                                        \
        emit_changed();                                                       \
    }                                                                         \
    float DCTerrainGeneratorNoise::get_##field() const { return config_.field; }

#define NOISE_PROP_INT(field)                                                \
    void DCTerrainGeneratorNoise::set_##field(int p_val) {                   \
        if (config_.field == p_val) return;                                    \
        config_.field = p_val;                                                 \
        dirty_ = true;                                                         \
        emit_changed();                                                        \
    }                                                                          \
    int DCTerrainGeneratorNoise::get_##field() const { return config_.field; }

#define NOISE_PROP_BOOL(field)                                               \
    void DCTerrainGeneratorNoise::set_##field(bool p_val) {                  \
        if (config_.field == p_val) return;                                    \
        config_.field = p_val;                                                 \
        dirty_ = true;                                                         \
        emit_changed();                                                        \
    }                                                                          \
    bool DCTerrainGeneratorNoise::get_##field() const { return config_.field; }

NOISE_PROP_INT(seed)

NOISE_PROP_FLOAT(primary_frequency)
NOISE_PROP_FLOAT(primary_strength)
NOISE_PROP_FLOAT(tunnel_frequency)
NOISE_PROP_FLOAT(tunnel_strength)
NOISE_PROP_FLOAT(tunnel_radius)
NOISE_PROP_FLOAT(ridge_frequency)
NOISE_PROP_FLOAT(ridge_strength)
NOISE_PROP_FLOAT(warp_frequency)
NOISE_PROP_FLOAT(warp_strength)
NOISE_PROP_FLOAT(detail_frequency)
NOISE_PROP_FLOAT(detail_strength)
NOISE_PROP_FLOAT(ground_bias)
NOISE_PROP_FLOAT(solid_floor_depth)
NOISE_PROP_FLOAT(sky_height)
NOISE_PROP_FLOAT(flat_center_radius)
NOISE_PROP_FLOAT(terrain_size)
NOISE_PROP_FLOAT(planet_radius)
NOISE_PROP_FLOAT(floor_strength)
NOISE_PROP_FLOAT(floor_height_min)
NOISE_PROP_FLOAT(floor_height_max)
NOISE_PROP_FLOAT(room_height)

NOISE_PROP_BOOL(use_noise)
NOISE_PROP_BOOL(spherical)

NOISE_PROP_INT(max_noise_octaves)
NOISE_PROP_INT(max_ridge_octaves)
NOISE_PROP_INT(voronoi_cell_count)

#undef NOISE_PROP_FLOAT
#undef NOISE_PROP_INT
#undef NOISE_PROP_BOOL

void DCTerrainGeneratorNoise::_bind_methods() {
    // -- Macro for binding float properties --
#define BIND_FLOAT(prop, hint_range)                                                       \
    ClassDB::bind_method(D_METHOD("set_" #prop, "value"), &DCTerrainGeneratorNoise::set_##prop); \
    ClassDB::bind_method(D_METHOD("get_" #prop), &DCTerrainGeneratorNoise::get_##prop);          \
    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, #prop, PROPERTY_HINT_RANGE, hint_range),           \
                 "set_" #prop, "get_" #prop);

#define BIND_INT(prop, hint_range)                                                         \
    ClassDB::bind_method(D_METHOD("set_" #prop, "value"), &DCTerrainGeneratorNoise::set_##prop); \
    ClassDB::bind_method(D_METHOD("get_" #prop), &DCTerrainGeneratorNoise::get_##prop);          \
    ADD_PROPERTY(PropertyInfo(Variant::INT, #prop, PROPERTY_HINT_RANGE, hint_range),             \
                 "set_" #prop, "get_" #prop);

#define BIND_BOOL(prop)                                                                    \
    ClassDB::bind_method(D_METHOD("set_" #prop, "value"), &DCTerrainGeneratorNoise::set_##prop); \
    ClassDB::bind_method(D_METHOD("get_" #prop), &DCTerrainGeneratorNoise::get_##prop);          \
    ADD_PROPERTY(PropertyInfo(Variant::BOOL, #prop),                                             \
                 "set_" #prop, "get_" #prop);

    // -- Noise group --
    ADD_GROUP("Noise", "");
    BIND_BOOL(use_noise)
    BIND_INT(seed, "")

    BIND_FLOAT(primary_frequency, "0.001,1.0,0.001")
    BIND_FLOAT(primary_strength, "0.0,5.0,0.01")

    BIND_FLOAT(tunnel_frequency, "0.001,1.0,0.001")
    BIND_FLOAT(tunnel_strength, "0.0,5.0,0.01")
    BIND_FLOAT(tunnel_radius, "0.0,2.0,0.01")

    BIND_FLOAT(ridge_frequency, "0.001,1.0,0.001")
    BIND_FLOAT(ridge_strength, "0.0,5.0,0.01")

    BIND_FLOAT(warp_frequency, "0.001,1.0,0.001")
    BIND_FLOAT(warp_strength, "0.0,30.0,0.1")

    BIND_FLOAT(detail_frequency, "0.001,1.0,0.001")
    BIND_FLOAT(detail_strength, "0.0,2.0,0.01")

    // -- Terrain Shape group --
    ADD_GROUP("Terrain Shape", "");
    BIND_FLOAT(ground_bias, "-1.0,1.0,0.001")
    BIND_FLOAT(solid_floor_depth, "0.0,100.0,0.5")
    BIND_FLOAT(sky_height, "0.0,100.0,0.5")
    BIND_FLOAT(flat_center_radius, "0.0,50.0,0.5")
    BIND_FLOAT(terrain_size, "10.0,500.0,1.0")

    // -- Spherical group --
    ADD_GROUP("Spherical", "");
    BIND_BOOL(spherical)
    BIND_FLOAT(planet_radius, "10.0,1000000.0,100.0")
    BIND_INT(max_noise_octaves, "0,20,1")
    BIND_INT(max_ridge_octaves, "0,20,1")

    // -- Voronoi group --
    ADD_GROUP("Voronoi Floors", "");
    BIND_INT(voronoi_cell_count, "0,20,1")
    BIND_FLOAT(floor_strength, "0.0,3.0,0.01")
    BIND_FLOAT(floor_height_min, "-50.0,50.0,0.5")
    BIND_FLOAT(floor_height_max, "-50.0,50.0,0.5")
    BIND_FLOAT(room_height, "1.0,30.0,0.5")

#undef BIND_FLOAT
#undef BIND_INT
#undef BIND_BOOL
}

// ===========================================================================
// DCTerrainGeneratorMeshSDF
// ===========================================================================

void DCTerrainGeneratorMeshSDF::set_mesh_path(const String& p_path) {
    if (mesh_path_ == p_path) return;
    mesh_path_ = p_path;
    reload();
    emit_changed();
}

String DCTerrainGeneratorMeshSDF::get_mesh_path() const { return mesh_path_; }

void DCTerrainGeneratorMeshSDF::reload() {
    if (mesh_path_.is_empty()) return;

    // For OBJ files, use direct C++ loader
    if (mesh_path_.ends_with(".obj")) {
        String resolved = mesh_path_;
        if (resolved.begins_with("res://") || resolved.begins_with("user://")) {
            auto* ps = ProjectSettings::get_singleton();
            if (ps) {
                resolved = ps->globalize_path(resolved);
            } else {
                UtilityFunctions::printerr("DCTerrainGeneratorMeshSDF: ProjectSettings not available");
                return;
            }
        }
        std::string path = resolved.utf8().get_data();
        if (!impl_.load_obj(path)) {
            UtilityFunctions::printerr("DCTerrainGeneratorMeshSDF: failed to load OBJ ", mesh_path_);
        } else {
            UtilityFunctions::print("DCTerrainGeneratorMeshSDF: loaded OBJ (", impl_.triangle_count(), " tris)");
        }
        return;
    }

    // For GLB/GLTF and other Godot-supported formats, load via ResourceLoader
    auto* loader = ResourceLoader::get_singleton();
    if (!loader) {
        UtilityFunctions::printerr("DCTerrainGeneratorMeshSDF: ResourceLoader not available");
        return;
    }

    Ref<PackedScene> scene = loader->load(mesh_path_);
    if (!scene.is_valid()) {
        UtilityFunctions::printerr("DCTerrainGeneratorMeshSDF: failed to load ", mesh_path_);
        return;
    }

    // Instantiate scene to find the first MeshInstance3D
    Node* root = scene->instantiate();
    if (!root) {
        UtilityFunctions::printerr("DCTerrainGeneratorMeshSDF: failed to instantiate ", mesh_path_);
        return;
    }

    // Find first MeshInstance3D (depth-first)
    Ref<Mesh> mesh;
    std::function<void(Node*)> find_mesh = [&](Node* node) {
        if (mesh.is_valid()) return;
        auto* mi = Object::cast_to<MeshInstance3D>(node);
        if (mi && mi->get_mesh().is_valid()) {
            mesh = mi->get_mesh();
            return;
        }
        for (int i = 0; i < node->get_child_count(); i++) {
            find_mesh(node->get_child(i));
        }
    };
    find_mesh(root);
    memdelete(root);

    if (!mesh.is_valid() || mesh->get_surface_count() == 0) {
        UtilityFunctions::printerr("DCTerrainGeneratorMeshSDF: no mesh found in ", mesh_path_);
        return;
    }

    // Extract vertex and index arrays from first surface
    Array arrays = mesh->surface_get_arrays(0);
    PackedVector3Array verts = arrays[Mesh::ARRAY_VERTEX];
    PackedInt32Array indices = arrays[Mesh::ARRAY_INDEX];

    if (verts.size() < 3 || indices.size() < 3) {
        UtilityFunctions::printerr("DCTerrainGeneratorMeshSDF: mesh has no geometry in ", mesh_path_);
        return;
    }

    // Convert to raw float/int arrays for libdc
    std::vector<float> positions(verts.size() * 3);
    for (int i = 0; i < verts.size(); i++) {
        positions[i * 3 + 0] = verts[i].x;
        positions[i * 3 + 1] = verts[i].y;
        positions[i * 3 + 2] = verts[i].z;
    }

    int num_tris = indices.size() / 3;
    std::vector<int> tri_indices(indices.size());
    for (int i = 0; i < indices.size(); i++) {
        tri_indices[i] = indices[i];
    }

    if (!impl_.load_arrays(positions.data(), verts.size(), tri_indices.data(), num_tris)) {
        UtilityFunctions::printerr("DCTerrainGeneratorMeshSDF: failed to build SDF from ", mesh_path_);
    } else {
        UtilityFunctions::print("DCTerrainGeneratorMeshSDF: loaded ", mesh_path_,
                                 " (", impl_.triangle_count(), " tris)");
    }
}

int DCTerrainGeneratorMeshSDF::get_triangle_count() const {
    return impl_.triangle_count();
}

void DCTerrainGeneratorMeshSDF::_bind_methods() {
    ClassDB::bind_method(D_METHOD("set_mesh_path", "path"), &DCTerrainGeneratorMeshSDF::set_mesh_path);
    ClassDB::bind_method(D_METHOD("get_mesh_path"), &DCTerrainGeneratorMeshSDF::get_mesh_path);
    ADD_PROPERTY(PropertyInfo(Variant::STRING, "mesh_path", PROPERTY_HINT_GLOBAL_FILE, "*.obj"),
                 "set_mesh_path", "get_mesh_path");

    ClassDB::bind_method(D_METHOD("reload"), &DCTerrainGeneratorMeshSDF::reload);
    ClassDB::bind_method(D_METHOD("get_triangle_count"), &DCTerrainGeneratorMeshSDF::get_triangle_count);
}

// ===========================================================================
// DCTerrainGeneratorScript
// ===========================================================================

float DCTerrainGeneratorScript::ScriptBridge::evaluate(
        float x, float y, float z, float lod_voxel_size) {
    float ret = 0.0f;
    if (GDVIRTUAL_IS_OVERRIDDEN_PTR(owner_, _evaluate)) {
        GDVIRTUAL_CALL_PTR(owner_, _evaluate, x, y, z, lod_voxel_size, ret);
    }
    return ret;
}

void DCTerrainGeneratorScript::_bind_methods() {
    GDVIRTUAL_BIND(_evaluate, "x", "y", "z", "lod_voxel_size")
}



// ---------------------------------------------------------------------------
// DCTerrainGeneratorGeiss
// ---------------------------------------------------------------------------

void DCTerrainGeneratorGeiss::_bind_methods() {
    ClassDB::bind_method(D_METHOD("set_volume_dir", "path"), &DCTerrainGeneratorGeiss::set_volume_dir);
    ClassDB::bind_method(D_METHOD("get_volume_dir"), &DCTerrainGeneratorGeiss::get_volume_dir);
    ClassDB::bind_method(D_METHOD("reload"), &DCTerrainGeneratorGeiss::reload);
    ClassDB::bind_method(D_METHOD("is_loaded"), &DCTerrainGeneratorGeiss::is_loaded);
    ADD_PROPERTY(PropertyInfo(Variant::STRING, "volume_dir", PROPERTY_HINT_DIR),
                 "set_volume_dir", "get_volume_dir");
}

void DCTerrainGeneratorGeiss::set_volume_dir(const String& p_path) {
    if (volume_dir_ == p_path) return;
    volume_dir_ = p_path;
    reload();
}

bool DCTerrainGeneratorGeiss::reload() {
    loaded_ = false;
    if (volume_dir_.is_empty()) return false;
    // globalize so a res:// path works from the editor as well as an export.
    const String abs = ProjectSettings::get_singleton()->globalize_path(volume_dir_);
    loaded_ = impl_.load_volumes(std::string(abs.utf8().get_data()));
    if (!loaded_) {
        UtilityFunctions::push_warning(
            "DCTerrainGeneratorGeiss: could not load the eight .vol files from ", volume_dir_,
            " -- the generator stays GPU-disabled and will produce only air.");
    }
    return loaded_;
}

} // namespace godot