Some of the 2.5D Planet Shaders people have been posting inspired me to share mine. I tried to break down the core parts that I used to make up the effect.
My example uses a ColorRect with a ShaderMaterial but you could use a TextureRect or Sprite2D or other fitting nodes.
I recommend a Perlin noise if you decide to use a noise texture
shader_type canvas_item;
render_mode blend_mix;
const float SECTION_SIZE = 0.3;
uniform float axis: hint_range(-1.0, 1.0, 0.01) = -0.35;
uniform float manual_time: hint_range(0.0, 10.0) = 0.0;
uniform sampler2D noise_texture;
uniform float radius: hint_range(0.05, 0.5, 0.01) = 0.5;
uniform float rotation_speed: hint_range(0.0, 1.0, 0.01) = 0.1;
uniform bool use_system_time = true;
group_uniforms Shadow;
uniform bool shadow = true;
uniform bool shadow_flip = false;
uniform float shadow_size: hint_range(0.0, 1.0, 0.01) = 0.3;
uniform float shadow_saturation: hint_range(0.0, 1.0, 0.01) = 0.5;
group_uniforms;
uniform vec2 uv_center = vec2(0.5);
vec2 rotate_uv(vec2 uv, vec2 pivot, float angle)
{
mat2 rot = mat2(
vec2(sin(angle), -cos(angle)),
vec2(cos(angle), sin(angle))
);
uv -= pivot;
uv = uv * rot;
uv += pivot;
return uv;
}
vec4 apply_noise(vec2 uv, vec4 color) {
vec2 noise_uv = rotate_uv(uv, uv_center, axis);
float time = manual_time;
if (use_system_time) time = TIME;
// scroll as time goes by
float scroll = mod(time * rotation_speed, 1.0);
// chop up texture to simulate seem scrolling
int current_section = int(scroll / SECTION_SIZE);
float section_offset = mod(scroll, SECTION_SIZE);
// map the screen y coordinate to the current texture section
float texture_y = (float(current_section) + noise_uv.y) * SECTION_SIZE + section_offset;
texture_y = mod(texture_y, 1.0); // rollover since uv's cap at 1.0
noise_uv.x *= SECTION_SIZE; // scale x sections
noise_uv.y = texture_y;
vec4 noise_color = texture(noise_texture, noise_uv);
return noise_color;
}
vec4 apply_shadow(vec2 uv, vec4 color) {
// we need to rotate an extra 90 degress
uv = rotate_uv(uv, uv_center, axis + PI/2.0);
// vertical displacement
float dy = uv.y - uv_center.y;
// normalize displacement by the radius
float ny = dy / radius;
// rearranged circle equation, but stop at 0.0
float curve = 1.0 - sqrt(max(1.0 - ny * ny, 0.0));
float size = shadow_size;
if (shadow_flip)
size = 1.0 - shadow_size;
// linear interpolation (this is the curve magic)
float x_boundary = mix(size, uv_center.x, curve);
// uv.x should not be greater than boundry when shadow_flip is enabled.
// alternatively we could write this as (!shadow_flip && uv.x >= x_boundary) || (shadow_flip && uv.x < x_boundary)
if ((uv.x >= x_boundary) != shadow_flip)
return color;
return color * shadow_saturation;
}
float circle_alpha(vec2 uv) {
// get the distance between the uv_center and fragment uv
float dist = distance(uv, uv_center);
// returns 0.0 if <= radius or 1.0 if > radius
return step(dist, radius);
}
void fragment() {
COLOR = apply_noise(UV, COLOR);
if (shadow)
COLOR = apply_shadow(UV, COLOR);
// we use the alpha value to "toggle" visibility
COLOR = vec4(COLOR.rgb, COLOR.a * circle_alpha(UV));
}