OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain commits / 4dc8188

procnodes: stop discarding lod_voxel_size — clamp the wrapper's octaves
float x, float y, float z, float lod_voxel_size) { (void)lod_voxel_size; The LOD voxel size was handed to the generator and thrown away, which is why this generator had no Nyquist protection on any layer: the wrapper passed literal octave counts (mountain 8, relief 2, detail 4) and the graph nodes' min(N, pn_max_octaves) is disarmed by pn_max_octaves = 99. Raising mountain_scale then walks the fine mountain octaves under the voxel size and the terrain shatters into vertical spikes. Both paths now compute a fractional limit per layer and taper the last octave. The GLSL reads voxel_scale, which is declared by BOTH shaders the body is injected into -- density_fill.glsl and edge_intersect.glsl -- above the injection point. The CPU reads the parameter it was already being given. sample_density passes 0, which nyquist_octaves reads as "no limit", so collision keeps the full field. That is deliberate: near the player nothing is clamped anyway (LOD 0-5 keep all 8 octaves), so the collidable and drawn surfaces still agree where it matters, and the parity oracle checks it. The oracle gained --voxel and now models the clamp on both sides, because the clamp is a FUNCTION of voxel size -- comparing wrappers at different voxel sizes would manufacture a disagreement. Green at three LODs: voxel 1 m GLSL-vs-C++ 0.0039 wrappers AGREE voxel 64 m GLSL-vs-C++ 0.0034 wrappers AGREE voxel 512 m GLSL-vs-C++ 0.0030 wrappers AGREE The engine's assembled 45642-byte shader, dumped via get_compiled_glsl(), compiles clean under glslc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4dc818846763a83a817a5075f6254b003dd3761b
by plankatron <fleeky@gmail.com> · 2026-08-31T01:10:24.000Z

Changed files (2) +60 -22

src/dc_terrain_generator_procnodes.cpp +34 -11
……@@ -437,10 +437,19 @@ void DCTerrainGeneratorProcNodes::_recompile_glsl() {
437437 " // accumulated so far, putting the roughness on high ground. Plain fBm is\n"
438438 " // statistically homogeneous -- equally rugged in the basins and on the\n"
439439 " // peaks -- which is what reads as every range looking the same.\n"
440 " vec3 mtn = fractal_multifractal_2d(vec2(x, z) * inv_scale * " + mscale_str + ", 1.0, " + moct_str + ", " + lac_str + ", " + mgain_str + ", " + String::num(mountain_variance_, 4) + ");\n"
440 " // NYQUIST LIMITS. voxel_scale is the LOD's voxel size in metres and is\n"
441 " // declared by BOTH shaders this body is injected into (density_fill.glsl\n"
442 " // and edge_intersect.glsl), above the injection point. One cycle of a\n"
443 " // layer spans terrain_height/scale world metres, so that is its base\n"
444 " // wavelength. Fractional, so an octave fades in as a chunk refines\n"
445 " // rather than snapping on and moving the surface underfoot.\n"
446 " float mtn_lim = nyquist_octaves(" + th_str + " / max(" + mscale_str + ", 1e-6), voxel_scale, " + lac_str + ");\n"
447 " float rel_lim = nyquist_octaves(" + th_str + " / max(" + rscale_str + ", 1e-6), voxel_scale, 2.0);\n"
448 " float det_lim = nyquist_octaves(" + th_str + " / max(" + dscale_str + ", 1e-6), voxel_scale, " + lac_str + ");\n"
449 " 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"
441450 " float relief_amp = 1.0;\n"
442451 " if (" + rscale_str + " > 0.0) {\n"
443 " vec3 rel = fractal_noise_2d(vec2(x, z) * inv_scale * " + rscale_str + ", 1.0, 2, 2.0, 0.5);\n"
452 " vec3 rel = fractal_noise_2d_lod(vec2(x, z) * inv_scale * " + rscale_str + ", 1.0, 2, 2.0, 0.5, rel_lim);\n"
444453 " float t = clamp(rel.x * 0.5 + 0.5, 0.0, 1.0);\n"
445454 " relief_amp = " + rfloor_str + " + (1.0 - " + rfloor_str + ") * t;\n"
446455 " }\n"
……@@ -461,7 +470,7 @@ void DCTerrainGeneratorProcNodes::_recompile_glsl() {
461470 " // noise. Detail is what water runs over, not what steers it.\n"
462471 " float det_offset = 0.0;\n"
463472 " if (" + dscale_str + " > 0.0) {\n"
464 " vec3 det = fractal_noise_2d(vec2(x, z) * inv_scale * " + dscale_str + ", 1.0, " + doct_str + ", " + lac_str + ", " + dgain_str + ");\n"
473 " vec3 det = fractal_noise_2d_lod(vec2(x, z) * inv_scale * " + dscale_str + ", 1.0, " + doct_str + ", " + lac_str + ", " + dgain_str + ", det_lim);\n"
465474 " det_offset = det.x * " + dfrac_str + ";\n"
466475 " }\n"
467476 + (height_space_sqrt_
……@@ -492,7 +501,12 @@ void DCTerrainGeneratorProcNodes::_recompile_glsl() {
492501
493502float DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate(
494503 float x, float y, float z, float lod_voxel_size) {
495 (void)lod_voxel_size;
504 // lod_voxel_size USED to be discarded here, which is why this generator had
505 // no Nyquist protection on any layer: the wrapper passed literal octave
506 // counts and the graph nodes' min(N, pn_max_octaves) is disarmed by
507 // pn_max_octaves = 99. DCTerrain::sample_density passes 0 on purpose
508 // (dc_terrain.cpp:2841) and nyquist_octaves reads that as "no limit", so
509 // collision keeps evaluating the full field.
496510
497511 // Read graph parameters (same values as the GLSL adapter uses)
498512 float terrain_height = owner_->terrain_height_;
……@@ -543,14 +557,23 @@ float DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate(
543557 // remaining piece.
544558 using dcerosion::vec2;
545559 using dcerosion::vec3;
546 vec3 mtn = dcerosion::fractal_multifractal_2d(vec2(x, z) * inv_scale * mtn_scale, 1.0f,
547 mtn_octaves, owner_->noise_lacunarity_, owner_->mountain_gain_,
548 owner_->mountain_variance_);
560 // MUST match the GLSL above: same base wavelengths, same lacunarities.
561 const float lac = owner_->noise_lacunarity_;
562 float mtn_lim = dcerosion::nyquist_octaves(terrain_height / MAX(mtn_scale, 1e-6f),
563 lod_voxel_size, lac);
564 float rel_lim = dcerosion::nyquist_octaves(
565 terrain_height / MAX(owner_->relief_scale_, 1e-6f), lod_voxel_size, 2.0f);
566 float det_lim = dcerosion::nyquist_octaves(
567 terrain_height / MAX(owner_->detail_scale_, 1e-6f), lod_voxel_size, lac);
568
569 vec3 mtn = dcerosion::fractal_multifractal_2d_lod(vec2(x, z) * inv_scale * mtn_scale, 1.0f,
570 mtn_octaves, lac, owner_->mountain_gain_,
571 owner_->mountain_variance_, mtn_lim);
549572 // Relief field — MUST stay identical to the GLSL above.
550573 float relief_amp = 1.0f;
551574 if (owner_->relief_scale_ > 0.0f) {
552 vec3 rel = dcerosion::fractal_noise_2d(vec2(x, z) * inv_scale * owner_->relief_scale_,
553 1.0f, 2, 2.0f, 0.5f);
575 vec3 rel = dcerosion::fractal_noise_2d_lod(vec2(x, z) * inv_scale * owner_->relief_scale_,
576 1.0f, 2, 2.0f, 0.5f, rel_lim);
554577 float t = dcerosion::clamp(rel.x * 0.5f + 0.5f, 0.0f, 1.0f);
555578 relief_amp = owner_->relief_floor_ + (1.0f - owner_->relief_floor_) * t;
556579 }
……@@ -563,8 +586,8 @@ float DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate(
563586 float det_offset = 0.0f;
564587 if (owner_->detail_scale_ > 0.0f) {
565588 float detail_frac = (y_range > 0.01f) ? (owner_->detail_height_ / y_range) : 0.0f;
566 vec3 det = dcerosion::fractal_noise_2d(vec2(x, z) * inv_scale * owner_->detail_scale_,
567 1.0f, owner_->detail_octaves_, owner_->noise_lacunarity_, owner_->detail_gain_);
589 vec3 det = dcerosion::fractal_noise_2d_lod(vec2(x, z) * inv_scale * owner_->detail_scale_,
590 1.0f, owner_->detail_octaves_, lac, owner_->detail_gain_, det_lim);
568591 det_offset = det.x * detail_frac;
569592 }
570593
tests/test_erosion_parity.cpp +26 -11
……@@ -109,7 +109,13 @@ static const float MAP_MOUNTAIN_HEIGHT = 0.55f; // map_dc_erosion.gd:55
109109static const float MAP_RELIEF_SCALE = 0.5f; // map_dc_erosion.gd:60
110110static const float MAP_RELIEF_FLOOR = 0.08f; // map_dc_erosion.gd:62
111111static const float MAP_MOUNTAIN_GAIN = 0.68f;
112static const float MAP_MOUNTAIN_VARIANCE = 0.0f; // generator "mountain_variance" // map_dc_erosion.gd:69
112static const float MAP_MOUNTAIN_VARIANCE = 0.0f; // generator "mountain_variance"
113
114// The LOD voxel size the wrappers are compared AT. Both sides must use the same
115// value or the Nyquist clamp alone would make them disagree -- the clamp is a
116// function of it. 1.0 m is LOD 0, where nothing is clamped; raise it to compare
117// the wrappers at a coarse LOD instead.
118static float MAP_VOXEL_SIZE = 1.0f; // map_dc_erosion.gd:69
113119#ifndef MAP_DETAIL_SCALE_OVERRIDE
114120#define MAP_DETAIL_SCALE_OVERRIDE 600.0f
115121#endif
……@@ -149,15 +155,19 @@ static const float EN_HEIGHT_AMP = 0.25f;
149155// gn.x * HEIGHT_AMP * 0.5 = gn.x * 0.125.
150156static vec3 terrain_offset(float x, float z) {
151157 const float inv_scale = 1.0f / MAP_TERRAIN_HEIGHT;
152 vec3 mtn = dcerosion::fractal_multifractal_2d(
158 float mtn_lim = dcerosion::nyquist_octaves(
159 MAP_TERRAIN_HEIGHT / MAP_MOUNTAIN_SCALE, MAP_VOXEL_SIZE, MAP_NOISE_LACUNARITY);
160 vec3 mtn = dcerosion::fractal_multifractal_2d_lod(
153161 vec2(x, z) * inv_scale * MAP_MOUNTAIN_SCALE, 1.0f,
154162 MAP_MOUNTAIN_OCT, MAP_NOISE_LACUNARITY, MAP_MOUNTAIN_GAIN,
155 MAP_MOUNTAIN_VARIANCE);
163 MAP_MOUNTAIN_VARIANCE, mtn_lim);
156164
157165 float relief_amp = 1.0f;
158166 if (MAP_RELIEF_SCALE > 0.0f) {
159 vec3 rel = dcerosion::fractal_noise_2d(
160 vec2(x, z) * inv_scale * MAP_RELIEF_SCALE, 1.0f, 2, 2.0f, 0.5f);
167 vec3 rel = dcerosion::fractal_noise_2d_lod(
168 vec2(x, z) * inv_scale * MAP_RELIEF_SCALE, 1.0f, 2, 2.0f, 0.5f,
169 dcerosion::nyquist_octaves(MAP_TERRAIN_HEIGHT / MAP_RELIEF_SCALE,
170 MAP_VOXEL_SIZE, 2.0f));
161171 float t = dcerosion::clamp(rel.x * 0.5f + 0.5f, 0.0f, 1.0f);
162172 relief_amp = MAP_RELIEF_FLOOR + (1.0f - MAP_RELIEF_FLOOR) * t;
163173 }
……@@ -170,9 +180,11 @@ static float detail_offset(float x, float z) {
170180 if (MAP_DETAIL_SCALE <= 0.0f) return 0.0f;
171181 const float inv_scale = 1.0f / MAP_TERRAIN_HEIGHT;
172182 const float y_range = MAP_CEILING_Y - MAP_FLOOR_Y;
173 vec3 det = dcerosion::fractal_noise_2d(
183 vec3 det = dcerosion::fractal_noise_2d_lod(
174184 vec2(x, z) * inv_scale * MAP_DETAIL_SCALE, 1.0f,
175 MAP_DETAIL_OCT, MAP_NOISE_LACUNARITY, MAP_DETAIL_GAIN);
185 MAP_DETAIL_OCT, MAP_NOISE_LACUNARITY, MAP_DETAIL_GAIN,
186 dcerosion::nyquist_octaves(MAP_TERRAIN_HEIGHT / MAP_DETAIL_SCALE,
187 MAP_VOXEL_SIZE, MAP_NOISE_LACUNARITY));
176188 return det.x * (MAP_DETAIL_HEIGHT / y_range);
177189}
178190
……@@ -450,6 +462,7 @@ static std::string build_shader(const std::string& inc) {
450462 "const float M_FLOOR_Y=%.9g, M_CEIL_Y=%.9g, M_TH=%.9g;\n"
451463 "const float M_MSCALE=%.9g, M_MHEIGHT=%.9g, M_MGAIN=%.9g;\n"
452464 "const float M_MVAR=%.9g;\n"
465 "const float M_VOXEL=%.9g;\n"
453466 "const int M_MOCT=%d;\n"
454467 "const float M_RSCALE=%.9g, M_RFLOOR=%.9g;\n"
455468 "const float M_DSCALE=%.9g, M_DHEIGHT=%.9g, M_DGAIN=%.9g;\n"
……@@ -462,7 +475,7 @@ static std::string build_shader(const std::string& inc) {
462475 "const float EN_ESCALE=%.9g;\n",
463476 MAP_FLOOR_Y, MAP_CEILING_Y, MAP_TERRAIN_HEIGHT,
464477 MAP_MOUNTAIN_SCALE, MAP_MOUNTAIN_HEIGHT, MAP_MOUNTAIN_GAIN,
465 MAP_MOUNTAIN_VARIANCE, MAP_MOUNTAIN_OCT,
478 MAP_MOUNTAIN_VARIANCE, MAP_VOXEL_SIZE, MAP_MOUNTAIN_OCT,
466479 MAP_RELIEF_SCALE, MAP_RELIEF_FLOOR,
467480 MAP_DETAIL_SCALE, MAP_DETAIL_HEIGHT, MAP_DETAIL_GAIN, MAP_DETAIL_OCT,
468481 MAP_NOISE_LACUNARITY,
……@@ -474,10 +487,11 @@ static std::string build_shader(const std::string& inc) {
474487 s +=
475488 "vec3 terrain_offset(float x, float z) {\n"
476489 " float inv_scale = 1.0 / M_TH;\n"
477 " vec3 mtn = fractal_multifractal_2d(vec2(x,z)*inv_scale*M_MSCALE, 1.0, M_MOCT, M_LAC, M_MGAIN, M_MVAR);\n"
490 " float mtn_lim = nyquist_octaves(M_TH / M_MSCALE, M_VOXEL, M_LAC);\n"
491 " vec3 mtn = fractal_multifractal_2d_lod(vec2(x,z)*inv_scale*M_MSCALE, 1.0, M_MOCT, M_LAC, M_MGAIN, M_MVAR, mtn_lim);\n"
478492 " float relief_amp = 1.0;\n"
479493 " if (M_RSCALE > 0.0) {\n"
480 " vec3 rel = fractal_noise_2d(vec2(x,z)*inv_scale*M_RSCALE, 1.0, 2, 2.0, 0.5);\n"
494 " vec3 rel = fractal_noise_2d_lod(vec2(x,z)*inv_scale*M_RSCALE, 1.0, 2, 2.0, 0.5, nyquist_octaves(M_TH/M_RSCALE, M_VOXEL, 2.0));\n"
481495 " float t = clamp(rel.x*0.5+0.5, 0.0, 1.0);\n"
482496 " relief_amp = M_RFLOOR + (1.0-M_RFLOOR)*t;\n"
483497 " }\n"
……@@ -488,7 +502,7 @@ static std::string build_shader(const std::string& inc) {
488502 " if (M_DSCALE <= 0.0) return 0.0;\n"
489503 " float inv_scale = 1.0 / M_TH;\n"
490504 " float y_range = M_CEIL_Y - M_FLOOR_Y;\n"
491 " vec3 det = fractal_noise_2d(vec2(x,z)*inv_scale*M_DSCALE, 1.0, M_DOCT, M_LAC, M_DGAIN);\n"
505 " vec3 det = fractal_noise_2d_lod(vec2(x,z)*inv_scale*M_DSCALE, 1.0, M_DOCT, M_LAC, M_DGAIN, nyquist_octaves(M_TH/M_DSCALE, M_VOXEL, M_LAC));\n"
492506 " return det.x * (M_DHEIGHT / y_range);\n"
493507 "}\n"
494508 "float y_mapped_of(float y, float det_off) {\n"
……@@ -538,6 +552,7 @@ int main(int argc, char** argv) {
538552 for (int i = 1; i < argc; i++) {
539553 if (!strcmp(argv[i], "--inc") && i + 1 < argc) inc_path = argv[++i];
540554 else if (!strcmp(argv[i], "--columns") && i + 1 < argc) columns = atoi(argv[++i]);
555 else if (!strcmp(argv[i], "--voxel") && i + 1 < argc) MAP_VOXEL_SIZE = (float)atof(argv[++i]);
541556 }
542557
543558 std::string inc = read_file(inc_path);