Mondherz-Ancients (Saumkern-Kristall als Herz, Sichelklingen, Ringe, Krone, Teamwappen, Energieadern) mit den Stufen stabil/instabil/kritisch, kurzem Zusammenbruch und bleibender Ruine; der Sieg wartet nie auf die Grafik. Turmfamilie aus einem Blender-Generator: Grenzwacht (Tower 1), Bastion (Tower 2), Kernwacht (Basisturm in der Formensprache des Ancients), mit Schadensstufen, Einsturz, Ruine und sichtbarem Angriff (Zielring, Aufladen, Bolzen, Einschlag) - der Schaden bleibt vollständig in der Simulation. Bossgrotte Mondschlund mit Tor, Treppe in die Tiefe, Altar, Säulen, Felsen und fünf Zuständen; der Generator hält Abstand zur Top-Lane, die Höhle braucht eine kleine Geländesenke (nur Darstellung). Superboss Schlundkrone mit 24 Knochen und 11 Clips (inkl. Brüllen und Sieg), Adern in Teamfarbe, dasselbe Modell für Grotte und Lane. Gemeinsame LN-Materialfamilie (Stein, Metall, Kristall, Rune, Stoff, Mondwasser, Kreaturenhaut), Occlusion-Fade ohne dithernde Schatten. Keine Regeländerung. Geprüft: verify_g2 245/245, verify_g1 443/443, Regeln 91/91 und 73/73, Stress 17/17, Balance 40 Partien identisch mit G1, verify_m1 38/38, verify_m2 14/14, Netz 43/43 und Loopback 12/12, Legacy 209 und 463. 21 Ansichten vorher/nachher (1920x1080), Videos Turmkampf und Bossbegegnung, Leistung G1/G2 auf demselben Mac (+0,5 ms in vollen Kampfansichten). Pakete 0.17.0-g2 für macOS (Universal, ad-hoc signiert), Windows x64/ARM64 und Linux; macOS nativ und unter Rosetta, beide Windows-Pakete in Windows 11 ARM (Parallels, OpenGL-Fallback) mit --lune-smoke bestanden. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
434 lines
18 KiB
GDScript
434 lines
18 KiB
GDScript
class_name LuneTerrain
|
|
extends RefCounted
|
|
## Visual height field of the G1 map, derived from LuneMap alone. Every term depends on
|
|
## the axis coordinate u = (x + z) / √2 and the distance a = |z - x| / √2 from the axis,
|
|
## or on symmetric map features, so the ground is the exact mirror image across x = z.
|
|
## The simulation stays two-dimensional: views read walk_height() to stand units on it.
|
|
##
|
|
## Regions: the north-western highland (Top) behind an escarpment, the central valley
|
|
## with the Kluft river on the axis, the south-eastern lowland (Bottom) with the delta,
|
|
## base plateaus, the grotto terrace and a ring of mountains around the playable square.
|
|
|
|
const EXTENT: float = 60.0
|
|
const STEP: float = 1.0
|
|
## Coarse outer ring (mountains and the river outlet) around the fine grid.
|
|
const OUTER_EXTENT: float = 132.0
|
|
const OUTER_STEP: float = 4.0
|
|
const OUTER_HOLE: float = 56.0
|
|
const WATER_LEVEL: float = -0.35
|
|
const BED_LEVEL: float = -1.2
|
|
const BASE_HEIGHT: float = 0.6
|
|
const BASE_RADIUS: float = 10.6
|
|
const HIGHLAND: float = 3.0
|
|
const VALLEY: float = 0.18
|
|
const LOWLAND: float = -0.1
|
|
const ARENA_HEIGHT: float = 1.7
|
|
const ARENA_RADIUS: float = 7.8
|
|
## Cave notch behind the grotto gate in the grotto's axis frame: u from, u to, half width,
|
|
## depth below the terrace (G2, matches build_bossgrotte.py).
|
|
const CAVE_NOTCH := Vector4(-11.5, -8.5, 1.8, 1.05)
|
|
const PLATFORM_HEIGHT: float = 0.62
|
|
const PLATFORM_RADIUS: float = 6.6
|
|
const FORD_HEIGHT: float = -0.16
|
|
## Lane corridor half widths (flat ground under the paving) per lane.
|
|
const CORRIDOR: Dictionary = {"top": 3.6, "mid": 4.5, "bot": 3.6}
|
|
const BLEND: float = 5.0
|
|
## River half width along the axis coordinate u (piecewise linear).
|
|
const RIVER_WIDTH: Array[Vector2] = [Vector2(-11.6, 1.6), Vector2(-9.0, 2.4), Vector2(-4.0, 3.1), Vector2(0.0, 3.5),
|
|
Vector2(12.0, 3.0), Vector2(26.0, 3.5), Vector2(32.0, 5.0), Vector2(36.5, 7.2), Vector2(42.0, 13.0), Vector2(60.0, 20.0)]
|
|
const RIVER_START: float = -11.6
|
|
|
|
var map: LuneMap
|
|
var size: int = 0
|
|
var ground := PackedFloat32Array()
|
|
var walk := PackedFloat32Array()
|
|
var colors := PackedColorArray()
|
|
## lane -> PackedFloat32Array of the lane surface height per metre of lane coordinate.
|
|
var lane_profiles: Dictionary = {}
|
|
var _noise := FastNoiseLite.new()
|
|
var _detail := FastNoiseLite.new()
|
|
var _segments: Array = []
|
|
var _buckets: Dictionary = {}
|
|
## [position, height] of every tower: a flat pad at the height of the lane beside it.
|
|
var _pads: Array = []
|
|
const BUCKET: float = 6.0
|
|
|
|
static var _cache: LuneTerrain
|
|
|
|
## One terrain per map layout and process; a rematch reuses it.
|
|
static func for_map(match_map: LuneMap) -> LuneTerrain:
|
|
if _cache == null:
|
|
_cache = LuneTerrain.new()
|
|
_cache.build(match_map)
|
|
return _cache
|
|
|
|
func build(match_map: LuneMap) -> void:
|
|
map = match_map
|
|
_noise.seed = 4417
|
|
_noise.frequency = 0.045
|
|
_noise.fractal_octaves = 3
|
|
_detail.seed = 911
|
|
_detail.frequency = 0.21
|
|
_detail.fractal_octaves = 2
|
|
for lane: String in LuneMap.LANES:
|
|
lane_profiles[lane] = _lane_profile(lane)
|
|
_index_segments()
|
|
for definition: Dictionary in map.buildings:
|
|
if str(definition.tier) in ["inner", "outer"]:
|
|
_pads.append([Vector2(definition.position), lane_height(str(definition.lane), map.s_for(int(definition.team), str(definition.lane), float(definition.progress)))])
|
|
elif str(definition.tier) == "base":
|
|
_pads.append([Vector2(definition.position), BASE_HEIGHT])
|
|
var grid: Dictionary = _grid(EXTENT, STEP, 0.0)
|
|
size = grid.size
|
|
ground = grid.ground
|
|
walk = grid.walk
|
|
colors = grid.colors
|
|
|
|
## Evaluates a square grid. Only the Blau side (z >= x) and the axis are computed; the
|
|
## grid is symmetric under i <-> j, so the Rot side is copied. `hole` skips the interior
|
|
## (used by the coarse outer ring, which also sinks slightly where it overlaps).
|
|
func _grid(extent: float, step: float, hole: float) -> Dictionary:
|
|
var n: int = int(round(extent * 2.0 / step)) + 1
|
|
var heights := PackedFloat32Array()
|
|
heights.resize(n * n)
|
|
var walks := PackedFloat32Array()
|
|
walks.resize(n * n)
|
|
var lane_distance := PackedFloat32Array()
|
|
lane_distance.resize(n * n)
|
|
var carved := PackedByteArray()
|
|
carved.resize(n * n)
|
|
for j: int in range(n):
|
|
for i: int in range(j + 1):
|
|
var p := Vector2(-extent + i * step, -extent + j * step)
|
|
if hole > 0.0 and maxf(absf(p.x), absf(p.y)) < hole - step:
|
|
continue
|
|
var sample: Array = _evaluate(p)
|
|
var height: float = sample[0]
|
|
if hole > 0.0 and maxf(absf(p.x), absf(p.y)) < EXTENT + 0.5:
|
|
height -= 0.5
|
|
for index: int in [j * n + i, i * n + j]:
|
|
heights[index] = height
|
|
walks[index] = sample[1]
|
|
lane_distance[index] = sample[2]
|
|
carved[index] = sample[3]
|
|
var grid_colors := PackedColorArray()
|
|
grid_colors.resize(n * n)
|
|
for j: int in range(n):
|
|
for i: int in range(j + 1):
|
|
var index: int = j * n + i
|
|
var left: float = heights[j * n + maxi(0, i - 1)]
|
|
var right: float = heights[j * n + mini(n - 1, i + 1)]
|
|
var up: float = heights[maxi(0, j - 1) * n + i]
|
|
var down: float = heights[mini(n - 1, j + 1) * n + i]
|
|
var slope: float = Vector2(right - left, down - up).length() / (2.0 * step)
|
|
var color: Color = _color(Vector2(-extent + i * step, -extent + j * step), heights[index], slope, lane_distance[index], carved[index] == 1)
|
|
grid_colors[index] = color
|
|
grid_colors[i * n + j] = color
|
|
return {"size": n, "extent": extent, "step": step, "hole": hole, "ground": heights, "walk": walks, "colors": grid_colors}
|
|
|
|
# ---------------------------------------------------------------- queries
|
|
static func axis_u(p: Vector2) -> float:
|
|
return (p.x + p.y) * 0.70710678
|
|
|
|
static func axis_a(p: Vector2) -> float:
|
|
return absf(p.y - p.x) * 0.70710678
|
|
|
|
func height_at(p: Vector2) -> float:
|
|
return _bilinear(ground, p)
|
|
|
|
## Height a unit stands on: the lane decks (bridges, ford, platform) instead of the river bed.
|
|
func walk_height(p: Vector2) -> float:
|
|
return _bilinear(walk, p)
|
|
|
|
func lane_height(lane: String, s: float) -> float:
|
|
var profile: PackedFloat32Array = lane_profiles[lane]
|
|
var f: float = clampf(s, 0.0, float(profile.size() - 1))
|
|
var i: int = mini(int(f), profile.size() - 2)
|
|
return lerpf(profile[i], profile[i + 1], f - i)
|
|
|
|
func river_half_width(u: float) -> float:
|
|
if u < RIVER_START:
|
|
return 0.0
|
|
for k: int in range(1, RIVER_WIDTH.size()):
|
|
if u <= RIVER_WIDTH[k].x:
|
|
var a: Vector2 = RIVER_WIDTH[k - 1]
|
|
var b: Vector2 = RIVER_WIDTH[k]
|
|
return lerpf(a.y, b.y, clampf((u - a.x) / (b.x - a.x), 0.0, 1.0))
|
|
return RIVER_WIDTH[RIVER_WIDTH.size() - 1].y
|
|
|
|
func is_water(p: Vector2) -> bool:
|
|
return height_at(p) < WATER_LEVEL - 0.02
|
|
|
|
func _bilinear(field: PackedFloat32Array, p: Vector2) -> float:
|
|
var fx: float = clampf((p.x + EXTENT) / STEP, 0.0, size - 1.001)
|
|
var fz: float = clampf((p.y + EXTENT) / STEP, 0.0, size - 1.001)
|
|
var i: int = int(fx)
|
|
var j: int = int(fz)
|
|
var tx: float = fx - i
|
|
var tz: float = fz - j
|
|
var a: float = lerpf(field[j * size + i], field[j * size + i + 1], tx)
|
|
var b: float = lerpf(field[(j + 1) * size + i], field[(j + 1) * size + i + 1], tx)
|
|
return lerpf(a, b, tz)
|
|
|
|
# ---------------------------------------------------------------- regions
|
|
func escarpment(a: float) -> float:
|
|
return -27.4 + 4.6 * smoothstep(6.0, 20.0, a) + 1.1 * sin(a * 0.43)
|
|
|
|
## Ground before lanes, river and arena: highland, valley, lowland, bases, mountains.
|
|
func _region(p: Vector2) -> float:
|
|
var u: float = axis_u(p)
|
|
var a: float = axis_a(p)
|
|
var edge: float = escarpment(a)
|
|
var hills: float = _noise.get_noise_2d(u, a)
|
|
var height: float = lerpf(HIGHLAND + hills * 0.5, VALLEY + hills * 0.35, smoothstep(edge - 1.3, edge + 1.3, u))
|
|
height = lerpf(height, LOWLAND + hills * 0.22, smoothstep(20.0, 33.0, u))
|
|
# The lookout hill beyond the Top corner.
|
|
height += 1.1 * (1.0 - smoothstep(4.0, 11.0, p.distance_to(Vector2(-31.0, -31.0))))
|
|
for team: int in range(2):
|
|
var d: float = p.distance_to(map.core_position(team))
|
|
height = lerpf(BASE_HEIGHT, height, smoothstep(BASE_RADIUS, BASE_RADIUS + 3.0, d))
|
|
# Mountains around the playable square: a rounded square (superellipse, symmetric under
|
|
# x <-> z) with a noisy foot line, so the frame does not read as a straight wall.
|
|
var border: float = pow(pow(absf(p.x), 5.0) + pow(absf(p.y), 5.0), 0.2) + hills * 3.4 + _detail.get_noise_2d(u * 0.35, a * 0.35) * 1.8
|
|
if border > 39.0:
|
|
var over: float = border - 39.0
|
|
height += minf(11.0, over * 0.62 + over * over * 0.035) * (0.75 + 0.25 * _detail.get_noise_2d(u * 0.5, a * 0.5))
|
|
return height
|
|
|
|
func _arena_blend(p: Vector2, height: float) -> float:
|
|
var d: float = p.distance_to(LuneMap.GROTTO)
|
|
if d > ARENA_RADIUS + 5.0:
|
|
return height
|
|
return _cave_notch(p, lerpf(ARENA_HEIGHT, height, smoothstep(ARENA_RADIUS, ARENA_RADIUS + 3.5, d)))
|
|
|
|
## G2: the cave behind the grotto gate reaches about a metre below the terrace, so its stairs
|
|
## can lead down (presentation only; the rock shell of the grotto hides the notch walls and no
|
|
## figure ever stands there). Symmetric about the axis like everything else.
|
|
func _cave_notch(p: Vector2, height: float) -> float:
|
|
var u: float = axis_u(p) - axis_u(LuneMap.GROTTO)
|
|
var a: float = axis_a(p)
|
|
if u < CAVE_NOTCH.x - .6 or u > CAVE_NOTCH.y + .6 or a > CAVE_NOTCH.z + .6:
|
|
return height
|
|
var inside: float = smoothstep(CAVE_NOTCH.x - .4, CAVE_NOTCH.x, u) * (1.0 - smoothstep(CAVE_NOTCH.y, CAVE_NOTCH.y + .4, u)) \
|
|
* (1.0 - smoothstep(CAVE_NOTCH.z, CAVE_NOTCH.z + .4, a))
|
|
return lerpf(height, minf(height, ARENA_HEIGHT - CAVE_NOTCH.w), inside)
|
|
|
|
## Lane surface height: the smoothed ground along the lane, lifted onto platform and ford.
|
|
func _lane_profile(lane: String) -> PackedFloat32Array:
|
|
var path: LuneMap.LanePath = map.lanes[lane]
|
|
var count: int = int(ceil(path.length)) + 1
|
|
var raw := PackedFloat32Array()
|
|
raw.resize(count)
|
|
for k: int in range(count):
|
|
raw[k] = _arena_blend(path.point_at(float(k)), _region(path.point_at(float(k))))
|
|
var smooth: PackedFloat32Array = raw
|
|
for pass_index: int in range(3):
|
|
var next := PackedFloat32Array()
|
|
next.resize(count)
|
|
for k: int in range(count):
|
|
var total: float = 0.0
|
|
var weight: float = 0.0
|
|
for o: int in range(-6, 7):
|
|
var m: int = clampi(k + o, 0, count - 1)
|
|
total += smooth[m]
|
|
weight += 1.0
|
|
next[k] = total / weight
|
|
smooth = next
|
|
var middle: float = path.length * 0.5
|
|
for k: int in range(count):
|
|
var from_middle: float = absf(float(k) - middle)
|
|
# The lane leaves the base on the plateau and only then descends.
|
|
var at: Vector2 = path.point_at(float(k))
|
|
var core_d: float = minf(at.distance_to(map.core_position(0)), at.distance_to(map.core_position(1)))
|
|
smooth[k] = lerpf(BASE_HEIGHT, smooth[k], smoothstep(BASE_RADIUS - 1.0, BASE_RADIUS + 5.0, core_d))
|
|
if lane == "mid":
|
|
smooth[k] = lerpf(PLATFORM_HEIGHT, smooth[k], smoothstep(PLATFORM_RADIUS - 1.0, PLATFORM_RADIUS + 7.0, from_middle))
|
|
elif lane == "bot":
|
|
smooth[k] = lerpf(FORD_HEIGHT, smooth[k], smoothstep(6.0, 16.0, from_middle))
|
|
smooth[k] = maxf(smooth[k], WATER_LEVEL + 0.18)
|
|
return smooth
|
|
|
|
# ---------------------------------------------------------------- lanes (spatial index)
|
|
func _index_segments() -> void:
|
|
for lane: String in LuneMap.LANES:
|
|
var path: LuneMap.LanePath = map.lanes[lane]
|
|
for i: int in range(1, path.points.size()):
|
|
var a: Vector2 = path.points[i - 1]
|
|
var b: Vector2 = path.points[i]
|
|
var index: int = _segments.size()
|
|
_segments.append([a, b, lane, float(path.cumulative[i - 1]), a.distance_to(b)])
|
|
var reach: float = float(CORRIDOR[lane]) + BLEND + 1.0
|
|
var lo := Vector2(minf(a.x, b.x) - reach, minf(a.y, b.y) - reach)
|
|
var hi := Vector2(maxf(a.x, b.x) + reach, maxf(a.y, b.y) + reach)
|
|
for bx: int in range(floori(lo.x / BUCKET), floori(hi.x / BUCKET) + 1):
|
|
for bz: int in range(floori(lo.y / BUCKET), floori(hi.y / BUCKET) + 1):
|
|
var key := Vector2i(bx, bz)
|
|
if not _buckets.has(key):
|
|
_buckets[key] = []
|
|
(_buckets[key] as Array).append(index)
|
|
|
|
## [distance, lane, lane coordinate] of the closest lane centre line, or [INF].
|
|
func nearest_lane(p: Vector2) -> Array:
|
|
var key := Vector2i(floori(p.x / BUCKET), floori(p.y / BUCKET))
|
|
if not _buckets.has(key):
|
|
return [INF, "", 0.0]
|
|
var best: float = INF
|
|
var best_lane: String = ""
|
|
var best_s: float = 0.0
|
|
for index: int in _buckets[key]:
|
|
var segment: Array = _segments[index]
|
|
var a: Vector2 = segment[0]
|
|
var ab: Vector2 = (segment[1] as Vector2) - a
|
|
var t: float = clampf((p - a).dot(ab) / maxf(0.0001, ab.length_squared()), 0.0, 1.0)
|
|
var d: float = p.distance_to(a + ab * t)
|
|
if d < best:
|
|
best = d
|
|
best_lane = segment[2]
|
|
best_s = float(segment[3]) + float(segment[4]) * t
|
|
return [best, best_lane, best_s]
|
|
|
|
# ---------------------------------------------------------------- one grid point
|
|
func _evaluate(p: Vector2) -> Array:
|
|
var u: float = axis_u(p)
|
|
var a: float = axis_a(p)
|
|
var height: float = _arena_blend(p, _region(p))
|
|
var lane_info: Array = nearest_lane(p)
|
|
var lane_d: float = lane_info[0]
|
|
var on_lane: bool = false
|
|
var lane_surface: float = 0.0
|
|
if lane_d < INF:
|
|
var corridor: float = CORRIDOR[lane_info[1]]
|
|
lane_surface = lane_height(lane_info[1], lane_info[2])
|
|
height = lerpf(lane_surface, height, smoothstep(corridor, corridor + BLEND, lane_d))
|
|
on_lane = lane_d < corridor
|
|
for pad: Array in _pads:
|
|
var pad_d: float = p.distance_to(pad[0])
|
|
if pad_d < 3.8:
|
|
height = lerpf(float(pad[1]), height, smoothstep(2.3, 3.8, pad_d))
|
|
# The river is carved last so bridges and the ford span open water.
|
|
var width: float = river_half_width(u)
|
|
var carved: bool = false
|
|
if width > 0.0:
|
|
var bank: float = lerpf(BED_LEVEL, height, smoothstep(width - 0.9, width + 1.9, a))
|
|
# Round spring pool at the foot of the grotto terrace.
|
|
var start: float = smoothstep(RIVER_START - 0.2, RIVER_START + 1.6, u)
|
|
bank = lerpf(height, bank, start)
|
|
if bank < height:
|
|
carved = height - bank > 0.25
|
|
height = bank
|
|
var walk_height: float = lane_surface if on_lane else maxf(height, WATER_LEVEL)
|
|
if p.length() < PLATFORM_RADIUS:
|
|
walk_height = maxf(walk_height, PLATFORM_HEIGHT)
|
|
return [height, walk_height, lane_d, 1 if carved else 0]
|
|
|
|
## Blockout palette per zone; alpha carries rockiness for the terrain shader.
|
|
func _color(p: Vector2, height: float, slope: float, lane_d: float, carved: bool) -> Color:
|
|
var u: float = axis_u(p)
|
|
var a: float = axis_a(p)
|
|
var variation: float = _detail.get_noise_2d(u * 1.7, a * 1.7)
|
|
var grass := Color(.2, .31, .12).lerp(Color(.25, .33, .14), variation * .5 + .5)
|
|
var edge: float = escarpment(a)
|
|
var color: Color = grass
|
|
var rock: float = 0.0
|
|
if u < edge + 1.4:
|
|
color = Color(.29, .32, .17).lerp(Color(.35, .34, .25), clampf(variation * 1.4, 0.0, 1.0))
|
|
rock = clampf(variation * 1.2, 0.0, .6)
|
|
if u > 20.0:
|
|
color = color.lerp(Color(.17, .25, .12).lerp(Color(.24, .22, .15), clampf(-variation, 0.0, 1.0)), smoothstep(20.0, 30.0, u))
|
|
# Steep ground (escarpment, terrace walls, mountains) reads as rock.
|
|
if slope > 0.55:
|
|
color = color.lerp(Color(.37, .34, .29), smoothstep(0.55, 1.1, slope))
|
|
rock = maxf(rock, smoothstep(0.55, 1.1, slope))
|
|
if maxf(absf(p.x), absf(p.y)) > 40.0:
|
|
rock = maxf(rock, 0.35 + variation * 0.3)
|
|
if carved or height < WATER_LEVEL + 0.25:
|
|
color = Color(.31, .29, .21).lerp(Color(.15, .16, .12), clampf((WATER_LEVEL + 0.2 - height) / 0.8, 0.0, 1.0))
|
|
rock = 0.25
|
|
if lane_d < 5.2:
|
|
color = color.lerp(Color(.31, .26, .18), 1.0 - smoothstep(3.8, 5.2, lane_d))
|
|
for team: int in range(2):
|
|
if p.distance_to(map.core_position(team)) < BASE_RADIUS + 0.3:
|
|
color = Color(.41, .38, .32).lerp(Color(.36, .33, .28), variation * .5 + .5)
|
|
rock = 0.45
|
|
if p.distance_to(LuneMap.GROTTO) < ARENA_RADIUS + 0.2:
|
|
color = Color(.36, .34, .33)
|
|
rock = 0.55
|
|
# The palette is authored in sRGB; vertex colours reach the shader as linear values.
|
|
var linear: Color = Color(color.r, color.g, color.b).srgb_to_linear()
|
|
linear.a = rock
|
|
return linear
|
|
|
|
# ---------------------------------------------------------------- mesh
|
|
var _inner_mesh: ArrayMesh
|
|
var _outer_mesh: ArrayMesh
|
|
|
|
func build_mesh() -> ArrayMesh:
|
|
return _mesh({"size": size, "extent": EXTENT, "step": STEP, "hole": 0.0, "ground": ground, "colors": colors})
|
|
|
|
## The distant mountains and the river outlet at 4 m resolution, with a hole for the fine grid.
|
|
func build_outer_mesh() -> ArrayMesh:
|
|
return _mesh(_grid(OUTER_EXTENT, OUTER_STEP, OUTER_HOLE))
|
|
|
|
## Cached meshes: a rematch or a second match in the same process reuses them.
|
|
func inner_mesh() -> ArrayMesh:
|
|
if _inner_mesh == null:
|
|
_inner_mesh = build_mesh()
|
|
return _inner_mesh
|
|
|
|
func outer_mesh() -> ArrayMesh:
|
|
if _outer_mesh == null:
|
|
_outer_mesh = build_outer_mesh()
|
|
return _outer_mesh
|
|
|
|
func _mesh(grid: Dictionary) -> ArrayMesh:
|
|
var n: int = grid.size
|
|
var extent: float = grid.extent
|
|
var step: float = grid.step
|
|
var hole: float = grid.hole
|
|
var heights: PackedFloat32Array = grid.ground
|
|
var vertices := PackedVector3Array()
|
|
var normals := PackedVector3Array()
|
|
vertices.resize(n * n)
|
|
normals.resize(n * n)
|
|
for j: int in range(n):
|
|
for i: int in range(n):
|
|
var index: int = j * n + i
|
|
vertices[index] = Vector3(-extent + i * step, heights[index], -extent + j * step)
|
|
var left: float = heights[j * n + maxi(0, i - 1)]
|
|
var right: float = heights[j * n + mini(n - 1, i + 1)]
|
|
var up: float = heights[maxi(0, j - 1) * n + i]
|
|
var down: float = heights[mini(n - 1, j + 1) * n + i]
|
|
normals[index] = Vector3(left - right, 2.0 * step, up - down).normalized()
|
|
var indices := PackedInt32Array()
|
|
indices.resize((n - 1) * (n - 1) * 6)
|
|
var cursor: int = 0
|
|
for j: int in range(n - 1):
|
|
for i: int in range(n - 1):
|
|
if hole > 0.0:
|
|
var cx: float = absf(-extent + (i + 0.5) * step)
|
|
var cz: float = absf(-extent + (j + 0.5) * step)
|
|
if maxf(cx, cz) < hole:
|
|
continue
|
|
var a: int = j * n + i
|
|
var b: int = a + 1
|
|
var c: int = a + n
|
|
var d: int = c + 1
|
|
# Alternating diagonals; cell (i, j) and its mirror (j, i) share the parity,
|
|
# so mirrored cells triangulate as mirror images (clockwise front faces).
|
|
if (i + j) % 2 == 0:
|
|
indices[cursor] = a; indices[cursor + 1] = b; indices[cursor + 2] = d
|
|
indices[cursor + 3] = a; indices[cursor + 4] = d; indices[cursor + 5] = c
|
|
else:
|
|
indices[cursor] = a; indices[cursor + 1] = b; indices[cursor + 2] = c
|
|
indices[cursor + 3] = b; indices[cursor + 4] = d; indices[cursor + 5] = c
|
|
cursor += 6
|
|
indices.resize(cursor)
|
|
var arrays: Array = []
|
|
arrays.resize(Mesh.ARRAY_MAX)
|
|
arrays[Mesh.ARRAY_VERTEX] = vertices
|
|
arrays[Mesh.ARRAY_NORMAL] = normals
|
|
arrays[Mesh.ARRAY_COLOR] = grid.colors
|
|
arrays[Mesh.ARRAY_INDEX] = indices
|
|
var mesh := ArrayMesh.new()
|
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
|
return mesh
|