3D Realistic Picture-in-picture scope

Contains an eyebox, scope shadow, reticle, and fake parallax

Shader code
shader_type spatial;
render_mode unshaded, blend_mix, cull_back, depth_draw_opaque, shadows_disabled;


const vec3 BLACK = vec3(0.0);

/** Viewport texture from a 2D Scene that will become the reticle */
uniform sampler2D reticle_texture: repeat_disable;
/** Viewport texture from a 3D Camera that will become the scope's view */
uniform sampler2D scope_texture: repeat_disable;
/** Radius for the scope's physical outer radius */
uniform float scope_radius = 0.025;
/** Depth before the reticle appears */
uniform float reticle_depth = 2.0;
uniform float parallax_factor: hint_range(0.0, .3, 0.001) = 0.1;
uniform float view_edge_fade_factor: hint_range(0.0, 0.5, 0.001) = .05;


group_uniforms Eyebox;
/**
Position of eyebox. When the eye is this distance away it is considered perfect */
uniform float eyebox_position = .15;
/**
Forgiveness for user eye positioning. Allows a +- tolerance around "eyebox_position" */
uniform float eyebox_tolerance = 0.005;
/** Specify the distance over which the scope shadow will fade to black */
uniform float eyebox_fade_distance = 0.05;


group_uniforms Scope_Shadow;
/**
Percent of the scope that will display before the scope shadow's fade starts.
0.0 is fully dark, 1.0 is fully displayed (except for the scope shadow's fade).*/
uniform float shadow_inner_radius: hint_range(0.0, 1.0, 0.01) = 1.0;
/**
Factor for the eyebox's scope shadow */
uniform float shadow_fade_factor = 0.2;

/**
Changes how sensitive the scope-shadow is to movement */
uniform float shadow_movement_factor: hint_range(0.5, 1.0, 0.01) = 1.0;


varying vec3 view;


// "Pushes" the scope uv into the scope making it appear deeper inside the scope.
// Returns altered UV
vec2 get_depth_effect(vec2 uv, vec3 direcion, float depth) {
	vec2 result = uv - direcion.xy * (depth * 2.0);
	result -= vec2(0.5);
	result *= 0.5;
	return result + 0.5;
}


// Creates the scope shadow effect
// Returns color
vec3 get_scope_shadow_effect(vec2 uv, vec4 scope_sample, vec3 view_dir, float eye_distance) {
	float delta = eye_distance - eyebox_position;

	vec2 eye_offset = view_dir.xy / -shadow_movement_factor;
	//((uv - vec2(0.5)) / 2.0)
	vec2 shifted_uv = uv - eye_offset;
	float dist = length((shifted_uv - vec2(0.5)) * 2.0);

	// Fade logic for close / far
	float fade_near = smoothstep(-eyebox_tolerance, -eyebox_tolerance - eyebox_fade_distance, delta);
	float fade_far  = smoothstep( eyebox_tolerance,  eyebox_tolerance + eyebox_fade_distance, delta);
	float distance_fade = max(fade_near, fade_far);

	// Scale scope shadow radius to distance
	float dynamic_radius = mix(0.0, shadow_inner_radius, 1.0 - distance_fade);
	float inner = dynamic_radius - shadow_fade_factor;
	float outer = dynamic_radius;
	float fade = smoothstep(inner, outer, dist);

	return mix(scope_sample.rgb, BLACK, fade);
}


// Scope viewport edge fade
// Returns color
vec3 get_scope_edge_fade_effect(vec2 adj_uv, vec4 scope_sample) {
	vec2 center_uv = vec2(0.5);
	float dist = length(adj_uv - center_uv);

	float fade_edge_end = 0.5;
	float fade_edge_start = fade_edge_end - view_edge_fade_factor;
	float fade_edge = smoothstep(fade_edge_start, fade_edge_end, dist);
	return mix(scope_sample.rgb, BLACK, fade_edge);
}


float get_in_bounds_factor(vec2 uv) {
	const float lower = 0.01;
	const float upper = 0.99;
	return step(lower, uv.x) * step(uv.x, upper) * step(lower, uv.y) * step(uv.y, upper);
}


void vertex() {
	view = (MODELVIEW_MATRIX * vec4(VERTEX, 1.0)).xyz - (MODELVIEW_MATRIX * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
}


void fragment() {
	float eye_distance = length(NODE_POSITION_WORLD - CAMERA_POSITION_WORLD);
	vec3 view_dir = normalize(normalize(-VERTEX + EYE_OFFSET) * mat3(TANGENT, -BINORMAL, NORMAL));

	// Inside of scope
	if (length(view) <= scope_radius) {
		vec2 adj_uv = get_depth_effect(UV, view_dir, reticle_depth);

		// This setup seems to cause a parallax-like effect
		// minimize using parallax_factor
		vec2 parallax_uv = mix(adj_uv, UV, parallax_factor);

		vec4 scope_sample = texture(scope_texture, adj_uv);
		scope_sample.rgb = get_scope_shadow_effect(UV, scope_sample, view_dir, eye_distance);
		scope_sample.rgb = get_scope_edge_fade_effect(adj_uv, scope_sample);

		vec4 reticle_sample = texture(reticle_texture, parallax_uv);

		// Mask to in-bounds to discard reticle sample "stretching to infinity" at edge of UV
		reticle_sample.a *= get_in_bounds_factor(adj_uv);

		ALPHA = 1.0;
		ALBEDO = mix(scope_sample.rgb, reticle_sample.rgb, reticle_sample.a);

	// Outside radius: Discard to make a perfect circle
	} else {
		discard;
	}
}
Tags
3d, eyebox, in, optic, parallax, picture, picture-in-picture, pip, Reticle, Scope, scope shadow, sniper, sniper scope, VR, VR Compatible
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.

Related shaders

Realistic Water

Water Shader for Realistic Look

Realistic Water with reflection and refraction

guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments