feat: 添加Math库和Google Test测试框架

- 新增Math库: Vector2/3/4, Matrix3/4, Quaternion, Transform, Color等
- 新增测试框架: Google Test (gtest)
- 新增140个单元测试,覆盖Vector, Matrix, Quaternion, Geometry
- VolumeRenderer支持vcpkg的NanoVDB
- 添加TESTING.md测试文档
This commit is contained in:
2026-03-13 18:43:14 +08:00
parent 5efa171050
commit 7c54a62f9e
41 changed files with 8530 additions and 1845 deletions

View File

@@ -1,6 +1,12 @@
cmake_minimum_required(VERSION 3.15) cmake_minimum_required(VERSION 3.15)
project(XCVolumeRenderer VERSION 1.0 LANGUAGES CXX) project(XCVolumeRenderer VERSION 1.0 LANGUAGES CXX)
# ============================================================
# Vcpkg Integration
# ============================================================
set(VCPKG_ROOT "D:/vcpkg")
set(CMAKE_TOOLCHAIN_FILE "${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "")
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
@@ -28,6 +34,7 @@ target_compile_options(${PROJECT_NAME} PRIVATE /utf-8 /MT)
target_include_directories(${PROJECT_NAME} PRIVATE target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}
${VCPKG_ROOT}/installed/x64-windows/include
) )
target_link_libraries(${PROJECT_NAME} PRIVATE target_link_libraries(${PROJECT_NAME} PRIVATE

285
docs/TESTING.md Normal file
View File

@@ -0,0 +1,285 @@
# XCEngine 测试体系文档
> **版本**: 1.0
> **日期**: 2026-03-13
---
## 1. 测试架构
```
tests/
├── CMakeLists.txt # 测试构建配置
├── run_tests.cmake # 测试运行脚本
├── fixtures/ # 测试夹具
│ └── MathFixtures.h
├── math/ # Math 单元测试
│ ├── CMakeLists.txt
│ ├── test_vector.cpp
│ ├── test_matrix.cpp
│ ├── test_quaternion.cpp
│ └── test_geometry.cpp
├── core/ # Core 测试
├── threading/ # 线程测试
├── memory/ # 内存测试
├── containers/ # 容器测试
└── rendering/ # 渲染测试
├── unit/ # 单元测试
├── integration/ # 集成测试
└── screenshots/ # 参考图
```
---
## 2. 测试分类
| 类型 | 目录 | 目的 | 运行频率 |
|------|------|------|---------|
| **Unit Test** | `tests/*/` | 验证单个函数/类 | 每次提交 |
| **Integration Test** | `tests/rendering/integration/` | 验证多模块协作 | 每次提交 |
| **Benchmark** | `tests/benchmark/` | 性能回归检测 | 每日/每周 |
| **Screenshot Test** | `tests/rendering/screenshots/` | 渲染正确性 | 每次提交 |
---
## 3. 测试命名规范
```cpp
// 格式: test_<模块>_<功能>_<场景>
TEST(Math_Vector3, Dot_TwoVectors_ReturnsCorrectValue) { }
TEST(Math_Vector3, Normalize_ZeroVector_ReturnsZeroVector) { }
TEST(Math_Matrix4, Inverse_Identity_ReturnsIdentity) { }
TEST(Math_Matrix4, TRS_Decompose_RecoversOriginalValues) { }
TEST(Math_Quaternion, Slerp_ShortestPath_InterpolatesCorrectly) { }
// 边界情况
TEST(Math_Vector3, Normalize_ZeroVector_DoesNotCrash) { }
TEST(Math_Matrix4, Inverse_SingularMatrix_ReturnsIdentity) { }
```
---
## 4. 断言规范
### 4.1 浮点数比较
```cpp
// 必须使用容差
EXPECT_NEAR(actual, expected, 1e-5f);
ASSERT_FLOAT_EQ(actual, expected); // gtest 内部有容差
// 数组比较
for (int i = 0; i < 4; i++) {
EXPECT_NEAR(actual.m[i], expected.m[i], 1e-5f);
}
```
### 4.2 常用断言
```cpp
EXPECT_TRUE(condition);
EXPECT_FALSE(condition);
EXPECT_EQ(actual, expected);
EXPECT_NE(actual, expected);
EXPECT_STREQ(actual, expected);
EXPECT_THROW(expression, exception_type);
```
---
## 5. 测试夹具 (Fixture)
```cpp
class MathFixture : public ::testing::Test {
protected:
void SetUp() override {
v1 = Vector3(1, 0, 0);
v2 = Vector3(0, 1, 0);
v3 = Vector3(1, 1, 1);
m1 = Matrix4x4::Identity();
m2 = Matrix4x4::Translation(Vector3(1, 2, 3));
}
Vector3 v1, v2, v3;
Matrix4x4 m1, m2;
const float epsilon = 1e-5f;
};
TEST_F(MathFixture, Dot_OrthogonalVectors_ReturnsZero) {
EXPECT_FLOAT_EQ(Vector3::Dot(v1, v2), 0.0f);
}
```
---
## 6. 参数化测试
```cpp
class MatrixInverseTest : public ::testing::TestWithParam<Matrix4x4> {};
INSTANTIATE_TEST_SUITE_P(
InverseCases,
MatrixInverseTest,
testing::Values(
Matrix4x4::Identity(),
Matrix4x4::Translation(Vector3(1,2,3)),
Matrix4x4::Scale(Vector3(2,2,2))
)
);
TEST_P(MatrixInverseTest, InverseOfInverse_EqualsOriginal) {
Matrix4x4 original = GetParam();
Matrix4x4 inverted = original.Inverse();
Matrix4x4 recovered = inverted.Inverse();
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
EXPECT_NEAR(original.m[i][j], recovered.m[i][j], 1e-4f);
}
}
}
```
---
## 7. Math 测试用例
### 7.1 Vector2/3/4 测试
| 测试类别 | 测试用例 |
|---------|---------|
| **构造** | 默认构造、参数构造、从 Vector3 构造 |
| **运算** | 加、减、乘、除、点积、叉积 |
| **归一化** | Normalize、Normalized、Magnitude、SqrMagnitude |
| **插值** | Lerp、MoveTowards |
| **投影** | Project、ProjectOnPlane |
| **角度** | Angle、Reflect |
### 7.2 Matrix 测试
| 测试类别 | 测试用例 |
|---------|---------|
| **构造** | Identity、Zero |
| **变换** | Translation、Rotation、Scale、TRS |
| **相机** | LookAt、Perspective、Orthographic |
| **运算** | 乘法、点乘、叉乘 |
| **分解** | Inverse、Transpose、Determinant、Decompose |
### 7.3 Quaternion 测试
| 测试类别 | 测试用例 |
|---------|---------|
| **构造** | Identity、FromAxisAngle、FromEulerAngles |
| **转换** | ToEulerAngles、ToMatrix4x4、FromRotationMatrix |
| **插值** | Slerp |
| **运算** | 乘法和逆 |
### 7.4 几何测试
| 测试类型 | 测试用例 |
|---------|---------|
| **Ray** | GetPoint、Intersects(Sphere/Box/Plane) |
| **Sphere** | Contains、Intersects |
| **Box** | Contains、Intersects |
| **Plane** | FromPoints、GetDistanceToPoint、Intersects |
| **Frustum** | Contains(Point/Sphere/Bounds)、Intersects |
| **Bounds** | GetMinMax、Intersects、Contains、Encapsulate |
---
## 8. 构建与运行
### 8.1 构建测试
```bash
# 创建构建目录
mkdir build && cd build
# 配置 CMake
cmake .. -G "Visual Studio 17 2022" -A x64
# 构建测试
cmake --build . --config Debug --target xcengine_math_tests
```
### 8.2 运行测试
```bash
# 运行所有测试
ctest --output-on-failure
# 运行 Math 测试
./tests/xcengine_math_tests.exe
# 运行特定测试
./tests/xcengine_math_tests.exe --gtest_filter=Math_Vector3.*
# 运行测试并显示详细信息
./tests/xcengine_math_tests.exe --gtest_also_run_disabled_tests --gtest_print_time=1
```
### 8.3 测试过滤器
```bash
# 运行所有 Vector3 测试
--gtest_filter=Math_Vector3.*
# 运行除某测试外的所有测试
--gtest_filter=-Math_Matrix4.SingularMatrix*
# 运行多个测试
--gtest_filter=Math_Vector3.*:Math_Matrix4.*
```
---
## 9. 覆盖率要求
| 模块 | 最低覆盖率 | 关键测试 |
|------|-----------|---------|
| Math | 90% | 所有公开 API |
| Core | 80% | 智能指针、Event |
| Containers | 85% | 边界、迭代器 |
| Memory | 90% | 分配/泄漏 |
| Threading | 70% | 基本功能 |
---
## 10. 持续集成
```yaml
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Configure
run: cmake -B build -DENABLE_COVERAGE=ON
- name: Build
run: cmake --build build --config Debug
- name: Test
run: ctest --test-dir build --output-on-failure
- name: Coverage
run: cmake --build build --target coverage
```
---
## 11. 注意事项
1. **浮点数比较** - 必须使用容差 (通常 1e-5 或 1e-6)
2. **边界条件** - 必须测试零向量、奇异矩阵等
3. **随机性** - 如需固定 seed 保证确定性
4. **线程安全** - 线程测试需设置超时
5. **内存泄漏** - 使用 Valgrind 或 CRT 检测

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,151 @@
# XCEngine 渲染引擎 - 第一阶段计划
> **目标**: 构建核心基础层,为上层渲染系统提供底层依赖
> **版本**: 1.0
> **日期**: 2026-03-13
---
## 阶段目标
第一阶段聚焦于引擎底层基础设施的建设,确保后续渲染系统开发有稳定的基础。
---
## 模块规划
### 1.1 数学库 (Math Library)
| 项目 | 内容 |
|------|------|
| **优先级** | P0 |
| **预计工作量** | 5天 |
| **包含类型** | `Vector2`, `Vector3`, `Vector4`, `Matrix3x3`, `Matrix4x4`, `Quaternion`, `Transform`, `Color`, `Rect`, `RectInt`, `Viewport`, `Ray`, `Sphere`, `Box`, `Plane`, `Frustum`, `Bounds`, `AABB`, `OBB` |
| **功能要求** | 向量运算、矩阵变换、四元数、欧拉角转换、视锥体剔除基础 |
### 1.2 Core 基础类型
| 项目 | 内容 |
|------|------|
| **优先级** | P0 |
| **预计工作量** | 2天 |
| **包含类型** | 基础类型别名 (`int8`, `uint8`, `int16`, `uint16`, `int32`, `uint32`, `int64`, `uint64`, `byte`)、`RefCounted``Ref<T>``UniqueRef<T>``Event<T>` |
| **功能要求** | 基础类型别名、引用计数、智能指针、事件系统 |
| **依赖** | 无 |
### 1.3 线程系统 (Threading)
| 项目 | 内容 |
|------|------|
| **优先级** | P0 |
| **预计工作量** | 4天 |
| **包含类型** | `ITask`, `LambdaTask`, `TaskGroup`, `TaskSystem`, `TaskSystemConfig`, `Mutex`, `SpinLock`, `ReadWriteLock`, `Thread` |
| **功能要求** | 任务调度、依赖管理、并行计算、同步原语 |
| **依赖** | Core基础类型 |
### 1.4 内存管理 (Memory Management)
| 项目 | 内容 |
|------|------|
| **优先级** | P0 |
| **预计工作量** | 3天 |
| **包含类型** | `IAllocator`, `LinearAllocator`, `PoolAllocator`, `ProxyAllocator`, `MemoryManager` |
| **功能要求** | 内存分配、追踪、泄漏检测、线性/池化分配策略 |
| **依赖** | 线程系统ProxyAllocator需要Mutex |
### 1.5 容器库 (Containers)
| 项目 | 内容 |
|------|------|
| **优先级** | P0 |
| **预计工作量** | 3天 |
| **包含类型** | `Array<T>`, `String`, `String::npos`, `HashMap<K,V>` |
| **功能要求** | 动态数组、字符串操作含npos常量、哈希映射 |
| **依赖** | Core基础类型, 内存管理 |
### 1.6 日志与调试系统
| 项目 | 内容 |
|------|------|
| **优先级** | P1 |
| **预计工作量** | 2天 |
| **包含类型** | `Logger`, `ConsoleLogSink`, `FileLogSink`, `FileWriter`, `Profiler`, `Assert` |
| **功能要求** | 分级日志、分类输出、文件写入、性能分析、断言 |
| **依赖** | Core基础类型, 容器库(String) |
---
## 时间安排
| 周次 | 内容 |
|------|------|
| 第1周 | 数学库 + Core基础类型 |
| 第2周 | 线程系统 + 内存管理 |
| 第3周 | 容器库 + 日志系统 |
> 内存管理依赖线程系统完成ProxyAllocator需要Mutex因此调整顺序
---
## 测试方案
### 测试框架
- **推荐**: Google Test (gtest) 或 Doctest
### 测试用例设计
| 模块 | 测试类别 | 测试用例示例 |
|------|---------|-------------|
| **Math** | 向量运算 | `Vector3::Dot`, `Cross`, `Normalize`, `Lerp` 精度测试 |
| **Math** | 矩阵运算 | `Matrix4x4::TRS`, `LookAt`, `Perspective` 结果正确性 |
| **Math** | 四元数 | `FromEulerAngles`, `Slerp`, `ToMatrix4x4` 精度验证 |
| **Core** | 引用计数 | `RefCounted` 多线程安全释放 |
| **Core** | 事件系统 | 订阅/取消订阅、线程安全调用 |
| **Threading** | 任务系统 | 依赖链、优先级、并行For、TaskSystemConfig |
| **Threading** | 同步原语 | 锁竞争、死锁检测 |
| **Memory** | 分配器 | 边界检查、碎片率、线性分配器回滚测试 |
| **Memory** | 泄漏检测 | 分配/释放计数、峰值追踪 |
| **Containers** | Array | 边界访问、迭代器、内存增长策略 |
| **Containers** | String | 子串、查找、大小写转换 |
| **Containers** | HashMap | 冲突处理、负载因子重分布 |
| **Logger** | 日志级别 | 过滤、分类、格式化 |
### 执行命令
```bash
# 编译并运行所有单元测试
cmake --build build --target xcengine_tests
./build/tests/xcengine_tests.exe
# 性能基准测试
./build/tests/xcengine_tests.exe --benchmark
```
---
## 验收标准
- [ ] 数学库通过全部运算正确性测试
- [ ] Core基础类型引用计数、智能指针工作正常
- [ ] 线程系统在高并发下稳定运行
- [ ] 内存分配器无内存泄漏
- [ ] 容器操作边界安全
- [ ] 日志系统输出格式正确
---
## 依赖关系
```
Math Library (无依赖)
├──▶ Core 基础类型 (无依赖)
│ │
│ ├──▶ Threading (依赖 Core)
│ │
│ ├──▶ Memory Management (依赖 Threading)
│ │ │
│ │ └──▶ Containers (依赖 Memory, Core)
│ │
│ └──▶ Logging & Debug (依赖 Core, Containers)
```

43
engine/CMakeLists.txt Normal file
View File

@@ -0,0 +1,43 @@
cmake_minimum_required(VERSION 3.15)
project(XCEngine)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(XCEngine STATIC
include/XCEngine/Math/Math.h
include/XCEngine/Math/Vector2.h
include/XCEngine/Math/Vector3.h
include/XCEngine/Math/Vector4.h
include/XCEngine/Math/Matrix3.h
include/XCEngine/Math/Matrix4.h
include/XCEngine/Math/Quaternion.h
include/XCEngine/Math/Transform.h
include/XCEngine/Math/Color.h
include/XCEngine/Math/Ray.h
include/XCEngine/Math/Plane.h
include/XCEngine/Math/Sphere.h
include/XCEngine/Math/Box.h
include/XCEngine/Math/Bounds.h
include/XCEngine/Math/Frustum.h
include/XCEngine/Math/Rect.h
include/XCEngine/Math/AABB.h
src/Math/Matrix.cpp
src/Math/Quaternion.cpp
src/Math/Transform.cpp
src/Math/Color.cpp
src/Math/Geometry.cpp
src/Math/FrustumBounds.cpp
)
target_include_directories(XCEngine PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine
${CMAKE_CURRENT_SOURCE_DIR}/src
)
if(MSVC)
target_compile_options(XCEngine PRIVATE /W3)
else()
target_compile_options(XCEngine PRIVATE -Wall)
endif()

View File

@@ -0,0 +1,12 @@
#pragma once
#include "Types.h"
#include "RefCounted.h"
#include "SmartPtr.h"
#include "Event.h"
namespace XCEngine {
namespace Core {
} // namespace Core
} // namespace XCEngine

View File

@@ -0,0 +1,79 @@
#pragma once
#include <functional>
#include <vector>
#include <mutex>
#include <algorithm>
namespace XCEngine {
namespace Core {
template<typename... Args>
class Event {
public:
using Callback = std::function<void(Args...)>;
using Listener = std::pair<uint64_t, Callback>;
using Iterator = typename std::vector<Listener>::iterator;
uint64_t Subscribe(Callback callback) {
std::lock_guard<std::mutex> lock(m_mutex);
uint64_t id = ++m_nextId;
m_listeners.emplace_back(id, std::move(callback));
return id;
}
void Unsubscribe(uint64_t id) {
std::lock_guard<std::mutex> lock(m_mutex);
m_pendingUnsubscribes.push_back(id);
}
void ProcessUnsubscribes() {
std::lock_guard<std::mutex> lock(m_mutex);
for (uint64_t id : m_pendingUnsubscribes) {
m_listeners.erase(
std::remove_if(m_listeners.begin(), m_listeners.end(),
[id](const auto& pair) { return pair.first == id; }),
m_listeners.end()
);
}
m_pendingUnsubscribes.clear();
}
void Invoke(Args... args) const {
std::vector<Listener> listenersCopy;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_pendingUnsubscribes.empty()) {
for (uint64_t id : m_pendingUnsubscribes) {
m_listeners.erase(
std::remove_if(m_listeners.begin(), m_listeners.end(),
[id](const auto& pair) { return pair.first == id; }),
m_listeners.end()
);
}
m_pendingUnsubscribes.clear();
}
listenersCopy = m_listeners;
}
for (const auto& [id, callback] : listenersCopy) {
callback(args...);
}
}
void Clear() {
std::lock_guard<std::mutex> lock(m_mutex);
m_listeners.clear();
}
Iterator begin() { return m_listeners.begin(); }
Iterator end() { return m_listeners.end(); }
private:
mutable std::mutex m_mutex;
std::vector<Listener> m_listeners;
std::vector<uint64_t> m_pendingUnsubscribes;
uint64_t m_nextId = 0;
};
} // namespace Core
} // namespace XCEngine

View File

@@ -0,0 +1,27 @@
#pragma once
#include <atomic>
namespace XCEngine {
namespace Core {
class RefCounted {
public:
RefCounted() : m_refCount(1) {}
virtual ~RefCounted() = default;
void AddRef() { ++m_refCount; }
void Release() {
if (--m_refCount == 0) {
delete this;
}
}
uint32_t GetRefCount() const { return m_refCount.load(); }
protected:
std::atomic<uint32_t> m_refCount;
};
} // namespace Core
} // namespace XCEngine

View File

@@ -0,0 +1,25 @@
#pragma once
#include <memory>
namespace XCEngine {
namespace Core {
template<typename T>
using Ref = std::shared_ptr<T>;
template<typename T>
using UniqueRef = std::unique_ptr<T>;
template<typename T, typename... Args>
Ref<T> MakeRef(Args&&... args) {
return std::make_shared<T>(std::forward<Args>(args)...);
}
template<typename T, typename... Args>
UniqueRef<T> MakeUnique(Args&&... args) {
return std::make_unique<T>(std::forward<Args>(args)...);
}
} // namespace Core
} // namespace XCEngine

View File

@@ -0,0 +1,19 @@
#pragma once
#include <cstdint>
namespace XCEngine {
namespace Core {
using int8 = int8_t;
using int16 = int16_t;
using int32 = int32_t;
using int64 = int64_t;
using uint8 = uint8_t;
using uint16 = uint16_t;
using uint32 = uint32_t;
using uint64 = uint64_t;
using byte = uint8_t;
} // namespace Core
} // namespace XCEngine

View File

@@ -0,0 +1,123 @@
#pragma once
#include "Math.h"
#include "Vector3.h"
#include "Bounds.h"
#include "Matrix4.h"
namespace XCEngine {
namespace Math {
struct Sphere;
struct OBB {
Vector3 center;
Vector3 extents;
Matrix4 transform;
OBB() : center(Vector3::Zero()), extents(Vector3::One()), transform(Matrix4::Identity()) {}
OBB(const Vector3& center, const Vector3& extents)
: center(center), extents(extents), transform(Matrix4::Identity()) {}
Vector3 GetAxis(int index) const {
return Vector3(transform.m[0][index], transform.m[1][index], transform.m[2][index]);
}
Vector3 GetMin() const {
return center - extents;
}
Vector3 GetMax() const {
return center + extents;
}
bool Contains(const Vector3& point) const {
Vector3 localPoint = transform.Inverse().MultiplyPoint(point);
return std::abs(localPoint.x) <= extents.x &&
std::abs(localPoint.y) <= extents.y &&
std::abs(localPoint.z) <= extents.z;
}
bool Intersects(const OBB& other) const {
OBB a = *this;
OBB b = other;
Vector3 ea = a.extents;
Vector3 eb = b.extents;
Vector3 pa = a.center;
Vector3 pb = b.center;
Vector3 na = a.GetAxis(0);
Vector3 nb = a.GetAxis(1);
Vector3 nc = a.GetAxis(2);
Vector3 nd = b.GetAxis(0);
Vector3 ne = b.GetAxis(1);
Vector3 nf = b.GetAxis(2);
Vector3 d = pb - pa;
float ra, rb;
ra = ea.x;
rb = std::abs(Vector3::Dot(d, na)) +
eb.x * std::abs(Vector3::Dot(nd, na)) +
eb.y * std::abs(Vector3::Dot(ne, na)) +
eb.z * std::abs(Vector3::Dot(nf, na));
if (ra < rb) return false;
ra = ea.y;
rb = std::abs(Vector3::Dot(d, nb)) +
eb.x * std::abs(Vector3::Dot(nd, nb)) +
eb.y * std::abs(Vector3::Dot(ne, nb)) +
eb.z * std::abs(Vector3::Dot(nf, nb));
if (ra < rb) return false;
ra = ea.z;
rb = std::abs(Vector3::Dot(d, nc)) +
eb.x * std::abs(Vector3::Dot(nd, nc)) +
eb.y * std::abs(Vector3::Dot(ne, nc)) +
eb.z * std::abs(Vector3::Dot(nf, nc));
if (ra < rb) return false;
ra = ea.x * std::abs(Vector3::Dot(na, nd)) +
ea.y * std::abs(Vector3::Dot(nb, nd)) +
ea.z * std::abs(Vector3::Dot(nc, nd));
rb = eb.x +
eb.y * std::abs(Vector3::Dot(ne, nd)) +
eb.z * std::abs(Vector3::Dot(nf, nd));
if (ra < rb) return false;
ra = ea.x * std::abs(Vector3::Dot(na, ne)) +
ea.y * std::abs(Vector3::Dot(nb, ne)) +
ea.z * std::abs(Vector3::Dot(nc, ne));
rb = eb.y +
eb.x * std::abs(Vector3::Dot(nd, ne)) +
eb.z * std::abs(Vector3::Dot(nf, ne));
if (ra < rb) return false;
ra = ea.x * std::abs(Vector3::Dot(na, nf)) +
ea.y * std::abs(Vector3::Dot(nb, nf)) +
ea.z * std::abs(Vector3::Dot(nc, nf));
rb = eb.z +
eb.x * std::abs(Vector3::Dot(nd, nf)) +
eb.y * std::abs(Vector3::Dot(ne, nf));
if (ra < rb) return false;
return true;
}
bool Intersects(const Sphere& sphere) const {
Vector3 localCenter = transform.Inverse().MultiplyPoint(sphere.center);
Vector3 closestPoint;
closestPoint.x = std::clamp(localCenter.x, -extents.x, extents.x);
closestPoint.y = std::clamp(localCenter.y, -extents.y, extents.y);
closestPoint.z = std::clamp(localCenter.z, -extents.z, extents.z);
float sqrDist = Vector3::SqrMagnitude(localCenter - closestPoint);
return sqrDist <= sphere.radius * sphere.radius;
}
};
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,31 @@
#pragma once
#include "Math.h"
#include "Vector3.h"
namespace XCEngine {
namespace Math {
struct Bounds {
Vector3 center = Vector3::Zero();
Vector3 extents = Vector3::Zero();
Bounds() = default;
Bounds(const Vector3& center, const Vector3& size);
Vector3 GetMin() const;
Vector3 GetMax() const;
void SetMinMax(const Vector3& min, const Vector3& max);
bool Intersects(const Bounds& other) const;
bool Contains(const Vector3& point) const;
void Encapsulate(const Vector3& point);
void Encapsulate(const Bounds& bounds);
void Expand(float amount);
void Expand(const Vector3& amount);
Vector3 GetClosestPoint(const Vector3& point) const;
float GetVolume() const;
};
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,30 @@
#pragma once
#include "Math.h"
#include "Vector3.h"
#include "Matrix4.h"
namespace XCEngine {
namespace Math {
struct Sphere;
struct Ray;
struct Box {
Vector3 center = Vector3::Zero();
Vector3 extents = Vector3::Zero();
Matrix4x4 transform = Matrix4x4::Identity();
Box() = default;
Box(const Vector3& center, const Vector3& extents);
Vector3 GetMin() const;
Vector3 GetMax() const;
bool Contains(const Vector3& point) const;
bool Intersects(const Sphere& sphere) const;
bool Intersects(const Box& other) const;
bool Intersects(const Ray& ray, float& t) const;
};
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,43 @@
#pragma once
#include "Math.h"
#include "Vector2.h"
#include "Vector3.h"
#include "Vector4.h"
namespace XCEngine {
namespace Math {
struct Color {
float r = 1.0f;
float g = 1.0f;
float b = 1.0f;
float a = 1.0f;
Color() = default;
constexpr Color(float r, float g, float b, float a = 1.0f) : r(r), g(g), b(b), a(a) {}
static Color White() { return Color(1, 1, 1, 1); }
static Color Black() { return Color(0, 0, 0, 1); }
static Color Red() { return Color(1, 0, 0, 1); }
static Color Green() { return Color(0, 1, 0, 1); }
static Color Blue() { return Color(0, 0, 1, 1); }
static Color Yellow() { return Color(1, 1, 0, 1); }
static Color Cyan() { return Color(0, 1, 1, 1); }
static Color Magenta() { return Color(1, 0, 1, 1); }
static Color Clear() { return Color(0, 0, 0, 0); }
static Color Lerp(const Color& a, const Color& b, float t);
uint32_t ToRGBA() const;
Vector3 ToVector3() const;
Vector4 ToVector4() const;
Color operator+(const Color& other) const;
Color operator-(const Color& other) const;
Color operator*(float scalar) const;
Color operator/(float scalar) const;
};
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,32 @@
#pragma once
#include "Math.h"
#include "Plane.h"
#include "Sphere.h"
#include "Bounds.h"
namespace XCEngine {
namespace Math {
class Frustum {
public:
Plane planes[6];
enum class PlaneIndex {
Left = 0,
Right = 1,
Bottom = 2,
Top = 3,
Near = 4,
Far = 5
};
bool Contains(const Vector3& point) const;
bool Contains(const Sphere& sphere) const;
bool Contains(const Bounds& bounds) const;
bool Intersects(const Bounds& bounds) const;
bool Intersects(const Sphere& sphere) const;
};
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,27 @@
#pragma once
#include <cmath>
#include <algorithm>
#include <cstdint>
#ifdef _MSC_VER
#define _USE_MATH_DEFINES
#include <math.h>
#endif
namespace XCEngine {
namespace Math {
constexpr float PI = 3.14159265358979323846f;
constexpr float TWO_PI = 6.28318530717958647692f;
constexpr float HALF_PI = 1.57079632679489661923f;
constexpr float DEG_TO_RAD = PI / 180.0f;
constexpr float RAD_TO_DEG = 180.0f / PI;
constexpr float EPSILON = 1e-6f;
constexpr float FLOAT_MAX = 3.402823466e+38f;
inline float Radians(float degrees) { return degrees * DEG_TO_RAD; }
inline float Degrees(float radians) { return radians * RAD_TO_DEG; }
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,50 @@
#pragma once
#include "Math.h"
#include "Vector3.h"
namespace XCEngine {
namespace Math {
struct Quaternion;
struct Matrix3x3 {
float m[3][3];
Matrix3x3() {
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
m[i][j] = 0.0f;
}
static Matrix3x3 Identity() {
Matrix3x3 result;
result.m[0][0] = 1.0f;
result.m[1][1] = 1.0f;
result.m[2][2] = 1.0f;
return result;
}
static Matrix3x3 Zero() {
return Matrix3x3();
}
static Matrix3x3 RotationX(float radians);
static Matrix3x3 RotationY(float radians);
static Matrix3x3 RotationZ(float radians);
static Matrix3x3 Scale(const Vector3& scale);
Matrix3x3 operator*(const Matrix3x3& other) const;
Vector3 operator*(const Vector3& v) const;
Matrix3x3 Transpose() const;
Matrix3x3 Inverse() const;
float Determinant() const;
float* operator[](int row) { return m[row]; }
const float* operator[](int row) const { return m[row]; }
};
using Matrix3 = Matrix3x3;
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,66 @@
#pragma once
#include "Math.h"
#include "Vector3.h"
#include "Vector4.h"
namespace XCEngine {
namespace Math {
struct Quaternion;
struct Matrix4x4 {
public:
float m[4][4];
Matrix4x4() {
for (int i = 0; i < 4; i++)
for (int j = 0; j < 4; j++)
m[i][j] = 0.0f;
}
static Matrix4x4 Identity() {
Matrix4x4 result;
result.m[0][0] = 1.0f;
result.m[1][1] = 1.0f;
result.m[2][2] = 1.0f;
result.m[3][3] = 1.0f;
return result;
}
static Matrix4x4 Zero() {
return Matrix4x4();
}
static Matrix4x4 Translation(const Vector3& v);
static Matrix4x4 Rotation(const Quaternion& q);
static Matrix4x4 Scale(const Vector3& v);
static Matrix4x4 TRS(const Vector3& translation, const Quaternion& rotation, const Vector3& scale);
static Matrix4x4 LookAt(const Vector3& eye, const Vector3& target, const Vector3& up);
static Matrix4x4 Perspective(float fov, float aspect, float near, float far);
static Matrix4x4 Orthographic(float left, float right, float bottom, float top, float near, float far);
static Matrix4x4 RotationX(float radians);
static Matrix4x4 RotationY(float radians);
static Matrix4x4 RotationZ(float radians);
Matrix4x4 operator*(const Matrix4x4& other) const;
Vector4 operator*(const Vector4& v) const;
Vector3 MultiplyPoint(const Vector3& v) const;
Vector3 MultiplyVector(const Vector3& v) const;
Matrix4x4 Transpose() const;
Matrix4x4 Inverse() const;
float Determinant() const;
Vector3 GetTranslation() const;
Quaternion GetRotation() const;
Vector3 GetScale() const;
void Decompose(Vector3& translation, Quaternion& rotation, Vector3& scale) const;
float* operator[](int row) { return m[row]; }
const float* operator[](int row) const { return m[row]; }
};
using Matrix4 = Matrix4x4;
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,29 @@
#pragma once
#include "Math.h"
#include "Vector3.h"
namespace XCEngine {
namespace Math {
struct Sphere;
struct Box;
struct Ray;
struct Plane {
Vector3 normal = Vector3::Up();
float distance = 0.0f;
Plane() = default;
Plane(const Vector3& normal, float distance);
static Plane FromPoints(const Vector3& a, const Vector3& b, const Vector3& c);
float GetDistanceToPoint(const Vector3& point) const;
Vector3 GetClosestPoint(const Vector3& point) const;
bool GetSide(const Vector3& point) const;
bool Intersects(const Sphere& sphere) const;
};
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,40 @@
#pragma once
#include "Math.h"
#include "Vector3.h"
namespace XCEngine {
namespace Math {
struct Matrix4x4;
struct Quaternion {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float w = 1.0f;
Quaternion() = default;
constexpr Quaternion(float x, float y, float z, float w) : x(x), y(y), z(z), w(w) {}
static Quaternion Identity() { return Quaternion(0, 0, 0, 1); }
static Quaternion FromAxisAngle(const Vector3& axis, float radians);
static Quaternion FromEulerAngles(float pitch, float yaw, float roll);
static Quaternion FromEulerAngles(const Vector3& euler);
static Quaternion FromRotationMatrix(const Matrix4x4& matrix);
static Quaternion Slerp(const Quaternion& a, const Quaternion& b, float t);
static Quaternion LookRotation(const Vector3& forward, const Vector3& up = Vector3::Up());
Vector3 ToEulerAngles() const;
Matrix4x4 ToMatrix4x4() const;
Quaternion operator*(const Quaternion& other) const;
Quaternion Inverse() const;
float Dot(const Quaternion& other) const;
float Magnitude() const;
Quaternion Normalized() const;
static Quaternion Normalize(const Quaternion& q);
};
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,27 @@
#pragma once
#include "Math.h"
#include "Vector3.h"
namespace XCEngine {
namespace Math {
struct Sphere;
struct Box;
struct Plane;
struct Ray {
Vector3 origin;
Vector3 direction;
Ray() = default;
Ray(const Vector3& origin, const Vector3& direction);
Vector3 GetPoint(float t) const;
bool Intersects(const Sphere& sphere, float& t) const;
bool Intersects(const Box& box, float& t) const;
bool Intersects(const Plane& plane, float& t) const;
};
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,120 @@
#pragma once
#include "Math.h"
#include "Vector2.h"
namespace XCEngine {
namespace Math {
struct Rect {
float x = 0.0f;
float y = 0.0f;
float width = 0.0f;
float height = 0.0f;
Rect() = default;
Rect(float x, float y, float w, float h) : x(x), y(y), width(w), height(h) {}
float GetLeft() const { return x; }
float GetRight() const { return x + width; }
float GetTop() const { return y; }
float GetBottom() const { return y + height; }
Vector2 GetPosition() const { return Vector2(x, y); }
Vector2 GetSize() const { return Vector2(width, height); }
Vector2 GetCenter() const { return Vector2(x + width * 0.5f, y + height * 0.5f); }
bool Contains(float px, float py) const {
return px >= x && px < x + width && py >= y && py < y + height;
}
bool Contains(const Vector2& point) const {
return Contains(point.x, point.y);
}
bool Intersects(const Rect& other) const {
return !(GetRight() < other.GetLeft() || GetLeft() > other.GetRight() ||
GetBottom() < other.GetTop() || GetTop() > other.GetBottom());
}
static Rect Intersect(const Rect& a, const Rect& b) {
float left = std::max(a.GetLeft(), b.GetLeft());
float top = std::max(a.GetTop(), b.GetTop());
float right = std::min(a.GetRight(), b.GetRight());
float bottom = std::min(a.GetBottom(), b.GetBottom());
return Rect(left, top, std::max(0.0f, right - left), std::max(0.0f, bottom - top));
}
static Rect Union(const Rect& a, const Rect& b) {
float left = std::min(a.GetLeft(), b.GetLeft());
float top = std::min(a.GetTop(), b.GetTop());
float right = std::max(a.GetRight(), b.GetRight());
float bottom = std::max(a.GetBottom(), b.GetBottom());
return Rect(left, top, right - left, bottom - top);
}
void Set(float newX, float newY, float newWidth, float newHeight) {
x = newX; y = newY; width = newWidth; height = newHeight;
}
void SetPosition(float newX, float newY) {
x = newX; y = newY;
}
void SetPosition(const Vector2& position) {
x = position.x; y = position.y;
}
};
struct RectInt {
int32_t x = 0;
int32_t y = 0;
int32_t width = 0;
int32_t height = 0;
RectInt() = default;
RectInt(int32_t x, int32_t y, int32_t w, int32_t h) : x(x), y(y), width(w), height(h) {}
int32_t GetLeft() const { return x; }
int32_t GetRight() const { return x + width; }
int32_t GetTop() const { return y; }
int32_t GetBottom() const { return y + height; }
Vector2 GetPosition() const { return Vector2(static_cast<float>(x), static_cast<float>(y)); }
Vector2 GetSize() const { return Vector2(static_cast<float>(width), static_cast<float>(height)); }
Vector2 GetCenter() const { return Vector2(static_cast<float>(x + width / 2), static_cast<float>(y + height / 2)); }
bool Contains(int32_t px, int32_t py) const {
return px >= x && px < x + width && py >= y && py < y + height;
}
bool Intersects(const RectInt& other) const {
return !(GetRight() < other.GetLeft() || GetLeft() > other.GetRight() ||
GetBottom() < other.GetTop() || GetTop() > other.GetBottom());
}
};
struct Viewport {
float x = 0.0f;
float y = 0.0f;
float width = 0.0f;
float height = 0.0f;
float minDepth = 0.0f;
float maxDepth = 1.0f;
Viewport() = default;
Viewport(float x, float y, float w, float h) : x(x), y(y), width(w), height(h) {}
Viewport(float x, float y, float w, float h, float minD, float maxD)
: x(x), y(y), width(w), height(h), minDepth(minD), maxDepth(maxD) {}
float GetAspectRatio() const {
return height > 0.0f ? width / height : 0.0f;
}
Rect GetRect() const {
return Rect(x, y, width, height);
}
};
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,21 @@
#pragma once
#include "Math.h"
#include "Vector3.h"
namespace XCEngine {
namespace Math {
struct Sphere {
Vector3 center = Vector3::Zero();
float radius = 0.0f;
Sphere() = default;
Sphere(const Vector3& center, float radius);
bool Contains(const Vector3& point) const;
bool Intersects(const Sphere& other) const;
};
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,31 @@
#pragma once
#include "Math.h"
#include "Vector3.h"
#include "Quaternion.h"
#include "Matrix4.h"
namespace XCEngine {
namespace Math {
enum class Space {
Self,
World
};
struct Transform {
Vector3 position = Vector3::Zero();
Quaternion rotation = Quaternion::Identity();
Vector3 scale = Vector3::One();
Matrix4 ToMatrix() const;
Transform Inverse() const;
Transform operator*(const Transform& other) const;
Vector3 TransformPoint(const Vector3& point) const;
Vector3 TransformDirection(const Vector3& direction) const;
Vector3 InverseTransformPoint(const Vector3& point) const;
Vector3 InverseTransformDirection(const Vector3& direction) const;
};
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,66 @@
#pragma once
#include "Math.h"
namespace XCEngine {
namespace Math {
struct Vector2 {
float x = 0.0f;
float y = 0.0f;
Vector2() = default;
constexpr Vector2(float x, float y) : x(x), y(y) {}
static Vector2 Zero() { return Vector2(0, 0); }
static Vector2 One() { return Vector2(1, 1); }
static Vector2 Up() { return Vector2(0, 1); }
static Vector2 Down() { return Vector2(0, -1); }
static Vector2 Right() { return Vector2(1, 0); }
static Vector2 Left() { return Vector2(-1, 0); }
static float Dot(const Vector2& a, const Vector2& b) { return a.x * b.x + a.y * b.y; }
static float Cross(const Vector2& a, const Vector2& b) { return a.x * b.y - a.y * b.x; }
static Vector2 Normalize(const Vector2& v) {
float mag = Magnitude(v);
return mag > EPSILON ? Vector2(v.x / mag, v.y / mag) : Zero();
}
static float Magnitude(const Vector2& v) { return std::sqrt(v.x * v.x + v.y * v.y); }
static float SqrMagnitude(const Vector2& v) { return v.x * v.x + v.y * v.y; }
static Vector2 Lerp(const Vector2& a, const Vector2& b, float t) {
t = std::clamp(t, 0.0f, 1.0f);
return Vector2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
}
static Vector2 MoveTowards(const Vector2& current, const Vector2& target, float maxDistance) {
Vector2 diff = target - current;
float dist = Magnitude(diff);
if (dist <= maxDistance || dist < EPSILON) return target;
return current + Normalize(diff) * maxDistance;
}
float Magnitude() const { return Magnitude(*this); }
float SqrMagnitude() const { return SqrMagnitude(*this); }
Vector2 Normalized() const { return Normalize(*this); }
Vector2 operator+(const Vector2& other) const { return Vector2(x + other.x, y + other.y); }
Vector2 operator-(const Vector2& other) const { return Vector2(x - other.x, y - other.y); }
Vector2 operator*(float scalar) const { return Vector2(x * scalar, y * scalar); }
Vector2 operator/(float scalar) const { return Vector2(x / scalar, y / scalar); }
Vector2& operator+=(const Vector2& other) { x += other.x; y += other.y; return *this; }
Vector2& operator-=(const Vector2& other) { x -= other.x; y -= other.y; return *this; }
Vector2& operator*=(float scalar) { x *= scalar; y *= scalar; return *this; }
Vector2& operator/=(float scalar) { x /= scalar; y /= scalar; return *this; }
bool operator==(const Vector2& other) const {
return std::abs(x - other.x) < EPSILON && std::abs(y - other.y) < EPSILON;
}
bool operator!=(const Vector2& other) const { return !(*this == other); }
};
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,117 @@
#pragma once
#include "Math.h"
namespace XCEngine {
namespace Math {
struct Quaternion;
struct Vector3 {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
Vector3() = default;
constexpr Vector3(float x, float y, float z) : x(x), y(y), z(z) {}
static Vector3 Zero() { return Vector3(0, 0, 0); }
static Vector3 One() { return Vector3(1, 1, 1); }
static Vector3 Forward() { return Vector3(0, 0, 1); }
static Vector3 Back() { return Vector3(0, 0, -1); }
static Vector3 Up() { return Vector3(0, 1, 0); }
static Vector3 Down() { return Vector3(0, -1, 0); }
static Vector3 Right() { return Vector3(1, 0, 0); }
static Vector3 Left() { return Vector3(-1, 0, 0); }
static float Dot(const Vector3& a, const Vector3& b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
static Vector3 Cross(const Vector3& a, const Vector3& b) {
return Vector3(
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x
);
}
static Vector3 Normalize(const Vector3& v) {
float mag = Magnitude(v);
return mag > EPSILON ? Vector3(v.x / mag, v.y / mag, v.z / mag) : Zero();
}
static float Magnitude(const Vector3& v) { return std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z); }
static float SqrMagnitude(const Vector3& v) { return v.x * v.x + v.y * v.y + v.z * v.z; }
static Vector3 Lerp(const Vector3& a, const Vector3& b, float t) {
t = std::clamp(t, 0.0f, 1.0f);
return Vector3(
a.x + (b.x - a.x) * t,
a.y + (b.y - a.y) * t,
a.z + (b.z - a.z) * t
);
}
static Vector3 MoveTowards(const Vector3& current, const Vector3& target, float maxDistance) {
Vector3 diff = target - current;
float dist = Magnitude(diff);
if (dist <= maxDistance || dist < EPSILON) return target;
return current + Normalize(diff) * maxDistance;
}
static Vector3 Project(const Vector3& vector, const Vector3& onNormal) {
float sqrMag = Dot(onNormal, onNormal);
if (sqrMag < EPSILON) return Zero();
float dot = Dot(vector, onNormal);
return onNormal * (dot / sqrMag);
}
static Vector3 ProjectOnPlane(const Vector3& vector, const Vector3& planeNormal) {
float sqrMag = Dot(planeNormal, planeNormal);
if (sqrMag < EPSILON) return vector;
float dot = Dot(vector, planeNormal);
return vector - planeNormal * (dot / sqrMag);
}
static float Angle(const Vector3& from, const Vector3& to) {
float denominator = std::sqrt(SqrMagnitude(from) * SqrMagnitude(to));
if (denominator < EPSILON) return 0.0f;
float dot = std::clamp(Dot(from, to) / denominator, -1.0f, 1.0f);
return std::acos(dot) * RAD_TO_DEG;
}
static Vector3 Reflect(const Vector3& inDirection, const Vector3& inNormal) {
float dot = Dot(inDirection, inNormal);
return inDirection - inNormal * (2.0f * dot);
}
float Magnitude() const { return Magnitude(*this); }
float SqrMagnitude() const { return SqrMagnitude(*this); }
Vector3 Normalized() const { return Normalize(*this); }
Vector3 operator+(const Vector3& other) const { return Vector3(x + other.x, y + other.y, z + other.z); }
Vector3 operator-(const Vector3& other) const { return Vector3(x - other.x, y - other.y, z - other.z); }
Vector3 operator*(float scalar) const { return Vector3(x * scalar, y * scalar, z * scalar); }
Vector3 operator/(float scalar) const { return Vector3(x / scalar, y / scalar, z / scalar); }
Vector3 operator*(const Vector3& other) const { return Vector3(x * other.x, y * other.y, z * other.z); }
Vector3 operator/(const Vector3& other) const { return Vector3(x / other.x, y / other.y, z / other.z); }
Vector3& operator+=(const Vector3& other) { x += other.x; y += other.y; z += other.z; return *this; }
Vector3& operator-=(const Vector3& other) { x -= other.x; y -= other.y; z -= other.z; return *this; }
Vector3& operator*=(float scalar) { x *= scalar; y *= scalar; z *= scalar; return *this; }
Vector3& operator/=(float scalar) { x /= scalar; y /= scalar; z /= scalar; return *this; }
float operator[](int index) const { return (&x)[index]; }
float& operator[](int index) { return (&x)[index]; }
bool operator==(const Vector3& other) const {
return std::abs(x - other.x) < EPSILON &&
std::abs(y - other.y) < EPSILON &&
std::abs(z - other.z) < EPSILON;
}
bool operator!=(const Vector3& other) const { return !(*this == other); }
};
Vector3 operator*(const Quaternion& q, const Vector3& v);
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,57 @@
#pragma once
#include "Math.h"
namespace XCEngine {
namespace Math {
struct Vector4 {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float w = 0.0f;
Vector4() = default;
constexpr Vector4(float x, float y, float z, float w) : x(x), y(y), z(z), w(w) {}
explicit Vector4(const Vector3& v, float w = 0.0f) : x(v.x), y(v.y), z(v.z), w(w) {}
static Vector4 Zero() { return Vector4(0, 0, 0, 0); }
static Vector4 One() { return Vector4(1, 1, 1, 1); }
static float Dot(const Vector4& a, const Vector4& b) {
return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
}
static Vector4 Project(const Vector4& vector, const Vector4& onNormal) {
float sqrMag = Dot(onNormal, onNormal);
if (sqrMag < EPSILON) return Zero();
float dot = Dot(vector, onNormal);
return onNormal * (dot / sqrMag);
}
Vector3 ToVector3() const { return Vector3(x, y, z); }
float operator[](int index) const { return (&x)[index]; }
float& operator[](int index) { return (&x)[index]; }
Vector4 operator+(const Vector4& other) const {
return Vector4(x + other.x, y + other.y, z + other.z, w + other.w);
}
Vector4 operator-(const Vector4& other) const {
return Vector4(x - other.x, y - other.y, z - other.z, w - other.w);
}
Vector4 operator*(float scalar) const {
return Vector4(x * scalar, y * scalar, z * scalar, w * scalar);
}
bool operator==(const Vector4& other) const {
return std::abs(x - other.x) < EPSILON &&
std::abs(y - other.y) < EPSILON &&
std::abs(z - other.z) < EPSILON &&
std::abs(w - other.w) < EPSILON;
}
bool operator!=(const Vector4& other) const { return !(*this == other); }
};
} // namespace Math
} // namespace XCEngine

49
engine/src/Math/Color.cpp Normal file
View File

@@ -0,0 +1,49 @@
#include "Math/Color.h"
namespace XCEngine {
namespace Math {
Color Color::Lerp(const Color& a, const Color& b, float t) {
t = std::clamp(t, 0.0f, 1.0f);
return Color(
a.r + (b.r - a.r) * t,
a.g + (b.g - a.g) * t,
a.b + (b.b - a.b) * t,
a.a + (b.a - a.a) * t
);
}
uint32_t Color::ToRGBA() const {
uint32_t r = static_cast<uint32_t>(std::clamp(r * 255.0f, 0.0f, 255.0f));
uint32_t g = static_cast<uint32_t>(std::clamp(g * 255.0f, 0.0f, 255.0f));
uint32_t b = static_cast<uint32_t>(std::clamp(b * 255.0f, 0.0f, 255.0f));
uint32_t a = static_cast<uint32_t>(std::clamp(a * 255.0f, 0.0f, 255.0f));
return (r << 24) | (g << 16) | (b << 8) | a;
}
Vector3 Color::ToVector3() const {
return Vector3(r, g, b);
}
Vector4 Color::ToVector4() const {
return Vector4(r, g, b, a);
}
Color Color::operator+(const Color& other) const {
return Color(r + other.r, g + other.g, b + other.b, a + other.a);
}
Color Color::operator-(const Color& other) const {
return Color(r - other.r, g - other.g, b - other.b, a - other.a);
}
Color Color::operator*(float scalar) const {
return Color(r * scalar, g * scalar, b * scalar, a * scalar);
}
Color Color::operator/(float scalar) const {
return Color(r / scalar, g / scalar, b / scalar, a / scalar);
}
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,159 @@
#include "Math/Frustum.h"
#include "Math/Bounds.h"
namespace XCEngine {
namespace Math {
bool Frustum::Contains(const Vector3& point) const {
for (int i = 0; i < 6; i++) {
if (planes[i].GetDistanceToPoint(point) < 0.0f) {
return false;
}
}
return true;
}
bool Frustum::Contains(const Sphere& sphere) const {
for (int i = 0; i < 6; i++) {
float distance = planes[i].GetDistanceToPoint(sphere.center);
if (distance < -sphere.radius) {
return false;
}
}
return true;
}
bool Frustum::Contains(const Bounds& bounds) const {
Vector3 corners[8] = {
bounds.GetMin(),
Vector3(bounds.GetMax().x, bounds.GetMin().y, bounds.GetMin().z),
Vector3(bounds.GetMin().x, bounds.GetMax().y, bounds.GetMin().z),
Vector3(bounds.GetMin().x, bounds.GetMin().y, bounds.GetMax().z),
bounds.GetMax(),
Vector3(bounds.GetMin().x, bounds.GetMax().y, bounds.GetMax().z),
Vector3(bounds.GetMax().x, bounds.GetMin().y, bounds.GetMax().z),
Vector3(bounds.GetMax().x, bounds.GetMax().y, bounds.GetMin().z)
};
for (int i = 0; i < 6; i++) {
bool allOutside = true;
for (int j = 0; j < 8; j++) {
if (planes[i].GetDistanceToPoint(corners[j]) >= 0.0f) {
allOutside = false;
break;
}
}
if (allOutside) {
return false;
}
}
return true;
}
bool Frustum::Intersects(const Bounds& bounds) const {
Vector3 corners[8] = {
bounds.GetMin(),
Vector3(bounds.GetMax().x, bounds.GetMin().y, bounds.GetMin().z),
Vector3(bounds.GetMin().x, bounds.GetMax().y, bounds.GetMin().z),
Vector3(bounds.GetMin().x, bounds.GetMin().y, bounds.GetMax().z),
bounds.GetMax(),
Vector3(bounds.GetMin().x, bounds.GetMax().y, bounds.GetMax().z),
Vector3(bounds.GetMax().x, bounds.GetMin().y, bounds.GetMax().z),
Vector3(bounds.GetMax().x, bounds.GetMax().y, bounds.GetMin().z)
};
for (int i = 0; i < 6; i++) {
bool allPositive = true;
bool allNegative = true;
for (int j = 0; j < 8; j++) {
float dist = planes[i].GetDistanceToPoint(corners[j]);
if (dist >= 0.0f) allNegative = false;
else allPositive = false;
}
if (allPositive || allNegative) {
return false;
}
}
return true;
}
bool Frustum::Intersects(const Sphere& sphere) const {
for (int i = 0; i < 6; i++) {
if (planes[i].GetDistanceToPoint(sphere.center) < -sphere.radius) {
return false;
}
}
return true;
}
Bounds::Bounds(const Vector3& center, const Vector3& size)
: center(center), extents(size * 0.5f) {}
Vector3 Bounds::GetMin() const {
return center - extents;
}
Vector3 Bounds::GetMax() const {
return center + extents;
}
void Bounds::SetMinMax(const Vector3& min, const Vector3& max) {
center = (min + max) * 0.5f;
extents = (max - min) * 0.5f;
}
bool Bounds::Intersects(const Bounds& other) const {
if (std::abs(center.x - other.center.x) > (extents.x + other.extents.x)) return false;
if (std::abs(center.y - other.center.y) > (extents.y + other.extents.y)) return false;
if (std::abs(center.z - other.center.z) > (extents.z + other.extents.z)) return false;
return true;
}
bool Bounds::Contains(const Vector3& point) const {
return point.x >= GetMin().x && point.x <= GetMax().x &&
point.y >= GetMin().y && point.y <= GetMax().y &&
point.z >= GetMin().z && point.z <= GetMax().z;
}
void Bounds::Encapsulate(const Vector3& point) {
Vector3 min = GetMin();
Vector3 max = GetMax();
min = Vector3(std::min(min.x, point.x), std::min(min.y, point.y), std::min(min.z, point.z));
max = Vector3(std::max(max.x, point.x), std::max(max.y, point.y), std::max(max.z, point.z));
SetMinMax(min, max);
}
void Bounds::Encapsulate(const Bounds& bounds) {
Vector3 min = GetMin();
Vector3 max = GetMax();
Vector3 boundsMin = bounds.GetMin();
Vector3 boundsMax = bounds.GetMax();
min = Vector3(std::min(min.x, boundsMin.x), std::min(min.y, boundsMin.y), std::min(min.z, boundsMin.z));
max = Vector3(std::max(max.x, boundsMax.x), std::max(max.y, boundsMax.y), std::max(max.z, boundsMax.z));
SetMinMax(min, max);
}
void Bounds::Expand(float amount) {
extents += Vector3(amount, amount, amount) * 0.5f;
}
void Bounds::Expand(const Vector3& amount) {
extents += amount * 0.5f;
}
Vector3 Bounds::GetClosestPoint(const Vector3& point) const {
Vector3 result = point;
Vector3 min = GetMin();
Vector3 max = GetMax();
result.x = std::clamp(result.x, min.x, max.x);
result.y = std::clamp(result.y, min.y, max.y);
result.z = std::clamp(result.z, min.z, max.z);
return result;
}
float Bounds::GetVolume() const {
return (extents.x * 2.0f) * (extents.y * 2.0f) * (extents.z * 2.0f);
}
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,161 @@
#include "Math/Sphere.h"
#include "Math/Plane.h"
#include "Math/Box.h"
#include "Math/Ray.h"
namespace XCEngine {
namespace Math {
Ray::Ray(const Vector3& origin, const Vector3& direction)
: origin(origin), direction(Vector3::Normalize(direction)) {}
Vector3 Ray::GetPoint(float t) const {
return origin + direction * t;
}
bool Ray::Intersects(const Sphere& sphere, float& t) const {
Vector3 oc = origin - sphere.center;
float a = Vector3::Dot(direction, direction);
float b = 2.0f * Vector3::Dot(oc, direction);
float c = Vector3::Dot(oc, oc) - sphere.radius * sphere.radius;
float discriminant = b * b - 4 * a * c;
if (discriminant < 0) {
return false;
}
float sqrtDiscriminant = std::sqrt(discriminant);
float t0 = (-b - sqrtDiscriminant) / (2.0f * a);
float t1 = (-b + sqrtDiscriminant) / (2.0f * a);
if (t0 > 0) {
t = t0;
return true;
} else if (t1 > 0) {
t = t1;
return true;
}
return false;
}
bool Ray::Intersects(const Box& box, float& t) const {
float tmin = 0.0f;
float tmax = FLOAT_MAX;
Vector3 invDir = Vector3(1.0f / direction.x, 1.0f / direction.y, 1.0f / direction.z);
int sign[3] = { invDir.x < 0, invDir.y < 0, invDir.z < 0 };
Vector3 bounds[2] = { box.GetMin(), box.GetMax() };
for (int i = 0; i < 3; i++) {
float t0 = (bounds[sign[i]][i] - origin[i]) * invDir[i];
float t1 = (bounds[1 - sign[i]][i] - origin[i]) * invDir[i];
tmin = std::max(tmin, std::min(t0, t1));
tmax = std::min(tmax, std::max(t0, t1));
if (tmax < tmin) {
return false;
}
}
if (tmin > 0) {
t = tmin;
} else {
t = tmax;
}
return tmax >= 0;
}
bool Ray::Intersects(const Plane& plane, float& t) const {
float denom = Vector3::Dot(plane.normal, direction);
if (std::abs(denom) < EPSILON) {
return false;
}
t = -(Vector3::Dot(plane.normal, origin) + plane.distance) / denom;
return t >= 0;
}
Sphere::Sphere(const Vector3& center, float radius) : center(center), radius(radius) {}
bool Sphere::Contains(const Vector3& point) const {
return Vector3::SqrMagnitude(point - center) <= radius * radius;
}
bool Sphere::Intersects(const Sphere& other) const {
float sqrDist = Vector3::SqrMagnitude(center - other.center);
float sumRadii = radius + other.radius;
return sqrDist <= sumRadii * sumRadii;
}
Plane::Plane(const Vector3& normal, float distance) : normal(Vector3::Normalize(normal)), distance(distance) {}
Plane Plane::FromPoints(const Vector3& a, const Vector3& b, const Vector3& c) {
Vector3 normal = Vector3::Normalize(Vector3::Cross(b - a, c - a));
float distance = Vector3::Dot(normal, a);
return Plane(normal, distance);
}
float Plane::GetDistanceToPoint(const Vector3& point) const {
return Vector3::Dot(normal, point) + distance;
}
Vector3 Plane::GetClosestPoint(const Vector3& point) const {
float distance = GetDistanceToPoint(point);
return point - normal * distance;
}
bool Plane::GetSide(const Vector3& point) const {
return GetDistanceToPoint(point) > 0.0f;
}
bool Plane::Intersects(const Sphere& sphere) const {
return std::abs(GetDistanceToPoint(sphere.center)) <= sphere.radius;
}
Box::Box(const Vector3& center, const Vector3& extents) : center(center), extents(extents) {}
Vector3 Box::GetMin() const {
return center - extents;
}
Vector3 Box::GetMax() const {
return center + extents;
}
bool Box::Contains(const Vector3& point) const {
Vector3 localPoint = transform.Inverse().MultiplyPoint(point);
return std::abs(localPoint.x) <= extents.x &&
std::abs(localPoint.y) <= extents.y &&
std::abs(localPoint.z) <= extents.z;
}
bool Box::Intersects(const Sphere& sphere) const {
Vector3 localCenter = transform.Inverse().MultiplyPoint(sphere.center);
Vector3 closestPoint;
closestPoint.x = std::clamp(localCenter.x, -extents.x, extents.x);
closestPoint.y = std::clamp(localCenter.y, -extents.y, extents.y);
closestPoint.z = std::clamp(localCenter.z, -extents.z, extents.z);
float sqrDist = Vector3::SqrMagnitude(localCenter - closestPoint);
return sqrDist <= sphere.radius * sphere.radius;
}
bool Box::Intersects(const Box& other) const {
Vector3 aMin = GetMin();
Vector3 aMax = GetMax();
Vector3 bMin = other.GetMin();
Vector3 bMax = other.GetMax();
return (aMin.x <= bMax.x && aMax.x >= bMin.x) &&
(aMin.y <= bMax.y && aMax.y >= bMin.y) &&
(aMin.z <= bMax.z && aMax.z >= bMin.z);
}
bool Box::Intersects(const Ray& ray, float& t) const {
return ray.Intersects(*this, t);
}
} // namespace Math
} // namespace XCEngine

329
engine/src/Math/Matrix.cpp Normal file
View File

@@ -0,0 +1,329 @@
#include "Math/Matrix3.h"
#include "Math/Matrix4.h"
#include "Math/Quaternion.h"
#include "Math/Vector3.h"
namespace XCEngine {
namespace Math {
Matrix3 Matrix3::RotationX(float radians) {
float c = std::cos(radians);
float s = std::sin(radians);
Matrix3 result;
result.m[1][1] = c; result.m[1][2] = -s;
result.m[2][1] = s; result.m[2][2] = c;
return result;
}
Matrix3 Matrix3::RotationY(float radians) {
float c = std::cos(radians);
float s = std::sin(radians);
Matrix3 result;
result.m[0][0] = c; result.m[0][2] = s;
result.m[2][0] = -s; result.m[2][2] = c;
return result;
}
Matrix3 Matrix3::RotationZ(float radians) {
float c = std::cos(radians);
float s = std::sin(radians);
Matrix3 result;
result.m[0][0] = c; result.m[0][1] = -s;
result.m[1][0] = s; result.m[1][1] = c;
return result;
}
Matrix3 Matrix3::Scale(const Vector3& scale) {
Matrix3 result;
result.m[0][0] = scale.x;
result.m[1][1] = scale.y;
result.m[2][2] = scale.z;
return result;
}
Matrix3 Matrix3::operator*(const Matrix3& other) const {
Matrix3 result;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
result.m[i][j] = m[i][0] * other.m[0][j] +
m[i][1] * other.m[1][j] +
m[i][2] * other.m[2][j];
}
}
return result;
}
Vector3 Matrix3::operator*(const Vector3& v) const {
return Vector3(
m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z,
m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z,
m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z
);
}
Matrix3 Matrix3::Transpose() const {
Matrix3 result;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
result.m[i][j] = m[j][i];
return result;
}
float Matrix3::Determinant() const {
return m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) -
m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) +
m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
}
Matrix3 Matrix3::Inverse() const {
float det = Determinant();
if (std::abs(det) < EPSILON) {
return Identity();
}
float invDet = 1.0f / det;
Matrix3 result;
result.m[0][0] = (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * invDet;
result.m[0][1] = (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * invDet;
result.m[0][2] = (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * invDet;
result.m[1][0] = (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * invDet;
result.m[1][1] = (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * invDet;
result.m[1][2] = (m[1][0] * m[0][2] - m[0][0] * m[1][2]) * invDet;
result.m[2][0] = (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * invDet;
result.m[2][1] = (m[2][0] * m[0][1] - m[0][0] * m[2][1]) * invDet;
result.m[2][2] = (m[0][0] * m[1][1] - m[1][0] * m[0][1]) * invDet;
return result;
}
Matrix4 Matrix4::Translation(const Vector3& v) {
Matrix4 result = Identity();
result.m[0][3] = v.x;
result.m[1][3] = v.y;
result.m[2][3] = v.z;
return result;
}
Matrix4 Matrix4::Rotation(const Quaternion& q) {
return q.ToMatrix4x4();
}
Matrix4 Matrix4::Scale(const Vector3& v) {
Matrix4 result;
result.m[0][0] = v.x;
result.m[1][1] = v.y;
result.m[2][2] = v.z;
result.m[3][3] = 1.0f;
return result;
}
Matrix4 Matrix4::TRS(const Vector3& translation, const Quaternion& rotation, const Vector3& scale) {
return Translation(translation) * Rotation(rotation) * Scale(scale);
}
Matrix4 Matrix4::LookAt(const Vector3& eye, const Vector3& target, const Vector3& up) {
Vector3 z = Vector3::Normalize(eye - target);
if (Vector3::SqrMagnitude(z) < EPSILON) {
z = Vector3::Forward();
}
Vector3 x = Vector3::Normalize(Vector3::Cross(up, z));
if (Vector3::SqrMagnitude(x) < EPSILON) {
x = Vector3::Right();
}
Vector3 y = Vector3::Cross(z, x);
Matrix4 result;
result.m[0][0] = x.x; result.m[0][1] = x.y; result.m[0][2] = x.z; result.m[0][3] = -Vector3::Dot(x, eye);
result.m[1][0] = y.x; result.m[1][1] = y.y; result.m[1][2] = y.z; result.m[1][3] = -Vector3::Dot(y, eye);
result.m[2][0] = z.x; result.m[2][1] = z.y; result.m[2][2] = z.z; result.m[2][3] = -Vector3::Dot(z, eye);
result.m[3][0] = 0.0f; result.m[3][1] = 0.0f; result.m[3][2] = 0.0f; result.m[3][3] = 1.0f;
return result;
}
Matrix4 Matrix4::Perspective(float fov, float aspect, float near, float far) {
float tanHalfFov = std::tan(fov * 0.5f);
Matrix4 result;
result.m[0][0] = 1.0f / (aspect * tanHalfFov);
result.m[1][1] = 1.0f / tanHalfFov;
result.m[2][2] = -(far + near) / (far - near);
result.m[2][3] = -(2.0f * far * near) / (far - near);
result.m[3][2] = -1.0f;
return result;
}
Matrix4 Matrix4::Orthographic(float left, float right, float bottom, float top, float near, float far) {
Matrix4 result = Identity();
result.m[0][0] = 2.0f / (right - left);
result.m[1][1] = 2.0f / (top - bottom);
result.m[2][2] = -2.0f / (far - near);
result.m[0][3] = -(right + left) / (right - left);
result.m[1][3] = -(top + bottom) / (top - bottom);
result.m[2][3] = -(far + near) / (far - near);
return result;
}
Matrix4 Matrix4::RotationX(float radians) {
float c = std::cos(radians);
float s = std::sin(radians);
Matrix4 result = Identity();
result.m[1][1] = c; result.m[1][2] = -s;
result.m[2][1] = s; result.m[2][2] = c;
return result;
}
Matrix4 Matrix4::RotationY(float radians) {
float c = std::cos(radians);
float s = std::sin(radians);
Matrix4 result = Identity();
result.m[0][0] = c; result.m[0][2] = s;
result.m[2][0] = -s; result.m[2][2] = c;
return result;
}
Matrix4 Matrix4::RotationZ(float radians) {
float c = std::cos(radians);
float s = std::sin(radians);
Matrix4 result = Identity();
result.m[0][0] = c; result.m[0][1] = -s;
result.m[1][0] = s; result.m[1][1] = c;
return result;
}
Matrix4 Matrix4::operator*(const Matrix4& other) const {
Matrix4 result;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
result.m[i][j] = m[i][0] * other.m[0][j] +
m[i][1] * other.m[1][j] +
m[i][2] * other.m[2][j] +
m[i][3] * other.m[3][j];
}
}
return result;
}
Vector4 Matrix4::operator*(const Vector4& v) const {
return Vector4(
m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z + m[0][3] * v.w,
m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z + m[1][3] * v.w,
m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z + m[2][3] * v.w,
m[3][0] * v.x + m[3][1] * v.y + m[3][2] * v.z + m[3][3] * v.w
);
}
Vector3 Matrix4::MultiplyPoint(const Vector3& v) const {
Vector4 v4(v.x, v.y, v.z, 1.0f);
Vector4 result = (*this) * v4;
return Vector3(result.x, result.y, result.z);
}
Vector3 Matrix4::MultiplyVector(const Vector3& v) const {
Vector4 v4(v.x, v.y, v.z, 0.0f);
Vector4 result = (*this) * v4;
return Vector3(result.x, result.y, result.z);
}
Matrix4 Matrix4::Transpose() const {
Matrix4 result;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
result.m[i][j] = m[j][i];
}
}
return result;
}
float Matrix4::Determinant() const {
float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3];
float a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3];
float a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3];
float a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3];
float b00 = a00 * a11 - a01 * a10;
float b01 = a00 * a12 - a02 * a10;
float b02 = a00 * a13 - a03 * a10;
float b03 = a01 * a12 - a02 * a11;
float b04 = a01 * a13 - a03 * a11;
float b05 = a02 * a13 - a03 * a12;
float b06 = a20 * a31 - a21 * a30;
float b07 = a20 * a32 - a22 * a30;
float b08 = a20 * a33 - a23 * a30;
float b09 = a21 * a32 - a22 * a31;
float b10 = a21 * a33 - a23 * a31;
float b11 = a22 * a33 - a23 * a32;
return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
}
Matrix4 Matrix4::Inverse() const {
float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3];
float a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3];
float a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3];
float a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3];
float b00 = a00 * a11 - a01 * a10;
float b01 = a00 * a12 - a02 * a10;
float b02 = a00 * a13 - a03 * a10;
float b03 = a01 * a12 - a02 * a11;
float b04 = a01 * a13 - a03 * a11;
float b05 = a02 * a13 - a03 * a12;
float b06 = a20 * a31 - a21 * a30;
float b07 = a20 * a32 - a22 * a30;
float b08 = a20 * a33 - a23 * a30;
float b09 = a21 * a32 - a22 * a31;
float b10 = a21 * a33 - a23 * a31;
float b11 = a22 * a33 - a23 * a32;
float det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
if (std::abs(det) < EPSILON) {
return Identity();
}
float invDet = 1.0f / det;
Matrix4 result;
result.m[0][0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet;
result.m[0][1] = (a02 * b10 - a01 * b11 - a03 * b09) * invDet;
result.m[0][2] = (a31 * b05 - a32 * b04 + a33 * b03) * invDet;
result.m[0][3] = (a22 * b04 - a21 * b05 - a23 * b03) * invDet;
result.m[1][0] = (a12 * b08 - a10 * b11 - a13 * b07) * invDet;
result.m[1][1] = (a00 * b11 - a02 * b08 + a03 * b07) * invDet;
result.m[1][2] = (a32 * b02 - a30 * b05 - a33 * b01) * invDet;
result.m[1][3] = (a20 * b05 - a22 * b02 + a23 * b01) * invDet;
result.m[2][0] = (a10 * b10 - a11 * b08 + a13 * b06) * invDet;
result.m[2][1] = (a01 * b08 - a00 * b10 - a03 * b06) * invDet;
result.m[2][2] = (a30 * b04 - a31 * b02 + a33 * b00) * invDet;
result.m[2][3] = (a21 * b02 - a20 * b04 - a23 * b00) * invDet;
result.m[3][0] = (a11 * b07 - a10 * b09 - a12 * b06) * invDet;
result.m[3][1] = (a00 * b09 - a01 * b07 + a02 * b06) * invDet;
result.m[3][2] = (a31 * b01 - a30 * b03 - a32 * b00) * invDet;
result.m[3][3] = (a20 * b03 - a21 * b01 + a22 * b00) * invDet;
return result;
}
Vector3 Matrix4::GetTranslation() const {
return Vector3(m[0][3], m[1][3], m[2][3]);
}
Quaternion Matrix4::GetRotation() const {
return Quaternion::FromRotationMatrix(*this);
}
Vector3 Matrix4::GetScale() const {
return Vector3(
std::sqrt(m[0][0] * m[0][0] + m[1][0] * m[1][0] + m[2][0] * m[2][0]),
std::sqrt(m[0][1] * m[0][1] + m[1][1] * m[1][1] + m[2][1] * m[2][1]),
std::sqrt(m[0][2] * m[0][2] + m[1][2] * m[1][2] + m[2][2] * m[2][2])
);
}
void Matrix4::Decompose(Vector3& translation, Quaternion& rotation, Vector3& scale) const {
translation = GetTranslation();
scale = GetScale();
rotation = GetRotation();
}
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,223 @@
#include "Math/Quaternion.h"
#include "Math/Matrix4.h"
#include "Math/Vector3.h"
namespace XCEngine {
namespace Math {
Quaternion Quaternion::FromAxisAngle(const Vector3& axis, float radians) {
float halfAngle = radians * 0.5f;
float s = std::sin(halfAngle);
return Quaternion(
axis.x * s,
axis.y * s,
axis.z * s,
std::cos(halfAngle)
);
}
Quaternion Quaternion::FromEulerAngles(float pitch, float yaw, float roll) {
float halfPitch = pitch * 0.5f;
float halfYaw = yaw * 0.5f;
float halfRoll = roll * 0.5f;
float sinPitch = std::sin(halfPitch);
float cosPitch = std::cos(halfPitch);
float sinYaw = std::sin(halfYaw);
float cosYaw = std::cos(halfYaw);
float sinRoll = std::sin(halfRoll);
float cosRoll = std::cos(halfRoll);
return Quaternion(
cosYaw * sinPitch * cosRoll + sinYaw * cosPitch * sinRoll,
sinYaw * cosPitch * cosRoll - cosYaw * sinPitch * sinRoll,
cosYaw * cosPitch * sinRoll - sinYaw * sinPitch * cosRoll,
cosYaw * cosPitch * cosRoll + sinYaw * sinPitch * sinRoll
);
}
Quaternion Quaternion::FromEulerAngles(const Vector3& euler) {
return FromEulerAngles(euler.x, euler.y, euler.z);
}
Quaternion Quaternion::FromRotationMatrix(const Matrix4& matrix) {
float trace = matrix.m[0][0] + matrix.m[1][1] + matrix.m[2][2];
Quaternion q;
if (trace > 0.0f) {
float s = std::sqrt(trace + 1.0f) * 2.0f;
q.w = 0.25f * s;
q.x = (matrix.m[2][1] - matrix.m[1][2]) / s;
q.y = (matrix.m[0][2] - matrix.m[2][0]) / s;
q.z = (matrix.m[1][0] - matrix.m[0][1]) / s;
} else if (matrix.m[0][0] > matrix.m[1][1] && matrix.m[0][0] > matrix.m[2][2]) {
float s = std::sqrt(1.0f + matrix.m[0][0] - matrix.m[1][1] - matrix.m[2][2]) * 2.0f;
q.w = (matrix.m[2][1] - matrix.m[1][2]) / s;
q.x = 0.25f * s;
q.y = (matrix.m[0][1] + matrix.m[1][0]) / s;
q.z = (matrix.m[0][2] + matrix.m[2][0]) / s;
} else if (matrix.m[1][1] > matrix.m[2][2]) {
float s = std::sqrt(1.0f + matrix.m[1][1] - matrix.m[0][0] - matrix.m[2][2]) * 2.0f;
q.w = (matrix.m[0][2] - matrix.m[2][0]) / s;
q.x = (matrix.m[0][1] + matrix.m[1][0]) / s;
q.y = 0.25f * s;
q.z = (matrix.m[1][2] + matrix.m[2][1]) / s;
} else {
float s = std::sqrt(1.0f + matrix.m[2][2] - matrix.m[0][0] - matrix.m[1][1]) * 2.0f;
q.w = (matrix.m[1][0] - matrix.m[0][1]) / s;
q.x = (matrix.m[0][2] + matrix.m[2][0]) / s;
q.y = (matrix.m[1][2] + matrix.m[2][1]) / s;
q.z = 0.25f * s;
}
return q.Normalized();
}
Quaternion Quaternion::Slerp(const Quaternion& a, const Quaternion& b, float t) {
t = std::clamp(t, 0.0f, 1.0f);
float cosHalfTheta = a.w * b.w + a.x * b.x + a.y * b.y + a.z * b.z;
if (cosHalfTheta < 0.0f) {
return Slerp(Quaternion(-b.x, -b.y, -b.z, -b.w), Quaternion(a.x, a.y, a.z, a.w), 1.0f - t);
}
if (std::abs(cosHalfTheta) >= 1.0f) {
return a;
}
float halfTheta = std::acos(cosHalfTheta);
float sinHalfTheta = std::sqrt(1.0f - cosHalfTheta * cosHalfTheta);
if (std::abs(sinHalfTheta) < EPSILON) {
return Quaternion(
a.w * 0.5f + b.w * 0.5f,
a.x * 0.5f + b.x * 0.5f,
a.y * 0.5f + b.y * 0.5f,
a.z * 0.5f + b.z * 0.5f
);
}
float ratioA = std::sin((1.0f - t) * halfTheta) / sinHalfTheta;
float ratioB = std::sin(t * halfTheta) / sinHalfTheta;
return Quaternion(
a.x * ratioA + b.x * ratioB,
a.y * ratioA + b.y * ratioB,
a.z * ratioA + b.z * ratioB,
a.w * ratioA + b.w * ratioB
);
}
Quaternion Quaternion::LookRotation(const Vector3& forward, const Vector3& up) {
Vector3 f = Vector3::Normalize(forward);
Vector3 r = Vector3::Normalize(Vector3::Cross(up, f));
Vector3 u = Vector3::Cross(f, r);
Matrix4 m;
m.m[0][0] = r.x; m.m[0][1] = r.y; m.m[0][2] = r.z;
m.m[1][0] = u.x; m.m[1][1] = u.y; m.m[1][2] = u.z;
m.m[2][0] = f.x; m.m[2][1] = f.y; m.m[2][2] = f.z;
m.m[0][3] = m[1][3] = m[2][3] = 0.0f;
m.m[3][0] = m[3][1] = m[3][2] = 0.0f;
m.m[3][3] = 1.0f;
return FromRotationMatrix(m);
}
Vector3 Quaternion::ToEulerAngles() const {
float sinrCosp = 2.0f * (w * x + y * z);
float cosrCosp = 1.0f - 2.0f * (x * x + y * y);
float roll = std::atan2(sinrCosp, cosrCosp);
float sinp = 2.0f * (w * y - z * x);
float pitch;
if (std::abs(sinp) >= 1.0f) {
pitch = std::copysign(PI * 0.5f, sinp);
} else {
pitch = std::asin(sinp);
}
float sinyCosp = 2.0f * (w * z + x * y);
float cosyCosp = 1.0f - 2.0f * (y * y + z * z);
float yaw = std::atan2(sinyCosp, cosyCosp);
return Vector3(pitch, yaw, roll);
}
Matrix4 Quaternion::ToMatrix4x4() const {
float xx = x * x;
float yy = y * y;
float zz = z * z;
float xy = x * y;
float xz = x * z;
float yz = y * z;
float wx = w * x;
float wy = w * y;
float wz = w * z;
Matrix4 result;
result.m[0][0] = 1.0f - 2.0f * (yy + zz);
result.m[0][1] = 2.0f * (xy - wz);
result.m[0][2] = 2.0f * (xz + wy);
result.m[1][0] = 2.0f * (xy + wz);
result.m[1][1] = 1.0f - 2.0f * (xx + zz);
result.m[1][2] = 2.0f * (yz - wx);
result.m[2][0] = 2.0f * (xz - wy);
result.m[2][1] = 2.0f * (yz + wx);
result.m[2][2] = 1.0f - 2.0f * (xx + yy);
result.m[3][3] = 1.0f;
return result;
}
Quaternion Quaternion::operator*(const Quaternion& other) const {
return Quaternion(
w * other.x + x * other.w + y * other.z - z * other.y,
w * other.y - x * other.z + y * other.w + z * other.x,
w * other.z + x * other.y - y * other.x + z * other.w,
w * other.w - x * other.x - y * other.y - z * other.z
);
}
Quaternion Quaternion::Inverse() const {
float norm = x * x + y * y + z * z + w * w;
if (norm > 0.0f) {
float invNorm = 1.0f / norm;
return Quaternion(-x * invNorm, -y * invNorm, -z * invNorm, w * invNorm);
}
return Identity();
}
float Quaternion::Dot(const Quaternion& other) const {
return x * other.x + y * other.y + z * other.z + w * other.w;
}
float Quaternion::Magnitude() const {
return std::sqrt(x * x + y * y + z * z + w * w);
}
Quaternion Quaternion::Normalized() const {
float mag = Magnitude();
if (mag > EPSILON) {
return Quaternion(x / mag, y / mag, z / mag, w / mag);
}
return Identity();
}
Quaternion Quaternion::Normalize(const Quaternion& q) {
return q.Normalized();
}
Vector3 operator*(const Quaternion& q, const Vector3& v) {
Vector3 qv(q.x, q.y, q.z);
Vector3 uv = Vector3::Cross(qv, v);
Vector3 uuv = Vector3::Cross(qv, uv);
return v + (uv * q.w + uuv) * 2.0f;
}
} // namespace Math
} // namespace XCEngine

View File

@@ -0,0 +1,44 @@
#include "Math/Transform.h"
#include "Math/Matrix4.h"
namespace XCEngine {
namespace Math {
Matrix4 Transform::ToMatrix() const {
return Matrix4x4::TRS(position, rotation, scale);
}
Transform Transform::Inverse() const {
Transform result;
result.rotation = rotation.Inverse();
result.scale = Vector3::One() / scale;
result.position = result.rotation * (Vector3::Zero() - position) * result.scale;
return result;
}
Transform Transform::operator*(const Transform& other) const {
Transform result;
result.position = position + rotation * (scale * other.position);
result.rotation = rotation * other.rotation;
result.scale = scale * other.scale;
return result;
}
Vector3 Transform::TransformPoint(const Vector3& point) const {
return position + rotation * (scale * point);
}
Vector3 Transform::TransformDirection(const Vector3& direction) const {
return rotation * (scale * direction);
}
Vector3 Transform::InverseTransformPoint(const Vector3& point) const {
return (rotation.Inverse() * (point - position)) / scale;
}
Vector3 Transform::InverseTransformDirection(const Vector3& direction) const {
return rotation.Inverse() * direction / scale;
}
} // namespace Math
} // namespace XCEngine

50
tests/CMakeLists.txt Normal file
View File

@@ -0,0 +1,50 @@
cmake_minimum_required(VERSION 3.15)
project(XCEngineTests)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ============================================================
# Test Configuration
# ============================================================
option(ENABLE_COVERAGE "Enable code coverage" OFF)
option(ENABLE_BENCHMARK "Enable benchmark tests" OFF)
# ============================================================
# Dependencies
# ============================================================
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://gitee.com/mirrors/googletest.git
GIT_TAG v1.14.0
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
# ============================================================
# Engine Library
# ============================================================
add_subdirectory(../engine engine)
# ============================================================
# Test Subdirectories
# ============================================================
add_subdirectory(math)
# ============================================================
# Test Summary
# ============================================================
add_custom_target(print_tests
COMMAND ${CMAKE_COMMAND} -E echo "===== XCEngine Test Suite ====="
COMMAND ${CMAKE_CTEST_COMMAND} -N
COMMENT "Available tests:"
)

40
tests/fixtures/MathFixtures.h vendored Normal file
View File

@@ -0,0 +1,40 @@
#pragma once
#include <gtest/gtest.h>
#include <XCEngine/Math/Math.h>
namespace XCEngine {
namespace Math {
namespace Test {
constexpr float TEST_EPSILON = 1e-5f;
constexpr double TEST_EPSILON_D = 1e-8;
class MathTest : public ::testing::Test {
protected:
void SetUp() override {
v1 = Vector3(1.0f, 0.0f, 0.0f);
v2 = Vector3(0.0f, 1.0f, 0.0f);
v3 = Vector3(0.0f, 0.0f, 1.0f);
v4 = Vector3(1.0f, 1.0f, 1.0f);
vZero = Vector3::Zero();
vOne = Vector3::One();
mIdentity = Matrix4x4::Identity();
mTranslation = Matrix4x4::Translation(Vector3(1.0f, 2.0f, 3.0f));
mScale = Matrix4x4::Scale(Vector3(2.0f, 2.0f, 2.0f));
qIdentity = Quaternion::Identity();
qRotationX = Quaternion::FromAxisAngle(Vector3::Right(), Math::PI * 0.5f);
qRotationY = Quaternion::FromAxisAngle(Vector3::Up(), Math::PI * 0.5f);
}
Vector3 v1, v2, v3, v4;
Vector3 vZero, vOne;
Matrix4x4 mIdentity, mTranslation, mScale;
Quaternion qIdentity, qRotationX, qRotationY;
};
} // namespace Test
} // namespace Math
} // namespace XCEngine

33
tests/math/CMakeLists.txt Normal file
View File

@@ -0,0 +1,33 @@
# ============================================================
# Math Library Tests
# ============================================================
set(MATH_TEST_SOURCES
test_vector.cpp
test_matrix.cpp
test_quaternion.cpp
test_geometry.cpp
)
add_executable(xcengine_math_tests ${MATH_TEST_SOURCES})
# Exclude all static runtime libraries to avoid conflicts
if(MSVC)
set_target_properties(xcengine_math_tests PROPERTIES
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
)
endif()
target_link_libraries(xcengine_math_tests
PRIVATE
XCEngine
GTest::gtest
GTest::gtest_main
)
target_include_directories(xcengine_math_tests PRIVATE
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/tests/fixtures
)
add_test(NAME MathTests COMMAND xcengine_math_tests)

View File

@@ -0,0 +1,385 @@
#include <gtest/gtest.h>
#include <XCEngine/Math/Ray.h>
#include <XCEngine/Math/Sphere.h>
#include <XCEngine/Math/Box.h>
#include <XCEngine/Math/Plane.h>
#include <XCEngine/Math/Bounds.h>
#include <XCEngine/Math/Frustum.h>
#include <XCEngine/Math/Matrix4.h>
using namespace XCEngine::Math;
namespace {
// ============================================================
// Ray Tests
// ============================================================
TEST(Math_Ray, DefaultConstructor) {
Ray ray;
EXPECT_FLOAT_EQ(ray.origin.x, 0.0f);
EXPECT_FLOAT_EQ(ray.origin.y, 0.0f);
EXPECT_FLOAT_EQ(ray.origin.z, 0.0f);
EXPECT_FLOAT_EQ(ray.direction.x, 0.0f);
EXPECT_FLOAT_EQ(ray.direction.y, 0.0f);
EXPECT_FLOAT_EQ(ray.direction.z, 0.0f);
}
TEST(Math_Ray, ParameterConstructor) {
Vector3 origin(1, 2, 3);
Vector3 direction(0, 0, 1);
Ray ray(origin, direction);
EXPECT_FLOAT_EQ(ray.origin.x, 1.0f);
EXPECT_FLOAT_EQ(ray.origin.y, 2.0f);
EXPECT_FLOAT_EQ(ray.origin.z, 3.0f);
}
TEST(Math_Ray, GetPoint) {
Ray ray(Vector3::Zero(), Vector3::Right());
Vector3 point = ray.GetPoint(5.0f);
EXPECT_FLOAT_EQ(point.x, 5.0f);
EXPECT_FLOAT_EQ(point.y, 0.0f);
EXPECT_FLOAT_EQ(point.z, 0.0f);
}
TEST(Math_Ray, IntersectsSphere_Hit) {
Ray ray(Vector3(-5, 0, 0), Vector3::Right());
Sphere sphere(Vector3::Zero(), 1.0f);
float t;
bool hit = ray.Intersects(sphere, t);
EXPECT_TRUE(hit);
EXPECT_GE(t, 0.0f);
}
TEST(Math_Ray, IntersectsSphere_Miss) {
Ray ray(Vector3(0, 5, 0), Vector3::Right());
Sphere sphere(Vector3::Zero(), 1.0f);
float t;
bool hit = ray.Intersects(sphere, t);
EXPECT_FALSE(hit);
}
TEST(Math_Ray, IntersectsBox_Hit) {
Ray ray(Vector3(-5, 0, 0), Vector3::Right());
Box box(Vector3::Zero(), Vector3(1, 1, 1));
float t;
bool hit = ray.Intersects(box, t);
EXPECT_TRUE(hit);
EXPECT_GE(t, 0.0f);
}
TEST(Math_Ray, IntersectsBox_Miss) {
Ray ray(Vector3(0, 5, 0), Vector3::Right());
Box box(Vector3(10, 0, 0), Vector3(1, 1, 1));
float t;
bool hit = ray.Intersects(box, t);
EXPECT_FALSE(hit);
}
TEST(Math_Ray, IntersectsPlane_Hit) {
Ray ray(Vector3(0, 0, -5), Vector3::Forward());
Plane plane(Vector3::Forward(), 0.0f);
float t;
bool hit = ray.Intersects(plane, t);
EXPECT_TRUE(hit);
EXPECT_NEAR(t, 5.0f, 1e-3f);
}
TEST(Math_Ray, IntersectsPlane_Miss) {
Ray ray(Vector3(0, 0, -5), Vector3::Back());
Plane plane(Vector3::Forward(), 0.0f);
float t;
bool hit = ray.Intersects(plane, t);
EXPECT_FALSE(hit);
}
// ============================================================
// Sphere Tests
// ============================================================
TEST(Math_Sphere, DefaultConstructor) {
Sphere sphere;
EXPECT_FLOAT_EQ(sphere.center.x, 0.0f);
EXPECT_FLOAT_EQ(sphere.radius, 0.0f);
}
TEST(Math_Sphere, ParameterConstructor) {
Sphere sphere(Vector3(1, 2, 3), 5.0f);
EXPECT_FLOAT_EQ(sphere.center.x, 1.0f);
EXPECT_FLOAT_EQ(sphere.center.y, 2.0f);
EXPECT_FLOAT_EQ(sphere.center.z, 3.0f);
EXPECT_FLOAT_EQ(sphere.radius, 5.0f);
}
TEST(Math_Sphere, Contains_Inside) {
Sphere sphere(Vector3::Zero(), 2.0f);
EXPECT_TRUE(sphere.Contains(Vector3(1, 0, 0)));
}
TEST(Math_Sphere, Contains_Outside) {
Sphere sphere(Vector3::Zero(), 1.0f);
EXPECT_FALSE(sphere.Contains(Vector3(5, 0, 0)));
}
TEST(Math_Sphere, Contains_OnSurface) {
Sphere sphere(Vector3::Zero(), 1.0f);
EXPECT_TRUE(sphere.Contains(Vector3(1, 0, 0)));
}
TEST(Math_Sphere, Intersects_Overlap) {
Sphere a(Vector3::Zero(), 1.0f);
Sphere b(Vector3(1.5f, 0, 0), 1.0f);
EXPECT_TRUE(a.Intersects(b));
}
TEST(Math_Sphere, Intersects_Separate) {
Sphere a(Vector3::Zero(), 1.0f);
Sphere b(Vector3(5, 0, 0), 1.0f);
EXPECT_FALSE(a.Intersects(b));
}
// ============================================================
// Plane Tests
// ============================================================
TEST(Math_Plane, DefaultConstructor) {
Plane plane;
EXPECT_FLOAT_EQ(plane.normal.x, 0.0f);
EXPECT_FLOAT_EQ(plane.normal.y, 1.0f);
EXPECT_FLOAT_EQ(plane.normal.z, 0.0f);
EXPECT_FLOAT_EQ(plane.distance, 0.0f);
}
TEST(Math_Plane, ParameterConstructor) {
Plane plane(Vector3::Forward(), 5.0f);
EXPECT_FLOAT_EQ(plane.normal.z, 1.0f);
EXPECT_FLOAT_EQ(plane.distance, 5.0f);
}
TEST(Math_Plane, FromPoints) {
Plane plane = Plane::FromPoints(
Vector3(0, 0, 0),
Vector3(1, 0, 0),
Vector3(0, 1, 0)
);
EXPECT_FLOAT_EQ(plane.normal.z, -1.0f);
EXPECT_FLOAT_EQ(plane.distance, 0.0f);
}
TEST(Math_Plane, GetDistanceToPoint) {
Plane plane(Vector3::Forward(), 0.0f);
float dist = plane.GetDistanceToPoint(Vector3(0, 0, 5));
EXPECT_FLOAT_EQ(dist, 5.0f);
}
TEST(Math_Plane, GetClosestPoint) {
Plane plane(Vector3::Forward(), 0.0f);
Vector3 point = plane.GetClosestPoint(Vector3(3, 4, 5));
EXPECT_FLOAT_EQ(point.x, 3.0f);
EXPECT_FLOAT_EQ(point.y, 4.0f);
EXPECT_FLOAT_EQ(point.z, 0.0f);
}
TEST(Math_Plane, GetSide_Positive) {
Plane plane(Vector3::Forward(), 0.0f);
EXPECT_TRUE(plane.GetSide(Vector3(0, 0, 1)));
}
TEST(Math_Plane, GetSide_Negative) {
Plane plane(Vector3::Forward(), 0.0f);
EXPECT_FALSE(plane.GetSide(Vector3(0, 0, -1)));
}
TEST(Math_Plane, IntersectsSphere_Inside) {
Plane plane(Vector3::Forward(), 0.0f);
Sphere sphere(Vector3(0, 0, 0), 1.0f);
EXPECT_TRUE(plane.Intersects(sphere));
}
TEST(Math_Plane, IntersectsSphere_Outside) {
Plane plane(Vector3::Forward(), 5.0f);
Sphere sphere(Vector3(0, 0, 0), 1.0f);
EXPECT_FALSE(plane.Intersects(sphere));
}
// ============================================================
// Bounds Tests
// ============================================================
TEST(Math_Bounds, DefaultConstructor) {
Bounds bounds;
EXPECT_FLOAT_EQ(bounds.center.x, 0.0f);
EXPECT_FLOAT_EQ(bounds.extents.x, 0.0f);
}
TEST(Math_Bounds, ParameterConstructor) {
Bounds bounds(Vector3(5, 5, 5), Vector3(10, 10, 10));
EXPECT_FLOAT_EQ(bounds.center.x, 5.0f);
EXPECT_FLOAT_EQ(bounds.extents.x, 5.0f);
}
TEST(Math_Bounds, GetMin) {
Bounds bounds(Vector3(5, 5, 5), Vector3(10, 10, 10));
Vector3 min = bounds.GetMin();
EXPECT_FLOAT_EQ(min.x, 0.0f);
EXPECT_FLOAT_EQ(min.y, 0.0f);
EXPECT_FLOAT_EQ(min.z, 0.0f);
}
TEST(Math_Bounds, GetMax) {
Bounds bounds(Vector3(5, 5, 5), Vector3(10, 10, 10));
Vector3 max = bounds.GetMax();
EXPECT_FLOAT_EQ(max.x, 10.0f);
EXPECT_FLOAT_EQ(max.y, 10.0f);
EXPECT_FLOAT_EQ(max.z, 10.0f);
}
TEST(Math_Bounds, SetMinMax) {
Bounds bounds;
bounds.SetMinMax(Vector3(0, 0, 0), Vector3(10, 10, 10));
EXPECT_FLOAT_EQ(bounds.center.x, 5.0f);
EXPECT_FLOAT_EQ(bounds.extents.x, 5.0f);
}
TEST(Math_Bounds, Contains_Inside) {
Bounds bounds(Vector3::Zero(), Vector3(10, 10, 10));
EXPECT_TRUE(bounds.Contains(Vector3::Zero()));
}
TEST(Math_Bounds, Contains_Outside) {
Bounds bounds(Vector3::Zero(), Vector3(2, 2, 2));
EXPECT_FALSE(bounds.Contains(Vector3(5, 5, 5)));
}
TEST(Math_Bounds, Contains_OnSurface) {
Bounds bounds(Vector3::Zero(), Vector3(2, 2, 2));
EXPECT_TRUE(bounds.Contains(Vector3(1, 1, 1)));
}
TEST(Math_Bounds, Intersects_Overlap) {
Bounds a(Vector3::Zero(), Vector3(2, 2, 2));
Bounds b(Vector3(1, 1, 1), Vector3(2, 2, 2));
EXPECT_TRUE(a.Intersects(b));
}
TEST(Math_Bounds, Intersects_Separate) {
Bounds a(Vector3::Zero(), Vector3(2, 2, 2));
Bounds b(Vector3(10, 10, 10), Vector3(2, 2, 2));
EXPECT_FALSE(a.Intersects(b));
}
TEST(Math_Bounds, Encapsulate_Point) {
Bounds bounds(Vector3::Zero(), Vector3(2, 2, 2));
bounds.Encapsulate(Vector3(5, 5, 5));
EXPECT_GT(bounds.GetMax().x, 5.0f);
}
TEST(Math_Bounds, Encapsulate_Bounds) {
Bounds a(Vector3::Zero(), Vector3(2, 2, 2));
Bounds b(Vector3(5, 5, 5), Vector3(2, 2, 2));
a.Encapsulate(b);
EXPECT_GT(a.GetMax().x, 5.0f);
}
TEST(Math_Bounds, Expand) {
Bounds bounds(Vector3::Zero(), Vector3(2, 2, 2));
bounds.Expand(2.0f);
EXPECT_GT(bounds.GetMax().x, 3.0f);
}
TEST(Math_Bounds, GetVolume) {
Bounds bounds(Vector3::Zero(), Vector3(2, 4, 6));
float volume = bounds.GetVolume();
EXPECT_FLOAT_EQ(volume, 48.0f);
}
TEST(Math_Bounds, GetClosestPoint) {
Bounds bounds(Vector3::Zero(), Vector3(2, 2, 2));
Vector3 point = bounds.GetClosestPoint(Vector3(10, 10, 10));
EXPECT_FLOAT_EQ(point.x, 1.0f);
EXPECT_FLOAT_EQ(point.y, 1.0f);
EXPECT_FLOAT_EQ(point.z, 1.0f);
}
// ============================================================
// Frustum Tests
// ============================================================
TEST(Math_Frustum, Contains_Point) {
Frustum frustum;
Plane nearPlane;
nearPlane.normal = Vector3::Forward();
nearPlane.distance = 1.0f;
frustum.planes[4] = nearPlane;
EXPECT_TRUE(frustum.Contains(Vector3(0, 0, 2)));
}
TEST(Math_Frustum, Contains_Sphere) {
Frustum frustum;
Plane nearPlane;
nearPlane.normal = Vector3::Forward();
nearPlane.distance = 1.0f;
frustum.planes[4] = nearPlane;
Sphere sphere(Vector3(0, 0, 5), 1.0f);
EXPECT_TRUE(frustum.Contains(sphere));
}
TEST(Math_Frustum, Intersects_Sphere) {
Frustum frustum;
Plane nearPlane;
nearPlane.normal = Vector3::Forward();
nearPlane.distance = 1.0f;
frustum.planes[4] = nearPlane;
Sphere sphere(Vector3(0, 0, 2), 0.5f);
EXPECT_TRUE(frustum.Intersects(sphere));
}
TEST(Math_Frustum, Intersects_Bounds) {
Frustum frustum;
Plane nearPlane;
nearPlane.normal = Vector3::Forward();
nearPlane.distance = 1.0f;
frustum.planes[4] = nearPlane;
Bounds bounds(Vector3(0, 0, 2), Vector3(1, 1, 1));
EXPECT_TRUE(frustum.Intersects(bounds));
}
} // namespace

312
tests/math/test_matrix.cpp Normal file
View File

@@ -0,0 +1,312 @@
#include <gtest/gtest.h>
#include <XCEngine/Math/Matrix4.h>
#include <XCEngine/Math/Vector3.h>
#include <XCEngine/Math/Quaternion.h>
using namespace XCEngine::Math;
namespace {
TEST(Math_Matrix4, DefaultConstructor_InitializesToZero) {
Matrix4x4 m;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
EXPECT_FLOAT_EQ(m.m[i][j], 0.0f);
}
}
}
TEST(Math_Matrix4, Identity_IsCorrect) {
Matrix4x4 id = Matrix4x4::Identity();
EXPECT_FLOAT_EQ(id.m[0][0], 1.0f);
EXPECT_FLOAT_EQ(id.m[1][1], 1.0f);
EXPECT_FLOAT_EQ(id.m[2][2], 1.0f);
EXPECT_FLOAT_EQ(id.m[3][3], 1.0f);
EXPECT_FLOAT_EQ(id.m[0][1], 0.0f);
EXPECT_FLOAT_EQ(id.m[1][2], 0.0f);
}
TEST(Math_Matrix4, Zero_IsCorrect) {
Matrix4x4 zero = Matrix4x4::Zero();
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
EXPECT_FLOAT_EQ(zero.m[i][j], 0.0f);
}
}
}
TEST(Math_Matrix4, Translation) {
Vector3 translation(1.0f, 2.0f, 3.0f);
Matrix4x4 m = Matrix4x4::Translation(translation);
EXPECT_FLOAT_EQ(m.m[0][3], 1.0f);
EXPECT_FLOAT_EQ(m.m[1][3], 2.0f);
EXPECT_FLOAT_EQ(m.m[2][3], 3.0f);
EXPECT_FLOAT_EQ(m.m[3][3], 1.0f);
}
TEST(Math_Matrix4, Translation_TransformsPoint) {
Matrix4x4 m = Matrix4x4::Translation(Vector3(1, 2, 3));
Vector3 point(0, 0, 0);
Vector3 result = m.MultiplyPoint(point);
EXPECT_FLOAT_EQ(result.x, 1.0f);
EXPECT_FLOAT_EQ(result.y, 2.0f);
EXPECT_FLOAT_EQ(result.z, 3.0f);
}
TEST(Math_Matrix4, Scale) {
Vector3 scale(2.0f, 3.0f, 4.0f);
Matrix4x4 m = Matrix4x4::Scale(scale);
EXPECT_FLOAT_EQ(m.m[0][0], 2.0f);
EXPECT_FLOAT_EQ(m.m[1][1], 3.0f);
EXPECT_FLOAT_EQ(m.m[2][2], 4.0f);
EXPECT_FLOAT_EQ(m.m[3][3], 1.0f);
}
TEST(Math_Matrix4, Scale_TransformsPoint) {
Matrix4x4 m = Matrix4x4::Scale(Vector3(2, 3, 4));
Vector3 point(1, 1, 1);
Vector3 result = m.MultiplyPoint(point);
EXPECT_FLOAT_EQ(result.x, 2.0f);
EXPECT_FLOAT_EQ(result.y, 3.0f);
EXPECT_FLOAT_EQ(result.z, 4.0f);
}
TEST(Math_Matrix4, RotationX) {
Matrix4x4 m = Matrix4x4::RotationX(PI * 0.5f);
EXPECT_NEAR(m.m[1][1], 0.0f, 1e-5f);
EXPECT_NEAR(m.m[1][2], -1.0f, 1e-5f);
EXPECT_NEAR(m.m[2][1], 1.0f, 1e-5f);
EXPECT_NEAR(m.m[2][2], 0.0f, 1e-5f);
}
TEST(Math_Matrix4, RotationY) {
Matrix4x4 m = Matrix4x4::RotationY(PI * 0.5f);
EXPECT_NEAR(m.m[0][0], 0.0f, 1e-5f);
EXPECT_NEAR(m.m[0][2], 1.0f, 1e-5f);
EXPECT_NEAR(m.m[2][0], -1.0f, 1e-5f);
EXPECT_NEAR(m.m[2][2], 0.0f, 1e-5f);
}
TEST(Math_Matrix4, RotationZ) {
Matrix4x4 m = Matrix4x4::RotationZ(PI * 0.5f);
EXPECT_NEAR(m.m[0][0], 0.0f, 1e-5f);
EXPECT_NEAR(m.m[0][1], -1.0f, 1e-5f);
EXPECT_NEAR(m.m[1][0], 1.0f, 1e-5f);
EXPECT_NEAR(m.m[1][1], 0.0f, 1e-5f);
}
TEST(Math_Matrix4, TRS) {
Vector3 translation(1, 2, 3);
Quaternion rotation = Quaternion::Identity();
Vector3 scale(2, 2, 2);
Matrix4x4 m = Matrix4x4::TRS(translation, rotation, scale);
EXPECT_FLOAT_EQ(m.m[0][3], 1.0f);
EXPECT_FLOAT_EQ(m.m[1][3], 2.0f);
EXPECT_FLOAT_EQ(m.m[2][3], 3.0f);
}
TEST(Math_Matrix4, LookAt) {
Vector3 eye(0, 0, 5);
Vector3 target(0, 0, 0);
Vector3 up(0, 1, 0);
Matrix4x4 m = Matrix4x4::LookAt(eye, target, up);
EXPECT_FLOAT_EQ(m.m[0][3], 0.0f);
EXPECT_FLOAT_EQ(m.m[1][3], 0.0f);
EXPECT_FLOAT_EQ(m.m[2][3], -5.0f);
}
TEST(Math_Matrix4, Perspective) {
float fov = PI * 0.25f;
float aspect = 16.0f / 9.0f;
float nearPlane = 0.1f;
float farPlane = 100.0f;
Matrix4x4 m = Matrix4x4::Perspective(fov, aspect, nearPlane, farPlane);
EXPECT_FLOAT_EQ(m.m[0][0], 1.0f / (aspect * std::tan(fov * 0.5f)));
EXPECT_FLOAT_EQ(m.m[1][1], 1.0f / std::tan(fov * 0.5f));
EXPECT_FLOAT_EQ(m.m[2][2], -(farPlane + nearPlane) / (farPlane - nearPlane));
EXPECT_FLOAT_EQ(m.m[2][3], -(2.0f * farPlane * nearPlane) / (farPlane - nearPlane));
EXPECT_FLOAT_EQ(m.m[3][2], -1.0f);
}
TEST(Math_Matrix4, Orthographic) {
float left = -10.0f, right = 10.0f;
float bottom = -10.0f, top = 10.0f;
float nearPlane = 0.1f, farPlane = 100.0f;
Matrix4x4 m = Matrix4x4::Orthographic(left, right, bottom, top, nearPlane, farPlane);
EXPECT_FLOAT_EQ(m.m[0][0], 0.1f);
EXPECT_FLOAT_EQ(m.m[1][1], 0.1f);
EXPECT_FLOAT_EQ(m.m[2][2], -2.0f / (farPlane - nearPlane));
}
TEST(Math_Matrix4, Multiply_MatrixWithMatrix) {
Matrix4x4 a = Matrix4x4::Identity();
Matrix4x4 b = Matrix4x4::Translation(Vector3(1, 2, 3));
Matrix4x4 result = a * b;
EXPECT_FLOAT_EQ(result.m[0][3], 1.0f);
EXPECT_FLOAT_EQ(result.m[1][3], 2.0f);
EXPECT_FLOAT_EQ(result.m[2][3], 3.0f);
}
TEST(Math_Matrix4, Multiply_MatrixWithVector4) {
Matrix4x4 m = Matrix4x4::Translation(Vector3(1, 2, 3));
Vector4 v(1, 1, 1, 1);
Vector4 result = m * v;
EXPECT_FLOAT_EQ(result.x, 2.0f);
EXPECT_FLOAT_EQ(result.y, 3.0f);
EXPECT_FLOAT_EQ(result.z, 4.0f);
}
TEST(Math_Matrix4, MultiplyPoint) {
Matrix4x4 m = Matrix4x4::Translation(Vector3(1, 2, 3));
Vector3 p(0, 0, 0);
Vector3 result = m.MultiplyPoint(p);
EXPECT_FLOAT_EQ(result.x, 1.0f);
EXPECT_FLOAT_EQ(result.y, 2.0f);
EXPECT_FLOAT_EQ(result.z, 3.0f);
}
TEST(Math_Matrix4, MultiplyVector) {
Matrix4x4 m = Matrix4x4::Scale(Vector3(2, 2, 2));
Vector3 v(1, 1, 1);
Vector3 result = m.MultiplyVector(v);
EXPECT_FLOAT_EQ(result.x, 2.0f);
EXPECT_FLOAT_EQ(result.y, 2.0f);
EXPECT_FLOAT_EQ(result.z, 2.0f);
}
TEST(Math_Matrix4, Transpose) {
Matrix4x4 m;
m.m[0][1] = 1.0f;
m.m[1][2] = 2.0f;
m.m[2][3] = 3.0f;
Matrix4x4 t = m.Transpose();
EXPECT_FLOAT_EQ(t.m[1][0], 1.0f);
EXPECT_FLOAT_EQ(t.m[2][1], 2.0f);
EXPECT_FLOAT_EQ(t.m[3][2], 3.0f);
}
TEST(Math_Matrix4, Inverse_Identity_ReturnsIdentity) {
Matrix4x4 id = Matrix4x4::Identity();
Matrix4x4 inv = id.Inverse();
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
EXPECT_NEAR(inv.m[i][j], id.m[i][j], 1e-5f);
}
}
}
TEST(Math_Matrix4, Inverse_Translation) {
Matrix4x4 m = Matrix4x4::Translation(Vector3(1, 2, 3));
Matrix4x4 inv = m.Inverse();
Vector3 p(0, 0, 0);
Vector3 transformed = m.MultiplyPoint(p);
Vector3 recovered = inv.MultiplyPoint(transformed);
EXPECT_NEAR(recovered.x, 0.0f, 1e-5f);
EXPECT_NEAR(recovered.y, 0.0f, 1e-5f);
EXPECT_NEAR(recovered.z, 0.0f, 1e-5f);
}
TEST(Math_Matrix4, Inverse_OfInverse_EqualsOriginal) {
Matrix4x4 original = Matrix4x4::TRS(
Vector3(1, 2, 3),
Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f),
Vector3(2, 2, 2)
);
Matrix4x4 inverted = original.Inverse();
Matrix4x4 recovered = inverted.Inverse();
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
EXPECT_NEAR(original.m[i][j], recovered.m[i][j], 1e-4f);
}
}
}
TEST(Math_Matrix4, Determinant_Identity_EqualsOne) {
Matrix4x4 id = Matrix4x4::Identity();
EXPECT_FLOAT_EQ(id.Determinant(), 1.0f);
}
TEST(Math_Matrix4, GetTranslation) {
Vector3 translation(1, 2, 3);
Matrix4x4 m = Matrix4x4::Translation(translation);
Vector3 result = m.GetTranslation();
EXPECT_FLOAT_EQ(result.x, 1.0f);
EXPECT_FLOAT_EQ(result.y, 2.0f);
EXPECT_FLOAT_EQ(result.z, 3.0f);
}
TEST(Math_Matrix4, GetScale) {
Vector3 scale(2, 3, 4);
Matrix4x4 m = Matrix4x4::Scale(scale);
Vector3 result = m.GetScale();
EXPECT_NEAR(result.x, 2.0f, 1e-5f);
EXPECT_NEAR(result.y, 3.0f, 1e-5f);
EXPECT_NEAR(result.z, 4.0f, 1e-5f);
}
TEST(Math_Matrix4, Decompose_TRS) {
Vector3 originalPos(1, 2, 3);
Quaternion originalRot = Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f);
Vector3 originalScale(2, 2, 2);
Matrix4x4 m = Matrix4x4::TRS(originalPos, originalRot, originalScale);
Vector3 pos, scale;
Quaternion rot;
m.Decompose(pos, rot, scale);
EXPECT_NEAR(pos.x, originalPos.x, 1e-4f);
EXPECT_NEAR(pos.y, originalPos.y, 1e-4f);
EXPECT_NEAR(pos.z, originalPos.z, 1e-4f);
EXPECT_NEAR(scale.x, originalScale.x, 1e-4f);
EXPECT_NEAR(scale.y, originalScale.y, 1e-4f);
EXPECT_NEAR(scale.z, originalScale.z, 1e-4f);
}
TEST(Math_Matrix4, IndexOperator) {
Matrix4x4 m;
m[0][0] = 1.0f;
EXPECT_FLOAT_EQ(m[0][0], 1.0f);
EXPECT_FLOAT_EQ(m.m[0][0], 1.0f);
}
} // namespace

View File

@@ -0,0 +1,277 @@
#include <gtest/gtest.h>
#include <XCEngine/Math/Quaternion.h>
#include <XCEngine/Math/Matrix4.h>
#include <XCEngine/Math/Vector3.h>
#include <XCEngine/Math/Math.h>
using namespace XCEngine::Math;
namespace {
TEST(Math_Quaternion, DefaultConstructor) {
Quaternion q;
EXPECT_FLOAT_EQ(q.x, 0.0f);
EXPECT_FLOAT_EQ(q.y, 0.0f);
EXPECT_FLOAT_EQ(q.z, 0.0f);
EXPECT_FLOAT_EQ(q.w, 1.0f);
}
TEST(Math_Quaternion, ParameterConstructor) {
Quaternion q(1.0f, 2.0f, 3.0f, 4.0f);
EXPECT_FLOAT_EQ(q.x, 1.0f);
EXPECT_FLOAT_EQ(q.y, 2.0f);
EXPECT_FLOAT_EQ(q.z, 3.0f);
EXPECT_FLOAT_EQ(q.w, 4.0f);
}
TEST(Math_Quaternion, Identity) {
Quaternion q = Quaternion::Identity();
EXPECT_FLOAT_EQ(q.x, 0.0f);
EXPECT_FLOAT_EQ(q.y, 0.0f);
EXPECT_FLOAT_EQ(q.z, 0.0f);
EXPECT_FLOAT_EQ(q.w, 1.0f);
}
TEST(Math_Quaternion, FromAxisAngle) {
Quaternion q = Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f);
float sinHalfAngle = std::sin(PI * 0.25f);
float cosHalfAngle = std::cos(PI * 0.25f);
EXPECT_NEAR(q.x, 0.0f, 1e-5f);
EXPECT_NEAR(q.y, sinHalfAngle, 1e-5f);
EXPECT_NEAR(q.z, 0.0f, 1e-5f);
EXPECT_NEAR(q.w, cosHalfAngle, 1e-5f);
}
TEST(Math_Quaternion, FromAxisAngle_IdentityRotation) {
Quaternion q = Quaternion::FromAxisAngle(Vector3::Up(), 0.0f);
EXPECT_FLOAT_EQ(q.x, 0.0f);
EXPECT_FLOAT_EQ(q.y, 0.0f);
EXPECT_FLOAT_EQ(q.z, 0.0f);
EXPECT_FLOAT_EQ(q.w, 1.0f);
}
TEST(Math_Quaternion, FromEulerAngles) {
Quaternion q = Quaternion::FromEulerAngles(PI * 0.5f, 0.0f, 0.0f);
float sinHalfPitch = std::sin(PI * 0.25f);
float cosHalfPitch = std::cos(PI * 0.25f);
EXPECT_NEAR(q.x, sinHalfPitch, 1e-5f);
EXPECT_NEAR(q.w, cosHalfPitch, 1e-5f);
}
TEST(Math_Quaternion, ToEulerAngles_FromEulerAngles) {
Vector3 euler(PI * 0.5f, PI * 0.25f, PI * 0.125f);
Quaternion q = Quaternion::FromEulerAngles(euler.x, euler.y, euler.z);
Vector3 result = q.ToEulerAngles();
EXPECT_NEAR(result.x, euler.x, 1e-4f);
EXPECT_NEAR(result.y, euler.y, 1e-4f);
EXPECT_NEAR(result.z, euler.z, 1e-4f);
}
TEST(Math_Quaternion, ToMatrix4x4_Identity) {
Quaternion q = Quaternion::Identity();
Matrix4x4 m = q.ToMatrix4x4();
EXPECT_FLOAT_EQ(m.m[0][0], 1.0f);
EXPECT_FLOAT_EQ(m.m[1][1], 1.0f);
EXPECT_FLOAT_EQ(m.m[2][2], 1.0f);
EXPECT_FLOAT_EQ(m.m[3][3], 1.0f);
}
TEST(Math_Quaternion, ToMatrix4x4_RotationY) {
Quaternion q = Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f);
Matrix4x4 m = q.ToMatrix4x4();
EXPECT_NEAR(m.m[0][0], 0.0f, 1e-5f);
EXPECT_NEAR(m.m[0][2], 1.0f, 1e-5f);
EXPECT_NEAR(m.m[2][0], -1.0f, 1e-5f);
EXPECT_NEAR(m.m[2][2], 0.0f, 1e-5f);
}
TEST(Math_Quaternion, FromRotationMatrix) {
Matrix4x4 m = Matrix4x4::RotationY(PI * 0.5f);
Quaternion q = Quaternion::FromRotationMatrix(m);
EXPECT_NEAR(std::abs(q.y), 0.707f, 1e-3f);
}
TEST(Math_Quaternion, Slerp_IdentityToIdentity) {
Quaternion a = Quaternion::Identity();
Quaternion b = Quaternion::Identity();
Quaternion result = Quaternion::Slerp(a, b, 0.5f);
EXPECT_FLOAT_EQ(result.x, 0.0f);
EXPECT_FLOAT_EQ(result.y, 0.0f);
EXPECT_FLOAT_EQ(result.z, 0.0f);
EXPECT_FLOAT_EQ(result.w, 1.0f);
}
TEST(Math_Quaternion, Slerp_QuarterRotation) {
Quaternion a = Quaternion::Identity();
Quaternion b = Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f);
Quaternion result = Quaternion::Slerp(a, b, 0.5f);
float expectedAngle = PI * 0.25f;
Quaternion expected = Quaternion::FromAxisAngle(Vector3::Up(), expectedAngle);
EXPECT_NEAR(result.x, expected.x, 1e-4f);
EXPECT_NEAR(result.y, expected.y, 1e-4f);
EXPECT_NEAR(result.z, expected.z, 1e-4f);
EXPECT_NEAR(result.w, expected.w, 1e-4f);
}
TEST(Math_Quaternion, Slerp_Start_ReturnsFirst) {
Quaternion a(0.1f, 0.2f, 0.3f, 0.9f);
Quaternion b(0.9f, 0.1f, 0.1f, 0.1f);
Quaternion result = Quaternion::Slerp(a, b, 0.0f);
EXPECT_NEAR(result.x, a.x, 1e-5f);
EXPECT_NEAR(result.y, a.y, 1e-5f);
EXPECT_NEAR(result.z, a.z, 1e-5f);
EXPECT_NEAR(result.w, a.w, 1e-5f);
}
TEST(Math_Quaternion, Slerp_End_ReturnsSecond) {
Quaternion a(0.1f, 0.2f, 0.3f, 0.9f);
Quaternion b(0.9f, 0.1f, 0.1f, 0.1f);
Quaternion result = Quaternion::Slerp(a, b, 1.0f);
EXPECT_NEAR(result.x, b.x, 1e-4f);
EXPECT_NEAR(result.y, b.y, 1e-4f);
EXPECT_NEAR(result.z, b.z, 1e-4f);
EXPECT_NEAR(result.w, b.w, 1e-4f);
}
TEST(Math_Quaternion, Slerp_ClampsT) {
Quaternion a = Quaternion::Identity();
Quaternion b = Quaternion::FromAxisAngle(Vector3::Up(), PI);
Quaternion result1 = Quaternion::Slerp(a, b, -0.5f);
Quaternion result2 = Quaternion::Slerp(a, b, 1.5f);
EXPECT_NEAR(result1.x, a.x, 1e-5f);
EXPECT_NEAR(result2.x, b.x, 1e-4f);
}
TEST(Math_Quaternion, LookRotation_Forward) {
Quaternion q = Quaternion::LookRotation(Vector3::Forward());
EXPECT_NEAR(q.x, 0.0f, 1e-5f);
EXPECT_NEAR(q.y, 0.0f, 1e-5f);
EXPECT_NEAR(q.z, 0.0f, 1e-5f);
EXPECT_NEAR(q.w, 1.0f, 1e-5f);
}
TEST(Math_Quaternion, LookRotation_Up) {
Quaternion q = Quaternion::LookRotation(Vector3::Up());
EXPECT_NEAR(q.x, 0.707f, 1e-3f);
EXPECT_NEAR(q.w, 0.707f, 1e-3f);
}
TEST(Math_Quaternion, LookRotation_Right) {
Quaternion q = Quaternion::LookRotation(Vector3::Right());
EXPECT_NEAR(q.y, -0.707f, 1e-3f);
EXPECT_NEAR(q.w, 0.707f, 1e-3f);
}
TEST(Math_Quaternion, Multiply_ProducesRotation) {
Quaternion q1 = Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f);
Quaternion q2 = Quaternion::FromAxisAngle(Vector3::Right(), PI * 0.5f);
Quaternion result = q1 * q2;
EXPECT_TRUE(result.Magnitude() > 0.0f);
}
TEST(Math_Quaternion, Multiply_Identity) {
Quaternion q(0.1f, 0.2f, 0.3f, 0.9f);
Quaternion identity = Quaternion::Identity();
Quaternion result = q * identity;
EXPECT_NEAR(result.x, q.x, 1e-5f);
EXPECT_NEAR(result.y, q.y, 1e-5f);
EXPECT_NEAR(result.z, q.z, 1e-5f);
EXPECT_NEAR(result.w, q.w, 1e-5f);
}
TEST(Math_Quaternion, Multiply_Vector3) {
Quaternion q = Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f);
Vector3 v = Vector3::Right();
Vector3 result = q * v;
EXPECT_NEAR(result.x, 0.0f, 1e-5f);
EXPECT_NEAR(result.z, 1.0f, 1e-5f);
}
TEST(Math_Quaternion, Inverse_Identity) {
Quaternion q = Quaternion::Identity();
Quaternion inv = q.Inverse();
EXPECT_FLOAT_EQ(inv.x, 0.0f);
EXPECT_FLOAT_EQ(inv.y, 0.0f);
EXPECT_FLOAT_EQ(inv.z, 0.0f);
EXPECT_FLOAT_EQ(inv.w, 1.0f);
}
TEST(Math_Quaternion, Inverse_Rotation) {
Quaternion q = Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f);
Quaternion inv = q.Inverse();
Quaternion product = q * inv;
EXPECT_NEAR(product.x, 0.0f, 1e-5f);
EXPECT_NEAR(product.y, 0.0f, 1e-5f);
EXPECT_NEAR(product.z, 0.0f, 1e-5f);
EXPECT_NEAR(product.w, 1.0f, 1e-5f);
}
TEST(Math_Quaternion, Dot) {
Quaternion a(1, 0, 0, 0);
Quaternion b(1, 0, 0, 0);
float result = a.Dot(b);
EXPECT_FLOAT_EQ(result, 1.0f);
}
TEST(Math_Quaternion, Dot_Orthogonal) {
Quaternion a(1, 0, 0, 0);
Quaternion b(0, 1, 0, 0);
float result = a.Dot(b);
EXPECT_FLOAT_EQ(result, 0.0f);
}
TEST(Math_Quaternion, Magnitude) {
Quaternion q(1, 0, 0, 0);
EXPECT_FLOAT_EQ(q.Magnitude(), 1.0f);
}
TEST(Math_Quaternion, Normalized) {
Quaternion q(1, 2, 3, 4);
Quaternion normalized = q.Normalized();
EXPECT_NEAR(normalized.Magnitude(), 1.0f, 1e-5f);
}
TEST(Math_Quaternion, Normalize) {
Quaternion q(1, 2, 3, 4);
Quaternion result = Quaternion::Normalize(q);
EXPECT_NEAR(result.Magnitude(), 1.0f, 1e-5f);
}
} // namespace

346
tests/math/test_vector.cpp Normal file
View File

@@ -0,0 +1,346 @@
#include <gtest/gtest.h>
#include <XCEngine/Math/Vector2.h>
#include <XCEngine/Math/Vector3.h>
#include <XCEngine/Math/Vector4.h>
#include <XCEngine/Math/Math.h>
using namespace XCEngine::Math;
namespace {
// ============================================================
// Vector2 Tests
// ============================================================
TEST(Math_Vector2, DefaultConstructor_InitializesToZero) {
Vector2 v;
EXPECT_FLOAT_EQ(v.x, 0.0f);
EXPECT_FLOAT_EQ(v.y, 0.0f);
}
TEST(Math_Vector2, ParameterConstructor) {
Vector2 v(3.0f, 4.0f);
EXPECT_FLOAT_EQ(v.x, 3.0f);
EXPECT_FLOAT_EQ(v.y, 4.0f);
}
TEST(Math_Vector2, StaticFactories) {
EXPECT_EQ(Vector2::Zero(), Vector2(0, 0));
EXPECT_EQ(Vector2::One(), Vector2(1, 1));
EXPECT_EQ(Vector2::Up(), Vector2(0, 1));
EXPECT_EQ(Vector2::Down(), Vector2(0, -1));
EXPECT_EQ(Vector2::Right(), Vector2(1, 0));
EXPECT_EQ(Vector2::Left(), Vector2(-1, 0));
}
TEST(Math_Vector2, Dot_Orthogonal_ReturnsZero) {
EXPECT_FLOAT_EQ(Vector2::Dot(Vector2::Right(), Vector2::Up()), 0.0f);
}
TEST(Math_Vector2, Dot_SameDirection_ReturnsMagnitudeProduct) {
EXPECT_FLOAT_EQ(Vector2::Dot(Vector2(1, 0), Vector2(2, 0)), 2.0f);
}
TEST(Math_Vector2, Cross_ReturnsScalar) {
EXPECT_FLOAT_EQ(Vector2::Cross(Vector2::Right(), Vector2::Up()), 1.0f);
}
TEST(Math_Vector2, Magnitude) {
EXPECT_FLOAT_EQ(Vector2(3, 4).Magnitude(), 5.0f);
}
TEST(Math_Vector2, SqrMagnitude) {
EXPECT_FLOAT_EQ(Vector2(3, 4).SqrMagnitude(), 25.0f);
}
TEST(Math_Vector2, Normalize_NonZero_ReturnsUnitVector) {
Vector2 v = Vector2(3, 4).Normalized();
EXPECT_NEAR(v.Magnitude(), 1.0f, 1e-6f);
}
TEST(Math_Vector2, Normalize_ZeroVector_ReturnsZeroVector) {
Vector2 v = Vector2::Zero();
Vector2 normalized = Vector2::Normalize(v);
EXPECT_FLOAT_EQ(normalized.x, 0.0f);
EXPECT_FLOAT_EQ(normalized.y, 0.0f);
}
TEST(Math_Vector2, Lerp) {
Vector2 a(0, 0);
Vector2 b(10, 10);
Vector2 result = Vector2::Lerp(a, b, 0.5f);
EXPECT_FLOAT_EQ(result.x, 5.0f);
EXPECT_FLOAT_EQ(result.y, 5.0f);
}
TEST(Math_Vector2, Lerp_ClampsToZero) {
Vector2 a(0, 0);
Vector2 b(10, 10);
Vector2 result = Vector2::Lerp(a, b, -0.5f);
EXPECT_FLOAT_EQ(result.x, 0.0f);
}
TEST(Math_Vector2, Lerp_ClampsToOne) {
Vector2 a(0, 0);
Vector2 b(10, 10);
Vector2 result = Vector2::Lerp(a, b, 1.5f);
EXPECT_FLOAT_EQ(result.x, 10.0f);
}
TEST(Math_Vector2, MoveTowards) {
Vector2 current(0, 0);
Vector2 target(10, 0);
Vector2 result = Vector2::MoveTowards(current, target, 3.0f);
EXPECT_FLOAT_EQ(result.x, 3.0f);
EXPECT_FLOAT_EQ(result.y, 0.0f);
}
TEST(Math_Vector2, MoveTowards_AlreadyAtTarget) {
Vector2 current(10, 10);
Vector2 target(10, 10);
Vector2 result = Vector2::MoveTowards(current, target, 5.0f);
EXPECT_EQ(result, target);
}
TEST(Math_Vector2, Operators) {
Vector2 a(1, 2);
Vector2 b(3, 4);
Vector2 sum = a + b;
EXPECT_FLOAT_EQ(sum.x, 4.0f);
EXPECT_FLOAT_EQ(sum.y, 6.0f);
Vector2 diff = b - a;
EXPECT_FLOAT_EQ(diff.x, 2.0f);
EXPECT_FLOAT_EQ(diff.y, 2.0f);
Vector2 scaled = a * 2.0f;
EXPECT_FLOAT_EQ(scaled.x, 2.0f);
EXPECT_FLOAT_EQ(scaled.y, 4.0f);
Vector2 divided = a / 2.0f;
EXPECT_FLOAT_EQ(divided.x, 0.5f);
EXPECT_FLOAT_EQ(divided.y, 1.0f);
}
TEST(Math_Vector2, Equality) {
Vector2 a(1.0f, 2.0f);
Vector2 b(1.0f, 2.0f);
Vector2 c(1.0f, 3.0f);
EXPECT_TRUE(a == b);
EXPECT_FALSE(a == c);
}
// ============================================================
// Vector3 Tests
// ============================================================
TEST(Math_Vector3, DefaultConstructor_InitializesToZero) {
Vector3 v;
EXPECT_FLOAT_EQ(v.x, 0.0f);
EXPECT_FLOAT_EQ(v.y, 0.0f);
EXPECT_FLOAT_EQ(v.z, 0.0f);
}
TEST(Math_Vector3, ParameterConstructor) {
Vector3 v(1.0f, 2.0f, 3.0f);
EXPECT_FLOAT_EQ(v.x, 1.0f);
EXPECT_FLOAT_EQ(v.y, 2.0f);
EXPECT_FLOAT_EQ(v.z, 3.0f);
}
TEST(Math_Vector3, StaticFactories) {
EXPECT_EQ(Vector3::Zero(), Vector3(0, 0, 0));
EXPECT_EQ(Vector3::One(), Vector3(1, 1, 1));
EXPECT_EQ(Vector3::Forward(), Vector3(0, 0, 1));
EXPECT_EQ(Vector3::Back(), Vector3(0, 0, -1));
EXPECT_EQ(Vector3::Up(), Vector3(0, 1, 0));
EXPECT_EQ(Vector3::Down(), Vector3(0, -1, 0));
EXPECT_EQ(Vector3::Right(), Vector3(1, 0, 0));
EXPECT_EQ(Vector3::Left(), Vector3(-1, 0, 0));
}
TEST(Math_Vector3, Dot_Orthogonal_ReturnsZero) {
EXPECT_FLOAT_EQ(Vector3::Dot(Vector3::Right(), Vector3::Up()), 0.0f);
}
TEST(Math_Vector3, Cross_Orthogonal_ReturnsPerpendicular) {
Vector3 result = Vector3::Cross(Vector3::Right(), Vector3::Up());
EXPECT_FLOAT_EQ(result.x, 0.0f);
EXPECT_FLOAT_EQ(result.y, 0.0f);
EXPECT_FLOAT_EQ(result.z, 1.0f);
}
TEST(Math_Vector3, Cross_SameVector_ReturnsZero) {
Vector3 result = Vector3::Cross(Vector3::Right(), Vector3::Right());
EXPECT_FLOAT_EQ(result.x, 0.0f);
EXPECT_FLOAT_EQ(result.y, 0.0f);
EXPECT_FLOAT_EQ(result.z, 0.0f);
}
TEST(Math_Vector3, Magnitude) {
EXPECT_FLOAT_EQ(Vector3(1, 2, 2).Magnitude(), 3.0f);
}
TEST(Math_Vector3, SqrMagnitude) {
EXPECT_FLOAT_EQ(Vector3(1, 2, 2).SqrMagnitude(), 9.0f);
}
TEST(Math_Vector3, Normalize) {
Vector3 v = Vector3(3, 4, 0).Normalized();
EXPECT_NEAR(v.Magnitude(), 1.0f, 1e-6f);
}
TEST(Math_Vector3, Lerp) {
Vector3 a(0, 0, 0);
Vector3 b(10, 10, 10);
Vector3 result = Vector3::Lerp(a, b, 0.5f);
EXPECT_FLOAT_EQ(result.x, 5.0f);
EXPECT_FLOAT_EQ(result.y, 5.0f);
EXPECT_FLOAT_EQ(result.z, 5.0f);
}
TEST(Math_Vector3, Project_OntoNormal) {
Vector3 v(1, 1, 0);
Vector3 normal(1, 0, 0);
Vector3 result = Vector3::Project(v, normal);
EXPECT_FLOAT_EQ(result.x, 1.0f);
EXPECT_FLOAT_EQ(result.y, 0.0f);
EXPECT_FLOAT_EQ(result.z, 0.0f);
}
TEST(Math_Vector3, ProjectOnPlane) {
Vector3 v(1, 1, 1);
Vector3 planeNormal(0, 0, 1);
Vector3 result = Vector3::ProjectOnPlane(v, planeNormal);
EXPECT_FLOAT_EQ(result.x, 1.0f);
EXPECT_FLOAT_EQ(result.y, 1.0f);
EXPECT_FLOAT_EQ(result.z, 0.0f);
}
TEST(Math_Vector3, Angle) {
float angle = Vector3::Angle(Vector3::Right(), Vector3::Up());
EXPECT_NEAR(angle, 90.0f, 1e-5f);
}
TEST(Math_Vector3, Reflect) {
Vector3 direction(1, -1, 0);
Vector3 normal(0, 1, 0);
Vector3 result = Vector3::Reflect(direction, normal);
EXPECT_FLOAT_EQ(result.x, 1.0f);
EXPECT_FLOAT_EQ(result.y, 1.0f);
EXPECT_FLOAT_EQ(result.z, 0.0f);
}
TEST(Math_Vector3, MoveTowards) {
Vector3 current(0, 0, 0);
Vector3 target(10, 0, 0);
Vector3 result = Vector3::MoveTowards(current, target, 4.0f);
EXPECT_FLOAT_EQ(result.x, 4.0f);
}
TEST(Math_Vector3, Operators) {
Vector3 a(1, 2, 3);
Vector3 b(4, 5, 6);
Vector3 sum = a + b;
EXPECT_FLOAT_EQ(sum.x, 5.0f);
EXPECT_FLOAT_EQ(sum.y, 7.0f);
EXPECT_FLOAT_EQ(sum.z, 9.0f);
Vector3 diff = b - a;
EXPECT_FLOAT_EQ(diff.x, 3.0f);
EXPECT_FLOAT_EQ(diff.y, 3.0f);
EXPECT_FLOAT_EQ(diff.z, 3.0f);
Vector3 scaled = a * 2.0f;
EXPECT_FLOAT_EQ(scaled.x, 2.0f);
EXPECT_FLOAT_EQ(scaled.y, 4.0f);
EXPECT_FLOAT_EQ(scaled.z, 6.0f);
}
TEST(Math_Vector3, IndexOperator) {
Vector3 v(1, 2, 3);
EXPECT_FLOAT_EQ(v[0], 1.0f);
EXPECT_FLOAT_EQ(v[1], 2.0f);
EXPECT_FLOAT_EQ(v[2], 3.0f);
}
// ============================================================
// Vector4 Tests
// ============================================================
TEST(Math_Vector4, DefaultConstructor_InitializesToZero) {
Vector4 v;
EXPECT_FLOAT_EQ(v.x, 0.0f);
EXPECT_FLOAT_EQ(v.y, 0.0f);
EXPECT_FLOAT_EQ(v.z, 0.0f);
EXPECT_FLOAT_EQ(v.w, 0.0f);
}
TEST(Math_Vector4, ParameterConstructor) {
Vector4 v(1.0f, 2.0f, 3.0f, 4.0f);
EXPECT_FLOAT_EQ(v.x, 1.0f);
EXPECT_FLOAT_EQ(v.y, 2.0f);
EXPECT_FLOAT_EQ(v.z, 3.0f);
EXPECT_FLOAT_EQ(v.w, 4.0f);
}
TEST(Math_Vector4, ConstructFromVector3) {
Vector3 v3(1, 2, 3);
Vector4 v(v3, 4.0f);
EXPECT_FLOAT_EQ(v.x, 1.0f);
EXPECT_FLOAT_EQ(v.y, 2.0f);
EXPECT_FLOAT_EQ(v.z, 3.0f);
EXPECT_FLOAT_EQ(v.w, 4.0f);
}
TEST(Math_Vector4, StaticFactories) {
EXPECT_EQ(Vector4::Zero(), Vector4(0, 0, 0, 0));
EXPECT_EQ(Vector4::One(), Vector4(1, 1, 1, 1));
}
TEST(Math_Vector4, Dot) {
Vector4 a(1, 2, 3, 4);
Vector4 b(2, 3, 4, 5);
float result = Vector4::Dot(a, b);
EXPECT_FLOAT_EQ(result, 40.0f);
}
TEST(Math_Vector4, ToVector3) {
Vector4 v(1, 2, 3, 4);
Vector3 v3 = v.ToVector3();
EXPECT_FLOAT_EQ(v3.x, 1.0f);
EXPECT_FLOAT_EQ(v3.y, 2.0f);
EXPECT_FLOAT_EQ(v3.z, 3.0f);
}
TEST(Math_Vector4, Operators) {
Vector4 a(1, 2, 3, 4);
Vector4 b(5, 6, 7, 8);
Vector4 sum = a + b;
EXPECT_FLOAT_EQ(sum.x, 6.0f);
EXPECT_FLOAT_EQ(sum.y, 8.0f);
EXPECT_FLOAT_EQ(sum.z, 10.0f);
EXPECT_FLOAT_EQ(sum.w, 12.0f);
Vector4 scaled = a * 2.0f;
EXPECT_FLOAT_EQ(scaled.x, 2.0f);
}
} // namespace