Scenery (all procedural, seeded, no imported assets): - Terrain shader: grass/dry grass/dirt/sand/rock strata/snow + detail normals - 5 tree types, faceted mossy rocks, cliff outcrops, sea stacks, bushes, logs and stumps, batched as MultiMesh per 48 m cell - Wind-animated grass and flowers streamed in tiles around the player - Lake (depth-shaded water with shore foam), ocean to the horizon, cloud sky shader, volumetric fog, glow, softer shadows - Swimming: float at the surface, swim-jump to climb out World bounds: - Steep mountain rim with an invisible wall partway up; one side opens to a sheer sea cliff with the wall 1 m inside the edge; whole chunk is an island so the mesh edge is never visible - Respawn safety net if the player leaves the walkable area or the map - Fix: the cliff edge now tracks the wall in the headlands (it used to drop away inside the wall, leaving a pit with lake water hanging over it) Tests: tests/test_boundary.gd (24-direction escape attempt) and tests/test_terrain_edges.gd (no ground below waterline near the wall). Upgrades the project to Godot 4.7 (project.godot rewritten by the 4.7 importer, .uid files added) and documents it in the setup guide.
461 lines
17 KiB
GDScript
461 lines
17 KiB
GDScript
extends Node3D
|
|
## Terrain generator.
|
|
##
|
|
## One heightmap chunk built from deterministic noise: a walkable valley of
|
|
## rolling hills with a lake, enclosed by a ring of steep mountains that opens on
|
|
## one side to a sheer sea cliff. The whole chunk is an island: its edge sinks
|
|
## into ocean so the end of the mesh is never visible.
|
|
##
|
|
## The rim is the world boundary: mountain slopes are too steep to walk up
|
|
## (> floor_max_angle), and an invisible wall stands partway up them, or just
|
|
## inside the cliff edge on the sea side, so nobody can climb out or fall off. Chunk
|
|
## streaming and biomes come in later milestones (docs/ARCHITECTURE.md §4).
|
|
##
|
|
## Heights are computed once into a grid that feeds the render mesh, the
|
|
## collision shape and `sample_height()`, so props and the player sit exactly on
|
|
## the visible surface.
|
|
##
|
|
## Collision is a HeightMapShape3D. Do NOT swap it for a concave trimesh: those
|
|
## are one-sided and the player fell straight through them from above.
|
|
|
|
signal ready_built ## emitted once the mesh + collision exist
|
|
|
|
@export var size: int = 320 ## chunk is size x size metres
|
|
@export var resolution: int = 256 ## grid cells per side
|
|
@export var height_scale: float = 9.0 ## rolling-hill amplitude in the valley
|
|
@export var noise_seed: int = 1337
|
|
|
|
@export_group("Boundary")
|
|
@export var play_radius: float = 100.0 ## where the mountain rim starts rising
|
|
@export var rim_width: float = 30.0 ## horizontal run of the rim's steep face
|
|
@export var mountain_height: float = 58.0
|
|
@export var rim_warp: float = 16.0 ## how irregular the rim's outline is
|
|
@export_range(0.2, 0.9) var wall_frac: float = 0.45 ## invisible wall, as a fraction up the rim
|
|
|
|
@export_group("Sea cliff")
|
|
@export var sea_angle_deg: float = -75.0 ## direction of the coast (atan2(z, x); -90 = straight -Z)
|
|
@export var sea_half_width_deg: float = 38.0 ## full-sea arc either side of sea_angle
|
|
@export var sea_fade_deg: float = 22.0 ## headland transition back to mountains
|
|
@export var sea_level: float = -24.0
|
|
@export var cliff_top: float = 0.5 ## height the coast rises/falls to at the edge
|
|
@export var cliff_offset: float = 4.0 ## cliff edge, in rim distance past play_radius
|
|
@export var cliff_drop_width: float = 6.0 ## horizontal run of the drop (smaller = sheerer)
|
|
|
|
@export_group("Water & snow")
|
|
@export var water_level: float = -2.6
|
|
@export var lake_center: Vector2 = Vector2(14.0, -46.0)
|
|
@export var lake_radius: float = 24.0
|
|
@export var snow_height: float = 40.0
|
|
|
|
@export_group("Colours")
|
|
@export var grass_lush: Color = Color(0.25, 0.40, 0.15)
|
|
@export var grass_dry: Color = Color(0.42, 0.49, 0.21)
|
|
|
|
var built: bool = false
|
|
var min_height: float = 0.0
|
|
var max_height: float = 0.0
|
|
var step: float = 1.0
|
|
var half: float = 0.0
|
|
var material: ShaderMaterial
|
|
var noise_tex: NoiseTexture2D ## shared by every world shader (see common.gdshaderinc)
|
|
var normal_tex: NoiseTexture2D ## detail normal map for ground and water
|
|
|
|
var _noise: FastNoiseLite
|
|
var _detail: FastNoiseLite
|
|
var _ridge: FastNoiseLite
|
|
var _warp: FastNoiseLite
|
|
var _wall_ring: Array[Vector3] = []
|
|
var _heights := PackedFloat32Array()
|
|
var _normals := PackedVector3Array()
|
|
|
|
func _ready() -> void:
|
|
_build()
|
|
|
|
# --- height function --------------------------------------------------------
|
|
|
|
func _ensure_noise() -> void:
|
|
if _noise != null:
|
|
return
|
|
_noise = FastNoiseLite.new()
|
|
_noise.seed = noise_seed
|
|
_noise.noise_type = FastNoiseLite.TYPE_PERLIN
|
|
_noise.frequency = 0.012
|
|
_noise.fractal_octaves = 4
|
|
|
|
_detail = FastNoiseLite.new()
|
|
_detail.seed = noise_seed + 1
|
|
_detail.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
|
_detail.frequency = 0.06
|
|
_detail.fractal_octaves = 2
|
|
|
|
_ridge = FastNoiseLite.new()
|
|
_ridge.seed = noise_seed + 2
|
|
_ridge.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
|
_ridge.frequency = 0.022
|
|
_ridge.fractal_octaves = 4
|
|
|
|
_warp = FastNoiseLite.new()
|
|
_warp.seed = noise_seed + 3
|
|
_warp.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
|
_warp.frequency = 0.03
|
|
|
|
## Radial offset that makes the rim wobble instead of being a perfect circle.
|
|
## Sampled around a circle, so it depends only on the angle from the centre.
|
|
func _rim_offset(x: float, z: float) -> float:
|
|
var ang := atan2(z, x)
|
|
return _warp.get_noise_2d(cos(ang) * 50.0, sin(ang) * 50.0) * rim_warp
|
|
|
|
## "Rim distance": distance from the centre plus the wobble. The rim starts at
|
|
## play_radius and the invisible wall stands at _wall_d() in this same measure.
|
|
func _rim_d(x: float, z: float) -> float:
|
|
return sqrt(x * x + z * z) + _rim_offset(x, z)
|
|
|
|
func _wall_d() -> float:
|
|
return play_radius + rim_width * wall_frac
|
|
|
|
func _cliff_d() -> float:
|
|
return play_radius + cliff_offset
|
|
|
|
## 1 on the open-sea side of the rim, 0 where the mountains are, smooth between
|
|
## (the headlands). Depends only on the angle from the centre.
|
|
func _sea_w(x: float, z: float) -> float:
|
|
var diff := absf(wrapf(atan2(z, x) - deg_to_rad(sea_angle_deg), -PI, PI))
|
|
return 1.0 - smoothstep(deg_to_rad(sea_half_width_deg), deg_to_rad(sea_half_width_deg + sea_fade_deg), diff)
|
|
|
|
## Boundary threshold in rim distance: partway up the mountains, or just
|
|
## inside the cliff edge on the sea side.
|
|
func _wall_threshold(x: float, z: float) -> float:
|
|
return lerpf(_wall_d(), _cliff_d() - 1.0, _sea_w(x, z))
|
|
|
|
func _height_fn(x: float, z: float) -> float:
|
|
_ensure_noise()
|
|
var valley := _noise.get_noise_2d(x, z) * height_scale
|
|
valley += _detail.get_noise_2d(x, z) * 1.4
|
|
var d := _rim_d(x, z)
|
|
|
|
# Mountain rim.
|
|
var land := valley
|
|
var t := smoothstep(play_radius, play_radius + rim_width, d)
|
|
if t > 0.0:
|
|
var ridge := 1.0 - absf(_ridge.get_noise_2d(x, z))
|
|
var m := pow(t, 1.4) * mountain_height * (0.65 + 0.55 * ridge)
|
|
m += maxf(d - (play_radius + rim_width), 0.0) * 0.35 * (0.5 + ridge)
|
|
# Stepped ledges so the faces read as layered cliffs, not smooth ramps.
|
|
var band := 6.0
|
|
var stepped := floorf(m / band) * band + smoothstep(0.35, 0.65, fposmod(m, band) / band) * band
|
|
m = lerpf(m, stepped, 0.45 * t)
|
|
land = valley * (1.0 - t * 0.6) + m
|
|
|
|
# Sea side: the valley eases to a cliff top, then drops sheer to the seabed.
|
|
# The edge sits 1 m past the invisible wall at every angle, including the
|
|
# headlands where the wall moves outward. (With the edge fixed at _cliff_d(),
|
|
# the headlands dropped away INSIDE the wall: pits you could fall into, with
|
|
# lake water hanging over them. tests/test_terrain_edges.gd guards this.)
|
|
var sw := _sea_w(x, z)
|
|
if sw > 0.0:
|
|
var edge := _wall_threshold(x, z) + 1.0
|
|
var top := cliff_top + _detail.get_noise_2d(x * 0.5, z * 0.5) * 1.5
|
|
var coast := lerpf(valley, top, smoothstep(play_radius - 45.0, edge, d))
|
|
var seabed := sea_level - 7.0 - minf(maxf(d - edge, 0.0), 60.0) * 0.08 + _detail.get_noise_2d(x, z) * 2.0
|
|
coast = lerpf(coast, seabed, smoothstep(edge, edge + cliff_drop_width, d))
|
|
land = lerpf(land, coast, sw)
|
|
|
|
# Lake basin: blend the ground down to a bed below the waterline.
|
|
var ld := Vector2(x - lake_center.x, z - lake_center.y).length()
|
|
ld += _detail.get_noise_2d(x * 0.5, z * 0.5) * 5.0
|
|
land = lerpf(land, water_level - 3.2, smoothstep(lake_radius, lake_radius * 0.35, ld))
|
|
|
|
# Ocean all round the chunk edge, so the world is an island and the edge of
|
|
# the mesh is always hidden under deep water.
|
|
var e := smoothstep(size * 0.5 - 28.0, size * 0.5 - 4.0, sqrt(x * x + z * z))
|
|
return lerpf(land, sea_level - 10.0 + _detail.get_noise_2d(x, z) * 2.0, e)
|
|
|
|
# --- build ------------------------------------------------------------------
|
|
|
|
func _build() -> void:
|
|
_ensure_noise()
|
|
step = float(size) / float(resolution)
|
|
half = size * 0.5
|
|
var n := resolution + 1
|
|
|
|
_heights.resize(n * n)
|
|
min_height = INF
|
|
max_height = -INF
|
|
for iz in range(n):
|
|
for ix in range(n):
|
|
var hv := _height_fn(ix * step - half, iz * step - half)
|
|
_heights[iz * n + ix] = hv
|
|
min_height = minf(min_height, hv)
|
|
max_height = maxf(max_height, hv)
|
|
|
|
_normals.resize(n * n)
|
|
for iz in range(n):
|
|
for ix in range(n):
|
|
var hl := _heights[iz * n + maxi(ix - 1, 0)]
|
|
var hr := _heights[iz * n + mini(ix + 1, resolution)]
|
|
var hd := _heights[maxi(iz - 1, 0) * n + ix]
|
|
var hu := _heights[mini(iz + 1, resolution) * n + ix]
|
|
_normals[iz * n + ix] = Vector3(hl - hr, 2.0 * step, hd - hu).normalized()
|
|
|
|
_make_textures()
|
|
_build_mesh(n)
|
|
_build_collision(n)
|
|
_build_boundary()
|
|
_build_water()
|
|
|
|
built = true
|
|
ready_built.emit()
|
|
|
|
func _make_textures() -> void:
|
|
var fn := FastNoiseLite.new()
|
|
fn.seed = noise_seed + 10
|
|
fn.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
|
fn.frequency = 0.008
|
|
fn.fractal_octaves = 5
|
|
noise_tex = NoiseTexture2D.new()
|
|
noise_tex.width = 512
|
|
noise_tex.height = 512
|
|
noise_tex.seamless = true
|
|
noise_tex.noise = fn
|
|
|
|
var fn2 := FastNoiseLite.new()
|
|
fn2.seed = noise_seed + 11
|
|
fn2.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
|
fn2.frequency = 0.02
|
|
fn2.fractal_octaves = 4
|
|
normal_tex = NoiseTexture2D.new()
|
|
normal_tex.width = 512
|
|
normal_tex.height = 512
|
|
normal_tex.seamless = true
|
|
normal_tex.as_normal_map = true
|
|
normal_tex.bump_strength = 6.0
|
|
normal_tex.noise = fn2
|
|
|
|
func _build_mesh(n: int) -> void:
|
|
var verts := PackedVector3Array()
|
|
var uvs := PackedVector2Array()
|
|
verts.resize(n * n)
|
|
uvs.resize(n * n)
|
|
for iz in range(n):
|
|
for ix in range(n):
|
|
var i := iz * n + ix
|
|
verts[i] = Vector3(ix * step - half, _heights[i], iz * step - half)
|
|
uvs[i] = Vector2(float(ix) / resolution, float(iz) / resolution)
|
|
|
|
var idx := PackedInt32Array()
|
|
idx.resize(resolution * resolution * 6)
|
|
var k := 0
|
|
for iz in range(resolution):
|
|
for ix in range(resolution):
|
|
var i00 := iz * n + ix
|
|
var i10 := i00 + 1
|
|
var i01 := i00 + n
|
|
var i11 := i01 + 1
|
|
# Winding chosen so the top face is the front face in Godot.
|
|
idx[k] = i00; idx[k + 1] = i11; idx[k + 2] = i01
|
|
idx[k + 3] = i00; idx[k + 4] = i10; idx[k + 5] = i11
|
|
k += 6
|
|
|
|
var arrays := []
|
|
arrays.resize(Mesh.ARRAY_MAX)
|
|
arrays[Mesh.ARRAY_VERTEX] = verts
|
|
arrays[Mesh.ARRAY_NORMAL] = _normals
|
|
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
|
arrays[Mesh.ARRAY_INDEX] = idx
|
|
var mesh := ArrayMesh.new()
|
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
|
|
|
material = ShaderMaterial.new()
|
|
material.shader = preload("res://shaders/terrain.gdshader")
|
|
material.set_shader_parameter("noise_tex", noise_tex)
|
|
material.set_shader_parameter("normal_tex", normal_tex)
|
|
material.set_shader_parameter("water_level", water_level)
|
|
material.set_shader_parameter("snow_height", snow_height)
|
|
material.set_shader_parameter("grass_lush", grass_lush)
|
|
material.set_shader_parameter("grass_dry", grass_dry)
|
|
|
|
var mi := MeshInstance3D.new()
|
|
mi.name = "TerrainMesh"
|
|
mi.mesh = mesh
|
|
mi.material_override = material
|
|
add_child(mi)
|
|
|
|
func _build_collision(n: int) -> void:
|
|
var hm := HeightMapShape3D.new()
|
|
hm.map_width = n
|
|
hm.map_depth = n
|
|
hm.map_data = _heights
|
|
var body := StaticBody3D.new()
|
|
body.name = "TerrainBody"
|
|
add_child(body)
|
|
var col := CollisionShape3D.new()
|
|
col.shape = hm
|
|
body.add_child(col)
|
|
# HeightMapShape3D samples are 1 unit apart; scale to match the mesh.
|
|
body.scale = Vector3(step, 1.0, step)
|
|
|
|
## Invisible wall ringing the valley partway up the mountain slopes, plus four
|
|
## world-boundary planes at the chunk edges as a last line of defence.
|
|
func _build_boundary() -> void:
|
|
var body := StaticBody3D.new()
|
|
body.name = "Boundary"
|
|
add_child(body)
|
|
|
|
var segs := 128
|
|
var pts: Array[Vector3] = []
|
|
for i in range(segs):
|
|
var ang := TAU * i / segs
|
|
var dir := Vector3(cos(ang), 0.0, sin(ang))
|
|
# Solve for the radius whose rim distance equals the wall threshold.
|
|
var r := _wall_threshold(dir.x, dir.z) - _rim_offset(dir.x, dir.z)
|
|
pts.append(dir * r)
|
|
_wall_ring = pts
|
|
for i in range(segs):
|
|
var a := pts[i]
|
|
var b := pts[(i + 1) % segs]
|
|
var mid := (a + b) * 0.5
|
|
var length := a.distance_to(b) + 0.6
|
|
var box := BoxShape3D.new()
|
|
box.size = Vector3(length, 400.0, 1.5)
|
|
var cs := CollisionShape3D.new()
|
|
cs.shape = box
|
|
var along := (b - a).normalized()
|
|
var seg_basis := Basis(along, Vector3.UP, along.cross(Vector3.UP))
|
|
cs.transform = Transform3D(seg_basis, Vector3(mid.x, 100.0, mid.z))
|
|
body.add_child(cs)
|
|
|
|
var planes := [
|
|
Plane(Vector3.RIGHT, -half), Plane(Vector3.LEFT, -half),
|
|
Plane(Vector3.BACK, -half), Plane(Vector3.FORWARD, -half),
|
|
]
|
|
for p: Plane in planes:
|
|
var wb := WorldBoundaryShape3D.new()
|
|
wb.plane = p
|
|
var cs := CollisionShape3D.new()
|
|
cs.shape = wb
|
|
body.add_child(cs)
|
|
|
|
## Two water bodies: inland water (lake and ponds) as a disc clipped to the
|
|
## boundary ring, so it can't hang in the air past the sea cliff; and an ocean
|
|
## plane out to the horizon at sea level.
|
|
func _build_water() -> void:
|
|
var verts := PackedVector3Array([Vector3.ZERO])
|
|
for p in _wall_ring:
|
|
verts.append(p)
|
|
var count := verts.size()
|
|
var normals := PackedVector3Array()
|
|
var tangents := PackedFloat32Array()
|
|
var uvs := PackedVector2Array()
|
|
for v in verts:
|
|
normals.append(Vector3.UP)
|
|
tangents.append_array([1.0, 0.0, 0.0, 1.0])
|
|
uvs.append(Vector2(v.x, v.z) / size + Vector2(0.5, 0.5))
|
|
var idx := PackedInt32Array()
|
|
for i in range(1, count):
|
|
idx.append_array([0, i, i % (count - 1) + 1])
|
|
var arrays := []
|
|
arrays.resize(Mesh.ARRAY_MAX)
|
|
arrays[Mesh.ARRAY_VERTEX] = verts
|
|
arrays[Mesh.ARRAY_NORMAL] = normals
|
|
arrays[Mesh.ARRAY_TANGENT] = tangents
|
|
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
|
arrays[Mesh.ARRAY_INDEX] = idx
|
|
var disc := ArrayMesh.new()
|
|
disc.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
|
_add_water("Water", disc, water_level, {})
|
|
|
|
var ocean := PlaneMesh.new()
|
|
ocean.size = Vector2(8000.0, 8000.0)
|
|
_add_water("Ocean", ocean, sea_level, {
|
|
"shallow_col": Color(0.16, 0.44, 0.50),
|
|
"deep_col": Color(0.02, 0.10, 0.20),
|
|
"clarity": 6.0,
|
|
})
|
|
|
|
func _add_water(label: String, mesh: Mesh, y: float, params: Dictionary) -> void:
|
|
var mat := ShaderMaterial.new()
|
|
mat.shader = preload("res://shaders/water.gdshader")
|
|
mat.set_shader_parameter("noise_tex", noise_tex)
|
|
mat.set_shader_parameter("normal_tex", normal_tex)
|
|
for k in params:
|
|
mat.set_shader_parameter(k, params[k])
|
|
var mi := MeshInstance3D.new()
|
|
mi.name = label
|
|
mi.mesh = mesh
|
|
mi.material_override = mat
|
|
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
mi.position = Vector3(0.0, y, 0.0)
|
|
add_child(mi)
|
|
|
|
# --- queries ----------------------------------------------------------------
|
|
|
|
func in_bounds(x: float, z: float) -> bool:
|
|
return absf(x) < half - step and absf(z) < half - step
|
|
|
|
## True inside the walkable valley. A positive margin shrinks the area, a
|
|
## negative one extends it past the invisible wall.
|
|
func inside_boundary(x: float, z: float, margin: float = 0.0) -> bool:
|
|
_ensure_noise()
|
|
return _rim_d(x, z) < _wall_threshold(x, z) - margin
|
|
|
|
## How strongly (x, z) lies on the sea side of the rim (0..1).
|
|
func sea_weight(x: float, z: float) -> float:
|
|
_ensure_noise()
|
|
return _sea_w(x, z)
|
|
|
|
## Point on the coast along `angle_deg` (atan2(z, x) convention), at rim
|
|
## distance `past_edge` beyond the cliff edge (negative = inland of it).
|
|
func coast_point(angle_deg: float, past_edge: float) -> Vector2:
|
|
_ensure_noise()
|
|
var dir := Vector2(cos(deg_to_rad(angle_deg)), sin(deg_to_rad(angle_deg)))
|
|
var r := _cliff_d() + past_edge - _rim_offset(dir.x, dir.y)
|
|
return dir * r
|
|
|
|
## Surface height at (x, z), interpolated on the same triangles as the mesh.
|
|
func sample_height(x: float, z: float) -> float:
|
|
if _heights.is_empty():
|
|
return _height_fn(x, z)
|
|
var n := resolution + 1
|
|
var gx := clampf((x + half) / step, 0.0, resolution - 0.0001)
|
|
var gz := clampf((z + half) / step, 0.0, resolution - 0.0001)
|
|
var ix := int(gx)
|
|
var iz := int(gz)
|
|
var fx := gx - ix
|
|
var fz := gz - iz
|
|
var h00 := _heights[iz * n + ix]
|
|
var h10 := _heights[iz * n + ix + 1]
|
|
var h01 := _heights[(iz + 1) * n + ix]
|
|
var h11 := _heights[(iz + 1) * n + ix + 1]
|
|
if fx > fz:
|
|
return h00 + (h10 - h00) * fx + (h11 - h10) * fz
|
|
return h00 + (h11 - h01) * fx + (h01 - h00) * fz
|
|
|
|
## Smoothed surface normal at (x, z).
|
|
func sample_normal(x: float, z: float) -> Vector3:
|
|
if _normals.is_empty():
|
|
return Vector3.UP
|
|
var n := resolution + 1
|
|
var gx := clampf((x + half) / step, 0.0, resolution - 0.0001)
|
|
var gz := clampf((z + half) / step, 0.0, resolution - 0.0001)
|
|
var ix := int(gx)
|
|
var iz := int(gz)
|
|
var fx := gx - ix
|
|
var fz := gz - iz
|
|
var a := _normals[iz * n + ix].lerp(_normals[iz * n + ix + 1], fx)
|
|
var b := _normals[(iz + 1) * n + ix].lerp(_normals[(iz + 1) * n + ix + 1], fx)
|
|
return a.lerp(b, fz).normalized()
|
|
|
|
## Nearest dry, gentle spot to `near` (spiral search), for spawning.
|
|
func find_spawn(near: Vector2) -> Vector3:
|
|
for ring in range(0, 30):
|
|
var r := ring * 2.0
|
|
var count := maxi(1, ring * 6)
|
|
for i in range(count):
|
|
var ang := TAU * i / count
|
|
var x := near.x + cos(ang) * r
|
|
var z := near.y + sin(ang) * r
|
|
var h := sample_height(x, z)
|
|
if h > water_level + 1.2 and sample_normal(x, z).y > 0.93 and inside_boundary(x, z, 15.0):
|
|
return Vector3(x, h, z)
|
|
return Vector3(near.x, sample_height(near.x, near.y), near.y)
|