Interior mapping shader

This effect uses a single texture (in this case a cubemap) to give the illusion of a 3D room – this is very useful (for example) for showing something with actual depth behind a window (or multiple windows) without any real geometry.

Based on this wonderful talk by Pedro Estebanez. 

It features:

  • Control over apparent depth of the room
  • Normalmap support
  • Emission map support and emission strength parameter

Unlike the shader described in the video above, it is designed to work with plane meshes, since I’ve made it to add individual distant windows in my scene. When the depth parameter is set to 0, the shader simply draws the albedo/normal/emission for the back wall of the room.

The shader requires all mapped textures to be cubemaps with the layout described in the first image below. I also included my testing cubemaps.

Note that the -Z square in the cubemap would be the side facing the camera which is obviously invisible. It is therefore unused and doesn’t need a texture.

 
Shader code
shader_type spatial;
//render_mode unshaded;

const float ROOM_SIZE = 2.;

uniform sampler2D cubemap_albedo : hint_albedo;
uniform sampler2D cubemap_normalmap : hint_normal;
uniform sampler2D cubemap_emission : hint_black_albedo;

uniform float room_depth : hint_range(0., 4.0, 0.01) = 1.;
uniform float emission_strength : hint_range(0., 10., 0.01) = 0.;

varying vec3 obj_vertex;
varying flat vec3 obj_cam;

float remap_range(float value, float min_in, float max_in, float min_out, float max_out) {
    return(value - min_in) / (max_in - min_in) * (max_out - min_out) + min_out;
}

float bool_to_sign(bool b) {
	return (b) ? 1. : -1.;
}

vec3 sample_cubemap(sampler2D cubemap, vec2 uv, vec3 face) {
	face = normalize(face);
	
	if (face.x == 1.) {
		//+X
		uv.x = remap_range(uv.x, 0., 1., 0., 1./3.);
		uv.y = remap_range(uv.y, 0., 1., 0., 1./2.);
	}
	else if (face.x == -1.) {
		//-X
		uv.x = remap_range(uv.x, 0., 1., 0., 1./3.);
		uv.y = remap_range(uv.y, 0., 1., 1./2., 1.);
	}
	else if (face.y == 1.) {
		//+Y
		uv.x = remap_range(uv.x, 0., 1., 1./3., 2./3.);
		uv.y = remap_range(uv.y, 0., 1., 0., 1./2.);
	}
	else if (face.y == -1.) {
		//-Y
		uv.x = remap_range(uv.x, 0., 1., 1./3., 2./3.);
		uv.y = remap_range(uv.y, 0., 1., 1./2., 1.);
	}
	else if (face.z == 1.) {
		//+Z
		uv.x = remap_range(uv.x, 0., 1., 2./3., 1.);
		uv.y = remap_range(uv.y, 0., 1., 0., 1./2.);
	}
	else if (face.z == -1.) {
		//-Z
		uv.x = remap_range(uv.x, 0., 1., 2./3., 1.);
		uv.y = remap_range(uv.y, 0., 1., 1./2., 1.);
	}
	
	return texture(cubemap, uv).rgb;
}

void vertex() {
	if (room_depth != 0.) {
		vec2 d = vec2(ROOM_SIZE, ROOM_SIZE)/2.;
		vec3 delta = vec3(d.x, 0., d.y);
		
		obj_vertex = VERTEX - delta;
		
		//camera pos in obj space
		obj_cam = inverse(MODELVIEW_MATRIX)[3].xyz - delta;
	}
}

void fragment() {
	vec3 cm_face = vec3(0., 0., 1.);
	vec2 cm_uv = UV;
		
	if (room_depth != 0.) {
		float depth = room_depth * 2.;
		vec3 cam2pix = obj_vertex - obj_cam;
		
		//camp2pix.y <= 0 --> show floor
		//camp2pix.y > 0 --> show ceiling
		float is_floor = step(cam2pix.y, 0.);
		float ceil_y   = ceil(obj_vertex.y / depth - is_floor) * depth;
		float ceil_t   = (ceil_y - obj_cam.y) / cam2pix.y;
		
		//camp2pix.z <= 0 --> show north
		//camp2pix.z > 0 --> show south
		float is_north = step(cam2pix.z, 0.);
		float wall_f_z = ceil(obj_vertex.z / ROOM_SIZE - is_north) * ROOM_SIZE;
		float wall_f_t = (wall_f_z - obj_cam.z) / cam2pix.z;
		
		//camp2pix.x <= 0 --> show east
		//camp2pix.x > 0 --> show west
		float is_east  = step(cam2pix.x, 0.);
		float wall_e_z = ceil(obj_vertex.x / ROOM_SIZE - is_east) * ROOM_SIZE;
		float wall_e_t = (wall_e_z - obj_cam.x) / cam2pix.x;
		
		vec2 tex_coord;
		float min_t = min(min(ceil_t, wall_e_t), wall_f_t);
		
		if (wall_e_t == min_t) {
			//East / West
			tex_coord = obj_cam.zy + wall_e_t * cam2pix.zy;
			cm_face = vec3((is_east == 0.) ? 1. : -1., 0., 0.);
		}
		else if (wall_f_t == min_t) {
			//Front / Back
			tex_coord = obj_cam.xy + wall_f_t * cam2pix.xy;
			cm_face = vec3(0., (is_north == 0.) ? -1. : 1., 0.);
		}
		else if (ceil_t == min_t) {
			//Ceiling / Floor
			tex_coord = obj_cam.xz + ceil_t * cam2pix.xz;
			cm_face = vec3(0., 0., (is_floor == 0.) ? -1. : 1.);
		}
		
		if (!(ceil_t == min_t)) {
			tex_coord.y /= room_depth;
		}
		
		cm_uv = (tex_coord*.5 + 1.);
		
		cm_uv.x = clamp(cm_uv.x, 0., 1.);
		cm_uv.y = clamp(cm_uv.y, 0., 1.);
	}	
		
	ALBEDO = sample_cubemap(cubemap_albedo, cm_uv, cm_face);
	NORMAL = sample_cubemap(cubemap_normalmap, cm_uv, cm_face);
	EMISSION = sample_cubemap(cubemap_emission, cm_uv, cm_face) * emission_strength;
}
Tags
interior mapping. illusion, spatial. GLES3
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from AndreaTerenz

Simple spatial CRT effect

Related shaders

Fake Interior Shader

Uniplanar mapping with dithered blending

Contact Refinement Parallax Mapping

Subscribe
Notify of
guest

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
ViLa4480
1 year ago

Hi, just a quick Godot v4.2.1 update – I think the first cubemap lines are now as such:

uniform sampler2D cubemap_albedo : hint_default_white;
uniform sampler2D cubemap_normalmap : hint_normal;
uniform sampler2D cubemap_emission : hint_default_black;

I’m not sure about the albedo one, so I used hint_default_white, please clarify if there’s a better solution.

Last edited 1 year ago by ViLa4480
James
James
7 months ago
Reply to  ViLa4480

Hi, I think the cube map projection is fully broken in Godot 4.2