第二十五章:3D变换与坐标系

第二十五章: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变换与坐标系:

  1. 3D坐标系统:右手坐标系、坐标空间
  2. Vector3详解:基本操作、实用方法
  3. Basis与旋转:矩阵、欧拉角、四元数
  4. Transform3D:变换矩阵、操作方法
  5. 空间转换:本地/全局、视口/世界
  6. 实用工具:数学工具、相机工具
  7. 实际案例:轨道相机控制器

下一章将学习网格与模型导入。


上一章:3D节点详解

下一章:网格与模型导入

← 返回目录