Maintain runtime proxy frustum visibility
Add periodic nudging to restore proxy frustum visibility when a proxy is near the local player to prevent proxies from remaining invisible after a frustum cull. Introduce kRuntimeProxyVisibilityNudgeInterval and kRuntimeProxyForceVisibilityRange, track lastObservedProxyVisible on ProxyActorSlot, and add MaintainRuntimeProxyFrustumVisibility and NudgeProxyFrustumVisibilityIfNearLocalPlayer helpers (with throttled logging and animation re-apply when visibility is regained). Switch several readiness checks from ProxyMeshReadyForPresentation to ProxyHasLoaded3D, and stop gating puppet updates on IsVisible so puppets continue to update while the mesh is loaded even if frustum-cleared. Also reset lastObservedProxyVisible when clearing a slot.
This commit is contained in:
@@ -125,6 +125,8 @@ namespace
|
||||
constexpr auto kWarningLogInterval = 5s;
|
||||
constexpr auto kProxyMovementDiagnosticLogInterval = 2s;
|
||||
constexpr auto kRuntimeProxyVisibilityDiagnosticLogInterval = 2s;
|
||||
constexpr auto kRuntimeProxyVisibilityNudgeInterval = 250ms;
|
||||
constexpr float kRuntimeProxyForceVisibilityRange = 8192.0F;
|
||||
constexpr auto kProxyNeutralizationInterval = 2s;
|
||||
constexpr bool kEnableProxyAIMovementIntentSuppression = false;
|
||||
bool g_preserveProxyAIForAnimationTesting = true;
|
||||
@@ -310,6 +312,7 @@ namespace
|
||||
std::chrono::steady_clock::time_point proxy3DRecoverySettleStartTime{};
|
||||
bool headMeshRefreshAttempted{ false };
|
||||
bool headMeshPendingStable{ false };
|
||||
std::optional<bool> lastObservedProxyVisible;
|
||||
bool skipSmoothFrameOnce{ false };
|
||||
std::optional<std::uint32_t> cachedProxyFormId;
|
||||
RE::NiPointer<RE::Actor> cachedProxyActor;
|
||||
@@ -2060,6 +2063,64 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
void NudgeProxyFrustumVisibilityIfNearLocalPlayer(RE::Actor& a_proxy, const RE::PlayerCharacter& a_player)
|
||||
{
|
||||
const auto distance = GetDistance(a_proxy.GetPosition(), a_player.GetPosition());
|
||||
if (distance > kRuntimeProxyForceVisibilityRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only restore the frustum function bit; occlusion/faded stay engine-owned.
|
||||
constexpr auto frustrum = std::to_underlying(RE::ACTOR_VISIBILITY_MASK::kFrustrum);
|
||||
a_proxy.visFlags = static_cast<std::uint32_t>(a_proxy.visFlags | frustrum);
|
||||
}
|
||||
|
||||
// Fallout clears Actor::visFlags (especially kFrustrum) when a ref leaves the camera
|
||||
// frustum. We only refresh on spawn today, so once visibilityRefreshPending clears the
|
||||
// proxy stops getting Update3DPosition nudges and stays invisible until the camera moves.
|
||||
void MaintainRuntimeProxyFrustumVisibility(
|
||||
ProxyActorSlot& a_slot,
|
||||
RE::Actor& a_proxy,
|
||||
const RE::PlayerCharacter& a_player)
|
||||
{
|
||||
if (!ProxyHasLoaded3D(a_proxy)) {
|
||||
a_slot.lastObservedProxyVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const bool isVisible = a_proxy.IsVisible();
|
||||
const bool wasVisible = a_slot.lastObservedProxyVisible.value_or(false);
|
||||
|
||||
if (!isVisible) {
|
||||
if (ShouldLog(
|
||||
"runtime_proxy_visibility_nudge_" + std::to_string(a_slot.remotePlayerId),
|
||||
kRuntimeProxyVisibilityNudgeInterval)) {
|
||||
RefreshRuntimeProxyVisibility(a_proxy);
|
||||
NudgeProxyFrustumVisibilityIfNearLocalPlayer(a_proxy, a_player);
|
||||
}
|
||||
a_slot.visibilityRefreshPending = true;
|
||||
} else {
|
||||
if (a_slot.visibilityRefreshPending) {
|
||||
a_slot.visibilityRefreshPending = false;
|
||||
a_slot.visibilityRefreshLogged = false;
|
||||
}
|
||||
if (a_slot.lastObservedProxyVisible.has_value() && !wasVisible) {
|
||||
a_slot.restoredForCurrentState = false;
|
||||
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
|
||||
a_slot.hasLoggedInitialAnimationState = false;
|
||||
LogThrottledInfo(
|
||||
"runtime_proxy_visibility_regained_" + std::to_string(a_slot.remotePlayerId),
|
||||
std::format(
|
||||
"Runtime proxy actor {:08X} for remote player {} regained visibility after frustum cull; forcing animation re-apply.",
|
||||
a_proxy.GetFormID(),
|
||||
a_slot.remotePlayerId),
|
||||
kRuntimeProxyVisibilityDiagnosticLogInterval);
|
||||
}
|
||||
}
|
||||
|
||||
a_slot.lastObservedProxyVisible = isVisible;
|
||||
}
|
||||
|
||||
void RecoverRuntimeProxyAfter3DReload(
|
||||
ProxyActorSlot& a_slot,
|
||||
RE::Actor& a_proxy,
|
||||
@@ -5408,6 +5469,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
a_slot.lastObservedRemoteCellId.reset();
|
||||
a_slot.headMeshRefreshAttempted = false;
|
||||
a_slot.headMeshPendingStable = false;
|
||||
a_slot.lastObservedProxyVisible.reset();
|
||||
}
|
||||
|
||||
void ResetSlotVisualSneakTracking(ProxyActorSlot& a_slot)
|
||||
@@ -6176,6 +6238,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
EnsureRuntimeProxyVisibility(a_slot, *proxy, "active-refresh");
|
||||
}
|
||||
|
||||
MaintainRuntimeProxyFrustumVisibility(a_slot, *proxy, a_player);
|
||||
MaintainRuntimeProxy3DState(a_slot, *proxy, a_remotePlayer, a_player, a_sameCellPassed);
|
||||
|
||||
if (a_slot.proxyPending3DRecovery) {
|
||||
@@ -6191,13 +6254,6 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
*a_slot.lastObservedRemoteCellId != a_remotePlayer.cellId;
|
||||
a_slot.lastObservedRemoteCellId = a_remotePlayer.cellId;
|
||||
|
||||
if (!ProxyMeshReadyForPresentation(*proxy)) {
|
||||
if (remoteCellChanged || ShouldSnapRemoteMovement(a_remotePlayer.movementType)) {
|
||||
a_slot.restoredForCurrentState = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ProxyHeadNodesMissing(*proxy)) {
|
||||
if (!a_slot.headMeshRefreshAttempted) {
|
||||
EnsureProxyHeadMesh(a_slot, *proxy);
|
||||
@@ -6332,7 +6388,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
RE::Actor& a_proxy,
|
||||
float a_measuredSpeed)
|
||||
{
|
||||
if (!ProxyMeshReadyForPresentation(a_proxy) || a_slot.proxyPending3DRecovery) {
|
||||
if (!ProxyHasLoaded3D(a_proxy) || a_slot.proxyPending3DRecovery) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6399,7 +6455,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ProxyMeshReadyForPresentation(*proxy) || slot.proxyPending3DRecovery) {
|
||||
if (!ProxyHasLoaded3D(*proxy) || slot.proxyPending3DRecovery) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace
|
||||
|
||||
bool CanWriteProxyGraph(RE::Actor& a_proxy, std::uint32_t a_remotePlayerId, std::uint32_t a_actorId)
|
||||
{
|
||||
if (!ProxyMeshReadyForPresentation(a_proxy)) {
|
||||
if (!ProxyHasLoaded3D(a_proxy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -156,12 +156,11 @@ namespace F4T::ProxyPuppet
|
||||
return;
|
||||
}
|
||||
|
||||
// Driving motion feedback while the mesh is unloaded or only partially loaded
|
||||
// corrupts PlaceAtMe proxies on reload (A-pose, missing head). Wait for a
|
||||
// fully loaded mesh before UpdateNoAI / motion feedback runs.
|
||||
// Wait for a loaded mesh before UpdateNoAI / motion feedback runs. Do not gate on
|
||||
// IsVisible — frustum cull clears visFlags while the mesh stays loaded; skipping
|
||||
// puppet updates there leaves the proxy invisible until the camera moves.
|
||||
const bool meshReadyForPuppet =
|
||||
a_this->IsVisible() &&
|
||||
(a_this->GetFullyLoaded3D() != nullptr || a_this->Get3D() != nullptr);
|
||||
a_this->GetFullyLoaded3D() != nullptr || a_this->Get3D() != nullptr;
|
||||
if (!meshReadyForPuppet) {
|
||||
return;
|
||||
}
|
||||
@@ -262,8 +261,7 @@ namespace F4T::ProxyPuppet
|
||||
}
|
||||
|
||||
const bool meshReadyForPuppet =
|
||||
a_actor.IsVisible() &&
|
||||
(a_actor.GetFullyLoaded3D() != nullptr || a_actor.Get3D() != nullptr);
|
||||
a_actor.GetFullyLoaded3D() != nullptr || a_actor.Get3D() != nullptr;
|
||||
if (!meshReadyForPuppet) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user