Screen-space texture projection made easy
Screen-space texture projection is a way of displaying an image on a 3D object where the image doesn’t move or rotate with the object. Instead, it stays aligned with the camera, making it look like you’re looking through the object into another image or world.
You can use pretty much any image or texture, such as galaxies, clouds, water, or patterns.
Shader code
shader_type spatial;
render_mode unshaded, cull_back, depth_draw_opaque;
uniform int source_mode : hint_enum("Panorama Image", "Color Gradient", "Gradient Texture (1D)") = 0;
uniform sampler2D texture_image : source_color, filter_linear_mipmap, repeat_enable;
uniform sampler2D gradient_texture : source_color, filter_linear;
uniform vec4 gradient_top_color : source_color = vec4(0.005, 0.008, 0.025, 1.0);
uniform vec4 gradient_middle_color : source_color = vec4(0.025, 0.10, 0.38, 1.0);
uniform vec4 gradient_bottom_color : source_color = vec4(0.16, 0.025, 0.32, 1.0);
uniform float gradient_midpoint : hint_range(0.1, 0.9, 0.01) = 0.5;
uniform int gradient_direction : hint_enum("Top to Bottom", "Left to Right") = 0;
uniform vec4 color_tint : source_color = vec4(1.0, 1.0, 1.0, 1.0);
uniform float emission_strength : hint_range(0.0, 3.0, 0.05) = 0.35;
uniform float rotation_degrees : hint_range(-180.0, 180.0, 1.0) = 0.0;
uniform float texture_scale : hint_range(0.25, 4.0, 0.01) = 1.0;
uniform float horizontal_offset : hint_range(0.0, 1.0, 0.001) = 0.0;
uniform bool flip_vertical = false;
uniform bool gradient_stars_enabled = true;
const float STAR_GRID_X = 88.0;
const float STAR_GRID_Y = 44.0;
const float STAR_PROBABILITY = 1.0;
const float STAR_RADIUS = 0.0013;
const float STAR_DRIFT_SPEED = 0.002777777778;
const float STAR_BRIGHTNESS = 1.5;
uniform bool outline_enabled = true;
uniform vec4 outline_color : source_color = vec4(0.10, 0.72, 1.0, 1.0);
uniform float outline_width : hint_range(0.01, 0.5, 0.005) = 0.12;
uniform float outline_softness : hint_range(0.001, 0.25, 0.001) = 0.035;
uniform float outline_strength : hint_range(0.0, 5.0, 0.05) = 1.5;
float star_hash(vec2 p) {
vec3 p3 = fract(vec3(p.x, p.y, p.x) * vec3(0.1031, 0.1030, 0.0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
float sample_fixed_sky_stars(vec2 input_uv) {
vec2 sky_uv = vec2(fract(input_uv.x), clamp(input_uv.y, 0.0001, 0.9999));
vec2 grid_size = vec2(STAR_GRID_X, STAR_GRID_Y);
vec2 grid_position = sky_uv * grid_size;
vec2 base_cell = floor(grid_position);
float star_mask = 0.0;
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
vec2 cell = base_cell + vec2(float(x), float(y));
if (cell.y >= 0.0 && cell.y < grid_size.y) {
vec2 hash_cell = vec2(
mod(cell.x + grid_size.x, grid_size.x),
cell.y
);
vec2 seed_cell = hash_cell + vec2(42.17, 73.31);
float random_x = star_hash(seed_cell + vec2(1.7, 9.2));
float random_y = star_hash(seed_cell + vec2(4.1, 2.8));
vec2 star_center = cell + vec2(random_x, random_y);
float star_latitude = (0.5 - star_center.y / grid_size.y) * PI;
float latitude_factor = max(cos(star_latitude), 0.0);
float present_random = star_hash(seed_cell + vec2(13.1, 7.7));
float present = step(
present_random,
STAR_PROBABILITY * latitude_factor
);
vec2 delta_uv = (grid_position - star_center) / grid_size;
float longitude_scale = max(cos(star_latitude), 0.10);
vec2 angular_delta = vec2(
delta_uv.x * TAU * longitude_scale,
delta_uv.y * PI
);
float angular_distance = length(angular_delta);
float radius = STAR_RADIUS * longitude_scale;
float edge_softness = max(radius * 0.24, 0.00008);
float dot_mask = 1.0 - smoothstep(
max(radius - edge_softness, 0.0),
radius + edge_softness,
angular_distance
);
star_mask = max(star_mask, dot_mask * present);
}
}
}
return star_mask;
}
vec3 sample_color_gradient(float t) {
t = clamp(t, 0.0, 1.0);
float midpoint = clamp(gradient_midpoint, 0.001, 0.999);
if (t < midpoint) {
return mix(
gradient_top_color.rgb,
gradient_middle_color.rgb,
t / midpoint
);
}
return mix(
gradient_middle_color.rgb,
gradient_bottom_color.rgb,
(t - midpoint) / (1.0 - midpoint)
);
}
void fragment() {
// Camera-space projection.
vec3 view_ray = normalize(VERTEX);
view_ray.xy *= texture_scale;
view_ray = normalize(view_ray);
// Convert to world direction.
vec3 ray_dir = normalize(mat3(INV_VIEW_MATRIX) * view_ray);
float angle = radians(rotation_degrees);
float ca = cos(angle);
float sa = sin(angle);
ray_dir.xz = vec2(
ca * ray_dir.x - sa * ray_dir.z,
sa * ray_dir.x + ca * ray_dir.z
);
float longitude = atan(ray_dir.z, ray_dir.x);
float latitude = asin(clamp(ray_dir.y, -1.0, 1.0));
// Equirectangular UV mapping.
vec2 uv = vec2(
longitude / (2.0 * PI) + 0.5,
0.5 - latitude / PI
);
uv.x = fract(uv.x + horizontal_offset);
if (flip_vertical) {
uv.y = 1.0 - uv.y;
}
uv.y = clamp(uv.y, 0.001, 0.999);
// Select texture source.
vec3 environment_color;
if (source_mode == 0) {
environment_color = texture(texture_image, uv).rgb;
} else {
float gradient_t = uv.y;
if (gradient_direction == 1) {
gradient_t = fract(uv.x);
}
if (source_mode == 1) {
environment_color = sample_color_gradient(gradient_t);
} else {
environment_color = texture(
gradient_texture,
vec2(gradient_t, 0.5)
).rgb;
}
}
environment_color *= color_tint.rgb;
// Optional procedural stars.
float star_mask = 0.0;
if (source_mode != 0 && gradient_stars_enabled) {
vec2 star_uv = vec2(
fract(uv.x - TIME * STAR_DRIFT_SPEED),
uv.y
);
star_mask = sample_fixed_sky_stars(star_uv);
environment_color = mix(
environment_color,
vec3(1.0),
star_mask
);
}
// Fresnel outline.
float facing = clamp(
abs(dot(normalize(NORMAL), normalize(VIEW))),
0.0,
1.0
);
float outline_mask = 0.0;
if (outline_enabled) {
outline_mask = 1.0 - smoothstep(
outline_width,
outline_width + outline_softness,
facing
);
}
ALBEDO = mix(environment_color, outline_color.rgb, outline_mask);
EMISSION = environment_color * emission_strength;
EMISSION += vec3(1.0) * star_mask * STAR_BRIGHTNESS;
EMISSION += outline_color.rgb * outline_mask * outline_strength;
ROUGHNESS = 1.0;
METALLIC = 0.0;
}
