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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user