From bc0795ed29fb34b413d4032824a0aeb5c9f7db9c Mon Sep 17 00:00:00 2001 From: jracek Date: Mon, 7 Apr 2025 13:21:35 +0200 Subject: [PATCH] FPS cam movement, dynamic subdivision, Navmeshes --- .vscode/tasks.json | 4 +- Makefile | 1 + output.bin | Bin 411048 -> 439688 bytes src/camera.cpp | 12 +- src/camera.hh | 14 +- src/gtemath.cpp | 2 +- src/gtemath.hh | 2 +- src/main.cpp | 113 +++++++++------- src/navmesh.cpp | 119 +++++++++++++++++ src/navmesh.hh | 24 ++++ src/renderer.cpp | 327 ++++++++++++++++++++++++++++++++------------- src/renderer.hh | 23 ++-- src/splashpack.cpp | 47 +++++-- src/splashpack.hh | 29 +--- 14 files changed, 518 insertions(+), 199 deletions(-) create mode 100644 src/navmesh.cpp create mode 100644 src/navmesh.hh diff --git a/.vscode/tasks.json b/.vscode/tasks.json index b46ac12..8b0e05d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,7 +4,7 @@ { "label": "Build Debug", "type": "shell", - "command": "make BUILD=Debug", + "command": "make -j12 BUILD=Debug", "group": { "kind": "build", "isDefault": true @@ -16,7 +16,7 @@ { "label": "Build Release", "type": "shell", - "command": "make", + "command": "make -j12", "group": { "kind": "build", "isDefault": true diff --git a/Makefile b/Makefile index fdfcb64..c6fc09f 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ src/renderer.cpp \ src/splashpack.cpp \ src/camera.cpp \ src/gtemath.cpp \ +src/navmesh.cpp \ output.o include third_party/nugget/psyqo/psyqo.mk diff --git a/output.bin b/output.bin index 30e0b6b061bc3db2175e4c54caaed16bfa045cab..30528a2dbaad4070850aa0cdf4ed6ae93659d62d 100644 GIT binary patch delta 73707 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 mUQjNRUypOJzxc7QKk~7!57^J3{YB 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 K$>ME4@P7g6XDr|V diff --git a/src/camera.cpp b/src/camera.cpp index 5649944..c0c629c 100644 --- a/src/camera.cpp +++ b/src/camera.cpp @@ -10,19 +10,19 @@ psxsplash::Camera::Camera() { m_rotationMatrix = psyqo::SoftMath::generateRotationMatrix33(0, psyqo::SoftMath::Axis::X, m_trig); } -void psxsplash::Camera::moveX(psyqo::FixedPoint<12> x) { m_position.x += -x; } +void psxsplash::Camera::MoveX(psyqo::FixedPoint<12> x) { m_position.x += x; } -void psxsplash::Camera::moveY(psyqo::FixedPoint<12> y) { m_position.y += -y; } +void psxsplash::Camera::MoveY(psyqo::FixedPoint<12> y) { m_position.y += y; } -void psxsplash::Camera::moveZ(psyqo::FixedPoint<12> z) { m_position.z += -z; } +void psxsplash::Camera::MoveZ(psyqo::FixedPoint<12> z) { m_position.z += z; } -void psxsplash::Camera::setPosition(psyqo::FixedPoint<12> x, psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z) { +void psxsplash::Camera::SetPosition(psyqo::FixedPoint<12> x, psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z) { m_position.x = x; m_position.y = y; m_position.z = z; } -void psxsplash::Camera::setRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z) { +void psxsplash::Camera::SetRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z) { auto rotX = psyqo::SoftMath::generateRotationMatrix33(x, psyqo::SoftMath::Axis::X, m_trig); auto rotY = psyqo::SoftMath::generateRotationMatrix33(y, psyqo::SoftMath::Axis::Y, m_trig); auto rotZ = psyqo::SoftMath::generateRotationMatrix33(z, psyqo::SoftMath::Axis::Z, m_trig); @@ -34,4 +34,4 @@ void psxsplash::Camera::setRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle m_rotationMatrix = rotY; } -psyqo::Matrix33& psxsplash::Camera::getRotation() { return m_rotationMatrix; } \ No newline at end of file +psyqo::Matrix33& psxsplash::Camera::GetRotation() { return m_rotationMatrix; } \ No newline at end of file diff --git a/src/camera.hh b/src/camera.hh index 0d404df..51ef552 100644 --- a/src/camera.hh +++ b/src/camera.hh @@ -10,15 +10,15 @@ class Camera { public: Camera(); - void moveX(psyqo::FixedPoint<12> x); - void moveY(psyqo::FixedPoint<12> y); - void moveZ(psyqo::FixedPoint<12> y); + void MoveX(psyqo::FixedPoint<12> x); + void MoveY(psyqo::FixedPoint<12> y); + void MoveZ(psyqo::FixedPoint<12> y); - void setPosition(psyqo::FixedPoint<12> x, psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z); - psyqo::Vec3& getPosition() { return m_position; } + void SetPosition(psyqo::FixedPoint<12> x, psyqo::FixedPoint<12> y, psyqo::FixedPoint<12> z); + psyqo::Vec3& GetPosition() { return m_position; } - void setRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z); - psyqo::Matrix33& getRotation(); + void SetRotation(psyqo::Angle x, psyqo::Angle y, psyqo::Angle z); + psyqo::Matrix33& GetRotation(); private: psyqo::Matrix33 m_rotationMatrix; diff --git a/src/gtemath.cpp b/src/gtemath.cpp index a782f03..6c16805 100644 --- a/src/gtemath.cpp +++ b/src/gtemath.cpp @@ -5,7 +5,7 @@ using namespace psyqo::GTE; -void psxsplash::matrixMultiplyGTE(const psyqo::Matrix33 &matA, const psyqo::Matrix33 &matB, psyqo::Matrix33 *result) { +void psxsplash::MatrixMultiplyGTE(const psyqo::Matrix33 &matA, const psyqo::Matrix33 &matB, psyqo::Matrix33 *result) { writeSafe(matA); psyqo::Vec3 t; diff --git a/src/gtemath.hh b/src/gtemath.hh index d5bec9d..059f34e 100644 --- a/src/gtemath.hh +++ b/src/gtemath.hh @@ -1,5 +1,5 @@ #include namespace psxsplash { -void matrixMultiplyGTE(const psyqo::Matrix33 &matA, const psyqo::Matrix33 &matB, psyqo::Matrix33 *result); +void MatrixMultiplyGTE(const psyqo::Matrix33 &matA, const psyqo::Matrix33 &matB, psyqo::Matrix33 *result); }; diff --git a/src/main.cpp b/src/main.cpp index c7d43ca..b94b1d6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,8 +10,10 @@ #include #include +#include "EASTL/algorithm.h" #include "camera.hh" -#include "gameobject.hh" +#include "navmesh.hh" +#include "psyqo/vector.hh" #include "renderer.hh" #include "splashpack.hh" @@ -30,6 +32,9 @@ class PSXSplash final : public psyqo::Application { public: psyqo::Font<> m_font; psyqo::AdvancedPad m_input; + psxsplash::SplashPackLoader m_loader; + static constexpr uint8_t m_stickDeadzone = 0x30; + }; class MainScene final : public psyqo::Scene { @@ -39,12 +44,14 @@ class MainScene final : public psyqo::Scene { psxsplash::Camera m_mainCamera; psyqo::Angle camRotX, camRotY, camRotZ; - eastl::vector m_objects; psyqo::Trig<> m_trig; uint32_t m_lastFrameCounter; - static constexpr psyqo::FixedPoint<12> moveSpeed = 0.01_fp; + static constexpr psyqo::FixedPoint<12> moveSpeed = 0.002_fp; static constexpr psyqo::Angle rotSpeed = 0.01_pi; + bool m_sprinting = 0; + static constexpr psyqo::FixedPoint<12> sprintSpeed = 0.003_fp; + }; PSXSplash psxSplash; @@ -61,7 +68,7 @@ void PSXSplash::prepare() { gpu().initialize(config); // Initialize the Renderer singleton - psxsplash::Renderer::init(gpu()); + psxsplash::Renderer::Init(gpu()); } void PSXSplash::createScene() { @@ -71,10 +78,12 @@ void PSXSplash::createScene() { } void MainScene::start(StartReason reason) { - m_objects = psxsplash::LoadSplashpack(_binary_output_bin_start); - psxsplash::Renderer::getInstance().setCamera(m_mainCamera); + psxSplash.m_loader.LoadSplashpack(_binary_output_bin_start); + psxsplash::Renderer::GetInstance().SetCamera(m_mainCamera); } +psyqo::FixedPoint<12> pheight = 0.0_fp; + void MainScene::frame() { uint32_t beginFrame = gpu().now(); auto currentFrameCounter = gpu().getFrameCount(); @@ -86,66 +95,68 @@ void MainScene::frame() { } mainScene.m_lastFrameCounter = currentFrameCounter; + + uint8_t rightX = psxSplash.m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 0); + uint8_t rightY = psxSplash.m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 1); - auto& input = psxSplash.m_input; + uint8_t leftX = psxSplash.m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 2); + uint8_t leftY = psxSplash.m_input.getAdc(psyqo::AdvancedPad::Pad::Pad1a, 3); - if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Right)) { - m_mainCamera.moveX((m_trig.cos(camRotY) * moveSpeed * deltaTime)); - m_mainCamera.moveZ(-(m_trig.sin(camRotY) * moveSpeed * deltaTime)); + int16_t rightXOffset = (int16_t)rightX - 0x80; + int16_t rightYOffset = (int16_t)rightY - 0x80; + int16_t leftXOffset = (int16_t)leftX - 0x80; + int16_t leftYOffset = (int16_t)leftY - 0x80; + + if(__builtin_abs(leftXOffset) < psxSplash.m_stickDeadzone && + __builtin_abs(leftYOffset) < psxSplash.m_stickDeadzone) { + m_sprinting = false; } - if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Left)) { - m_mainCamera.moveX(-(m_trig.cos(camRotY) * moveSpeed * deltaTime)); - m_mainCamera.moveZ((m_trig.sin(camRotY) * moveSpeed * deltaTime)); + if(psxSplash.m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L3)) { + m_sprinting = true; } - if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Up)) { - m_mainCamera.moveX((m_trig.sin(camRotY) * m_trig.cos(camRotX)) * moveSpeed * deltaTime); - m_mainCamera.moveY(-(m_trig.sin(camRotX) * moveSpeed)); - m_mainCamera.moveZ((m_trig.cos(camRotY) * m_trig.cos(camRotX)) * moveSpeed * deltaTime); + psyqo::FixedPoint<12> speed = m_sprinting ? sprintSpeed : moveSpeed; + + if (__builtin_abs(rightXOffset) > psxSplash.m_stickDeadzone) { + camRotY += (rightXOffset * rotSpeed * deltaTime) >> 7; + } + if (__builtin_abs(rightYOffset) > psxSplash.m_stickDeadzone) { + camRotX -= (rightYOffset * rotSpeed * deltaTime) >> 7; + camRotX = eastl::clamp(camRotX, -0.5_pi, 0.5_pi); + } + m_mainCamera.SetRotation(camRotX, camRotY, camRotZ); + + if (__builtin_abs(leftYOffset) > psxSplash.m_stickDeadzone) { + psyqo::FixedPoint<12> forward = -(leftYOffset * speed * deltaTime) >> 7; + m_mainCamera.MoveX((m_trig.sin(camRotY) * forward)); + m_mainCamera.MoveZ((m_trig.cos(camRotY) * forward)); + } + if (__builtin_abs(leftXOffset) > psxSplash.m_stickDeadzone) { + psyqo::FixedPoint<12> strafe = -(leftXOffset * speed * deltaTime) >> 7; + m_mainCamera.MoveX(-(m_trig.cos(camRotY) * strafe)); + m_mainCamera.MoveZ((m_trig.sin(camRotY) * strafe)); } - if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Down)) { - m_mainCamera.moveX(-((m_trig.sin(camRotY) * m_trig.cos(camRotX)) * moveSpeed * deltaTime)); - m_mainCamera.moveY((m_trig.sin(camRotX) * moveSpeed * deltaTime)); - m_mainCamera.moveZ(-((m_trig.cos(camRotY) * m_trig.cos(camRotX)) * moveSpeed * deltaTime)); + if(psxSplash.m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::L1)) { + pheight += 0.01_fp; + } + if(psxSplash.m_input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Button::R1)) { + pheight -= 0.01_fp; } - if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::R1)) { - m_mainCamera.moveY(-moveSpeed * deltaTime); - } - - if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::L1)) { - m_mainCamera.moveY(moveSpeed * deltaTime); - } - - if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Cross)) { - camRotX -= rotSpeed * deltaTime; - m_mainCamera.setRotation(camRotX, camRotY, camRotZ); - } - - if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Triangle)) { - camRotX += rotSpeed * deltaTime; - m_mainCamera.setRotation(camRotX, camRotY, camRotZ); - } - - if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Circle)) { - camRotY += rotSpeed * deltaTime; - m_mainCamera.setRotation(camRotX, camRotY, camRotZ); - } - - if (input.isButtonPressed(psyqo::AdvancedPad::Pad::Pad1a, psyqo::AdvancedPad::Square)) { - camRotY -= rotSpeed * deltaTime; - m_mainCamera.setRotation(camRotX, camRotY, camRotZ); - } - - psxsplash::Renderer::getInstance().render(m_objects); + psyqo::Vec3 adjustedPosition = + psxsplash::ComputeNavmeshPosition(m_mainCamera.GetPosition(), *psxSplash.m_loader.navmeshes[0], -0.05_fp); + m_mainCamera.SetPosition(adjustedPosition.x, adjustedPosition.y, adjustedPosition.z); + psxsplash::Renderer::GetInstance().Render(psxSplash.m_loader.gameObjects); + // psxsplash::Renderer::getInstance().renderNavmeshPreview(*psxSplash.m_loader.navmeshes[0], true); psxSplash.m_font.chainprintf(gpu(), {{.x = 2, .y = 2}}, {{.r = 0xff, .g = 0xff, .b = 0xff}}, "FPS: %i", gpu().getRefreshRate() / deltaTime); + gpu().pumpCallbacks(); uint32_t endFrame = gpu().now(); uint32_t spent = endFrame - beginFrame; } -int main() { return psxSplash.run(); } +int main() { return psxSplash.run(); } \ No newline at end of file diff --git a/src/navmesh.cpp b/src/navmesh.cpp new file mode 100644 index 0000000..91e5aaf --- /dev/null +++ b/src/navmesh.cpp @@ -0,0 +1,119 @@ +#include "navmesh.hh" + +#include + +#include "psyqo/fixed-point.hh" +#include "psyqo/vector.hh" + +using namespace psyqo::fixed_point_literals; + +namespace psxsplash { + +psyqo::FixedPoint<12> DotProduct2D(const psyqo::Vec2& a, const psyqo::Vec2& b) { return a.x * b.x + a.y * b.y; } + +psyqo::Vec2 ClosestPointOnSegment(const psyqo::Vec2& A, const psyqo::Vec2& B, const psyqo::Vec2& P) { + psyqo::Vec2 AB = {B.x - A.x, B.y - A.y}; + psyqo::Vec2 AP = {P.x - A.x, P.y - A.y}; + auto abDot = DotProduct2D(AB, AB); + if (abDot == 0) return A; + psyqo::FixedPoint<12> t = DotProduct2D(AP, AB) / abDot; + if (t < 0.0_fp) t = 0.0_fp; + if (t > 1.0_fp) t = 1.0_fp; + return {(A.x + AB.x * t), (A.y + AB.y * t)}; +} + +bool PointInTriangle(psyqo::Vec3& p, NavMeshTri& tri) { + psyqo::Vec2 A = {tri.v0.x * 100, tri.v0.z * 100}; + psyqo::Vec2 B = {tri.v1.x * 100, tri.v1.z * 100}; + psyqo::Vec2 C = {tri.v2.x * 100, tri.v2.z * 100}; + psyqo::Vec2 P = {p.x * 100, p.z * 100}; + + psyqo::Vec2 v0 = {B.x - A.x, B.y - A.y}; + psyqo::Vec2 v1 = {C.x - A.x, C.y - A.y}; + psyqo::Vec2 v2 = {P.x - A.x, P.y - A.y}; + + auto d00 = DotProduct2D(v0, v0); + auto d01 = DotProduct2D(v0, v1); + auto d11 = DotProduct2D(v1, v1); + auto d20 = DotProduct2D(v2, v0); + auto d21 = DotProduct2D(v2, v1); + + psyqo::FixedPoint<12> denom = d00 * d11 - d01 * d01; + if (denom == 0.0_fp) { + return false; + } + auto invDenom = 1.0_fp / denom; + auto u = (d11 * d20 - d01 * d21) * invDenom; + auto w = (d00 * d21 - d01 * d20) * invDenom; + + return (u >= 0.0_fp) && (w >= 0.0_fp) && (u + w <= 1.0_fp); +} + +psyqo::Vec3 ComputeNormal(const NavMeshTri& tri) { + psyqo::Vec3 v1 = {tri.v1.x * 100 - tri.v0.x * 100, tri.v1.y * 100 - tri.v0.y * 100, tri.v1.z * 100 - tri.v0.z * 100}; + psyqo::Vec3 v2 = {tri.v2.x * 100 - tri.v0.x * 100, tri.v2.y * 100 - tri.v0.y * 100, tri.v2.z * 100 - tri.v0.z * 100}; + + psyqo::Vec3 normal = { + v1.y * v2.z - v1.z * v2.y, + v1.z * v2.x - v1.x * v2.z, + v1.x * v2.y - v1.y * v2.x + }; + return normal; +} + +psyqo::FixedPoint<12> CalculateY(const psyqo::Vec3& p, const NavMeshTri& tri) { + psyqo::Vec3 normal = ComputeNormal(tri); + + psyqo::FixedPoint<12> A = normal.x; + psyqo::FixedPoint<12> B = normal.y; + psyqo::FixedPoint<12> C = normal.z; + + psyqo::FixedPoint<12> D = -(A * tri.v0.x + B * tri.v0.y + C * tri.v0.z); + + if (B != 0.0_fp) { + return -(A * p.x + C * p.z + D) / B; + } else { + return p.y; + } +} + +psyqo::Vec3 ComputeNavmeshPosition(psyqo::Vec3& position, Navmesh& navmesh, psyqo::FixedPoint<12> pheight) { + for (int i = 0; i < navmesh.triangleCount; i++) { + if (PointInTriangle(position, navmesh.polygons[i])) { + position.y = CalculateY(position, navmesh.polygons[i]) + pheight; + return position; + } + } + + psyqo::Vec2 P = {position.x * 100, position.z * 100}; + + psyqo::Vec2 closestPoint; + psyqo::FixedPoint<12> minDist = 0x7ffff; + + for (int i = 0; i < navmesh.triangleCount; i++) { + NavMeshTri& tri = navmesh.polygons[i]; + psyqo::Vec2 A = {tri.v0.x * 100, tri.v0.z * 100}; + psyqo::Vec2 B = {tri.v1.x * 100, tri.v1.z * 100}; + psyqo::Vec2 C = {tri.v2.x * 100, tri.v2.z * 100}; + + std::array, 3> edges = {{{A, B}, {B, C}, {C, A}}}; + + for (auto& edge : edges) { + psyqo::Vec2 proj = ClosestPointOnSegment(edge.first, edge.second, P); + psyqo::Vec2 diff = {proj.x - P.x, proj.y - P.y}; + auto distSq = DotProduct2D(diff, diff); + if (distSq < minDist) { + minDist = distSq; + closestPoint = proj; + position.y = CalculateY(position, navmesh.polygons[i]) + pheight; + } + } + } + + position.x = closestPoint.x / 100; + position.z = closestPoint.y / 100; + + return position; +} + +} // namespace psxsplash diff --git a/src/navmesh.hh b/src/navmesh.hh new file mode 100644 index 0000000..121c880 --- /dev/null +++ b/src/navmesh.hh @@ -0,0 +1,24 @@ +#pragma once + +#include "psyqo/gte-registers.hh" + +namespace psxsplash { + +class NavMeshTri final { + public: + psyqo::Vec3 v0, v1, v2; +}; + +class Navmesh final { + public: + union { + NavMeshTri* polygons; + uint32_t polygonsOffset; + }; + uint16_t triangleCount; + uint16_t reserved; +}; + +psyqo::Vec3 ComputeNavmeshPosition(psyqo::Vec3& position, Navmesh& navmesh, psyqo::FixedPoint<12> pheight); + +} // namespace psxsplash \ No newline at end of file diff --git a/src/renderer.cpp b/src/renderer.cpp index 6baa0b5..94ecfee 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -14,7 +14,9 @@ #include #include +#include "EASTL/array.h" #include "gtemath.hh" +#include "splashpack.hh" using namespace psyqo::fixed_point_literals; using namespace psyqo::trig_literals; @@ -22,7 +24,7 @@ using namespace psyqo::GTE; psxsplash::Renderer *psxsplash::Renderer::instance = nullptr; -void psxsplash::Renderer::init(psyqo::GPU &gpuInstance) { +void psxsplash::Renderer::Init(psyqo::GPU &gpuInstance) { psyqo::Kernel::assert(instance == nullptr, "A second intialization of Renderer was tried"); clear(); @@ -42,9 +44,10 @@ void psxsplash::Renderer::init(psyqo::GPU &gpuInstance) { } } -void psxsplash::Renderer::setCamera(psxsplash::Camera &camera) { m_currentCamera = &camera; } +void psxsplash::Renderer::SetCamera(psxsplash::Camera &camera) { m_currentCamera = &camera; } -void psxsplash::Renderer::render(eastl::vector &objects) { + +void psxsplash::Renderer::Render(eastl::vector &objects) { psyqo::Kernel::assert(m_currentCamera != nullptr, "PSXSPLASH: Tried to render without an active camera"); uint8_t parity = m_gpu.getParity(); @@ -64,8 +67,8 @@ void psxsplash::Renderer::render(eastl::vector &objects) { ::clear(); // Rotate the camera Translation vector by the camera rotation - writeSafe(m_currentCamera->getRotation()); - writeSafe(m_currentCamera->getPosition()); + writeSafe(m_currentCamera->GetRotation()); + writeSafe(-m_currentCamera->GetPosition()); Kernels::mvmva(); cameraPosition = readSafe(); @@ -80,7 +83,7 @@ void psxsplash::Renderer::render(eastl::vector &objects) { objectPosition.z += cameraPosition.z; // Combine object and camera rotations - matrixMultiplyGTE(m_currentCamera->getRotation(), obj->rotation, &finalMatrix); + MatrixMultiplyGTE(m_currentCamera->GetRotation(), obj->rotation, &finalMatrix); psyqo::GTE::writeSafe(objectPosition); psyqo::GTE::writeSafe(finalMatrix); @@ -101,10 +104,19 @@ void psxsplash::Renderer::render(eastl::vector &objects) { if (mac0 <= 0) continue; int32_t zIndex = 0; - uint32_t sz0, sz1, sz2; - read(&sz0); - read(&sz1); - read(&sz2); + uint32_t u0, u1, u2; + + read(&u0); + read(&u1); + read(&u2); + + int32_t sz0 = (int32_t)u0; + int32_t sz1 = (int32_t)u1; + int32_t sz2 = (int32_t)u2; + + if ((sz0 < 1 && sz1 < 1 && sz2 < 1)) { + continue; + }; zIndex = eastl::max(eastl::max(sz0, sz1), sz2); if (zIndex < 0 || zIndex >= ORDERING_TABLE_SIZE) continue; @@ -113,7 +125,7 @@ void psxsplash::Renderer::render(eastl::vector &objects) { read(&projected[1].packed); read(&projected[2].packed); - iterativeSubdivideAndRender(tri, projected, zIndex, 3); + recursiveSubdivideAndRender(tri, projected, zIndex, 1); } } m_gpu.getNextClear(clear.primitive, m_clearcolor); @@ -121,101 +133,236 @@ void psxsplash::Renderer::render(eastl::vector &objects) { m_gpu.chain(ot); } -static inline psyqo::Color averageColor(const psyqo::Color &c1, const psyqo::Color &c2) { - uint8_t r = (c1.r + c2.r) >> 1; - uint8_t g = (c1.g + c2.g) >> 1; - uint8_t b = (c1.b + c2.b) >> 1; - psyqo::Color c; +void psxsplash::Renderer::RenderNavmeshPreview(psxsplash::Navmesh navmesh, bool isOnMesh) { + uint8_t parity = m_gpu.getParity(); + eastl::array projected; - c.r = r; - c.g = g; - c.b = b; + auto &ot = m_ots[parity]; + auto &clear = m_clear[parity]; + auto &balloc = m_ballocs[parity]; + balloc.reset(); - return c; -} + psyqo::Vec3 cameraPosition; + ::clear(); + ::clear(); + ::clear(); -// Temporary subdivision code. I'm told this is terrible. -void psxsplash::Renderer::iterativeSubdivideAndRender(const Tri &initialTri, - const eastl::array &initialProj, int zIndex, - int maxIterations) { - struct Subdiv { - Tri tri; - eastl::array proj; - int iterations; - }; + // Rotate the camera Translation vector by the camera rotation + writeSafe(m_currentCamera->GetRotation()); + writeSafe(m_currentCamera->GetPosition()); - // Reserve space knowing the max subdivisions (for maxIterations=3, max elements are small) - eastl::vector stack; - stack.reserve(16); - stack.push_back({initialTri, initialProj, maxIterations}); + Kernels::mvmva(); + cameraPosition = readSafe(); - while (!stack.empty()) { - Subdiv s = stack.back(); - stack.pop_back(); + write(-cameraPosition.x.raw()); + write(-cameraPosition.y.raw()); + write(-cameraPosition.z.raw()); - uint16_t minX = eastl::min({s.proj[0].x, s.proj[1].x, s.proj[2].x}); - uint16_t maxX = eastl::max({s.proj[0].x, s.proj[1].x, s.proj[2].x}); - uint16_t minY = eastl::min({s.proj[0].y, s.proj[1].y, s.proj[2].y}); - uint16_t maxY = eastl::max({s.proj[0].y, s.proj[1].y, s.proj[2].y}); - uint16_t width = maxX - minX; - uint16_t height = maxY - minY; + psyqo::GTE::writeSafe(m_currentCamera->GetRotation()); - // Base case: small enough or no iterations left. - if (s.iterations == 0 || (width < 2048 && height < 1024)) { - auto &balloc = m_ballocs[m_gpu.getParity()]; - auto &prim = balloc.allocateFragment(); + for (int i = 0; i < navmesh.triangleCount; i++) { + NavMeshTri &tri = navmesh.polygons[i]; + psyqo::Vec3 result; - prim.primitive.pointA = s.proj[0]; - prim.primitive.pointB = s.proj[1]; - prim.primitive.pointC = s.proj[2]; - prim.primitive.uvA = s.tri.uvA; - prim.primitive.uvB = s.tri.uvB; - prim.primitive.uvC = s.tri.uvC; - prim.primitive.tpage = s.tri.tpage; - psyqo::PrimPieces::ClutIndex clut(s.tri.clutX, s.tri.clutY); - prim.primitive.clutIndex = clut; - prim.primitive.setColorA(s.tri.colorA); - prim.primitive.setColorB(s.tri.colorB); - prim.primitive.setColorC(s.tri.colorC); - prim.primitive.setOpaque(); + writeSafe(tri.v0); + writeSafe(tri.v1); + writeSafe(tri.v2); - m_ots[m_gpu.getParity()].insert(prim, zIndex); - continue; + Kernels::rtpt(); + Kernels::nclip(); + + int32_t mac0 = 0; + read(reinterpret_cast(&mac0)); + if (mac0 <= 0) continue; + + int32_t zIndex = 0; + uint32_t u0, u1, u2; + read(&u0); + read(&u1); + read(&u2); + + int32_t sz0 = *reinterpret_cast(&u0); + int32_t sz1 = *reinterpret_cast(&u1); + int32_t sz2 = *reinterpret_cast(&u2); + + zIndex = eastl::max(eastl::max(sz0, sz1), sz2); + if (zIndex < 0 || zIndex >= ORDERING_TABLE_SIZE) continue; + + auto &prim = balloc.allocateFragment(); + + prim.primitive.pointA = projected[0]; + prim.primitive.pointB = projected[1]; + prim.primitive.pointC = projected[2]; + + psyqo::Color heightColor; + + if (isOnMesh) { + heightColor.r = 0; + heightColor.g = ((tri.v0.y.raw() + tri.v1.y.raw() + tri.v2.y.raw()) / 3) * 100 % 256; + heightColor.b = 0; + } else { + heightColor.r = ((tri.v0.y.raw() + tri.v1.y.raw() + tri.v2.y.raw()) / 3) * 100 % 256; + heightColor.g = 0; + heightColor.b = 0; } - // Compute midpoint between projected[0] and projected[1]. - psyqo::Vertex mid; - mid.x = (s.proj[0].x + s.proj[1].x) >> 1; - mid.y = (s.proj[0].y + s.proj[1].y) >> 1; - - // Interpolate UV and color. - psyqo::PrimPieces::UVCoords newUV; - newUV.u = (s.tri.uvA.u + s.tri.uvB.u) / 2; - newUV.v = (s.tri.uvA.v + s.tri.uvB.v) / 2; - psyqo::Color newColor = averageColor(s.tri.colorA, s.tri.colorB); - - // Prepare new projected vertices. - eastl::array projA = {s.proj[0], mid, s.proj[2]}; - eastl::array projB = {mid, s.proj[1], s.proj[2]}; - - // Construct new Tris - Tri triA = s.tri; - triA.uvB = newUV; - triA.colorB = newColor; - - Tri triB = s.tri; - triB.uvA = newUV; - triB.colorA = newColor; - - // Push new subdivisions on stack. - stack.push_back({triA, projA, s.iterations - 1}); - stack.push_back({triB, projB, s.iterations - 1}); + prim.primitive.setColor(heightColor); + prim.primitive.setOpaque(); + ot.insert(prim, zIndex); } + m_gpu.getNextClear(clear.primitive, m_clearcolor); + m_gpu.chain(clear); + m_gpu.chain(ot); } -void psxsplash::Renderer::vramUpload(const uint16_t *imageData, int16_t posX, int16_t posY, int16_t width, +void psxsplash::Renderer::VramUpload(const uint16_t *imageData, int16_t posX, int16_t posY, int16_t width, int16_t height) { psyqo::Rect uploadRect{.a = {.x = posX, .y = posY}, .b = {width, height}}; m_gpu.uploadToVRAM(imageData, uploadRect); +} + +psyqo::Color averageColor(const psyqo::Color &a, const psyqo::Color &b) { + return psyqo::Color{static_cast((a.r + b.r) >> 1), static_cast((a.g + b.g) >> 1), + static_cast((a.b + b.b) >> 1)}; +} + +void psxsplash::Renderer::recursiveSubdivideAndRender(Tri &tri, eastl::array &projected, int zIndex, + int maxIterations) { + uint16_t minX = eastl::min({projected[0].x, projected[1].x, projected[2].x}); + uint16_t maxX = eastl::max({projected[0].x, projected[1].x, projected[2].x}); + uint16_t minY = eastl::min({projected[0].y, projected[1].y, projected[2].y}); + uint16_t maxY = eastl::max({projected[0].y, projected[1].y, projected[2].y}); + uint16_t width = maxX - minX; + uint16_t height = maxY - minY; + + bool leavingScreenSpace = false; + if (projected[0].x < -100 || projected[0].y < -100 || projected[1].x < -100 || projected[1].y < -100 || + projected[2].x < -100 || projected[2].y < -100 || width > 420 || height > 356) { + leavingScreenSpace = true; + } + + if (maxIterations == 0 || ((width < 512 && height < 256 && !leavingScreenSpace))) { + auto &balloc = m_ballocs[m_gpu.getParity()]; + auto &prim = balloc.allocateFragment(); + + prim.primitive.pointA = projected[0]; + prim.primitive.pointB = projected[1]; + prim.primitive.pointC = projected[2]; + + prim.primitive.uvA = tri.uvA; + prim.primitive.uvB = tri.uvB; + prim.primitive.uvC = tri.uvC; // uvC remains UVCoordsPadded. + prim.primitive.tpage = tri.tpage; + psyqo::PrimPieces::ClutIndex clut(tri.clutX, tri.clutY); + prim.primitive.clutIndex = clut; + prim.primitive.setColorA(tri.colorA); + prim.primitive.setColorB(tri.colorB); + prim.primitive.setColorC(tri.colorC); + prim.primitive.setOpaque(); + + m_ots[m_gpu.getParity()].insert(prim, zIndex); + return; + } + + // Subdivide the triangle + auto distanceSq = [](const psyqo::Vertex &a, const psyqo::Vertex &b) -> uint32_t { + int dx = a.x - b.x; + int dy = a.y - b.y; + return dx * dx + dy * dy; + }; + + uint32_t d0 = distanceSq(projected[0], projected[1]); + uint32_t d1 = distanceSq(projected[1], projected[2]); + uint32_t d2 = distanceSq(projected[2], projected[0]); + + int i, j, k; + if (d0 >= d1 && d0 >= d2) { + i = 0; + j = 1; + k = 2; + } else if (d1 >= d0 && d1 >= d2) { + i = 1; + j = 2; + k = 0; + } else { + i = 2; + j = 0; + k = 1; + } + + auto getUVu = [&](int idx) -> uint8_t { + if (idx == 0) return tri.uvA.u; + if (idx == 1) return tri.uvB.u; + return tri.uvC.u; + }; + + auto getUVv = [&](int idx) -> uint8_t { + if (idx == 0) return tri.uvA.v; + if (idx == 1) return tri.uvB.v; + return tri.uvC.v; + }; + + auto getColor = [&](int idx) -> psyqo::Color { + if (idx == 0) return tri.colorA; + if (idx == 1) return tri.colorB; + return tri.colorC; + }; + + psyqo::Vertex mid; + mid.x = (projected[i].x + projected[j].x) >> 1; + mid.y = (projected[i].y + projected[j].y) >> 1; + + uint8_t newU = (getUVu(i) + getUVu(j)) / 2; + uint8_t newV = (getUVv(i) + getUVv(j)) / 2; + + psyqo::Color newColor = averageColor(getColor(i), getColor(j)); + + eastl::array projA, projB; + projA[0] = projected[i]; + projA[1] = mid; + projA[2] = projected[k]; + + projB[0] = mid; + projB[1] = projected[j]; + projB[2] = projected[k]; + + Tri triA, triB; + + triA.uvA = {getUVu(i), getUVv(i)}; + triA.uvB = {newU, newV}; + triA.uvC = {getUVu(k), getUVv(k)}; + + triA.colorA = getColor(i); + triA.colorB = newColor; + triA.colorC = getColor(k); + + /*triA.colorA = {.r = 255}; + triA.colorB = {.r = 255}; + triA.colorC = {.r = 255};*/ + + triA.tpage = tri.tpage; + triA.clutX = tri.clutX; + triA.clutY = tri.clutY; + triA.normal = tri.normal; + + triB.uvA = {newU, newV}; + triB.uvB = {getUVu(j), getUVv(j)}; + triB.uvC = {getUVu(k), getUVv(k)}; + + triB.colorA = newColor; + triB.colorB = getColor(j); + triB.colorC = getColor(k); + + /*triB.colorA = {.g = 255}; + triB.colorB = {.g = 255}; + triB.colorC = {.g = 255};*/ + + triB.tpage = tri.tpage; + triB.clutX = tri.clutX; + triB.clutY = tri.clutY; + triB.normal = tri.normal; + + recursiveSubdivideAndRender(triA, projA, zIndex, maxIterations - 1); + recursiveSubdivideAndRender(triB, projB, zIndex, maxIterations - 1); } \ No newline at end of file diff --git a/src/renderer.hh b/src/renderer.hh index 1a9e4ce..8b73344 100644 --- a/src/renderer.hh +++ b/src/renderer.hh @@ -16,6 +16,7 @@ #include "camera.hh" #include "gameobject.hh" +#include "splashpack.hh" namespace psxsplash { @@ -25,18 +26,19 @@ class Renderer final { Renderer& operator=(const Renderer&) = delete; static constexpr size_t ORDERING_TABLE_SIZE = 2048 * 3; - static constexpr size_t BUMP_ALLOCATOR_SIZE = 8096 * 16; + static constexpr size_t BUMP_ALLOCATOR_SIZE = 8096 * 24; - static void init(psyqo::GPU& gpuInstance); + static void Init(psyqo::GPU& gpuInstance); - void setCamera(Camera& camera); + void SetCamera(Camera& camera); - void render(eastl::vector& objects); - void iterativeSubdivideAndRender(const Tri& initialTri, const eastl::array& initialProj, - int zIndex, int maxIterations); - void vramUpload(const uint16_t* imageData, int16_t posX, int16_t posY, int16_t width, int16_t height); + + void Render(eastl::vector& objects); + void RenderNavmeshPreview(psxsplash::Navmesh navmesh, bool isOnMesh); - static Renderer& getInstance() { + void VramUpload(const uint16_t* imageData, int16_t posX, int16_t posY, int16_t width, int16_t height); + + static Renderer& GetInstance() { psyqo::Kernel::assert(instance != nullptr, "Access to renderer was tried without prior initialization"); return *instance; } @@ -56,7 +58,10 @@ class Renderer final { psyqo::Fragments::SimpleFragment m_clear[2]; psyqo::BumpAllocator m_ballocs[2]; - psyqo::Color m_clearcolor = {.r = 63, .g = 63, .b = 100}; + psyqo::Color m_clearcolor = {.r = 0, .g = 0, .b = 0}; + + void recursiveSubdivideAndRender(Tri &tri, eastl::array &projected, int zIndex, + int maxIterations); }; } // namespace psxsplash \ No newline at end of file diff --git a/src/splashpack.cpp b/src/splashpack.cpp index a4922cd..56c7df2 100644 --- a/src/splashpack.cpp +++ b/src/splashpack.cpp @@ -10,13 +10,37 @@ namespace psxsplash { -eastl::vector LoadSplashpack(uint8_t *data) { +struct SPLASHPACKFileHeader { + char magic[2]; + uint16_t version; + uint16_t gameObjectCount; + uint16_t navmeshCount; + uint16_t textureAtlasCount; + uint16_t clutCount; + uint16_t pad[2]; +}; + +struct SPLASHPACKTextureAtlas { + uint32_t polygonsOffset; + uint16_t width, height; + uint16_t x, y; +}; + +struct SPLASHPACKClut { + uint32_t clutOffset; + uint16_t clutPackingX; + uint16_t clutPackingY; + uint16_t length; + uint16_t pad; +}; + +void SplashPackLoader::LoadSplashpack(uint8_t *data) { psyqo::Kernel::assert(data != nullptr, "Splashpack loading data pointer is null"); psxsplash::SPLASHPACKFileHeader *header = reinterpret_cast(data); psyqo::Kernel::assert(memcmp(header->magic, "SP", 2) == 0, "Splashpack has incorrect magic"); - eastl::vector gameObjects; gameObjects.reserve(header->gameObjectCount); + navmeshes.reserve(header->navmeshCount); uint8_t *curentPointer = data + sizeof(psxsplash::SPLASHPACKFileHeader); @@ -27,23 +51,28 @@ eastl::vector LoadSplashpack(uint8_t *data) { curentPointer += sizeof(psxsplash::GameObject); } + for (uint16_t i = 0; i < header->navmeshCount; i++) { + psxsplash::Navmesh *navmesh = reinterpret_cast(curentPointer); + navmesh->polygons = reinterpret_cast(data + navmesh->polygonsOffset); + navmeshes.push_back(navmesh); + curentPointer += sizeof(psxsplash::Navmesh); + } + for (uint16_t i = 0; i < header->textureAtlasCount; i++) { psxsplash::SPLASHPACKTextureAtlas *atlas = reinterpret_cast(curentPointer); - - uint8_t *offsetData = data + atlas->polygonsOffset; + uint8_t *offsetData = data + atlas->polygonsOffset; uint16_t *castedData = reinterpret_cast(offsetData); - psxsplash::Renderer::getInstance().vramUpload(castedData, atlas->x, atlas->y, atlas->width, atlas->height); + psxsplash::Renderer::GetInstance().VramUpload(castedData, atlas->x, atlas->y, atlas->width, atlas->height); curentPointer += sizeof(psxsplash::SPLASHPACKTextureAtlas); } for (uint16_t i = 0; i < header->clutCount; i++) { psxsplash::SPLASHPACKClut *clut = reinterpret_cast(curentPointer); - uint8_t* clutOffset = data + clut->clutOffset; - psxsplash::Renderer::getInstance().vramUpload((uint16_t*) clutOffset, clut->clutPackingX * 16, clut->clutPackingY, clut->length, 1); + uint8_t *clutOffset = data + clut->clutOffset; + psxsplash::Renderer::GetInstance().VramUpload((uint16_t *)clutOffset, clut->clutPackingX * 16, + clut->clutPackingY, clut->length, 1); curentPointer += sizeof(psxsplash::SPLASHPACKClut); } - - return gameObjects; } } // namespace psxsplash diff --git a/src/splashpack.hh b/src/splashpack.hh index f2fa068..7218ed8 100644 --- a/src/splashpack.hh +++ b/src/splashpack.hh @@ -5,32 +5,15 @@ #include #include "gameobject.hh" +#include "navmesh.hh" namespace psxsplash { -struct SPLASHPACKFileHeader { - char magic[2]; - uint16_t version; - uint16_t gameObjectCount; - uint16_t textureAtlasCount; - uint16_t clutCount; - uint16_t pad[3]; +class SplashPackLoader { + public: + eastl::vector gameObjects; + eastl::vector navmeshes; + void LoadSplashpack(uint8_t *data); }; -struct SPLASHPACKTextureAtlas { - uint32_t polygonsOffset; - uint16_t width, height; - uint16_t x, y; -}; - -struct SPLASHPACKClut { - uint32_t clutOffset; - uint16_t clutPackingX; - uint16_t clutPackingY; - uint16_t length; - uint16_t pad; -}; - -eastl::vector LoadSplashpack(uint8_t *data); - }; // namespace psxsplash \ No newline at end of file