big
This commit is contained in:
@@ -1,57 +1,259 @@
|
||||
shader_type spatial;
|
||||
render_mode unshaded, fog_disabled, depth_draw_never, depth_test_disabled, cull_disabled;
|
||||
|
||||
// Screen-space ink outline: detects depth and normal discontinuities and
|
||||
// draws thin dark edges over the scene — the drawn-line pass that unifies
|
||||
// every model (kit, hero, character) into the cel look. Attach to a
|
||||
// fullscreen quad (POSITION override) so it runs for whatever camera renders.
|
||||
// ── The 3D-to-2D pass ────────────────────────────────────────────────────────
|
||||
//
|
||||
// One fullscreen quad doing three jobs, in this order:
|
||||
//
|
||||
// sky repaint the background, because this pass owns those pixels
|
||||
// ink screen-space line work from the depth buffer
|
||||
// grade the anime split-tone
|
||||
//
|
||||
// ## Lines come from a SECOND DIFFERENCE of linearised depth
|
||||
//
|
||||
// This is the change that matters, and it is worth spelling out because the
|
||||
// previous version of this file did the usual thing and it was wrong:
|
||||
//
|
||||
// float ddiff = abs(dl - dr) + abs(du - dd); // <- first difference
|
||||
//
|
||||
// A first difference measures the SLOPE of the depth buffer, and slope is large
|
||||
// wherever a surface is oblique to the camera — which the ground always is. So
|
||||
// the road, the pavement, every roof and every long wall seen at an angle all
|
||||
// carried ink, and the threshold had to be raised until it smeared rather than
|
||||
// drew. Raising it then lost the real silhouettes at distance, which is why
|
||||
// `depth_threshold` had to be scaled by `dc` to stop far geometry dissolving
|
||||
// into noise: the whole formula was fighting itself.
|
||||
//
|
||||
// A second difference measures CURVATURE, and curvature is exactly zero across
|
||||
// any planar surface no matter how oblique it is to the camera. So it fires on
|
||||
// real silhouettes and real creases and on nothing else — a flat road grazing
|
||||
// the camera produces literally no signal, and the threshold can therefore be
|
||||
// set low enough to catch a distant railing.
|
||||
//
|
||||
// The two signs are kept apart and drawn differently, which is also from the
|
||||
// reference: positive curvature (the near side of a silhouette, a convex ridge)
|
||||
// inks strongly, negative curvature (an inside corner, where two surfaces meet
|
||||
// away from the viewer) inks faintly. That is what an animator does — the
|
||||
// outside line of a shape is heavy, the contact line inside it is light.
|
||||
|
||||
uniform sampler2D screen_tex : hint_screen_texture, filter_linear;
|
||||
uniform sampler2D depth_tex : hint_depth_texture, filter_nearest;
|
||||
uniform sampler2D normal_tex : hint_normal_roughness_texture, filter_nearest;
|
||||
|
||||
uniform vec4 ink_color : source_color = vec4(0.07, 0.06, 0.09, 1.0);
|
||||
uniform float depth_threshold : hint_range(0.0, 4.0) = 1.1;
|
||||
uniform float normal_threshold : hint_range(0.0, 1.0) = 0.55;
|
||||
uniform float edge_strength : hint_range(0.0, 1.0) = 0.8;
|
||||
uniform float max_distance : hint_range(10.0, 500.0) = 220.0;
|
||||
group_uniforms ink;
|
||||
uniform vec4 ink_color : source_color = vec4(0.224, 0.196, 0.310, 1.0);
|
||||
// How far apart the taps sit, in pixels. This IS the line weight. The reference
|
||||
// runs 1.35 at a 1.5-2x internal resolution; this project renders at 1x, and the
|
||||
// brief asks for a slightly harder and thicker line than the reference, so it
|
||||
// sits at 2.0 — about a two-pixel line at 1080p.
|
||||
uniform float ink_thickness : hint_range(0.5, 6.0) = 2.3;
|
||||
// Curvature at which a convex edge reaches full ink. Small, because the second
|
||||
// difference is already normalised by distance and produces almost nothing on
|
||||
// flat ground — this can be sensitive in a way the old first-difference test
|
||||
// could never afford to be.
|
||||
uniform float ink_sensitivity : hint_range(0.0005, 0.05) = 0.0034;
|
||||
uniform float ink_concave : hint_range(0.002, 0.2) = 0.024;
|
||||
uniform float ink_concave_amount : hint_range(0.0, 1.0) = 0.45;
|
||||
// Hardness: where the ramp to full ink STARTS, as a fraction of the threshold.
|
||||
// The reference effectively runs 0.32 — ink begins to appear at a third of the
|
||||
// threshold curvature, which is a soft, tapering line. Pushing it toward 1.0
|
||||
// gives a harder, more decisive one, which is what the brief asks for.
|
||||
//
|
||||
// It cannot BE 1.0: that is a step(), and a step on a diagonal edge is a
|
||||
// staircase that shimmers the moment anything moves.
|
||||
//
|
||||
// It also sets the NOISE FLOOR, and that is not a side effect to ignore. A
|
||||
// grazing ground plane quantises in the depth buffer into a staircase, and a
|
||||
// staircase has a large second difference at every step — at 0.32 those steps
|
||||
// ink faintly and the road comes out finely hatched, which is precisely what a
|
||||
// first render at a wider tap spacing showed.
|
||||
uniform float ink_hardness : hint_range(0.1, 0.95) = 0.62;
|
||||
// How much of the surface's own colour survives inside the line. Low but not
|
||||
// zero: pure flat ink over every edge reads as pasted on, a whisper of the
|
||||
// underlying hue reads as drawn.
|
||||
uniform float ink_hue_bleed : hint_range(0.0, 0.6) = 0.14;
|
||||
uniform float ink_strength : hint_range(0.0, 1.0) = 1.0;
|
||||
// Let the far background dissolve into haze instead of turning into a mess of
|
||||
// busy line work.
|
||||
uniform float ink_fade_start : hint_range(5.0, 400.0) = 85.0;
|
||||
uniform float ink_fade_end : hint_range(10.0, 900.0) = 220.0;
|
||||
// Depth taps land on the sky. Clamping rather than special-casing them keeps
|
||||
// the silhouette signal huge but bounded, so an object against the sky inks at
|
||||
// full weight on BOTH sides of its edge — which is where the reference's early
|
||||
// return leaves the line only half as thick as it should be.
|
||||
uniform float ink_sky_depth : hint_range(50.0, 4000.0) = 900.0;
|
||||
|
||||
// ── The grade ────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// A split-tone: cool violet into the darks, warm paper-white into the lights,
|
||||
// plus a lift that keeps shadow off the floor. Godot tonemaps and converts to
|
||||
// sRGB after this pass, so unlike the reference this stays in linear and does
|
||||
// NOT do its own sRGB conversion — doing both would gamma the frame twice.
|
||||
group_uniforms grade;
|
||||
uniform bool grade_enabled = true;
|
||||
uniform vec3 grade_shadow_tint : source_color = vec3(0.678, 0.659, 0.816);
|
||||
uniform vec3 grade_light_tint : source_color = vec3(1.0, 0.969, 0.910);
|
||||
uniform float grade_saturation : hint_range(0.0, 2.0) = 1.12;
|
||||
uniform float grade_lift : hint_range(0.0, 0.2) = 0.028;
|
||||
uniform float grade_warmth : hint_range(0.0, 0.3) = 0.05;
|
||||
uniform float grade_vignette : hint_range(0.0, 0.6) = 0.15;
|
||||
|
||||
// The fullscreen pass owns the background pixels as well as the geometry ones.
|
||||
// Godot's screen copy holds only the viewport clear colour at far depth, so
|
||||
// sampling screen_tex there silently erases the real Environment sky — these
|
||||
// repaint it with the same palette and law as anime_sky.gdshader, and
|
||||
// LevelEnvironment keeps the two materials in sync.
|
||||
group_uniforms sky;
|
||||
uniform vec3 sky_top_color : source_color = vec3(0.18, 0.40, 0.85);
|
||||
uniform vec3 sky_horizon_color : source_color = vec3(0.72, 0.88, 0.98);
|
||||
uniform vec3 sky_ground_color : source_color = vec3(0.24, 0.22, 0.30);
|
||||
uniform vec3 sky_cloud_color : source_color = vec3(1.0, 0.99, 0.97);
|
||||
uniform vec3 sky_cloud_shadow : source_color = vec3(0.72, 0.75, 0.88);
|
||||
uniform float sky_cloud_cover : hint_range(0.0, 1.0) = 0.46;
|
||||
uniform float sky_cloud_scale : hint_range(0.2, 8.0) = 1.6;
|
||||
uniform float sky_cloud_softness : hint_range(0.001, 0.2) = 0.035;
|
||||
uniform float sky_drift : hint_range(0.0, 0.02) = 0.0022;
|
||||
uniform float sky_horizon_falloff : hint_range(0.1, 2.0) = 0.45;
|
||||
uniform vec3 sky_sun_direction = vec3(0.45, 0.34, -0.82);
|
||||
uniform vec3 sky_sun_color : source_color = vec3(1.0, 0.91, 0.72);
|
||||
uniform float sky_sun_halo : hint_range(0.0, 2.0) = 0.35;
|
||||
|
||||
|
||||
#include "res://assets/shaders/anime_clouds.gdshaderinc"
|
||||
|
||||
|
||||
vec3 painted_sky(vec3 dir) {
|
||||
float up = clamp(dir.y, 0.0, 1.0);
|
||||
float down = clamp(-dir.y, 0.0, 1.0);
|
||||
vec3 col = dir.y >= 0.0
|
||||
? mix(sky_horizon_color, sky_top_color, pow(up, sky_horizon_falloff))
|
||||
: mix(sky_horizon_color, sky_ground_color, pow(down, 0.62));
|
||||
float sun = pow(max(dot(dir, normalize(sky_sun_direction)), 0.0), 28.0);
|
||||
col += sky_sun_color * sun * sky_sun_halo;
|
||||
|
||||
if (dir.y > 0.005) {
|
||||
vec2 uv = dir.xz / (dir.y + 0.18) * sky_cloud_scale
|
||||
+ vec2(TIME * sky_drift, 0.0);
|
||||
float n = cloud_field(uv);
|
||||
float edge = max(sky_cloud_softness, fwidth(n) * 1.35);
|
||||
float shape = smoothstep(
|
||||
sky_cloud_cover - edge, sky_cloud_cover + edge, n);
|
||||
vec3 sun_direction = normalize(sky_sun_direction);
|
||||
float facing = cloud_sun_response(uv, sun_direction);
|
||||
float core = smoothstep(
|
||||
sky_cloud_cover + 0.045 - edge,
|
||||
sky_cloud_cover + 0.155 + edge, n);
|
||||
float carving = smoothstep(
|
||||
0.38, 0.64, cloud_detail(uv + vec2(2.4, 9.7)));
|
||||
float lit_face = clamp(
|
||||
0.12 + core * 0.25 + facing * 0.58 + carving * 0.05,
|
||||
0.0, 1.0);
|
||||
vec3 lit_color = mix(sky_cloud_color, sky_sun_color, 0.18);
|
||||
vec3 cloud = mix(sky_cloud_shadow, lit_color, lit_face);
|
||||
float boundary = 1.0 - smoothstep(
|
||||
sky_cloud_cover + edge,
|
||||
sky_cloud_cover + 0.105 + edge, n);
|
||||
float silver_lining = boundary * facing * facing
|
||||
* mix(0.10, 0.24, clamp(sun_direction.y, 0.0, 1.0));
|
||||
cloud += sky_sun_color * silver_lining;
|
||||
col = mix(col, cloud, shape * smoothstep(0.0, 0.22, dir.y));
|
||||
}
|
||||
return col;
|
||||
}
|
||||
|
||||
|
||||
// Linear view depth, with the sky pinned to a finite value so the second
|
||||
// difference across a silhouette is large but never infinite.
|
||||
float linear_depth(vec2 uv, mat4 inv_proj) {
|
||||
float d = texture(depth_tex, uv).r;
|
||||
// Godot 4.3+ is reverse-Z: the far plane and the sky read exactly 0.
|
||||
if (d <= 0.000001) {
|
||||
return ink_sky_depth;
|
||||
}
|
||||
vec4 ndc = vec4(uv * 2.0 - 1.0, d, 1.0);
|
||||
vec4 view = inv_proj * ndc;
|
||||
return -view.z / view.w;
|
||||
return min(-view.z / view.w, ink_sky_depth);
|
||||
}
|
||||
|
||||
|
||||
vec3 apply_grade(vec3 c, vec2 uv) {
|
||||
if (!grade_enabled) {
|
||||
return c;
|
||||
}
|
||||
float l = dot(c, vec3(0.2126, 0.7152, 0.0722));
|
||||
|
||||
// Split-tone: cool violet in the darks, warm paper white in the lights.
|
||||
float k = smoothstep(0.02, 0.55, l);
|
||||
c *= mix(grade_shadow_tint, grade_light_tint, k);
|
||||
|
||||
// A gentle overall warmth, like late afternoon light through blossom.
|
||||
c += vec3(grade_warmth, grade_warmth * 0.45, 0.0) * l * 0.35;
|
||||
|
||||
// Keep the shadows readable — never crushed to black.
|
||||
c += grade_lift * (1.0 - k);
|
||||
|
||||
c = mix(vec3(l), c, grade_saturation);
|
||||
|
||||
float r = length(uv - 0.5) * 1.42;
|
||||
c *= 1.0 - grade_vignette * pow(clamp(r, 0.0, 1.0), 2.6);
|
||||
|
||||
return max(c, vec3(0.0));
|
||||
}
|
||||
|
||||
|
||||
void vertex() {
|
||||
POSITION = vec4(VERTEX.xy, 1.0, 1.0);
|
||||
}
|
||||
|
||||
|
||||
void fragment() {
|
||||
vec2 px = 1.0 / VIEWPORT_SIZE;
|
||||
vec2 uv = SCREEN_UV;
|
||||
|
||||
// Background first: where the depth buffer has no geometry the screen copy
|
||||
// holds nothing but the clear colour, so those pixels are painted rather
|
||||
// than sampled. They still go through the ink test below — an object read
|
||||
// against the sky should be inked on the sky side of its edge too, which is
|
||||
// what makes the silhouette the heaviest line in the frame.
|
||||
float raw_depth = texture(depth_tex, uv).r;
|
||||
vec3 col;
|
||||
if (raw_depth <= 0.000001) {
|
||||
vec4 far_view = INV_PROJECTION_MATRIX * vec4(uv * 2.0 - 1.0, 0.0, 1.0);
|
||||
vec3 view_dir = normalize(far_view.xyz / far_view.w);
|
||||
vec3 world_dir = normalize((INV_VIEW_MATRIX * vec4(view_dir, 0.0)).xyz);
|
||||
col = painted_sky(world_dir);
|
||||
} else {
|
||||
col = texture(screen_tex, uv).rgb;
|
||||
}
|
||||
|
||||
vec2 t = px * ink_thickness;
|
||||
float dc = linear_depth(uv, INV_PROJECTION_MATRIX);
|
||||
float dl = linear_depth(uv - vec2(px.x, 0.0), INV_PROJECTION_MATRIX);
|
||||
float dr = linear_depth(uv + vec2(px.x, 0.0), INV_PROJECTION_MATRIX);
|
||||
float du = linear_depth(uv - vec2(0.0, px.y), INV_PROJECTION_MATRIX);
|
||||
float dd = linear_depth(uv + vec2(0.0, px.y), INV_PROJECTION_MATRIX);
|
||||
float dl = linear_depth(uv - vec2(t.x, 0.0), INV_PROJECTION_MATRIX);
|
||||
float dr = linear_depth(uv + vec2(t.x, 0.0), INV_PROJECTION_MATRIX);
|
||||
float du = linear_depth(uv + vec2(0.0, t.y), INV_PROJECTION_MATRIX);
|
||||
float dd = linear_depth(uv - vec2(0.0, t.y), INV_PROJECTION_MATRIX);
|
||||
|
||||
// Depth edge: neighbor difference scaled by distance so far geometry
|
||||
// doesn't dissolve into noise.
|
||||
float ddiff = abs(dl - dr) + abs(du - dd);
|
||||
float depth_edge = step(depth_threshold * (0.5 + dc * 0.08), ddiff);
|
||||
// Second difference of linear depth, normalised by distance so that a given
|
||||
// real-world crease inks with the same weight near and far.
|
||||
float sx = (dl + dr - 2.0 * dc) / dc;
|
||||
float sy = (du + dd - 2.0 * dc) / dc;
|
||||
|
||||
// Normal edge: crease lines on same-depth corners.
|
||||
vec3 nc = texture(normal_tex, uv).xyz * 2.0 - 1.0;
|
||||
vec3 nl = texture(normal_tex, uv - vec2(px.x, 0.0)).xyz * 2.0 - 1.0;
|
||||
vec3 nu = texture(normal_tex, uv - vec2(0.0, px.y)).xyz * 2.0 - 1.0;
|
||||
float ndiff = length(nc - nl) + length(nc - nu);
|
||||
float normal_edge = step(normal_threshold, ndiff) * step(ddiff, depth_threshold);
|
||||
float convex = max(0.0, sx) + max(0.0, sy);
|
||||
float concave = max(0.0, -sx) + max(0.0, -sy);
|
||||
|
||||
float fade = 1.0 - smoothstep(max_distance * 0.6, max_distance, dc);
|
||||
float edge = max(depth_edge, normal_edge * 0.6) * edge_strength * fade;
|
||||
float edge = smoothstep(
|
||||
ink_sensitivity * ink_hardness, ink_sensitivity, convex);
|
||||
edge = max(edge, smoothstep(
|
||||
ink_concave, ink_concave * 3.4, concave) * ink_concave_amount);
|
||||
|
||||
vec3 scene = texture(screen_tex, uv).rgb;
|
||||
ALBEDO = mix(scene, ink_color.rgb, edge);
|
||||
// Fade on the nearest real surface involved in the edge, not on dc: a sky
|
||||
// pixel beside a nearby roof would otherwise be measured at 900 m and its
|
||||
// half of the silhouette would fade out while the roof's half did not.
|
||||
float near_d = min(dc, min(min(dl, dr), min(du, dd)));
|
||||
edge *= 1.0 - smoothstep(ink_fade_start, ink_fade_end, near_d);
|
||||
edge *= ink_strength;
|
||||
|
||||
vec3 line = mix(ink_color.rgb, col * 0.42, ink_hue_bleed);
|
||||
col = mix(col, line, clamp(edge, 0.0, 1.0));
|
||||
|
||||
ALBEDO = apply_grade(col, uv);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user