feat(foliage): add recovering grass trails and bush contact
This commit is contained in:
@@ -49,6 +49,49 @@ values; viewport scale restoration is guarded by the current quality owner so a
|
|||||||
second world cannot clobber the active owner. Compatibility/mobile feature
|
second world cannot clobber the active owner. Compatibility/mobile feature
|
||||||
selection never forces unsupported volumetric fog on.
|
selection never forces unsupported volumetric fog on.
|
||||||
|
|
||||||
|
## Grass trails and bush contact
|
||||||
|
|
||||||
|
`GrassInteractionController` samples the existing `grass_interactors` group:
|
||||||
|
player, NPCs, hostile creatures and animals. The player keeps one slot; other
|
||||||
|
visible actors are ranked by camera distance within the loaded actor container.
|
||||||
|
High updates at 0.08 seconds with eight actors; Balanced uses 0.12 seconds and
|
||||||
|
four. Low stops updates and immediately releases both grass and bushes.
|
||||||
|
|
||||||
|
`FoliageTrailMap` maintains a disposable 128×128 RGBA float texture covering a
|
||||||
|
64 m square around the camera. Swept footprints part grass to either side and
|
||||||
|
lower its tips; the field recovers over eight seconds after contact. Sparse
|
||||||
|
history is clipped to the moving window. The 256 KiB texture is reused with
|
||||||
|
[`ImageTexture.update`](https://docs.godotengine.org/en/stable/classes/class_imagetexture.html#class-imagetexture-method-update),
|
||||||
|
at most 3.125 MiB/s of texture data at the High update rate. The vertex shader
|
||||||
|
adds one field lookup per grass vertex; there are no blade collision bodies,
|
||||||
|
per-blade scripts, render targets or added grass draw calls.
|
||||||
|
|
||||||
|
`StylizedBerryPatch` shares the field and contact positions between its leaf
|
||||||
|
and berry MultiMeshes. Both bend in world space around the planted bush root,
|
||||||
|
including under rotated/scaled parents, and spring back faster than grass.
|
||||||
|
The five village berry placeholders now use this same amount-aware presentation
|
||||||
|
as the existing riverbank bush. Contact does not change berries, resource
|
||||||
|
amounts, navigation, targets or collision geometry.
|
||||||
|
|
||||||
|
The controller clears history after `SimulationManager.state_restored`, on
|
||||||
|
quality changes and on world disposal. Missing/recreated actors, teleports over
|
||||||
|
3 m between samples and update gaps over 0.5 seconds start a new footprint rather
|
||||||
|
than a connecting trail. Actor foot height gates deformation so bridge traffic
|
||||||
|
does not flatten grass below. Trails are visual only and never enter saves.
|
||||||
|
The exported actor, primary actor, foliage and simulation paths can be rebound
|
||||||
|
when placing the controller in another loaded scene container.
|
||||||
|
|
||||||
|
`tests/foliage_interaction_test.gd` checks sweep direction, recovery, teleport
|
||||||
|
rejection, camera movement, world isolation, restore/quality reset, the primary
|
||||||
|
actor slot and shared leaf/fruit bindings. Run `tools/capture_foliage.gd` with
|
||||||
|
the native renderer for before/contact/trail/recovery images and controller
|
||||||
|
timing in `docs/baselines/foliage_*`. That capture freezes simulation and wind, and moves
|
||||||
|
the player presentation through the actual meadow, checking the state checksum
|
||||||
|
is unchanged. CPU update timing is not a GPU or weak-PC frame-rate guarantee.
|
||||||
|
Native Metal and Compatibility both render the effect; existing particle-shader
|
||||||
|
shutdown warnings (and four Compatibility texture leaks) also reproduce in the
|
||||||
|
untouched pre-art baseline. The headless quality gate retains its exact allowlist.
|
||||||
|
|
||||||
## Runtime hot paths already bounded
|
## Runtime hot paths already bounded
|
||||||
|
|
||||||
The woodland storybook art pass adds portable Blender animal meshes and an
|
The woodland storybook art pass adds portable Blender animal meshes and an
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1006 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1008 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 997 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"active_trail_texels": 145,
|
||||||
|
"controller_update_p50_ms": 0.307,
|
||||||
|
"controller_update_p95_ms": 0.338,
|
||||||
|
"draw_calls": 582.0,
|
||||||
|
"note": "CPU timing includes scanning, field painting and material uploads; not isolated GPU frame time.",
|
||||||
|
"profile": "High",
|
||||||
|
"renderer": "metal",
|
||||||
|
"resolution": "1600x900",
|
||||||
|
"simulation_checksum_unchanged": true,
|
||||||
|
"trail_texture_bytes": 262144,
|
||||||
|
"workload": "Actual Jajce meadow; 8 nearest actors, player presentation follows an 11 m sweep"
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1000 KiB |
@@ -0,0 +1,210 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const Controller := preload("res://world/jajce/GrassInteractionController.gd")
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
class RestoreSource:
|
||||||
|
extends Node
|
||||||
|
signal state_restored
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
_check_trail_lifecycle()
|
||||||
|
await _check_loaded_binding()
|
||||||
|
if failures.is_empty():
|
||||||
|
print("Foliage interaction checks passed")
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
for failure in failures:
|
||||||
|
push_error(failure)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _check_trail_lifecycle() -> void:
|
||||||
|
var field := FoliageTrailMap.new()
|
||||||
|
var positions: Dictionary[int, Vector3] = {1: Vector3(0.25, 0, 0.25)}
|
||||||
|
field.advance(0.08, Vector3.ZERO, positions)
|
||||||
|
positions[1] = Vector3(2.25, 0, 0.25)
|
||||||
|
field.advance(0.08, Vector3.ZERO, positions)
|
||||||
|
_check(_sample(field, Vector2(1.25, 0.25)).b > 0.9, "Walking should leave a continuous trail")
|
||||||
|
_check(
|
||||||
|
_sample(field, Vector2(1.25, 0.75)).g > 0 and _sample(field, Vector2(1.25, -0.25)).g < 0,
|
||||||
|
"The two sides of a trail should bend away from the travelled line"
|
||||||
|
)
|
||||||
|
positions[1] = Vector3(18.25, 0, 0.25)
|
||||||
|
field.advance(0.08, Vector3.ZERO, positions)
|
||||||
|
_check(_sample(field, Vector2(10.25, 0.25)).b == 0, "Teleports must not paint connecting lines")
|
||||||
|
_check(
|
||||||
|
_sample(field, Vector2(18.25, 0.25)).b > 0.9,
|
||||||
|
"Teleported actors still press their destination"
|
||||||
|
)
|
||||||
|
field.advance(0.08, Vector3(1, 0, 1), {})
|
||||||
|
_check(
|
||||||
|
_sample(field, Vector2(1.25, 0.25)).b > 0.8,
|
||||||
|
"Camera movement must keep trails in world space"
|
||||||
|
)
|
||||||
|
field.advance(4.0, Vector3.ZERO, {})
|
||||||
|
var recovering := _sample(field, Vector2(1.25, 0.25)).b
|
||||||
|
_check(recovering > 0.1 and recovering < 0.6, "Abandoned trails should gradually recover")
|
||||||
|
field.advance(4.1, Vector3.ZERO, {})
|
||||||
|
_check(
|
||||||
|
(field.get("_cells") as Dictionary).is_empty(),
|
||||||
|
"Recovered trails should release sparse history"
|
||||||
|
)
|
||||||
|
positions[1] = Vector3(0.25, 6, 0.25)
|
||||||
|
field.advance(0.08, Vector3.ZERO, positions)
|
||||||
|
var elevated := _sample(field, Vector2(0.25, 0.25))
|
||||||
|
_check(
|
||||||
|
is_equal_approx(elevated.a / elevated.b, 6.0),
|
||||||
|
"Foot height must reach the shader for bridge rejection"
|
||||||
|
)
|
||||||
|
field.advance(0.08, Vector3(300, 0, 300), {})
|
||||||
|
_check((field.get("_cells") as Dictionary).is_empty(), "Off-window trails must be discarded")
|
||||||
|
positions[1] = Vector3(2.25, 0, 0.25)
|
||||||
|
field.advance(0.08, Vector3.ZERO, positions)
|
||||||
|
_check(
|
||||||
|
_sample(field, Vector2(1.25, 0.25)).b == 0,
|
||||||
|
"Re-entering actors must not connect to stale positions"
|
||||||
|
)
|
||||||
|
field.clear()
|
||||||
|
_check(_sample(field, Vector2(2.25, 0.25)).b == 0, "Reset must clear the uploaded field")
|
||||||
|
_check(
|
||||||
|
(field.get("_previous_positions") as Dictionary).is_empty(),
|
||||||
|
"Reset must discard actor history"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _check_loaded_binding() -> void:
|
||||||
|
var stage := Node3D.new()
|
||||||
|
root.add_child(stage)
|
||||||
|
var camera := Camera3D.new()
|
||||||
|
stage.add_child(camera)
|
||||||
|
camera.current = true
|
||||||
|
var simulation := RestoreSource.new()
|
||||||
|
simulation.name = "SimulationManager"
|
||||||
|
stage.add_child(simulation)
|
||||||
|
var world := Node3D.new()
|
||||||
|
stage.add_child(world)
|
||||||
|
var terrain := Node3D.new()
|
||||||
|
world.add_child(terrain)
|
||||||
|
var patch := (
|
||||||
|
load("res://world/jajce/StylizedBerryPatch.tscn").instantiate() as StylizedBerryPatch
|
||||||
|
)
|
||||||
|
world.add_child(patch)
|
||||||
|
patch.scale = Vector3(1.2, 0.8, 1.1)
|
||||||
|
patch.rotation.y = 0.6
|
||||||
|
var controller := Controller.new()
|
||||||
|
controller.grass_material = (
|
||||||
|
load("res://world/jajce/materials/cozy_grass_material.tres").duplicate(true)
|
||||||
|
)
|
||||||
|
terrain.add_child(controller)
|
||||||
|
var foreign := Node3D.new()
|
||||||
|
root.add_child(foreign)
|
||||||
|
foreign.add_to_group("grass_interactors")
|
||||||
|
var actors: Array[Node3D] = []
|
||||||
|
for index in 12:
|
||||||
|
var actor := Node3D.new()
|
||||||
|
stage.add_child(actor)
|
||||||
|
actor.position.x = 1.0 + index
|
||||||
|
actor.add_to_group("grass_interactors")
|
||||||
|
actors.append(actor)
|
||||||
|
await process_frame
|
||||||
|
controller.set_process(false)
|
||||||
|
controller.call("_refresh_interactors", 0.08)
|
||||||
|
var material := controller.grass_material
|
||||||
|
var positions: PackedVector3Array = material.get_shader_parameter("interactor_positions")
|
||||||
|
_check(
|
||||||
|
int(material.get_shader_parameter("interactor_count")) == 8,
|
||||||
|
"Contact work must respect the eight-actor cap"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
positions[0] == actors[0].position,
|
||||||
|
"A closer actor in another scene container must be rejected"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
bool(material.get_shader_parameter("foliage_trails_enabled")),
|
||||||
|
"Loaded grass must enable its trail field"
|
||||||
|
)
|
||||||
|
var leaves := patch.get_node("Leaves") as MultiMeshInstance3D
|
||||||
|
var berries := patch.get_node("Berries") as MultiMeshInstance3D
|
||||||
|
var leaf_material := leaves.multimesh.mesh.surface_get_material(0) as ShaderMaterial
|
||||||
|
var berry_material := berries.multimesh.mesh.surface_get_material(0) as ShaderMaterial
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
leaf_material.get_shader_parameter("foliage_trail_map")
|
||||||
|
== material.get_shader_parameter("foliage_trail_map")
|
||||||
|
)
|
||||||
|
and (
|
||||||
|
berry_material.get_shader_parameter("foliage_trail_map")
|
||||||
|
== material.get_shader_parameter("foliage_trail_map")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"Bush leaves, berries and meadow must use the same world-local footprint field"
|
||||||
|
)
|
||||||
|
var visible_berries := patch.get_visible_berry_count()
|
||||||
|
actors[0].hide()
|
||||||
|
controller.primary_interactor_path = controller.get_path_to(actors.back())
|
||||||
|
controller.max_interactors = 4
|
||||||
|
controller.call("_refresh_interactors", 0.12)
|
||||||
|
positions = material.get_shader_parameter("interactor_positions")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
int(material.get_shader_parameter("interactor_count")) == 4
|
||||||
|
and positions[0].x == 12
|
||||||
|
and positions[1].x == 2
|
||||||
|
),
|
||||||
|
"Balanced must reserve the primary actor slot, cap actors and ignore hidden visuals"
|
||||||
|
)
|
||||||
|
simulation.state_restored.emit()
|
||||||
|
_check(
|
||||||
|
not bool(material.get_shader_parameter("foliage_trails_enabled")),
|
||||||
|
"Successful restores must clear visual trails immediately"
|
||||||
|
)
|
||||||
|
controller.set_interaction_enabled(false)
|
||||||
|
_check(not controller.is_processing(), "Low quality must stop interaction updates")
|
||||||
|
_check(
|
||||||
|
not bool(leaf_material.get_shader_parameter("foliage_trails_enabled")),
|
||||||
|
"Low quality must release bushes as well as grass"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
int(material.get_shader_parameter("interactor_count")) == 0,
|
||||||
|
"Low quality must clear direct contacts"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
patch.get_visible_berry_count() == visible_berries,
|
||||||
|
"Contact bending must not change resource presentation amounts"
|
||||||
|
)
|
||||||
|
controller.set_interaction_enabled(true)
|
||||||
|
controller.call("_refresh_interactors", 0.08)
|
||||||
|
_check(
|
||||||
|
bool(berry_material.get_shader_parameter("foliage_trails_enabled")),
|
||||||
|
"Bush interaction must resume after Low quality"
|
||||||
|
)
|
||||||
|
var other_field := FoliageTrailMap.new()
|
||||||
|
_check(
|
||||||
|
other_field.texture != material.get_shader_parameter("foliage_trail_map"),
|
||||||
|
"Each world must own a distinct trail texture"
|
||||||
|
)
|
||||||
|
foreign.free()
|
||||||
|
stage.free()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _sample(field: FoliageTrailMap, position: Vector2) -> Color:
|
||||||
|
var image := field.get("_image") as Image
|
||||||
|
var pixel := Vector2i(
|
||||||
|
(position - Vector2(field.world_rect.x, field.world_rect.y)) / field.CELL_SIZE
|
||||||
|
)
|
||||||
|
return image.get_pixelv(pixel)
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://clvjg2p81ay3b
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Native renderer review of a scripted walk through the actual loaded meadow.
|
||||||
|
## The capture moves presentation only and verifies the simulation checksum.
|
||||||
|
|
||||||
|
const OUTPUT := "res://docs/baselines/foliage_"
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
root.size = Vector2i(1600, 900)
|
||||||
|
var main := load("res://main.tscn").instantiate() as Node3D
|
||||||
|
main.get_node("SimulationManager").set("debug_logs", false)
|
||||||
|
root.add_child(main)
|
||||||
|
await process_frame
|
||||||
|
var manager := main.get_node("SimulationManager")
|
||||||
|
manager.set_process(false)
|
||||||
|
var checksum: String = manager.get_state_checksum()
|
||||||
|
var world := main.get_node("JajceWorld") as JajceWorld
|
||||||
|
world.apply_presentation_quality(JajceWorld.PresentationQuality.HIGH)
|
||||||
|
var controller := world.grass_controller
|
||||||
|
controller.set_process(false)
|
||||||
|
var cycle := world.get_node("DayNightCycle")
|
||||||
|
cycle.set_process(false)
|
||||||
|
cycle.call("_apply_light_rotation", 0.4)
|
||||||
|
cycle.call("_apply_environment", 0.4)
|
||||||
|
for layer in main.get_children():
|
||||||
|
if layer is CanvasLayer:
|
||||||
|
layer.hide()
|
||||||
|
for actor in get_nodes_in_group("grass_interactors"):
|
||||||
|
actor.set_physics_process(false)
|
||||||
|
actor.set_process(false)
|
||||||
|
var player := main.get_node("Player") as CharacterBody3D
|
||||||
|
player.get_node("Visual/Sword").hide()
|
||||||
|
main.get_node("CameraRig").set_physics_process(false)
|
||||||
|
var camera := root.get_camera_3d()
|
||||||
|
camera.reparent(main)
|
||||||
|
camera.global_position = Vector3(16, 7.5, 23)
|
||||||
|
camera.look_at(Vector3(11, 0.6, 10.5))
|
||||||
|
camera.fov = 48
|
||||||
|
player.position = Vector3(5, 0, 10.85)
|
||||||
|
player.get_node("Visual").rotation.y = -PI / 2.0
|
||||||
|
for _frame in 90:
|
||||||
|
await process_frame
|
||||||
|
# Freeze wind and particles so the four review frames isolate contact deformation.
|
||||||
|
Engine.time_scale = 0.0
|
||||||
|
var demo := main.get_node("DemoController")
|
||||||
|
if demo.get("debug_overlay_visible"):
|
||||||
|
demo.call("toggle_debug_overlay")
|
||||||
|
for layer in main.get_children():
|
||||||
|
if layer is CanvasLayer:
|
||||||
|
layer.hide()
|
||||||
|
controller.call("reset_interaction")
|
||||||
|
await _save("before.png")
|
||||||
|
var update_times: Array[float] = []
|
||||||
|
for step in 61:
|
||||||
|
player.position.x = 5.0 + step * 0.18
|
||||||
|
player.position.y = world.terrain.data.get_height(player.position) + 0.02
|
||||||
|
var start := Time.get_ticks_usec()
|
||||||
|
controller.call("_refresh_interactors", 0.08)
|
||||||
|
update_times.append(float(Time.get_ticks_usec() - start) / 1000.0)
|
||||||
|
for _frame in 5:
|
||||||
|
await process_frame
|
||||||
|
if step == 37:
|
||||||
|
await _save("contact.png")
|
||||||
|
await _save("trail.png")
|
||||||
|
var active_cells := (controller.get("_trail_map").get("_cells") as Dictionary).size()
|
||||||
|
var draws := Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)
|
||||||
|
for _step in 110:
|
||||||
|
controller.call("_refresh_interactors", 0.08)
|
||||||
|
for _frame in 5:
|
||||||
|
await process_frame
|
||||||
|
await _save("recovered.png")
|
||||||
|
update_times.sort()
|
||||||
|
var report := {
|
||||||
|
"renderer": RenderingServer.get_current_rendering_driver_name(),
|
||||||
|
"resolution": "1600x900",
|
||||||
|
"profile": "High",
|
||||||
|
"workload":
|
||||||
|
"Actual Jajce meadow; 8 nearest actors, player presentation follows an 11 m sweep",
|
||||||
|
"controller_update_p50_ms": update_times[30],
|
||||||
|
"controller_update_p95_ms": update_times[57],
|
||||||
|
"active_trail_texels": active_cells,
|
||||||
|
"trail_texture_bytes": 128 * 128 * 16,
|
||||||
|
"draw_calls": draws,
|
||||||
|
"simulation_checksum_unchanged": manager.get_state_checksum() == checksum,
|
||||||
|
"note":
|
||||||
|
"CPU timing includes scanning, field painting and material uploads; not isolated GPU frame time."
|
||||||
|
}
|
||||||
|
var file := FileAccess.open(OUTPUT + "render_metrics.json", FileAccess.WRITE)
|
||||||
|
file.store_string(JSON.stringify(report, "\t") + "\n")
|
||||||
|
print(JSON.stringify(report))
|
||||||
|
assert(manager.get_state_checksum() == checksum)
|
||||||
|
main.free()
|
||||||
|
Engine.time_scale = 1.0
|
||||||
|
await process_frame
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _save(filename: String) -> void:
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
var capture := root.get_texture().get_image()
|
||||||
|
assert(not capture.is_empty())
|
||||||
|
assert(capture.save_png(OUTPUT + filename) == OK)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bqd5coilksebi
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
class_name FoliageTrailMap
|
||||||
|
extends RefCounted
|
||||||
|
## Disposable, camera-local footprint field. No per-blade nodes or physics.
|
||||||
|
## RG = weighted bend direction, B = pressure, A = weighted actor foot height.
|
||||||
|
|
||||||
|
const RESOLUTION := 128
|
||||||
|
const CELL_SIZE := 0.5
|
||||||
|
const WORLD_SIZE := RESOLUTION * CELL_SIZE
|
||||||
|
const TRAIL_RADIUS := 0.95
|
||||||
|
const RECOVERY_SECONDS := 8.0
|
||||||
|
const MAX_SEGMENT_LENGTH := 3.0
|
||||||
|
|
||||||
|
var texture: ImageTexture
|
||||||
|
var world_rect := Vector4.ZERO
|
||||||
|
|
||||||
|
var _image: Image
|
||||||
|
var _origin := Vector2i.ZERO
|
||||||
|
var _cells: Dictionary[Vector2i, Color] = {}
|
||||||
|
var _previous_positions: Dictionary[int, Vector3] = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
_image = Image.create(RESOLUTION, RESOLUTION, false, Image.FORMAT_RGBAF)
|
||||||
|
_image.fill(Color(0, 0, 0, 0))
|
||||||
|
texture = ImageTexture.create_from_image(_image)
|
||||||
|
|
||||||
|
|
||||||
|
func clear() -> void:
|
||||||
|
_cells.clear()
|
||||||
|
_previous_positions.clear()
|
||||||
|
_image.fill(Color(0, 0, 0, 0))
|
||||||
|
texture.update(_image)
|
||||||
|
|
||||||
|
|
||||||
|
func advance(delta: float, center: Vector3, positions: Dictionary[int, Vector3]) -> void:
|
||||||
|
var next_origin := _world_cell(Vector2(center.x, center.z)) - Vector2i.ONE * (RESOLUTION / 2)
|
||||||
|
var dirty := not _cells.is_empty() or next_origin != _origin
|
||||||
|
_origin = next_origin
|
||||||
|
world_rect = Vector4(_origin.x * CELL_SIZE, _origin.y * CELL_SIZE, WORLD_SIZE, WORLD_SIZE)
|
||||||
|
_decay_and_clip(maxf(delta, 0.0))
|
||||||
|
for actor_id in positions:
|
||||||
|
var end := positions[actor_id]
|
||||||
|
var start: Vector3 = _previous_positions.get(actor_id, end)
|
||||||
|
# A load, teleport, spawn, or skipped actor must never draw a connecting scar.
|
||||||
|
if start.distance_to(end) > MAX_SEGMENT_LENGTH or delta > 0.5:
|
||||||
|
start = end
|
||||||
|
_stamp_segment(start, end)
|
||||||
|
_previous_positions = positions.duplicate()
|
||||||
|
if not dirty and _cells.is_empty():
|
||||||
|
return
|
||||||
|
_image.fill(Color(0, 0, 0, 0))
|
||||||
|
for cell in _cells:
|
||||||
|
var pixel := cell - _origin
|
||||||
|
_image.set_pixelv(pixel, _cells[cell])
|
||||||
|
texture.update(_image)
|
||||||
|
|
||||||
|
|
||||||
|
func _decay_and_clip(delta: float) -> void:
|
||||||
|
for cell in _cells.keys():
|
||||||
|
var value := _cells[cell]
|
||||||
|
var pressure := maxf(0.0, value.b - delta / RECOVERY_SECONDS)
|
||||||
|
if pressure <= 0.001 or not _contains(cell):
|
||||||
|
_cells.erase(cell)
|
||||||
|
else:
|
||||||
|
_cells[cell] = value * (pressure / value.b)
|
||||||
|
|
||||||
|
|
||||||
|
func _stamp_segment(start: Vector3, end: Vector3) -> void:
|
||||||
|
var a := Vector2(start.x, start.z)
|
||||||
|
var b := Vector2(end.x, end.z)
|
||||||
|
var line := b - a
|
||||||
|
var length_squared := line.length_squared()
|
||||||
|
var radius := Vector2.ONE * TRAIL_RADIUS
|
||||||
|
var first := _world_cell(a.min(b) - radius).max(_origin)
|
||||||
|
var last := _world_cell(a.max(b) + radius).min(_origin + Vector2i.ONE * (RESOLUTION - 1))
|
||||||
|
for y in range(first.y, last.y + 1):
|
||||||
|
for x in range(first.x, last.x + 1):
|
||||||
|
var cell := Vector2i(x, y)
|
||||||
|
var point := (Vector2(cell) + Vector2.ONE * 0.5) * CELL_SIZE
|
||||||
|
var along := clampf((point - a).dot(line) / maxf(length_squared, 0.0001), 0.0, 1.0)
|
||||||
|
var offset := point - a - line * along
|
||||||
|
var pressure := 1.0 - smoothstep(0.16, TRAIL_RADIUS, offset.length())
|
||||||
|
var existing: Color = _cells.get(cell, Color(0, 0, 0, 0))
|
||||||
|
if pressure <= 0.001 or pressure < existing.b:
|
||||||
|
continue
|
||||||
|
# A swept capsule parts blades to both sides of the travelled line.
|
||||||
|
var away := offset.normalized()
|
||||||
|
var height := lerpf(start.y, end.y, along)
|
||||||
|
_cells[cell] = Color(away.x, away.y, 1.0, height) * pressure
|
||||||
|
|
||||||
|
|
||||||
|
func _world_cell(position: Vector2) -> Vector2i:
|
||||||
|
return Vector2i(floori(position.x / CELL_SIZE), floori(position.y / CELL_SIZE))
|
||||||
|
|
||||||
|
|
||||||
|
func _contains(cell: Vector2i) -> bool:
|
||||||
|
var pixel := cell - _origin
|
||||||
|
return pixel.x >= 0 and pixel.y >= 0 and pixel.x < RESOLUTION and pixel.y < RESOLUTION
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://6llj4y8dy0jm
|
||||||
@@ -7,14 +7,40 @@ const CLEARING_LIMIT := 4
|
|||||||
@export_range(1, SHADER_INTERACTOR_LIMIT, 1) var max_interactors := 8
|
@export_range(1, SHADER_INTERACTOR_LIMIT, 1) var max_interactors := 8
|
||||||
@export_range(0.02, 0.5, 0.01) var update_interval := 0.08
|
@export_range(0.02, 0.5, 0.01) var update_interval := 0.08
|
||||||
@export var path_clearings: Array[NodePath] = []
|
@export var path_clearings: Array[NodePath] = []
|
||||||
|
@export var actor_root_path := NodePath("../../..")
|
||||||
|
@export var foliage_root_path := NodePath("../..")
|
||||||
|
@export var simulation_manager_path := NodePath("../../../SimulationManager")
|
||||||
|
@export var primary_interactor_path := NodePath("../../../Player")
|
||||||
|
|
||||||
var _elapsed := 0.0
|
var _elapsed := 0.0
|
||||||
|
var _trail_map := FoliageTrailMap.new()
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
# JajceWorld first isolates its mutable materials in the parent's _ready().
|
# JajceWorld first isolates its mutable materials in the parent's _ready().
|
||||||
call_deferred("_configure_path_clearings")
|
call_deferred("_configure_interaction")
|
||||||
_refresh_interactors()
|
|
||||||
|
|
||||||
|
func _configure_interaction() -> void:
|
||||||
|
_configure_path_clearings()
|
||||||
|
var simulation := get_node_or_null(simulation_manager_path)
|
||||||
|
if simulation != null and simulation.has_signal("state_restored"):
|
||||||
|
simulation.connect("state_restored", reset_interaction)
|
||||||
|
if is_processing():
|
||||||
|
_refresh_interactors(0.0)
|
||||||
|
|
||||||
|
|
||||||
|
func set_interaction_enabled(enabled: bool) -> void:
|
||||||
|
set_process(enabled)
|
||||||
|
reset_interaction()
|
||||||
|
_elapsed = update_interval
|
||||||
|
|
||||||
|
|
||||||
|
func reset_interaction() -> void:
|
||||||
|
_trail_map.clear()
|
||||||
|
if grass_material != null:
|
||||||
|
grass_material.set_shader_parameter("interactor_count", 0)
|
||||||
|
_bind_trail_materials(false)
|
||||||
|
|
||||||
|
|
||||||
func _configure_path_clearings() -> void:
|
func _configure_path_clearings() -> void:
|
||||||
@@ -44,35 +70,76 @@ func _process(delta: float) -> void:
|
|||||||
_elapsed += delta
|
_elapsed += delta
|
||||||
if _elapsed < update_interval:
|
if _elapsed < update_interval:
|
||||||
return
|
return
|
||||||
|
_refresh_interactors(_elapsed)
|
||||||
_elapsed = 0.0
|
_elapsed = 0.0
|
||||||
_refresh_interactors()
|
|
||||||
|
|
||||||
|
|
||||||
func _refresh_interactors() -> void:
|
func _refresh_interactors(delta: float = 0.08) -> void:
|
||||||
if grass_material == null:
|
if grass_material == null:
|
||||||
return
|
return
|
||||||
var camera := get_viewport().get_camera_3d()
|
var camera := get_viewport().get_camera_3d()
|
||||||
var origin := camera.global_position if camera != null else Vector3.ZERO
|
var origin := camera.global_position if camera != null else Vector3.ZERO
|
||||||
|
var actor_root := get_node_or_null(actor_root_path)
|
||||||
|
var primary := get_node_or_null(primary_interactor_path)
|
||||||
var nearest: Array[Node3D] = []
|
var nearest: Array[Node3D] = []
|
||||||
|
var limit := clampi(max_interactors, 1, SHADER_INTERACTOR_LIMIT)
|
||||||
for value in get_tree().get_nodes_in_group("grass_interactors"):
|
for value in get_tree().get_nodes_in_group("grass_interactors"):
|
||||||
var candidate := value as Node3D
|
var candidate := value as Node3D
|
||||||
if candidate == null:
|
if (
|
||||||
|
candidate == null
|
||||||
|
or candidate.is_queued_for_deletion()
|
||||||
|
or not candidate.is_visible_in_tree()
|
||||||
|
or candidate.get_viewport() != get_viewport()
|
||||||
|
or (actor_root != null and not actor_root.is_ancestor_of(candidate))
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
var insert_at := nearest.size()
|
var insert_at := nearest.size()
|
||||||
var candidate_distance := origin.distance_squared_to(candidate.global_position)
|
var candidate_distance := origin.distance_squared_to(candidate.global_position)
|
||||||
for index in nearest.size():
|
for index in nearest.size():
|
||||||
if candidate_distance < origin.distance_squared_to(nearest[index].global_position):
|
if (
|
||||||
|
candidate == primary
|
||||||
|
or (
|
||||||
|
nearest[index] != primary
|
||||||
|
and (
|
||||||
|
candidate_distance
|
||||||
|
< origin.distance_squared_to(nearest[index].global_position)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
insert_at = index
|
insert_at = index
|
||||||
break
|
break
|
||||||
nearest.insert(insert_at, candidate)
|
nearest.insert(insert_at, candidate)
|
||||||
if nearest.size() > max_interactors:
|
if nearest.size() > limit:
|
||||||
nearest.pop_back()
|
nearest.pop_back()
|
||||||
|
|
||||||
var positions := PackedVector3Array()
|
var positions := PackedVector3Array()
|
||||||
positions.resize(SHADER_INTERACTOR_LIMIT)
|
positions.resize(SHADER_INTERACTOR_LIMIT)
|
||||||
for index in SHADER_INTERACTOR_LIMIT:
|
for index in SHADER_INTERACTOR_LIMIT:
|
||||||
positions[index] = Vector3(0.0, -1000.0, 0.0)
|
positions[index] = Vector3(0.0, -1000.0, 0.0)
|
||||||
|
var trail_positions: Dictionary[int, Vector3] = {}
|
||||||
for index in nearest.size():
|
for index in nearest.size():
|
||||||
positions[index] = nearest[index].global_position
|
positions[index] = nearest[index].global_position
|
||||||
|
trail_positions[nearest[index].get_instance_id()] = positions[index]
|
||||||
grass_material.set_shader_parameter("interactor_count", nearest.size())
|
grass_material.set_shader_parameter("interactor_count", nearest.size())
|
||||||
grass_material.set_shader_parameter("interactor_positions", positions)
|
grass_material.set_shader_parameter("interactor_positions", positions)
|
||||||
|
_trail_map.advance(delta, origin, trail_positions)
|
||||||
|
_bind_trail_materials(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _bind_trail_materials(enabled: bool) -> void:
|
||||||
|
if grass_material != null:
|
||||||
|
grass_material.set_shader_parameter("foliage_trail_map", _trail_map.texture)
|
||||||
|
grass_material.set_shader_parameter("foliage_trail_rect", _trail_map.world_rect)
|
||||||
|
grass_material.set_shader_parameter("foliage_trails_enabled", enabled)
|
||||||
|
var foliage_root := get_node_or_null(foliage_root_path)
|
||||||
|
if foliage_root == null:
|
||||||
|
return
|
||||||
|
for value in get_tree().get_nodes_in_group("interactive_foliage"):
|
||||||
|
var patch := value as StylizedBerryPatch
|
||||||
|
if patch != null and foliage_root.is_ancestor_of(patch):
|
||||||
|
patch.set_contact_field(_trail_map.texture, _trail_map.world_rect, enabled)
|
||||||
|
if grass_material != null:
|
||||||
|
patch.set_contact_actors(
|
||||||
|
grass_material.get_shader_parameter("interactor_positions"),
|
||||||
|
int(grass_material.get_shader_parameter("interactor_count")) if enabled else 0
|
||||||
|
)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
|
|
||||||
[ext_resource type="ArrayMesh" path="res://assets/storybook/meadow_grass.res" id="31_meadow"]
|
[ext_resource type="ArrayMesh" path="res://assets/storybook/meadow_grass.res" id="31_meadow"]
|
||||||
[ext_resource type="Shader" path="res://world/jajce/materials/storybook_sky.gdshader" id="32_sky"]
|
[ext_resource type="Shader" path="res://world/jajce/materials/storybook_sky.gdshader" id="32_sky"]
|
||||||
|
[ext_resource type="PackedScene" path="res://world/jajce/StylizedBerryPatch.tscn" id="33_berry"]
|
||||||
|
|
||||||
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
||||||
_shader_parameters = {
|
_shader_parameters = {
|
||||||
@@ -499,6 +500,11 @@ safety_risk = 0.05
|
|||||||
comfort_distance = 18.0
|
comfort_distance = 18.0
|
||||||
discovery_priority = 1.0
|
discovery_priority = 1.0
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" parent="WorldObjects/ResourceNodes/BerryBush_01/Visual"]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="BerryPatchPresentation" parent="WorldObjects/ResourceNodes/BerryBush_01/Visual" instance=ExtResource("33_berry")]
|
||||||
|
|
||||||
[node name="BerryBush_02" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
[node name="BerryBush_02" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||||
position = Vector3(-10, 0, -4)
|
position = Vector3(-10, 0, -4)
|
||||||
node_id = &"berry_bush_02"
|
node_id = &"berry_bush_02"
|
||||||
@@ -508,6 +514,12 @@ safety_risk = 0.08
|
|||||||
comfort_distance = 20.0
|
comfort_distance = 20.0
|
||||||
discovery_priority = 0.5
|
discovery_priority = 0.5
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" parent="WorldObjects/ResourceNodes/BerryBush_02/Visual"]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="BerryPatchPresentation" parent="WorldObjects/ResourceNodes/BerryBush_02/Visual" instance=ExtResource("33_berry")]
|
||||||
|
rotation_degrees = Vector3(0, 38, 0)
|
||||||
|
|
||||||
[node name="BerryPatch_River_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
[node name="BerryPatch_River_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||||
position = Vector3(12, 0, 10)
|
position = Vector3(12, 0, 10)
|
||||||
node_id = &"berry_patch_river_01"
|
node_id = &"berry_patch_river_01"
|
||||||
@@ -518,6 +530,12 @@ safety_risk = 0.18
|
|||||||
comfort_distance = 22.0
|
comfort_distance = 22.0
|
||||||
discovery_priority = 0.25
|
discovery_priority = 0.25
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" parent="WorldObjects/ResourceNodes/BerryPatch_River_01/Visual"]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="BerryPatchPresentation" parent="WorldObjects/ResourceNodes/BerryPatch_River_01/Visual" instance=ExtResource("33_berry")]
|
||||||
|
scale = Vector3(1.15, 1.15, 1.15)
|
||||||
|
|
||||||
[node name="AnimalCamp_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
[node name="AnimalCamp_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||||
position = Vector3(-12, 0, 8)
|
position = Vector3(-12, 0, 8)
|
||||||
node_id = &"animal_camp_01"
|
node_id = &"animal_camp_01"
|
||||||
@@ -536,6 +554,12 @@ safety_risk = 0.12
|
|||||||
comfort_distance = 24.0
|
comfort_distance = 24.0
|
||||||
discovery_priority = 0.1
|
discovery_priority = 0.1
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" parent="WorldObjects/ResourceNodes/BerryPatch_South_01/Visual"]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="BerryPatchPresentation" parent="WorldObjects/ResourceNodes/BerryPatch_South_01/Visual" instance=ExtResource("33_berry")]
|
||||||
|
rotation_degrees = Vector3(0, -24, 0)
|
||||||
|
|
||||||
[node name="AnimalCamp_River_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
[node name="AnimalCamp_River_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||||
position = Vector3(20, 0, 16)
|
position = Vector3(20, 0, 16)
|
||||||
node_id = &"animal_camp_river_01"
|
node_id = &"animal_camp_river_01"
|
||||||
@@ -626,6 +650,11 @@ safety_risk = 0.1
|
|||||||
comfort_distance = 20.0
|
comfort_distance = 20.0
|
||||||
discovery_priority = 0.45
|
discovery_priority = 0.45
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" parent="WorldObjects/ResourceNodes/BerryPatch_Mill_01/Visual"]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="BerryPatchPresentation" parent="WorldObjects/ResourceNodes/BerryPatch_Mill_01/Visual" instance=ExtResource("33_berry")]
|
||||||
|
|
||||||
[node name="ResourceClusters" type="Node3D" parent="WorldObjects"]
|
[node name="ResourceClusters" type="Node3D" parent="WorldObjects"]
|
||||||
|
|
||||||
[node name="RiverbankResourceCluster" parent="WorldObjects/ResourceClusters" instance=ExtResource("26_resource_cluster")]
|
[node name="RiverbankResourceCluster" parent="WorldObjects/ResourceClusters" instance=ExtResource("26_resource_cluster")]
|
||||||
|
|||||||
@@ -29,11 +29,28 @@ const BERRY_POSITIONS := [
|
|||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
add_to_group("interactive_foliage")
|
||||||
_build_leaves()
|
_build_leaves()
|
||||||
_build_berries()
|
_build_berries()
|
||||||
super()
|
super()
|
||||||
|
|
||||||
|
|
||||||
|
func set_contact_field(texture: Texture2D, world_rect: Vector4, enabled: bool) -> void:
|
||||||
|
for child in [$Leaves, $Berries]:
|
||||||
|
var material := (child as MultiMeshInstance3D).multimesh.mesh.material as ShaderMaterial
|
||||||
|
material.set_shader_parameter("foliage_trail_map", texture)
|
||||||
|
material.set_shader_parameter("foliage_trail_rect", world_rect)
|
||||||
|
material.set_shader_parameter("foliage_trails_enabled", enabled)
|
||||||
|
material.set_shader_parameter("contact_root", global_position)
|
||||||
|
|
||||||
|
|
||||||
|
func set_contact_actors(positions: PackedVector3Array, count: int) -> void:
|
||||||
|
for child in [$Leaves, $Berries]:
|
||||||
|
var material := (child as MultiMeshInstance3D).multimesh.mesh.material as ShaderMaterial
|
||||||
|
material.set_shader_parameter("interactor_count", count)
|
||||||
|
material.set_shader_parameter("interactor_positions", positions)
|
||||||
|
|
||||||
|
|
||||||
func get_presentation_instance_count() -> int:
|
func get_presentation_instance_count() -> int:
|
||||||
return LEAF_POSITIONS.size() + BERRY_POSITIONS.size()
|
return LEAF_POSITIONS.size() + BERRY_POSITIONS.size()
|
||||||
|
|
||||||
@@ -75,6 +92,7 @@ func _build_leaves() -> void:
|
|||||||
material.set_shader_parameter("wind_strength", 0.045)
|
material.set_shader_parameter("wind_strength", 0.045)
|
||||||
material.set_shader_parameter("wind_speed", 0.48)
|
material.set_shader_parameter("wind_speed", 0.48)
|
||||||
material.set_shader_parameter("wind_phase", 1.7)
|
material.set_shader_parameter("wind_phase", 1.7)
|
||||||
|
material.set_shader_parameter("contact_strength", 0.55)
|
||||||
mesh.material = material
|
mesh.material = material
|
||||||
var transforms: Array[Transform3D] = []
|
var transforms: Array[Transform3D] = []
|
||||||
for index in LEAF_POSITIONS.size():
|
for index in LEAF_POSITIONS.size():
|
||||||
@@ -90,9 +108,14 @@ func _build_berries() -> void:
|
|||||||
mesh.height = 0.21
|
mesh.height = 0.21
|
||||||
mesh.radial_segments = 8
|
mesh.radial_segments = 8
|
||||||
mesh.rings = 4
|
mesh.rings = 4
|
||||||
var material := StandardMaterial3D.new()
|
var material := ShaderMaterial.new()
|
||||||
material.albedo_color = Color("#8f3148")
|
material.shader = WIND_SHADER
|
||||||
material.roughness = 0.78
|
material.set_shader_parameter("albedo", Color("#8f3148"))
|
||||||
|
material.set_shader_parameter("roughness", 0.78)
|
||||||
|
material.set_shader_parameter("wind_strength", 0.012)
|
||||||
|
material.set_shader_parameter("wind_speed", 0.48)
|
||||||
|
material.set_shader_parameter("wind_phase", 1.7)
|
||||||
|
material.set_shader_parameter("contact_strength", 0.55)
|
||||||
mesh.material = material
|
mesh.material = material
|
||||||
var transforms: Array[Transform3D] = []
|
var transforms: Array[Transform3D] = []
|
||||||
for position in BERRY_POSITIONS:
|
for position in BERRY_POSITIONS:
|
||||||
|
|||||||
@@ -252,8 +252,7 @@ func _apply_grass_quality(
|
|||||||
grass_field.set_physics_process(physics_enabled)
|
grass_field.set_physics_process(physics_enabled)
|
||||||
grass_controller.set("max_interactors", maxi(max_interactors, 1))
|
grass_controller.set("max_interactors", maxi(max_interactors, 1))
|
||||||
grass_controller.set("update_interval", update_interval)
|
grass_controller.set("update_interval", update_interval)
|
||||||
grass_controller.set_process(controller_enabled)
|
grass_controller.call("set_interaction_enabled", controller_enabled)
|
||||||
grass_controller.set("_elapsed", update_interval)
|
|
||||||
|
|
||||||
var particles: Array = grass_field.get("particle_nodes")
|
var particles: Array = grass_field.get("particle_nodes")
|
||||||
for value in particles:
|
for value in particles:
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
shader_type spatial;
|
shader_type spatial;
|
||||||
render_mode skip_vertex_transform, cull_disabled, specular_disabled;
|
render_mode skip_vertex_transform, cull_disabled, specular_disabled;
|
||||||
|
|
||||||
|
#include "foliage_trail.gdshaderinc"
|
||||||
|
|
||||||
const int MAX_INTERACTORS = 8;
|
const int MAX_INTERACTORS = 8;
|
||||||
uniform vec2 wind_direction = vec2(1.0, 0.7);
|
uniform vec2 wind_direction = vec2(1.0, 0.7);
|
||||||
uniform vec3 base_color : source_color = vec3(0.23, 0.40, 0.22);
|
uniform vec3 base_color : source_color = vec3(0.23, 0.40, 0.22);
|
||||||
@@ -9,7 +11,7 @@ uniform vec3 tip_color : source_color = vec3(0.53, 0.67, 0.35);
|
|||||||
uniform float wind_amount : hint_range(0.0, 1.0) = 0.24;
|
uniform float wind_amount : hint_range(0.0, 1.0) = 0.24;
|
||||||
uniform int interactor_count : hint_range(0, 8) = 0;
|
uniform int interactor_count : hint_range(0, 8) = 0;
|
||||||
uniform vec3 interactor_positions[MAX_INTERACTORS];
|
uniform vec3 interactor_positions[MAX_INTERACTORS];
|
||||||
uniform float interaction_radius : hint_range(0.25, 4.0) = 1.8;
|
uniform float interaction_radius : hint_range(0.25, 4.0) = 1.1;
|
||||||
uniform float interaction_strength : hint_range(0.0, 2.0) = 0.55;
|
uniform float interaction_strength : hint_range(0.0, 2.0) = 0.55;
|
||||||
uniform int clearing_count = 0;
|
uniform int clearing_count = 0;
|
||||||
uniform vec4 clearing_segments[4];
|
uniform vec4 clearing_segments[4];
|
||||||
@@ -36,7 +38,10 @@ void vertex() {
|
|||||||
vec3 world_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
vec3 world_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
float phase = dot(anchor.xz, vec2(0.38, 0.29));
|
float phase = dot(anchor.xz, vec2(0.38, 0.29));
|
||||||
float gust = sin(TIME * 1.15 + phase) * 0.65 + sin(TIME * 0.58 + phase * 0.42) * 0.35;
|
float gust = sin(TIME * 1.15 + phase) * 0.65 + sin(TIME * 0.58 + phase * 0.42) * 0.35;
|
||||||
world_vertex.xz += normalize(wind_direction) * gust * wind_amount * tip_weight;
|
vec3 trail = sample_foliage_trail(anchor);
|
||||||
|
world_vertex.xz += normalize(wind_direction) * gust * wind_amount * tip_weight * (1.0 - trail.z * 0.75);
|
||||||
|
vec2 contact_bend = trail.xy * 0.72;
|
||||||
|
float pressure = trail.z;
|
||||||
shade_variation = sin(anchor.x * 0.30 + anchor.z * 0.21) * 0.5 + 0.5;
|
shade_variation = sin(anchor.x * 0.30 + anchor.z * 0.21) * 0.5 + 0.5;
|
||||||
pressed_weight = 0.0;
|
pressed_weight = 0.0;
|
||||||
for (int index = 0; index < MAX_INTERACTORS; index++) {
|
for (int index = 0; index < MAX_INTERACTORS; index++) {
|
||||||
@@ -44,11 +49,17 @@ void vertex() {
|
|||||||
vec2 offset = anchor.xz - interactor_positions[index].xz;
|
vec2 offset = anchor.xz - interactor_positions[index].xz;
|
||||||
float distance_to_actor = length(offset);
|
float distance_to_actor = length(offset);
|
||||||
float influence = 1.0 - smoothstep(0.25, interaction_radius, distance_to_actor);
|
float influence = 1.0 - smoothstep(0.25, interaction_radius, distance_to_actor);
|
||||||
|
influence *= 1.0 - smoothstep(0.55, 1.35, abs(anchor.y - interactor_positions[index].y));
|
||||||
vec2 away = offset / max(distance_to_actor, 0.08);
|
vec2 away = offset / max(distance_to_actor, 0.08);
|
||||||
world_vertex.xz += away * influence * interaction_strength * tip_weight;
|
if (influence > pressure) {
|
||||||
world_vertex.y -= influence * 0.18 * tip_weight;
|
contact_bend = away * influence * interaction_strength;
|
||||||
pressed_weight = max(pressed_weight, influence);
|
pressure = influence;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
world_vertex.xz += contact_bend * tip_weight;
|
||||||
|
// Lower relative to the blade's own root so short tufts stay above the ground.
|
||||||
|
world_vertex.y -= max(world_vertex.y - anchor.y, 0.0) * pressure * 0.78 * blade_height;
|
||||||
|
pressed_weight = pressure;
|
||||||
VERTEX = (VIEW_MATRIX * vec4(world_vertex, 1.0)).xyz;
|
VERTEX = (VIEW_MATRIX * vec4(world_vertex, 1.0)).xyz;
|
||||||
NORMAL = normalize((VIEW_MATRIX * vec4(0.0, 1.0, 0.0, 0.0)).xyz);
|
NORMAL = normalize((VIEW_MATRIX * vec4(0.0, 1.0, 0.0, 0.0)).xyz);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ shader_parameter/tip_color = Color(0.53, 0.67, 0.35, 1)
|
|||||||
shader_parameter/wind_amount = 0.24
|
shader_parameter/wind_amount = 0.24
|
||||||
shader_parameter/interactor_count = 0
|
shader_parameter/interactor_count = 0
|
||||||
shader_parameter/interactor_positions = PackedVector3Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
shader_parameter/interactor_positions = PackedVector3Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||||
shader_parameter/interaction_radius = 1.8
|
shader_parameter/interaction_radius = 1.1
|
||||||
shader_parameter/interaction_strength = 0.55
|
shader_parameter/interaction_strength = 0.55
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Per-world data texture, deliberately not a global shader parameter.
|
||||||
|
uniform bool foliage_trails_enabled = false;
|
||||||
|
uniform sampler2D foliage_trail_map : filter_linear, repeat_disable;
|
||||||
|
uniform vec4 foliage_trail_rect = vec4(0.0, 0.0, 64.0, 64.0);
|
||||||
|
|
||||||
|
vec3 sample_foliage_trail(vec3 ground_position) {
|
||||||
|
if (!foliage_trails_enabled) { return vec3(0.0); }
|
||||||
|
vec2 uv = (ground_position.xz - foliage_trail_rect.xy) / foliage_trail_rect.zw;
|
||||||
|
if (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) { return vec3(0.0); }
|
||||||
|
vec4 trail = textureLod(foliage_trail_map, uv, 0.0);
|
||||||
|
float foot_height = trail.a / max(trail.b, 0.0001);
|
||||||
|
// Walkers on a bridge or above a slope do not press plants underneath them.
|
||||||
|
float same_level = 1.0 - smoothstep(0.55, 1.35, abs(ground_position.y - foot_height));
|
||||||
|
float edge = smoothstep(0.0, 0.025, min(min(uv.x, uv.y), min(1.0 - uv.x, 1.0 - uv.y)));
|
||||||
|
return trail.rgb * same_level * edge;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b6e1cvtl55oyv
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
shader_type spatial;
|
shader_type spatial;
|
||||||
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
|
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
|
||||||
|
|
||||||
|
#include "foliage_trail.gdshaderinc"
|
||||||
|
|
||||||
uniform vec2 wind_direction = vec2(0.94, 0.34);
|
uniform vec2 wind_direction = vec2(0.94, 0.34);
|
||||||
uniform float wind_strength : hint_range(0.0, 0.2) = 0.07;
|
uniform float wind_strength : hint_range(0.0, 0.2) = 0.07;
|
||||||
uniform float wind_speed : hint_range(0.0, 2.0) = 0.55;
|
uniform float wind_speed : hint_range(0.0, 2.0) = 0.55;
|
||||||
@@ -10,6 +12,10 @@ uniform float flutter_strength : hint_range(0.0, 0.05) = 0.012;
|
|||||||
uniform float wind_phase : hint_range(0.0, 6.2832) = 0.0;
|
uniform float wind_phase : hint_range(0.0, 6.2832) = 0.0;
|
||||||
uniform vec3 albedo : source_color = vec3(0.19, 0.38, 0.17);
|
uniform vec3 albedo : source_color = vec3(0.19, 0.38, 0.17);
|
||||||
uniform float roughness : hint_range(0.0, 1.0) = 0.9;
|
uniform float roughness : hint_range(0.0, 1.0) = 0.9;
|
||||||
|
uniform vec3 contact_root = vec3(0.0);
|
||||||
|
uniform float contact_strength = 0.0;
|
||||||
|
uniform int interactor_count = 0;
|
||||||
|
uniform vec3 interactor_positions[8];
|
||||||
varying vec3 paint_position;
|
varying vec3 paint_position;
|
||||||
|
|
||||||
void vertex() {
|
void vertex() {
|
||||||
@@ -28,6 +34,36 @@ void vertex() {
|
|||||||
|
|
||||||
VERTEX.xz += direction * bend * height_factor;
|
VERTEX.xz += direction * bend * height_factor;
|
||||||
VERTEX.xz += crosswind * flutter * height_factor;
|
VERTEX.xz += crosswind * flutter * height_factor;
|
||||||
|
if (foliage_trails_enabled && contact_strength > 0.0) {
|
||||||
|
// One bend for the whole bush, including fruit; its base remains planted.
|
||||||
|
vec3 contact = sample_foliage_trail(contact_root);
|
||||||
|
vec3 edge_contact = sample_foliage_trail(contact_root + vec3(0.45, 0.0, 0.0));
|
||||||
|
if (edge_contact.z > contact.z) { contact = edge_contact; }
|
||||||
|
edge_contact = sample_foliage_trail(contact_root - vec3(0.45, 0.0, 0.0));
|
||||||
|
if (edge_contact.z > contact.z) { contact = edge_contact; }
|
||||||
|
edge_contact = sample_foliage_trail(contact_root + vec3(0.0, 0.0, 0.45));
|
||||||
|
if (edge_contact.z > contact.z) { contact = edge_contact; }
|
||||||
|
edge_contact = sample_foliage_trail(contact_root - vec3(0.0, 0.0, 0.45));
|
||||||
|
if (edge_contact.z > contact.z) { contact = edge_contact; }
|
||||||
|
vec3 world_position = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
|
float rooted = smoothstep(0.0, 1.25, world_position.y - contact_root.y);
|
||||||
|
// Bushes spring back faster than the flattened meadow trail.
|
||||||
|
float spring = pow(contact.z, 5.0);
|
||||||
|
vec2 bend_direction = contact.xy / max(length(contact.xy), 0.001);
|
||||||
|
for (int index = 0; index < 8; index++) {
|
||||||
|
if (index >= interactor_count) { break; }
|
||||||
|
vec2 offset = contact_root.xz - interactor_positions[index].xz;
|
||||||
|
float distance_to_actor = length(offset);
|
||||||
|
float influence = 1.0 - smoothstep(0.25, 1.8, distance_to_actor);
|
||||||
|
influence *= 1.0 - smoothstep(0.55, 1.35, abs(contact_root.y - interactor_positions[index].y));
|
||||||
|
if (influence > spring) {
|
||||||
|
spring = influence;
|
||||||
|
bend_direction = offset / max(distance_to_actor, 0.001);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vec3 displacement = vec3(bend_direction.x, -0.18, bend_direction.y) * spring * contact_strength * rooted;
|
||||||
|
VERTEX += (inverse(MODEL_MATRIX) * vec4(displacement, 0.0)).xyz;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
|
|||||||
Reference in New Issue
Block a user