feat: screen-space ink edge pass — drawn outlines on everything
New assets/shaders/ink_edge.gdshader: fullscreen depth+normal discontinuity detection draws thin ink lines over the whole scene — the renderer-level line pass anime-styled games use. Works on every model regardless of mesh topology (kit buildings, cars, heroes, characters), with distance fade so the skyline stays clean. Attached via LevelEnvironment so every map gets it for whatever camera renders (first-person, third-person, spectator). Street capture verified: crisp uniform outlines citywide, no artifacts. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
911066875b
commit
5224a455a6
@@ -0,0 +1,57 @@
|
||||
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.
|
||||
|
||||
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;
|
||||
|
||||
float linear_depth(vec2 uv, mat4 inv_proj) {
|
||||
float d = texture(depth_tex, uv).r;
|
||||
vec4 ndc = vec4(uv * 2.0 - 1.0, d, 1.0);
|
||||
vec4 view = inv_proj * ndc;
|
||||
return -view.z / view.w;
|
||||
}
|
||||
|
||||
void vertex() {
|
||||
POSITION = vec4(VERTEX.xy, 1.0, 1.0);
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec2 px = 1.0 / VIEWPORT_SIZE;
|
||||
vec2 uv = SCREEN_UV;
|
||||
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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 fade = 1.0 - smoothstep(max_distance * 0.6, max_distance, dc);
|
||||
float edge = max(depth_edge, normal_edge * 0.6) * edge_strength * fade;
|
||||
|
||||
vec3 scene = texture(screen_tex, uv).rgb;
|
||||
ALBEDO = mix(scene, ink_color.rgb, edge);
|
||||
}
|
||||
@@ -34,6 +34,20 @@ static func add_to(level: Node, sky_variant: String = "day") -> WorldEnvironment
|
||||
fill.shadow_enabled = false
|
||||
level.add_child(fill)
|
||||
|
||||
# Screen-space ink edge pass: drawn outlines on every model for whatever
|
||||
# camera renders (fullscreen POSITION-override quad, never culled).
|
||||
if not level.has_node("InkEdgePost"):
|
||||
var ink := MeshInstance3D.new()
|
||||
ink.name = "InkEdgePost"
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2(2, 2)
|
||||
var ink_mat := ShaderMaterial.new()
|
||||
ink_mat.shader = load("res://assets/shaders/ink_edge.gdshader")
|
||||
quad.material = ink_mat
|
||||
ink.mesh = quad
|
||||
ink.extra_cull_margin = 16384.0
|
||||
level.add_child(ink)
|
||||
|
||||
# Quiet ambient bed so the arena never sits in dead air.
|
||||
if not level.has_node("AmbientBed") and ResourceLoader.exists("res://assets/sounds/wind.wav"):
|
||||
var amb := AudioStreamPlayer.new()
|
||||
|
||||
Reference in New Issue
Block a user