第二十五章:3D变换与坐标系
"精确掌控3D空间中的位置、旋转和缩放,是3D游戏开发的核心技能。"
3D变换比2D更复杂,涉及三个轴向的变化和四元数旋转。本章将深入讲解3D坐标系统、变换矩阵、四元数以及各种空间转换。
25.1 3D坐标系统
25.1.1 右手坐标系
Godot使用右手坐标系:
- 用右手,拇指指向X+(右)
- 食指指向Y+(上)
- 中指指向Z+(向屏幕外/向观察者)
Y+ (绿色)
│
│
└──────► X+ (红色)
/
/
▼
Z+ (蓝色)
旋转方向(右手定则):
- 拇指指向旋转轴正方向
- 四指弯曲方向即为正旋转方向
25.1.2 坐标空间
extends Node3D
func understand_spaces():
# 本地空间(Local Space)
# - 以物体自身为原点
# - 变换相对于父节点
var local_pos = position # 本地位置
var local_rot = rotation # 本地旋转
# 世界空间(World Space)
# - 以场景原点为参考
# - 绝对位置
var world_pos = global_position # 世界位置
var world_rot = global_rotation # 世界旋转
# 视图空间(View Space)
# - 以相机为原点
# - 用于渲染计算
# 屏幕空间(Screen Space)
# - 2D像素坐标
# - 用于UI和输入
25.2 Vector3详解
25.2.1 基本操作
extends Node3D
func vector3_basics():
# 创建向量
var v1 = Vector3(1, 2, 3)
var v2 = Vector3.ONE # (1, 1, 1)
var v3 = Vector3.ZERO # (0, 0, 0)
var v4 = Vector3.UP # (0, 1, 0)
var v5 = Vector3.DOWN # (0, -1, 0)
var v6 = Vector3.LEFT # (-1, 0, 0)
var v7 = Vector3.RIGHT # (1, 0, 0)
var v8 = Vector3.FORWARD # (0, 0, -1) 注意:向屏幕内
var v9 = Vector3.BACK # (0, 0, 1)
# 分量访问
var x = v1.x
var y = v1.y
var z = v1.z
# 基本运算
var add = v1 + v2
var sub = v1 - v2
var mul = v1 * 2.0
var div = v1 / 2.0
func vector3_operations():
var v1 = Vector3(3, 4, 0)
var v2 = Vector3(1, 0, 0)
# 长度
var length = v1.length() # 5.0
var length_sq = v1.length_squared() # 25.0 (更快)
# 归一化
var normalized = v1.normalized() # 单位向量
# 距离
var dist = v1.distance_to(v2)
var dist_sq = v1.distance_squared_to(v2)
# 点积(用于计算角度)
var dot = v1.dot(v2) # |v1||v2|cos(θ)
# 叉积(用于计算垂直向量)
var cross = v1.cross(v2) # 垂直于v1和v2的向量
# 角度
var angle = v1.angle_to(v2) # 弧度
25.2.2 实用方法
extends Node3D
func vector3_utilities():
var v = Vector3(1, 2, 3)
var target = Vector3(10, 5, 8)
# 方向
var direction = v.direction_to(target) # 归一化方向向量
# 线性插值
var lerped = v.lerp(target, 0.5) # 中点
# 球面插值(旋转时使用)
var slerped = v.slerp(target, 0.5)
# 移动向目标
var moved = v.move_toward(target, 1.0) # 移动1单位
# 限制长度
var limited = v.limit_length(5.0)
# 反射
var normal = Vector3.UP
var reflected = v.reflect(normal)
# 弹跳
var bounced = v.bounce(normal)
# 投影
var projected = v.project(Vector3.RIGHT) # 投影到X轴
# 滑动(沿表面)
var slid = v.slide(normal)
func snap_to_grid(pos: Vector3, grid_size: float) -> Vector3:
return Vector3(
snappedf(pos.x, grid_size),
snappedf(pos.y, grid_size),
snappedf(pos.z, grid_size)
)
25.3 Basis与旋转
25.3.1 Basis矩阵
extends Node3D
func understand_basis():
# Basis是3x3矩阵,表示旋转和缩放
var basis = Basis() # 单位矩阵
# 三个列向量
var x_axis = basis.x # 右方向
var y_axis = basis.y # 上方向
var z_axis = basis.z # 前方向(负Z)
# 从欧拉角创建
basis = Basis.from_euler(Vector3(0, PI/4, 0)) # Y轴旋转45度
# 从轴角创建
basis = Basis(Vector3.UP, PI/4) # 绕Y轴旋转45度
# 获取欧拉角
var euler = basis.get_euler()
# 获取缩放
var scale = basis.get_scale()
# 正交化(修复浮点误差)
basis = basis.orthonormalized()
func basis_operations():
var b1 = Basis.from_euler(Vector3(0, PI/4, 0))
var b2 = Basis.from_euler(Vector3(PI/6, 0, 0))
# 组合旋转
var combined = b1 * b2
# 逆旋转
var inverse = b1.inverse()
# 变换向量
var local_dir = Vector3.FORWARD
var world_dir = b1 * local_dir
# 插值
var slerped = b1.slerp(b2, 0.5)
25.3.2 欧拉角
extends Node3D
func euler_angles():
# 欧拉角:绕X、Y、Z轴的旋转角度
# Godot使用YXZ旋转顺序
# 设置旋转(弧度)
rotation = Vector3(PI/6, PI/4, PI/3) # X, Y, Z
# 使用角度
rotation_degrees = Vector3(30, 45, 60)
# 分别设置
rotation.x = deg_to_rad(30) # 俯仰(Pitch)
rotation.y = deg_to_rad(45) # 偏航(Yaw)
rotation.z = deg_to_rad(60) # 翻滚(Roll)
# 万向锁问题
func gimbal_lock_demo():
# 当俯仰角接近±90度时,偏航和翻滚会合并
# 导致失去一个自由度
rotation_degrees = Vector3(90, 0, 0) # 可能出现万向锁
# 解决方案:使用四元数
25.3.3 四元数
extends Node3D
func quaternion_basics():
# Quaternion避免万向锁,适合3D旋转
var q = Quaternion() # 单位四元数
# 从轴角创建
q = Quaternion(Vector3.UP, PI/4) # 绕Y轴旋转45度
# 从欧拉角创建
q = Quaternion.from_euler(Vector3(0, PI/4, 0))
# 转换为欧拉角
var euler = q.get_euler()
# 转换为Basis
var basis = Basis(q)
# 获取旋转轴和角度
var axis = q.get_axis()
var angle = q.get_angle()
func quaternion_operations():
var q1 = Quaternion(Vector3.UP, PI/4)
var q2 = Quaternion(Vector3.RIGHT, PI/6)
# 组合旋转
var combined = q1 * q2
# 逆旋转
var inverse = q1.inverse()
# 球面插值(平滑旋转)
var slerped = q1.slerp(q2, 0.5)
# 归一化
var normalized = q1.normalized()
# 变换向量
var rotated_vector = q1 * Vector3.FORWARD
# 平滑旋转示例
var target_quaternion: Quaternion
func _process(delta: float):
var current = Quaternion(global_transform.basis)
var smoothed = current.slerp(target_quaternion, 5.0 * delta)
global_transform.basis = Basis(smoothed)
25.4 Transform3D
25.4.1 变换矩阵结构
extends Node3D
func understand_transform3d():
# Transform3D = Basis + Origin
var t = Transform3D()
# 组成部分
var basis = t.basis # 3x3旋转缩放矩阵
var origin = t.origin # 位置向量
# 创建变换
t = Transform3D(Basis(), Vector3(1, 2, 3))
# 从位置创建
t = Transform3D(Basis.IDENTITY, Vector3(5, 0, 0))
func transform_creation():
# 单位变换
var identity = Transform3D.IDENTITY
# 朝向目标
var looking = Transform3D.IDENTITY.looking_at(Vector3(10, 0, 10), Vector3.UP)
# 从旋转和位置
var rot = Basis.from_euler(Vector3(0, PI/4, 0))
var pos = Vector3(5, 0, 5)
var t = Transform3D(rot, pos)
25.4.2 变换操作
extends Node3D
func transform_operations():
var t = Transform3D.IDENTITY
# 平移
t = t.translated(Vector3(1, 0, 0))
t = t.translated_local(Vector3(1, 0, 0)) # 本地空间
# 旋转
t = t.rotated(Vector3.UP, PI/4)
t = t.rotated_local(Vector3.UP, PI/4) # 本地空间
# 缩放
t = t.scaled(Vector3(2, 2, 2))
t = t.scaled_local(Vector3(2, 2, 2))
# 逆变换
var inverse = t.affine_inverse()
# 组合变换(注意顺序)
var t1 = Transform3D.IDENTITY.translated(Vector3(5, 0, 0))
var t2 = Transform3D.IDENTITY.rotated(Vector3.UP, PI/2)
var combined = t2 * t1 # 先平移,再旋转
# 变换点
var local_point = Vector3(1, 0, 0)
var world_point = t * local_point
# 变换方向(不受位移影响)
var local_dir = Vector3.FORWARD
var world_dir = t.basis * local_dir
# 插值
var t_start = transform
var t_end = Transform3D(Basis(), Vector3(10, 0, 0))
var interpolated = t_start.interpolate_with(t_end, 0.5)
25.5 空间转换
25.5.1 本地与全局转换
extends Node3D
func space_conversion():
# 本地坐标转全局坐标
var local_pos = Vector3(1, 0, 0)
var global_pos = to_global(local_pos)
# 等价于:global_transform * local_pos
# 全局坐标转本地坐标
var world_pos = Vector3(10, 5, 3)
var local = to_local(world_pos)
# 等价于:global_transform.affine_inverse() * world_pos
# 方向转换
var local_dir = Vector3.FORWARD
var global_dir = global_transform.basis * local_dir
var world_dir = Vector3(1, 0, 0)
var local_direction = global_transform.basis.inverse() * world_dir
25.5.2 视口与世界转换
extends Node3D
func viewport_conversions():
var camera = get_viewport().get_camera_3d()
# 屏幕坐标转世界射线
var screen_pos = get_viewport().get_mouse_position()
var ray_origin = camera.project_ray_origin(screen_pos)
var ray_direction = camera.project_ray_normal(screen_pos)
# 世界坐标转屏幕坐标
var world_pos = Vector3(5, 0, 5)
var screen = camera.unproject_position(world_pos)
# 检查点是否在相机后面
var is_behind = camera.is_position_behind(world_pos)
func get_mouse_world_position(distance: float = 10.0) -> Vector3:
var camera = get_viewport().get_camera_3d()
var mouse_pos = get_viewport().get_mouse_position()
var ray_origin = camera.project_ray_origin(mouse_pos)
var ray_direction = camera.project_ray_normal(mouse_pos)
return ray_origin + ray_direction * distance
func raycast_from_mouse() -> Dictionary:
var camera = get_viewport().get_camera_3d()
var mouse_pos = get_viewport().get_mouse_position()
var ray_origin = camera.project_ray_origin(mouse_pos)
var ray_end = ray_origin + camera.project_ray_normal(mouse_pos) * 1000
var space = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(ray_origin, ray_end)
return space.intersect_ray(query)
25.6 实用工具
25.6.1 3D数学工具类
# math_utils_3d.gd
class_name MathUtils3D
# 计算两点之间的水平距离(忽略Y轴)
static func horizontal_distance(a: Vector3, b: Vector3) -> float:
var diff = Vector3(b.x - a.x, 0, b.z - a.z)
return diff.length()
# 计算水平方向
static func horizontal_direction(from: Vector3, to: Vector3) -> Vector3:
var diff = Vector3(to.x - from.x, 0, to.z - from.z)
return diff.normalized()
# 计算仰角
static func elevation_angle(from: Vector3, to: Vector3) -> float:
var horizontal_dist = horizontal_distance(from, to)
var vertical_dist = to.y - from.y
return atan2(vertical_dist, horizontal_dist)
# 在平面上投影点
static func project_on_plane(point: Vector3, plane_normal: Vector3, plane_point: Vector3) -> Vector3:
var d = plane_normal.dot(point - plane_point)
return point - plane_normal * d
# 计算三角形法线
static func triangle_normal(a: Vector3, b: Vector3, c: Vector3) -> Vector3:
var ab = b - a
var ac = c - a
return ab.cross(ac).normalized()
# 最近点(点到线段)
static func closest_point_on_segment(point: Vector3, segment_start: Vector3, segment_end: Vector3) -> Vector3:
var segment = segment_end - segment_start
var t = clamp((point - segment_start).dot(segment) / segment.length_squared(), 0.0, 1.0)
return segment_start + segment * t
# 随机球面上的点
static func random_point_on_sphere(radius: float = 1.0) -> Vector3:
var theta = randf() * TAU
var phi = acos(2.0 * randf() - 1.0)
return Vector3(
radius * sin(phi) * cos(theta),
radius * sin(phi) * sin(theta),
radius * cos(phi)
)
# 随机球内的点
static func random_point_in_sphere(radius: float = 1.0) -> Vector3:
var r = radius * pow(randf(), 1.0/3.0)
return random_point_on_sphere(r)
25.6.2 相机辅助工具
# camera_utils.gd
class_name CameraUtils
# 计算物体在屏幕上的大小
static func get_screen_size(camera: Camera3D, world_size: float, world_pos: Vector3) -> float:
var distance = camera.global_position.distance_to(world_pos)
var fov_rad = deg_to_rad(camera.fov)
var screen_height = camera.get_viewport().get_visible_rect().size.y
var projected_size = (world_size / distance) / tan(fov_rad / 2) * screen_height / 2
return projected_size
# 计算相机可见区域
static func get_frustum_corners(camera: Camera3D, distance: float) -> PackedVector3Array:
var corners = PackedVector3Array()
var viewport = camera.get_viewport()
var size = viewport.get_visible_rect().size
var screen_corners = [
Vector2(0, 0),
Vector2(size.x, 0),
Vector2(size.x, size.y),
Vector2(0, size.y)
]
for corner in screen_corners:
var ray_origin = camera.project_ray_origin(corner)
var ray_dir = camera.project_ray_normal(corner)
corners.append(ray_origin + ray_dir * distance)
return corners
25.7 实际案例
25.7.1 轨道相机控制器
# orbit_camera.gd
extends Camera3D
@export var target: Node3D
@export var distance: float = 10.0
@export var min_distance: float = 2.0
@export var max_distance: float = 50.0
@export var rotation_speed: float = 0.005
@export var zoom_speed: float = 1.0
@export var min_pitch: float = -80.0
@export var max_pitch: float = 80.0
var orbit_rotation: Vector2 = Vector2.ZERO
var is_orbiting: bool = false
func _ready():
if target:
_update_camera()
func _input(event: InputEvent):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_RIGHT:
is_orbiting = event.pressed
elif event.button_index == MOUSE_BUTTON_WHEEL_UP:
distance = max(min_distance, distance - zoom_speed)
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
distance = min(max_distance, distance + zoom_speed)
elif event is InputEventMouseMotion and is_orbiting:
orbit_rotation.x -= event.relative.x * rotation_speed
orbit_rotation.y -= event.relative.y * rotation_speed
orbit_rotation.y = clamp(orbit_rotation.y, deg_to_rad(min_pitch), deg_to_rad(max_pitch))
func _process(delta: float):
if target:
_update_camera()
func _update_camera():
var offset = Vector3.BACK * distance
offset = offset.rotated(Vector3.RIGHT, orbit_rotation.y)
offset = offset.rotated(Vector3.UP, orbit_rotation.x)
global_position = target.global_position + offset
look_at(target.global_position)
本章小结
本章深入学习了3D变换与坐标系:
- 3D坐标系统:右手坐标系、坐标空间
- Vector3详解:基本操作、实用方法
- Basis与旋转:矩阵、欧拉角、四元数
- Transform3D:变换矩阵、操作方法
- 空间转换:本地/全局、视口/世界
- 实用工具:数学工具、相机工具
- 实际案例:轨道相机控制器
下一章将学习网格与模型导入。
上一章:3D节点详解
下一章:网格与模型导入