#include "dc_terrain_generator_procnodes.h"
#include <godot_cpp/variant/utility_functions.hpp>
// THE erosion filter, shared with proc-nodes' GLSL. This file used to carry
// its own transcription of it, and the transcription had drifted: it called
// erosion_filter with scale=5.0 where the shader passes 0.08333, which is 60x
// in both gully wavelength and gully amplitude, and it omitted the [0,1]
// remap, the water-line gate and the height offset entirely. The result was
// that DCTerrain::sample_density, get_distance, get_surface_normal and
// get_capsule_push -- collision -- reported a surface the mesher never drew,
// by a mean of 4 km on map_dc_erosion, with 6 of 16 probe columns having no
// collidable ground at all under terrain that renders fine.
//
// The header is godot-free, so this does NOT reintroduce a compile-time
// dependency on proc-nodes' classes -- the ClassDB reflection in the header
// comment still stands. It is a shared math header and nothing more.
#include "erosion_cpp_shim.h"
// The curve as C++, inside the shim's namespace so it sees the GLSL builtins
// -- the same arrangement the shim uses for erosion_filter.inc. The shader
// compiles this identical text via HYPSO_CURVE_GLSL, so get_waterline_norm()
// below cannot drift from what the GPU computes.
namespace dcerosion {
#include "hypso_curve.inc"
}
#include "erosion_wrapper_glsl.h"
#include <godot_cpp/classes/file_access.hpp>
namespace godot {
void DCTerrainGeneratorProcNodes::_bind_methods() {
// Bound so a gate can ASK whether the graph reached the GPU, instead of a
// human having to spot an error line. hyper-games #64 NEXT 4.
ClassDB::bind_method(D_METHOD("get_last_compile_error"), &DCTerrainGeneratorProcNodes::get_last_compile_error);
ClassDB::bind_method(D_METHOD("has_compiled_shader"), &DCTerrainGeneratorProcNodes::has_compiled_shader);
ClassDB::bind_method(D_METHOD("set_erosion_minimal", "value"), &DCTerrainGeneratorProcNodes::set_erosion_minimal);
ClassDB::bind_method(D_METHOD("get_erosion_minimal"), &DCTerrainGeneratorProcNodes::get_erosion_minimal);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "erosion_minimal"), "set_erosion_minimal", "get_erosion_minimal");
ClassDB::bind_method(D_METHOD("set_erosion_enabled", "value"), &DCTerrainGeneratorProcNodes::set_erosion_enabled);
ClassDB::bind_method(D_METHOD("get_erosion_enabled"), &DCTerrainGeneratorProcNodes::get_erosion_enabled);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "erosion_enabled"), "set_erosion_enabled", "get_erosion_enabled");
ClassDB::bind_method(D_METHOD("set_mountain_scale", "value"), &DCTerrainGeneratorProcNodes::set_mountain_scale);
ClassDB::bind_method(D_METHOD("get_mountain_scale"), &DCTerrainGeneratorProcNodes::get_mountain_scale);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "mountain_scale"), "set_mountain_scale", "get_mountain_scale");
ClassDB::bind_method(D_METHOD("set_mountain_height", "value"), &DCTerrainGeneratorProcNodes::set_mountain_height);
ClassDB::bind_method(D_METHOD("get_mountain_height"), &DCTerrainGeneratorProcNodes::get_mountain_height);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "mountain_height"), "set_mountain_height", "get_mountain_height");
ClassDB::bind_method(D_METHOD("set_mountain_gain", "value"), &DCTerrainGeneratorProcNodes::set_mountain_gain);
ClassDB::bind_method(D_METHOD("get_mountain_gain"), &DCTerrainGeneratorProcNodes::get_mountain_gain);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "mountain_gain"), "set_mountain_gain", "get_mountain_gain");
ClassDB::bind_method(D_METHOD("set_mountain_variance", "value"), &DCTerrainGeneratorProcNodes::set_mountain_variance);
ClassDB::bind_method(D_METHOD("get_mountain_variance"), &DCTerrainGeneratorProcNodes::get_mountain_variance);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "mountain_variance"), "set_mountain_variance", "get_mountain_variance");
ClassDB::bind_method(D_METHOD("set_relief_scale", "value"), &DCTerrainGeneratorProcNodes::set_relief_scale);
ClassDB::bind_method(D_METHOD("get_relief_scale"), &DCTerrainGeneratorProcNodes::get_relief_scale);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "relief_scale"), "set_relief_scale", "get_relief_scale");
ClassDB::bind_method(D_METHOD("set_relief_floor", "value"), &DCTerrainGeneratorProcNodes::set_relief_floor);
ClassDB::bind_method(D_METHOD("get_relief_floor"), &DCTerrainGeneratorProcNodes::get_relief_floor);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "relief_floor"), "set_relief_floor", "get_relief_floor");
ClassDB::bind_method(D_METHOD("set_detail_scale", "value"), &DCTerrainGeneratorProcNodes::set_detail_scale);
ClassDB::bind_method(D_METHOD("get_detail_scale"), &DCTerrainGeneratorProcNodes::get_detail_scale);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "detail_scale"), "set_detail_scale", "get_detail_scale");
ClassDB::bind_method(D_METHOD("set_detail_height", "value"), &DCTerrainGeneratorProcNodes::set_detail_height);
ClassDB::bind_method(D_METHOD("get_detail_height"), &DCTerrainGeneratorProcNodes::get_detail_height);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "detail_height"), "set_detail_height", "get_detail_height");
ClassDB::bind_method(D_METHOD("set_detail_gain", "value"), &DCTerrainGeneratorProcNodes::set_detail_gain);
ClassDB::bind_method(D_METHOD("get_detail_gain"), &DCTerrainGeneratorProcNodes::get_detail_gain);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "detail_gain"), "set_detail_gain", "get_detail_gain");
ClassDB::bind_method(D_METHOD("set_noise_lacunarity", "value"), &DCTerrainGeneratorProcNodes::set_noise_lacunarity);
ClassDB::bind_method(D_METHOD("get_noise_lacunarity"), &DCTerrainGeneratorProcNodes::get_noise_lacunarity);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "noise_lacunarity"), "set_noise_lacunarity", "get_noise_lacunarity");
ClassDB::bind_method(D_METHOD("set_erosion_scale", "value"), &DCTerrainGeneratorProcNodes::set_erosion_scale);
ClassDB::bind_method(D_METHOD("get_erosion_scale"), &DCTerrainGeneratorProcNodes::get_erosion_scale);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "erosion_scale"), "set_erosion_scale", "get_erosion_scale");
ClassDB::bind_method(D_METHOD("set_field_half_range", "value"), &DCTerrainGeneratorProcNodes::set_field_half_range);
ClassDB::bind_method(D_METHOD("get_field_half_range"), &DCTerrainGeneratorProcNodes::get_field_half_range);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "field_half_range"), "set_field_half_range", "get_field_half_range");
ClassDB::bind_method(D_METHOD("set_input_height_amplitude", "value"), &DCTerrainGeneratorProcNodes::set_input_height_amplitude);
ClassDB::bind_method(D_METHOD("get_input_height_amplitude"), &DCTerrainGeneratorProcNodes::get_input_height_amplitude);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "input_height_amplitude"), "set_input_height_amplitude", "get_input_height_amplitude");
ClassDB::bind_method(D_METHOD("set_octave_lacunarity", "value"), &DCTerrainGeneratorProcNodes::set_octave_lacunarity);
ClassDB::bind_method(D_METHOD("get_octave_lacunarity"), &DCTerrainGeneratorProcNodes::get_octave_lacunarity);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "octave_lacunarity"), "set_octave_lacunarity", "get_octave_lacunarity");
ClassDB::bind_method(D_METHOD("set_gully_depth", "value"), &DCTerrainGeneratorProcNodes::set_gully_depth);
ClassDB::bind_method(D_METHOD("get_gully_depth"), &DCTerrainGeneratorProcNodes::get_gully_depth);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "gully_depth"), "set_gully_depth", "get_gully_depth");
ClassDB::bind_method(D_METHOD("set_fine_gully_slope_restriction", "value"), &DCTerrainGeneratorProcNodes::set_fine_gully_slope_restriction);
ClassDB::bind_method(D_METHOD("get_fine_gully_slope_restriction"), &DCTerrainGeneratorProcNodes::get_fine_gully_slope_restriction);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "fine_gully_slope_restriction"), "set_fine_gully_slope_restriction", "get_fine_gully_slope_restriction");
ClassDB::bind_method(D_METHOD("set_gully_normalize_threshold", "value"), &DCTerrainGeneratorProcNodes::set_gully_normalize_threshold);
ClassDB::bind_method(D_METHOD("get_gully_normalize_threshold"), &DCTerrainGeneratorProcNodes::get_gully_normalize_threshold);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "gully_normalize_threshold"), "set_gully_normalize_threshold", "get_gully_normalize_threshold");
ClassDB::bind_method(D_METHOD("set_height_offset", "value"), &DCTerrainGeneratorProcNodes::set_height_offset);
ClassDB::bind_method(D_METHOD("get_height_offset"), &DCTerrainGeneratorProcNodes::get_height_offset);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "height_offset"), "set_height_offset", "get_height_offset");
ClassDB::bind_method(D_METHOD("set_height_offset_follows_altitude", "value"), &DCTerrainGeneratorProcNodes::set_height_offset_follows_altitude);
ClassDB::bind_method(D_METHOD("get_height_offset_follows_altitude"), &DCTerrainGeneratorProcNodes::get_height_offset_follows_altitude);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "height_offset_follows_altitude"), "set_height_offset_follows_altitude", "get_height_offset_follows_altitude");
ClassDB::bind_method(D_METHOD("set_crease_rounding", "value"), &DCTerrainGeneratorProcNodes::set_crease_rounding);
ClassDB::bind_method(D_METHOD("get_crease_rounding"), &DCTerrainGeneratorProcNodes::get_crease_rounding);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "crease_rounding"), "set_crease_rounding", "get_crease_rounding");
ClassDB::bind_method(D_METHOD("set_submarine_amp", "v"), &DCTerrainGeneratorProcNodes::set_submarine_amp);
ClassDB::bind_method(D_METHOD("get_submarine_amp"), &DCTerrainGeneratorProcNodes::get_submarine_amp);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "submarine_amp"), "set_submarine_amp", "get_submarine_amp");
ClassDB::bind_method(D_METHOD("set_submarine_wavelength", "v"), &DCTerrainGeneratorProcNodes::set_submarine_wavelength);
ClassDB::bind_method(D_METHOD("get_submarine_wavelength"), &DCTerrainGeneratorProcNodes::get_submarine_wavelength);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "submarine_wavelength"), "set_submarine_wavelength", "get_submarine_wavelength");
ClassDB::bind_method(D_METHOD("set_submarine_octaves", "v"), &DCTerrainGeneratorProcNodes::set_submarine_octaves);
ClassDB::bind_method(D_METHOD("get_submarine_octaves"), &DCTerrainGeneratorProcNodes::get_submarine_octaves);
ADD_PROPERTY(PropertyInfo(Variant::INT, "submarine_octaves"), "set_submarine_octaves", "get_submarine_octaves");
ClassDB::bind_method(D_METHOD("set_submarine_buffer", "a", "b"), &DCTerrainGeneratorProcNodes::set_submarine_buffer);
ClassDB::bind_method(D_METHOD("set_erosion_mask_depth", "v"), &DCTerrainGeneratorProcNodes::set_erosion_mask_depth);
ClassDB::bind_method(D_METHOD("get_erosion_mask_depth"), &DCTerrainGeneratorProcNodes::get_erosion_mask_depth);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "erosion_mask_depth"), "set_erosion_mask_depth", "get_erosion_mask_depth");
ClassDB::bind_method(D_METHOD("set_submarine_water_y", "v"), &DCTerrainGeneratorProcNodes::set_submarine_water_y);
ClassDB::bind_method(D_METHOD("get_submarine_water_y"), &DCTerrainGeneratorProcNodes::get_submarine_water_y);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "submarine_water_y"), "set_submarine_water_y", "get_submarine_water_y");
ClassDB::bind_method(D_METHOD("set_detail_slope_lo", "value"), &DCTerrainGeneratorProcNodes::set_detail_slope_lo);
ClassDB::bind_method(D_METHOD("get_detail_slope_lo"), &DCTerrainGeneratorProcNodes::get_detail_slope_lo);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "detail_slope_lo"), "set_detail_slope_lo", "get_detail_slope_lo");
ClassDB::bind_method(D_METHOD("set_detail_slope_hi", "value"), &DCTerrainGeneratorProcNodes::set_detail_slope_hi);
ClassDB::bind_method(D_METHOD("get_detail_slope_hi"), &DCTerrainGeneratorProcNodes::get_detail_slope_hi);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "detail_slope_hi"), "set_detail_slope_hi", "get_detail_slope_hi");
ClassDB::bind_method(D_METHOD("set_hypso_enabled", "value"), &DCTerrainGeneratorProcNodes::set_hypso_enabled);
ClassDB::bind_method(D_METHOD("get_hypso_enabled"), &DCTerrainGeneratorProcNodes::get_hypso_enabled);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "hypso_enabled"), "set_hypso_enabled", "get_hypso_enabled");
ClassDB::bind_method(D_METHOD("set_hypso_sea", "value"), &DCTerrainGeneratorProcNodes::set_hypso_sea);
ClassDB::bind_method(D_METHOD("get_hypso_sea"), &DCTerrainGeneratorProcNodes::get_hypso_sea);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "hypso_sea"), "set_hypso_sea", "get_hypso_sea");
ClassDB::bind_method(D_METHOD("set_hypso_shelf_width", "value"), &DCTerrainGeneratorProcNodes::set_hypso_shelf_width);
ClassDB::bind_method(D_METHOD("get_hypso_shelf_width"), &DCTerrainGeneratorProcNodes::get_hypso_shelf_width);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "hypso_shelf_width"), "set_hypso_shelf_width", "get_hypso_shelf_width");
ClassDB::bind_method(D_METHOD("set_hypso_slope_width", "value"), &DCTerrainGeneratorProcNodes::set_hypso_slope_width);
ClassDB::bind_method(D_METHOD("get_hypso_slope_width"), &DCTerrainGeneratorProcNodes::get_hypso_slope_width);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "hypso_slope_width"), "set_hypso_slope_width", "get_hypso_slope_width");
ClassDB::bind_method(D_METHOD("set_hypso_abyss", "value"), &DCTerrainGeneratorProcNodes::set_hypso_abyss);
ClassDB::bind_method(D_METHOD("get_hypso_abyss"), &DCTerrainGeneratorProcNodes::get_hypso_abyss);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "hypso_abyss"), "set_hypso_abyss", "get_hypso_abyss");
ClassDB::bind_method(D_METHOD("set_hypso_plat_slope", "value"), &DCTerrainGeneratorProcNodes::set_hypso_plat_slope);
ClassDB::bind_method(D_METHOD("get_hypso_plat_slope"), &DCTerrainGeneratorProcNodes::get_hypso_plat_slope);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "hypso_plat_slope"), "set_hypso_plat_slope", "get_hypso_plat_slope");
ClassDB::bind_method(D_METHOD("set_hypso_mtn_onset", "value"), &DCTerrainGeneratorProcNodes::set_hypso_mtn_onset);
ClassDB::bind_method(D_METHOD("get_hypso_mtn_onset"), &DCTerrainGeneratorProcNodes::get_hypso_mtn_onset);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "hypso_mtn_onset"), "set_hypso_mtn_onset", "get_hypso_mtn_onset");
ClassDB::bind_method(D_METHOD("set_hypso_mtn_width", "value"), &DCTerrainGeneratorProcNodes::set_hypso_mtn_width);
ClassDB::bind_method(D_METHOD("get_hypso_mtn_width"), &DCTerrainGeneratorProcNodes::get_hypso_mtn_width);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "hypso_mtn_width"), "set_hypso_mtn_width", "get_hypso_mtn_width");
ClassDB::bind_method(D_METHOD("set_hypso_mtn_slope", "value"), &DCTerrainGeneratorProcNodes::set_hypso_mtn_slope);
ClassDB::bind_method(D_METHOD("get_hypso_mtn_slope"), &DCTerrainGeneratorProcNodes::get_hypso_mtn_slope);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "hypso_mtn_slope"), "set_hypso_mtn_slope", "get_hypso_mtn_slope");
ClassDB::bind_method(D_METHOD("set_hypso_peak_onset", "value"), &DCTerrainGeneratorProcNodes::set_hypso_peak_onset);
ClassDB::bind_method(D_METHOD("get_hypso_peak_onset"), &DCTerrainGeneratorProcNodes::get_hypso_peak_onset);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "hypso_peak_onset"), "set_hypso_peak_onset", "get_hypso_peak_onset");
ClassDB::bind_method(D_METHOD("set_hypso_peak_gain", "value"), &DCTerrainGeneratorProcNodes::set_hypso_peak_gain);
ClassDB::bind_method(D_METHOD("get_hypso_peak_gain"), &DCTerrainGeneratorProcNodes::get_hypso_peak_gain);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "hypso_peak_gain"), "set_hypso_peak_gain", "get_hypso_peak_gain");
ClassDB::bind_method(D_METHOD("get_waterline_norm"), &DCTerrainGeneratorProcNodes::get_waterline_norm);
ClassDB::bind_method(D_METHOD("set_ridge_rounding", "value"), &DCTerrainGeneratorProcNodes::set_ridge_rounding);
ClassDB::bind_method(D_METHOD("get_ridge_rounding"), &DCTerrainGeneratorProcNodes::get_ridge_rounding);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "ridge_rounding"), "set_ridge_rounding", "get_ridge_rounding");
ClassDB::bind_method(D_METHOD("set_input_feature_rounding", "value"), &DCTerrainGeneratorProcNodes::set_input_feature_rounding);
ClassDB::bind_method(D_METHOD("get_input_feature_rounding"), &DCTerrainGeneratorProcNodes::get_input_feature_rounding);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "input_feature_rounding"), "set_input_feature_rounding", "get_input_feature_rounding");
ClassDB::bind_method(D_METHOD("set_rounding_per_octave", "value"), &DCTerrainGeneratorProcNodes::set_rounding_per_octave);
ClassDB::bind_method(D_METHOD("get_rounding_per_octave"), &DCTerrainGeneratorProcNodes::get_rounding_per_octave);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "rounding_per_octave"), "set_rounding_per_octave", "get_rounding_per_octave");
ClassDB::bind_method(D_METHOD("set_gully_onset_slope", "value"), &DCTerrainGeneratorProcNodes::set_gully_onset_slope);
ClassDB::bind_method(D_METHOD("get_gully_onset_slope"), &DCTerrainGeneratorProcNodes::get_gully_onset_slope);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "gully_onset_slope"), "set_gully_onset_slope", "get_gully_onset_slope");
ClassDB::bind_method(D_METHOD("set_octave_onset_slope", "value"), &DCTerrainGeneratorProcNodes::set_octave_onset_slope);
ClassDB::bind_method(D_METHOD("get_octave_onset_slope"), &DCTerrainGeneratorProcNodes::get_octave_onset_slope);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "octave_onset_slope"), "set_octave_onset_slope", "get_octave_onset_slope");
ClassDB::bind_method(D_METHOD("set_ridge_onset_slope", "value"), &DCTerrainGeneratorProcNodes::set_ridge_onset_slope);
ClassDB::bind_method(D_METHOD("get_ridge_onset_slope"), &DCTerrainGeneratorProcNodes::get_ridge_onset_slope);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "ridge_onset_slope"), "set_ridge_onset_slope", "get_ridge_onset_slope");
ClassDB::bind_method(D_METHOD("set_ridge_octave_onset", "value"), &DCTerrainGeneratorProcNodes::set_ridge_octave_onset);
ClassDB::bind_method(D_METHOD("get_ridge_octave_onset"), &DCTerrainGeneratorProcNodes::get_ridge_octave_onset);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "ridge_octave_onset"), "set_ridge_octave_onset", "get_ridge_octave_onset");
ClassDB::bind_method(D_METHOD("set_assumed_slope", "value"), &DCTerrainGeneratorProcNodes::set_assumed_slope);
ClassDB::bind_method(D_METHOD("get_assumed_slope"), &DCTerrainGeneratorProcNodes::get_assumed_slope);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "assumed_slope"), "set_assumed_slope", "get_assumed_slope");
ClassDB::bind_method(D_METHOD("set_assumed_slope_mix", "value"), &DCTerrainGeneratorProcNodes::set_assumed_slope_mix);
ClassDB::bind_method(D_METHOD("get_assumed_slope_mix"), &DCTerrainGeneratorProcNodes::get_assumed_slope_mix);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "assumed_slope_mix"), "set_assumed_slope_mix", "get_assumed_slope_mix");
ClassDB::bind_method(D_METHOD("set_mountain_octaves", "value"), &DCTerrainGeneratorProcNodes::set_mountain_octaves);
ClassDB::bind_method(D_METHOD("get_mountain_octaves"), &DCTerrainGeneratorProcNodes::get_mountain_octaves);
ADD_PROPERTY(PropertyInfo(Variant::INT, "mountain_octaves"), "set_mountain_octaves", "get_mountain_octaves");
ClassDB::bind_method(D_METHOD("set_detail_octaves", "value"), &DCTerrainGeneratorProcNodes::set_detail_octaves);
ClassDB::bind_method(D_METHOD("get_detail_octaves"), &DCTerrainGeneratorProcNodes::get_detail_octaves);
ADD_PROPERTY(PropertyInfo(Variant::INT, "detail_octaves"), "set_detail_octaves", "get_detail_octaves");
ClassDB::bind_method(D_METHOD("set_lookdev_wrapper", "value"), &DCTerrainGeneratorProcNodes::set_lookdev_wrapper);
ClassDB::bind_method(D_METHOD("get_lookdev_wrapper"), &DCTerrainGeneratorProcNodes::get_lookdev_wrapper);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "lookdev_wrapper"), "set_lookdev_wrapper", "get_lookdev_wrapper");
ClassDB::bind_method(D_METHOD("set_graph", "graph"), &DCTerrainGeneratorProcNodes::set_graph);
ClassDB::bind_method(D_METHOD("get_graph"), &DCTerrainGeneratorProcNodes::get_graph);
ClassDB::bind_method(D_METHOD("set_terrain_height", "height"), &DCTerrainGeneratorProcNodes::set_terrain_height);
ClassDB::bind_method(D_METHOD("get_terrain_height"), &DCTerrainGeneratorProcNodes::get_terrain_height);
ClassDB::bind_method(D_METHOD("recompile"), &DCTerrainGeneratorProcNodes::recompile);
ClassDB::bind_method(D_METHOD("get_compiled_glsl"), &DCTerrainGeneratorProcNodes::get_compiled_glsl);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "graph", PROPERTY_HINT_RESOURCE_TYPE, "ProcNodeGraph"),
"set_graph", "get_graph");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "terrain_height", PROPERTY_HINT_RANGE, "1.0,10000.0,0.1"),
"set_terrain_height", "get_terrain_height");
ClassDB::bind_method(D_METHOD("set_floor_y", "value"), &DCTerrainGeneratorProcNodes::set_floor_y);
ClassDB::bind_method(D_METHOD("get_floor_y"), &DCTerrainGeneratorProcNodes::get_floor_y);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "floor_y", PROPERTY_HINT_RANGE, "-1000.0,1000.0,0.5"),
"set_floor_y", "get_floor_y");
ClassDB::bind_method(D_METHOD("set_ceiling_y", "value"), &DCTerrainGeneratorProcNodes::set_ceiling_y);
ClassDB::bind_method(D_METHOD("get_ceiling_y"), &DCTerrainGeneratorProcNodes::get_ceiling_y);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "ceiling_y", PROPERTY_HINT_RANGE, "-1000.0,1000.0,0.5"),
"set_ceiling_y", "get_ceiling_y");
ClassDB::bind_method(D_METHOD("set_heightfield_path", "path"), &DCTerrainGeneratorProcNodes::set_heightfield_path);
ClassDB::bind_method(D_METHOD("get_heightfield_path"), &DCTerrainGeneratorProcNodes::get_heightfield_path);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "heightfield_path", PROPERTY_HINT_FILE, "*.hblk"),
"set_heightfield_path", "get_heightfield_path");
ClassDB::bind_method(D_METHOD("set_heightfield_only", "value"), &DCTerrainGeneratorProcNodes::set_heightfield_only);
ClassDB::bind_method(D_METHOD("get_heightfield_only"), &DCTerrainGeneratorProcNodes::get_heightfield_only);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "heightfield_only"),
"set_heightfield_only", "get_heightfield_only");
ClassDB::bind_method(D_METHOD("set_height_space_sqrt", "value"), &DCTerrainGeneratorProcNodes::set_height_space_sqrt);
ClassDB::bind_method(D_METHOD("get_height_space_sqrt"), &DCTerrainGeneratorProcNodes::get_height_space_sqrt);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "height_space_sqrt"),
"set_height_space_sqrt", "get_height_space_sqrt");
}
void DCTerrainGeneratorProcNodes::set_mountain_scale(float p_val) {
if (mountain_scale_ == p_val) return;
mountain_scale_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_mountain_height(float p_val) {
if (mountain_height_ == p_val) return;
mountain_height_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_mountain_gain(float p_val) {
if (mountain_gain_ == p_val) return;
mountain_gain_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_mountain_variance(float p_val) {
if (mountain_variance_ == p_val) return;
mountain_variance_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_relief_scale(float p_val) {
if (relief_scale_ == p_val) return;
relief_scale_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_relief_floor(float p_val) {
if (relief_floor_ == p_val) return;
relief_floor_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_detail_scale(float p_val) {
if (detail_scale_ == p_val) return;
detail_scale_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_detail_height(float p_val) {
if (detail_height_ == p_val) return;
detail_height_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_detail_gain(float p_val) {
if (detail_gain_ == p_val) return;
detail_gain_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_noise_lacunarity(float p_val) {
if (noise_lacunarity_ == p_val) return;
noise_lacunarity_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_erosion_scale(float p_val) {
if (erosion_scale_ == p_val) return;
erosion_scale_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_field_half_range(float p_val) {
if (field_half_range_ == p_val) return;
field_half_range_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_input_height_amplitude(float p_val) {
if (input_height_amplitude_ == p_val) return;
input_height_amplitude_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_octave_lacunarity(float p_val) {
if (octave_lacunarity_ == p_val) return;
octave_lacunarity_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_gully_depth(float p_val) {
if (gully_depth_ == p_val) return;
gully_depth_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_fine_gully_slope_restriction(float p_val) {
if (fine_gully_slope_restriction_ == p_val) return;
fine_gully_slope_restriction_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_gully_normalize_threshold(float p_val) {
if (gully_normalize_threshold_ == p_val) return;
gully_normalize_threshold_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_height_offset(float p_val) {
if (height_offset_ == p_val) return;
height_offset_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_height_offset_follows_altitude(float p_val) {
if (height_offset_follows_altitude_ == p_val) return;
height_offset_follows_altitude_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_crease_rounding(float p_val) {
if (crease_rounding_ == p_val) return;
crease_rounding_ = p_val;
_recompile_glsl();
emit_changed();
}
float DCTerrainGeneratorProcNodes::get_waterline_norm() const {
// Sea level in the curve's OUTPUT units. The curve is monotonic, so the
// waterline is simply its value at `sea` -- no bisection, no second
// constant to fall out of step with the first.
return dcerosion::hypso_curve(hypso_sea_, hypso_sea_, hypso_shelf_width_,
hypso_slope_width_, hypso_abyss_, hypso_plat_slope_, hypso_mtn_onset_,
hypso_mtn_width_, hypso_mtn_slope_, hypso_peak_onset_,
hypso_peak_gain_).x;
}
void DCTerrainGeneratorProcNodes::set_submarine_amp(float v) {
if (submarine_amp_ == v) return;
submarine_amp_ = v; _recompile_glsl(); emit_changed();
}
void DCTerrainGeneratorProcNodes::set_submarine_wavelength(float v) {
if (submarine_wavelength_ == v) return;
submarine_wavelength_ = v; _recompile_glsl(); emit_changed();
}
void DCTerrainGeneratorProcNodes::set_submarine_octaves(int v) {
if (submarine_octaves_ == v) return;
submarine_octaves_ = v; _recompile_glsl(); emit_changed();
}
void DCTerrainGeneratorProcNodes::set_submarine_buffer(float a, float b) {
submarine_buffer_start_ = a; submarine_buffer_end_ = b;
_recompile_glsl(); emit_changed();
}
void DCTerrainGeneratorProcNodes::set_erosion_mask_depth(float v) {
if (erosion_mask_depth_ == v) return;
erosion_mask_depth_ = v; _recompile_glsl(); emit_changed();
}
void DCTerrainGeneratorProcNodes::set_submarine_water_y(float v) {
if (submarine_water_y_ == v) return;
submarine_water_y_ = v; _recompile_glsl(); emit_changed();
}
void DCTerrainGeneratorProcNodes::set_detail_slope_lo(float p_val) {
if (detail_slope_lo_ == p_val) return;
detail_slope_lo_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_detail_slope_hi(float p_val) {
if (detail_slope_hi_ == p_val) return;
detail_slope_hi_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_hypso_enabled(float p_val) {
if (hypso_enabled_ == p_val) return;
hypso_enabled_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_hypso_sea(float p_val) {
if (hypso_sea_ == p_val) return;
hypso_sea_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_hypso_shelf_width(float p_val) {
if (hypso_shelf_width_ == p_val) return;
hypso_shelf_width_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_hypso_slope_width(float p_val) {
if (hypso_slope_width_ == p_val) return;
hypso_slope_width_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_hypso_abyss(float p_val) {
if (hypso_abyss_ == p_val) return;
hypso_abyss_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_hypso_plat_slope(float p_val) {
if (hypso_plat_slope_ == p_val) return;
hypso_plat_slope_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_hypso_mtn_onset(float p_val) {
if (hypso_mtn_onset_ == p_val) return;
hypso_mtn_onset_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_hypso_mtn_width(float p_val) {
if (hypso_mtn_width_ == p_val) return;
hypso_mtn_width_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_hypso_mtn_slope(float p_val) {
if (hypso_mtn_slope_ == p_val) return;
hypso_mtn_slope_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_hypso_peak_onset(float p_val) {
if (hypso_peak_onset_ == p_val) return;
hypso_peak_onset_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_hypso_peak_gain(float p_val) {
if (hypso_peak_gain_ == p_val) return;
hypso_peak_gain_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_ridge_rounding(float p_val) {
if (ridge_rounding_ == p_val) return;
ridge_rounding_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_input_feature_rounding(float p_val) {
if (input_feature_rounding_ == p_val) return;
input_feature_rounding_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_rounding_per_octave(float p_val) {
if (rounding_per_octave_ == p_val) return;
rounding_per_octave_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_gully_onset_slope(float p_val) {
if (gully_onset_slope_ == p_val) return;
gully_onset_slope_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_octave_onset_slope(float p_val) {
if (octave_onset_slope_ == p_val) return;
octave_onset_slope_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_ridge_onset_slope(float p_val) {
if (ridge_onset_slope_ == p_val) return;
ridge_onset_slope_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_ridge_octave_onset(float p_val) {
if (ridge_octave_onset_ == p_val) return;
ridge_octave_onset_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_assumed_slope(float p_val) {
if (assumed_slope_ == p_val) return;
assumed_slope_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_assumed_slope_mix(float p_val) {
if (assumed_slope_mix_ == p_val) return;
assumed_slope_mix_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_mountain_octaves(int p_val) {
if (mountain_octaves_ == p_val) return;
mountain_octaves_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_detail_octaves(int p_val) {
if (detail_octaves_ == p_val) return;
detail_octaves_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_lookdev_wrapper(bool p_val) {
if (lookdev_wrapper_ == p_val) return;
lookdev_wrapper_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_erosion_minimal(bool p_val) {
if (erosion_minimal_ == p_val) return;
erosion_minimal_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_erosion_enabled(bool p_val) {
if (erosion_enabled_ == p_val) return;
erosion_enabled_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_terrain_height(float p_height) {
if (terrain_height_ == p_height) return;
terrain_height_ = p_height;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_floor_y(float p_val) {
if (floor_y_ == p_val) return;
floor_y_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_ceiling_y(float p_val) {
if (ceiling_y_ == p_val) return;
ceiling_y_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_height_space_sqrt(bool p_val) {
if (height_space_sqrt_ == p_val) return;
height_space_sqrt_ = p_val;
_recompile_glsl();
emit_changed();
}
// Load a .hblk written by terrain-lookdev/stitch_block.py. The format is
// deliberately trivial -- a 32-byte header and int16 metres -- because the
// alternative was a Godot Image import path, and the field has to be readable
// identically by the standalone harness that gates it, which has no engine.
bool DCTerrainGeneratorProcNodes::_load_heightfield(const String &p_path) {
hblk_.clear(); hblk_n_ = 0; hblk_version_++;
if (p_path.is_empty()) return true;
Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
if (f.is_null()) {
UtilityFunctions::push_warning("DCTerrainGeneratorProcNodes: cannot open heightfield ", p_path);
return false;
}
PackedByteArray hdr = f->get_buffer(32);
if (hdr.size() != 32 || hdr[0] != 'H' || hdr[1] != 'B' || hdr[2] != 'L' || hdr[3] != 'K') {
UtilityFunctions::push_warning("DCTerrainGeneratorProcNodes: not an .hblk: ", p_path);
return false;
}
const uint8_t *p = hdr.ptr();
uint32_t ver, n; float mpp, ox, oz;
memcpy(&ver, p + 4, 4); memcpy(&n, p + 8, 4); memcpy(&mpp, p + 12, 4);
memcpy(&ox, p + 16, 4); memcpy(&oz, p + 20, 4);
if (ver != 1u || n == 0u) {
UtilityFunctions::push_warning("DCTerrainGeneratorProcNodes: unsupported .hblk version/size");
return false;
}
PackedByteArray body = f->get_buffer((int64_t)n * (int64_t)n * 2);
if (body.size() != (int64_t)n * (int64_t)n * 2) {
UtilityFunctions::push_warning("DCTerrainGeneratorProcNodes: short .hblk data");
return false;
}
hblk_.resize((size_t)n * (size_t)n);
const uint8_t *b = body.ptr();
for (size_t i = 0; i < hblk_.size(); i++) {
int16_t v; memcpy(&v, b + i * 2, 2);
hblk_[i] = (float)v;
}
hblk_n_ = (int)n; hblk_mpp_ = mpp; hblk_ox_ = ox; hblk_oz_ = oz;
hblk_version_++;
UtilityFunctions::print("[DCTerrainGeneratorProcNodes] heightfield ", p_path,
": ", (int)n, "x", (int)n, " @ ", mpp, " m/px, origin x ", ox, " z ", oz);
return true;
}
void DCTerrainGeneratorProcNodes::set_heightfield_only(bool p_val) {
if (heightfield_only_ == p_val) return;
heightfield_only_ = p_val;
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_heightfield_path(const String &p_path) {
if (heightfield_path_ == p_path) return;
heightfield_path_ = p_path;
_load_heightfield(p_path);
_recompile_glsl();
emit_changed();
}
void DCTerrainGeneratorProcNodes::set_graph(const Ref<Resource> &p_graph) {
if (p_graph.is_valid()) {
// Validate that the resource is a ProcNodeGraph (from proc-nodes GDExtension)
if (!p_graph->has_method("get_node_count") || !p_graph->has_method("compile_to_glsl_function")) {
UtilityFunctions::push_error(
"DCTerrainGeneratorProcNodes: assigned resource is not a ProcNodeGraph "
"(missing get_node_count/compile_to_glsl_function). "
"Is the proc-nodes GDExtension loaded?");
return;
}
}
graph_ = p_graph;
_discover_params();
_collect_node_data();
_recompile_glsl(); // Eager — must happen on main thread before worker sees this generator
notify_property_list_changed();
emit_changed();
}
// ── Parameter Discovery ───────────────────────────────────────────────────────
// Collect opaque GPU data from the graph's nodes.
//
// dc-terrain transports floats and knows nothing about them; a NODE decides what
// they mean. So the generator asks each node "do you have data?" and hands the
// first answer up as custom data. This is what lets a heightfield live in the
// graph rather than as another property on this class.
//
// One producer for now, which matches one bound buffer. More than one wants an
// offset table emitted alongside, which is the same change a grid of tiles needs.
void DCTerrainGeneratorProcNodes::_collect_node_data() {
if (graph_.is_null()) return;
Variant nc_var = graph_->call("get_node_count");
if (nc_var.get_type() != Variant::INT) return;
int node_count = nc_var;
for (int ni = 0; ni < node_count; ni++) {
Variant node_var = graph_->call("get_node", ni);
if (node_var.get_type() != Variant::OBJECT) continue;
Object *node_obj = node_var.operator Object *();
if (!node_obj || !node_obj->has_method("get_gpu_data")) continue;
PackedFloat32Array d = node_obj->call("get_gpu_data");
if (d.is_empty()) continue;
hblk_.assign(d.ptr(), d.ptr() + d.size());
hblk_version_++;
// THE PACKED TILE GRID, if the node supplied one. Without this the CPU
// twin would keep reading the buffer as a single hblk_n_ square -- which
// is now the INDEX TABLE followed by tile rects, so it would return
// table entries as elevations and the collision surface would be
// somewhere else entirely from the rendered one.
hgrid_ = HeightGridDesc();
if (node_obj->has_method("get_grid_info")) {
Dictionary gi = node_obj->call("get_grid_info");
if ((bool)gi.get("active", false)) {
hgrid_.nx = (int)gi.get("nx", 0);
hgrid_.ny = (int)gi.get("ny", 0);
hgrid_.tx0 = (int)gi.get("tx0", 0);
hgrid_.ty0 = (int)gi.get("ty0", 0);
hgrid_.payload = (int)gi.get("payload", 0);
hgrid_.apron = (int)gi.get("apron", 0);
hgrid_.n = (int)gi.get("n", 0);
hgrid_.table = (int)gi.get("table", 0);
hgrid_.mpp = (float)(double)gi.get("mpp", 30.0);
hgrid_.stub = (float)(double)gi.get("stub", 0.0);
// ⛔ AND THE GEOMETRY, WHICH IS THE WHOLE POINT.
//
// hblk_n_/ox_/oz_ were taken from get_tile_size()/get_tile_origin()
// above -- the SINGLE tile's, i.e. the node's n_, which is 0 when
// only the grid is loaded because set_tile_path() was never called.
//
// And `hblk_n_ > 0` gates BOTH the four CPU heightfield sites AND
// the emission at :1117 that substitutes heightmap_gn(x,z) for the
// mountain-noise layer. Left at 0, 213 MiB of tiles upload
// successfully and NOTHING EVER SAMPLES THEM: the CPU meshes the
// base noise, the GLSL emits the noise branch, and the only symptom
// is a CPU/GPU spawn-height disagreement.
hblk_n_ = (int)gi.get("side", 0);
hblk_ox_ = (float)(double)gi.get("ox", 0.0);
hblk_oz_ = (float)(double)gi.get("oz", 0.0);
hblk_mpp_ = hgrid_.mpp;
UtilityFunctions::print("[DCTerrainGeneratorProcNodes] tile grid ",
hgrid_.nx, "x", hgrid_.ny, " cells, ", (int)gi.get("tiles", 0),
" tiles resident, ", hblk_n_, " texels/side from (",
hblk_ox_, ",", hblk_oz_, ")");
}
}
// ASK THE NODE FOR THE SIDE LENGTH; DO NOT DERIVE IT FROM THE ARRAY.
//
// get_gpu_data() returns the MIP PYRAMID, not one square: 1,403,226
// floats for a 1026 tile. sqrt of that is 1184.6, so the old derivation
// produced 1185 and every consumer of this copy -- the lookdev
// wrapper's sampler and the CPU twin -- would have read the block with
// a stride 159 texels too wide, which shears the terrain rather than
// failing. It has been latent because nothing used hblk_n_ on the
// node-supplied path yet.
if (node_obj->has_method("get_tile_size")) {
hblk_n_ = (int)(int64_t)node_obj->call("get_tile_size");
} else {
hblk_n_ = (int)Math::round(Math::sqrt((double)d.size()));
}
// AND ITS GEOMETRY. Copying the floats without the origin and pitch left
// the CPU sampling at world/30 from (0,0) -- off the end of the tile, so
// every query clamped to a corner and reported the block's minimum. The
// data means nothing without where it sits.
if (node_obj->has_method("get_tile_mpp"))
hblk_mpp_ = (float)(double)node_obj->call("get_tile_mpp");
if (node_obj->has_method("get_tile_origin")) {
Vector2 o = node_obj->call("get_tile_origin");
hblk_ox_ = o.x;
hblk_oz_ = o.y;
}
UtilityFunctions::print("[DCTerrainGeneratorProcNodes] node ", ni,
" supplied ", (int64_t)d.size(), " floats of custom data");
return;
}
}
// ONE CPU heightfield sampler, for the four sites that used to each carry their
// own bilinear. Paged when the node supplied a tile grid, a plain square
// otherwise -- and the paged half is erosion_wrapper_glsl.h's, the same file
// that emits the GLSL, so the twin cannot drift.
float DCTerrainGeneratorProcNodes::_hblk_sample_cpu(float x, float z) const {
if (hblk_.empty()) return 0.0f;
if (hgrid_.active())
return erosion_heightmap_grid_sample(hblk_.data(), hgrid_, x, z);
const int hn = hblk_n_;
const float u0 = dcerosion::clamp((x - hblk_ox_) / hblk_mpp_, 0.0f, (float)(hn - 1));
const float v0 = dcerosion::clamp((z - hblk_oz_) / hblk_mpp_, 0.0f, (float)(hn - 1));
const int i0 = (int)floorf(u0), j0 = (int)floorf(v0);
const int i1 = MIN(i0 + 1, hn - 1), j1 = MIN(j0 + 1, hn - 1);
const float fu = u0 - (float)i0, fv = v0 - (float)j0;
const float h00 = hblk_[(size_t)j0*hn + i0], h10 = hblk_[(size_t)j0*hn + i1];
const float h01 = hblk_[(size_t)j1*hn + i0], h11 = hblk_[(size_t)j1*hn + i1];
const float a = h00 + (h10 - h00) * fu, b = h01 + (h11 - h01) * fu;
return a + (b - a) * fv;
}
void DCTerrainGeneratorProcNodes::_discover_params() {
discovered_params_.clear();
if (graph_.is_null()) return;
Variant nc_var = graph_->call("get_node_count");
if (nc_var.get_type() != Variant::INT) return;
int node_count = nc_var;
for (int ni = 0; ni < node_count; ni++) {
Variant node_var = graph_->call("get_node", ni);
if (node_var.get_type() != Variant::OBJECT) continue;
Object *node_obj = node_var.operator Object *();
if (!node_obj) continue;
// Only include GPU-capable nodes (these are the ones that compile to GLSL)
Variant gpu_var = node_obj->call("supports_gpu");
if (gpu_var.get_type() != Variant::BOOL || !(bool)gpu_var) continue;
String title = node_obj->call("get_node_title");
int input_count = (int)(int64_t)node_obj->call("get_input_count");
for (int si = 0; si < input_count; si++) {
Variant val = node_obj->call("get_input_default", si);
// Only expose Float and Int parameters (not Geometry, Vector, etc.)
if (val.get_type() != Variant::FLOAT && val.get_type() != Variant::INT)
continue;
String name = node_obj->call("get_input_name", si);
if (name.is_empty()) name = "param_" + itos(si);
GraphParam gp;
gp.node_idx = ni;
gp.socket_idx = si;
gp.node_title = title;
gp.socket_name = name;
gp.type = val.get_type();
gp.default_value = val;
discovered_params_.push_back(gp);
}
}
UtilityFunctions::print("DCTerrainGeneratorProcNodes: Discovered ",
discovered_params_.size(), " parameters from graph");
}
// ── Dynamic Inspector Properties ──────────────────────────────────────────────
bool DCTerrainGeneratorProcNodes::_set(const StringName &p_name, const Variant &p_value) {
String name_str = p_name;
for (int i = 0; i < discovered_params_.size(); i++) {
GraphParam &gp = discovered_params_.ptrw()[i];
String prop_name = gp.node_title + String("/") + gp.socket_name;
if (prop_name == name_str) {
// Apply to the proc-nodes graph node
if (!graph_.is_null()) {
Variant node_var = graph_->call("get_node", gp.node_idx);
if (node_var.get_type() == Variant::OBJECT) {
Object *node_obj = node_var.operator Object *();
if (node_obj) {
node_obj->call("set_input_default", gp.socket_idx, p_value);
}
}
}
gp.default_value = p_value;
_recompile_glsl(); // Eager — rebake params into GLSL on main thread
emit_changed(); // Triggers DC terrain rebuild
return true;
}
}
return false;
}
bool DCTerrainGeneratorProcNodes::_get(const StringName &p_name, Variant &r_ret) const {
String name_str = p_name;
for (int i = 0; i < discovered_params_.size(); i++) {
const GraphParam &gp = discovered_params_[i];
String prop_name = gp.node_title + String("/") + gp.socket_name;
if (prop_name == name_str) {
r_ret = gp.default_value;
return true;
}
}
return false;
}
void DCTerrainGeneratorProcNodes::_get_property_list(List<PropertyInfo> *p_list) const {
for (int i = 0; i < discovered_params_.size(); i++) {
const GraphParam &gp = discovered_params_[i];
String prop_name = gp.node_title + String("/") + gp.socket_name;
Variant::Type vtype = (gp.type == Variant::INT) ? Variant::INT : Variant::FLOAT;
p_list->push_back(PropertyInfo(vtype, prop_name));
}
}
// ── GLSL Compilation ──────────────────────────────────────────────────────────
// ============================================================================
// A COMPILE FAILURE HERE IS SILENT, AND SILENCE IS THE BUG.
//
// Every early return below leaves cached_glsl_ empty. has_gpu_shader() then
// answers false, dc_worker_cpu takes the CPU path, and DCTerrain may swap in
// the BUILT-IN noise generator (dc_terrain.cpp, _propagate_config and
// set_generator) -- so the map renders a plausible landscape that is not the
// one the graph describes, with nothing in the log.
//
// proc-nodes' compile_to_glsl_function REFUSES on purpose: a node that claims
// GPU support and emits nothing would leave a variable nothing declares, so it
// returns "" and pushes its own error. That refusal used to arrive here and
// vanish. It does not any more.
//
// last_compile_error_ is bound so a headless gate can ASK, rather than a human
// having to notice a line scroll past. See hyper-games #64 NEXT 4.
// ============================================================================
void DCTerrainGeneratorProcNodes::_recompile_glsl() {
cached_glsl_.clear();
last_compile_error_ = String();
if (graph_.is_null()) {
last_compile_error_ = "no graph assigned";
// Not an error: an unconfigured generator is a legitimate state, and a
// scene that has not been wired up yet would scream on every load. It
// is recorded so a gate can still tell this apart from a real failure.
return;
}
// Extract noise parameters from discovered graph parameters.
float base_freq = 3.0f;
float height_scale = 5.0f;
for (int i = 0; i < discovered_params_.size(); i++) {
const GraphParam &gp = discovered_params_[i];
if (gp.node_title == "Gradient Noise" && gp.socket_name == "Scale") {
base_freq = (float)(double)gp.default_value;
}
if (gp.node_title == "Erosion Noise" && gp.socket_name == "HeightScale") {
height_scale = (float)(double)gp.default_value;
}
}
// Cross-extension call with octave clamping enabled.
// proc-nodes will emit min(N, pn_max_octaves) for all integer (octave) params.
// SKIP THE EROSION NODE when the toggle is off. Not in the graph's emission
// means not in the shader at all: no filter, and no noise library either,
// because nothing left in a heightfield graph mentions noise.
PackedStringArray skip;
if (!erosion_enabled_) {
skip.push_back("ErosionNoise");
}
Variant glsl_var = graph_->call("compile_to_glsl_function", "proc_density", true, skip);
if (glsl_var.get_type() != Variant::STRING) {
last_compile_error_ = "compile_to_glsl_function returned non-string "
"(is the proc-nodes extension loaded?)";
UtilityFunctions::push_error(
"DCTerrainGeneratorProcNodes: ", last_compile_error_,
" -- no GPU shader. The terrain will not be the graph's.");
return;
}
String proc_glsl = glsl_var;
if (proc_glsl.is_empty()) {
// THE REFUSAL. proc-nodes returns "" when a node claims GPU support and
// emits no GLSL, and it has already said which node. What it cannot know
// is that the caller was a terrain generator, so say what the refusal
// COSTS -- that is the half that was missing.
last_compile_error_ = "compile_to_glsl_function REFUSED (a node claims "
"GPU support and emitted no GLSL -- see the proc-nodes error "
"above it for which)";
UtilityFunctions::push_error(
"DCTerrainGeneratorProcNodes: ", last_compile_error_,
". No GPU shader: this terrain will mesh from the CPU wrapper, "
"which is a hardcoded gradient-noise -> erosion pipeline and NOT "
"your graph.");
return;
}
// Bridge proc-nodes UV-space noise to dc-terrain world-space SDF.
//
// proc-nodes noise (GradientNoise Scale=3.0) is designed for UV [0,1] evaluation,
// same as erosion_terrain.gdshader. dc-terrain evaluates at world meters.
//
// terrain_height controls the coordinate scale: XZ world coords are divided by it
// to convert to UV-like space. E.g., terrain_height=10 means the noise pattern
// that fills [0,1] in the shader now fills [0,10] meters.
//
// Height is controlled DIRECTLY by ErosionNoise HeightScale — no output multiplier.
// HeightScale=5 means ~2m of surface variation (same as the erosion shader test).
//
// pn_max_octaves must be declared BEFORE proc_density (which reads it).
String th_str = String::num(terrain_height_, 4);
String floor_str = String::num(floor_y_, 4);
String ceil_str_early = String::num(ceiling_y_, 4);
// ── HEIGHTFIELD ONLY ───────────────────────────────────────────────────
// Everything below this point -- the graph, the noise library, the mountain
// and relief and detail layers, the filter-space mapping -- is skipped. The
// density IS the field. If this does not show the block, nothing downstream
// of it can, and the fault is in getting the bytes to the GPU rather than in
// anything about terrain.
// NOT over a packed grid: the sampler emitted below is single-tile
// addressing, custom_data[j*HBLK_N + i], and with HBLK_N = the grid's texel
// extent that indexes 928M entries into a 55.8M buffer. The node's own
// prelude already emits the PAGED sampler.
if (heightfield_only_ && hblk_n_ > 0 && !hgrid_.active()) {
String hb =
String("const int HBLK_N = ") + itos(hblk_n_) + ";\n"
"const float HBLK_OX = " + String::num(hblk_ox_, 4) + ";\n"
"const float HBLK_OZ = " + String::num(hblk_oz_, 4) + ";\n"
"const float HBLK_MPP = " + String::num(hblk_mpp_, 6) + ";\n"
"float hblk_height_m(float x, float z) {\n"
" float u = (x - HBLK_OX) / HBLK_MPP;\n"
" float v = (z - HBLK_OZ) / HBLK_MPP;\n"
" u = clamp(u, 0.0, float(HBLK_N - 1));\n"
" v = clamp(v, 0.0, float(HBLK_N - 1));\n"
" int i0 = int(floor(u)); int j0 = int(floor(v));\n"
" int i1 = min(i0 + 1, HBLK_N - 1); int j1 = min(j0 + 1, HBLK_N - 1);\n"
" float fu = u - float(i0); float fv = v - float(j0);\n"
" float h00 = custom_data[j0*HBLK_N + i0]; float h10 = custom_data[j0*HBLK_N + i1];\n"
" float h01 = custom_data[j1*HBLK_N + i0]; float h11 = custom_data[j1*HBLK_N + i1];\n"
" return mix(mix(h00, h10, fu), mix(h01, h11, fu), fv);\n"
"}\n"
"float evaluate_density(float x, float y, float z) {\n"
" float d = hblk_height_m(x, z) - y;\n"
" d = max(d, (" + floor_str + " - y) * 2.0);\n"
" d = min(d, (" + ceil_str_early + " - y) * 2.0);\n"
" return d;\n"
"}\n";
CharString u8 = hb.utf8();
cached_glsl_ = std::string(u8.get_data(), u8.length());
UtilityFunctions::print("[DCTerrainGeneratorProcNodes] HEIGHTFIELD ONLY: ",
String::num_int64(cached_glsl_.size()), " bytes of GLSL, no graph, no erosion");
return;
}
String ceil_str = String::num(ceiling_y_, 4);
// y_scale maps [floor_y, ceiling_y] to proc_density's native [0, HeightScale]
float y_range = ceiling_y_ - floor_y_;
if (y_range < 0.01f) y_range = 0.01f;
// NINE DECIMALS, NOT SIX -- the same bug proc-nodes' gpu_compiler.cpp fixed
// in its own literal emission, still live here.
//
// String::num's second argument is DECIMAL PLACES, so for a value of about
// 2.4e-4 six of them leave THREE significant figures: 5/21000 =
// 0.000238095238 was emitted as 0.000238. The HeightfieldNode's matching
// constant is emitted by gpu_compiler at nine, so the two halves of the same
// mapping disagreed by 4.0e-4 relative -- and since the density is their
// difference, the surface came out where they balance rather than where the
// data says:
//
// height -5300 m -> density zero at -5306.466 m (-6.466 m)
// height 0 m -> -4.351 m (-4.351 m)
// height +165 m -> +160.715 m (-4.285 m)
// height +5300 m -> +5297.765 m (-2.235 m)
//
// (tools/oracles/density_precision.py). A smooth error, so it does not make
// the ground rough -- it just puts the world several metres from its own
// heightfield, which is why this map's spawn height had to be measured by
// hand instead of read from the block.
//
// Nine also makes the two literals IDENTICAL when the map derives the node's
// YScale from the same height_scale/y_range, so the difference is exact
// rather than merely closer.
// Cell size from the graph's own Erosion Noise node -- the same place the
// CPU path and the lookdev read it, so the three cannot drift.
float emit_cell_scale = 1.0f;
for (int i = 0; i < discovered_params_.size(); i++) {
const GraphParam &gp = discovered_params_[i];
if (gp.node_title == "Erosion Noise" && gp.socket_name == "CellScale")
emit_cell_scale = (float)(double)gp.default_value;
}
// HEIGHT SCALE CANNOT BE ZERO.
//
// It is the erosion node's HeightScale socket and the divisor of the whole
// vertical mapping, and the terrain panel gives it a slider that starts at
// 0. At zero, y_range / height_scale is infinite and String::num emits the
// literal "inf" -- which GLSL has no identifier for:
//
// ERROR: 0:1751: 'inf' : undeclared identifier
//
// The density shader then never compiles, EVERY chunk falls back, and the
// world has no terrain. It is sticky, too: the value came from a socket the
// panel writes and GameState persists durably, so it survives a restart and
// every later rebuild re-emits "inf".
//
// Emission must not be able to produce a shader that cannot compile.
if (!(height_scale > 1e-6f)) {
UtilityFunctions::push_warning(
"DCTerrainGeneratorProcNodes: Erosion Noise/HeightScale is ", height_scale,
" — clamping to 1e-6 so the shader still compiles.");
height_scale = 1e-6f;
}
String yscale_str = String::num(height_scale / y_range, 9);
String inv_yscale_str = String::num(y_range / height_scale, 9);
// detail_height_ is in METRES; mtn_offset is in units where 1.0 == y_range
// metres (y_mapped divides by y_scale = height_scale / y_range). Convert once
// here so the shader carries a plain fraction.
float detail_frac = (y_range > 0.01f) ? (detail_height_ / y_range) : 0.0f;
String mgain_str = String::num(mountain_gain_, 4);
String lac_str = String::num(noise_lacunarity_, 4);
String dscale_str = String::num(detail_scale_, 4);
String dfrac_str = String::num(detail_frac, 8);
String doct_str = itos(detail_octaves_);
String dgain_str = String::num(detail_gain_, 4);
String rscale_str = String::num(relief_scale_, 4);
String rfloor_str = String::num(relief_floor_, 4);
String mscale_str = String::num(mountain_scale_, 4);
String mheight_str = String::num(mountain_height_, 4);
String moct_str = itos(mountain_octaves_);
// ── Baked heightfield, if one is loaded ────────────────────────────────
// The SAMPLER is ours (storage is per-consumer); the metres -> filter-space
// CONVERSION comes from erosion_wrapper_glsl.h, the same text the standalone
// harness and its identity gate compile. That is the whole point of the
// split: a unit error here would be invisible in-engine and is exactly the
// class of bug that cost #45 a session.
String hblk_glsl_sampler;
String hblk_glsl;
// ONLY for the generator's OWN heightfield_path. When the data came from a
// NODE, that node emits its own sampler in its prelude -- emitting a second
// one here is a redefinition, and this older copy still names the buffer
// custom_data. FIXED: it named the buffer hblk_data, which stopped existing
// when dc-terrain moved to opaque custom_data. #55 section 2.3 recorded this
// as fixed; it was not, and the heightfield_only isolation oracle failed to
// compile on EVERY run -- "Custom shader failed, falling back" -- which is
// what made the map's own GPU/CPU oracle report a 9.1 m disagreement.
if (hblk_n_ > 0 && !heightfield_path_.is_empty() && !hgrid_.active()) {
// The sampler alone — HBLK_* consts plus hblk_height_m. Split out because
// the lookdev path needs ONLY this: erosion_wrapper_glsl emits its own
// M_* consts and its own heightmap_gn, and emitting ours as well would be
// a redefinition, which is precisely how this file once shipped a shader
// that never compiled and a world with no terrain.
hblk_glsl_sampler =
String("const int HBLK_N = ") + itos(hblk_n_) + ";\n"
"const float HBLK_OX = " + String::num(hblk_ox_, 4) + ";\n"
"const float HBLK_OZ = " + String::num(hblk_oz_, 4) + ";\n"
"const float HBLK_MPP = " + String::num(hblk_mpp_, 6) + ";\n"
"float hblk_height_m(float x, float z) {\n"
" float u = (x - HBLK_OX) / HBLK_MPP;\n"
" float v = (z - HBLK_OZ) / HBLK_MPP;\n"
" u = clamp(u, 0.0, float(HBLK_N - 1));\n"
" v = clamp(v, 0.0, float(HBLK_N - 1));\n"
" int i0 = int(floor(u)); int j0 = int(floor(v));\n"
" int i1 = min(i0 + 1, HBLK_N - 1); int j1 = min(j0 + 1, HBLK_N - 1);\n"
" float fu = u - float(i0); float fv = v - float(j0);\n"
" float h00 = custom_data[j0*HBLK_N + i0]; float h10 = custom_data[j0*HBLK_N + i1];\n"
" float h01 = custom_data[j1*HBLK_N + i0]; float h11 = custom_data[j1*HBLK_N + i1];\n"
" return mix(mix(h00, h10, fu), mix(h01, h11, fu), fv);\n"
"}\n"
// The five names erosion_heightmap_gn_glsl() reads. EN_HAMP is the
// filter's height_amp, which erosion_node_eval BAKES at 0.25 on the
// engine path -- the same 0.125 = HAMP*0.5 the mountain layer's
// "* 8.0" below is the inverse of.
;
hblk_glsl = hblk_glsl_sampler
+ "const float M_FLOOR_Y = " + floor_str + ";\n"
"const float M_CEIL_Y = " + ceil_str + ";\n"
"const float M_TH = " + th_str + ";\n"
"const float EN_HAMP = 0.25;\n"
"const float HBLK_STEP = " + String::num(hblk_mpp_, 6) + ";\n"
+ String(erosion_heightmap_gn_glsl().c_str());
}
// ── LOOKDEV WRAPPER ────────────────────────────────────────────────────
if (lookdev_wrapper_ && hblk_n_ > 0) {
ErosionWrapperParams wp;
wp.floor_y = floor_y_;
wp.ceiling_y = ceiling_y_;
wp.terrain_height = terrain_height_;
wp.noise_lacunarity = noise_lacunarity_;
wp.detail_scale = detail_scale_;
wp.detail_height = detail_height_;
wp.detail_gain = detail_gain_;
wp.detail_octaves = detail_octaves_;
wp.en_erosion_scale = erosion_scale_;
wp.field_half_range = field_half_range_;
wp.voxel_size = 1.0f;
wp.heightmap_source = true;
wp.heightmap_step = hblk_mpp_;
for (int i = 0; i < discovered_params_.size(); i++) {
const GraphParam &gp = discovered_params_[i];
if (gp.node_title != "Erosion Noise") continue;
if (gp.socket_name == "HeightScale") wp.en_height_scale = (float)(double)gp.default_value;
else if (gp.socket_name == "ErosionStr") wp.en_strength = (float)(double)gp.default_value;
else if (gp.socket_name == "SlopePower") wp.en_slope_power = (float)(double)gp.default_value;
else if (gp.socket_name == "CellScale") wp.en_cell_scale = (float)(double)gp.default_value;
else if (gp.socket_name == "Octaves") wp.en_octaves = (int)gp.default_value;
else if (gp.socket_name == "Gain") wp.en_gain = (float)(double)gp.default_value;
}
String lk = String("int pn_max_octaves = 99;\n") + proc_glsl + "\n" + hblk_glsl_sampler
+ String(erosion_wrapper_glsl(wp).c_str())
+ "float evaluate_density(float x, float y, float z) {\n"
" return gpu_wrapper(x, y, z);\n"
"}\n";
CharString u8 = lk.utf8();
cached_glsl_ = std::string(u8.get_data(), u8.length());
UtilityFunctions::print("[DCTerrainGeneratorProcNodes] LOOKDEV WRAPPER: ",
String::num_int64(cached_glsl_.size()), " bytes; detail_scale=",
detail_scale_, " (0 = heightmap + erosion ONLY)");
return;
}
// DOES THE GRAPH BRING THE NOISE LIBRARY?
//
// compile_to_function emits it only when a node mentions noise, and this
// wrapper calls fractal_* and assigns pn_terrain_offset / pn_fade_div /
// pn_erosion_scale -- all of which the LIBRARY declares. A graph that needs
// no noise (a heightfield node on its own) leaves every one of those
// undeclared and the shader does not compile, which the map experiences as
// a silent fallback. So the wrapper's noise layers are emitted only when
// there is a library to support them.
// ── EROSION-MINIMAL WRAPPER ────────────────────────────────────────────
//
// The heightmap is the terrain and the filter is the enhancement, so this
// emits exactly those two and the mapping between them:
//
// proc_density = Heightfield -> Erosion (the graph, unchanged)
// evaluate_density = y-map, call, rescale, clip
//
// No mountain, no relief, no detail, no det_offset. Compare with the
// general path below, which carries all three because it was written when
// the mountain was the terrain -- on this map they contribute nothing but
// branches, and a branch that can be mis-gated is how the filter's own
// parameters went unset for weeks (hyper-games #53 section 2).
//
// The erosion knobs are still emitted, and still gated on the noise library
// that DECLARES them: with erosion_enabled false there is no library, and
// assigning pn_ero_* would name identifiers that do not exist.
if (erosion_minimal_) {
const bool lib = proc_glsl.find("fractal_noise_2d_lod") != -1;
String mm =
"// EROSION-MINIMAL: heightfield + erosion filter, and nothing else.\n"
"int pn_max_octaves;\n\n"
+ proc_glsl + "\n"
+ hblk_glsl +
"float evaluate_density(float x, float y, float z) {\n"
" float inv_scale = 1.0 / " + th_str + ";\n"
" pn_max_octaves = 99;\n"
+ (lib ? String(
" pn_fade_div = 4.8;\n"
" pn_erosion_scale = " + String::num(erosion_scale_, 6) + ";\n"
" pn_ero_input_height_amplitude = " + String::num(input_height_amplitude_, 9) + ";\n"
" pn_ero_octave_lacunarity = " + String::num(octave_lacunarity_, 9) + ";\n"
" pn_ero_gully_depth = " + String::num(gully_depth_, 9) + ";\n"
" pn_ero_fine_gully_slope_restriction = " + String::num(fine_gully_slope_restriction_, 9) + ";\n"
" pn_ero_gully_normalize_threshold = " + String::num(gully_normalize_threshold_, 9) + ";\n"
" pn_ero_height_offset = " + String::num(height_offset_, 9) + ";\n"
" pn_ero_height_offset_follows_altitude = " + String::num(height_offset_follows_altitude_, 9) + ";\n"
" pn_ero_crease_rounding = " + String::num(crease_rounding_, 9) + ";\n"
" pn_ero_ridge_rounding = " + String::num(ridge_rounding_, 9) + ";\n"
" pn_ero_input_feature_rounding = " + String::num(input_feature_rounding_, 9) + ";\n"
" pn_ero_rounding_per_octave = " + String::num(rounding_per_octave_, 9) + ";\n"
" pn_ero_gully_onset_slope = " + String::num(gully_onset_slope_, 9) + ";\n"
" pn_ero_octave_onset_slope = " + String::num(octave_onset_slope_, 9) + ";\n"
" pn_ero_ridge_onset_slope = " + String::num(ridge_onset_slope_, 9) + ";\n"
" pn_ero_ridge_octave_onset = " + String::num(ridge_octave_onset_, 9) + ";\n"
" pn_ero_assumed_slope = " + String::num(assumed_slope_, 9) + ";\n"
" pn_ero_assumed_slope_mix = " + String::num(assumed_slope_mix_, 9) + ";\n"
// The coarsest gully spans terrain_height * gully_band_scale *
// cell_size in world metres and halves per octave; nyquist_octaves
// of that against this chunk's voxel is how many octaves it can
// actually resolve.
" pn_ero_max_octaves = nyquist_octaves(" +
String::num(terrain_height_ * erosion_scale_ * emit_cell_scale, 6) +
", voxel_scale, 2.0);\n") : String())
+ " float y_mapped = (y - " + floor_str + ") * " + yscale_str + ";\n"
" float d = proc_density(vec3(x * inv_scale, y_mapped, z * inv_scale));\n"
" d *= " + inv_yscale_str + ";\n"
" d = max(d, (" + floor_str + " - y) * 2.0);\n"
" d = min(d, (" + ceil_str + " - y) * 2.0);\n"
" return d;\n"
"}\n";
CharString u8 = mm.utf8();
cached_glsl_ = std::string(u8.get_data(), u8.length());
UtilityFunctions::print("[DCTerrainGeneratorProcNodes] EROSION-MINIMAL: ",
String::num_int64(cached_glsl_.size()), " bytes (erosion ",
erosion_enabled_ ? "ON" : "OFF", ")");
return;
}
const bool has_noise_lib = proc_glsl.find("fractal_noise_2d_lod") != -1;
const bool want_mountain = has_noise_lib && mountain_height_ != 0.0f;
const bool want_detail = has_noise_lib && detail_scale_ > 0.0f;
// THE MOUNTAIN ASSIGNMENT, with the land/sea curve folded in.
//
// The curve goes HERE -- on the mountain layer, BEFORE the erosion filter
// -- because that is the order the landscape happens in: continents and
// ocean basins are the shape, erosion is what runs over it. Applying it
// after erosion would stretch gullies wherever the curve is steep.
//
// ⚠ THE GRADIENT MUST BE REMAPPED TOO. The filter is slope-aware and
// steers gullies by the gradient the wrapper hands it, so a height that
// has been remapped and a gradient that has not describes a surface that
// does not exist. hypso_curve returns dout/dh for exactly this.
//
// Disabled emits the ORIGINAL line, byte for byte, so `hypso_enabled 0`
// is not "the curve with neutral parameters" -- it is the previous shader.
String mtn_assign = (hypso_enabled_ != 0.0f)
? String(
" vec2 hc = hypso_curve(mtn.x, ")
+ String::num(hypso_sea_, 6) + ", " + String::num(hypso_shelf_width_, 6) + ", "
+ String::num(hypso_slope_width_, 6) + ", "
+ String::num(hypso_abyss_, 6) + ", " + String::num(hypso_plat_slope_, 6) + ",\n"
" "
+ String::num(hypso_mtn_onset_, 6) + ", " + String::num(hypso_mtn_width_, 6) + ", "
+ String::num(hypso_mtn_slope_, 6) + ", " + String::num(hypso_peak_onset_, 6) + ", "
+ String::num(hypso_peak_gain_, 6) + ");\n"
// ⚠ mtn_amp IS NOT USED HERE, and that is deliberate.
//
// mtn_amp = mountain_height * relief_amp, and relief_amp varies
// SPATIALLY from relief_floor (0.08) to 1.0 -- a 12.5x swing.
// Scaling the curve's output by it would put sea level at a
// different world Y in every region, and a single water plane
// then cannot match the shoreline anywhere. The curve defines
// where the sea is; that cannot be a function of position.
//
// So the hypso path uses mountain_height ALONE. The cost is that
// relief_scale/relief_floor stop modulating how mountainous a
// region is while the curve is on. Re-coupling them belongs on
// the curve's INPUT (or on mtn_slope/peak_gain), not on its
// output, and is deliberately not done here.
" pn_terrain_offset = vec3(hc.x, mtn.y * " + mscale_str + " * hc.y, mtn.z * "
+ mscale_str + " * hc.y) * " + mheight_str + " * 8.0;\n"
// EROSION IS SUBAERIAL. hc.x is the elevation BEFORE the filter runs,
// so the column's depth is known here and the mask can be set before
// proc_density is entered. Metres per curve unit is mountain_height *
// inv_yscale: pn_terrain_offset carries hc.x * mheight * 8 and reaches
// the surface through a factor of 0.125, so the base surface in mapped
// METRES PER CURVE UNIT = mountain_height * erosion height_scale *
// inv_yscale. The full chain: pn_terrain_offset carries hc.x * mheight
// * 8, gn reaches the surface through input_height_amplitude * 0.5 =
// 0.125, and erosion_node_eval scales the result by height_scale. So
// 8 * 0.125 * height_scale = height_scale.
//
// ⚠ The height_scale factor was MISSING at first, giving 0.55 * 4200 =
// 2310 m/unit against a world whose own relief readout says 11550 --
// exactly 5x out, which is height_scale. Caught by comparing the
// emitted constant against a number the map already prints.
+ (erosion_mask_depth_ > 0.0f ? String(
" pn_erosion_mask = 1.0 - smoothstep(0.0, ")
+ String::num(erosion_mask_depth_, 3) + ", ("
+ String::num(get_waterline_norm(), 8) + " - hc.x) * "
+ String::num(mountain_height_ * erosion_height_scale_, 6)
+ " * " + inv_yscale_str + ");\n"
: String())
: String(" pn_terrain_offset = vec3(mtn.x, mtn.y * ") + mscale_str
+ ", mtn.z * " + mscale_str + ") * mtn_amp * 8.0;\n";
String adapted =
"// Octave limit — fixed high value for LOD-consistent surface position.\n"
"// Nyquist clamping shifts the erosion surface between LOD levels, causing\n"
"// chunks to disappear during refinement. Consistent octaves = consistent surface.\n"
"int pn_max_octaves;\n\n"
// pn_terrain_offset / pn_fade_div are NOT declared here. The noise
// library (gpu_compiler.cpp) already declares them with defaults so a
// shader with no wrapper still compiles, and the library is emitted
// INSIDE proc_glsl below -- i.e. before evaluate_density -- so this
// function can just assign to them.
//
// Declaring them here as well is what shipped a dead map: two
// declarations in one translation unit is
// ERROR: 0:1272: 'pn_terrain_offset' : redefinition
// the density shader never compiles, every chunk comes back empty, and
// the symptom is a world with no terrain and a perfectly healthy log
// right up to the shader compile. pn_max_octaves is different: nothing
// else declares it, so it stays here.
+ proc_glsl + "\n"
+ hblk_glsl +
"float evaluate_density(float x, float y, float z) {\n"
" float inv_scale = 1.0 / " + th_str + ";\n"
" pn_max_octaves = 99;\n"
" // MOUNTAIN LAYER, height AND gradient. The gradient used to be computed and\n"
" // thrown away: mtn_offset was added to y_mapped AFTER the erosion, so the\n"
" // filter never saw the range it was carving and aligned its gullies to gn\n"
" // alone -- three octaves at gain 0.1, i.e. one smooth 6.7 km blob. It read\n"
" // as noise laid over a mountain rather than erosion of one.\n"
" // MULTIFRACTAL. variance 0 is bit-identical to fractal_noise_2d, so the\n"
" // default costs nothing; above 0 each octave is weighted by the elevation\n"
" // accumulated so far, putting the roughness on high ground. Plain fBm is\n"
" // statistically homogeneous -- equally rugged in the basins and on the\n"
" // peaks -- which is what reads as every range looking the same.\n"
" // NYQUIST LIMITS. voxel_scale is the LOD's voxel size in metres and is\n"
" // declared by BOTH shaders this body is injected into (density_fill.glsl\n"
" // and edge_intersect.glsl), above the injection point. One cycle of a\n"
" // layer spans terrain_height/scale world metres, so that is its base\n"
" // wavelength. Fractional, so an octave fades in as a chunk refines\n"
" // rather than snapping on and moving the surface underfoot.\n"
+ (want_mountain ? String(
" float mtn_lim = nyquist_octaves(" + th_str + " / max(" + mscale_str + ", 1e-6), voxel_scale, " + lac_str + ");\n"
" float rel_lim = nyquist_octaves(" + th_str + " / max(" + rscale_str + ", 1e-6), voxel_scale, 2.0);\n"
" float det_lim = nyquist_octaves(" + th_str + " / max(" + dscale_str + ", 1e-6), voxel_scale, " + lac_str + ");\n"
" vec3 mtn = fractal_multifractal_2d_lod(vec2(x, z) * inv_scale * " + mscale_str + ", 1.0, " + moct_str + ", " + lac_str + ", " + mgain_str + ", " + String::num(mountain_variance_, 4) + ", mtn_lim);\n"
" float relief_amp = 1.0;\n"
" if (" + rscale_str + " > 0.0) {\n"
" vec3 rel = fractal_noise_2d_lod(vec2(x, z) * inv_scale * " + rscale_str + ", 1.0, 2, 2.0, 0.5, rel_lim);\n"
" float t = clamp(rel.x * 0.5 + 0.5, 0.0, 1.0);\n"
" relief_amp = " + rfloor_str + " + (1.0 - " + rfloor_str + ") * t;\n"
" }\n"
" float mtn_amp = " + mheight_str + " * relief_amp;\n"
" // mtn is evaluated at p * mountain_scale, so d/dp = mtn.yz * mountain_scale.\n"
" // d(relief_amp)/dp is dropped: relief is a 40 km envelope, orders coarser\n"
" // than anything gully direction responds to. The 8.0 is exact, not fitted --\n"
" // gn reaches the surface as gn.x * HEIGHT_AMP * 0.5 = gn.x * 0.125, so\n"
" // dividing the offset by 0.125 reproduces the identical base surface at\n"
" // zero erosion strength.\n"
+ (hblk_n_ > 0
? String(" pn_terrain_offset = heightmap_gn(x, z);\n")
: mtn_assign)
+
" // DETAIL rides ON TOP of the eroded surface and is deliberately NOT folded\n"
" // into pn_terrain_offset: its gradient carries a factor of detail_scale, so\n"
" // inside one gully cell the direction flips several times and the stripes go\n"
" // chaotic -- measured 77.6 m of relief across a 2 m step, i.e. vertical\n"
" // noise. Detail is what water runs over, not what steers it.\n"
) : String())
// GATED ON THE NOISE LIBRARY, because that is what DECLARES every name
// below. A graph with no erosion node -- a heightfield on its own, which
// is what the map's EROSION_ENABLED toggle produces -- brings no library,
// and assigning to pn_fade_div / pn_ero_* / calling nyquist_octaves then
// references identifiers that do not exist and the shader does not
// compile. Same reason want_mountain and want_detail are gated.
+ (has_noise_lib ? String(
" pn_fade_div = 4.8;\n"
" pn_erosion_scale = " + String::num(erosion_scale_, 6) + ";\n"
" pn_ero_input_height_amplitude = " + String::num(input_height_amplitude_, 9) + ";\n"
" pn_ero_octave_lacunarity = " + String::num(octave_lacunarity_, 9) + ";\n"
" pn_ero_gully_depth = " + String::num(gully_depth_, 9) + ";\n"
" pn_ero_fine_gully_slope_restriction = " + String::num(fine_gully_slope_restriction_, 9) + ";\n"
" pn_ero_gully_normalize_threshold = " + String::num(gully_normalize_threshold_, 9) + ";\n"
" pn_ero_height_offset = " + String::num(height_offset_, 9) + ";\n"
" pn_ero_height_offset_follows_altitude = " + String::num(height_offset_follows_altitude_, 9) + ";\n"
" pn_ero_crease_rounding = " + String::num(crease_rounding_, 9) + ";\n"
" pn_ero_ridge_rounding = " + String::num(ridge_rounding_, 9) + ";\n"
" pn_ero_input_feature_rounding = " + String::num(input_feature_rounding_, 9) + ";\n"
" pn_ero_rounding_per_octave = " + String::num(rounding_per_octave_, 9) + ";\n"
" pn_ero_gully_onset_slope = " + String::num(gully_onset_slope_, 9) + ";\n"
" pn_ero_octave_onset_slope = " + String::num(octave_onset_slope_, 9) + ";\n"
" pn_ero_ridge_onset_slope = " + String::num(ridge_onset_slope_, 9) + ";\n"
" pn_ero_ridge_octave_onset = " + String::num(ridge_octave_onset_, 9) + ";\n"
" pn_ero_assumed_slope = " + String::num(assumed_slope_, 9) + ";\n"
" pn_ero_assumed_slope_mix = " + String::num(assumed_slope_mix_, 9) + ";\n"
" pn_ero_max_octaves = nyquist_octaves(" +
String::num(terrain_height_ * erosion_scale_ * emit_cell_scale, 6) +
", voxel_scale, 2.0);\n"
) : String())
+ String(" float det_offset = 0.0;\n")
+ (want_detail ? String(
" if (" + dscale_str + " > 0.0) {\n"
" vec3 det = fractal_noise_2d_lod(vec2(x, z) * inv_scale * " + dscale_str + ", 1.0, " + doct_str + ", " + lac_str + ", " + dgain_str + ", det_lim);\n"
// DETAIL BELONGS ON GENTLE GROUND, NOT EVERYWHERE.
//
// It was applied flat, so the same 100 m mottling sat on flats,
// cliff faces, ridges and gully floors alike -- and because the
// material reads the NORMAL, that mottling propagated straight
// into the colour, which is why the terrain read as crumpled foil
// rather than as landforms.
//
// Physically it is soil, scree and regolith: it ACCUMULATES on low
// slopes and is stripped from steep faces. Masking by slope is the
// same statement.
//
// The gradient is free -- pn_terrain_offset.yz is the mountain
// slope computed a few lines above, in gn units per unit p. No
// extra evaluation, no new field.
" float det_slope = length(vec2(pn_terrain_offset.y, pn_terrain_offset.z));\n"
" float det_mask = 1.0 - smoothstep(" + String::num(detail_slope_lo_, 6)
+ ", " + String::num(detail_slope_hi_, 6) + ", det_slope);\n"
" det_offset = det.x * " + dfrac_str + " * det_mask;\n"
" }\n"
) : String())
+ (height_space_sqrt_
? String(" float un = (y - ") + floor_str + ") / " + String::num(y_range, 4) + ";\n"
" float y_mapped = sign(un) * sqrt(abs(un)) * " + String::num(height_scale, 4) +
" - det_offset * " + String::num(height_scale, 4) + ";\n"
: String(" float y_mapped = (y - ") + floor_str + ") * " + yscale_str +
" - det_offset * " + String::num(height_scale, 4) + ";\n")
+ ""
+ ""
" float d = proc_density(vec3(x * inv_scale, y_mapped, z * inv_scale));\n"
" d *= " + inv_yscale_str + ";\n"
// pn_out_grad LEAVES proc_density IN NODE SPACE: d(mapped height)/d(p),
// with p = world / terrain_height. A consumer that wants a surface
// normal needs d(world y)/d(world x), and the two constants that close
// that gap are emitted right here as literals. Converting in the
// consumer would mean copying 1/60000 and 4200 into a shader by hand --
// exactly the multi-stage rescale this file has already got wrong once
// (the waterline, off by 3.4x). Do it where the constants live.
//
// ⚠ det_offset's slope is NOT in here. Detail is subtracted from
// y_mapped and its gradient is deliberately not folded into
// pn_terrain_offset (see the DETAIL comment above: the direction flips
// several times inside one gully cell). The normal this yields is the
// ERODED MOUNTAIN's, which is the structure that survives to distance.
+ (height_space_sqrt_
? String(
" // sqrt height space: the mapped->world conversion is not linear and\n"
" // the inverse is not derived here. Zeroed rather than scaled by a\n"
" // constant that would be quietly wrong -- a consumer sees FLAT, which\n"
" // is visible, instead of a normal tilted by an unaudited factor.\n"
" pn_out_grad = vec2(0.0);\n")
: String(
" pn_out_grad *= inv_scale * ") + inv_yscale_str + ";\n")
// ── SUBMARINE TERRAIN ──────────────────────────────────────────
// Geiss (GPU Gems 3 ch.1): add a 3D fBm to a heightfield density and
// the surface stops being single-valued -- caves, arches and overhangs
// appear wherever the 3D term outruns the vertical gradient. A
// heightfield seabed cannot have any of those, and the whole world is
// meant to be reachable.
//
// GATED ON DEPTH, not applied globally. sea_depth uses the UNPERTURBED
// surface (y + d before this line), so the weight is a property of the
// column and cannot feed back on itself. The first tens of metres stay
// a pure heightfield because beaches and the waterline need a
// single-valued surface -- a cave mouth at the shoreline has no clean
// coastline, and the ocean's depth query assumes d = H - y holds.
+ (submarine_amp_ > 0.0f ? String(
" // Submarine volumetric seabed, below a smooth coastal buffer.\n"
" float sea_depth = ") + String::num(submarine_water_y_, 3) + " - (y + d);\n"
" float sub_w = smoothstep(" + String::num(submarine_buffer_start_, 3) + ", "
+ String::num(submarine_buffer_end_, 3) + ", sea_depth);\n"
" if (sub_w > 0.0) {\n"
" float sub_f = 1.0 / " + String::num(submarine_wavelength_, 4) + ";\n"
// AMPLITUDE IS CAPPED BY THE WATER OVER IT. A 1500 m feature in 400 m
// of water pushes the seabed straight through the surface and puts an
// island where there should be a reef. Half the local depth is the
// most that can be added without breaching, and it makes the caves
// scale with the basin they are in -- big ones offshore, gentle relief
// in the shallows, which is what a real seabed does.
" float sub_a = min(" + String::num(submarine_amp_, 4)
+ ", max(sea_depth, 0.0) * 0.5);\n"
" d += ps_fbm_noise_3d(x * sub_f, y * sub_f, z * sub_f, "
+ String::num_int64(submarine_octaves_) + ", "
+ String::num(submarine_roughness_, 4) + ", 2.0) * sub_a * sub_w;\n"
" }\n" : String())
+ String(" // Clip: solid below floor_y, air above ceiling_y.\n"
" d = max(d, (") + floor_str + " - y) * 2.0);\n"
" d = min(d, (" + ceil_str + " - y) * 2.0);\n"
" return d;\n"
"}\n";
CharString utf8 = adapted.utf8();
cached_glsl_ = std::string(utf8.get_data(), utf8.length());
UtilityFunctions::print("DCTerrainGeneratorProcNodes: Compiled GLSL (",
String::num_int64(cached_glsl_.size()), " bytes, base_freq=",
String::num(base_freq, 4), ", terrain_height=",
String::num(terrain_height_, 1), ")");
}
// ── ProcNodesBridge ────────────────────────────────────────────────────────────
// ============================================================================
// THE DENSITY WRAPPER, SPLIT IN TWO.
//
// It used to be one function, and every line of it ran PER VOXEL -- including
// two linear scans over discovered_params_ comparing Godot Strings, to answer
// questions ("what is Erosion Noise's Gain", "is there an Erosion Noise node
// at all") whose answers cannot change between two points of the same grid.
// DensityGrid::fill asks 33^3 = 35937 of those per 32^3 chunk.
//
// So: _resolve_eval_params does the part that depends on the GRAPH and the LOD,
// once; _eval_at does the part that depends on the POINT. evaluate() is the two
// in sequence and is unchanged in behaviour; evaluate_batch() resolves once and
// loops, which is what dc_worker_cpu's fill now calls.
//
// See hyper-games #64 NEXT 2. ⚠ test_erosion_parity.cpp, which that item names
// as the gate, CANNOT SEE THIS FUNCTION -- it restates both wrappers itself and
// its own header says so. demo/procnodes_eval_probe.gd is the instrument that
// can: it drives sample_density through the real bridge, and this split was
// verified bit-identical across 2601 samples with it.
// ============================================================================
DCTerrainGeneratorProcNodes::EvalParams
DCTerrainGeneratorProcNodes::_resolve_eval_params(float lod_voxel_size) const {
EvalParams p;
p.lod_voxel_size = lod_voxel_size;
// lod_voxel_size USED to be discarded here, which is why this generator had
// no Nyquist protection on any layer: the wrapper passed literal octave
// counts and the graph nodes' min(N, pn_max_octaves) is disarmed by
// pn_max_octaves = 99. DCTerrain::sample_density passes 0 on purpose
// (dc_terrain.cpp:2841) and nyquist_octaves reads that as "no limit", so
// collision keeps evaluating the full field.
// Read graph parameters (same values as the GLSL adapter uses)
float terrain_height = terrain_height_;
float floor_y = floor_y_;
float ceiling_y = ceiling_y_;
float mtn_scale = mountain_scale_;
float mtn_height = mountain_height_;
int mtn_octaves = mountain_octaves_;
// Extract noise params from discovered graph parameters
// gn_detail defaults to ZERO, not 3. These defaults stand in for a node that
// is not in the graph, and a graph with no Gradient Noise node has no base
// fBm -- defaulting to 3 invented three octaves the GPU never evaluates, and
// the CPU and GPU then disagree by exactly that. The loop below overwrites
// this when the node IS present.
float base_freq = 3.0f, gn_detail = 0, gn_gain = 0.1f;
float en_height_scale = 5.0f, en_erosion_str = 0.16f, en_slope_power = 0.6f;
float en_cell_scale = 1.0f; int en_octaves = 5; float en_gain = 0.5f;
// MountainNode's sockets. Height 0 means "no mountain node in this graph",
// which is the honest default: a layer that is not in the graph contributes
// nothing. The generator's own mountain_* properties are not read here any
// more -- they are what made the CPU compute a layer the shader had stopped
// emitting, and vice versa.
for (int i = 0; i < discovered_params_.size(); i++) {
const auto &gp = discovered_params_[i];
if (gp.node_title == "Gradient Noise" && gp.socket_name == "Scale")
base_freq = (float)(double)gp.default_value;
if (gp.node_title == "Gradient Noise" && gp.socket_name == "Detail")
gn_detail = (int)gp.default_value;
if (gp.node_title == "Gradient Noise" && gp.socket_name == "Gain")
gn_gain = (float)(double)gp.default_value;
if (gp.node_title == "Erosion Noise" && gp.socket_name == "HeightScale")
en_height_scale = (float)(double)gp.default_value;
if (gp.node_title == "Erosion Noise" && gp.socket_name == "ErosionStr")
en_erosion_str = (float)(double)gp.default_value;
if (gp.node_title == "Erosion Noise" && gp.socket_name == "SlopePower")
en_slope_power = (float)(double)gp.default_value;
if (gp.node_title == "Erosion Noise" && gp.socket_name == "CellScale")
en_cell_scale = (float)(double)gp.default_value;
if (gp.node_title == "Erosion Noise" && gp.socket_name == "Octaves")
en_octaves = (int)gp.default_value;
if (gp.node_title == "Erosion Noise" && gp.socket_name == "Gain")
en_gain = (float)(double)gp.default_value;
}
float inv_scale = 1.0f / terrain_height;
float y_range = ceiling_y - floor_y;
if (y_range < 0.01f) y_range = 0.01f;
float y_scale = en_height_scale / y_range;
float inv_y_scale = y_range / en_height_scale;
// DOES THE GRAPH ACTUALLY HAVE AN EROSION NODE?
//
// This function is a hardcoded pipeline -- gn, then terrain_offset, then
// erosion_node_eval -- written when the graph was always GradientNoise ->
// ErosionNoise. A graph of one Heightfield node runs none of that on the
// GPU, where the node sets pos.y itself, so the CPU answered from a pipeline
// the shader no longer runs and reported a surface 5.3 km out. sample_density
// drives the spawn, _lift_out_of_terrain and the fall-respawn, so that is a
// player floating kilometres off the ground.
//
// The honest fix is a CPU evaluator that walks the graph the way the GPU
// compiler does. Until then, match the two shapes that exist.
bool graph_has_erosion = false;
for (int i = 0; i < discovered_params_.size(); i++) {
if (discovered_params_[i].node_title == "Erosion Noise") {
graph_has_erosion = true;
break;
}
}
p.terrain_height = terrain_height;
p.floor_y = floor_y;
p.ceiling_y = ceiling_y;
p.mtn_scale = mtn_scale;
p.mtn_height = mtn_height;
p.mtn_octaves = mtn_octaves;
p.base_freq = base_freq;
p.gn_detail = gn_detail;
p.gn_gain = gn_gain;
p.en_height_scale = en_height_scale;
p.en_erosion_str = en_erosion_str;
p.en_slope_power = en_slope_power;
p.en_cell_scale = en_cell_scale;
p.en_octaves = en_octaves;
p.en_gain = en_gain;
p.inv_scale = inv_scale;
p.y_range = y_range;
p.y_scale = y_scale;
p.inv_y_scale = inv_y_scale;
p.graph_has_erosion = graph_has_erosion;
return p;
}
float DCTerrainGeneratorProcNodes::_eval_at(const EvalParams &p,
float x, float y, float z) const {
// The resolved constants, under the names they had when this was one
// function, so every line of arithmetic below is UNCHANGED TEXT rather than
// a rename. That was the whole point of the split: the wrapper this lives
// in once passed erosion_filter a `scale` 60x out and put the surface you
// walk on 4 km from the one you see, and a refactor of it has to be
// provably the same function, not carefully reviewed.
const float terrain_height = p.terrain_height;
const float floor_y = p.floor_y;
const float ceiling_y = p.ceiling_y;
const float mtn_scale = p.mtn_scale;
const float mtn_height = p.mtn_height;
const int mtn_octaves = p.mtn_octaves;
const float base_freq = p.base_freq;
const float gn_detail = p.gn_detail;
const float gn_gain = p.gn_gain;
const float en_height_scale = p.en_height_scale;
const float en_erosion_str = p.en_erosion_str;
const float en_slope_power = p.en_slope_power;
const float en_cell_scale = p.en_cell_scale;
const int en_octaves = p.en_octaves;
const float en_gain = p.en_gain;
const float inv_scale = p.inv_scale;
const float y_range = p.y_range;
const float y_scale = p.y_scale;
const float inv_y_scale = p.inv_y_scale;
const bool graph_has_erosion = p.graph_has_erosion;
const float lod_voxel_size = p.lod_voxel_size;
// Mountain layer. dcerosion::fractal_noise_2d is THE one from
// erosion_filter.inc — the same text the shader compiles — so this layer
// and its GLSL twin above can no longer disagree about the noise itself.
// What they can still disagree about is the ALGEBRA around it, which is
// why the "MUST stay identical" notes below are still here and still mean
// it. Hoisting this wrapper the way erosion_node_eval was hoisted is the
// remaining piece.
using dcerosion::vec2;
using dcerosion::vec3;
if (!graph_has_erosion && hblk_n_ > 0) {
// Heightfield alone: density IS height minus query, exactly what the
// node emits. No filter space, no offsets, nothing to get wrong.
const float hmpp = hblk_mpp_;
float d = _hblk_sample_cpu(x, z) - y;
d = MAX(d, (floor_y - y) * 2.0f);
d = MIN(d, (ceiling_y - y) * 2.0f);
return d;
}
// HEIGHTFIELD ONLY — the CPU twin, and deliberately trivial. There is no
// algebra to get wrong here, so if the world still looks like noise with
// this on, the field is not reaching the GPU and this is what you are
// standing on instead.
if (heightfield_only_ && hblk_n_ > 0) {
const float hmpp = hblk_mpp_;
float d = _hblk_sample_cpu(x, z) - y;
d = MAX(d, (floor_y - y) * 2.0f);
d = MIN(d, (ceiling_y - y) * 2.0f);
return d;
}
// MUST match the GLSL above: same base wavelengths, same lacunarities.
const float lac = noise_lacunarity_;
float mtn_lim = dcerosion::nyquist_octaves(terrain_height / MAX(mtn_scale, 1e-6f),
lod_voxel_size, lac);
float rel_lim = dcerosion::nyquist_octaves(
terrain_height / MAX(relief_scale_, 1e-6f), lod_voxel_size, 2.0f);
float det_lim = dcerosion::nyquist_octaves(
terrain_height / MAX(detail_scale_, 1e-6f), lod_voxel_size, lac);
vec3 mtn = dcerosion::fractal_multifractal_2d_lod(vec2(x, z) * inv_scale * mtn_scale, 1.0f,
mtn_octaves, lac, mountain_gain_,
mountain_variance_, mtn_lim);
// Relief field — MUST stay identical to the GLSL above.
float relief_amp = 1.0f;
if (relief_scale_ > 0.0f) {
vec3 rel = dcerosion::fractal_noise_2d_lod(vec2(x, z) * inv_scale * relief_scale_,
1.0f, 2, 2.0f, 0.5f, rel_lim);
float t = dcerosion::clamp(rel.x * 0.5f + 0.5f, 0.0f, 1.0f);
relief_amp = relief_floor_ + (1.0f - relief_floor_) * t;
}
float mtn_amp = mtn_height * relief_amp;
// The mountain's HEIGHT AND GRADIENT, in gn units, to hand to the erosion —
// the pn_terrain_offset the GLSL sets.
// THE LAND/SEA CURVE, mirroring the GLSL emission exactly. Without this
// the CPU answers from the un-remapped fBm while the GPU meshes the
// remapped one -- the meshed surface and the collidable surface differ,
// which is the failure this function's own note below was written about.
//
// mountain_height alone, NOT mtn_amp: relief_amp varies spatially and
// scaling the curve by it would move sea level per region. Same reason,
// same line, as the emission site.
vec3 terrain_offset;
// Hoisted: the subaerial erosion mask below needs the pre-erosion elevation,
// and hc is otherwise scoped to the branch that computes it.
float pre_erosion_h = mtn.x;
if (hypso_enabled_ != 0.0f) {
dcerosion::vec2 hc = dcerosion::hypso_curve(mtn.x,
hypso_sea_, hypso_shelf_width_, hypso_slope_width_, hypso_abyss_, hypso_plat_slope_,
hypso_mtn_onset_, hypso_mtn_width_, hypso_mtn_slope_,
hypso_peak_onset_, hypso_peak_gain_);
terrain_offset = vec3(hc.x, mtn.y * mtn_scale * hc.y,
mtn.z * mtn_scale * hc.y) * (mtn_height * 8.0f);
pre_erosion_h = hc.x;
} else {
terrain_offset = vec3(mtn.x, mtn.y * mtn_scale, mtn.z * mtn_scale) * (mtn_amp * 8.0f);
}
// THE SAME SUBSTITUTION THE GLSL MAKES.
//
// Not for collision -- that is COLLISION_TRIMESH, built from the GPU mesh.
// For two other reasons, both of which cost real time when this was missing:
// 1. sample_density() is this path, so without it every headless gate and
// every map script asking "where is the ground" answers from the fBm
// world the heightfield replaced -- and a gate that cannot see the
// field cannot tell a correct shader from one that never ran.
// 2. The !gpu_filled fallback in dc_worker_cpu.cpp fills chunks from here.
// Left unwired it generates DIFFERENT TERRAIN from its neighbours.
//
// The algebra is erosion_wrapper_glsl.h's erosion_heightmap_gn_cpu, sitting
// beside the GLSL it mirrors, so the two cannot drift apart quietly.
if (hblk_n_ > 0) {
const float hmpp = hblk_mpp_;
auto sample = [&](float sx, float sz) -> float {
return _hblk_sample_cpu(sx, sz);
};
float oh, odx, odz;
erosion_heightmap_gn_cpu(x, z, floor_y, ceiling_y, terrain_height,
/* height_amp */ 0.25f, erosion_scale_,
field_half_range_, hmpp, sample, oh, odx, odz);
terrain_offset = vec3(oh, odx, odz);
}
// Detail rides on TOP and is NOT folded in — MUST stay identical to the GLSL.
float det_offset = 0.0f;
if (detail_scale_ > 0.0f) {
float detail_frac = (y_range > 0.01f) ? (detail_height_ / y_range) : 0.0f;
vec3 det = dcerosion::fractal_noise_2d_lod(vec2(x, z) * inv_scale * detail_scale_,
1.0f, detail_octaves_, lac, detail_gain_, det_lim);
// MIRRORS THE GLSL. See the note at the emission site: detail is
// masked by slope so it accumulates on gentle ground and is stripped
// from steep faces. terrain_offset is pn_terrain_offset's twin.
float det_slope = std::sqrt(terrain_offset.y * terrain_offset.y +
terrain_offset.z * terrain_offset.z);
float dm_t = (detail_slope_hi_ > detail_slope_lo_)
? (det_slope - detail_slope_lo_) / (detail_slope_hi_ - detail_slope_lo_)
: 0.0f;
dm_t = dm_t < 0.0f ? 0.0f : (dm_t > 1.0f ? 1.0f : dm_t);
float det_mask = 1.0f - (dm_t * dm_t * (3.0f - 2.0f * dm_t));
det_offset = det.x * detail_frac * det_mask;
}
// Map world Y to proc_density space
// MUST mirror the GLSL above, both branches.
float y_mapped;
if (height_space_sqrt_) {
float un = (y - floor_y) / y_range;
float su = (un < 0.0f) ? -sqrtf(-un) : sqrtf(un);
y_mapped = su * en_height_scale - det_offset * en_height_scale;
} else {
y_mapped = (y - floor_y) * y_scale - det_offset * en_height_scale;
}
// GradientNoiseNode. Lacunarity is hardcoded 2.0 in the node
// (noise_nodes.cpp:1303) and is NOT noise_lacunarity — do not "fix" that
// here without fixing the node, or these two diverge again.
vec2 uv = vec2(x * inv_scale, z * inv_scale);
vec3 gn = dcerosion::fractal_noise_2d(uv, base_freq, (int)gn_detail, 2.0f, gn_gain);
// ErosionNoiseNode. THIS LINE IS THE FIX. What stood here was a
// reimplementation that passed en_height_scale (5.0) as erosion_filter's
// `scale` where the node passes 0.08333, and skipped the [0,1] remap, the
// water gate and the height offset. erosion_node_eval is the node's own
// wrapper, shared with the GLSL, so there is nothing left to get wrong.
// It returns vec2(height * height_scale, erosion); .y is the ridge/crease
// value, unused on the density path.
// slope_power is gone from the signature -- it was never read. Parsed off
// the graph above and deliberately unused, so old graphs still load.
(void)en_slope_power;
// THE FRACTIONAL OCTAVE LIMIT, computed here exactly as the shader computes
// it. If these two expressions ever differ the surface you walk on and the
// surface you see part company by however much an octave is worth, which is
// the failure test_erosion_parity exists to catch -- it caught a 60x version
// of it once already.
//
// The coarsest gully spans terrain_height * gully_band_scale * cell_size in
// world metres and halves per octave, so that is the base wavelength.
float ero_max_oct = 999.0f;
if (lod_voxel_size > 0.0f) {
ero_max_oct = dcerosion::nyquist_octaves(
terrain_height * erosion_scale_ * en_cell_scale,
lod_voxel_size, 2.0f);
}
// EROSION IS SUBAERIAL -- mirrors pn_erosion_mask in the emitted GLSL.
// Written out rather than shared because the CPU path calls the node's
// wrapper directly and has no globals to set. If these two expressions
// differ, the surface you collide with and the surface you see part
// company, which is what test_erosion_parity exists to catch.
float ero_mask = 1.0f;
if (erosion_mask_depth_ > 0.0f) {
float depth_m = (get_waterline_norm() - pre_erosion_h)
* mountain_height_ * erosion_height_scale_ * inv_y_scale;
float t = depth_m / erosion_mask_depth_;
t = t < 0.0f ? 0.0f : (t > 1.0f ? 1.0f : t);
ero_mask = 1.0f - (t * t * (3.0f - 2.0f * t));
}
vec2 en = dcerosion::erosion_node_eval(
uv, gn + terrain_offset, en_height_scale, en_erosion_str * ero_mask,
en_cell_scale, en_octaves, en_gain, 4.8f,
erosion_scale_, ero_max_oct);
// SDF convention: negative = inside, positive = outside.
float d = en.x - y_mapped;
d *= inv_y_scale;
// ⚠ SUBMARINE TERRAIN IS NOT MIRRORED HERE, AND THAT IS A KNOWN GAP.
// The GPU density adds a 3D fBm below the coastal buffer; this CPU twin
// does not, so the meshed seabed has caves and arches that the CPU path
// does not know about. Everything meshes on the GPU today (GPUPATH reports
// gpu_mesh 100%), so the visible surface is right -- but any consumer of
// THIS function below the buffer is wrong by up to submarine_amp metres.
//
// The blocker is erosion_cpp_shim.h: it implements the 2D subset the
// erosion filter needs, and proc_simplex.inc wants vec4, floor(vec3) and
// scalar-vector operators it does not have. Extending the shim is the fix;
// faking the noise here would recreate the exact divergence
// test_erosion_parity exists to catch.
// Clip: solid below floor_y, air above ceiling_y
d = MAX(d, (floor_y - y) * 2.0f);
d = MIN(d, (ceiling_y - y) * 2.0f);
return d;
}
float DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate(
float x, float y, float z, float lod_voxel_size) {
return owner_->_eval_at(owner_->_resolve_eval_params(lod_voxel_size), x, y, z);
}
// THE POINT OF THE SPLIT. One resolve for the whole grid instead of 35937.
void DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate_batch(
const float *p_xyz, int p_count, float p_lod_voxel_size, float *r_out) {
const EvalParams ep = owner_->_resolve_eval_params(p_lod_voxel_size);
for (int i = 0; i < p_count; i++) {
r_out[i] = owner_->_eval_at(ep, p_xyz[i * 3], p_xyz[i * 3 + 1], p_xyz[i * 3 + 2]);
}
}
bool DCTerrainGeneratorProcNodes::ProcNodesBridge::has_gpu_shader() const {
// Read-only cache check — safe to call from worker thread.
// GLSL is compiled eagerly on main thread in set_graph() / _set().
return !owner_->cached_glsl_.empty();
}
const char *DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_shader_source() const {
// Read-only cache access — safe to call from worker thread.
return owner_->cached_glsl_.empty() ? nullptr : owner_->cached_glsl_.c_str();
}
// The CONTENT version. hblk_ is written by two producers and, once tiles can
// arrive at runtime, by growth in place -- and std::vector::assign into spare
// capacity keeps data() unchanged, so the pointer cannot be the signal. Every
// site that touches hblk_ bumps this.
uint32_t DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_custom_data_version() const {
return owner_->hblk_version_;
}
const float *DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_custom_data() const {
return owner_->hblk_.empty() ? nullptr : owner_->hblk_.data();
}
int DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_custom_data_count() const {
// THE NUMBER OF FLOATS, not the tile's side.
//
// hblk_ has two producers and they store different shapes: _load_heightfield
// stores a plain n*n square, while _collect_node_data stores the node's MIP
// PYRAMID -- 1,403,226 floats for a 1026 tile, because hblk_sample needs the
// coarser levels to band-limit itself per LOD. Returning the side made
// DCCompute upload side*side, which is exactly level 0 and nothing else, so
// HBLK_OFF[1] = 1052676 pointed one float past the end of the buffer.
//
// Every read at LOD >= 1 then came back zero and the terrain was flat
// wherever the voxel exceeded the block's 30 m pitch -- which is most of a
// 917 km world.
return (int)owner_->hblk_.size();
}
} // namespace godot