OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain commits / adb27c6

geiss: the density as a dc-terrain generator, and a path for its volumes
DCGeneratorGeiss meshes the SAME field hyper-tps raymarches, so "is marching or meshing cheaper for this terrain" becomes a measurement rather than an argument. Both sides read one generated body -- shaders/generators/geiss_sdf.glsl, emitted from hyper-tps/shaders/geiss_density.gdshaderinc by gen_geiss_dc_glsl.py, and embedded as geiss_sdf_glsl.h because gpu_shader_source() returns a const char* and a generator has no shader-directory resolution of its own. One source, so the meshed terrain and the marched terrain cannot drift apart. The field SAMPLES noise instead of computing it, which no generator could do before: DCGenerator gains gpu_noise_volumes()/gpu_noise_dim()/gpu_noise_version() alongside the existing gpu_custom_data family, under the same stable-pointer + content-version contract. Threaded through GpuFillRequest and both submit() paths, including dc_worker_cpu.cpp -- the per-chunk one, which is the path that actually meshes anything. Upload happens BEFORE the shader compile in the GPU thread: set_custom_shader rebuilds the uniform set, and the descriptors it writes must already point at the real volumes rather than the 1^3 placeholders. load_volumes() refuses partially: if any of the eight files is missing it leaves the generator GPU-disabled instead of meshing a field that silently differs from the raymarched one. The CPU evaluate() is deliberately just -y, present so a chunk requested while the GPU is down is air rather than a crash; it is NOT the field and a mesh built from it would be wrong. Not yet reachable from GDScript -- no Resource wrapper or registration -- so nothing calls it and existing maps are unaffected. Verified on map_dc_erosion: edge_intersect compiles twice, 101 chunk lines, no shader or uniform-set errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
adb27c6316805b4e96498ce06436981fc4a263de
by plankatron <fleeky@gmail.com> · 2026-09-07T22:33:54.000Z

Changed files (7) +547 -3

libdc/include/dc_generator.h +14 -0
……@@ -90,6 +90,20 @@ public:
9090 // Bump this whenever the bytes change, whatever the pointer does.
9191 virtual uint32_t gpu_custom_data_version() const { return 0; }
9292
93 // NOISE VOLUMES for a density body that SAMPLES noise rather than computing
94 // it (GPU Gems 3 ch.1 fetches eight 16^3 RGBA volumes; that is why nine
95 // octaves were affordable in 2007). Transported to sampler3D bindings the
96 // host shader declares by name, under the same stable-pointer contract as
97 // gpu_custom_data above.
98 //
99 // Returns an array of EIGHT pointers -- plain 0-3, then packed 4-7 -- each
100 // to dim^3 RGBA float32. nullptr means the body samples no volumes, and
101 // then neither shader declares the bindings.
102 virtual const float* const* gpu_noise_volumes() const { return nullptr; }
103 // Side length, unlike gpu_custom_data_count which is an element count.
104 virtual int gpu_noise_dim() const { return 0; }
105 virtual uint32_t gpu_noise_version() const { return 0; }
106
93107 // Optional: bounding box hint for grid allocation.
94108 // Writes axis-aligned min/max into min[3]/max[3]. Returns false if unbounded.
95109 virtual bool get_bounds(float min[3], float max[3]) const { return false; }
libdc/include/dc_generator_geiss.h +114 -0
……@@ -0,0 +1,114 @@
1#ifndef DC_GENERATOR_GEISS_H
2#define DC_GENERATOR_GEISS_H
3
4// GPU Gems 3 ch.1 (Geiss) terrain as a dc-terrain generator.
5//
6// EXISTS TO BE COMPARED. The same density function is raymarched by
7// hyper-tps/shaders/geiss_volume.gdshader and meshed here, so "is marching or
8// meshing cheaper for this field" can be measured instead of argued. Both read
9// the SAME generated body (shaders/generators/geiss_sdf.glsl, embedded as
10// geiss_sdf_glsl.h) from the SAME source include, so the two cannot drift.
11//
12// The field SAMPLES noise rather than computing it: eight 16^3 RGBA volumes
13// shipped with the demo. Those reach the shader through gpu_noise_volumes().
14
15#include "dc_generator.h"
16#include "geiss_sdf_glsl.h"
17
18#include <cstdint>
19#include <cstdio>
20#include <string>
21#include <vector>
22
23class DCGeneratorGeiss : public DCGenerator {
24public:
25 bool has_gpu_shader() const override { return volumes_ok_; }
26 const char* gpu_shader_source() const override { return GEISS_SDF_GLSL; }
27
28 const float* const* gpu_noise_volumes() const override {
29 return volumes_ok_ ? volume_ptrs_ : nullptr;
30 }
31 int gpu_noise_dim() const override { return dim_; }
32 uint32_t gpu_noise_version() const override { return version_; }
33
34 // dir must contain the demo's noise_half_16cubed_mips_0N.vol and
35 // packednoise_half_16cubed_mips_0N.vol. Returns false and leaves the
36 // generator GPU-disabled if any of the eight is missing, rather than
37 // meshing a field that silently differs from the raymarched one.
38 bool load_volumes(const std::string& dir) {
39 static const char* kNames[8] = {
40 "noise_half_16cubed_mips_00.vol", "noise_half_16cubed_mips_01.vol",
41 "noise_half_16cubed_mips_02.vol", "noise_half_16cubed_mips_03.vol",
42 "packednoise_half_16cubed_mips_00.vol", "packednoise_half_16cubed_mips_01.vol",
43 "packednoise_half_16cubed_mips_02.vol", "packednoise_half_16cubed_mips_03.vol",
44 };
45 volumes_ok_ = false;
46 for (int i = 0; i < 8; i++) {
47 if (!_load_one(dir + "/" + kNames[i], storage_[i])) {
48 fprintf(stderr, "DCGeneratorGeiss: cannot read %s\n", kNames[i]);
49 return false;
50 }
51 volume_ptrs_[i] = storage_[i].data();
52 }
53 volumes_ok_ = true;
54 version_++;
55 return true;
56 }
57
58 // CPU fallback. The GPU path is the point of this generator, so this exists
59 // only so a chunk requested while the GPU is unavailable is air rather than
60 // a crash -- it is NOT the field, and a mesh built from it would be wrong.
61 float evaluate(float, float y, float, float) override { return -y; }
62
63private:
64 // 5-int header (w, h, d, bytes-per-texel, body-bytes), then RGBA half
65 // floats, mip 0 first. Only mip 0 is read: the density samples
66 // textureLod(..., 0) everywhere.
67 static bool _load_one(const std::string& path, std::vector<float>& out) {
68 FILE* f = fopen(path.c_str(), "rb");
69 if (!f) return false;
70 int32_t hdr[5];
71 if (fread(hdr, sizeof(int32_t), 5, f) != 5) { fclose(f); return false; }
72 const int w = hdr[0], h = hdr[1], d = hdr[2];
73 if (w <= 0 || h <= 0 || d <= 0 || w != h || h != d) { fclose(f); return false; }
74 const size_t n = size_t(w) * h * d * 4;
75 std::vector<uint16_t> halfs(n);
76 const bool read_ok = fread(halfs.data(), sizeof(uint16_t), n, f) == n;
77 fclose(f);
78 if (!read_ok) return false;
79 out.resize(n);
80 for (size_t i = 0; i < n; i++) out[i] = _half_to_float(halfs[i]);
81 return true;
82 }
83
84 static float _half_to_float(uint16_t h) {
85 const uint32_t sign = uint32_t(h & 0x8000) << 16;
86 uint32_t exp = (h >> 10) & 0x1F;
87 uint32_t man = h & 0x3FF;
88 uint32_t bits;
89 if (exp == 0) {
90 if (man == 0) { bits = sign; }
91 else { // subnormal: normalise it
92 exp = 1;
93 while ((man & 0x400) == 0) { man <<= 1; exp--; }
94 man &= 0x3FF;
95 bits = sign | ((exp + 112) << 23) | (man << 13);
96 }
97 } else if (exp == 31) { // inf / nan
98 bits = sign | 0x7F800000u | (man << 13);
99 } else {
100 bits = sign | ((exp + 112) << 23) | (man << 13);
101 }
102 float out;
103 __builtin_memcpy(&out, &bits, 4);
104 return out;
105 }
106
107 std::vector<float> storage_[8];
108 const float* volume_ptrs_[8] = {};
109 int dim_ = 16;
110 uint32_t version_ = 0;
111 bool volumes_ok_ = false;
112};
113
114#endif // DC_GENERATOR_GEISS_H
libdc/include/geiss_sdf_glsl.h +198 -0
……@@ -0,0 +1,198 @@
1// GENERATED by hyper-tps/tools/gen_geiss_dc_glsl.py -- DO NOT EDIT.
2// Mirror of shaders/generators/geiss_sdf.glsl, embedded because
3// DCGenerator::gpu_shader_source() returns a const char* and the generator
4// has no shader-directory resolution of its own.
5#ifndef GEISS_SDF_GLSL_H
6#define GEISS_SDF_GLSL_H
7
8static const char* GEISS_SDF_GLSL = R"GEISSGLSL(
9// GENERATED by hyper-tps/tools/gen_geiss_dc_glsl.py from
10// hyper-tps/shaders/geiss_density.gdshaderinc -- DO NOT EDIT.
11//
12// GPU Gems 3 ch.1 (Geiss) density, as a dc-terrain generator body.
13//
14// Provides: float evaluate_density(float x, float y, float z)
15//
16// Requires the host shader to declare the eight noise volumes by NAME
17// (noiseVol0..3, packedNoiseVol0..3). The binding NUMBERS differ between the
18// two shaders this is injected into -- 14..21 in edge_intersect.glsl, 15..22 in
19// the density header, where binding 14 is already custom_data_tex -- which is
20// safe precisely because this body names them and never numbers them.
21//
22// Sign convention matches: Geiss's "positive values are inside the terrain" is
23// dc-terrain's "positive = solid".
24
25// GPU Gems 3 ch.1 (Geiss), ported verbatim from
26// gamedev/gpu-gems-3/GPU Gems 3 code/content/01/demo/shaders/{density,sampleNoise}.h
27// sampling the demo's own 16^3 noise volumes.
28//
29// SHARED so the volume BAKE and anything that evaluates the field directly
30// cannot drift apart -- the article's own pipeline builds a density volume
31// once (1_build_density_vol.psh) and everything downstream samples that.
32
33
34const float NOISE_LATTICE_SIZE = 16.0;
35const float INV_LATTICE_SIZE = 1.0 / 16.0;
36
37const float prewarp_str = 25.0;
38// The terrain cannot leave this vertical slab; everything outside is sky.
39
40// ── sampleNoise.h ───────────────────────────────────────────────────────────
41vec4 NLQu(vec3 uvw, sampler3D t) { return textureLod(t, uvw, 0.0); }
42vec4 NLQs(vec3 uvw, sampler3D t) { return NLQu(uvw, t) * 2.0 - 1.0; }
43
44vec3 nmq_warp(vec3 uvw) {
45 vec3 t = fract(uvw * NOISE_LATTICE_SIZE + 0.5);
46 vec3 t2 = (3.0 - 2.0 * t) * t * t;
47 return uvw + (t2 - t) * INV_LATTICE_SIZE;
48}
49vec4 NMQu(vec3 uvw, sampler3D t) { return NLQu(nmq_warp(uvw), t); }
50vec4 NMQs(vec3 uvw, sampler3D t) { return NLQs(nmq_warp(uvw), t); }
51
52// Two-sample manual trilinear against the PACKED volumes, per the article:
53// .yzw carry the +Y, +Z and +YZ neighbours of .x.
54float NHQu(vec3 uvw, sampler3D tex, float smooth_it) {
55 vec3 uvw2 = floor(uvw * NOISE_LATTICE_SIZE) * INV_LATTICE_SIZE;
56 vec3 t = (uvw - uvw2) * NOISE_LATTICE_SIZE;
57 t = mix(t, t * t * (3.0 - 2.0 * t), smooth_it);
58 vec2 d = vec2(INV_LATTICE_SIZE, 0.0);
59 // ⚠ .xzyw, NOT the .zxyw that sampleNoise.h line 86 writes.
60 //
61 // _about_the_noise_volumes.txt and a channel-by-channel check of the shipped
62 // files both confirm packed = (R base, G +Y, B +Z, A +YZ). The manual
63 // trilinear below interpolates X, then Y, then Z, so it needs the tuple
64 // ordered (base, +Z, +Y, +YZ) -- which is .xzyw. Verified numerically
65 // against the 8-tap reference in the #if 0 block of the original: .xzyw
66 // reproduces it EXACTLY (max err 0.000000 over 20k points) while .zxyw
67 // gives max err 0.907 / mean 0.083.
68 //
69 // NHQs feeds the whole 25-unit prewarp and the amplitude-32 octave, so this
70 // was corrupting the two largest contributions to the terrain.
71 vec4 f1 = textureLod(tex, uvw2, 0.0).xzyw;
72 vec4 f2 = textureLod(tex, uvw2 + d.xyy, 0.0).xzyw;
73 vec4 f3 = mix(f1, f2, t.xxxx);
74 vec2 f4 = mix(f3.xy, f3.zw, t.yy);
75 return mix(f4.x, f4.y, t.z);
76}
77float NHQs(vec3 uvw, sampler3D tex, float smooth_it) { return NHQu(uvw, tex, smooth_it) * 2.0 - 1.0; }
78
79float smooth_snap(float t, float m) {
80 float c = (t > 0.5) ? 1.0 : 0.0;
81 float s = 1.0 - c * 2.0;
82 return c + s * pow((c + s * t) * 2.0, m) * 0.5;
83}
84
85// octaveMat0..7 are uniforms. main.nma ships them as identity, but that is only
86// a placeholder: TerrainDemo::Randomize() (TerrainDemo.cpp:2588) overwrites every
87// one with a random Euler-XYZ rotation in [-pi, pi], and Init.cpp:364 calls it
88// during startup -- after the constructor has already done srand(timeGetTime())
89// (TerrainDemo.cpp:670).
90//
91// So the demo draws a DIFFERENT terrain on every launch, and there is no single
92// canonical Geiss terrain to match; the article's figures are one draw. Any
93// fixed rotation set is therefore exactly as faithful as any other. These two
94// are one such draw, generated the way Randomize() does it.
95//
96// Only c6 and c7 are consumed by DENSITY, so only these two matter. rot() uses
97// the upper-left 3x3 only, so Randomize()'s translation is deliberately absent.
98const mat3 OCTAVE_MAT6 = mat3(vec3( 0.534965, -0.143815, 0.832544),
99 vec3( 0.228474, 0.973317, 0.021323),
100 vec3(-0.813395, 0.178808, 0.553548));
101const mat3 OCTAVE_MAT7 = mat3(vec3(-0.361212, 0.871481, 0.331733),
102 vec3( 0.932478, 0.338770, 0.125376),
103 vec3(-0.003119, 0.354621, -0.935005));
104
105vec3 rot6(vec3 p) { return OCTAVE_MAT6 * p; }
106vec3 rot7(vec3 p) { return OCTAVE_MAT7 * p; }
107
108// ── density.h ───────────────────────────────────────────────────────────────
109// The article's own EVAL_CHEAP path: it drops the highest-frequency octaves
110// ("skipped for long-range ambo"), which is exactly the right trade for the
111// MARCH -- they cannot move the crossing by more than their amplitude. The
112// full function is still used for the final normal.
113float DENSITY_impl(vec3 ws, bool cheap) {
114 vec3 ws_orig = ws;
115 float density = 0.0;
116
117 vec4 uulf_rand = clamp(NMQu(ws * 0.000718, noiseVol0) * 2.0 - 0.5, 0.0, 1.0);
118 vec4 uulf_rand2 = NMQu(ws * 0.000632, noiseVol1);
119 vec4 uulf_rand3 = NMQu(ws * 0.000695, noiseVol2);
120
121 // PRE-WARP. 25 units of domain warping -- the single biggest source of the
122 // overhangs in the article's figures, and the thing our own attempt had
123 // none of.
124 vec3 ulf_rand;
125 ulf_rand.x = NHQs(ws * 0.0041 * 0.971, packedNoiseVol2, 1.0) * 0.64
126 + NHQs(ws * 0.0041 * 0.461, packedNoiseVol3, 1.0) * 0.32;
127 ulf_rand.y = NHQs(ws * 0.0041 * 0.997, packedNoiseVol1, 1.0) * 0.64
128 + NHQs(ws * 0.0041 * 0.453, packedNoiseVol0, 1.0) * 0.32;
129 ulf_rand.z = NHQs(ws * 0.0041 * 1.032, packedNoiseVol3, 1.0) * 0.64
130 + NHQs(ws * 0.0041 * 0.511, packedNoiseVol2, 1.0) * 0.32;
131 ws += ulf_rand * prewarp_str * clamp(uulf_rand3.x * 1.4 - 0.3, 0.0, 1.0);
132
133 vec3 c6 = rot6(ws);
134 vec3 c7 = rot7(ws);
135
136 // MAIN SHAPE: general ground plane, gradient 1.0 per unit.
137 density = -ws.y;
138 density += clamp((-4.0 - ws_orig.y * 0.3) * 3.0, 0.0, 1.0) * 40.0 * uulf_rand2.z;
139
140 // CRUSTY SHELF -- smooth tops, crumbly eroded undersides.
141 float shelf_thickness_y = 2.5;
142 float shelf_pos_y = -1.0;
143 float shelf_strength = 9.5;
144 density = mix(density, shelf_strength,
145 0.83 * clamp(shelf_thickness_y - abs(ws.y - shelf_pos_y), 0.0, 1.0)
146 * clamp(uulf_rand.y * 1.5 - 0.5, 0.0, 1.0));
147
148 // FLAT TERRACES -- and the overhang term, explicitly.
149 {
150 float terraces_can_warp = 0.5 * uulf_rand2.y;
151 float terrace_freq_y = 0.13;
152 float terrace_str = 3.0 * clamp(uulf_rand.z * 2.0 - 1.0, 0.0, 1.0);
153 float overhang_str = 1.0 * clamp(uulf_rand.z * 2.0 - 1.0, 0.0, 1.0);
154 float fy = -mix(ws_orig.y, ws.y, terraces_can_warp) * terrace_freq_y;
155 float orig_t = fract(fy);
156 float t = smooth_snap(orig_t, 16.0);
157 fy = floor(fy) + t;
158 density += fy * terrace_str;
159 density += (t - orig_t) * overhang_str;
160 }
161
162 // Repeating ridges on the warped Y coord -- note the 27x anisotropy.
163 density += NLQs(ws * vec3(2.0, 27.0, 2.0) * 0.0037, noiseVol0).x * 2.0
164 * clamp(uulf_rand2.w * 2.0 - 1.0, 0.0, 1.0);
165
166 // Nine octaves. Amplitude * frequency is ~0.05 for every one of them, so
167 // they sum to ~0.41 against a base gradient of 1.0 -- the noise ALONE
168 // cannot fold the surface. The folds come from the warp and the terms above.
169 float HFM = cheap ? 0.0 : 1.0;
170 density +=
171 NLQs(ws * 0.1600 * 1.021, noiseVol1).x * 0.32 * 1.16 * HFM
172 + NLQs(ws * 0.0800 * 0.985, noiseVol2).x * 0.64 * 1.12 * HFM
173 + NLQs(ws * 0.0400 * 1.051, noiseVol0).x * 1.28 * 1.08 * HFM
174 + NLQs(ws * 0.0200 * 1.020, noiseVol1).x * 2.56 * 1.04
175 + NLQs(ws * 0.0100 * 0.968, noiseVol3).x * 5.0
176 + NMQs(ws * 0.0050 * 0.994, noiseVol0).x * 10.0
177 + NMQs(c6 * 0.0025 * 1.045, noiseVol2).x * 20.0 * 0.9
178 + NHQs(c7 * 0.0012 * 0.972, packedNoiseVol3, 1.0) * 40.0 * 0.8;
179
180 // density.h ends with `density -= wsChunkSize.x*0.009`, an LOD bias that
181 // shrinks coarse chunks so finer ones enclose them. There are no chunks
182 // here, so it is deliberately omitted.
183 return density;
184}
185
186// Godot's shading language has no forward declarations, so these live after the
187// definition rather than before it.
188float DENSITY(vec3 ws) { return DENSITY_impl(ws, false); }
189float DENSITY_CHEAP(vec3 ws) { return DENSITY_impl(ws, true); }
190
191
192// dc-terrain calls the field with loose floats.
193float evaluate_density(float x, float y, float z) {
194 return DENSITY(vec3(x, y, z));
195}
196)GEISSGLSL";
197
198#endif // GEISS_SDF_GLSL_H
shaders/generators/geiss_sdf.glsl +187 -0
……@@ -0,0 +1,187 @@
1// GENERATED by hyper-tps/tools/gen_geiss_dc_glsl.py from
2// hyper-tps/shaders/geiss_density.gdshaderinc -- DO NOT EDIT.
3//
4// GPU Gems 3 ch.1 (Geiss) density, as a dc-terrain generator body.
5//
6// Provides: float evaluate_density(float x, float y, float z)
7//
8// Requires the host shader to declare the eight noise volumes by NAME
9// (noiseVol0..3, packedNoiseVol0..3). The binding NUMBERS differ between the
10// two shaders this is injected into -- 14..21 in edge_intersect.glsl, 15..22 in
11// the density header, where binding 14 is already custom_data_tex -- which is
12// safe precisely because this body names them and never numbers them.
13//
14// Sign convention matches: Geiss's "positive values are inside the terrain" is
15// dc-terrain's "positive = solid".
16
17// GPU Gems 3 ch.1 (Geiss), ported verbatim from
18// gamedev/gpu-gems-3/GPU Gems 3 code/content/01/demo/shaders/{density,sampleNoise}.h
19// sampling the demo's own 16^3 noise volumes.
20//
21// SHARED so the volume BAKE and anything that evaluates the field directly
22// cannot drift apart -- the article's own pipeline builds a density volume
23// once (1_build_density_vol.psh) and everything downstream samples that.
24
25
26const float NOISE_LATTICE_SIZE = 16.0;
27const float INV_LATTICE_SIZE = 1.0 / 16.0;
28
29const float prewarp_str = 25.0;
30// The terrain cannot leave this vertical slab; everything outside is sky.
31
32// ── sampleNoise.h ───────────────────────────────────────────────────────────
33vec4 NLQu(vec3 uvw, sampler3D t) { return textureLod(t, uvw, 0.0); }
34vec4 NLQs(vec3 uvw, sampler3D t) { return NLQu(uvw, t) * 2.0 - 1.0; }
35
36vec3 nmq_warp(vec3 uvw) {
37 vec3 t = fract(uvw * NOISE_LATTICE_SIZE + 0.5);
38 vec3 t2 = (3.0 - 2.0 * t) * t * t;
39 return uvw + (t2 - t) * INV_LATTICE_SIZE;
40}
41vec4 NMQu(vec3 uvw, sampler3D t) { return NLQu(nmq_warp(uvw), t); }
42vec4 NMQs(vec3 uvw, sampler3D t) { return NLQs(nmq_warp(uvw), t); }
43
44// Two-sample manual trilinear against the PACKED volumes, per the article:
45// .yzw carry the +Y, +Z and +YZ neighbours of .x.
46float NHQu(vec3 uvw, sampler3D tex, float smooth_it) {
47 vec3 uvw2 = floor(uvw * NOISE_LATTICE_SIZE) * INV_LATTICE_SIZE;
48 vec3 t = (uvw - uvw2) * NOISE_LATTICE_SIZE;
49 t = mix(t, t * t * (3.0 - 2.0 * t), smooth_it);
50 vec2 d = vec2(INV_LATTICE_SIZE, 0.0);
51 // ⚠ .xzyw, NOT the .zxyw that sampleNoise.h line 86 writes.
52 //
53 // _about_the_noise_volumes.txt and a channel-by-channel check of the shipped
54 // files both confirm packed = (R base, G +Y, B +Z, A +YZ). The manual
55 // trilinear below interpolates X, then Y, then Z, so it needs the tuple
56 // ordered (base, +Z, +Y, +YZ) -- which is .xzyw. Verified numerically
57 // against the 8-tap reference in the #if 0 block of the original: .xzyw
58 // reproduces it EXACTLY (max err 0.000000 over 20k points) while .zxyw
59 // gives max err 0.907 / mean 0.083.
60 //
61 // NHQs feeds the whole 25-unit prewarp and the amplitude-32 octave, so this
62 // was corrupting the two largest contributions to the terrain.
63 vec4 f1 = textureLod(tex, uvw2, 0.0).xzyw;
64 vec4 f2 = textureLod(tex, uvw2 + d.xyy, 0.0).xzyw;
65 vec4 f3 = mix(f1, f2, t.xxxx);
66 vec2 f4 = mix(f3.xy, f3.zw, t.yy);
67 return mix(f4.x, f4.y, t.z);
68}
69float NHQs(vec3 uvw, sampler3D tex, float smooth_it) { return NHQu(uvw, tex, smooth_it) * 2.0 - 1.0; }
70
71float smooth_snap(float t, float m) {
72 float c = (t > 0.5) ? 1.0 : 0.0;
73 float s = 1.0 - c * 2.0;
74 return c + s * pow((c + s * t) * 2.0, m) * 0.5;
75}
76
77// octaveMat0..7 are uniforms. main.nma ships them as identity, but that is only
78// a placeholder: TerrainDemo::Randomize() (TerrainDemo.cpp:2588) overwrites every
79// one with a random Euler-XYZ rotation in [-pi, pi], and Init.cpp:364 calls it
80// during startup -- after the constructor has already done srand(timeGetTime())
81// (TerrainDemo.cpp:670).
82//
83// So the demo draws a DIFFERENT terrain on every launch, and there is no single
84// canonical Geiss terrain to match; the article's figures are one draw. Any
85// fixed rotation set is therefore exactly as faithful as any other. These two
86// are one such draw, generated the way Randomize() does it.
87//
88// Only c6 and c7 are consumed by DENSITY, so only these two matter. rot() uses
89// the upper-left 3x3 only, so Randomize()'s translation is deliberately absent.
90const mat3 OCTAVE_MAT6 = mat3(vec3( 0.534965, -0.143815, 0.832544),
91 vec3( 0.228474, 0.973317, 0.021323),
92 vec3(-0.813395, 0.178808, 0.553548));
93const mat3 OCTAVE_MAT7 = mat3(vec3(-0.361212, 0.871481, 0.331733),
94 vec3( 0.932478, 0.338770, 0.125376),
95 vec3(-0.003119, 0.354621, -0.935005));
96
97vec3 rot6(vec3 p) { return OCTAVE_MAT6 * p; }
98vec3 rot7(vec3 p) { return OCTAVE_MAT7 * p; }
99
100// ── density.h ───────────────────────────────────────────────────────────────
101// The article's own EVAL_CHEAP path: it drops the highest-frequency octaves
102// ("skipped for long-range ambo"), which is exactly the right trade for the
103// MARCH -- they cannot move the crossing by more than their amplitude. The
104// full function is still used for the final normal.
105float DENSITY_impl(vec3 ws, bool cheap) {
106 vec3 ws_orig = ws;
107 float density = 0.0;
108
109 vec4 uulf_rand = clamp(NMQu(ws * 0.000718, noiseVol0) * 2.0 - 0.5, 0.0, 1.0);
110 vec4 uulf_rand2 = NMQu(ws * 0.000632, noiseVol1);
111 vec4 uulf_rand3 = NMQu(ws * 0.000695, noiseVol2);
112
113 // PRE-WARP. 25 units of domain warping -- the single biggest source of the
114 // overhangs in the article's figures, and the thing our own attempt had
115 // none of.
116 vec3 ulf_rand;
117 ulf_rand.x = NHQs(ws * 0.0041 * 0.971, packedNoiseVol2, 1.0) * 0.64
118 + NHQs(ws * 0.0041 * 0.461, packedNoiseVol3, 1.0) * 0.32;
119 ulf_rand.y = NHQs(ws * 0.0041 * 0.997, packedNoiseVol1, 1.0) * 0.64
120 + NHQs(ws * 0.0041 * 0.453, packedNoiseVol0, 1.0) * 0.32;
121 ulf_rand.z = NHQs(ws * 0.0041 * 1.032, packedNoiseVol3, 1.0) * 0.64
122 + NHQs(ws * 0.0041 * 0.511, packedNoiseVol2, 1.0) * 0.32;
123 ws += ulf_rand * prewarp_str * clamp(uulf_rand3.x * 1.4 - 0.3, 0.0, 1.0);
124
125 vec3 c6 = rot6(ws);
126 vec3 c7 = rot7(ws);
127
128 // MAIN SHAPE: general ground plane, gradient 1.0 per unit.
129 density = -ws.y;
130 density += clamp((-4.0 - ws_orig.y * 0.3) * 3.0, 0.0, 1.0) * 40.0 * uulf_rand2.z;
131
132 // CRUSTY SHELF -- smooth tops, crumbly eroded undersides.
133 float shelf_thickness_y = 2.5;
134 float shelf_pos_y = -1.0;
135 float shelf_strength = 9.5;
136 density = mix(density, shelf_strength,
137 0.83 * clamp(shelf_thickness_y - abs(ws.y - shelf_pos_y), 0.0, 1.0)
138 * clamp(uulf_rand.y * 1.5 - 0.5, 0.0, 1.0));
139
140 // FLAT TERRACES -- and the overhang term, explicitly.
141 {
142 float terraces_can_warp = 0.5 * uulf_rand2.y;
143 float terrace_freq_y = 0.13;
144 float terrace_str = 3.0 * clamp(uulf_rand.z * 2.0 - 1.0, 0.0, 1.0);
145 float overhang_str = 1.0 * clamp(uulf_rand.z * 2.0 - 1.0, 0.0, 1.0);
146 float fy = -mix(ws_orig.y, ws.y, terraces_can_warp) * terrace_freq_y;
147 float orig_t = fract(fy);
148 float t = smooth_snap(orig_t, 16.0);
149 fy = floor(fy) + t;
150 density += fy * terrace_str;
151 density += (t - orig_t) * overhang_str;
152 }
153
154 // Repeating ridges on the warped Y coord -- note the 27x anisotropy.
155 density += NLQs(ws * vec3(2.0, 27.0, 2.0) * 0.0037, noiseVol0).x * 2.0
156 * clamp(uulf_rand2.w * 2.0 - 1.0, 0.0, 1.0);
157
158 // Nine octaves. Amplitude * frequency is ~0.05 for every one of them, so
159 // they sum to ~0.41 against a base gradient of 1.0 -- the noise ALONE
160 // cannot fold the surface. The folds come from the warp and the terms above.
161 float HFM = cheap ? 0.0 : 1.0;
162 density +=
163 NLQs(ws * 0.1600 * 1.021, noiseVol1).x * 0.32 * 1.16 * HFM
164 + NLQs(ws * 0.0800 * 0.985, noiseVol2).x * 0.64 * 1.12 * HFM
165 + NLQs(ws * 0.0400 * 1.051, noiseVol0).x * 1.28 * 1.08 * HFM
166 + NLQs(ws * 0.0200 * 1.020, noiseVol1).x * 2.56 * 1.04
167 + NLQs(ws * 0.0100 * 0.968, noiseVol3).x * 5.0
168 + NMQs(ws * 0.0050 * 0.994, noiseVol0).x * 10.0
169 + NMQs(c6 * 0.0025 * 1.045, noiseVol2).x * 20.0 * 0.9
170 + NHQs(c7 * 0.0012 * 0.972, packedNoiseVol3, 1.0) * 40.0 * 0.8;
171
172 // density.h ends with `density -= wsChunkSize.x*0.009`, an LOD bias that
173 // shrinks coarse chunks so finer ones enclose them. There are no chunks
174 // here, so it is deliberately omitted.
175 return density;
176}
177
178// Godot's shading language has no forward declarations, so these live after the
179// definition rather than before it.
180float DENSITY(vec3 ws) { return DENSITY_impl(ws, false); }
181float DENSITY_CHEAP(vec3 ws) { return DENSITY_impl(ws, true); }
182
183
184// dc-terrain calls the field with loose floats.
185float evaluate_density(float x, float y, float z) {
186 return DENSITY(vec3(x, y, z));
187}
src/dc_gpu_thread.h +21 -1
……@@ -42,6 +42,9 @@ struct GpuFillRequest {
4242 const float* custom_data = nullptr; // optional: baked custom data (binding 13)
4343 int custom_data_n = 0;
4444 uint32_t custom_data_version = 0; // content version; see DCGenerator
45 const float* const* noise_volumes = nullptr; // optional: 8 sampler3D volumes
46 int noise_dim = 0; // side length (16 for the GPU Gems volumes)
47 uint32_t noise_version = 0;
4548 const std::vector<TerrainEdit>* edits = nullptr; // terrain edits for GPU evaluation
4649 bool wants_gpu_mesh = false; // request GPU mesh generation (skip CPU DC)
4750 DCMeshData* mesh_output = nullptr; // where to write GPU-generated mesh
……@@ -104,7 +107,9 @@ public:
104107 const float* cell_origin = nullptr, float voxel_size = 0.0f,
105108 uint32_t mesh_quality_flags = 0,
106109 const float* custom_data = nullptr, int custom_data_n = 0,
107 uint32_t custom_data_version = 0) {
110 uint32_t custom_data_version = 0,
111 const float* const* noise_volumes = nullptr, int noise_dim = 0,
112 uint32_t noise_version = 0) {
108113 if (!running_.load() || !gpu_available_.load()) {
109114 return false;
110115 }
……@@ -121,6 +126,9 @@ public:
121126 req.custom_data = custom_data;
122127 req.custom_data_n = custom_data_n;
123128 req.custom_data_version = custom_data_version;
129 req.noise_volumes = noise_volumes;
130 req.noise_dim = noise_dim;
131 req.noise_version = noise_version;
124132 req.edits = edits;
125133 if (mesh_output && cell_origin) {
126134 req.wants_gpu_mesh = true;
……@@ -158,6 +166,8 @@ private:
158166 DCGpuMeshPipeline mesh_pipeline_;
159167 bool mesh_pipeline_available_ = false;
160168 const char* current_custom_glsl_ = nullptr; // track to avoid recompiling same shader
169 const float* const* current_noise_volumes_ = nullptr;
170 uint32_t current_noise_version_ = 0;
161171 const float* current_custom_data_ = nullptr; // same trick: re-upload only on change
162172 uint32_t current_custom_data_version_ = 0; // ...and on content version, for growth
163173
……@@ -264,6 +274,16 @@ private:
264274 // Apply custom shader only if it changed since last batch.
265275 // custom_glsl points to cached_glsl_.c_str() which is stable
266276 // until _recompile_glsl() runs on the main thread.
277 // Noise volumes BEFORE the shader compile: set_custom_shader rebuilds
278 // the uniform set, and the descriptors it writes must already point at
279 // the real volumes rather than the 1^3 placeholders.
280 if (batch[0].noise_version != current_noise_version_
281 || batch[0].noise_volumes != current_noise_volumes_) {
282 compute_.set_noise_volumes(batch[0].noise_volumes, batch[0].noise_dim);
283 current_noise_volumes_ = batch[0].noise_volumes;
284 current_noise_version_ = batch[0].noise_version;
285 }
286
267287 const char* glsl = batch[0].custom_glsl;
268288 if (glsl != current_custom_glsl_) {
269289 if (!compute_.set_custom_shader(glsl)) {
src/dc_terrain.cpp +4 -1
……@@ -3199,9 +3199,12 @@ PackedFloat32Array DCTerrain::debug_gpu_density(const Vector3 &origin, float vox
31993199 const float* hb = generator_ptr_->gpu_custom_data();
32003200 int hbn = generator_ptr_->gpu_custom_data_count();
32013201 uint32_t hbv = generator_ptr_->gpu_custom_data_version();
3202 const float* const* nv = generator_ptr_->gpu_noise_volumes();
3203 int nvd = generator_ptr_->gpu_noise_dim();
3204 uint32_t nvv = generator_ptr_->gpu_noise_version();
32023205
32033206 if (!gpu_thread_->submit(grid, density_, glsl, nullptr, nullptr,
3204 nullptr, 0.0f, 0u, hb, hbn, hbv)) {
3207 nullptr, 0.0f, 0u, hb, hbn, hbv, nv, nvd, nvv)) {
32053208 return out; // empty: the GPU declined, and that is the finding
32063209 }
32073210 out.resize((int64_t)grid.data.size());
src/dc_worker_cpu.cpp +9 -1
……@@ -239,11 +239,18 @@ void DCWorkerCPU::_process_cell(const CellRequest &request, int64_t picked_up_us
239239 const float* custom_data = nullptr;
240240 int custom_data_n = 0;
241241 uint32_t custom_data_version = 0;
242 const float* const* noise_volumes = nullptr;
243 int noise_dim = 0;
244 uint32_t noise_version = 0;
242245 if (request.generator && request.generator->has_gpu_shader()) {
243246 custom_glsl = request.generator->gpu_shader_source();
244247 custom_data = request.generator->gpu_custom_data();
245248 custom_data_n = request.generator->gpu_custom_data_count();
246249 custom_data_version = request.generator->gpu_custom_data_version();
250 // Volumes for a body that SAMPLES noise instead of computing it.
251 noise_volumes = request.generator->gpu_noise_volumes();
252 noise_dim = request.generator->gpu_noise_dim();
253 noise_version = request.generator->gpu_noise_version();
247254 }
248255 const std::vector<TerrainEdit>* gpu_edit_ptr =
249256 (request.edits && !request.edits->empty()) ? request.edits.get() : nullptr;
……@@ -251,7 +258,8 @@ void DCWorkerCPU::_process_cell(const CellRequest &request, int64_t picked_up_us
251258 gpu_filled = gpu_thread_->submit(grid, *request.density, custom_glsl, gpu_edit_ptr,
252259 &gpu_mesh, request.cell_origin, request.voxel_size,
253260 (request.use_adaptive_qef ? 2u : 0u) | (request.use_shortest_diagonal ? 1u : 0u),
254 custom_data, custom_data_n, custom_data_version);
261 custom_data, custom_data_n, custom_data_version,
262 noise_volumes, noise_dim, noise_version);
255263 if (!gpu_mesh.indices.empty()) {
256264 gpu_meshed = true;
257265 }