feat(environment): add painterly skies and living meadows
@@ -0,0 +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,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dmkubk6dhhyis"
|
||||||
|
path="res://.godot/imported/painted_sky.png-9888802278aff2f83effdab2649f1f05.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/painterly/painted_sky.png"
|
||||||
|
dest_files=["res://.godot/imported/painted_sky.png-9888802278aff2f83effdab2649f1f05.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
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=false
|
||||||
|
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=1
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,11 +38,11 @@ spawn is a next integration.
|
|||||||
`JajceWorld` owns reversible High, Balanced, and Low presentation profiles with
|
`JajceWorld` owns reversible High, Balanced, and Low presentation profiles with
|
||||||
instance-local mutable environment/grass resources and viewport-scale ownership.
|
instance-local mutable environment/grass resources and viewport-scale ownership.
|
||||||
|
|
||||||
| Profile | 3D scale | Shadow distance | Volumetric/glow | Grass |
|
| Profile | 3D scale / MSAA | Shadow distance | Volumetric/glow | Grass |
|
||||||
| --- | ---: | ---: | --- | --- |
|
| --- | ---: | ---: | --- | --- |
|
||||||
| High | authored | authored (currently 180 m) | authored when renderer supports it | authored density/interactors |
|
| High | authored / authored 4× | authored (currently 180 m) | authored when renderer supports it | authored density/interactors |
|
||||||
| Balanced (default) | 0.85 cap | 120 m cap | authored ordinary lookdev; unsupported volumetric disabled | reduced spacing/interactors/update rate |
|
| Balanced (default) | 0.85 cap / 2× cap | 120 m cap | authored ordinary lookdev; unsupported volumetric disabled | reduced spacing/interactors/update rate |
|
||||||
| Low | 0.70 cap | 80 m, orthogonal | volumetric/glow/adjustment disabled; ordinary fog retained | hidden/stopped, emitters off, shader interactors zero |
|
| Low | 0.70 cap / off | 80 m, orthogonal | volumetric/glow/adjustment disabled; ordinary fog retained | hidden/stopped, emitters off, shader interactors zero |
|
||||||
|
|
||||||
UI remains at native resolution. High/Low switches restore captured authored
|
UI remains at native resolution. High/Low switches restore captured authored
|
||||||
values; viewport scale restoration is guarded by the current quality owner so a
|
values; viewport scale restoration is guarded by the current quality owner so a
|
||||||
@@ -103,7 +103,7 @@ profession tint, velocity-driven motion, inventory cues and death through the
|
|||||||
existing presentation controllers. No saved state or navigation changes.
|
existing presentation controllers. No saved state or navigation changes.
|
||||||
|
|
||||||
Characters and buildings use soft cel bands; tree crowns retain the shared
|
Characters and buildings use soft cel bands; tree crowns retain the shared
|
||||||
breeze with painted color patches. A static procedural cloud sky, cooler
|
breeze with painted color patches. A painted cloud panorama, cooler
|
||||||
ambient fill and warm sunlight follow the simulation day/night clock. The
|
ambient fill and warm sunlight follow the simulation day/night clock. The
|
||||||
cloud shader adds no volumetric pass, outline pass or screen-space postprocess.
|
cloud shader adds no volumetric pass, outline pass or screen-space postprocess.
|
||||||
See [the asset contract and rebuild commands](../assets/storybook/README.md).
|
See [the asset contract and rebuild commands](../assets/storybook/README.md).
|
||||||
@@ -161,3 +161,85 @@ ledger, not brittle cross-machine CI thresholds.
|
|||||||
|
|
||||||
See [the benchmark ledger](benchmarks/README.md), [regional contract](REGIONAL_SIMULATION.md),
|
See [the benchmark ledger](benchmarks/README.md), [regional contract](REGIONAL_SIMULATION.md),
|
||||||
and [the build-in-public visual plan](BUILD_IN_PUBLIC_PLAN.md).
|
and [the build-in-public visual plan](BUILD_IN_PUBLIC_PLAN.md).
|
||||||
|
|
||||||
|
## Painterly environment pass — September 2026
|
||||||
|
|
||||||
|
The reference study highlighted flat cloud bands, primitive tree silhouettes,
|
||||||
|
sparse ground cover, opaque green water, and a sun arc offset from daytime
|
||||||
|
brightness. The current pass addresses those through shared presentation assets:
|
||||||
|
|
||||||
|
- The WorldEnvironment uses cool blue ambient fill, warm sunlight, restrained
|
||||||
|
glow and ordinary aerial fog. The solar arc now peaks at noon and the warm
|
||||||
|
transitions overlap dawn/dusk. An original painted cloud panorama supplies
|
||||||
|
layered cumulus shapes; a static sky shader remaps the artwork, blends its
|
||||||
|
seam/horizon, and tints it from the simulation clock. It adds no TIME-driven
|
||||||
|
cubemap updates or volumetric cloud pass. Generation provenance and the exact
|
||||||
|
prompt are in [the asset guide](../assets/painterly/README.md).
|
||||||
|
- Original Blender broadleaf crowns/trunks and layered conifers replace the
|
||||||
|
primitive foliage. Seventeen decorative edge trees form a woodland backdrop.
|
||||||
|
The four village tree placeholders now use `HarvestableTreePresentation`,
|
||||||
|
retaining their existing IDs, amounts, interaction points and resource count.
|
||||||
|
Full/low/depleted state and restored crowns still derive from real amounts.
|
||||||
|
- `PainterlyMeadow` creates at most 1,680 decorative flower clumps in four
|
||||||
|
MultiMesh batches, with cream daisies, pink cosmos, buttercups and lavender.
|
||||||
|
Local seeded drifts follow terrain and exclude houses, paths, work sites and
|
||||||
|
resource approach points. High/Balanced/Low expose at most 1,680/1,092/536
|
||||||
|
clumps, with shorter distance fading in cheaper profiles. Flowers share the
|
||||||
|
prevailing breeze; the existing grass and berry contact field remains intact.
|
||||||
|
Explicit white instance colors preserve the painted GLB palette when custom
|
||||||
|
instance data is enabled in Compatibility; native review caught and verified
|
||||||
|
that renderer-specific regression.
|
||||||
|
Flowers have no collision, harvesting, actor-contact deformation or saved
|
||||||
|
state. See [mesh sources and budgets](../assets/meadow/README.md).
|
||||||
|
- The blue/turquoise river follows actual centerline terrain heights, keeps
|
||||||
|
level cross-sections, and clips dry edges at the shoreline. The pool and its
|
||||||
|
outlet meet without overlapping coplanar transparent sheets. Broken foam,
|
||||||
|
subtle flowing strokes and painted reflection patches replace the hard ring
|
||||||
|
and featureless surface. These reflections are stylized procedural patches
|
||||||
|
plus ordinary sky specular response, not exact reflected scene geometry,
|
||||||
|
screen-space reflections, refraction or depth sampling. Water obeys scene
|
||||||
|
lighting at night and adds no per-frame scripts.
|
||||||
|
- MSAA follows the existing viewport ownership rules: High preserves authored
|
||||||
|
4×, Balanced caps at 2×, and Low disables it. Closing an inactive world cannot
|
||||||
|
overwrite the active world's antialiasing or an external viewport override.
|
||||||
|
|
||||||
|
The editable Blender sources live under `art/painterly/` and `art/meadow/`,
|
||||||
|
protected by `.gdignore`; runtime consumes portable GLBs. The broadleaf is
|
||||||
|
5,350 triangles / five surfaces and the conifer is 1,580 / two. All flower
|
||||||
|
clumps together are capped at 362,880 triangles / four surfaces before
|
||||||
|
quality and distance reduction. No scene collision, navigation bake, terrain
|
||||||
|
heightmap, simulation rule or save schema was changed.
|
||||||
|
|
||||||
|
`tools/capture_painterly_environment.gd -- --label=after` captures the running
|
||||||
|
main scene with paused simulation, fixed daytime presentation and hidden HUD,
|
||||||
|
including the normal gameplay camera, valley, lower village view and night.
|
||||||
|
The valley/near-village `painterly_before_*` and `painterly_after_*`
|
||||||
|
images are matched cameras; `after_gameplay` fixes the normal rig at its default
|
||||||
|
yaw/zoom with mouse input disabled, and `after_vista` is an additional review angle.
|
||||||
|
The tool also records static-camera profile samples in
|
||||||
|
`painterly_after_metrics.json`; wall-frame latency includes vsync and is not
|
||||||
|
isolated GPU time or weak-PC proof. Runtime camera defaults are unchanged.
|
||||||
|
|
||||||
|
Remaining visual work is primarily authored terrain/shoreline detail and the
|
||||||
|
blockout-like building/fortress forms. This pass improves atmosphere and
|
||||||
|
foliage without treating those assets as finished film-quality environments.
|
||||||
|
|
||||||
|
Validation for this pass: the full macOS Godot 4.7 gate passed, including all
|
||||||
|
headless scenarios, the Compatibility quality scenario, and 182 GUT tests /
|
||||||
|
2,279 assertions. Main-scene headless startup passed the gate's existing exact
|
||||||
|
diagnostic allowlist. Native Metal daytime/gameplay/valley/night images were
|
||||||
|
reviewed at 1600×900, with a separate native Compatibility capture confirming
|
||||||
|
colored petals after the instance-buffer fix. The presentation-quality scenario
|
||||||
|
also passed with native Compatibility, including white instance-color readback
|
||||||
|
(the headless dummy renderer cannot provide that readback).
|
||||||
|
The static M1 Max sample reports approximately 16.7 ms
|
||||||
|
median wall-frame time in all three profiles; see its JSON for p95, draw calls
|
||||||
|
and primitives. This includes vsync and does not establish GPU headroom.
|
||||||
|
|
||||||
|
Native renderer shutdown still reports the particle shader/material RID
|
||||||
|
retention previously documented in the storybook/foliage pass; the headless
|
||||||
|
allowlist was not expanded. Compatibility has different color/tonemapping and
|
||||||
|
its existing particle/texture shutdown diagnostics. Metal is the color
|
||||||
|
reference for the saved art-review images.
|
||||||
|
The native multi-world quality scenario additionally logged a transient macOS
|
||||||
|
OpenGL cubemap-unloadable diagnostic; the final main-scene captures did not.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.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 |
@@ -32,6 +32,10 @@ window/size/viewport_height=1020
|
|||||||
|
|
||||||
enabled=PackedStringArray("res://addons/terrain_3d/plugin.cfg", "res://addons/gut/plugin.cfg", "res://addons/dialogue_manager/plugin.cfg")
|
enabled=PackedStringArray("res://addons/terrain_3d/plugin.cfg", "res://addons/gut/plugin.cfg", "res://addons/dialogue_manager/plugin.cfg")
|
||||||
|
|
||||||
|
[rendering]
|
||||||
|
|
||||||
|
anti_aliasing/quality/msaa_3d=2
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
||||||
move_forward={
|
move_forward={
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ func _initialize() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
|
var viewport := root as Viewport
|
||||||
|
var original_msaa := viewport.msaa_3d
|
||||||
var camera := Camera3D.new()
|
var camera := Camera3D.new()
|
||||||
camera.current = true
|
camera.current = true
|
||||||
root.add_child(camera)
|
root.add_child(camera)
|
||||||
@@ -16,13 +18,14 @@ func _run() -> void:
|
|||||||
await process_frame
|
await process_frame
|
||||||
await physics_frame
|
await physics_frame
|
||||||
|
|
||||||
var viewport := root as Viewport
|
|
||||||
var environment: Environment = (
|
var environment: Environment = (
|
||||||
(world.get_node("WorldEnvironment") as WorldEnvironment).environment
|
(world.get_node("WorldEnvironment") as WorldEnvironment).environment
|
||||||
)
|
)
|
||||||
var light := world.get_node("DirectionalLight3D") as DirectionalLight3D
|
var light := world.get_node("DirectionalLight3D") as DirectionalLight3D
|
||||||
var grass := world.get_node("TerrainRoot/Terrain3D/CozyGrassField") as Node3D
|
var grass := world.get_node("TerrainRoot/Terrain3D/CozyGrassField") as Node3D
|
||||||
var grass_controller := world.get_node("TerrainRoot/GrassInteractionController") as Node
|
var grass_controller := world.get_node("TerrainRoot/GrassInteractionController") as Node
|
||||||
|
var meadow := world.get_node("PainterlyMeadow") as PainterlyMeadow
|
||||||
|
var balanced_meadow := meadow.get_presentation_stats()
|
||||||
var world_grass_material := grass.get("mesh_material_override") as ShaderMaterial
|
var world_grass_material := grass.get("mesh_material_override") as ShaderMaterial
|
||||||
var world_process_material := grass.get("process_material") as ShaderMaterial
|
var world_process_material := grass.get("process_material") as ShaderMaterial
|
||||||
var world_sky: Sky = environment.sky
|
var world_sky: Sky = environment.sky
|
||||||
@@ -41,6 +44,10 @@ func _run() -> void:
|
|||||||
+ " (found %.2f)" % balanced_render_scale
|
+ " (found %.2f)" % balanced_render_scale
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
_check(
|
||||||
|
viewport.msaa_3d == mini(original_msaa, Viewport.MSAA_2X),
|
||||||
|
"Balanced should cap authored multisample antialiasing at 2x"
|
||||||
|
)
|
||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
light.directional_shadow_max_distance <= JajceWorld.BALANCED_SHADOW_DISTANCE
|
light.directional_shadow_max_distance <= JajceWorld.BALANCED_SHADOW_DISTANCE
|
||||||
@@ -54,6 +61,14 @@ func _run() -> void:
|
|||||||
world_grass_material == grass_controller.get("grass_material"),
|
world_grass_material == grass_controller.get("grass_material"),
|
||||||
"Grass rendering and interaction should share this world's isolated material"
|
"Grass rendering and interaction should share this world's isolated material"
|
||||||
)
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
int(balanced_meadow["multimesh_batch_count"]) == 4
|
||||||
|
and int(balanced_meadow["flower_clump_count"]) in range(1, 1801)
|
||||||
|
and _meadow_has_finite_placements(meadow)
|
||||||
|
),
|
||||||
|
"Meadow batches should retain neutral instance colors, finite placement and their clump budget"
|
||||||
|
)
|
||||||
|
|
||||||
_check(
|
_check(
|
||||||
world.apply_presentation_quality(JajceWorld.PresentationQuality.HIGH),
|
world.apply_presentation_quality(JajceWorld.PresentationQuality.HIGH),
|
||||||
@@ -63,6 +78,21 @@ func _run() -> void:
|
|||||||
var high_particle_count := int(grass.get("particle_count"))
|
var high_particle_count := int(grass.get("particle_count"))
|
||||||
var high_render_scale := viewport.scaling_3d_scale
|
var high_render_scale := viewport.scaling_3d_scale
|
||||||
var high_shadow_distance := light.directional_shadow_max_distance
|
var high_shadow_distance := light.directional_shadow_max_distance
|
||||||
|
_check(
|
||||||
|
viewport.msaa_3d == original_msaa,
|
||||||
|
"High should restore the authored multisample antialiasing setting"
|
||||||
|
)
|
||||||
|
var high_meadow := meadow.get_presentation_stats()
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
int(high_meadow["visible_flower_clump_count"]) == int(high_meadow["flower_clump_count"])
|
||||||
|
and (
|
||||||
|
int(balanced_meadow["visible_flower_clump_count"])
|
||||||
|
< int(high_meadow["visible_flower_clump_count"])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"High should restore every flower clump while Balanced reduces meadow density"
|
||||||
|
)
|
||||||
_check(
|
_check(
|
||||||
high_particle_count in range(9000, 15001),
|
high_particle_count in range(9000, 15001),
|
||||||
"High should restore the authored grass population"
|
"High should restore the authored grass population"
|
||||||
@@ -94,6 +124,9 @@ func _run() -> void:
|
|||||||
viewport.scaling_3d_scale <= JajceWorld.LOW_RENDER_SCALE + 0.001,
|
viewport.scaling_3d_scale <= JajceWorld.LOW_RENDER_SCALE + 0.001,
|
||||||
"Low should reduce 3D render resolution"
|
"Low should reduce 3D render resolution"
|
||||||
)
|
)
|
||||||
|
_check(
|
||||||
|
viewport.msaa_3d == Viewport.MSAA_DISABLED, "Low should disable multisample antialiasing"
|
||||||
|
)
|
||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
environment.fog_enabled
|
environment.fog_enabled
|
||||||
@@ -120,6 +153,18 @@ func _run() -> void:
|
|||||||
)
|
)
|
||||||
_check(not grass_controller.is_processing(), "Low should stop grass interaction scans")
|
_check(not grass_controller.is_processing(), "Low should stop grass interaction scans")
|
||||||
_check(_not_any_grass_particle_emitting(grass), "Low should stop every grass particle emitter")
|
_check(_not_any_grass_particle_emitting(grass), "Low should stop every grass particle emitter")
|
||||||
|
var low_meadow := meadow.get_presentation_stats()
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
int(low_meadow["visible_flower_clump_count"]) > 0
|
||||||
|
and (
|
||||||
|
int(low_meadow["visible_flower_clump_count"])
|
||||||
|
< int(balanced_meadow["visible_flower_clump_count"])
|
||||||
|
)
|
||||||
|
and low_meadow["flower_clump_count"] == high_meadow["flower_clump_count"]
|
||||||
|
),
|
||||||
|
"Low should retain a reduced flower population without regenerating placements"
|
||||||
|
)
|
||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
world_grass_material != null
|
world_grass_material != null
|
||||||
@@ -136,6 +181,7 @@ func _run() -> void:
|
|||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
is_equal_approx(viewport.scaling_3d_scale, high_render_scale)
|
is_equal_approx(viewport.scaling_3d_scale, high_render_scale)
|
||||||
|
and viewport.msaa_3d == original_msaa
|
||||||
and environment.glow_enabled
|
and environment.glow_enabled
|
||||||
and (environment.volumetric_fog_enabled == world.call("_supports_volumetric_fog"))
|
and (environment.volumetric_fog_enabled == world.call("_supports_volumetric_fog"))
|
||||||
and environment.adjustment_enabled
|
and environment.adjustment_enabled
|
||||||
@@ -153,12 +199,19 @@ func _run() -> void:
|
|||||||
),
|
),
|
||||||
"Returning to High should restore grass rendering and processing"
|
"Returning to High should restore grass rendering and processing"
|
||||||
)
|
)
|
||||||
|
_check(
|
||||||
|
meadow.get_presentation_stats() == high_meadow,
|
||||||
|
"Returning to High should restore the same complete flower batches"
|
||||||
|
)
|
||||||
|
|
||||||
var world_base_scale := float(world.get("_authored_render_scale"))
|
var world_base_scale := float(world.get("_authored_render_scale"))
|
||||||
world.free()
|
world.free()
|
||||||
_check(
|
_check(
|
||||||
is_equal_approx(viewport.scaling_3d_scale, world_base_scale),
|
(
|
||||||
"An exiting world should restore the viewport scale it originally owned"
|
is_equal_approx(viewport.scaling_3d_scale, world_base_scale)
|
||||||
|
and viewport.msaa_3d == original_msaa
|
||||||
|
),
|
||||||
|
"An exiting world should restore the viewport scale and antialiasing it originally owned"
|
||||||
)
|
)
|
||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
@@ -183,6 +236,7 @@ func _run() -> void:
|
|||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
fresh_world.get_active_presentation_quality() == JajceWorld.PresentationQuality.BALANCED
|
fresh_world.get_active_presentation_quality() == JajceWorld.PresentationQuality.BALANCED
|
||||||
|
and viewport.msaa_3d == mini(original_msaa, Viewport.MSAA_2X)
|
||||||
and fresh_environment.fog_enabled
|
and fresh_environment.fog_enabled
|
||||||
and not fresh_environment.volumetric_fog_enabled
|
and not fresh_environment.volumetric_fog_enabled
|
||||||
and fresh_grass.visible
|
and fresh_grass.visible
|
||||||
@@ -205,8 +259,11 @@ func _run() -> void:
|
|||||||
var original_scale := float(fresh_world.get("_authored_render_scale"))
|
var original_scale := float(fresh_world.get("_authored_render_scale"))
|
||||||
fresh_world.free()
|
fresh_world.free()
|
||||||
_check(
|
_check(
|
||||||
is_equal_approx(viewport.scaling_3d_scale, original_scale),
|
(
|
||||||
"A sequential world should release its viewport-scale ownership"
|
is_equal_approx(viewport.scaling_3d_scale, original_scale)
|
||||||
|
and viewport.msaa_3d == original_msaa
|
||||||
|
),
|
||||||
|
"A sequential world should release its viewport scale and antialiasing ownership"
|
||||||
)
|
)
|
||||||
|
|
||||||
var older_owner := _create_presentation_only_world(JajceWorld.PresentationQuality.LOW)
|
var older_owner := _create_presentation_only_world(JajceWorld.PresentationQuality.LOW)
|
||||||
@@ -216,19 +273,29 @@ func _run() -> void:
|
|||||||
root.add_child(newer_owner)
|
root.add_child(newer_owner)
|
||||||
await process_frame
|
await process_frame
|
||||||
_check(
|
_check(
|
||||||
is_equal_approx(float(newer_owner.get("_authored_render_scale")), original_scale),
|
(
|
||||||
"Concurrent owners should inherit the viewport base, not another owner's reduced scale"
|
is_equal_approx(float(newer_owner.get("_authored_render_scale")), original_scale)
|
||||||
|
and int(newer_owner.get("_authored_msaa")) == original_msaa
|
||||||
|
),
|
||||||
|
"Concurrent owners should inherit the viewport's base scale and antialiasing"
|
||||||
)
|
)
|
||||||
older_owner.free()
|
older_owner.free()
|
||||||
_check(
|
_check(
|
||||||
is_equal_approx(viewport.scaling_3d_scale, JajceWorld.BALANCED_RENDER_SCALE),
|
(
|
||||||
"A stale owner exiting should not overwrite the active owner's scale"
|
is_equal_approx(viewport.scaling_3d_scale, JajceWorld.BALANCED_RENDER_SCALE)
|
||||||
|
and viewport.msaa_3d == mini(original_msaa, Viewport.MSAA_2X)
|
||||||
|
),
|
||||||
|
"A stale owner exiting should preserve the active owner's scale and antialiasing"
|
||||||
)
|
)
|
||||||
newer_owner.free()
|
newer_owner.free()
|
||||||
_check(
|
_check(
|
||||||
is_equal_approx(viewport.scaling_3d_scale, original_scale),
|
(
|
||||||
"The final viewport owner should restore the original scale on exit"
|
is_equal_approx(viewport.scaling_3d_scale, original_scale)
|
||||||
|
and viewport.msaa_3d == original_msaa
|
||||||
|
),
|
||||||
|
"The final viewport owner should restore the original scale and antialiasing on exit"
|
||||||
)
|
)
|
||||||
|
await _check_msaa_previous_owner_and_external_change(viewport)
|
||||||
|
|
||||||
var main_scene: Node = load("res://main.tscn").instantiate()
|
var main_scene: Node = load("res://main.tscn").instantiate()
|
||||||
var time_dial := main_scene.get_node("UI/TimeDial") as Control
|
var time_dial := main_scene.get_node("UI/TimeDial") as Control
|
||||||
@@ -264,6 +331,32 @@ func _all_grass_particles_emitting(grass: Node3D) -> bool:
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _meadow_has_finite_placements(meadow: PainterlyMeadow) -> bool:
|
||||||
|
var terrain := meadow.get_node(meadow.terrain_path) as Terrain3D
|
||||||
|
# The dummy headless renderer returns black for every instance-color readback.
|
||||||
|
var can_read_instance_colors := DisplayServer.get_name() != "headless"
|
||||||
|
for child in meadow.get_children():
|
||||||
|
var batch := child as MultiMeshInstance3D
|
||||||
|
if batch == null or batch.multimesh == null:
|
||||||
|
return false
|
||||||
|
if not batch.multimesh.use_colors or not batch.multimesh.use_custom_data:
|
||||||
|
return false
|
||||||
|
for index in batch.multimesh.instance_count:
|
||||||
|
if (
|
||||||
|
can_read_instance_colors
|
||||||
|
and not batch.multimesh.get_instance_color(index).is_equal_approx(Color.WHITE)
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
var placement := batch.multimesh.get_instance_transform(index)
|
||||||
|
if not placement.is_finite():
|
||||||
|
return false
|
||||||
|
var point := batch.to_global(placement.origin)
|
||||||
|
var height := terrain.data.get_height(point)
|
||||||
|
if is_nan(height) or absf(point.y - height) > 0.025:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func _create_presentation_only_world(quality: int) -> JajceWorld:
|
func _create_presentation_only_world(quality: int) -> JajceWorld:
|
||||||
var world: JajceWorld = load("res://world/jajce/JajceWorld.tscn").instantiate()
|
var world: JajceWorld = load("res://world/jajce/JajceWorld.tscn").instantiate()
|
||||||
# Stable simulation-facing IDs deliberately allow only one loaded authority
|
# Stable simulation-facing IDs deliberately allow only one loaded authority
|
||||||
@@ -273,6 +366,33 @@ func _create_presentation_only_world(quality: int) -> JajceWorld:
|
|||||||
return world
|
return world
|
||||||
|
|
||||||
|
|
||||||
|
func _check_msaa_previous_owner_and_external_change(viewport: Viewport) -> void:
|
||||||
|
var original_msaa := viewport.msaa_3d
|
||||||
|
viewport.msaa_3d = Viewport.MSAA_8X
|
||||||
|
var previous_owner := _create_presentation_only_world(JajceWorld.PresentationQuality.LOW)
|
||||||
|
root.add_child(previous_owner)
|
||||||
|
await process_frame
|
||||||
|
var active_owner := _create_presentation_only_world(JajceWorld.PresentationQuality.HIGH)
|
||||||
|
root.add_child(active_owner)
|
||||||
|
await process_frame
|
||||||
|
_check(
|
||||||
|
viewport.msaa_3d == Viewport.MSAA_8X,
|
||||||
|
"A High owner should inherit authored antialiasing even when the previous owner uses Low"
|
||||||
|
)
|
||||||
|
active_owner.free()
|
||||||
|
_check(
|
||||||
|
viewport.msaa_3d == Viewport.MSAA_DISABLED,
|
||||||
|
"An exiting active owner should restore the surviving previous owner's antialiasing"
|
||||||
|
)
|
||||||
|
viewport.msaa_3d = Viewport.MSAA_2X
|
||||||
|
previous_owner.free()
|
||||||
|
_check(
|
||||||
|
viewport.msaa_3d == Viewport.MSAA_2X,
|
||||||
|
"Releasing the final owner should preserve an external antialiasing change"
|
||||||
|
)
|
||||||
|
viewport.msaa_3d = original_msaa
|
||||||
|
|
||||||
|
|
||||||
func _not_any_grass_particle_emitting(grass: Node3D) -> bool:
|
func _not_any_grass_particle_emitting(grass: Node3D) -> bool:
|
||||||
var particles: Array = grass.get("particle_nodes")
|
var particles: Array = grass.get("particle_nodes")
|
||||||
for value in particles:
|
for value in particles:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func _run() -> void:
|
|||||||
|
|
||||||
var manager = main_scene.get_node("SimulationManager")
|
var manager = main_scene.get_node("SimulationManager")
|
||||||
manager.set_process(false)
|
manager.set_process(false)
|
||||||
|
_check_village_tree_bindings(main_scene, manager)
|
||||||
var cluster := main_scene.get_node(CLUSTER_PATH) as RiverbankResourceCluster
|
var cluster := main_scene.get_node(CLUSTER_PATH) as RiverbankResourceCluster
|
||||||
var berry := cluster.get_node("ResourceAnchors/BerryBush_River_02") as ResourceNode
|
var berry := cluster.get_node("ResourceAnchors/BerryBush_River_02") as ResourceNode
|
||||||
var tree := cluster.get_node("ResourceAnchors/Tree_River_Resource_01") as ResourceNode
|
var tree := cluster.get_node("ResourceAnchors/Tree_River_Resource_01") as ResourceNode
|
||||||
@@ -120,6 +121,34 @@ func _run() -> void:
|
|||||||
_finish()
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _check_village_tree_bindings(main_scene: Node, manager: Node) -> void:
|
||||||
|
var snapshot: String = manager.serialize_state()
|
||||||
|
for tree_name in ["Tree_01", "Tree_02", "Tree_North_Outskirts_01", "Tree_South_Edge_01"]:
|
||||||
|
var tree := (
|
||||||
|
main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/" + tree_name)
|
||||||
|
as ResourceNode
|
||||||
|
)
|
||||||
|
var visual := tree.get_node("Visual/TreePresentation") as HarvestableTreePresentation
|
||||||
|
_check(
|
||||||
|
visual.resource_node == tree,
|
||||||
|
"Village tree art should bind the existing finite resource"
|
||||||
|
)
|
||||||
|
while tree.state.can_extract():
|
||||||
|
manager.harvest_resource_node(tree)
|
||||||
|
_check(visual.is_stump_visible(), "Harvesting the real village tree should leave a stump")
|
||||||
|
_check(manager.restore_state_from_json(snapshot), "Village tree harvest review should restore")
|
||||||
|
for tree_name in ["Tree_01", "Tree_02", "Tree_North_Outskirts_01", "Tree_South_Edge_01"]:
|
||||||
|
var visual := (
|
||||||
|
main_scene.get_node(
|
||||||
|
"JajceWorld/WorldObjects/ResourceNodes/" + tree_name + "/Visual/TreePresentation"
|
||||||
|
)
|
||||||
|
as HarvestableTreePresentation
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
visual.get_visible_canopy_count() == 4, "Restore rebuilds each full village tree crown"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _check_initial_presentation(
|
func _check_initial_presentation(
|
||||||
berry_visual: StylizedBerryPatch, tree_visual: HarvestableTreePresentation
|
berry_visual: StylizedBerryPatch, tree_visual: HarvestableTreePresentation
|
||||||
) -> void:
|
) -> void:
|
||||||
|
|||||||
@@ -63,10 +63,13 @@ func _run() -> void:
|
|||||||
|
|
||||||
var river_surface := world.get_node("WaterRoot/RiverSurface") as Node3D
|
var river_surface := world.get_node("WaterRoot/RiverSurface") as Node3D
|
||||||
_check_ribbon_tracks_terrain(
|
_check_ribbon_tracks_terrain(
|
||||||
terrain, river_surface.get_node("RiverRibbon") as MeshInstance3D, "Downstream water"
|
terrain, river_surface.get_node("RiverRibbon") as MeshInstance3D, "Downstream water", false
|
||||||
)
|
)
|
||||||
_check_ribbon_tracks_terrain(
|
_check_ribbon_tracks_terrain(
|
||||||
terrain, river_surface.get_node("UpperRiverRibbon") as MeshInstance3D, "Upper-stream water"
|
terrain,
|
||||||
|
river_surface.get_node("UpperRiverRibbon") as MeshInstance3D,
|
||||||
|
"Upper-stream water",
|
||||||
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
@@ -83,23 +86,98 @@ func _terrain_height(terrain: Terrain3D, world_x: float, world_z: float) -> floa
|
|||||||
|
|
||||||
|
|
||||||
func _check_ribbon_tracks_terrain(
|
func _check_ribbon_tracks_terrain(
|
||||||
terrain: Terrain3D, ribbon: MeshInstance3D, label: String
|
terrain: Terrain3D, ribbon: MeshInstance3D, label: String, is_upper: bool
|
||||||
) -> void:
|
) -> void:
|
||||||
_check(ribbon != null and ribbon.mesh is ArrayMesh, "%s ribbon should be generated" % label)
|
_check(ribbon != null and ribbon.mesh is ArrayMesh, "%s ribbon should be generated" % label)
|
||||||
if ribbon == null or not ribbon.mesh is ArrayMesh:
|
if ribbon == null or not ribbon.mesh is ArrayMesh:
|
||||||
return
|
return
|
||||||
var arrays := ribbon.mesh.surface_get_arrays(0)
|
var arrays := ribbon.mesh.surface_get_arrays(0)
|
||||||
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||||
|
var uvs: PackedVector2Array = arrays[Mesh.ARRAY_TEX_UV]
|
||||||
_check(not vertices.is_empty(), "%s ribbon should contain vertices" % label)
|
_check(not vertices.is_empty(), "%s ribbon should contain vertices" % label)
|
||||||
for vertex_index in range(0, vertices.size(), maxi(1, vertices.size() / 12)):
|
if vertices.is_empty() or uvs.size() != vertices.size():
|
||||||
|
failures.append("%s ribbon requires a complete coordinate profile" % label)
|
||||||
|
return
|
||||||
|
var start_z := (
|
||||||
|
JajceWatercourse.UPPER_STREAM_START_Z if is_upper else JajceWatercourse.DOWNSTREAM_START_Z
|
||||||
|
)
|
||||||
|
var end_z := (
|
||||||
|
JajceWatercourse.UPPER_STREAM_END_Z if is_upper else JajceWatercourse.DOWNSTREAM_END_Z
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
absf(ribbon.to_global(vertices[0]).z - start_z) < 0.03
|
||||||
|
and absf(ribbon.to_global(vertices[-1]).z - end_z) < 0.03
|
||||||
|
),
|
||||||
|
"%s ribbon should span its entire authored stream length" % label
|
||||||
|
)
|
||||||
|
var pool := ribbon.get_parent().get_node("PlungePool") as MeshInstance3D
|
||||||
|
var pool_mesh := pool.mesh as PlaneMesh
|
||||||
|
var pool_radii := pool_mesh.size * Vector2(pool.scale.x, pool.scale.z) * 0.5
|
||||||
|
var min_depth := INF
|
||||||
|
var max_depth := -INF
|
||||||
|
for vertex_index in vertices.size():
|
||||||
var world_vertex := ribbon.to_global(vertices[vertex_index])
|
var world_vertex := ribbon.to_global(vertices[vertex_index])
|
||||||
|
var world_z := lerpf(start_z, end_z, uvs[vertex_index].y)
|
||||||
|
var center_x := (
|
||||||
|
JajceWatercourse.upper_stream_center_x(world_z)
|
||||||
|
if is_upper
|
||||||
|
else JajceWatercourse.downstream_center_x(world_z)
|
||||||
|
)
|
||||||
|
var half_width := (
|
||||||
|
JajceWatercourse.upper_stream_half_width(world_z)
|
||||||
|
if is_upper
|
||||||
|
else JajceWatercourse.downstream_half_width(world_z)
|
||||||
|
)
|
||||||
|
if not is_upper:
|
||||||
|
var pool_progress := (world_z - pool.global_position.z) / pool_radii.y
|
||||||
|
if absf(pool_progress) < 1.0:
|
||||||
|
half_width = maxf(
|
||||||
|
half_width, pool_radii.x * sqrt(1.0 - pool_progress * pool_progress)
|
||||||
|
)
|
||||||
|
var center_height := _terrain_height(terrain, center_x, world_z)
|
||||||
var terrain_height := terrain.data.get_height(world_vertex)
|
var terrain_height := terrain.data.get_height(world_vertex)
|
||||||
if is_nan(terrain_height):
|
if (
|
||||||
|
not world_vertex.is_finite()
|
||||||
|
or not is_finite(terrain_height)
|
||||||
|
or not is_finite(center_height)
|
||||||
|
):
|
||||||
failures.append("%s ribbon sampled outside Terrain3D" % label)
|
failures.append("%s ribbon sampled outside Terrain3D" % label)
|
||||||
return
|
return
|
||||||
if absf(world_vertex.y - terrain_height - JajceWatercourse.WATER_SURFACE_OFFSET) > 0.03:
|
if absf(world_vertex.y - center_height - JajceWatercourse.WATER_SURFACE_OFFSET) > 0.03:
|
||||||
failures.append("%s ribbon should hug the carved terrain" % label)
|
failures.append("%s cross-section should follow its actual carved centerline" % label)
|
||||||
return
|
return
|
||||||
|
if (
|
||||||
|
absf(world_vertex.z - world_z) > 0.03
|
||||||
|
or absf(world_vertex.x - center_x) > half_width + 0.03
|
||||||
|
):
|
||||||
|
failures.append(
|
||||||
|
"%s ribbon should stay inside the authored stream/pool footprint" % label
|
||||||
|
)
|
||||||
|
return
|
||||||
|
var depth := world_vertex.y - terrain_height
|
||||||
|
min_depth = minf(min_depth, depth)
|
||||||
|
max_depth = maxf(max_depth, depth)
|
||||||
|
if depth < -0.03 or depth > JajceWatercourse.WATER_SURFACE_OFFSET + 0.03:
|
||||||
|
failures.append("%s surface should remain shallow and above the channel bed" % label)
|
||||||
|
return
|
||||||
|
var across := uvs[vertex_index].x
|
||||||
|
if is_zero_approx(across) or is_equal_approx(across, 1.0):
|
||||||
|
var edge_distance := absf(world_vertex.x - center_x)
|
||||||
|
if absf(edge_distance - half_width) > 0.03 and absf(depth) > 0.03:
|
||||||
|
failures.append(
|
||||||
|
"%s edges must reach the authored extent or the actual shoreline" % label
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if (
|
||||||
|
(is_zero_approx(across) and world_vertex.x >= center_x)
|
||||||
|
or (is_equal_approx(across, 1.0) and world_vertex.x <= center_x)
|
||||||
|
):
|
||||||
|
failures.append(
|
||||||
|
"%s water must retain its authored centerline inside both banks" % label
|
||||||
|
)
|
||||||
|
return
|
||||||
|
print("[TEST] %s depth bounds | min=%.3f max=%.3f" % [label, min_depth, max_depth])
|
||||||
|
|
||||||
|
|
||||||
func _check(condition: bool, message: String) -> void:
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ func _run() -> void:
|
|||||||
if not tree.has_node("Canopy"):
|
if not tree.has_node("Canopy"):
|
||||||
continue
|
continue
|
||||||
var canopy := tree.get_node("Canopy") as MeshInstance3D
|
var canopy := tree.get_node("Canopy") as MeshInstance3D
|
||||||
var material := canopy.mesh.material as ShaderMaterial
|
var material := canopy.get_active_material(0) as ShaderMaterial
|
||||||
tree_wind_is_bounded = (
|
tree_wind_is_bounded = (
|
||||||
tree_wind_is_bounded
|
tree_wind_is_bounded
|
||||||
and material != null
|
and material != null
|
||||||
@@ -249,6 +249,8 @@ func _run() -> void:
|
|||||||
upper_river_ribbon.mesh is ArrayMesh,
|
upper_river_ribbon.mesh is ArrayMesh,
|
||||||
"Waterfall shelf should expose a terrain-following upper stream"
|
"Waterfall shelf should expose a terrain-following upper stream"
|
||||||
)
|
)
|
||||||
|
_check_water_cross_sections(river_ribbon)
|
||||||
|
_check_water_cross_sections(upper_river_ribbon)
|
||||||
_check(
|
_check(
|
||||||
world.has_node("WaterRoot/RiverSurface/PlungePool"),
|
world.has_node("WaterRoot/RiverSurface/PlungePool"),
|
||||||
"River should widen into a readable plunge pool beneath the waterfall"
|
"River should widen into a readable plunge pool beneath the waterfall"
|
||||||
@@ -270,6 +272,7 @@ func _run() -> void:
|
|||||||
),
|
),
|
||||||
"Plunge-pool water should follow the carved basin height"
|
"Plunge-pool water should follow the carved basin height"
|
||||||
)
|
)
|
||||||
|
_check_pool_outlet(river_ribbon, plunge_pool, terrain)
|
||||||
var environment: Environment = (
|
var environment: Environment = (
|
||||||
(world.get_node("WorldEnvironment") as WorldEnvironment).environment
|
(world.get_node("WorldEnvironment") as WorldEnvironment).environment
|
||||||
)
|
)
|
||||||
@@ -628,6 +631,44 @@ func _run() -> void:
|
|||||||
quit(1)
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _check_water_cross_sections(ribbon: MeshInstance3D) -> void:
|
||||||
|
var arrays := ribbon.mesh.surface_get_arrays(0)
|
||||||
|
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||||
|
var uvs: PackedVector2Array = arrays[Mesh.ARRAY_TEX_UV]
|
||||||
|
var row_height := 0.0
|
||||||
|
for index in vertices.size():
|
||||||
|
if is_zero_approx(uvs[index].x):
|
||||||
|
row_height = vertices[index].y
|
||||||
|
_check(
|
||||||
|
is_equal_approx(vertices[index].y, row_height),
|
||||||
|
"Water cross-sections should remain level instead of climbing either terrain bank"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _check_pool_outlet(ribbon: MeshInstance3D, pool: MeshInstance3D, terrain: Terrain3D) -> void:
|
||||||
|
var vertices: PackedVector3Array = ribbon.mesh.surface_get_arrays(0)[Mesh.ARRAY_VERTEX]
|
||||||
|
var start := ribbon.to_global(vertices[0])
|
||||||
|
var pool_mesh := pool.mesh as PlaneMesh
|
||||||
|
var radii := pool_mesh.size * Vector2(pool.scale.x, pool.scale.z) * 0.5
|
||||||
|
var progress := (start.z - pool.global_position.z) / radii.y
|
||||||
|
var expected_edge := pool.global_position.x - radii.x * sqrt(1.0 - progress * progress)
|
||||||
|
var meets_ellipse := absf(start.x - expected_edge) < 0.03
|
||||||
|
var meets_shore := (
|
||||||
|
start.x >= expected_edge
|
||||||
|
and start.x < pool.global_position.x
|
||||||
|
and absf(start.y - terrain.data.get_height(start)) < 0.03
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(meets_ellipse or meets_shore) and absf(start.y - pool.global_position.y) < 0.03,
|
||||||
|
"River outlet should meet the pool ellipse or its terrain shoreline at the same surface height"
|
||||||
|
)
|
||||||
|
var material := pool_mesh.surface_get_material(0) as ShaderMaterial
|
||||||
|
_check(
|
||||||
|
is_equal_approx(float(material.get_shader_parameter("pool_outlet_z")), start.z),
|
||||||
|
"Pool transparency should end where the river takes over to avoid coplanar overlap"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _wait_for_navigation_map(navigation_map: RID) -> void:
|
func _wait_for_navigation_map(navigation_map: RID) -> void:
|
||||||
for attempt in 30:
|
for attempt in 30:
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
extends GutTest
|
||||||
|
|
||||||
|
const CYCLE := preload("res://world/jajce/day_night_cycle.gd")
|
||||||
|
|
||||||
|
|
||||||
|
func test_sun_is_highest_at_noon_and_lights_the_ground_throughout_day() -> void:
|
||||||
|
var cycle := CYCLE.new()
|
||||||
|
var light := DirectionalLight3D.new()
|
||||||
|
cycle.directional_light = light
|
||||||
|
cycle._apply_light_rotation(0.5)
|
||||||
|
var noon_elevation := -light.rotation_degrees.x
|
||||||
|
assert_gt(noon_elevation, 45.0, "Noon should have an elevated sun, not grazing light")
|
||||||
|
for time in [0.15, 0.25, 0.4, 0.6, 0.75, 0.85]:
|
||||||
|
cycle._apply_light_rotation(time)
|
||||||
|
assert_lt(light.rotation_degrees.x, 0.0, "The daylight source points down toward terrain")
|
||||||
|
assert_lt(-light.rotation_degrees.x, noon_elevation, "The solar arc peaks at noon")
|
||||||
|
cycle.free()
|
||||||
|
light.free()
|
||||||
|
|
||||||
|
|
||||||
|
func test_warm_transitions_overlap_dawn_and_dusk_without_persisting_at_noon() -> void:
|
||||||
|
var cycle := CYCLE.new()
|
||||||
|
for time in [0.22, 0.78]:
|
||||||
|
assert_gt(cycle._day_factor(time), 0.0)
|
||||||
|
assert_lt(cycle._day_factor(time), 1.0)
|
||||||
|
assert_gt(cycle._sunrise_sunset_weight(time), 0.8)
|
||||||
|
assert_eq(cycle._sunrise_sunset_weight(0.5), 0.0)
|
||||||
|
assert_eq(cycle._sunrise_sunset_weight(0.0), 0.0)
|
||||||
|
cycle.free()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dnjghjrvx52cr
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
"""Build the original Jajce meadow flowers in Blender 5.1+.
|
||||||
|
|
||||||
|
Blender --background --python tools/art/build_meadow_flowers.py
|
||||||
|
Each clump has one opaque vertex-colored surface and no textures. Coordinates
|
||||||
|
are authored Y-up and converted for Blender; GLB export restores Godot Y-up.
|
||||||
|
The .blend source files are editable and excluded from Godot import.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
SOURCE = ROOT / "art/meadow"
|
||||||
|
OUTPUT = ROOT / "assets/meadow"
|
||||||
|
SOURCE.mkdir(parents=True, exist_ok=True)
|
||||||
|
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
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 coord(point):
|
||||||
|
return (point[0], -point[2], point[1])
|
||||||
|
|
||||||
|
|
||||||
|
class FlowerMesh:
|
||||||
|
def __init__(self):
|
||||||
|
self.vertices = []
|
||||||
|
self.faces = []
|
||||||
|
self.colors = []
|
||||||
|
|
||||||
|
def face(self, points, color):
|
||||||
|
first = len(self.vertices)
|
||||||
|
self.vertices.extend(coord(point) for point in points)
|
||||||
|
self.faces.append(tuple(range(first, first + len(points))))
|
||||||
|
self.colors.extend([(*linear(color), 1.0)] * len(points))
|
||||||
|
|
||||||
|
def stem(self, base, top):
|
||||||
|
bottom = Vector(base)
|
||||||
|
tip = Vector(top)
|
||||||
|
middle = bottom.lerp(tip, 0.55) + Vector((0.015, 0.0, -0.018))
|
||||||
|
for start, end in ((bottom, middle), (middle, tip)):
|
||||||
|
for side in range(4):
|
||||||
|
a = side * math.tau / 4
|
||||||
|
b = (side + 1) * math.tau / 4
|
||||||
|
edge_a = Vector((math.cos(a), 0, math.sin(a))) * 0.009
|
||||||
|
edge_b = Vector((math.cos(b), 0, math.sin(b))) * 0.009
|
||||||
|
self.face((start + edge_a, start + edge_b, end + edge_b * 0.5, end + edge_a * 0.5), "54864b")
|
||||||
|
|
||||||
|
def leaf(self, base, angle, length=0.18, width=0.042):
|
||||||
|
root = Vector(base)
|
||||||
|
direction = Vector((math.cos(angle), 0.6, math.sin(angle))) * length
|
||||||
|
side = Vector((-math.sin(angle), 0, math.cos(angle))) * width
|
||||||
|
middle = root + direction * 0.52
|
||||||
|
tip = root + direction
|
||||||
|
# Raised crease gives a leaf silhouette from both ground and game camera.
|
||||||
|
crease = middle + Vector((0, 0.028, 0))
|
||||||
|
self.face((root, middle + side, crease), "6fa74f")
|
||||||
|
self.face((middle + side, tip, crease), "80af58")
|
||||||
|
self.face((root, crease, middle - side), "4f8548")
|
||||||
|
self.face((middle - side, crease, tip), "679c4e")
|
||||||
|
|
||||||
|
def blossom(self, center, radius, petals, color, highlight, heart="e6b344"):
|
||||||
|
center = Vector(center)
|
||||||
|
for petal in range(petals):
|
||||||
|
angle = petal * math.tau / petals + 0.12
|
||||||
|
forward = Vector((math.cos(angle), 0, math.sin(angle)))
|
||||||
|
side = Vector((-math.sin(angle), 0, math.cos(angle)))
|
||||||
|
root = center + forward * radius * 0.12
|
||||||
|
middle = center + forward * radius * 0.65 + Vector((0, radius * 0.09, 0))
|
||||||
|
tip = center + forward * radius + Vector((0, radius * 0.24, 0))
|
||||||
|
width = radius * (0.30 if petals < 7 else 0.23)
|
||||||
|
self.face((root, middle + side * width, tip + side * width * 0.42), color)
|
||||||
|
self.face((root, tip + side * width * 0.42, tip - side * width * 0.42), highlight)
|
||||||
|
self.face((root, tip - side * width * 0.42, middle - side * width), color)
|
||||||
|
# An opaque domed pollen heart rather than a texture or billboard.
|
||||||
|
for side in range(8):
|
||||||
|
a = side * math.tau / 8
|
||||||
|
b = (side + 1) * math.tau / 8
|
||||||
|
self.face((center + Vector((0, radius * 0.24, 0)),
|
||||||
|
center + Vector((math.cos(a) * radius * 0.24, 0.008, math.sin(a) * radius * 0.24)),
|
||||||
|
center + Vector((math.cos(b) * radius * 0.24, 0.008, math.sin(b) * radius * 0.24))), heart)
|
||||||
|
|
||||||
|
def build(self, name):
|
||||||
|
mesh = bpy.data.meshes.new(name)
|
||||||
|
mesh.from_pydata(self.vertices, [], self.faces)
|
||||||
|
mesh.update()
|
||||||
|
attr = mesh.color_attributes.new(name="Color", type="FLOAT_COLOR", domain="CORNER")
|
||||||
|
for index, loop in enumerate(mesh.loops):
|
||||||
|
attr.data[index].color = self.colors[loop.vertex_index]
|
||||||
|
mesh.materials.append(bpy.data.materials["MeadowPalette"])
|
||||||
|
obj = bpy.data.objects.new(name, mesh)
|
||||||
|
bpy.context.collection.objects.link(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def clear_scene():
|
||||||
|
bpy.ops.object.select_all(action="SELECT")
|
||||||
|
bpy.ops.object.delete(use_global=False)
|
||||||
|
|
||||||
|
|
||||||
|
def create_material():
|
||||||
|
material = bpy.data.materials.new("MeadowPalette")
|
||||||
|
material.use_nodes = True
|
||||||
|
material.use_backface_culling = False
|
||||||
|
nodes = material.node_tree.nodes
|
||||||
|
nodes.clear()
|
||||||
|
color = nodes.new("ShaderNodeVertexColor")
|
||||||
|
color.layer_name = "Color"
|
||||||
|
surface = nodes.new("ShaderNodeBsdfPrincipled")
|
||||||
|
surface.inputs["Roughness"].default_value = 1.0
|
||||||
|
material.node_tree.links.new(color.outputs["Color"], surface.inputs["Base Color"])
|
||||||
|
output = nodes.new("ShaderNodeOutputMaterial")
|
||||||
|
material.node_tree.links.new(surface.outputs["BSDF"], output.inputs["Surface"])
|
||||||
|
|
||||||
|
|
||||||
|
def flower(name, colors, petals, height, radius):
|
||||||
|
clear_scene()
|
||||||
|
builder = FlowerMesh()
|
||||||
|
for index, (x, z, scale) in enumerate(((0, 0, 1.0), (-0.16, 0.1, 0.76), (0.16, -0.1, 0.88))):
|
||||||
|
top = (x + 0.035 * math.cos(index * 2.2), height * scale, z + 0.04)
|
||||||
|
builder.stem((x, 0, z), top)
|
||||||
|
for leaf in range(3):
|
||||||
|
y = height * scale * (0.17 + leaf * 0.16)
|
||||||
|
builder.leaf((x, y, z), index * 2.1 + leaf * 2.45, 0.17 * scale, 0.034)
|
||||||
|
if name == "lavender_spire":
|
||||||
|
for tier in range(4):
|
||||||
|
pos = (top[0], top[1] - tier * 0.086, top[2])
|
||||||
|
builder.blossom(pos, radius * (0.58 + tier * 0.13), 5, *colors, heart="c7b3df")
|
||||||
|
else:
|
||||||
|
builder.blossom(top, radius * scale, petals, *colors)
|
||||||
|
obj = builder.build(name)
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
obj.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = obj
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
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")))
|
||||||
|
obj.data.calc_loop_triangles()
|
||||||
|
return {
|
||||||
|
"triangles": len(obj.data.loop_triangles), "surfaces": 1,
|
||||||
|
"flower_stems": 3, "bytes": (OUTPUT / (name + ".glb")).stat().st_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
clear_scene()
|
||||||
|
create_material()
|
||||||
|
specs = (
|
||||||
|
("cream_daisy", ("f0e7c4", "fff4d8"), 8, 0.47, 0.125),
|
||||||
|
("pink_cosmos", ("d780a7", "f1abc4"), 7, 0.64, 0.165),
|
||||||
|
("yellow_buttercup", ("edc45a", "ffdf83"), 5, 0.36, 0.10),
|
||||||
|
("lavender_spire", ("9585b8", "bfafd6"), 5, 0.79, 0.080),
|
||||||
|
)
|
||||||
|
stats = {spec[0]: flower(*spec) for spec in specs}
|
||||||
|
(OUTPUT / "mesh_budget.json").write_text(json.dumps(stats, indent=2) + "\n")
|
||||||
|
print(json.dumps(stats, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
"""Build original scalloped tree meshes, editable in Blender and portable to Godot.
|
||||||
|
|
||||||
|
Run: Blender --background --python tools/art/build_painterly_trees.py
|
||||||
|
One crown surface and one joined trunk/branch surface; no textures or alpha cards.
|
||||||
|
All coordinates use Blender Z-up and glTF exports them as Godot Y-up.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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/painterly"
|
||||||
|
OUTPUT = ROOT / "assets/painterly"
|
||||||
|
SOURCE.mkdir(parents=True, exist_ok=True)
|
||||||
|
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
(SOURCE / ".gdignore").write_text("")
|
||||||
|
RNG = random.Random(2741)
|
||||||
|
|
||||||
|
bpy.ops.object.select_all(action="SELECT")
|
||||||
|
bpy.ops.object.delete(use_global=False)
|
||||||
|
|
||||||
|
|
||||||
|
def material(name):
|
||||||
|
mat = bpy.data.materials.new(name)
|
||||||
|
mat.use_nodes = True
|
||||||
|
nodes = mat.node_tree.nodes
|
||||||
|
shader = nodes.get("Principled BSDF")
|
||||||
|
shader.inputs["Roughness"].default_value = 0.95
|
||||||
|
shader.inputs["Specular IOR Level"].default_value = 0.0
|
||||||
|
color = nodes.new("ShaderNodeVertexColor")
|
||||||
|
color.layer_name = "Color"
|
||||||
|
mat.node_tree.links.new(color.outputs["Color"], shader.inputs["Base Color"])
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
def mesh_object(name, vertices, faces, colors, mat):
|
||||||
|
mesh = bpy.data.meshes.new(name)
|
||||||
|
mesh.from_pydata(vertices, [], faces)
|
||||||
|
mesh.update()
|
||||||
|
obj = bpy.data.objects.new(name, mesh)
|
||||||
|
bpy.context.collection.objects.link(obj)
|
||||||
|
attr = mesh.color_attributes.new(name="Color", type="FLOAT_COLOR", domain="POINT")
|
||||||
|
for entry, color in zip(attr.data, colors):
|
||||||
|
entry.color = (*color, 1.0)
|
||||||
|
mesh.materials.append(mat)
|
||||||
|
for polygon in mesh.polygons:
|
||||||
|
polygon.use_smooth = True
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def source_tint(mat, tint):
|
||||||
|
"""Set an editing preview tint after portable glTF export."""
|
||||||
|
nodes = mat.node_tree.nodes
|
||||||
|
mix = nodes.new("ShaderNodeMixRGB")
|
||||||
|
mix.blend_type = "MULTIPLY"
|
||||||
|
mix.inputs[0].default_value = 1.0
|
||||||
|
mix.inputs[2].default_value = (*tint, 1.0)
|
||||||
|
mat.node_tree.links.new(nodes.get("Color Attribute").outputs["Color"], mix.inputs[1])
|
||||||
|
mat.node_tree.links.new(mix.outputs[0], nodes.get("Principled BSDF").inputs["Base Color"])
|
||||||
|
|
||||||
|
|
||||||
|
# Connected, irregular crown with broad painted lobes and small leafy tips.
|
||||||
|
# Perturbing an envelope keeps the silhouette solid and removes internal overdraw.
|
||||||
|
bumps = []
|
||||||
|
for index in range(23):
|
||||||
|
z = 1 - 2 * (index + 0.5) / 23
|
||||||
|
angle = index * 2.399963 + 0.2
|
||||||
|
direction = Vector((math.sqrt(1 - z * z) * math.cos(angle),
|
||||||
|
math.sqrt(1 - z * z) * math.sin(angle), z))
|
||||||
|
bumps.append((direction, RNG.uniform(0.12, 0.25)))
|
||||||
|
|
||||||
|
|
||||||
|
def envelope(direction):
|
||||||
|
ridges = sum(height * math.exp((direction.dot(axis) - 1) * 30)
|
||||||
|
for axis, height in bumps)
|
||||||
|
fine = math.sin(direction.x * 22 + direction.z * 9) * math.sin(direction.y * 19) * 0.018
|
||||||
|
radius = 0.84 + ridges + fine
|
||||||
|
return Vector((direction.x * radius, direction.y * radius * 0.95,
|
||||||
|
direction.z * radius * 0.86))
|
||||||
|
|
||||||
|
|
||||||
|
def pigment(point):
|
||||||
|
broad = math.sin(point.x * 8 + point.z * 2) * math.sin(point.y * 7 - point.z * 5)
|
||||||
|
light = 0.82 + 0.11 * broad + 0.07 * max(0, point.z)
|
||||||
|
return (light * (1.0 + 0.035 * broad), light, light * (0.95 - 0.025 * broad))
|
||||||
|
|
||||||
|
|
||||||
|
vertices, faces, colors = [], [], []
|
||||||
|
segments, rings = 32, 16
|
||||||
|
vertices.append(envelope(Vector((0, 0, 1))))
|
||||||
|
for ring in range(1, rings):
|
||||||
|
phi = math.pi * ring / rings
|
||||||
|
for segment in range(segments):
|
||||||
|
theta = math.tau * segment / segments
|
||||||
|
direction = Vector((math.sin(phi) * math.cos(theta), math.sin(phi) * math.sin(theta), math.cos(phi)))
|
||||||
|
vertices.append(envelope(direction))
|
||||||
|
vertices.append(envelope(Vector((0, 0, -1))))
|
||||||
|
for segment in range(segments):
|
||||||
|
faces.append((0, 1 + segment, 1 + (segment + 1) % segments))
|
||||||
|
for ring in range(rings - 2):
|
||||||
|
for segment in range(segments):
|
||||||
|
a = 1 + ring * segments + segment
|
||||||
|
b = 1 + ring * segments + (segment + 1) % segments
|
||||||
|
faces.append((a, a + segments, b + segments, b))
|
||||||
|
last = len(vertices) - 1
|
||||||
|
for segment in range(segments):
|
||||||
|
faces.append((last, last - segments + (segment + 1) % segments, last - segments + segment))
|
||||||
|
colors = [pigment(point) for point in vertices]
|
||||||
|
|
||||||
|
# Bent solid leaf tips give a scalloped edge at close range. They are tiny opaque
|
||||||
|
# wedges with both sides, so they do not require cull-disabled or alpha materials.
|
||||||
|
for axis, _height in bumps:
|
||||||
|
for leaf in range(2):
|
||||||
|
tangent = axis.cross(Vector((0, 0, 1)))
|
||||||
|
if tangent.length < 0.01:
|
||||||
|
tangent = Vector((1, 0, 0))
|
||||||
|
tangent.normalize()
|
||||||
|
sideways = axis.cross(tangent).normalized()
|
||||||
|
direction = (axis + tangent * (leaf - 0.5) * 0.13).normalized()
|
||||||
|
base = envelope(direction) * 0.98
|
||||||
|
length = RNG.uniform(0.11, 0.17)
|
||||||
|
width = RNG.uniform(0.04, 0.065)
|
||||||
|
middle = base + direction * length * 0.55
|
||||||
|
start = len(vertices)
|
||||||
|
vertices.extend([base, middle + tangent * width, middle + sideways * width * 0.28,
|
||||||
|
middle - tangent * width, base + direction * length])
|
||||||
|
faces.extend([tuple(start + j for j in indices) for indices in
|
||||||
|
((0, 1, 2), (0, 2, 3), (1, 4, 2), (2, 4, 3), (0, 3, 1), (1, 3, 4))])
|
||||||
|
colors.extend([pigment(point) for point in vertices[-5:]])
|
||||||
|
canopy = mesh_object("Canopy", vertices, faces, colors, material("CanopyPigment"))
|
||||||
|
|
||||||
|
# A crooked trunk, joined forks and roots use one surface. Colors are linear,
|
||||||
|
# with grey warm bark, moss at the base and subtle broad plane variation.
|
||||||
|
vertices, faces, colors = [], [], []
|
||||||
|
|
||||||
|
|
||||||
|
def tube(points, radii, sides=9):
|
||||||
|
start = len(vertices)
|
||||||
|
for index, raw in enumerate(points):
|
||||||
|
point = Vector(raw)
|
||||||
|
tangent = Vector(points[min(index + 1, len(points) - 1)]) - Vector(points[max(0, index - 1)])
|
||||||
|
tangent.normalize()
|
||||||
|
side = tangent.cross(Vector((0, 1, 0))).normalized()
|
||||||
|
other = tangent.cross(side).normalized()
|
||||||
|
for segment in range(sides):
|
||||||
|
angle = math.tau * segment / sides
|
||||||
|
offset = (math.cos(angle) * side + math.sin(angle) * other) * radii[index]
|
||||||
|
vertices.append(point + offset)
|
||||||
|
shade = 0.90 + math.sin(angle * 3 + 0.6) * 0.12
|
||||||
|
moss = max(0, 1 - point.z / 0.55) * (0.5 + 0.5 * math.cos(angle))
|
||||||
|
colors.append(((0.20 - moss * 0.035) * shade, (0.13 + moss * 0.01) * shade,
|
||||||
|
(0.068 - moss * 0.005) * shade))
|
||||||
|
if index:
|
||||||
|
for segment in range(sides):
|
||||||
|
a = start + (index - 1) * sides + segment
|
||||||
|
b = start + (index - 1) * sides + (segment + 1) % sides
|
||||||
|
faces.append((a, b, b + sides, a + sides))
|
||||||
|
faces.append(tuple(reversed(range(start, start + sides))))
|
||||||
|
faces.append(tuple(range(len(vertices) - sides, len(vertices))))
|
||||||
|
|
||||||
|
|
||||||
|
tube([(0, 0, 0), (-0.07, 0.025, 0.22), (0.02, 0.0, 1.0), (-0.10, 0.025, 1.85),
|
||||||
|
(0.03, 0.02, 2.62), (0.22, -0.04, 3.48)], [0.38, 0.27, 0.23, 0.20, 0.14, 0.025])
|
||||||
|
tube([(-0.06, 0, 1.70), (-0.36, 0.04, 2.35), (-0.84, 0.03, 2.83), (-1.05, 0.09, 3.23)],
|
||||||
|
[0.18, 0.13, 0.075, 0.012], 7)
|
||||||
|
tube([(0.00, 0, 2.10), (0.39, 0.12, 2.48), (0.81, 0.18, 2.94), (1.00, 0.25, 3.29)],
|
||||||
|
[0.14, 0.10, 0.055, 0.012], 7)
|
||||||
|
tube([(-0.06, 0.025, 2.34), (-0.22, -0.38, 2.83), (-0.35, -0.62, 3.26)],
|
||||||
|
[0.10, 0.058, 0.012], 7)
|
||||||
|
for index in range(5):
|
||||||
|
angle = index * math.tau / 5 + 0.2
|
||||||
|
tube([(math.cos(angle) * 0.13, math.sin(angle) * 0.13, 0.30),
|
||||||
|
(math.cos(angle) * 0.36, math.sin(angle) * 0.36, 0.08),
|
||||||
|
(math.cos(angle + 0.12) * 0.66, math.sin(angle + 0.12) * 0.66, -0.015)],
|
||||||
|
[0.12, 0.085, 0.012], 6)
|
||||||
|
trunk = mesh_object("Trunk", vertices, faces, colors, material("BarkPigment"))
|
||||||
|
|
||||||
|
# Export object origins at zero; runtime scales/repositions four shared crowns.
|
||||||
|
bpy.ops.object.select_all(action="SELECT")
|
||||||
|
bpy.ops.export_scene.gltf(filepath=str(OUTPUT / "painterly_tree.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)
|
||||||
|
|
||||||
|
# Display a complete tree in the editable source without baking placement into GLB.
|
||||||
|
source_tint(canopy.data.materials[0], (0.22, 0.42, 0.10))
|
||||||
|
canopy.location.z = 3.8
|
||||||
|
canopy.scale = (1.8, 1.55, 1.4)
|
||||||
|
for location, scale in (((-0.85, 0.05, 3.55), (1.28, 1.22, 1.15)),
|
||||||
|
((0.87, -0.08, 3.67), (1.27, 1.15, 1.1)),
|
||||||
|
((0.05, 0, 4.46), (1.20, 1.08, 1.0))):
|
||||||
|
crown = canopy.copy()
|
||||||
|
crown.data = canopy.data
|
||||||
|
bpy.context.collection.objects.link(crown)
|
||||||
|
crown.location = location
|
||||||
|
crown.scale = scale
|
||||||
|
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 / "painterly_tree.blend"))
|
||||||
|
|
||||||
|
budget = {}
|
||||||
|
for obj in (canopy, trunk):
|
||||||
|
obj.data.calc_loop_triangles()
|
||||||
|
budget[obj.name] = {"triangles": len(obj.data.loop_triangles), "surfaces": 1,
|
||||||
|
"vertices": len(obj.data.vertices)}
|
||||||
|
budget["runtime_tree"] = {"triangles": budget["Canopy"]["triangles"] * 4 + budget["Trunk"]["triangles"],
|
||||||
|
"surfaces": 5, "shared_meshes": 2}
|
||||||
|
budget["glb_bytes"] = (OUTPUT / "painterly_tree.glb").stat().st_size
|
||||||
|
(OUTPUT / "tree_mesh_budget.json").write_text(json.dumps(budget, indent=2) + "\n")
|
||||||
|
print("PAINTERLY TREE BUDGET", json.dumps(budget))
|
||||||
|
|
||||||
|
# Narrow evergreen boughs, scalloped skirts and an uneven leader. One opaque
|
||||||
|
# surface is joined across eight whorls, preserving a restrained draw budget.
|
||||||
|
bpy.ops.object.select_all(action="SELECT")
|
||||||
|
bpy.ops.object.delete(use_global=False)
|
||||||
|
vertices, faces, colors = [], [], []
|
||||||
|
segments = 24
|
||||||
|
for tier in range(8):
|
||||||
|
height = 0.64 + tier * 0.27
|
||||||
|
radius = 0.62 * (1 - tier / 8.8)
|
||||||
|
start = len(vertices)
|
||||||
|
angle_offset = tier * 0.61
|
||||||
|
lean_x = math.sin(tier * 0.8) * 0.035
|
||||||
|
lean_y = math.cos(tier * 1.1) * 0.025
|
||||||
|
for ring, (radial, lift) in enumerate(((0.78, 0.04), (1.0, 0.08), (0.63, 0.25), (0.12, 0.57))):
|
||||||
|
for segment in range(segments):
|
||||||
|
angle = math.tau * segment / segments + angle_offset
|
||||||
|
tip = (1 + math.cos(angle * 8 + tier)) * 0.5
|
||||||
|
organic = math.sin(angle * 3 + tier) * 0.08 + math.sin(angle * 5 - tier * 2) * 0.035
|
||||||
|
r = radius * radial * (0.84 + tip * 0.16 + organic)
|
||||||
|
z = height + lift - tip * (0.085 if ring < 2 else 0.022)
|
||||||
|
vertices.append((lean_x + math.cos(angle) * r, lean_y + math.sin(angle) * r, z))
|
||||||
|
pigment = 0.72 + tier * 0.015 + ring * 0.05 + tip * 0.045
|
||||||
|
colors.append((pigment * 0.94, pigment, pigment * 0.93))
|
||||||
|
if ring:
|
||||||
|
a = start + (ring - 1) * segments + segment
|
||||||
|
b = start + (ring - 1) * segments + (segment + 1) % segments
|
||||||
|
faces.append((a, b, b + segments, a + segments))
|
||||||
|
faces.append(tuple(reversed(range(start, start + segments))))
|
||||||
|
faces.append(tuple(range(len(vertices) - segments, len(vertices))))
|
||||||
|
conifer = mesh_object("ConiferCanopy", vertices, faces, colors, material("EvergreenPigment"))
|
||||||
|
vertices, faces, colors = [], [], []
|
||||||
|
tube([(0, 0, 0), (0.015, -0.01, 0.48), (-0.02, 0, 1.5), (0, 0, 2.58), (0.01, 0, 2.84)],
|
||||||
|
[0.10, 0.07, 0.045, 0.02, 0.004], 8)
|
||||||
|
conifer_trunk = mesh_object("ConiferTrunk", vertices, faces, colors, material("EvergreenBark"))
|
||||||
|
bpy.ops.object.select_all(action="SELECT")
|
||||||
|
bpy.ops.export_scene.gltf(filepath=str(OUTPUT / "painterly_conifer.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)
|
||||||
|
source_tint(conifer.data.materials[0], (0.10, 0.28, 0.17))
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SOURCE / "painterly_conifer.blend"))
|
||||||
|
for obj in (conifer, conifer_trunk):
|
||||||
|
obj.data.calc_loop_triangles()
|
||||||
|
budget["conifer"] = {"triangles": sum(len(o.data.loop_triangles) for o in (conifer, conifer_trunk)),
|
||||||
|
"surfaces": 2, "height_m": 3.10,
|
||||||
|
"glb_bytes": (OUTPUT / "painterly_conifer.glb").stat().st_size}
|
||||||
|
(OUTPUT / "tree_mesh_budget.json").write_text(json.dumps(budget, indent=2) + "\n")
|
||||||
|
print("CONIFER BUDGET", json.dumps(budget["conifer"]))
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Main-scene review at fixed simulation time; never writes a save.
|
||||||
|
|
||||||
|
const OUTPUT := "res://docs/baselines/"
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
root.size = Vector2i(1600, 900)
|
||||||
|
var label := "after"
|
||||||
|
for argument in OS.get_cmdline_user_args():
|
||||||
|
if argument.begins_with("--label="):
|
||||||
|
label = argument.trim_prefix("--label=").validate_filename()
|
||||||
|
var main := load("res://main.tscn").instantiate() as Node3D
|
||||||
|
main.get_node("SimulationManager").set("debug_logs", false)
|
||||||
|
root.add_child(main)
|
||||||
|
var rig := main.get_node("CameraRig")
|
||||||
|
rig.set_process_unhandled_input(false)
|
||||||
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||||
|
await process_frame
|
||||||
|
main.get_node("SimulationManager").set_process(false)
|
||||||
|
var cycle := main.get_node("JajceWorld/DayNightCycle")
|
||||||
|
cycle.set_process(false)
|
||||||
|
cycle.call("_apply_light_rotation", 0.4)
|
||||||
|
cycle.call("_apply_environment", 0.4)
|
||||||
|
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()
|
||||||
|
for _frame in 45:
|
||||||
|
await process_frame
|
||||||
|
main.get_node("Player").set_physics_process(false)
|
||||||
|
rig.set_physics_process(false)
|
||||||
|
rig.call("apply_presentation_preset", main.get_node("Player").global_position, 0.0, 0.5)
|
||||||
|
await _save(label + "_gameplay")
|
||||||
|
var camera := root.get_camera_3d()
|
||||||
|
camera.reparent(main)
|
||||||
|
camera.global_position = Vector3(38, 24, 44)
|
||||||
|
camera.look_at(Vector3(-4, 3, 0))
|
||||||
|
camera.fov = 53.0
|
||||||
|
await _save(label + "_valley")
|
||||||
|
camera.global_position = Vector3(38, 24, 44)
|
||||||
|
camera.look_at(Vector3(-4, 12, 0))
|
||||||
|
camera.fov = 58.0
|
||||||
|
await _save(label + "_vista")
|
||||||
|
camera.global_position = Vector3(5, 4.8, 24)
|
||||||
|
camera.look_at(Vector3(-7, 2.8, 9))
|
||||||
|
camera.fov = 55.0
|
||||||
|
await _save(label + "_meadow")
|
||||||
|
cycle.call("_apply_light_rotation", 0.92)
|
||||||
|
cycle.call("_apply_environment", 0.92)
|
||||||
|
await _save(label + "_night")
|
||||||
|
cycle.call("_apply_light_rotation", 0.4)
|
||||||
|
cycle.call("_apply_environment", 0.4)
|
||||||
|
camera.global_position = Vector3(38, 24, 44)
|
||||||
|
camera.look_at(Vector3(-4, 3, 0))
|
||||||
|
camera.fov = 53.0
|
||||||
|
await _sample_profiles(main, label)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _sample_profiles(main: Node3D, label: String) -> void:
|
||||||
|
var world := main.get_node("JajceWorld") as JajceWorld
|
||||||
|
var samples: Array[Dictionary] = []
|
||||||
|
for quality in [0, 1, 2]:
|
||||||
|
world.apply_presentation_quality(quality)
|
||||||
|
for _frame in 45:
|
||||||
|
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()
|
||||||
|
var sample := {
|
||||||
|
"profile": world.get_active_presentation_quality_name(),
|
||||||
|
"render_scale": root.scaling_3d_scale,
|
||||||
|
"msaa_enum": root.msaa_3d,
|
||||||
|
"wall_frame_p50_ms": frame_times[60],
|
||||||
|
"wall_frame_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),
|
||||||
|
"active_npcs": main.get_node("ActiveNPCs").get_child_count(),
|
||||||
|
}
|
||||||
|
if world.has_node("PainterlyMeadow"):
|
||||||
|
sample["meadow"] = world.get_node("PainterlyMeadow").get_presentation_stats()
|
||||||
|
samples.append(sample)
|
||||||
|
var file := FileAccess.open(OUTPUT + "painterly_" + label + "_metrics.json", FileAccess.WRITE)
|
||||||
|
assert(file != null)
|
||||||
|
(
|
||||||
|
file
|
||||||
|
. store_string(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
JSON
|
||||||
|
. stringify(
|
||||||
|
{
|
||||||
|
"renderer": RenderingServer.get_current_rendering_driver_name(),
|
||||||
|
"device": RenderingServer.get_video_adapter_name(),
|
||||||
|
"resolution": "1600x900",
|
||||||
|
"workload":
|
||||||
|
"Paused main scene, static valley camera; wall time includes vsync",
|
||||||
|
"samples": samples,
|
||||||
|
},
|
||||||
|
"\t"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _save(label: String) -> void:
|
||||||
|
for _frame in 75:
|
||||||
|
await process_frame
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
var capture := root.get_texture().get_image()
|
||||||
|
assert(not capture.is_empty())
|
||||||
|
var path := OUTPUT + "painterly_" + label + ".png"
|
||||||
|
assert(capture.save_png(path) == OK)
|
||||||
|
print("[TOOL] Saved " + path)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d136scmaukb0d
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
[gd_scene load_steps=68 format=3]
|
[gd_scene load_steps=71 format=3]
|
||||||
|
|
||||||
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
|
[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"]
|
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
[ext_resource type="PackedScene" path="res://addons/terrain_3d/extras/particle_example/Terrain3DParticles.tscn" id="19_terrain_particles"]
|
[ext_resource type="PackedScene" path="res://addons/terrain_3d/extras/particle_example/Terrain3DParticles.tscn" id="19_terrain_particles"]
|
||||||
[ext_resource type="Material" path="res://world/jajce/materials/cozy_grass_material.tres" id="20_grass_material"]
|
[ext_resource type="Material" path="res://world/jajce/materials/cozy_grass_material.tres" id="20_grass_material"]
|
||||||
[ext_resource type="Script" path="res://world/jajce/GrassInteractionController.gd" id="21_grass_controller"]
|
[ext_resource type="Script" path="res://world/jajce/GrassInteractionController.gd" id="21_grass_controller"]
|
||||||
[ext_resource type="PackedScene" path="res://assets/foliage/tree.glb" id="22_conifer"]
|
[ext_resource type="PackedScene" path="res://world/jajce/PainterlyConifer.tscn" id="22_conifer"]
|
||||||
[ext_resource type="PackedScene" path="res://assets/foliage/tree-high.glb" id="23_conifer_high"]
|
[ext_resource type="PackedScene" path="res://world/jajce/PainterlyConifer.tscn" id="23_conifer_high"]
|
||||||
[ext_resource type="PackedScene" path="res://world/jajce/CozyButterfly.tscn" id="24_butterfly"]
|
[ext_resource type="PackedScene" path="res://world/jajce/CozyButterfly.tscn" id="24_butterfly"]
|
||||||
[ext_resource type="Material" path="res://world/jajce/materials/cozy_grass_process_material.tres" id="25_grass_process"]
|
[ext_resource type="Material" path="res://world/jajce/materials/cozy_grass_process_material.tres" id="25_grass_process"]
|
||||||
[ext_resource type="PackedScene" path="res://world/jajce/RiverbankResourceCluster.tscn" id="26_resource_cluster"]
|
[ext_resource type="PackedScene" path="res://world/jajce/RiverbankResourceCluster.tscn" id="26_resource_cluster"]
|
||||||
@@ -33,8 +33,12 @@
|
|||||||
|
|
||||||
[ext_resource type="ArrayMesh" path="res://assets/storybook/meadow_grass.res" id="31_meadow"]
|
[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"]
|
[ext_resource type="Shader" path="res://world/jajce/materials/storybook_sky.gdshader" id="32_sky"]
|
||||||
|
[ext_resource type="Texture2D" path="res://assets/painterly/painted_sky.png" id="36_painted_sky"]
|
||||||
[ext_resource type="PackedScene" path="res://world/jajce/StylizedBerryPatch.tscn" id="33_berry"]
|
[ext_resource type="PackedScene" path="res://world/jajce/StylizedBerryPatch.tscn" id="33_berry"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/jajce/PainterlyMeadow.gd" id="34_meadow"]
|
||||||
|
[ext_resource type="PackedScene" path="res://world/jajce/HarvestableTreePresentation.tscn" id="35_harvest_tree"]
|
||||||
|
|
||||||
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
||||||
_shader_parameters = {
|
_shader_parameters = {
|
||||||
"auto_base_texture": 0,
|
"auto_base_texture": 0,
|
||||||
@@ -42,8 +46,8 @@ _shader_parameters = {
|
|||||||
"auto_slope": 0.62,
|
"auto_slope": 0.62,
|
||||||
"blend_sharpness": 0.88,
|
"blend_sharpness": 0.88,
|
||||||
"enable_macro_variation": true,
|
"enable_macro_variation": true,
|
||||||
"macro_variation1": Color(0.84, 0.95, 0.73, 1),
|
"macro_variation1": Color(0.91, 0.98, 0.80, 1),
|
||||||
"macro_variation2": Color(0.60, 0.79, 0.61, 1),
|
"macro_variation2": Color(0.76, 0.89, 0.76, 1),
|
||||||
"macro_variation_slope": 0.48
|
"macro_variation_slope": 0.48
|
||||||
}
|
}
|
||||||
world_background = 0
|
world_background = 0
|
||||||
@@ -179,6 +183,7 @@ size = Vector3(2.9, 0.08, 1.4)
|
|||||||
[sub_resource type="ShaderMaterial" id="SkyMaterial_jajce"]
|
[sub_resource type="ShaderMaterial" id="SkyMaterial_jajce"]
|
||||||
resource_local_to_scene = true
|
resource_local_to_scene = true
|
||||||
shader = ExtResource("32_sky")
|
shader = ExtResource("32_sky")
|
||||||
|
shader_parameter/painted_panorama = ExtResource("36_painted_sky")
|
||||||
|
|
||||||
[sub_resource type="Sky" id="Sky_jajce"]
|
[sub_resource type="Sky" id="Sky_jajce"]
|
||||||
resource_local_to_scene = true
|
resource_local_to_scene = true
|
||||||
@@ -188,17 +193,17 @@ sky_material = SubResource("SkyMaterial_jajce")
|
|||||||
resource_local_to_scene = true
|
resource_local_to_scene = true
|
||||||
background_mode = 2
|
background_mode = 2
|
||||||
sky = SubResource("Sky_jajce")
|
sky = SubResource("Sky_jajce")
|
||||||
background_energy_multiplier = 0.9
|
background_energy_multiplier = 1.0
|
||||||
ambient_light_source = 3
|
ambient_light_source = 2
|
||||||
ambient_light_color = Color(0.95, 0.9, 0.76, 1)
|
ambient_light_color = Color(0.65, 0.8, 0.87, 1)
|
||||||
ambient_light_energy = 0.75
|
ambient_light_energy = 0.65
|
||||||
reflected_light_source = 2
|
reflected_light_source = 2
|
||||||
tonemap_mode = 2
|
tonemap_mode = 2
|
||||||
glow_enabled = true
|
glow_enabled = true
|
||||||
glow_normalized = true
|
glow_normalized = true
|
||||||
glow_intensity = 0.18
|
glow_intensity = 0.12
|
||||||
glow_strength = 0.9
|
glow_strength = 0.9
|
||||||
glow_bloom = 0.03
|
glow_bloom = 0.015
|
||||||
fog_enabled = true
|
fog_enabled = true
|
||||||
fog_light_color = Color(0.88, 0.84, 0.7, 1)
|
fog_light_color = Color(0.88, 0.84, 0.7, 1)
|
||||||
fog_light_energy = 0.78
|
fog_light_energy = 0.78
|
||||||
@@ -216,8 +221,8 @@ volumetric_fog_length = 96.0
|
|||||||
volumetric_fog_detail_spread = 1.6
|
volumetric_fog_detail_spread = 1.6
|
||||||
adjustment_enabled = true
|
adjustment_enabled = true
|
||||||
adjustment_brightness = 1.0
|
adjustment_brightness = 1.0
|
||||||
adjustment_contrast = 1.03
|
adjustment_contrast = 1.015
|
||||||
adjustment_saturation = 1.04
|
adjustment_saturation = 1.07
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_guard"]
|
[sub_resource type="StandardMaterial3D" id="Material_guard"]
|
||||||
albedo_color = Color(0.55, 0.45, 0.35, 1)
|
albedo_color = Color(0.55, 0.45, 0.35, 1)
|
||||||
@@ -384,6 +389,9 @@ scale = Vector3(1.1, 1, 9)
|
|||||||
cast_shadow = 0
|
cast_shadow = 0
|
||||||
mesh = SubResource("Mesh_path_readability")
|
mesh = SubResource("Mesh_path_readability")
|
||||||
|
|
||||||
|
[node name="PainterlyMeadow" type="Node3D" parent="."]
|
||||||
|
script = ExtResource("34_meadow")
|
||||||
|
|
||||||
[node name="FoliageRoot" type="Node3D" parent="."]
|
[node name="FoliageRoot" type="Node3D" parent="."]
|
||||||
|
|
||||||
[node name="Tree_West_01" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
[node name="Tree_West_01" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
@@ -448,6 +456,91 @@ position = Vector3(8, 0, -31)
|
|||||||
rotation_degrees = Vector3(0, -20, 0)
|
rotation_degrees = Vector3(0, -20, 0)
|
||||||
scale = Vector3(2.65, 2.65, 2.65)
|
scale = Vector3(2.65, 2.65, 2.65)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_01" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(-34, 0, -29)
|
||||||
|
rotation_degrees = Vector3(0, 0, 0)
|
||||||
|
scale = Vector3(1.3, 1.3, 1.3)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_02" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(-39, 0, -25)
|
||||||
|
rotation_degrees = Vector3(0, 137, 0)
|
||||||
|
scale = Vector3(1.6, 1.6, 1.6)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_03" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(-44, 0, -18)
|
||||||
|
rotation_degrees = Vector3(0, 274, 0)
|
||||||
|
scale = Vector3(1.5, 1.5, 1.5)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_04" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(-45, 0, -8)
|
||||||
|
rotation_degrees = Vector3(0, 51, 0)
|
||||||
|
scale = Vector3(1.8, 1.8, 1.8)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_05" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(-43, 0, 4)
|
||||||
|
rotation_degrees = Vector3(0, 188, 0)
|
||||||
|
scale = Vector3(1.15, 1.15, 1.15)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_06" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(-41, 0, 16)
|
||||||
|
rotation_degrees = Vector3(0, 325, 0)
|
||||||
|
scale = Vector3(1.3, 1.3, 1.3)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_07" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(-36, 0, 28)
|
||||||
|
rotation_degrees = Vector3(0, 102, 0)
|
||||||
|
scale = Vector3(1.2, 1.2, 1.2)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_08" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(-26, 0, -35)
|
||||||
|
rotation_degrees = Vector3(0, 239, 0)
|
||||||
|
scale = Vector3(1.2, 1.2, 1.2)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_09" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(-19, 0, -37)
|
||||||
|
rotation_degrees = Vector3(0, 16, 0)
|
||||||
|
scale = Vector3(1.55, 1.55, 1.55)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_10" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(-11, 0, -38)
|
||||||
|
rotation_degrees = Vector3(0, 153, 0)
|
||||||
|
scale = Vector3(1.35, 1.35, 1.35)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_11" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(-3, 0, -36)
|
||||||
|
rotation_degrees = Vector3(0, 290, 0)
|
||||||
|
scale = Vector3(1.4, 1.4, 1.4)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_12" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(5, 0, -39)
|
||||||
|
rotation_degrees = Vector3(0, 67, 0)
|
||||||
|
scale = Vector3(1.25, 1.25, 1.25)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_13" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(13, 0, -35)
|
||||||
|
rotation_degrees = Vector3(0, 204, 0)
|
||||||
|
scale = Vector3(1.3, 1.3, 1.3)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_14" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(43, 0, -14)
|
||||||
|
rotation_degrees = Vector3(0, 341, 0)
|
||||||
|
scale = Vector3(1.5, 1.5, 1.5)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_15" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(46, 0, -5)
|
||||||
|
rotation_degrees = Vector3(0, 118, 0)
|
||||||
|
scale = Vector3(1.5, 1.5, 1.5)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_16" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(43, 0, 4)
|
||||||
|
rotation_degrees = Vector3(0, 255, 0)
|
||||||
|
scale = Vector3(1.15, 1.15, 1.15)
|
||||||
|
|
||||||
|
[node name="Woodland_Edge_17" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
||||||
|
position = Vector3(43, 0, 14)
|
||||||
|
rotation_degrees = Vector3(0, 32, 0)
|
||||||
|
scale = Vector3(1.3, 1.3, 1.3)
|
||||||
|
|
||||||
[node name="WildlifeRoot" type="Node3D" parent="."]
|
[node name="WildlifeRoot" type="Node3D" parent="."]
|
||||||
|
|
||||||
[node name="Butterfly_Meadow_01" parent="WildlifeRoot" instance=ExtResource("24_butterfly")]
|
[node name="Butterfly_Meadow_01" parent="WildlifeRoot" instance=ExtResource("24_butterfly")]
|
||||||
@@ -579,6 +672,12 @@ yield_per_action = 3.0
|
|||||||
safety_risk = 0.12
|
safety_risk = 0.12
|
||||||
comfort_distance = 24.0
|
comfort_distance = 24.0
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" parent="WorldObjects/ResourceNodes/Tree_01/Visual"]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="TreePresentation" parent="WorldObjects/ResourceNodes/Tree_01/Visual" instance=ExtResource("35_harvest_tree")]
|
||||||
|
scale = Vector3(0.82, 0.82, 0.82)
|
||||||
|
|
||||||
[node name="Tree_02" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
[node name="Tree_02" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||||
position = Vector3(10, 0, -4)
|
position = Vector3(10, 0, -4)
|
||||||
node_id = &"tree_02"
|
node_id = &"tree_02"
|
||||||
@@ -589,6 +688,12 @@ yield_per_action = 3.0
|
|||||||
safety_risk = 0.16
|
safety_risk = 0.16
|
||||||
comfort_distance = 26.0
|
comfort_distance = 26.0
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" parent="WorldObjects/ResourceNodes/Tree_02/Visual"]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="TreePresentation" parent="WorldObjects/ResourceNodes/Tree_02/Visual" instance=ExtResource("35_harvest_tree")]
|
||||||
|
scale = Vector3(0.82, 0.82, 0.82)
|
||||||
|
|
||||||
[node name="Tree_North_Outskirts_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
[node name="Tree_North_Outskirts_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||||
position = Vector3(6, 0, 12)
|
position = Vector3(6, 0, 12)
|
||||||
node_id = &"tree_north_outskirts_01"
|
node_id = &"tree_north_outskirts_01"
|
||||||
@@ -600,6 +705,12 @@ safety_risk = 0.2
|
|||||||
comfort_distance = 28.0
|
comfort_distance = 28.0
|
||||||
discovery_priority = 0.25
|
discovery_priority = 0.25
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" parent="WorldObjects/ResourceNodes/Tree_North_Outskirts_01/Visual"]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="TreePresentation" parent="WorldObjects/ResourceNodes/Tree_North_Outskirts_01/Visual" instance=ExtResource("35_harvest_tree")]
|
||||||
|
scale = Vector3(0.82, 0.82, 0.82)
|
||||||
|
|
||||||
[node name="Tree_South_Edge_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
[node name="Tree_South_Edge_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||||
position = Vector3(-12, 0, -12)
|
position = Vector3(-12, 0, -12)
|
||||||
node_id = &"tree_south_edge_01"
|
node_id = &"tree_south_edge_01"
|
||||||
@@ -610,6 +721,12 @@ yield_per_action = 2.0
|
|||||||
safety_risk = 0.24
|
safety_risk = 0.24
|
||||||
comfort_distance = 26.0
|
comfort_distance = 26.0
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" parent="WorldObjects/ResourceNodes/Tree_South_Edge_01/Visual"]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="TreePresentation" parent="WorldObjects/ResourceNodes/Tree_South_Edge_01/Visual" instance=ExtResource("35_harvest_tree")]
|
||||||
|
scale = Vector3(0.82, 0.82, 0.82)
|
||||||
|
|
||||||
[node name="WoodPile_Village_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
[node name="WoodPile_Village_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||||
position = Vector3(5, 0, -13)
|
position = Vector3(5, 0, -13)
|
||||||
node_id = &"wood_pile_village_01"
|
node_id = &"wood_pile_village_01"
|
||||||
@@ -914,7 +1031,8 @@ text = "Rest Bench"
|
|||||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||||
rotation_degrees = Vector3(-55, -35, 0)
|
rotation_degrees = Vector3(-55, -35, 0)
|
||||||
light_color = Color(1, 0.87, 0.72, 1)
|
light_color = Color(1, 0.87, 0.72, 1)
|
||||||
light_energy = 1.45
|
light_energy = 1.35
|
||||||
|
light_angular_distance = 0.6
|
||||||
shadow_enabled = true
|
shadow_enabled = true
|
||||||
directional_shadow_max_distance = 180.0
|
directional_shadow_max_distance = 180.0
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
extends Node3D
|
||||||
|
## Shared opaque boughs; decorative presentation without simulation state.
|
||||||
|
|
||||||
|
const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader")
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var canopy := $Geometry/ConiferCanopy as MeshInstance3D
|
||||||
|
var material := ShaderMaterial.new()
|
||||||
|
material.shader = WIND_SHADER
|
||||||
|
material.set_shader_parameter("albedo", Color(0.28, 0.47, 0.33))
|
||||||
|
material.set_shader_parameter("painted_vertex_weight", 1.0)
|
||||||
|
material.set_shader_parameter("wind_strength", 0.055)
|
||||||
|
material.set_shader_parameter("wind_speed", 0.55)
|
||||||
|
material.set_shader_parameter("wind_phase", float(posmod(hash(global_position), 6283)) / 1000.0)
|
||||||
|
material.set_shader_parameter("wind_base_height", 0.45)
|
||||||
|
material.set_shader_parameter("wind_full_height", 3.1)
|
||||||
|
canopy.material_override = material
|
||||||
|
var trunk := $Geometry/ConiferTrunk as MeshInstance3D
|
||||||
|
var bark := StandardMaterial3D.new()
|
||||||
|
bark.vertex_color_use_as_albedo = true
|
||||||
|
bark.roughness = 0.95
|
||||||
|
bark.specular_mode = BaseMaterial3D.SPECULAR_DISABLED
|
||||||
|
trunk.material_override = bark
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bk3cxfe3kph2x
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[gd_scene load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/jajce/PainterlyConifer.gd" id="1_script"]
|
||||||
|
[ext_resource type="PackedScene" path="res://assets/painterly/painterly_conifer.glb" id="2_geometry"]
|
||||||
|
|
||||||
|
[node name="PainterlyConifer" type="Node3D"]
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="Geometry" parent="." instance=ExtResource("2_geometry")]
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
class_name PainterlyMeadow
|
||||||
|
extends Node3D
|
||||||
|
## Decorative flower drifts. Local seeded placement never touches simulation RNG.
|
||||||
|
|
||||||
|
const FLOWER_SCENES: Array[PackedScene] = [
|
||||||
|
preload("res://assets/meadow/cream_daisy.glb"),
|
||||||
|
preload("res://assets/meadow/pink_cosmos.glb"),
|
||||||
|
preload("res://assets/meadow/yellow_buttercup.glb"),
|
||||||
|
preload("res://assets/meadow/lavender_spire.glb"),
|
||||||
|
]
|
||||||
|
const FLOWER_SHADER := preload("res://world/jajce/materials/meadow_flowers.gdshader")
|
||||||
|
const PROFILE_DENSITY := [1.0, 0.65, 0.32]
|
||||||
|
const PROFILE_DISTANCE := [56.0, 46.0, 34.0]
|
||||||
|
|
||||||
|
@export var terrain_path := NodePath("../TerrainRoot/Terrain3D")
|
||||||
|
@export_range(0, 450, 1) var clumps_per_species := 420
|
||||||
|
@export var placement_seed := 57091
|
||||||
|
## Local x/z center, then ellipse x/z radii. Each drift favors one flower color.
|
||||||
|
@export var drifts: Array[Vector4] = [
|
||||||
|
Vector4(7.0, 20.0, 7.0, 3.5),
|
||||||
|
Vector4(15.0, 13.0, 2.4, 4.0),
|
||||||
|
Vector4(-5.0, 20.0, 5.0, 3.0),
|
||||||
|
Vector4(-18.0, -21.0, 4.5, 3.2),
|
||||||
|
Vector4(14.0, -10.0, 3.8, 4.5),
|
||||||
|
Vector4(-25.0, -17.0, 3.5, 4.0),
|
||||||
|
Vector4(8.0, -20.0, 5.0, 2.5),
|
||||||
|
Vector4(-2.0, -24.0, 6.0, 2.2),
|
||||||
|
Vector4(17.0, 24.0, 4.0, 3.0),
|
||||||
|
Vector4(-9.0, 21.0, 3.6, 2.5),
|
||||||
|
Vector4(9.0, 7.2, 3.2, 1.8),
|
||||||
|
Vector4(-16.0, 19.0, 3.0, 2.4),
|
||||||
|
]
|
||||||
|
|
||||||
|
var _terrain: Terrain3D
|
||||||
|
var _batches: Array[MultiMeshInstance3D] = []
|
||||||
|
var _material: ShaderMaterial
|
||||||
|
var _quality := 0
|
||||||
|
var _clearings: Array[Vector3] = []
|
||||||
|
var _path_strips: Array[MeshInstance3D] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
call_deferred("_build_meadow")
|
||||||
|
|
||||||
|
|
||||||
|
func apply_presentation_quality(quality: int) -> void:
|
||||||
|
_quality = clampi(quality, 0, PROFILE_DENSITY.size() - 1)
|
||||||
|
for batch in _batches:
|
||||||
|
batch.multimesh.visible_instance_count = floori(
|
||||||
|
batch.multimesh.instance_count * PROFILE_DENSITY[_quality]
|
||||||
|
)
|
||||||
|
if _material != null:
|
||||||
|
_material.set_shader_parameter("fade_begin", PROFILE_DISTANCE[_quality] - 12.0)
|
||||||
|
_material.set_shader_parameter("fade_end", PROFILE_DISTANCE[_quality])
|
||||||
|
|
||||||
|
|
||||||
|
func get_presentation_stats() -> Dictionary:
|
||||||
|
var total := 0
|
||||||
|
var visible_count := 0
|
||||||
|
for batch in _batches:
|
||||||
|
total += batch.multimesh.instance_count
|
||||||
|
visible_count += batch.multimesh.visible_instance_count
|
||||||
|
return {
|
||||||
|
"flower_clump_count": total,
|
||||||
|
"visible_flower_clump_count": visible_count,
|
||||||
|
"multimesh_batch_count": _batches.size(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _build_meadow() -> void:
|
||||||
|
if not _batches.is_empty():
|
||||||
|
return
|
||||||
|
_terrain = get_node_or_null(terrain_path) as Terrain3D
|
||||||
|
if _terrain == null or _terrain.data == null or drifts.is_empty():
|
||||||
|
return
|
||||||
|
_collect_clearings()
|
||||||
|
_material = ShaderMaterial.new()
|
||||||
|
_material.shader = FLOWER_SHADER
|
||||||
|
for species in FLOWER_SCENES.size():
|
||||||
|
var source := FLOWER_SCENES[species].instantiate()
|
||||||
|
var mesh := _find_mesh(source)
|
||||||
|
if mesh != null:
|
||||||
|
_add_batch(mesh, species)
|
||||||
|
source.free()
|
||||||
|
apply_presentation_quality(_quality)
|
||||||
|
|
||||||
|
|
||||||
|
func _find_mesh(node: Node) -> Mesh:
|
||||||
|
if node is MeshInstance3D:
|
||||||
|
return (node as MeshInstance3D).mesh
|
||||||
|
for child in node.get_children():
|
||||||
|
var mesh := _find_mesh(child)
|
||||||
|
if mesh != null:
|
||||||
|
return mesh
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _add_batch(mesh: Mesh, species: int) -> void:
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = placement_seed + species * 173
|
||||||
|
var transforms: Array[Transform3D] = []
|
||||||
|
var variations: Array[Color] = []
|
||||||
|
for _attempt in clumps_per_species * 18:
|
||||||
|
if transforms.size() >= clumps_per_species:
|
||||||
|
break
|
||||||
|
var drift_index := rng.randi_range(0, drifts.size() - 1)
|
||||||
|
# Broad color masses with a few companion flowers at the edges.
|
||||||
|
if drift_index % FLOWER_SCENES.size() != species and rng.randf() > 0.12:
|
||||||
|
continue
|
||||||
|
var drift := drifts[drift_index]
|
||||||
|
var angle := rng.randf_range(0.0, TAU)
|
||||||
|
var radius := pow(rng.randf(), 0.7) * (0.9 + 0.12 * sin(angle * 3.0 + drift_index))
|
||||||
|
var offset := Vector3(
|
||||||
|
drift.x + cos(angle) * radius * drift.z, 0.0, drift.y + sin(angle) * radius * drift.w
|
||||||
|
)
|
||||||
|
var world_sample := to_global(offset)
|
||||||
|
if _is_cleared(world_sample):
|
||||||
|
continue
|
||||||
|
var height := _terrain.data.get_height(world_sample)
|
||||||
|
if is_nan(height):
|
||||||
|
continue
|
||||||
|
var nearby := _terrain.data.get_height(world_sample + Vector3(0.35, 0.0, 0.35))
|
||||||
|
if is_nan(nearby) or absf(height - nearby) > 0.32:
|
||||||
|
continue
|
||||||
|
var local_position := to_local(Vector3(world_sample.x, height - 0.018, world_sample.z))
|
||||||
|
var scale_variation := rng.randf_range(0.80, 1.28)
|
||||||
|
var basis := Basis(Vector3.UP, rng.randf_range(0.0, TAU)).scaled(
|
||||||
|
Vector3.ONE * scale_variation
|
||||||
|
)
|
||||||
|
transforms.append(Transform3D(basis, local_position))
|
||||||
|
variations.append(Color(rng.randf(), 0.0, 0.0, 1.0))
|
||||||
|
var multimesh := MultiMesh.new()
|
||||||
|
multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||||||
|
# Compatibility needs an explicit neutral color alongside the custom buffer
|
||||||
|
# to preserve the GLB's painted vertex colors.
|
||||||
|
multimesh.use_colors = true
|
||||||
|
multimesh.use_custom_data = true
|
||||||
|
multimesh.mesh = mesh
|
||||||
|
multimesh.instance_count = transforms.size()
|
||||||
|
for index in transforms.size():
|
||||||
|
multimesh.set_instance_transform(index, transforms[index])
|
||||||
|
multimesh.set_instance_color(index, Color.WHITE)
|
||||||
|
multimesh.set_instance_custom_data(index, variations[index])
|
||||||
|
var batch := MultiMeshInstance3D.new()
|
||||||
|
batch.name = "FlowerDrifts_%d" % species
|
||||||
|
batch.multimesh = multimesh
|
||||||
|
batch.material_override = _material
|
||||||
|
batch.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
batch.extra_cull_margin = 0.4
|
||||||
|
add_child(batch)
|
||||||
|
_batches.append(batch)
|
||||||
|
|
||||||
|
|
||||||
|
func _collect_clearings() -> void:
|
||||||
|
var village := get_node_or_null("../VillageRoot")
|
||||||
|
if village != null:
|
||||||
|
for child in village.get_children():
|
||||||
|
if child is MeshInstance3D and str(child.name).begins_with("Path_"):
|
||||||
|
_path_strips.append(child)
|
||||||
|
elif child is Node3D:
|
||||||
|
var radius := 4.3 if str(child.name).begins_with("House_") else 4.5
|
||||||
|
_add_clearing(child, radius)
|
||||||
|
var fortress := get_node_or_null("../FortressBlockout") as Node3D
|
||||||
|
if fortress != null:
|
||||||
|
_add_clearing(fortress, 11.0)
|
||||||
|
for root_path in [
|
||||||
|
"../WorldObjects/ResourceNodes",
|
||||||
|
"../WorldObjects/StorageSites",
|
||||||
|
"../WorldObjects/ActivitySites",
|
||||||
|
"../WorldObjects/AnimalHabitats",
|
||||||
|
]:
|
||||||
|
var root := get_node_or_null(root_path)
|
||||||
|
if root != null:
|
||||||
|
for child in root.get_children():
|
||||||
|
if child is Node3D:
|
||||||
|
_add_worksite_clearing(child)
|
||||||
|
var clusters := get_node_or_null("../WorldObjects/ResourceClusters")
|
||||||
|
if clusters != null:
|
||||||
|
for cluster in clusters.get_children():
|
||||||
|
var anchors := cluster.get_node_or_null("ResourceAnchors")
|
||||||
|
if anchors == null:
|
||||||
|
continue
|
||||||
|
for anchor in anchors.get_children():
|
||||||
|
if anchor is Node3D:
|
||||||
|
_add_worksite_clearing(anchor)
|
||||||
|
|
||||||
|
|
||||||
|
func _add_worksite_clearing(node: Node3D) -> void:
|
||||||
|
_add_clearing(node, 2.0)
|
||||||
|
var interaction := node.get_node_or_null("InteractionPoint") as Node3D
|
||||||
|
if interaction != null:
|
||||||
|
_add_clearing(interaction, 1.2)
|
||||||
|
|
||||||
|
|
||||||
|
func _add_clearing(node: Node3D, radius: float) -> void:
|
||||||
|
_clearings.append(Vector3(node.global_position.x, node.global_position.z, radius))
|
||||||
|
|
||||||
|
|
||||||
|
func _is_cleared(point: Vector3) -> bool:
|
||||||
|
var flat := Vector2(point.x, point.z)
|
||||||
|
for clearing in _clearings:
|
||||||
|
if flat.distance_squared_to(Vector2(clearing.x, clearing.y)) < clearing.z * clearing.z:
|
||||||
|
return true
|
||||||
|
for strip in _path_strips:
|
||||||
|
var box := strip.mesh as BoxMesh
|
||||||
|
if box == null:
|
||||||
|
continue
|
||||||
|
var local := strip.to_local(point)
|
||||||
|
var half_width := box.size.x * 0.5 + 0.35 / strip.global_basis.x.length()
|
||||||
|
if absf(local.x) < half_width and absf(local.z) < box.size.z * 0.5 + 0.08:
|
||||||
|
return true
|
||||||
|
var river_distance := absf(point.x - JajceWatercourse.downstream_center_x(point.z))
|
||||||
|
return (
|
||||||
|
point.z >= JajceWatercourse.DOWNSTREAM_START_Z - 3.0
|
||||||
|
and point.z <= JajceWatercourse.DOWNSTREAM_END_Z
|
||||||
|
and river_distance < JajceWatercourse.downstream_half_width(point.z) + 1.7
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cacjl430m8mcw
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
||||||
[ext_resource type="PackedScene" path="res://world/jajce/StylizedTree.tscn" id="3_tree"]
|
[ext_resource type="PackedScene" path="res://world/jajce/StylizedTree.tscn" id="3_tree"]
|
||||||
[ext_resource type="PackedScene" path="res://world/jajce/StylizedBerryPatch.tscn" id="4_berry"]
|
[ext_resource type="PackedScene" path="res://world/jajce/StylizedBerryPatch.tscn" id="4_berry"]
|
||||||
[ext_resource type="PackedScene" path="res://assets/foliage/tree.glb" id="5_conifer"]
|
[ext_resource type="PackedScene" path="res://world/jajce/PainterlyConifer.tscn" id="5_conifer"]
|
||||||
[ext_resource type="PackedScene" path="res://world/jajce/HarvestableTreePresentation.tscn" id="6_harvestable_tree"]
|
[ext_resource type="PackedScene" path="res://world/jajce/HarvestableTreePresentation.tscn" id="6_harvestable_tree"]
|
||||||
|
|
||||||
[node name="RiverbankResourceCluster" type="Node3D"]
|
[node name="RiverbankResourceCluster" type="Node3D"]
|
||||||
|
|||||||
@@ -7,15 +7,20 @@ enum CanopyVariant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const COLORS := {
|
const COLORS := {
|
||||||
CanopyVariant.GREEN: Color(0.32, 0.51, 0.29),
|
CanopyVariant.GREEN: Color(0.40, 0.62, 0.28),
|
||||||
CanopyVariant.DARK_GREEN: Color(0.22, 0.41, 0.30),
|
CanopyVariant.DARK_GREEN: Color(0.26, 0.51, 0.34),
|
||||||
CanopyVariant.YELLOW_GREEN: Color(0.49, 0.60, 0.28),
|
CanopyVariant.YELLOW_GREEN: Color(0.55, 0.67, 0.29),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TREE_MODEL := preload("res://assets/painterly/painterly_tree.glb")
|
||||||
const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader")
|
const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader")
|
||||||
const WIND_STRENGTH := 0.07
|
const WIND_STRENGTH := 0.07
|
||||||
const WIND_SPEED := 0.55
|
const WIND_SPEED := 0.55
|
||||||
|
|
||||||
|
static var _canopy_mesh: Mesh
|
||||||
|
static var _trunk_mesh: Mesh
|
||||||
|
static var _bark_material: StandardMaterial3D
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
for child in get_children():
|
for child in get_children():
|
||||||
@@ -28,76 +33,56 @@ func _ready() -> void:
|
|||||||
var canopy_radius := 1.25 + rng.randf() * 0.6
|
var canopy_radius := 1.25 + rng.randf() * 0.6
|
||||||
var wind_phase := rng.randf_range(0.0, TAU)
|
var wind_phase := rng.randf_range(0.0, TAU)
|
||||||
|
|
||||||
var trunk_mesh := CylinderMesh.new()
|
_ensure_shared_meshes()
|
||||||
trunk_mesh.top_radius = 0.22
|
|
||||||
trunk_mesh.bottom_radius = 0.32
|
|
||||||
trunk_mesh.height = 3.4
|
|
||||||
trunk_mesh.radial_segments = 9
|
|
||||||
|
|
||||||
var trunk_mat := StandardMaterial3D.new()
|
|
||||||
trunk_mat.albedo_color = Color(0.27, 0.16, 0.09)
|
|
||||||
trunk_mat.roughness = 0.95
|
|
||||||
trunk_mesh.material = trunk_mat
|
|
||||||
|
|
||||||
var trunk := MeshInstance3D.new()
|
var trunk := MeshInstance3D.new()
|
||||||
trunk.name = "Trunk"
|
trunk.name = "Trunk"
|
||||||
trunk.mesh = trunk_mesh
|
trunk.mesh = _trunk_mesh
|
||||||
trunk.position = Vector3(0, 1.7, 0)
|
trunk.material_override = _bark_material
|
||||||
add_child(trunk)
|
add_child(trunk)
|
||||||
|
|
||||||
_add_branch(Vector3(-0.18, 2.45, 0.0), Vector3(0.0, 0.0, 42.0), 1.45, trunk_mat)
|
|
||||||
_add_branch(Vector3(0.2, 2.6, 0.08), Vector3(24.0, 0.0, -38.0), 1.25, trunk_mat)
|
|
||||||
|
|
||||||
var canopy_color: Color = COLORS[canopy_variant]
|
var canopy_color: Color = COLORS[canopy_variant]
|
||||||
var crown_height := 3.7 + (canopy_radius - 1.2) * 0.45
|
var crown_height := 3.7 + (canopy_radius - 1.2) * 0.45
|
||||||
_add_canopy(
|
_add_canopy(
|
||||||
"Canopy",
|
"Canopy",
|
||||||
Vector3(0.0, crown_height, 0.0),
|
Vector3(0.0, crown_height, 0.0),
|
||||||
Vector3(canopy_radius * 1.12, canopy_radius, canopy_radius),
|
Vector3(canopy_radius * 1.18, canopy_radius * 0.96, canopy_radius * 1.05),
|
||||||
canopy_color,
|
canopy_color,
|
||||||
wind_phase
|
wind_phase
|
||||||
)
|
)
|
||||||
_add_canopy(
|
_add_canopy(
|
||||||
"CanopyLeft",
|
"CanopyLeft",
|
||||||
Vector3(-0.82, crown_height - 0.15, 0.1),
|
Vector3(-0.94, crown_height - 0.27, 0.14),
|
||||||
Vector3(canopy_radius * 0.78, canopy_radius * 0.72, canopy_radius * 0.76),
|
Vector3(canopy_radius * 0.84, canopy_radius * 0.79, canopy_radius * 0.84),
|
||||||
canopy_color.lightened(0.05),
|
canopy_color.lightened(0.05),
|
||||||
wind_phase + 0.12
|
wind_phase + 0.12
|
||||||
)
|
)
|
||||||
_add_canopy(
|
_add_canopy(
|
||||||
"CanopyRight",
|
"CanopyRight",
|
||||||
Vector3(0.78, crown_height - 0.05, -0.12),
|
Vector3(0.91, crown_height - 0.08, -0.2),
|
||||||
Vector3(canopy_radius * 0.74, canopy_radius * 0.68, canopy_radius * 0.72),
|
Vector3(canopy_radius * 0.82, canopy_radius * 0.73, canopy_radius * 0.79),
|
||||||
canopy_color.darkened(0.04),
|
canopy_color.darkened(0.04),
|
||||||
wind_phase - 0.1
|
wind_phase - 0.1
|
||||||
)
|
)
|
||||||
_add_canopy(
|
_add_canopy(
|
||||||
"CanopyTop",
|
"CanopyTop",
|
||||||
Vector3(0.08, crown_height + 0.72, 0.04),
|
Vector3(0.13, crown_height + 0.68, -0.08),
|
||||||
Vector3(canopy_radius * 0.72, canopy_radius * 0.66, canopy_radius * 0.7),
|
Vector3(canopy_radius * 0.78, canopy_radius * 0.74, canopy_radius * 0.75),
|
||||||
canopy_color.lightened(0.08),
|
canopy_color.lightened(0.08),
|
||||||
wind_phase + 0.05
|
wind_phase + 0.05
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func _add_branch(
|
static func _ensure_shared_meshes() -> void:
|
||||||
branch_position: Vector3,
|
if _canopy_mesh != null:
|
||||||
branch_rotation_degrees: Vector3,
|
return
|
||||||
branch_height: float,
|
var source := TREE_MODEL.instantiate()
|
||||||
material: StandardMaterial3D
|
_canopy_mesh = (source.find_child("Canopy", true, false) as MeshInstance3D).mesh
|
||||||
) -> void:
|
_trunk_mesh = (source.find_child("Trunk", true, false) as MeshInstance3D).mesh
|
||||||
var mesh := CylinderMesh.new()
|
_bark_material = StandardMaterial3D.new()
|
||||||
mesh.top_radius = 0.08
|
_bark_material.vertex_color_use_as_albedo = true
|
||||||
mesh.bottom_radius = 0.14
|
_bark_material.roughness = 0.95
|
||||||
mesh.height = branch_height
|
_bark_material.specular_mode = BaseMaterial3D.SPECULAR_DISABLED
|
||||||
mesh.radial_segments = 7
|
source.free()
|
||||||
mesh.material = material
|
|
||||||
var branch := MeshInstance3D.new()
|
|
||||||
branch.name = "Branch"
|
|
||||||
branch.mesh = mesh
|
|
||||||
branch.position = branch_position
|
|
||||||
branch.rotation_degrees = branch_rotation_degrees
|
|
||||||
add_child(branch)
|
|
||||||
|
|
||||||
|
|
||||||
func _add_canopy(
|
func _add_canopy(
|
||||||
@@ -107,11 +92,6 @@ func _add_canopy(
|
|||||||
canopy_color: Color,
|
canopy_color: Color,
|
||||||
wind_phase: float
|
wind_phase: float
|
||||||
) -> void:
|
) -> void:
|
||||||
var mesh := SphereMesh.new()
|
|
||||||
mesh.radius = 1.0
|
|
||||||
mesh.height = 2.0
|
|
||||||
mesh.radial_segments = 12
|
|
||||||
mesh.rings = 6
|
|
||||||
var material := ShaderMaterial.new()
|
var material := ShaderMaterial.new()
|
||||||
material.shader = WIND_SHADER
|
material.shader = WIND_SHADER
|
||||||
material.set_shader_parameter("albedo", canopy_color)
|
material.set_shader_parameter("albedo", canopy_color)
|
||||||
@@ -119,10 +99,12 @@ func _add_canopy(
|
|||||||
material.set_shader_parameter("wind_strength", WIND_STRENGTH)
|
material.set_shader_parameter("wind_strength", WIND_STRENGTH)
|
||||||
material.set_shader_parameter("wind_speed", WIND_SPEED)
|
material.set_shader_parameter("wind_speed", WIND_SPEED)
|
||||||
material.set_shader_parameter("wind_phase", wind_phase)
|
material.set_shader_parameter("wind_phase", wind_phase)
|
||||||
mesh.material = material
|
material.set_shader_parameter("painted_vertex_weight", 1.0)
|
||||||
var canopy := MeshInstance3D.new()
|
var canopy := MeshInstance3D.new()
|
||||||
canopy.name = canopy_name
|
canopy.name = canopy_name
|
||||||
canopy.mesh = mesh
|
canopy.mesh = _canopy_mesh
|
||||||
|
canopy.material_override = material
|
||||||
canopy.position = canopy_position
|
canopy.position = canopy_position
|
||||||
canopy.scale = canopy_scale
|
canopy.scale = canopy_scale
|
||||||
|
canopy.rotation.y = wind_phase * 0.43 + canopy_position.x * 0.31
|
||||||
add_child(canopy)
|
add_child(canopy)
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ func _build_ribbon(
|
|||||||
var uvs := PackedVector2Array()
|
var uvs := PackedVector2Array()
|
||||||
var indices := PackedInt32Array()
|
var indices := PackedInt32Array()
|
||||||
var row_size := width_segments + 1
|
var row_size := width_segments + 1
|
||||||
|
var pool := $PlungePool as MeshInstance3D
|
||||||
|
var pool_mesh := pool.mesh as PlaneMesh
|
||||||
|
var pool_radii := pool_mesh.size * Vector2(pool.scale.x, pool.scale.z) * 0.5
|
||||||
for z_index in segment_count + 1:
|
for z_index in segment_count + 1:
|
||||||
var progress := float(z_index) / float(segment_count)
|
var progress := float(z_index) / float(segment_count)
|
||||||
var world_z := lerpf(start_z, end_z, progress)
|
var world_z := lerpf(start_z, end_z, progress)
|
||||||
@@ -61,16 +64,30 @@ func _build_ribbon(
|
|||||||
if is_upper
|
if is_upper
|
||||||
else Watercourse.downstream_half_width(world_z)
|
else Watercourse.downstream_half_width(world_z)
|
||||||
)
|
)
|
||||||
|
if not is_upper:
|
||||||
|
# The outlet continues the existing pool silhouette without drawing a
|
||||||
|
# second, coplanar sheet on top of the pool's transparent material.
|
||||||
|
var pool_progress := (world_z - Watercourse.PLUNGE_POOL_CENTER.y) / pool_radii.y
|
||||||
|
if absf(pool_progress) < 1.0:
|
||||||
|
var pool_width := pool_radii.x * sqrt(1.0 - pool_progress * pool_progress)
|
||||||
|
half_width = maxf(half_width, pool_width)
|
||||||
|
# A stream has one surface across its width. Sampling each bank separately
|
||||||
|
# wraps the water mesh up the terrain and makes it read as a solid ramp.
|
||||||
|
var terrain_height := terrain.data.get_height(Vector3(center_x, 0.0, world_z))
|
||||||
|
if is_nan(terrain_height):
|
||||||
|
terrain_height = global_position.y
|
||||||
|
var surface_height := terrain_height + water_offset
|
||||||
|
var left_x := _shoreline_x(
|
||||||
|
terrain, center_x, center_x - half_width, world_z, surface_height
|
||||||
|
)
|
||||||
|
var right_x := _shoreline_x(
|
||||||
|
terrain, center_x, center_x + half_width, world_z, surface_height
|
||||||
|
)
|
||||||
for x_index in width_segments + 1:
|
for x_index in width_segments + 1:
|
||||||
var across := float(x_index) / float(width_segments)
|
var across := float(x_index) / float(width_segments)
|
||||||
var world_x := center_x + lerpf(-half_width, half_width, across)
|
var world_x := lerpf(left_x, right_x, across)
|
||||||
var world_sample := Vector3(world_x, 0.0, world_z)
|
var world_sample := Vector3(world_x, 0.0, world_z)
|
||||||
var terrain_height := terrain.data.get_height(world_sample)
|
vertices.append(to_local(Vector3(world_sample.x, surface_height, world_sample.z)))
|
||||||
if is_nan(terrain_height):
|
|
||||||
terrain_height = global_position.y
|
|
||||||
vertices.append(
|
|
||||||
to_local(Vector3(world_sample.x, terrain_height + water_offset, world_sample.z))
|
|
||||||
)
|
|
||||||
normals.append(Vector3.UP)
|
normals.append(Vector3.UP)
|
||||||
uvs.append(Vector2(across, progress))
|
uvs.append(Vector2(across, progress))
|
||||||
|
|
||||||
@@ -96,8 +113,37 @@ func _build_ribbon(
|
|||||||
return mesh
|
return mesh
|
||||||
|
|
||||||
|
|
||||||
|
func _shoreline_x(
|
||||||
|
terrain: Terrain3D, center_x: float, edge_x: float, world_z: float, surface_height: float
|
||||||
|
) -> float:
|
||||||
|
var edge_height := terrain.data.get_height(Vector3(edge_x, 0.0, world_z))
|
||||||
|
if is_nan(edge_height) or edge_height <= surface_height:
|
||||||
|
return edge_x
|
||||||
|
# The carved basin is wider on one side. Stop its visible surface at the
|
||||||
|
# shore instead of extending the pool's ideal ellipse metres under a hill.
|
||||||
|
var wet_x := center_x
|
||||||
|
var dry_x := edge_x
|
||||||
|
for step in range(1, 17):
|
||||||
|
var sample_x := lerpf(center_x, edge_x, float(step) / 16.0)
|
||||||
|
var sample_height := terrain.data.get_height(Vector3(sample_x, 0.0, world_z))
|
||||||
|
if is_nan(sample_height) or sample_height > surface_height:
|
||||||
|
dry_x = sample_x
|
||||||
|
break
|
||||||
|
wet_x = sample_x
|
||||||
|
for _step in 10:
|
||||||
|
var sample_x := (wet_x + dry_x) * 0.5
|
||||||
|
var sample_height := terrain.data.get_height(Vector3(sample_x, 0.0, world_z))
|
||||||
|
if is_nan(sample_height) or sample_height > surface_height:
|
||||||
|
dry_x = sample_x
|
||||||
|
else:
|
||||||
|
wet_x = sample_x
|
||||||
|
return wet_x
|
||||||
|
|
||||||
|
|
||||||
func _align_pool_to_terrain(terrain: Terrain3D) -> void:
|
func _align_pool_to_terrain(terrain: Terrain3D) -> void:
|
||||||
var pool := $PlungePool as MeshInstance3D
|
var pool := $PlungePool as MeshInstance3D
|
||||||
|
var pool_material := pool.mesh.surface_get_material(0) as ShaderMaterial
|
||||||
|
pool_material.set_shader_parameter("pool_outlet_z", Watercourse.DOWNSTREAM_START_Z)
|
||||||
var pool_sample := Vector3(
|
var pool_sample := Vector3(
|
||||||
Watercourse.PLUNGE_POOL_CENTER.x, 0.0, Watercourse.PLUNGE_POOL_CENTER.y
|
Watercourse.PLUNGE_POOL_CENTER.x, 0.0, Watercourse.PLUNGE_POOL_CENTER.y
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,34 +1,32 @@
|
|||||||
[gd_scene load_steps=8 format=3]
|
[gd_scene load_steps=11 format=3]
|
||||||
|
|
||||||
[ext_resource type="Shader" path="res://world/jajce/materials/water.gdshader" id="1_water_shader"]
|
[ext_resource type="Shader" path="res://world/jajce/materials/water.gdshader" id="1_water_shader"]
|
||||||
[ext_resource type="Script" path="res://world/jajce/TerrainRiverRibbon.gd" id="2_river_script"]
|
[ext_resource type="Script" path="res://world/jajce/TerrainRiverRibbon.gd" id="2_river_script"]
|
||||||
|
[ext_resource type="Shader" path="res://world/jajce/materials/water_foam.gdshader" id="3_foam_shader"]
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_water"]
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_water"]
|
||||||
shader = ExtResource("1_water_shader")
|
shader = ExtResource("1_water_shader")
|
||||||
|
|
||||||
[sub_resource type="CylinderMesh" id="Mesh_plunge_pool"]
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_pool"]
|
||||||
material = SubResource("ShaderMaterial_water")
|
shader = ExtResource("1_water_shader")
|
||||||
top_radius = 7.6
|
shader_parameter/pool_surface = true
|
||||||
bottom_radius = 7.6
|
|
||||||
height = 0.08
|
|
||||||
radial_segments = 32
|
|
||||||
rings = 2
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_foam"]
|
[sub_resource type="PlaneMesh" id="Mesh_plunge_pool"]
|
||||||
transparency = 1
|
material = SubResource("ShaderMaterial_pool")
|
||||||
shading_mode = 0
|
size = Vector2(15.2, 15.2)
|
||||||
albedo_color = Color(0.8, 0.93, 0.86, 0.22)
|
subdivide_width = 20
|
||||||
emission_enabled = true
|
subdivide_depth = 20
|
||||||
emission = Color(0.34, 0.56, 0.48, 1)
|
|
||||||
emission_energy_multiplier = 0.06
|
|
||||||
roughness = 0.74
|
|
||||||
|
|
||||||
[sub_resource type="TorusMesh" id="Mesh_foam_ring"]
|
[sub_resource type="ShaderMaterial" id="Material_foam"]
|
||||||
material = SubResource("Material_foam")
|
shader = ExtResource("3_foam_shader")
|
||||||
inner_radius = 5.15
|
|
||||||
outer_radius = 5.5
|
[sub_resource type="ShaderMaterial" id="Material_pool_foam"]
|
||||||
rings = 28
|
shader = ExtResource("3_foam_shader")
|
||||||
ring_segments = 10
|
shader_parameter/circular_wash = true
|
||||||
|
|
||||||
|
[sub_resource type="PlaneMesh" id="Mesh_foam_ring"]
|
||||||
|
material = SubResource("Material_pool_foam")
|
||||||
|
size = Vector2(11, 11)
|
||||||
|
|
||||||
[sub_resource type="PlaneMesh" id="Mesh_foam_patch"]
|
[sub_resource type="PlaneMesh" id="Mesh_foam_patch"]
|
||||||
material = SubResource("Material_foam")
|
material = SubResource("Material_foam")
|
||||||
@@ -49,10 +47,11 @@ cast_shadow = 0
|
|||||||
|
|
||||||
[node name="PlungePool" type="MeshInstance3D" parent="."]
|
[node name="PlungePool" type="MeshInstance3D" parent="."]
|
||||||
scale = Vector3(1.25, 1, 0.82)
|
scale = Vector3(1.25, 1, 0.82)
|
||||||
|
cast_shadow = 0
|
||||||
mesh = SubResource("Mesh_plunge_pool")
|
mesh = SubResource("Mesh_plunge_pool")
|
||||||
|
|
||||||
[node name="FoamRing" type="MeshInstance3D" parent="."]
|
[node name="FoamRing" type="MeshInstance3D" parent="."]
|
||||||
scale = Vector3(1.25, 0.12, 0.82)
|
scale = Vector3(1.25, 1, 0.82)
|
||||||
cast_shadow = 0
|
cast_shadow = 0
|
||||||
mesh = SubResource("Mesh_foam_ring")
|
mesh = SubResource("Mesh_foam_ring")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[gd_scene load_steps=12 format=3]
|
[gd_scene load_steps=13 format=3]
|
||||||
|
|
||||||
[ext_resource type="Shader" path="res://world/jajce/materials/waterfall.gdshader" id="1_waterfall_shader"]
|
[ext_resource type="Shader" path="res://world/jajce/materials/waterfall.gdshader" id="1_waterfall_shader"]
|
||||||
|
[ext_resource type="Shader" path="res://world/jajce/materials/water_foam.gdshader" id="2_foam_shader"]
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_waterfall"]
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_waterfall"]
|
||||||
shader = ExtResource("1_waterfall_shader")
|
shader = ExtResource("1_waterfall_shader")
|
||||||
@@ -34,14 +35,8 @@ size = Vector3(4.8, 5.5, 3.4)
|
|||||||
material = SubResource("Material_rock_moss")
|
material = SubResource("Material_rock_moss")
|
||||||
size = Vector3(10.4, 0.65, 3.0)
|
size = Vector3(10.4, 0.65, 3.0)
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_lip_foam"]
|
[sub_resource type="ShaderMaterial" id="Material_lip_foam"]
|
||||||
transparency = 1
|
shader = ExtResource("2_foam_shader")
|
||||||
shading_mode = 0
|
|
||||||
albedo_color = Color(0.82, 0.95, 0.87, 0.72)
|
|
||||||
emission_enabled = true
|
|
||||||
emission = Color(0.46, 0.72, 0.62, 1)
|
|
||||||
emission_energy_multiplier = 0.22
|
|
||||||
roughness = 0.62
|
|
||||||
|
|
||||||
[sub_resource type="PlaneMesh" id="Mesh_lip_foam"]
|
[sub_resource type="PlaneMesh" id="Mesh_lip_foam"]
|
||||||
material = SubResource("Material_lip_foam")
|
material = SubResource("Material_lip_foam")
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ extends Node
|
|||||||
|
|
||||||
const TO_RAD := PI / 180.0
|
const TO_RAD := PI / 180.0
|
||||||
|
|
||||||
const DAY_SKY_TOP := Color(0.27, 0.57, 0.74, 1)
|
const DAY_SKY_TOP := Color(0.12, 0.49, 0.69, 1)
|
||||||
const DAY_SKY_HORIZON := Color(0.79, 0.88, 0.82, 1)
|
const DAY_SKY_HORIZON := Color(0.65, 0.84, 0.84, 1)
|
||||||
const DAY_GROUND_BOTTOM := Color(0.12, 0.17, 0.12, 1)
|
const DAY_GROUND_BOTTOM := Color(0.12, 0.17, 0.12, 1)
|
||||||
const DAY_GROUND_HORIZON := Color(0.56, 0.69, 0.57, 1)
|
const DAY_GROUND_HORIZON := DAY_SKY_HORIZON
|
||||||
const DAY_AMBIENT := Color(0.72, 0.83, 0.88, 1)
|
const DAY_AMBIENT := Color(0.65, 0.80, 0.87, 1)
|
||||||
const DAY_AMBIENT_ENERGY := 0.55
|
const DAY_AMBIENT_ENERGY := 0.65
|
||||||
const DAY_FOG_LIGHT := Color(0.70, 0.83, 0.81, 1)
|
const DAY_FOG_LIGHT := Color(0.63, 0.80, 0.82, 1)
|
||||||
const DAY_FOG_LIGHT_ENERGY := 0.8
|
const DAY_FOG_LIGHT_ENERGY := 0.8
|
||||||
const DAY_FOG_DENSITY := 0.0015
|
const DAY_FOG_DENSITY := 0.0018
|
||||||
const DAY_LIGHT_COLOR := Color(1, 0.94, 0.79, 1)
|
const DAY_LIGHT_COLOR := Color(1, 0.94, 0.79, 1)
|
||||||
const DAY_LIGHT_ENERGY := 1.2
|
const DAY_LIGHT_ENERGY := 1.35
|
||||||
|
|
||||||
const NIGHT_SKY_TOP := Color(0.04, 0.04, 0.12, 1)
|
const NIGHT_SKY_TOP := Color(0.04, 0.04, 0.12, 1)
|
||||||
const NIGHT_SKY_HORIZON := Color(0.08, 0.06, 0.14, 1)
|
const NIGHT_SKY_HORIZON := Color(0.08, 0.06, 0.14, 1)
|
||||||
@@ -76,8 +76,13 @@ func _process(delta: float) -> void:
|
|||||||
func _apply_light_rotation(cycle: float) -> void:
|
func _apply_light_rotation(cycle: float) -> void:
|
||||||
if directional_light == null:
|
if directional_light == null:
|
||||||
return
|
return
|
||||||
var angle_rad: float = cycle * 2.0 * PI
|
# The bright interval and the sun's arc share dawn/noon/dusk. The old
|
||||||
directional_light.rotation = Vector3(-PI / 4.0 + sin(angle_rad) * PI / 4.0, angle_rad, 0)
|
# quarter-cycle offset put the brightest morning sun on the horizon.
|
||||||
|
var solar_progress := clampf((cycle - 0.15) / 0.7, 0.0, 1.0)
|
||||||
|
var elevation := 8.0 + sin(solar_progress * PI) * 52.0
|
||||||
|
if cycle < 0.15 or cycle > 0.85:
|
||||||
|
elevation = 32.0 # One cool moon fill during the night interval.
|
||||||
|
directional_light.rotation = Vector3(-elevation * TO_RAD, cycle * TAU, 0)
|
||||||
|
|
||||||
|
|
||||||
func _apply_environment(cycle: float) -> void:
|
func _apply_environment(cycle: float) -> void:
|
||||||
@@ -85,7 +90,6 @@ func _apply_environment(cycle: float) -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
var day_factor := _day_factor(cycle)
|
var day_factor := _day_factor(cycle)
|
||||||
var night_factor := 1.0 - day_factor
|
|
||||||
|
|
||||||
directional_light.light_color = _lerp_color(NIGHT_LIGHT_COLOR, DAY_LIGHT_COLOR, day_factor)
|
directional_light.light_color = _lerp_color(NIGHT_LIGHT_COLOR, DAY_LIGHT_COLOR, day_factor)
|
||||||
directional_light.light_energy = lerpf(NIGHT_LIGHT_ENERGY, DAY_LIGHT_ENERGY, day_factor)
|
directional_light.light_energy = lerpf(NIGHT_LIGHT_ENERGY, DAY_LIGHT_ENERGY, day_factor)
|
||||||
@@ -136,6 +140,7 @@ func _apply_environment(cycle: float) -> void:
|
|||||||
"ground_horizon_color", NIGHT_GROUND_HORIZON.lerp(DAY_GROUND_HORIZON, day_factor)
|
"ground_horizon_color", NIGHT_GROUND_HORIZON.lerp(DAY_GROUND_HORIZON, day_factor)
|
||||||
)
|
)
|
||||||
mat.set_shader_parameter("daylight", day_factor)
|
mat.set_shader_parameter("daylight", day_factor)
|
||||||
|
mat.set_shader_parameter("golden_hour", transition_peak)
|
||||||
|
|
||||||
|
|
||||||
func _find_simulation_node() -> void:
|
func _find_simulation_node() -> void:
|
||||||
@@ -155,10 +160,10 @@ func _day_factor(cycle: float) -> float:
|
|||||||
|
|
||||||
|
|
||||||
func _sunrise_sunset_weight(cycle: float) -> float:
|
func _sunrise_sunset_weight(cycle: float) -> float:
|
||||||
if cycle < 0.15:
|
if cycle < 0.3:
|
||||||
return smoothstep(0.05, 0.15, cycle) * (1.0 - smoothstep(0.1, 0.15, cycle))
|
return smoothstep(0.15, 0.21, cycle) * (1.0 - smoothstep(0.23, 0.3, cycle))
|
||||||
if cycle > 0.85:
|
if cycle > 0.7:
|
||||||
return smoothstep(0.85, 0.95, cycle) * (1.0 - smoothstep(0.9, 0.95, cycle))
|
return smoothstep(0.7, 0.77, cycle) * (1.0 - smoothstep(0.79, 0.85, cycle))
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ static var _viewport_scale_states: Dictionary = {}
|
|||||||
var _active_presentation_quality := -1
|
var _active_presentation_quality := -1
|
||||||
var _authored_presentation_captured := false
|
var _authored_presentation_captured := false
|
||||||
var _authored_render_scale := 1.0
|
var _authored_render_scale := 1.0
|
||||||
|
var _authored_msaa: int = Viewport.MSAA_DISABLED
|
||||||
var _authored_glow_enabled := false
|
var _authored_glow_enabled := false
|
||||||
var _authored_volumetric_fog_enabled := HIGH_VOLUMETRIC_FOG_ENABLED
|
var _authored_volumetric_fog_enabled := HIGH_VOLUMETRIC_FOG_ENABLED
|
||||||
var _authored_adjustment_enabled := false
|
var _authored_adjustment_enabled := false
|
||||||
@@ -45,6 +46,7 @@ var _authored_grass_interactors := 1
|
|||||||
var _authored_grass_update_interval := 0.1
|
var _authored_grass_update_interval := 0.1
|
||||||
var _authored_grass_controller_enabled := true
|
var _authored_grass_controller_enabled := true
|
||||||
var _last_applied_render_scale := -1.0
|
var _last_applied_render_scale := -1.0
|
||||||
|
var _last_applied_msaa := -1
|
||||||
var _viewport_instance_id := 0
|
var _viewport_instance_id := 0
|
||||||
|
|
||||||
|
|
||||||
@@ -78,7 +80,8 @@ func apply_presentation_quality(quality: int) -> bool:
|
|||||||
_authored_volumetric_fog_enabled,
|
_authored_volumetric_fog_enabled,
|
||||||
_authored_adjustment_enabled,
|
_authored_adjustment_enabled,
|
||||||
_authored_shadow_distance,
|
_authored_shadow_distance,
|
||||||
_authored_shadow_mode
|
_authored_shadow_mode,
|
||||||
|
_authored_msaa
|
||||||
)
|
)
|
||||||
_apply_grass_quality(
|
_apply_grass_quality(
|
||||||
_authored_grass_visible,
|
_authored_grass_visible,
|
||||||
@@ -96,7 +99,8 @@ func apply_presentation_quality(quality: int) -> bool:
|
|||||||
false,
|
false,
|
||||||
_authored_adjustment_enabled,
|
_authored_adjustment_enabled,
|
||||||
minf(_authored_shadow_distance, BALANCED_SHADOW_DISTANCE),
|
minf(_authored_shadow_distance, BALANCED_SHADOW_DISTANCE),
|
||||||
DirectionalLight3D.SHADOW_PARALLEL_2_SPLITS
|
DirectionalLight3D.SHADOW_PARALLEL_2_SPLITS,
|
||||||
|
mini(_authored_msaa, Viewport.MSAA_2X)
|
||||||
)
|
)
|
||||||
_apply_grass_quality(
|
_apply_grass_quality(
|
||||||
_authored_grass_visible,
|
_authored_grass_visible,
|
||||||
@@ -114,7 +118,8 @@ func apply_presentation_quality(quality: int) -> bool:
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
minf(_authored_shadow_distance, LOW_SHADOW_DISTANCE),
|
minf(_authored_shadow_distance, LOW_SHADOW_DISTANCE),
|
||||||
DirectionalLight3D.SHADOW_ORTHOGONAL
|
DirectionalLight3D.SHADOW_ORTHOGONAL,
|
||||||
|
Viewport.MSAA_DISABLED
|
||||||
)
|
)
|
||||||
_apply_grass_quality(
|
_apply_grass_quality(
|
||||||
false,
|
false,
|
||||||
@@ -126,6 +131,9 @@ func apply_presentation_quality(quality: int) -> bool:
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var meadow := get_node_or_null("PainterlyMeadow")
|
||||||
|
if meadow != null:
|
||||||
|
meadow.call("apply_presentation_quality", quality)
|
||||||
_active_presentation_quality = quality
|
_active_presentation_quality = quality
|
||||||
return true
|
return true
|
||||||
|
|
||||||
@@ -223,11 +231,14 @@ func _apply_environment_quality(
|
|||||||
volumetric_fog_enabled: bool,
|
volumetric_fog_enabled: bool,
|
||||||
adjustment_enabled: bool,
|
adjustment_enabled: bool,
|
||||||
shadow_distance: float,
|
shadow_distance: float,
|
||||||
shadow_mode: int
|
shadow_mode: int,
|
||||||
|
msaa: int
|
||||||
) -> void:
|
) -> void:
|
||||||
_claim_viewport_scale_owner()
|
_claim_viewport_scale_owner()
|
||||||
get_viewport().scaling_3d_scale = render_scale
|
get_viewport().scaling_3d_scale = render_scale
|
||||||
|
get_viewport().msaa_3d = msaa
|
||||||
_last_applied_render_scale = render_scale
|
_last_applied_render_scale = render_scale
|
||||||
|
_last_applied_msaa = msaa
|
||||||
var environment := _environment()
|
var environment := _environment()
|
||||||
if environment != null:
|
if environment != null:
|
||||||
environment.glow_enabled = glow_enabled
|
environment.glow_enabled = glow_enabled
|
||||||
@@ -285,8 +296,13 @@ func _register_viewport_scale_owner() -> void:
|
|||||||
_viewport_instance_id = viewport.get_instance_id()
|
_viewport_instance_id = viewport.get_instance_id()
|
||||||
var state: Dictionary = _viewport_scale_states.get(_viewport_instance_id, {})
|
var state: Dictionary = _viewport_scale_states.get(_viewport_instance_id, {})
|
||||||
if state.is_empty():
|
if state.is_empty():
|
||||||
state = {"base_scale": viewport.scaling_3d_scale, "owners": []}
|
state = {
|
||||||
|
"base_scale": viewport.scaling_3d_scale,
|
||||||
|
"base_msaa": viewport.msaa_3d,
|
||||||
|
"owners": [],
|
||||||
|
}
|
||||||
_authored_render_scale = float(state["base_scale"])
|
_authored_render_scale = float(state["base_scale"])
|
||||||
|
_authored_msaa = int(state["base_msaa"])
|
||||||
_viewport_scale_states[_viewport_instance_id] = state
|
_viewport_scale_states[_viewport_instance_id] = state
|
||||||
_claim_viewport_scale_owner()
|
_claim_viewport_scale_owner()
|
||||||
|
|
||||||
@@ -324,13 +340,20 @@ func _release_viewport_scale_owner() -> void:
|
|||||||
if not owners.is_empty():
|
if not owners.is_empty():
|
||||||
state["owners"] = owners
|
state["owners"] = owners
|
||||||
_viewport_scale_states[_viewport_instance_id] = state
|
_viewport_scale_states[_viewport_instance_id] = state
|
||||||
if (
|
if was_active_owner:
|
||||||
was_active_owner
|
|
||||||
and is_equal_approx(viewport.scaling_3d_scale, _last_applied_render_scale)
|
|
||||||
):
|
|
||||||
var previous_owner := (owners[-1] as WeakRef).get_ref() as JajceWorld
|
var previous_owner := (owners[-1] as WeakRef).get_ref() as JajceWorld
|
||||||
if previous_owner != null and previous_owner._last_applied_render_scale >= 0.0:
|
if (
|
||||||
|
previous_owner != null
|
||||||
|
and previous_owner._last_applied_render_scale >= 0.0
|
||||||
|
and is_equal_approx(viewport.scaling_3d_scale, _last_applied_render_scale)
|
||||||
|
):
|
||||||
viewport.scaling_3d_scale = previous_owner._last_applied_render_scale
|
viewport.scaling_3d_scale = previous_owner._last_applied_render_scale
|
||||||
|
if (
|
||||||
|
previous_owner != null
|
||||||
|
and previous_owner._last_applied_msaa >= 0
|
||||||
|
and viewport.msaa_3d == _last_applied_msaa
|
||||||
|
):
|
||||||
|
viewport.msaa_3d = previous_owner._last_applied_msaa
|
||||||
else:
|
else:
|
||||||
_viewport_scale_states.erase(_viewport_instance_id)
|
_viewport_scale_states.erase(_viewport_instance_id)
|
||||||
if (
|
if (
|
||||||
@@ -338,8 +361,11 @@ func _release_viewport_scale_owner() -> void:
|
|||||||
and is_equal_approx(viewport.scaling_3d_scale, _last_applied_render_scale)
|
and is_equal_approx(viewport.scaling_3d_scale, _last_applied_render_scale)
|
||||||
):
|
):
|
||||||
viewport.scaling_3d_scale = float(state["base_scale"])
|
viewport.scaling_3d_scale = float(state["base_scale"])
|
||||||
|
if was_active_owner and viewport.msaa_3d == _last_applied_msaa:
|
||||||
|
viewport.msaa_3d = int(state["base_msaa"])
|
||||||
_viewport_instance_id = 0
|
_viewport_instance_id = 0
|
||||||
_last_applied_render_scale = -1.0
|
_last_applied_render_scale = -1.0
|
||||||
|
_last_applied_msaa = -1
|
||||||
|
|
||||||
|
|
||||||
func _initialize_world_presentation() -> void:
|
func _initialize_world_presentation() -> void:
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Original opaque petal geometry: soft wrap lighting without alpha overdraw.
|
||||||
|
shader_type spatial;
|
||||||
|
render_mode skip_vertex_transform, cull_disabled, specular_disabled;
|
||||||
|
|
||||||
|
uniform vec2 wind_direction = vec2(1.0, 0.7);
|
||||||
|
uniform float wind_amount : hint_range(0.0, 1.0) = 0.115;
|
||||||
|
uniform float fade_begin = 42.0;
|
||||||
|
uniform float fade_end = 56.0;
|
||||||
|
varying float palette_variation;
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
vec3 anchor = MODEL_MATRIX[3].xyz;
|
||||||
|
float distance_to_camera = distance(anchor, CAMERA_POSITION_WORLD);
|
||||||
|
float presence = 1.0 - smoothstep(fade_begin, fade_end, distance_to_camera);
|
||||||
|
float height = max(VERTEX.y, 0.0);
|
||||||
|
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;
|
||||||
|
VERTEX *= presence;
|
||||||
|
vec3 world_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
|
world_vertex.xz += normalize(wind_direction) * wind_amount * gust * height * height * presence;
|
||||||
|
VERTEX = (VIEW_MATRIX * vec4(world_vertex, 1.0)).xyz;
|
||||||
|
vec3 local_normal = normalize(NORMAL + vec3(0.0, 1.6, 0.0));
|
||||||
|
NORMAL = normalize((MODELVIEW_MATRIX * vec4(local_normal, 0.0)).xyz);
|
||||||
|
palette_variation = mix(0.92, 1.06, INSTANCE_CUSTOM.r);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
ALBEDO = COLOR.rgb * palette_variation;
|
||||||
|
ROUGHNESS = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void light() {
|
||||||
|
float sunlight = smoothstep(-0.35, 0.55, dot(NORMAL, LIGHT));
|
||||||
|
vec3 shade = mix(vec3(0.43, 0.58, 0.55), vec3(1.0, 0.98, 0.90), sunlight);
|
||||||
|
DIFFUSE_LIGHT += shade * LIGHT_COLOR * ATTENUATION / PI;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b1r81tjhi1o0y
|
||||||
@@ -1,27 +1,26 @@
|
|||||||
shader_type sky;
|
shader_type sky;
|
||||||
uniform vec3 sky_top_color : source_color = vec3(0.27, 0.57, 0.74);
|
// Original painted panorama; no volume or per-frame sky pass.
|
||||||
uniform vec3 sky_horizon_color : source_color = vec3(0.79, 0.88, 0.82);
|
uniform sampler2D painted_panorama : source_color, filter_linear_mipmap, repeat_enable;
|
||||||
|
uniform vec3 sky_top_color : source_color = vec3(0.12, 0.49, 0.69);
|
||||||
|
uniform vec3 sky_horizon_color : source_color = vec3(0.65, 0.84, 0.84);
|
||||||
uniform vec3 ground_bottom_color : source_color = vec3(0.12, 0.17, 0.12);
|
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 vec3 ground_horizon_color : source_color = vec3(0.65, 0.84, 0.84);
|
||||||
uniform float daylight : hint_range(0.0, 1.0) = 1.0;
|
uniform float daylight : hint_range(0.0, 1.0) = 1.0;
|
||||||
|
uniform float golden_hour : hint_range(0.0, 1.0) = 0.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() {
|
void sky() {
|
||||||
float height = max(EYEDIR.y, 0.0);
|
float height = max(EYEDIR.y, 0.0);
|
||||||
vec3 sky_color = mix(sky_horizon_color, sky_top_color, pow(height, 0.48));
|
vec3 sky_gradient = mix(sky_horizon_color, sky_top_color, pow(height, 0.45));
|
||||||
vec2 p = EYEDIR.xz / max(0.17, height) * 1.15;
|
// Art horizon is at 69% of the sheet. Remap only the upper hemisphere;
|
||||||
float cloud = noise(p) * 0.65 + noise(p * 2.1 + 7.3) * 0.25 + noise(p * 4.3) * 0.10;
|
// the ground bounce uses the day/night palette and never paints terrain.
|
||||||
float mask = smoothstep(0.55, 0.64, cloud) * smoothstep(0.02, 0.20, height);
|
vec2 panorama_uv = vec2(fract(SKY_COORDS.x * 2.0 + 0.40), clamp((SKY_COORDS.y - 0.26) * 2.875, 0.0, 0.69));
|
||||||
vec3 cloud_shadow = mix(vec3(0.08, 0.10, 0.19), vec3(0.70, 0.80, 0.80), daylight);
|
vec3 painted = texture(painted_panorama, panorama_uv).rgb;
|
||||||
vec3 cloud_light = mix(vec3(0.15, 0.18, 0.29), vec3(1.0, 0.98, 0.87), daylight);
|
vec3 seam_color = (texture(painted_panorama, vec2(0.001, panorama_uv.y)).rgb + texture(painted_panorama, vec2(0.999, panorama_uv.y)).rgb) * 0.5;
|
||||||
vec3 cloud_color = mix(cloud_shadow, cloud_light, smoothstep(0.61, 0.77, cloud));
|
painted = mix(seam_color, painted, smoothstep(0.0, 0.025, min(panorama_uv.x, 1.0 - panorama_uv.x)));
|
||||||
sky_color = mix(sky_color, cloud_color, mask);
|
painted = mix(painted, painted * vec3(1.18, 0.79, 0.66), golden_hour * 0.50);
|
||||||
|
vec3 night_paint = painted * vec3(0.045, 0.07, 0.14) + sky_gradient * 0.12;
|
||||||
|
vec3 sky_color = mix(night_paint, painted, daylight);
|
||||||
|
// Dissolve the panorama edge into the haze at the actual horizon.
|
||||||
|
sky_color = mix(sky_gradient, sky_color, smoothstep(0.0, 0.075, height));
|
||||||
COLOR = EYEDIR.y >= 0.0 ? sky_color : mix(ground_horizon_color, ground_bottom_color, pow(-EYEDIR.y, 0.4));
|
COLOR = EYEDIR.y >= 0.0 ? sky_color : mix(ground_horizon_color, ground_bottom_color, pow(-EYEDIR.y, 0.4));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,81 @@
|
|||||||
shader_type spatial;
|
shader_type spatial;
|
||||||
render_mode blend_mix, depth_prepass_alpha, cull_disabled, diffuse_burley, specular_schlick_ggx;
|
render_mode blend_mix, depth_prepass_alpha, cull_disabled, diffuse_burley, specular_schlick_ggx;
|
||||||
|
|
||||||
uniform float wave_strength : hint_range(0.0, 0.3) = 0.055;
|
uniform float wave_strength : hint_range(0.0, 0.3) = 0.025;
|
||||||
uniform float wave_speed : hint_range(0.0, 3.0) = 0.72;
|
uniform float wave_speed : hint_range(0.0, 3.0) = 0.72;
|
||||||
uniform float flow_speed : hint_range(0.0, 2.0) = 0.46;
|
uniform float flow_speed : hint_range(0.0, 2.0) = 0.28;
|
||||||
uniform vec3 deep_color : source_color = vec3(0.03, 0.39, 0.45);
|
uniform bool pool_surface = false;
|
||||||
uniform vec3 shallow_color : source_color = vec3(0.07, 0.67, 0.64);
|
uniform float pool_outlet_z = -20.0;
|
||||||
uniform vec3 sunlit_color : source_color = vec3(0.55, 0.95, 0.78);
|
uniform vec3 deep_color : source_color = vec3(0.035, 0.34, 0.54);
|
||||||
uniform vec3 foam_color : source_color = vec3(0.82, 0.94, 0.84);
|
uniform vec3 shallow_color : source_color = vec3(0.12, 0.65, 0.69);
|
||||||
|
uniform vec3 sunlit_color : source_color = vec3(0.57, 0.83, 0.88);
|
||||||
|
uniform vec3 foam_color : source_color = vec3(0.9, 0.96, 0.91);
|
||||||
|
|
||||||
|
varying vec3 water_position;
|
||||||
|
|
||||||
|
|
||||||
|
float painted_noise(vec2 p) {
|
||||||
|
vec2 cell = floor(p);
|
||||||
|
vec2 blend = fract(p);
|
||||||
|
blend = blend * blend * (3.0 - 2.0 * blend);
|
||||||
|
vec4 corners = fract(sin(vec4(
|
||||||
|
dot(cell, vec2(127.1, 311.7)),
|
||||||
|
dot(cell + vec2(1.0, 0.0), vec2(127.1, 311.7)),
|
||||||
|
dot(cell + vec2(0.0, 1.0), vec2(127.1, 311.7)),
|
||||||
|
dot(cell + vec2(1.0), vec2(127.1, 311.7))
|
||||||
|
)) * 43758.5453);
|
||||||
|
return mix(mix(corners.x, corners.y, blend.x), mix(corners.z, corners.w, blend.x), blend.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void vertex() {
|
void vertex() {
|
||||||
float broad_wave = sin(VERTEX.x * 0.42 + VERTEX.z * 0.18 + TIME * wave_speed);
|
// World coordinates keep the pool and both ribbons at the same ripple scale.
|
||||||
float crossing_wave = cos(VERTEX.x * 0.7 - VERTEX.z * 0.31 + TIME * wave_speed * 0.73);
|
water_position = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
|
float broad_wave = sin(water_position.x * 0.42 + water_position.z * 0.18 + TIME * wave_speed);
|
||||||
|
float crossing_wave = cos(water_position.x * 0.7 - water_position.z * 0.31 + TIME * wave_speed * 0.73);
|
||||||
VERTEX.y += (broad_wave + crossing_wave * 0.45) * wave_strength;
|
VERTEX.y += (broad_wave + crossing_wave * 0.45) * wave_strength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
vec2 flowing_uv = UV * vec2(7.0, 10.0);
|
// The ribbon owns the outlet so two transparent surfaces never overlap.
|
||||||
flowing_uv.y -= TIME * flow_speed;
|
if (pool_surface && water_position.z >= pool_outlet_z) {
|
||||||
float ribbon_a = sin(flowing_uv.y + sin(flowing_uv.x * 0.72) * 1.25) * 0.5 + 0.5;
|
discard;
|
||||||
float ribbon_b = sin(flowing_uv.y * 1.7 - flowing_uv.x * 0.55 + TIME * 0.23) * 0.5 + 0.5;
|
}
|
||||||
float quiet_ripple = smoothstep(0.72, 0.98, ribbon_a * 0.62 + ribbon_b * 0.38);
|
vec2 flow = water_position.xz - vec2(0.0, TIME * flow_speed);
|
||||||
float fresnel = pow(1.0 - clamp(dot(normalize(NORMAL), normalize(VIEW)), 0.0, 1.0), 2.4);
|
float broad_patch = painted_noise(flow * vec2(0.18, 0.3));
|
||||||
float bank_distance = min(UV.x, 1.0 - UV.x);
|
float small_patch = painted_noise(flow * vec2(0.64, 0.85));
|
||||||
float bank_foam = 1.0 - smoothstep(0.0, 0.07, bank_distance);
|
float ripple_phase = flow.y * 3.2 + sin(flow.x * 0.83) * 1.2 + small_patch * 0.8;
|
||||||
bank_foam *= 0.72 + sin(UV.y * 52.0 + TIME * 0.35) * 0.12;
|
float ribbon = sin(ripple_phase);
|
||||||
|
float stroke_width = max(fwidth(ribbon) * 0.7, 0.012);
|
||||||
vec3 water_color = mix(deep_color, shallow_color, ribbon_a * 0.2 + 0.48);
|
float strokes = smoothstep(0.975 - stroke_width, 0.975 + stroke_width, ribbon);
|
||||||
water_color = mix(water_color, sunlit_color, fresnel * 0.42);
|
strokes *= smoothstep(0.58, 0.8, painted_noise(flow * vec2(1.1, 1.8))) * 0.16;
|
||||||
water_color = mix(water_color, foam_color, quiet_ripple * 0.05);
|
strokes *= 1.0 - smoothstep(0.28, 0.8, fwidth(ripple_phase));
|
||||||
water_color = mix(water_color, foam_color, bank_foam * 0.3);
|
vec3 ripple_normal = normalize(vec3(
|
||||||
|
cos(flow.x * 0.83 + flow.y * 0.37 + broad_patch * 6.0) * 0.006,
|
||||||
|
1.0,
|
||||||
|
cos(ripple_phase + small_patch * 5.0) * 0.008
|
||||||
|
));
|
||||||
|
NORMAL = normalize((VIEW_MATRIX * vec4(ripple_normal, 0.0)).xyz);
|
||||||
|
float fresnel = pow(1.0 - clamp(dot(NORMAL, normalize(VIEW)), 0.0, 1.0), 2.6);
|
||||||
|
// Painted sky patches need no screen/depth texture and work in Compatibility.
|
||||||
|
vec3 world_view = normalize((INV_VIEW_MATRIX * vec4(VIEW, 0.0)).xyz);
|
||||||
|
vec3 reflection = reflect(-world_view, ripple_normal);
|
||||||
|
vec2 sky_uv = reflection.xz / max(reflection.y + 0.35, 0.25);
|
||||||
|
float clouds = painted_noise(sky_uv * 2.0 + vec2(0.7, 2.4));
|
||||||
|
clouds = smoothstep(0.49, 0.77, clouds + painted_noise(sky_uv * 4.5) * 0.18);
|
||||||
|
float bank_distance = min(UV.x, 1.0 - UV.x) * 2.0;
|
||||||
|
if (pool_surface) {
|
||||||
|
bank_distance = 1.0 - length(UV * 2.0 - 1.0);
|
||||||
|
}
|
||||||
|
float bank_foam = (1.0 - smoothstep(0.01, 0.045, bank_distance));
|
||||||
|
bank_foam *= smoothstep(0.4, 0.75, small_patch) * 0.48;
|
||||||
|
vec3 water_color = mix(deep_color, shallow_color, 0.19 + broad_patch * 0.2);
|
||||||
|
water_color = mix(water_color, sunlit_color, clouds * (0.13 + fresnel * 0.35));
|
||||||
|
water_color = mix(water_color, foam_color, strokes + bank_foam);
|
||||||
|
|
||||||
ALBEDO = water_color;
|
ALBEDO = water_color;
|
||||||
ROUGHNESS = mix(0.34, 0.5, ribbon_b);
|
ROUGHNESS = 0.38 + broad_patch * 0.08;
|
||||||
SPECULAR = 0.38;
|
SPECULAR = 0.14;
|
||||||
EMISSION = water_color * 0.12 + sunlit_color * (quiet_ripple * 0.05 + fresnel * 0.03);
|
ALPHA = smoothstep(0.0, 0.018, bank_distance) * 0.97;
|
||||||
EMISSION += foam_color * bank_foam * 0.025;
|
|
||||||
ALPHA = mix(0.94, 0.99, fresnel);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
shader_type spatial;
|
||||||
|
render_mode blend_mix, depth_prepass_alpha, cull_disabled, diffuse_burley;
|
||||||
|
|
||||||
|
uniform bool circular_wash = false;
|
||||||
|
uniform vec3 foam_color : source_color = vec3(0.83, 0.94, 0.93);
|
||||||
|
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
vec2 p = UV * 2.0 - 1.0;
|
||||||
|
float wash = sin(p.x * 18.0 + sin(p.y * 12.0 + TIME * 0.8) * 1.5);
|
||||||
|
wash *= sin(p.y * 23.0 - TIME * 1.4 + p.x * 3.0);
|
||||||
|
float mask = (1.0 - smoothstep(0.45, 1.0, abs(p.x)));
|
||||||
|
mask *= 1.0 - smoothstep(0.2, 1.0, abs(p.y));
|
||||||
|
if (circular_wash) {
|
||||||
|
float radius = length(p);
|
||||||
|
float angle = atan(p.y, p.x);
|
||||||
|
float broken_arcs = sin(radius * 32.0 - TIME * 0.75 + sin(angle * 5.0) * 0.6);
|
||||||
|
mask = smoothstep(0.65, 0.94, broken_arcs);
|
||||||
|
mask *= smoothstep(0.1, 0.35, radius) * (1.0 - smoothstep(0.65, 0.98, radius));
|
||||||
|
mask *= smoothstep(-0.3, 0.8, sin(angle * 3.0 + radius * 9.0));
|
||||||
|
}
|
||||||
|
ALBEDO = foam_color;
|
||||||
|
ROUGHNESS = 0.65;
|
||||||
|
ALPHA = mask * smoothstep(-0.45, 0.6, wash) * 0.48;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bcw0dj01f0m3h
|
||||||
@@ -2,9 +2,9 @@ shader_type spatial;
|
|||||||
render_mode blend_mix, depth_prepass_alpha, cull_disabled, diffuse_burley, specular_schlick_ggx;
|
render_mode blend_mix, depth_prepass_alpha, cull_disabled, diffuse_burley, specular_schlick_ggx;
|
||||||
|
|
||||||
uniform float scroll_speed : hint_range(0.0, 4.0) = 1.35;
|
uniform float scroll_speed : hint_range(0.0, 4.0) = 1.35;
|
||||||
uniform vec3 shadow_color : source_color = vec3(0.075, 0.33, 0.34);
|
uniform vec3 shadow_color : source_color = vec3(0.11, 0.39, 0.54);
|
||||||
uniform vec3 water_color : source_color = vec3(0.31, 0.68, 0.64);
|
uniform vec3 water_color : source_color = vec3(0.39, 0.73, 0.82);
|
||||||
uniform vec3 light_color : source_color = vec3(0.78, 0.94, 0.83);
|
uniform vec3 light_color : source_color = vec3(0.85, 0.96, 0.94);
|
||||||
uniform vec3 foam_color : source_color = vec3(0.91, 0.97, 0.9);
|
uniform vec3 foam_color : source_color = vec3(0.91, 0.97, 0.9);
|
||||||
|
|
||||||
|
|
||||||
@@ -22,17 +22,16 @@ void fragment() {
|
|||||||
float soft_thread = sin(flow_uv.x * 13.0 - flow_uv.y * 7.0) * 0.5 + 0.5;
|
float soft_thread = sin(flow_uv.x * 13.0 - flow_uv.y * 7.0) * 0.5 + 0.5;
|
||||||
float falling_detail = sin(flow_uv.y * 27.0 + flow_uv.x * 7.0) * 0.5 + 0.5;
|
float falling_detail = sin(flow_uv.y * 27.0 + flow_uv.x * 7.0) * 0.5 + 0.5;
|
||||||
float thread = smoothstep(0.36, 0.9, long_thread * 0.6 + soft_thread * 0.4);
|
float thread = smoothstep(0.36, 0.9, long_thread * 0.6 + soft_thread * 0.4);
|
||||||
float broken_foam = smoothstep(0.72, 0.98, falling_detail * thread);
|
float broken_foam = smoothstep(0.61, 0.94, falling_detail * thread);
|
||||||
float base_foam = smoothstep(0.58, 1.0, UV.y);
|
float base_foam = smoothstep(0.58, 1.0, UV.y);
|
||||||
float edge_fade = smoothstep(0.0, 0.08, UV.x) * smoothstep(1.0, 0.92, UV.x);
|
float edge_fade = smoothstep(0.0, 0.08, UV.x) * (1.0 - smoothstep(0.92, 1.0, UV.x));
|
||||||
|
|
||||||
vec3 color = mix(shadow_color, water_color, 0.5 + thread * 0.35);
|
vec3 color = mix(shadow_color, water_color, 0.5 + thread * 0.35);
|
||||||
color = mix(color, light_color, broken_foam * 0.5);
|
color = mix(color, light_color, thread * 0.35 + broken_foam * 0.35);
|
||||||
color = mix(color, foam_color, base_foam * (0.38 + broken_foam * 0.42));
|
color = mix(color, foam_color, base_foam * (0.38 + broken_foam * 0.42));
|
||||||
|
|
||||||
ALBEDO = color;
|
ALBEDO = color;
|
||||||
ROUGHNESS = mix(0.16, 0.32, broken_foam);
|
ROUGHNESS = mix(0.16, 0.32, broken_foam);
|
||||||
SPECULAR = 0.65;
|
SPECULAR = 0.65;
|
||||||
EMISSION = light_color * (broken_foam * 0.07 + base_foam * 0.04);
|
ALPHA = edge_fade * mix(0.76, 0.95, thread * 0.56 + base_foam * 0.3);
|
||||||
ALPHA = edge_fade * mix(0.62, 0.9, thread * 0.56 + base_foam * 0.3);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ uniform float gust_speed : hint_range(0.0, 1.0) = 0.12;
|
|||||||
uniform float gust_strength : hint_range(0.0, 0.8) = 0.35;
|
uniform float gust_strength : hint_range(0.0, 0.8) = 0.35;
|
||||||
uniform float flutter_strength : hint_range(0.0, 0.05) = 0.012;
|
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 float wind_phase : hint_range(0.0, 6.2832) = 0.0;
|
||||||
|
uniform float wind_base_height = -1.2;
|
||||||
|
uniform float wind_full_height = 1.2;
|
||||||
uniform vec3 albedo : source_color = vec3(0.19, 0.38, 0.17);
|
uniform vec3 albedo : source_color = vec3(0.19, 0.38, 0.17);
|
||||||
uniform float roughness : hint_range(0.0, 1.0) = 0.9;
|
uniform float roughness : hint_range(0.0, 1.0) = 0.9;
|
||||||
|
uniform float painted_vertex_weight : hint_range(0.0, 1.0) = 0.0;
|
||||||
uniform vec3 contact_root = vec3(0.0);
|
uniform vec3 contact_root = vec3(0.0);
|
||||||
uniform float contact_strength = 0.0;
|
uniform float contact_strength = 0.0;
|
||||||
uniform int interactor_count = 0;
|
uniform int interactor_count = 0;
|
||||||
@@ -20,8 +23,7 @@ varying vec3 paint_position;
|
|||||||
|
|
||||||
void vertex() {
|
void vertex() {
|
||||||
paint_position = VERTEX;
|
paint_position = VERTEX;
|
||||||
float half_height = 1.2;
|
float height_factor = clamp((VERTEX.y - wind_base_height) / max(wind_full_height - wind_base_height, 0.01), 0.0, 1.0);
|
||||||
float height_factor = clamp((VERTEX.y + half_height) / (half_height * 2.0), 0.0, 1.0);
|
|
||||||
height_factor = height_factor * height_factor;
|
height_factor = height_factor * height_factor;
|
||||||
|
|
||||||
vec2 direction = normalize(wind_direction);
|
vec2 direction = normalize(wind_direction);
|
||||||
@@ -32,8 +34,10 @@ void vertex() {
|
|||||||
float bend = sin(TIME * wind_speed + world_phase) * wind_strength * gust;
|
float bend = sin(TIME * wind_speed + world_phase) * wind_strength * gust;
|
||||||
float flutter = sin(TIME * 1.7 + world_phase * 1.9 + VERTEX.y * 2.0) * flutter_strength;
|
float flutter = sin(TIME * 1.7 + world_phase * 1.9 + VERTEX.y * 2.0) * flutter_strength;
|
||||||
|
|
||||||
VERTEX.xz += direction * bend * height_factor;
|
// Crown orientation varies, but all foliage follows the same world breeze.
|
||||||
VERTEX.xz += crosswind * flutter * height_factor;
|
vec3 world_bend = vec3(direction.x, 0.0, direction.y) * bend;
|
||||||
|
world_bend += vec3(crosswind.x, 0.0, crosswind.y) * flutter;
|
||||||
|
VERTEX += (inverse(MODEL_MATRIX) * vec4(world_bend, 0.0)).xyz * height_factor;
|
||||||
if (foliage_trails_enabled && contact_strength > 0.0) {
|
if (foliage_trails_enabled && contact_strength > 0.0) {
|
||||||
// One bend for the whole bush, including fruit; its base remains planted.
|
// One bend for the whole bush, including fruit; its base remains planted.
|
||||||
vec3 contact = sample_foliage_trail(contact_root);
|
vec3 contact = sample_foliage_trail(contact_root);
|
||||||
@@ -69,7 +73,10 @@ void vertex() {
|
|||||||
void fragment() {
|
void fragment() {
|
||||||
float patches = sin(paint_position.x * 6.0 + sin(paint_position.z * 7.0)) * sin(paint_position.y * 5.0);
|
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);
|
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);
|
vec3 pigment = mix(vec3(1.0), COLOR.rgb, painted_vertex_weight);
|
||||||
|
vec3 shade_tint = mix(vec3(0.77, 0.95, 1.01), vec3(1.10, 1.06, 0.87), top);
|
||||||
|
ALBEDO = albedo * pigment * shade_tint * mix(0.91, 1.13, top);
|
||||||
|
ALBEDO *= 1.0 + smoothstep(0.18, 0.65, patches) * 0.07;
|
||||||
ROUGHNESS = roughness;
|
ROUGHNESS = roughness;
|
||||||
SPECULAR = 0.0;
|
SPECULAR = 0.0;
|
||||||
}
|
}
|
||||||
|
|||||||