feat(art): add storybook meadow and woodland villagers

This commit is contained in:
Rijad Zuzo
2026-09-05 18:56:44 +02:00
parent 6aae41d36f
commit da3bed67a0
60 changed files with 1222 additions and 373 deletions
+38
View File
@@ -0,0 +1,38 @@
class_name AnimalAppearance
extends RefCounted
## Disposable woodland identities, rebuilt from stable NPC IDs. No simulation RNG.
const CEL_SHADER := preload("res://world/presentation/materials/storybook_cel.gdshader")
const SPECIES: Array[StringName] = [&"fox", &"rabbit", &"badger", &"otter"]
const MODELS: Array[PackedScene] = [
preload("res://assets/storybook/fox.glb"),
preload("res://assets/storybook/rabbit.glb"),
preload("res://assets/storybook/badger.glb"),
preload("res://assets/storybook/otter.glb"),
]
const PARTS: Array[StringName] = [
&"Body", &"Head", &"Hair", &"ArmLeft", &"ArmRight", &"LegLeft", &"LegRight", &"Tail"
]
static func apply_to(root: Node3D, identity: int, npc_body := false) -> void:
var index := posmod(identity, MODELS.size())
var source := MODELS[index].instantiate()
var material := ShaderMaterial.new()
material.shader = CEL_SHADER
root.set_meta("animal_species", SPECIES[index])
for part in PARTS:
var source_mesh := source.find_child(String(part), true, false) as MeshInstance3D
var target_name := "MeshInstance3D" if npc_body and part == &"Body" else String(part)
var target := root.get_node(target_name) as MeshInstance3D
assert(source_mesh != null, "Missing Blender animation part: " + String(part))
target.mesh = source_mesh.mesh
target.material_override = material
source.free()
static func set_profession_color(root: Node3D, color: Color) -> void:
var body := root.get_node("MeshInstance3D") as MeshInstance3D
var material := body.material_override as ShaderMaterial
material.set_shader_parameter("coat_color", color)
material.set_shader_parameter("profession_tint", 0.28)
@@ -0,0 +1 @@
uid://dfb2a5yut45av
@@ -0,0 +1,23 @@
shader_type spatial;
render_mode specular_disabled;
// Opaque vertex-painted models: alpha is a clothing mask, never transparency.
uniform vec4 coat_color : source_color = vec4(1.0);
uniform float profession_tint : hint_range(0.0, 1.0) = 0.0;
uniform vec3 shadow_color : source_color = vec3(0.55, 0.64, 0.70);
uniform float band_softness : hint_range(0.005, 0.2) = 0.045;
void fragment() {
ALBEDO = mix(COLOR.rgb, COLOR.rgb * coat_color.rgb * 1.6, COLOR.a * profession_tint);
ROUGHNESS = 1.0;
SPECULAR = 0.0;
}
void light() {
float diffuse = dot(NORMAL, LIGHT);
float mid = smoothstep(-0.15 - band_softness, -0.15 + band_softness, diffuse);
float sun = smoothstep(0.50 - band_softness, 0.50 + band_softness, diffuse);
vec3 tone = mix(shadow_color * 0.36, vec3(0.70), mid);
tone = mix(tone, vec3(1.0, 0.97, 0.87), sun);
DIFFUSE_LIGHT += tone * ATTENUATION * LIGHT_COLOR / PI;
}
@@ -0,0 +1 @@
uid://bs1ip7dyu7jy8
@@ -0,0 +1,24 @@
shader_type spatial;
render_mode specular_disabled;
uniform vec3 albedo : source_color = vec3(0.8, 0.72, 0.52);
uniform float paint_strength : hint_range(0.0, 0.2) = 0.04;
varying vec3 paint_position;
void vertex() {
paint_position = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
}
void fragment() {
float brush = sin(paint_position.x * 2.1 + sin(paint_position.y * 3.0)) * sin(paint_position.z * 2.8 + paint_position.y);
ALBEDO = albedo * (1.0 + brush * paint_strength);
ROUGHNESS = 1.0;
}
void light() {
float ndl = dot(NORMAL, LIGHT);
float middle = smoothstep(-0.10, 0.03, ndl);
float sunny = smoothstep(0.48, 0.58, ndl);
vec3 shade = mix(vec3(0.26, 0.34, 0.40), vec3(0.74), middle);
shade = mix(shade, vec3(1.0, 0.97, 0.88), sunny);
DIFFUSE_LIGHT += shade * ATTENUATION * LIGHT_COLOR / PI;
}
@@ -0,0 +1 @@
uid://c8biomtfwtf4w