feat(art): add storybook meadow and woodland villagers
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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).
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
@@ -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
|
||||
@@ -51,6 +51,25 @@ selection never forces unsupported volumetric fog on.
|
||||
|
||||
## 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 static procedural cloud sky, 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).
|
||||
|
||||
`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.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 809 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 798 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
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"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
+8
-108
@@ -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)
|
||||
|
||||
+5
-19
@@ -4,9 +4,9 @@ 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
|
||||
|
||||
@@ -76,7 +76,7 @@ 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 + 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)
|
||||
@@ -123,16 +123,7 @@ 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)
|
||||
|
||||
|
||||
func _apply_profession_presentation() -> void:
|
||||
@@ -142,12 +133,7 @@ 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
|
||||
|
||||
+12
-103
@@ -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)
|
||||
|
||||
@@ -14,6 +14,7 @@ var walk_phase := 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
player_body = get_parent() as CharacterBody3D
|
||||
AnimalAppearance.apply_to(self, 0)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
|
||||
@@ -535,7 +535,7 @@ func _run() -> void:
|
||||
(
|
||||
main_scene.has_node("Player/Visual/Body")
|
||||
and main_scene.has_node("Player/Visual/Head")
|
||||
and main_scene.has_node("Player/Visual/Scarf")
|
||||
and main_scene.get_node("Player/Visual").get_meta("animal_species") == &"fox"
|
||||
),
|
||||
"Player should use the same readable multi-part visual language as villagers"
|
||||
)
|
||||
|
||||
@@ -89,6 +89,11 @@ func _run() -> void:
|
||||
"Village center should include a few oversized ambient butterflies"
|
||||
)
|
||||
var grass_field := world.get_node("TerrainRoot/Terrain3D/CozyGrassField")
|
||||
var grass_mesh := grass_field.get("mesh") as ArrayMesh
|
||||
_check(
|
||||
grass_mesh != null and grass_mesh.get_faces().size() == 105,
|
||||
"Meadow grass must reuse the 35-triangle Blender tuft"
|
||||
)
|
||||
var grass_material := grass_field.get("mesh_material_override") as ShaderMaterial
|
||||
var grass_process := grass_field.get("process_material") as ShaderMaterial
|
||||
_check(
|
||||
@@ -112,6 +117,10 @@ func _run() -> void:
|
||||
"Grass placement should stay short enough to preserve villagers and paths"
|
||||
)
|
||||
var grass_controller := world.get_node("TerrainRoot/GrassInteractionController")
|
||||
_check(
|
||||
grass_material.get_shader_parameter("clearing_count") == 4,
|
||||
"Grass must leave the four authored walking paths clear"
|
||||
)
|
||||
_check(
|
||||
grass_controller.get("grass_material") == grass_material,
|
||||
"Grass interaction should update the same material rendered by Terrain3D particles"
|
||||
|
||||
@@ -26,17 +26,28 @@ func _run() -> void:
|
||||
var trust_reaction_root: Node3D = visual.get_node("TrustReactionRoot")
|
||||
_check(not trust_reaction_root.visible, "A newly spawned visual should not replay reactions")
|
||||
_check(
|
||||
body.material_override.albedo_color == profession_definition.visual_color,
|
||||
"NPC body color should come from its profession definition"
|
||||
(
|
||||
body.material_override is ShaderMaterial
|
||||
and (
|
||||
body.material_override.get_shader_parameter("coat_color")
|
||||
== profession_definition.visual_color
|
||||
)
|
||||
),
|
||||
"NPC cel material should retain its authoritative profession color"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
body.mesh is CylinderMesh
|
||||
body.mesh is ArrayMesh
|
||||
and visual.has_node("Head")
|
||||
and visual.has_node("ArmLeft")
|
||||
and visual.has_node("LegRight/Foot")
|
||||
and visual.get_node("Tail").mesh is ArrayMesh
|
||||
),
|
||||
"NPC presentation should use a readable multi-part villager silhouette"
|
||||
"NPC presentation should use the Blender animal body and tail"
|
||||
)
|
||||
var original_species: StringName = visual.get_meta("animal_species")
|
||||
_check(
|
||||
visual.get_node("TaskGlyphRoot").position.y >= 2.9,
|
||||
"Task cues must sit above the animal cast's tall ears"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
@@ -184,6 +195,10 @@ func _run() -> void:
|
||||
_check(view.reload_npc_visual(npc.id), "Visual should reload")
|
||||
var reloaded: Node3D = view.active_npc_visuals[npc.id]
|
||||
reloaded.set_physics_process(false)
|
||||
_check(
|
||||
reloaded.get_meta("animal_species") == original_species,
|
||||
"Reload must derive the same animal identity without saved appearance or RNG"
|
||||
)
|
||||
_check(
|
||||
reloaded.global_position.is_equal_approx(authoritative_position),
|
||||
"Reloaded visual should use authoritative position"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
"""Rebuild the original woodland cast and meadow tuft with Blender 5.1+.
|
||||
|
||||
Blender --background --python tools/art/build_storybook_assets.py
|
||||
Editable .blend sources are outside Godot's import tree. Runtime GLBs contain
|
||||
vertex colors, one material and eight rigid animation parts per character.
|
||||
Coordinates below use Godot's Y-up, +Z-facing convention; export converts it.
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE = ROOT / "art/storybook"
|
||||
OUTPUT = ROOT / "assets/storybook"
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
SOURCE.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
INK = "303733"
|
||||
CREAM = "f4e5bf"
|
||||
PINK = "c98678"
|
||||
LEATHER = "654933"
|
||||
GOLD = "d9aa54"
|
||||
PIVOTS = {
|
||||
"Body": (0, 1.05, 0), "Head": (0, 1.68, 0), "Hair": (0, 1.9, 0),
|
||||
"ArmLeft": (-0.39, 1.30, 0), "ArmRight": (0.39, 1.30, 0),
|
||||
"LegLeft": (-0.18, 0.61, 0), "LegRight": (0.18, 0.61, 0),
|
||||
"Tail": (0, 0.8, -0.22),
|
||||
}
|
||||
PARTS = {}
|
||||
STATS = {}
|
||||
|
||||
|
||||
def coord(v):
|
||||
return Vector((v[0], -v[2], v[1]))
|
||||
|
||||
|
||||
def linear(hex_color):
|
||||
values = [int(hex_color[i:i + 2], 16) / 255 for i in (0, 2, 4)]
|
||||
return tuple(v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4 for v in values)
|
||||
|
||||
|
||||
def paint(obj, color, part, cloth=False):
|
||||
attr = obj.data.color_attributes.new(name="Color", type="FLOAT_COLOR", domain="CORNER")
|
||||
rgb = linear(color)
|
||||
for entry in attr.data:
|
||||
entry.color = (*rgb, 1.0 if cloth else 0.0)
|
||||
obj.data.materials.append(bpy.data.materials["StorybookPalette"])
|
||||
for poly in obj.data.polygons:
|
||||
poly.use_smooth = True
|
||||
PARTS.setdefault(part, []).append(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def ellipsoid(part, center, radius, color, cloth=False, tilt=0, segments=16, rings=10):
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(segments=segments, ring_count=rings, location=coord(center))
|
||||
obj = bpy.context.object
|
||||
obj.scale = (radius[0], radius[2], radius[1])
|
||||
obj.rotation_euler.y = math.radians(tilt)
|
||||
return paint(obj, color, part, cloth)
|
||||
|
||||
|
||||
def tube(part, points, radius, color, cloth=False, sides=8):
|
||||
verts, faces = [], []
|
||||
for i, point in enumerate(points):
|
||||
p = coord(point)
|
||||
tangent = coord(points[min(i + 1, len(points) - 1)]) - coord(points[max(0, i - 1)])
|
||||
tangent.normalize()
|
||||
side = tangent.cross(Vector((0, 1, 0)))
|
||||
if side.length < 0.01:
|
||||
side = tangent.cross(Vector((1, 0, 0)))
|
||||
side.normalize()
|
||||
other = tangent.cross(side).normalized()
|
||||
r = radius[i] if isinstance(radius, list) else radius
|
||||
for j in range(sides):
|
||||
a = math.tau * j / sides
|
||||
verts.append(p + r * (math.cos(a) * side + math.sin(a) * other))
|
||||
if i:
|
||||
for j in range(sides):
|
||||
a, b = (i - 1) * sides + j, (i - 1) * sides + (j + 1) % sides
|
||||
faces.append((a, b, b + sides, a + sides))
|
||||
faces.extend([tuple(reversed(range(sides))), tuple(range(len(verts) - sides, len(verts)))])
|
||||
mesh = bpy.data.meshes.new(part + "Detail")
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
obj = bpy.data.objects.new(part + "Detail", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
return paint(obj, color, part, cloth)
|
||||
|
||||
|
||||
def leaf_ear(part, x, bottom, height, width, fur, tilt):
|
||||
ellipsoid(part, (x, bottom + height * 0.46, -0.025), (width, height * 0.55, width * 0.57), fur, tilt=tilt)
|
||||
ellipsoid(part, (x, bottom + height * 0.48, width * 0.48), (width * 0.58, height * 0.39, 0.024), PINK, tilt=tilt, segments=12, rings=8)
|
||||
|
||||
|
||||
def reset():
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.object.delete(use_global=False)
|
||||
PARTS.clear()
|
||||
material = bpy.data.materials.get("StorybookPalette") or bpy.data.materials.new("StorybookPalette")
|
||||
material.use_nodes = True
|
||||
tree = material.node_tree
|
||||
tree.nodes.clear()
|
||||
vertex = tree.nodes.new("ShaderNodeVertexColor")
|
||||
vertex.layer_name = "Color"
|
||||
shader = tree.nodes.new("ShaderNodeBsdfPrincipled")
|
||||
shader.inputs["Roughness"].default_value = 0.88
|
||||
tree.links.new(vertex.outputs["Color"], shader.inputs["Base Color"])
|
||||
output = tree.nodes.new("ShaderNodeOutputMaterial")
|
||||
tree.links.new(shader.outputs["BSDF"], output.inputs["Surface"])
|
||||
|
||||
|
||||
def finish(name):
|
||||
objects = []
|
||||
for part, pieces in PARTS.items():
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj in pieces:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = pieces[0]
|
||||
bpy.ops.object.join()
|
||||
obj = bpy.context.object
|
||||
obj.name = part
|
||||
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
|
||||
bpy.context.scene.cursor.location = coord(PIVOTS.get(part, (0, 0, 0)))
|
||||
bpy.ops.object.origin_set(type="ORIGIN_CURSOR")
|
||||
# Join left duplicate palette slots; one surface per animated part.
|
||||
obj.data.materials.clear()
|
||||
obj.data.materials.append(bpy.data.materials["StorybookPalette"])
|
||||
for polygon in obj.data.polygons:
|
||||
polygon.material_index = 0
|
||||
obj.data.calc_loop_triangles()
|
||||
objects.append(obj)
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=str(OUTPUT / (name + ".glb")), export_format="GLB",
|
||||
use_selection=True, export_yup=True, export_animations=False,
|
||||
export_vertex_color="ACTIVE", export_all_vertex_colors=False,
|
||||
export_materials="EXPORT", export_extras=False, export_cameras=False,
|
||||
)
|
||||
bpy.context.scene.cursor.location = (0, 0, 0)
|
||||
# A useful material-colored editing view on reopening the source.
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
area.spaces.active.shading.type = "MATERIAL"
|
||||
bpy.context.preferences.filepaths.save_version = 0
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(SOURCE / (name + ".blend")))
|
||||
STATS[name] = {"triangles": sum(len(o.data.loop_triangles) for o in objects),
|
||||
"surfaces": len(objects), "bytes": (OUTPUT / (name + ".glb")).stat().st_size}
|
||||
|
||||
|
||||
def character(species, fur, coat, scarf):
|
||||
reset()
|
||||
stout = 1.13 if species == "badger" else (0.90 if species == "rabbit" else 1.0)
|
||||
# Pear-shaped coat, soft collar, pockets, rolled hems and hand-sewn buttons.
|
||||
ellipsoid("Body", (0, 0.98, 0), (0.40 * stout, 0.50, 0.29), coat, True)
|
||||
ellipsoid("Body", (0, 1.25, 0), (0.30 * stout, 0.28, 0.23), coat, True)
|
||||
tube("Body", [(-0.22, 1.40, 0.14), (0, 1.34, 0.245), (0.23, 1.40, 0.13)], 0.065, scarf)
|
||||
ellipsoid("Body", (0.17, 1.23, 0.287), (0.085, 0.22, 0.025), scarf, tilt=-12)
|
||||
for y in (0.83, 1.0, 1.17):
|
||||
ellipsoid("Body", (-0.045, y, 0.292), (0.032, 0.032, 0.022), GOLD, segments=8, rings=6)
|
||||
for x in (-0.235, 0.235):
|
||||
ellipsoid("Body", (x, 0.83, 0.243), (0.108, 0.10, 0.022), coat, True, tilt=x * 28, segments=12, rings=8)
|
||||
tube("Body", [(x - 0.07, 0.895, 0.266), (x + 0.07, 0.895, 0.266)], 0.012, GOLD, sides=5)
|
||||
# Leather sling and tiny clasp. The inventory props remain state-controlled.
|
||||
tube("Body", [(-0.26, 1.39, 0.18), (-0.06, 1.16, 0.302), (0.21, 0.88, 0.29), (0.34, 0.73, 0.07)], 0.027, LEATHER)
|
||||
ellipsoid("Body", (0.37, 0.74, 0.04), (0.15, 0.18, 0.12), LEATHER)
|
||||
ellipsoid("Body", (0.39, 0.80, 0.15), (0.04, 0.035, 0.015), GOLD, segments=8, rings=6)
|
||||
for side, x in (("Left", -0.39), ("Right", 0.39)):
|
||||
ellipsoid("Arm" + side, (x, 1.10, 0), (0.145, 0.31, 0.145), coat, True, tilt=-x * 10)
|
||||
ellipsoid("Arm" + side, (x, 0.82, 0.018), (0.145, 0.065, 0.148), scarf)
|
||||
ellipsoid("Arm" + side, (x, 0.73, 0.024), (0.123, 0.115, 0.12), fur)
|
||||
ellipsoid("Arm" + side, (x * 0.78, 0.745, 0.075), (0.055, 0.066, 0.05), fur, segments=10, rings=6)
|
||||
for side, x in (("Left", -0.18), ("Right", 0.18)):
|
||||
ellipsoid("Leg" + side, (x, 0.41, 0), (0.137, 0.28, 0.135), "52605a")
|
||||
ellipsoid("Leg" + side, (x, 0.14, 0.075), (0.16, 0.13, 0.225), LEATHER)
|
||||
tube("Leg" + side, [(x - 0.09, 0.22, 0.19), (x + 0.09, 0.22, 0.19)], 0.014, GOLD, sides=5)
|
||||
# A broad face, cheek planes and projecting muzzle, not a capsule head.
|
||||
head_w = 0.37 * stout
|
||||
ellipsoid("Head", (0, 1.76, 0), (head_w, 0.34, 0.29), fur)
|
||||
if species == "fox":
|
||||
for s in (-1, 1):
|
||||
tube("Head", [(s * 0.23, 1.99, 0), (s * 0.29, 2.19, -0.03), (s * 0.33, 2.39, -0.055)], [0.135, 0.095, 0.005], fur, sides=8)
|
||||
tube("Head", [(s * 0.24, 2.05, 0.105), (s * 0.30, 2.25, 0.02)], [0.06, 0.006], PINK, sides=6)
|
||||
ellipsoid("Head", (s * 0.19, 1.63, 0.21), (0.19, 0.15, 0.16), CREAM, tilt=s * 18)
|
||||
ellipsoid("Head", (0, 1.69, 0.29), (0.16, 0.105, 0.22), CREAM)
|
||||
tube("Tail", [(0, 0.81, -0.2), (0.17, 0.52, -0.49), (0.44, 0.54, -0.76), (0.56, 0.82, -0.76)], [0.12, 0.23, 0.24, 0.17], fur, sides=12)
|
||||
tube("Tail", [(0.53, 0.74, -0.78), (0.57, 0.95, -0.72), (0.48, 1.12, -0.60)], [0.195, 0.15, 0.003], CREAM, sides=12)
|
||||
elif species == "rabbit":
|
||||
leaf_ear("Head", -0.19, 1.98, 0.70, 0.10, fur, -12)
|
||||
leaf_ear("Head", 0.21, 1.96, 0.62, 0.12, fur, 22)
|
||||
ellipsoid("Tail", (0, 0.74, -0.36), (0.19, 0.18, 0.19), CREAM)
|
||||
elif species == "badger":
|
||||
for s in (-1, 1):
|
||||
ellipsoid("Head", (s * 0.30, 2.01, -0.02), (0.125, 0.145, 0.09), INK)
|
||||
ellipsoid("Head", (s * 0.30, 2.025, 0.053), (0.07, 0.085, 0.025), CREAM)
|
||||
ellipsoid("Head", (s * 0.19, 1.82, 0.226), (0.10, 0.25, 0.093), INK, tilt=s * -18)
|
||||
ellipsoid("Head", (0, 1.68, 0.32), (0.22, 0.14, 0.16), CREAM)
|
||||
ellipsoid("Tail", (0, 0.67, -0.34), (0.13, 0.25, 0.16), fur)
|
||||
else:
|
||||
for s in (-1, 1):
|
||||
ellipsoid("Head", (s * 0.31, 1.94, 0), (0.10, 0.115, 0.075), fur)
|
||||
ellipsoid("Head", (s * 0.32, 1.94, 0.063), (0.054, 0.064, 0.025), PINK)
|
||||
tube("Tail", [(0, 0.80, -0.22), (0, 0.46, -0.52), (0.18, 0.22, -0.78), (0.37, 0.19, -0.81)], [0.16, 0.15, 0.09, 0.008], fur, sides=10)
|
||||
if species != "fox":
|
||||
for s in (-1, 1):
|
||||
ellipsoid("Head", (s * 0.09, 1.64, 0.282), (0.15, 0.10, 0.115), CREAM)
|
||||
nose_z = 0.48 if species == "fox" else 0.408
|
||||
ellipsoid("Head", (0, 1.706, nose_z), (0.058, 0.043, 0.047), PINK if species == "rabbit" else INK, segments=12, rings=8)
|
||||
for s in (-1, 1):
|
||||
eye_x = s * (0.16 if species != "badger" else 0.20)
|
||||
eye_z = 0.33 if species == "badger" else 0.26
|
||||
ellipsoid("Head", (eye_x, 1.845, eye_z), (0.049, 0.061, 0.037), INK)
|
||||
ellipsoid("Head", (eye_x - 0.012, 1.868, eye_z + 0.03), (0.013, 0.018, 0.012), CREAM, segments=8, rings=6)
|
||||
tube("Head", [(eye_x - 0.047, 1.954, eye_z), (eye_x, 1.97 + s * 0.012, eye_z + 0.01), (eye_x + 0.042, 1.95, eye_z)], 0.017, CREAM if species == "badger" else INK, sides=6)
|
||||
ellipsoid("Head", (s * 0.26, 1.688, 0.248), (0.046, 0.024, 0.025), PINK, segments=10, rings=6)
|
||||
tube("Head", [(0, 1.659, nose_z - 0.008), (s * 0.06, 1.611, nose_z - 0.045), (s * 0.117, 1.636, nose_z - 0.085)], 0.009, INK, sides=5)
|
||||
# Accessories give each silhouette a small joke and a practical occupation.
|
||||
if species == "badger":
|
||||
for s in (-1, 1):
|
||||
points = [(s * 0.20 + math.cos(a * math.tau / 20) * 0.09, 1.845 + math.sin(a * math.tau / 20) * 0.08, 0.373) for a in range(21)]
|
||||
tube("Head", points, 0.009, GOLD, sides=5)
|
||||
tube("Head", [(-0.11, 1.85, 0.373), (0, 1.88, 0.39), (0.11, 1.85, 0.373)], 0.011, GOLD, sides=5)
|
||||
if species in ("otter", "rabbit"):
|
||||
hat_color = "d3b66f" if species == "rabbit" else "777d52"
|
||||
ellipsoid("Hair", (0, 2.025, -0.01), (0.44, 0.055, 0.34), hat_color)
|
||||
ellipsoid("Hair", (0, 2.09, -0.035), (0.28, 0.14, 0.245), hat_color)
|
||||
tube("Hair", [(-0.25, 2.063, 0.09), (0, 2.06, 0.23), (0.24, 2.063, 0.09)], 0.026, scarf)
|
||||
ellipsoid("Hair", (0.25, 2.15, 0.15), (0.038, 0.12, 0.065), "8f9c5a", tilt=40, segments=10, rings=6)
|
||||
else:
|
||||
# A little forelock above the brow, merged into the existing animated slot.
|
||||
ellipsoid("Hair", (-0.04, 2.04, 0.05), (0.11, 0.07, 0.15), fur, tilt=-25, segments=12, rings=8)
|
||||
finish(species)
|
||||
|
||||
|
||||
def grass():
|
||||
reset()
|
||||
rng = random.Random(7103)
|
||||
verts, faces, uv = [], [], []
|
||||
for blade in range(7):
|
||||
angle = blade * 2.399
|
||||
center = Vector((math.cos(angle) * 0.15, math.sin(angle) * 0.15, 0))
|
||||
width = rng.uniform(0.025, 0.055)
|
||||
height = rng.uniform(0.8, 1.0)
|
||||
sideways = Vector((math.cos(angle), math.sin(angle), 0))
|
||||
bend = Vector((math.cos(angle + 0.8), math.sin(angle + 0.8), 0)) * rng.uniform(0.045, 0.12)
|
||||
start = len(verts)
|
||||
for level in (0, 0.42, 0.77):
|
||||
for side in (-1, 1):
|
||||
verts.append(center + Vector((0, 0, height * level)) + bend * level * level + sideways * side * width * (1 - level))
|
||||
uv.append(((side + 1) / 2, level))
|
||||
verts.append(center + Vector((0, 0, height)) + bend)
|
||||
uv.append((0.5, 1))
|
||||
for level in range(2):
|
||||
a = start + level * 2
|
||||
faces.extend([(a, a + 1, a + 2), (a + 1, a + 3, a + 2)])
|
||||
faces.append((start + 4, start + 5, start + 6))
|
||||
mesh = bpy.data.meshes.new("MeadowTuft")
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
layer = mesh.uv_layers.new(name="UVMap")
|
||||
for loop in mesh.loops:
|
||||
layer.data[loop.index].uv = uv[loop.vertex_index]
|
||||
obj = bpy.data.objects.new("Grass", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
paint(obj, "ffffff", "Grass")
|
||||
finish("meadow_grass")
|
||||
|
||||
|
||||
character("fox", "c17b42", "568b82", "d6934d")
|
||||
character("rabbit", "d4c5a5", "839d70", "c68061")
|
||||
character("badger", "b8b9a8", "7c879c", "c5aa6d")
|
||||
character("otter", "947055", "b77c58", "8caa98")
|
||||
grass()
|
||||
(OUTPUT / "mesh_budget.json").write_text(json.dumps(STATS, indent=2) + "\n")
|
||||
print("STORYBOOK ASSET BUDGET", json.dumps(STATS))
|
||||
@@ -0,0 +1,171 @@
|
||||
extends SceneTree
|
||||
## Reproducible native-renderer character and runtime camera review.
|
||||
|
||||
const OUTPUT := "res://docs/baselines/"
|
||||
const CAPTURE_SIZE := Vector2i(1600, 900)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = CAPTURE_SIZE
|
||||
if OS.get_cmdline_user_args().has("--cast"):
|
||||
await _capture_cast()
|
||||
else:
|
||||
await _capture_world()
|
||||
quit(0)
|
||||
|
||||
|
||||
func _capture_world() -> void:
|
||||
var main := load("res://main.tscn").instantiate() as Node3D
|
||||
main.get_node("SimulationManager").set("debug_logs", false)
|
||||
root.add_child(main)
|
||||
await process_frame
|
||||
var manager := main.get_node("SimulationManager")
|
||||
manager.set_process(false)
|
||||
var cycle := main.get_node("JajceWorld/DayNightCycle")
|
||||
cycle.set_process(false)
|
||||
cycle.call("_apply_light_rotation", 0.40)
|
||||
cycle.call("_apply_environment", 0.40)
|
||||
var demo := main.get_node("DemoController")
|
||||
if demo.get("debug_overlay_visible"):
|
||||
demo.call("toggle_debug_overlay")
|
||||
for child in main.get_children():
|
||||
if child is CanvasLayer:
|
||||
child.hide()
|
||||
var rig := main.get_node("CameraRig") as Node3D
|
||||
rig.set_physics_process(false)
|
||||
rig.set_process_unhandled_input(false)
|
||||
var player := main.get_node("Player") as CharacterBody3D
|
||||
for _frame in 30:
|
||||
await process_frame
|
||||
player.set_physics_process(false)
|
||||
var camera := root.get_camera_3d()
|
||||
camera.reparent(main)
|
||||
camera.global_position = player.global_position + Vector3(7.5, 5.5, 10.5)
|
||||
camera.look_at(player.global_position + Vector3(0, 1.1, 0))
|
||||
camera.fov = 48.0
|
||||
for _frame in 90:
|
||||
await process_frame
|
||||
await _save("storybook_gameplay.png")
|
||||
camera.global_position = Vector3(38, 24, 44)
|
||||
camera.look_at(Vector3(-4, 3, 0))
|
||||
camera.fov = 53.0
|
||||
for _frame in 60:
|
||||
await process_frame
|
||||
await _save("storybook_valley.png")
|
||||
var world := main.get_node("JajceWorld") as JajceWorld
|
||||
var samples: Array[Dictionary] = []
|
||||
for quality in [
|
||||
JajceWorld.PresentationQuality.HIGH,
|
||||
JajceWorld.PresentationQuality.BALANCED,
|
||||
JajceWorld.PresentationQuality.LOW
|
||||
]:
|
||||
world.apply_presentation_quality(quality)
|
||||
for _frame in 60:
|
||||
await process_frame
|
||||
var frame_times: Array[float] = []
|
||||
for _frame in 120:
|
||||
var start := Time.get_ticks_usec()
|
||||
await process_frame
|
||||
frame_times.append(float(Time.get_ticks_usec() - start) / 1000.0)
|
||||
frame_times.sort()
|
||||
(
|
||||
samples
|
||||
. append(
|
||||
{
|
||||
"profile": world.get_active_presentation_quality_name(),
|
||||
"frame_wall_p50_ms": frame_times[60],
|
||||
"frame_wall_p95_ms": frame_times[114],
|
||||
"draw_calls":
|
||||
Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME),
|
||||
"primitives":
|
||||
Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME),
|
||||
"objects": Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME),
|
||||
"active_npcs": main.get_node("ActiveNPCs").get_child_count(),
|
||||
}
|
||||
)
|
||||
)
|
||||
var file := FileAccess.open(OUTPUT + "storybook_render_metrics.json", FileAccess.WRITE)
|
||||
(
|
||||
file
|
||||
. store_string(
|
||||
(
|
||||
(
|
||||
JSON
|
||||
. stringify(
|
||||
{
|
||||
"renderer": RenderingServer.get_current_rendering_driver_name(),
|
||||
"resolution": "1600x900",
|
||||
"workload":
|
||||
"Paused static valley; wall-frame latency includes vsync, not isolated GPU time",
|
||||
"samples": samples
|
||||
},
|
||||
"\t"
|
||||
)
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _capture_cast() -> void:
|
||||
var stage := Node3D.new()
|
||||
root.add_child(stage)
|
||||
var environment := WorldEnvironment.new()
|
||||
environment.environment = Environment.new()
|
||||
var env := environment.environment
|
||||
env.background_mode = Environment.BG_COLOR
|
||||
env.background_color = Color("d8e3d5")
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
env.ambient_light_color = Color("bbd2d3")
|
||||
env.ambient_light_energy = 0.65
|
||||
env.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
||||
stage.add_child(environment)
|
||||
var light := DirectionalLight3D.new()
|
||||
light.rotation_degrees = Vector3(-42, -32, 0)
|
||||
light.light_color = Color("fff0cd")
|
||||
light.light_energy = 1.3
|
||||
light.shadow_enabled = true
|
||||
stage.add_child(light)
|
||||
var floor_mesh := MeshInstance3D.new()
|
||||
var plane := PlaneMesh.new()
|
||||
plane.size = Vector2(200, 200)
|
||||
var material := StandardMaterial3D.new()
|
||||
material.albedo_color = Color("bdcbb5")
|
||||
material.roughness = 1.0
|
||||
plane.material = material
|
||||
floor_mesh.mesh = plane
|
||||
stage.add_child(floor_mesh)
|
||||
for index in 4:
|
||||
var actor := load("res://player/PlayerVisual.tscn").instantiate() as Node3D
|
||||
stage.add_child(actor)
|
||||
actor.set_process(false)
|
||||
AnimalAppearance.apply_to(actor, index)
|
||||
actor.get_node("Sword").hide()
|
||||
actor.position = Vector3((float(index) - 1.5) * 2.0, 0, 0)
|
||||
actor.rotation_degrees.y = -12.0 + float(index) * 5.0
|
||||
var camera := Camera3D.new()
|
||||
stage.add_child(camera)
|
||||
camera.position = Vector3(2.2, 3.1, 10.5)
|
||||
camera.look_at(Vector3(0, 1.25, 0))
|
||||
camera.projection = Camera3D.PROJECTION_ORTHOGONAL
|
||||
camera.size = 6.2
|
||||
camera.current = true
|
||||
for _frame in 60:
|
||||
await process_frame
|
||||
await _save("storybook_cast.png")
|
||||
stage.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _save(filename: String) -> void:
|
||||
await RenderingServer.frame_post_draw
|
||||
var capture := root.get_texture().get_image()
|
||||
assert(not capture.is_empty())
|
||||
var error := capture.save_png(OUTPUT + filename)
|
||||
assert(error == OK)
|
||||
print("[TOOL] Saved " + OUTPUT + filename)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bqcj3uman431w
|
||||
@@ -0,0 +1,12 @@
|
||||
extends SceneTree
|
||||
## Run after Blender export and Godot import; the particle field requires a Mesh.
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var scene := load("res://assets/storybook/meadow_grass.glb") as PackedScene
|
||||
var instance := scene.instantiate()
|
||||
var grass := instance.find_child("Grass", true, false) as MeshInstance3D
|
||||
assert(grass != null)
|
||||
var error := ResourceSaver.save(grass.mesh, "res://assets/storybook/meadow_grass.res")
|
||||
instance.free()
|
||||
quit(0 if error == OK else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://blkknmqjyo6uj
|
||||
@@ -1,16 +1,18 @@
|
||||
[gd_scene load_steps=9 format=3]
|
||||
[gd_scene load_steps=10 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_deck"]
|
||||
albedo_color = Color(0.61, 0.53, 0.42, 1)
|
||||
roughness = 0.94
|
||||
[ext_resource type="Shader" path="res://world/presentation/materials/storybook_surface.gdshader" id="story_surface"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="Material_deck"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.61, 0.53, 0.42, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_deck"]
|
||||
material = SubResource("Material_deck")
|
||||
size = Vector3(15.2, 0.32, 3.15)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_stone"]
|
||||
albedo_color = Color(0.48, 0.47, 0.41, 1)
|
||||
roughness = 0.98
|
||||
[sub_resource type="ShaderMaterial" id="Material_stone"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.48, 0.47, 0.41, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_abutment"]
|
||||
material = SubResource("Material_stone")
|
||||
@@ -20,9 +22,9 @@ size = Vector3(2.25, 1.45, 4.2)
|
||||
material = SubResource("Material_stone")
|
||||
size = Vector3(1.05, 1.65, 3.7)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_rail"]
|
||||
albedo_color = Color(0.2, 0.115, 0.055, 1)
|
||||
roughness = 0.92
|
||||
[sub_resource type="ShaderMaterial" id="Material_rail"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.2, 0.115, 0.055, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_rail"]
|
||||
material = SubResource("Material_rail")
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
[gd_scene load_steps=19 format=3]
|
||||
[gd_scene load_steps=21 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_rock"]
|
||||
albedo_color = Color(0.25, 0.27, 0.24, 1)
|
||||
roughness = 1.0
|
||||
[ext_resource type="Shader" path="res://world/presentation/materials/storybook_surface.gdshader" id="story_surface"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="Material_rock"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.25, 0.27, 0.24, 1)
|
||||
|
||||
[sub_resource type="CylinderMesh" id="Mesh_rock_plinth"]
|
||||
material = SubResource("Material_rock")
|
||||
@@ -11,13 +13,13 @@ bottom_radius = 10.2
|
||||
height = 3.2
|
||||
radial_segments = 9
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_stone"]
|
||||
albedo_color = Color(0.47, 0.47, 0.43, 1)
|
||||
roughness = 0.96
|
||||
[sub_resource type="ShaderMaterial" id="Material_stone"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.47, 0.47, 0.43, 1)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_stone_light"]
|
||||
albedo_color = Color(0.57, 0.56, 0.51, 1)
|
||||
roughness = 0.95
|
||||
[sub_resource type="ShaderMaterial" id="Material_stone_light"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.57, 0.56, 0.51, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_keep"]
|
||||
material = SubResource("Material_stone_light")
|
||||
@@ -57,9 +59,9 @@ size = Vector3(1.25, 3.7, 1.7)
|
||||
material = SubResource("Material_stone_light")
|
||||
size = Vector3(4.6, 1.05, 1.7)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_gate"]
|
||||
albedo_color = Color(0.18, 0.11, 0.065, 1)
|
||||
roughness = 0.9
|
||||
[sub_resource type="ShaderMaterial" id="Material_gate"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.18, 0.11, 0.065, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_gate"]
|
||||
material = SubResource("Material_gate")
|
||||
@@ -72,17 +74,17 @@ bottom_radius = 0.09
|
||||
height = 4.2
|
||||
radial_segments = 8
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_banner"]
|
||||
albedo_color = Color(0.58, 0.1, 0.08, 1)
|
||||
roughness = 0.76
|
||||
[sub_resource type="ShaderMaterial" id="Material_banner"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.58, 0.1, 0.08, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_banner"]
|
||||
material = SubResource("Material_banner")
|
||||
size = Vector3(0.12, 2.2, 1.35)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_window_slit"]
|
||||
albedo_color = Color(0.12, 0.14, 0.13, 1)
|
||||
roughness = 0.98
|
||||
[sub_resource type="ShaderMaterial" id="Material_window_slit"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.12, 0.14, 0.13, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_window_slit"]
|
||||
material = SubResource("Material_window_slit")
|
||||
|
||||
@@ -1,18 +1,45 @@
|
||||
extends Node
|
||||
|
||||
const SHADER_INTERACTOR_LIMIT := 8
|
||||
const CLEARING_LIMIT := 4
|
||||
|
||||
@export var grass_material: ShaderMaterial
|
||||
@export_range(1, SHADER_INTERACTOR_LIMIT, 1) var max_interactors := 8
|
||||
@export_range(0.02, 0.5, 0.01) var update_interval := 0.08
|
||||
@export var path_clearings: Array[NodePath] = []
|
||||
|
||||
var _elapsed := 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# JajceWorld first isolates its mutable materials in the parent's _ready().
|
||||
call_deferred("_configure_path_clearings")
|
||||
_refresh_interactors()
|
||||
|
||||
|
||||
func _configure_path_clearings() -> void:
|
||||
if grass_material == null:
|
||||
return
|
||||
var segments := PackedVector4Array()
|
||||
segments.resize(CLEARING_LIMIT)
|
||||
var widths := Vector4.ZERO
|
||||
var count := 0
|
||||
for path in path_clearings.slice(0, CLEARING_LIMIT):
|
||||
var strip := get_node_or_null(path) as MeshInstance3D
|
||||
if strip == null or not strip.mesh is BoxMesh:
|
||||
continue
|
||||
var box := strip.mesh as BoxMesh
|
||||
var half_length := strip.global_basis.z * box.size.z * 0.5
|
||||
var start := strip.global_position - half_length
|
||||
var end := strip.global_position + half_length
|
||||
segments[count] = Vector4(start.x, start.z, end.x, end.z)
|
||||
widths[count] = strip.global_basis.x.length() * box.size.x * 0.5 + 0.12
|
||||
count += 1
|
||||
grass_material.set_shader_parameter("clearing_count", count)
|
||||
grass_material.set_shader_parameter("clearing_segments", segments)
|
||||
grass_material.set_shader_parameter("clearing_half_widths", widths)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_elapsed += delta
|
||||
if _elapsed < update_interval:
|
||||
|
||||
+17
-16
@@ -1,4 +1,4 @@
|
||||
[gd_scene load_steps=62 format=3]
|
||||
[gd_scene load_steps=68 format=3]
|
||||
|
||||
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
|
||||
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
||||
@@ -31,6 +31,9 @@
|
||||
[ext_resource type="PackedScene" path="res://world/animals/goat/dunja_habitat.tscn" id="29_habitat"]
|
||||
[ext_resource type="PackedScene" path="res://world/animals/goat/zora_shelter.tscn" id="30_zora_shelter"]
|
||||
|
||||
[ext_resource type="ArrayMesh" path="res://assets/storybook/meadow_grass.res" id="31_meadow"]
|
||||
[ext_resource type="Shader" path="res://world/jajce/materials/storybook_sky.gdshader" id="32_sky"]
|
||||
|
||||
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
||||
_shader_parameters = {
|
||||
"auto_base_texture": 0,
|
||||
@@ -38,8 +41,8 @@ _shader_parameters = {
|
||||
"auto_slope": 0.62,
|
||||
"blend_sharpness": 0.88,
|
||||
"enable_macro_variation": true,
|
||||
"macro_variation1": Color(0.78, 0.9, 0.42, 1),
|
||||
"macro_variation2": Color(0.55, 0.75, 0.38, 1),
|
||||
"macro_variation1": Color(0.84, 0.95, 0.73, 1),
|
||||
"macro_variation2": Color(0.60, 0.79, 0.61, 1),
|
||||
"macro_variation_slope": 0.48
|
||||
}
|
||||
world_background = 0
|
||||
@@ -172,13 +175,9 @@ size = Vector3(0.42, 0.08, 0.3)
|
||||
material = SubResource("Material_site_canvas")
|
||||
size = Vector3(2.9, 0.08, 1.4)
|
||||
|
||||
[sub_resource type="ProceduralSkyMaterial" id="SkyMaterial_jajce"]
|
||||
[sub_resource type="ShaderMaterial" id="SkyMaterial_jajce"]
|
||||
resource_local_to_scene = true
|
||||
sky_top_color = Color(0.28, 0.52, 0.72, 1)
|
||||
sky_horizon_color = Color(0.88, 0.74, 0.52, 1)
|
||||
ground_bottom_color = Color(0.18, 0.24, 0.14, 1)
|
||||
ground_horizon_color = Color(0.68, 0.6, 0.44, 1)
|
||||
sun_angle_max = 18.0
|
||||
shader = ExtResource("32_sky")
|
||||
|
||||
[sub_resource type="Sky" id="Sky_jajce"]
|
||||
resource_local_to_scene = true
|
||||
@@ -196,17 +195,17 @@ reflected_light_source = 2
|
||||
tonemap_mode = 2
|
||||
glow_enabled = true
|
||||
glow_normalized = true
|
||||
glow_intensity = 0.5
|
||||
glow_intensity = 0.18
|
||||
glow_strength = 0.9
|
||||
glow_bloom = 0.12
|
||||
glow_bloom = 0.03
|
||||
fog_enabled = true
|
||||
fog_light_color = Color(0.88, 0.84, 0.7, 1)
|
||||
fog_light_energy = 0.78
|
||||
fog_density = 0.002
|
||||
fog_height = -2.0
|
||||
fog_height_density = 0.08
|
||||
fog_aerial_perspective = 0.35
|
||||
fog_sky_affect = 0.28
|
||||
fog_aerial_perspective = 0.5
|
||||
fog_sky_affect = 0.12
|
||||
volumetric_fog_enabled = false
|
||||
volumetric_fog_density = 0.0035
|
||||
volumetric_fog_albedo = Color(0.82, 0.9, 0.82, 1)
|
||||
@@ -215,9 +214,9 @@ volumetric_fog_emission_energy = 0.12
|
||||
volumetric_fog_length = 96.0
|
||||
volumetric_fog_detail_spread = 1.6
|
||||
adjustment_enabled = true
|
||||
adjustment_brightness = 1.08
|
||||
adjustment_contrast = 1.04
|
||||
adjustment_saturation = 1.08
|
||||
adjustment_brightness = 1.0
|
||||
adjustment_contrast = 1.03
|
||||
adjustment_saturation = 1.04
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_guard"]
|
||||
albedo_color = Color(0.55, 0.45, 0.35, 1)
|
||||
@@ -269,6 +268,7 @@ rows = 34
|
||||
amount = 1156
|
||||
process_fixed_fps = 24
|
||||
process_material = ExtResource("25_grass_process")
|
||||
mesh = ExtResource("31_meadow")
|
||||
shadow_mode = 0
|
||||
mesh_material_override = ExtResource("20_grass_material")
|
||||
min_draw_distance = 36.0
|
||||
@@ -277,6 +277,7 @@ particle_count = 10404
|
||||
[node name="GrassInteractionController" type="Node" parent="TerrainRoot"]
|
||||
script = ExtResource("21_grass_controller")
|
||||
grass_material = ExtResource("20_grass_material")
|
||||
path_clearings = Array[NodePath]([NodePath("../../VillageRoot/Path_Village_Spine"), NodePath("../../VillageRoot/Path_Pantry_Worksites"), NodePath("../../VillageRoot/Path_River_Bridge"), NodePath("../../VillageRoot/Path_Forest_Edge")])
|
||||
|
||||
[node name="FortressBlockout" type="Node3D" parent="."]
|
||||
position = Vector3(-25, 7, 9)
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
[gd_scene load_steps=15 format=3]
|
||||
[gd_scene load_steps=17 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_foundation"]
|
||||
albedo_color = Color(0.43, 0.42, 0.36, 1)
|
||||
roughness = 0.98
|
||||
[ext_resource type="Shader" path="res://world/presentation/materials/storybook_surface.gdshader" id="story_surface"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="Material_foundation"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.43, 0.42, 0.36, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_foundation"]
|
||||
material = SubResource("Material_foundation")
|
||||
size = Vector3(4.35, 0.65, 5.35)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_walls"]
|
||||
albedo_color = Color(0.78, 0.7, 0.56, 1)
|
||||
roughness = 0.94
|
||||
[sub_resource type="ShaderMaterial" id="Material_walls"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.78, 0.7, 0.56, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_walls"]
|
||||
material = SubResource("Material_walls")
|
||||
size = Vector3(4, 2.75, 5)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_roof"]
|
||||
albedo_color = Color(0.48, 0.2, 0.12, 1)
|
||||
roughness = 0.9
|
||||
[sub_resource type="ShaderMaterial" id="Material_roof"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.48, 0.2, 0.12, 1)
|
||||
|
||||
[sub_resource type="PrismMesh" id="Mesh_roof"]
|
||||
material = SubResource("Material_roof")
|
||||
size = Vector3(4.65, 1.7, 5.65)
|
||||
left_to_right = 0.5
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_timber"]
|
||||
albedo_color = Color(0.2, 0.105, 0.05, 1)
|
||||
roughness = 0.92
|
||||
[sub_resource type="ShaderMaterial" id="Material_timber"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.2, 0.105, 0.05, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_beam_vertical"]
|
||||
material = SubResource("Material_timber")
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
[gd_scene load_steps=7 format=3]
|
||||
[gd_scene load_steps=8 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_body"]
|
||||
albedo_color = Color(0.55, 0.38, 0.22, 1)
|
||||
roughness = 0.8
|
||||
[ext_resource type="Shader" path="res://world/presentation/materials/storybook_surface.gdshader" id="story_surface"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="Material_body"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.55, 0.38, 0.22, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_body"]
|
||||
material = SubResource("Material_body")
|
||||
size = Vector3(3, 3, 4)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_roof"]
|
||||
albedo_color = Color(0.55, 0.30, 0.18, 1)
|
||||
roughness = 0.75
|
||||
[sub_resource type="ShaderMaterial" id="Material_roof"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.55, 0.30, 0.18, 1)
|
||||
|
||||
[sub_resource type="PrismMesh" id="Mesh_roof"]
|
||||
material = SubResource("Material_roof")
|
||||
size = Vector3(3.4, 1.2, 4.4)
|
||||
left_to_right = 0.5
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_wheel"]
|
||||
albedo_color = Color(0.20, 0.12, 0.06, 1)
|
||||
roughness = 0.7
|
||||
[sub_resource type="ShaderMaterial" id="Material_wheel"]
|
||||
shader = ExtResource("story_surface")
|
||||
shader_parameter/albedo = Color(0.20, 0.12, 0.06, 1)
|
||||
|
||||
[sub_resource type="CylinderMesh" id="Mesh_wheel"]
|
||||
material = SubResource("Material_wheel")
|
||||
|
||||
@@ -7,9 +7,9 @@ enum CanopyVariant {
|
||||
}
|
||||
|
||||
const COLORS := {
|
||||
CanopyVariant.GREEN: Color(0.18, 0.39, 0.16),
|
||||
CanopyVariant.DARK_GREEN: Color(0.10, 0.28, 0.12),
|
||||
CanopyVariant.YELLOW_GREEN: Color(0.34, 0.47, 0.16),
|
||||
CanopyVariant.GREEN: Color(0.32, 0.51, 0.29),
|
||||
CanopyVariant.DARK_GREEN: Color(0.22, 0.41, 0.30),
|
||||
CanopyVariant.YELLOW_GREEN: Color(0.49, 0.60, 0.28),
|
||||
}
|
||||
|
||||
const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader")
|
||||
@@ -25,7 +25,7 @@ func _ready() -> void:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = hash_seed
|
||||
var canopy_variant := rng.randi() % COLORS.size() as CanopyVariant
|
||||
var canopy_radius := 1.0 + rng.randf() * 0.55
|
||||
var canopy_radius := 1.25 + rng.randf() * 0.6
|
||||
var wind_phase := rng.randf_range(0.0, TAU)
|
||||
|
||||
var trunk_mesh := CylinderMesh.new()
|
||||
|
||||
@@ -7,17 +7,17 @@ extends Node
|
||||
|
||||
const TO_RAD := PI / 180.0
|
||||
|
||||
const DAY_SKY_TOP := Color(0.23, 0.43, 0.62, 1)
|
||||
const DAY_SKY_HORIZON := Color(0.82, 0.68, 0.5, 1)
|
||||
const DAY_SKY_TOP := Color(0.27, 0.57, 0.74, 1)
|
||||
const DAY_SKY_HORIZON := Color(0.79, 0.88, 0.82, 1)
|
||||
const DAY_GROUND_BOTTOM := Color(0.12, 0.17, 0.12, 1)
|
||||
const DAY_GROUND_HORIZON := Color(0.62, 0.55, 0.42, 1)
|
||||
const DAY_AMBIENT := Color(0.91, 0.85, 0.72, 1)
|
||||
const DAY_AMBIENT_ENERGY := 0.7
|
||||
const DAY_FOG_LIGHT := Color(0.82, 0.78, 0.66, 1)
|
||||
const DAY_FOG_LIGHT_ENERGY := 0.72
|
||||
const DAY_FOG_DENSITY := 0.002
|
||||
const DAY_LIGHT_COLOR := Color(1, 0.87, 0.72, 1)
|
||||
const DAY_LIGHT_ENERGY := 1.45
|
||||
const DAY_GROUND_HORIZON := Color(0.56, 0.69, 0.57, 1)
|
||||
const DAY_AMBIENT := Color(0.72, 0.83, 0.88, 1)
|
||||
const DAY_AMBIENT_ENERGY := 0.55
|
||||
const DAY_FOG_LIGHT := Color(0.70, 0.83, 0.81, 1)
|
||||
const DAY_FOG_LIGHT_ENERGY := 0.8
|
||||
const DAY_FOG_DENSITY := 0.0015
|
||||
const DAY_LIGHT_COLOR := Color(1, 0.94, 0.79, 1)
|
||||
const DAY_LIGHT_ENERGY := 1.2
|
||||
|
||||
const NIGHT_SKY_TOP := Color(0.04, 0.04, 0.12, 1)
|
||||
const NIGHT_SKY_HORIZON := Color(0.08, 0.06, 0.14, 1)
|
||||
@@ -123,6 +123,19 @@ func _apply_environment(cycle: float) -> void:
|
||||
mat.sky_horizon_color = _lerp_color(NIGHT_SKY_HORIZON, DAY_SKY_HORIZON, day_factor)
|
||||
mat.ground_bottom_color = _lerp_color(NIGHT_GROUND_BOTTOM, DAY_GROUND_BOTTOM, day_factor)
|
||||
mat.ground_horizon_color = _lerp_color(NIGHT_GROUND_HORIZON, DAY_GROUND_HORIZON, day_factor)
|
||||
elif sky != null and sky.sky_material is ShaderMaterial:
|
||||
var mat := sky.sky_material as ShaderMaterial
|
||||
mat.set_shader_parameter("sky_top_color", NIGHT_SKY_TOP.lerp(DAY_SKY_TOP, day_factor))
|
||||
mat.set_shader_parameter(
|
||||
"sky_horizon_color", NIGHT_SKY_HORIZON.lerp(DAY_SKY_HORIZON, day_factor)
|
||||
)
|
||||
mat.set_shader_parameter(
|
||||
"ground_bottom_color", NIGHT_GROUND_BOTTOM.lerp(DAY_GROUND_BOTTOM, day_factor)
|
||||
)
|
||||
mat.set_shader_parameter(
|
||||
"ground_horizon_color", NIGHT_GROUND_HORIZON.lerp(DAY_GROUND_HORIZON, day_factor)
|
||||
)
|
||||
mat.set_shader_parameter("daylight", day_factor)
|
||||
|
||||
|
||||
func _find_simulation_node() -> void:
|
||||
|
||||
@@ -1,70 +1,66 @@
|
||||
// Adapted for The Steward from the bundled Terrain3D particle-grass example.
|
||||
// Original Blender tuft. Opaque blades avoid alpha overdraw and sorting.
|
||||
shader_type spatial;
|
||||
|
||||
render_mode skip_vertex_transform, cull_disabled, blend_mix;
|
||||
render_mode skip_vertex_transform, cull_disabled, specular_disabled;
|
||||
|
||||
const int MAX_INTERACTORS = 8;
|
||||
|
||||
uniform vec2 wind_direction = vec2(1.0, 0.7);
|
||||
uniform vec3 base_color : source_color = vec3(0.16, 0.31, 0.08);
|
||||
uniform vec3 tip_color : source_color = vec3(0.54, 0.68, 0.24);
|
||||
uniform float wind_amount : hint_range(0.0, 1.0) = 0.34;
|
||||
uniform vec3 base_color : source_color = vec3(0.23, 0.40, 0.22);
|
||||
uniform vec3 tip_color : source_color = vec3(0.53, 0.67, 0.35);
|
||||
uniform float wind_amount : hint_range(0.0, 1.0) = 0.24;
|
||||
uniform int interactor_count : hint_range(0, 8) = 0;
|
||||
uniform vec3 interactor_positions[MAX_INTERACTORS];
|
||||
uniform float interaction_radius : hint_range(0.25, 4.0) = 1.8;
|
||||
uniform float interaction_strength : hint_range(0.0, 2.0) = 0.75;
|
||||
|
||||
varying vec4 grass_data;
|
||||
uniform float interaction_strength : hint_range(0.0, 2.0) = 0.55;
|
||||
uniform int clearing_count = 0;
|
||||
uniform vec4 clearing_segments[4];
|
||||
uniform vec4 clearing_half_widths = vec4(0.0);
|
||||
varying float blade_height;
|
||||
varying float shade_variation;
|
||||
varying float pressed_weight;
|
||||
|
||||
|
||||
void vertex() {
|
||||
grass_data[2] = VERTEX.y + 0.55;
|
||||
grass_data[2] *= grass_data[2];
|
||||
VERTEX.xz *= 1.0 - grass_data[2];
|
||||
COLOR = mix(COLOR, vec4(1.0), smoothstep(0.9, 1.0, grass_data[2]));
|
||||
COLOR *= INSTANCE_CUSTOM[3] < 0.35 ? 1.0 : mix(1.0, 0.75, smoothstep(0.35, 0.0, grass_data[2]));
|
||||
grass_data.rg = INSTANCE_CUSTOM.rg;
|
||||
|
||||
blade_height = UV.y;
|
||||
float tip_weight = blade_height * blade_height;
|
||||
vec3 anchor = MODEL_MATRIX[3].xyz;
|
||||
float clearing_scale = 1.0;
|
||||
for (int index = 0; index < 4; index++) {
|
||||
if (index >= clearing_count) { break; }
|
||||
vec2 start = clearing_segments[index].xy;
|
||||
vec2 line = clearing_segments[index].zw - start;
|
||||
float along = clamp(dot(anchor.xz - start, line) / max(dot(line, line), 0.001), 0.0, 1.0);
|
||||
float distance_to_path = length(anchor.xz - start - line * along);
|
||||
clearing_scale *= smoothstep(clearing_half_widths[index], clearing_half_widths[index] + 0.20, distance_to_path);
|
||||
}
|
||||
VERTEX *= clearing_scale;
|
||||
tip_weight *= clearing_scale;
|
||||
vec3 world_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
float scale = pow(INSTANCE_CUSTOM[3] * INSTANCE_CUSTOM[3], 0.707);
|
||||
float force = INSTANCE_CUSTOM[2] * grass_data[2] * scale * wind_amount;
|
||||
force -= fract(force * 256.0) * force * 0.05;
|
||||
force = pow(max(force, 0.0), 0.707);
|
||||
float lateral_wobble = sin(TIME * 2.0 * (1.0 + grass_data.r + grass_data.g)) * 0.2 * (1.0 - INSTANCE_CUSTOM[2]);
|
||||
vec2 direction = normalize(wind_direction);
|
||||
world_vertex.xz -= (vec2(-direction.y, direction.x) * lateral_wobble + direction) * force;
|
||||
world_vertex.y -= INSTANCE_CUSTOM[2] * force * grass_data[2];
|
||||
grass_data[3] = force;
|
||||
|
||||
float phase = dot(anchor.xz, vec2(0.38, 0.29));
|
||||
float gust = sin(TIME * 1.15 + phase) * 0.65 + sin(TIME * 0.58 + phase * 0.42) * 0.35;
|
||||
world_vertex.xz += normalize(wind_direction) * gust * wind_amount * tip_weight;
|
||||
shade_variation = sin(anchor.x * 0.30 + anchor.z * 0.21) * 0.5 + 0.5;
|
||||
pressed_weight = 0.0;
|
||||
for (int index = 0; index < MAX_INTERACTORS; index++) {
|
||||
if (index >= interactor_count) {
|
||||
break;
|
||||
}
|
||||
vec2 offset = world_vertex.xz - interactor_positions[index].xz;
|
||||
if (index >= interactor_count) { break; }
|
||||
vec2 offset = anchor.xz - interactor_positions[index].xz;
|
||||
float distance_to_actor = length(offset);
|
||||
float influence = 1.0 - smoothstep(0.25, interaction_radius, distance_to_actor);
|
||||
vec2 away = offset / max(distance_to_actor, 0.08);
|
||||
world_vertex.xz += away * influence * interaction_strength * grass_data[2];
|
||||
world_vertex.y -= influence * 0.28 * grass_data[2];
|
||||
world_vertex.xz += away * influence * interaction_strength * tip_weight;
|
||||
world_vertex.y -= influence * 0.18 * tip_weight;
|
||||
pressed_weight = max(pressed_weight, influence);
|
||||
}
|
||||
|
||||
VERTEX = (VIEW_MATRIX * vec4(world_vertex, 1.0)).xyz;
|
||||
NORMAL = MODELVIEW_NORMAL_MATRIX * NORMAL;
|
||||
BINORMAL = MODELVIEW_NORMAL_MATRIX * BINORMAL;
|
||||
TANGENT = MODELVIEW_NORMAL_MATRIX * TANGENT;
|
||||
NORMAL = normalize((VIEW_MATRIX * vec4(0.0, 1.0, 0.0, 0.0)).xyz);
|
||||
}
|
||||
|
||||
|
||||
void fragment() {
|
||||
vec3 color = mix(base_color, tip_color, grass_data[2] * 0.62);
|
||||
color.rg *= grass_data.rg * 0.18 + 0.91;
|
||||
color *= pow(COLOR.rgb, vec3(2.2));
|
||||
ALBEDO = mix(color, base_color * 0.72, pressed_weight * 0.42);
|
||||
float specular_roughness = clamp(max(grass_data[2], grass_data[3]), 0.0, 1.0);
|
||||
ROUGHNESS = 1.0 - specular_roughness * 0.22;
|
||||
SPECULAR = clamp(specular_roughness * 0.2, 0.0, 0.12);
|
||||
BACKLIGHT = tip_color * 0.24;
|
||||
vec3 color = mix(base_color, tip_color, smoothstep(0.05, 1.0, blade_height) * 0.78);
|
||||
color *= mix(0.90, 1.09, shade_variation);
|
||||
ALBEDO = mix(color, base_color, pressed_weight * 0.15);
|
||||
ROUGHNESS = 1.0;
|
||||
}
|
||||
|
||||
void light() {
|
||||
float band = smoothstep(0.05, 0.16, dot(NORMAL, LIGHT));
|
||||
DIFFUSE_LIGHT += mix(vec3(0.46, 0.60, 0.52), vec3(1.0, 0.98, 0.85), band) * LIGHT_COLOR * ATTENUATION / PI;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ resource_local_to_scene = true
|
||||
render_priority = 0
|
||||
shader = ExtResource("1_grass")
|
||||
shader_parameter/wind_direction = Vector2(1, 0.7)
|
||||
shader_parameter/base_color = Color(0.16, 0.31, 0.08, 1)
|
||||
shader_parameter/tip_color = Color(0.54, 0.68, 0.24, 1)
|
||||
shader_parameter/wind_amount = 0.34
|
||||
shader_parameter/base_color = Color(0.23, 0.40, 0.22, 1)
|
||||
shader_parameter/tip_color = Color(0.53, 0.67, 0.35, 1)
|
||||
shader_parameter/wind_amount = 0.24
|
||||
shader_parameter/interactor_count = 0
|
||||
shader_parameter/interactor_positions = PackedVector3Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
shader_parameter/interaction_radius = 1.8
|
||||
shader_parameter/interaction_strength = 0.75
|
||||
shader_parameter/interaction_strength = 0.55
|
||||
|
||||
@@ -16,13 +16,13 @@ resource_local_to_scene = true
|
||||
shader = ExtResource("1_process_shader")
|
||||
shader_parameter/main_noise = SubResource("NoiseTexture_grass")
|
||||
shader_parameter/main_noise_scale = 0.012
|
||||
shader_parameter/position_offset = Vector3(0, 0.22, 0)
|
||||
shader_parameter/position_offset = Vector3(0, 0.015, 0)
|
||||
shader_parameter/align_to_normal = true
|
||||
shader_parameter/normal_strength = 0.22
|
||||
shader_parameter/random_rotation = true
|
||||
shader_parameter/random_spacing = 0.72
|
||||
shader_parameter/min_scale = Vector3(0.09, 0.22, 0.09)
|
||||
shader_parameter/max_scale = Vector3(0.13, 0.48, 0.13)
|
||||
shader_parameter/min_scale = Vector3(0.72, 0.28, 0.72)
|
||||
shader_parameter/max_scale = Vector3(1.15, 0.48, 1.15)
|
||||
shader_parameter/noise_scale = 0.0041
|
||||
shader_parameter/wind_speed = 0.022
|
||||
shader_parameter/wind_strength = 0.6
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
shader_type sky;
|
||||
uniform vec3 sky_top_color : source_color = vec3(0.27, 0.57, 0.74);
|
||||
uniform vec3 sky_horizon_color : source_color = vec3(0.79, 0.88, 0.82);
|
||||
uniform vec3 ground_bottom_color : source_color = vec3(0.12, 0.17, 0.12);
|
||||
uniform vec3 ground_horizon_color : source_color = vec3(0.56, 0.69, 0.57);
|
||||
uniform float daylight : hint_range(0.0, 1.0) = 1.0;
|
||||
|
||||
float hash21(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
||||
float noise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
return mix(mix(hash21(i), hash21(i + vec2(1.0, 0.0)), f.x), mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0)), f.x), f.y);
|
||||
}
|
||||
|
||||
void sky() {
|
||||
float height = max(EYEDIR.y, 0.0);
|
||||
vec3 sky_color = mix(sky_horizon_color, sky_top_color, pow(height, 0.48));
|
||||
vec2 p = EYEDIR.xz / max(0.17, height) * 1.15;
|
||||
float cloud = noise(p) * 0.65 + noise(p * 2.1 + 7.3) * 0.25 + noise(p * 4.3) * 0.10;
|
||||
float mask = smoothstep(0.55, 0.64, cloud) * smoothstep(0.02, 0.20, height);
|
||||
vec3 cloud_shadow = mix(vec3(0.08, 0.10, 0.19), vec3(0.70, 0.80, 0.80), daylight);
|
||||
vec3 cloud_light = mix(vec3(0.15, 0.18, 0.29), vec3(1.0, 0.98, 0.87), daylight);
|
||||
vec3 cloud_color = mix(cloud_shadow, cloud_light, smoothstep(0.61, 0.77, cloud));
|
||||
sky_color = mix(sky_color, cloud_color, mask);
|
||||
COLOR = EYEDIR.y >= 0.0 ? sky_color : mix(ground_horizon_color, ground_bottom_color, pow(-EYEDIR.y, 0.4));
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://c10spbmnnwwto
|
||||
@@ -10,8 +10,10 @@ uniform float flutter_strength : hint_range(0.0, 0.05) = 0.012;
|
||||
uniform float wind_phase : hint_range(0.0, 6.2832) = 0.0;
|
||||
uniform vec3 albedo : source_color = vec3(0.19, 0.38, 0.17);
|
||||
uniform float roughness : hint_range(0.0, 1.0) = 0.9;
|
||||
varying vec3 paint_position;
|
||||
|
||||
void vertex() {
|
||||
paint_position = VERTEX;
|
||||
float half_height = 1.2;
|
||||
float height_factor = clamp((VERTEX.y + half_height) / (half_height * 2.0), 0.0, 1.0);
|
||||
height_factor = height_factor * height_factor;
|
||||
@@ -29,6 +31,17 @@ void vertex() {
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
ALBEDO = albedo;
|
||||
float patches = sin(paint_position.x * 6.0 + sin(paint_position.z * 7.0)) * sin(paint_position.y * 5.0);
|
||||
float top = smoothstep(-0.65, 0.8, paint_position.y);
|
||||
ALBEDO = albedo * mix(0.85, 1.17, top) * (1.0 + smoothstep(0.2, 0.4, patches) * 0.10);
|
||||
ROUGHNESS = roughness;
|
||||
SPECULAR = 0.0;
|
||||
}
|
||||
|
||||
void light() {
|
||||
float band = smoothstep(0.08, 0.18, dot(NORMAL, LIGHT));
|
||||
float highlight = smoothstep(0.62, 0.74, dot(NORMAL, LIGHT));
|
||||
vec3 tone = mix(vec3(0.32, 0.48, 0.39), vec3(0.87, 0.92, 0.72), band);
|
||||
tone = mix(tone, vec3(1.12, 1.08, 0.82), highlight);
|
||||
DIFFUSE_LIGHT += tone * ATTENUATION * LIGHT_COLOR / PI;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
class_name AnimalAppearance
|
||||
extends RefCounted
|
||||
## Disposable woodland identities, rebuilt from stable NPC IDs. No simulation RNG.
|
||||
|
||||
const CEL_SHADER := preload("res://world/presentation/materials/storybook_cel.gdshader")
|
||||
const SPECIES: Array[StringName] = [&"fox", &"rabbit", &"badger", &"otter"]
|
||||
const MODELS: Array[PackedScene] = [
|
||||
preload("res://assets/storybook/fox.glb"),
|
||||
preload("res://assets/storybook/rabbit.glb"),
|
||||
preload("res://assets/storybook/badger.glb"),
|
||||
preload("res://assets/storybook/otter.glb"),
|
||||
]
|
||||
const PARTS: Array[StringName] = [
|
||||
&"Body", &"Head", &"Hair", &"ArmLeft", &"ArmRight", &"LegLeft", &"LegRight", &"Tail"
|
||||
]
|
||||
|
||||
|
||||
static func apply_to(root: Node3D, identity: int, npc_body := false) -> void:
|
||||
var index := posmod(identity, MODELS.size())
|
||||
var source := MODELS[index].instantiate()
|
||||
var material := ShaderMaterial.new()
|
||||
material.shader = CEL_SHADER
|
||||
root.set_meta("animal_species", SPECIES[index])
|
||||
for part in PARTS:
|
||||
var source_mesh := source.find_child(String(part), true, false) as MeshInstance3D
|
||||
var target_name := "MeshInstance3D" if npc_body and part == &"Body" else String(part)
|
||||
var target := root.get_node(target_name) as MeshInstance3D
|
||||
assert(source_mesh != null, "Missing Blender animation part: " + String(part))
|
||||
target.mesh = source_mesh.mesh
|
||||
target.material_override = material
|
||||
source.free()
|
||||
|
||||
|
||||
static func set_profession_color(root: Node3D, color: Color) -> void:
|
||||
var body := root.get_node("MeshInstance3D") as MeshInstance3D
|
||||
var material := body.material_override as ShaderMaterial
|
||||
material.set_shader_parameter("coat_color", color)
|
||||
material.set_shader_parameter("profession_tint", 0.28)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dfb2a5yut45av
|
||||
@@ -0,0 +1,23 @@
|
||||
shader_type spatial;
|
||||
render_mode specular_disabled;
|
||||
|
||||
// Opaque vertex-painted models: alpha is a clothing mask, never transparency.
|
||||
uniform vec4 coat_color : source_color = vec4(1.0);
|
||||
uniform float profession_tint : hint_range(0.0, 1.0) = 0.0;
|
||||
uniform vec3 shadow_color : source_color = vec3(0.55, 0.64, 0.70);
|
||||
uniform float band_softness : hint_range(0.005, 0.2) = 0.045;
|
||||
|
||||
void fragment() {
|
||||
ALBEDO = mix(COLOR.rgb, COLOR.rgb * coat_color.rgb * 1.6, COLOR.a * profession_tint);
|
||||
ROUGHNESS = 1.0;
|
||||
SPECULAR = 0.0;
|
||||
}
|
||||
|
||||
void light() {
|
||||
float diffuse = dot(NORMAL, LIGHT);
|
||||
float mid = smoothstep(-0.15 - band_softness, -0.15 + band_softness, diffuse);
|
||||
float sun = smoothstep(0.50 - band_softness, 0.50 + band_softness, diffuse);
|
||||
vec3 tone = mix(shadow_color * 0.36, vec3(0.70), mid);
|
||||
tone = mix(tone, vec3(1.0, 0.97, 0.87), sun);
|
||||
DIFFUSE_LIGHT += tone * ATTENUATION * LIGHT_COLOR / PI;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bs1ip7dyu7jy8
|
||||
@@ -0,0 +1,24 @@
|
||||
shader_type spatial;
|
||||
render_mode specular_disabled;
|
||||
uniform vec3 albedo : source_color = vec3(0.8, 0.72, 0.52);
|
||||
uniform float paint_strength : hint_range(0.0, 0.2) = 0.04;
|
||||
varying vec3 paint_position;
|
||||
|
||||
void vertex() {
|
||||
paint_position = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
float brush = sin(paint_position.x * 2.1 + sin(paint_position.y * 3.0)) * sin(paint_position.z * 2.8 + paint_position.y);
|
||||
ALBEDO = albedo * (1.0 + brush * paint_strength);
|
||||
ROUGHNESS = 1.0;
|
||||
}
|
||||
|
||||
void light() {
|
||||
float ndl = dot(NORMAL, LIGHT);
|
||||
float middle = smoothstep(-0.10, 0.03, ndl);
|
||||
float sunny = smoothstep(0.48, 0.58, ndl);
|
||||
vec3 shade = mix(vec3(0.26, 0.34, 0.40), vec3(0.74), middle);
|
||||
shade = mix(shade, vec3(1.0, 0.97, 0.88), sunny);
|
||||
DIFFUSE_LIGHT += shade * ATTENUATION * LIGHT_COLOR / PI;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8biomtfwtf4w
|
||||
Reference in New Issue
Block a user