shader_type spatial; // Cel/toon surface shader used across the game. // - Banded (stepped) diffuse with a tinted shadow color instead of black // - Soft rim light for silhouette pop // - Optional world-space triplanar albedo (level geometry: no UVs needed, // grid stays world-scaled) — otherwise standard UV sampling (characters) 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_triplanar = false; uniform float triplanar_tile = 2.0; // world units per texture tile uniform float band_edge : hint_range(-1.0, 1.0) = 0.05; // NdotL where light band starts uniform float band_softness : hint_range(0.001, 0.5) = 0.04; uniform float mid_band_edge : hint_range(-1.0, 1.0) = 0.55; // second, brighter band uniform vec4 shadow_color : source_color = vec4(0.62, 0.65, 0.78, 1.0); // cool shadow tint // 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; varying vec3 world_pos; varying vec3 world_normal; void vertex() { world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; world_normal = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz); } vec3 sample_triplanar(vec3 p, vec3 n) { vec3 w = abs(n); w = pow(w, vec3(4.0)); w /= (w.x + w.y + w.z); vec2 uv_x = p.zy / triplanar_tile; vec2 uv_y = p.xz / triplanar_tile; vec2 uv_z = p.xy / triplanar_tile; vec3 cx = texture(albedo_texture, uv_x).rgb; vec3 cy = texture(albedo_texture, uv_y).rgb; vec3 cz = texture(albedo_texture, uv_z).rgb; return cx * w.x + cy * w.y + cz * w.z; } void fragment() { vec3 base = albedo_color.rgb; if (has_texture) { if (use_triplanar) { base *= sample_triplanar(world_pos, world_normal); } else { base *= texture(albedo_texture, UV).rgb; } } ALBEDO = base; ROUGHNESS = 1.0; SPECULAR = 0.0; // 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; } 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); // 3 tones: shadow tint -> base band (0.82) -> full light. float tone = mix(0.82, 1.0, mid); vec3 shade = mix(shadow_color.rgb, vec3(tone), band); DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR / PI * shade; // Stepped specular dot for glossy toon highlights. 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; }