Feat/outline thickness and tp weapon hold #22
@@ -15,6 +15,11 @@ 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
|
||||
// How dark the mid band sits below full light. Near 1.0 the second step almost
|
||||
// disappears, which is what textures that already carry painted cel shading
|
||||
// (imported anime characters) need — a hard step on top of painted shading
|
||||
// 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
|
||||
// Matte-anime defaults: zero specular (any stepped glint reads as shine
|
||||
// sweeping across hair/cloth when the camera moves), whisper of rim.
|
||||
@@ -69,8 +74,8 @@ void light() {
|
||||
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);
|
||||
// 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);
|
||||
DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR / PI * shade;
|
||||
|
||||
|
||||
@@ -154,6 +154,11 @@ func load_model(path: String) -> void:
|
||||
|
||||
# Cel-shaded look: toon shading over the imported textures + ink outline.
|
||||
LevelMaterials.apply_toon_recursive(scene)
|
||||
# ...then the character-only pass: flat line-work + softer banding (see the
|
||||
# function — toon-lighting the model's own outline shell is what put a white
|
||||
# rim on every hair strand, and re-banding already-shaded textures read as
|
||||
# gloss).
|
||||
LevelMaterials.apply_character_look(scene)
|
||||
if animation_player:
|
||||
_index_animations()
|
||||
_setup_anim_tree(scene)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
extends SceneTree
|
||||
|
||||
## Dev tool: a skin's RAW import beside the same model with the game's character
|
||||
## treatment, under identical lighting — the reference shot for tuning
|
||||
## LevelMaterials.apply_character_look() against the model's source render.
|
||||
## godot --path . --windowed --resolution 1280x720 \
|
||||
## -s res://debug/character_look_capture.gd -- <out_dir> [res-path-to-glb]
|
||||
## Saves look_head.png and look_body.png (left = raw import, right = ours).
|
||||
|
||||
const DEFAULT_GLB := "res://assets/characters/skins/taila.glb"
|
||||
|
||||
var _frames := 0
|
||||
var _out_dir := "."
|
||||
var _glb_path := DEFAULT_GLB
|
||||
var _cam: Camera3D = null
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
if args.size() > 0:
|
||||
_out_dir = args[0]
|
||||
if args.size() > 1:
|
||||
_glb_path = args[1]
|
||||
|
||||
var scene := Node3D.new()
|
||||
root.add_child(scene)
|
||||
current_scene = scene
|
||||
|
||||
# Neutral studio light, matched to the game's tonemap so colours compare.
|
||||
var env := WorldEnvironment.new()
|
||||
var e := Environment.new()
|
||||
e.background_mode = Environment.BG_COLOR
|
||||
e.background_color = Color(0.30, 0.32, 0.38)
|
||||
e.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
e.ambient_light_color = Color(0.85, 0.87, 0.95)
|
||||
e.ambient_light_energy = 1.1
|
||||
e.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
||||
e.tonemap_white = 2.4
|
||||
env.environment = e
|
||||
scene.add_child(env)
|
||||
|
||||
var sun := DirectionalLight3D.new()
|
||||
sun.rotation_degrees = Vector3(-38, 28, 0)
|
||||
sun.light_energy = 2.0
|
||||
scene.add_child(sun)
|
||||
|
||||
var raw := GLBLoader.load(_glb_path)
|
||||
if not raw:
|
||||
printerr("character_look_capture: could not load ", _glb_path)
|
||||
quit(1)
|
||||
return
|
||||
raw.position = Vector3(-0.45, 0, 0)
|
||||
scene.add_child(raw)
|
||||
|
||||
var ours := GLBLoader.load(_glb_path)
|
||||
ours.position = Vector3(0.45, 0, 0)
|
||||
scene.add_child(ours)
|
||||
LevelMaterials.apply_toon_recursive(ours)
|
||||
LevelMaterials.apply_character_look(ours)
|
||||
|
||||
_cam = Camera3D.new()
|
||||
scene.add_child(_cam)
|
||||
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
_frames += 1
|
||||
if _frames < 30:
|
||||
return false
|
||||
if _frames == 30:
|
||||
_look(Vector3(0, 1.42, 1.05), Vector3(0, 1.38, 0))
|
||||
return false
|
||||
if _frames == 32:
|
||||
_snap("look_head")
|
||||
_look(Vector3(0, 1.15, 2.2), Vector3(0, 0.95, 0))
|
||||
return false
|
||||
if _frames == 34:
|
||||
_snap("look_body")
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _look(pos: Vector3, at: Vector3) -> void:
|
||||
_cam.global_position = pos
|
||||
_cam.look_at(at, Vector3.UP)
|
||||
|
||||
|
||||
func _snap(tag: String) -> void:
|
||||
var img := root.get_viewport().get_texture().get_image()
|
||||
img.save_png(_out_dir + "/" + tag + ".png")
|
||||
print("character_look_capture: saved ", tag)
|
||||
@@ -87,7 +87,7 @@ static func toonify(src: Material) -> Material:
|
||||
mat.set_shader_parameter("albedo_color", col)
|
||||
# Fully matte characters: NO specular (even a 2% stepped glint reads as
|
||||
# shine sweeping across hair when the camera moves) and only a whisper
|
||||
# of rim for silhouette separation. Cel banding carries all the shape.
|
||||
# of rim for silhouette separation.
|
||||
mat.set_shader_parameter("rim_strength", 0.05)
|
||||
mat.set_shader_parameter("rim_width", 0.28)
|
||||
mat.set_shader_parameter("specular_strength", 0.0)
|
||||
@@ -95,6 +95,59 @@ static func toonify(src: Material) -> Material:
|
||||
return mat
|
||||
|
||||
|
||||
## Ink used for the line-work an imported character carries in its own mesh.
|
||||
const CHARACTER_INK := Color(0.07, 0.06, 0.09)
|
||||
|
||||
|
||||
## Second pass for IMPORTED CHARACTER models (the anime GLB skins), run right
|
||||
## after apply_toon_recursive. Two things those models need that props don't:
|
||||
##
|
||||
## 1. Their line-work is part of the mesh. Anime GLBs ship an inverted-hull
|
||||
## outline shell plus eye-line/eye-highlight cards as extra, UNTEXTURED
|
||||
## surfaces (Taila names them FullBlack / EyesFullBlack / EyesInvL / EyesHL).
|
||||
## They are authored to read as flat black — or flat white for a highlight —
|
||||
## and the glTF import hands them a default near-white albedo. Toon-LIGHTING
|
||||
## that turns every black shell pale: that was the thin white rim tracing
|
||||
## every hair strand.
|
||||
##
|
||||
## 2. Their textures are ALREADY painted with cel shading. Stacking the hard
|
||||
## 3-tone break on top read as gloss — a bright stripe sliding across the
|
||||
## hair as the camera moved. Characters get a soft terminator and an
|
||||
## almost-invisible second step so the painted shading carries the form.
|
||||
##
|
||||
## Props and level geometry keep the crisp banding they were calibrated with.
|
||||
static func apply_character_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 src: BaseMaterial3D = mi.mesh.surface_get_material(s) as BaseMaterial3D
|
||||
if src == null:
|
||||
continue
|
||||
if src.albedo_texture == null:
|
||||
# Untextured surface on a character = the model's own line-work.
|
||||
var flat := StandardMaterial3D.new()
|
||||
flat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
flat.cull_mode = src.cull_mode
|
||||
# "HL" marks a highlight card (the glint in the pupil) — that
|
||||
# one really is meant to be white.
|
||||
var is_highlight: bool = src.resource_name.to_lower().contains("hl")
|
||||
flat.albedo_color = Color.WHITE if is_highlight else CHARACTER_INK
|
||||
mi.set_surface_override_material(s, flat)
|
||||
continue
|
||||
var toon: ShaderMaterial = mi.get_surface_override_material(s) as ShaderMaterial
|
||||
if toon == null:
|
||||
continue
|
||||
toon.set_shader_parameter("band_softness", 0.16)
|
||||
toon.set_shader_parameter("mid_tone", 0.92)
|
||||
# Shadow tint measured off the Sketchfab reference render: sampling
|
||||
# Taila's hair there, shadow/midtone lands near (0.63, 0.53, 0.70).
|
||||
# Green drops hardest, and that is what keeps copper hair COPPER in
|
||||
# shadow — the level default (0.62, 0.65, 0.78) lifts green above
|
||||
# red and washes ginger toward a dull brown.
|
||||
toon.set_shader_parameter("shadow_color", Color(0.64, 0.56, 0.72))
|
||||
|
||||
|
||||
## 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).
|
||||
|
||||
Reference in New Issue
Block a user