Improve first-time Looks menu face framing

Replace ineffective third-person zoom with FOV narrowing (40°) to magnify the character's face for easier facial-feature editing. Center the player on the fixed LooksMenu camera's optical axis for the first few polls to prevent off-center framing caused by the camera not being perfectly aimed at the player. Capture and restore FOV and player transforms on menu open/close. Add diagnostic logging to track camera position, player position, and menu camera behavior.
This commit is contained in:
2026-06-28 21:31:15 +12:00
parent 2f37feb5b7
commit 37d6afbd5a
4 changed files with 239 additions and 2 deletions
+168 -2
View File
@@ -32,6 +32,15 @@ namespace
constexpr int kLooksMenuCameraNudgeMaxPolls = 20;
constexpr float kPi = 3.14159265358979323846F;
constexpr float kLooksMenuFaceCameraZoomOffset = -90.0F;
// LooksMenu runs its own fixed camera, so ThirdPersonState zoom is ignored. A narrow
// world FOV is used instead to magnify the face for easier facial-feature editing.
constexpr float kLooksMenuFaceCameraFOV = 40.0F;
constexpr float kDefaultWorldFOV = 80.0F;
// LooksMenu camera is fixed and not aimed at the player, so the zoom pushes the face
// off-centre. Move the player onto the camera's optical axis for the first few polls to
// centre them, then stop so the menu's own model rotation/controls work normally.
constexpr int kLooksMenuCenterMaxPolls = 6;
constexpr float kLooksMenuCenterFallbackDistance = 150.0F;
struct ThirdPersonCameraSnapshot
{
@@ -72,6 +81,15 @@ namespace
bool g_sawSpecialMenuOpen = false;
bool g_looksMenuFallbackIssued = false;
bool g_looksMenuCameraContextApplied = false;
bool g_looksMenuFovApplied = false;
float g_preLooksWorldFov = 0.0F;
float g_preLooksFirstPersonFov = 0.0F;
bool g_looksMenuCameraCaptured = false;
RE::NiPoint3 g_looksMenuCameraPos{};
float g_looksMenuCameraYaw = 0.0F;
int g_looksMenuCenterPolls = 0;
std::optional<RE::NiPoint3> g_preLooksPlayerPos;
std::optional<RE::NiPoint3> g_preLooksPlayerAngle;
std::optional<ThirdPersonCameraSnapshot> g_preLooksCameraSnapshot;
int g_spawnPollFrameCounter = 0;
int g_spawnWaitAttempts = 0;
@@ -407,6 +425,131 @@ namespace
LogInfo("Prepared camera context before Looks menu.");
}
void ApplyLooksMenuFaceFOV()
{
auto* camera = RE::PlayerCamera::GetSingleton();
if (!camera) {
LogWarning("PlayerCamera unavailable while applying face-edit FOV.");
return;
}
if (!g_looksMenuFovApplied) {
g_preLooksWorldFov = camera->worldFOV > 1.0F ? camera->worldFOV : kDefaultWorldFOV;
g_preLooksFirstPersonFov = camera->firstPersonFOV > 1.0F ? camera->firstPersonFOV : kDefaultWorldFOV;
g_looksMenuFovApplied = true;
LogInfo(std::format(
"Captured FOV before face-edit zoom (world={:.1f}, firstPerson={:.1f}).",
g_preLooksWorldFov,
g_preLooksFirstPersonFov));
}
// Console 'fov <value>' sets both first-person and world FOV; a narrow value
// magnifies the LooksMenu face view because the menu camera distance is fixed.
RE::Console::ExecuteCommand(std::format("fov {:.0f}", kLooksMenuFaceCameraFOV).c_str());
}
void RestoreLooksMenuFaceFOV()
{
if (!g_looksMenuFovApplied) {
return;
}
const float firstPersonFov = g_preLooksFirstPersonFov > 1.0F ? g_preLooksFirstPersonFov : kDefaultWorldFOV;
const float worldFov = g_preLooksWorldFov > 1.0F ? g_preLooksWorldFov : kDefaultWorldFOV;
// 'fov <first-person> <world>' restores both planes to their pre-LooksMenu values.
RE::Console::ExecuteCommand(std::format("fov {:.0f} {:.0f}", firstPersonFov, worldFov).c_str());
g_looksMenuFovApplied = false;
g_preLooksWorldFov = 0.0F;
g_preLooksFirstPersonFov = 0.0F;
LogInfo("Restored FOV after face-edit zoom.");
}
void CenterPlayerForLooksMenu()
{
auto* camera = RE::PlayerCamera::GetSingleton();
auto* player = RE::PlayerCharacter::GetSingleton();
if (!camera || !player) {
return;
}
const RE::NiPoint3 playerPos = player->GetPosition();
if (!g_preLooksPlayerPos) {
g_preLooksPlayerPos = playerPos;
g_preLooksPlayerAngle = RE::NiPoint3{ player->data.angle };
LogInfo("Captured player transform before LooksMenu centering.");
}
// Read the camera each poll so we can detect (via logs) whether the LooksMenu camera
// is fixed in world or follows the player after we move them.
RE::NiPoint3 freshCameraPos{};
const bool gotCamera = camera->GetCameraPosition(freshCameraPos, true);
const float freshYaw = camera->heading;
if (!g_looksMenuCameraCaptured) {
g_looksMenuCameraPos = freshCameraPos;
g_looksMenuCameraYaw = freshYaw;
g_looksMenuCameraCaptured = true;
}
const float dx = playerPos.x - g_looksMenuCameraPos.x;
const float dy = playerPos.y - g_looksMenuCameraPos.y;
float distance = std::sqrt(dx * dx + dy * dy);
if (distance < 1.0F) {
distance = kLooksMenuCenterFallbackDistance;
}
// Place the player on the camera's forward axis (centres them horizontally) at the
// same distance (preserves the current zoom), facing back toward the camera.
const RE::NiPoint3 centeredPos{
g_looksMenuCameraPos.x + distance * std::sin(g_looksMenuCameraYaw),
g_looksMenuCameraPos.y + distance * std::cos(g_looksMenuCameraYaw),
playerPos.z
};
player->SetPosition(centeredPos, true);
player->SetAngleOnReference(RE::NiPoint3{ 0.0F, 0.0F, NormalizeRadians(g_looksMenuCameraYaw + kPi) });
player->Update3DPosition(true);
const RE::NiPoint3 afterPos = player->GetPosition();
LogInfo(std::format(
"Center poll {}: gotCam={} capCam=({:.1f},{:.1f},{:.1f}) freshCam=({:.1f},{:.1f},{:.1f}) yaw={:.3f}/{:.3f} dist={:.1f} before=({:.1f},{:.1f},{:.1f}) target=({:.1f},{:.1f},{:.1f}) after=({:.1f},{:.1f},{:.1f}).",
g_looksMenuCenterPolls,
gotCamera,
g_looksMenuCameraPos.x, g_looksMenuCameraPos.y, g_looksMenuCameraPos.z,
freshCameraPos.x, freshCameraPos.y, freshCameraPos.z,
g_looksMenuCameraYaw, freshYaw,
distance,
playerPos.x, playerPos.y, playerPos.z,
centeredPos.x, centeredPos.y, centeredPos.z,
afterPos.x, afterPos.y, afterPos.z));
}
void RestorePlayerAfterLooksMenu()
{
if (!g_preLooksPlayerPos) {
g_looksMenuCameraCaptured = false;
g_looksMenuCenterPolls = 0;
return;
}
auto* player = RE::PlayerCharacter::GetSingleton();
if (player) {
player->SetPosition(*g_preLooksPlayerPos, true);
if (g_preLooksPlayerAngle) {
player->SetAngleOnReference(*g_preLooksPlayerAngle);
}
player->Update3DPosition(true);
LogInfo("Restored player transform after LooksMenu centering.");
}
g_preLooksPlayerPos.reset();
g_preLooksPlayerAngle.reset();
g_looksMenuCameraCaptured = false;
g_looksMenuCenterPolls = 0;
}
void ApplyLooksMenuCameraContext()
{
if (g_looksMenuCameraContextApplied) {
@@ -417,20 +560,23 @@ namespace
CapturePreLooksCameraState();
ForcePlayerThirdPersonCamera();
RE::Console::ExecuteCommand("ForceThirdPerson");
ApplyLooksMenuFaceFOV();
g_looksMenuCameraContextApplied = true;
g_looksMenuCameraNudgePolls = 0;
LogInfo("Applied chargen camera context after Looks menu open.");
LogInfo("Applied close face-edit camera context (narrow FOV) after Looks menu open.");
}
void RestoreCharGenCameraContext()
{
if (!g_looksMenuCameraContextApplied && !g_preLooksCameraSnapshot) {
if (!g_looksMenuCameraContextApplied && !g_looksMenuFovApplied && !g_preLooksCameraSnapshot && !g_preLooksPlayerPos) {
return;
}
if (g_looksMenuCameraContextApplied) {
RE::Console::ExecuteCommand("SetInChargen 0");
}
RestoreLooksMenuFaceFOV();
RestorePlayerAfterLooksMenu();
RestorePreLooksCameraState();
g_looksMenuCameraContextApplied = false;
g_looksMenuCameraNudgePolls = 0;
@@ -449,6 +595,13 @@ namespace
g_sawSpecialMenuOpen = false;
g_looksMenuFallbackIssued = false;
g_looksMenuCameraContextApplied = false;
g_looksMenuFovApplied = false;
g_preLooksWorldFov = 0.0F;
g_preLooksFirstPersonFov = 0.0F;
g_looksMenuCameraCaptured = false;
g_looksMenuCenterPolls = 0;
g_preLooksPlayerPos.reset();
g_preLooksPlayerAngle.reset();
g_preLooksCameraSnapshot.reset();
g_spawnPollFrameCounter = 0;
g_spawnWaitAttempts = 0;
@@ -469,6 +622,13 @@ namespace
g_sawSpecialMenuOpen = false;
g_looksMenuFallbackIssued = false;
g_looksMenuCameraContextApplied = false;
g_looksMenuFovApplied = false;
g_preLooksWorldFov = 0.0F;
g_preLooksFirstPersonFov = 0.0F;
g_looksMenuCameraCaptured = false;
g_looksMenuCenterPolls = 0;
g_preLooksPlayerPos.reset();
g_preLooksPlayerAngle.reset();
g_preLooksCameraSnapshot.reset();
g_spawnPollFrameCounter = 0;
g_spawnWaitAttempts = 0;
@@ -669,6 +829,7 @@ namespace
g_looksMenuFallbackIssued = false;
g_looksMenuOpenPolls = 0;
g_looksMenuCameraNudgePolls = 0;
g_looksMenuCenterPolls = 0;
OpenLooksMenuPrimary();
g_charGenPhase = CharGenPhase::WaitingForLooksMenuClose;
return;
@@ -684,6 +845,11 @@ namespace
++g_looksMenuCameraNudgePolls;
ForcePlayerThirdPersonCamera();
RE::Console::ExecuteCommand("ForceThirdPerson");
ApplyLooksMenuFaceFOV();
}
if (g_looksMenuCenterPolls < kLooksMenuCenterMaxPolls) {
++g_looksMenuCenterPolls;
CenterPlayerForLooksMenu();
}
return;
}