How to make a Triplanar Shader in Godot

How to make a Triplanar Shader in Godot

Here's how to make a simple triplanar shader that blends textures based on the surface angle (normal). You can find the full code at the end of this article.

Triplanar mapping solves a common terrain problem: standard UV mapping stretches or distorts textures on steep or irregular surfaces. By projecting textures along all 3 world axes and blending them by surface normal, you get clean results on any geometry. The technique samples the same texture from X, Y, and Z directions simultaneously, then mixes the 3 samples using weights derived from the surface normal. Each world axis contributes roughly in proportion to how much the face points in that direction, so flat ground gets mostly the top projection while cliffs get the side ones.

0:00
/0:16

Initialization

To make texture mapping UV-independent we needed to find some sort of alternative coordinates space. We've decided to use world-space vertex coordinates, initialized like the following:

  1. Rotate the vertex position by the triplanar_angle parameter.
  2. Scale the result by triplanar_scale to control texture tiling.
  3. Offset by triplanar_offset to shift the projection in world space.
uniform vec3 _triplanar_pos;

void vertex()
{
  // initialize the world space sampling coordinate by
  // rotating, scaling and offsetting the vertex position.
  _triplanar_pos = rotate(triplanar_angle) * VERTEX;
  _triplanar_pos *= triplanar_scale;
  _triplanar_pos += triplanar_offset;

  //...
}

Snippet pseudo code

0:00
/0:02

Considering that triplanar mapping projects textures along all 3 axes, to avoid over-saturation we also calculate some blending weights as shown. The weights are based on the absolute value of the surface normal raised to the power of triplanar_sharpness. A sharpness value of 1.0 gives a smooth blend; higher values produce a sharper transition between projections. The weights are then normalized so they always sum to 1.0, keeping the final color in the correct range:

uniform vec3 _triplanar_weight;

void vertex()
{
  //...
  // compute the weights that will be used to get the correct
  // final color after sampling the texture
  vec3 _triplanar_weight = pow(abs(NORMAL), vec3(triplanar_sharpness));
_triplanar_weight /= dot(result, vec3(1.0));
}

Snippet pseudo code

Texture sampling

Texture mapping uses only two coordinates (u and v) but the _triplanar_pos is a vec3! However, if we use only two components per time, we are actually projecting the texture on a plane aligned with each axes (up, right and forward).

  1. Sample the texture using XY components (projects along the Z axis).
  2. Sample using XZ components (projects along the Y axis, the "top" view).
  3. Sample using ZY components with a horizontal flip (projects along the X axis).
  4. Multiply each sample by its corresponding weight and sum them together.
void fragment()
{
  vec4 color = vec4(0.0);

  // sample the texture three times, one for each axes in world space,
  // and multiply each axes with the relative weight
  color += texture(sampler, _triplanar_pos.xy) * _triplanar_weight.z;
  color += texture(sampler, _triplanar_pos.xz) * _triplanar_weight.y;
  color += texture(sampler, _triplanar_pos.zy * vec2(-1.0, 1.0))  * _triplanar_weight.x;
  color *= triplanar_strength;

  //...
}

Mixing

Finally, we can transition between two different textures based on the surface normal. The dot product between the up vector and the surface normal gives a value between -1 and 1, and smoothstep turns that into a clean interpolation factor controlled by _Angle and _Transition. The result is that flat ground uses one texture (e.g., grass) and steep surfaces use another (e.g., rock), with a configurable blend zone between them:

void fragment()
{
  //...

  // calulate the angle between up-vector and surface normal
  float transition = dot(vec3(0.0, 1.0, 0.0), normal);

  // calculate interpolation factor
  float threshold = cos(degToRad(_Angle));
  float interpolation = 1.0 - smoothstep(
                                threshold - _Transition,
                                threshold + _Transition,
                                transition);

  // transition between two textures
  vec3 color = mix(first_triplanar_texture.rgb,
  		second_triplanar_texture.rgb,
  		interpolation);

  ALBEDO = color;
}
0:00
/0:05

Putting it all together

The complete shader below combines all 3 steps: the vertex function computes world-space position and blending weights, and the fragment function samples the texture 3 times (once per axis) then blends the two terrain textures based on surface angle. The shader exposes triplanar_sharpness, triplanar_scale, and _Angle/_Transition as uniform parameters, so you can tune the look directly in the Godot inspector without touching the GLSL.

The rest of this post is available for free!

Please create an account to continue reading! It helps us knowing that humans are reading us, and also helps us staying independent and keep posting what we love. Thanks!

Create a Free Account & Continue Reading Already have an account? Log in

Want game-ready tools or need specific solutions?

Work with us