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;
}
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.
Hi, I think the cube map projection is fully broken in Godot 4.2