I ended abandoning the triplanar mapping (although I am glad I learned about it) and going back to this awesome tutorial and extended it by calculating the partial derivatives to calculate the tangent/normal vectors. I am quite happy with the result.
For anyone that is interested, here's the shader
shader_type spatial;
// EXTERNAL PARAMS =============================================================
uniform int seed = 0;
// Colors for each type of terrain
uniform vec4 land_color: hint_color = vec4(0.17, 0.29, 0.16, 1.0);
uniform vec4 beach_color: hint_color = vec4(0.89, 0.80, 0.61, 1.0);
uniform vec4 ocean_color: hint_color = vec4(0.08, 0.25, 0.41, 1.0);
uniform vec4 mountain_color: hint_color = vec4(0.75, 0.75, 0.75, 1.0);
// Controls intensity of the normal mapping
uniform float bump_strength: hint_range(0.01, 1.0) = 0.5;
// Parameters for the noise model
// See https://docs.godotengine.org/en/stable/classes/class_opensimplexnoise.html
// for an explanation of the terms. This is my own implementation of it.
uniform float scale: hint_range(0.0, 2.0) = 1.06;
uniform float period: hint_range(0.01, 0.5) = 0.339;
uniform int octaves: hint_range(1, 10) = 5;
uniform float lacunarity: hint_range(0.1, 10.0) = 2.447;
uniform float persistence: hint_range(0.0, 1.0) = 0.493;
uniform float time = 0;
// INTERNAL PARAMS =============================================================
const float pi = 3.14159265;
// Magnitude of offset used for calculating tangent vectors
const float delta = 0.0001;
// Thresholds controlling transitions between each terrain color
const float th_beach_min = 0.5;
const float th_beach_max = 0.55;
const float th_land_min = 0.53;
const float th_land_max = 0.55;
const float th_mountain_min = 0.65;
const float th_mountain_max = 0.70;
// Author: Inigo Quilez
// https://www.shadertoy.com/view/Xsl3Dl
vec3 hash(vec3 p) {
p = vec3(dot(p, vec3(127.1, 311.7, 74.7)),
dot(p, vec3(269.5, 183.3, 246.1)),
dot(p, vec3(113.5, 271.9, 124.6)));
return -1.0 + 2.0 * fract(sin(p + float(seed)) * 43758.5453123);
}
// Author: Inigo Quilez
// https://www.shadertoy.com/view/Xsl3Dl
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
vec3 u = f * f * (3.0 - 2.0 * f);
float n = mix(mix(mix(dot(hash(i + vec3(0.0, 0.0, 0.0)), f - vec3(0.0, 0.0, 0.0)),
dot(hash(i + vec3(1.0, 0.0, 0.0)), f - vec3(1.0, 0.0, 0.0)), u.x),
mix(dot(hash(i + vec3(0.0, 1.0, 0.0)), f - vec3(0.0, 1.0, 0.0)),
dot(hash(i + vec3(1.0, 1.0, 0.0)), f - vec3(1.0, 1.0, 0.0)), u.x), u.y),
mix(mix(dot(hash(i + vec3(0.0, 0.0, 1.0)), f - vec3(0.0, 0.0, 1.0)),
dot(hash(i + vec3(1.0, 0.0, 1.0)), f - vec3(1.0, 0.0, 1.0)), u.x),
mix(dot(hash(i + vec3(0.0, 1.0, 1.0)), f - vec3(0.0, 1.0, 1.0)),
dot(hash(i + vec3(1.0, 1.0, 1.0)), f - vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z );
// Normalize to [0.0, 1.0]
return 0.5*(n + 1.0);
}
// Converts (phi,theta) to (x,y,z) on a sphere
vec3 sph2cart(float phi, float theta) {
vec3 unit = vec3(0.0, 0.0, 0.0);
unit.x = sin(phi) * sin(theta);
unit.y = cos(theta) * -1.0;
unit.z = cos(phi) * sin(theta);
return normalize(unit);
}
void fragment() {
float theta = UV.y * pi;
float phi = UV.x * 3.14159 * 2.0;
// Get unit vectors which will be used to sample the 3D noise
// Perturb the unit vector in the theta/phi directions. Will be used
// to calculate the normals later
vec3 e = sph2cart(phi, theta);
vec3 e_dx = sph2cart(phi + delta, theta);
vec3 e_dy = sph2cart(phi, theta + delta);
// Accumulation variables for the noise samples. The noise is accumulated
// by adding the noise components for each octaves.
float n = 0.0;
float n_dx = 0.0;
float n_dy = 0.0;
float a = 1.0; // Amplitude for current octave
float max_amp = a; // Accumulate max amplitude so we can normalize after
float p = period; // Period for current octave
for(int i = 0; i < octaves; i++) {
// Sample the 3D noise
n += a*noise(e/p + time);
n_dx += a*noise(e_dx/p + time);
n_dy += a*noise(e_dy/p + time);
// Amplitude decay for higher octaves
a *= persistence;
max_amp += a;
// Divide period by lacunarity
p = p / lacunarity;
}
// Scale noise to range [0.0, 1.0]
n /= float(max_amp) / scale;
n_dx /= float(max_amp) / scale;
n_dy /= float(max_amp) / scale;
// Perturb the unit vectors we calculated earlier by the noise
vec3 r = e*(1.0 + bump_strength*n);
vec3 r_dx = e_dx*(1.0 + bump_strength*n_dx);
vec3 r_dy = e_dy*(1.0 + bump_strength*n_dy);
// Build the tangent vectors and calculate normal
vec3 tangent_dx = normalize(r_dx - r);
vec3 tangent_dy = normalize(r_dy - r);
// Threshold values for transitioning from each terrain type
float th_beach = smoothstep(th_beach_min, th_beach_max, n);
float th_land = smoothstep(th_land_min, th_land_max, n);
float th_mountain = smoothstep(th_mountain_min, th_mountain_max, n);
// Mix between the flat surface normal (unit) and the normal associated with
// the terrain. This will cause the ocean areas to be shaded as a flat sphere.
vec3 normal_model = mix(e, cross(tangent_dx, tangent_dy), th_beach);
// Mix between the ocean color and the land color
vec3 color = mix(ocean_color.rgb, beach_color.rgb, th_beach);
color = mix(color.rgb, land_color.rgb, th_land);
color = mix(color.rgb, mountain_color.rgb, th_mountain);
// OUTPUTS
ALBEDO = color.rgb;
// Transform normals from model to view space
NORMAL = (INV_CAMERA_MATRIX*(WORLD_MATRIX*vec4(normal_model, 0.0))).xyz;
}