422 lines
11 KiB
Rust
422 lines
11 KiB
Rust
|
|
//! 2D 相机(纯数学实现)
|
|||
|
|
//!
|
|||
|
|
//! 2D camera (pure math implementation).
|
|||
|
|
|
|||
|
|
use glam::{Mat3, Vec2};
|
|||
|
|
|
|||
|
|
/// 2D 相机
|
|||
|
|
///
|
|||
|
|
/// 提供正交投影、坐标转换等功能。
|
|||
|
|
/// 纯数学实现,不依赖任何图形 API。
|
|||
|
|
///
|
|||
|
|
/// 2D camera.
|
|||
|
|
/// Provides orthographic projection, coordinate conversion, etc.
|
|||
|
|
/// Pure math implementation, no graphics API dependencies.
|
|||
|
|
#[derive(Debug, Clone, Copy)]
|
|||
|
|
pub struct Camera2D {
|
|||
|
|
/// 相机位置(世界坐标)| Camera position (world coordinates)
|
|||
|
|
position: Vec2,
|
|||
|
|
|
|||
|
|
/// 旋转角度(弧度,顺时针为正)| Rotation (radians, clockwise positive)
|
|||
|
|
rotation: f32,
|
|||
|
|
|
|||
|
|
/// 缩放级别 | Zoom level
|
|||
|
|
zoom: f32,
|
|||
|
|
|
|||
|
|
/// 视口宽度 | Viewport width
|
|||
|
|
width: f32,
|
|||
|
|
|
|||
|
|
/// 视口高度 | Viewport height
|
|||
|
|
height: f32,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl Default for Camera2D {
|
|||
|
|
fn default() -> Self {
|
|||
|
|
Self {
|
|||
|
|
position: Vec2::ZERO,
|
|||
|
|
rotation: 0.0,
|
|||
|
|
zoom: 1.0,
|
|||
|
|
width: 800.0,
|
|||
|
|
height: 600.0,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl Camera2D {
|
|||
|
|
/// 创建新相机
|
|||
|
|
///
|
|||
|
|
/// Create new camera.
|
|||
|
|
pub fn new(width: f32, height: f32) -> Self {
|
|||
|
|
Self {
|
|||
|
|
width,
|
|||
|
|
height,
|
|||
|
|
..Default::default()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Builder Pattern ====================
|
|||
|
|
|
|||
|
|
/// 设置位置
|
|||
|
|
///
|
|||
|
|
/// Set position.
|
|||
|
|
pub fn with_position(mut self, x: f32, y: f32) -> Self {
|
|||
|
|
self.position = Vec2::new(x, y);
|
|||
|
|
self
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 设置缩放
|
|||
|
|
///
|
|||
|
|
/// Set zoom.
|
|||
|
|
pub fn with_zoom(mut self, zoom: f32) -> Self {
|
|||
|
|
self.zoom = zoom.max(0.001);
|
|||
|
|
self
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 设置旋转
|
|||
|
|
///
|
|||
|
|
/// Set rotation.
|
|||
|
|
pub fn with_rotation(mut self, rotation: f32) -> Self {
|
|||
|
|
self.rotation = rotation;
|
|||
|
|
self
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Getters ====================
|
|||
|
|
|
|||
|
|
/// 获取位置
|
|||
|
|
///
|
|||
|
|
/// Get position.
|
|||
|
|
pub fn position(&self) -> Vec2 {
|
|||
|
|
self.position
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取 X 坐标
|
|||
|
|
///
|
|||
|
|
/// Get X coordinate.
|
|||
|
|
pub fn x(&self) -> f32 {
|
|||
|
|
self.position.x
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取 Y 坐标
|
|||
|
|
///
|
|||
|
|
/// Get Y coordinate.
|
|||
|
|
pub fn y(&self) -> f32 {
|
|||
|
|
self.position.y
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取旋转
|
|||
|
|
///
|
|||
|
|
/// Get rotation.
|
|||
|
|
pub fn rotation(&self) -> f32 {
|
|||
|
|
self.rotation
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取缩放
|
|||
|
|
///
|
|||
|
|
/// Get zoom.
|
|||
|
|
pub fn zoom(&self) -> f32 {
|
|||
|
|
self.zoom
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取视口宽度
|
|||
|
|
///
|
|||
|
|
/// Get viewport width.
|
|||
|
|
pub fn width(&self) -> f32 {
|
|||
|
|
self.width
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取视口高度
|
|||
|
|
///
|
|||
|
|
/// Get viewport height.
|
|||
|
|
pub fn height(&self) -> f32 {
|
|||
|
|
self.height
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Setters ====================
|
|||
|
|
|
|||
|
|
/// 设置位置
|
|||
|
|
///
|
|||
|
|
/// Set position.
|
|||
|
|
pub fn set_position(&mut self, x: f32, y: f32) {
|
|||
|
|
self.position = Vec2::new(x, y);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 设置旋转
|
|||
|
|
///
|
|||
|
|
/// Set rotation.
|
|||
|
|
pub fn set_rotation(&mut self, rotation: f32) {
|
|||
|
|
self.rotation = rotation;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 设置缩放
|
|||
|
|
///
|
|||
|
|
/// Set zoom.
|
|||
|
|
pub fn set_zoom(&mut self, zoom: f32) {
|
|||
|
|
self.zoom = zoom.max(0.001);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 调整视口大小
|
|||
|
|
///
|
|||
|
|
/// Resize viewport.
|
|||
|
|
pub fn resize(&mut self, width: f32, height: f32) {
|
|||
|
|
self.width = width;
|
|||
|
|
self.height = height;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Transform Methods ====================
|
|||
|
|
|
|||
|
|
/// 移动相机
|
|||
|
|
///
|
|||
|
|
/// Move camera.
|
|||
|
|
pub fn translate(&mut self, dx: f32, dy: f32) {
|
|||
|
|
self.position.x += dx;
|
|||
|
|
self.position.y += dy;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 旋转相机
|
|||
|
|
///
|
|||
|
|
/// Rotate camera.
|
|||
|
|
pub fn rotate(&mut self, delta: f32) {
|
|||
|
|
self.rotation += delta;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 缩放相机
|
|||
|
|
///
|
|||
|
|
/// Zoom camera.
|
|||
|
|
pub fn zoom_by(&mut self, factor: f32) {
|
|||
|
|
self.zoom = (self.zoom * factor).max(0.001);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Matrix Generation ====================
|
|||
|
|
|
|||
|
|
/// 获取投影矩阵
|
|||
|
|
///
|
|||
|
|
/// 将世界坐标转换为 NDC(-1 到 1)。
|
|||
|
|
///
|
|||
|
|
/// Get projection matrix.
|
|||
|
|
/// Transforms world coordinates to NDC (-1 to 1).
|
|||
|
|
pub fn projection_matrix(&self) -> Mat3 {
|
|||
|
|
// 计算缩放
|
|||
|
|
let scale_x = 2.0 / self.width * self.zoom;
|
|||
|
|
let scale_y = 2.0 / self.height * self.zoom;
|
|||
|
|
|
|||
|
|
// 计算旋转
|
|||
|
|
let cos_r = self.rotation.cos();
|
|||
|
|
let sin_r = self.rotation.sin();
|
|||
|
|
|
|||
|
|
// 计算平移(相机位置取反)
|
|||
|
|
let tx = -self.position.x;
|
|||
|
|
let ty = -self.position.y;
|
|||
|
|
|
|||
|
|
// 构建变换矩阵:Scale * Rotate * Translate
|
|||
|
|
// 先平移,再旋转,最后缩放
|
|||
|
|
Mat3::from_cols_array(&[
|
|||
|
|
scale_x * cos_r,
|
|||
|
|
scale_y * sin_r,
|
|||
|
|
0.0,
|
|||
|
|
-scale_x * sin_r,
|
|||
|
|
scale_y * cos_r,
|
|||
|
|
0.0,
|
|||
|
|
scale_x * (tx * cos_r - ty * sin_r),
|
|||
|
|
scale_y * (tx * sin_r + ty * cos_r),
|
|||
|
|
1.0,
|
|||
|
|
])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取视图矩阵
|
|||
|
|
///
|
|||
|
|
/// Get view matrix.
|
|||
|
|
pub fn view_matrix(&self) -> Mat3 {
|
|||
|
|
let cos_r = self.rotation.cos();
|
|||
|
|
let sin_r = self.rotation.sin();
|
|||
|
|
|
|||
|
|
let tx = -self.position.x;
|
|||
|
|
let ty = -self.position.y;
|
|||
|
|
|
|||
|
|
Mat3::from_cols_array(&[
|
|||
|
|
cos_r,
|
|||
|
|
sin_r,
|
|||
|
|
0.0,
|
|||
|
|
-sin_r,
|
|||
|
|
cos_r,
|
|||
|
|
0.0,
|
|||
|
|
tx * cos_r - ty * sin_r,
|
|||
|
|
tx * sin_r + ty * cos_r,
|
|||
|
|
1.0,
|
|||
|
|
])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取逆投影矩阵
|
|||
|
|
///
|
|||
|
|
/// Get inverse projection matrix.
|
|||
|
|
pub fn inverse_projection_matrix(&self) -> Mat3 {
|
|||
|
|
self.projection_matrix().inverse()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Coordinate Conversion ====================
|
|||
|
|
|
|||
|
|
/// 屏幕坐标转世界坐标
|
|||
|
|
///
|
|||
|
|
/// Screen to world coordinates.
|
|||
|
|
///
|
|||
|
|
/// # Parameters
|
|||
|
|
///
|
|||
|
|
/// - `screen_pos`: 屏幕坐标(像素,左上角为原点)| Screen coordinates (pixels, origin at top-left)
|
|||
|
|
///
|
|||
|
|
/// # Returns
|
|||
|
|
///
|
|||
|
|
/// 世界坐标 | World coordinates
|
|||
|
|
pub fn screen_to_world(&self, screen_pos: Vec2) -> Vec2 {
|
|||
|
|
// 屏幕坐标转 NDC
|
|||
|
|
let ndc_x = (screen_pos.x / self.width) * 2.0 - 1.0;
|
|||
|
|
let ndc_y = 1.0 - (screen_pos.y / self.height) * 2.0; // Y 轴翻转
|
|||
|
|
|
|||
|
|
// NDC 转世界坐标
|
|||
|
|
let inv_proj = self.inverse_projection_matrix();
|
|||
|
|
let world = inv_proj * glam::Vec3::new(ndc_x, ndc_y, 1.0);
|
|||
|
|
|
|||
|
|
Vec2::new(world.x, world.y)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 世界坐标转屏幕坐标
|
|||
|
|
///
|
|||
|
|
/// World to screen coordinates.
|
|||
|
|
///
|
|||
|
|
/// # Parameters
|
|||
|
|
///
|
|||
|
|
/// - `world_pos`: 世界坐标 | World coordinates
|
|||
|
|
///
|
|||
|
|
/// # Returns
|
|||
|
|
///
|
|||
|
|
/// 屏幕坐标(像素,左上角为原点)| Screen coordinates (pixels, origin at top-left)
|
|||
|
|
pub fn world_to_screen(&self, world_pos: Vec2) -> Vec2 {
|
|||
|
|
// 世界坐标转 NDC
|
|||
|
|
let proj = self.projection_matrix();
|
|||
|
|
let ndc = proj * glam::Vec3::new(world_pos.x, world_pos.y, 1.0);
|
|||
|
|
|
|||
|
|
// NDC 转屏幕坐标
|
|||
|
|
let screen_x = (ndc.x + 1.0) * 0.5 * self.width;
|
|||
|
|
let screen_y = (1.0 - ndc.y) * 0.5 * self.height; // Y 轴翻转
|
|||
|
|
|
|||
|
|
Vec2::new(screen_x, screen_y)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取可见区域(世界坐标 AABB)
|
|||
|
|
///
|
|||
|
|
/// Get visible bounds (world coordinate AABB).
|
|||
|
|
pub fn visible_bounds(&self) -> (Vec2, Vec2) {
|
|||
|
|
// 四个角的屏幕坐标
|
|||
|
|
let corners = [
|
|||
|
|
Vec2::new(0.0, 0.0),
|
|||
|
|
Vec2::new(self.width, 0.0),
|
|||
|
|
Vec2::new(self.width, self.height),
|
|||
|
|
Vec2::new(0.0, self.height),
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// 转换为世界坐标
|
|||
|
|
let world_corners: Vec<Vec2> = corners.iter().map(|c| self.screen_to_world(*c)).collect();
|
|||
|
|
|
|||
|
|
// 计算 AABB
|
|||
|
|
let mut min = world_corners[0];
|
|||
|
|
let mut max = world_corners[0];
|
|||
|
|
|
|||
|
|
for corner in &world_corners[1..] {
|
|||
|
|
min = min.min(*corner);
|
|||
|
|
max = max.max(*corner);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
(min, max)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 检查点是否在可见区域内
|
|||
|
|
///
|
|||
|
|
/// Check if point is visible.
|
|||
|
|
pub fn is_point_visible(&self, world_pos: Vec2) -> bool {
|
|||
|
|
let (min, max) = self.visible_bounds();
|
|||
|
|
world_pos.x >= min.x && world_pos.x <= max.x && world_pos.y >= min.y && world_pos.y <= max.y
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 检查矩形是否与可见区域相交
|
|||
|
|
///
|
|||
|
|
/// Check if rectangle intersects visible area.
|
|||
|
|
pub fn is_rect_visible(&self, pos: Vec2, size: Vec2) -> bool {
|
|||
|
|
let (min, max) = self.visible_bounds();
|
|||
|
|
let rect_max = pos + size;
|
|||
|
|
|
|||
|
|
pos.x <= max.x && rect_max.x >= min.x && pos.y <= max.y && rect_max.y >= min.y
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[cfg(test)]
|
|||
|
|
mod tests {
|
|||
|
|
use super::*;
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn test_camera_creation() {
|
|||
|
|
let camera = Camera2D::new(800.0, 600.0);
|
|||
|
|
assert_eq!(camera.width(), 800.0);
|
|||
|
|
assert_eq!(camera.height(), 600.0);
|
|||
|
|
assert_eq!(camera.position(), Vec2::ZERO);
|
|||
|
|
assert_eq!(camera.zoom(), 1.0);
|
|||
|
|
assert_eq!(camera.rotation(), 0.0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn test_camera_builder() {
|
|||
|
|
let camera = Camera2D::new(800.0, 600.0)
|
|||
|
|
.with_position(100.0, 50.0)
|
|||
|
|
.with_zoom(2.0)
|
|||
|
|
.with_rotation(std::f32::consts::PI / 4.0);
|
|||
|
|
|
|||
|
|
assert_eq!(camera.position(), Vec2::new(100.0, 50.0));
|
|||
|
|
assert_eq!(camera.zoom(), 2.0);
|
|||
|
|
assert!((camera.rotation() - std::f32::consts::PI / 4.0).abs() < 0.0001);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn test_screen_to_world_identity() {
|
|||
|
|
let camera = Camera2D::new(800.0, 600.0);
|
|||
|
|
|
|||
|
|
// 屏幕中心应该对应世界原点
|
|||
|
|
let center = camera.screen_to_world(Vec2::new(400.0, 300.0));
|
|||
|
|
assert!((center.x).abs() < 0.001);
|
|||
|
|
assert!((center.y).abs() < 0.001);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn test_world_to_screen_identity() {
|
|||
|
|
let camera = Camera2D::new(800.0, 600.0);
|
|||
|
|
|
|||
|
|
// 世界原点应该对应屏幕中心
|
|||
|
|
let center = camera.world_to_screen(Vec2::ZERO);
|
|||
|
|
assert!((center.x - 400.0).abs() < 0.001);
|
|||
|
|
assert!((center.y - 300.0).abs() < 0.001);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn test_coordinate_roundtrip() {
|
|||
|
|
let camera = Camera2D::new(800.0, 600.0)
|
|||
|
|
.with_position(100.0, 50.0)
|
|||
|
|
.with_zoom(1.5)
|
|||
|
|
.with_rotation(0.3);
|
|||
|
|
|
|||
|
|
let world_pos = Vec2::new(200.0, 150.0);
|
|||
|
|
let screen_pos = camera.world_to_screen(world_pos);
|
|||
|
|
let back_to_world = camera.screen_to_world(screen_pos);
|
|||
|
|
|
|||
|
|
assert!((back_to_world.x - world_pos.x).abs() < 0.01);
|
|||
|
|
assert!((back_to_world.y - world_pos.y).abs() < 0.01);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn test_visible_bounds() {
|
|||
|
|
let camera = Camera2D::new(800.0, 600.0);
|
|||
|
|
let (min, max) = camera.visible_bounds();
|
|||
|
|
|
|||
|
|
// 默认相机应该看到 -400 到 400(水平),-300 到 300(垂直)
|
|||
|
|
assert!((min.x - (-400.0)).abs() < 0.01);
|
|||
|
|
assert!((max.x - 400.0).abs() < 0.01);
|
|||
|
|
assert!((min.y - (-300.0)).abs() < 0.01);
|
|||
|
|
assert!((max.y - 300.0).abs() < 0.01);
|
|||
|
|
}
|
|||
|
|
}
|