This commit is contained in:
Nicholas Butzke
2026-08-02 02:20:02 -04:00
parent 61669627db
commit 922983429e
226 changed files with 34032 additions and 18521 deletions
+96
View File
@@ -0,0 +1,96 @@
#ifndef ANIME_CLOUDS_INCLUDED
#define ANIME_CLOUDS_INCLUDED
// Organic 2D cloud field shared by the actual Sky shader and the fullscreen
// ink composite. Broad domain-warped masses define the cloud, a second rotated
// octave breaks up its contour into billows, and a quiet high-frequency layer
// erodes the edge. Unlike a grid of radial primitives, no stage exposes a
// circle or a square as the final silhouette.
float cloud_hash(vec2 p) {
// Sine-free hash: substantially cheaper when the field is sampled several
// times to estimate the direction the sun meets the cloud.
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
float cloud_noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
// Quintic interpolation keeps both the value and its slope continuous at
// cell boundaries, so a density threshold cannot reveal the noise grid.
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = cloud_hash(i);
float b = cloud_hash(i + vec2(1.0, 0.0));
float c = cloud_hash(i + vec2(0.0, 1.0));
float d = cloud_hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float cloud_fbm(vec2 p) {
mat2 turn = mat2(vec2(0.80, 0.60), vec2(-0.60, 0.80));
float value = cloud_noise(p) * 0.52;
p = turn * p * 2.03 + vec2(9.17, 4.31);
value += cloud_noise(p) * 0.27;
p = turn * p * 2.07 + vec2(3.73, 12.61);
value += cloud_noise(p) * 0.14;
p = turn * p * 2.01 + vec2(15.19, 7.07);
value += cloud_noise(p) * 0.07;
return value;
}
float cloud_detail(vec2 p) {
mat2 turn = mat2(vec2(0.86, 0.51), vec2(-0.51, 0.86));
return cloud_noise(turn * p * 2.75 + vec2(23.7, 6.1)) * 0.68
+ cloud_noise(p * 5.20 + vec2(2.9, 31.4)) * 0.32;
}
float cloud_field(vec2 p) {
// Low-frequency vector warp makes the mass curl and fork without making
// the outline busy. The two components use unrelated offsets so this does
// not merely slide the source noise along its own contour.
vec2 domain = p * 0.34;
vec2 warp = vec2(
cloud_noise(domain + vec2(17.2, 4.8)),
cloud_noise(domain + vec2(3.1, 29.6))) - 0.5;
vec2 q = p + warp * 1.35;
float mass = cloud_fbm(q * 0.52);
float billows = cloud_fbm(
mat2(vec2(0.74, 0.67), vec2(-0.67, 0.74)) * q * 1.18
+ vec2(11.3, 8.7));
float erosion = cloud_detail(q);
float density = mass * 0.70 + billows * 0.24 + erosion * 0.06;
// A third, much broader field separates the weather into distinct banks.
// Without this macro mask a perfectly organic contour can still connect
// across half the dome and read as one enormous sheet instead of clouds.
float islands = cloud_fbm(q * 0.24 + vec2(41.7, 18.3));
return density - (1.0 - islands) * 0.16;
}
float cloud_sun_response(vec2 p, vec3 sun_direction) {
// Treat density as a painted height field. Sampling a short distance toward
// and away from the sun tells us which edge faces it. This gives the cloud
// a bright leading rim and a colored lee side, and naturally reverses when
// the DirectionalLight turns.
vec2 sun_xz = sun_direction.xz;
float horizontal = length(sun_xz);
vec2 axis = horizontal > 0.0001
? sun_xz / horizontal : vec2(0.7071, 0.7071);
float toward = cloud_field(p + axis * 0.075);
float away = cloud_field(p - axis * 0.075);
float directional_slope = (away - toward) / 0.15;
// A high sun lights more of the cloud face; a low sun makes the directional
// division stronger and leaves a broader colored underside.
float overhead = clamp(sun_direction.y, 0.0, 1.0);
return smoothstep(-0.22, 0.22, directional_slope + overhead * 0.08);
}
#endif
@@ -0,0 +1 @@
uid://dbh0unyul1h7d
+90
View File
@@ -0,0 +1,90 @@
shader_type sky;
// Cel sky: a painted gradient with rounded, flat-toned anime clouds.
//
// ProceduralSkyMaterial gives a smooth two-colour ramp, and a smooth ramp over
// 40% of the frame is exactly what tools/levels.py was reporting as dead%:
// somewhere between a quarter and a third of every shot was a single flat
// shade with nothing in it. Sky is the cheapest real estate in the picture and
// it was empty.
//
// The clouds are drawn the way an anime background artist draws them — a
// silhouette filled with two or three flat tones and a crisp edge, NOT a soft
// volumetric puff. That is both the correct style and the cheap option.
uniform vec3 top_color : source_color = vec3(0.18, 0.40, 0.85);
uniform vec3 horizon_color : source_color = vec3(0.72, 0.88, 0.98);
uniform vec3 ground_color : source_color = vec3(0.24, 0.22, 0.30);
uniform vec3 cloud_color : source_color = vec3(1.0, 0.99, 0.97);
uniform vec3 cloud_shadow : source_color = vec3(0.72, 0.75, 0.88);
// Raise to thin the clouds out. This is a threshold on the noise, so it is the
// knob for "how much sky is covered", not a density.
uniform float cloud_cover : hint_range(0.0, 1.0) = 0.52;
uniform float cloud_scale : hint_range(0.2, 8.0) = 1.6;
uniform float cloud_softness : hint_range(0.001, 0.2) = 0.035;
// Slow. This is a competitive shooter and anything the eye can track in the
// sky is a distraction; it only needs to move enough that the sky is not a
// photograph.
uniform float drift : hint_range(0.0, 0.02) = 0.0022;
uniform float horizon_falloff : hint_range(0.1, 2.0) = 0.45;
uniform float sun_halo : hint_range(0.0, 2.0) = 0.35;
#include "res://assets/shaders/anime_clouds.gdshaderinc"
void sky() {
float up = clamp(EYEDIR.y, 0.0, 1.0);
float down = clamp(-EYEDIR.y, 0.0, 1.0);
vec3 col = EYEDIR.y >= 0.0
? mix(horizon_color, top_color, pow(up, horizon_falloff))
: mix(horizon_color, ground_color, pow(down, 0.62));
// Soft halo around the sun. The procedural sky drew a disk here; a broad
// glow reads better under the filmic shoulder and does not clip.
if (LIGHT0_ENABLED) {
float d = clamp(dot(EYEDIR, LIGHT0_DIRECTION), 0.0, 1.0);
col += LIGHT0_COLOR * pow(d, 24.0) * sun_halo;
}
if (EYEDIR.y > 0.005) {
// Project the dome onto a plane overhead, so clouds compress toward the
// horizon the way real ones do instead of smearing across the zenith.
vec2 uv = EYEDIR.xz / (EYEDIR.y + 0.18) * cloud_scale + vec2(TIME * drift, 0.0);
float n = cloud_field(uv);
// The edge remains clean enough for the anime style, but it follows a
// layered organic field instead of revealing primitive circles.
float edge = max(cloud_softness, fwidth(n) * 1.35);
float shape = smoothstep(cloud_cover - edge, cloud_cover + edge, n);
vec3 sun_direction = LIGHT0_ENABLED
? normalize(LIGHT0_DIRECTION)
: normalize(vec3(0.45, 0.55, -0.70));
vec3 sun_color = LIGHT0_ENABLED
? LIGHT0_COLOR : vec3(1.0, 0.91, 0.72);
float facing = cloud_sun_response(uv, sun_direction);
float core = smoothstep(
cloud_cover + 0.045 - edge,
cloud_cover + 0.155 + edge, n);
float carving = smoothstep(
0.38, 0.64, cloud_detail(uv + vec2(2.4, 9.7)));
// Three painted tones respond to the actual DirectionalLight: a colored
// lee side, a broad lit face, and a narrow sunward silver lining.
float lit_face = clamp(
0.12 + core * 0.25 + facing * 0.58 + carving * 0.05,
0.0, 1.0);
vec3 lit_color = mix(cloud_color, sun_color, 0.18);
vec3 cloud = mix(cloud_shadow, lit_color, lit_face);
float boundary = 1.0 - smoothstep(
cloud_cover + edge, 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 += sun_color * silver_lining;
// Fade out near the horizon, where the projection stretches a cloud
// into an infinitely long streak.
col = mix(col, cloud, shape * smoothstep(0.0, 0.22, EYEDIR.y));
}
COLOR = col;
}
+1
View File
@@ -0,0 +1 @@
uid://cx2rt7638txa1
+231 -29
View File
@@ -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);
}
+258
View File
@@ -0,0 +1,258 @@
shader_type spatial;
// ── The cel law ──────────────────────────────────────────────────────────────
//
// Ported from the Sakura Crossing renderer (three.js MeshToonMaterial + a
// patched toon BRDF). Two ideas, and the second one is the one that matters:
//
// 1. Direct light is QUANTISED against a hand-authored ramp — a tiny
// nearest-filtered texture sampled at `dot(N,L) * 0.5 + 0.5`. So a surface
// is never lit by a falloff, only ever by one of 2-5 flat values.
//
// 2. The darker bands are HUE-SHIFTED toward a cool violet rather than being
// a darker version of the base colour:
//
// irradiance = band * mix(shadow_tint, vec3(1.0), band)
//
// The band drives its own tint, so full light is untinted, the mid band is
// slightly cooled and the darkest band is almost entirely shadow-hue. That
// hue shift in shadow is most of what separates "anime cel" from
// "low-poly 3D" — a wall whose shade is just 40% of its lit colour reads
// as an unlit polygon; a wall whose shade swings violet reads as painted.
//
// Godot's light() is called per light and adds into DIFFUSE_LIGHT, and its
// Lambert term already carries the 1/PI that three.js puts in BRDF_Lambert, so
// the two pipelines agree once the ramp value is substituted for `dot(N,L)`.
//
// This is a SEPARATE shader from toon.gdshader on purpose. That one carries the
// weapon-viewmodel colour shaping and the character band calibration, both
// tuned against imported textures that are already painted with cel shading.
// This one is for FLAT-COLOURED WORLD GEOMETRY, which is what the reference is
// built out of end to end, and it would be wrong to make either compromise for
// the other.
// ── Ramps ────────────────────────────────────────────────────────────────────
//
// The reference's stop tables, kept as raw 0-255 values so they can be checked
// against the source line by line. These are LINEAR values, not sRGB: three.js
// gives a DataTexture no colour space, so the stops are used as authored, and
// the same has to be true here or every band lands too bright.
//
// 2/3/4/5 the standard ramps. 3 is the default and does most of the world.
// 6 "soft" — high key, 2 bands, for pale masses (blossom, cloud,
// 7 "soft3" — high key, 3 bands plaster) that must stay light even
// on the shadow side. Without these a
// cherry canopy goes grey the moment
// it turns away from the sun, which
// is the one thing a blossom tree may
// never do.
const float RAMP_2[5] = float[](96.0, 255.0, 255.0, 255.0, 255.0);
const float RAMP_3[5] = float[](92.0, 178.0, 255.0, 255.0, 255.0);
const float RAMP_4[5] = float[](80.0, 142.0, 202.0, 255.0, 255.0);
const float RAMP_5[5] = float[](74.0, 124.0, 172.0, 214.0, 255.0);
const float RAMP_SOFT[5] = float[](180.0, 255.0, 255.0, 255.0, 255.0);
const float RAMP_SOFT3[5] = float[](172.0, 214.0, 255.0, 255.0, 255.0);
// Which ramp, and how many of its stops are live.
// 0 = 2 band 1 = 3 band (default) 2 = 4 band 3 = 5 band
// 4 = soft 2 band (high key) 5 = soft 3 band (high key)
uniform int ramp_id : hint_range(0, 5) = 1;
uniform vec4 albedo_color : source_color = vec4(1.0);
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap, repeat_enable;
uniform bool has_texture = false;
uniform bool use_vertex_color = false;
// The cool violet the shadow bands swing toward. 0x6c5f8c is the reference's
// default; individual materials shift it (warmer for timber and blossom, bluer
// for metal and water) the same way the reference's `cel({tint})` does.
uniform vec4 shadow_tint : source_color = vec4(0.424, 0.373, 0.549, 1.0);
// Faceted shading. The reference sets `flatShading: true` on essentially
// everything, and it is not a shortcut — a quantised ramp over SMOOTH normals
// puts a curved band boundary across a curved surface, which reads as an
// airbrushed gradient no matter how few steps it has. Faceting forces every
// band edge onto a polygon edge, which is where a cel painter would put it.
uniform bool flat_shading = true;
// ── Cast shadow ──────────────────────────────────────────────────────────────
//
// The reference folds shadow attenuation into the light colour, so an occluded
// pixel there goes to zero direct light and is carried entirely by ambient.
// That works when the whole world is one authored scene; in a shooter, where
// players have to be read against the geometry they are standing on, a cast
// shadow that reaches black swallows them.
//
// So occlusion drives the surface to the ramp's DARKEST STOP — fully tinted,
// scaled by cast_shadow_depth — rather than to nothing. The result is still a
// hard, dark, clearly separate shape (which is what an animator inks), but it
// keeps hue, so a character standing in the shade of a shopfront stays legible.
uniform float cast_shadow_depth : hint_range(0.0, 1.0) = 0.72;
uniform float cast_shadow_softness : hint_range(0.01, 1.0) = 0.30;
// Band-edge anti-aliasing floor, in NdotL. Nearest-filtered quantisation is
// a hard step, and a hard step on a slowly-curving surface crawls and
// stair-steps as the camera moves — this is shading aliasing, so MSAA cannot
// touch it. See light() for how the screen-space term works.
uniform float band_softness : hint_range(0.0, 0.5) = 0.012;
// Rim and specular are OFF by default and should mostly stay that way: the
// reference has neither, and on large level surfaces a toon rim reads as a
// giant soft blob smeared across a wall.
uniform float rim_strength : hint_range(0.0, 2.0) = 0.0;
uniform float rim_width : hint_range(0.0, 1.0) = 0.28;
// ── Surface law ──────────────────────────────────────────────────────────────
//
// Carried over from toon.gdshader, because the problem it solves is real here
// too: a flat cel colour is the style, but a twelve-metre wall holding exactly
// one value is not stylised, it is empty. Both default to off — the reference
// answers this with geometry (every fascia, sill and downpipe is modelled), and
// where this map does the same it does not need them.
group_uniforms detail;
uniform float seam_scale = 0.0; // metres per panel; 0 = off
uniform float seam_strength : hint_range(0.0, 1.0) = 0.24;
uniform float seam_width : hint_range(0.001, 0.2) = 0.010;
uniform float grade_height = 0.0; // metres to fade over; 0 = off
uniform float grade_strength : hint_range(0.0, 1.0) = 0.18;
uniform float grade_floor = 0.0; // world Y the grade starts from
varying vec3 world_pos;
varying vec3 world_normal;
varying float normal_slope;
float ramp_stop(int i) {
// GLSL ES 3.0 will not index a const array with a non-constant expression on
// every driver, so the ramp is selected by branch and the stop by a small
// unrolled pick. Both indices are uniform-or-loop-constant, so this costs
// nothing measurable and works everywhere.
float s[5] = RAMP_3;
if (ramp_id == 0) { s = RAMP_2; }
else if (ramp_id == 2) { s = RAMP_4; }
else if (ramp_id == 3) { s = RAMP_5; }
else if (ramp_id == 4) { s = RAMP_SOFT; }
else if (ramp_id == 5) { s = RAMP_SOFT3; }
float v = s[0];
if (i == 1) { v = s[1]; }
else if (i == 2) { v = s[2]; }
else if (i == 3) { v = s[3]; }
else if (i >= 4) { v = s[4]; }
return v / 255.0;
}
int ramp_bands() {
if (ramp_id == 0 || ramp_id == 4) { return 2; }
if (ramp_id == 2) { return 4; }
if (ramp_id == 3) { return 5; }
return 3;
}
float seam_lines(vec2 uv) {
vec2 g = abs(fract(uv) - 0.5);
vec2 fw = fwidth(uv) + 0.0001;
vec2 line = smoothstep(0.5 - seam_width - fw, 0.5 - seam_width + fw, g);
return max(line.x, line.y);
}
float panel_seam(vec3 p, vec3 n) {
vec3 w = pow(abs(n), vec3(4.0));
w /= (w.x + w.y + w.z);
float s = seam_lines(p.zy / seam_scale) * w.x
+ seam_lines(p.xz / seam_scale) * w.y
+ seam_lines(p.xy / seam_scale) * w.z;
float cell_px = length(fwidth(p)) / seam_scale;
return s * (1.0 - smoothstep(0.25, 0.5, cell_px));
}
void vertex() {
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
world_normal = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz);
}
void fragment() {
vec3 base = albedo_color.rgb;
if (has_texture) {
base *= texture(albedo_texture, UV).rgb;
}
if (use_vertex_color) {
base *= COLOR.rgb;
}
if (seam_scale > 0.0) {
base *= 1.0 - panel_seam(world_pos, world_normal) * seam_strength;
}
if (grade_height > 0.0) {
float h = clamp((world_pos.y - grade_floor) / grade_height, 0.0, 1.0);
base *= mix(1.0 - grade_strength, 1.0, h);
}
if (flat_shading) {
// VERTEX is view-space position here, so its screen derivatives span the
// triangle's own plane and their cross product is the true face normal.
// Forced to face the viewer, because the winding of that cross product
// flips with the handedness of the projection and a back-to-front normal
// lights the facet from behind.
vec3 fn = normalize(cross(dFdx(VERTEX), dFdy(VERTEX)));
NORMAL = fn * sign(dot(fn, VIEW));
}
ALBEDO = base;
ROUGHNESS = 1.0;
SPECULAR = 0.0;
// Screen-space rate of turn of the normal, measured here because
// derivatives are a fragment-stage operation — taking fwidth() inside
// light(), which runs once per light, is undefined on some drivers.
// Faceted geometry has a slope of zero across a facet, so this only ever
// widens the band edge where the surface is genuinely curving.
normal_slope = length(fwidth(NORMAL));
float rim = 1.0 - clamp(dot(normalize(VIEW), NORMAL), 0.0, 1.0);
rim = smoothstep(1.0 - rim_width, 1.0, rim);
EMISSION = base * rim * rim_strength;
}
void light() {
float ndotl = dot(NORMAL, LIGHT);
// Quantise. `t` is the reference's ramp lookup coordinate exactly — the
// texture is sampled at dot(N,L) * 0.5 + 0.5 with NearestFilter, so band
// boundaries fall at evenly spaced values of that coordinate and nowhere
// else. With the 3-stop ramp that puts them at dot(N,L) = ±1/3.
int bands = ramp_bands();
float fb = clamp(ndotl * 0.5 + 0.5, 0.0, 0.999999) * float(bands);
int idx = int(floor(fb));
float frac_b = fb - float(idx);
float hi = ramp_stop(idx);
float lo = ramp_stop(max(idx - 1, 0));
// Anti-aliasing budget. band_softness is in NdotL, which says nothing about
// how many PIXELS a transition covers; normal_slope does. Widening the step
// to at least the on-screen rate of turn keeps every band edge about a pixel
// wide, and leaves edges that are already wider exactly as authored. The
// d(fb)/d(ndotl) factor of bands*0.5 converts the NdotL width into the
// band-index units `frac_b` is measured in.
float w = max(band_softness, normal_slope * 0.9) * float(bands) * 0.5;
float band = mix(lo, hi, smoothstep(0.0, max(w * 2.0, 0.0001), frac_b));
// The hue shift. Band drives its own tint: full light is untinted, the
// darkest band is very nearly pure shadow hue.
vec3 lit = band * mix(shadow_tint.rgb, vec3(1.0), band);
// Cast shadow: down to the darkest stop, fully tinted, never to black.
float floor_band = ramp_stop(0);
vec3 occluded = floor_band * mix(shadow_tint.rgb, vec3(1.0), floor_band)
* cast_shadow_depth;
float occ = smoothstep(0.0, cast_shadow_softness, ATTENUATION);
vec3 shade = mix(occluded, lit, occ);
DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR / PI * shade;
}
+1
View File
@@ -0,0 +1 @@
uid://dk5nb354iw044
+212 -8
View File
@@ -9,6 +9,7 @@ shader_type spatial;
uniform vec4 albedo_color : source_color = vec4(1.0);
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap, repeat_enable;
uniform bool has_texture = true;
uniform bool use_vertex_color = false;
uniform bool use_triplanar = false;
uniform float triplanar_tile = 2.0; // world units per texture tile
@@ -21,15 +22,84 @@ uniform float mid_band_edge : hint_range(-1.0, 1.0) = 0.55; // second, brighter
// reads as a glossy stripe sweeping across the hair.
uniform float mid_tone : hint_range(0.0, 1.0) = 0.82;
uniform vec4 shadow_color : source_color = vec4(0.62, 0.65, 0.78, 1.0); // cool shadow tint
// How far a CAST shadow drops below the form-shadow tint. Cel art draws the
// two separately: the terminator (a surface turning away from the sun) is a
// tone step, a cast shadow (something standing in the way) is a darker, harder
// shape. See light() for why collapsing them made benches float.
uniform float cast_shadow_depth : hint_range(0.0, 1.0) = 0.62;
// Width of the cast-shadow edge, in ATTENUATION. Small keeps it crisp like
// inked artwork; not zero, or the shadow map's own stair-steps show.
uniform float cast_shadow_softness : hint_range(0.01, 1.0) = 0.35;
// Matte-anime defaults: zero specular (any stepped glint reads as shine
// sweeping across hair/cloth when the camera moves), whisper of rim.
uniform float rim_strength : hint_range(0.0, 2.0) = 0.05;
uniform float rim_width : hint_range(0.0, 1.0) = 0.28;
uniform float specular_strength : hint_range(0.0, 1.0) = 0.0;
uniform float specular_shininess : hint_range(1.0, 128.0) = 64.0;
// Viewmodel/readability controls. Both default to zero so world materials are
// byte-for-byte unchanged. The first keeps near-black authored metal from
// collapsing below display range; the second is a controlled painted fill,
// useful in a transparent viewmodel viewport that has no environment ambient.
uniform float albedo_floor : hint_range(0.0, 0.5) = 0.0;
uniform float ambient_fill : hint_range(0.0, 1.0) = 0.0;
// Viewmodel color shaping. Defaults preserve every world/character material;
// weapon profiles opt in so dark receiver metal, pale trim and painted parts
// do not all converge on the same middle grey under a uniform fill.
uniform float albedo_contrast : hint_range(0.5, 2.0) = 1.0;
uniform float albedo_saturation : hint_range(0.0, 2.0) = 1.0;
uniform float albedo_gain : hint_range(0.25, 1.5) = 1.0;
uniform float viewmodel_light_strength : hint_range(0.0, 2.0) = 0.0;
uniform vec3 viewmodel_key_direction = vec3(-0.42, 0.58, 0.70);
uniform vec3 viewmodel_key_color : source_color = vec3(1.0, 0.91, 0.80);
uniform vec3 viewmodel_fill_color : source_color = vec3(0.72, 0.80, 1.0);
// Viewmodel ink controls. The old deterministic light was an additive
// key/fill wash: it made every normal visible, but also lifted receiver
// recesses and polymer undersides toward the same middle grey. These controls
// make that painted light a four-tone cel ramp and add a thin curvature ink
// pass. They default off/neutral so world and character materials are
// unchanged.
uniform float viewmodel_shadow_tone : hint_range(0.0, 1.0) = 0.18;
uniform float viewmodel_mid_tone : hint_range(0.0, 1.0) = 0.52;
uniform float viewmodel_crease_strength : hint_range(0.0, 1.0) = 0.0;
uniform float viewmodel_crease_threshold : hint_range(0.001, 1.0) = 0.12;
// ── Surface law ──────────────────────────────────────────────────────────────
// Flat cel colour is the style, but a wall that is ONE value from end to end is
// not stylised, it is empty — tools/levels.py measured 0.28 mean adjacent-pixel
// difference across the probe, i.e. essentially nothing but the silhouettes.
// These give a surface its own quiet structure without touching the flat-shaded
// look: everything here is authored into ALBEDO, so the cel bands still land on
// top of it exactly as before.
//
// All default to OFF (scale 0). A material opts in through LevelMaterials'
// surface laws, so props, vehicles and foliage keep plain flat colour and only
// architecture and ground get the treatment.
group_uniforms detail;
// Panel seams: a world-space grid of thin darker lines, triplanar so it needs
// no UVs and stays the same physical size on every surface it crosses. This is
// the single biggest change to how "built" a surface reads.
uniform float seam_scale = 0.0; // metres per panel; 0 = off
uniform float seam_strength : hint_range(0.0, 1.0) = 0.30;
uniform float seam_width : hint_range(0.001, 0.2) = 0.012; // fraction of a panel
// Vertical grade: surfaces darken toward the ground. A painter would put this
// in by hand — it is ambient occlusion as artwork rather than as a screen
// effect, it costs nothing, and it works at any distance, which SSAO does not.
uniform float grade_height = 0.0; // metres to fade over; 0 = off
uniform float grade_strength : hint_range(0.0, 1.0) = 0.16;
uniform float grade_floor = 0.0; // world Y the grade starts from
// Halftone: screen-tone dots inside the shadow band. The signature manga move
// and ZZZ does use it, but sparingly — off unless a material asks.
uniform float halftone_scale = 0.0; // screen px per dot; 0 = off
uniform float halftone_strength : hint_range(0.0, 1.0) = 0.35;
varying vec3 world_pos;
varying vec3 world_normal;
// How fast the normal is turning across one pixel, measured in fragment() and
// read in light(). This is the band-edge anti-aliasing budget — see light().
varying float normal_slope;
// Screen-tone dot mask, measured in fragment() where FRAGCOORD lives and spent
// in light() where the shadow band is known.
varying float halftone_dot;
void vertex() {
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
@@ -49,6 +119,33 @@ vec3 sample_triplanar(vec3 p, vec3 n) {
return cx * w.x + cy * w.y + cz * w.z;
}
// One axis-plane's worth of panel grid. Returns 1 on a seam line, 0 on the
// panel face, anti-aliased by the on-screen size of a cell so the lines neither
// shimmer at distance nor turn into fat bars up close — a seam drawn with
// step() looks fine in a screenshot and crawls badly the moment anything moves.
float seam_lines(vec2 uv) {
vec2 g = abs(fract(uv) - 0.5);
vec2 fw = fwidth(uv) + 0.0001;
vec2 line = smoothstep(0.5 - seam_width - fw, 0.5 - seam_width + fw, g);
return max(line.x, line.y);
}
// Triplanar panel seams, weighted the same way the albedo triplanar is so the
// lines agree with the texture across a corner instead of crossfading against
// it. Far enough away that a cell is sub-pixel the whole thing fades out,
// otherwise the grid turns into moiré.
float panel_seam(vec3 p, vec3 n) {
vec3 w = pow(abs(n), vec3(4.0));
w /= (w.x + w.y + w.z);
float s = seam_lines(p.zy / seam_scale) * w.x
+ seam_lines(p.xz / seam_scale) * w.y
+ seam_lines(p.xy / seam_scale) * w.z;
float cell_px = length(fwidth(p)) / seam_scale;
return s * (1.0 - smoothstep(0.25, 0.5, cell_px));
}
void fragment() {
vec3 base = albedo_color.rgb;
if (has_texture) {
@@ -58,30 +155,137 @@ void fragment() {
base *= texture(albedo_texture, UV).rgb;
}
}
if (use_vertex_color) {
base *= COLOR.rgb;
}
base *= albedo_gain;
float source_luma = dot(base, vec3(0.2126, 0.7152, 0.0722));
base = mix(vec3(source_luma), base, albedo_saturation);
// Non-clipping power curve. Imported gun greys arrive as very small LINEAR
// values (an sRGB 0.17 receiver is roughly 0.025 here); a conventional 18%
// linear contrast pivot clips all of those finishes to exactly zero. The
// reciprocal curve expands their separation without moving black or white.
base = pow(max(base, vec3(0.0)),
vec3(1.0 / max(albedo_contrast, 0.001)));
if (albedo_floor > 0.0) {
float luma = dot(base, vec3(0.2126, 0.7152, 0.0722));
base = min(base * max(1.0, albedo_floor / max(luma, 0.001)), vec3(1.0));
}
// Surface law, into the albedo so the cel bands still light it normally.
if (seam_scale > 0.0) {
base *= 1.0 - panel_seam(world_pos, world_normal) * seam_strength;
}
if (grade_height > 0.0) {
float h = clamp((world_pos.y - grade_floor) / grade_height, 0.0, 1.0);
base *= mix(1.0 - grade_strength, 1.0, h);
}
// A one-pixel technical-ink accent where the surface normal changes
// quickly. Unlike an inverted hull this follows receiver cut-lines and
// bevels without exploding on the weapon FBXs' split hard normals.
float local_normal_slope = length(fwidth(normalize(NORMAL)));
float vm_crease = smoothstep(
viewmodel_crease_threshold,
viewmodel_crease_threshold * 2.6,
local_normal_slope);
base *= 1.0 - vm_crease * viewmodel_crease_strength;
// Screen-tone dots, rotated 45° the way a real screentone sheet is. Spent
// in light(), which is where we know whether this pixel is in shadow.
halftone_dot = 0.0;
if (halftone_scale > 0.0) {
vec2 hp = FRAGCOORD.xy / halftone_scale;
vec2 r = vec2(hp.x - hp.y, hp.x + hp.y) * 0.70710678;
halftone_dot = smoothstep(0.36, 0.30, length(fract(r) - 0.5));
}
ALBEDO = base;
ROUGHNESS = 1.0;
SPECULAR = 0.0;
// Screen-space rate of change of the normal, for the band-edge width in
// light(). It has to be measured here: derivatives are a fragment-stage
// operation, and light() runs once per light — taking fwidth() inside that
// loop is undefined behaviour on some drivers.
normal_slope = length(fwidth(NORMAL));
// Rim: brighten grazing angles for that inked-silhouette pop.
float rim = 1.0 - clamp(dot(normalize(VIEW), NORMAL), 0.0, 1.0);
rim = smoothstep(1.0 - rim_width, 1.0, rim);
EMISSION = base * rim * rim_strength;
// Deterministic view-space key/fill for the separate transparent viewmodel
// viewport. Directional lights parented to its camera are renderer- and
// world-sharing-sensitive; this painted light map follows the gun normals
// directly and therefore survives every level environment.
float vm_ndotl = dot(normalize(NORMAL), normalize(viewmodel_key_direction));
float vm_leave_shadow = smoothstep(-0.24, -0.04, vm_ndotl);
float vm_enter_light = smoothstep(0.18, 0.34, vm_ndotl);
float vm_highlight = smoothstep(0.64, 0.78, vm_ndotl);
float vm_tone = mix(viewmodel_shadow_tone, viewmodel_mid_tone,
vm_leave_shadow);
vm_tone = mix(vm_tone, 0.82, vm_enter_light);
vm_tone = mix(vm_tone, 1.0, vm_highlight);
vec3 vm_light = mix(viewmodel_fill_color, viewmodel_key_color,
vm_enter_light);
EMISSION = base * (
vec3(rim * rim_strength + ambient_fill)
+ vm_light * vm_tone * viewmodel_light_strength);
}
void light() {
float ndotl = dot(NORMAL, LIGHT);
// Shadowing folds into the band test so shadow edges band too.
float lit = ndotl * ATTENUATION;
float band = smoothstep(band_edge - band_softness, band_edge + band_softness, lit);
float mid = smoothstep(mid_band_edge - band_softness, mid_band_edge + band_softness, lit);
// Band-edge width, with a screen-space floor.
//
// band_softness is measured in NdotL, which says nothing about how many
// PIXELS the transition actually covers. On a slowly-curving surface it can
// span a third of a wall; on a tight curve, or simply at distance, it
// collapses below one pixel and the terminator crawls and stair-steps as
// the camera moves. MSAA cannot touch this — it is shading aliasing, not
// geometry. normal_slope is how much the normal turns across one pixel, so
// widening the step to at least that much keeps every band edge about a
// pixel wide while leaving edges that are already wider exactly as authored.
// Flat surfaces have a slope of zero and so keep their crisp break.
float w = max(band_softness, normal_slope * 0.9);
// FORM shadow — the surface turning away from the light. This is the cel
// terminator, and its numbers are what the whole look was calibrated on, so
// it still lands exactly where it did.
float band = smoothstep(band_edge - w, band_edge + w, ndotl);
float mid = smoothstep(mid_band_edge - w, mid_band_edge + w, ndotl);
// 3 tones: shadow tint -> base band (mid_tone) -> full light.
float tone = mix(mid_tone, 1.0, mid);
vec3 shade = mix(shadow_color.rgb, vec3(tone), band);
// CAST shadow — something is standing between this pixel and the light.
//
// These used to be one thing: NdotL was multiplied by ATTENUATION and the
// PRODUCT was banded, so a cast shadow could never be darker than the
// terminator's own tint, about 0.65 of full light. Under the old ambient
// flood (Neon Alley used to run 1.35) that came out as a dip of roughly a
// seventh — which is why a bench standing in direct sun threw nothing onto
// the pavement, and why tools/levels.py measured a 1st percentile of 119 on
// a daylit frame: nothing in it was dark, so nothing rested on anything.
//
// Cel art draws the two separately, and so does this now: the terminator
// keeps its calibrated tone step, and occlusion drops below it to
// cast_shadow_depth as a harder, deeper shape.
float occ = smoothstep(0.0, cast_shadow_softness, ATTENUATION);
shade = mix(shadow_color.rgb * cast_shadow_depth, shade, occ);
// Screen tone, in the shadow only — that is where a manga artist puts it,
// and confining it there means it reads as shading rather than as a filter
// laid over the picture. Scaled by how deep in shadow the pixel is, so the
// dots fade in across the terminator instead of switching on at it.
float in_shadow = (1.0 - band) * (1.0 - occ * 0.4);
shade *= 1.0 - halftone_dot * in_shadow * halftone_strength;
DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR / PI * shade;
// Stepped specular dot for glossy toon highlights.
// Stepped specular dot for glossy toon highlights. Gated by occ so a
// highlight cannot sit inside a shadow.
vec3 h = normalize(VIEW + LIGHT);
float spec = pow(clamp(dot(NORMAL, h), 0.0, 1.0), specular_shininess);
spec = smoothstep(0.5 - band_softness, 0.5 + band_softness, spec);
SPECULAR_LIGHT += LIGHT_COLOR * spec * specular_strength * band;
spec = smoothstep(0.5 - w, 0.5 + w, spec);
SPECULAR_LIGHT += LIGHT_COLOR * spec * specular_strength * band * occ;
}