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 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/` |
|
||||
| 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` |
|
||||
| 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
|
||||
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 |
|
||||
| Balanced (default) | 0.85 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 |
|
||||
| High | authored / authored 4× | authored (currently 180 m) | authored when renderer supports it | authored density/interactors |
|
||||
| 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 / 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
|
||||
values; viewport scale restoration is guarded by the current quality owner so a
|
||||
second world cannot clobber the active owner. Compatibility/mobile feature
|
||||
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
|
||||
|
||||
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;
|
||||
they no longer rebuild all candidate data every rendered frame.
|
||||
- `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),
|
||||
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"]
|
||||
|
||||
[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"]
|
||||
albedo_color = Color(0.72, 0.74, 0.78, 1)
|
||||
roughness = 0.32
|
||||
@@ -85,67 +20,29 @@ top_radius = 0.045
|
||||
bottom_radius = 0.045
|
||||
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"]
|
||||
script = ExtResource("1_visual")
|
||||
|
||||
[node name="Body" type="MeshInstance3D" parent="."]
|
||||
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="."]
|
||||
position = Vector3(0, 1.68, 0)
|
||||
mesh = SubResource("Mesh_head")
|
||||
|
||||
[node name="Hair" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 1.9, -0.015)
|
||||
mesh = SubResource("Mesh_hair")
|
||||
|
||||
[node name="ArmLeft" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-0.39, 1.08, 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")
|
||||
position = Vector3(-0.39, 1.3, 0)
|
||||
|
||||
[node name="ArmRight" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0.39, 1.08, 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")
|
||||
position = Vector3(0.39, 1.3, 0)
|
||||
|
||||
[node name="LegLeft" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-0.18, 0.43, 0)
|
||||
mesh = SubResource("Mesh_leg")
|
||||
|
||||
[node name="Foot" type="MeshInstance3D" parent="LegLeft"]
|
||||
position = Vector3(0, -0.28, 0.09)
|
||||
mesh = SubResource("Mesh_foot")
|
||||
position = Vector3(-0.18, 0.61, 0)
|
||||
|
||||
[node name="LegRight" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0.18, 0.43, 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")
|
||||
position = Vector3(0.18, 0.61, 0)
|
||||
|
||||
[node name="Sword" type="Node3D" parent="."]
|
||||
position = Vector3(0.52, 1.22, 0.12)
|
||||
@@ -158,3 +55,6 @@ mesh = SubResource("Mesh_blade")
|
||||
[node name="Grip" type="MeshInstance3D" parent="Sword"]
|
||||
position = Vector3(0, -0.05, 0)
|
||||
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 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 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 appearance: CharacterAppearanceProfile
|
||||
|
||||
@export var move_speed := 3.0
|
||||
@export var rotation_speed := 8.0
|
||||
@@ -58,6 +59,9 @@ var walk_phase := 0.0
|
||||
var glyph_phase := 0.0
|
||||
var concern_phase := 0.0
|
||||
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:
|
||||
@@ -76,11 +80,13 @@ func _process(delta: float) -> void:
|
||||
return
|
||||
glyph_phase = fmod(glyph_phase + delta * 2.2, TAU)
|
||||
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
|
||||
if opportunity_concern_root.visible:
|
||||
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
|
||||
var concern_scale := 0.96 + sin(concern_phase) * 0.04
|
||||
opportunity_concern_root.scale = Vector3.ONE * concern_scale
|
||||
@@ -92,20 +98,24 @@ func _process(delta: float) -> void:
|
||||
var swing := sin(walk_phase)
|
||||
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)
|
||||
left_arm.rotation_degrees.x = lerpf(left_arm.rotation_degrees.x, -swing * 20.0, blend)
|
||||
right_arm.rotation_degrees.x = lerpf(right_arm.rotation_degrees.x, swing * 20.0, blend)
|
||||
left_arm.rotation_degrees.x = lerpf(
|
||||
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
|
||||
body_mesh.position.y = lerpf(body_mesh.position.y, 1.05 + step_bob, blend)
|
||||
head_mesh.position.y = lerpf(head_mesh.position.y, 1.68 + step_bob, blend)
|
||||
hair_mesh.position.y = lerpf(hair_mesh.position.y, 1.9 + 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, rest_heights.y + step_bob, blend)
|
||||
hair_mesh.position.y = lerpf(hair_mesh.position.y, rest_heights.z + step_bob, blend)
|
||||
else:
|
||||
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)
|
||||
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)
|
||||
body_mesh.position.y = lerpf(body_mesh.position.y, 1.05, blend)
|
||||
head_mesh.position.y = lerpf(head_mesh.position.y, 1.68, blend)
|
||||
hair_mesh.position.y = lerpf(hair_mesh.position.y, 1.9, blend)
|
||||
body_mesh.position.y = lerpf(body_mesh.position.y, rest_heights.x, blend)
|
||||
head_mesh.position.y = lerpf(head_mesh.position.y, rest_heights.y, blend)
|
||||
hair_mesh.position.y = lerpf(hair_mesh.position.y, rest_heights.z, blend)
|
||||
|
||||
|
||||
func setup_from_sim(npc: SimNPC) -> void:
|
||||
@@ -123,16 +133,12 @@ func setup_from_sim(npc: SimNPC) -> void:
|
||||
|
||||
|
||||
func _apply_personal_details() -> void:
|
||||
var hair_colors := [
|
||||
Color(0.16, 0.09, 0.055, 1.0),
|
||||
Color(0.29, 0.16, 0.075, 1.0),
|
||||
Color(0.09, 0.065, 0.05, 1.0),
|
||||
Color(0.42, 0.27, 0.12, 1.0),
|
||||
]
|
||||
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
|
||||
AnimalAppearance.apply_to(self, sim_id, true, appearance)
|
||||
rest_heights = AnimalAppearance.rest_heights(self, true)
|
||||
arm_swing = get_meta("arm_swing", 1.0)
|
||||
cue_lift = maxf(0.0, float(get_meta("character_top", 2.6)) - 2.6)
|
||||
task_glyph_root.position.y = 2.9 + cue_lift
|
||||
profession_label.position.y = 2.8 + cue_lift
|
||||
|
||||
|
||||
func _apply_profession_presentation() -> void:
|
||||
@@ -142,23 +148,31 @@ func _apply_profession_presentation() -> void:
|
||||
profession_prop.visible = false
|
||||
return
|
||||
|
||||
var body_material := StandardMaterial3D.new()
|
||||
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
|
||||
AnimalAppearance.set_profession_color(self, definition.visual_color)
|
||||
|
||||
var prop_material := StandardMaterial3D.new()
|
||||
prop_material.albedo_color = definition.prop_color
|
||||
prop_material.roughness = 0.78
|
||||
profession_prop.material_override = prop_material
|
||||
profession_prop.mesh = _create_profession_prop_mesh()
|
||||
_fit_carried_props()
|
||||
profession_prop.visible = profession_prop.mesh != null
|
||||
profession_label.text = "%s\n%s" % [npc_name, definition.display_name]
|
||||
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:
|
||||
profession_prop.position = Vector3(0.48, 0.85, 0.0)
|
||||
profession_prop.rotation_degrees = Vector3.ZERO
|
||||
@@ -218,7 +232,7 @@ func set_opportunity_concern_visible(is_visible: bool) -> void:
|
||||
if opportunity_concern_root == null:
|
||||
return
|
||||
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.scale = Vector3.ONE
|
||||
if is_visible:
|
||||
@@ -236,7 +250,7 @@ func play_trust_gain_reaction() -> void:
|
||||
return
|
||||
_stop_trust_gain_reaction()
|
||||
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.scale = TRUST_REACTION_START_SCALE
|
||||
_set_trust_reaction_transparency(1.0)
|
||||
@@ -248,14 +262,14 @@ func play_trust_gain_reaction() -> void:
|
||||
trust_reaction_tween.tween_property(
|
||||
trust_reaction_root,
|
||||
"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
|
||||
)
|
||||
trust_reaction_tween.parallel().tween_property(trust_reaction_root, "rotation:y", 0.32, 0.68)
|
||||
trust_reaction_tween.tween_property(
|
||||
trust_reaction_root,
|
||||
"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
|
||||
)
|
||||
trust_reaction_tween.parallel().tween_property(
|
||||
@@ -272,7 +286,7 @@ func _set_trust_reaction_transparency(value: float) -> void:
|
||||
|
||||
func _finish_trust_gain_reaction() -> void:
|
||||
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.scale = TRUST_REACTION_START_SCALE
|
||||
_set_trust_reaction_transparency(1.0)
|
||||
@@ -286,7 +300,7 @@ func _stop_trust_gain_reaction() -> void:
|
||||
if trust_reaction_root == null:
|
||||
return
|
||||
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.scale = TRUST_REACTION_START_SCALE
|
||||
_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"]
|
||||
|
||||
[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"]
|
||||
albedo_color = Color(0.38, 0.22, 0.08, 1)
|
||||
roughness = 0.9
|
||||
@@ -139,62 +78,29 @@ shape = SubResource("CapsuleShape3D_d844k")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 1.05, 0)
|
||||
mesh = SubResource("CylinderMesh_body")
|
||||
|
||||
[node name="Head" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 1.68, 0)
|
||||
mesh = SubResource("SphereMesh_head")
|
||||
|
||||
[node name="Hair" type="MeshInstance3D" parent="."]
|
||||
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="."]
|
||||
position = Vector3(-0.39, 1.08, 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")
|
||||
position = Vector3(-0.39, 1.3, 0)
|
||||
|
||||
[node name="ArmRight" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0.39, 1.08, 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")
|
||||
position = Vector3(0.39, 1.3, 0)
|
||||
|
||||
[node name="LegLeft" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-0.18, 0.43, 0)
|
||||
mesh = SubResource("CapsuleMesh_leg")
|
||||
|
||||
[node name="Foot" type="MeshInstance3D" parent="LegLeft"]
|
||||
position = Vector3(0, -0.28, 0.09)
|
||||
mesh = SubResource("BoxMesh_foot")
|
||||
position = Vector3(-0.18, 0.61, 0)
|
||||
|
||||
[node name="LegRight" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0.18, 0.43, 0)
|
||||
mesh = SubResource("CapsuleMesh_leg")
|
||||
|
||||
[node name="Foot" type="MeshInstance3D" parent="LegRight"]
|
||||
position = Vector3(0, -0.28, 0.09)
|
||||
mesh = SubResource("BoxMesh_foot")
|
||||
position = Vector3(0.18, 0.61, 0)
|
||||
|
||||
[node name="ProfessionProp" type="MeshInstance3D" parent="."]
|
||||
|
||||
[node name="ProfessionLabel" type="Label3D" parent="."]
|
||||
position = Vector3(0, 2.05, 0)
|
||||
position = Vector3(0, 2.8, 0)
|
||||
billboard = 1
|
||||
no_depth_test = true
|
||||
text = "Villager"
|
||||
@@ -223,13 +129,13 @@ mesh = SubResource("CylinderMesh_wood")
|
||||
|
||||
[node name="TaskGlyphRoot" type="Node3D" parent="."]
|
||||
visible = false
|
||||
position = Vector3(0, 2.35, 0)
|
||||
position = Vector3(0, 2.9, 0)
|
||||
|
||||
[node name="TaskGlyph" type="MeshInstance3D" parent="TaskGlyphRoot"]
|
||||
|
||||
[node name="OpportunityConcernRoot" type="Node3D" parent="."]
|
||||
visible = false
|
||||
position = Vector3(-0.4, 2.72, 0)
|
||||
position = Vector3(-0.4, 3.25, 0)
|
||||
|
||||
[node name="EmptyBowl" type="MeshInstance3D" parent="OpportunityConcernRoot"]
|
||||
rotation_degrees = Vector3(72, 0, 0)
|
||||
@@ -250,7 +156,7 @@ mesh = SubResource("SphereMesh_concern")
|
||||
|
||||
[node name="TrustReactionRoot" type="Node3D" parent="."]
|
||||
visible = false
|
||||
position = Vector3(0, 2.72, 0)
|
||||
position = Vector3(0, 3.25, 0)
|
||||
scale = Vector3(0.24, 0.24, 0.24)
|
||||
|
||||
[node name="Halo" type="MeshInstance3D" parent="TrustReactionRoot"]
|
||||
@@ -278,3 +184,6 @@ path_desired_distance = 0.3
|
||||
target_desired_distance = 0.5
|
||||
height = 1.7
|
||||
radius = 0.4
|
||||
|
||||
[node name="Tail" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 0.8, -0.22)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
extends Node3D
|
||||
|
||||
@export var appearance: CharacterAppearanceProfile = preload(
|
||||
"res://assets/characters/profiles/reference_cat.tres"
|
||||
)
|
||||
|
||||
@onready var body_mesh: MeshInstance3D = $Body
|
||||
@onready var head_mesh: MeshInstance3D = $Head
|
||||
@onready var hair_mesh: MeshInstance3D = $Hair
|
||||
@@ -10,10 +14,15 @@ extends Node3D
|
||||
|
||||
var player_body: CharacterBody3D
|
||||
var walk_phase := 0.0
|
||||
var rest_heights := Vector3(1.05, 1.68, 1.9)
|
||||
var arm_swing := 1.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
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:
|
||||
@@ -26,18 +35,22 @@ func _process(delta: float) -> void:
|
||||
var swing := sin(walk_phase)
|
||||
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)
|
||||
left_arm.rotation_degrees.x = lerpf(left_arm.rotation_degrees.x, -swing * 22.0, blend)
|
||||
right_arm.rotation_degrees.x = lerpf(right_arm.rotation_degrees.x, swing * 22.0, blend)
|
||||
left_arm.rotation_degrees.x = lerpf(
|
||||
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
|
||||
body_mesh.position.y = lerpf(body_mesh.position.y, 1.05 + step_bob, blend)
|
||||
head_mesh.position.y = lerpf(head_mesh.position.y, 1.68 + step_bob, blend)
|
||||
hair_mesh.position.y = lerpf(hair_mesh.position.y, 1.9 + 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, rest_heights.y + step_bob, blend)
|
||||
hair_mesh.position.y = lerpf(hair_mesh.position.y, rest_heights.z + step_bob, blend)
|
||||
return
|
||||
|
||||
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)
|
||||
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)
|
||||
body_mesh.position.y = lerpf(body_mesh.position.y, 1.05, blend)
|
||||
head_mesh.position.y = lerpf(head_mesh.position.y, 1.68, blend)
|
||||
hair_mesh.position.y = lerpf(hair_mesh.position.y, 1.9, blend)
|
||||
body_mesh.position.y = lerpf(body_mesh.position.y, rest_heights.x, blend)
|
||||
head_mesh.position.y = lerpf(head_mesh.position.y, rest_heights.y, 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)
|
||||
]
|
||||
}
|
||||
interact={
|
||||
interact={
|
||||
"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)
|
||||
]
|
||||
@@ -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)
|
||||
]
|
||||
}
|
||||
|
||||
[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)
|
||||