OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain commits / ffac461

dc-terrain: a volumetric seabed, and the depth mask that gates it
SUBMARINE TERRAIN, after 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 can hold none of those, and the whole world is meant to be reachable. GATED ON DEPTH, not applied globally. sea_depth is computed from the UNPERTURBED surface, so the weight is a property of the column and cannot feed back on itself. The first tens of metres of depth 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 because the ocean shader's own depth query assumes d = H - y holds. amp 0 disables it and the density is bit-identical to before. pn_erosion_mask is set here, from hc.x: the hypso curve output is the elevation BEFORE the filter runs, so a column's depth is known in the wrapper and the mask can be set before proc_density is entered. ⚠ THE DEPTH CONVERSION WAS WRONG FIRST TIME. It emitted 0.55 * 4200 = 2310 m per curve unit against a world whose own relief readout says 11550 -- exactly 5x out, which is the erosion node's height_scale. The full chain is pn_terrain_offset carrying hc.x * mheight * 8, gn reaching the surface through input_height_amplitude * 0.5 = 0.125, and erosion_node_eval scaling by height_scale, so 8 * 0.125 * height_scale = height_scale. Caught by comparing the emitted constant against a number the map already prints, which is worth more than re-reading the derivation. erosion_height_scale_ mirrors the node's parameter because the generator cannot see it. A mismatch fades the mask over the wrong depth rather than erroring. ⚠ CONSEQUENCE FOR tests/test_shore_profile.cpp: it converts with an asserted 7790 m/unit taken from a comment, against 11550 derived here. The surf zone it reports would be WIDER, not narrower, so nothing built on it breaks -- but the ocean's shore_full_depth and shore_foam_depth were tuned against those numbers and should be re-checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ffac46145bfa3c8a62c969e04cc5c58fac24ac2d
by plankatron <fleeky@gmail.com> · 2026-09-07T20:01:27.000Z

Changed files (2) +129 -0

src/dc_terrain_generator_procnodes.cpp +88 -0
……@@ -98,6 +98,22 @@ void DCTerrainGeneratorProcNodes::_bind_methods() {
9898 ClassDB::bind_method(D_METHOD("set_crease_rounding", "value"), &DCTerrainGeneratorProcNodes::set_crease_rounding);
9999 ClassDB::bind_method(D_METHOD("get_crease_rounding"), &DCTerrainGeneratorProcNodes::get_crease_rounding);
100100 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "crease_rounding"), "set_crease_rounding", "get_crease_rounding");
101 ClassDB::bind_method(D_METHOD("set_submarine_amp", "v"), &DCTerrainGeneratorProcNodes::set_submarine_amp);
102 ClassDB::bind_method(D_METHOD("get_submarine_amp"), &DCTerrainGeneratorProcNodes::get_submarine_amp);
103 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "submarine_amp"), "set_submarine_amp", "get_submarine_amp");
104 ClassDB::bind_method(D_METHOD("set_submarine_wavelength", "v"), &DCTerrainGeneratorProcNodes::set_submarine_wavelength);
105 ClassDB::bind_method(D_METHOD("get_submarine_wavelength"), &DCTerrainGeneratorProcNodes::get_submarine_wavelength);
106 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "submarine_wavelength"), "set_submarine_wavelength", "get_submarine_wavelength");
107 ClassDB::bind_method(D_METHOD("set_submarine_octaves", "v"), &DCTerrainGeneratorProcNodes::set_submarine_octaves);
108 ClassDB::bind_method(D_METHOD("get_submarine_octaves"), &DCTerrainGeneratorProcNodes::get_submarine_octaves);
109 ADD_PROPERTY(PropertyInfo(Variant::INT, "submarine_octaves"), "set_submarine_octaves", "get_submarine_octaves");
110 ClassDB::bind_method(D_METHOD("set_submarine_buffer", "a", "b"), &DCTerrainGeneratorProcNodes::set_submarine_buffer);
111 ClassDB::bind_method(D_METHOD("set_erosion_mask_depth", "v"), &DCTerrainGeneratorProcNodes::set_erosion_mask_depth);
112 ClassDB::bind_method(D_METHOD("get_erosion_mask_depth"), &DCTerrainGeneratorProcNodes::get_erosion_mask_depth);
113 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "erosion_mask_depth"), "set_erosion_mask_depth", "get_erosion_mask_depth");
114 ClassDB::bind_method(D_METHOD("set_submarine_water_y", "v"), &DCTerrainGeneratorProcNodes::set_submarine_water_y);
115 ClassDB::bind_method(D_METHOD("get_submarine_water_y"), &DCTerrainGeneratorProcNodes::get_submarine_water_y);
116 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "submarine_water_y"), "set_submarine_water_y", "get_submarine_water_y");
101117 ClassDB::bind_method(D_METHOD("set_detail_slope_lo", "value"), &DCTerrainGeneratorProcNodes::set_detail_slope_lo);
102118 ClassDB::bind_method(D_METHOD("get_detail_slope_lo"), &DCTerrainGeneratorProcNodes::get_detail_slope_lo);
103119 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "detail_slope_lo"), "set_detail_slope_lo", "get_detail_slope_lo");
……@@ -373,6 +389,31 @@ float DCTerrainGeneratorProcNodes::get_waterline_norm() const {
373389 hypso_peak_gain_).x;
374390}
375391
392void DCTerrainGeneratorProcNodes::set_submarine_amp(float v) {
393 if (submarine_amp_ == v) return;
394 submarine_amp_ = v; _recompile_glsl(); emit_changed();
395}
396void DCTerrainGeneratorProcNodes::set_submarine_wavelength(float v) {
397 if (submarine_wavelength_ == v) return;
398 submarine_wavelength_ = v; _recompile_glsl(); emit_changed();
399}
400void DCTerrainGeneratorProcNodes::set_submarine_octaves(int v) {
401 if (submarine_octaves_ == v) return;
402 submarine_octaves_ = v; _recompile_glsl(); emit_changed();
403}
404void DCTerrainGeneratorProcNodes::set_submarine_buffer(float a, float b) {
405 submarine_buffer_start_ = a; submarine_buffer_end_ = b;
406 _recompile_glsl(); emit_changed();
407}
408void DCTerrainGeneratorProcNodes::set_erosion_mask_depth(float v) {
409 if (erosion_mask_depth_ == v) return;
410 erosion_mask_depth_ = v; _recompile_glsl(); emit_changed();
411}
412void DCTerrainGeneratorProcNodes::set_submarine_water_y(float v) {
413 if (submarine_water_y_ == v) return;
414 submarine_water_y_ = v; _recompile_glsl(); emit_changed();
415}
416
376417void DCTerrainGeneratorProcNodes::set_detail_slope_lo(float p_val) {
377418 if (detail_slope_lo_ == p_val) return;
378419 detail_slope_lo_ = p_val;
……@@ -1308,6 +1349,28 @@ void DCTerrainGeneratorProcNodes::_recompile_glsl() {
13081349 // output, and is deliberately not done here.
13091350 " pn_terrain_offset = vec3(hc.x, mtn.y * " + mscale_str + " * hc.y, mtn.z * "
13101351 + mscale_str + " * hc.y) * " + mheight_str + " * 8.0;\n"
1352 // EROSION IS SUBAERIAL. hc.x is the elevation BEFORE the filter runs,
1353 // so the column's depth is known here and the mask can be set before
1354 // proc_density is entered. Metres per curve unit is mountain_height *
1355 // inv_yscale: pn_terrain_offset carries hc.x * mheight * 8 and reaches
1356 // the surface through a factor of 0.125, so the base surface in mapped
1357 // METRES PER CURVE UNIT = mountain_height * erosion height_scale *
1358 // inv_yscale. The full chain: pn_terrain_offset carries hc.x * mheight
1359 // * 8, gn reaches the surface through input_height_amplitude * 0.5 =
1360 // 0.125, and erosion_node_eval scales the result by height_scale. So
1361 // 8 * 0.125 * height_scale = height_scale.
1362 //
1363 // ⚠ The height_scale factor was MISSING at first, giving 0.55 * 4200 =
1364 // 2310 m/unit against a world whose own relief readout says 11550 --
1365 // exactly 5x out, which is height_scale. Caught by comparing the
1366 // emitted constant against a number the map already prints.
1367 + (erosion_mask_depth_ > 0.0f ? String(
1368 " pn_erosion_mask = 1.0 - smoothstep(0.0, ")
1369 + String::num(erosion_mask_depth_, 3) + ", ("
1370 + String::num(get_waterline_norm(), 8) + " - hc.x) * "
1371 + String::num(mountain_height_ * erosion_height_scale_, 6)
1372 + " * " + inv_yscale_str + ");\n"
1373 : String())
13111374 : String(" pn_terrain_offset = vec3(mtn.x, mtn.y * ") + mscale_str
13121375 + ", mtn.z * " + mscale_str + ") * mtn_amp * 8.0;\n";
13131376
……@@ -1465,6 +1528,31 @@ void DCTerrainGeneratorProcNodes::_recompile_glsl() {
14651528 " pn_out_grad = vec2(0.0);\n")
14661529 : String(
14671530 " pn_out_grad *= inv_scale * ") + inv_yscale_str + ";\n")
1531 // ── SUBMARINE TERRAIN ──────────────────────────────────────────
1532 // Geiss (GPU Gems 3 ch.1): add a 3D fBm to a heightfield density and
1533 // the surface stops being single-valued -- caves, arches and overhangs
1534 // appear wherever the 3D term outruns the vertical gradient. A
1535 // heightfield seabed cannot have any of those, and the whole world is
1536 // meant to be reachable.
1537 //
1538 // GATED ON DEPTH, not applied globally. sea_depth uses the UNPERTURBED
1539 // surface (y + d before this line), so the weight is a property of the
1540 // column and cannot feed back on itself. The first tens of metres stay
1541 // a pure heightfield because beaches and the waterline need a
1542 // single-valued surface -- a cave mouth at the shoreline has no clean
1543 // coastline, and the ocean's depth query assumes d = H - y holds.
1544 + (submarine_amp_ > 0.0f ? String(
1545 " // Submarine volumetric seabed, below a smooth coastal buffer.\n"
1546 " float sea_depth = ") + String::num(submarine_water_y_, 3) + " - (y + d);\n"
1547 " float sub_w = smoothstep(" + String::num(submarine_buffer_start_, 3) + ", "
1548 + String::num(submarine_buffer_end_, 3) + ", sea_depth);\n"
1549 " if (sub_w > 0.0) {\n"
1550 " float sub_f = 1.0 / " + String::num(submarine_wavelength_, 4) + ";\n"
1551 " d += ps_fbm_noise_3d(x * sub_f, y * sub_f, z * sub_f, "
1552 + String::num_int64(submarine_octaves_) + ", "
1553 + String::num(submarine_roughness_, 4) + ", 2.0) * "
1554 + String::num(submarine_amp_, 4) + " * sub_w;\n"
1555 " }\n" : String())
14681556 + String(" // Clip: solid below floor_y, air above ceiling_y.\n"
14691557 " d = max(d, (") + floor_str + " - y) * 2.0);\n"
14701558 " d = min(d, (" + ceil_str + " - y) * 2.0);\n"
src/dc_terrain_generator_procnodes.h +41 -0
……@@ -234,6 +234,35 @@ class DCTerrainGeneratorProcNodes : public DCTerrainGenerator {
234234
235235 // Detail-layer slope mask: full detail below lo, none above hi.
236236 // Units are gn-per-p, the same as pn_terrain_offset.yz.
237 // ── SUBMARINE TERRAIN ──────────────────────────────────────────────
238 // Below a smooth coastal buffer the seabed stops being a heightfield and
239 // becomes VOLUMETRIC: a 3D fBm added to the density, after Geiss (GPU Gems
240 // 3 ch.1). A heightfield cannot contain a sea cave, an arch or an overhang,
241 // and the whole world is meant to be reachable.
242 //
243 // The buffer is why it is depth-gated rather than global. Beaches and the
244 // waterline need a single-valued surface -- water meeting a cave mouth at
245 // the shoreline has no clean coastline -- so the first tens of metres of
246 // depth stay a pure heightfield and the volumetric term fades in below.
247 //
248 // amp 0 DISABLES it, so a map that has not opted in is bit-identical.
249 float submarine_amp_ = 0.0f; // metres of 3D perturbation
250 float submarine_wavelength_ = 3000.0f;
251 int submarine_octaves_ = 4;
252 float submarine_roughness_ = 0.5f;
253 float submarine_buffer_start_ = 50.0f; // depth where it begins
254 float submarine_buffer_end_ = 400.0f; // depth where it is full
255 float submarine_water_y_ = 700.0f;
256 // Depth over which erosion fades out below the waterline, metres. Erosion
257 // is subaerial: it stops AT the shoreline in reality, but a hard cut leaves
258 // a visible seam along every coast, so it ramps. 0 disables the mask.
259 float erosion_mask_depth_ = 150.0f;
260 // MUST MATCH ErosionNoiseNode's height_scale (its pc.param1). The depth
261 // conversion below needs it and the generator cannot see the node's
262 // parameters, so it is mirrored here -- and a mismatch shows up as an
263 // erosion mask that fades over the wrong depth, not as an error.
264 float erosion_height_scale_ = 5.0f;
265
237266 float detail_slope_lo_ = 0.30f;
238267 float detail_slope_hi_ = 1.20f;
239268
……@@ -428,6 +457,18 @@ public:
428457 void set_crease_rounding(float p_val);
429458 float get_crease_rounding() const { return crease_rounding_; }
430459
460 void set_submarine_amp(float v);
461 float get_submarine_amp() const { return submarine_amp_; }
462 void set_submarine_wavelength(float v);
463 float get_submarine_wavelength() const { return submarine_wavelength_; }
464 void set_submarine_octaves(int v);
465 int get_submarine_octaves() const { return submarine_octaves_; }
466 void set_submarine_buffer(float a, float b);
467 void set_submarine_water_y(float v);
468 void set_erosion_mask_depth(float v);
469 float get_erosion_mask_depth() const { return erosion_mask_depth_; }
470 float get_submarine_water_y() const { return submarine_water_y_; }
471
431472 void set_detail_slope_lo(float p_val);
432473 float get_detail_slope_lo() const { return detail_slope_lo_; }
433474 void set_detail_slope_hi(float p_val);