From cd91c92927b07a3bf2c180dd0be3767fd7fec2a3 Mon Sep 17 00:00:00 2001 From: mwilsnd Date: Tue, 13 Oct 2020 01:58:52 -0500 Subject: [PATCH] Beta 1.1 --- CHANGES.md | 15 + MCM/SmoothCamMCM.pex | Bin 82307 -> 83127 bytes MCM/mcm.psc | 42 ++- SmoothCam/include/camera.h | 16 +- SmoothCam/include/config.h | 20 +- SmoothCam/include/havok/hkpCastCollector.h | 177 ++++++++++++ SmoothCam/include/mmath.h | 9 - SmoothCam/include/pch.h | 3 + SmoothCam/include/raycast.h | 72 +---- SmoothCam/include/skyrimSE/ThirdPersonState.h | 3 + SmoothCam/include/skyrimSE/bhkWorld.h | 4 +- SmoothCam/source/arrow_fixes.cpp | 105 +++++-- SmoothCam/source/camera.cpp | 270 +++++++++-------- SmoothCam/source/camera_state.cpp | 13 +- SmoothCam/source/config.cpp | 17 ++ SmoothCam/source/detours.cpp | 39 ++- SmoothCam/source/main.cpp | 6 +- SmoothCam/source/mmath.cpp | 99 ------- SmoothCam/source/papyrus.cpp | 13 + SmoothCam/source/raycast.cpp | 271 ++++++------------ 20 files changed, 677 insertions(+), 517 deletions(-) create mode 100644 SmoothCam/include/havok/hkpCastCollector.h diff --git a/CHANGES.md b/CHANGES.md index ddfd95c..1ef8c44 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,18 @@ +## Beta 1.1 +* Bumped module and MCM version number to 10 +* Added a compatibility patch for Immersive First Person View. +* Tweaked Improved Camera compatibility when on horseback. +* Remove dynamic casting for camera state method invocations, not really required. +* Config now reads fNearDistance, in case the player changes it - changing it isn't really advised though if you use Immersive First Person View (We check the expected near value against what it actually is, combined with a distance check, to try and figure out if we are in IFPV's hacked first person mode). +* Config now reads fMinCurrentZoom rather than use the hard coded value. +* Adjusted config defaults to no longer enable compat options - these should be opt-in. +* Using new raycasting method for the crosshair, dropped the jank custom intersection test. +* Fix arrow and magic projectiles spawning behind the player when using the archery patch with SSE Engine Fixes. +* Changed some of the interpolation math to FP64 to gain a bit more precision (might not end up being necessary). +* Switched to SKSE's ITimer over using naked qpc. +* Updated config defaults and added a new MCM option to restore default values. +* As some users have reported local space smoothing causing jitter, the local space smoothing rate now defaults to 1, making it opt-in. Jitter is mostly caused by a sub-60 frame rate, currently looking into options for correcting jitter in a later update. + ## Beta 1 * Bumped module and MCM version number to 9 * Promoted to beta. diff --git a/MCM/SmoothCamMCM.pex b/MCM/SmoothCamMCM.pex index a3543d359e2857a4794dcdf37e9a30b91428a8b6..9b374680a6c579830ce9ab7a0bb71ef90ce707fd 100644 GIT binary patch literal 83127 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 trK$)c5X34@w4w7M*lCJ+r?Z_UL;N$FeQ_nYZ%H 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 = AddToggleOption(this->displayName, false) + } + + MACRO implSelectHandler = { + if (ShowMessage("Are you sure? This will reset all settings to their default values.")) + SmoothCam_ResetConfig() + ShowMessage("Settings reset.") + endIf + } + + MACRO implDesc = { + SetInfoText(this->desc) + } +} + #constexpr_struct ListSetting { real_int ref = 0 string settingName = "" @@ -201,7 +223,7 @@ endFunction } ScriptMeta scriptMetaInfo -> { - version: 9 + version: 10 } ; Presets @@ -276,6 +298,17 @@ ToggleSetting disableDuringDialog -> { displayName: "Disable During Dialog" desc: "Disables SmoothCam when the dialog menu is open." } +ToggleSetting ifpvCompat -> { + settingName: "IFPVCompat" + displayName: "Immersive First Person View" + desc: "Enable compat fixes for Improved First Person View." +} + +; Reset +ResetSetting reset -> { + displayName: "Reset All Settings" + desc: "Set all settings back to their defual values." +} ; Following ToggleSetting interpEnabled -> { @@ -1370,6 +1403,9 @@ event OnPageReset(string a_page) IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { icFirstPersonHorse, icFirstPersonDragon, icFirstPersonSitting }) + + AddHeaderOption("Immersive First Person View") + ifpvCompat->!implControl elseIf (a_page == " Following") AddHeaderOption("Interpolation") IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { @@ -1406,7 +1442,7 @@ event OnPageReset(string a_page) AddHeaderOption("Misc") IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - shoulderSwapKey, swapDistanceClampXAxis, zoomMul, disableDeltaTime + shoulderSwapKey, swapDistanceClampXAxis, zoomMul, disableDeltaTime, reset }) elseIf (a_page == " Crosshair") AddHeaderOption("3D Crosshair Settings") @@ -1657,6 +1693,7 @@ endEvent event OnOptionSelect(int a_option) IMPL_IFCHAIN_MACRO_INVOKE(a_option, ref, implSelectHandler, { IMPL_ALL_IMPLS_OF_STRUCT(ToggleSetting), + IMPL_ALL_IMPLS_OF_STRUCT(ResetSetting), IMPL_ALL_IMPLS_OF_STRUCT(LoadPresetSetting) }) endEvent @@ -1693,6 +1730,7 @@ event OnOptionHighlight(int a_option) IMPL_IFCHAIN_MACRO_INVOKE(a_option, ref, implDesc, { IMPL_ALL_IMPLS_OF_STRUCT(SliderSetting), IMPL_ALL_IMPLS_OF_STRUCT(ToggleSetting), + IMPL_ALL_IMPLS_OF_STRUCT(ResetSetting), IMPL_ALL_IMPLS_OF_STRUCT(ListSetting), IMPL_ALL_IMPLS_OF_STRUCT(SavePresetSetting), IMPL_ALL_IMPLS_OF_STRUCT(LoadPresetSetting), diff --git a/SmoothCam/include/camera.h b/SmoothCam/include/camera.h index 198ef8a..a97f2b7 100644 --- a/SmoothCam/include/camera.h +++ b/SmoothCam/include/camera.h @@ -38,7 +38,6 @@ namespace Camera { const auto UNIT_FORWARD = glm::vec3(1.0f, 0.0f, 0.0f); const auto UNIT_RIGHT = glm::vec3(0.0f, 1.0f, 0.0f); const auto UNIT_UP = glm::vec3(0.0f, 0.0f, 1.0f); - constexpr auto SKYRIM_MIN_ZOOM_FRACTION = 0.2f; class SmoothCamera { public: @@ -50,6 +49,8 @@ namespace Camera { ~SmoothCamera() = default; public: + // Runs before the internal game camera logic + void PreGameUpdate(PlayerCharacter* player, CorrectedPlayerCamera* camera); // Selects the correct update method and positions the camera void UpdateCamera(PlayerCharacter* player, CorrectedPlayerCamera* camera); // Called when the player toggles the POV @@ -72,6 +73,9 @@ namespace Camera { const bool UpdateCameraPOVState(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) noexcept; /// Camera state updates + // Check if the camera is near the player's head (for first person mods) + bool CameraNearHead(const PlayerCharacter* player, const CorrectedPlayerCamera* camere, float cutOff = 32.0f); + bool IFPV_InFirstPersonState(const PlayerCharacter* player, const CorrectedPlayerCamera* camera); // Returns the current camera state for use in selecting an update method const GameState::CameraState GetCurrentCameraState(const PlayerCharacter* player, const CorrectedPlayerCamera* camera); // Returns the current camera action state for use in the selected update method @@ -109,7 +113,7 @@ namespace Camera { // Returns the full local-space camera offset for the current player state glm::vec3 GetCurrentCameraOffset(PlayerCharacter* player, const CorrectedPlayerCamera* camera) const noexcept; // Returns the current smoothing scalar to use for the given distance to the player - float GetCurrentSmoothingScalar(const float distance, ScalarSelector method = ScalarSelector::Normal) const; + double GetCurrentSmoothingScalar(const float distance, ScalarSelector method = ScalarSelector::Normal) const; // Returns the user defined distance clamping vector pair std::tuple GetDistanceClamping() const noexcept; // Returns true if interpolation is allowed in the current state @@ -132,6 +136,8 @@ namespace Camera { void SetCrosshairEnabled(bool enabled) const; /// Camera getters + // Update the internal rotation + void UpdateInternalRotation(CorrectedPlayerCamera* camera) noexcept; // Returns the camera's yaw float GetCameraYawRotation(const CorrectedPlayerCamera* camera) const noexcept; // Returns the camera's pitch @@ -196,11 +202,17 @@ namespace Camera { CameraActionState lastActionState = CameraActionState::Unknown; mmath::NiMatrix44 worldToScreen = {}; + float lastNearPlane = 0.0f; + glm::vec3 gameInitialWorldPosition = { 0.0f, 0.0f, 0.0f }; + glm::vec3 gameLastActualPosition = { 0.0f, 0.0f, 0.0f }; glm::vec3 lastPosition = { 0.0f, 0.0f, 0.0f }; glm::vec3 currentPosition = { 0.0f, 0.0f, 0.0f }; glm::vec3 lastLocalPosition = { 0.0f, 0.0f, 0.0f }; glm::vec3 lastWorldPosition = { 0.0f, 0.0f, 0.0f }; + glm::vec2 currentRotation = { 0.0f, 0.0f }; + glm::quat currentQuat = glm::identity(); + template struct TransitionGroup { T lastPosition = {}; diff --git a/SmoothCam/include/config.h b/SmoothCam/include/config.h index 92d4eff..e6fba9e 100644 --- a/SmoothCam/include/config.h +++ b/SmoothCam/include/config.h @@ -75,6 +75,8 @@ namespace Config { typedef struct gameConf { float f3PArrowTiltUpAngle = 2.5f; float f3PBoltTiltUpAngle = 2.5f; + float fNearDistance = 15.0f; + float fMinCurrentZoom = -0.200000003; } GameConfig; typedef struct offsetGroup { @@ -127,9 +129,10 @@ namespace Config { // Comapt bool disableDuringDialog = false; - bool comaptIC_FirstPersonHorse = true; - bool comaptIC_FirstPersonDragon = true; - bool compatIC_FirstPersonSitting = true; + bool comaptIC_FirstPersonHorse = false; + bool comaptIC_FirstPersonDragon = false; + bool compatIC_FirstPersonSitting = false; + bool compatIFPV = false; // Primary interpolation bool enableInterp = true; @@ -142,15 +145,15 @@ namespace Config { // Separate local space interpolation bool separateLocalInterp = true; - ScalarMethods separateLocalScalar = ScalarMethods::CIRC_IN; - float localScalarRate = 0.75f; + ScalarMethods separateLocalScalar = ScalarMethods::EXP_IN; + float localScalarRate = 1.0f; // Separate Z bool separateZInterp = true; ScalarMethods separateZScalar = ScalarMethods::SINE_IN; float separateZMaxSmoothingDistance = 60.0f; - float separateZMinFollowRate = 0.15f; - float separateZMaxFollowRate = 0.4f; + float separateZMinFollowRate = 0.2f; + float separateZMaxFollowRate = 0.6f; // Offset interpolation bool enableOffsetInterpolation = true; @@ -168,7 +171,7 @@ namespace Config { float cameraDistanceClampXMax = 35.0f; bool cameraDistanceClampYEnable = true; float cameraDistanceClampYMin = -100.0f; - float cameraDistanceClampYMax = 20.0f; + float cameraDistanceClampYMax = 10.0f; bool cameraDistanceClampZEnable = true; float cameraDistanceClampZMin = -60.0f; float cameraDistanceClampZMax = 60.0f; @@ -198,6 +201,7 @@ namespace Config { void ReadConfigFile(); void SaveCurrentConfig(); UserConfig* GetCurrentConfig() noexcept; + void ResetConfig(); // Returns "" if ok, otherwise has an error message BSFixedString SaveConfigAsPreset(int slot, const BSFixedString& name); diff --git a/SmoothCam/include/havok/hkpCastCollector.h b/SmoothCam/include/havok/hkpCastCollector.h new file mode 100644 index 0000000..e8ef3b5 --- /dev/null +++ b/SmoothCam/include/havok/hkpCastCollector.h @@ -0,0 +1,177 @@ +#pragma once +using hkpShape = void; +using hkpCdBody = void; +using hkpCollidable = void; +using hkpShapeKey = uint32_t; + +typedef struct bhkShapeList { + hkpShape* shape; + uint64_t unk0; + void* shapeInfo; + bhkShapeList* next; + glm::vec3 unk1; + uint32_t flags; + uint32_t unk2; + uint32_t unk3; + uint32_t unk4; +} bhkShapeList; + +struct hkpRayHitResult { + glm::vec3 normal; + float hitFraction; + bhkShapeList* hit; +}; + +struct hkpAllCdPointTempResult { + glm::vec4 normal; + float hitFraction; +}; + +class hkpCastCollector { + public: + hkpCastCollector() { + results.reserve(64); + } + + inline void reset() { + results.clear(); + m_hitFraction = 0.0f; + m_earlyOutHitFraction = 1.0f; + } + + virtual void addRayHit(bhkShapeList* list, const hkpAllCdPointTempResult* hitInfo) { + hkpRayHitResult hitResult; + hitResult.hitFraction = hitInfo->hitFraction; + hitResult.normal = static_cast(hitInfo->normal); + + while (list) { + if (!list->next) break; + list = list->next; + } + + hitResult.hit = list; + if (hitResult.hit) { + const uint64_t m = 1ULL << static_cast(hitResult.hit->flags & 0x7F); + constexpr uint64_t filter = 0x40122716; + if ((m & filter) != 0) { + results.push_back(hitResult); + // We only want further hits to be closer than this + m_earlyOutHitFraction = hitResult.hitFraction; + } + } + } + + public: + enum { MAX_HIERARCHY_DEPTH = 8 }; + + float m_earlyOutHitFraction = 1.0f; //0x08 + uint32_t pad0; //0x0C + uint64_t pad1[2]; //0x10, 0x18 + float m_hitFraction = 0.0f; //0x20 + uint32_t unk0 = 0; //0x24 + hkpShapeKey shapeKey; //0x28 + uint32_t pad2; //0x2C + hkpShapeKey m_shapeKeys[MAX_HIERARCHY_DEPTH]; + uint32_t m_shapeKeyIndex = 0; + uint32_t pad3; + uint64_t unk1 = 0; + hkpCollidable* m_rootCollidable = nullptr; + uint64_t unk2 = 0; + + std::vector results; +}; + +typedef __declspec(align(16)) struct hkpRayCastInfo { + glm::vec4 start; // 0x0 + glm::vec4 unkVec; // 0x10 + bool unk0 = false; // 0x20 + + // auVar11 = subps(*(undefined *)(param_2 + 0x24),(undefined [16])0x0); + // auVar10 = ZEXT812(SUB168(auVar11,0) & 0x7ff ... + // uVar3 = movmskps(uVar3,CONCAT412(0xffffffff, ... + // if (((byte)uVar3 & 7) != 7) + // write unkVec.x SUB124(*hkpRayCastInfo >> 0x20, 0) + (float)hkpRayCastInfo[0x24] + // ... + // write unkVec.w SUB164(*hkpRayCastInfo >> 0x60, 0) + (float)hkpRayCastInfo[0x27] + // unk0 = 0 + uint32_t flags = 0; // 0x24 + + uint64_t unk1_0 = 0; + uint64_t unk1_1 = 0; + uint64_t unk1_2 = 0; + float unk2 = 1.0f; // 0x40 + glm::ivec3 unk3 = { -1, -1, -1 }; // 0x44 + uint64_t unk4_0 = 0; + uint64_t unk4_1 = 0; + uint64_t unk4_2 = 0; + uint64_t unk4_3 = 0; + uint32_t unk5 = 0; // 0x70 + uintptr_t unk6 = 0; + uint64_t unk7 = 0; // 0x80 + uintptr_t unk8 = 0; + + glm::vec4 end; // 0x90 + + // collector = *(longlong *)(param_2 + 0x2c); + // lVar2 = *(longlong *)(param_2 + 0x2e); + // if ((collector == 0) && (lVar2 == 0)) + // collector = *(longlong *)(param_2 + 0x2a); + // if (collector == 0) + // FUN_140a7b000_UnkTraceRelated(lVar5, hkpRayCastInfo, hkpRayCastInfo + 0xc); + // | + // v + // local_58[0] = &vtable.hkpSimpleWorldRayCaster; + // broadphase = broadphase = *(longlong **)(lVar5 + 0x88); + // filter = *(longlong *)(this + 0xd0); + // hkpSimpleWorldCastRayBroadphaseCache( + // local_58, + // broadphase, + // hkpRayCastInfo, + // filter, + // 0, // cache + // hkpRayCastInfo + 0xc + // ); + + // hkpSimpleWorldCastRayBroadphaseCache: + // (**(code **)(*broadphase + 200))(broadphase, hkpRayCastInfo, this, 0); + + // local_88[0] = &vtable.hkpWorldRayCaster; + // broadphase = *(longlong **)(lVar5 + 0x88); + // filter = *(longlong *)(lVar5 + 0xd0); + // cache = *(longlong *)(param_2 + 0x28); + // 140b28060:castRay(local_88, broadphase, hkpRayCastInfo, filter, cache, collector_00); + // broadphase->FUN_140b306f0 + // [ (**(code **)(*broadphase + 200))(broadphase, hkpRayCastInfo, this, 0) ] vtable.hkpWorldRayCaster->FUN_140b284f0 + // if ((*(byte *)(*(longlong *)(broadphase + 0xb0) + 0x10 + uVar9 * 0x18) & 1) == 0) { + // (**(code **)(*this + 8))( + // SUB168(auVar28,0), this, + // *(undefined8 *)(*(longlong *)(broadphase + 0xb0) + 0x10 + uVar9 * 0x18), + // (ulonglong)uVar34); [ hkpShape->FUN_140a59340:invokeCollector ] + // FUN_140a59340:invokeCollector(auVar28, this, broadphaseOffset, uVar34) + // (**(code **)*param_4)(param_4,param_3,&local_58); [ virtual addRayHit ] + + uint64_t unk9 = 0; // 0xA0 + hkpCastCollector* collector; // 0xA8 + uint64_t unk10 = 0; + uint64_t unk11 = 0; + bool unk12 = false; // 0xC0 + + // lVar22 = 3; + // plVar16 = 0xC0 + // plVar16 = plVar16 + 2; + // lVar22 += -1; + // while (lVar22 != 0) + uint64_t unk13[11] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +} hkpRayCastInfo; +static_assert(offsetof(hkpRayCastInfo, start.w) == 0xC); +static_assert(offsetof(hkpRayCastInfo, unk0) == 0x20); +static_assert(offsetof(hkpRayCastInfo, flags) == 0x24); +static_assert(offsetof(hkpRayCastInfo, unk2) == 0x40); +static_assert(offsetof(hkpRayCastInfo, unk5) == 0x70); +static_assert(offsetof(hkpRayCastInfo, unk7) == 0x80); +static_assert(offsetof(hkpRayCastInfo, end) == 0x90); +static_assert(offsetof(hkpRayCastInfo, end.w) == 0x9C); +static_assert(offsetof(hkpRayCastInfo, unk9) == 0xA0); +static_assert(offsetof(hkpRayCastInfo, collector) == 0xA8); +static_assert(offsetof(hkpRayCastInfo, unk12) == 0xC0); +static_assert(sizeof(hkpRayCastInfo) == 0x120); \ No newline at end of file diff --git a/SmoothCam/include/mmath.h b/SmoothCam/include/mmath.h index 7de41c6..74ec0d6 100644 --- a/SmoothCam/include/mmath.h +++ b/SmoothCam/include/mmath.h @@ -52,15 +52,6 @@ namespace mmath { void DecomposeToBasis(const glm::vec3& point, const glm::vec3& rotation, glm::vec3& forward, glm::vec3& right, glm::vec3& up, glm::vec3& coef) noexcept; - // Construct an AABB for an actor - AABB GetReferAABB(TESObjectREFR* ref); - AABB RotateAABB(const AABB& axisAligned, const NiMatrix33& mat) noexcept; - AABB GetActorAABB(Actor* actor); - - // Ray-AABB intersect - bool IntersectRayAABB(const glm::vec3& start, const glm::vec3& dir, const AABB& aabb, - glm::vec3& hitPos) noexcept; - glm::vec2 PointToScreen(const glm::vec3& point); template diff --git a/SmoothCam/include/pch.h b/SmoothCam/include/pch.h index 26c180a..14b0fa5 100644 --- a/SmoothCam/include/pch.h +++ b/SmoothCam/include/pch.h @@ -106,8 +106,11 @@ #include "addrlib/offsets.h" #ifdef _DEBUG +//# define DEBUG_DRAWING # include "profile.h" +# ifdef DEBUG_DRAWING # include "debug_drawing.h" +# endif #endif #include "basicdetour.h" diff --git a/SmoothCam/include/raycast.h b/SmoothCam/include/raycast.h index 5254d0b..6d02136 100644 --- a/SmoothCam/include/raycast.h +++ b/SmoothCam/include/raycast.h @@ -1,59 +1,7 @@ #pragma once +#include "havok/hkpCastCollector.h" namespace Raycast { - constexpr auto MaxActorIntersections = 16; - - // Thanks to CBPC SSE for revealing this handy object - // While not perfect, it works well enough for now - class AIProcessManager { - public: - static AIProcessManager* GetSingleton(); - - UInt8 unk000; // 008 - bool enableDetection; // 001 - bool unk002; // 002 - UInt8 unk003; // 003 - UInt32 unk004; // 004 - bool enableHighProcess; // 008 - bool enableLowProcess; // 009 - bool enableMiddleHighProcess; // 00A - bool enableMiddleLowProcess; // 00B - bool enableAISchedules; // 00C - UInt8 unk00D; // 00D - UInt8 unk00E; // 00E - UInt8 unk00F; // 00F - SInt32 numActorsInHighProcess; // 010 - UInt32 unk014[(0x30 - 0x014) / sizeof(UInt32)]; - tArray actorsHigh; // 030 - tArray actorsLow; // 048 - tArray actorsMiddleLow; // 060 - tArray actorsMiddleHigh; // 078 - UInt32 unk90[(0xF0 - 0x7C) / sizeof(UInt32)]; - tArray activeEffectShaders; // 108 - //mutable BSUniqueLock activeEffectShadersLock; // 120 - }; - - static_assert(offsetof(AIProcessManager, numActorsInHighProcess) >= 0x10, "Unk141F831B0::actorsHigh is too early!"); - static_assert(offsetof(AIProcessManager, numActorsInHighProcess) <= 0x10, "Unk141F831B0::actorsHigh is too late!"); - - static_assert(offsetof(AIProcessManager, actorsHigh) >= 0x030, "Unk141F831B0::actorsHigh is too early!"); - static_assert(offsetof(AIProcessManager, actorsHigh) <= 0x039, "Unk141F831B0::actorsHigh is too late!"); - - static_assert(offsetof(AIProcessManager, actorsLow) >= 0x048, "Unk141F831B0::actorsLow is too early!"); - static_assert(offsetof(AIProcessManager, actorsLow) <= 0x048, "Unk141F831B0::actorsLow is too late!"); - - static_assert(offsetof(AIProcessManager, actorsMiddleLow) >= 0x060, "Unk141F831B0::actorsMiddleLow is too early!"); - static_assert(offsetof(AIProcessManager, actorsMiddleLow) <= 0x060, "Unk141F831B0::actorsMiddleLow is too late!"); - - static_assert(offsetof(AIProcessManager, actorsMiddleHigh) >= 0x078, "Unk141F831B0::actorsMiddleHigh is too early!"); - static_assert(offsetof(AIProcessManager, actorsMiddleHigh) <= 0x078, "Unk141F831B0::actorsMiddleHigh is too late!"); - - static_assert(offsetof(AIProcessManager, activeEffectShaders) >= 0x108, "Unk141F831B0::activeEffectShaders is too early!"); - static_assert(offsetof(AIProcessManager, activeEffectShaders) <= 0x108, "Unk141F831B0::activeEffectShaders is too late!"); - - // These require more inspection - They all contain vtables of assorted physics shapes - // It would be nice to figure these out to the point that a TESObjectREFR could be - // extracted from them struct hkpGenericShapeData { intptr_t* unk; uint32_t shapeType; @@ -97,12 +45,8 @@ namespace Raycast { } RayResult; static_assert(sizeof(RayResult) == 128); - using ActorRayResults = std::array; - uint8_t IntersectRayAABBAllActorsIn(const tArray& list, ActorRayResults& results, const glm::vec3& start, - const glm::vec3& dir, float distance, uint8_t currentCount = 0); - RayResult InteresctRayAABBAllActors(const glm::vec3& start, const glm::vec3& end); - // Cast a ray from 'start' to 'end', returning the first thing it hits + // This variant is used by the camera to test with the world for clipping // Params: // glm::vec4 start: Starting position for the trace in world space // glm::vec4 end: End position for the trace in world space @@ -113,4 +57,16 @@ namespace Raycast { // A structure holding the results of the ray cast. // If the ray hit something, result.hit will be true. RayResult CastRay(glm::vec4 start, glm::vec4 end, float traceHullSize, bool intersectCharacters = false); + + // Cast a ray from 'start' to 'end', returning the first thing it hits + // This variant collides with pretty much any solid geometry + // Params: + // glm::vec4 start: Starting position for the trace in world space + // glm::vec4 end: End position for the trace in world space + // + // Returns: + // RayResult: + // A structure holding the results of the ray cast. + // If the ray hit something, result.hit will be true. + RayResult hkpCastRay(glm::vec4 start, glm::vec4 end); } \ No newline at end of file diff --git a/SmoothCam/include/skyrimSE/ThirdPersonState.h b/SmoothCam/include/skyrimSE/ThirdPersonState.h index e8e8d33..7f1300a 100644 --- a/SmoothCam/include/skyrimSE/ThirdPersonState.h +++ b/SmoothCam/include/skyrimSE/ThirdPersonState.h @@ -8,6 +8,9 @@ class CorrectedThirdPersonState : public TESCameraState virtual void Unk_09(void); // 0x48 virtual void Unk_0A(void); // 0x50 virtual void UpdateMode(bool weaponDrawn); // 0x58 + virtual void Unk01(); + virtual void Unk02(); + virtual void UpdateRotation(); PlayerInputHandler inputHandler; // 20 NiNode* cameraNode; // 30 diff --git a/SmoothCam/include/skyrimSE/bhkWorld.h b/SmoothCam/include/skyrimSE/bhkWorld.h index a08bc90..bbe82a0 100644 --- a/SmoothCam/include/skyrimSE/bhkWorld.h +++ b/SmoothCam/include/skyrimSE/bhkWorld.h @@ -1,5 +1,7 @@ #pragma once +struct hkpRayCastInfo; + class bhkWorld { public: virtual void unk1(); // 0x0 ==> FUN_140dac670 @@ -53,7 +55,7 @@ class bhkWorld { virtual void unk49(); // 0x180 ==> FUN_140ddc750 virtual void unk50(); // 0x188 ==> FUN_140ddc790 virtual void unk51(); // 0x190 ==> FUN_140da64e0 - virtual void unk52(); // 0x198 ==> FUN_140da7580 + virtual void CastRay(hkpRayCastInfo*); // 0x198 ==> FUN_140da7580 virtual void unk53(); // 0x1A0 ==> FUN_140da79c0 virtual void unk54(); // 0x1A8 ==> FUN_140da79f0 virtual void unk55(); // 0x1B0 ==> FUN_140da7af0 diff --git a/SmoothCam/source/arrow_fixes.cpp b/SmoothCam/source/arrow_fixes.cpp index 31beda2..c34a3d6 100644 --- a/SmoothCam/source/arrow_fixes.cpp +++ b/SmoothCam/source/arrow_fixes.cpp @@ -1,7 +1,7 @@ #include "arrow_fixes.h" #include "game_state.h" -#ifdef _DEBUG +#ifdef DEBUG_DRAWING SkyrimSE::ArrowProjectile* current; std::mutex segmentLock; std::vector> segments; @@ -16,19 +16,6 @@ void ArrowFixes::Draw() { } } -typedef uintptr_t(*UpdateTraceArrowProjectile)(SkyrimSE::ArrowProjectile*, NiPoint3*, NiPoint3*); -UpdateTraceArrowProjectile fnUpdateTraceArrowProjectile; -std::unique_ptr detUpdateTraceArrowProjectile; -uintptr_t mUpdateTraceArrowProjectile(SkyrimSE::ArrowProjectile* arrow, NiPoint3* to, NiPoint3* from) { - if (arrow->shooter == 0x00100000) { - std::lock_guard lock(segmentLock); - segments.push_back(std::make_tuple(glm::vec3{ from->x, from->y, from->z }, glm::vec3{ to->x, to->y, to->z })); - } - - auto ret = fnUpdateTraceArrowProjectile(arrow, to, from); - return ret; -} - typedef UInt32(*MaybeSpawnArrow)(uint32_t* arrowHandle, ArrowFixes::LaunchData* launchData, uintptr_t param_3, uintptr_t** param_4); MaybeSpawnArrow arrOrig; @@ -46,11 +33,61 @@ UInt32 mMaybeSpawnArrow(uint32_t* arrowHandle, ArrowFixes::LaunchData* launchDat std::lock_guard lock(segmentLock); segments.clear(); } + return ret; +} +typedef uintptr_t(*UpdateTraceArrowProjectile)(SkyrimSE::ArrowProjectile*, NiPoint3&, NiPoint3&); +UpdateTraceArrowProjectile fnUpdateTraceArrowProjectile; +std::unique_ptr detUpdateTraceArrowProjectile; +uintptr_t mUpdateTraceArrowProjectile(SkyrimSE::ArrowProjectile* arrow, NiPoint3& to, NiPoint3& from) { + if (arrow->shooter == 0x00100000) { + std::lock_guard lock(segmentLock); + segments.push_back(std::make_tuple(glm::vec3{ from.x, from.y, from.z }, glm::vec3{ to.x, to.y, to.z })); + } + auto ret = fnUpdateTraceArrowProjectile(arrow, to, from); return ret; } #endif +//FUN_14084b430:49866 +typedef void(*FactorCameraOffset)(CorrectedPlayerCamera* camera, NiPoint3& pos, bool fac); +FactorCameraOffset fnFactorCameraOffset; +std::unique_ptr detFactorCameraOffset; +void mFactorCameraOffset(CorrectedPlayerCamera* camera, NiPoint3& pos, bool fac) { + if (fac) { + fnFactorCameraOffset(camera, pos, fac); + return; + } + + auto tps = reinterpret_cast(camera->cameraStates[CorrectedPlayerCamera::kCameraState_ThirdPerson2]); + if (!tps) { + fnFactorCameraOffset(camera, pos, fac); + return; + } + + NiQuaternion quat; + NiMatrix33 mat; + + tps->UpdateRotation(); + typedef void(__thiscall CorrectedThirdPersonState::* GetRotation)(NiQuaternion&); + (tps->*reinterpret_cast(&CorrectedThirdPersonState::Unk_04))(quat); + + //1cfa50:makeMatrix33Qua + typedef void(*makeMatrix33Qua)(NiQuaternion& q, NiMatrix33& m); + Offsets::Get(15612)(quat, mat); + + NiPoint3 offsetActual; + + // SSE Engine Fixes will call GetEyeVector with factorCameraOffset = true + // We appear to screw this computation up as a side effect of correcting the interaction crosshair + // So, yeah. Just fix it here. + if (GameState::IsThirdPerson(camera) || GameState::IsInHorseCamera(camera) || GameState::IsInDragonCamera(camera)) + offsetActual = { 0, 0, 0 }; + else + offsetActual = tps->offsetVector; + pos = mat * offsetActual; +} + typedef void(*UpdateArrowFlightPath)(SkyrimSE::ArrowProjectile* arrow); UpdateArrowFlightPath fnUpdateArrowFlightPath; std::unique_ptr detArrowFlightPath; @@ -140,34 +177,34 @@ void mUpdateArrowFlightPath(SkyrimSE::ArrowProjectile* arrow) { bool ArrowFixes::Attach() { { - //140750150::UpdateArrowFlightPath - fnUpdateArrowFlightPath = Offsets::Get(42998); - detArrowFlightPath = std::make_unique( - reinterpret_cast(&fnUpdateArrowFlightPath), - mUpdateArrowFlightPath + //FUN_14084b430:FactorCameraOffset:GetEyeVector + fnFactorCameraOffset = Offsets::Get(49866); + detFactorCameraOffset = std::make_unique( + reinterpret_cast(&fnFactorCameraOffset), + mFactorCameraOffset ); - if (!detArrowFlightPath->Attach()) { + if (!detFactorCameraOffset->Attach()) { _ERROR("Failed to place detour on target function, this error is fatal."); FatalError(L"Failed to place detour on target function, this error is fatal."); } } -#ifdef _DEBUG { - //140751430::UpdateTraceArrowProjectile - fnUpdateTraceArrowProjectile = Offsets::Get(43008); - detUpdateTraceArrowProjectile = std::make_unique( - reinterpret_cast(&fnUpdateTraceArrowProjectile), - mUpdateTraceArrowProjectile + //140750150::UpdateArrowFlightPath + fnUpdateArrowFlightPath = Offsets::Get(42998); + detArrowFlightPath = std::make_unique( + reinterpret_cast(&fnUpdateArrowFlightPath), + mUpdateArrowFlightPath ); - if (!detUpdateTraceArrowProjectile->Attach()) { + if (!detArrowFlightPath->Attach()) { _ERROR("Failed to place detour on target function, this error is fatal."); FatalError(L"Failed to place detour on target function, this error is fatal."); } } +#ifdef DEBUG_DRAWING { arrOrig = Offsets::Get(42928); detMaybeArrow = std::make_unique( @@ -180,6 +217,20 @@ bool ArrowFixes::Attach() { FatalError(L"Failed to place detour on target function, this error is fatal."); } } + + { + //140751430::UpdateTraceArrowProjectile + fnUpdateTraceArrowProjectile = Offsets::Get(43008); + detUpdateTraceArrowProjectile = std::make_unique( + reinterpret_cast(&fnUpdateTraceArrowProjectile), + mUpdateTraceArrowProjectile + ); + + if (!detUpdateTraceArrowProjectile->Attach()) { + _ERROR("Failed to place detour on target function, this error is fatal."); + FatalError(L"Failed to place detour on target function, this error is fatal."); + } + } #endif return true; diff --git a/SmoothCam/source/camera.cpp b/SmoothCam/source/camera.cpp index 9e42678..214482e 100644 --- a/SmoothCam/source/camera.cpp +++ b/SmoothCam/source/camera.cpp @@ -1,11 +1,15 @@ #include "camera.h" #include "arrow_fixes.h" #ifdef _DEBUG +#ifdef DEBUG_DRAWING #include "debug_drawing.h" #endif +#endif +double CurTime() noexcept; +double CurQPC() noexcept; double GetFrameDelta() noexcept; -double GetTime() noexcept; +double GetQPCDelta() noexcept; Camera::SmoothCamera::SmoothCamera() noexcept : config(Config::GetCurrentConfig()) { cameraStates[static_cast(GameState::CameraState::ThirdPerson)] = @@ -48,6 +52,40 @@ const bool Camera::SmoothCamera::UpdateCameraPOVState(const PlayerCharacter* pla } #pragma region Camera state updates +// Check if the camera is near the player's head (for first person mods) +bool Camera::SmoothCamera::CameraNearHead(const PlayerCharacter* player, const CorrectedPlayerCamera* camere, float cutOff) { + // Grab the eye vector, if we can't find the head node the origin will be our fallback + NiPoint3 niOrigin, niNormal; + typedef void(__thiscall PlayerCharacter::* GetEyeVector)(NiPoint3& origin, NiPoint3& normal, bool factorCameraOffset) const; + (player->*reinterpret_cast(&PlayerCharacter::Unk_C2))(niOrigin, niNormal, false); + + BSFixedString name = "NPC Head [Head]"; + auto node = player->loadedState->node->GetObjectByName(&name.data); + if (node) { + niOrigin = node->m_worldTransform.pos; + } + + const auto dist = glm::distance( + glm::vec3{ + niOrigin.x, + niOrigin.y, + niOrigin.z + }, + gameLastActualPosition + ); + + return dist <= cutOff; +} + +// Immersive First Person patch +// Kind of in a bind here due to how IFPV patches the camera state transition, combined +// with the point during code execution when we run vs. they run - Just do a distance test +bool Camera::SmoothCamera::IFPV_InFirstPersonState(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) { + // IFPV also changes the near plane which we can check to reduce false positives + // This is pretty damn hackey but without a better way to detect this we don't have much choice + return (CameraNearHead(player, camera) && lastNearPlane != Config::GetGameConfig()->fNearDistance); +} + // Returns the current camera state for use in selecting an update method const GameState::CameraState Camera::SmoothCamera::GetCurrentCameraState(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) { GameState::CameraState newState = GameState::CameraState::Unknown; @@ -62,11 +100,28 @@ const GameState::CameraState Camera::SmoothCamera::GetCurrentCameraState(const P } newState = GameState::GetCameraState(player, camera); - if (newState == GameState::CameraState::Horseback && config->comaptIC_FirstPersonHorse) { + + const auto minZoom = Config::GetGameConfig()->fMinCurrentZoom; + + if (config->compatIFPV && (newState == GameState::CameraState::ThirdPerson || newState == GameState::CameraState::ThirdPersonCombat)) { + const auto tps = reinterpret_cast(camera->cameraState); + if (tps->cameraZoom == minZoom && tps->cameraLastZoom == minZoom) { + // IFPV + if (IFPV_InFirstPersonState(player, camera)) + newState = GameState::CameraState::FirstPerson; + } + } else if (config->compatIFPV && newState == GameState::CameraState::Horseback) { + // ditto + if (IFPV_InFirstPersonState(player, camera)) + newState = GameState::CameraState::FirstPerson; + } + + if (newState == GameState::CameraState::Horseback && config->comaptIC_FirstPersonHorse && !config->compatIFPV) { const auto tps = reinterpret_cast(camera->cameraState); if (tps) { - if ((tps->cameraZoom == -SKYRIM_MIN_ZOOM_FRACTION && tps->cameraLastZoom == -SKYRIM_MIN_ZOOM_FRACTION) || - currentActionState == CameraActionState::FirstPersonHorseback) + if ((tps->cameraZoom == minZoom && tps->cameraLastZoom == minZoom) || + currentActionState == CameraActionState::FirstPersonHorseback || + CameraNearHead(player, camera)) { if (povWasPressed) newState = GameState::CameraState::Horseback; @@ -81,7 +136,7 @@ const GameState::CameraState Camera::SmoothCamera::GetCurrentCameraState(const P } else if (newState == GameState::CameraState::Dragon && config->comaptIC_FirstPersonDragon) { const auto tps = reinterpret_cast(camera->cameraState); if (tps) { - if ((tps->cameraZoom == -SKYRIM_MIN_ZOOM_FRACTION && tps->cameraLastZoom == -SKYRIM_MIN_ZOOM_FRACTION) || + if ((tps->cameraZoom == minZoom && tps->cameraLastZoom == minZoom) || currentActionState == CameraActionState::FirstPersonDragon) { newState = GameState::CameraState::FirstPerson; @@ -173,28 +228,16 @@ void Camera::SmoothCamera::OnCameraStateTransition(const PlayerCharacter* player { switch (oldState) { case GameState::CameraState::ThirdPerson: { - if (cameraStates.at(static_cast(GameState::CameraState::ThirdPerson))) { - dynamic_cast( - cameraStates.at(static_cast(GameState::CameraState::ThirdPerson)).get() - )->OnEnd(player, camera); - break; - } + cameraStates.at(static_cast(GameState::CameraState::ThirdPerson))->OnEnd(player, camera); + break; } case GameState::CameraState::ThirdPersonCombat: { - if (cameraStates.at(static_cast(GameState::CameraState::ThirdPersonCombat))) { - dynamic_cast( - cameraStates.at(static_cast(GameState::CameraState::ThirdPersonCombat)).get() - )->OnEnd(player, camera); - break; - } + cameraStates.at(static_cast(GameState::CameraState::ThirdPersonCombat))->OnEnd(player, camera); + break; } case GameState::CameraState::Horseback: { - if (cameraStates.at(static_cast(GameState::CameraState::Horseback))) { - dynamic_cast( - cameraStates.at(static_cast(GameState::CameraState::Horseback)).get() - )->OnEnd(player, camera); - break; - } + cameraStates.at(static_cast(GameState::CameraState::Horseback))->OnEnd(player, camera); + break; } default: break; @@ -202,28 +245,16 @@ void Camera::SmoothCamera::OnCameraStateTransition(const PlayerCharacter* player switch (newState) { case GameState::CameraState::ThirdPerson: { - if (cameraStates.at(static_cast(GameState::CameraState::ThirdPerson))) { - dynamic_cast( - cameraStates.at(static_cast(GameState::CameraState::ThirdPerson)).get() - )->OnBegin(player, camera); - break; - } + cameraStates.at(static_cast(GameState::CameraState::ThirdPerson))->OnBegin(player, camera); + break; } case GameState::CameraState::ThirdPersonCombat: { - if (cameraStates.at(static_cast(GameState::CameraState::ThirdPersonCombat))) { - dynamic_cast( - cameraStates.at(static_cast(GameState::CameraState::ThirdPersonCombat)).get() - )->OnBegin(player, camera); - break; - } + cameraStates.at(static_cast(GameState::CameraState::ThirdPersonCombat))->OnBegin(player, camera); + break; } case GameState::CameraState::Horseback: { - if (cameraStates.at(static_cast(GameState::CameraState::Horseback))) { - dynamic_cast( - cameraStates.at(static_cast(GameState::CameraState::Horseback)).get() - )->OnBegin(player, camera); - break; - } + cameraStates.at(static_cast(GameState::CameraState::Horseback))->OnBegin(player, camera); + break; } default: break; @@ -493,11 +524,7 @@ void Camera::SmoothCamera::SetPosition(const glm::vec3& pos, const CorrectedPlay if (!mmath::IsValid(currentPosition)) { __debugbreak(); // Oops, go ahead and clear both - lastPosition = currentPosition = { - cameraNode->m_worldTransform.pos.x, - cameraNode->m_worldTransform.pos.y, - cameraNode->m_worldTransform.pos.z - }; + lastPosition = currentPosition = gameInitialWorldPosition; return; } #endif @@ -529,38 +556,46 @@ void Camera::SmoothCamera::UpdateInternalWorldToScreenMatrix(NiCamera* camera, f } // Returns the current smoothing scalar to use for the given distance to the player -float Camera::SmoothCamera::GetCurrentSmoothingScalar(const float distance, ScalarSelector method) const { +double Camera::SmoothCamera::GetCurrentSmoothingScalar(const float distance, ScalarSelector method) const { Config::ScalarMethods scalarMethod; + + // Work in FP64 here to eek out some more precision + // Avoid a divide-by-zero error by clamping to this lower bound + constexpr const double minZero = 0.000000000001; - float scalar = 1.0f; - float interpValue = 1.0f; - float remapped = 1.0f; + double scalar = 1.0; + double interpValue = 1.0; + double remapped = 1.0; if (method == ScalarSelector::SepZ) { - const auto max = config->separateZMaxSmoothingDistance; - scalar = glm::clamp(1.0f - ((max - distance) / max), 0.0f, 1.0f); - remapped = mmath::Remap(scalar, 0.0f, 1.0f, config->separateZMinFollowRate, config->separateZMaxFollowRate); + const auto max = static_cast(config->separateZMaxSmoothingDistance); + scalar = glm::clamp(glm::max(1.0 - (max - distance), minZero) / max, 0.0, 1.0); + remapped = mmath::Remap( + scalar, 0.0, 1.0, static_cast(config->separateZMinFollowRate), static_cast(config->separateZMaxFollowRate) + ); scalarMethod = config->separateZScalar; } else if (method == ScalarSelector::LocalSpace) { remapped = distance; scalarMethod = config->separateLocalScalar; } else { - const auto max = config->zoomMaxSmoothingDistance; - scalar = glm::clamp(1.0f - ((max - distance) / max), 0.0f, 1.0f); - remapped = mmath::Remap(scalar, 0.0f, 1.0f, config->minCameraFollowRate, config->maxCameraFollowRate); + const auto max = static_cast(config->zoomMaxSmoothingDistance); + scalar = glm::clamp(glm::max(1.0 - (max - distance), minZero) / max, 0.0, 1.0); + remapped = mmath::Remap( + scalar, 0.0, 1.0, static_cast(config->minCameraFollowRate), static_cast(config->maxCameraFollowRate) + ); scalarMethod = config->currentScalar; } if (!config->disableDeltaTime) { - const auto delta = GetFrameDelta(); - const auto fps = 1.0 / delta; - const auto mul = -fps * glm::log2(1.0f - remapped); - interpValue = static_cast(glm::clamp(1.0 - glm::exp2(-mul * delta), 0.0, 1.0)); + const double delta = glm::max(GetFrameDelta(), minZero); + const double fps = 1.0 / delta; + const double mul = -fps * glm::log2(1.0 - remapped); + interpValue = glm::clamp(1.0 - glm::exp2(-mul * delta), 0.0, 1.0); } else { interpValue = remapped; } - return mmath::RunScalarFunction(scalarMethod, interpValue); + return mmath::RunScalarFunction(scalarMethod, interpValue); } // Returns the user defined distance clamping vector pair @@ -647,9 +682,9 @@ void Camera::SmoothCamera::UpdateCrosshairPosition(PlayerCharacter* player, cons // @Note: I'm sure there is some way to make this perfect, but this is close enough float fac = 0.0f; - if (GameState::IsUsingCrossbow(*g_thePlayer)) { + if (GameState::IsUsingCrossbow(player)) { fac = glm::radians(Config::GetGameConfig()->f3PBoltTiltUpAngle) * 0.5f; - } else if (GameState::IsUsingBow(*g_thePlayer)) { + } else if (GameState::IsUsingBow(player)) { fac = glm::radians(Config::GetGameConfig()->f3PArrowTiltUpAngle) * 0.5f; } @@ -679,7 +714,7 @@ void Camera::SmoothCamera::UpdateCrosshairPosition(PlayerCharacter* player, cons constexpr auto rayLength = 6000.0f; // Range of most (all?) arrows auto origin = glm::vec4(niOrigin.x, niOrigin.y, niOrigin.z, 0.0f); auto ray = glm::vec4(niNormal.x, niNormal.y, niNormal.z, 0.0f) * rayLength; - const auto result = Raycast::CastRay(origin, origin + ray, 0.1f, true); + const auto result = Raycast::hkpCastRay(origin, origin + ray); auto port = NiRect(); auto menu = MenuManager::GetSingleton()->GetMenu(&UIStringHolder::GetSingleton()->hudMenu); @@ -720,7 +755,7 @@ void Camera::SmoothCamera::UpdateCrosshairPosition(PlayerCharacter* player, cons }; } -#ifdef _DEBUG +#ifdef DEBUG_DRAWING auto lineStart = mmath::PointToScreen(origin); auto lineEnd = mmath::PointToScreen(result.hit ? result.hitPos : origin + ray); DebugDrawing::Submit(DebugDrawing::DrawLine(lineStart, lineEnd, { 0.0f, 1.0f, 0.0f })); @@ -805,33 +840,56 @@ void Camera::SmoothCamera::SetCrosshairEnabled(bool enabled) const { #pragma endregion #pragma region Camera getters +void Camera::SmoothCamera::UpdateInternalRotation(CorrectedPlayerCamera* camera) noexcept { + const auto tps = reinterpret_cast(camera->cameraState); + if (!tps) return; + tps->UpdateRotation(); + currentQuat = glm::quat{ tps->rotation.m_fW, tps->rotation.m_fX, tps->rotation.m_fY, tps->rotation.m_fZ }; + + const auto pitch = glm::pitch(currentQuat); + const auto yaw = glm::roll(currentQuat); + currentRotation.x = pitch *-1; + currentRotation.y = yaw *-1; +} + // Returns the camera's pitch float Camera::SmoothCamera::GetCameraPitchRotation(const CorrectedPlayerCamera* camera) const noexcept { - const auto mat = camera->cameraNode->m_localTransform.rot; - const auto a = glm::clamp(-mat.data[2][1], -1.0f, 1.0f); - return glm::asin(a); + return currentRotation.x; } // Returns the camera's yaw float Camera::SmoothCamera::GetCameraYawRotation(const CorrectedPlayerCamera* camera) const noexcept { - const auto mtx = reinterpret_cast(camera->cameraNode->m_children.m_data[0])->m_worldTransform.rot; - if (mtx.data[0][0] <= 0.0000001f || mtx.data[2][2] <= 0.0000001f) { - auto ab = glm::atan(mtx.data[0][2], mtx.data[1][2]); - return ab - mmath::half_pi; - } - - return glm::atan(mtx.data[0][0], mtx.data[1][0]); - + return currentRotation.y; } // Returns the camera's current zoom level - Camera must extend ThirdPersonState float Camera::SmoothCamera::GetCameraZoomScalar(const CorrectedPlayerCamera* camera, uint16_t cameraState) const noexcept { const auto state = reinterpret_cast(camera->cameraStates[cameraState]); if (!state) return 0.0f; - return state->cameraZoom + SKYRIM_MIN_ZOOM_FRACTION; + return state->cameraZoom + (Config::GetGameConfig()->fMinCurrentZoom *-1); } #pragma endregion +// Use this method to snatch modifications done by mods that run after us +// Called before the internal game method runs which will overwrite most of that +void Camera::SmoothCamera::PreGameUpdate(PlayerCharacter* player, CorrectedPlayerCamera* camera) { + // Store the last actual position the game used for rendering + auto cameraNi = reinterpret_cast( + camera->cameraNode->m_children.m_size == 0 ? + nullptr : + camera->cameraNode->m_children.m_data[0] + ); + if (cameraNi) + gameLastActualPosition = { + cameraNi->m_worldTransform.pos.x, + cameraNi->m_worldTransform.pos.y, + cameraNi->m_worldTransform.pos.z + }; + + // Grab the last near plane value too, for IFPV compat checks + lastNearPlane = cameraNi->m_frustum.m_fNear; +} + // Selects the correct update method and positions the camera void Camera::SmoothCamera::UpdateCamera(PlayerCharacter* player, CorrectedPlayerCamera* camera) { #ifdef _DEBUG @@ -845,21 +903,23 @@ void Camera::SmoothCamera::UpdateCamera(PlayerCharacter* player, CorrectedPlayer auto cameraNode = camera->cameraNode; config = Config::GetCurrentConfig(); + gameInitialWorldPosition = { + cameraNode->m_worldTransform.pos.x, + cameraNode->m_worldTransform.pos.y, + cameraNode->m_worldTransform.pos.z + }; + // Update states & effects const auto pov = UpdateCameraPOVState(player, camera); const auto state = GetCurrentCameraState(player, camera); const auto actionState = GetCurrentCameraActionState(player, camera); offsetState.currentGroup = GetOffsetForState(actionState); const auto currentOffset = GetCurrentCameraOffset(player, camera); - const auto curTime = GetTime(); + const auto curTime = CurTime(); // Perform a bit of setup to smooth out camera loading if (!firstFrame) { - lastPosition = lastWorldPosition = currentPosition = { - cameraNode->m_worldTransform.pos.x, - cameraNode->m_worldTransform.pos.y, - cameraNode->m_worldTransform.pos.z - }; + lastPosition = lastWorldPosition = currentPosition = gameInitialWorldPosition; firstFrame = true; } @@ -891,41 +951,28 @@ void Camera::SmoothCamera::UpdateCamera(PlayerCharacter* player, CorrectedPlayer zoomTransitionState.currentPosition, offsetTransitionState.currentPosition.y }; - + // Save the camera position lastPosition = currentPosition; if (config->disableDuringDialog && dialogMenuOpen) { - lastPosition = lastWorldPosition = currentPosition = { - cameraNode->m_worldTransform.pos.x, - cameraNode->m_worldTransform.pos.y, - cameraNode->m_worldTransform.pos.z - }; + lastPosition = lastWorldPosition = currentPosition = gameInitialWorldPosition; } else { switch (state) { case GameState::CameraState::ThirdPerson: { - if (cameraStates.at(static_cast(GameState::CameraState::ThirdPerson))) { - dynamic_cast( - cameraStates.at(static_cast(GameState::CameraState::ThirdPerson)).get() - )->Update(player, camera); - break; - } + UpdateInternalRotation(camera); + cameraStates.at(static_cast(GameState::CameraState::ThirdPerson))->Update(player, camera); + break; } case GameState::CameraState::ThirdPersonCombat: { - if (cameraStates.at(static_cast(GameState::CameraState::ThirdPersonCombat))) { - dynamic_cast( - cameraStates.at(static_cast(GameState::CameraState::ThirdPersonCombat)).get() - )->Update(player, camera); - break; - } + UpdateInternalRotation(camera); + cameraStates.at(static_cast(GameState::CameraState::ThirdPersonCombat))->Update(player, camera); + break; } case GameState::CameraState::Horseback: { - if (cameraStates.at(static_cast(GameState::CameraState::Horseback))) { - dynamic_cast( - cameraStates.at(static_cast(GameState::CameraState::Horseback)).get() - )->Update(player, camera); - break; - } + UpdateInternalRotation(camera); + cameraStates.at(static_cast(GameState::CameraState::Horseback))->Update(player, camera); + break; } // Here just for my own reference that these are unused (for now) @@ -941,15 +988,12 @@ void Camera::SmoothCamera::UpdateCamera(PlayerCharacter* player, CorrectedPlayer case GameState::CameraState::Bleedout: case GameState::CameraState::Dragon: case GameState::CameraState::Unknown: - default: { + default: + { SetCrosshairEnabled(true); CenterCrosshair(); SetCrosshairSize({ baseCrosshairData.xScale, baseCrosshairData.yScale }); - lastPosition = lastWorldPosition = currentPosition = { - cameraNode->m_worldTransform.pos.x, - cameraNode->m_worldTransform.pos.y, - cameraNode->m_worldTransform.pos.z - }; + lastPosition = lastWorldPosition = currentPosition = gameInitialWorldPosition; break; } } diff --git a/SmoothCam/source/camera_state.cpp b/SmoothCam/source/camera_state.cpp index 2f695fc..0f453de 100644 --- a/SmoothCam/source/camera_state.cpp +++ b/SmoothCam/source/camera_state.cpp @@ -1,5 +1,6 @@ #include "camera_state.h" #include "camera.h" +#include "raycast.h" Camera::State::BaseCameraState::BaseCameraState(Camera::SmoothCamera* camera) noexcept : camera(camera) {} @@ -194,13 +195,13 @@ glm::vec3 Camera::State::BaseCameraState::UpdateInterpolatedLocalPosition(Player return rot; } - const auto pos = mmath::Interpolate( + const auto pos = mmath::Interpolate( camera->lastLocalPosition, rot, camera->GetCurrentSmoothingScalar( camera->config->localScalarRate, ScalarSelector::LocalSpace ) ); - StoreLastLocalPosition(pos); + StoreLastLocalPosition(static_cast(pos)); return pos; } @@ -215,21 +216,21 @@ glm::vec3 Camera::State::BaseCameraState::UpdateInterpolatedWorldPosition(Player } if (GetConfig()->separateZInterp) { - const auto xy = mmath::Interpolate( + const auto xy = mmath::Interpolate( camera->lastWorldPosition, pos, camera->GetCurrentSmoothingScalar(distance) ); - const auto z = mmath::Interpolate( + const auto z = mmath::Interpolate( camera->lastWorldPosition, pos, camera->GetCurrentSmoothingScalar(distance, ScalarSelector::SepZ) ); return { xy.x, xy.y, z.z }; } else { - const auto ret = mmath::Interpolate( + const auto ret = mmath::Interpolate( camera->lastWorldPosition, pos, camera->GetCurrentSmoothingScalar(distance) ); - return ret; + return static_cast(ret); } } diff --git a/SmoothCam/source/config.cpp b/SmoothCam/source/config.cpp index 5952320..61e5251 100644 --- a/SmoothCam/source/config.cpp +++ b/SmoothCam/source/config.cpp @@ -75,6 +75,7 @@ void Config::to_json(json& j, const UserConfig& obj) { CREATE_JSON_VALUE(obj, comaptIC_FirstPersonHorse), CREATE_JSON_VALUE(obj, comaptIC_FirstPersonDragon), CREATE_JSON_VALUE(obj, compatIC_FirstPersonSitting), + CREATE_JSON_VALUE(obj, compatIFPV), CREATE_JSON_VALUE(obj, minCameraFollowDistance), CREATE_JSON_VALUE(obj, minCameraFollowRate), CREATE_JSON_VALUE(obj, maxCameraFollowRate), @@ -134,6 +135,7 @@ void Config::from_json(const json& j, UserConfig& obj) { VALUE_FROM_JSON(obj, comaptIC_FirstPersonHorse) VALUE_FROM_JSON(obj, comaptIC_FirstPersonDragon) VALUE_FROM_JSON(obj, compatIC_FirstPersonSitting) + VALUE_FROM_JSON(obj, compatIFPV) VALUE_FROM_JSON(obj, minCameraFollowDistance) VALUE_FROM_JSON(obj, minCameraFollowRate) VALUE_FROM_JSON(obj, maxCameraFollowRate) @@ -221,6 +223,16 @@ void Config::ReadConfigFile() { wchar_t* end; gameConfig.f3PBoltTiltUpAngle = std::wcstof(buf, &end); } + + if (GetPrivateProfileString(L"Display", L"fNearDistance", L"15.0", buf, 16, inipath.c_str()) != 0) { + wchar_t* end; + gameConfig.fNearDistance = std::wcstof(buf, &end); + } + + if (GetPrivateProfileString(L"Camera", L"fMinCurrentZoom", L"-0.200000003", buf, 16, inipath.c_str()) != 0) { + wchar_t* end; + gameConfig.fMinCurrentZoom = std::wcstof(buf, &end); + } } currentConfig = cfg; @@ -236,6 +248,11 @@ Config::UserConfig* Config::GetCurrentConfig() noexcept { return ¤tConfig; } +void Config::ResetConfig() { + currentConfig = {}; + SaveCurrentConfig(); +} + BSFixedString Config::SaveConfigAsPreset(int slot, const BSFixedString& name) { if (slot >= MaxPresetSlots) { return { "ERROR: Preset index out of range" }; diff --git a/SmoothCam/source/detours.cpp b/SmoothCam/source/detours.cpp index f51d604..8b0a4fb 100644 --- a/SmoothCam/source/detours.cpp +++ b/SmoothCam/source/detours.cpp @@ -2,13 +2,24 @@ #include "camera.h" #include "arrow_fixes.h" +#include + static PLH::VFuncMap origVFuncs_PlayerInput; static PLH::VFuncMap origVFuncs_MenuOpenClose; std::weak_ptr g_theCamera; + +static ITimer timer; double curFrame = 0.0; double lastFrame = 0.0; -double GetTime() noexcept { +double curQPC = 0.0; +double lastQPC = 0.0; + +static double GetTime() noexcept { + return timer.GetElapsedTime(); +} + +static double GetQPC() noexcept { LARGE_INTEGER f, i; if (QueryPerformanceCounter(&i) && QueryPerformanceFrequency(&f)) { auto frequency = 1.0 / static_cast(f.QuadPart); @@ -21,23 +32,40 @@ void StepFrameTime() noexcept { lastFrame = curFrame; curFrame = GetTime(); -#ifdef _DEBUG + lastQPC = curQPC; + curQPC = GetQPC(); + +#ifdef DEBUG_DRAWING ArrowFixes::Draw(); #endif } +double CurTime() noexcept { + return curFrame; +} + +double CurQPC() noexcept { + return curQPC; +} + double GetFrameDelta() noexcept { - return static_cast(curFrame - lastFrame); + return curFrame - lastFrame; +} + +double GetQPCDelta() noexcept { + return curQPC - lastQPC; } #define CAMERA_UPDATE_DETOUR_IMPL(name) \ static PLH::VFuncMap origVFuncs_##name##; \ void __fastcall mCameraStateUpdate##name##(TESCameraState* pThis, void* unk) { \ - reinterpret_cast(origVFuncs_##name##.at(3))(pThis, unk); \ StepFrameTime(); \ + std::shared_ptr lockedPtr; \ auto player = *g_thePlayer; \ auto camera = CorrectedPlayerCamera::GetSingleton(); \ - std::shared_ptr lockedPtr; \ + if ((camera && player); lockedPtr = g_theCamera.lock(), lockedPtr != nullptr) \ + lockedPtr->PreGameUpdate(player, camera); \ + reinterpret_cast(origVFuncs_##name##.at(3))(pThis, unk); \ if ((camera && player); lockedPtr = g_theCamera.lock(), lockedPtr != nullptr) \ lockedPtr->UpdateCamera(player, camera); \ } @@ -108,6 +136,7 @@ EventResult __fastcall mMenuOpenCloseHandler(uintptr_t pThis, MenuOpenCloseEvent bool Detours::Attach(std::shared_ptr theCamera) { g_theCamera = theCamera; + timer.Start(); { PLH::VFuncSwapHook playerInputHooks( diff --git a/SmoothCam/source/main.cpp b/SmoothCam/source/main.cpp index 1c4069b..468d369 100644 --- a/SmoothCam/source/main.cpp +++ b/SmoothCam/source/main.cpp @@ -24,7 +24,7 @@ void SKSEMessageHandler(SKSEMessagingInterface::Message* message) { } break; } -#ifdef _DEBUG +#ifdef DEBUG_DRAWING case SKSEMessagingInterface::kMessage_InputLoaded: { DebugDrawing::DetourD3D11(); } @@ -57,7 +57,7 @@ extern "C" { info->infoVersion = PluginInfo::kInfoVersion; info->name = "SmoothCam"; - info->version = 9; + info->version = 10; g_pluginHandle = skse->GetPluginHandle(); @@ -66,7 +66,7 @@ extern "C" { } if (skse->runtimeVersion != RUNTIME_VERSION_1_5_97) { - _WARNING("This module was compiled for skse 1.5.97, you are running an unsupported verion. You may experience crashes or other strange issues."); + _WARNING("This module was compiled for skse 1.5.97, you are running an unsupported version. You may experience crashes or other strange issues."); } return true; diff --git a/SmoothCam/source/mmath.cpp b/SmoothCam/source/mmath.cpp index c495d2f..ef0128e 100644 --- a/SmoothCam/source/mmath.cpp +++ b/SmoothCam/source/mmath.cpp @@ -123,105 +123,6 @@ void mmath::DecomposeToBasis(const glm::vec3& point, const glm::vec3& rotation, }; } -mmath::AABB mmath::GetReferAABB(TESObjectREFR* ref) { - const auto mins = (ref->*reinterpret_cast(&TESObjectREFR::Unk_73))(); - const auto maxs = (ref->*reinterpret_cast(&TESObjectREFR::Unk_74))(); - - auto center = (mins + maxs) * 0.5f; - auto extent = (maxs - mins) * 0.5f; - auto mmins = center - extent; - auto mmaxs = center + extent; - - return { - {mmins.x, mmins.y, mmins.z}, - {mmaxs.x, mmaxs.y, mmaxs.z} - }; -} - -NiNode* FindByName(NiNode* root, const char* name) { - if (!root->m_children.m_data) return nullptr; - - for (auto i = 0; i < root->m_children.m_size; i++) { - NiNode* node = DYNAMIC_CAST(root->m_children.m_data[i], NiAVObject, NiNode); - if (!node || !node->m_name) continue; - if (strcmp(node->m_name, name) == 0) return node; - node = FindByName(node, name); - if (node) return node; - } - - return nullptr; -} - -mmath::AABB mmath::GetActorAABB(Actor* actor) { - // @Note: currently having issues with proper AABB rotation - // Additionally, some non-human actors need to be handled differently - if (!actor->loadedState || !actor->loadedState->node) return {}; - auto parent = actor->loadedState->node->m_parent; - - auto comName = "NPC Root [Root]"; - auto root = FindByName(actor->loadedState->node, comName); - if (!root) return {}; - - auto common = root->m_children.m_data[0]; - if (!common) return {}; - - const auto aabb = GetReferAABB(actor); - const auto rot = common->m_worldTransform.rot; - return mmath::RotateAABB(aabb, rot) + actor->pos; -} - -mmath::AABB mmath::RotateAABB(const AABB& aabb, const NiMatrix33& mat) noexcept { - auto blf = mat * NiPoint3{ aabb.mins.x, aabb.mins.y, aabb.mins.z }; - auto brf = mat * NiPoint3{ aabb.maxs.x, aabb.mins.y, aabb.mins.z }; - auto blb = mat * NiPoint3{ aabb.mins.x, aabb.maxs.y, aabb.mins.z }; - auto brb = mat * NiPoint3{ aabb.maxs.x, aabb.maxs.y, aabb.mins.z }; - auto tlf = mat * NiPoint3{ aabb.mins.x, aabb.mins.y, aabb.maxs.z }; - auto trf = mat * NiPoint3{ aabb.maxs.x, aabb.mins.y, aabb.maxs.z }; - auto tlb = mat * NiPoint3{ aabb.mins.x, aabb.maxs.y, aabb.maxs.z }; - auto trb = mat * NiPoint3{ aabb.maxs.x, aabb.maxs.y, aabb.maxs.z }; - - return { - { - std::min({blf.x, brf.x, blb.x, brb.x, tlf.x, trf.x, tlb.x, trb.x}), - std::min({blf.y, brf.y, blb.y, brb.y, tlf.y, trf.y, tlb.y, trb.y}), - std::min({blf.z, brf.z, blb.z, brb.z, tlf.z, trf.z, tlb.z, trb.z}) - }, - { - std::max({blf.x, brf.x, blb.x, brb.x, tlf.x, trf.x, tlb.x, trb.x}), - std::max({blf.y, brf.y, blb.y, brb.y, tlf.y, trf.y, tlb.y, trb.y}), - std::max({blf.z, brf.z, blb.z, brb.z, tlf.z, trf.z, tlb.z, trb.z}) - } - }; -} - -bool mmath::IntersectRayAABB(const glm::vec3& start, const glm::vec3& dir, const AABB& aabb, - glm::vec3& hitPos) noexcept -{ - float lo = std::numeric_limits::min(); - float hi = std::numeric_limits::max(); - - for (auto i = 0; i < 3; i++) { - auto dimLo = (aabb.mins[i] - start[i]) / dir[i]; - auto dimHi = (aabb.maxs[i] - start[i]) / dir[i]; - if (dimLo > dimHi) std::swap(dimLo, dimHi); - - if (dimHi < lo || dimLo > hi) - return false; - - if (dimLo > lo) lo = dimLo; - if (dimHi < hi) hi = dimHi; - } - - if (lo > hi) return false; - hitPos = { - start.x + dir.x * lo, - start.y + dir.y * lo, - start.z + dir.z * lo - }; - - return true; -} - glm::vec2 mmath::PointToScreen(const glm::vec3& point) { auto port = NiRect(); port.m_left = -1.0f; diff --git a/SmoothCam/source/papyrus.cpp b/SmoothCam/source/papyrus.cpp index 9818ce2..6146fc3 100644 --- a/SmoothCam/source/papyrus.cpp +++ b/SmoothCam/source/papyrus.cpp @@ -60,6 +60,7 @@ const std::unordered_map> boolGetter IMPL_GETTER("FirstPersonHorse", comaptIC_FirstPersonHorse) IMPL_GETTER("FirstPersonDragon", comaptIC_FirstPersonDragon) IMPL_GETTER("FirstPersonSitting", compatIC_FirstPersonSitting) + IMPL_GETTER("IFPVCompat", compatIFPV) IMPL_GETTER("InterpolationEnabled", enableInterp) IMPL_GETTER("SeparateLocalInterpolation", separateLocalInterp) IMPL_GETTER("DisableDeltaTime", disableDeltaTime) @@ -247,6 +248,7 @@ const std::unordered_map> boolSetter IMPL_SETTER("FirstPersonHorse", comaptIC_FirstPersonHorse, bool) IMPL_SETTER("FirstPersonDragon", comaptIC_FirstPersonDragon, bool) IMPL_SETTER("FirstPersonSitting", compatIC_FirstPersonSitting, bool) + IMPL_SETTER("IFPVCompat", compatIFPV, bool) IMPL_SETTER("InterpolationEnabled", enableInterp, bool) IMPL_SETTER("SeparateLocalInterpolation", separateLocalInterp, bool) IMPL_SETTER("DisableDeltaTime", disableDeltaTime, bool) @@ -581,4 +583,15 @@ void PapyrusBindings::Bind(VMClassRegistry* registry) { registry ) ); + + registry->RegisterFunction( + new NativeFunction0( + "SmoothCam_ResetConfig", + ScriptClassName, + [](StaticFunctionTag* thisInput) { + Config::ResetConfig(); + }, + registry + ) + ); } \ No newline at end of file diff --git a/SmoothCam/source/raycast.cpp b/SmoothCam/source/raycast.cpp index 31245a1..97417c4 100644 --- a/SmoothCam/source/raycast.cpp +++ b/SmoothCam/source/raycast.cpp @@ -1,187 +1,14 @@ -#include +#include "raycast.h" namespace { typedef bool(__fastcall* RayCastFunType)( UnkPhysicsHolder* physics, bhkWorld* world, glm::vec4& rayStart, glm::vec4& rayEnd, uint32_t* rayResultInfo, Character** hitCharacter, float traceHullSize ); - - constexpr auto ThreadActorIntersectionAtCount = 64; -} - -#ifdef _DEBUG -void DrawAABB(const mmath::AABB& aabb, bool hit) { - const auto hitColor = glm::vec3{ 0.0f, 1.0f, 0.0f }; - const auto missColor = glm::vec3{ 1.0f, 0.0f, 0.0f }; - const auto color = hit ? hitColor : missColor; - const auto blf = mmath::PointToScreen(aabb.mins); - const auto brf = mmath::PointToScreen({ aabb.maxs.x, aabb.mins.y, aabb.mins.z }); - const auto blb = mmath::PointToScreen({ aabb.mins.x, aabb.maxs.y, aabb.mins.z }); - const auto brb = mmath::PointToScreen({ aabb.maxs.x, aabb.maxs.y, aabb.mins.z }); - const auto tlf = mmath::PointToScreen({ aabb.mins.x, aabb.mins.y, aabb.maxs.z }); - const auto trf = mmath::PointToScreen({ aabb.maxs.x, aabb.mins.y, aabb.maxs.z }); - const auto tlb = mmath::PointToScreen({ aabb.mins.x, aabb.maxs.y, aabb.maxs.z }); - const auto trb = mmath::PointToScreen(aabb.maxs); - DebugDrawing::Submit(DebugDrawing::DrawBox(DebugDrawing::DrawBox::BoxPoints({ - blf, brf, blb, brb, - tlf, trf, tlb, trb - }), color)); -} -#endif - -Raycast::AIProcessManager* Raycast::AIProcessManager::GetSingleton() { - static auto ofs = Offsets::Get(514167); - return *ofs; -} - -// Perform a ray-AABB intersection test with all actors in the given list, return the hits -uint8_t Raycast::IntersectRayAABBAllActorsIn(const tArray& list, ActorRayResults& results, - const glm::vec3& start, const glm::vec3& dir, float distance, uint8_t currentCount) -{ - uint8_t hitCount = 0; -#ifdef _DEBUG - Profiler prof; -#endif - - // Lazy effort to multithread the tests when dealing with a large amount of actors - if (list.count >= ThreadActorIntersectionAtCount) { - std::atomic_uint counter = currentCount; - - std::for_each( - std::execution::par_unseq, - list.entries, - list.entries + list.count, - [&counter, &results, &start, &dir, &distance](UInt32 refID) { - if (counter >= Raycast::MaxActorIntersections) return; - - NiPointer ref; - (*LookupREFRByHandle)(refID, ref); - if (!ref) return; - auto actor = DYNAMIC_CAST(ref, TESObjectREFR, Actor); - if (!actor) return; - - const auto bits = std::bitset<32>(actor->flags1); - constexpr const auto setOnDeath = 23; - if (bits[setOnDeath]) return; - - actor->IncRef(); - { - const auto pos = glm::vec3(actor->pos.x, actor->pos.y, actor->pos.z); - if (glm::length2(static_cast(start) - pos) <= distance) { - const auto aabb = mmath::GetActorAABB(actor); - glm::vec3 hit; - if (mmath::IntersectRayAABB(start, dir, aabb, hit)) { - Raycast::RayResult res; - res.hit = true; - res.hitPos = glm::vec4(hit, 0.0f); - res.rayLength = glm::length(hit - start); - res.hitCharacter = reinterpret_cast(actor); - const auto old = counter.fetch_add(1, std::memory_order::memory_order_relaxed); - if (old < ThreadActorIntersectionAtCount) - results[old] = res; -#ifdef _DEBUG - DrawAABB(aabb, true); -#endif - } -#ifdef _DEBUG - else - DrawAABB(aabb, false); -#endif - } - } - actor->DecRef(); - } - ); - - hitCount = counter; - } else { - uint32_t counter = currentCount; - - std::for_each( - std::execution::seq, - list.entries, - list.entries + list.count, - [&counter, &results, &start, &dir, &distance](UInt32 refID) { - if (counter >= Raycast::MaxActorIntersections) return; - - NiPointer ref; - (*LookupREFRByHandle)(refID, ref); - if (!ref) return; - auto actor = DYNAMIC_CAST(ref, TESObjectREFR, Actor); - if (!actor) return; - - const auto bits = std::bitset<32>(actor->flags1); - constexpr const auto setOnDeath = 23; - if (bits[setOnDeath]) return; - - actor->IncRef(); - { - const auto pos = glm::vec3(actor->pos.x, actor->pos.y, actor->pos.z); - if (glm::length2(static_cast(start) - pos) <= distance) { - const auto aabb = mmath::GetActorAABB(actor); - glm::vec3 hit; - if (mmath::IntersectRayAABB(start, dir, aabb, hit)) { - Raycast::RayResult res; - res.hit = true; - res.hitPos = glm::vec4(hit, 0.0f); - res.rayLength = glm::length(hit - start); - res.hitCharacter = reinterpret_cast(actor); - results[counter] = res; - counter++; -#ifdef _DEBUG - DrawAABB(aabb, true); -#endif - } -#ifdef _DEBUG - else - DrawAABB(aabb, false); -#endif - } - } - actor->DecRef(); - } - ); - - hitCount = counter; - } - -#ifdef _DEBUG - const auto snap = prof.Snap(); - int bp = 0; -#endif - - return hitCount; -} - -Raycast::RayResult Raycast::InteresctRayAABBAllActors(const glm::vec3& start, const glm::vec3& end) { - const auto aiMgr = AIProcessManager::GetSingleton(); - const auto rayDistance = glm::length2(start - static_cast(end)); - const auto dir = glm::normalize(static_cast(end) - start); - std::array hitActors; - - // Intersect with all actors in the high process - // @Note: With debug drawing on, I can see actors right next to the player that aren't in this list - // until they start a new AI activity - for now this will have to do - const auto hitCount = IntersectRayAABBAllActorsIn(aiMgr->actorsHigh, hitActors, start, dir, rayDistance); - - Raycast::RayResult closestHit; - for (auto i = 0; i < hitCount; i++) { - auto hit = hitActors[i]; - if (closestHit.hit) { - if (closestHit.rayLength > hit.rayLength) - closestHit = hit; - } else { - closestHit = hit; - } - } - - return closestHit; } Raycast::RayResult Raycast::CastRay(glm::vec4 start, glm::vec4 end, float traceHullSize, bool intersectCharacters) { RayResult res; - RayResult actorRes; - #ifdef _DEBUG // A saftey net for my own bad code - if the engine EVER gets a nan or inf float // shit WILL hit the fan in the audio & rendering systems (but mostly the audio system) @@ -197,11 +24,6 @@ Raycast::RayResult Raycast::CastRay(glm::vec4 start, glm::vec4 end, float traceH ply->handleRefObject.IncRef(); { - // Homebrew intersection with actors - // This is pretty lame, need to find an engine method for proper ray-actor testing - if (intersectCharacters) - actorRes = InteresctRayAABBAllActors(start, end); - auto physicsWorld = Physics::GetWorld(ply->parentCell); if (physicsWorld) { res.hit = Offsets::Get(32270)( // 0x4f45f0 @@ -218,11 +40,92 @@ Raycast::RayResult Raycast::CastRay(glm::vec4 start, glm::vec4 end, float traceH res.rayLength = glm::length(static_cast(res.hitPos) - static_cast(start)); } - // Check if we hit an actor and if the hit is closer to us - if (intersectCharacters && actorRes.hit && res.hit) { - if (actorRes.rayLength < res.rayLength) - return actorRes; // Hit an actor + return res; +} + +hkpCastCollector* getCastCollector() { + static hkpCastCollector collector = hkpCastCollector(); + return &collector; +} + +Raycast::RayResult Raycast::hkpCastRay(glm::vec4 start, glm::vec4 end) { +#ifdef _DEBUG + if (!mmath::IsValid(start) || !mmath::IsValid(end)) { + __debugbreak(); + return {}; } +#endif - return res; + constexpr auto hkpScale = 0.0142875f; + const auto dif = end - start; + + hkpRayCastInfo info; + info.start = start * hkpScale; + info.end = dif * hkpScale; + info.collector = getCastCollector(); + info.collector->reset(); + + auto ply = *g_thePlayer; + ply->handleRefObject.IncRef(); + { + auto physicsWorld = Physics::GetWorld(ply->parentCell); + if (physicsWorld) { + physicsWorld->CastRay(&info); + } + } + ply->handleRefObject.DecRef(); + + hkpRayHitResult best = {}; + best.hitFraction = 1.0f; + glm::vec4 bestPos = {}; + + for (auto& hit : info.collector->results) { + const auto pos = (dif * hit.hitFraction) + start; + if (best.hit == nullptr) { + best = hit; + bestPos = pos; + continue; + } + + if (hit.hitFraction < best.hitFraction) { + best = hit; + bestPos = pos; + } + } + + RayResult result; + result.hitPos = bestPos; + result.rayLength = glm::length(bestPos - start); + + // FUN_1404f45f0 <32270> + // 140dad060:GetAVObjectFromHavok <76160> + // 1402945e0:ExtractCharacterFromTraceRes <19323> + + /* + MOV EDI,dword ptr [TheCamera->unk120 + 0x2c] + AND EDI,0x7f + CALL 140dad060:GetAVObjectFromHavok <76160> + + lVar1 = (**(code **)(*(longlong *)param_1 + 0x28))(param_1); + mov rax, rcx + ret + */ + + if (!best.hit) return result; + typedef NiAVObject*(__fastcall* _GetUserData)(bhkShapeList*); + auto av = Offsets::Get<_GetUserData>(76160)(best.hit); + result.hit = av != nullptr; + + // What a useless function, only returning a valid character if it hits the actor origin? + /* + typedef Character*(__fastcall* ExtractCharacterFromTraceRes)(NiAVObject*); + if (result.hit) { + auto character = Offsets::Get(19323)(av); + if (character && *reinterpret_cast(reinterpret_cast(character) + 0x1a) == '>') { + result.hitCharacter = character; + } + } + */ + + return result; } \ No newline at end of file