Help with procedural planets

Godot Version

Godot 4.6.3

Question

Good evening all,

I’m building procedural planets in Godot, all in-shader — no heightmaps. The terrain is 3D noise sampled in object space (so no UV seams at the poles), and I get the lit “3D” look by deriving a surface normal from the gradient of the height field via finite differences, then letting the directional sun shade it.

Honestly it’s not bad overall, but I’ve got some artifacts I can’t shake: diagonal streaking, faint grid/contour-ish lines, and thin slivers in the relief — mostly when you zoom in (screenshots attached).

Here’s the relevant code:

// --- noise ---
float hash13(vec3 p) {
    p = fract(p * 0.1031);
    p += dot(p, p.zyx + 31.32);
    return fract((p.x + p.y) * p.z);
}

float vnoise(vec3 x) {                       // value noise, cubic fade
    vec3 i = floor(x);
    vec3 f = fract(x);
    f = f * f * (3.0 - 2.0 * f);
    float c000 = hash13(i + vec3(0,0,0));
    float c100 = hash13(i + vec3(1,0,0));
    float c010 = hash13(i + vec3(0,1,0));
    float c110 = hash13(i + vec3(1,1,0));
    float c001 = hash13(i + vec3(0,0,1));
    float c101 = hash13(i + vec3(1,0,1));
    float c011 = hash13(i + vec3(0,1,1));
    float c111 = hash13(i + vec3(1,1,1));
    return mix(mix(mix(c000,c100,f.x), mix(c010,c110,f.x), f.y),
               mix(mix(c001,c101,f.x), mix(c011,c111,f.x), f.y), f.z);
}

float fbm(vec3 p) {                          // 5 octaves
    float sum = 0.0, amp = 0.5;
    for (int i = 0; i < 5; i++) { sum += amp * vnoise(p); p *= 2.02; amp *= 0.5; }
    return sum;
}

float ridged_fbm(vec3 p) {                   // mountains
    float sum = 0.0, amp = 0.5, norm = 0.0;
    for (int i = 0; i < 5; i++) {
        float n = vnoise(p) * 2.0 - 1.0;
        n = 1.0 - abs(n);                    // fold -> ridge lines
        n = n * n;
        sum += amp * n; norm += amp; p *= 2.02; amp *= 0.5;
    }
    return sum / norm;
}

// --- height field: warped continents + ridged mountains gated to land ---
float terrain_height(vec3 dir) {
    vec3 base = dir * continent_scale + vec3(seed * 3.7);
    float warp = fbm(base * 1.7) - 0.5;
    vec3 wb = base + warp_amount * vec3(warp);          // domain warp
    float cont = fbm(wb);
    float ranges = smoothstep(mountain_coverage, mountain_coverage + 0.18,
                              fbm(base * 0.6 + vec3(31.0)));
    float mtn = ridged_fbm(wb * 2.4 + vec3(17.0));
    float land_gate = smoothstep(sea_level - 0.05, sea_level + 0.05, cont);
    return cont + mtn * ranges * mountain_height * land_gate;
}

// --- relief normal from the height-field gradient (finite difference) ---
// (this is where the artifacts show up)
vec3 nrm = normalize(v_local);
float h = terrain_height(nrm);
vec3 up_ref = abs(nrm.y) < 0.99 ? vec3(0,1,0) : vec3(1,0,0);
vec3 t1 = normalize(cross(nrm, up_ref));
vec3 t2 = cross(nrm, t1);
float eps = relief_eps;
float hu = terrain_height(normalize(nrm + t1 * eps));
float hv = terrain_height(normalize(nrm + t2 * eps));
vec3 grad = (hu - h) * t1 + (hv - h) * t2;             // forward difference
vec3 tilt = relief_strength * grad;
float tl = length(tilt);
if (tl > relief_max_tilt) tilt *= relief_max_tilt / tl; // clamp so it can't flip past the horizon
vec3 relief_n = normalize(nrm - tilt);                 // -> NORMAL, shaded by the sun

Has anyone hit this before? I’m trying to work out whether the streaking and the faint grid are coming from the value noise, the finite-difference normal, or something in how I’m warping the domain. Any pointers appreciated — thanks!

screen shots:

Some of the work that has come out better if your curious

still need to make the storm lines wider and less fine but I like it.

UPDATE — Just in case anyone is curious I found the problem and it as actually two separate problems. First: my noise coordinates were getting too big for a float. I was offsetting the noise domain by seed * 3.7, and since fBM multiplies the whole coordinate every octave, high seeds had the top octaves sampling at coords of 50,000+ — where a float barely has any precision left for the position inside a noise cell. Terrain dissolved into blocky displaced tiles, getting worse with bigger seeds. Fix: hash the seed into a small bounded offset (0–8) instead of using it raw, so coordinates stay tiny at any seed. That alone was ~95% of it.

Second, the sneaky one: the classic float “hash without sine” takes fract() of values around 10,000, where a one-ulp difference flips the result completely — and two compilations of the same shader (FMA contraction, or Godot’s ubershader vs the final specialized pipeline) can differ by exactly that ulp. Result: ~10% of noise cells hashed differently between compiles, displacing whole squares of terrain and even making the planet flicker between two versions of itself while pipelines warmed up. Fix: switch to an integer hash (PCG3D) — integer math is bit-exact on every GPU/driver/compile, and it’s faster than the float hash too. If you’re seeing displaced chunks or seams that move between runs, this might me it. still work to be done but it came out pretty nice I think,