Compare commits
5 Commits
6aae41d36f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ff8858b19 | |||
| 70ee6feddf | |||
| ec16e2f2ca | |||
| 7899d36f03 | |||
| da3bed67a0 |
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# Woodland character kit
|
||||||
|
|
||||||
|
Original Blender geometry inspired by the supplied cat-traveler reference:
|
||||||
|
broad cheeks, relaxed almond eyes, whiskers, layered clothing, rolled cuffs,
|
||||||
|
boots, a scarf, and a travel pack with a bedroll. The reference image is not
|
||||||
|
embedded in the assets. Player and NPC visuals use this kit in the main game.
|
||||||
|
|
||||||
|
## Try and configure
|
||||||
|
|
||||||
|
Run `tools/CharacterWorkshop.tscn` in Godot 4.7 (F6), or:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/Applications/Godot.app/Contents/MacOS/Godot --path "$PWD" tools/CharacterWorkshop.tscn
|
||||||
|
```
|
||||||
|
|
||||||
|
Choose one of six presets, change the parts and proportions, drag the model to
|
||||||
|
turn it, scroll over the model to zoom, and enable the walking preview. **Save
|
||||||
|
profile** writes a `CharacterAppearanceProfile` `.tres` to the chosen file.
|
||||||
|
Assign that resource to `PlayerVisual.appearance` or `NpcVisual.appearance` in
|
||||||
|
the Inspector. The player defaults to `profiles/reference_cat.tres`. NPCs with
|
||||||
|
no authored override derive their appearance from their stable simulation ID.
|
||||||
|
The workshop is a separate authoring scene; it does not modify a game save.
|
||||||
|
|
||||||
|
| Control | Available values |
|
||||||
|
| --- | --- |
|
||||||
|
| Head and tail | Cat, fox, rabbit, badger, otter |
|
||||||
|
| Ears | Species default, or any of six ear pairs including lop ears |
|
||||||
|
| Height | 0.80–1.15 times the base height |
|
||||||
|
| Body fullness | 0–1 Blender `Stout` shape key |
|
||||||
|
| Head size / leg length / ear length | 0.88–1.12 / 0.85–1.15 / 0.65–1.20 |
|
||||||
|
| Clothing | Open vest, long coat, work tunic with apron |
|
||||||
|
| Arms | Relaxed or holding pack straps |
|
||||||
|
| Accessories | Scarf, spectacles, beret, brimmed hat, satchel, travel pack |
|
||||||
|
| Palette | Fur, muzzle, outfit, shirt, scarf, trousers, leather |
|
||||||
|
|
||||||
|
The workshop exposes the four main palette colors. All seven channels are
|
||||||
|
editable on the profile resource. Profession tint remains a separate, subtle
|
||||||
|
overlay driven by the actual NPC profession.
|
||||||
|
|
||||||
|
## Blender source and rebuilding
|
||||||
|
|
||||||
|
Open `art/characters/woodland_character_kit.blend`. The visible collection is an
|
||||||
|
assembled traveler; the hidden library meshes contain the editable building
|
||||||
|
blocks. Unhide a library object to edit it. Torso and wearable meshes have
|
||||||
|
`Basis` and `Stout` shape keys. Preview objects are copies of library objects.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/Applications/Blender.app/Contents/MacOS/Blender --background \
|
||||||
|
--python tools/art/build_character_kit.py
|
||||||
|
/Applications/Godot.app/Contents/MacOS/Godot --headless --editor --path "$PWD" --import
|
||||||
|
```
|
||||||
|
|
||||||
|
The reproducible Blender 5.1 generator rebuilds the source, GLB, and triangle
|
||||||
|
manifest. Regenerating replaces manual edits to those generated files. Commit
|
||||||
|
changes to the generator when they should survive a rebuild. Godot only imports
|
||||||
|
the portable GLB; `.gdignore` excludes the Blender authoring directory.
|
||||||
|
|
||||||
|
## Mesh and animation contract
|
||||||
|
|
||||||
|
`woodland_parts.glb` contains 31 named parts, each with one opaque surface. The
|
||||||
|
reference cat uses 10 surfaces and 20,070 base triangles before Godot's generated
|
||||||
|
LODs. The GLB is approximately 4.6 MiB. `mesh_budget.json` records each part's
|
||||||
|
triangle count, origin and shape-key support. Accessories are optional; only
|
||||||
|
selected meshes enter the live scene.
|
||||||
|
|
||||||
|
- `AnimalAppearance` caches the imported mesh library once. Characters share
|
||||||
|
`ArrayMesh` resources and have their own shader material and blend-shape
|
||||||
|
weights. No per-frame mesh rebuilding or CPU deformation is performed.
|
||||||
|
- Godot Y is up, +Z is forward. The existing body, head, arm, leg and tail node
|
||||||
|
paths remain animation sockets. Ear/glasses attachments follow the head;
|
||||||
|
scarf and bag attachments follow the torso. `Hair` is the optional hat socket.
|
||||||
|
- Fullness deforms the torso and worn gear together. Shoulder/hip spacing follows
|
||||||
|
it. Leg length raises the upper body while keeping the soles on the ground.
|
||||||
|
Gait uses configured rest heights; the hiker pose has restrained arm swing.
|
||||||
|
- Animation remains the existing rigid-part procedural gait. The kit does not
|
||||||
|
add a skeletal animation library, facial rig, cloth simulation, or physics
|
||||||
|
bodies. Changes to these appearance controls are cosmetic: collision,
|
||||||
|
navigation, interaction reach, authoritative positions, and saves retain
|
||||||
|
their existing contracts. Very unusual mixed parts may need art adjustment.
|
||||||
|
- `UV2.x` stores a discrete palette role (0 fixed vertex paint, 1 fur, 2 muzzle,
|
||||||
|
3 outfit, 4 linen, 5 scarf, 6 trousers, 7 leather, 8 fur markings, 9 outfit seams).
|
||||||
|
Preserve both UV layers and vertex colors on export. Do not regenerate UV2
|
||||||
|
lightmap coordinates. `character_cel.gdshader` supplies soft cel bands with
|
||||||
|
no texture sampling, transparency, outline pass, or screen-space effect.
|
||||||
|
|
||||||
|
## Verification and review
|
||||||
|
|
||||||
|
`tests/character_appearance_test.gd` checks the real imported geometry, palette
|
||||||
|
roles, independent morph weights, shared meshes, attachment reuse, all six
|
||||||
|
presets, extreme proportions, stable IDs/RNG isolation, gait rest heights, and
|
||||||
|
NPC cue clearance. Existing lifecycle scenarios cover profession, inventory,
|
||||||
|
death, and save/load behavior. Run the full repository quality gate before a
|
||||||
|
commit.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tools/quality.sh
|
||||||
|
/Applications/Godot.app/Contents/MacOS/Godot --path "$PWD" \
|
||||||
|
--rendering-driver metal --script tools/capture_character_kit.gd
|
||||||
|
/Applications/Godot.app/Contents/MacOS/Godot --path "$PWD" \
|
||||||
|
--rendering-method gl_compatibility --script tools/capture_character_kit.gd
|
||||||
|
/Applications/Godot.app/Contents/MacOS/Godot --path "$PWD" \
|
||||||
|
--rendering-driver metal --script tools/capture_character_kit.gd -- --world
|
||||||
|
```
|
||||||
|
|
||||||
|
The `docs/baselines/character_*.png` images are actual Godot renders. They show
|
||||||
|
the traveler, rear pack detail, preset lineup, workshop and main-world placement.
|
||||||
|
These local Apple M1 Max checks do not establish a frame-time budget for a
|
||||||
|
large crowd or a weaker GPU.
|
||||||
|
|
||||||
|
The workshop uses filmic tonemapping with a separately calibrated Compatibility
|
||||||
|
exposure to preserve cream-fabric highlights. This only stages the authoring
|
||||||
|
preview; the main world retains its existing day/night environment. Metal and
|
||||||
|
Compatibility images are separate visual reviews, not a pixel-parity claim.
|
||||||
|
The full Godot 4.7 gate passes 61 scenario runs (including 111 character checks)
|
||||||
|
and 182 GUT tests / 2,279 assertions. The native workshop controls and profile
|
||||||
|
export were also exercised directly.
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
{
|
||||||
|
"TorsoVest": {
|
||||||
|
"triangles": 2914,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.05,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": true
|
||||||
|
},
|
||||||
|
"TorsoCoat": {
|
||||||
|
"triangles": 3074,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.05,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": true
|
||||||
|
},
|
||||||
|
"TorsoTunic": {
|
||||||
|
"triangles": 1894,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.05,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": true
|
||||||
|
},
|
||||||
|
"ArmRelaxedLeft": {
|
||||||
|
"triangles": 996,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
-0.44,
|
||||||
|
1.3,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"ArmHikerLeft": {
|
||||||
|
"triangles": 2144,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
-0.44,
|
||||||
|
1.3,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"LegLeft": {
|
||||||
|
"triangles": 1074,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
-0.2,
|
||||||
|
0.61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"ArmRelaxedRight": {
|
||||||
|
"triangles": 996,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0.44,
|
||||||
|
1.3,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"ArmHikerRight": {
|
||||||
|
"triangles": 2144,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0.44,
|
||||||
|
1.3,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"LegRight": {
|
||||||
|
"triangles": 1074,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0.2,
|
||||||
|
0.61,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"HeadCat": {
|
||||||
|
"triangles": 6798,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.68,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"EarsCat": {
|
||||||
|
"triangles": 202,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
2.2,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"TailCat": {
|
||||||
|
"triangles": 584,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
0.8,
|
||||||
|
-0.22
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"HeadFox": {
|
||||||
|
"triangles": 6798,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.68,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"EarsFox": {
|
||||||
|
"triangles": 202,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
2.2,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"TailFox": {
|
||||||
|
"triangles": 636,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
0.8,
|
||||||
|
-0.22
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"HeadRabbit": {
|
||||||
|
"triangles": 6684,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.68,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"EarsRabbit": {
|
||||||
|
"triangles": 328,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
2.2,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"TailRabbit": {
|
||||||
|
"triangles": 440,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
0.8,
|
||||||
|
-0.22
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"HeadBadger": {
|
||||||
|
"triangles": 6684,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.68,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"EarsBadger": {
|
||||||
|
"triangles": 1456,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
2.2,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"TailBadger": {
|
||||||
|
"triangles": 248,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
0.8,
|
||||||
|
-0.22
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"HeadOtter": {
|
||||||
|
"triangles": 6684,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.68,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"EarsOtter": {
|
||||||
|
"triangles": 1456,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
2.2,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"TailOtter": {
|
||||||
|
"triangles": 360,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
0.8,
|
||||||
|
-0.22
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"EarsLop": {
|
||||||
|
"triangles": 328,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
2.2,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"Scarf": {
|
||||||
|
"triangles": 396,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.05,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": true
|
||||||
|
},
|
||||||
|
"BagTravel": {
|
||||||
|
"triangles": 2740,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.05,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": true
|
||||||
|
},
|
||||||
|
"BagSatchel": {
|
||||||
|
"triangles": 640,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.05,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": true
|
||||||
|
},
|
||||||
|
"HatBeret": {
|
||||||
|
"triangles": 880,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.9,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"HatBrim": {
|
||||||
|
"triangles": 976,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.9,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
},
|
||||||
|
"Spectacles": {
|
||||||
|
"triangles": 644,
|
||||||
|
"surfaces": 1,
|
||||||
|
"pivot": [
|
||||||
|
0,
|
||||||
|
1.68,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"stout_shape": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[gd_resource type="Resource" script_class="CharacterAppearanceProfile" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/presentation/CharacterAppearanceProfile.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
species = 3
|
||||||
|
height = 0.98
|
||||||
|
body_fullness = 1.0
|
||||||
|
leg_length = 0.9
|
||||||
|
outfit = 2
|
||||||
|
arm_pose = 0
|
||||||
|
bag = 0
|
||||||
|
hat = 1
|
||||||
|
scarf = false
|
||||||
|
fur_color = Color(0.79, 0.77, 0.71, 1)
|
||||||
|
outfit_color = Color(0.47, 0.5, 0.35, 1)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[gd_resource type="Resource" script_class="CharacterAppearanceProfile" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/presentation/CharacterAppearanceProfile.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
species = 1
|
||||||
|
height = 1.12
|
||||||
|
body_fullness = 0.15
|
||||||
|
leg_length = 1.12
|
||||||
|
outfit = 1
|
||||||
|
arm_pose = 0
|
||||||
|
hat = 2
|
||||||
|
bag = 1
|
||||||
|
fur_color = Color(0.72, 0.43, 0.24, 1)
|
||||||
|
outfit_color = Color(0.42, 0.49, 0.33, 1)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[gd_resource type="Resource" script_class="CharacterAppearanceProfile" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/presentation/CharacterAppearanceProfile.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
species = 2
|
||||||
|
ears = 4
|
||||||
|
height = 0.92
|
||||||
|
body_fullness = 0.8
|
||||||
|
ear_length = 1.12
|
||||||
|
outfit = 2
|
||||||
|
arm_pose = 0
|
||||||
|
bag = 0
|
||||||
|
scarf = false
|
||||||
|
fur_color = Color(0.80, 0.78, 0.73, 1)
|
||||||
|
outfit_color = Color(0.6, 0.4, 0.28, 1)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[gd_resource type="Resource" script_class="CharacterAppearanceProfile" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/presentation/CharacterAppearanceProfile.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
species = 4
|
||||||
|
height = 1.06
|
||||||
|
body_fullness = 0.42
|
||||||
|
head_size = 0.94
|
||||||
|
outfit = 0
|
||||||
|
arm_pose = 0
|
||||||
|
bag = 1
|
||||||
|
fur_color = Color(0.51, 0.42, 0.33, 1)
|
||||||
|
outfit_color = Color(0.41, 0.43, 0.54, 1)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[gd_resource type="Resource" script_class="CharacterAppearanceProfile" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/presentation/CharacterAppearanceProfile.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
species = 2
|
||||||
|
height = 0.84
|
||||||
|
body_fullness = 0.18
|
||||||
|
head_size = 1.08
|
||||||
|
leg_length = 0.92
|
||||||
|
outfit = 1
|
||||||
|
arm_pose = 0
|
||||||
|
bag = 1
|
||||||
|
spectacles = true
|
||||||
|
fur_color = Color(0.76, 0.72, 0.64, 1)
|
||||||
|
outfit_color = Color(0.57, 0.37, 0.31, 1)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_resource type="Resource" script_class="CharacterAppearanceProfile" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/presentation/CharacterAppearanceProfile.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://b4p2tfex61t8o"
|
||||||
|
path="res://.godot/imported/woodland_parts.glb-1394516718c8f9a3ee6122be9d99f45f.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/characters/woodland_parts.glb"
|
||||||
|
dest_files=["res://.godot/imported/woodland_parts.glb-1394516718c8f9a3ee6122be9d99f45f.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Jajce meadow flowers
|
||||||
|
|
||||||
|
Original mesh assets built for The Steward. Four small clumps give authored
|
||||||
|
meadow drifts a cream, rose, butter-yellow and lavender palette. Each contains
|
||||||
|
three stems, folded leaves and opaque petal geometry with painted vertex colors.
|
||||||
|
There are no external textures, dependencies, collision shapes or gameplay IDs.
|
||||||
|
|
||||||
|
Editable sources live in `art/meadow/`, outside Godot's import tree. Rebuild with
|
||||||
|
Blender 5.1 or later:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
Blender --background --python tools/art/build_meadow_flowers.py
|
||||||
|
```
|
||||||
|
|
||||||
|
`mesh_budget.json` records exported triangle/surface/file-size counts. Runtime
|
||||||
|
`PainterlyMeadow` uses one MultiMesh per species and its own soft-lit wind shader;
|
||||||
|
the GLB material preserves the original palette when opened in other tools.
|
||||||
|
|
||||||
|
Godot 4.7 Compatibility requires explicit white MultiMesh instance colors when
|
||||||
|
the custom instance buffer is enabled. `PainterlyMeadow` supplies that neutral
|
||||||
|
color so both Compatibility and Forward+ preserve the painted GLB palette.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://cnoywbu65c0hg"
|
||||||
|
path="res://.godot/imported/cream_daisy.glb-7a51f417838affb7d915e8a81cdabcc9.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/meadow/cream_daisy.glb"
|
||||||
|
dest_files=["res://.godot/imported/cream_daisy.glb-7a51f417838affb7d915e8a81cdabcc9.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dur42glc0ybm3"
|
||||||
|
path="res://.godot/imported/lavender_spire.glb-ca4666e5e14c84270d4621d49918d5e7.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/meadow/lavender_spire.glb"
|
||||||
|
dest_files=["res://.godot/imported/lavender_spire.glb-ca4666e5e14c84270d4621d49918d5e7.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"cream_daisy": {
|
||||||
|
"triangles": 180,
|
||||||
|
"surfaces": 1,
|
||||||
|
"flower_stems": 3,
|
||||||
|
"bytes": 19896
|
||||||
|
},
|
||||||
|
"pink_cosmos": {
|
||||||
|
"triangles": 171,
|
||||||
|
"surfaces": 1,
|
||||||
|
"flower_stems": 3,
|
||||||
|
"bytes": 18888
|
||||||
|
},
|
||||||
|
"yellow_buttercup": {
|
||||||
|
"triangles": 153,
|
||||||
|
"surfaces": 1,
|
||||||
|
"flower_stems": 3,
|
||||||
|
"bytes": 16840
|
||||||
|
},
|
||||||
|
"lavender_spire": {
|
||||||
|
"triangles": 360,
|
||||||
|
"surfaces": 1,
|
||||||
|
"flower_stems": 3,
|
||||||
|
"bytes": 40444
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dqksrghyqlfec"
|
||||||
|
path="res://.godot/imported/pink_cosmos.glb-f2a2c19fa8aeebec56761be5c75e2426.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/meadow/pink_cosmos.glb"
|
||||||
|
dest_files=["res://.godot/imported/pink_cosmos.glb-f2a2c19fa8aeebec56761be5c75e2426.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://urkk7ybwrnv7"
|
||||||
|
path="res://.godot/imported/yellow_buttercup.glb-88c3511b4d2c884beab5a1c921cdc3cb.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/meadow/yellow_buttercup.glb"
|
||||||
|
dest_files=["res://.godot/imported/yellow_buttercup.glb-88c3511b4d2c884beab5a1c921cdc3cb.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Painterly valley assets
|
||||||
|
|
||||||
|
Original broadleaf and evergreen meshes created in Blender for The Steward.
|
||||||
|
Scalloped crowns, painted vertex variation, crooked forks and root flares replace
|
||||||
|
the primitive tree silhouettes. Editable sources are in `art/painterly/`;
|
||||||
|
`.gdignore` keeps Blender outside Godot's runtime import dependencies. The two
|
||||||
|
portable GLBs use opaque geometry without textures or alpha cards.
|
||||||
|
|
||||||
|
## Rebuild and mesh budget
|
||||||
|
|
||||||
|
From the project root, using Blender 5.1 and Godot 4.7:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
"$BLENDER_BIN" --background --python tools/art/build_painterly_trees.py
|
||||||
|
"$GODOT_BIN" --headless --editor --path "$PWD" --import
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the isolated Godot profile configured by `tools/quality.sh` for headless
|
||||||
|
validation. Runtime consumes the GLBs directly; no mesh-extraction step is
|
||||||
|
needed. Preserve their `.import` files when updating exports.
|
||||||
|
|
||||||
|
| Geometry | Triangles | Surfaces |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| Shared broadleaf crown | 1,236 | 1 |
|
||||||
|
| Joined broadleaf trunk, forks and roots | 406 | 1 |
|
||||||
|
| Complete broadleaf: four crowns plus trunk | 5,350 | 5 |
|
||||||
|
| Complete conifer: boughs plus trunk | 1,580 | 2 |
|
||||||
|
|
||||||
|
`tree_mesh_budget.json` records these export counts. The broadleaf GLB is
|
||||||
|
45,676 bytes; the conifer GLB is 40,836 bytes and stands 3.10 m before placement
|
||||||
|
scaling. A separate temporary rebuild with Blender 5.1.2 produced byte-identical
|
||||||
|
copies of both GLBs and the budget JSON. This establishes generator
|
||||||
|
reproducibility with that Blender version, not identical `.blend` serialization
|
||||||
|
across versions.
|
||||||
|
|
||||||
|
`StylizedTree` shares imported meshes and isolates crown tint/wind materials.
|
||||||
|
Its four direct `Canopy*` children retain the existing thinning and stump
|
||||||
|
presentation contract. `PainterlyConifer` shares imported bough/trunk meshes.
|
||||||
|
Both use the same world-direction breeze and explicit painted bark colors;
|
||||||
|
neither adds simulation state, collision or per-frame GDScript work.
|
||||||
|
|
||||||
|
## Painted sky provenance
|
||||||
|
|
||||||
|
`painted_sky.png` is an original 1774 × 887 image generated with the built-in
|
||||||
|
ImageGen tool, rather than a CLI generator. The prompt asks for a painted
|
||||||
|
cumulus panorama with warm ivory cloud tops, cool turquoise cloud shadows and
|
||||||
|
a clear blue summer atmosphere. Film references informed the color and mood;
|
||||||
|
no external film image was copied into the texture. The exact generation
|
||||||
|
prompt is recorded in [sky_prompt.md](sky_prompt.md).
|
||||||
|
|
||||||
|
`storybook_sky.gdshader` maps the panorama's horizon at 69% of its height onto
|
||||||
|
the upper hemisphere, repeats the painted band twice around the azimuth to
|
||||||
|
retain cloud proportions, blends its seam/edge into haze, and follows the existing
|
||||||
|
day/night palette. The image supplies the sky backdrop only; it does not paint
|
||||||
|
terrain or create volumetric clouds.
|
||||||
|
|
||||||
|
Final renderer QA and the complete project quality gate belong to the
|
||||||
|
integrated environment review. See the [presentation guide](../../docs/FEATURE_PRESENTATION_PERFORMANCE.md)
|
||||||
|
for current verification evidence and remaining hardware limits.
|
||||||
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,41 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dmkubk6dhhyis"
|
||||||
|
path.s3tc="res://.godot/imported/painted_sky.png-9888802278aff2f83effdab2649f1f05.s3tc.ctex"
|
||||||
|
metadata={
|
||||||
|
"imported_formats": ["s3tc_bptc"],
|
||||||
|
"vram_texture": true
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/painterly/painted_sky.png"
|
||||||
|
dest_files=["res://.godot/imported/painted_sky.png-9888802278aff2f83effdab2649f1f05.s3tc.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=2
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=true
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=0
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://clcvatvbyixma"
|
||||||
|
path="res://.godot/imported/painterly_conifer.glb-596d48094a6a072cfc85374d478c7bbe.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/painterly/painterly_conifer.glb"
|
||||||
|
dest_files=["res://.godot/imported/painterly_conifer.glb-596d48094a6a072cfc85374d478c7bbe.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://blu5r5vupqvpd"
|
||||||
|
path="res://.godot/imported/painterly_tree.glb-12cb20acdabe08bb0fe06cba12958067.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/painterly/painterly_tree.glb"
|
||||||
|
dest_files=["res://.godot/imported/painterly_tree.glb-12cb20acdabe08bb0fe06cba12958067.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Painted sky provenance
|
||||||
|
|
||||||
|
Created for this project with the built-in imagegen tool on 2026-09-05.
|
||||||
|
Original output: 1774 × 887 PNG, copied without pixel editing to
|
||||||
|
`assets/painterly/painted_sky.png`. The sky shader samples the upper portion
|
||||||
|
and blends its horizon into the simulation-driven day/night palette.
|
||||||
|
No film frame or downloaded artwork is used as a runtime texture.
|
||||||
|
|
||||||
|
## Generation prompt
|
||||||
|
|
||||||
|
Use case: stylized-concept. Asset type: original 360-degree panoramic sky
|
||||||
|
texture for a Godot game, 2:1 equirectangular landscape, ideally 3072x1536.
|
||||||
|
Create ONLY SKY, absolutely no ground, hills, buildings, ocean, objects, text
|
||||||
|
or sun disk. Art direction: exquisitely hand-painted Japanese animation
|
||||||
|
background, Studio Ghibli inspired summer pastoral fantasy, gouache brush
|
||||||
|
texture, saturated clear turquoise-blue atmosphere, towering luminous cumulus
|
||||||
|
clouds with creamy warm sunlit edges, intricate scalloped billowing forms,
|
||||||
|
pale cyan and slate blue softly painted shadow volumes. Composition is a full
|
||||||
|
spherical sky environment texture: upper half of image shows the upward
|
||||||
|
hemisphere, zenith at top is mostly deep clean blue, large beautiful cumulus
|
||||||
|
towers occupy the upper middle band between y=15% and y=44%, and the horizon
|
||||||
|
at exactly image vertical halfway is uniform pale atmospheric aqua. Lower
|
||||||
|
half is entirely an unobtrusive smooth pale aqua color gradient, with NO
|
||||||
|
landscape. Cloud coverage about 40% of upper hemisphere, generous varied blue
|
||||||
|
gaps, asymmetrical naturally spaced cloud masses. Left and right edges must
|
||||||
|
wrap seamlessly in color and shape, keep thin strips at far left/right clear
|
||||||
|
sky to avoid seam. No photographic cloud noise, no 3D render or vector
|
||||||
|
outlines, no flat cartoon circles. Painterly refined layered brushwork like a
|
||||||
|
cinematic animated film background, not a flat gradient. Use only original
|
||||||
|
cloud shapes; no copied film imagery.
|
||||||
|
|
||||||
|
The returned image does not exactly meet the requested resolution or horizon
|
||||||
|
position. The runtime shader uses its actual dimensions and remaps the painted
|
||||||
|
horizon at 69% image height. Blender mesh assets are deterministic rebuilds;
|
||||||
|
this generated image is a pinned art asset, not a deterministic generator output.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"Canopy": {
|
||||||
|
"triangles": 1236,
|
||||||
|
"surfaces": 1,
|
||||||
|
"vertices": 712
|
||||||
|
},
|
||||||
|
"Trunk": {
|
||||||
|
"triangles": 406,
|
||||||
|
"surfaces": 1,
|
||||||
|
"vertices": 221
|
||||||
|
},
|
||||||
|
"runtime_tree": {
|
||||||
|
"triangles": 5350,
|
||||||
|
"surfaces": 5,
|
||||||
|
"shared_meshes": 2
|
||||||
|
},
|
||||||
|
"glb_bytes": 45676,
|
||||||
|
"conifer": {
|
||||||
|
"triangles": 1580,
|
||||||
|
"surfaces": 2,
|
||||||
|
"height_m": 3.1,
|
||||||
|
"glb_bytes": 40836
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Woodland storybook art
|
||||||
|
|
||||||
|
Original Blender models for The Steward's warm, painted animation direction.
|
||||||
|
The runtime cast is a fox traveler, rabbit gardener, bespectacled badger and
|
||||||
|
otter in oversized coats, scarves, boots and satchels. These are original
|
||||||
|
characters, not models of existing film characters.
|
||||||
|
|
||||||
|
`art/storybook/` contains the editable Blender 5.1 sources. Its `.gdignore`
|
||||||
|
keeps Blender out of Godot's runtime import dependency chain. This directory
|
||||||
|
contains portable GLBs and the extracted grass mesh consumed by Terrain3D.
|
||||||
|
|
||||||
|
## Rebuild
|
||||||
|
|
||||||
|
From the project root, with Blender and Godot 4.7 installed:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
"$BLENDER_BIN" --background --python tools/art/build_storybook_assets.py
|
||||||
|
"$GODOT_BIN" --headless --editor --path "$PWD" --import
|
||||||
|
"$GODOT_BIN" --headless --path "$PWD" --script tools/import_storybook_grass.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
On macOS the binaries used for this pass were
|
||||||
|
`/Applications/Blender.app/Contents/MacOS/Blender` and
|
||||||
|
`/Applications/Godot.app/Contents/MacOS/Godot`. Headless CI should use the
|
||||||
|
isolated profile configured by `tools/quality.sh`.
|
||||||
|
|
||||||
|
## Mesh and material contract
|
||||||
|
|
||||||
|
- The seven-blade meadow tuft has **35 triangles**, one surface, rooted Y=0,
|
||||||
|
and UV.y=0 at each root / 1 at each tip. Wind and actor bending run on the
|
||||||
|
GPU. The material is opaque and double-sided, with no alpha cards, texture
|
||||||
|
lookups, per-blade nodes, shadow casting, or outline pass.
|
||||||
|
- Terrain3D owns the existing nine camera-local emitter cells: 10,404 tuft
|
||||||
|
slots on High, 5,184 on Balanced, hidden and stopped on Low. Terrain masks
|
||||||
|
and distance fading further reduce visible grass. Four scene-derived path
|
||||||
|
clearings collapse blades around the walking strips. Changing density does
|
||||||
|
not alter the terrain, navigation, resource state or collision.
|
||||||
|
- Each animal has eight mesh surfaces, **7,884–10,050 triangles** total, and
|
||||||
|
vertex colors. `mesh_budget.json` is emitted by the Blender generator.
|
||||||
|
Vertex alpha is a clothing tint mask; the cel shader never writes ALPHA.
|
||||||
|
- `AnimalAppearance` derives NPC species from stable ID modulo four; the
|
||||||
|
player is a fox. It shares imported meshes, creates an isolated material
|
||||||
|
per actor and tints clothing from the existing profession definition.
|
||||||
|
`Body`, `Head`, `Hair`, four limbs and `Tail` preserve the visual controllers'
|
||||||
|
animation slots. Shoulder/hip pivots support the existing velocity-driven
|
||||||
|
gait. Ears, face details, boots and scarves are joined into those surfaces.
|
||||||
|
- Inventory sacks, carried logs, profession tools, task glyphs, trust reactions,
|
||||||
|
death and player sword cues remain controlled by their existing state paths.
|
||||||
|
Animal species here is appearance only: no animal simulation classification,
|
||||||
|
RNG calls, saved fields, migration or gameplay mechanics are introduced.
|
||||||
|
|
||||||
|
## Review captures
|
||||||
|
|
||||||
|
```sh
|
||||||
|
"$GODOT_BIN" --path "$PWD" --script tools/capture_storybook.gd -- --cast
|
||||||
|
"$GODOT_BIN" --path "$PWD" --script tools/capture_storybook.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
These require a real renderer. The first writes `storybook_cast.png`; the
|
||||||
|
second captures the running main scene at a controlled daytime presentation,
|
||||||
|
then writes gameplay/valley PNGs and a small profile sample to `docs/baselines/`.
|
||||||
|
It pauses simulation only in the capture process and does not save anything.
|
||||||
|
The gameplay camera defaults remain unchanged. The matched environment camera
|
||||||
|
can also be captured with `tools/capture_jajce_lookdev.gd -- --hide-debug-labels
|
||||||
|
--output=res://docs/baselines/storybook_after.png`.
|
||||||
|
|
||||||
|
Profile samples are local wall-frame latency at a static camera with six NPC
|
||||||
|
visuals, including vsync. They are not isolated GPU timings or weak-PC proof.
|
||||||
|
|
||||||
|
The full macOS quality gate passed on Godot 4.7, including all scenarios,
|
||||||
|
Compatibility profile checks and 180 GUT tests / 2,258 assertions. Native
|
||||||
|
Metal captures were visually reviewed. The character shader also rendered on
|
||||||
|
Compatibility, whose color/tonemapping differs substantially from Metal; the
|
||||||
|
approved color reference is `storybook_cast.png` (Metal).
|
||||||
|
|
||||||
|
Native Metal shutdown reports one `ParticlesShaderRD` / material Shader RID
|
||||||
|
retained at exit. The identical diagnostic was reproduced by importing and
|
||||||
|
running untouched commit `6aae41d` in a separate temporary directory. The
|
||||||
|
existing headless gate allowlist was not changed. Import/startup diagnostics
|
||||||
|
and the baseline comparison are in ignored `logs/quality/storybook-*.log`.
|
||||||
|
|
||||||
|
Shader interfaces follow the official [Godot 4.7 spatial shader contract](https://docs.godotengine.org/en/4.7/tutorials/shaders/shader_reference/spatial_shader.html).
|
||||||
|
The exporter uses Blender's [glTF export API](https://docs.blender.org/api/main/bpy.ops.export_scene.html).
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://buehm0sth6etc"
|
||||||
|
path="res://.godot/imported/badger.glb-4a20c61fae6d6e8d68c151cba8d056d5.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/storybook/badger.glb"
|
||||||
|
dest_files=["res://.godot/imported/badger.glb-4a20c61fae6d6e8d68c151cba8d056d5.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://cbne2ol0h0uku"
|
||||||
|
path="res://.godot/imported/fox.glb-972d78ae35ada569a710c17f00c10a3e.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/storybook/fox.glb"
|
||||||
|
dest_files=["res://.godot/imported/fox.glb-972d78ae35ada569a710c17f00c10a3e.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://bd6mrof50g4wn"
|
||||||
|
path="res://.godot/imported/meadow_grass.glb-0321cd60906212db637f52f1f1493ea1.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/storybook/meadow_grass.glb"
|
||||||
|
dest_files=["res://.godot/imported/meadow_grass.glb-0321cd60906212db637f52f1f1493ea1.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"fox": {
|
||||||
|
"triangles": 7884,
|
||||||
|
"surfaces": 8,
|
||||||
|
"bytes": 271636
|
||||||
|
},
|
||||||
|
"rabbit": {
|
||||||
|
"triangles": 9060,
|
||||||
|
"surfaces": 8,
|
||||||
|
"bytes": 314636
|
||||||
|
},
|
||||||
|
"badger": {
|
||||||
|
"triangles": 10050,
|
||||||
|
"surfaces": 8,
|
||||||
|
"bytes": 345484
|
||||||
|
},
|
||||||
|
"otter": {
|
||||||
|
"triangles": 9088,
|
||||||
|
"surfaces": 8,
|
||||||
|
"bytes": 314108
|
||||||
|
},
|
||||||
|
"meadow_grass": {
|
||||||
|
"triangles": 35,
|
||||||
|
"surfaces": 1,
|
||||||
|
"bytes": 3648
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://6f01q2ru2sq4"
|
||||||
|
path="res://.godot/imported/otter.glb-92d54495ed52927d77a0a5213ac92330.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/storybook/otter.glb"
|
||||||
|
dest_files=["res://.godot/imported/otter.glb-92d54495ed52927d77a0a5213ac92330.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://bywpllx00b6aq"
|
||||||
|
path="res://.godot/imported/rabbit.glb-ed7bf640394b68863b9dc6437e0f1808.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/storybook/rabbit.glb"
|
||||||
|
dest_files=["res://.godot/imported/rabbit.glb-ed7bf640394b68863b9dc6437e0f1808.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
@@ -38,6 +38,7 @@ when extending the game.
|
|||||||
| Add caravans, routes, scheduled work, or regional saves | [Regional simulation and persistence](FEATURE_REGIONAL_SIMULATION_AND_PERSISTENCE.md) | `simulation/regional/`, `simulation/state/*StateRecord.gd` |
|
| Add caravans, routes, scheduled work, or regional saves | [Regional simulation and persistence](FEATURE_REGIONAL_SIMULATION_AND_PERSISTENCE.md) | `simulation/regional/`, `simulation/state/*StateRecord.gd` |
|
||||||
| Add an enemy or animal using an existing behavior | [Entity families](FEATURE_ENTITIES_COMBAT_ANIMALS.md) | `CombatantFactory.gd`, `AnimalFactory.gd`, shared visual roots |
|
| Add an enemy or animal using an existing behavior | [Entity families](FEATURE_ENTITIES_COMBAT_ANIMALS.md) | `CombatantFactory.gd`, `AnimalFactory.gd`, shared visual roots |
|
||||||
| Change rendering or support weaker PCs | [Presentation and performance](FEATURE_PRESENTATION_PERFORMANCE.md) | `world/jajce/jajce_world.gd`, `world/presentation/` |
|
| Change rendering or support weaker PCs | [Presentation and performance](FEATURE_PRESENTATION_PERFORMANCE.md) | `world/jajce/jajce_world.gd`, `world/presentation/` |
|
||||||
|
| Author humanoid animal characters or combine clothing and body shapes | [Modular character kit](../assets/characters/README.md) | `tools/CharacterWorkshop.tscn`, `CharacterAppearanceProfile.gd`, `tools/art/build_character_kit.py` |
|
||||||
| Change a saved field or migration | [Save schema and migrations](FEATURE_SAVE_SCHEMA_AND_MIGRATIONS.md) | `SimulationStateRecord.gd`, nested state records, `SaveSlotStore.gd` |
|
| Change a saved field or migration | [Save schema and migrations](FEATURE_SAVE_SCHEMA_AND_MIGRATIONS.md) | `SimulationStateRecord.gd`, nested state records, `SaveSlotStore.gd` |
|
||||||
| Add a regression, benchmark, or run the gate | [Testing and benchmarks](FEATURE_TESTING_AND_BENCHMARKS.md) | `tests/`, `tools/`, `docs/benchmarks/` |
|
| Add a regression, benchmark, or run the gate | [Testing and benchmarks](FEATURE_TESTING_AND_BENCHMARKS.md) | `tests/`, `tools/`, `docs/benchmarks/` |
|
||||||
|
|
||||||
|
|||||||
@@ -38,19 +38,92 @@ spawn is a next integration.
|
|||||||
`JajceWorld` owns reversible High, Balanced, and Low presentation profiles with
|
`JajceWorld` owns reversible High, Balanced, and Low presentation profiles with
|
||||||
instance-local mutable environment/grass resources and viewport-scale ownership.
|
instance-local mutable environment/grass resources and viewport-scale ownership.
|
||||||
|
|
||||||
| Profile | 3D scale | Shadow distance | Volumetric/glow | Grass |
|
| Profile | 3D scale / MSAA | Shadow distance | Volumetric/glow | Grass |
|
||||||
| --- | ---: | ---: | --- | --- |
|
| --- | ---: | ---: | --- | --- |
|
||||||
| High | authored | authored (currently 180 m) | authored when renderer supports it | authored density/interactors |
|
| High | authored / authored 4× | authored (currently 180 m) | authored when renderer supports it | authored density/interactors |
|
||||||
| Balanced (default) | 0.85 cap | 120 m cap | authored ordinary lookdev; unsupported volumetric disabled | reduced spacing/interactors/update rate |
|
| Balanced (default) | 0.85 cap / 2× cap | 120 m cap | authored ordinary lookdev; unsupported volumetric disabled | reduced spacing/interactors/update rate |
|
||||||
| Low | 0.70 cap | 80 m, orthogonal | volumetric/glow/adjustment disabled; ordinary fog retained | hidden/stopped, emitters off, shader interactors zero |
|
| Low | 0.70 cap / off | 80 m, orthogonal | volumetric/glow/adjustment disabled; ordinary fog retained | hidden/stopped, emitters off, shader interactors zero |
|
||||||
|
|
||||||
UI remains at native resolution. High/Low switches restore captured authored
|
UI remains at native resolution. High/Low switches restore captured authored
|
||||||
values; viewport scale restoration is guarded by the current quality owner so a
|
values; viewport scale restoration is guarded by the current quality owner so a
|
||||||
second world cannot clobber the active owner. Compatibility/mobile feature
|
second world cannot clobber the active owner. Compatibility/mobile feature
|
||||||
selection never forces unsupported volumetric fog on.
|
selection never forces unsupported volumetric fog on.
|
||||||
|
|
||||||
|
## Grass trails and bush contact
|
||||||
|
|
||||||
|
`GrassInteractionController` samples the existing `grass_interactors` group:
|
||||||
|
player, NPCs, hostile creatures and animals. The player keeps one slot; other
|
||||||
|
visible actors are ranked by camera distance within the loaded actor container.
|
||||||
|
High updates at 0.08 seconds with eight actors; Balanced uses 0.12 seconds and
|
||||||
|
four. Low stops updates and immediately releases both grass and bushes.
|
||||||
|
|
||||||
|
`FoliageTrailMap` maintains a disposable 128×128 RGBA float texture covering a
|
||||||
|
64 m square around the camera. Swept footprints part grass to either side and
|
||||||
|
lower its tips; the field recovers over eight seconds after contact. Sparse
|
||||||
|
history is clipped to the moving window. The 256 KiB texture is reused with
|
||||||
|
[`ImageTexture.update`](https://docs.godotengine.org/en/stable/classes/class_imagetexture.html#class-imagetexture-method-update),
|
||||||
|
at most 3.125 MiB/s of texture data at the High update rate. The vertex shader
|
||||||
|
adds one field lookup per grass vertex; there are no blade collision bodies,
|
||||||
|
per-blade scripts, render targets or added grass draw calls.
|
||||||
|
|
||||||
|
`StylizedBerryPatch` shares the field and contact positions between its leaf
|
||||||
|
and berry MultiMeshes. Both bend in world space around the planted bush root,
|
||||||
|
including under rotated/scaled parents, and spring back faster than grass.
|
||||||
|
The five village berry placeholders now use this same amount-aware presentation
|
||||||
|
as the existing riverbank bush. Contact does not change berries, resource
|
||||||
|
amounts, navigation, targets or collision geometry.
|
||||||
|
|
||||||
|
The controller clears history after `SimulationManager.state_restored`, on
|
||||||
|
quality changes and on world disposal. Missing/recreated actors, teleports over
|
||||||
|
3 m between samples and update gaps over 0.5 seconds start a new footprint rather
|
||||||
|
than a connecting trail. Actor foot height gates deformation so bridge traffic
|
||||||
|
does not flatten grass below. Trails are visual only and never enter saves.
|
||||||
|
The exported actor, primary actor, foliage and simulation paths can be rebound
|
||||||
|
when placing the controller in another loaded scene container.
|
||||||
|
|
||||||
|
`tests/foliage_interaction_test.gd` checks sweep direction, recovery, teleport
|
||||||
|
rejection, camera movement, world isolation, restore/quality reset, the primary
|
||||||
|
actor slot and shared leaf/fruit bindings. Run `tools/capture_foliage.gd` with
|
||||||
|
the native renderer for before/contact/trail/recovery images and controller
|
||||||
|
timing in `docs/baselines/foliage_*`. That capture freezes simulation and wind, and moves
|
||||||
|
the player presentation through the actual meadow, checking the state checksum
|
||||||
|
is unchanged. CPU update timing is not a GPU or weak-PC frame-rate guarantee.
|
||||||
|
Native Metal and Compatibility both render the effect; existing particle-shader
|
||||||
|
shutdown warnings (and four Compatibility texture leaks) also reproduce in the
|
||||||
|
untouched pre-art baseline. The headless quality gate retains its exact allowlist.
|
||||||
|
|
||||||
## Runtime hot paths already bounded
|
## Runtime hot paths already bounded
|
||||||
|
|
||||||
|
The woodland storybook art pass adds portable Blender animal meshes and an
|
||||||
|
opaque 35-triangle, seven-blade grass tuft. The existing camera-local grass
|
||||||
|
field and quality controls own its instance budget; four loaded path strips
|
||||||
|
provide disposable grass-clearance data after material isolation. Animal
|
||||||
|
appearance is derived from stable NPC IDs, shares mesh resources, and retains
|
||||||
|
profession tint, velocity-driven motion, inventory cues and death through the
|
||||||
|
existing presentation controllers. No saved state or navigation changes.
|
||||||
|
|
||||||
|
Characters and buildings use soft cel bands; tree crowns retain the shared
|
||||||
|
breeze with painted color patches. A painted cloud panorama, cooler
|
||||||
|
ambient fill and warm sunlight follow the simulation day/night clock. The
|
||||||
|
cloud shader adds no volumetric pass, outline pass or screen-space postprocess.
|
||||||
|
See [the asset contract and rebuild commands](../assets/storybook/README.md).
|
||||||
|
|
||||||
|
The current humanoid cast uses the [modular woodland character kit](../assets/characters/README.md):
|
||||||
|
31 Blender parts, five animal heads, six authored examples and independently
|
||||||
|
configurable proportions, outfits and accessories. `CharacterAppearanceProfile`
|
||||||
|
resources are cosmetic. Meshes are shared; `Stout` morph weights and palette
|
||||||
|
materials are per actor. The reference traveler has 20,070 base triangles across
|
||||||
|
10 opaque surfaces; Godot imports LODs. The standalone
|
||||||
|
`tools/CharacterWorkshop.tscn` previews and saves profiles. Existing motion,
|
||||||
|
profession/inventory/death cues, and shader grass interaction remain owned by
|
||||||
|
their existing controllers. The appearance scenario verifies imported parts,
|
||||||
|
independent shape weights, stable identity, extreme proportions and cue clearance.
|
||||||
|
|
||||||
|
`docs/baselines/storybook_*.png` records the native Metal review. The accompanying
|
||||||
|
`storybook_render_metrics.json` records a bounded local 1600×900 static-camera
|
||||||
|
sample on Apple M1 Max with six NPCs and High/Balanced/Low settings. Wall-frame
|
||||||
|
latency includes vsync and does not close the weak-PC evidence gap below.
|
||||||
|
|
||||||
- Player interaction and villager inspection probes are throttled/event-driven;
|
- Player interaction and villager inspection probes are throttled/event-driven;
|
||||||
they no longer rebuild all candidate data every rendered frame.
|
they no longer rebuild all candidate data every rendered frame.
|
||||||
- `LoadedResourceSpatialIndex` bounds finite-resource discovery.
|
- `LoadedResourceSpatialIndex` bounds finite-resource discovery.
|
||||||
@@ -99,3 +172,85 @@ ledger, not brittle cross-machine CI thresholds.
|
|||||||
|
|
||||||
See [the benchmark ledger](benchmarks/README.md), [regional contract](REGIONAL_SIMULATION.md),
|
See [the benchmark ledger](benchmarks/README.md), [regional contract](REGIONAL_SIMULATION.md),
|
||||||
and [the build-in-public visual plan](BUILD_IN_PUBLIC_PLAN.md).
|
and [the build-in-public visual plan](BUILD_IN_PUBLIC_PLAN.md).
|
||||||
|
|
||||||
|
## Painterly environment pass — September 2026
|
||||||
|
|
||||||
|
The reference study highlighted flat cloud bands, primitive tree silhouettes,
|
||||||
|
sparse ground cover, opaque green water, and a sun arc offset from daytime
|
||||||
|
brightness. The current pass addresses those through shared presentation assets:
|
||||||
|
|
||||||
|
- The WorldEnvironment uses cool blue ambient fill, warm sunlight, restrained
|
||||||
|
glow and ordinary aerial fog. The solar arc now peaks at noon and the warm
|
||||||
|
transitions overlap dawn/dusk. An original painted cloud panorama supplies
|
||||||
|
layered cumulus shapes; a static sky shader remaps the artwork, blends its
|
||||||
|
seam/horizon, and tints it from the simulation clock. It adds no TIME-driven
|
||||||
|
cubemap updates or volumetric cloud pass. Generation provenance and the exact
|
||||||
|
prompt are in [the asset guide](../assets/painterly/README.md).
|
||||||
|
- Original Blender broadleaf crowns/trunks and layered conifers replace the
|
||||||
|
primitive foliage. Seventeen decorative edge trees form a woodland backdrop.
|
||||||
|
The four village tree placeholders now use `HarvestableTreePresentation`,
|
||||||
|
retaining their existing IDs, amounts, interaction points and resource count.
|
||||||
|
Full/low/depleted state and restored crowns still derive from real amounts.
|
||||||
|
- `PainterlyMeadow` creates at most 1,680 decorative flower clumps in four
|
||||||
|
MultiMesh batches, with cream daisies, pink cosmos, buttercups and lavender.
|
||||||
|
Local seeded drifts follow terrain and exclude houses, paths, work sites and
|
||||||
|
resource approach points. High/Balanced/Low expose at most 1,680/1,092/536
|
||||||
|
clumps, with shorter distance fading in cheaper profiles. Flowers share the
|
||||||
|
prevailing breeze; the existing grass and berry contact field remains intact.
|
||||||
|
Explicit white instance colors preserve the painted GLB palette when custom
|
||||||
|
instance data is enabled in Compatibility; native review caught and verified
|
||||||
|
that renderer-specific regression.
|
||||||
|
Flowers have no collision, harvesting, actor-contact deformation or saved
|
||||||
|
state. See [mesh sources and budgets](../assets/meadow/README.md).
|
||||||
|
- The blue/turquoise river follows actual centerline terrain heights, keeps
|
||||||
|
level cross-sections, and clips dry edges at the shoreline. The pool and its
|
||||||
|
outlet meet without overlapping coplanar transparent sheets. Broken foam,
|
||||||
|
subtle flowing strokes and painted reflection patches replace the hard ring
|
||||||
|
and featureless surface. These reflections are stylized procedural patches
|
||||||
|
plus ordinary sky specular response, not exact reflected scene geometry,
|
||||||
|
screen-space reflections, refraction or depth sampling. Water obeys scene
|
||||||
|
lighting at night and adds no per-frame scripts.
|
||||||
|
- MSAA follows the existing viewport ownership rules: High preserves authored
|
||||||
|
4×, Balanced caps at 2×, and Low disables it. Closing an inactive world cannot
|
||||||
|
overwrite the active world's antialiasing or an external viewport override.
|
||||||
|
|
||||||
|
The editable Blender sources live under `art/painterly/` and `art/meadow/`,
|
||||||
|
protected by `.gdignore`; runtime consumes portable GLBs. The broadleaf is
|
||||||
|
5,350 triangles / five surfaces and the conifer is 1,580 / two. All flower
|
||||||
|
clumps together are capped at 362,880 triangles / four surfaces before
|
||||||
|
quality and distance reduction. No scene collision, navigation bake, terrain
|
||||||
|
heightmap, simulation rule or save schema was changed.
|
||||||
|
|
||||||
|
`tools/capture_painterly_environment.gd -- --label=after` captures the running
|
||||||
|
main scene with paused simulation, fixed daytime presentation and hidden HUD,
|
||||||
|
including the normal gameplay camera, valley, lower village view and night.
|
||||||
|
The valley/near-village `painterly_before_*` and `painterly_after_*`
|
||||||
|
images are matched cameras; `after_gameplay` fixes the normal rig at its default
|
||||||
|
yaw/zoom with mouse input disabled, and `after_vista` is an additional review angle.
|
||||||
|
The tool also records static-camera profile samples in
|
||||||
|
`painterly_after_metrics.json`; wall-frame latency includes vsync and is not
|
||||||
|
isolated GPU time or weak-PC proof. Runtime camera defaults are unchanged.
|
||||||
|
|
||||||
|
Remaining visual work is primarily authored terrain/shoreline detail and the
|
||||||
|
blockout-like building/fortress forms. This pass improves atmosphere and
|
||||||
|
foliage without treating those assets as finished film-quality environments.
|
||||||
|
|
||||||
|
Validation for this pass: the full macOS Godot 4.7 gate passed, including all
|
||||||
|
headless scenarios, the Compatibility quality scenario, and 182 GUT tests /
|
||||||
|
2,279 assertions. Main-scene headless startup passed the gate's existing exact
|
||||||
|
diagnostic allowlist. Native Metal daytime/gameplay/valley/night images were
|
||||||
|
reviewed at 1600×900, with a separate native Compatibility capture confirming
|
||||||
|
colored petals after the instance-buffer fix. The presentation-quality scenario
|
||||||
|
also passed with native Compatibility, including white instance-color readback
|
||||||
|
(the headless dummy renderer cannot provide that readback).
|
||||||
|
The static M1 Max sample reports approximately 16.7 ms
|
||||||
|
median wall-frame time in all three profiles; see its JSON for p95, draw calls
|
||||||
|
and primitives. This includes vsync and does not establish GPU headroom.
|
||||||
|
|
||||||
|
Native renderer shutdown still reports the particle shader/material RID
|
||||||
|
retention previously documented in the storybook/foliage pass; the headless
|
||||||
|
allowlist was not expanded. Compatibility has different color/tonemapping and
|
||||||
|
its existing particle/texture shutdown diagnostics. Metal is the color
|
||||||
|
reference for the saved art-review images.
|
||||||
|
The native multi-world quality scenario additionally logged a transient macOS
|
||||||
|
OpenGL cubemap-unloadable diagnostic; the final main-scene captures did not.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 371 KiB |
|
After Width: | Height: | Size: 461 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 1006 KiB |
|
After Width: | Height: | Size: 1008 KiB |
|
After Width: | Height: | Size: 997 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"active_trail_texels": 145,
|
||||||
|
"controller_update_p50_ms": 0.307,
|
||||||
|
"controller_update_p95_ms": 0.338,
|
||||||
|
"draw_calls": 582.0,
|
||||||
|
"note": "CPU timing includes scanning, field painting and material uploads; not isolated GPU frame time.",
|
||||||
|
"profile": "High",
|
||||||
|
"renderer": "metal",
|
||||||
|
"resolution": "1600x900",
|
||||||
|
"simulation_checksum_unchanged": true,
|
||||||
|
"trail_texture_bytes": 262144,
|
||||||
|
"workload": "Actual Jajce meadow; 8 nearest actors, player presentation follows an 11 m sweep"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1000 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"device": "Apple M1 Max (Apple7)",
|
||||||
|
"renderer": "metal",
|
||||||
|
"resolution": "1600x900",
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"active_npcs": 6,
|
||||||
|
"draw_calls": 825.0,
|
||||||
|
"meadow": {
|
||||||
|
"flower_clump_count": 1680,
|
||||||
|
"multimesh_batch_count": 4,
|
||||||
|
"visible_flower_clump_count": 1680
|
||||||
|
},
|
||||||
|
"msaa_enum": 2,
|
||||||
|
"primitives": 1070038.0,
|
||||||
|
"profile": "high",
|
||||||
|
"render_scale": 1.0,
|
||||||
|
"wall_frame_p50_ms": 16.649,
|
||||||
|
"wall_frame_p95_ms": 17.327
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"active_npcs": 6,
|
||||||
|
"draw_calls": 710.0,
|
||||||
|
"meadow": {
|
||||||
|
"flower_clump_count": 1680,
|
||||||
|
"multimesh_batch_count": 4,
|
||||||
|
"visible_flower_clump_count": 1092
|
||||||
|
},
|
||||||
|
"msaa_enum": 1,
|
||||||
|
"primitives": 810830.0,
|
||||||
|
"profile": "balanced",
|
||||||
|
"render_scale": 0.850000023841858,
|
||||||
|
"wall_frame_p50_ms": 16.672,
|
||||||
|
"wall_frame_p95_ms": 17.06
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"active_npcs": 6,
|
||||||
|
"draw_calls": 657.0,
|
||||||
|
"meadow": {
|
||||||
|
"flower_clump_count": 1680,
|
||||||
|
"multimesh_batch_count": 4,
|
||||||
|
"visible_flower_clump_count": 536
|
||||||
|
},
|
||||||
|
"msaa_enum": 0,
|
||||||
|
"primitives": 645010.0,
|
||||||
|
"profile": "low",
|
||||||
|
"render_scale": 0.699999988079071,
|
||||||
|
"wall_frame_p50_ms": 16.676,
|
||||||
|
"wall_frame_p95_ms": 17.069
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"workload": "Paused main scene, static valley camera; wall time includes vsync"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 709 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 815 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"device": "Apple M1 Max",
|
||||||
|
"renderer": "opengl3",
|
||||||
|
"resolution": "1600x900",
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"active_npcs": 6,
|
||||||
|
"draw_calls": 1304.0,
|
||||||
|
"meadow": {
|
||||||
|
"flower_clump_count": 1680,
|
||||||
|
"multimesh_batch_count": 4,
|
||||||
|
"visible_flower_clump_count": 1680
|
||||||
|
},
|
||||||
|
"msaa_enum": 2,
|
||||||
|
"primitives": 1070038.0,
|
||||||
|
"profile": "high",
|
||||||
|
"render_scale": 1.0,
|
||||||
|
"wall_frame_p50_ms": 22.166,
|
||||||
|
"wall_frame_p95_ms": 23.467
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"active_npcs": 6,
|
||||||
|
"draw_calls": 1166.0,
|
||||||
|
"meadow": {
|
||||||
|
"flower_clump_count": 1680,
|
||||||
|
"multimesh_batch_count": 4,
|
||||||
|
"visible_flower_clump_count": 1092
|
||||||
|
},
|
||||||
|
"msaa_enum": 1,
|
||||||
|
"primitives": 810758.0,
|
||||||
|
"profile": "balanced",
|
||||||
|
"render_scale": 0.850000023841858,
|
||||||
|
"wall_frame_p50_ms": 17.903,
|
||||||
|
"wall_frame_p95_ms": 26.363
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"active_npcs": 6,
|
||||||
|
"draw_calls": 1084.0,
|
||||||
|
"meadow": {
|
||||||
|
"flower_clump_count": 1680,
|
||||||
|
"multimesh_batch_count": 4,
|
||||||
|
"visible_flower_clump_count": 536
|
||||||
|
},
|
||||||
|
"msaa_enum": 0,
|
||||||
|
"primitives": 645010.0,
|
||||||
|
"profile": "low",
|
||||||
|
"render_scale": 0.699999988079071,
|
||||||
|
"wall_frame_p50_ms": 13.533,
|
||||||
|
"wall_frame_p95_ms": 16.623
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"workload": "Paused main scene, static valley camera; wall time includes vsync"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 469 KiB |
|
After Width: | Height: | Size: 809 KiB |
|
After Width: | Height: | Size: 798 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 937 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"renderer": "metal",
|
||||||
|
"resolution": "1600x900",
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"active_npcs": 6,
|
||||||
|
"draw_calls": 702.0,
|
||||||
|
"frame_wall_p50_ms": 16.646,
|
||||||
|
"frame_wall_p95_ms": 16.903,
|
||||||
|
"objects": 1114.0,
|
||||||
|
"primitives": 684656.0,
|
||||||
|
"profile": "high"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"active_npcs": 6,
|
||||||
|
"draw_calls": 613.0,
|
||||||
|
"frame_wall_p50_ms": 16.665,
|
||||||
|
"frame_wall_p95_ms": 16.93,
|
||||||
|
"objects": 1031.0,
|
||||||
|
"primitives": 486192.0,
|
||||||
|
"profile": "balanced"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"active_npcs": 6,
|
||||||
|
"draw_calls": 592.0,
|
||||||
|
"frame_wall_p50_ms": 16.659,
|
||||||
|
"frame_wall_p95_ms": 16.989,
|
||||||
|
"objects": 999.0,
|
||||||
|
"primitives": 377596.0,
|
||||||
|
"profile": "low"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"workload": "Paused static valley; wall-frame latency includes vsync, not isolated GPU time"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.1 MiB |
@@ -1,72 +1,7 @@
|
|||||||
[gd_scene load_steps=17 format=3]
|
[gd_scene load_steps=6 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://player/player_visual.gd" id="1_visual"]
|
[ext_resource type="Script" path="res://player/player_visual.gd" id="1_visual"]
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_tunic"]
|
|
||||||
albedo_color = Color(0.18, 0.46, 0.48, 1)
|
|
||||||
roughness = 0.84
|
|
||||||
|
|
||||||
[sub_resource type="CylinderMesh" id="Mesh_body"]
|
|
||||||
material = SubResource("Material_tunic")
|
|
||||||
top_radius = 0.28
|
|
||||||
bottom_radius = 0.42
|
|
||||||
height = 0.9
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_skin"]
|
|
||||||
albedo_color = Color(0.84, 0.61, 0.39, 1)
|
|
||||||
roughness = 0.88
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="Mesh_head"]
|
|
||||||
material = SubResource("Material_skin")
|
|
||||||
radius = 0.29
|
|
||||||
height = 0.56
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_hair"]
|
|
||||||
albedo_color = Color(0.2, 0.105, 0.05, 1)
|
|
||||||
roughness = 0.92
|
|
||||||
|
|
||||||
[sub_resource type="CylinderMesh" id="Mesh_hair"]
|
|
||||||
material = SubResource("Material_hair")
|
|
||||||
top_radius = 0.24
|
|
||||||
bottom_radius = 0.29
|
|
||||||
height = 0.18
|
|
||||||
|
|
||||||
[sub_resource type="CylinderMesh" id="Mesh_arm"]
|
|
||||||
material = SubResource("Material_tunic")
|
|
||||||
top_radius = 0.095
|
|
||||||
bottom_radius = 0.11
|
|
||||||
height = 0.58
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="Mesh_hand"]
|
|
||||||
material = SubResource("Material_skin")
|
|
||||||
radius = 0.12
|
|
||||||
height = 0.22
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_pants"]
|
|
||||||
albedo_color = Color(0.15, 0.21, 0.24, 1)
|
|
||||||
roughness = 0.9
|
|
||||||
|
|
||||||
[sub_resource type="CapsuleMesh" id="Mesh_leg"]
|
|
||||||
material = SubResource("Material_pants")
|
|
||||||
radius = 0.115
|
|
||||||
height = 0.62
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_boot"]
|
|
||||||
albedo_color = Color(0.13, 0.075, 0.04, 1)
|
|
||||||
roughness = 0.95
|
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="Mesh_foot"]
|
|
||||||
material = SubResource("Material_boot")
|
|
||||||
size = Vector3(0.23, 0.15, 0.36)
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_accent"]
|
|
||||||
albedo_color = Color(0.86, 0.4, 0.18, 1)
|
|
||||||
roughness = 0.82
|
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="Mesh_scarf"]
|
|
||||||
material = SubResource("Material_accent")
|
|
||||||
size = Vector3(0.48, 0.1, 0.38)
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_sword"]
|
[sub_resource type="StandardMaterial3D" id="Material_sword"]
|
||||||
albedo_color = Color(0.72, 0.74, 0.78, 1)
|
albedo_color = Color(0.72, 0.74, 0.78, 1)
|
||||||
roughness = 0.32
|
roughness = 0.32
|
||||||
@@ -85,67 +20,29 @@ top_radius = 0.045
|
|||||||
bottom_radius = 0.045
|
bottom_radius = 0.045
|
||||||
height = 0.16
|
height = 0.16
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="Mesh_satchel"]
|
|
||||||
material = SubResource("Material_accent")
|
|
||||||
radius = 0.22
|
|
||||||
height = 0.34
|
|
||||||
|
|
||||||
[node name="PlayerVisual" type="Node3D"]
|
[node name="PlayerVisual" type="Node3D"]
|
||||||
script = ExtResource("1_visual")
|
script = ExtResource("1_visual")
|
||||||
|
|
||||||
[node name="Body" type="MeshInstance3D" parent="."]
|
[node name="Body" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(0, 1.05, 0)
|
position = Vector3(0, 1.05, 0)
|
||||||
mesh = SubResource("Mesh_body")
|
|
||||||
|
|
||||||
[node name="Scarf" type="MeshInstance3D" parent="."]
|
|
||||||
position = Vector3(0, 1.42, 0.02)
|
|
||||||
mesh = SubResource("Mesh_scarf")
|
|
||||||
|
|
||||||
[node name="Head" type="MeshInstance3D" parent="."]
|
[node name="Head" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(0, 1.68, 0)
|
position = Vector3(0, 1.68, 0)
|
||||||
mesh = SubResource("Mesh_head")
|
|
||||||
|
|
||||||
[node name="Hair" type="MeshInstance3D" parent="."]
|
[node name="Hair" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(0, 1.9, -0.015)
|
position = Vector3(0, 1.9, -0.015)
|
||||||
mesh = SubResource("Mesh_hair")
|
|
||||||
|
|
||||||
[node name="ArmLeft" type="MeshInstance3D" parent="."]
|
[node name="ArmLeft" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(-0.39, 1.08, 0)
|
position = Vector3(-0.39, 1.3, 0)
|
||||||
rotation = Vector3(0, 0, -0.139626)
|
|
||||||
mesh = SubResource("Mesh_arm")
|
|
||||||
|
|
||||||
[node name="Hand" type="MeshInstance3D" parent="ArmLeft"]
|
|
||||||
position = Vector3(0, -0.35, 0)
|
|
||||||
mesh = SubResource("Mesh_hand")
|
|
||||||
|
|
||||||
[node name="ArmRight" type="MeshInstance3D" parent="."]
|
[node name="ArmRight" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(0.39, 1.08, 0)
|
position = Vector3(0.39, 1.3, 0)
|
||||||
rotation = Vector3(0, 0, 0.139626)
|
|
||||||
mesh = SubResource("Mesh_arm")
|
|
||||||
|
|
||||||
[node name="Hand" type="MeshInstance3D" parent="ArmRight"]
|
|
||||||
position = Vector3(0, -0.35, 0)
|
|
||||||
mesh = SubResource("Mesh_hand")
|
|
||||||
|
|
||||||
[node name="LegLeft" type="MeshInstance3D" parent="."]
|
[node name="LegLeft" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(-0.18, 0.43, 0)
|
position = Vector3(-0.18, 0.61, 0)
|
||||||
mesh = SubResource("Mesh_leg")
|
|
||||||
|
|
||||||
[node name="Foot" type="MeshInstance3D" parent="LegLeft"]
|
|
||||||
position = Vector3(0, -0.28, 0.09)
|
|
||||||
mesh = SubResource("Mesh_foot")
|
|
||||||
|
|
||||||
[node name="LegRight" type="MeshInstance3D" parent="."]
|
[node name="LegRight" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(0.18, 0.43, 0)
|
position = Vector3(0.18, 0.61, 0)
|
||||||
mesh = SubResource("Mesh_leg")
|
|
||||||
|
|
||||||
[node name="Foot" type="MeshInstance3D" parent="LegRight"]
|
|
||||||
position = Vector3(0, -0.28, 0.09)
|
|
||||||
mesh = SubResource("Mesh_foot")
|
|
||||||
|
|
||||||
[node name="Satchel" type="MeshInstance3D" parent="."]
|
|
||||||
position = Vector3(-0.43, 0.87, -0.04)
|
|
||||||
mesh = SubResource("Mesh_satchel")
|
|
||||||
|
|
||||||
[node name="Sword" type="Node3D" parent="."]
|
[node name="Sword" type="Node3D" parent="."]
|
||||||
position = Vector3(0.52, 1.22, 0.12)
|
position = Vector3(0.52, 1.22, 0.12)
|
||||||
@@ -158,3 +55,6 @@ mesh = SubResource("Mesh_blade")
|
|||||||
[node name="Grip" type="MeshInstance3D" parent="Sword"]
|
[node name="Grip" type="MeshInstance3D" parent="Sword"]
|
||||||
position = Vector3(0, -0.05, 0)
|
position = Vector3(0, -0.05, 0)
|
||||||
mesh = SubResource("Mesh_grip")
|
mesh = SubResource("Mesh_grip")
|
||||||
|
|
||||||
|
[node name="Tail" type="MeshInstance3D" parent="."]
|
||||||
|
position = Vector3(0, 0.8, -0.22)
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ signal arrived_at_target(sim_id: int)
|
|||||||
signal navigation_failed(sim_id: int)
|
signal navigation_failed(sim_id: int)
|
||||||
signal position_changed(sim_id: int, active_position: Vector3)
|
signal position_changed(sim_id: int, active_position: Vector3)
|
||||||
|
|
||||||
const TRUST_REACTION_BASE_POSITION := Vector3(0.0, 2.72, 0.0)
|
const TRUST_REACTION_BASE_POSITION := Vector3(0.0, 3.25, 0.0)
|
||||||
const TRUST_REACTION_START_SCALE := Vector3(0.24, 0.24, 0.24)
|
const TRUST_REACTION_START_SCALE := Vector3(0.24, 0.24, 0.24)
|
||||||
const CONCERN_BASE_POSITION := Vector3(-0.4, 2.72, 0.0)
|
const CONCERN_BASE_POSITION := Vector3(-0.4, 3.25, 0.0)
|
||||||
|
|
||||||
@export var debug_logs := false
|
@export var debug_logs := false
|
||||||
|
@export var appearance: CharacterAppearanceProfile
|
||||||
|
|
||||||
@export var move_speed := 3.0
|
@export var move_speed := 3.0
|
||||||
@export var rotation_speed := 8.0
|
@export var rotation_speed := 8.0
|
||||||
@@ -58,6 +59,9 @@ var walk_phase := 0.0
|
|||||||
var glyph_phase := 0.0
|
var glyph_phase := 0.0
|
||||||
var concern_phase := 0.0
|
var concern_phase := 0.0
|
||||||
var trust_reaction_tween: Tween
|
var trust_reaction_tween: Tween
|
||||||
|
var rest_heights := Vector3(1.05, 1.68, 1.9)
|
||||||
|
var arm_swing := 1.0
|
||||||
|
var cue_lift := 0.0
|
||||||
|
|
||||||
|
|
||||||
func debug_log(message: String) -> void:
|
func debug_log(message: String) -> void:
|
||||||
@@ -76,11 +80,13 @@ func _process(delta: float) -> void:
|
|||||||
return
|
return
|
||||||
glyph_phase = fmod(glyph_phase + delta * 2.2, TAU)
|
glyph_phase = fmod(glyph_phase + delta * 2.2, TAU)
|
||||||
if task_glyph_root.visible:
|
if task_glyph_root.visible:
|
||||||
task_glyph_root.position.y = 2.35 + sin(glyph_phase) * 0.045
|
task_glyph_root.position.y = 2.9 + cue_lift + sin(glyph_phase) * 0.045
|
||||||
task_glyph_root.rotation.y = glyph_phase * 0.35
|
task_glyph_root.rotation.y = glyph_phase * 0.35
|
||||||
if opportunity_concern_root.visible:
|
if opportunity_concern_root.visible:
|
||||||
concern_phase = fmod(concern_phase + delta * 1.65, TAU)
|
concern_phase = fmod(concern_phase + delta * 1.65, TAU)
|
||||||
opportunity_concern_root.position.y = (CONCERN_BASE_POSITION.y + sin(concern_phase) * 0.045)
|
opportunity_concern_root.position.y = (
|
||||||
|
CONCERN_BASE_POSITION.y + cue_lift + sin(concern_phase) * 0.045
|
||||||
|
)
|
||||||
opportunity_concern_root.rotation.y = sin(concern_phase * 0.7) * 0.16
|
opportunity_concern_root.rotation.y = sin(concern_phase * 0.7) * 0.16
|
||||||
var concern_scale := 0.96 + sin(concern_phase) * 0.04
|
var concern_scale := 0.96 + sin(concern_phase) * 0.04
|
||||||
opportunity_concern_root.scale = Vector3.ONE * concern_scale
|
opportunity_concern_root.scale = Vector3.ONE * concern_scale
|
||||||
@@ -92,20 +98,24 @@ func _process(delta: float) -> void:
|
|||||||
var swing := sin(walk_phase)
|
var swing := sin(walk_phase)
|
||||||
left_leg.rotation_degrees.x = lerpf(left_leg.rotation_degrees.x, swing * 28.0, blend)
|
left_leg.rotation_degrees.x = lerpf(left_leg.rotation_degrees.x, swing * 28.0, blend)
|
||||||
right_leg.rotation_degrees.x = lerpf(right_leg.rotation_degrees.x, -swing * 28.0, blend)
|
right_leg.rotation_degrees.x = lerpf(right_leg.rotation_degrees.x, -swing * 28.0, blend)
|
||||||
left_arm.rotation_degrees.x = lerpf(left_arm.rotation_degrees.x, -swing * 20.0, blend)
|
left_arm.rotation_degrees.x = lerpf(
|
||||||
right_arm.rotation_degrees.x = lerpf(right_arm.rotation_degrees.x, swing * 20.0, blend)
|
left_arm.rotation_degrees.x, -swing * 20.0 * arm_swing, blend
|
||||||
|
)
|
||||||
|
right_arm.rotation_degrees.x = lerpf(
|
||||||
|
right_arm.rotation_degrees.x, swing * 20.0 * arm_swing, blend
|
||||||
|
)
|
||||||
var step_bob := absf(sin(walk_phase * 2.0)) * 0.035
|
var step_bob := absf(sin(walk_phase * 2.0)) * 0.035
|
||||||
body_mesh.position.y = lerpf(body_mesh.position.y, 1.05 + step_bob, blend)
|
body_mesh.position.y = lerpf(body_mesh.position.y, rest_heights.x + step_bob, blend)
|
||||||
head_mesh.position.y = lerpf(head_mesh.position.y, 1.68 + step_bob, blend)
|
head_mesh.position.y = lerpf(head_mesh.position.y, rest_heights.y + step_bob, blend)
|
||||||
hair_mesh.position.y = lerpf(hair_mesh.position.y, 1.9 + step_bob, blend)
|
hair_mesh.position.y = lerpf(hair_mesh.position.y, rest_heights.z + step_bob, blend)
|
||||||
else:
|
else:
|
||||||
left_leg.rotation_degrees.x = lerpf(left_leg.rotation_degrees.x, 0.0, blend)
|
left_leg.rotation_degrees.x = lerpf(left_leg.rotation_degrees.x, 0.0, blend)
|
||||||
right_leg.rotation_degrees.x = lerpf(right_leg.rotation_degrees.x, 0.0, blend)
|
right_leg.rotation_degrees.x = lerpf(right_leg.rotation_degrees.x, 0.0, blend)
|
||||||
left_arm.rotation_degrees.x = lerpf(left_arm.rotation_degrees.x, 0.0, blend)
|
left_arm.rotation_degrees.x = lerpf(left_arm.rotation_degrees.x, 0.0, blend)
|
||||||
right_arm.rotation_degrees.x = lerpf(right_arm.rotation_degrees.x, 0.0, blend)
|
right_arm.rotation_degrees.x = lerpf(right_arm.rotation_degrees.x, 0.0, blend)
|
||||||
body_mesh.position.y = lerpf(body_mesh.position.y, 1.05, blend)
|
body_mesh.position.y = lerpf(body_mesh.position.y, rest_heights.x, blend)
|
||||||
head_mesh.position.y = lerpf(head_mesh.position.y, 1.68, blend)
|
head_mesh.position.y = lerpf(head_mesh.position.y, rest_heights.y, blend)
|
||||||
hair_mesh.position.y = lerpf(hair_mesh.position.y, 1.9, blend)
|
hair_mesh.position.y = lerpf(hair_mesh.position.y, rest_heights.z, blend)
|
||||||
|
|
||||||
|
|
||||||
func setup_from_sim(npc: SimNPC) -> void:
|
func setup_from_sim(npc: SimNPC) -> void:
|
||||||
@@ -123,16 +133,12 @@ func setup_from_sim(npc: SimNPC) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _apply_personal_details() -> void:
|
func _apply_personal_details() -> void:
|
||||||
var hair_colors := [
|
AnimalAppearance.apply_to(self, sim_id, true, appearance)
|
||||||
Color(0.16, 0.09, 0.055, 1.0),
|
rest_heights = AnimalAppearance.rest_heights(self, true)
|
||||||
Color(0.29, 0.16, 0.075, 1.0),
|
arm_swing = get_meta("arm_swing", 1.0)
|
||||||
Color(0.09, 0.065, 0.05, 1.0),
|
cue_lift = maxf(0.0, float(get_meta("character_top", 2.6)) - 2.6)
|
||||||
Color(0.42, 0.27, 0.12, 1.0),
|
task_glyph_root.position.y = 2.9 + cue_lift
|
||||||
]
|
profession_label.position.y = 2.8 + cue_lift
|
||||||
var hair_material := StandardMaterial3D.new()
|
|
||||||
hair_material.albedo_color = hair_colors[maxi(sim_id, 0) % hair_colors.size()]
|
|
||||||
hair_material.roughness = 0.9
|
|
||||||
hair_mesh.material_override = hair_material
|
|
||||||
|
|
||||||
|
|
||||||
func _apply_profession_presentation() -> void:
|
func _apply_profession_presentation() -> void:
|
||||||
@@ -142,23 +148,31 @@ func _apply_profession_presentation() -> void:
|
|||||||
profession_prop.visible = false
|
profession_prop.visible = false
|
||||||
return
|
return
|
||||||
|
|
||||||
var body_material := StandardMaterial3D.new()
|
AnimalAppearance.set_profession_color(self, definition.visual_color)
|
||||||
body_material.albedo_color = definition.visual_color
|
|
||||||
body_material.roughness = 0.82
|
|
||||||
body_mesh.material_override = body_material
|
|
||||||
left_arm.material_override = body_material
|
|
||||||
right_arm.material_override = body_material
|
|
||||||
|
|
||||||
var prop_material := StandardMaterial3D.new()
|
var prop_material := StandardMaterial3D.new()
|
||||||
prop_material.albedo_color = definition.prop_color
|
prop_material.albedo_color = definition.prop_color
|
||||||
prop_material.roughness = 0.78
|
prop_material.roughness = 0.78
|
||||||
profession_prop.material_override = prop_material
|
profession_prop.material_override = prop_material
|
||||||
profession_prop.mesh = _create_profession_prop_mesh()
|
profession_prop.mesh = _create_profession_prop_mesh()
|
||||||
|
_fit_carried_props()
|
||||||
profession_prop.visible = profession_prop.mesh != null
|
profession_prop.visible = profession_prop.mesh != null
|
||||||
profession_label.text = "%s\n%s" % [npc_name, definition.display_name]
|
profession_label.text = "%s\n%s" % [npc_name, definition.display_name]
|
||||||
profession_label.visible = debug_overlay_visible
|
profession_label.visible = debug_overlay_visible
|
||||||
|
|
||||||
|
|
||||||
|
func _fit_carried_props() -> void:
|
||||||
|
var profile := get_meta("character_appearance") as CharacterAppearanceProfile
|
||||||
|
var lift := Vector3.UP * 0.61 * (profile.leg_length - 1.0) * profile.height
|
||||||
|
var reach := 0.60 + 0.10 * profile.body_fullness
|
||||||
|
profession_prop.position.x = signf(profession_prop.position.x) * reach
|
||||||
|
profession_prop.position.z = 0.20
|
||||||
|
profession_prop.position = profession_prop.position * profile.height + lift
|
||||||
|
profession_prop.scale = Vector3.ONE * profile.height
|
||||||
|
carried_food_visual.position = Vector3(reach, 0.70, 0.32) * profile.height + lift
|
||||||
|
carried_wood_visual.position = Vector3(-reach, 0.88, 0.18) * profile.height + lift
|
||||||
|
|
||||||
|
|
||||||
func _create_profession_prop_mesh() -> PrimitiveMesh:
|
func _create_profession_prop_mesh() -> PrimitiveMesh:
|
||||||
profession_prop.position = Vector3(0.48, 0.85, 0.0)
|
profession_prop.position = Vector3(0.48, 0.85, 0.0)
|
||||||
profession_prop.rotation_degrees = Vector3.ZERO
|
profession_prop.rotation_degrees = Vector3.ZERO
|
||||||
@@ -218,7 +232,7 @@ func set_opportunity_concern_visible(is_visible: bool) -> void:
|
|||||||
if opportunity_concern_root == null:
|
if opportunity_concern_root == null:
|
||||||
return
|
return
|
||||||
opportunity_concern_root.visible = is_visible and not is_dead_visual
|
opportunity_concern_root.visible = is_visible and not is_dead_visual
|
||||||
opportunity_concern_root.position = CONCERN_BASE_POSITION
|
opportunity_concern_root.position = CONCERN_BASE_POSITION + Vector3.UP * cue_lift
|
||||||
opportunity_concern_root.rotation = Vector3.ZERO
|
opportunity_concern_root.rotation = Vector3.ZERO
|
||||||
opportunity_concern_root.scale = Vector3.ONE
|
opportunity_concern_root.scale = Vector3.ONE
|
||||||
if is_visible:
|
if is_visible:
|
||||||
@@ -236,7 +250,7 @@ func play_trust_gain_reaction() -> void:
|
|||||||
return
|
return
|
||||||
_stop_trust_gain_reaction()
|
_stop_trust_gain_reaction()
|
||||||
trust_reaction_root.visible = true
|
trust_reaction_root.visible = true
|
||||||
trust_reaction_root.position = TRUST_REACTION_BASE_POSITION
|
trust_reaction_root.position = TRUST_REACTION_BASE_POSITION + Vector3.UP * cue_lift
|
||||||
trust_reaction_root.rotation = Vector3.ZERO
|
trust_reaction_root.rotation = Vector3.ZERO
|
||||||
trust_reaction_root.scale = TRUST_REACTION_START_SCALE
|
trust_reaction_root.scale = TRUST_REACTION_START_SCALE
|
||||||
_set_trust_reaction_transparency(1.0)
|
_set_trust_reaction_transparency(1.0)
|
||||||
@@ -248,14 +262,14 @@ func play_trust_gain_reaction() -> void:
|
|||||||
trust_reaction_tween.tween_property(
|
trust_reaction_tween.tween_property(
|
||||||
trust_reaction_root,
|
trust_reaction_root,
|
||||||
"position",
|
"position",
|
||||||
TRUST_REACTION_BASE_POSITION + Vector3(0.0, 0.16, 0.0),
|
TRUST_REACTION_BASE_POSITION + Vector3(0.0, 0.16 + cue_lift, 0.0),
|
||||||
0.68
|
0.68
|
||||||
)
|
)
|
||||||
trust_reaction_tween.parallel().tween_property(trust_reaction_root, "rotation:y", 0.32, 0.68)
|
trust_reaction_tween.parallel().tween_property(trust_reaction_root, "rotation:y", 0.32, 0.68)
|
||||||
trust_reaction_tween.tween_property(
|
trust_reaction_tween.tween_property(
|
||||||
trust_reaction_root,
|
trust_reaction_root,
|
||||||
"position",
|
"position",
|
||||||
TRUST_REACTION_BASE_POSITION + Vector3(0.0, 0.44, 0.0),
|
TRUST_REACTION_BASE_POSITION + Vector3(0.0, 0.44 + cue_lift, 0.0),
|
||||||
0.55
|
0.55
|
||||||
)
|
)
|
||||||
trust_reaction_tween.parallel().tween_property(
|
trust_reaction_tween.parallel().tween_property(
|
||||||
@@ -272,7 +286,7 @@ func _set_trust_reaction_transparency(value: float) -> void:
|
|||||||
|
|
||||||
func _finish_trust_gain_reaction() -> void:
|
func _finish_trust_gain_reaction() -> void:
|
||||||
trust_reaction_root.visible = false
|
trust_reaction_root.visible = false
|
||||||
trust_reaction_root.position = TRUST_REACTION_BASE_POSITION
|
trust_reaction_root.position = TRUST_REACTION_BASE_POSITION + Vector3.UP * cue_lift
|
||||||
trust_reaction_root.rotation = Vector3.ZERO
|
trust_reaction_root.rotation = Vector3.ZERO
|
||||||
trust_reaction_root.scale = TRUST_REACTION_START_SCALE
|
trust_reaction_root.scale = TRUST_REACTION_START_SCALE
|
||||||
_set_trust_reaction_transparency(1.0)
|
_set_trust_reaction_transparency(1.0)
|
||||||
@@ -286,7 +300,7 @@ func _stop_trust_gain_reaction() -> void:
|
|||||||
if trust_reaction_root == null:
|
if trust_reaction_root == null:
|
||||||
return
|
return
|
||||||
trust_reaction_root.visible = false
|
trust_reaction_root.visible = false
|
||||||
trust_reaction_root.position = TRUST_REACTION_BASE_POSITION
|
trust_reaction_root.position = TRUST_REACTION_BASE_POSITION + Vector3.UP * cue_lift
|
||||||
trust_reaction_root.rotation = Vector3.ZERO
|
trust_reaction_root.rotation = Vector3.ZERO
|
||||||
trust_reaction_root.scale = TRUST_REACTION_START_SCALE
|
trust_reaction_root.scale = TRUST_REACTION_START_SCALE
|
||||||
_set_trust_reaction_transparency(1.0)
|
_set_trust_reaction_transparency(1.0)
|
||||||
|
|||||||
@@ -1,70 +1,9 @@
|
|||||||
[gd_scene load_steps=24 format=3 uid="uid://dhxxyprqflotq"]
|
[gd_scene load_steps=13 format=3 uid="uid://dhxxyprqflotq"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://bha8uf40p3sa0" path="res://player/npc/NpcVisual.gd" id="1_ixfoq"]
|
[ext_resource type="Script" uid="uid://bha8uf40p3sa0" path="res://player/npc/NpcVisual.gd" id="1_ixfoq"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_d844k"]
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_d844k"]
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_d844k"]
|
|
||||||
albedo_color = Color(0.46, 0.58, 0.33, 1)
|
|
||||||
roughness = 0.82
|
|
||||||
|
|
||||||
[sub_resource type="CylinderMesh" id="CylinderMesh_body"]
|
|
||||||
material = SubResource("StandardMaterial3D_d844k")
|
|
||||||
top_radius = 0.28
|
|
||||||
bottom_radius = 0.42
|
|
||||||
height = 0.9
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_skin"]
|
|
||||||
albedo_color = Color(0.84, 0.61, 0.39, 1)
|
|
||||||
roughness = 0.88
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="SphereMesh_head"]
|
|
||||||
material = SubResource("StandardMaterial3D_skin")
|
|
||||||
radius = 0.29
|
|
||||||
height = 0.56
|
|
||||||
|
|
||||||
[sub_resource type="CylinderMesh" id="CylinderMesh_hair"]
|
|
||||||
top_radius = 0.24
|
|
||||||
bottom_radius = 0.29
|
|
||||||
height = 0.18
|
|
||||||
|
|
||||||
[sub_resource type="CylinderMesh" id="CylinderMesh_arm"]
|
|
||||||
material = SubResource("StandardMaterial3D_d844k")
|
|
||||||
top_radius = 0.095
|
|
||||||
bottom_radius = 0.11
|
|
||||||
height = 0.58
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="SphereMesh_hand"]
|
|
||||||
material = SubResource("StandardMaterial3D_skin")
|
|
||||||
radius = 0.12
|
|
||||||
height = 0.22
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_pants"]
|
|
||||||
albedo_color = Color(0.16, 0.22, 0.25, 1)
|
|
||||||
roughness = 0.9
|
|
||||||
|
|
||||||
[sub_resource type="CapsuleMesh" id="CapsuleMesh_leg"]
|
|
||||||
material = SubResource("StandardMaterial3D_pants")
|
|
||||||
radius = 0.115
|
|
||||||
height = 0.62
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_boot"]
|
|
||||||
albedo_color = Color(0.13, 0.075, 0.04, 1)
|
|
||||||
roughness = 0.95
|
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="BoxMesh_foot"]
|
|
||||||
material = SubResource("StandardMaterial3D_boot")
|
|
||||||
size = Vector3(0.23, 0.15, 0.36)
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_eye"]
|
|
||||||
albedo_color = Color(0.06, 0.045, 0.035, 1)
|
|
||||||
roughness = 0.8
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="SphereMesh_eye"]
|
|
||||||
material = SubResource("StandardMaterial3D_eye")
|
|
||||||
radius = 0.035
|
|
||||||
height = 0.06
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_food"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_food"]
|
||||||
albedo_color = Color(0.38, 0.22, 0.08, 1)
|
albedo_color = Color(0.38, 0.22, 0.08, 1)
|
||||||
roughness = 0.9
|
roughness = 0.9
|
||||||
@@ -139,62 +78,29 @@ shape = SubResource("CapsuleShape3D_d844k")
|
|||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(0, 1.05, 0)
|
position = Vector3(0, 1.05, 0)
|
||||||
mesh = SubResource("CylinderMesh_body")
|
|
||||||
|
|
||||||
[node name="Head" type="MeshInstance3D" parent="."]
|
[node name="Head" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(0, 1.68, 0)
|
position = Vector3(0, 1.68, 0)
|
||||||
mesh = SubResource("SphereMesh_head")
|
|
||||||
|
|
||||||
[node name="Hair" type="MeshInstance3D" parent="."]
|
[node name="Hair" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(0, 1.9, -0.015)
|
position = Vector3(0, 1.9, -0.015)
|
||||||
mesh = SubResource("CylinderMesh_hair")
|
|
||||||
|
|
||||||
[node name="EyeLeft" type="MeshInstance3D" parent="."]
|
|
||||||
position = Vector3(-0.095, 1.72, 0.274)
|
|
||||||
mesh = SubResource("SphereMesh_eye")
|
|
||||||
|
|
||||||
[node name="EyeRight" type="MeshInstance3D" parent="."]
|
|
||||||
position = Vector3(0.095, 1.72, 0.274)
|
|
||||||
mesh = SubResource("SphereMesh_eye")
|
|
||||||
|
|
||||||
[node name="ArmLeft" type="MeshInstance3D" parent="."]
|
[node name="ArmLeft" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(-0.39, 1.08, 0)
|
position = Vector3(-0.39, 1.3, 0)
|
||||||
rotation = Vector3(0, 0, -0.139626)
|
|
||||||
mesh = SubResource("CylinderMesh_arm")
|
|
||||||
|
|
||||||
[node name="Hand" type="MeshInstance3D" parent="ArmLeft"]
|
|
||||||
position = Vector3(0, -0.35, 0)
|
|
||||||
mesh = SubResource("SphereMesh_hand")
|
|
||||||
|
|
||||||
[node name="ArmRight" type="MeshInstance3D" parent="."]
|
[node name="ArmRight" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(0.39, 1.08, 0)
|
position = Vector3(0.39, 1.3, 0)
|
||||||
rotation = Vector3(0, 0, 0.139626)
|
|
||||||
mesh = SubResource("CylinderMesh_arm")
|
|
||||||
|
|
||||||
[node name="Hand" type="MeshInstance3D" parent="ArmRight"]
|
|
||||||
position = Vector3(0, -0.35, 0)
|
|
||||||
mesh = SubResource("SphereMesh_hand")
|
|
||||||
|
|
||||||
[node name="LegLeft" type="MeshInstance3D" parent="."]
|
[node name="LegLeft" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(-0.18, 0.43, 0)
|
position = Vector3(-0.18, 0.61, 0)
|
||||||
mesh = SubResource("CapsuleMesh_leg")
|
|
||||||
|
|
||||||
[node name="Foot" type="MeshInstance3D" parent="LegLeft"]
|
|
||||||
position = Vector3(0, -0.28, 0.09)
|
|
||||||
mesh = SubResource("BoxMesh_foot")
|
|
||||||
|
|
||||||
[node name="LegRight" type="MeshInstance3D" parent="."]
|
[node name="LegRight" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(0.18, 0.43, 0)
|
position = Vector3(0.18, 0.61, 0)
|
||||||
mesh = SubResource("CapsuleMesh_leg")
|
|
||||||
|
|
||||||
[node name="Foot" type="MeshInstance3D" parent="LegRight"]
|
|
||||||
position = Vector3(0, -0.28, 0.09)
|
|
||||||
mesh = SubResource("BoxMesh_foot")
|
|
||||||
|
|
||||||
[node name="ProfessionProp" type="MeshInstance3D" parent="."]
|
[node name="ProfessionProp" type="MeshInstance3D" parent="."]
|
||||||
|
|
||||||
[node name="ProfessionLabel" type="Label3D" parent="."]
|
[node name="ProfessionLabel" type="Label3D" parent="."]
|
||||||
position = Vector3(0, 2.05, 0)
|
position = Vector3(0, 2.8, 0)
|
||||||
billboard = 1
|
billboard = 1
|
||||||
no_depth_test = true
|
no_depth_test = true
|
||||||
text = "Villager"
|
text = "Villager"
|
||||||
@@ -223,13 +129,13 @@ mesh = SubResource("CylinderMesh_wood")
|
|||||||
|
|
||||||
[node name="TaskGlyphRoot" type="Node3D" parent="."]
|
[node name="TaskGlyphRoot" type="Node3D" parent="."]
|
||||||
visible = false
|
visible = false
|
||||||
position = Vector3(0, 2.35, 0)
|
position = Vector3(0, 2.9, 0)
|
||||||
|
|
||||||
[node name="TaskGlyph" type="MeshInstance3D" parent="TaskGlyphRoot"]
|
[node name="TaskGlyph" type="MeshInstance3D" parent="TaskGlyphRoot"]
|
||||||
|
|
||||||
[node name="OpportunityConcernRoot" type="Node3D" parent="."]
|
[node name="OpportunityConcernRoot" type="Node3D" parent="."]
|
||||||
visible = false
|
visible = false
|
||||||
position = Vector3(-0.4, 2.72, 0)
|
position = Vector3(-0.4, 3.25, 0)
|
||||||
|
|
||||||
[node name="EmptyBowl" type="MeshInstance3D" parent="OpportunityConcernRoot"]
|
[node name="EmptyBowl" type="MeshInstance3D" parent="OpportunityConcernRoot"]
|
||||||
rotation_degrees = Vector3(72, 0, 0)
|
rotation_degrees = Vector3(72, 0, 0)
|
||||||
@@ -250,7 +156,7 @@ mesh = SubResource("SphereMesh_concern")
|
|||||||
|
|
||||||
[node name="TrustReactionRoot" type="Node3D" parent="."]
|
[node name="TrustReactionRoot" type="Node3D" parent="."]
|
||||||
visible = false
|
visible = false
|
||||||
position = Vector3(0, 2.72, 0)
|
position = Vector3(0, 3.25, 0)
|
||||||
scale = Vector3(0.24, 0.24, 0.24)
|
scale = Vector3(0.24, 0.24, 0.24)
|
||||||
|
|
||||||
[node name="Halo" type="MeshInstance3D" parent="TrustReactionRoot"]
|
[node name="Halo" type="MeshInstance3D" parent="TrustReactionRoot"]
|
||||||
@@ -278,3 +184,6 @@ path_desired_distance = 0.3
|
|||||||
target_desired_distance = 0.5
|
target_desired_distance = 0.5
|
||||||
height = 1.7
|
height = 1.7
|
||||||
radius = 0.4
|
radius = 0.4
|
||||||
|
|
||||||
|
[node name="Tail" type="MeshInstance3D" parent="."]
|
||||||
|
position = Vector3(0, 0.8, -0.22)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
extends Node3D
|
extends Node3D
|
||||||
|
|
||||||
|
@export var appearance: CharacterAppearanceProfile = preload(
|
||||||
|
"res://assets/characters/profiles/reference_cat.tres"
|
||||||
|
)
|
||||||
|
|
||||||
@onready var body_mesh: MeshInstance3D = $Body
|
@onready var body_mesh: MeshInstance3D = $Body
|
||||||
@onready var head_mesh: MeshInstance3D = $Head
|
@onready var head_mesh: MeshInstance3D = $Head
|
||||||
@onready var hair_mesh: MeshInstance3D = $Hair
|
@onready var hair_mesh: MeshInstance3D = $Hair
|
||||||
@@ -10,10 +14,15 @@ extends Node3D
|
|||||||
|
|
||||||
var player_body: CharacterBody3D
|
var player_body: CharacterBody3D
|
||||||
var walk_phase := 0.0
|
var walk_phase := 0.0
|
||||||
|
var rest_heights := Vector3(1.05, 1.68, 1.9)
|
||||||
|
var arm_swing := 1.0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
player_body = get_parent() as CharacterBody3D
|
player_body = get_parent() as CharacterBody3D
|
||||||
|
AnimalAppearance.apply_to(self, 0, false, appearance)
|
||||||
|
rest_heights = AnimalAppearance.rest_heights(self)
|
||||||
|
arm_swing = get_meta("arm_swing", 1.0)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
@@ -26,18 +35,22 @@ func _process(delta: float) -> void:
|
|||||||
var swing := sin(walk_phase)
|
var swing := sin(walk_phase)
|
||||||
left_leg.rotation_degrees.x = lerpf(left_leg.rotation_degrees.x, swing * 30.0, blend)
|
left_leg.rotation_degrees.x = lerpf(left_leg.rotation_degrees.x, swing * 30.0, blend)
|
||||||
right_leg.rotation_degrees.x = lerpf(right_leg.rotation_degrees.x, -swing * 30.0, blend)
|
right_leg.rotation_degrees.x = lerpf(right_leg.rotation_degrees.x, -swing * 30.0, blend)
|
||||||
left_arm.rotation_degrees.x = lerpf(left_arm.rotation_degrees.x, -swing * 22.0, blend)
|
left_arm.rotation_degrees.x = lerpf(
|
||||||
right_arm.rotation_degrees.x = lerpf(right_arm.rotation_degrees.x, swing * 22.0, blend)
|
left_arm.rotation_degrees.x, -swing * 22.0 * arm_swing, blend
|
||||||
|
)
|
||||||
|
right_arm.rotation_degrees.x = lerpf(
|
||||||
|
right_arm.rotation_degrees.x, swing * 22.0 * arm_swing, blend
|
||||||
|
)
|
||||||
var step_bob := absf(sin(walk_phase * 2.0)) * 0.035
|
var step_bob := absf(sin(walk_phase * 2.0)) * 0.035
|
||||||
body_mesh.position.y = lerpf(body_mesh.position.y, 1.05 + step_bob, blend)
|
body_mesh.position.y = lerpf(body_mesh.position.y, rest_heights.x + step_bob, blend)
|
||||||
head_mesh.position.y = lerpf(head_mesh.position.y, 1.68 + step_bob, blend)
|
head_mesh.position.y = lerpf(head_mesh.position.y, rest_heights.y + step_bob, blend)
|
||||||
hair_mesh.position.y = lerpf(hair_mesh.position.y, 1.9 + step_bob, blend)
|
hair_mesh.position.y = lerpf(hair_mesh.position.y, rest_heights.z + step_bob, blend)
|
||||||
return
|
return
|
||||||
|
|
||||||
left_leg.rotation_degrees.x = lerpf(left_leg.rotation_degrees.x, 0.0, blend)
|
left_leg.rotation_degrees.x = lerpf(left_leg.rotation_degrees.x, 0.0, blend)
|
||||||
right_leg.rotation_degrees.x = lerpf(right_leg.rotation_degrees.x, 0.0, blend)
|
right_leg.rotation_degrees.x = lerpf(right_leg.rotation_degrees.x, 0.0, blend)
|
||||||
left_arm.rotation_degrees.x = lerpf(left_arm.rotation_degrees.x, 0.0, blend)
|
left_arm.rotation_degrees.x = lerpf(left_arm.rotation_degrees.x, 0.0, blend)
|
||||||
right_arm.rotation_degrees.x = lerpf(right_arm.rotation_degrees.x, 0.0, blend)
|
right_arm.rotation_degrees.x = lerpf(right_arm.rotation_degrees.x, 0.0, blend)
|
||||||
body_mesh.position.y = lerpf(body_mesh.position.y, 1.05, blend)
|
body_mesh.position.y = lerpf(body_mesh.position.y, rest_heights.x, blend)
|
||||||
head_mesh.position.y = lerpf(head_mesh.position.y, 1.68, blend)
|
head_mesh.position.y = lerpf(head_mesh.position.y, rest_heights.y, blend)
|
||||||
hair_mesh.position.y = lerpf(hair_mesh.position.y, 1.9, blend)
|
hair_mesh.position.y = lerpf(hair_mesh.position.y, rest_heights.z, blend)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ move_right={
|
|||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
interact={
|
interact={
|
||||||
"deadzone": 0.2,
|
"deadzone": 0.2,
|
||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
||||||
]
|
]
|
||||||
@@ -69,3 +69,7 @@ dash={
|
|||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[rendering]
|
||||||
|
|
||||||
|
anti_aliasing/quality/msaa_3d=2
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Imported geometry, independent morphology, stable identities, and real animation sockets.
|
||||||
|
|
||||||
|
const PLAYER := preload("res://player/PlayerVisual.tscn")
|
||||||
|
const NPC := preload("res://player/npc/NpcVisual.tscn")
|
||||||
|
const PROFILE_DIR := "res://assets/characters/profiles/"
|
||||||
|
const PRESETS := [
|
||||||
|
"reference_cat",
|
||||||
|
"fox_rambler",
|
||||||
|
"rabbit_scholar",
|
||||||
|
"badger_baker",
|
||||||
|
"otter_courier",
|
||||||
|
"lop_gardener"
|
||||||
|
]
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
var checks := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var first := PLAYER.instantiate() as Node3D
|
||||||
|
var second := PLAYER.instantiate() as Node3D
|
||||||
|
root.add_child(first)
|
||||||
|
root.add_child(second)
|
||||||
|
first.set_process(false)
|
||||||
|
second.set_process(false)
|
||||||
|
var lean := CharacterAppearanceProfile.new()
|
||||||
|
lean.body_fullness = 0.0
|
||||||
|
var stout := CharacterAppearanceProfile.new()
|
||||||
|
stout.body_fullness = 1.0
|
||||||
|
AnimalAppearance.apply_to(first, 0, false, lean)
|
||||||
|
AnimalAppearance.apply_to(second, 0, false, stout)
|
||||||
|
for path in ["Body", "Body/Bag", "Body/Scarf"]:
|
||||||
|
var a := first.get_node(path) as MeshInstance3D
|
||||||
|
var b := second.get_node(path) as MeshInstance3D
|
||||||
|
_check(a.mesh == b.mesh, path + " must share imported geometry")
|
||||||
|
var shape := a.find_blend_shape_by_name("Stout")
|
||||||
|
_check(shape >= 0, path + " must retain its Blender Stout shape")
|
||||||
|
if shape >= 0:
|
||||||
|
_check(
|
||||||
|
is_zero_approx(a.get_blend_shape_value(shape)),
|
||||||
|
"Lean shape weight must remain independent"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
is_equal_approx(b.get_blend_shape_value(shape), 1.0),
|
||||||
|
"Stout shape weight must remain independent"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
first.get_node("Body").material_override != second.get_node("Body").material_override,
|
||||||
|
"Palettes must be per actor"
|
||||||
|
)
|
||||||
|
var body := first.get_node("Body") as MeshInstance3D
|
||||||
|
var arrays := body.mesh.surface_get_arrays(0)
|
||||||
|
var roles: PackedVector2Array = arrays[Mesh.ARRAY_TEX_UV2]
|
||||||
|
_check(not roles.is_empty(), "Palette roles must survive GLB import in UV2")
|
||||||
|
var coat_vertices := 0
|
||||||
|
for role in roles:
|
||||||
|
if is_equal_approx(role.x, 3.0):
|
||||||
|
coat_vertices += 1
|
||||||
|
_check(coat_vertices > 0, "Coat must have an independently recolorable palette role")
|
||||||
|
var child_count := first.find_children("*", "MeshInstance3D", true, false).size()
|
||||||
|
for preset in PRESETS:
|
||||||
|
var profile: CharacterAppearanceProfile = load(PROFILE_DIR + preset + ".tres")
|
||||||
|
AnimalAppearance.apply_to(first, 0, false, profile)
|
||||||
|
_check(
|
||||||
|
first.find_children("*", "MeshInstance3D", true, false).size() == child_count,
|
||||||
|
"Changing profiles must reuse attachment nodes"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
first.get_node("Head/Ears").get_parent() == first.get_node("Head"),
|
||||||
|
"Ears must follow the head"
|
||||||
|
)
|
||||||
|
_check(first.get_node("Body/Bag").get_parent() == body, "Pack must follow the torso")
|
||||||
|
var triangles := 0
|
||||||
|
var draws := 0
|
||||||
|
for mesh_node: MeshInstance3D in first.find_children("*", "MeshInstance3D", true, false):
|
||||||
|
if mesh_node.mesh == null or mesh_node.name in ["Blade", "Grip"]:
|
||||||
|
continue
|
||||||
|
_check(
|
||||||
|
mesh_node.mesh.get_surface_count() == 1,
|
||||||
|
"Each character part must remain one surface"
|
||||||
|
)
|
||||||
|
triangles += mesh_node.mesh.surface_get_array_index_len(0) / 3
|
||||||
|
draws += 1
|
||||||
|
_check(
|
||||||
|
triangles < 23000 and draws <= 12,
|
||||||
|
"Selected character must stay within geometry and surface budgets"
|
||||||
|
)
|
||||||
|
for extreme in [0, 1]:
|
||||||
|
var profile := CharacterAppearanceProfile.new()
|
||||||
|
profile.height = 0.80 if extreme == 0 else 1.15
|
||||||
|
profile.leg_length = 0.85 if extreme == 0 else 1.15
|
||||||
|
profile.head_size = 0.88 if extreme == 0 else 1.12
|
||||||
|
profile.body_fullness = float(extreme)
|
||||||
|
profile.ear_length = 0.65 if extreme == 0 else 1.20
|
||||||
|
profile.species = 2
|
||||||
|
AnimalAppearance.apply_to(first, 0, false, profile)
|
||||||
|
for side in ["Left", "Right"]:
|
||||||
|
var leg := first.get_node("Leg" + side) as MeshInstance3D
|
||||||
|
var bounds := leg.transform * leg.get_aabb()
|
||||||
|
_check(
|
||||||
|
absf(bounds.position.y) < 0.03, "Leg-length variation must keep feet on the ground"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
first.get_meta("character_top") > first.get_node("Head").position.y,
|
||||||
|
"Visible height must include ears"
|
||||||
|
)
|
||||||
|
# An ID lookup must neither consume global random state nor change its result after another lookup.
|
||||||
|
seed(8123)
|
||||||
|
var expected := randf()
|
||||||
|
seed(8123)
|
||||||
|
var identity := CharacterAppearanceProfile.for_identity(731)
|
||||||
|
for value in 30:
|
||||||
|
CharacterAppearanceProfile.for_identity(value)
|
||||||
|
var rebuilt := CharacterAppearanceProfile.for_identity(731)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
identity.height == rebuilt.height
|
||||||
|
and identity.body_fullness == rebuilt.body_fullness
|
||||||
|
and identity.species == rebuilt.species
|
||||||
|
),
|
||||||
|
"Identity must rebuild deterministically"
|
||||||
|
)
|
||||||
|
_check(randf() == expected, "Appearance must not consume global RNG")
|
||||||
|
first.free()
|
||||||
|
second.free()
|
||||||
|
_test_animation_and_cues()
|
||||||
|
await process_frame
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] PASS character_appearance (", checks, " checks)")
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
for failure in failures:
|
||||||
|
push_error(failure)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _test_animation_and_cues() -> void:
|
||||||
|
var profile := CharacterAppearanceProfile.new()
|
||||||
|
profile.species = 2
|
||||||
|
profile.height = 1.15
|
||||||
|
profile.head_size = 1.12
|
||||||
|
profile.ear_length = 1.2
|
||||||
|
profile.leg_length = 1.15
|
||||||
|
var parent := CharacterBody3D.new()
|
||||||
|
root.add_child(parent)
|
||||||
|
var player := PLAYER.instantiate() as Node3D
|
||||||
|
player.appearance = profile
|
||||||
|
parent.add_child(player)
|
||||||
|
player.set_process(false)
|
||||||
|
parent.velocity = Vector3(2, 0, 0)
|
||||||
|
var heights := AnimalAppearance.rest_heights(player)
|
||||||
|
for frame in 30:
|
||||||
|
player.call("_process", 1.0 / 60.0)
|
||||||
|
_check(
|
||||||
|
player.get_node("Head").position.y >= heights.y,
|
||||||
|
"Player gait must retain configured head height"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
player.get_node("Body").position.y >= heights.x,
|
||||||
|
"Player gait must retain configured torso height"
|
||||||
|
)
|
||||||
|
var npc := NPC.instantiate() as Node3D
|
||||||
|
npc.appearance = profile
|
||||||
|
root.add_child(npc)
|
||||||
|
npc.set_physics_process(false)
|
||||||
|
npc.set_process(false)
|
||||||
|
npc.call("_apply_personal_details")
|
||||||
|
var cue_height: float = npc.get_node("TaskGlyphRoot").position.y
|
||||||
|
npc.call("_apply_personal_details")
|
||||||
|
_check(
|
||||||
|
is_equal_approx(cue_height, npc.get_node("TaskGlyphRoot").position.y),
|
||||||
|
"Reapplying appearance must not accumulate cue lift"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
cue_height > float(npc.get_meta("character_top")) + 0.2,
|
||||||
|
"NPC cues must clear the tallest supported ears"
|
||||||
|
)
|
||||||
|
npc.velocity = Vector3(2, 0, 0)
|
||||||
|
for frame in 30:
|
||||||
|
npc.call("_process", 1.0 / 60.0)
|
||||||
|
_check(
|
||||||
|
npc.get_node("Head").position.y >= heights.y, "NPC gait must retain configured head height"
|
||||||
|
)
|
||||||
|
parent.free()
|
||||||
|
npc.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
checks += 1
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ba4wrqkojd2pa
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const Controller := preload("res://world/jajce/GrassInteractionController.gd")
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
class RestoreSource:
|
||||||
|
extends Node
|
||||||
|
signal state_restored
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
_check_trail_lifecycle()
|
||||||
|
await _check_loaded_binding()
|
||||||
|
if failures.is_empty():
|
||||||
|
print("Foliage interaction checks passed")
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
for failure in failures:
|
||||||
|
push_error(failure)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _check_trail_lifecycle() -> void:
|
||||||
|
var field := FoliageTrailMap.new()
|
||||||
|
var positions: Dictionary[int, Vector3] = {1: Vector3(0.25, 0, 0.25)}
|
||||||
|
field.advance(0.08, Vector3.ZERO, positions)
|
||||||
|
positions[1] = Vector3(2.25, 0, 0.25)
|
||||||
|
field.advance(0.08, Vector3.ZERO, positions)
|
||||||
|
_check(_sample(field, Vector2(1.25, 0.25)).b > 0.9, "Walking should leave a continuous trail")
|
||||||
|
_check(
|
||||||
|
_sample(field, Vector2(1.25, 0.75)).g > 0 and _sample(field, Vector2(1.25, -0.25)).g < 0,
|
||||||
|
"The two sides of a trail should bend away from the travelled line"
|
||||||
|
)
|
||||||
|
positions[1] = Vector3(18.25, 0, 0.25)
|
||||||
|
field.advance(0.08, Vector3.ZERO, positions)
|
||||||
|
_check(_sample(field, Vector2(10.25, 0.25)).b == 0, "Teleports must not paint connecting lines")
|
||||||
|
_check(
|
||||||
|
_sample(field, Vector2(18.25, 0.25)).b > 0.9,
|
||||||
|
"Teleported actors still press their destination"
|
||||||
|
)
|
||||||
|
field.advance(0.08, Vector3(1, 0, 1), {})
|
||||||
|
_check(
|
||||||
|
_sample(field, Vector2(1.25, 0.25)).b > 0.8,
|
||||||
|
"Camera movement must keep trails in world space"
|
||||||
|
)
|
||||||
|
field.advance(4.0, Vector3.ZERO, {})
|
||||||
|
var recovering := _sample(field, Vector2(1.25, 0.25)).b
|
||||||
|
_check(recovering > 0.1 and recovering < 0.6, "Abandoned trails should gradually recover")
|
||||||
|
field.advance(4.1, Vector3.ZERO, {})
|
||||||
|
_check(
|
||||||
|
(field.get("_cells") as Dictionary).is_empty(),
|
||||||
|
"Recovered trails should release sparse history"
|
||||||
|
)
|
||||||
|
positions[1] = Vector3(0.25, 6, 0.25)
|
||||||
|
field.advance(0.08, Vector3.ZERO, positions)
|
||||||
|
var elevated := _sample(field, Vector2(0.25, 0.25))
|
||||||
|
_check(
|
||||||
|
is_equal_approx(elevated.a / elevated.b, 6.0),
|
||||||
|
"Foot height must reach the shader for bridge rejection"
|
||||||
|
)
|
||||||
|
field.advance(0.08, Vector3(300, 0, 300), {})
|
||||||
|
_check((field.get("_cells") as Dictionary).is_empty(), "Off-window trails must be discarded")
|
||||||
|
positions[1] = Vector3(2.25, 0, 0.25)
|
||||||
|
field.advance(0.08, Vector3.ZERO, positions)
|
||||||
|
_check(
|
||||||
|
_sample(field, Vector2(1.25, 0.25)).b == 0,
|
||||||
|
"Re-entering actors must not connect to stale positions"
|
||||||
|
)
|
||||||
|
field.clear()
|
||||||
|
_check(_sample(field, Vector2(2.25, 0.25)).b == 0, "Reset must clear the uploaded field")
|
||||||
|
_check(
|
||||||
|
(field.get("_previous_positions") as Dictionary).is_empty(),
|
||||||
|
"Reset must discard actor history"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _check_loaded_binding() -> void:
|
||||||
|
var stage := Node3D.new()
|
||||||
|
root.add_child(stage)
|
||||||
|
var camera := Camera3D.new()
|
||||||
|
stage.add_child(camera)
|
||||||
|
camera.current = true
|
||||||
|
var simulation := RestoreSource.new()
|
||||||
|
simulation.name = "SimulationManager"
|
||||||
|
stage.add_child(simulation)
|
||||||
|
var world := Node3D.new()
|
||||||
|
stage.add_child(world)
|
||||||
|
var terrain := Node3D.new()
|
||||||
|
world.add_child(terrain)
|
||||||
|
var patch := (
|
||||||
|
load("res://world/jajce/StylizedBerryPatch.tscn").instantiate() as StylizedBerryPatch
|
||||||
|
)
|
||||||
|
world.add_child(patch)
|
||||||
|
patch.scale = Vector3(1.2, 0.8, 1.1)
|
||||||
|
patch.rotation.y = 0.6
|
||||||
|
var controller := Controller.new()
|
||||||
|
controller.grass_material = (
|
||||||
|
load("res://world/jajce/materials/cozy_grass_material.tres").duplicate(true)
|
||||||
|
)
|
||||||
|
terrain.add_child(controller)
|
||||||
|
var foreign := Node3D.new()
|
||||||
|
root.add_child(foreign)
|
||||||
|
foreign.add_to_group("grass_interactors")
|
||||||
|
var actors: Array[Node3D] = []
|
||||||
|
for index in 12:
|
||||||
|
var actor := Node3D.new()
|
||||||
|
stage.add_child(actor)
|
||||||
|
actor.position.x = 1.0 + index
|
||||||
|
actor.add_to_group("grass_interactors")
|
||||||
|
actors.append(actor)
|
||||||
|
await process_frame
|
||||||
|
controller.set_process(false)
|
||||||
|
controller.call("_refresh_interactors", 0.08)
|
||||||
|
var material := controller.grass_material
|
||||||
|
var positions: PackedVector3Array = material.get_shader_parameter("interactor_positions")
|
||||||
|
_check(
|
||||||
|
int(material.get_shader_parameter("interactor_count")) == 8,
|
||||||
|
"Contact work must respect the eight-actor cap"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
positions[0] == actors[0].position,
|
||||||
|
"A closer actor in another scene container must be rejected"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
bool(material.get_shader_parameter("foliage_trails_enabled")),
|
||||||
|
"Loaded grass must enable its trail field"
|
||||||
|
)
|
||||||
|
var leaves := patch.get_node("Leaves") as MultiMeshInstance3D
|
||||||
|
var berries := patch.get_node("Berries") as MultiMeshInstance3D
|
||||||
|
var leaf_material := leaves.multimesh.mesh.surface_get_material(0) as ShaderMaterial
|
||||||
|
var berry_material := berries.multimesh.mesh.surface_get_material(0) as ShaderMaterial
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
leaf_material.get_shader_parameter("foliage_trail_map")
|
||||||
|
== material.get_shader_parameter("foliage_trail_map")
|
||||||
|
)
|
||||||
|
and (
|
||||||
|
berry_material.get_shader_parameter("foliage_trail_map")
|
||||||
|
== material.get_shader_parameter("foliage_trail_map")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"Bush leaves, berries and meadow must use the same world-local footprint field"
|
||||||
|
)
|
||||||
|
var visible_berries := patch.get_visible_berry_count()
|
||||||
|
actors[0].hide()
|
||||||
|
controller.primary_interactor_path = controller.get_path_to(actors.back())
|
||||||
|
controller.max_interactors = 4
|
||||||
|
controller.call("_refresh_interactors", 0.12)
|
||||||
|
positions = material.get_shader_parameter("interactor_positions")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
int(material.get_shader_parameter("interactor_count")) == 4
|
||||||
|
and positions[0].x == 12
|
||||||
|
and positions[1].x == 2
|
||||||
|
),
|
||||||
|
"Balanced must reserve the primary actor slot, cap actors and ignore hidden visuals"
|
||||||
|
)
|
||||||
|
simulation.state_restored.emit()
|
||||||
|
_check(
|
||||||
|
not bool(material.get_shader_parameter("foliage_trails_enabled")),
|
||||||
|
"Successful restores must clear visual trails immediately"
|
||||||
|
)
|
||||||
|
controller.set_interaction_enabled(false)
|
||||||
|
_check(not controller.is_processing(), "Low quality must stop interaction updates")
|
||||||
|
_check(
|
||||||
|
not bool(leaf_material.get_shader_parameter("foliage_trails_enabled")),
|
||||||
|
"Low quality must release bushes as well as grass"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
int(material.get_shader_parameter("interactor_count")) == 0,
|
||||||
|
"Low quality must clear direct contacts"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
patch.get_visible_berry_count() == visible_berries,
|
||||||
|
"Contact bending must not change resource presentation amounts"
|
||||||
|
)
|
||||||
|
controller.set_interaction_enabled(true)
|
||||||
|
controller.call("_refresh_interactors", 0.08)
|
||||||
|
_check(
|
||||||
|
bool(berry_material.get_shader_parameter("foliage_trails_enabled")),
|
||||||
|
"Bush interaction must resume after Low quality"
|
||||||
|
)
|
||||||
|
var other_field := FoliageTrailMap.new()
|
||||||
|
_check(
|
||||||
|
other_field.texture != material.get_shader_parameter("foliage_trail_map"),
|
||||||
|
"Each world must own a distinct trail texture"
|
||||||
|
)
|
||||||
|
foreign.free()
|
||||||
|
stage.free()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _sample(field: FoliageTrailMap, position: Vector2) -> Color:
|
||||||
|
var image := field.get("_image") as Image
|
||||||
|
var pixel := Vector2i(
|
||||||
|
(position - Vector2(field.world_rect.x, field.world_rect.y)) / field.CELL_SIZE
|
||||||
|
)
|
||||||
|
return image.get_pixelv(pixel)
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||