feat: scale village queries and enrich Jajce center
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
```text
|
||||
SimulationManager tick
|
||||
-> ActionExecutionSystem advances needs and working progress
|
||||
-> SimulationPopulationView refreshes transient living/starving indexes
|
||||
-> VillageOpportunitySystem re-derives the current capable helper
|
||||
-> ActionSelectionSystem chooses an action when the NPC is idle
|
||||
-> SimulationManager stores the selected action
|
||||
@@ -25,6 +26,8 @@ SimulationManager tick
|
||||
- queries `RelationshipSystem` after personal survival/schedule overrides, so a
|
||||
trusted starving acquaintance can redirect ordinary work toward food
|
||||
gathering while the pantry is low;
|
||||
- consumes the manager's current `SimulationPopulationView` for that social
|
||||
query instead of rebuilding an all-NPC lookup for each idle decision;
|
||||
- accepts the current ephemeral `OpportunityHelperResult` and, only for its
|
||||
matching NPC, selects the reported ordinary gather/deposit action after
|
||||
urgent self-care and sleep/meal schedule branches;
|
||||
@@ -71,7 +74,10 @@ SimulationManager tick
|
||||
SimulationManager remains the orchestrator and event boundary. It owns
|
||||
simulation records, invokes the focused systems, stores selected actions and
|
||||
targets, and translates presentation callbacks into simulation transitions.
|
||||
It also publishes the latest `ActionSelectionResult` for presentation; the UI
|
||||
It also owns one disposable `SimulationPopulationView`, rebuilt at tick start
|
||||
and refreshed after each NPC advances so interleaved decision semantics remain
|
||||
unchanged.
|
||||
The manager publishes the latest `ActionSelectionResult` for presentation; the UI
|
||||
does not recompute decisions. Each idle selection re-derives the capable helper
|
||||
instead of consulting persisted assignment state. At completion it atomically
|
||||
pays any definition-backed stored-resource cost before applying the action
|
||||
@@ -128,6 +134,8 @@ simulation ticks.
|
||||
- stores the exact event ID as the trust cause and ignores replayed or older
|
||||
events;
|
||||
- selects trusted starving subjects deterministically for action selection;
|
||||
- uses a supplied per-tick population view when available, with a compatibility
|
||||
fallback for isolated callers;
|
||||
- does not choose actions, perform transactions, or format presentation text.
|
||||
|
||||
These collaborators are `RefCounted` rule services, not additional scene-tree
|
||||
|
||||
@@ -10,6 +10,7 @@ results.
|
||||
```text
|
||||
SimulationClock
|
||||
-> SimulationManager orchestrates one deterministic tick
|
||||
-> SimulationPopulationView indexes all/living/starving NPCs by stable ID
|
||||
-> ActionExecutionSystem advances needs and work
|
||||
-> VillageOpportunitySystem re-derives the current capable helper
|
||||
-> ActionSelectionSystem chooses an action
|
||||
@@ -42,6 +43,10 @@ would otherwise obscure that lifecycle:
|
||||
first without weakening its direct-source or one-hop checks;
|
||||
- `simulation/relationships/RelationshipSystem.gd` owns directed relationship
|
||||
queries, event-driven trust changes, and deterministic social tie-breaking;
|
||||
- `simulation/population/SimulationPopulationView.gd` owns a transient derived
|
||||
all/living/starving stable-ID index. The manager rebuilds it once at the tick
|
||||
boundary and refreshes it after each interleaved NPC update; relationship and
|
||||
action queries may read it, but it never enters save records;
|
||||
- `simulation/opportunities/VillageOpportunitySystem.gd` observes immutable
|
||||
event references plus current NPC/storage state, then owns the bounded shared
|
||||
open/resolved/invalidated lifecycle for the proven pantry-food and blocked-
|
||||
@@ -76,6 +81,7 @@ hard to read.
|
||||
| `simulation/events/` | Immutable event history and derived event queries |
|
||||
| `simulation/knowledge/` | Per-NPC knowledge of objective event IDs |
|
||||
| `simulation/relationships/` | Directed social consequences and relationship queries |
|
||||
| `simulation/population/` | Transient per-tick population query indexes |
|
||||
| `simulation/opportunities/` | Knowledge-gated unresolved-condition projections |
|
||||
| `simulation/state/` | Versioned, serializable mutable records |
|
||||
| `simulation/definitions/` | Stable IDs and immutable gameplay definitions |
|
||||
@@ -118,6 +124,12 @@ improving ownership.
|
||||
- Benchmark fixtures use ordinary simulation records and must round-trip
|
||||
through the current state schema. They may control workload setup, but must
|
||||
not add benchmark-only fields or branches to production saves and ticks.
|
||||
- Derived population indexes are disposable acceleration structures. NPC
|
||||
records remain authoritative, and rebuilding the view after initialization,
|
||||
restore, or a tick must produce the same decisions and checksum.
|
||||
- Camera-local grass, ambient butterflies, water animation, smoke, and foliage
|
||||
motion are presentation only. Grass consumes bounded real actor transforms;
|
||||
it does not write NPC positions or become persistent state.
|
||||
- Opportunity records reference stable NPC, storage, resource, trigger-event,
|
||||
and resolution-event IDs. Their generator may observe authoritative state
|
||||
and history, but it does not mutate the economy or command NPC behavior. The
|
||||
|
||||
@@ -412,6 +412,13 @@ coverage validates the texture contract, presentation nodes, terrain shape,
|
||||
stable resources, and navigation. Clip capture and a visual performance review
|
||||
remain presentation checkpoints rather than missing implementation.
|
||||
|
||||
`Jajce Center 02` adds the first reviewed composition pass beyond that baseline:
|
||||
the summit citadel and residential roof amphitheater read as one center, the
|
||||
waterfall feeds a Terrain3D-sampled turquoise ribbon, and bounded camera-local
|
||||
grass, conifers, richer deciduous silhouettes, and oversized butterflies add
|
||||
life without changing simulation authority. The matched review lives in
|
||||
[`baselines/JAJCE_CENTER_02.md`](baselines/JAJCE_CENTER_02.md).
|
||||
|
||||
### Exit condition
|
||||
|
||||
The lookdev scene can produce a compelling 10–20 second clip with no gameplay.
|
||||
@@ -755,14 +762,25 @@ Completed:
|
||||
NPC and event counts. The first reviewed capture sets a local 600-NPC target,
|
||||
records identical checksums across samples, and exposes repeated scarce-food
|
||||
population scans plus objective-event growth as the next measured work.
|
||||
33. Shared population query view: stable-ID all/living/starving indexes are
|
||||
built once per deterministic tick and refreshed through interleaved NPC
|
||||
updates. Baseline 02 preserves every checksum and improves the 600-NPC case
|
||||
by 23.3% without simulation LOD.
|
||||
34. `Jajce Center 02`: the reviewed matched capture strengthens the citadel,
|
||||
cascading homes, bridge, waterfall, terrain-following river, conifer/tree
|
||||
clusters, and ambient butterflies. Roughly 10,400 camera-local grass
|
||||
particles remain short and patchy and bend from real actor transforms.
|
||||
|
||||
Next:
|
||||
|
||||
1. Reuse one stable per-tick population view in the existing
|
||||
trusted-starving-subject query, preserve exact action/checksum behavior, and
|
||||
rerun simulation scaling baseline 01. Let the resulting profile determine
|
||||
whether the following slice needs another bounded query optimization, a
|
||||
spatial index, or the first active/abstract LOD contract.
|
||||
1. Measure and implement a loaded-resource spatial query through
|
||||
`ActiveWorldAdapter` for far-apart Terrain3D-authored finite sources. Keep
|
||||
simulation-owned resource records, stable IDs, current score/reachability,
|
||||
reservations, player parity, and unload/rebind behavior unchanged.
|
||||
2. Sculpt a real Terrain3D river channel and waterfall shelf before adding a
|
||||
broad foliage asset pass. Use Terrain3D particles for nearby grass and
|
||||
intentional instancing for tree/fern/rock masses; add livestock only with a
|
||||
real identity and interaction contract.
|
||||
|
||||
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
||||
mechanics. The next proof is a beautiful stage for the systems that already
|
||||
|
||||
@@ -469,8 +469,10 @@ important history.
|
||||
|
||||
[Simulation scaling baseline 01](benchmarks/SIMULATION_SCALING_BASELINE_01.md)
|
||||
sets the first local target at 600 data-only full-fidelity NPCs while preserving
|
||||
deterministic state. Revisit that target when world presentation or LOD enters
|
||||
the measured workload.
|
||||
deterministic state. [Baseline 02](benchmarks/SIMULATION_SCALING_BASELINE_02.md)
|
||||
keeps identical final checksums after one reusable per-tick population view and
|
||||
improves that case from 65.84 to 85.81 ticks per second. Revisit the target when
|
||||
world presentation or LOD enters the measured workload.
|
||||
|
||||
## Milestone 9 — Player interaction and social agency
|
||||
|
||||
@@ -862,15 +864,35 @@ workload exclusions, checksums, and local 50-ticks-per-second reference target
|
||||
live in
|
||||
[Simulation scaling baseline 01](benchmarks/SIMULATION_SCALING_BASELINE_01.md).
|
||||
|
||||
The immediate next slice should build one bounded per-tick population view for
|
||||
the existing trusted-starving-subject query. That scarce-food path currently
|
||||
rebuilds an all-NPC lookup for each applicable idle decision, making it the
|
||||
first code-level candidate consistent with the measured superlinear growth.
|
||||
Preserve exact selection/checksum behavior and rerun the same ledger before
|
||||
choosing a spatial index or active/abstract LOD design.
|
||||
The first measured optimization is also complete. `SimulationPopulationView`
|
||||
builds stable-ID all/living/starving indexes once per tick, refreshes them after
|
||||
each interleaved NPC update, and serves relationship/action queries without
|
||||
entering saved state. Every baseline-02 checksum exactly matches baseline 01.
|
||||
The 600-NPC case falls from 15,187.74 to 11,654.10 microseconds per tick, a
|
||||
23.3% improvement, while small fixtures expose the expected fixed dictionary
|
||||
cost. See
|
||||
[Simulation scaling baseline 02](benchmarks/SIMULATION_SCALING_BASELINE_02.md).
|
||||
|
||||
The immediate next systems slice should now prove spatial discovery for the
|
||||
world vision: index loaded finite `ResourceNode` anchors through
|
||||
`ActiveWorldAdapter` so Terrain3D-authored sources can be placed much farther
|
||||
apart without an all-node scan for every resolution. Preserve stable IDs,
|
||||
current distance/risk/comfort/priority scoring, reachability, reservations,
|
||||
player parity, unload/rebind authority, and exact deterministic outcomes. First
|
||||
measure 18, 180, and 1,800 loaded candidates; do not choose a generic spatial
|
||||
framework or active/abstract LOD mode before that real consumer proves the
|
||||
contract.
|
||||
|
||||
Recently completed:
|
||||
|
||||
- Shared population query view: one transient per-tick all/living/starving
|
||||
index replaces repeated scarce-food relationship scans, preserves exact
|
||||
checksums, and improves the reviewed 600-NPC workload by 23.3%.
|
||||
- `Jajce Center 02`: matched before/after captures now document a larger summit
|
||||
citadel, cascading timber-plaster homes, terrain-following turquoise river,
|
||||
rock-framed waterfall, richer tree silhouettes, restrained conifer clusters,
|
||||
oversized butterflies, and short camera-local grass that bends around real
|
||||
player/NPC transforms without becoming simulation state.
|
||||
- Deterministic simulation scaling baseline: schema-valid full-fidelity
|
||||
fixtures, a repeatable CLI runner, and a fast headless regression now record
|
||||
population throughput, phase timing, serialized-state growth, history
|
||||
|
||||
+14
-5
@@ -2,7 +2,7 @@
|
||||
|
||||
> Agent-facing context for understanding the project quickly.
|
||||
>
|
||||
> Snapshot basis: repository state on July 14, 2026. Treat the code as the
|
||||
> Snapshot basis: repository state on July 16, 2026. Treat the code as the
|
||||
> source of truth when this document and the implementation differ.
|
||||
|
||||
See the [documentation map](README.md) for the authority and scope of each plan.
|
||||
@@ -193,12 +193,15 @@ study, and patrol target typed activity sites. The migration is documented in
|
||||
and GUT run through the local quality gate)
|
||||
- **Main scene:** `res://main.tscn`
|
||||
- **Terrain:** Terrain3D 1.0.2 is installed and enabled
|
||||
- **Jajce runtime:** reusable 512 m Terrain3D seed, greybox landmarks, stable
|
||||
ResourceNode placement, lookdev camera, and Terrain3D-derived runtime
|
||||
navigation with collision guardrails
|
||||
- **Jajce runtime:** reusable 512 m Terrain3D seed, stable ResourceNode
|
||||
placement, larger citadel/village-center composition, terrain-following river
|
||||
ribbon, reactive camera-local grass, and Terrain3D-derived runtime navigation
|
||||
with collision guardrails
|
||||
- **Lookdev baseline:** `Jajce Lookdev 01` is captured at
|
||||
`docs/baselines/jajce_lookdev_01.png` with
|
||||
`tools/capture_jajce_lookdev.gd`
|
||||
- **Matched center baseline:** `Jajce Center 02` preserves a fixed before/after
|
||||
overview plus a close grass-readability capture under `docs/baselines/`
|
||||
- **Runtime baseline:** `Simulation Garden 01` is captured as paired debug and
|
||||
cinematic `main.tscn` images plus an empty/restocked pantry close pair under
|
||||
`docs/baselines/` with `tools/capture_simulation_garden.gd`
|
||||
@@ -336,13 +339,16 @@ distance with resource `safety_risk`, `comfort_distance`, and
|
||||
|
||||
- a centered 512 m Terrain3D landscape split across four regions;
|
||||
- deterministic shaped terrain data and six game-owned surface layers;
|
||||
- fortress, houses, mill, bridge, shader river/waterfall, mist, and smoke;
|
||||
- a larger summit citadel, eight cascading houses, mill, full-span bridge,
|
||||
rock-framed waterfall, mist, smoke, and a Terrain3D-sampled river ribbon;
|
||||
- authored dirt path strips that make the current village task loop readable;
|
||||
- compact silhouette props for the ridge landmark, guard, study, and rest
|
||||
sites, plus a physical pantry whose empty/low/stocked arrangement follows
|
||||
authoritative food storage;
|
||||
- warm sky, fog, shadows, coherent wind-reactive foliage, and breeze-aligned
|
||||
chimney smoke;
|
||||
- short patchy camera-local grass that receives at most eight real player/NPC
|
||||
transforms, authored conifer clusters, and oversized ambient butterflies;
|
||||
- one sparse terrain-aware calligraphic gust field with two or three soft
|
||||
tapered strokes per burst;
|
||||
- eighteen ResourceNodes preserving stable food/wood discovery IDs;
|
||||
@@ -418,6 +424,8 @@ views, modifiers, and priorities.
|
||||
Focused `RefCounted` collaborators keep rule ownership visible:
|
||||
|
||||
- action systems own selection, execution progress, and target resolution;
|
||||
- `SimulationPopulationView` owns the disposable all/living/starving stable-ID
|
||||
query indexes rebuilt at deterministic tick boundaries;
|
||||
- `VillageEconomy` owns storage/inventory transactions and synchronized
|
||||
village resource views;
|
||||
- `SimulationEventLog` owns deterministic event history and queries;
|
||||
@@ -545,6 +553,7 @@ NpcVisual navigates through the active world
|
||||
│ ├── events/ Ordered event history and queries
|
||||
│ ├── knowledge/ Per-NPC facts, provenance, transfer, and retention
|
||||
│ ├── opportunities/ Knowledge-gated unresolved-condition projection
|
||||
│ ├── population/ Transient per-tick NPC query indexes
|
||||
│ ├── persistence/ Validated local save-slot storage
|
||||
│ ├── relationships/ Directed social consequences and queries
|
||||
│ └── state/ Versioned simulation-state records
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# Jajce Center 02
|
||||
|
||||
Captured: 2026-07-16
|
||||
|
||||
## Files
|
||||
|
||||
- [before overview](jajce_center_02_before.png)
|
||||
- [after overview](jajce_center_02_after.png)
|
||||
- [close gameplay grass check](jajce_grass_gameplay_02.png)
|
||||
- `tools/capture_jajce_lookdev.gd`
|
||||
|
||||
The two overview images use the same 1280×720 camera, fixed daylight value,
|
||||
warmup, and Metal renderer. The gameplay image is a separate readability check,
|
||||
not part of the matched comparison.
|
||||
|
||||
## Reference reading
|
||||
|
||||
The reduction uses Jajce's hierarchy rather than attempting a literal city
|
||||
reconstruction:
|
||||
|
||||
- the [UNESCO tentative-list description](https://whc.unesco.org/en/tentativelists/2098/)
|
||||
describes the citadel at the summit of a rocky pyramid, ramparts descending
|
||||
its slopes, and residential roofs forming an amphitheater beneath it;
|
||||
- the [official Jajce fortress overview](https://www.agencija-jajce.ba/en/the-fortress/)
|
||||
supports treating the fortress as the primary long-distance landmark;
|
||||
- official Studio Ghibli still collections for
|
||||
[My Neighbor Totoro](https://www.ghibli.jp/works/totoro/) and
|
||||
[Princess Mononoke](https://www.ghibli.jp/works/mononoke/) were used only as
|
||||
broad references for layered green masses, clear silhouettes, restrained
|
||||
natural motion, and small warm human details inside large landscapes;
|
||||
- [Tiny Glade](https://store.steampowered.com/app/2198150/Tiny_Glade/) supports
|
||||
the low-detail diorama reading of whimsical castles, cottages, ruins, and
|
||||
incidental life;
|
||||
- the [Terrain3D instancer guidance](https://terrain3d.readthedocs.io/en/latest/docs/instancer.html)
|
||||
informed the split between camera-local particle grass and intentional tree
|
||||
placement.
|
||||
|
||||
No building, frame, or asset is copied from those references. The project keeps
|
||||
its own compact low-poly language and real simulation constraints.
|
||||
|
||||
## Implemented reduction
|
||||
|
||||
- The ridge block becomes a larger warm-stone citadel with a rocky plinth,
|
||||
ramparts, descending walls, towers, gate, crenellations, banner, and narrow
|
||||
window slits.
|
||||
- Eight timber-accented plaster houses now cascade below the citadel instead of
|
||||
leaving three isolated boxes in an empty field.
|
||||
- The waterfall is wider, softly animated, misted, and framed by irregular
|
||||
mossy rock columns. Its plunge pool feeds a turquoise ribbon that samples the
|
||||
live Terrain3D height field instead of disappearing beneath it.
|
||||
- The bridge gains a full river span, stone abutments, center pier, rails, and
|
||||
posts. House windows and chimney smoke add small warm points.
|
||||
- Existing rounded deciduous trees are richer and wind-reactive; authored
|
||||
conifer clusters strengthen the citadel, falls, and valley edges.
|
||||
- Three intentionally oversized butterflies add quiet ambient life.
|
||||
- A bounded camera-local Terrain3D grass field uses roughly 10,400 particles.
|
||||
Its project-owned process material keeps blades short and patchy, while a
|
||||
shader receives at most eight real player/NPC transforms and bends grass
|
||||
away from their feet.
|
||||
- Haze and saturation are reduced so green terrain, warm roofs, stone, and
|
||||
turquoise water separate more clearly.
|
||||
|
||||
## Authority boundary
|
||||
|
||||
Grass, butterflies, smoke, water motion, and foliage sway are transient
|
||||
presentation. They do not enter saves or checksums. Player and `NpcVisual`
|
||||
nodes merely expose real transforms to the grass shader.
|
||||
|
||||
Finite food and wood still come only from stable-ID `ResourceNode` anchors
|
||||
bound to simulation-owned records. The conifers and grass are decorative; the
|
||||
next resource-discovery phase can pair selected Terrain3D-authored visuals with
|
||||
intentional resource anchors without turning every blade or background tree
|
||||
into authoritative state. Livestock is deliberately deferred until one animal
|
||||
has a real identity/state/interaction contract; ambient butterflies do not
|
||||
pretend to be simulated food or work.
|
||||
|
||||
## Remaining visual work
|
||||
|
||||
This is a stronger prototype composition, not final environment art. The next
|
||||
art pass should sculpt a true river channel and waterfall shelf into Terrain3D,
|
||||
replace procedural fortress/house geometry gradually, add bank stones and
|
||||
flower/fern clusters through bounded instancing, and validate a representative
|
||||
frame-time budget. The matched comparison should remain the checkpoint before
|
||||
those asset-heavy changes.
|
||||
|
||||
## Capture command
|
||||
|
||||
```bash
|
||||
HOME="$PWD/logs/quality/godot_profile" \
|
||||
APPDATA="$PWD/logs/quality/godot_profile" \
|
||||
LOCALAPPDATA="$PWD/logs/quality/godot_profile" \
|
||||
/Applications/Godot.app/Contents/MacOS/Godot \
|
||||
--path "$PWD" --script res://tools/capture_jajce_lookdev.gd -- \
|
||||
--output=res://docs/baselines/jajce_center_02_after.png
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 729 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dcg13u5wqr23m"
|
||||
path="res://.godot/imported/jajce_center_02_after.png-be0a331c22c145d626fa09f682c7e379.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/baselines/jajce_center_02_after.png"
|
||||
dest_files=["res://.godot/imported/jajce_center_02_after.png-be0a331c22c145d626fa09f682c7e379.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.
|
After Width: | Height: | Size: 704 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dipu1epmps356"
|
||||
path="res://.godot/imported/jajce_center_02_before.png-a6dad6237a960278853b124b5c1180c2.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/baselines/jajce_center_02_before.png"
|
||||
dest_files=["res://.godot/imported/jajce_center_02_before.png-a6dad6237a960278853b124b5c1180c2.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.
|
After Width: | Height: | Size: 553 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://du1awnpktb7q0"
|
||||
path="res://.godot/imported/jajce_grass_gameplay_02.png-4b072b6e4f1c146d62492370903db793.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://docs/baselines/jajce_grass_gameplay_02.png"
|
||||
dest_files=["res://.godot/imported/jajce_grass_gameplay_02.png-4b072b6e4f1c146d62492370903db793.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
|
||||
@@ -21,3 +21,6 @@ Reviewed captures:
|
||||
- [Simulation scaling baseline 01](SIMULATION_SCALING_BASELINE_01.md) records
|
||||
the first full-fidelity population/history measurements and the bounded next
|
||||
optimization.
|
||||
- [Simulation scaling baseline 02](SIMULATION_SCALING_BASELINE_02.md) records
|
||||
checksum-identical results after the shared per-tick population view: the
|
||||
600-NPC case is 23.3% faster, while small fixtures expose its fixed cost.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Simulation scaling baseline 02
|
||||
|
||||
This is the reviewed comparison after introducing one reusable per-tick
|
||||
`SimulationPopulationView`. The view indexes all, living, and starving NPCs by
|
||||
stable ID, is refreshed as each NPC advances, and is shared by relationship and
|
||||
action-selection queries. It is transient derived state and is not serialized.
|
||||
|
||||
The machine-readable samples are in
|
||||
[`simulation_scaling_baseline_02.json`](simulation_scaling_baseline_02.json).
|
||||
The workload, seed, host, exclusions, warmup, and three-sample median contract
|
||||
match [baseline 01](SIMULATION_SCALING_BASELINE_01.md).
|
||||
|
||||
## Results
|
||||
|
||||
| Case | NPCs | Seeded history | Median us/tick | Ticks/s | Simulated realtime | Arrival share | End JSON |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| Population 6 | 6 | 0 | 63.85 | 15,662.93 | 18,795.5x | 4.5% | 0.06 MiB |
|
||||
| Population 60 | 60 | 0 | 662.62 | 1,509.17 | 1,811.0x | 5.7% | 0.56 MiB |
|
||||
| Population 600 | 600 | 0 | 11,654.10 | 85.81 | 103.0x | 18.0% | 5.67 MiB |
|
||||
| History 600 | 60 | 600 | 722.60 | 1,383.90 | 1,660.7x | 5.2% | 0.71 MiB |
|
||||
| History 6,000 | 60 | 6,000 | 1,430.65 | 698.98 | 838.8x | 2.6% | 2.04 MiB |
|
||||
|
||||
All samples within each case were deterministic. More importantly, every final
|
||||
checksum exactly matches the corresponding baseline-01 checksum, so the
|
||||
optimization preserves selection, interleaved NPC updates, event growth, and
|
||||
serialized continuation state.
|
||||
|
||||
## Comparison
|
||||
|
||||
| Case | Baseline 01 us/tick | Baseline 02 us/tick | Change |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Population 6 | 55.74 | 63.85 | 14.6% slower |
|
||||
| Population 60 | 621.87 | 662.62 | 6.6% slower |
|
||||
| Population 600 | 15,187.74 | 11,654.10 | **23.3% faster** |
|
||||
| History 600 | 677.12 | 722.60 | 6.7% slower |
|
||||
| History 6,000 | 1,421.19 | 1,430.65 | 0.7% slower |
|
||||
|
||||
The shared dictionaries add a small fixed per-tick cost, visible in tiny
|
||||
fixtures and normal local timing noise. At the measured population knee they
|
||||
remove repeated all-NPC lookup construction and improve the 600-NPC case from
|
||||
65.84 to 85.81 ticks per second. The local 50-ticks-per-second reference target
|
||||
remains comfortably met without introducing simulation LOD.
|
||||
|
||||
## Decision and next measurement
|
||||
|
||||
Keep the population view. It has a real relationship/action consumer, improves
|
||||
the measured pressure point, and preserves exact state. Do not add batching or
|
||||
active/abstract NPC modes merely to improve this data-only number.
|
||||
|
||||
The next bounded scale slice should support the world vision directly: a
|
||||
loaded-resource spatial query owned by `ActiveWorldAdapter`. It should let
|
||||
villagers discover finite, stable-ID `ResourceNode` anchors placed much farther
|
||||
apart across Terrain3D while preserving the current score, reachability,
|
||||
reservation, and save/rebind contracts. A benchmark should compare 18, 180,
|
||||
and 1,800 loaded candidates before choosing a grid, tree, or other index.
|
||||
|
||||
Terrain3D-instanced visual trees and grass are not automatically authoritative
|
||||
resources. Intentional harvest anchors must continue to bind simulation-owned
|
||||
`ResourceStateRecord` data by stable ID; large decorative populations can stay
|
||||
presentation-only until a gameplay rule needs them.
|
||||
|
||||
## Capture command
|
||||
|
||||
```bash
|
||||
/Applications/Godot.app/Contents/MacOS/Godot \
|
||||
--headless --path "$PWD" \
|
||||
--script res://tools/benchmark_simulation_scaling.gd -- \
|
||||
--host-label=Apple_M1_Max_64_GB \
|
||||
--output=res://docs/benchmarks/simulation_scaling_baseline_02.json
|
||||
```
|
||||
@@ -0,0 +1,308 @@
|
||||
{
|
||||
"benchmark_seed": 8088,
|
||||
"captured_utc": "2026-07-16T21:38:42Z",
|
||||
"cases": [
|
||||
{
|
||||
"arrival_share_percent": 4.47176756206437,
|
||||
"arrival_usec_median": 571,
|
||||
"arrival_usec_samples": [
|
||||
563,
|
||||
571,
|
||||
579
|
||||
],
|
||||
"arrivals_processed": 209,
|
||||
"case_id": "population_006",
|
||||
"elapsed_usec_max": 12771,
|
||||
"elapsed_usec_median": 12769,
|
||||
"elapsed_usec_min": 12507,
|
||||
"elapsed_usec_samples": [
|
||||
12507,
|
||||
12769,
|
||||
12771
|
||||
],
|
||||
"end_event_count": 221,
|
||||
"end_known_reference_count": 0,
|
||||
"end_state_bytes": 58576,
|
||||
"end_tick": 210,
|
||||
"events_recorded": 209,
|
||||
"final_checksum": "100dd5dc1e0c22ea23f35dd876d6f56fd5c33c58b1586b6fa9aec47ee1536511",
|
||||
"history_seed_events": 0,
|
||||
"measured_ticks": 200,
|
||||
"npc_updates": 1200,
|
||||
"population": 6,
|
||||
"realtime_factor_median": 18795.5204009711,
|
||||
"sample_count": 3,
|
||||
"schema_version": 1,
|
||||
"seed": 8088,
|
||||
"setup_usec_median": 627,
|
||||
"setup_usec_samples": [
|
||||
546,
|
||||
627,
|
||||
6728
|
||||
],
|
||||
"simulation_usec_median": 12173,
|
||||
"simulation_usec_samples": [
|
||||
11916,
|
||||
12173,
|
||||
12186
|
||||
],
|
||||
"start_event_count": 12,
|
||||
"start_known_reference_count": 0,
|
||||
"start_state_bytes": 7632,
|
||||
"start_tick": 10,
|
||||
"state_growth_bytes": 50944,
|
||||
"tick_interval": 1.2,
|
||||
"ticks_per_second_median": 15662.9336674759,
|
||||
"usec_per_tick_median": 63.845,
|
||||
"warmup_arrivals": 12,
|
||||
"warmup_ticks": 10,
|
||||
"workload_id": "full_fidelity_headless_arrival"
|
||||
},
|
||||
{
|
||||
"arrival_share_percent": 5.66845000490481,
|
||||
"arrival_usec_median": 7512,
|
||||
"arrival_usec_samples": [
|
||||
7458,
|
||||
7512,
|
||||
7571
|
||||
],
|
||||
"arrivals_processed": 2090,
|
||||
"case_id": "population_060",
|
||||
"elapsed_usec_max": 133541,
|
||||
"elapsed_usec_median": 132523,
|
||||
"elapsed_usec_min": 130182,
|
||||
"elapsed_usec_samples": [
|
||||
130182,
|
||||
132523,
|
||||
133541
|
||||
],
|
||||
"end_event_count": 2210,
|
||||
"end_known_reference_count": 0,
|
||||
"end_state_bytes": 587324,
|
||||
"end_tick": 210,
|
||||
"events_recorded": 2090,
|
||||
"final_checksum": "58c7f96fc5df7f5399fd82d1a9c212b3c53067e807059e88ffa484ba0eb41e16",
|
||||
"history_seed_events": 0,
|
||||
"measured_ticks": 200,
|
||||
"npc_updates": 12000,
|
||||
"population": 60,
|
||||
"realtime_factor_median": 1811.00639134339,
|
||||
"sample_count": 3,
|
||||
"schema_version": 1,
|
||||
"seed": 8088,
|
||||
"setup_usec_median": 3788,
|
||||
"setup_usec_samples": [
|
||||
3481,
|
||||
3788,
|
||||
3917
|
||||
],
|
||||
"simulation_usec_median": 125039,
|
||||
"simulation_usec_samples": [
|
||||
122647,
|
||||
125039,
|
||||
125926
|
||||
],
|
||||
"start_event_count": 120,
|
||||
"start_known_reference_count": 0,
|
||||
"start_state_bytes": 71278,
|
||||
"start_tick": 10,
|
||||
"state_growth_bytes": 516046,
|
||||
"tick_interval": 1.2,
|
||||
"ticks_per_second_median": 1509.17199278616,
|
||||
"usec_per_tick_median": 662.615,
|
||||
"warmup_arrivals": 120,
|
||||
"warmup_ticks": 10,
|
||||
"workload_id": "full_fidelity_headless_arrival"
|
||||
},
|
||||
{
|
||||
"arrival_share_percent": 17.9571567087978,
|
||||
"arrival_usec_median": 418549,
|
||||
"arrival_usec_samples": [
|
||||
417929,
|
||||
418549,
|
||||
425849
|
||||
],
|
||||
"arrivals_processed": 20950,
|
||||
"case_id": "population_600",
|
||||
"elapsed_usec_max": 2346707,
|
||||
"elapsed_usec_median": 2330820,
|
||||
"elapsed_usec_min": 2326981,
|
||||
"elapsed_usec_samples": [
|
||||
2326981,
|
||||
2330820,
|
||||
2346707
|
||||
],
|
||||
"end_event_count": 22150,
|
||||
"end_known_reference_count": 0,
|
||||
"end_state_bytes": 5948447,
|
||||
"end_tick": 210,
|
||||
"events_recorded": 20950,
|
||||
"final_checksum": "fa4efac63fde63f92eb28141bebd1922f665e85ffe7b7ac2b9702dae6e4c24b7",
|
||||
"history_seed_events": 0,
|
||||
"measured_ticks": 200,
|
||||
"npc_updates": 120000,
|
||||
"population": 600,
|
||||
"realtime_factor_median": 102.968054161196,
|
||||
"sample_count": 3,
|
||||
"schema_version": 1,
|
||||
"seed": 8088,
|
||||
"setup_usec_median": 34966,
|
||||
"setup_usec_samples": [
|
||||
34505,
|
||||
34966,
|
||||
34980
|
||||
],
|
||||
"simulation_usec_median": 1912210,
|
||||
"simulation_usec_samples": [
|
||||
1908995,
|
||||
1912210,
|
||||
1920790
|
||||
],
|
||||
"start_event_count": 1200,
|
||||
"start_known_reference_count": 0,
|
||||
"start_state_bytes": 713361,
|
||||
"start_tick": 10,
|
||||
"state_growth_bytes": 5235086,
|
||||
"tick_interval": 1.2,
|
||||
"ticks_per_second_median": 85.8067118009971,
|
||||
"usec_per_tick_median": 11654.1,
|
||||
"warmup_arrivals": 1200,
|
||||
"warmup_ticks": 10,
|
||||
"workload_id": "full_fidelity_headless_arrival"
|
||||
},
|
||||
{
|
||||
"arrival_share_percent": 5.24844484116275,
|
||||
"arrival_usec_median": 7585,
|
||||
"arrival_usec_samples": [
|
||||
7540,
|
||||
7585,
|
||||
7604
|
||||
],
|
||||
"arrivals_processed": 2090,
|
||||
"case_id": "history_000600",
|
||||
"elapsed_usec_max": 145539,
|
||||
"elapsed_usec_median": 144519,
|
||||
"elapsed_usec_min": 144335,
|
||||
"elapsed_usec_samples": [
|
||||
144335,
|
||||
144519,
|
||||
145539
|
||||
],
|
||||
"end_event_count": 2810,
|
||||
"end_known_reference_count": 0,
|
||||
"end_state_bytes": 743004,
|
||||
"end_tick": 810,
|
||||
"events_recorded": 2090,
|
||||
"final_checksum": "7ae86044f7683359d6bfbc8753e2656f3af90f23d040039154c6869dca48cf65",
|
||||
"history_seed_events": 600,
|
||||
"measured_ticks": 200,
|
||||
"npc_updates": 12000,
|
||||
"population": 60,
|
||||
"realtime_factor_median": 1660.68129450107,
|
||||
"sample_count": 3,
|
||||
"schema_version": 1,
|
||||
"seed": 8088,
|
||||
"setup_usec_median": 22723,
|
||||
"setup_usec_samples": [
|
||||
21887,
|
||||
22723,
|
||||
23413
|
||||
],
|
||||
"simulation_usec_median": 136897,
|
||||
"simulation_usec_samples": [
|
||||
136707,
|
||||
136897,
|
||||
137966
|
||||
],
|
||||
"start_event_count": 720,
|
||||
"start_known_reference_count": 180,
|
||||
"start_state_bytes": 253098,
|
||||
"start_tick": 610,
|
||||
"state_growth_bytes": 489906,
|
||||
"tick_interval": 1.2,
|
||||
"ticks_per_second_median": 1383.90107875089,
|
||||
"usec_per_tick_median": 722.595,
|
||||
"warmup_arrivals": 120,
|
||||
"warmup_ticks": 10,
|
||||
"workload_id": "full_fidelity_headless_arrival"
|
||||
},
|
||||
{
|
||||
"arrival_share_percent": 2.64320413797924,
|
||||
"arrival_usec_median": 7563,
|
||||
"arrival_usec_samples": [
|
||||
7552,
|
||||
7563,
|
||||
7628
|
||||
],
|
||||
"arrivals_processed": 2090,
|
||||
"case_id": "history_006000",
|
||||
"elapsed_usec_max": 291609,
|
||||
"elapsed_usec_median": 286130,
|
||||
"elapsed_usec_min": 285483,
|
||||
"elapsed_usec_samples": [
|
||||
285483,
|
||||
286130,
|
||||
291609
|
||||
],
|
||||
"end_event_count": 8210,
|
||||
"end_known_reference_count": 0,
|
||||
"end_state_bytes": 2141615,
|
||||
"end_tick": 6210,
|
||||
"events_recorded": 2090,
|
||||
"final_checksum": "b4df2bac21ed5ce6dcd92736e210db5f3114d959dcb15ae7796493362911b6d7",
|
||||
"history_seed_events": 6000,
|
||||
"measured_ticks": 200,
|
||||
"npc_updates": 12000,
|
||||
"population": 60,
|
||||
"realtime_factor_median": 838.779575717331,
|
||||
"sample_count": 3,
|
||||
"schema_version": 1,
|
||||
"seed": 8088,
|
||||
"setup_usec_median": 153306,
|
||||
"setup_usec_samples": [
|
||||
153290,
|
||||
153306,
|
||||
154064
|
||||
],
|
||||
"simulation_usec_median": 278467,
|
||||
"simulation_usec_samples": [
|
||||
277892,
|
||||
278467,
|
||||
284022
|
||||
],
|
||||
"start_event_count": 6120,
|
||||
"start_known_reference_count": 180,
|
||||
"start_state_bytes": 1649700,
|
||||
"start_tick": 6010,
|
||||
"state_growth_bytes": 491915,
|
||||
"tick_interval": 1.2,
|
||||
"ticks_per_second_median": 698.982979764443,
|
||||
"usec_per_tick_median": 1430.65,
|
||||
"warmup_arrivals": 120,
|
||||
"warmup_ticks": 10,
|
||||
"workload_id": "full_fidelity_headless_arrival"
|
||||
}
|
||||
],
|
||||
"engine_version": "4.7-stable (official)",
|
||||
"exclusions": [
|
||||
"manager_and_fixture_setup",
|
||||
"state_serialization",
|
||||
"world_scene",
|
||||
"rendering",
|
||||
"navigation",
|
||||
"npc_visual"
|
||||
],
|
||||
"host_label": "Apple_M1_Max_64_GB",
|
||||
"measured_ticks": 200,
|
||||
"platform": "macOS",
|
||||
"processor_count": 10,
|
||||
"sample_count": 3,
|
||||
"schema_version": 1,
|
||||
"timed_phases": [
|
||||
"simulation_tick",
|
||||
"headless_arrival_completion"
|
||||
],
|
||||
"warmup_ticks": 10,
|
||||
"workload": "All NPCs receive full per-tick needs/task updates and ordinary action decisions; travel resolves through the deterministic immediate-arrival headless convention.",
|
||||
"workload_id": "full_fidelity_headless_arrival"
|
||||
}
|
||||
@@ -66,6 +66,7 @@ func debug_log(message: String) -> void:
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("grass_interactors")
|
||||
dead_material.albedo_color = Color(0.25, 0.25, 0.25, 1.0)
|
||||
_stop_trust_gain_reaction()
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ extends CharacterBody3D
|
||||
@export var interaction_range := 3.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("grass_interactors")
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
var input := Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ var latest_decisions: Dictionary = {}
|
||||
var action_selector := ActionSelectionSystem.new()
|
||||
var action_executor := ActionExecutionSystem.new()
|
||||
var target_resolver := ActionTargetResolver.new()
|
||||
var _population_view := SimulationPopulationView.new()
|
||||
var speed_index := 2
|
||||
const SPEED_LEVELS := [0.25, 0.5, 1.0, 2.0, 4.0, 10.0]
|
||||
const KNOWLEDGE_COMMUNICATION_RADIUS := 2.5
|
||||
@@ -134,6 +135,7 @@ func generate_npcs() -> void:
|
||||
if home_count > 0:
|
||||
for i in range(npcs.size()):
|
||||
npcs[i].home_position = home_positions[i % home_count]
|
||||
_population_view.rebuild(npcs)
|
||||
|
||||
|
||||
func _create_random_source(npc_id: int, stream_id: int) -> RandomNumberGenerator:
|
||||
@@ -155,6 +157,7 @@ func simulate_tick() -> void:
|
||||
if debug_logs:
|
||||
print("--- Tick ", tick_count, " ---")
|
||||
var village_was_changed := false
|
||||
_population_view.rebuild(npcs)
|
||||
for npc in npcs:
|
||||
village_was_changed = _simulate_npc_tick(npc) or village_was_changed
|
||||
if village_was_changed:
|
||||
@@ -178,6 +181,7 @@ func _simulate_npc_tick(npc: SimNPC) -> bool:
|
||||
var was_dead := npc.is_dead
|
||||
|
||||
action_executor.advance_npc(npc, village)
|
||||
_population_view.refresh_npc(npc)
|
||||
_select_action_if_idle(npc, previous_state)
|
||||
|
||||
if previous_task != npc.current_task and not previous_target.is_empty():
|
||||
@@ -192,6 +196,7 @@ func _simulate_npc_tick(npc: SimNPC) -> bool:
|
||||
var completed := not was_complete and npc.task_complete and not npc.is_dead
|
||||
if completed:
|
||||
_complete_current_action(npc)
|
||||
_population_view.refresh_npc(npc)
|
||||
|
||||
_debug_npc_tick(npc, previous_state)
|
||||
return completed
|
||||
@@ -205,7 +210,9 @@ func _select_action_if_idle(npc: SimNPC, previous_state: StringName) -> void:
|
||||
if npc.task_state not in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]:
|
||||
return
|
||||
var helper := get_active_opportunity_helper()
|
||||
var selection := action_selector.select_action(npc, village, clock.time_of_day(), npcs, helper)
|
||||
var selection := action_selector.select_action(
|
||||
npc, village, clock.time_of_day(), npcs, helper, _population_view
|
||||
)
|
||||
if selection == null:
|
||||
return
|
||||
latest_decisions[npc.id] = selection
|
||||
@@ -509,7 +516,7 @@ func get_activity_target_claim_count(target_id: StringName, except_npc_id: int =
|
||||
|
||||
|
||||
func _notify_mourning(dead_npc: SimNPC) -> void:
|
||||
var mourner := relationship_system.get_most_familiar_living_subject(dead_npc.id, npcs)
|
||||
var mourner := relationship_system.get_most_familiar_living(dead_npc.id, npcs, _population_view)
|
||||
if mourner == null:
|
||||
return
|
||||
mourner.mourning_ticks = 100
|
||||
@@ -761,10 +768,7 @@ func _get_lasting_event_ids(npc_id: int) -> Array[int]:
|
||||
|
||||
|
||||
func _find_npc_by_id(npc_id: int) -> SimNPC:
|
||||
for npc in npcs:
|
||||
if npc.id == npc_id:
|
||||
return npc
|
||||
return null
|
||||
return _population_view.get_any(npc_id)
|
||||
|
||||
|
||||
static func _sort_npcs_by_id(first: SimNPC, second: SimNPC) -> bool:
|
||||
@@ -1044,11 +1048,7 @@ func add_knowledge(amount: float) -> void:
|
||||
|
||||
|
||||
func get_starving_count() -> int:
|
||||
var count := 0
|
||||
for npc in npcs:
|
||||
if npc.is_starving:
|
||||
count += 1
|
||||
return count
|
||||
return _population_view.starving_count()
|
||||
|
||||
|
||||
func get_state_snapshot() -> Dictionary:
|
||||
@@ -1178,6 +1178,7 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
||||
npcs.clear()
|
||||
for npc_record in record.npcs:
|
||||
npcs.append(npc_record.restore(debug_logs))
|
||||
_population_view.rebuild(npcs)
|
||||
relationship_system.restore(record.relationships)
|
||||
event_knowledge_system.restore(record.event_knowledge)
|
||||
opportunity_system.restore(record.opportunities, int(record.simulation["next_opportunity_id"]))
|
||||
|
||||
@@ -22,7 +22,8 @@ func select_action(
|
||||
village: SimVillage,
|
||||
time_of_day: float = 0.5,
|
||||
all_npcs: Array = [],
|
||||
opportunity_helper: OpportunityHelperResult = null
|
||||
opportunity_helper: OpportunityHelperResult = null,
|
||||
population_view: SimulationPopulationView = null
|
||||
) -> ActionSelectionResult:
|
||||
if npc.is_dead:
|
||||
return null
|
||||
@@ -127,7 +128,7 @@ func select_action(
|
||||
)
|
||||
if village.food <= RELATIONSHIP_AID_PANTRY_THRESHOLD and relationship_system != null:
|
||||
var trusted_starving_npc: SimNPC = relationship_system.get_trusted_starving_subject(
|
||||
npc.id, all_npcs
|
||||
npc.id, all_npcs, population_view
|
||||
)
|
||||
if trusted_starving_npc != null:
|
||||
return ActionSelectionResult.new(
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
class_name SimulationPopulationView
|
||||
extends RefCounted
|
||||
|
||||
var all_by_id: Dictionary = {}
|
||||
var living_by_id: Dictionary = {}
|
||||
var starving_by_id: Dictionary = {}
|
||||
|
||||
|
||||
func _init(npcs: Array = []) -> void:
|
||||
rebuild(npcs)
|
||||
|
||||
|
||||
func rebuild(npcs: Array) -> void:
|
||||
all_by_id.clear()
|
||||
living_by_id.clear()
|
||||
starving_by_id.clear()
|
||||
for value in npcs:
|
||||
var npc := value as SimNPC
|
||||
if npc != null:
|
||||
refresh_npc(npc)
|
||||
|
||||
|
||||
func refresh_npc(npc: SimNPC) -> void:
|
||||
if npc == null:
|
||||
return
|
||||
all_by_id[npc.id] = npc
|
||||
if npc.is_dead:
|
||||
living_by_id.erase(npc.id)
|
||||
starving_by_id.erase(npc.id)
|
||||
return
|
||||
living_by_id[npc.id] = npc
|
||||
if npc.is_starving:
|
||||
starving_by_id[npc.id] = npc
|
||||
else:
|
||||
starving_by_id.erase(npc.id)
|
||||
|
||||
|
||||
func get_living(npc_id: int) -> SimNPC:
|
||||
return living_by_id.get(npc_id) as SimNPC
|
||||
|
||||
|
||||
func get_any(npc_id: int) -> SimNPC:
|
||||
return all_by_id.get(npc_id) as SimNPC
|
||||
|
||||
|
||||
func get_starving(npc_id: int) -> SimNPC:
|
||||
return starving_by_id.get(npc_id) as SimNPC
|
||||
|
||||
|
||||
func living_count() -> int:
|
||||
return living_by_id.size()
|
||||
|
||||
|
||||
func starving_count() -> int:
|
||||
return starving_by_id.size()
|
||||
@@ -0,0 +1 @@
|
||||
uid://btwlag4mpji24
|
||||
@@ -95,16 +95,17 @@ func get_primary_relationship(observer_id: int) -> RelationshipStateRecord:
|
||||
return best
|
||||
|
||||
|
||||
func get_most_familiar_living_subject(observer_id: int, npcs: Array[SimNPC]) -> SimNPC:
|
||||
var living_by_id := {}
|
||||
for npc in npcs:
|
||||
if not npc.is_dead:
|
||||
living_by_id[npc.id] = npc
|
||||
func get_most_familiar_living(
|
||||
observer_id: int, npcs: Array[SimNPC], population_view: SimulationPopulationView = null
|
||||
) -> SimNPC:
|
||||
var effective_view := population_view
|
||||
if effective_view == null:
|
||||
effective_view = SimulationPopulationView.new(npcs)
|
||||
var best_relationship: RelationshipStateRecord
|
||||
for relationship in relationships.values():
|
||||
if relationship.get_observer_id() != observer_id:
|
||||
continue
|
||||
if not living_by_id.has(relationship.get_subject_id()):
|
||||
if effective_view.get_living(relationship.get_subject_id()) == null:
|
||||
continue
|
||||
if (
|
||||
best_relationship == null
|
||||
@@ -117,21 +118,22 @@ func get_most_familiar_living_subject(observer_id: int, npcs: Array[SimNPC]) ->
|
||||
best_relationship = relationship
|
||||
if best_relationship == null:
|
||||
return null
|
||||
return living_by_id[best_relationship.get_subject_id()] as SimNPC
|
||||
return effective_view.get_living(best_relationship.get_subject_id())
|
||||
|
||||
|
||||
func get_trusted_starving_subject(observer_id: int, npcs: Array[SimNPC]) -> SimNPC:
|
||||
var starving_by_id := {}
|
||||
for npc in npcs:
|
||||
if not npc.is_dead and npc.is_starving:
|
||||
starving_by_id[npc.id] = npc
|
||||
func get_trusted_starving_subject(
|
||||
observer_id: int, npcs: Array, population_view: SimulationPopulationView = null
|
||||
) -> SimNPC:
|
||||
var effective_view := population_view
|
||||
if effective_view == null:
|
||||
effective_view = SimulationPopulationView.new(npcs)
|
||||
var best_relationship: RelationshipStateRecord
|
||||
for relationship in relationships.values():
|
||||
if relationship.get_observer_id() != observer_id:
|
||||
continue
|
||||
if relationship.get_trust() < TRUSTED_HELP_THRESHOLD:
|
||||
continue
|
||||
if not starving_by_id.has(relationship.get_subject_id()):
|
||||
if effective_view.get_starving(relationship.get_subject_id()) == null:
|
||||
continue
|
||||
if (
|
||||
best_relationship == null
|
||||
@@ -144,7 +146,7 @@ func get_trusted_starving_subject(observer_id: int, npcs: Array[SimNPC]) -> SimN
|
||||
best_relationship = relationship
|
||||
if best_relationship == null:
|
||||
return null
|
||||
return starving_by_id[best_relationship.get_subject_id()] as SimNPC
|
||||
return effective_view.get_starving(best_relationship.get_subject_id())
|
||||
|
||||
|
||||
func _get_or_create(observer_id: int, subject_id: int) -> RelationshipStateRecord:
|
||||
|
||||
@@ -31,6 +31,11 @@ func _run() -> void:
|
||||
await process_frame
|
||||
|
||||
_check(simulation_manager.npcs.size() == 6, "Baseline should create six NPCs")
|
||||
var runtime_player := main_scene.get_node("Player") as Node3D
|
||||
_check(
|
||||
runtime_player.is_in_group("grass_interactors"),
|
||||
"The real player transform should drive nearby grass response"
|
||||
)
|
||||
var inspector_label := (
|
||||
main_scene.get_node("UI/NpcInspectorPanel/MarginContainer/NpcInspectorLabel") as Label
|
||||
)
|
||||
@@ -60,6 +65,26 @@ func _run() -> void:
|
||||
"Runtime knowledge UI check should use a completed simulation transaction"
|
||||
)
|
||||
var world_view := main_scene.get_node("WorldViewManager")
|
||||
var all_npc_visuals_drive_grass := true
|
||||
for npc_visual in world_view.active_npc_visuals.values():
|
||||
all_npc_visuals_drive_grass = (
|
||||
all_npc_visuals_drive_grass and (npc_visual as Node3D).is_in_group("grass_interactors")
|
||||
)
|
||||
_check(
|
||||
all_npc_visuals_drive_grass,
|
||||
"Every active villager visual should drive local grass response"
|
||||
)
|
||||
var grass_material := (
|
||||
main_scene.get_node("JajceWorld/TerrainRoot/Terrain3D/CozyGrassField").get(
|
||||
"mesh_material_override"
|
||||
)
|
||||
as ShaderMaterial
|
||||
)
|
||||
var grass_interactor_count := int(grass_material.get_shader_parameter("interactor_count"))
|
||||
_check(
|
||||
grass_interactor_count in range(1, 9),
|
||||
"Grass shader should receive a bounded set of real nearby actor transforms"
|
||||
)
|
||||
var contributor_visual: Node3D = world_view.active_npc_visuals[contributor.id]
|
||||
var witness_visual: Node3D = world_view.active_npc_visuals[witness.id]
|
||||
var listener_visual: Node3D = world_view.active_npc_visuals[listener.id]
|
||||
|
||||
@@ -63,10 +63,48 @@ func _run() -> void:
|
||||
_check(world.has_node("WaterRoot"), "JajceWorld should expose WaterRoot")
|
||||
_check(world.has_node("VillageRoot"), "JajceWorld should expose VillageRoot")
|
||||
_check(world.has_node("FoliageRoot"), "JajceWorld should expose FoliageRoot")
|
||||
_check(world.has_node("WildlifeRoot"), "JajceWorld should expose ambient WildlifeRoot")
|
||||
_check(
|
||||
world.get_node("FoliageRoot").get_child_count() >= 10,
|
||||
"JajceWorld should include restrained authored foliage clusters"
|
||||
)
|
||||
var conifer_count := 0
|
||||
for foliage in world.get_node("FoliageRoot").get_children():
|
||||
if foliage.name.begins_with("Conifer_"):
|
||||
conifer_count += 1
|
||||
_check(conifer_count >= 6, "Citadel and river edges should include conifer silhouettes")
|
||||
_check(
|
||||
world.get_node("WildlifeRoot").get_child_count() >= 3,
|
||||
"Village center should include a few oversized ambient butterflies"
|
||||
)
|
||||
var grass_field := world.get_node("TerrainRoot/Terrain3D/CozyGrassField")
|
||||
var grass_material := grass_field.get("mesh_material_override") as ShaderMaterial
|
||||
var grass_process := grass_field.get("process_material") as ShaderMaterial
|
||||
_check(
|
||||
int(grass_field.get("particle_count")) in range(9000, 15001),
|
||||
"Camera-local Terrain3D grass should stay inside its bounded particle budget"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
grass_material != null
|
||||
and grass_material.shader == load("res://world/jajce/materials/cozy_grass.gdshader")
|
||||
),
|
||||
"Terrain grass should use the cozy wind-and-interaction shader"
|
||||
)
|
||||
var grass_max_scale := Vector3.ZERO
|
||||
var grass_clod_boost := INF
|
||||
if grass_process != null:
|
||||
grass_max_scale = grass_process.get_shader_parameter("max_scale") as Vector3
|
||||
grass_clod_boost = float(grass_process.get_shader_parameter("clod_scale_boost"))
|
||||
_check(
|
||||
grass_process != null and grass_max_scale.y <= 0.5 and grass_clod_boost <= 0.2,
|
||||
"Grass placement should stay short enough to preserve villagers and paths"
|
||||
)
|
||||
var grass_controller := world.get_node("TerrainRoot/GrassInteractionController")
|
||||
_check(
|
||||
grass_controller.get("grass_material") == grass_material,
|
||||
"Grass interaction should update the same material rendered by Terrain3D particles"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
not world.has_node("WindStrokes_Valley")
|
||||
@@ -156,7 +194,15 @@ func _run() -> void:
|
||||
)
|
||||
_check(tree_wind_is_bounded, "Tree wind should stay within the restrained cozy-motion budget")
|
||||
_check(world.has_node("FortressBlockout"), "JajceWorld should include the fortress landmark")
|
||||
_check(
|
||||
world.has_node("FortressBlockout/Keep/CitadelKeep"),
|
||||
"Fortress silhouette should expose a distinct summit citadel"
|
||||
)
|
||||
_check(world.has_node("WaterRoot/WaterfallBody"), "WaterRoot should include a waterfall drop")
|
||||
_check(
|
||||
world.has_node("WaterRoot/WaterfallBody/RockFrame"),
|
||||
"Waterfall should be framed by the rocky river threshold"
|
||||
)
|
||||
_check(
|
||||
world.has_node("WaterRoot/WaterfallMist"),
|
||||
"WaterRoot should include waterfall mist particles"
|
||||
@@ -167,10 +213,19 @@ func _run() -> void:
|
||||
"Waterfall mist particles should have visible billboard geometry"
|
||||
)
|
||||
_check(
|
||||
waterfall_mist.position.distance_to(Vector3(25.0, 0.8, -24.0)) < 0.01,
|
||||
waterfall_mist.position.distance_to(Vector3(25.0, 2.05, -24.0)) < 0.01,
|
||||
"Waterfall mist should stay at the waterfall base instead of obscuring the village"
|
||||
)
|
||||
_check(world.has_node("WaterRoot/RiverSurface"), "WaterRoot should include a river surface")
|
||||
var river_ribbon := world.get_node("WaterRoot/RiverSurface/RiverRibbon") as MeshInstance3D
|
||||
_check(
|
||||
river_ribbon.mesh is ArrayMesh,
|
||||
"River should build a lightweight ribbon from the current Terrain3D surface"
|
||||
)
|
||||
_check(
|
||||
world.has_node("WaterRoot/RiverSurface/PlungePool"),
|
||||
"River should widen into a readable plunge pool beneath the waterfall"
|
||||
)
|
||||
var environment: Environment = (
|
||||
(world.get_node("WorldEnvironment") as WorldEnvironment).environment
|
||||
)
|
||||
@@ -190,6 +245,11 @@ func _run() -> void:
|
||||
world.has_node("VillageRoot/Mill") and world.has_node("VillageRoot/Bridge"),
|
||||
"VillageRoot should include the mill and bridge blockouts"
|
||||
)
|
||||
var house_count := 0
|
||||
for village_child in world.get_node("VillageRoot").get_children():
|
||||
if village_child.name.begins_with("House_"):
|
||||
house_count += 1
|
||||
_check(house_count >= 8, "Roofs should form a small residential amphitheater below the citadel")
|
||||
var smoke_count := 0
|
||||
var smoke_is_translucent := true
|
||||
var smoke_follows_wind := true
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
extends SceneTree
|
||||
|
||||
const RelationshipSystemScript := preload("res://simulation/relationships/RelationshipSystem.gd")
|
||||
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var first := _create_npc(0, "First")
|
||||
var second := _create_npc(1, "Second")
|
||||
var observer := _create_npc(2, "Observer")
|
||||
first.is_starving = true
|
||||
var npcs: Array[SimNPC] = [first, second, observer]
|
||||
var view := SimulationPopulationView.new(npcs)
|
||||
_check(view.living_count() == 3, "The initial view should index every living NPC")
|
||||
_check(view.starving_count() == 1, "The initial view should index the starving NPC once")
|
||||
_check(view.get_starving(first.id) == first, "The starving lookup should preserve identity")
|
||||
|
||||
second.is_starving = true
|
||||
view.refresh_npc(second)
|
||||
_check(view.starving_count() == 2, "Refreshing should add a newly starving NPC")
|
||||
first.die_from_starvation()
|
||||
view.refresh_npc(first)
|
||||
_check(
|
||||
view.get_living(first.id) == null and view.get_starving(first.id) == null,
|
||||
"Refreshing should remove a dead NPC from both indexes"
|
||||
)
|
||||
_check(view.get_any(first.id) == first, "The identity index should retain a dead NPC record")
|
||||
|
||||
var relationships := RelationshipSystemScript.new()
|
||||
var records: Array[RelationshipStateRecord] = [
|
||||
RelationshipStateRecord.create(observer.id, first.id, 0.8, 0.9),
|
||||
RelationshipStateRecord.create(observer.id, second.id, 0.7, 0.7),
|
||||
]
|
||||
relationships.restore(records)
|
||||
_check(
|
||||
relationships.get_trusted_starving_subject(observer.id, npcs, view) == second,
|
||||
"The relationship query should ignore the stronger dead subject through the shared view"
|
||||
)
|
||||
_check(
|
||||
relationships.get_most_familiar_living(observer.id, npcs, view) == second,
|
||||
"The living-subject query should use the same refreshed view"
|
||||
)
|
||||
|
||||
first.is_dead = false
|
||||
first.is_starving = true
|
||||
view.refresh_npc(first)
|
||||
_check(
|
||||
relationships.get_trusted_starving_subject(observer.id, npcs, view) == first,
|
||||
"A refreshed living subject should regain eligibility without rebuilding the view"
|
||||
)
|
||||
_finish()
|
||||
|
||||
|
||||
func _create_npc(npc_id: int, npc_name: String) -> SimNPC:
|
||||
var random_source := RandomNumberGenerator.new()
|
||||
random_source.seed = 7000 + npc_id
|
||||
return SimNPC.new(npc_id, npc_name, SimulationIds.PROFESSION_FARMER, 5.0, 5.0, random_source)
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("[TEST] Simulation population view passed: refresh -> shared stable-ID queries")
|
||||
quit(0)
|
||||
return
|
||||
for failure in failures:
|
||||
push_error("[TEST] " + failure)
|
||||
quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ervmgkik5q2x
|
||||
@@ -1,7 +1,7 @@
|
||||
extends SceneTree
|
||||
|
||||
const OUTPUT_PATH := "res://docs/baselines/jajce_lookdev_01.png"
|
||||
const SCENE_PATH := "res://world/jajce/JajceLookdev.tscn"
|
||||
const DEFAULT_OUTPUT_PATH := "res://docs/baselines/jajce_lookdev_01.png"
|
||||
const DEFAULT_SCENE_PATH := "res://world/jajce/JajceLookdev.tscn"
|
||||
const CAPTURE_SIZE := Vector2i(1280, 720)
|
||||
const WARMUP_FRAMES := 90
|
||||
|
||||
@@ -12,15 +12,18 @@ func _initialize() -> void:
|
||||
|
||||
func _run() -> void:
|
||||
root.size = CAPTURE_SIZE
|
||||
var scene := load(SCENE_PATH) as PackedScene
|
||||
var scene_path := _get_argument_value("--scene=", DEFAULT_SCENE_PATH)
|
||||
var output_path := _get_argument_value("--output=", DEFAULT_OUTPUT_PATH)
|
||||
var scene := load(scene_path) as PackedScene
|
||||
if scene == null:
|
||||
push_error("Could not load %s" % SCENE_PATH)
|
||||
push_error("Could not load %s" % scene_path)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var lookdev := scene.instantiate() as Node3D
|
||||
root.add_child(lookdev)
|
||||
await process_frame
|
||||
_stabilize_lookdev(lookdev)
|
||||
for _frame in WARMUP_FRAMES:
|
||||
await process_frame
|
||||
|
||||
@@ -36,16 +39,40 @@ func _run() -> void:
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var error := image.save_png(OUTPUT_PATH)
|
||||
var error := image.save_png(output_path)
|
||||
if error != OK:
|
||||
push_error("Could not save %s: %s" % [OUTPUT_PATH, error_string(error)])
|
||||
push_error("Could not save %s: %s" % [output_path, error_string(error)])
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("[TOOL] Saved %s | %s" % [OUTPUT_PATH, analysis])
|
||||
print("[TOOL] Saved %s | %s" % [output_path, analysis])
|
||||
quit(0)
|
||||
|
||||
|
||||
func _stabilize_lookdev(lookdev: Node3D) -> void:
|
||||
var camera := root.get_camera_3d()
|
||||
if camera != null:
|
||||
camera.set_process(false)
|
||||
var simulation_manager := lookdev.get_node_or_null("SimulationManager")
|
||||
if simulation_manager != null:
|
||||
simulation_manager.set_process(false)
|
||||
var day_night_cycle := lookdev.get_node_or_null("JajceWorld/DayNightCycle")
|
||||
if day_night_cycle == null:
|
||||
return
|
||||
day_night_cycle.set_process(false)
|
||||
day_night_cycle.call("_apply_light_rotation", 0.4)
|
||||
day_night_cycle.call("_apply_environment", 0.4)
|
||||
|
||||
|
||||
func _get_argument_value(prefix: String, default_value: String) -> String:
|
||||
for argument in OS.get_cmdline_user_args():
|
||||
if argument.begins_with(prefix):
|
||||
var value := argument.trim_prefix(prefix)
|
||||
if not value.is_empty():
|
||||
return value
|
||||
return default_value
|
||||
|
||||
|
||||
func _analyze_image(image: Image) -> Dictionary:
|
||||
var min_luma := INF
|
||||
var max_luma := -INF
|
||||
|
||||
@@ -1,33 +1,83 @@
|
||||
[gd_scene load_steps=5 format=3]
|
||||
[gd_scene load_steps=9 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_deck"]
|
||||
albedo_color = Color(0.62, 0.55, 0.44, 1)
|
||||
roughness = 0.8
|
||||
albedo_color = Color(0.61, 0.53, 0.42, 1)
|
||||
roughness = 0.94
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_deck"]
|
||||
material = SubResource("Material_deck")
|
||||
size = Vector3(8, 0.2, 3)
|
||||
size = Vector3(15.2, 0.32, 3.15)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_support"]
|
||||
albedo_color = Color(0.45, 0.42, 0.38, 1)
|
||||
roughness = 0.85
|
||||
[sub_resource type="StandardMaterial3D" id="Material_stone"]
|
||||
albedo_color = Color(0.48, 0.47, 0.41, 1)
|
||||
roughness = 0.98
|
||||
|
||||
[sub_resource type="CylinderMesh" id="Mesh_support"]
|
||||
material = SubResource("Material_support")
|
||||
top_radius = 0.2
|
||||
bottom_radius = 0.2
|
||||
height = 1.0
|
||||
[sub_resource type="BoxMesh" id="Mesh_abutment"]
|
||||
material = SubResource("Material_stone")
|
||||
size = Vector3(2.25, 1.45, 4.2)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_pier"]
|
||||
material = SubResource("Material_stone")
|
||||
size = Vector3(1.05, 1.65, 3.7)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_rail"]
|
||||
albedo_color = Color(0.2, 0.115, 0.055, 1)
|
||||
roughness = 0.92
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_rail"]
|
||||
material = SubResource("Material_rail")
|
||||
size = Vector3(15.0, 0.16, 0.16)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_post"]
|
||||
material = SubResource("Material_rail")
|
||||
size = Vector3(0.18, 1.25, 0.18)
|
||||
|
||||
[node name="BridgeBlockout" type="Node3D"]
|
||||
|
||||
[node name="Deck" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 0.6, 0)
|
||||
position = Vector3(0, 1.08, 0)
|
||||
mesh = SubResource("Mesh_deck")
|
||||
|
||||
[node name="SupportL" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-3.2, 0, 0)
|
||||
mesh = SubResource("Mesh_support")
|
||||
[node name="WestAbutment" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-6.75, 0.15, 0)
|
||||
mesh = SubResource("Mesh_abutment")
|
||||
|
||||
[node name="SupportR" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(3.2, 0, 0)
|
||||
mesh = SubResource("Mesh_support")
|
||||
[node name="EastAbutment" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(6.75, 0.15, 0)
|
||||
mesh = SubResource("Mesh_abutment")
|
||||
|
||||
[node name="CenterPier" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 0.05, 0)
|
||||
mesh = SubResource("Mesh_pier")
|
||||
|
||||
[node name="NorthRail" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 1.76, -1.45)
|
||||
mesh = SubResource("Mesh_rail")
|
||||
|
||||
[node name="SouthRail" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 1.76, 1.45)
|
||||
mesh = SubResource("Mesh_rail")
|
||||
|
||||
[node name="PostNorthWest" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-6.1, 1.48, -1.45)
|
||||
mesh = SubResource("Mesh_post")
|
||||
|
||||
[node name="PostNorthCenter" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 1.48, -1.45)
|
||||
mesh = SubResource("Mesh_post")
|
||||
|
||||
[node name="PostNorthEast" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(6.1, 1.48, -1.45)
|
||||
mesh = SubResource("Mesh_post")
|
||||
|
||||
[node name="PostSouthWest" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-6.1, 1.48, 1.45)
|
||||
mesh = SubResource("Mesh_post")
|
||||
|
||||
[node name="PostSouthCenter" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 1.48, 1.45)
|
||||
mesh = SubResource("Mesh_post")
|
||||
|
||||
[node name="PostSouthEast" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(6.1, 1.48, 1.45)
|
||||
mesh = SubResource("Mesh_post")
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
extends Node3D
|
||||
|
||||
@export var orbit_radius := 1.2
|
||||
@export var orbit_speed := 0.45
|
||||
@export var bob_height := 0.35
|
||||
@export var phase := 0.0
|
||||
|
||||
@onready var left_wing: MeshInstance3D = $LeftWing
|
||||
@onready var right_wing: MeshInstance3D = $RightWing
|
||||
|
||||
var _center := Vector3.ZERO
|
||||
var _elapsed := 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_center = position
|
||||
_elapsed = phase
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_elapsed += delta
|
||||
var angle := _elapsed * orbit_speed
|
||||
position = (
|
||||
_center
|
||||
+ Vector3(
|
||||
cos(angle) * orbit_radius,
|
||||
sin(_elapsed * 1.7) * bob_height,
|
||||
sin(angle) * orbit_radius * 0.65
|
||||
)
|
||||
)
|
||||
rotation.y = -angle + PI * 0.5
|
||||
var flap := 22.0 + sin(_elapsed * 10.0) * 34.0
|
||||
left_wing.rotation_degrees.z = flap
|
||||
right_wing.rotation_degrees.z = -flap
|
||||
@@ -0,0 +1 @@
|
||||
uid://di3mr7j4bdna3
|
||||
@@ -0,0 +1,45 @@
|
||||
[gd_scene load_steps=6 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/jajce/CozyButterfly.gd" id="1_script"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_body"]
|
||||
albedo_color = Color(0.18, 0.1, 0.06, 1)
|
||||
roughness = 0.85
|
||||
|
||||
[sub_resource type="CapsuleMesh" id="Mesh_body"]
|
||||
material = SubResource("Material_body")
|
||||
radius = 0.08
|
||||
height = 0.42
|
||||
radial_segments = 8
|
||||
rings = 3
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_wing"]
|
||||
albedo_color = Color(0.94, 0.53, 0.19, 1)
|
||||
roughness = 0.72
|
||||
emission_enabled = true
|
||||
emission = Color(0.32, 0.08, 0.01, 1)
|
||||
emission_energy_multiplier = 0.18
|
||||
|
||||
[sub_resource type="SphereMesh" id="Mesh_wing"]
|
||||
material = SubResource("Material_wing")
|
||||
radius = 0.42
|
||||
height = 0.7
|
||||
radial_segments = 12
|
||||
rings = 6
|
||||
|
||||
[node name="CozyButterfly" type="Node3D"]
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="Body" type="MeshInstance3D" parent="."]
|
||||
rotation_degrees = Vector3(90, 0, 0)
|
||||
mesh = SubResource("Mesh_body")
|
||||
|
||||
[node name="LeftWing" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-0.3, 0, 0)
|
||||
scale = Vector3(1, 0.12, 0.72)
|
||||
mesh = SubResource("Mesh_wing")
|
||||
|
||||
[node name="RightWing" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0.3, 0, 0)
|
||||
scale = Vector3(1, 0.12, 0.72)
|
||||
mesh = SubResource("Mesh_wing")
|
||||
@@ -1,119 +1,238 @@
|
||||
[gd_scene load_steps=12 format=3]
|
||||
[gd_scene load_steps=19 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_keep"]
|
||||
albedo_color = Color(0.45, 0.42, 0.38, 1)
|
||||
roughness = 0.85
|
||||
[sub_resource type="StandardMaterial3D" id="Material_rock"]
|
||||
albedo_color = Color(0.25, 0.27, 0.24, 1)
|
||||
roughness = 1.0
|
||||
|
||||
[sub_resource type="CylinderMesh" id="Mesh_rock_plinth"]
|
||||
material = SubResource("Material_rock")
|
||||
top_radius = 8.4
|
||||
bottom_radius = 10.2
|
||||
height = 3.2
|
||||
radial_segments = 9
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_stone"]
|
||||
albedo_color = Color(0.47, 0.47, 0.43, 1)
|
||||
roughness = 0.96
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_stone_light"]
|
||||
albedo_color = Color(0.57, 0.56, 0.51, 1)
|
||||
roughness = 0.95
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_keep"]
|
||||
material = SubResource("Material_keep")
|
||||
size = Vector3(7, 6, 9)
|
||||
material = SubResource("Material_stone_light")
|
||||
size = Vector3(7.4, 7.8, 8.6)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_parapet"]
|
||||
albedo_color = Color(0.42, 0.39, 0.35, 1)
|
||||
roughness = 0.85
|
||||
[sub_resource type="BoxMesh" id="Mesh_wall_long"]
|
||||
material = SubResource("Material_stone")
|
||||
size = Vector3(13.8, 3.3, 1.15)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_parapet"]
|
||||
material = SubResource("Material_parapet")
|
||||
size = Vector3(6.6, 1.5, 7.8)
|
||||
[sub_resource type="BoxMesh" id="Mesh_wall_short"]
|
||||
material = SubResource("Material_stone")
|
||||
size = Vector3(1.15, 3.3, 11.8)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_turret"]
|
||||
albedo_color = Color(0.45, 0.42, 0.38, 1)
|
||||
roughness = 0.85
|
||||
[sub_resource type="CylinderMesh" id="Mesh_tower"]
|
||||
material = SubResource("Material_stone_light")
|
||||
top_radius = 1.65
|
||||
bottom_radius = 1.95
|
||||
height = 6.0
|
||||
radial_segments = 10
|
||||
|
||||
[sub_resource type="CylinderMesh" id="Mesh_turret"]
|
||||
material = SubResource("Material_turret")
|
||||
top_radius = 0.6
|
||||
bottom_radius = 0.7
|
||||
height = 4.0
|
||||
[sub_resource type="CylinderMesh" id="Mesh_tower_cap"]
|
||||
material = SubResource("Material_stone")
|
||||
top_radius = 1.9
|
||||
bottom_radius = 1.9
|
||||
height = 0.65
|
||||
radial_segments = 10
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_crenellation"]
|
||||
material = SubResource("Material_parapet")
|
||||
size = Vector3(0.55, 0.9, 0.7)
|
||||
material = SubResource("Material_stone_light")
|
||||
size = Vector3(0.72, 0.92, 0.82)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_pole"]
|
||||
albedo_color = Color(0.24, 0.13, 0.07, 1)
|
||||
roughness = 0.82
|
||||
[sub_resource type="BoxMesh" id="Mesh_gate_post"]
|
||||
material = SubResource("Material_stone_light")
|
||||
size = Vector3(1.25, 3.7, 1.7)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_gate_lintel"]
|
||||
material = SubResource("Material_stone_light")
|
||||
size = Vector3(4.6, 1.05, 1.7)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_gate"]
|
||||
albedo_color = Color(0.18, 0.11, 0.065, 1)
|
||||
roughness = 0.9
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_gate"]
|
||||
material = SubResource("Material_gate")
|
||||
size = Vector3(2.2, 2.8, 0.22)
|
||||
|
||||
[sub_resource type="CylinderMesh" id="Mesh_pole"]
|
||||
material = SubResource("Material_pole")
|
||||
top_radius = 0.08
|
||||
bottom_radius = 0.1
|
||||
height = 3.5
|
||||
material = SubResource("Material_gate")
|
||||
top_radius = 0.07
|
||||
bottom_radius = 0.09
|
||||
height = 4.2
|
||||
radial_segments = 8
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_banner"]
|
||||
albedo_color = Color(0.68, 0.12, 0.1, 1)
|
||||
roughness = 0.65
|
||||
albedo_color = Color(0.58, 0.1, 0.08, 1)
|
||||
roughness = 0.76
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_banner"]
|
||||
material = SubResource("Material_banner")
|
||||
size = Vector3(0.14, 2.0, 1.25)
|
||||
size = Vector3(0.12, 2.2, 1.35)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_window_slit"]
|
||||
albedo_color = Color(0.12, 0.14, 0.13, 1)
|
||||
roughness = 0.98
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_window_slit"]
|
||||
material = SubResource("Material_window_slit")
|
||||
size = Vector3(0.38, 1.25, 0.14)
|
||||
|
||||
[node name="FortressBlockout" type="Node3D"]
|
||||
|
||||
[node name="Keep" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 3, 0)
|
||||
[node name="RockyCitadelHill" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 1.35, 0)
|
||||
scale = Vector3(1.22, 1, 1)
|
||||
mesh = SubResource("Mesh_rock_plinth")
|
||||
|
||||
[node name="CitadelKeep" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-0.8, 7.05, 0.35)
|
||||
rotation_degrees = Vector3(0, -4, 0)
|
||||
mesh = SubResource("Mesh_keep")
|
||||
|
||||
[node name="Parapet" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 6.75, 0)
|
||||
mesh = SubResource("Mesh_parapet")
|
||||
[node name="KeepWindowSouthLeft" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-2.55, 7.0, 4.72)
|
||||
rotation_degrees = Vector3(0, -4, 0)
|
||||
mesh = SubResource("Mesh_window_slit")
|
||||
|
||||
[node name="Crenellation_01" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-2.8, 7.7, -3.1)
|
||||
[node name="KeepWindowSouthRight" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0.85, 7.0, 4.72)
|
||||
rotation_degrees = Vector3(0, -4, 0)
|
||||
mesh = SubResource("Mesh_window_slit")
|
||||
|
||||
[node name="KeepWindowEast" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(2.95, 7.45, 1.45)
|
||||
rotation_degrees = Vector3(0, 86, 0)
|
||||
mesh = SubResource("Mesh_window_slit")
|
||||
|
||||
[node name="NorthRampart" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 4.05, -5.55)
|
||||
rotation_degrees = Vector3(0, -2, 0)
|
||||
mesh = SubResource("Mesh_wall_long")
|
||||
|
||||
[node name="SouthRampart" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-1.1, 4.0, 5.75)
|
||||
rotation_degrees = Vector3(0, 4, 0)
|
||||
mesh = SubResource("Mesh_wall_long")
|
||||
|
||||
[node name="WestRampart" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-6.55, 4.0, 0)
|
||||
mesh = SubResource("Mesh_wall_short")
|
||||
|
||||
[node name="EastRampart" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(6.55, 4.0, 0)
|
||||
mesh = SubResource("Mesh_wall_short")
|
||||
|
||||
[node name="DescendingWallEast" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(9.5, 2.8, 2.7)
|
||||
rotation_degrees = Vector3(0, 23, -7)
|
||||
scale = Vector3(0.65, 0.7, 1)
|
||||
mesh = SubResource("Mesh_wall_short")
|
||||
|
||||
[node name="DescendingWallSouth" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(4.8, 2.7, 8.1)
|
||||
rotation_degrees = Vector3(0, 38, 5)
|
||||
scale = Vector3(0.72, 0.7, 1)
|
||||
mesh = SubResource("Mesh_wall_short")
|
||||
|
||||
[node name="TowerNorthWest" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-6.5, 4.2, -5.5)
|
||||
mesh = SubResource("Mesh_tower")
|
||||
|
||||
[node name="TowerNorthWestCap" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-6.5, 7.28, -5.5)
|
||||
mesh = SubResource("Mesh_tower_cap")
|
||||
|
||||
[node name="TowerNorthEast" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(6.5, 4.2, -5.5)
|
||||
mesh = SubResource("Mesh_tower")
|
||||
|
||||
[node name="TowerNorthEastCap" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(6.5, 7.28, -5.5)
|
||||
mesh = SubResource("Mesh_tower_cap")
|
||||
|
||||
[node name="TowerSouthWest" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-6.5, 4.2, 5.5)
|
||||
mesh = SubResource("Mesh_tower")
|
||||
|
||||
[node name="TowerSouthWestCap" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-6.5, 7.28, 5.5)
|
||||
mesh = SubResource("Mesh_tower_cap")
|
||||
|
||||
[node name="GatePostLeft" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(4.45, 4.1, 5.62)
|
||||
mesh = SubResource("Mesh_gate_post")
|
||||
|
||||
[node name="GatePostRight" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(7.15, 4.1, 5.62)
|
||||
mesh = SubResource("Mesh_gate_post")
|
||||
|
||||
[node name="GateLintel" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(5.8, 6.35, 5.62)
|
||||
mesh = SubResource("Mesh_gate_lintel")
|
||||
|
||||
[node name="GateDoor" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(5.8, 3.65, 6.22)
|
||||
mesh = SubResource("Mesh_gate")
|
||||
|
||||
[node name="CrenellationNorth01" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-5.2, 6.12, -5.55)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_02" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-1.4, 7.7, -3.1)
|
||||
[node name="CrenellationNorth02" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-2.6, 6.12, -5.55)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_03" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 7.7, -3.1)
|
||||
[node name="CrenellationNorth03" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 6.12, -5.55)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_04" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(1.4, 7.7, -3.1)
|
||||
[node name="CrenellationNorth04" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(2.6, 6.12, -5.55)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_05" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(2.8, 7.7, -3.1)
|
||||
[node name="CrenellationNorth05" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(5.2, 6.12, -5.55)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_06" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-2.8, 7.7, 3.1)
|
||||
[node name="CrenellationSouth01" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-5.2, 6.08, 5.75)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_07" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-1.4, 7.7, 3.1)
|
||||
[node name="CrenellationSouth02" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-2.6, 6.08, 5.75)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_08" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 7.7, 3.1)
|
||||
[node name="CrenellationSouth03" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 6.08, 5.75)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_09" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(1.4, 7.7, 3.1)
|
||||
[node name="KeepCrenellationLeft" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-3.5, 11.35, 3.8)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Crenellation_10" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(2.8, 7.7, 3.1)
|
||||
[node name="KeepCrenellationCenter" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-0.8, 11.35, 3.8)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="Turret_A" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-3.2, 2.0, -4)
|
||||
mesh = SubResource("Mesh_turret")
|
||||
|
||||
[node name="Turret_B" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(3.2, 2.0, -4)
|
||||
mesh = SubResource("Mesh_turret")
|
||||
|
||||
[node name="Turret_C" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(3.2, 2.0, 4)
|
||||
mesh = SubResource("Mesh_turret")
|
||||
[node name="KeepCrenellationRight" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(1.9, 11.35, 3.8)
|
||||
mesh = SubResource("Mesh_crenellation")
|
||||
|
||||
[node name="RidgeBannerPole" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 8.4, 3.8)
|
||||
position = Vector3(-0.8, 13.1, 3.45)
|
||||
mesh = SubResource("Mesh_pole")
|
||||
|
||||
[node name="RidgeBanner" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 9.2, 4.45)
|
||||
position = Vector3(-0.8, 13.75, 4.12)
|
||||
mesh = SubResource("Mesh_banner")
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
extends Node
|
||||
|
||||
const SHADER_INTERACTOR_LIMIT := 8
|
||||
|
||||
@export var grass_material: ShaderMaterial
|
||||
@export_range(1, SHADER_INTERACTOR_LIMIT, 1) var max_interactors := 8
|
||||
@export_range(0.02, 0.5, 0.01) var update_interval := 0.08
|
||||
|
||||
var _elapsed := 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_refresh_interactors()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_elapsed += delta
|
||||
if _elapsed < update_interval:
|
||||
return
|
||||
_elapsed = 0.0
|
||||
_refresh_interactors()
|
||||
|
||||
|
||||
func _refresh_interactors() -> void:
|
||||
if grass_material == null:
|
||||
return
|
||||
var camera := get_viewport().get_camera_3d()
|
||||
var origin := camera.global_position if camera != null else Vector3.ZERO
|
||||
var nearest: Array[Node3D] = []
|
||||
for value in get_tree().get_nodes_in_group("grass_interactors"):
|
||||
var candidate := value as Node3D
|
||||
if candidate == null:
|
||||
continue
|
||||
var insert_at := nearest.size()
|
||||
var candidate_distance := origin.distance_squared_to(candidate.global_position)
|
||||
for index in nearest.size():
|
||||
if candidate_distance < origin.distance_squared_to(nearest[index].global_position):
|
||||
insert_at = index
|
||||
break
|
||||
nearest.insert(insert_at, candidate)
|
||||
if nearest.size() > max_interactors:
|
||||
nearest.pop_back()
|
||||
|
||||
var positions := PackedVector3Array()
|
||||
positions.resize(SHADER_INTERACTOR_LIMIT)
|
||||
for index in SHADER_INTERACTOR_LIMIT:
|
||||
positions[index] = Vector3(0.0, -1000.0, 0.0)
|
||||
for index in nearest.size():
|
||||
positions[index] = nearest[index].global_position
|
||||
grass_material.set_shader_parameter("interactor_count", nearest.size())
|
||||
grass_material.set_shader_parameter("interactor_positions", positions)
|
||||
@@ -0,0 +1 @@
|
||||
uid://3py86vi5qjwy
|
||||
+123
-6
@@ -1,4 +1,4 @@
|
||||
[gd_scene load_steps=50 format=3]
|
||||
[gd_scene load_steps=57 format=3]
|
||||
|
||||
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
|
||||
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
||||
@@ -18,6 +18,13 @@
|
||||
[ext_resource type="NavigationMesh" path="res://world/jajce/JajceNavigationMesh.tres" id="16_nav"]
|
||||
[ext_resource type="PackedScene" path="res://world/jajce/vfx/WindGustField.tscn" id="17_wind_gust"]
|
||||
[ext_resource type="Script" path="res://world/storage/PantryStockVisual.gd" id="18_pantry_visual"]
|
||||
[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="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://assets/foliage/tree-high.glb" id="23_conifer_high"]
|
||||
[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"]
|
||||
|
||||
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
||||
_shader_parameters = {
|
||||
@@ -193,16 +200,16 @@ fog_height_density = 0.08
|
||||
fog_aerial_perspective = 0.35
|
||||
fog_sky_affect = 0.28
|
||||
volumetric_fog_enabled = true
|
||||
volumetric_fog_density = 0.006
|
||||
volumetric_fog_albedo = Color(0.94, 0.88, 0.75, 1)
|
||||
volumetric_fog_emission = Color(0.1, 0.07, 0.04, 1)
|
||||
volumetric_fog_emission_energy = 0.22
|
||||
volumetric_fog_density = 0.0035
|
||||
volumetric_fog_albedo = Color(0.82, 0.9, 0.82, 1)
|
||||
volumetric_fog_emission = Color(0.06, 0.09, 0.07, 1)
|
||||
volumetric_fog_emission_energy = 0.12
|
||||
volumetric_fog_length = 96.0
|
||||
volumetric_fog_detail_spread = 1.6
|
||||
adjustment_enabled = true
|
||||
adjustment_brightness = 1.08
|
||||
adjustment_contrast = 1.04
|
||||
adjustment_saturation = 1.25
|
||||
adjustment_saturation = 1.08
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_guard"]
|
||||
albedo_color = Color(0.55, 0.45, 0.35, 1)
|
||||
@@ -246,10 +253,28 @@ collision_mask = 3
|
||||
collision_mode = 1
|
||||
top_level = true
|
||||
|
||||
[node name="CozyGrassField" parent="TerrainRoot/Terrain3D" instance=ExtResource("19_terrain_particles")]
|
||||
instance_spacing = 0.703125
|
||||
cell_width = 24.0
|
||||
grid_width = 3
|
||||
rows = 34
|
||||
amount = 1156
|
||||
process_fixed_fps = 24
|
||||
process_material = ExtResource("25_grass_process")
|
||||
shadow_mode = 0
|
||||
mesh_material_override = ExtResource("20_grass_material")
|
||||
min_draw_distance = 36.0
|
||||
particle_count = 10404
|
||||
|
||||
[node name="GrassInteractionController" type="Node" parent="TerrainRoot"]
|
||||
script = ExtResource("21_grass_controller")
|
||||
grass_material = ExtResource("20_grass_material")
|
||||
|
||||
[node name="FortressBlockout" type="Node3D" parent="."]
|
||||
position = Vector3(-25, 7, 9)
|
||||
|
||||
[node name="Keep" parent="FortressBlockout" instance=ExtResource("5_fortress")]
|
||||
scale = Vector3(1.06, 1.06, 1.06)
|
||||
|
||||
[node name="WaterRoot" type="Node3D" parent="."]
|
||||
|
||||
@@ -263,22 +288,58 @@ position = Vector3(-25, 7, 9)
|
||||
|
||||
[node name="House_01" parent="VillageRoot" instance=ExtResource("6_house")]
|
||||
position = Vector3(-15, 2, 7)
|
||||
rotation_degrees = Vector3(0, 8, 0)
|
||||
|
||||
[node name="Smoke" parent="VillageRoot/House_01" instance=ExtResource("12_smoke")]
|
||||
position = Vector3(0, 4.5, 0)
|
||||
|
||||
[node name="House_02" parent="VillageRoot" instance=ExtResource("6_house")]
|
||||
position = Vector3(-8, 2, 2)
|
||||
rotation_degrees = Vector3(0, -12, 0)
|
||||
scale = Vector3(0.92, 0.92, 0.92)
|
||||
|
||||
[node name="Smoke" parent="VillageRoot/House_02" instance=ExtResource("12_smoke")]
|
||||
position = Vector3(0, 4.5, 0)
|
||||
|
||||
[node name="House_03" parent="VillageRoot" instance=ExtResource("6_house")]
|
||||
position = Vector3(-15, 2, -4)
|
||||
rotation_degrees = Vector3(0, 18, 0)
|
||||
scale = Vector3(0.88, 0.88, 0.88)
|
||||
|
||||
[node name="Smoke" parent="VillageRoot/House_03" instance=ExtResource("12_smoke")]
|
||||
position = Vector3(0, 4.5, 0)
|
||||
|
||||
[node name="House_04" parent="VillageRoot" instance=ExtResource("6_house")]
|
||||
position = Vector3(-19, 0, 15)
|
||||
rotation_degrees = Vector3(0, -19, 0)
|
||||
scale = Vector3(0.82, 0.82, 0.82)
|
||||
|
||||
[node name="Smoke" parent="VillageRoot/House_04" instance=ExtResource("12_smoke")]
|
||||
position = Vector3(0, 4.5, 0)
|
||||
|
||||
[node name="House_05" parent="VillageRoot" instance=ExtResource("6_house")]
|
||||
position = Vector3(-10, 0, 13)
|
||||
rotation_degrees = Vector3(0, 15, 0)
|
||||
scale = Vector3(0.76, 0.76, 0.76)
|
||||
|
||||
[node name="Smoke" parent="VillageRoot/House_05" instance=ExtResource("12_smoke")]
|
||||
position = Vector3(0, 4.5, 0)
|
||||
|
||||
[node name="House_06" parent="VillageRoot" instance=ExtResource("6_house")]
|
||||
position = Vector3(-7, 0, -13)
|
||||
rotation_degrees = Vector3(0, 31, 0)
|
||||
scale = Vector3(0.9, 0.9, 0.9)
|
||||
|
||||
[node name="House_07" parent="VillageRoot" instance=ExtResource("6_house")]
|
||||
position = Vector3(2, 0, 12)
|
||||
rotation_degrees = Vector3(0, -24, 0)
|
||||
scale = Vector3(0.8, 0.8, 0.8)
|
||||
|
||||
[node name="House_08" parent="VillageRoot" instance=ExtResource("6_house")]
|
||||
position = Vector3(-22, 0, -9)
|
||||
rotation_degrees = Vector3(0, 36, 0)
|
||||
scale = Vector3(0.78, 0.78, 0.78)
|
||||
|
||||
[node name="Mill" parent="VillageRoot" instance=ExtResource("7_mill")]
|
||||
position = Vector3(19, 2, 7)
|
||||
|
||||
@@ -353,6 +414,62 @@ scale = Vector3(0.9, 0.9, 0.9)
|
||||
position = Vector3(31, 0, 14)
|
||||
scale = Vector3(1.25, 1.25, 1.25)
|
||||
|
||||
[node name="Conifer_Citadel_01" parent="FoliageRoot" instance=ExtResource("23_conifer_high")]
|
||||
position = Vector3(-39, 0, 13)
|
||||
rotation_degrees = Vector3(0, 18, 0)
|
||||
scale = Vector3(2.4, 2.4, 2.4)
|
||||
|
||||
[node name="Conifer_Citadel_02" parent="FoliageRoot" instance=ExtResource("22_conifer")]
|
||||
position = Vector3(-34, 0, 22)
|
||||
rotation_degrees = Vector3(0, -35, 0)
|
||||
scale = Vector3(2.8, 2.8, 2.8)
|
||||
|
||||
[node name="Conifer_Citadel_03" parent="FoliageRoot" instance=ExtResource("23_conifer_high")]
|
||||
position = Vector3(-43, 0, 3)
|
||||
rotation_degrees = Vector3(0, 62, 0)
|
||||
scale = Vector3(2.15, 2.15, 2.15)
|
||||
|
||||
[node name="Conifer_Falls_01" parent="FoliageRoot" instance=ExtResource("23_conifer_high")]
|
||||
position = Vector3(36, 0, -17)
|
||||
rotation_degrees = Vector3(0, -18, 0)
|
||||
scale = Vector3(2.35, 2.35, 2.35)
|
||||
|
||||
[node name="Conifer_Falls_02" parent="FoliageRoot" instance=ExtResource("22_conifer")]
|
||||
position = Vector3(38, 0, -8)
|
||||
rotation_degrees = Vector3(0, 28, 0)
|
||||
scale = Vector3(2.7, 2.7, 2.7)
|
||||
|
||||
[node name="Conifer_River_01" parent="FoliageRoot" instance=ExtResource("22_conifer")]
|
||||
position = Vector3(38, 0, 20)
|
||||
rotation_degrees = Vector3(0, -51, 0)
|
||||
scale = Vector3(2.55, 2.55, 2.55)
|
||||
|
||||
[node name="Conifer_South_01" parent="FoliageRoot" instance=ExtResource("23_conifer_high")]
|
||||
position = Vector3(-21, 0, -29)
|
||||
rotation_degrees = Vector3(0, 44, 0)
|
||||
scale = Vector3(2.25, 2.25, 2.25)
|
||||
|
||||
[node name="Conifer_South_02" parent="FoliageRoot" instance=ExtResource("22_conifer")]
|
||||
position = Vector3(8, 0, -31)
|
||||
rotation_degrees = Vector3(0, -20, 0)
|
||||
scale = Vector3(2.65, 2.65, 2.65)
|
||||
|
||||
[node name="WildlifeRoot" type="Node3D" parent="."]
|
||||
|
||||
[node name="Butterfly_Meadow_01" parent="WildlifeRoot" instance=ExtResource("24_butterfly")]
|
||||
position = Vector3(-3, 3.1, -5)
|
||||
phase = 0.4
|
||||
|
||||
[node name="Butterfly_Houses_01" parent="WildlifeRoot" instance=ExtResource("24_butterfly")]
|
||||
position = Vector3(-11, 4.2, 8)
|
||||
scale = Vector3(0.9, 0.9, 0.9)
|
||||
phase = 2.3
|
||||
|
||||
[node name="Butterfly_River_01" parent="WildlifeRoot" instance=ExtResource("24_butterfly")]
|
||||
position = Vector3(10, 3.6, 5)
|
||||
scale = Vector3(1.12, 1.12, 1.12)
|
||||
phase = 4.4
|
||||
|
||||
[node name="WorldObjects" type="Node3D" parent="."]
|
||||
|
||||
[node name="ResourceNodes" type="Node3D" parent="WorldObjects"]
|
||||
|
||||
@@ -1,40 +1,116 @@
|
||||
[gd_scene load_steps=7 format=3]
|
||||
[gd_scene load_steps=15 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_foundation"]
|
||||
albedo_color = Color(0.43, 0.42, 0.36, 1)
|
||||
roughness = 0.98
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_foundation"]
|
||||
material = SubResource("Material_foundation")
|
||||
size = Vector3(4.35, 0.65, 5.35)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_walls"]
|
||||
albedo_color = Color(0.72, 0.65, 0.52, 1)
|
||||
roughness = 0.85
|
||||
albedo_color = Color(0.78, 0.7, 0.56, 1)
|
||||
roughness = 0.94
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_walls"]
|
||||
material = SubResource("Material_walls")
|
||||
size = Vector3(4, 2.5, 5)
|
||||
size = Vector3(4, 2.75, 5)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_roof"]
|
||||
albedo_color = Color(0.55, 0.30, 0.18, 1)
|
||||
roughness = 0.75
|
||||
albedo_color = Color(0.48, 0.2, 0.12, 1)
|
||||
roughness = 0.9
|
||||
|
||||
[sub_resource type="PrismMesh" id="Mesh_roof"]
|
||||
material = SubResource("Material_roof")
|
||||
size = Vector3(4.2, 1.5, 5.2)
|
||||
size = Vector3(4.65, 1.7, 5.65)
|
||||
left_to_right = 0.5
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_door"]
|
||||
albedo_color = Color(0.20, 0.12, 0.06, 1)
|
||||
roughness = 0.7
|
||||
[sub_resource type="StandardMaterial3D" id="Material_timber"]
|
||||
albedo_color = Color(0.2, 0.105, 0.05, 1)
|
||||
roughness = 0.92
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_beam_vertical"]
|
||||
material = SubResource("Material_timber")
|
||||
size = Vector3(0.17, 2.7, 0.16)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_beam_horizontal"]
|
||||
material = SubResource("Material_timber")
|
||||
size = Vector3(4.15, 0.16, 0.16)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_door"]
|
||||
material = SubResource("Material_door")
|
||||
size = Vector3(0.8, 1.4, 0.1)
|
||||
material = SubResource("Material_timber")
|
||||
size = Vector3(0.92, 1.65, 0.14)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_window"]
|
||||
albedo_color = Color(0.9, 0.62, 0.25, 1)
|
||||
roughness = 0.55
|
||||
emission_enabled = true
|
||||
emission = Color(0.72, 0.31, 0.08, 1)
|
||||
emission_energy_multiplier = 0.35
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_window"]
|
||||
material = SubResource("Material_window")
|
||||
size = Vector3(0.72, 0.72, 0.12)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_window_cross_vertical"]
|
||||
material = SubResource("Material_timber")
|
||||
size = Vector3(0.08, 0.82, 0.08)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_window_cross_horizontal"]
|
||||
material = SubResource("Material_timber")
|
||||
size = Vector3(0.82, 0.08, 0.08)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_chimney"]
|
||||
material = SubResource("Material_foundation")
|
||||
size = Vector3(0.62, 1.65, 0.62)
|
||||
|
||||
[node name="StylizedHouse" type="Node3D"]
|
||||
|
||||
[node name="Walls" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 1.25, 0)
|
||||
[node name="StoneFoundation" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 0.32, 0)
|
||||
mesh = SubResource("Mesh_foundation")
|
||||
|
||||
[node name="PlasterWalls" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 1.85, 0)
|
||||
mesh = SubResource("Mesh_walls")
|
||||
|
||||
[node name="Roof" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 3.25, 0)
|
||||
position = Vector3(0, 4.08, 0)
|
||||
mesh = SubResource("Mesh_roof")
|
||||
|
||||
[node name="FrontBeamLeft" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(-1.78, 1.9, 2.56)
|
||||
mesh = SubResource("Mesh_beam_vertical")
|
||||
|
||||
[node name="FrontBeamRight" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(1.78, 1.9, 2.56)
|
||||
mesh = SubResource("Mesh_beam_vertical")
|
||||
|
||||
[node name="FrontBeamTop" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 3.0, 2.56)
|
||||
mesh = SubResource("Mesh_beam_horizontal")
|
||||
|
||||
[node name="FrontBeamMiddle" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 1.62, 2.56)
|
||||
mesh = SubResource("Mesh_beam_horizontal")
|
||||
|
||||
[node name="Door" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 1.0, 2.55)
|
||||
position = Vector3(-0.72, 1.15, 2.59)
|
||||
mesh = SubResource("Mesh_door")
|
||||
|
||||
[node name="Window" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0.92, 2.1, 2.6)
|
||||
mesh = SubResource("Mesh_window")
|
||||
|
||||
[node name="WindowCrossVertical" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0.92, 2.1, 2.69)
|
||||
mesh = SubResource("Mesh_window_cross_vertical")
|
||||
|
||||
[node name="WindowCrossHorizontal" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0.92, 2.1, 2.69)
|
||||
mesh = SubResource("Mesh_window_cross_horizontal")
|
||||
|
||||
[node name="Chimney" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(1.25, 4.45, -0.65)
|
||||
rotation_degrees = Vector3(0, 0, 2)
|
||||
mesh = SubResource("Mesh_chimney")
|
||||
|
||||
+84
-20
@@ -7,9 +7,9 @@ enum CanopyVariant {
|
||||
}
|
||||
|
||||
const COLORS := {
|
||||
CanopyVariant.GREEN: Color(0.19, 0.38, 0.17),
|
||||
CanopyVariant.DARK_GREEN: Color(0.12, 0.30, 0.12),
|
||||
CanopyVariant.YELLOW_GREEN: Color(0.30, 0.42, 0.15),
|
||||
CanopyVariant.GREEN: Color(0.18, 0.39, 0.16),
|
||||
CanopyVariant.DARK_GREEN: Color(0.10, 0.28, 0.12),
|
||||
CanopyVariant.YELLOW_GREEN: Color(0.34, 0.47, 0.16),
|
||||
}
|
||||
|
||||
const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader")
|
||||
@@ -25,13 +25,14 @@ func _ready() -> void:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = hash_seed
|
||||
var canopy_variant := rng.randi() % COLORS.size() as CanopyVariant
|
||||
var canopy_radius := 1.0 + rng.randf() * 0.8
|
||||
var canopy_radius := 1.0 + rng.randf() * 0.55
|
||||
var wind_phase := rng.randf_range(0.0, TAU)
|
||||
|
||||
var trunk_mesh := CylinderMesh.new()
|
||||
trunk_mesh.top_radius = 0.22
|
||||
trunk_mesh.bottom_radius = 0.32
|
||||
trunk_mesh.height = 2.8
|
||||
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)
|
||||
@@ -41,24 +42,87 @@ func _ready() -> void:
|
||||
var trunk := MeshInstance3D.new()
|
||||
trunk.name = "Trunk"
|
||||
trunk.mesh = trunk_mesh
|
||||
trunk.position = Vector3(0, 1.4, 0)
|
||||
trunk.position = Vector3(0, 1.7, 0)
|
||||
add_child(trunk)
|
||||
|
||||
var canopy_mesh := SphereMesh.new()
|
||||
canopy_mesh.radius = canopy_radius
|
||||
canopy_mesh.height = 2.4
|
||||
_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_mat := ShaderMaterial.new()
|
||||
canopy_mat.shader = WIND_SHADER
|
||||
canopy_mat.set_shader_parameter("albedo", COLORS[canopy_variant])
|
||||
canopy_mat.set_shader_parameter("roughness", 0.9)
|
||||
canopy_mat.set_shader_parameter("wind_strength", WIND_STRENGTH)
|
||||
canopy_mat.set_shader_parameter("wind_speed", WIND_SPEED)
|
||||
canopy_mat.set_shader_parameter("wind_phase", wind_phase)
|
||||
canopy_mesh.material = canopy_mat
|
||||
var canopy_color: Color = COLORS[canopy_variant]
|
||||
var crown_height := 3.7 + (canopy_radius - 1.2) * 0.45
|
||||
_add_canopy(
|
||||
"Canopy",
|
||||
Vector3(0.0, crown_height, 0.0),
|
||||
Vector3(canopy_radius * 1.12, canopy_radius, canopy_radius),
|
||||
canopy_color,
|
||||
wind_phase
|
||||
)
|
||||
_add_canopy(
|
||||
"CanopyLeft",
|
||||
Vector3(-0.82, crown_height - 0.15, 0.1),
|
||||
Vector3(canopy_radius * 0.78, canopy_radius * 0.72, canopy_radius * 0.76),
|
||||
canopy_color.lightened(0.05),
|
||||
wind_phase + 0.12
|
||||
)
|
||||
_add_canopy(
|
||||
"CanopyRight",
|
||||
Vector3(0.78, crown_height - 0.05, -0.12),
|
||||
Vector3(canopy_radius * 0.74, canopy_radius * 0.68, canopy_radius * 0.72),
|
||||
canopy_color.darkened(0.04),
|
||||
wind_phase - 0.1
|
||||
)
|
||||
_add_canopy(
|
||||
"CanopyTop",
|
||||
Vector3(0.08, crown_height + 0.72, 0.04),
|
||||
Vector3(canopy_radius * 0.72, canopy_radius * 0.66, canopy_radius * 0.7),
|
||||
canopy_color.lightened(0.08),
|
||||
wind_phase + 0.05
|
||||
)
|
||||
|
||||
|
||||
func _add_branch(
|
||||
branch_position: Vector3,
|
||||
branch_rotation_degrees: Vector3,
|
||||
branch_height: float,
|
||||
material: StandardMaterial3D
|
||||
) -> void:
|
||||
var mesh := CylinderMesh.new()
|
||||
mesh.top_radius = 0.08
|
||||
mesh.bottom_radius = 0.14
|
||||
mesh.height = branch_height
|
||||
mesh.radial_segments = 7
|
||||
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(
|
||||
canopy_name: String,
|
||||
canopy_position: Vector3,
|
||||
canopy_scale: Vector3,
|
||||
canopy_color: Color,
|
||||
wind_phase: float
|
||||
) -> void:
|
||||
var mesh := SphereMesh.new()
|
||||
mesh.radius = 1.0
|
||||
mesh.height = 2.0
|
||||
mesh.radial_segments = 12
|
||||
mesh.rings = 6
|
||||
var material := ShaderMaterial.new()
|
||||
material.shader = WIND_SHADER
|
||||
material.set_shader_parameter("albedo", canopy_color)
|
||||
material.set_shader_parameter("roughness", 0.9)
|
||||
material.set_shader_parameter("wind_strength", WIND_STRENGTH)
|
||||
material.set_shader_parameter("wind_speed", WIND_SPEED)
|
||||
material.set_shader_parameter("wind_phase", wind_phase)
|
||||
mesh.material = material
|
||||
var canopy := MeshInstance3D.new()
|
||||
canopy.name = "Canopy"
|
||||
canopy.mesh = canopy_mesh
|
||||
canopy.position = Vector3(0, 3.2 + (canopy_radius - 1.35) * 0.5, 0)
|
||||
canopy.name = canopy_name
|
||||
canopy.mesh = mesh
|
||||
canopy.position = canopy_position
|
||||
canopy.scale = canopy_scale
|
||||
add_child(canopy)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
extends Node3D
|
||||
|
||||
@export var terrain_path := NodePath("../../TerrainRoot/Terrain3D")
|
||||
@export var river_material: Material
|
||||
@export_range(6.0, 20.0, 0.5) var river_width := 8.0
|
||||
@export_range(2, 12, 1) var width_segments := 6
|
||||
@export_range(8, 96, 1) var length_segments := 36
|
||||
@export var local_start_z := -25.0
|
||||
@export var local_end_z := 40.0
|
||||
@export_range(0.05, 0.5, 0.01) var water_offset := 0.18
|
||||
|
||||
@onready var river_ribbon: MeshInstance3D = $RiverRibbon
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
call_deferred("_build_terrain_ribbon")
|
||||
|
||||
|
||||
func _build_terrain_ribbon() -> void:
|
||||
var terrain := get_node_or_null(terrain_path) as Terrain3D
|
||||
if terrain == null or terrain.data == null or river_material == null:
|
||||
push_warning("TerrainRiverRibbon: terrain or material is unavailable")
|
||||
return
|
||||
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var uvs := PackedVector2Array()
|
||||
var indices := PackedInt32Array()
|
||||
var row_size := width_segments + 1
|
||||
for z_index in length_segments + 1:
|
||||
var progress := float(z_index) / float(length_segments)
|
||||
var local_z := lerpf(local_start_z, local_end_z, progress)
|
||||
var downstream_curve := smoothstep(0.58, 1.0, progress) * 12.0
|
||||
var center_offset := sin(progress * TAU) * 1.4 + downstream_curve
|
||||
var downstream_taper := lerpf(1.0, 0.45, smoothstep(0.65, 1.0, progress))
|
||||
var half_width := river_width * lerpf(0.52, 0.45, progress) * downstream_taper
|
||||
for x_index in width_segments + 1:
|
||||
var across := float(x_index) / float(width_segments)
|
||||
var local_x := center_offset + lerpf(-half_width, half_width, across)
|
||||
var world_sample := to_global(Vector3(local_x, 0.0, local_z))
|
||||
var terrain_height := terrain.data.get_height(world_sample)
|
||||
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)
|
||||
uvs.append(Vector2(across, progress))
|
||||
|
||||
for z_index in length_segments:
|
||||
for x_index in width_segments:
|
||||
var top_left := z_index * row_size + x_index
|
||||
var top_right := top_left + 1
|
||||
var bottom_left := top_left + row_size
|
||||
var bottom_right := bottom_left + 1
|
||||
indices.append_array(
|
||||
[top_left, bottom_left, top_right, top_right, bottom_left, bottom_right]
|
||||
)
|
||||
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
||||
arrays[Mesh.ARRAY_INDEX] = indices
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
mesh.surface_set_material(0, river_material)
|
||||
river_ribbon.mesh = mesh
|
||||
_align_pool_to_terrain(terrain)
|
||||
|
||||
|
||||
func _align_pool_to_terrain(terrain: Terrain3D) -> void:
|
||||
var pool := $PlungePool as MeshInstance3D
|
||||
var pool_sample := to_global(Vector3(pool.position.x, 0.0, pool.position.z))
|
||||
var pool_height := terrain.data.get_height(pool_sample)
|
||||
if is_nan(pool_height):
|
||||
return
|
||||
var local_surface := to_local(Vector3(pool_sample.x, pool_height + water_offset, pool_sample.z))
|
||||
for node_name in [&"PlungePool", &"FoamRing", &"FoamAtFall"]:
|
||||
var surface := get_node(NodePath(node_name)) as MeshInstance3D
|
||||
surface.position.y = local_surface.y
|
||||
@@ -0,0 +1 @@
|
||||
uid://gri4c7mt0rmo
|
||||
@@ -1,16 +1,61 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
[gd_scene load_steps=8 format=3]
|
||||
|
||||
[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"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_water"]
|
||||
shader = ExtResource("1_water_shader")
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_water_body"]
|
||||
[sub_resource type="CylinderMesh" id="Mesh_plunge_pool"]
|
||||
material = SubResource("ShaderMaterial_water")
|
||||
size = Vector3(8, 0.08, 58)
|
||||
top_radius = 7.6
|
||||
bottom_radius = 7.6
|
||||
height = 0.08
|
||||
radial_segments = 32
|
||||
rings = 2
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_foam"]
|
||||
transparency = 1
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.8, 0.93, 0.86, 0.46)
|
||||
emission_enabled = true
|
||||
emission = Color(0.34, 0.56, 0.48, 1)
|
||||
emission_energy_multiplier = 0.18
|
||||
roughness = 0.74
|
||||
|
||||
[sub_resource type="TorusMesh" id="Mesh_foam_ring"]
|
||||
material = SubResource("Material_foam")
|
||||
inner_radius = 4.8
|
||||
outer_radius = 5.65
|
||||
rings = 28
|
||||
ring_segments = 10
|
||||
|
||||
[sub_resource type="PlaneMesh" id="Mesh_foam_patch"]
|
||||
material = SubResource("Material_foam")
|
||||
size = Vector2(10.5, 2.2)
|
||||
subdivide_width = 8
|
||||
subdivide_depth = 2
|
||||
|
||||
[node name="WaterBody" type="Node3D"]
|
||||
position = Vector3(25, 0.08, 0)
|
||||
position = Vector3(25, 0.1, 1)
|
||||
script = ExtResource("2_river_script")
|
||||
river_material = SubResource("ShaderMaterial_water")
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("Mesh_water_body")
|
||||
[node name="RiverRibbon" type="MeshInstance3D" parent="."]
|
||||
cast_shadow = 0
|
||||
|
||||
[node name="PlungePool" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, -0.02, -25)
|
||||
scale = Vector3(1.25, 1, 0.82)
|
||||
mesh = SubResource("Mesh_plunge_pool")
|
||||
|
||||
[node name="FoamRing" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 0.09, -25)
|
||||
scale = Vector3(1.25, 0.12, 0.82)
|
||||
cast_shadow = 0
|
||||
mesh = SubResource("Mesh_foam_ring")
|
||||
|
||||
[node name="FoamAtFall" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 0.12, -25.8)
|
||||
cast_shadow = 0
|
||||
mesh = SubResource("Mesh_foam_patch")
|
||||
|
||||
@@ -1,16 +1,81 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
[gd_scene load_steps=10 format=3]
|
||||
|
||||
[ext_resource type="Shader" path="res://world/jajce/materials/waterfall.gdshader" id="1_waterfall_shader"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_waterfall"]
|
||||
shader = ExtResource("1_waterfall_shader")
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_waterfall_body"]
|
||||
[sub_resource type="QuadMesh" id="Mesh_waterfall_body"]
|
||||
material = SubResource("ShaderMaterial_waterfall")
|
||||
size = Vector3(8, 12, 0.5)
|
||||
size = Vector2(10.8, 14)
|
||||
subdivide_width = 18
|
||||
subdivide_depth = 28
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_rock_dark"]
|
||||
albedo_color = Color(0.32, 0.36, 0.3, 1)
|
||||
roughness = 1.0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_rock_moss"]
|
||||
albedo_color = Color(0.3, 0.4, 0.23, 1)
|
||||
roughness = 1.0
|
||||
|
||||
[sub_resource type="CylinderMesh" id="Mesh_cliff_side"]
|
||||
material = SubResource("Material_rock_dark")
|
||||
top_radius = 2.05
|
||||
bottom_radius = 3.15
|
||||
height = 14.5
|
||||
radial_segments = 7
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_rock_lobe"]
|
||||
material = SubResource("Material_rock_dark")
|
||||
size = Vector3(4.8, 5.5, 3.4)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_lip"]
|
||||
material = SubResource("Material_rock_moss")
|
||||
size = Vector3(11.6, 0.7, 3.2)
|
||||
|
||||
[node name="WaterfallBody" type="Node3D"]
|
||||
position = Vector3(25, 6, -24)
|
||||
position = Vector3(25, 8.9, -24)
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="."]
|
||||
[node name="RockFrame" type="Node3D" parent="."]
|
||||
|
||||
[node name="WestCliff" type="MeshInstance3D" parent="RockFrame"]
|
||||
position = Vector3(-7.2, -0.45, -1)
|
||||
rotation_degrees = Vector3(0, 8, -3)
|
||||
mesh = SubResource("Mesh_cliff_side")
|
||||
|
||||
[node name="EastCliff" type="MeshInstance3D" parent="RockFrame"]
|
||||
position = Vector3(7.2, -0.25, -1.2)
|
||||
rotation_degrees = Vector3(0, -7, 4)
|
||||
mesh = SubResource("Mesh_cliff_side")
|
||||
|
||||
[node name="WestCrownRock" type="MeshInstance3D" parent="RockFrame"]
|
||||
position = Vector3(-5.0, 7.45, -1.25)
|
||||
rotation_degrees = Vector3(8, -14, -11)
|
||||
scale = Vector3(0.85, 0.62, 0.9)
|
||||
mesh = SubResource("Mesh_rock_lobe")
|
||||
|
||||
[node name="EastCrownRock" type="MeshInstance3D" parent="RockFrame"]
|
||||
position = Vector3(5.1, 7.55, -1.4)
|
||||
rotation_degrees = Vector3(-6, 12, 9)
|
||||
scale = Vector3(0.82, 0.6, 0.86)
|
||||
mesh = SubResource("Mesh_rock_lobe")
|
||||
|
||||
[node name="WestLowerRock" type="MeshInstance3D" parent="RockFrame"]
|
||||
position = Vector3(-5.9, -4.8, 0.1)
|
||||
rotation_degrees = Vector3(7, 14, 12)
|
||||
mesh = SubResource("Mesh_rock_lobe")
|
||||
|
||||
[node name="EastLowerRock" type="MeshInstance3D" parent="RockFrame"]
|
||||
position = Vector3(6.0, -4.4, 0)
|
||||
rotation_degrees = Vector3(-4, -12, -9)
|
||||
mesh = SubResource("Mesh_rock_lobe")
|
||||
|
||||
[node name="RiverLip" type="MeshInstance3D" parent="RockFrame"]
|
||||
position = Vector3(0, 7.05, 0.2)
|
||||
mesh = SubResource("Mesh_lip")
|
||||
|
||||
[node name="FallingWater" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 0, 1.05)
|
||||
cast_shadow = 0
|
||||
mesh = SubResource("Mesh_waterfall_body")
|
||||
|
||||
@@ -10,7 +10,7 @@ scale_min = 0.08
|
||||
scale_max = 2.0
|
||||
color = Color(0.82, 0.94, 0.96, 0.35)
|
||||
emission_shape = 1
|
||||
emission_box_extents = Vector3(4.5, 1.5, 1.0)
|
||||
emission_box_extents = Vector3(5.8, 1.5, 1.35)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_mist"]
|
||||
albedo_color = Color(0.82, 0.94, 0.96, 0.28)
|
||||
@@ -24,9 +24,9 @@ material = SubResource("Material_mist")
|
||||
size = Vector2(1.8, 1.8)
|
||||
|
||||
[node name="WaterfallMist" type="GPUParticles3D"]
|
||||
position = Vector3(25, 0.8, -24)
|
||||
position = Vector3(25, 2.05, -24)
|
||||
emitting = true
|
||||
amount = 72
|
||||
amount = 96
|
||||
lifetime = 3.0
|
||||
one_shot = false
|
||||
preprocess = 1.5
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Adapted for The Steward from the bundled Terrain3D particle-grass example.
|
||||
shader_type spatial;
|
||||
|
||||
render_mode skip_vertex_transform, cull_disabled, blend_mix;
|
||||
|
||||
const int MAX_INTERACTORS = 8;
|
||||
|
||||
uniform vec2 wind_direction = vec2(1.0, 0.7);
|
||||
uniform vec3 base_color : source_color = vec3(0.16, 0.31, 0.08);
|
||||
uniform vec3 tip_color : source_color = vec3(0.54, 0.68, 0.24);
|
||||
uniform float wind_amount : hint_range(0.0, 1.0) = 0.34;
|
||||
uniform int interactor_count : hint_range(0, 8) = 0;
|
||||
uniform vec3 interactor_positions[MAX_INTERACTORS];
|
||||
uniform float interaction_radius : hint_range(0.25, 4.0) = 1.8;
|
||||
uniform float interaction_strength : hint_range(0.0, 2.0) = 0.75;
|
||||
|
||||
varying vec4 grass_data;
|
||||
varying float pressed_weight;
|
||||
|
||||
|
||||
void vertex() {
|
||||
grass_data[2] = VERTEX.y + 0.55;
|
||||
grass_data[2] *= grass_data[2];
|
||||
VERTEX.xz *= 1.0 - grass_data[2];
|
||||
COLOR = mix(COLOR, vec4(1.0), smoothstep(0.9, 1.0, grass_data[2]));
|
||||
COLOR *= INSTANCE_CUSTOM[3] < 0.35 ? 1.0 : mix(1.0, 0.75, smoothstep(0.35, 0.0, grass_data[2]));
|
||||
grass_data.rg = INSTANCE_CUSTOM.rg;
|
||||
|
||||
vec3 world_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
float scale = pow(INSTANCE_CUSTOM[3] * INSTANCE_CUSTOM[3], 0.707);
|
||||
float force = INSTANCE_CUSTOM[2] * grass_data[2] * scale * wind_amount;
|
||||
force -= fract(force * 256.0) * force * 0.05;
|
||||
force = pow(max(force, 0.0), 0.707);
|
||||
float lateral_wobble = sin(TIME * 2.0 * (1.0 + grass_data.r + grass_data.g)) * 0.2 * (1.0 - INSTANCE_CUSTOM[2]);
|
||||
vec2 direction = normalize(wind_direction);
|
||||
world_vertex.xz -= (vec2(-direction.y, direction.x) * lateral_wobble + direction) * force;
|
||||
world_vertex.y -= INSTANCE_CUSTOM[2] * force * grass_data[2];
|
||||
grass_data[3] = force;
|
||||
|
||||
pressed_weight = 0.0;
|
||||
for (int index = 0; index < MAX_INTERACTORS; index++) {
|
||||
if (index >= interactor_count) {
|
||||
break;
|
||||
}
|
||||
vec2 offset = world_vertex.xz - interactor_positions[index].xz;
|
||||
float distance_to_actor = length(offset);
|
||||
float influence = 1.0 - smoothstep(0.25, interaction_radius, distance_to_actor);
|
||||
vec2 away = offset / max(distance_to_actor, 0.08);
|
||||
world_vertex.xz += away * influence * interaction_strength * grass_data[2];
|
||||
world_vertex.y -= influence * 0.28 * grass_data[2];
|
||||
pressed_weight = max(pressed_weight, influence);
|
||||
}
|
||||
|
||||
VERTEX = (VIEW_MATRIX * vec4(world_vertex, 1.0)).xyz;
|
||||
NORMAL = MODELVIEW_NORMAL_MATRIX * NORMAL;
|
||||
BINORMAL = MODELVIEW_NORMAL_MATRIX * BINORMAL;
|
||||
TANGENT = MODELVIEW_NORMAL_MATRIX * TANGENT;
|
||||
}
|
||||
|
||||
|
||||
void fragment() {
|
||||
vec3 color = mix(base_color, tip_color, grass_data[2] * 0.62);
|
||||
color.rg *= grass_data.rg * 0.18 + 0.91;
|
||||
color *= pow(COLOR.rgb, vec3(2.2));
|
||||
ALBEDO = mix(color, base_color * 0.72, pressed_weight * 0.42);
|
||||
float specular_roughness = clamp(max(grass_data[2], grass_data[3]), 0.0, 1.0);
|
||||
ROUGHNESS = 1.0 - specular_roughness * 0.22;
|
||||
SPECULAR = clamp(specular_roughness * 0.2, 0.0, 0.12);
|
||||
BACKLIGHT = tip_color * 0.24;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://3ayekivfakfr
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_resource type="ShaderMaterial" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Shader" path="res://world/jajce/materials/cozy_grass.gdshader" id="1_grass"]
|
||||
|
||||
[resource]
|
||||
render_priority = 0
|
||||
shader = ExtResource("1_grass")
|
||||
shader_parameter/wind_direction = Vector2(1, 0.7)
|
||||
shader_parameter/base_color = Vector3(0.16, 0.31, 0.08)
|
||||
shader_parameter/tip_color = Vector3(0.54, 0.68, 0.24)
|
||||
shader_parameter/wind_amount = 0.34
|
||||
shader_parameter/interactor_count = 0
|
||||
shader_parameter/interaction_radius = 1.8
|
||||
shader_parameter/interaction_strength = 0.75
|
||||
@@ -0,0 +1,47 @@
|
||||
[gd_resource type="ShaderMaterial" load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Shader" path="res://addons/terrain_3d/extras/particle_example/particles.gdshader" id="1_process_shader"]
|
||||
|
||||
[sub_resource type="FastNoiseLite" id="FastNoise_grass"]
|
||||
noise_type = 2
|
||||
frequency = 0.018
|
||||
cellular_return_type = 4
|
||||
|
||||
[sub_resource type="NoiseTexture2D" id="NoiseTexture_grass"]
|
||||
seamless = true
|
||||
noise = SubResource("FastNoise_grass")
|
||||
|
||||
[resource]
|
||||
shader = ExtResource("1_process_shader")
|
||||
shader_parameter/main_noise = SubResource("NoiseTexture_grass")
|
||||
shader_parameter/main_noise_scale = 0.012
|
||||
shader_parameter/position_offset = Vector3(0, 0.22, 0)
|
||||
shader_parameter/align_to_normal = true
|
||||
shader_parameter/normal_strength = 0.22
|
||||
shader_parameter/random_rotation = true
|
||||
shader_parameter/random_spacing = 0.72
|
||||
shader_parameter/min_scale = Vector3(0.09, 0.22, 0.09)
|
||||
shader_parameter/max_scale = Vector3(0.13, 0.48, 0.13)
|
||||
shader_parameter/noise_scale = 0.0041
|
||||
shader_parameter/wind_speed = 0.022
|
||||
shader_parameter/wind_strength = 0.6
|
||||
shader_parameter/wind_dithering = 4.0
|
||||
shader_parameter/wind_direction = Vector2(1, 0.7)
|
||||
shader_parameter/clod_scale_boost = 0.18
|
||||
shader_parameter/clod_min_threshold = 0.32
|
||||
shader_parameter/clod_max_threshold = 0.78
|
||||
shader_parameter/patch_min_threshold = 0.08
|
||||
shader_parameter/patch_max_threshold = 0.42
|
||||
shader_parameter/condition_dither_range = 0.18
|
||||
shader_parameter/surface_slope_min = 0.88
|
||||
shader_parameter/distance_fade_ammount = 0.7
|
||||
shader_parameter/max_dist = 1.0
|
||||
shader_parameter/camera_position = Vector3(0, 0, 0)
|
||||
shader_parameter/instance_rows = 1
|
||||
shader_parameter/instance_spacing = 0.703125
|
||||
shader_parameter/_background_mode = 0
|
||||
shader_parameter/_vertex_spacing = 1.0
|
||||
shader_parameter/_vertex_density = 1.0
|
||||
shader_parameter/_region_size = 1024.0
|
||||
shader_parameter/_region_texel_size = 0.000976563
|
||||
shader_parameter/_region_map_size = 32
|
||||
@@ -1,54 +1,42 @@
|
||||
shader_type spatial;
|
||||
render_mode blend_mix, depth_draw_opaque, cull_back, 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.5) = 0.08;
|
||||
uniform float wave_speed : hint_range(0.0, 5.0) = 0.8;
|
||||
uniform float flow_speed : hint_range(0.0, 3.0) = 0.6;
|
||||
uniform vec3 deep_color : source_color = vec3(0.06, 0.24, 0.38);
|
||||
uniform vec3 shallow_color : source_color = vec3(0.18, 0.44, 0.58);
|
||||
uniform vec3 foam_color : source_color = vec3(0.82, 0.88, 0.86);
|
||||
uniform float rim_power : hint_range(0.5, 5.0) = 2.5;
|
||||
uniform float sparkle_strength : hint_range(0.0, 1.0) = 0.12;
|
||||
uniform float wave_strength : hint_range(0.0, 0.3) = 0.055;
|
||||
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 vec3 deep_color : source_color = vec3(0.03, 0.39, 0.45);
|
||||
uniform vec3 shallow_color : source_color = vec3(0.07, 0.67, 0.64);
|
||||
uniform vec3 sunlit_color : source_color = vec3(0.55, 0.95, 0.78);
|
||||
uniform vec3 foam_color : source_color = vec3(0.82, 0.94, 0.84);
|
||||
|
||||
uniform vec3 sun_direction = vec3(0.4, 0.8, 0.4);
|
||||
|
||||
void vertex() {
|
||||
vec3 pos = VERTEX;
|
||||
float wave_a = sin(pos.x * 0.35 + pos.z * 0.25 + TIME * wave_speed) * wave_strength;
|
||||
float wave_b = cos(pos.x * 0.45 - pos.z * 0.55 + TIME * wave_speed * 0.7) * wave_strength * 0.6;
|
||||
float wave_c = sin(pos.z * 0.7 + TIME * wave_speed * 1.3) * wave_strength * 0.35;
|
||||
pos.y += wave_a + wave_b + wave_c;
|
||||
VERTEX = pos;
|
||||
float broad_wave = sin(VERTEX.x * 0.42 + VERTEX.z * 0.18 + TIME * wave_speed);
|
||||
float crossing_wave = cos(VERTEX.x * 0.7 - VERTEX.z * 0.31 + TIME * wave_speed * 0.73);
|
||||
VERTEX.y += (broad_wave + crossing_wave * 0.45) * wave_strength;
|
||||
}
|
||||
|
||||
|
||||
void fragment() {
|
||||
vec3 view_dir = VIEW;
|
||||
vec3 normal = NORMAL;
|
||||
vec3 view_norm = normalize(view_dir);
|
||||
vec2 flowing_uv = UV * vec2(7.0, 10.0);
|
||||
flowing_uv.y -= TIME * flow_speed;
|
||||
float ribbon_a = sin(flowing_uv.y + sin(flowing_uv.x * 0.72) * 1.25) * 0.5 + 0.5;
|
||||
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);
|
||||
float fresnel = pow(1.0 - clamp(dot(normalize(NORMAL), normalize(VIEW)), 0.0, 1.0), 2.4);
|
||||
float bank_distance = min(UV.x, 1.0 - UV.x);
|
||||
float bank_foam = 1.0 - smoothstep(0.0, 0.07, bank_distance);
|
||||
bank_foam *= 0.72 + sin(UV.y * 52.0 + TIME * 0.35) * 0.12;
|
||||
|
||||
float rim = 1.0 - abs(dot(normal, view_norm));
|
||||
rim = pow(rim, rim_power);
|
||||
rim = smoothstep(0.15, 0.7, rim);
|
||||
|
||||
vec2 uv = UV;
|
||||
uv.x += TIME * flow_speed * 0.4;
|
||||
uv.y += TIME * flow_speed * 0.3;
|
||||
float flow_a = sin(uv.x * 8.0 + uv.y * 5.0) * 0.5 + 0.5;
|
||||
float flow_b = cos(uv.y * 6.0 - uv.x * 4.0 + TIME * 0.25) * 0.5 + 0.5;
|
||||
float flow = mix(flow_a, flow_b, 0.5);
|
||||
|
||||
vec3 water_color = mix(deep_color, shallow_color, flow * 0.6 + 0.2);
|
||||
water_color = mix(water_color, foam_color, rim * 0.65);
|
||||
|
||||
vec3 half_vec = normalize(view_norm + normalize(sun_direction));
|
||||
float specular = pow(max(dot(normal, half_vec), 0.0), 120.0);
|
||||
specular *= smoothstep(0.4, 0.85, rim);
|
||||
float sparkle = specular * sparkle_strength;
|
||||
vec3 water_color = mix(deep_color, shallow_color, ribbon_a * 0.2 + 0.48);
|
||||
water_color = mix(water_color, sunlit_color, fresnel * 0.42);
|
||||
water_color = mix(water_color, foam_color, quiet_ripple * 0.05);
|
||||
water_color = mix(water_color, foam_color, bank_foam * 0.3);
|
||||
|
||||
ALBEDO = water_color;
|
||||
METALLIC = 0.08;
|
||||
ROUGHNESS = 0.22;
|
||||
EMISSION = water_color * 0.04 + foam_color * sparkle;
|
||||
ALPHA = 0.76;
|
||||
ALPHA_SCISSOR_THRESHOLD = 0.0;
|
||||
ROUGHNESS = mix(0.34, 0.5, ribbon_b);
|
||||
SPECULAR = 0.38;
|
||||
EMISSION = water_color * 0.12 + sunlit_color * (quiet_ripple * 0.05 + fresnel * 0.03);
|
||||
EMISSION += foam_color * bank_foam * 0.025;
|
||||
ALPHA = mix(0.94, 0.99, fresnel);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,38 @@
|
||||
shader_type spatial;
|
||||
render_mode blend_mix, depth_draw_opaque, cull_back, 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 vec3 shadow_color : source_color = vec3(0.075, 0.33, 0.34);
|
||||
uniform vec3 water_color : source_color = vec3(0.31, 0.68, 0.64);
|
||||
uniform vec3 light_color : source_color = vec3(0.78, 0.94, 0.83);
|
||||
uniform vec3 foam_color : source_color = vec3(0.91, 0.97, 0.9);
|
||||
|
||||
uniform float scroll_speed : hint_range(0.0, 5.0) = 1.8;
|
||||
uniform vec3 deep_color : source_color = vec3(0.1, 0.45, 0.55);
|
||||
uniform vec3 mid_color : source_color = vec3(0.45, 0.78, 0.82);
|
||||
uniform vec3 foam_color : source_color = vec3(0.9, 0.95, 0.95);
|
||||
uniform float foam_contrast : hint_range(0.5, 4.0) = 2.2;
|
||||
uniform float sparkle_strength : hint_range(0.0, 0.8) = 0.25;
|
||||
|
||||
void vertex() {
|
||||
float edge_weight = 1.0 - abs(UV.x * 2.0 - 1.0);
|
||||
float curtain_wobble = sin(UV.y * 13.0 - TIME * 2.2 + UV.x * 8.0) * 0.05;
|
||||
VERTEX.z += curtain_wobble * edge_weight;
|
||||
}
|
||||
|
||||
|
||||
void fragment() {
|
||||
vec2 uv = UV;
|
||||
uv.y += TIME * scroll_speed;
|
||||
vec2 flow_uv = UV;
|
||||
flow_uv.y += TIME * scroll_speed;
|
||||
float long_thread = sin(flow_uv.x * 32.0 + sin(flow_uv.y * 3.1) * 2.4) * 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 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 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 band_a = fract(uv.y * 5.0 + sin(uv.x * 2.0) * 0.15);
|
||||
band_a = smoothstep(0.2, 0.55, band_a) * smoothstep(0.8, 0.45, band_a);
|
||||
|
||||
float band_b = fract(uv.y * 3.5 + cos(uv.x * 2.5 + TIME * 0.3) * 0.12);
|
||||
band_b = smoothstep(0.1, 0.5, band_b) * smoothstep(0.85, 0.4, band_b);
|
||||
|
||||
float foam_a = fract(uv.y * 4.0);
|
||||
foam_a = pow(smoothstep(0.0, 0.12, foam_a) * smoothstep(0.3, 0.15, foam_a), foam_contrast);
|
||||
|
||||
float foam_b = fract(uv.y * 6.0 + uv.x * 1.5);
|
||||
foam_b = pow(smoothstep(0.5, 0.58, foam_b) * smoothstep(0.7, 0.6, foam_b), foam_contrast * 0.8);
|
||||
|
||||
float foam = foam_a * 0.7 + foam_b * 0.3;
|
||||
|
||||
vec3 base = mix(deep_color, mid_color, band_a * 0.5 + band_b * 0.3);
|
||||
vec3 color = mix(base, foam_color, foam);
|
||||
|
||||
float sparkle = pow(foam_a, 3.0) * sparkle_strength;
|
||||
float highlight = band_a * band_b * 0.35;
|
||||
float emission_value = sparkle + highlight * 0.3;
|
||||
vec3 color = mix(shadow_color, water_color, 0.5 + thread * 0.35);
|
||||
color = mix(color, light_color, broken_foam * 0.5);
|
||||
color = mix(color, foam_color, base_foam * (0.38 + broken_foam * 0.42));
|
||||
|
||||
ALBEDO = color;
|
||||
ALPHA = mix(0.25, 0.78, foam * 0.7 + 0.15);
|
||||
EMISSION = color * emission_value;
|
||||
METALLIC = 0.05;
|
||||
ROUGHNESS = 0.15;
|
||||
|
||||
vec3 normal = NORMAL;
|
||||
vec3 view_dir = VIEW;
|
||||
vec3 view_norm = normalize(view_dir);
|
||||
float rim = 1.0 - abs(dot(normal, view_norm));
|
||||
ALPHA = mix(ALPHA, ALPHA * 0.5, rim * 0.6);
|
||||
ROUGHNESS = mix(0.16, 0.32, broken_foam);
|
||||
SPECULAR = 0.65;
|
||||
EMISSION = light_color * (broken_foam * 0.07 + base_foam * 0.04);
|
||||
ALPHA = edge_fade * mix(0.62, 0.9, thread * 0.56 + base_foam * 0.3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user