Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
[submodule "Engine/cpp/ThirdParty/bx"]
path = Engine/cpp/ThirdParty/bx
url = https://github.com/bkaradzic/bx
[submodule "Engine/cpp/ThirdParty/bimg"]
path = Engine/cpp/ThirdParty/bimg
url = https://github.com/bkaradzic/bimg
[submodule "Engine/cpp/ThirdParty/bgfx"]
path = Engine/cpp/ThirdParty/bgfx
url = https://github.com/bkaradzic/bgfx
[submodule "Engine/cpp/ThirdParty/sdl"]
[submodule "Engine/cpp/thirdparty/sdl"]
path = Engine/cpp/ThirdParty/sdl
url = https://github.com/libsdl-org/SDL
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ project(DraconicEngine LANGUAGES C CXX)

find_package(Git QUIET)
find_package(Threads REQUIRED)
find_package(Vulkan REQUIRED)

if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
message(STATUS "Updating submodules...")
Expand All @@ -23,7 +24,6 @@ list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
include(CTest)
include(Compiler)
include(Modules)
include(Shaders)

# Engine runtime (C++)
add_subdirectory(Engine/cpp)
Expand Down
3 changes: 1 addition & 2 deletions Engine/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
add_subdirectory(ThirdParty SYSTEM)

add_modules_library(Runtime SHARED)

target_link_libraries(Runtime PUBLIC Core Shell Shell_Desktop Scene Rendering)
target_link_libraries(Runtime PUBLIC Core)

# TODO: binding library
40 changes: 36 additions & 4 deletions Engine/cpp/Runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,44 @@ add_modules_library(Core PIC)
add_modules_library(Shell)
add_modules_library(Shell/Null)
add_modules_library(Shell/Desktop)
add_modules_library(Rendering PIC)
add_modules_library(Scene)
add_modules_library(Rendering/Shaders)
add_modules_library(Rendering/RHI PIC)
add_modules_library(Rendering/RHI/Null)
add_modules_library(Rendering/RHI/Validation)
add_modules_library(Rendering/RHI/Vulkan)
if(WIN32)
add_modules_library(Rendering/RHI/DX12)
endif()
add_modules_library(Rendering/Shaders/System)

target_link_libraries(Core PUBLIC Definitions Math IO Memory Logging)
target_link_libraries(Shell PUBLIC Core)
# Rendering: RHI is the backend-agnostic interface module; Null is the headless
# no-op backend (for tests/tools). GPU backends (Vulkan, DX12) come later.
# Shaders: HLSL compilation via DXC. DXC's headers come from Draco::DXCHeaders
# (which also carries DRACO_DXC_PATH); the runtime library is dlopen'd, so only
# the platform's dl library is linked.
target_link_libraries(Rendering_Shaders PUBLIC Core Draco::DXCHeaders PRIVATE ${CMAKE_DL_LIBS})
# ShaderSystem: variant compile-on-demand + cache. Needs the RHI (to create GPU
# modules) on top of the RHI-free shaders module. Its test uses the Null backend.
target_link_libraries(Rendering_Shaders_System PUBLIC Rendering_Shaders Rendering_RHI Rendering_RHI_Null)
target_link_libraries(Rendering_RHI PUBLIC Core)
target_link_libraries(Rendering_RHI_Null PUBLIC Rendering_RHI)
# Validation wraps any backend; its test wraps the Null backend, so Null is
# PUBLIC here to give the test the rhi.null module interface.
target_link_libraries(Rendering_RHI_Validation PUBLIC Rendering_RHI Rendering_RHI_Null)
# Vulkan GPU backend. The Vulkan loader is PRIVATE: VkIncludes.h/vulkan.h live in
# the module's global fragment, so importers of rhi.vk don't need the headers, but
# the static lib's loader symbols still propagate to the final link.
target_link_libraries(Rendering_RHI_Vulkan PUBLIC Rendering_RHI PRIVATE Vulkan::Vulkan)
# DX12 GPU backend (Windows only). Links the D3D12/DXGI system libraries privately;
# IID_PPV_ARGS uses __uuidof, which Clang flags as a language extension.
if(WIN32)
target_link_libraries(Rendering_RHI_DX12 PUBLIC Rendering_RHI PRIVATE d3d12 dxgi dxguid d3dcompiler)
target_compile_options(Rendering_RHI_DX12 PRIVATE $<$<CXX_COMPILER_ID:Clang>:-Wno-language-extension-token>)
endif()
# Shell backends: Null is headless (interface only); Desktop is SDL3-based.
# SDL3 is PUBLIC on Desktop because SDL types (SDL3Window::handle()) surface in
# its exported module interface, so consumers (incl. its tests) need SDL headers.
target_link_libraries(Shell_Null PUBLIC Shell)
target_link_libraries(Shell_Desktop PUBLIC Shell SDL3::SDL3-static)
target_link_libraries(Rendering PUBLIC RHI RenderGraph Mesh Material QuadRenderer Renderer)
target_link_libraries(Scene PUBLIC Renderable TransformComponent Camera)
2 changes: 1 addition & 1 deletion Engine/cpp/Runtime/Core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ add_modules_library(IO)
add_modules_library(Memory)
add_modules_library(Logging)

target_link_libraries(Math PUBLIC Definitions bx)
target_link_libraries(Math PUBLIC Definitions)
target_link_libraries(IO PUBLIC Definitions stb_image)
target_link_libraries(Memory PUBLIC Definitions Math)
target_link_libraries(Logging PUBLIC Definitions)
1 change: 1 addition & 0 deletions Engine/cpp/Runtime/Core/Definitions/Definitions.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module;

export module core.defs;
export import core.stdtypes;
export import core.status;

static_assert(__cplusplus >= 202207L, "Minimum of C++23 required.");

Expand Down
38 changes: 38 additions & 0 deletions Engine/cpp/Runtime/Core/Definitions/Status.cppm
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Status / ErrorCode - success or an error code, no payload. For operations
// that return a value-or-error, use std::expected instead.

export module core.status;

import core.stdtypes;

export namespace draco
{
enum class ErrorCode : u32
{
Ok = 0,
Unknown,
InvalidArgument,
OutOfRange,
OutOfMemory,
NotFound,
NotSupported,
AlreadyExists,
Internal,
};

class Status
{
public:
constexpr Status() = default;
constexpr Status(ErrorCode code) : m_code(code) {}

[[nodiscard]] constexpr ErrorCode code() const { return m_code; }
[[nodiscard]] constexpr bool isOk() const { return m_code == ErrorCode::Ok; }
[[nodiscard]] constexpr explicit operator bool() const { return isOk(); }

friend constexpr bool operator==(Status a, Status b) { return a.m_code == b.m_code; }

private:
ErrorCode m_code = ErrorCode::Ok;
};
}
15 changes: 15 additions & 0 deletions Engine/cpp/Runtime/Core/Math/Functions.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@ export namespace draco::math {
return static_cast<T>(std::pow(x, y));
}

// Trigonometry (radians) and square root.
[[nodiscard]] inline f32 sin(f32 x) noexcept { return std::sin(x); }
[[nodiscard]] inline f32 cos(f32 x) noexcept { return std::cos(x); }
[[nodiscard]] inline f32 tan(f32 x) noexcept { return std::tan(x); }
[[nodiscard]] inline f32 acos(f32 x) noexcept { return std::acos(x); }
[[nodiscard]] inline f32 sqrt(f32 x) noexcept { return std::sqrt(x); }

// Approximate comparisons against an epsilon (defaults to CMP_EPSILON).
[[nodiscard]] inline bool nearlyEqual(f32 a, f32 b, f32 epsilon = CMP_EPSILON) noexcept {
return abs(a - b) <= epsilon;
}
[[nodiscard]] inline bool nearlyZero(f32 v, f32 epsilon = CMP_EPSILON) noexcept {
return abs(v) <= epsilon;
}

template <std::floating_point T>
constexpr T lerp(T from, T to, T weight) noexcept {
return std::lerp(from, to, weight);
Expand Down
2 changes: 2 additions & 0 deletions Engine/cpp/Runtime/Core/Math/Math.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export import core.defs;
export import core.math.constants;
export import core.math.functions;
export import core.math.types;
export import core.math.matrix4;
export import core.math.quaternion;
export import core.math.transform;
190 changes: 181 additions & 9 deletions Engine/cpp/Runtime/Core/Math/Math.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1423,15 +1423,187 @@ TEST_SUITE("vector4") {
using math::Transform;

static constexpr Transform transform;
STATIC_REQUIRE(transform.position[0] == 0.f);
STATIC_REQUIRE(transform.position[1] == 0.f);
STATIC_REQUIRE(transform.position[2] == 0.f);
STATIC_REQUIRE(transform.rotation[0] == 0.f);
STATIC_REQUIRE(transform.rotation[1] == 0.f);
STATIC_REQUIRE(transform.rotation[2] == 0.f);
STATIC_REQUIRE(transform.scale[0] == 1.f);
STATIC_REQUIRE(transform.scale[1] == 1.f);
STATIC_REQUIRE(transform.scale[2] == 1.f);
// Default: position 0, rotation identity, scale 1.
STATIC_REQUIRE(transform.position.x == 0.f);
STATIC_REQUIRE(transform.position.y == 0.f);
STATIC_REQUIRE(transform.position.z == 0.f);
STATIC_REQUIRE(transform.rotation.x == 0.f);
STATIC_REQUIRE(transform.rotation.y == 0.f);
STATIC_REQUIRE(transform.rotation.z == 0.f);
STATIC_REQUIRE(transform.rotation.w == 1.f);
STATIC_REQUIRE(transform.scale.x == 1.f);
STATIC_REQUIRE(transform.scale.y == 1.f);
STATIC_REQUIRE(transform.scale.z == 1.f);
}
}

TEST_SUITE("matrix4") {
TEST_CASE("identity and multiply") {
using namespace draco::math;
const Matrix4 id = Matrix4::identity();
const Matrix4 t = Matrix4::translation(Vector3{ 1.0f, 2.0f, 3.0f });

CHECK(nearlyEqual(id * t, t));
CHECK(nearlyEqual(t * id, t));

static_assert(Matrix4::identity()(0, 0) == 1.0f);
static_assert(Matrix4::identity()(0, 1) == 0.0f);
}

TEST_CASE("translation lives in the last row (row vectors)") {
using namespace draco::math;
const Matrix4 t = Matrix4::translation(Vector3{ 10.0f, 20.0f, 30.0f });
CHECK(t.m[3][0] == 10.0f);
CHECK(t.m[3][1] == 20.0f);
CHECK(t.m[3][2] == 30.0f);

const Vector3 p = transformPoint(Vector3{ 1.0f, 1.0f, 1.0f }, t);
CHECK(nearlyEqual(p, Vector3{ 11.0f, 21.0f, 31.0f }));

// Directions ignore translation.
CHECK(nearlyEqual(transformDirection(Vector3{ 1.0f, 0.0f, 0.0f }, t), Vector3{ 1.0f, 0.0f, 0.0f }));
}

TEST_CASE("rotationZ(90) maps +X to +Y (row vectors)") {
using namespace draco::math;
const Matrix4 rz = Matrix4::rotationZ(degToRad(90.0f));
CHECK(nearlyEqual(transformDirection(Vector3::xAxis(), rz), Vector3::yAxis()));

// rotationY(90) maps +Z to +X.
const Matrix4 ry = Matrix4::rotationY(degToRad(90.0f));
CHECK(nearlyEqual(transformDirection(Vector3::zAxis(), ry), Vector3::xAxis()));
}

TEST_CASE("2D affine helpers (transformPoint2D, operator==)") {
using namespace draco::math;
CHECK(Matrix4::identity() == Matrix4::identity());
CHECK_FALSE(Matrix4::translation(Vector3{ 1, 0, 0 }) == Matrix4::identity());

const Matrix4 t = Matrix4::translation(Vector3{ 5.0f, 7.0f, 0.0f });
CHECK(nearlyEqual(transformPoint2D(Vector2{ 1.0f, 2.0f }, t), Vector2{ 6.0f, 9.0f }));

// Row vectors, left-to-right: v * (S * T).
const Matrix4 st = Matrix4::scale(Vector3{ 2.0f, 3.0f, 1.0f }) * Matrix4::translation(Vector3{ 1.0f, 1.0f, 0.0f });
CHECK(nearlyEqual(transformPoint2D(Vector2{ 1.0f, 1.0f }, st), Vector2{ 3.0f, 4.0f }));

const Matrix4 rz = Matrix4::rotationZ(degToRad(90.0f));
CHECK(nearlyEqual(transformPoint2D(Vector2{ 1.0f, 0.0f }, rz), Vector2{ 0.0f, 1.0f }));
}

TEST_CASE("composition reads left-to-right (scale then translate)") {
using namespace draco::math;
const Matrix4 st = Matrix4::scale(Vector3{ 2.0f, 2.0f, 2.0f }) * Matrix4::translation(Vector3{ 1.0f, 0.0f, 0.0f });
const Vector3 p = transformPoint(Vector3{ 1.0f, 1.0f, 1.0f }, st);
CHECK(nearlyEqual(p, Vector3{ 3.0f, 2.0f, 2.0f }));
}

TEST_CASE("perspective has the expected projective structure") {
using namespace draco::math;
const Matrix4 proj = Matrix4::perspectiveFovRH(degToRad(90.0f), 1.0f, 1.0f, 100.0f);
CHECK(proj.m[2][3] == -1.0f); // w' = -z (RH)
CHECK(nearlyEqual(proj.m[0][0], 1.0f)); // xScale = 1/tan(45) at aspect 1
}

TEST_CASE("determinant") {
using namespace draco::math;
CHECK(nearlyEqual(determinant(Matrix4::identity()), 1.0f));
CHECK(nearlyEqual(determinant(Matrix4::scale(Vector3{ 2.0f, 3.0f, 4.0f })), 24.0f));
}

TEST_CASE("inverse undoes the transform") {
using namespace draco::math;
Transform xform;
xform.scale = Vector3{ 2.0f, 0.5f, 3.0f };
xform.rotation = Quaternion::fromAxisAngle(normalize(Vector3{ 1.0f, 2.0f, 3.0f }), degToRad(50.0f));
xform.position = Vector3{ 5.0f, -2.0f, 1.0f };

const Matrix4 m = xform.toMatrix();
const Matrix4 inv = inverse(m);

CHECK(nearlyEqual(m * inv, Matrix4::identity(), 1.0e-3f));
CHECK(nearlyEqual(inv * m, Matrix4::identity(), 1.0e-3f));

// Point transformed then inverse-transformed returns to itself.
const Vector3 p{ 3.0f, 4.0f, 5.0f };
const Vector3 roundTrip = transformPoint(transformPoint(p, m), inv);
CHECK(nearlyEqual(roundTrip, p, 1.0e-3f));

// Singular matrix -> identity (no divide-by-zero).
CHECK(nearlyEqual(inverse(Matrix4::scale(Vector3::zero)), Matrix4::identity()));
}
}

TEST_SUITE("quaternion") {
TEST_CASE("rotates vectors and agrees with its matrix") {
using namespace draco::math;
const Quaternion q = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(90.0f));

// 90 deg about Z maps +X to +Y.
CHECK(nearlyEqual(rotateVector(q, Vector3::xAxis()), Vector3::yAxis()));

// Quaternion rotation and its matrix agree.
const Matrix4 r = rotationMatrix(q);
CHECK(nearlyEqual(rotateVector(q, Vector3::xAxis()), transformDirection(Vector3::xAxis(), r)));
CHECK(nearlyEqual(rotateVector(q, Vector3{ 0.3f, -0.5f, 0.8f }),
transformDirection(Vector3{ 0.3f, -0.5f, 0.8f }, r)));

// Identity does nothing.
CHECK(nearlyEqual(rotateVector(Quaternion::identity, Vector3{ 1.0f, 2.0f, 3.0f }), Vector3{ 1.0f, 2.0f, 3.0f }));

// Composition: two 45-deg rotations == one 90-deg.
const Quaternion half = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(45.0f));
CHECK(nearlyEqual(rotateVector(half * half, Vector3::xAxis()), Vector3::yAxis()));
}

TEST_CASE("slerp endpoints and midpoint") {
using namespace draco::math;
const Quaternion a = Quaternion::identity;
const Quaternion b = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(90.0f));

CHECK(nearlyEqual(slerp(a, b, 0.0f), a));
CHECK(nearlyEqual(slerp(a, b, 1.0f), b));

// Halfway 0..90 deg about Z is 45 deg: +X -> (cos45, sin45, 0).
const Quaternion mid = slerp(a, b, 0.5f);
const Vector3 rotated = rotateVector(mid, Vector3::xAxis());
const f32 c = std::cos(degToRad(45.0f));
CHECK(nearlyEqual(rotated, Vector3{ c, c, 0.0f }, 1.0e-4f));
}
}

TEST_SUITE("transform") {
TEST_CASE("composes scale, rotation, translation") {
using namespace draco::math;
Transform xform;
xform.scale = Vector3{ 2.0f, 2.0f, 2.0f };
xform.rotation = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(90.0f));
xform.position = Vector3{ 5.0f, 0.0f, 0.0f };

const Matrix4 m = xform.toMatrix();

// (1,0,0) -> scale*2 -> (2,0,0) -> rot90Z -> (0,2,0) -> +translate -> (5,2,0)
const Vector3 p = transformPoint(Vector3::xAxis(), m);
CHECK(nearlyEqual(p, Vector3{ 5.0f, 2.0f, 0.0f }));

// Identity transform is a no-op.
Transform id;
CHECK(nearlyEqual(transformPoint(Vector3{ 7.0f, 8.0f, 9.0f }, id.toMatrix()),
Vector3{ 7.0f, 8.0f, 9.0f }));
}

TEST_CASE("lerp interpolates position, scale, and rotation") {
using namespace draco::math;
Transform a;
Transform b;
b.position = Vector3{ 10.0f, 0.0f, 0.0f };
b.scale = Vector3{ 3.0f, 3.0f, 3.0f };
b.rotation = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(90.0f));

const Transform mid = Transform::lerp(a, b, 0.5f);
CHECK(nearlyEqual(mid.position, Vector3{ 5.0f, 0.0f, 0.0f }));
CHECK(nearlyEqual(mid.scale, Vector3{ 2.0f, 2.0f, 2.0f }));
// Rotation slerped to 45 deg about Z: +X -> (cos45, sin45, 0).
const f32 c = std::cos(degToRad(45.0f));
CHECK(nearlyEqual(rotateVector(mid.rotation, Vector3::xAxis()), Vector3{ c, c, 0.0f }, 1.0e-4f));
}
}
Loading
Loading