456 lines
18 KiB
GDScript
456 lines
18 KiB
GDScript
extends RefCounted
|
||
class_name TerrainField
|
||
## Höhenfeld-Mathematik des Map-Editors (M2, Karte t_9aba04f1 — Terrain-Werkzeug).
|
||
##
|
||
## Alles static + rein: arbeitet auf einem dekodierten PackedFloat32Array
|
||
## (res×res, row-major, -Z → +Z) statt auf Base64-Strings — das Terrain-Werkzeug
|
||
## dekodiert EINMAL pro Stroke und arbeitet flott darauf. Das Doc (svmap-Format)
|
||
## bleibt der Besitzer der Daten: vor einem Stroke dekodieren, nach dem Stroke
|
||
## mit encode() zurück ins Doc.
|
||
##
|
||
## Der eigentliche Doc-Mutator bleibt main.gd/terrain_tool.gd — hier nur Mathematik.
|
||
|
||
enum Mode { RAISE, LOWER, SMOOTH, PAINT, FLATTEN, RAMP }
|
||
|
||
## Editor-MVP-Perf-Limit (Karte t_9aba04f1): das Format erlaubt 17–513, aber
|
||
## der Editor formt im MVP nur Grids bis 129 (≈ 32k Dreiecke — flüssig beim
|
||
## Zeichnen, Ziel 60+ FPS). Neu-Maps werden direkt mit 129 angelegt.
|
||
const EDITOR_MAX_RESOLUTION := 129
|
||
|
||
|
||
## Höhe an Welt-X/Z auf einem bereits dekodierten Feld (bilinear).
|
||
## Gleiche Geometrie-Konvention wie SvmapDocument.terrain_height_at, aber ohne
|
||
## Base64-Neudekodierung pro Aufruf (macht den Pinsel flott).
|
||
static func height_at(heights: PackedFloat32Array, res: int, size_x: float, size_z: float,
|
||
x: float, z: float) -> float:
|
||
var last := float(res - 1)
|
||
var fx := clampf((x / size_x + 0.5) * last, 0.0, last)
|
||
var fz := clampf((z / size_z + 0.5) * last, 0.0, last)
|
||
var xi := int(floor(fx))
|
||
var zi := int(floor(fz))
|
||
var tx := fx - float(xi)
|
||
var tz := fz - float(zi)
|
||
var xi1 := mini(xi + 1, res - 1)
|
||
var zi1 := mini(zi + 1, res - 1)
|
||
var h00 := heights[zi * res + xi]
|
||
var h10 := heights[zi * res + xi1]
|
||
var h01 := heights[zi1 * res + xi]
|
||
var h11 := heights[zi1 * res + xi1]
|
||
return lerpf(lerpf(h00, h10, tx), lerpf(h01, h11, tx), tz)
|
||
|
||
|
||
## Pinsel-Gewicht über einen einzigen Regler (Karte 25.10.): t = 1 − d/radius,
|
||
## w = lerp(t, t·t, falloff). falloff 0 = linear (hart, breite Mitte),
|
||
## 1 = quadratisch (weich, spitze Mitte). Ausserhalb des Radius 0.
|
||
static func falloff_weight(d: float, radius: float, falloff: float) -> float:
|
||
if radius <= 0.0 or d >= radius:
|
||
return 0.0
|
||
var t := clampf(1.0 - d / radius, 0.0, 1.0)
|
||
return lerpf(t, t * t, clampf(falloff, 0.0, 1.0))
|
||
|
||
|
||
## Pinsel-Stärke aus Eingabe: Pen-Druck (>0) gewinnt, sonst Controller-Trigger
|
||
## (> 0.02), sonst Maus = 1.0. Immer 0.05…1.
|
||
static func pressure_of(pen: float, trigger: float) -> float:
|
||
if pen > 0.0:
|
||
return clampf(pen, 0.05, 1.0)
|
||
if trigger > 0.02:
|
||
return clampf(trigger, 0.05, 1.0)
|
||
return 1.0
|
||
|
||
|
||
## Flatten: zieht Höhen im Kreis gewichtet zur Zielhöhe. amount = Mischfaktor 0–1.
|
||
## (Eigene Funktion, damit apply_brush-Signatur stabil bleibt.)
|
||
static func apply_flatten(heights: PackedFloat32Array, res: int, size_x: float, size_z: float,
|
||
cx: float, cz: float, radius: float, target: float, amount: float,
|
||
falloff: float) -> bool:
|
||
if res < 2 or radius <= 0.0 or amount <= 0.0 or heights.size() < res * res:
|
||
return false
|
||
var cell_x := size_x / float(res - 1)
|
||
var cell_z := size_z / float(res - 1)
|
||
var gx := (cx / size_x + 0.5) * float(res - 1)
|
||
var gz := (cz / size_z + 0.5) * float(res - 1)
|
||
var x0 := maxi(int(floor(gx - radius / cell_x)) - 1, 0)
|
||
var x1 := mini(int(ceil(gx + radius / cell_x)) + 1, res - 1)
|
||
var z0 := maxi(int(floor(gz - radius / cell_z)) - 1, 0)
|
||
var z1 := mini(int(ceil(gz + radius / cell_z)) + 1, res - 1)
|
||
var changed := false
|
||
for z in range(z0, z1 + 1):
|
||
for x in range(x0, x1 + 1):
|
||
var dx := (float(x) - gx) * cell_x
|
||
var dz := (float(z) - gz) * cell_z
|
||
var w := falloff_weight(sqrt(dx * dx + dz * dz), radius, falloff)
|
||
if w <= 0.0:
|
||
continue
|
||
var idx := z * res + x
|
||
var h := heights[idx]
|
||
var nh := lerpf(h, target, clampf(amount * w, 0.0, 1.0))
|
||
if nh != h:
|
||
heights[idx] = nh
|
||
changed = true
|
||
return changed
|
||
|
||
|
||
## Rampe: Höhe linear von ha (Punkt a) nach hb (Punkt b), Breite `width` m
|
||
## quer zur Achse; Kern (halbe Breite) voll, Falloff am Rand. a/b = Welt-X/Z (Vector2). Rückgabe: geändert.
|
||
static func apply_ramp(heights: PackedFloat32Array, res: int, size_x: float, size_z: float,
|
||
a: Vector2, b: Vector2, ha: float, hb: float, width: float, falloff: float) -> bool:
|
||
if res < 2 or width <= 0.0 or heights.size() < res * res:
|
||
return false
|
||
var cell_x := size_x / float(res - 1)
|
||
var cell_z := size_z / float(res - 1)
|
||
var half := width * 0.5
|
||
var ab := b - a
|
||
var len2 := ab.length_squared()
|
||
var lo := Vector2(minf(a.x, b.x) - half, minf(a.y, b.y) - half)
|
||
var hi := Vector2(maxf(a.x, b.x) + half, maxf(a.y, b.y) + half)
|
||
var x0 := maxi(int(floor((lo.x / size_x + 0.5) * float(res - 1))) - 1, 0)
|
||
var x1 := mini(int(ceil((hi.x / size_x + 0.5) * float(res - 1))) + 1, res - 1)
|
||
var z0 := maxi(int(floor((lo.y / size_z + 0.5) * float(res - 1))) - 1, 0)
|
||
var z1 := mini(int(ceil((hi.y / size_z + 0.5) * float(res - 1))) + 1, res - 1)
|
||
var changed := false
|
||
for z in range(z0, z1 + 1):
|
||
for x in range(x0, x1 + 1):
|
||
var p := Vector2((float(x) / float(res - 1) - 0.5) * size_x,
|
||
(float(z) / float(res - 1) - 0.5) * size_z)
|
||
var s := 0.0 if len2 < 0.000001 else clampf((p - a).dot(ab) / len2, 0.0, 1.0)
|
||
var d := p.distance_to(a + ab * s)
|
||
# Innere Hälfte der Breite voll (ebene Fahrfläche), äußere Hälfte
|
||
# läuft mit dem Falloff-Regler ins Gelände aus.
|
||
var core := half * 0.5
|
||
var w := 1.0 if d <= core else falloff_weight(d - core, half - core, falloff)
|
||
if d >= half:
|
||
w = 0.0
|
||
if w <= 0.0:
|
||
continue
|
||
var idx := z * res + x
|
||
var h := heights[idx]
|
||
var nh := lerpf(h, lerpf(ha, hb, s), w)
|
||
if nh != h:
|
||
heights[idx] = nh
|
||
changed = true
|
||
return changed
|
||
|
||
|
||
## Pinsel anwenden. amount:
|
||
## RAISE/LOWER → Meter (bereits strength × dt vom Aufrufer),
|
||
## SMOOTH → Mischfaktor 0–1 (Anteil Richtung 3×3-Umgebungsmittel).
|
||
## Gewicht: Kosinus-Falloff (1 in der Mitte, 0 am Rand) — weiche Hügel statt
|
||
## Kegel. Rückgabe true, wenn mindestens ein Punkt verändert wurde.
|
||
static func apply_brush(heights: PackedFloat32Array, res: int, size_x: float, size_z: float,
|
||
hmin: float, hmax: float, cx: float, cz: float, radius: float,
|
||
mode: int, amount: float, falloff: float = -1.0, target: float = 0.0) -> bool:
|
||
## falloff < 0 = alter Kosinus-Falloff (Kompatibilität), sonst Regler-Falloff.
|
||
## target nur für FLATTEN (Zielhöhe).
|
||
if mode == Mode.RAMP:
|
||
return false # Rampe hat ein eigenes Zwei-Punkt-API (apply_ramp)
|
||
if mode == Mode.FLATTEN:
|
||
return apply_flatten(heights, res, size_x, size_z, cx, cz, radius, target,
|
||
clampf(amount, 0.0, 1.0), maxf(falloff, 0.0))
|
||
if mode == Mode.PAINT:
|
||
# PAINT läuft über apply_material_brush (Material-Schicht, Karte
|
||
# t_603ec1a8) — Höhe bleibt unangetastet. Hier ohne Material-Argument
|
||
# aufrufbar, aber bewusst NEUTRAL: kein Pattern, keine Höhe.
|
||
return false
|
||
if res < 2 or radius <= 0.0 or amount <= 0.0 or heights.size() < res * res:
|
||
return false
|
||
var cell_x := size_x / float(res - 1)
|
||
var cell_z := size_z / float(res - 1)
|
||
var r_cells_x := radius / cell_x
|
||
var r_cells_z := radius / cell_z
|
||
var gx := (cx / size_x + 0.5) * float(res - 1)
|
||
var gz := (cz / size_z + 0.5) * float(res - 1)
|
||
var x0 := maxi(int(floor(gx - r_cells_x)) - 1, 0)
|
||
var x1 := mini(int(ceil(gx + r_cells_x)) + 1, res - 1)
|
||
var z0 := maxi(int(floor(gz - r_cells_z)) - 1, 0)
|
||
var z1 := mini(int(ceil(gz + r_cells_z)) + 1, res - 1)
|
||
if mode == Mode.SMOOTH:
|
||
return _apply_smooth(heights, res, hmin, hmax, gx, gz, r_cells_x, r_cells_z,
|
||
x0, x1, z0, z1, clampf(amount, 0.0, 1.0), falloff)
|
||
var changed := false
|
||
var z := z0
|
||
while z <= z1:
|
||
var x := x0
|
||
while x <= x1:
|
||
var dx := (float(x) - gx) * cell_x
|
||
var dz := (float(z) - gz) * cell_z
|
||
var d := sqrt(dx * dx + dz * dz)
|
||
if d >= radius:
|
||
x += 1
|
||
continue
|
||
var w := 0.5 * (1.0 + cos(PI * d / radius)) if falloff < 0.0 \
|
||
else falloff_weight(d, radius, falloff)
|
||
var idx := z * res + x
|
||
var h := heights[idx]
|
||
var nh := h + amount * w if mode == Mode.RAISE else h - amount * w
|
||
nh = clampf(nh, hmin, hmax)
|
||
if nh != h:
|
||
heights[idx] = nh
|
||
changed = true
|
||
x += 1
|
||
z += 1
|
||
return changed
|
||
|
||
|
||
## Material-Pinsel (Karte t_603ec1a8): setzt die Material-ID aller Zellen im
|
||
## Kreis (d < radius) auf material_id — Malen statt Formen, Höhe bleibt unangetastet.
|
||
## Kante bewusst CRISP (d < radius): Malen fühlt sich wie ein Malprogramm an,
|
||
## die Vertex-Interpolation des Meshes glättet die Grenze optisch.
|
||
## mats = dekodierte Material-Schicht (PackedByteArray, res² Einträge).
|
||
static func apply_material_brush(mats: PackedByteArray, res: int, size_x: float, size_z: float,
|
||
cx: float, cz: float, radius: float, material_id: int) -> bool:
|
||
if res < 2 or radius <= 0.0 or mats.size() < res * res:
|
||
return false
|
||
var gx := (cx / size_x + 0.5) * float(res - 1)
|
||
var gz := (cz / size_z + 0.5) * float(res - 1)
|
||
var r_cells_x := radius / (size_x / float(res - 1))
|
||
var r_cells_z := radius / (size_z / float(res - 1))
|
||
var x0 := maxi(int(floor(gx - r_cells_x)) - 1, 0)
|
||
var x1 := mini(int(ceil(gx + r_cells_x)) + 1, res - 1)
|
||
var z0 := maxi(int(floor(gz - r_cells_z)) - 1, 0)
|
||
var z1 := mini(int(ceil(gz + r_cells_z)) + 1, res - 1)
|
||
var changed := false
|
||
var z := z0
|
||
while z <= z1:
|
||
var x := x0
|
||
while x <= x1:
|
||
var dx := (float(x) - gx) * (size_x / float(res - 1))
|
||
var dz := (float(z) - gz) * (size_z / float(res - 1))
|
||
if dx * dx + dz * dz < radius * radius:
|
||
var idx := z * res + x
|
||
if mats[idx] != material_id:
|
||
mats[idx] = material_id
|
||
changed = true
|
||
x += 1
|
||
z += 1
|
||
return changed
|
||
|
||
|
||
## Malen MIT weichem Rand (Karte 25.10., Sly: „weicher Übergang der Texturen
|
||
## am Rand des Malwerkzeugs“). Jeder Punkt kann zwei Materialien mischen:
|
||
## a = mats (Hauptschicht), b = mix_b, Anteil von b = mix_w/255. Innerhalb
|
||
## radius·(1−weichheit) wird voll gemalt, außen fällt der Anteil des neuen
|
||
## Materials weich auf 0 (Smoothstep). Ein Punkt wird nur verstärkt, nie
|
||
## abgeschwächt (Nachmalen am Rand macht ihn kräftiger, nicht schwächer).
|
||
## Mischung bleibt zweiteilig: das neue Material + das bisher stärkste andere
|
||
## (kommt ein drittes Material dazu, wird das schwächere der beiden ersetzt).
|
||
## staerke (0–1, Controller-Trigger/Pen-Druck) deckelt den Zielanteil.
|
||
static func apply_material_brush_mix(mats: PackedByteArray, mix_b: PackedByteArray,
|
||
mix_w: PackedByteArray, res: int, size_x: float, size_z: float,
|
||
cx: float, cz: float, radius: float, material_id: int, weichheit: float,
|
||
staerke: float = 1.0) -> bool:
|
||
if res < 2 or radius <= 0.0 or mats.size() < res * res \
|
||
or mix_b.size() < res * res or mix_w.size() < res * res:
|
||
return false
|
||
var soft := clampf(weichheit, 0.0, 1.0)
|
||
var inner := radius * (1.0 - soft)
|
||
var cell_x := size_x / float(res - 1)
|
||
var cell_z := size_z / float(res - 1)
|
||
var gx := (cx / size_x + 0.5) * float(res - 1)
|
||
var gz := (cz / size_z + 0.5) * float(res - 1)
|
||
var x0 := maxi(int(floor(gx - radius / cell_x)) - 1, 0)
|
||
var x1 := mini(int(ceil(gx + radius / cell_x)) + 1, res - 1)
|
||
var z0 := maxi(int(floor(gz - radius / cell_z)) - 1, 0)
|
||
var z1 := mini(int(ceil(gz + radius / cell_z)) + 1, res - 1)
|
||
var changed := false
|
||
for z in range(z0, z1 + 1):
|
||
for x in range(x0, x1 + 1):
|
||
var dx := (float(x) - gx) * cell_x
|
||
var dz := (float(z) - gz) * cell_z
|
||
var d := sqrt(dx * dx + dz * dz)
|
||
if d >= radius:
|
||
continue
|
||
var f := 1.0
|
||
if d > inner:
|
||
var t := clampf((d - inner) / maxf(radius - inner, 0.0001), 0.0, 1.0)
|
||
f = 1.0 - t * t * (3.0 - 2.0 * t)
|
||
var fb := int(roundf(f * clampf(staerke, 0.0, 1.0) * 255.0))
|
||
if fb <= 0:
|
||
continue
|
||
var idx := z * res + x
|
||
var a := int(mats[idx])
|
||
var b := int(mix_b[idx])
|
||
var w := int(mix_w[idx])
|
||
var anteil := (255 - w if a == material_id else 0) + (w if b == material_id else 0)
|
||
if fb <= anteil:
|
||
continue
|
||
if fb >= 255:
|
||
mats[idx] = material_id
|
||
mix_b[idx] = material_id
|
||
mix_w[idx] = 0
|
||
else:
|
||
# Ziel in keinem Slot → das SCHWÄCHERE Slot-Material wird ersetzt
|
||
# (Anteil von b = w/255, a hat 255−w): w ≥ 128 → b bleibt, sonst a.
|
||
var andere: int
|
||
if a == material_id:
|
||
andere = b
|
||
elif b == material_id:
|
||
andere = a
|
||
else:
|
||
andere = b if w >= 128 else a
|
||
mats[idx] = andere
|
||
mix_b[idx] = material_id
|
||
mix_w[idx] = fb
|
||
changed = true
|
||
return changed
|
||
|
||
|
||
## Glätten: Mischung Richtung 3×3-Umgebungsmittel (zweiphasig — das Mittel wird
|
||
## aus dem STAND der Feldes berechnet, sonst wandert die Welle in eine Richtung).
|
||
static func _apply_smooth(heights: PackedFloat32Array, res: int, hmin: float, hmax: float,
|
||
gx: float, gz: float, r_cells_x: float, r_cells_z: float,
|
||
x0: int, x1: int, z0: int, z1: int, amount: float, falloff: float = -1.0) -> bool:
|
||
# Mittelwerte nur für das Pinsel-Rechteck (statt Puffer in Feldgröße pro Schritt).
|
||
var bw := x1 - x0 + 1
|
||
var avg := PackedFloat32Array()
|
||
avg.resize(bw * (z1 - z0 + 1))
|
||
for z in range(z0, z1 + 1):
|
||
for x in range(x0, x1 + 1):
|
||
var sum := 0.0
|
||
var count := 0
|
||
for zi in range(maxi(z - 1, 0), mini(z + 1, res - 1) + 1):
|
||
for xi in range(maxi(x - 1, 0), mini(x + 1, res - 1) + 1):
|
||
sum += heights[zi * res + xi]
|
||
count += 1
|
||
avg[(z - z0) * bw + (x - x0)] = sum / float(maxi(count, 1))
|
||
var changed := false
|
||
for z in range(z0, z1 + 1):
|
||
for x in range(x0, x1 + 1):
|
||
var dx := (float(x) - gx) / maxf(r_cells_x, 0.0001)
|
||
var dz := (float(z) - gz) / maxf(r_cells_z, 0.0001)
|
||
var d := sqrt(dx * dx + dz * dz)
|
||
if d >= 1.0:
|
||
continue
|
||
var w := falloff_weight(d, 1.0, falloff)
|
||
var idx := z * res + x
|
||
var nh := lerpf(heights[idx], avg[(z - z0) * bw + (x - x0)], clampf(amount * w, 0.0, 1.0))
|
||
nh = clampf(nh, hmin, hmax)
|
||
if nh != heights[idx]:
|
||
heights[idx] = nh
|
||
changed = true
|
||
return changed
|
||
|
||
|
||
## Höhenfeld → Base64-Rohdaten (svmap-Format: uint16 LE, row-major).
|
||
## Rundung halbwegs offen (round) — Fehler ≤ ½ Quantisierungsschritt.
|
||
static func encode(heights: PackedFloat32Array, hmin: float, hmax: float) -> String:
|
||
var span := maxf(hmax - hmin, 0.000001)
|
||
var bytes := PackedByteArray()
|
||
bytes.resize(heights.size() * 2)
|
||
for i in heights.size():
|
||
var v := int(round(clampf((heights[i] - hmin) / span, 0.0, 1.0) * 65535.0))
|
||
bytes.encode_u16(i * 2, v)
|
||
return Marshalls.raw_to_base64(bytes)
|
||
|
||
|
||
## Cursor-Strahl → erster Treffer auf dem Höhenfeld (ray-march + Bisektion).
|
||
## Für den Pinsel: der Kreis muss UNTER dem Mauszeiger auf dem GELÄNDE liegen,
|
||
## nicht auf der flachen Basis-Ebene. Rückgabe null = kein Treffer.
|
||
static func raycast(heights: PackedFloat32Array, res: int, size_x: float, size_z: float,
|
||
origin: Vector3, dir: Vector3, max_dist: float = 0.0) -> Variant:
|
||
if res < 2 or heights.size() < res * res:
|
||
return null
|
||
if max_dist <= 0.0:
|
||
max_dist = maxf(size_x, size_z) * 3.0
|
||
var d := dir
|
||
if d.length_squared() < 0.0000001:
|
||
return null
|
||
d = d.normalized()
|
||
var step := maxf(size_x, size_z) / float(res - 1) * 0.5
|
||
step = clampf(step, 0.5, 4.0)
|
||
# Tangenten-Band (Karte t_234490cf, Teil 2 des Gerätebefunds): Ein Strahl,
|
||
# der EXAKT auf eine Hügelkuppe zielt, grast die gewölbte Oberfläche —
|
||
# f(t) bleibt minimal knapp ÜBER 0 am Kuppen-Scheitel, DANACH steigt f
|
||
# wieder, obwohl weiter hinten (verdeckt hinter dem Hügel) noch ein
|
||
# Vorzeichenwechsel an der Basis-Ebene käme. Regel „erster sichtbarer
|
||
# Oberflächen-Kontakt gewinnt": kommt f innerhalb des Bandes (½ Schritt)
|
||
# an die Oberfläche und entfernt sich der Strahl wieder, gilt die
|
||
# Oberfläche als getroffen (lokal verfeinert). Ein echter Treffer
|
||
# (f ≤ 0) gewinnt weiterhin immer.
|
||
var graze := step * 0.5
|
||
var best_t := -1.0
|
||
var best_f := INF
|
||
var t := 0.0
|
||
var prev_t := 0.0
|
||
var prev_f := INF
|
||
while t < max_dist:
|
||
t = minf(t + step, max_dist)
|
||
var f := _ray_error(heights, res, size_x, size_z, origin, d, t)
|
||
if f < best_f:
|
||
best_f = f
|
||
best_t = t
|
||
if f <= 0.0:
|
||
# Vorzeichenwechsel zwischen prev_t und t → Bisektion.
|
||
var lo := prev_t
|
||
var hi := t
|
||
for i in 18:
|
||
var mid := (lo + hi) * 0.5
|
||
if _ray_error(heights, res, size_x, size_z, origin, d, mid) <= 0.0:
|
||
hi = mid
|
||
else:
|
||
lo = mid
|
||
return origin + d * hi
|
||
# Tangenten-Erkennung (Karte t_234490cf): war der Strahl im Band an
|
||
# der Oberfläche und steigt f JETZT wieder, hat er die Oberfläche
|
||
# tangential berührt und entfernt sich — der erste sichtbare Kontakt
|
||
# war das Graze-Minimum. Ohne das fiele der Strahl über die Kuppe
|
||
# hinweg und träfe die (verdeckte) Basis-Ebene weit hinten.
|
||
if f > prev_f and best_f <= graze:
|
||
return _graze_hit(heights, res, size_x, size_z, origin, d,
|
||
best_t, best_f, step, max_dist)
|
||
prev_f = f
|
||
prev_t = t
|
||
# Band-Minimum direkt am March-Ende (kein steigender Sample mehr kam):
|
||
if best_f <= graze and best_t > 0.0:
|
||
return _graze_hit(heights, res, size_x, size_z, origin, d,
|
||
best_t, best_f, step, max_dist)
|
||
# Nichts getroffen → Basis-Ebene als Fallback (wie interaction._ground_point).
|
||
if absf(d.y) > 0.0001:
|
||
var plane_y: float = _min_h(heights)
|
||
var t_plane: float = (plane_y - origin.y) / d.y
|
||
if t_plane > 0.0:
|
||
return origin + d * t_plane
|
||
return null
|
||
|
||
|
||
## Tangenten-Treffer (Karte t_234490cf): dichteste Annäherung des Strahls an
|
||
## die Oberfläche lokal verfeinern (Ternärsuche um den besten March-Sample)
|
||
## und den Punkt auf dem Strahl zurückgeben — der x/z-Punkt liegt damit dort,
|
||
## wo der Cursor die Oberfläche sichtbar berührt, nicht meterweit dahinter.
|
||
static func _graze_hit(heights: PackedFloat32Array, res: int, size_x: float, size_z: float,
|
||
origin: Vector3, d: Vector3, best_t: float, _best_f: float,
|
||
step: float, max_dist: float) -> Vector3:
|
||
var glo := maxf(best_t - step, 0.0)
|
||
var ghi := minf(best_t + step, max_dist)
|
||
for i in 12:
|
||
var ga := glo + (ghi - glo) / 3.0
|
||
var gb := ghi - (ghi - glo) / 3.0
|
||
if _ray_error(heights, res, size_x, size_z, origin, d, ga) < _ray_error(heights, res, size_x, size_z, origin, d, gb):
|
||
ghi = gb
|
||
else:
|
||
glo = ga
|
||
return origin + d * ((glo + ghi) * 0.5)
|
||
|
||
|
||
## Minimum/Maximum des Feldes (PackedFloat32Array hat kein .min()/.max()).
|
||
static func _min_h(heights: PackedFloat32Array) -> float:
|
||
var out := INF
|
||
for v in heights:
|
||
if v < out:
|
||
out = v
|
||
return out
|
||
|
||
|
||
static func _max_h(heights: PackedFloat32Array) -> float:
|
||
var out := -INF
|
||
for v in heights:
|
||
if v > out:
|
||
out = v
|
||
return out
|
||
|
||
|
||
## f(t) < 0 → Strahl unter dem Terrain. (prev_f-Nutzung vermieden Godot-Warnung.)
|
||
static func _ray_error(heights: PackedFloat32Array, res: int, size_x: float, size_z: float,
|
||
origin: Vector3, dir: Vector3, t: float) -> float:
|
||
var p := origin + dir * t
|
||
return p.y - height_at(heights, res, size_x, size_z, p.x, p.z)
|