Normal Maps for Triplanar Mapped Objects

dgreenheckdgreenheck Posts: 12Member

I'm seeing some lighting issues while using the TextureUniformTriplanar node in the Visual Shader. I have a NoiseTexture which I am using as both to calculate the planet colors and also to calculate the normal maps. If you look at the image below, you can see a light off in the distance (I've turned up the energy to make it more visible). The "shadowed" areas are not being lit correctly. If I go to the other side of the sphere, the lighting is correct.

Am I missing something here or is this a limitation of triplanar mapping?

Comments

  • CalinouCalinou Posts: 451Admin Godot Developer
    edited July 18

    Maybe the normal map direction is inverted. Godot uses normal maps with Y- direction (see this table for a comparison with other engines).

    You'll need to use an external tool to swap the Y coordinate in the normal map. (There might be an import option for doing this in 4.0.)

  • dgreenheckdgreenheck Posts: 12Member

    The normal map is generated by a built-in Godot plug-in so I would assume it's correct? The normal mapping is also correct on one side of the cube but it appears that the normals aren't adjusted properly for triplanar mapping.

    This article seems to address the problem: https://medium.com/@bgolus/normal-mapping-for-a-triplanar-shader-10bf39dca05a.

  • MegalomaniakMegalomaniak Posts: 2,748Admin
    edited July 18

    normal maps are dependent on coordinates iirc, for this particular case you might have a better result using either a derivative map or a bumpmap that you convert into normal data in the shader.

    http://www.rorydriscoll.com/2012/01/11/derivative-maps/

  • dgreenheckdgreenheck Posts: 12Member

    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;
    }
    

Leave a Comment

BoldItalicStrikethroughOrdered listUnordered list
Emoji
Image
Align leftAlign centerAlign rightToggle HTML viewToggle full pageToggle lights
Drop image/file