big
This commit is contained in:
@@ -53,11 +53,56 @@ static func tinted(tint: Color, dark: bool = false) -> Material:
|
||||
|
||||
static var _flat_cache: Dictionary = {}
|
||||
|
||||
|
||||
## Surface laws: what KIND of thing this is, rather than what colour it is.
|
||||
##
|
||||
## A flat cel colour is the style, but a forty-metre wall holding exactly one
|
||||
## value is not stylised, it is unfinished — the probe measured 0.28 mean
|
||||
## adjacent-pixel difference, meaning the only structure in the frame was the
|
||||
## silhouettes. These give a surface its own quiet detail underneath the cel
|
||||
## bands, all of it procedural and world-space, so nothing needs UVs, textures,
|
||||
## or an artist.
|
||||
##
|
||||
## The laws are deliberately few. Every one of them has to be a thing a cel
|
||||
## painter would actually draw:
|
||||
##
|
||||
## wall Panel seams at storey-ish spacing plus a grade that darkens the
|
||||
## bottom few metres. The grade is the important half: it is
|
||||
## painted-in ambient occlusion, it grounds a building at any
|
||||
## distance, and unlike SSAO it does not vanish when the camera
|
||||
## pulls back.
|
||||
## ground Wider seams and no grade — a floor has no "bottom" to darken,
|
||||
## and paving slabs are bigger than wall panels.
|
||||
## panel Tight seams for machined props: shutters, containers, kiosks.
|
||||
## trim Grade only. For kerbs, plinths and bases, where a seam grid
|
||||
## would fight the shape but contact darkening still helps.
|
||||
## "" Plain flat colour. Props, vehicles, foliage and anything whose
|
||||
## silhouette is doing the work — a seam grid crawling over a tree
|
||||
## or a car reads as dirt, not as construction.
|
||||
const SURFACE_LAW := {
|
||||
"wall": {"seam_scale": 2.6, "seam_strength": 0.20, "seam_width": 0.010,
|
||||
"grade_height": 7.0, "grade_strength": 0.20},
|
||||
"ground": {"seam_scale": 4.0, "seam_strength": 0.13, "seam_width": 0.006},
|
||||
"panel": {"seam_scale": 0.9, "seam_strength": 0.24, "seam_width": 0.020},
|
||||
"trim": {"grade_height": 1.6, "grade_strength": 0.24},
|
||||
}
|
||||
|
||||
|
||||
static func _apply_law(mat: ShaderMaterial, law: String) -> void:
|
||||
if law == "" or not SURFACE_LAW.has(law):
|
||||
return
|
||||
for param in SURFACE_LAW[law]:
|
||||
mat.set_shader_parameter(param, SURFACE_LAW[law][param])
|
||||
|
||||
|
||||
## Flat cel color: toon banding with NO grid texture — the full stylized
|
||||
## look for dressed maps (vs. tinted()'s greybox grid for blockouts).
|
||||
## Rim/specular stay off (they blob on large level surfaces).
|
||||
static func flat(tint: Color) -> Material:
|
||||
var key := tint.to_html()
|
||||
##
|
||||
## `law` names a SURFACE_LAW: what the surface is made of, not what colour it
|
||||
## is. Defaults to plain flat colour, so every existing call site is unchanged.
|
||||
static func flat(tint: Color, law: String = "") -> Material:
|
||||
var key := "%s|%s" % [tint.to_html(), law]
|
||||
if _flat_cache.has(key):
|
||||
return _flat_cache[key]
|
||||
var mat := ShaderMaterial.new()
|
||||
@@ -67,6 +112,7 @@ static func flat(tint: Color) -> Material:
|
||||
mat.set_shader_parameter("albedo_color", tint)
|
||||
mat.set_shader_parameter("rim_strength", 0.0)
|
||||
mat.set_shader_parameter("specular_strength", 0.0)
|
||||
_apply_law(mat, law)
|
||||
_flat_cache[key] = mat
|
||||
return mat
|
||||
|
||||
@@ -252,10 +298,114 @@ static func apply_character_look(root: Node, surfaces: SkinSurfaces = null) -> v
|
||||
toon.set_shader_parameter("shadow_color", CHARACTER_SHADOW)
|
||||
# Eye irises and highlights are flat art on a curved ball; an ink
|
||||
# line around them reads as a second pupil.
|
||||
# The dummy headless renderer has no material backend for an
|
||||
# outline pass and emits teardown errors while following next_pass
|
||||
# RIDs. CI needs the animation/scene graph, not pixels.
|
||||
toon.next_pass = null if detail == "eyes" \
|
||||
or DisplayServer.get_name() == "headless" \
|
||||
else outline(look["outline"])
|
||||
|
||||
|
||||
## First-person weapons live in their own viewport and are read at arm's
|
||||
## length, so they need a different emphasis from distant world props: a broad
|
||||
## cool rim for silhouette separation and a small stepped metal glint that
|
||||
## reveals receivers, magazines and sights without turning the gun glossy.
|
||||
## This is intentionally viewmodel-only; characters stay matte.
|
||||
const VIEWMODEL_LOOK := {
|
||||
"dark_metal": {
|
||||
"rim": 0.12, "rim_width": 0.26, "band": 0.035, "mid": 0.62,
|
||||
"shadow": Color(0.12, 0.15, 0.25), "cast": 0.34,
|
||||
"specular": 0.14, "shininess": 104.0, "floor": 0.0,
|
||||
"ambient": 0.0, "contrast": 1.12, "saturation": 1.02, "gain": 0.96,
|
||||
"painted_light": 1.15, "vm_shadow": 0.18, "vm_mid": 0.58,
|
||||
"crease": 0.34, "crease_threshold": 0.09,
|
||||
},
|
||||
"light_metal": {
|
||||
"rim": 0.11, "rim_width": 0.28, "band": 0.045, "mid": 0.70,
|
||||
"shadow": Color(0.30, 0.36, 0.52), "cast": 0.42,
|
||||
"specular": 0.10, "shininess": 112.0, "floor": 0.0,
|
||||
"ambient": 0.0, "contrast": 0.96, "saturation": 0.96, "gain": 0.52,
|
||||
"painted_light": 0.35, "vm_shadow": 0.16, "vm_mid": 0.50,
|
||||
"crease": 0.28, "crease_threshold": 0.10,
|
||||
},
|
||||
"painted": {
|
||||
"rim": 0.12, "rim_width": 0.30, "band": 0.050, "mid": 0.72,
|
||||
"shadow": Color(0.25, 0.30, 0.46), "cast": 0.44,
|
||||
"specular": 0.035, "shininess": 88.0, "floor": 0.0,
|
||||
"ambient": 0.0, "contrast": 0.90, "saturation": 1.30, "gain": 0.90,
|
||||
"painted_light": 0.88, "vm_shadow": 0.14, "vm_mid": 0.48,
|
||||
"crease": 0.24, "crease_threshold": 0.11,
|
||||
},
|
||||
"polymer": {
|
||||
"rim": 0.09, "rim_width": 0.26, "band": 0.045, "mid": 0.64,
|
||||
"shadow": Color(0.16, 0.19, 0.29), "cast": 0.38,
|
||||
"specular": 0.015, "shininess": 72.0, "floor": 0.0,
|
||||
"ambient": 0.0, "contrast": 0.82, "saturation": 1.02, "gain": 0.88,
|
||||
"painted_light": 0.88, "vm_shadow": 0.11, "vm_mid": 0.44,
|
||||
"crease": 0.30, "crease_threshold": 0.10,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
static func _viewmodel_profile(mat: ShaderMaterial) -> String:
|
||||
var value = mat.get_shader_parameter("albedo_color")
|
||||
var color := value as Color if value is Color else Color.WHITE
|
||||
var luma := color.get_luminance()
|
||||
var chroma := maxf(color.r, maxf(color.g, color.b)) \
|
||||
- minf(color.r, minf(color.g, color.b))
|
||||
var textured := bool(mat.get_shader_parameter("has_texture"))
|
||||
if textured or chroma > 0.045:
|
||||
return "painted"
|
||||
if luma < 0.34:
|
||||
return "dark_metal"
|
||||
if luma > 0.72:
|
||||
return "light_metal"
|
||||
return "polymer"
|
||||
|
||||
|
||||
static func apply_viewmodel_look(root: Node) -> void:
|
||||
for mi in root.find_children("*", "MeshInstance3D", true, false):
|
||||
if not mi.mesh:
|
||||
continue
|
||||
for s in range(mi.mesh.get_surface_count()):
|
||||
var toon := mi.get_surface_override_material(s) as ShaderMaterial
|
||||
if toon == null:
|
||||
continue
|
||||
var look: Dictionary = VIEWMODEL_LOOK[_viewmodel_profile(toon)]
|
||||
toon.set_shader_parameter("rim_strength", look["rim"])
|
||||
toon.set_shader_parameter("rim_width", look["rim_width"])
|
||||
toon.set_shader_parameter("band_softness", look["band"])
|
||||
toon.set_shader_parameter("mid_tone", look["mid"])
|
||||
toon.set_shader_parameter("shadow_color", look["shadow"])
|
||||
toon.set_shader_parameter("cast_shadow_depth", look["cast"])
|
||||
toon.set_shader_parameter("specular_strength", look["specular"])
|
||||
toon.set_shader_parameter("specular_shininess", look["shininess"])
|
||||
toon.set_shader_parameter("albedo_floor", look["floor"])
|
||||
toon.set_shader_parameter("ambient_fill", look["ambient"])
|
||||
toon.set_shader_parameter("albedo_contrast", look["contrast"])
|
||||
toon.set_shader_parameter("albedo_saturation", look["saturation"])
|
||||
toon.set_shader_parameter("albedo_gain", look["gain"])
|
||||
toon.set_shader_parameter(
|
||||
"viewmodel_light_strength", look["painted_light"])
|
||||
toon.set_shader_parameter(
|
||||
"viewmodel_shadow_tone", look["vm_shadow"])
|
||||
toon.set_shader_parameter(
|
||||
"viewmodel_mid_tone", look["vm_mid"])
|
||||
toon.set_shader_parameter(
|
||||
"viewmodel_crease_strength", look["crease"])
|
||||
toon.set_shader_parameter(
|
||||
"viewmodel_crease_threshold", look["crease_threshold"])
|
||||
# Set these explicitly rather than relying on shader defaults.
|
||||
# Runtime-created ShaderMaterials can predate a hot-reloaded shader
|
||||
# default in the editor cache, which left the fill at black.
|
||||
toon.set_shader_parameter(
|
||||
"viewmodel_key_direction", Vector3(-0.42, 0.58, 0.70))
|
||||
toon.set_shader_parameter(
|
||||
"viewmodel_key_color", Color(0.98, 0.88, 0.76))
|
||||
toon.set_shader_parameter(
|
||||
"viewmodel_fill_color", Color(0.62, 0.68, 0.88))
|
||||
|
||||
|
||||
## Swap every mesh surface under `node` to toon shading and add an
|
||||
## inverted-hull outline overlay. Safe on skinned meshes (material_overlay
|
||||
## re-renders the same deformed mesh).
|
||||
@@ -265,14 +415,99 @@ static func apply_toon_recursive(node: Node, outline_width: float = 0.005) -> vo
|
||||
var surface_count: int = mi.mesh.get_surface_count() if mi.mesh else 0
|
||||
for s in range(surface_count):
|
||||
var src := mi.get_active_material(s)
|
||||
# Some kit surfaces intentionally omit a material. Leaving those
|
||||
# null is visually equivalent to Godot's white default, but the
|
||||
# headless renderer cannot query instance shader parameters from a
|
||||
# null material during scene teardown. Give them an explicit toon
|
||||
# default so runtime map sweeps remain error-free.
|
||||
if src == null:
|
||||
var fallback := StandardMaterial3D.new()
|
||||
fallback.albedo_color = Color(0.78, 0.78, 0.82)
|
||||
src = fallback
|
||||
if src and not (src is ShaderMaterial):
|
||||
mi.set_surface_override_material(s, toonify(src))
|
||||
if outline_width > 0.0:
|
||||
var toon := mi.get_surface_override_material(s) as ShaderMaterial
|
||||
if toon:
|
||||
var arrays: Array = mi.mesh.surface_get_arrays(s)
|
||||
var has_vertex_colors: bool = arrays.size() > Mesh.ARRAY_COLOR \
|
||||
and arrays[Mesh.ARRAY_COLOR] is PackedColorArray \
|
||||
and not (arrays[Mesh.ARRAY_COLOR] as PackedColorArray).is_empty()
|
||||
toon.set_shader_parameter("use_vertex_color", has_vertex_colors)
|
||||
# Avoid constructing render-only overlay chains on the headless dummy
|
||||
# backend. The real game renderer still receives the complete ink pass.
|
||||
if outline_width > 0.0 and DisplayServer.get_name() != "headless":
|
||||
mi.material_overlay = outline(outline_width)
|
||||
for child in node.get_children():
|
||||
apply_toon_recursive(child, outline_width)
|
||||
|
||||
|
||||
## ── The sakura cel look ──────────────────────────────────────────────────────
|
||||
##
|
||||
## `flat()` above is the older path: toon.gdshader's three-tone break, calibrated
|
||||
## against imported character textures that already carry painted shading. These
|
||||
## build on sakura_cel.gdshader instead — a quantised N-band ramp with a
|
||||
## hue-shifted shadow — which is what flat-coloured world geometry wants.
|
||||
const CEL_SHADER := "res://assets/shaders/sakura_cel.gdshader"
|
||||
|
||||
## Ramp ids, matching `ramp_id` in sakura_cel.gdshader.
|
||||
##
|
||||
## RAMP_3 the default, and what most of a scene should be
|
||||
## RAMP_2 two hard tones, for small props whose silhouette does the work
|
||||
## RAMP_4/5 more steps, for large curved masses that would otherwise show
|
||||
## their band edges as hard bars across a wall
|
||||
## RAMP_SOFT* high key: the darkest band is 0.71 rather than 0.36, for pale
|
||||
## masses (blossom, cloud, plaster) that must stay light even on
|
||||
## the shadow side
|
||||
enum {
|
||||
RAMP_2 = 0,
|
||||
RAMP_3 = 1,
|
||||
RAMP_4 = 2,
|
||||
RAMP_5 = 3,
|
||||
RAMP_SOFT = 4,
|
||||
RAMP_SOFT3 = 5,
|
||||
}
|
||||
|
||||
static var _cel_cache: Dictionary = {}
|
||||
static var _unlit_cache: Dictionary = {}
|
||||
|
||||
|
||||
## A cel material. `law` names a SURFACE_LAW as `flat()` does; `faceted` maps to
|
||||
## the shader's flat_shading and should stay on for anything built out of boxes
|
||||
## and cylinders, which here is everything.
|
||||
static func cel(color: Color, ramp: int = RAMP_3, tint: Color = SakuraPalette.TINT,
|
||||
law: String = "", faceted: bool = true) -> ShaderMaterial:
|
||||
var key := "%s|%d|%s|%s|%s" % [color.to_html(), ramp, tint.to_html(), law, faceted]
|
||||
if _cel_cache.has(key):
|
||||
return _cel_cache[key]
|
||||
var mat := ShaderMaterial.new()
|
||||
mat.shader = load(CEL_SHADER)
|
||||
mat.set_shader_parameter("albedo_color", color)
|
||||
mat.set_shader_parameter("has_texture", false)
|
||||
mat.set_shader_parameter("ramp_id", ramp)
|
||||
mat.set_shader_parameter("shadow_tint", tint)
|
||||
mat.set_shader_parameter("flat_shading", faceted)
|
||||
_apply_law(mat, law)
|
||||
_cel_cache[key] = mat
|
||||
return mat
|
||||
|
||||
|
||||
## Unlit flat colour — the reference's `flat()`. For sky panels, distant
|
||||
## silhouettes, sign faces, glass, lit windows and the ink details a cel painter
|
||||
## would draw rather than light. Anything that takes light instead of being a
|
||||
## drawing should use cel().
|
||||
static func unlit(color: Color, double_sided: bool = false) -> StandardMaterial3D:
|
||||
var key := "%s|%s" % [color.to_html(), double_sided]
|
||||
if _unlit_cache.has(key):
|
||||
return _unlit_cache[key]
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.albedo_color = color
|
||||
if double_sided:
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
_unlit_cache[key] = mat
|
||||
return mat
|
||||
|
||||
|
||||
static func outline(width: float = 0.005) -> ShaderMaterial:
|
||||
var key := "%.4f" % width
|
||||
if _outline_cache.has(key):
|
||||
|
||||
Reference in New Issue
Block a user