Feat/outline thickness and tp weapon hold #22

Merged
Dotts merged 43 commits from feat/outline-thickness-and-tp-weapon-hold into main 2026-07-27 23:22:53 -07:00
4 changed files with 156 additions and 3 deletions
Showing only changes of commit d8b0c08611 - Show all commits
+7 -2
View File
@@ -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;
+5
View File
@@ -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)
+90
View File
@@ -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)
+54 -1
View File
@@ -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).