OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / src / dc_terrain_generator_procnodes.h

536 lines · c ref:
#ifndef DC_TERRAIN_GENERATOR_PROCNODES_H
#define DC_TERRAIN_GENERATOR_PROCNODES_H

#include "dc_terrain_generator.h"
#include <godot_cpp/variant/string.hpp>
#include <string>
// HeightGridDesc: the packed tile grid's layout, shared with the GLSL emitter
// and the C++ twin so the three cannot describe the buffer differently.
#include "erosion_wrapper_glsl.h"

namespace godot {

// Bridges a proc-nodes ProcNodeGraph to dc-terrain's DCGenerator interface.
// Uses ClassDB reflection (no compile-time dependency on proc-nodes).
//
// GPU path: compiles the graph to a GLSL density function via
// ProcNodeGraph::compile_to_glsl_function(), then wraps it with an adapter
// to match dc-terrain's evaluate_density(float x, float y, float z) signature.
//
// Inspector: auto-discovers Float/Int socket defaults from the graph and
// exposes them as grouped inspector properties (e.g. "Gradient Noise/Scale").
// Changing a property updates the graph, marks GLSL dirty, and triggers rebuild.
class DCTerrainGeneratorProcNodes : public DCTerrainGenerator {
	GDCLASS(DCTerrainGeneratorProcNodes, DCTerrainGenerator)

	// The ProcNodeGraph resource (stored as generic Resource for cross-extension)
	Ref<Resource> graph_;

	// Terrain height: XZ world coords divided by this to get UV-like space.
	float terrain_height_ = 32.0f;

	// Floor/ceiling: the terrain surface fills [floor_y, ceiling_y].
	// Valleys at floor_y, peaks at ceiling_y.
	float floor_y_ = 0.0f;
	float ceiling_y_ = 10.0f;

	// Mountain layer: low-frequency noise that creates large-scale terrain shape.
	// The erosion detail rides on top of this.

	// RELIEF FIELD — makes mountain amplitude a FUNCTION OF PLACE.
	//
	// mountain_height_ is one scalar for the whole world, so every region gets the
	// same amplitude and you cannot have an 8 km range and a 400 m one in the same
	// scene: adding octaves adds DETAIL, never a different amount of relief
	// somewhere else. This is a second, much lower-frequency noise that scales the
	// mountain term, so "how mountainous is it here" varies across the map.
	//
	// relief_scale_ = 0 DISABLES it (amplitude is uniform, exactly as before), so
	// every existing generator keeps its current output.

	// PERSISTENCE, which was hardcoded 0.5 and is why the terrain is smooth
	// underfoot. Octave k has amplitude gain^k of an amplitude set by
	// mountain_height * y_range — so at 0.5, by the time the wavelength reaches
	// player scale the amplitude is metres and the ground is a ramp. Measured:
	// raising mountain_octaves 4 -> 12 moved local roughness 0.72 -> 0.98 m per
	// 2 m stride; it cannot fix this, because it only adds ever-tinier ripples.
	// 0.5 is the previous behaviour exactly.

	// ── THE EROSION FILTER'S OWN KNOBS ────────────────────────────────────
	// Baked literals inside erosion_node_eval until now, because GpuCompiler's
	// push-constant block is param0..param6 -- seven floats, a hard cap, and
	// rounding and onset alone need eight. That cap only ever applied to SOCKET
	// parameters; pn_fade_div and pn_erosion_scale already escaped it by being
	// globals the wrapper sets. These take the same door.
	//
	// Defaults are runevision's reference values, so an untouched generator
	// produces exactly the previous surface.
	// scales the incoming height field into the filter's space
	// frequency step per gully octave
	// scales the gully contribution down
	// LOWER keeps high-frequency gullies to steeper slopes
	// only gullies above this are normalised -- keeps peaks pointy
	// flat offset, times the accumulated octave magnitude
	// mixes that offset toward -fade_target
	// rounding applied in creases
	// rounding applied on ridges
	// the INPUT height function's feature size -- must move when terrain_height does
	// rounding multiplier compounded per octave
	// slope at which gullies begin at all
	// per-octave slope gate
	// the same, for the ridge map
	// per-octave ridge gate
	// the slope magnitude to pretend the input has
	// 0 = use the real slope, 1 = fully assumed

	// ── Baked heightfield (hyper-games #50 section 5.3) ────────────────
	// A terrain-diffusion block in METRES, from terrain-lookdev/stitch_block.py.
	// When loaded it REPLACES the mountain layer: pn_terrain_offset stops being
	// the multifractal and becomes the field, which is exactly where the model's
	// large forms belong -- the erosion filter then carves the model's landscape
	// instead of an fBm blob, and the fine gn base noise is untouched.
	//
	// The geometry lives here as literals baked into the emitted GLSL, so
	// changing block invalidates the shader (a recompile), not a uniform.
	String heightfield_path_;
	std::vector<float> hblk_;
	int   hblk_n_ = 0;
	float hblk_mpp_ = 30.0f, hblk_ox_ = 0.0f, hblk_oz_ = 0.0f;
	bool  _load_heightfield(const String &p_path);

	// HEIGHTFIELD ONLY — the isolation switch.
	//
	// Density becomes height(x,z) - y and nothing else: no graph, no erosion, no
	// noise library, no filter-space conversion. It exists because "the terrain
	// is just perlin noise" has exactly one cheap explanation -- the GPU shader
	// failed and DCCompute fell back to the CPU path, which evaluates the fBm
	// mountain -- and the only way to tell that apart from a wrong field is to
	// remove everything that could be wrong except the field itself.
	//
	// Both paths implement it, so it also answers WHICH path ran: they cannot
	// disagree here, because there is nothing to disagree about.
	bool  heightfield_only_ = false;

	// LOOKDEV WRAPPER — emit the SHARED wrapper instead of this file's.
	//
	// _recompile_glsl assembles ~47 KB of engine-specific GLSL: a multifractal
	// mountain layer, a relief layer, a 4-octave detail fBm, a y-mapping and a
	// clip. The lookdev is none of that -- it is the heightfield and the erosion
	// filter, full stop. Swapping ONE term inside the engine wrapper and calling
	// it "identical to lookdev" was wrong: the detail layer alone puts +/-30 m of
	// fBm into the output that the lookdev never had.
	//
	// With this on the generator emits, instead:
	//     noise library  (GpuCompiler::get_noise_library_glsl)
	//     hblk sampler   (ours -- storage is per-consumer)
	//     erosion_wrapper_glsl(heightmap_source = true)
	//     evaluate_density(x,y,z) { return gpu_wrapper(x,y,z); }
	//
	// gpu_wrapper is the function erosion_render compiles and test_erosion_parity
	// guards, so the engine runs the lookdev's shader rather than a transcription
	// of it. That removes the entire hand-assembled wrapper as a source of error.

	// DETAIL LAYER — high frequency with its OWN amplitude, in METRES.
	//
	// Separate from the mountain fBm because the two want different amplitude
	// laws: ranges want a big smooth cascade, ground wants centimetre-to-metre
	// roughness everywhere. Expressing it as a fraction of [floor_y, ceiling_y]
	// like mountain_height does would mean typing 0.0005 for 5 m over a 10 km
	// band, so this is metres and converted internally.
	//
	// NOT MULTIPLIED BY THE RELIEF FIELD, deliberately. relief_amp scales the
	// mountains so an Everest region and a Vermont region can share a map — but
	// a Vermont hillside still has rock underfoot. Scaling detail by relief too
	// made the low-relief lobes glassy, which is where the player spawns.

	// LACUNARITY, hardcoded 2.0 until now — and 2.0 is exactly why the terrain
	// reads as self-similar. With frequency doubling and gain constant, every
	// octave is a scaled copy of the last and they all land on harmonics of each
	// other. A non-integer ratio (~1.87) stops them lining up. Applied to BOTH
	// the mountain and detail fBm, since the repetition shows in both.
	// Gully band position: coarsest gully wavelength is
	// terrain_height * erosion_scale * cell_scale, in world metres. Was baked at
	// 0.15 inside erosion_node_eval, which locked the gully band to a fixed
	// fraction of the terrain scale -- so raising terrain_height for view
	// distance dragged the gullies up with it.

	// Multifractal blend for the MOUNTAIN layer. 0 = plain fBm (bit-identical to
	// fractal_noise_2d, verified), 1 = each octave's amplitude weighted by the
	// elevation accumulated so far, giving rugged peaks above smooth valleys.
	// Measured at 1.0: roughness on high ground is 5.8x that on low ground,
	// against 0.98 -- i.e. homogeneous -- for fBm.

	// SIGNED-SQRT HEIGHT SPACE — opt in, default off.
	//
	// Real terrain's local roughness grows with local elevation; xandergos/
	// terrain-diffusion measures this directly and works in z' = sign(z)*sqrt(|z|)
	// so the dependence disappears (their elevation_dataset.py:269).
	//
	// Applied to the WORLD-HEIGHT side, because this function evaluates d at a
	// given y rather than solving for the surface:
	//     un       = (y - floor_y) / y_range          // 0..1 across the band
	//     y_mapped = sign(un) * sqrt(|un|) * height_scale
	// Endpoints are unchanged (H=0 -> floor_y, H=height_scale -> ceiling_y), so no
	// other parameter shifts meaning. The surface becomes
	//     y = floor_y + y_range * (H/height_scale)^2
	// so dy/dH = 2*y_range*H/height_scale^2: a CONSTANT noise amplitude produces
	// elevation-proportional relief, ~5x at the top of the band versus the floor.
	// That is what relief_scale/relief_floor were bolted on to fake.
	bool height_space_sqrt_ = false;

	// EROSION ON OR OFF — a real toggle, not a strength of zero.
	//
	// The heightmap IS the terrain: the Heightfield node samples the baked tile
	// and displaces the surface to it, which is a complete map by itself. The
	// erosion filter runs on a finer grid than the model's 30/90 m and
	// synthesises the detail below its resolution. Turning it off must not move
	// the ground, only remove that detail.
	//
	// False skips the ErosionNoise node when emitting, and because nothing else
	// in a heightfield graph mentions noise, the whole noise library goes with
	// it -- 60 KB of shader down to 5.6 KB. Strength 0 would cost the same
	// octave loop and leave the library in place.
	bool erosion_enabled_ = true;

	// EROSION-MINIMAL — the wrapper with nothing in it but the erosion.
	//
	// The general wrapper carries a mountain layer, a relief envelope and a
	// detail fBm, because it was written when the mountain WAS the terrain. On
	// a heightfield map the baked tile is the terrain and those three layers are
	// dead weight: mountain_height is 0 and detail_scale is 0, so they emit
	// nothing but their comments and a det_offset of zero that y_mapped still
	// subtracts.
	//
	// Dead weight is not free. Every one of them is a branch that can be
	// mis-gated, and mis-gating is exactly what hid the erosion filter's own
	// parameters inside want_mountain (hyper-games #53 section 2). This path has
	// no branches to mis-gate: heightfield in, filter, y-map, clip.
	bool erosion_minimal_ = false;

	// ── Layer and filter parameters ───────────────────────────────────────
	float mountain_scale_ = 1.0f;
	float mountain_height_ = 0.5f;
	float mountain_gain_ = 0.5f;
	float mountain_variance_ = 0.0f;
	float relief_scale_ = 0.0f;
	float relief_floor_ = 0.15f;
	float detail_scale_ = 0.0f;
	float detail_height_ = 0.0f;
	float detail_gain_ = 0.5f;
	float noise_lacunarity_ = 2.0f;
	float erosion_scale_ = 0.15f;
	// The FIELD's half-relief in metres, for the SLOPE normalisation only.
	// 0 = fall back to the playable y_range. See erosion_wrapper_glsl.h.
	float field_half_range_ = 0.0f;
	float input_height_amplitude_ = 0.25f;
	float octave_lacunarity_ = 2.0f;
	float gully_depth_ = 0.5f;
	float fine_gully_slope_restriction_ = 1.5f;
	float gully_normalize_threshold_ = 0.5f;
	float height_offset_ = -0.65f;
	float height_offset_follows_altitude_ = 0.0f;
	float crease_rounding_ = 0.1f;
	float ridge_rounding_ = 0.0f;

	// Detail-layer slope mask: full detail below lo, none above hi.
	// Units are gn-per-p, the same as pn_terrain_offset.yz.
	// ── SUBMARINE TERRAIN ──────────────────────────────────────────────
	// Below a smooth coastal buffer the seabed stops being a heightfield and
	// becomes VOLUMETRIC: a 3D fBm added to the density, after Geiss (GPU Gems
	// 3 ch.1). A heightfield cannot contain a sea cave, an arch or an overhang,
	// and the whole world is meant to be reachable.
	//
	// The buffer is why it is depth-gated rather than global. Beaches and the
	// waterline need a single-valued surface -- water meeting a cave mouth at
	// the shoreline has no clean coastline -- so the first tens of metres of
	// depth stay a pure heightfield and the volumetric term fades in below.
	//
	// amp 0 DISABLES it, so a map that has not opted in is bit-identical.
	float submarine_amp_ = 0.0f;          // metres of 3D perturbation
	float submarine_wavelength_ = 3000.0f;
	int   submarine_octaves_ = 4;
	float submarine_roughness_ = 0.5f;
	float submarine_buffer_start_ = 50.0f;   // depth where it begins
	float submarine_buffer_end_ = 400.0f;    // depth where it is full
	float submarine_water_y_ = 700.0f;
	// Depth over which erosion fades out below the waterline, metres. Erosion
	// is subaerial: it stops AT the shoreline in reality, but a hard cut leaves
	// a visible seam along every coast, so it ramps. 0 disables the mask.
	float erosion_mask_depth_ = 150.0f;
	// MUST MATCH ErosionNoiseNode's height_scale (its pc.param1). The depth
	// conversion below needs it and the generator cannot see the node's
	// parameters, so it is mirrored here -- and a mismatch shows up as an
	// erosion mask that fades over the wrong depth, not as an error.
	float erosion_height_scale_ = 5.0f;

	float detail_slope_lo_ = 0.30f;
	float detail_slope_hi_ = 1.20f;

	// --- hypsometric land/sea curve (hypso_curve.inc) ---
	float hypso_enabled_ = 0.0f;   // 0 = off, and off is BIT-IDENTICAL to before this landed
	float hypso_sea_ = 0.20f;   // input at the middle of the continental slope; sets land fraction (0.20 -> 33.7%)
	float hypso_shelf_width_ = 0.12f;   // gentle shelf below sea
	float hypso_slope_width_ = 0.05f;   // steep drop half-width   // half-width of the slope; smaller = sharper coast
	float hypso_abyss_ = -0.62f;   // OUTPUT value of the abyssal plain
	float hypso_plat_slope_ = 0.13f;   // continental platform gradient (gentle)
	float hypso_mtn_onset_ = 0.35f;   // where the mountain front begins
	float hypso_mtn_width_ = 0.10f;   // half-width of that front
	float hypso_mtn_slope_ = 1.30f;   // mountain gradient (steep)
	float hypso_peak_onset_ = 0.60f;   // where the summit lift begins
	float hypso_peak_gain_ = 0.30f;   // output added at h = 1

	float input_feature_rounding_ = 0.1f;
	float rounding_per_octave_ = 2.0f;
	float gully_onset_slope_ = 1.25f;
	float octave_onset_slope_ = 1.25f;
	float ridge_onset_slope_ = 2.8f;
	float ridge_octave_onset_ = 1.5f;
	float assumed_slope_ = 0.7f;
	float assumed_slope_mix_ = 1.0f;
	int mountain_octaves_ = 2;
	int detail_octaves_ = 4;
	bool lookdev_wrapper_ = false;

	// Cached GLSL shader source (adapter-wrapped for dc-terrain).
	// Compiled eagerly on main thread — worker only reads cache.
	std::string cached_glsl_;
	// Bumped on every hblk_ mutation. See ProcNodesBridge::gpu_custom_data_version.
	uint32_t hblk_version_ = 0;
	// The packed tile grid's layout, when the node supplies one. Empty means
	// hblk_ is a single square and every CPU site addresses it the old way.
	HeightGridDesc hgrid_;
	// ONE CPU heightfield sampler. There were FOUR copies of this bilinear --
	// two plain and two inside lambdas -- and the tile grid would have needed a
	// fifth. Collapsing them is what makes "does the CPU read the same field as
	// the GPU?" a question with one answer.
	float _hblk_sample_cpu(float x, float z) const;

public:
	// The ASSEMBLED shader string, exactly as handed to the compute pipeline.
	// Exposed because it was unreachable: _recompile_glsl builds it with
	// godot_cpp String concatenation, so nothing outside a running engine could
	// see it, and every test of "does the shader compile" was therefore testing
	// an approximation. That gap shipped a duplicate global declaration
	// (pn_terrain_offset) which failed to compile and produced a world with no
	// terrain and a clean-looking log. Dump this and run glslc on it.
	String get_compiled_glsl() const { return String(cached_glsl_.c_str()); }

private:

	// ── Auto-discovered graph parameters ──────────────────────────────
	struct GraphParam {
		int node_idx;
		int socket_idx;
		String node_title;   // "Gradient Noise"
		String socket_name;  // "Scale"
		Variant::Type type;  // FLOAT or INT
		Variant default_value;
	};
	Vector<GraphParam> discovered_params_;
	void _discover_params();
	void _collect_node_data();

	// Bridge: implements DCGenerator interface
	// THE CONSTANTS OF THE DENSITY WRAPPER THAT DO NOT DEPEND ON THE POINT.
	//
	// Resolved from the graph and the LOD once per grid instead of once per
	// voxel. Every field is named as the local it replaced, so _eval_at's
	// arithmetic is the same text it was when this was one function -- see the
	// note above _resolve_eval_params in the .cpp.
	struct EvalParams {
		float lod_voxel_size = 0.0f;
		float terrain_height = 32.0f, floor_y = 0.0f, ceiling_y = 10.0f;
		float mtn_scale = 0.0f, mtn_height = 0.0f;
		int mtn_octaves = 0;
		float base_freq = 3.0f, gn_detail = 0.0f, 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;
		float inv_scale = 0.0f, y_range = 0.0f, y_scale = 0.0f, inv_y_scale = 0.0f;
		bool graph_has_erosion = false;
	};
	EvalParams _resolve_eval_params(float lod_voxel_size) const;
	float _eval_at(const EvalParams &p, float x, float y, float z) const;

	class ProcNodesBridge : public DCGenerator {
		DCTerrainGeneratorProcNodes *owner_;

	public:
		ProcNodesBridge(DCTerrainGeneratorProcNodes *o) : owner_(o) {}

		float evaluate(float x, float y, float z, float lod_voxel_size) override;
		void evaluate_batch(const float *p_xyz, int p_count, float p_lod_voxel_size,
				float *r_out) override;
		bool has_gpu_shader() const override;
		const char *gpu_shader_source() const override;
		const float *gpu_custom_data() const override;
		int gpu_custom_data_count() const override;
		uint32_t gpu_custom_data_version() const override;
	};
	ProcNodesBridge bridge_{ this };

	void _recompile_glsl();

	// WHY THE LAST COMPILE PRODUCED NOTHING, empty when it produced something.
	// Bound so a headless gate can assert on it: "visible" has to mean
	// answerable, not just printed. See _recompile_glsl in the .cpp.
	//
	// ⚠ THE INITIAL VALUE IS NOT EMPTY, and that is the point. _recompile_glsl
	// only runs from set_graph()/_set(), so a generator nobody has configured
	// has never attempted a compile -- and "" would make that indistinguishable
	// from "compiled cleanly". Four states, four answers: "never compiled",
	// "no graph assigned", a specific failure, or empty for success.
	String last_compile_error_ = "never compiled (no graph has been assigned)";

protected:
	static void _bind_methods();

	// Dynamic inspector properties from graph
	bool _set(const StringName &p_name, const Variant &p_value);
	bool _get(const StringName &p_name, Variant &r_ret) const;
	void _get_property_list(List<PropertyInfo> *p_list) const;

public:
	DCGenerator *get_native_generator() override { return &bridge_; }

	String get_last_compile_error() const { return last_compile_error_; }
	bool has_compiled_shader() const { return !cached_glsl_.empty(); }

	void set_graph(const Ref<Resource> &p_graph);
	Ref<Resource> get_graph() const { return graph_; }

	void set_terrain_height(float p_height);
	float get_terrain_height() const { return terrain_height_; }

	void set_floor_y(float p_val);
	float get_floor_y() const { return floor_y_; }
	void set_ceiling_y(float p_val);
	float get_ceiling_y() const { return ceiling_y_; }

	void set_heightfield_path(const String &p_path);
	String get_heightfield_path() const { return heightfield_path_; }
	void set_heightfield_only(bool p_val);
	void set_erosion_enabled(bool p_val);
	bool get_erosion_enabled() const { return erosion_enabled_; }
	void set_erosion_minimal(bool p_val);
	bool get_erosion_minimal() const { return erosion_minimal_; }

	void set_mountain_scale(float p_val);
	float get_mountain_scale() const { return mountain_scale_; }
	void set_mountain_height(float p_val);
	float get_mountain_height() const { return mountain_height_; }
	void set_mountain_gain(float p_val);
	float get_mountain_gain() const { return mountain_gain_; }
	void set_mountain_variance(float p_val);
	float get_mountain_variance() const { return mountain_variance_; }
	void set_relief_scale(float p_val);
	float get_relief_scale() const { return relief_scale_; }
	void set_relief_floor(float p_val);
	float get_relief_floor() const { return relief_floor_; }
	void set_detail_scale(float p_val);
	float get_detail_scale() const { return detail_scale_; }
	void set_detail_height(float p_val);
	float get_detail_height() const { return detail_height_; }
	void set_detail_gain(float p_val);
	float get_detail_gain() const { return detail_gain_; }
	void set_noise_lacunarity(float p_val);
	float get_noise_lacunarity() const { return noise_lacunarity_; }
	void set_erosion_scale(float p_val);
	float get_erosion_scale() const { return erosion_scale_; }
	void set_field_half_range(float p_val);
	float get_field_half_range() const { return field_half_range_; }
	void set_input_height_amplitude(float p_val);
	float get_input_height_amplitude() const { return input_height_amplitude_; }
	void set_octave_lacunarity(float p_val);
	float get_octave_lacunarity() const { return octave_lacunarity_; }
	void set_gully_depth(float p_val);
	float get_gully_depth() const { return gully_depth_; }
	void set_fine_gully_slope_restriction(float p_val);
	float get_fine_gully_slope_restriction() const { return fine_gully_slope_restriction_; }
	void set_gully_normalize_threshold(float p_val);
	float get_gully_normalize_threshold() const { return gully_normalize_threshold_; }
	void set_height_offset(float p_val);
	float get_height_offset() const { return height_offset_; }
	void set_height_offset_follows_altitude(float p_val);
	float get_height_offset_follows_altitude() const { return height_offset_follows_altitude_; }
	void set_crease_rounding(float p_val);
	float get_crease_rounding() const { return crease_rounding_; }

	void set_submarine_amp(float v);
	float get_submarine_amp() const { return submarine_amp_; }
	void set_submarine_wavelength(float v);
	float get_submarine_wavelength() const { return submarine_wavelength_; }
	void set_submarine_octaves(int v);
	int get_submarine_octaves() const { return submarine_octaves_; }
	void set_submarine_buffer(float a, float b);
	void set_submarine_water_y(float v);
	void set_erosion_mask_depth(float v);
	float get_erosion_mask_depth() const { return erosion_mask_depth_; }
	float get_submarine_water_y() const { return submarine_water_y_; }

	void set_detail_slope_lo(float p_val);
	float get_detail_slope_lo() const { return detail_slope_lo_; }
	void set_detail_slope_hi(float p_val);
	float get_detail_slope_hi() const { return detail_slope_hi_; }
	void set_hypso_enabled(float p_val);
	float get_hypso_enabled() const { return hypso_enabled_; }
	void set_hypso_sea(float p_val);
	float get_hypso_sea() const { return hypso_sea_; }
	void set_hypso_shelf_width(float p_val);
	float get_hypso_shelf_width() const { return hypso_shelf_width_; }
	void set_hypso_slope_width(float p_val);
	float get_hypso_slope_width() const { return hypso_slope_width_; }
	void set_hypso_abyss(float p_val);
	float get_hypso_abyss() const { return hypso_abyss_; }
	void set_hypso_plat_slope(float p_val);
	float get_hypso_plat_slope() const { return hypso_plat_slope_; }
	void set_hypso_mtn_onset(float p_val);
	float get_hypso_mtn_onset() const { return hypso_mtn_onset_; }
	void set_hypso_mtn_width(float p_val);
	float get_hypso_mtn_width() const { return hypso_mtn_width_; }
	void set_hypso_mtn_slope(float p_val);
	float get_hypso_mtn_slope() const { return hypso_mtn_slope_; }
	void set_hypso_peak_onset(float p_val);
	float get_hypso_peak_onset() const { return hypso_peak_onset_; }
	void set_hypso_peak_gain(float p_val);
	float get_hypso_peak_gain() const { return hypso_peak_gain_; }
	// Sea level in OUTPUT units -- hypso_curve(sea). The curve is
	// monotonic so this is exact; do NOT also hand-set a water height.
	float get_waterline_norm() const;
	void set_ridge_rounding(float p_val);
	float get_ridge_rounding() const { return ridge_rounding_; }
	void set_input_feature_rounding(float p_val);
	float get_input_feature_rounding() const { return input_feature_rounding_; }
	void set_rounding_per_octave(float p_val);
	float get_rounding_per_octave() const { return rounding_per_octave_; }
	void set_gully_onset_slope(float p_val);
	float get_gully_onset_slope() const { return gully_onset_slope_; }
	void set_octave_onset_slope(float p_val);
	float get_octave_onset_slope() const { return octave_onset_slope_; }
	void set_ridge_onset_slope(float p_val);
	float get_ridge_onset_slope() const { return ridge_onset_slope_; }
	void set_ridge_octave_onset(float p_val);
	float get_ridge_octave_onset() const { return ridge_octave_onset_; }
	void set_assumed_slope(float p_val);
	float get_assumed_slope() const { return assumed_slope_; }
	void set_assumed_slope_mix(float p_val);
	float get_assumed_slope_mix() const { return assumed_slope_mix_; }
	void set_mountain_octaves(int p_val);
	int get_mountain_octaves() const { return mountain_octaves_; }
	void set_detail_octaves(int p_val);
	int get_detail_octaves() const { return detail_octaves_; }
	void set_lookdev_wrapper(bool p_val);
	bool get_lookdev_wrapper() const { return lookdev_wrapper_; }
	bool get_heightfield_only() const { return heightfield_only_; }
	void set_height_space_sqrt(bool p_val);
	bool get_height_space_sqrt() const { return height_space_sqrt_; }

	// Force recompilation of GLSL shader (must call from main thread)
	void recompile() { _recompile_glsl(); emit_changed(); }
};

} // namespace godot

#endif // DC_TERRAIN_GENERATOR_PROCNODES_H