feat(environment): add painterly skies and living meadows

This commit is contained in:
Rijad Zuzo
2026-09-05 20:27:09 +02:00
parent 7899d36f03
commit ec16e2f2ca
71 changed files with 2280 additions and 213 deletions
+1
View File
@@ -0,0 +1 @@
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
Binary file not shown.
+21
View File
@@ -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.
Binary file not shown.
+45
View File
@@ -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
Binary file not shown.
+45
View File
@@ -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
+26
View File
@@ -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
}
}
Binary file not shown.
+45
View File
@@ -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
Binary file not shown.
+45
View File
@@ -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
+59
View File
@@ -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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+40
View File
@@ -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
Binary file not shown.
@@ -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
Binary file not shown.
@@ -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
+35
View File
@@ -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.
+24
View File
@@ -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
}
}
+87 -5
View File
@@ -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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

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"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 709 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

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"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

+4
View File
@@ -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={
+131 -11
View File
@@ -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:
+85 -7
View File
@@ -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:
+42 -1
View File
@@ -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 (
+29
View File
@@ -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
+176
View File
@@ -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()
+267
View File
@@ -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"]))
+127
View File
@@ -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
+132 -14
View File
@@ -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
+24
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://bk3cxfe3kph2x
+9
View File
@@ -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")]
+217
View File
@@ -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
)
+1
View File
@@ -0,0 +1 @@
uid://cacjl430m8mcw
+1 -1
View File
@@ -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"]
+33 -51
View File
@@ -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)
+53 -7
View File
@@ -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
) )
+22 -23
View File
@@ -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")
+4 -9
View File
@@ -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")
+20 -15
View File
@@ -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
+36 -10
View File
@@ -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
+18 -19
View File
@@ -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));
} }
+66 -27
View File
@@ -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);
} }
+25
View File
@@ -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
+7 -8
View File
@@ -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);
} }
+12 -5
View File
@@ -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;
} }