Files

124 lines
5.6 KiB
GDScript

extends RefCounted
# Follow the headset first. Only the panel's path toward the ship engages the
# world-height stop, so a distant or side-on table never drags the HUD around.
const CLEARANCE := 0.05
const HEAD_OFFSET := Vector3(0.0, -0.12, -1.30)
const POSITION_TIME := 0.12
const PANEL_THICKNESS := 0.008
var initialized := false
var blocked := false
var lift := 0.0
var pose := Transform3D.IDENTITY
func reset() -> void:
initialized = false
blocked = false
lift = 0.0
static func transformed_bounds(local: AABB, transform: Transform3D) -> AABB:
var result := AABB(transform * local.position, Vector3.ZERO)
for x in [0.0, 1.0]:
for y in [0.0, 1.0]:
for z in [0.0, 1.0]:
result = result.expand(transform * (local.position + local.size * Vector3(x, y, z)))
return result
static func panel_bounds(transform: Transform3D, dimensions: Vector2) -> AABB:
var half := transform.basis.x.abs() * dimensions.x * 0.5 + transform.basis.y.abs() * dimensions.y * 0.5
return AABB(transform.origin - half, half * 2.0)
static func _boxes_intersect(first: Transform3D, first_box: AABB, second: Transform3D, second_box: AABB, margin := 0.0) -> bool:
# Separate the oriented boxes on face normals and edge cross products. A
# world AABB alone can stop the HUD beside a rotated ship with empty space.
var first_axes: Array[Vector3] = [first.basis.x.normalized(), first.basis.y.normalized(), first.basis.z.normalized()]
var second_axes: Array[Vector3] = [second.basis.x.normalized(), second.basis.y.normalized(), second.basis.z.normalized()]
var first_half := first_box.size * Vector3(first.basis.x.length(), first.basis.y.length(), first.basis.z.length()) * 0.5
var second_half := second_box.size * Vector3(second.basis.x.length(), second.basis.y.length(), second.basis.z.length()) * 0.5 + Vector3.ONE * margin
var separation := second * second_box.get_center() - first * first_box.get_center()
var axes: Array[Vector3] = []
axes.append_array(first_axes)
axes.append_array(second_axes)
for first_axis in first_axes:
for second_axis in second_axes:
var cross := first_axis.cross(second_axis)
if cross.length_squared() > 0.000001:
axes.append(cross.normalized())
for axis in axes:
var first_radius := 0.0
var second_radius := 0.0
for index in range(3):
first_radius += absf(axis.dot(first_axes[index])) * first_half[index]
second_radius += absf(axis.dot(second_axes[index])) * second_half[index]
if absf(axis.dot(separation)) > first_radius + second_radius:
return false
return true
static func overlaps_ship(panel: Transform3D, dimensions: Vector2, ship_transform: Transform3D, local_bounds: AABB, margin := 0.0) -> bool:
var half := Vector3(dimensions.x * 0.5, dimensions.y * 0.5, PANEL_THICKNESS)
return _boxes_intersect(panel, AABB(-half, half * 2.0), ship_transform, local_bounds, margin)
static func _facing(point: Vector3, viewer: Transform3D) -> Basis:
var toward := viewer.origin - point
if toward.length_squared() < 0.0001:
toward = viewer.basis.z
var up := viewer.basis.y.normalized()
if absf(toward.normalized().dot(up)) > 0.98:
up = viewer.basis.z.normalized()
return Basis.looking_at(toward, up, true)
static func _clear_position(point: Vector3, basis: Basis, dimensions: Vector2, ship: AABB) -> Vector3:
var half := basis.x.abs() * dimensions.x * 0.5 + basis.y.abs() * dimensions.y * 0.5
point.y = maxf(point.y, ship.end.y + CLEARANCE + half.y + PANEL_THICKNESS)
return point
func update(ship_transform: Transform3D, local_bounds: AABB, viewer: Transform3D, dimensions: Vector2, delta: float, snap := false) -> Transform3D:
var desired := viewer * Transform3D(Basis.IDENTITY, HEAD_OFFSET)
var path_length := viewer.origin.distance_to(desired.origin)
var path := Transform3D(Basis.looking_at(desired.origin - viewer.origin, viewer.basis.y), (viewer.origin + desired.origin) * 0.5)
var path_box := AABB(Vector3(0, 0, -path_length * 0.5), Vector3(0, 0, path_length))
# The full panel handles edge contact. Its center's viewing path also stops
# a large downward head motion tunnelling the panel entirely below the hull.
# Sweeping its full width all the way to the face would unnecessarily clamp
# it when looking away from a table close behind the viewer.
blocked = overlaps_ship(desired, dimensions, ship_transform, local_bounds, CLEARANCE) or _boxes_intersect(path, path_box, ship_transform, local_bounds, CLEARANCE)
var target_lift := 0.0
var ship := transformed_bounds(local_bounds, ship_transform)
if blocked:
var target := desired.origin
var target_basis := desired.basis
for iteration in range(6):
target = _clear_position(target, target_basis, dimensions, ship)
target_basis = _facing(target, viewer)
target_lift = maxf(0.0, target.y - desired.origin.y)
# Head motion itself is immediate. Smooth only release of the clearance
# offset; raising it immediately prevents a fast grab crossing the panel.
if not initialized or snap or target_lift >= lift:
lift = target_lift
else:
var amount := 1.0 - exp(-maxf(delta, 0.0) / POSITION_TIME)
lift = lerpf(lift, target_lift, amount)
if lift < 0.0001:
lift = 0.0
pose = desired
pose.origin.y += lift
if lift > 0.0:
pose.basis = _facing(pose.origin, viewer)
# A raised ship can meet the releasing panel even after the desired head
# pose has left its path. Enforce clearance for that actual pose as well.
if blocked or overlaps_ship(pose, dimensions, ship_transform, local_bounds, CLEARANCE):
for iteration in range(6):
pose.origin = _clear_position(pose.origin, pose.basis, dimensions, ship)
pose.basis = _facing(pose.origin, viewer)
lift = maxf(0.0, pose.origin.y - desired.origin.y)
initialized = true
return pose