feat(foliage): add recovering grass trails and bush contact
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user