---
title: "[Devlog] Kingdom Come: Deliverance II - Customizing the View: TPV
  Offsets, Input, and What's Under the Hood"
description: "Following up on the TPV toggle: a technical deep dive into
  implementing custom camera offsets in Kingdom Come: Deliverance II."
author: Aleks (Quang Trinh Khac, online as tkhquang)
published: 2025-05-07
updated: 2026-08-28
category: Technical
tags:
  - CryEngine
  - "Kingdom Come: Deliverance II"
  - Modding
  - Devlog
  - Reverse Engineering
  - C++
  - Camera
series: Third Person Camera
series_part: 2
series_total: 3
cover: https://tkhquang.dev/uploads/images/tpv-toggle-offset.jpg
canonical: https://tkhquang.dev/blog/posts/devlog-kingdom-come-deliverance-ii-customizing-the-view-tpv-offsets-input-and-whats-under-the-hood
---

# [Devlog] Kingdom Come: Deliverance II - Customizing the View: TPV Offsets, Input, and What's Under the Hood

I generate this file from the same source as the page at <https://tkhquang.dev/blog/posts/devlog-kingdom-come-deliverance-ii-customizing-the-view-tpv-offsets-input-and-whats-under-the-hood>, so it carries the prose, the code, the tables, and the diagrams as Mermaid source. What has to run in a browser is named where it stands, with the page's address beside it. Cite the canonical URL with the title and the dates above; the rest of the citation guidance is in <https://tkhquang.dev/llms.txt>.

---

Alright folks, our exploration of modding **Kingdom Come: Deliverance II** (KCD2) continues. In a <a href="https://tkhquang.dev/blog/posts/devlog-kingdom-come-deliverance-ii-finding-the-third-person-view-toggle-flag" target="_blank" rel="noopener noreferrer">previous devlog</a>, we navigated the memory landscape of `WHGame.DLL` to unearth and activate the game's built-in third-person view (TPV). While functional, that TPV was fairly basic and, as many noted, likely a debug feature with its share of quirks.

The community feedback was clear: greater control over the TPV was desired. Common requests included adjustable camera sensitivity, vertical pitch limits, and, crucially, the ability to offset the camera for a true over-the-shoulder feel. This post delves into how I tackled these enhancements, with a particular focus on the 3D mathematics, Vectors, Quaternions, and Matrices, that underpin the custom camera offset feature.

If you're looking for the mod itself, it's available here:
*   **NexusMods:** [Third Person View (TPV Camera) Enabler](https://www.nexusmods.com/kingdomcomedeliverance2/mods/1550)

And for the code that powers these features:
*   **GitHub:** [KCD2Tools](https://github.com/tkhquang/KCD2Tools)

## Beyond the Basic Toggle: The Need for Deeper Hooks

!["...it would be amazing to have the camera slightly off-center to the right."](https://tkhquang.dev/uploads/images/tpv-toggle-feedback-offset.png)

Simply flipping the game's TPV flag gives us a third-person perspective, but it doesn't allow us to change *where* that camera sits relative to Henry or how it responds to mouse movements. To achieve that, we need to intercept the game's own camera update and input processing logic. This involves finding specific functions within `WHGame.DLL` using AOB (Array of Bytes) scanning and then detouring them using a hooking library like MinHook.

For this post, we'll zoom in on the camera offset feature. The core challenge is: how do we take user-defined X, Y, and Z offset values and apply them correctly to the game's existing third-person camera so it *feels* right, regardless of where Henry or the camera is looking? This is where 3D math comes into play.

## The Core of Custom Offsets: Hooking the Camera `::Update()`

The game's `wh::game::C_CameraThirdPerson` object (which we identified in the previous RE effort) has an internal update function that is responsible for calculating the TPV camera's position and orientation every frame. Let's call this `TpvCameraUpdateFunc`. My goal was to let the game calculate its default TPV, then take that result and modify the camera's *position* before the game uses it for rendering.

```cpp title="tpv_camera_hook.cpp - Detour_TpvCameraUpdate (Simplified & Explained)"
// Typedef for the original function
typedef void(__fastcall *TpvCameraUpdateFunc)(uintptr_t thisPtr_TPVObject, uintptr_t outputPosePtr);
// Our trampoline to the original
static TpvCameraUpdateFunc fpTpvCameraUpdateOriginal = nullptr;

// The detour that gets called instead of the game's function
void __fastcall Detour_TpvCameraUpdate(uintptr_t thisPtr_TPVObject, uintptr_t outputPosePtr) {
    // 1. Always call the original game function FIRST.
    // This is crucial. We want the game to do its normal TPV calculations
    // (like basic distance, collision avoidance if any, character tracking).
    // The result (position and rotation) is written into the structure
    // pointed to by 'outputPosePtr'.
    if (fpTpvCameraUpdateOriginal) {
        fpTpvCameraUpdateOriginal(thisPtr_TPVObject, outputPosePtr);
    } else {
        // Safety: if the hook isn't set up right, don't do anything.
        return;
    }

    // Only apply our logic if TPV is actually active (via our toggle flag)
    if (getViewState() != 1) {
        return;
    }

    // outputPosePtr points to a game structure. Through reversing, we've
    // determined its layout. Let's assume:
    // - Position (Vector3: x,y,z) starts at outputPosePtr + Constants::TPV_OUTPUT_POSE_POSITION_OFFSET (e.g., 0x0)
    // - Rotation (Quaternion: x,y,z,w) starts at outputPosePtr + Constants::TPV_OUTPUT_POSE_ROTATION_OFFSET (e.g., 0x0C)

    // Get modifiable pointers to the game's calculated position and rotation
    Vector3* gamePositionPtr = reinterpret_cast<Vector3*>(outputPosePtr + Constants::TPV_OUTPUT_POSE_POSITION_OFFSET);
    Quaternion* gameRotationPtr = reinterpret_cast<Quaternion*>(outputPosePtr + Constants::TPV_OUTPUT_POSE_ROTATION_OFFSET);

    // Read the current camera state calculated by the game
     // World-space position
    Vector3 gameCalculatedPosition = *gamePositionPtr;
    // World-space orientation
    Quaternion cameraWorldRotation = *gameRotationPtr;

    // Get the desired *local* offset values (e.g., X=0.5, Y=-1.5, Z=0.1)
    // These are read from the mod's config, an active camera profile, or a current transition.
    Vector3 localOffset = GetActiveOffset(); // My helper function

    // If there's no offset to apply (all zeros), we're done.
    if (localOffset.x == 0.0f && localOffset.y == 0.0f && localOffset.z == 0.0f) {
        return;
    }

    // THIS IS THE CORE MATH: Transform the local offset into a world-space offset.
    // A 'localOffset' of (0.5, 0, 0) means "0.5 units to the camera's own right."
    // To apply this in the world, we need to rotate this local vector by the
    // camera's current world rotation.
    Vector3 worldOffset = cameraWorldRotation.Rotate(localOffset);
    // Our Quaternion struct has a Rotate method:
    // Vector3 Quaternion::Rotate(const Vector3& v) const {
    //     DirectX::XMVECTOR q = this->ToXMVector();         // Convert our Quat to XMVECTOR
    //     DirectX::XMVECTOR vec = v.ToXMVector();           // Convert input Vector3 to XMVECTOR
    //     DirectX::XMVECTOR rotatedVec = DirectX::XMVector3Rotate(vec, q); // DirectX does the math!
    //     return Vector3::FromXMVector(rotatedVec);         // Convert result back to our Vector3
    // }

    // Now, add the calculated world-space offset to the game's original TPV position.
    Vector3 finalPosition = gameCalculatedPosition + worldOffset;

    // Finally, write our modified position back into the game's output structure.
    *gamePositionPtr = finalPosition;
    // The camera's rotation/orientation (cameraWorldRotation) is left untouched by this logic;
    // we're only changing its position to achieve the offset.
}
```

This detour is established using an AOB scan for `Constants::TPV_CAMERA_UPDATE_AOB_PATTERN` to find the original `TpvCameraUpdateFunc` within `WHGame.DLL`.

```mermaid
---
config:
  flowchart:
    nodeSpacing: 30
---
graph TD
    subgraph "Camera Position Customization Flow"
        ModControl["Mod (e.g., Profile<br/>Change Hotkey or<br/>Live Adjustment Hotkey)"] --> ProfileMgr["CameraProfileManager<br/>(Manages<br/>g_currentCameraOffset,<br/>saved profiles, transitions)"];

        GameCall["Game Calls Original<br/>C_CameraThirdPerson::Update()"] --> Hook_Pos["HOOK: Detour_TpvCameraUpdate<br/>(tpv_camera_hook.cpp)"];
        Hook_Pos --> CallOrig_Pos["Call Original<br/>TpvCameraUpdateFunc()<br/>Game calculates default<br/>TPV Pose (Pos & Rot)"];
        CallOrig_Pos --> ReadGamePose["Mod Reads Game's<br/>TPV Position & Rotation"];

        ProfileMgr --> GetLocalOffset["Mod Gets Active Local Offset<br/>(from g_currentCameraOffset)"];

        subgraph "Offset Transformation"
           ReadGamePose -- "Camera World<br/>Rotation" --> TransformOffset;
           GetLocalOffset -- Local XYZ Offset --> TransformOffset;
           TransformOffset["Transform Local Offset<br/>to World Offset<br/>(worldOffset = cameraRotation<br/>.Rotate(localOffset))"];
        end

        TransformOffset --> ApplyWorldOffset["Apply World Offset<br/>to Game's Position<br/>(finalPos =<br/>gamePos + worldOffset)"];
        ApplyWorldOffset --> WriteBackPose["Mod Writes Modified<br/>Position Back to Game"];
        WriteBackPose --> Renderer["Game Renderer Uses<br/>Modified Position"];
    end

    classDef default fill:#282a36,stroke:#f8f8f2,stroke-width:2px,color:#f8f8f2;
    class ModControl,ProfileMgr,GameCall,Hook_Pos,CallOrig_Pos,ReadGamePose,GetLocalOffset,TransformOffset,ApplyWorldOffset,WriteBackPose,Renderer default;
```


## Deconstructing the 3D Math: Vectors, Quaternions, and Transforms

Related: <a href="https://tkhquang.dev/blog/posts/cryengine-eye-of-the-engine-the-camera" target="_blank" rel="noopener noreferrer">[CryEngine] Eye of the Engine: The Camera</a>

To truly grasp how the offset works, let's touch on the 3D math components involved:

1.  **`Vector3` (Our Simple Struct in `math_utils.h`)**:
    *   Represents a point or direction in 3D space with `x, y, z` components.
    *   We use it for:
        *   `gameCalculatedPosition`: The camera's position in world space as determined by KCD2.
        *   `localOffset`: The desired shift relative to the camera's own orientation (e.g., "0.5 units to my right, 1.5 units behind me, 0.1 units above me").
        *   `worldOffset`: The `localOffset` after being transformed into a world-space direction.
        *   `finalPosition`: The `gameCalculatedPosition + worldOffset`.
    *   It has basic vector operations like addition (`+`), subtraction (`-`), and conversion to/from `DirectX::XMVECTOR` for using DirectXMath library functions.

2.  **`Quaternion` (Our Struct in `math_utils.h`)**:
    *   Represents a rotation in 3D space with `x, y, z, w` components. Quaternions are fantastic for avoiding gimbal lock and for smooth interpolation of rotations (SLERP), which is used by the `TransitionManager` for camera profiles.
    *   `cameraWorldRotation`: This is the key. It's the orientation of the camera in world space *as calculated by the game*. We read this directly from the game's output pose structure.
    *   **`Quaternion::Rotate(const Vector3& v)` method:** This is where the magic happens for the offset.
        *   It takes a `Vector3` (our `localOffset`).
        *   Converts both the quaternion itself and the input vector to `DirectX::XMVECTOR`.
        *   Uses `DirectX::XMVector3Rotate(vectorToRotate, rotationQuaternion)`: This DirectXMath function applies the quaternion's rotation to the vector. If `localOffset` is `(0.5, 0, 0)` (local right) and `cameraWorldRotation` is facing North, `XMVector3Rotate` will return a vector pointing East in world coordinates.
        *   Converts the resulting `XMVECTOR` back to our `Vector3` struct. This is our `worldOffset`.

3.  **No Explicit Matrix Math in *This Specific* Detour, but It's Implied:**
    *   While my detour `Detour_TpvCameraUpdate` primarily uses `Vector3` and `Quaternion` logic for the offset, the underlying game structures (like the one `outputPosePtr` points to, or player/entity transforms such as `GameStructures::Matrix34f m_worldTransform;`) use 3x4 matrices (`Matrix34f`) to store full transformations (rotation and translation).
    *   A `Matrix34f` typically stores:
        *   The first 3x3 part is the rotation matrix, holding the object's X, Y, and Z axes in world space *in its columns* (Right = col0, Forward = col1, Up = col2).
        *   The last column (or often, specific elements like `m[0][3], m[1][3], m[2][3]`) is the translation (position) vector.
    *   Our `cameraWorldRotation` (Quaternion) is the rotational component of the camera's world transform. Together with `gameCalculatedPosition`, it places the camera in the world. The view matrix does the reverse: it transforms world points into camera space by applying the inverse camera transform. For a pure rotation matrix, that inverse is its transpose.
    *   My `Quaternion::LookRotation` is an example of building a quaternion from direction vectors, similar to how a view matrix is constructed. It uses `DirectX::XMMatrixLookToRH` (creates a view matrix) then inverts it to get a world orientation matrix, and finally extracts the quaternion from that. This shows the close relationship.

![Matrix34f Structure (CryEngine: row-major storage, with the axis basis in the columns)](https://tkhquang.dev/uploads/images/blog/tpv-camera-matrix34f.svg)

By letting the game compute `gameCalculatedPosition` and `cameraWorldRotation` first, we respect its core TPV logic (like its default distance from Henry or any rudimentary collision handling it might do). We then just nudge that final position by applying our `localOffset` transformed into the correct `worldOffset` using the camera's own orientation.

### FOV and distance

Moving the camera farther back changes its position in the world, and with it the perspective between the character and the background. Increasing the field of view widens the image from the same position. Both can make the character smaller on screen, but only moving the camera changes that perspective. Try changing one at a time below.

The rig below separates those two controls. In **The lens**, leave the subject still and change field of view without touching boom length. Then reset and change the boom length instead. Watch the character against the distant columns: widening the frame and moving the viewpoint produce different pictures.

> An interactive figure stands here: a camera on a spring arm, a subject, and one wall in the way. It needs a browser to run, so drive it on the page: <https://tkhquang.dev/blog/posts/devlog-kingdom-come-deliverance-ii-customizing-the-view-tpv-offsets-input-and-whats-under-the-hood>

## Refining TPV Input: Sensitivity and Pitch Limits

**`tpv_input_hook.cpp`**

The default TPV mouse input in KCD2 felt overly sensitive, particularly vertically. Players also (rightly) didn't want to accidentally flip the camera upside down. This required hooking a different function – one that processes mouse input specifically when the TPV camera controller is active.

I found this function via AOB (`Constants::TPV_INPUT_PROCESS_AOB_PATTERN`) and detoured it with `Detour_TpvCameraInput`.
*   **Input Event Structure (`GameStructures::InputEvent`):** KCD2 passes input information in a structure that includes an `eventId` (e.g., `0x10A` for TPV Yaw, `0x10B` for TPV Pitch) and a `deltaValue`.
*   **Sensitivity:** For yaw and pitch, my detour simply multiplies the incoming `event->deltaValue` by a user-configurable sensitivity factor (`g_config.tpv_yaw_sensitivity`, `g_config.tpv_pitch_sensitivity`) from the INI file.

    ```cpp
    // Example for yaw
    event->deltaValue *= g_config.tpv_yaw_sensitivity;
    ```

*   **Pitch Clamping:** For vertical (pitch) movement, I maintain a running total of the applied pitch (`g_currentPitch` in degrees). After applying sensitivity, the new proposed pitch is clamped between `g_config.tpv_pitch_min` and `g_config.tpv_pitch_max` (also from INI). The `event->deltaValue` is then adjusted to reflect only the movement allowed within these limits.

    ```cpp
    // Simplified pitch clamping
    float currentPitchDeg = g_currentPitch.load();
    float proposedPitchDeg = currentPitchDeg + (event->deltaValue * sensitivity);
    float clampedPitchDeg = std::clamp(proposedPitchDeg, g_config.tpv_pitch_min, g_config.tpv_pitch_max);
    event->deltaValue = clampedPitchDeg - currentPitchDeg; // Only apply the actual change
    g_currentPitch.store(clampedPitchDeg);
    ```

*   **Menu Input Blocking:** Crucially, `Detour_TpvCameraInput` *returns early* without calling the original game input function if my UI hooks (`ui_menu_hooks.cpp`) detect that a full-screen game menu (inventory, map, etc.) is active. This stops the TPV camera from erratically moving around while the player is interacting with UI, a common annoyance.

```mermaid
---
config:
  flowchart:
    nodeSpacing: 30
---
graph TD
    subgraph "TPV Mouse Input Customization Flow"
        GameMouseInput["Game's TPV Mouse Event Occurs<br/>(with original deltaValue)"] --> Hook_Input["HOOK: Detour_TpvCameraInput<br/>(tpv_input_hook.cpp)"];
        Hook_Input --> CheckMenu{"Is Game Menu Active?"};
        CheckMenu -- Yes --> ReturnEarly["Return<br/>(Block Input Processing)"];
        CheckMenu -- No --> ProcessInput["Process Mouse Event<br/>(Yaw/Pitch)"];
        ProcessInput --> ApplySensitivity["Apply Sensitivity Multiplier<br/>to deltaValue (from config:<br/>tpv_yaw_sensitivity,<br/>tpv_pitch_sensitivity)"];
        ApplySensitivity --> CheckPitchLimits{"Is Pitch Event AND<br/>Limits Enabled?"};
        CheckPitchLimits -- Yes --> ClampPitch["Calculate New Pitch<br/>Clamp Accumulated Pitch<br/>(g_currentPitch)<br/>Adjust deltaValue<br/>to Respect Limits"];
        CheckPitchLimits -- No --> CallOrig_Input;
        ClampPitch --> CallOrig_Input["Call Original<br/>TpvCameraInputFunc()<br/>(with potentially<br/>modified deltaValue)"];
        CallOrig_Input --> GameCameraLogic["Game Camera Responds<br/>to (Modified) Input"];
    end
    classDef default fill:#282a36,stroke:#f8f8f2,stroke-width:2px,color:#f8f8f2;
    class GameMouseInput,Hook_Input,CheckMenu,ReturnEarly,ProcessInput,ApplySensitivity,CheckPitchLimits,ClampPitch,CallOrig_Input,GameCameraLogic default;
```

## Other Quality-of-Life: Scroll Wheel and Overlay Management

**`event_hooks.cpp`**

To prevent the scroll wheel from zooming the TPV camera while also scrolling lists in an open UI menu, I also hook a more general game event handler (`EventHandlerDetour` via `Constants::EVENT_HANDLER_AOB_PATTERN`). If it detects a scroll wheel event (`Constants::MOUSE_WHEEL_EVENT_ID`) *and* my UI hooks indicate an overlay (like a menu) is active, it zeroes out the scroll `deltaValue` in the event structure *before* passing it to the original game function. This effectively "eats" the scroll input for camera zoom purposes when a UI overlay needs it.


```mermaid
---
title: UI Overlay & Scroll Wheel Management
---
graph TD
    GameEvents["Game Events"] --> EventRouter{Mod Hooks};
    EventRouter -- "Menu Opens/Closes" --> Hook_UI["Hook UI_Overlay_Show/Hide"];
    EventRouter -- "Mouse Wheel<br/>Scroll" --> Hook_Event["Hook General Event Handler"];

    subgraph "UI Overlay Logic (ui_overlay_hooks.cpp)"
        Hook_UI --> TrackOverlayState["Update g_isOverlayActive<br/>(atomic bool)"];
        TrackOverlayState --> SwitchFPV["If Overlay Opens:<br/>Request FPV<br/>via Main Thread"];
        TrackOverlayState --> RestoreView["If Overlay Closes:<br/>Request Previous<br/>View Restoration"];
    end

    RestoreView ~~~ Hook_Event;

    subgraph "Scroll Wheel Filtering (event_hooks.cpp)"
        Hook_Event --> CheckScrollCondition{"Overlay Active OR<br/>Not Holding<br/>Scroll Key?"};
        CheckScrollCondition -- Yes --> ZeroDelta["Zero Out Scroll<br/>Event's deltaValue"];
        CheckScrollCondition -- No --> PassThruScroll["Pass Scroll<br/>Event Through"];
        ZeroDelta --> OriginalEventHandler["Call Original Event Handler"];
        PassThruScroll --> OriginalEventHandler;
    end

    classDef default fill:#282a36,stroke:#f8f8f2,stroke-width:2px,color:#f8f8f2;
    class GameEvents,EventRouter,Hook_UI,TrackOverlayState,SwitchFPV,RestoreView,Hook_Event,CheckScrollCondition,ZeroDelta,PassThruScroll,OriginalEventHandler default;
```

## Challenges and the Nature of This TPV

This multi-faceted approach of toggling a native flag and then layering hooks for position and input gives a good amount of control. However, the core TPV is still the game's own, likely debug, camera.
*   **Raycast/Aiming Misalignment:** The most significant limitation of simple offset TPV is that the game's aiming and interaction raycasts still originate from the *original* FPV or default TPV camera position. This means your crosshair in an offset view won't accurately represent what Henry is aiming at or can interact with. Fixing this properly is a much deeper challenge, often requiring changes to how the game itself performs these raycasts.
*   **No Custom Animation Blending:** Henry's animations are designed for FPV. In TPV, especially with custom offsets, you might see some awkwardness as the player model isn't fully adapted for being viewed from these new angles constantly.

For a "perfect" TPV, one would need to write a completely new camera controller, handle animation states differently for TPV (like in dedicated third-person games), and integrate custom aiming/interaction systems. That's a monumental task. For now, enhancing the game's existing (if hidden) TPV offers a good balance of features versus development effort.

This ongoing RE work for KCD2, identifying target functions, AOB scanning, carefully detouring, and understanding game structures through disassembly and runtime analysis, is the heart of creating these kinds of mods. It's a continuous learning process, especially as game patches can (and do!) change memory layouts and function signatures.

---

The process of chipping away at an engine's internals to enable new functionality or refine existing behavior is always a rewarding puzzle. The camera is such a fundamental part of our game experience, and being able to tailor it makes a big difference.

If you're curious about the C++ code, feel free to browse the repository:
[KCD2Tools on GitHub](https://github.com/tkhquang/KCD2Tools)

KCD2 Modding: Because even a medieval peasant deserves good camera angles. CryEngine, ***CryMore***!
