Triplanar Mapping

I wanted to use Godot’s GridMap with small tiles (which together make larger tiles) but ran into the issue of textures not lining up when rotating. This lead me to discover how Triplanar Mapping works, which is a technique to map textures to each plane. This allows me to use world coordinates as the “UV”, sort of.

Along the way I wanted normal mapping to work properly, which required a bit of math to ensure the tangent and binormal were facing in a consistent direction to allow normal maps to work properly.

And to ensure this works with simple textures, I made sure to make normal maps and roughness optional, providing a boolean to disable those features. (and a roughness value you can set manually, only used when roughness is disabled.)

To use this material, slap it on a mesh of your chosing, and fill in the plane textures. At minimum you’ll need the albedo defined for each, and the roughness and normal mapping can be turned off.

While this was designed for gridmaps, there’s no real reason this can’t be used for any arbitrary mesh. Just note that if that mesh moves around, the textures will slide as it’s using world coordinates.

You could probably extend this shader to allow for custom shading on positive and negative planes, allowing you to shade only the top as grass or something. 

This might be handy for voxel worlds. No idea quite yet.

Shader code
shader_type spatial;
render_mode world_vertex_coords;

uniform bool enable_normal_map = false;
uniform bool enable_roughness = false;
uniform float roughness = 1;

group_uniforms x_plane;
uniform sampler2D x_albedo: source_color;
uniform sampler2D x_normal_map: hint_normal;
uniform sampler2D x_roughness: hint_roughness_r;
uniform float x_scale = 1.0;

group_uniforms y_plane;
uniform sampler2D y_albedo: source_color;
uniform sampler2D y_normal_map: hint_normal;
uniform sampler2D y_roughness: hint_roughness_r;
uniform float y_scale = 1.0;

group_uniforms z_plane;
uniform sampler2D z_albedo: source_color;
uniform sampler2D z_normal_map: hint_normal;
uniform sampler2D z_roughness: hint_roughness_r;
uniform float z_scale = 1.0;

vec3 triplanar_map(vec3 x, vec3 y, vec3 z, vec3 n) {
	n = n*n;
    return (x*n.x + y*n.y + z*n.z)/(n.x+n.y+n.z);
}

vec3 triplanar_tritexture(sampler2D x_texture, sampler2D y_texture, sampler2D z_texture, vec3 d, vec3 n) {
    vec3 colx = texture(x_texture, d.zy * x_scale).xyz;
    vec3 coly = texture(y_texture, d.xz * y_scale).xyz;
    vec3 colz = texture(z_texture, d.xy * z_scale).xyz;
    return triplanar_map(colx, coly, colz, n);
}

void fragment() {
	vec3 world_vertex = (INV_VIEW_MATRIX * vec4(VERTEX, 1.)).xyz;
	vec3 world_normal = (INV_VIEW_MATRIX * vec4(NORMAL, 0.)).xyz;
	if (enable_normal_map) {
		BINORMAL = triplanar_map(
			normalize((VIEW_MATRIX * vec4(0.,-1.,0.,0)).xyz),
			normalize((VIEW_MATRIX * vec4(0.,0.,1.,0)).xyz),
			normalize((VIEW_MATRIX * vec4(0,-1.,0.,0)).xyz),
			world_normal
		);

		TANGENT = triplanar_map(
			normalize((VIEW_MATRIX * vec4(0.,1.,0.,-1.)).xyz),
			normalize((VIEW_MATRIX * vec4(1.,0.,0.,1.)).xyz),
			normalize((VIEW_MATRIX * vec4(0,0.,1.,-1.)).xyz),
			world_normal
		);
		NORMAL_MAP = triplanar_tritexture(x_normal_map, y_normal_map, z_normal_map, world_vertex, world_normal);
	}

	ALBEDO = triplanar_tritexture(x_albedo, y_albedo, z_albedo, world_vertex, world_normal);
	if (enable_roughness) {
		ROUGHNESS = triplanar_tritexture(x_roughness, y_roughness, z_roughness, world_vertex, world_normal).r;
	} else {
		ROUGHNESS = roughness;
	}
}
Live Preview
Tags
mapping, triplanar, Voxel
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from Caaz

Related shaders

guest

6 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Belzecue
Belzecue
8 months ago

Can’t you just add:

render_mode world_vertex_coords;

then remove all the INV_VIEW_MATRIX/VIEW_MATRIX transforms out of fragment?

e.g. https://gist.github.com/belzecue/e939ba8f7b451bd4caba7276caebb8a6

Belzecue
Belzecue
8 months ago
Reply to  Caaz

For Godot’s internal triplanar shader, all the heavy lifting is being done in the vertex function. I’m mucking about with this code right now to try out Ben Golus’s various seam blending algorithms to hide the seams/mirroring.

https://gist.github.com/belzecue/55aab305ead47ebe8499cb236a8fe275

Belzecue
Belzecue
8 months ago

Also, need to add the missing sampler2d hints in order to get correct colors:

group_uniforms x_plane;
uniform sampler2D x_albedo : source_color;
uniform sampler2D x_normal_map : hint_normal;
uniform sampler2D x_roughness : hint_roughness_r;

group_uniforms y_plane;
uniform sampler2D y_albedo : source_color;
uniform sampler2D y_normal_map : hint_normal;
uniform sampler2D y_roughness : hint_roughness_r;

group_uniforms z_plane;
uniform sampler2D z_albedo : source_color;
uniform sampler2D z_normal_map : hint_normal;
uniform sampler2D z_roughness : hint_roughness_r;