feat: thinner cel outline + procedural model actually holds its weapon

Outline: the inverted-hull width was tuned when the look was heavier and
now reads as a thick marker stroke. Cut every call site to roughly a third
(characters 0.012 -> 0.004, level geometry 0.015 -> 0.005, Neon Alley's
size-scaled clamp 0.02..0.05 -> 0.007..0.018) plus the shader default, so
the ink line stays readable without fattening every silhouette.

Third-person weapon on HumanoidModel: the gun was parented to root_pivot at
a fixed (-0.15, 1.0, 0.4) — floating 40 cm off the chest while both arms
posed at it from a distance. Give the rig a real right-hand node at the end
of the forearm and hang the weapon there, so it travels with the arm. Its
aim is re-derived from the body each frame (hand_r_pivot's global basis is
copied from root_pivot), otherwise the barrel would swing around with the
arm animation. The hold pose is retuned to match: right elbow tucked at the
ribs with a level forearm, left arm crossing to the handguard.

_seat_weapon_in_hand() slides each weapon along its own barrel axis so a
plausible grip lands in the fist regardless of where the artist left the
model origin. It measures with MeshInstance3D.get_aabb(), not
mesh.get_aabb() — the FBX gun parts are skinned, so the Mesh resource still
carries bind-pose bounds tens of metres across.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Nicholas Butzke
2026-07-21 17:32:37 -04:00
co-authored by Claude Opus 4.8
parent 9f60982416
commit af8c9831c9
5 changed files with 78 additions and 31 deletions
+2 -2
View File
@@ -98,7 +98,7 @@ static func toonify(src: Material) -> Material:
## 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).
static func apply_toon_recursive(node: Node, outline_width: float = 0.015) -> void:
static func apply_toon_recursive(node: Node, outline_width: float = 0.005) -> void:
if node is MeshInstance3D:
var mi := node as MeshInstance3D
var surface_count: int = mi.mesh.get_surface_count() if mi.mesh else 0
@@ -112,7 +112,7 @@ static func apply_toon_recursive(node: Node, outline_width: float = 0.015) -> vo
apply_toon_recursive(child, outline_width)
static func outline(width: float = 0.015) -> ShaderMaterial:
static func outline(width: float = 0.005) -> ShaderMaterial:
var key := "%.4f" % width
if _outline_cache.has(key):
return _outline_cache[key]
+4 -4
View File
@@ -115,7 +115,7 @@ func _hero(key: String, pos: Vector3, yaw_deg: float, col_size: Vector3,
body.add_child(shape)
var inst: Node3D = _kit_scene_at("res://assets/props/hero/" + key + ".glb").instantiate()
body.add_child(inst)
LevelMaterials.apply_toon_recursive(inst, 0.015)
LevelMaterials.apply_toon_recursive(inst, 0.005)
## Small visual-only kit prop (parasols, awnings): no collider needed.
@@ -125,7 +125,7 @@ func _kit_prop(key: String, pos: Vector3, yaw_deg: float, scale_f: float) -> voi
add_child(inst)
inst.global_position = pos
inst.rotation_degrees.y = yaw_deg
LevelMaterials.apply_toon_recursive(inst, 0.02)
LevelMaterials.apply_toon_recursive(inst, 0.007)
## Place a kit model with a box collider matching its scaled bounds.
@@ -249,7 +249,7 @@ func _box_static(pos: Vector3, size: Vector3, color: Color, node_name: String =
for mi in body.find_children("*", "MeshInstance3D", false, false):
mi.mesh.surface_set_material(0, LevelMaterials.flat(color))
if maxf(size.x, maxf(size.y, size.z)) < 10.0:
var w := clampf(maxf(size.x, maxf(size.y, size.z)) * 0.008, 0.02, 0.05)
var w := clampf(maxf(size.x, maxf(size.y, size.z)) * 0.003, 0.007, 0.018)
mi.material_overlay = LevelMaterials.outline(w)
return body
@@ -707,7 +707,7 @@ func _build_rail_line() -> void:
fb.add_child(fb_shape)
var fb_inst: Node3D = _kit_scene_at("res://assets/props/hero/footbridge.glb").instantiate()
fb.add_child(fb_inst)
LevelMaterials.apply_toon_recursive(fb_inst, 0.015)
LevelMaterials.apply_toon_recursive(fb_inst, 0.005)
# Walkable stair ramps at both ends (over the stair visuals)
for e in [-1.0, 1.0]:
var ang := rad_to_deg(atan2(5.5, 6.4))