| … | … | @@ -90,6 +90,20 @@ public: |
| 90 | 90 | // Bump this whenever the bytes change, whatever the pointer does. |
| 91 | 91 | virtual uint32_t gpu_custom_data_version() const { return 0; } |
| 92 | 92 | |
| 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 | ||
| 93 | 107 | // Optional: bounding box hint for grid allocation. |
| 94 | 108 | // Writes axis-aligned min/max into min[3]/max[3]. Returns false if unbounded. |
| 95 | 109 | virtual bool get_bounds(float min[3], float max[3]) const { return false; } |
| … | … | @@ -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 | ||
| 23 | class DCGeneratorGeiss : public DCGenerator { | |
| 24 | public: | |
| 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 | ||
| 63 | private: | |
| 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 |
| … | … | @@ -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 | ||
| 8 | static 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 | ||
| 34 | const float NOISE_LATTICE_SIZE = 16.0; | |
| 35 | const float INV_LATTICE_SIZE = 1.0 / 16.0; | |
| 36 | ||
| 37 | const float prewarp_str = 25.0; | |
| 38 | // The terrain cannot leave this vertical slab; everything outside is sky. | |
| 39 | ||
| 40 | // ── sampleNoise.h ─────────────────────────────────────────────────────────── | |
| 41 | vec4 NLQu(vec3 uvw, sampler3D t) { return textureLod(t, uvw, 0.0); } | |
| 42 | vec4 NLQs(vec3 uvw, sampler3D t) { return NLQu(uvw, t) * 2.0 - 1.0; } | |
| 43 | ||
| 44 | vec3 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 | } | |
| 49 | vec4 NMQu(vec3 uvw, sampler3D t) { return NLQu(nmq_warp(uvw), t); } | |
| 50 | vec4 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. | |
| 54 | float 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 | } | |
| 77 | float NHQs(vec3 uvw, sampler3D tex, float smooth_it) { return NHQu(uvw, tex, smooth_it) * 2.0 - 1.0; } | |
| 78 | ||
| 79 | float 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. | |
| 98 | const 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)); | |
| 101 | const 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 | ||
| 105 | vec3 rot6(vec3 p) { return OCTAVE_MAT6 * p; } | |
| 106 | vec3 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. | |
| 113 | float 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. | |
| 188 | float DENSITY(vec3 ws) { return DENSITY_impl(ws, false); } | |
| 189 | float DENSITY_CHEAP(vec3 ws) { return DENSITY_impl(ws, true); } | |
| 190 | ||
| 191 | ||
| 192 | // dc-terrain calls the field with loose floats. | |
| 193 | float 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 |
| … | … | @@ -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 | ||
| 26 | const float NOISE_LATTICE_SIZE = 16.0; | |
| 27 | const float INV_LATTICE_SIZE = 1.0 / 16.0; | |
| 28 | ||
| 29 | const float prewarp_str = 25.0; | |
| 30 | // The terrain cannot leave this vertical slab; everything outside is sky. | |
| 31 | ||
| 32 | // ── sampleNoise.h ─────────────────────────────────────────────────────────── | |
| 33 | vec4 NLQu(vec3 uvw, sampler3D t) { return textureLod(t, uvw, 0.0); } | |
| 34 | vec4 NLQs(vec3 uvw, sampler3D t) { return NLQu(uvw, t) * 2.0 - 1.0; } | |
| 35 | ||
| 36 | vec3 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 | } | |
| 41 | vec4 NMQu(vec3 uvw, sampler3D t) { return NLQu(nmq_warp(uvw), t); } | |
| 42 | vec4 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. | |
| 46 | float 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 | } | |
| 69 | float NHQs(vec3 uvw, sampler3D tex, float smooth_it) { return NHQu(uvw, tex, smooth_it) * 2.0 - 1.0; } | |
| 70 | ||
| 71 | float 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. | |
| 90 | const 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)); | |
| 93 | const 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 | ||
| 97 | vec3 rot6(vec3 p) { return OCTAVE_MAT6 * p; } | |
| 98 | vec3 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. | |
| 105 | float 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. | |
| 180 | float DENSITY(vec3 ws) { return DENSITY_impl(ws, false); } | |
| 181 | float DENSITY_CHEAP(vec3 ws) { return DENSITY_impl(ws, true); } | |
| 182 | ||
| 183 | ||
| 184 | // dc-terrain calls the field with loose floats. | |
| 185 | float evaluate_density(float x, float y, float z) { | |
| 186 | return DENSITY(vec3(x, y, z)); | |
| 187 | } |
| … | … | @@ -42,6 +42,9 @@ struct GpuFillRequest { |
| 42 | 42 | const float* custom_data = nullptr; // optional: baked custom data (binding 13) |
| 43 | 43 | int custom_data_n = 0; |
| 44 | 44 | 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; | |
| 45 | 48 | const std::vector<TerrainEdit>* edits = nullptr; // terrain edits for GPU evaluation |
| 46 | 49 | bool wants_gpu_mesh = false; // request GPU mesh generation (skip CPU DC) |
| 47 | 50 | DCMeshData* mesh_output = nullptr; // where to write GPU-generated mesh |
| … | … | @@ -104,7 +107,9 @@ public: |
| 104 | 107 | const float* cell_origin = nullptr, float voxel_size = 0.0f, |
| 105 | 108 | uint32_t mesh_quality_flags = 0, |
| 106 | 109 | 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) { | |
| 108 | 113 | if (!running_.load() || !gpu_available_.load()) { |
| 109 | 114 | return false; |
| 110 | 115 | } |
| … | … | @@ -121,6 +126,9 @@ public: |
| 121 | 126 | req.custom_data = custom_data; |
| 122 | 127 | req.custom_data_n = custom_data_n; |
| 123 | 128 | 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; | |
| 124 | 132 | req.edits = edits; |
| 125 | 133 | if (mesh_output && cell_origin) { |
| 126 | 134 | req.wants_gpu_mesh = true; |
| … | … | @@ -158,6 +166,8 @@ private: |
| 158 | 166 | DCGpuMeshPipeline mesh_pipeline_; |
| 159 | 167 | bool mesh_pipeline_available_ = false; |
| 160 | 168 | 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; | |
| 161 | 171 | const float* current_custom_data_ = nullptr; // same trick: re-upload only on change |
| 162 | 172 | uint32_t current_custom_data_version_ = 0; // ...and on content version, for growth |
| 163 | 173 | |
| … | … | @@ -264,6 +274,16 @@ private: |
| 264 | 274 | // Apply custom shader only if it changed since last batch. |
| 265 | 275 | // custom_glsl points to cached_glsl_.c_str() which is stable |
| 266 | 276 | // 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 | ||
| 267 | 287 | const char* glsl = batch[0].custom_glsl; |
| 268 | 288 | if (glsl != current_custom_glsl_) { |
| 269 | 289 | if (!compute_.set_custom_shader(glsl)) { |
| … | … | @@ -3199,9 +3199,12 @@ PackedFloat32Array DCTerrain::debug_gpu_density(const Vector3 &origin, float vox |
| 3199 | 3199 | const float* hb = generator_ptr_->gpu_custom_data(); |
| 3200 | 3200 | int hbn = generator_ptr_->gpu_custom_data_count(); |
| 3201 | 3201 | 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(); | |
| 3202 | 3205 | |
| 3203 | 3206 | 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)) { | |
| 3205 | 3208 | return out; // empty: the GPU declined, and that is the finding |
| 3206 | 3209 | } |
| 3207 | 3210 | out.resize((int64_t)grid.data.size()); |
| … | … | @@ -239,11 +239,18 @@ void DCWorkerCPU::_process_cell(const CellRequest &request, int64_t picked_up_us |
| 239 | 239 | const float* custom_data = nullptr; |
| 240 | 240 | int custom_data_n = 0; |
| 241 | 241 | uint32_t custom_data_version = 0; |
| 242 | const float* const* noise_volumes = nullptr; | |
| 243 | int noise_dim = 0; | |
| 244 | uint32_t noise_version = 0; | |
| 242 | 245 | if (request.generator && request.generator->has_gpu_shader()) { |
| 243 | 246 | custom_glsl = request.generator->gpu_shader_source(); |
| 244 | 247 | custom_data = request.generator->gpu_custom_data(); |
| 245 | 248 | custom_data_n = request.generator->gpu_custom_data_count(); |
| 246 | 249 | 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(); | |
| 247 | 254 | } |
| 248 | 255 | const std::vector<TerrainEdit>* gpu_edit_ptr = |
| 249 | 256 | (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 |
| 251 | 258 | gpu_filled = gpu_thread_->submit(grid, *request.density, custom_glsl, gpu_edit_ptr, |
| 252 | 259 | &gpu_mesh, request.cell_origin, request.voxel_size, |
| 253 | 260 | (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); | |
| 255 | 263 | if (!gpu_mesh.indices.empty()) { |
| 256 | 264 | gpu_meshed = true; |
| 257 | 265 | } |