51 KiB
Fiche ERR145-SPEC-02 — Niveau N3 — Modèle prévu non précisé par la fiche | utilisé claude-opus-5-5 — Agent lp-impl — Skills sk-protocol, ue-cpp-build, lp-build, lp-ue-compat
ERR145-SPEC-02 : caméra C1 stable — ancre sur le buste, lacet recentré, témoin refait (2026-10-21)
Ordonnateur : Big Boss (local_104ff22d). SPEC complémentaire de ERR145-SPEC.md, étude seule : aucun code modifié, aucune
compilation, ni copie UE, ni MCP, ni commit. Seul fichier écrit : ce livrable.
Amont : ERR145-DIAG.md (H1), ERR145-SPEC.md (option A, témoin, partie C), essai T0 de l'utilisateur sur la build COM23
(2026-10-21, 08:13-08:25 locales), décisions de l'utilisateur relayées par l'ordonnateur (Q2, Q3, abandon de la partie C).
Journal lu : D:\Game\LifePlay_Build_5.8_Development_COM23\Windows\LifePlay\Saved\Logs\LifePlay.log, lu par une copie faite
le 2026-10-21 (2773 lignes, dernière ligne 06:24:31 UTC ; le jeu tournait peut-être encore). Heures du journal en UTC, locale = UTC + 2.
Les numéros de ligne « l. n » du § 1 renvoient à ce journal.
1. Objectif mesurable
1.1 Essai T0 : ce que dit le journal (établi)
| UTC | Lignes | Événement |
|---|---|---|
| 06:14:39 | 1519, 1581 | assis au meuble _40, en C2 (fnt_new048-f-0-sit) ; lever à 06:15:01 (l. 1635) |
| 06:15:14 | 1649, 1652 | C3 → C1, debout (C1Hidden=1) |
| 06:15:28 | 1712, 1722 | assis au meuble _34 sans quitter C1 (aucune touche C entre l. 1649 et l. 1738) : fait 1 (H1) |
| 06:16:02 → 06:16:10 | 1737-1746 | cycle C2 → C3 → C1, assis _34 |
| 06:17:00 → 06:17:15 | 1762-1785 | lever en C1, puis cycle debout |
| 06:17:58 | 1857, 1891 | assis _39 en C1 ; menu Scene_C de 06:18:00 à 06:18:08 (l. 1972, 2088) |
| 06:19:19 → 06:20:16 | 2256-2593 | cycle, assis _39 ; lever en C1 à 06:21:13 (l. 2612) |
| 06:21:33 | 2716, 2726 | assis _35, en C2 (C2 depuis 06:21:16, l. 2620-2622, C1Hidden=0) |
| 06:21:36 → 06:21:40 | 2727-2733 | C3 → C1 assis _35 ; lacet de contrôle +275 à 06:21:41 (l. 2734) |
| 06:22:09.8 → 06:22:11.2 | 2742-2752 | cycle, C2 pendant 0,6 s ; lacet de contrôle -90 à 06:22:11.9 (l. 2753) |
| 06:22:14.3 → 06:22:15.5 | 2755-2764 | cycle, C2 pendant 0,6 s ; lacet de contrôle -90 à 06:22:16.2 (l. 2765) |
| 06:22:17 → 06:22:21 | 2766-2770 | lacet -121, -179, -198, -203, -193 (souris vers la gauche) ; tangage jusqu'à -60 |
| 06:24:31 | 2773 | dernière ligne, toujours en C1 assis |
- Les quatre assises du joueur jouent
fnt_new048-f-0-sit(l. 1519, 1712, 1857, 2716). Aucune animation de repas (fnt_new017-f-0-use, vue le matin) n'apparaît dans la fenêtre T0 (Grepfnt_|-f-0-sur la copie du journal). - Entrées en C1 assis : 5 (06:16:10, 06:20:16, 06:21:40, 06:22:11, 06:22:15). Le récit de l'utilisateur compte deux séries de trois
cycles. Correspondance estimée : la série « Sit » est
_35(06:21:40 mauvaise, 06:22:11 et 06:22:15 bonnes) ; l'autre série (_34,_39) n'a que deux entrées dans cette fenêtre. Inconnu : la chaise « repas » du récit.
1.2 Lecture du code
Position (établi, DIAG § 3) : la minuterie UpdateRoomCam de Room (0,016 s) pose FirstPersonSM au milieu de lEye / rEye
(etat/CAM/CAM-B2s-IMPL.md:137, nœud 512). Ces os sont sous neckLower, masqué en C1. Ils sortent donc des os requis et restent figés
(SkeletalMeshComponent.cpp:2321-2332). Le fait 1 (06:15:28) le confirme une fois de plus.
Premier cycle mauvais (fait 2), inconnu :
- Avant l'entrée de 06:21:40, le cou était démasqué depuis 06:21:16 (
C1Hidden=0, l. 2620). La voie C1 lit les yeux avant de les masquer (CAM-B1-IMPL.md:22,:104-107). H1 seul prédit donc une bonne vue dès ce premier cycle, et le code lu n'explique pas l'échec. - Pistes estimées :
- (a) la pose assise n'est pas encore finale 7 s après l'assise (transition ou fondu de l'animation de meuble), et le masquage la fige à mi-course ; les entrées de 06:22:11 et 06:22:15 tombent 38 et 42 s après l'assise ;
- (b) une tête animée en boucle (L1 de
ERR145-SPEC.md§ 1.2) est figée à une phase quelconque.
- Pour la conception qui suit, la question devient sans objet : la position ne dépend plus ni de l'instant du masquage ni des os de la tête.
Lacet vers la droite (fait 3) :
- Établi : aucune étape de l'entrée en C1 n'écrit le lacet de contrôle.
- La voie P6 de Room lit ce lacet (
524 GetControlRotation → 514 → 539) et l'écrit surFirstPersonSMavantSetViewTargetWithBlend(CAM-B2s-IMPL.md:138,:124). La minuterie fait de même à chaque passage (:137). - Côté C++,
ResetC1PitchOnExitne remet que le tangage (LPCameraModeSubsystem.cpp:811-828), etTickC1Pitchn'écrit que le tangage (:787-790).
- La voie P6 de Room lit ce lacet (
- Établi : le lacet de contrôle est écrit ailleurs :
- par
PlaceC2, à l'entrée en C2 : lacet de l'acteur joueur (CAM-B2d-IMPL.md:117) ; - en C2 et en C1, par
CircleCameraetLeftRight, à la souris X et aux flèches (etat/CAM/CAM2/CAM2-IMPL-02.md:28,CAM-COMMUN6-DIAG.md:315). C3 ne l'écrit pas : la garde deCircleCameraexige la vueCamRoomouCamC1On(même source). Une entrée en C1 hérite donc du lacet laissé par C2.
- par
- Établi : assis, l'acteur n'est pas tourné par la marche.
Walkne pose le lacet de l'acteur que hors siège (LPDirectControlSubsystem.cpp:1545-1547). Le commentaire:564dit « son lacet est celui de la place ». Le joueur n'est pas possédé :SetControlRotationne tourne pas l'acteur (CAM-COMMUN3-DIAG.md:137-139). - Établi par le journal : aux 2e et 3e entrées, C2 a duré 0,6 s et le lacet vaut exactement -90 (l. 2753, 2765), soit le lacet de l'acteur
posé par
PlaceC2, sans souris (estimé fort). L'utilisateur tourne ensuite vers la gauche jusqu'à -193 / -203 (l. 2766-2770). - Estimé : l'avant du corps assis sur
_35est vers -190 ± 15°, soit environ 100° à gauche du lacet de l'acteur. La vue part donc vers la droite parce que l'animation de siège ne regarde pas dans l'axe de l'acteur. - Inconnu : pourquoi l'animation et l'acteur diffèrent (données de meuble ou d'animation). Le témoin le mesure (
lacet bustecontrelacet acteur, § 2.4).
1.3 Décisions de l'utilisateur prises en compte (relayées par l'ordonnateur)
- Q2 : « non : la caméra C1 doit rester stable, sans suivre les mouvements de tête des animations. »
- T0b : une fois la vue bien placée, aucune peau proche n'est visible. L2 est écartée et la partie C (
lp.C1NearClip,lp.C1Forward) est retirée. - Q3 : « oui ». La caméra stable sera étendue à la POV de scène dans une fiche à part, après l'IMPL d'ERR-145. Ici, seulement ce qui sera réutilisable (§ 4.5).
1.4 Objectifs (critères chiffrés au § 7)
- Position : en C1, à chaque image, la vue est posée sur une ancre : l'os parent de
neckLower(os visible, donc animé) plus un décalage fixe dans son repère. La vue ne lit plus les yeux. Témoinvue-ancre≤ 0,5 cm sans lissage, debout, assis, après un lever et à chaque cycle C, quel que soit l'ordre (entrer en C1 puis s'asseoir, ou l'inverse). - Stabilité : assis immobile, la vue ne suit aucun mouvement de tête de l'animation (décision Q2). Elle ne bouge qu'avec le buste.
- Lacet : à chaque entrée en C1 assis, et 0,5 s après s'être assis en C1, le lacet de contrôle est aligné sur l'avant du corps.
Témoin
ecart vue-bustedans ±10° sur la première ligne qui suit ; l'utilisateur regarde droit devant sans toucher la souris. - Rotation : en C1, la rotation de la vue est celle du contrôleur seul (tangage, lacet), roulis nul ; jamais celle de la tête.
- Repli :
lp.C1Stable 0à la console rend la vue d'avant dès l'image suivante. - Inchangés : POV de scène, Scene, C2, C3, Overview, studio, PNJ. Room, PC, GI, Person et tout asset restent intacts.
2. Fichiers touchés, ordre
2.0 Conception et ordre d'exécution
Le cœur de la conception : le modificateur de vue ULPRoomBoundsCameraModifier, déjà installé en permanence
(LPCameraModeSubsystem.cpp:873-881), est le dernier à écrire la vue. Il la reprend en C1. Room continue d'écrire FirstPersonSM,
mais cette écriture n'est plus vue.
- Ordre dans une image, établi :
- le monde d'abord :
GEngine->Tick(LaunchEngineLoop.cpp:5859), puis le ticker du cœur (:6103), dansFEngineLoop::Tick(:5575-6200). Le sous-système (FTSTicker,LPCameraModeSubsystem.cpp:733-735) tourne donc après le monde de l'image N et prépare l'image N+1 ; - dans
UWorld::Tick, viennent dans l'ordre les groupes d'acteurs (LevelTick.cpp:1750-1778), les minuteries, dontUpdateRoomCamde Room (:1816), puis les caméras, « after all actors have been ticked » (:1835-1847) ; - dans la caméra,
UpdateViewTargetappelleCalcCamera(vue deFirstPersonSM), puisApplyCameraModifiers(PlayerCameraManager.cpp:456-463). Le modificateur passe donc après la minuterie de Room, dans la même image. Il n'y a ni ordre de tick à établir ni asset à écrire.
- le monde d'abord :
- Voies écartées :
- (i) un
FTickableGameObject(LevelTick.cpp:1821, entre les minuteries et les caméras) qui écriraitFirstPersonSM: faisable, mais il écrit un composant partagé avec la POV (CAM-B1e-CART.md:89) et la minuterie le réécrit à chaque passage ; - (ii) un tick de composant en
TG_PostUpdateWork: il passe après les caméras (LevelTick.cpp:1877contre:1847), donc avec une image de retard ; - (iii) attacher
FirstPersonSMau buste : Room le détache et le replace à chaque entrée et à chaque passage de minuterie (CAM-B1-IMPL.md:22,CAM-B2s-IMPL.md:137-138) ; - (iv) l'option B du DIAG (Room, MCP) : la fiche demande du 100 % C++.
- (i) un
- Partage des rôles :
- le sous-système étalonne le décalage hors C1, lit les variables, choisit les réglages et écrit le témoin ;
- le modificateur calcule l'ancre et pose la vue et le lacet dans l'image même. L'entrée en C1 est vue par le modificateur dès la première image où la cible de vue est le joueur, sans attendre le libellé. Aucune image n'est donc montrée avec l'ancien lacet.
Ordre des fichiers (tous sous D:\Game\LifePlay-master 5.8\Source\LifePlay\) : (1) LPRoomBoundsCameraModifier.h,
(2) LPRoomBoundsCameraModifier.cpp, (3) LPCameraModeSubsystem.h, (4) LPCameraModeSubsystem.cpp.
Volume estimé : ≈ +320 / −40 lignes (modificateur ≈ +160, sous-système ≈ +160 dont 50 de témoin qui en remplacent 36). Aucune écriture
d'asset, une compilation.
2.1 LPRoomBoundsCameraModifier.h
- Commentaire de tête (l. 6-7) : ajouter « ERR145-SPEC-02 : en C1, la vue est posée sur l'ancre du buste et tournée par le contrôleur ».
- Après
#include "Camera/CameraModifier.h"(l. 12), déclaration avancéeclass USkeletalMeshComponent;. AvantUCLASS()(l. 15), une structure C++ simple, hors réflexion :
/** ERR145-SPEC-02 : parametres de la vue C1 stable, poses a chaque image par ULPCameraModeSubsystem (etat de la fin de
* l'image precedente). Player nul = vue C1 stable inactive. */
struct FLPC1StableParams
{
TWeakObjectPtr<AActor> Player;
TWeakObjectPtr<USkeletalMeshComponent> Mesh;
/** Parent de neckLower : os visible (Room ne masque que neckLower), donc requis et anime. */
FName Chest;
/** Milieu des yeux dans le repere du buste (espace composant). */
FVector LocalEye = FVector::ZeroVector;
/** Avant du corps dans le repere du buste ; nul = inconnu (repli sur les clavicules). */
FVector LocalForward = FVector::ZeroVector;
/** Person.OccupyingFnt valide. */
bool bSeated = false;
/** lp.C1AlignYaw : 0 jamais, 1 entree assis et assise en C1, 2 toute entree. */
int32 AlignYaw = 1;
/** lp.C1Smooth, constante de temps en s (0 = aucun lissage). */
float SmoothTime = 0.f;
};
- Section publique, après
SetStudio(l. 38) :
/** ERR145-SPEC-02 : vue C1 stable pour les images suivantes (Player nul = inactive). */
void SetC1Stable(const FLPC1StableParams& InC1);
/** Vrai si la derniere image a pose la vue C1 stable (temoin). */
bool IsC1Applied() const { return bC1Active; }
/** Ancre de tete non lissee, en monde : transformee du buste (espace composant) x LocalEye x transformee du composant.
* Fonctions statiques, sans etat : reprises telles quelles par la future POV stable (ERR145-SPEC-02 § 4.5). */
static FVector ComputeHeadAnchor(const USkeletalMeshComponent* Mesh, FName Chest, const FVector& LocalEye);
/** Lacet de l'avant du corps : repere du buste etalonne, sinon clavicules (lCollar, rCollar). Faux sans source. */
static bool ComputeBodyYaw(const USkeletalMeshComponent* Mesh, FName Chest, const FVector& LocalForward, double& OutYaw, const TCHAR*& OutSource);
/** Milieu des yeux dans le repere du buste, en pose de reference du maillage (repli avant tout etalonnage). */
static bool ComputeRefLocalEye(const USkeletalMeshComponent* Mesh, FName Chest, FVector& OutLocalEye);
- Section privée, à la fin :
void ApplyC1Stable(float DeltaTime, FMinimalViewInfo& InOutPOV, AActor* Player, USkeletalMeshComponent* Mesh);
FLPC1StableParams C1;
bool bC1Active = false;
bool bC1SeatedWas = false;
/** Delai avant l'alignement qui suit une assise en C1 ; negatif = aucun. */
float C1SeatAlignLeft = -1.f;
/** Ancre lissee, en espace composant (la marche et le deplacement du composant ne creent aucun retard). */
FVector C1SmoothedCS = FVector::ZeroVector;
uint64 C1LastFrame = 0;
FVector C1LastLocation = FVector::ZeroVector;
FRotator C1LastRotation = FRotator::ZeroRotator;
2.2 LPRoomBoundsCameraModifier.cpp
- Includes à ajouter :
Components/SkeletalMeshComponent.h,GameFramework/PlayerController.h,CoreGlobals.h(GFrameCounter,CoreGlobals.h:532),LifePlay.h(LogLifePlay,LifePlay.h:9). ModifyCamera: insérer après le bloc Studio (l. 42-47), avant le filtre des l. 49-54, qui rendfalsepour toute vue non bornée, C1 comprise :
// ERR145-SPEC-02 : vue C1 stable. Cible de vue relue a chaque image : hors de la vue du joueur, rien.
AActor* C1Player = C1.Player.Get();
USkeletalMeshComponent* C1Mesh = C1.Mesh.Get();
if (C1Player && C1Mesh && !C1.Chest.IsNone() && CameraOwner && CameraOwner->GetViewTarget() == C1Player)
{
ApplyC1Stable(DeltaTime, InOutPOV, C1Player, C1Mesh);
}
else if (bC1Active)
{
bC1Active = false;
C1SeatAlignLeft = -1.f;
}
- Nouvelles fonctions :
void ULPRoomBoundsCameraModifier::SetC1Stable(const FLPC1StableParams& InC1)
{
C1 = InC1;
}
FVector ULPRoomBoundsCameraModifier::ComputeHeadAnchor(const USkeletalMeshComponent* Mesh, FName Chest, const FVector& LocalEye)
{
const FTransform ChestCS = Mesh->GetSocketTransform(Chest, RTS_Component);
return Mesh->GetComponentTransform().TransformPosition(ChestCS.TransformPosition(LocalEye));
}
bool ULPRoomBoundsCameraModifier::ComputeBodyYaw(const USkeletalMeshComponent* Mesh, FName Chest, const FVector& LocalForward, double& OutYaw, const TCHAR*& OutSource)
{
if (!LocalForward.IsNearlyZero())
{
const FVector Forward = Mesh->GetSocketTransform(Chest, RTS_World).TransformVectorNoScale(LocalForward);
const FVector Flat(Forward.X, Forward.Y, 0.0);
// Buste a plus de ~72 degres de la verticale (couche) : pas de lacet fiable.
if (Flat.SizeSquared() > 0.09)
{
OutYaw = Flat.Rotation().Yaw;
OutSource = TEXT("buste");
return true;
}
}
static const FName LeftCollar(TEXT("lCollar"));
static const FName RightCollar(TEXT("rCollar"));
if (Mesh->GetBoneIndex(LeftCollar) != INDEX_NONE && Mesh->GetBoneIndex(RightCollar) != INDEX_NONE)
{
// UE : X avant, Y droite, Z haut ; (droite - gauche) x haut = avant.
const FVector Across = Mesh->GetSocketLocation(RightCollar) - Mesh->GetSocketLocation(LeftCollar);
const FVector Flat = FVector::CrossProduct(Across, FVector::UpVector).GetSafeNormal2D();
if (!Flat.IsNearlyZero())
{
OutYaw = Flat.Rotation().Yaw;
OutSource = TEXT("clavicules");
return true;
}
}
OutSource = TEXT("aucune");
return false;
}
bool ULPRoomBoundsCameraModifier::ComputeRefLocalEye(const USkeletalMeshComponent* Mesh, FName Chest, FVector& OutLocalEye)
{
// Transformee de reference en espace composant : produit des transformees locales de reference de l'os jusqu'a la racine,
// dans l'ordre enfant puis parent (C = A * B applique A puis B, TransformNonVectorized.h:472-473).
auto RefComponentSpace = [Mesh](FName Bone, FTransform& Out)
{
Out = FTransform::Identity;
for (FName Name = Bone; !Name.IsNone(); Name = Mesh->GetParentBone(Name))
{
const int32 Index = Mesh->GetBoneIndex(Name);
if (Index == INDEX_NONE)
{
return false;
}
Out = Out * Mesh->GetRefPoseTransform(Index);
}
return true;
};
FTransform ChestRef, LeftRef, RightRef;
if (!RefComponentSpace(Chest, ChestRef) || !RefComponentSpace(FName(TEXT("lEye")), LeftRef) || !RefComponentSpace(FName(TEXT("rEye")), RightRef))
{
return false;
}
OutLocalEye = ChestRef.InverseTransformPosition((LeftRef.GetLocation() + RightRef.GetLocation()) * 0.5);
return true;
}
// ERR145-SPEC-02 § 2.2 : la vue C1 ne lit plus les yeux (masques sous neckLower, donc figes, ERR145-DIAG § 3.2). Position = ancre
// du buste ; rotation = controleur seul ; lacet aligne sur l'avant du corps a l'entree assis et apres une assise en C1.
void ULPRoomBoundsCameraModifier::ApplyC1Stable(float DeltaTime, FMinimalViewInfo& InOutPOV, AActor* Player, USkeletalMeshComponent* Mesh)
{
// Second appel dans la meme image (fondu de vue : PlayerCameraManager.cpp:903 et :916) : meme resultat, etat inchange.
if (bC1Active && C1LastFrame == GFrameCounter)
{
InOutPOV.Location = C1LastLocation;
InOutPOV.Rotation = C1LastRotation;
return;
}
const bool bEntry = !bC1Active;
bC1Active = true;
C1LastFrame = GFrameCounter;
// Position : buste (espace composant) x decalage ; lissage dans l'espace composant, saut de plus de 30 cm = recale.
const FVector EyeCS = Mesh->GetSocketTransform(C1.Chest, RTS_Component).TransformPosition(C1.LocalEye);
if (bEntry || C1.SmoothTime <= 0.f || FVector::DistSquared(EyeCS, C1SmoothedCS) > FMath::Square(30.0))
{
C1SmoothedCS = EyeCS;
}
else
{
C1SmoothedCS = FMath::Lerp(C1SmoothedCS, EyeCS, 1.0 - FMath::Exp(-DeltaTime / C1.SmoothTime));
}
InOutPOV.Location = Mesh->GetComponentTransform().TransformPosition(C1SmoothedCS);
// Lacet : entree en C1 assise (ou toute entree si AlignYaw = 2) ; assise pendant C1 : 0,5 s apres (pose posee).
bool bAlign = bEntry && (C1.AlignYaw >= 2 || (C1.AlignYaw >= 1 && C1.bSeated));
if (bEntry)
{
C1SeatAlignLeft = -1.f;
}
else if (C1.bSeated && !bC1SeatedWas)
{
C1SeatAlignLeft = 0.5f;
}
bC1SeatedWas = C1.bSeated;
if (C1SeatAlignLeft >= 0.f)
{
C1SeatAlignLeft -= DeltaTime;
if (C1SeatAlignLeft < 0.f)
{
bAlign = C1.AlignYaw >= 1 && C1.bSeated;
}
}
APlayerController* PC = CameraOwner->GetOwningPlayerController();
FRotator Control = PC ? PC->GetControlRotation() : InOutPOV.Rotation;
if (PC && bAlign)
{
double BodyYaw = 0.0;
const TCHAR* Source = nullptr;
if (ComputeBodyYaw(Mesh, C1.Chest, C1.LocalForward, BodyYaw, Source))
{
UE_LOG(LogLifePlay, Log, TEXT("C1 stable : lacet de controle %+.0f -> %+.0f (avant du corps, source %s ; %s ; lacet de l'acteur %+.0f)"),
FRotator::NormalizeAxis(Control.Yaw), BodyYaw, Source, bEntry ? TEXT("entree en C1") : TEXT("assise en C1"),
FRotator::NormalizeAxis(Player->GetActorRotation().Yaw));
Control.Yaw = BodyYaw;
PC->SetControlRotation(FRotator(Control.Pitch, Control.Yaw, 0.0));
}
else
{
UE_LOG(LogLifePlay, Log, TEXT("C1 stable : lacet non aligne (aucune source pour l'avant du corps)"));
}
}
// Rotation : controleur seul (tangage : TickC1Pitch ; lacet : Room.CircleCamera a la souris), sans roulis.
InOutPOV.Rotation = FRotator(Control.Pitch, Control.Yaw, 0.0);
C1LastLocation = InOutPOV.Location;
C1LastRotation = InOutPOV.Rotation;
}
Le contrôleur écrit est celui que lit Room. SetControlRotation ne tourne pas l'acteur assis : seul Walk le tourne, et seulement hors
siège (LPDirectControlSubsystem.cpp:1545-1547). Au lever en C1, Walk tourne le corps vers ce lacet : continuité voulue.
2.3 LPCameraModeSubsystem.h
- Déclaration avancée
class USkeletalMeshComponent;(l. 17-25). - Remplacer le commentaire du témoin C1 (l. 71) par « Témoin C1 (ERR145-SPEC-02) : une ligne par seconde en C1, vue mesurée contre l'ancre du buste ».
- Après
float C1Cooldown = 0.f;(l. 74) :
/** ERR145-SPEC-02 : vue C1 stable, a chaque image (TickSwitchKey). Etalonne le decalage yeux-buste hors C1 (joueur debout,
* immobile, cou non masque), puis transmet les parametres au modificateur de vue (ancre, lacet, lissage). */
void TickC1Stable(const FString& FrameLabel, float DeltaTime);
/** Maillage etalonne, son buste (parent de neckLower) et ses decalages (reference, etalonne), avant du corps etalonne. */
TWeakObjectPtr<USkeletalMeshComponent> C1CalMesh;
FName C1Chest;
FVector C1RefEye = FVector::ZeroVector;
bool bC1RefEye = false;
FVector C1CalEye = FVector::ZeroVector;
FVector C1CalForward = FVector::ZeroVector;
int32 C1CalCount = 0;
float C1CalCooldown = 0.f;
2.4 LPCameraModeSubsystem.cpp
- Includes :
HAL/IConsoleManager.h(TAutoConsoleVariable,IConsoleManager.h:2028),Engine/SkinnedAsset.h(nom de l'asset au journal ;SkinnedAsset.h:57). - Espace
LPCameraModeLocal:- ajouter
const FName NeckLowerBone(TEXT("neckLower"));,const float C1CalInterval = 0.5f;etconst float C1CalMaxSpeed = 10.f;; - retirer
C1Interval,C1MaxEyeDistance,C1MaxPitch,C1MaxYawGap(l. 104, 106-108). Grep : ils ne servent qu'à l'ancienWitnessC1(l. 1203, 1220-1221) ; - garder
C1LogInterval(l. 105),C1PitchMin/C1PitchMax(l. 132-133, utilisés parTickC1Pitch),LeftEyeSocket/RightEyeSocket(l. 102-103).
- ajouter
- Après la fermeture de l'espace de noms (l. 728) :
// ERR145-SPEC-02 : bascules de l'essai en jeu (console, touche ² ou ~, DefaultInput.ini:166-168), sans recompiler.
static TAutoConsoleVariable<int32> CVarLPC1Stable(TEXT("lp.C1Stable"), 1,
TEXT("ERR145 : 1 = vue C1 stable (ancre sur le buste, rotation du controleur) ; 0 = vue d'avant (camera de Room sur les yeux)."));
static TAutoConsoleVariable<int32> CVarLPC1AlignYaw(TEXT("lp.C1AlignYaw"), 1,
TEXT("ERR145 : lacet aligne sur l'avant du corps : 0 = jamais ; 1 = entree en C1 assis et 0,5 s apres une assise en C1 ; 2 = toute entree en C1."));
static TAutoConsoleVariable<float> CVarLPC1Smooth(TEXT("lp.C1Smooth"), 0.f,
TEXT("ERR145 : lissage de la position C1, constante de temps en s (espace du maillage) ; 0 = aucun."));
static TAutoConsoleVariable<int32> CVarLPC1EyeSource(TEXT("lp.C1EyeSource"), 0,
TEXT("ERR145 : decalage yeux-buste : 0 = etalonne debout, sinon pose de reference ; 1 = pose de reference ; 2 = etalonne seulement."));
- Nouvelle méthode, après
ResetC1PitchOnExit(l. 828). Les accès suiventTickC1Pitch(l. 800-801) :
// ERR145-SPEC-02 § 2.4 : la vue C1 est posee par le modificateur sur le parent de neckLower plus un decalage fixe. Le decalage est
// etalonne hors C1, joueur debout, immobile et rendu, cou non masque (yeux animes) ; a defaut, pose de reference du maillage.
void ULPCameraModeSubsystem::TickC1Stable(const FString& FrameLabel, float DeltaTime)
{
UGameInstance* GI = GetGameInstance();
UWorld* World = GI ? GI->GetWorld() : nullptr;
APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr;
APlayerCameraManager* Manager = PC ? PC->PlayerCameraManager.Get() : nullptr;
ULPRoomBoundsCameraModifier* Modifier = Manager ? Cast<ULPRoomBoundsCameraModifier>(Manager->FindCameraModifierByClass(ULPRoomBoundsCameraModifier::StaticClass())) : nullptr;
ACharacter* Player = Cast<ACharacter>(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIPlayerCharacter));
USkeletalMeshComponent* Mesh = IsValid(Player) ? Player->GetMesh() : nullptr;
// Maillage nouveau (chargement, CharGen) : buste, reference et etalonnage repris de zero.
if (Mesh != C1CalMesh.Get())
{
C1CalMesh = Mesh;
C1Chest = Mesh ? Mesh->GetParentBone(LPCameraModeLocal::NeckLowerBone) : NAME_None;
C1CalCount = 0;
C1CalEye = FVector::ZeroVector;
C1CalForward = FVector::ZeroVector;
bC1RefEye = Mesh && !C1Chest.IsNone() && ULPRoomBoundsCameraModifier::ComputeRefLocalEye(Mesh, C1Chest, C1RefEye);
if (Mesh)
{
UE_LOG(LogLifePlay, Log, TEXT("C1 stable : maillage %s, buste %s, decalage de reference %s"), *GetNameSafe(Mesh->GetSkinnedAsset()),
*C1Chest.ToString(), bC1RefEye ? *FString::Printf(TEXT("%.0f cm (%.0f, %.0f, %.0f)"), C1RefEye.Size(), C1RefEye.X, C1RefEye.Y, C1RefEye.Z) : TEXT("absent"));
}
}
const bool bSexOn = LPCameraModeLocal::ReadBool(GI, LPCameraModeLocal::GISexOn);
const UObject* Room = LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom);
const bool bSeated = Mesh && IsValid(LPCameraModeLocal::ReadObject(Player, LPCameraModeLocal::PlayerOccupyingFnt));
// Etalonnage : toutes les 0,5 s, moyenne glissante (poids 0,2 apres la premiere mesure).
C1CalCooldown = FMath::Max(0.f, C1CalCooldown - DeltaTime);
if (Mesh && !C1Chest.IsNone() && C1CalCooldown <= 0.f && FrameLabel != LPCameraModeLocal::LabelC1 && !bSexOn && !bSeated
&& Player->GetVelocity().Size2D() < LPCameraModeLocal::C1CalMaxSpeed && Mesh->WasRecentlyRendered()
&& !Mesh->IsBoneHiddenByName(LPCameraModeLocal::NeckLowerBone)
&& Mesh->GetBoneIndex(LPCameraModeLocal::LeftEyeSocket) != INDEX_NONE && Mesh->GetBoneIndex(LPCameraModeLocal::RightEyeSocket) != INDEX_NONE)
{
C1CalCooldown = LPCameraModeLocal::C1CalInterval;
const FTransform ChestCS = Mesh->GetSocketTransform(C1Chest, RTS_Component);
const FVector EyesCS = (Mesh->GetSocketTransform(LPCameraModeLocal::LeftEyeSocket, RTS_Component).GetLocation()
+ Mesh->GetSocketTransform(LPCameraModeLocal::RightEyeSocket, RTS_Component).GetLocation()) * 0.5;
// Debout hors C1, le corps regarde l'avant de l'acteur (orientation vers la marche, LPDirectControlSubsystem.cpp:1538-1544).
const FVector ForwardCS = Mesh->GetComponentTransform().InverseTransformVectorNoScale(Player->GetActorForwardVector());
const double Weight = C1CalCount == 0 ? 1.0 : 0.2;
C1CalEye = FMath::Lerp(C1CalEye, ChestCS.InverseTransformPosition(EyesCS), Weight);
C1CalForward = FMath::Lerp(C1CalForward, ChestCS.InverseTransformVectorNoScale(ForwardCS).GetSafeNormal(), Weight).GetSafeNormal();
++C1CalCount;
if (C1CalCount == 1 || C1CalCount == 20)
{
UE_LOG(LogLifePlay, Log, TEXT("C1 stable : etalonnage %d (decalage yeux-buste %.0f cm, ecart a la reference %.0f cm)"), C1CalCount,
C1CalEye.Size(), bC1RefEye ? FVector::Dist(C1CalEye, C1RefEye) : -1.0);
}
}
// Parametres de l'image suivante ; arme si CamC1On et hors scene (le modificateur verifie en plus la cible de vue).
if (!Modifier)
{
return; // pose par TickViewBounds, appele juste avant dans la meme image (l. 1262, :873-881)
}
const int32 Source = CVarLPC1EyeSource.GetValueOnGameThread();
const bool bUseCal = C1CalCount > 0 && Source != 1;
const bool bUseRef = !bUseCal && bC1RefEye && Source != 2;
FLPC1StableParams Params;
if (Mesh && !C1Chest.IsNone() && (bUseCal || bUseRef) && CVarLPC1Stable.GetValueOnGameThread() != 0 && !bSexOn
&& LPCameraModeLocal::ReadBool(Room, LPCameraModeLocal::RoomCamC1On))
{
Params.Player = Player;
Params.Mesh = Mesh;
Params.Chest = C1Chest;
Params.LocalEye = bUseCal ? C1CalEye : C1RefEye;
Params.LocalForward = C1CalForward;
Params.bSeated = bSeated;
Params.AlignYaw = CVarLPC1AlignYaw.GetValueOnGameThread();
Params.SmoothTime = FMath::Max(0.f, CVarLPC1Smooth.GetValueOnGameThread());
}
Modifier->SetC1Stable(Params);
}
TickSwitchKey, après la l. 1264 (ResetC1PitchOnExit(...)) :TickC1Stable(FrameLabel, DeltaTime);.Deinitialize: rien à ajouter.RemoveViewBounds(l. 744) retire le modificateur avec son état (:970-979).- Témoin C1 : remplace
WitnessC1(l. 1196-1231). Appel inchangé (Tick, l. 1140-1143, libellé C1).
// ERR145-SPEC-02 : une ligne par seconde en C1, sans condition. L'ancre est recalculee ici, que la vue stable soit active ou non :
// avec lp.C1Stable 0, « vue-ancre » chiffre le defaut d'origine (yeux figes) ; avec 1, il vaut 0.
void ULPCameraModeSubsystem::WitnessC1(float DeltaTime)
{
C1Cooldown = FMath::Max(0.f, C1Cooldown - DeltaTime);
if (C1Cooldown > 0.f)
{
return;
}
C1Cooldown = LPCameraModeLocal::C1LogInterval;
UGameInstance* GI = GetGameInstance();
UWorld* World = GI ? GI->GetWorld() : nullptr;
APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr;
APlayerCameraManager* Manager = PC ? PC->PlayerCameraManager.Get() : nullptr;
ACharacter* Player = Cast<ACharacter>(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIPlayerCharacter));
USkeletalMeshComponent* Mesh = IsValid(Player) ? Player->GetMesh() : nullptr;
const USceneComponent* Camera = IsValid(Player) ? Cast<USceneComponent>(LPCameraModeLocal::ReadObject(Player, LPCameraModeLocal::PlayerFirstPersonSM)) : nullptr;
if (!Manager || !Mesh || !IsValid(Camera) || Mesh != C1CalMesh.Get() || C1Chest.IsNone())
{
return;
}
const bool bCal = C1CalCount > 0 && CVarLPC1EyeSource.GetValueOnGameThread() != 1;
const FVector Anchor = ULPRoomBoundsCameraModifier::ComputeHeadAnchor(Mesh, C1Chest, bCal ? C1CalEye : C1RefEye);
const FVector ChestAt = Mesh->GetSocketLocation(C1Chest);
const FVector Eyes = (Mesh->GetSocketLocation(LPCameraModeLocal::LeftEyeSocket) + Mesh->GetSocketLocation(LPCameraModeLocal::RightEyeSocket)) * 0.5;
const FVector ViewAt = Manager->GetCameraLocation();
const FRotator ViewRotation = Manager->GetCameraRotation();
double BodyYaw = 0.0;
const TCHAR* BodySource = TEXT("aucune");
const bool bBody = ULPRoomBoundsCameraModifier::ComputeBodyYaw(Mesh, C1Chest, C1CalForward, BodyYaw, BodySource);
const ULPRoomBoundsCameraModifier* Modifier = Cast<ULPRoomBoundsCameraModifier>(Manager->FindCameraModifierByClass(ULPRoomBoundsCameraModifier::StaticClass()));
const UObject* RoomObject = LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom);
UE_LOG(LogLifePlay, Log, TEXT("Temoin C1 : stable=%d appliquee=%d ; vue-ancre %.1f cm ; yeux-ancre %.0f cm ; camera-yeux %.0f cm ; ancre-buste %.0f cm (haut %+.0f) ; decalage %s (%d mesures, ecart a la reference %.0f cm) ; lacet vue %+.0f controle %+.0f buste %+.0f (%s) acteur %+.0f ; ecart vue-buste %+.0f ; tangage %+.0f ; assis=%d ; cou masque=%d ; rendu=%d ; lissage %.2f s ; minuterie=%d"),
CVarLPC1Stable.GetValueOnGameThread(), (Modifier && Modifier->IsC1Applied()) ? 1 : 0,
FVector::Dist(ViewAt, Anchor), FVector::Dist(Eyes, Anchor), FVector::Dist(Camera->GetComponentLocation(), Eyes),
FVector::Dist(Anchor, ChestAt), Anchor.Z - ChestAt.Z,
bCal ? TEXT("etalonne") : TEXT("reference"), C1CalCount, (C1CalCount > 0 && bC1RefEye) ? FVector::Dist(C1CalEye, C1RefEye) : -1.0,
FRotator::NormalizeAxis(ViewRotation.Yaw), FRotator::NormalizeAxis(PC->GetControlRotation().Yaw), bBody ? BodyYaw : 0.0, BodySource,
FRotator::NormalizeAxis(Player->GetActorRotation().Yaw), bBody ? FRotator::NormalizeAxis(ViewRotation.Yaw - BodyYaw) : 0.0,
FRotator::NormalizeAxis(ViewRotation.Pitch),
IsValid(LPCameraModeLocal::ReadObject(Player, LPCameraModeLocal::PlayerOccupyingFnt)) ? 1 : 0,
Mesh->IsBoneHiddenByName(LPCameraModeLocal::NeckLowerBone) ? 1 : 0, Mesh->WasRecentlyRendered() ? 1 : 0,
CVarLPC1Smooth.GetValueOnGameThread(),
LPCameraModeLocal::TimerActive(IsValid(RoomObject) ? RoomObject : nullptr, LPCameraModeLocal::RoomCamTimer, World) ? 1 : 0);
}
La chaîne de format a 22 champs pour 22 arguments (comptés). Le préfixe « Temoin C1 : » est gardé pour les recherches de journal.
- Plages attendues (estimées, proportions Genesis 8, non mesurées ; la VAL les cale sur la première ligne debout) :
| Champ | Debout, stable=1 |
Assis, stable=1 |
stable=0, debout → C1 → assis (défaut d'origine) |
|---|---|---|---|
appliquee |
1 | 1 | 0 |
vue-ancre |
0.0 (≤ lissage) | 0.0 | > 10 cm (vue sur les yeux figés) |
yeux-ancre |
0 à 5 cm | > 10 cm possible (yeux figés, sans effet sur la vue) | > 10 cm |
camera-yeux |
0 à 3 cm (Room, minuterie) | idem | idem |
ancre-buste / haut |
20 à 35 cm / +15 à +30 | idem (repère du buste) | idem |
decalage |
etalonne, mesures ≥ 1 ; écart à la référence < 5 cm |
idem | idem |
ecart vue-buste |
libre (souris) | ±10° à la 1re ligne après une entrée ou une assise | quelconque |
lacet acteur contre lacet buste |
≈ égaux | écart à mesurer (≈ 100° estimé sur _35) |
idem |
cou masque / rendu |
1 / 1 | 1 / 1 (0 si le corps sort de l'image) | 1 / 1 |
2.5 Retiré par rapport à ERR145-SPEC.md
- Option A (
TickC1BoneRefresh,lp.C1RefreshBones, écriture deVisibilityBasedAnimTickOption) : non livrée (§ 4.1). - Partie C (
SetC1View,lp.C1NearClip,lp.C1Forward, plan proche par vue, avance de la vue) : retirée (décision T0b). BodyForwardpar les clavicules seules : remplacé parComputeBodyYaw(repère du buste étalonné, puis clavicules).- Le témoin de
ERR145-SPEC.md§ 2.3 est remplacé par celui du § 2.4.
3. Repli
- Sans build :
lp.C1Stable 0à la console : le sous-système transmet des paramètres vides, et le modificateur cesse d'agir dès l'image suivante (vue de Room, comportement d'avant) ;lp.C1AlignYaw 0garde la position stable sans toucher au lacet ;lp.C1EyeSource 1force la pose de référence si l'étalonnage est mauvais ;- en Shipping, sans console, les variables gardent leur valeur par défaut.
- Code : instantané par la session principale avant l'IMPL (
Tools\Snapshot.ps1 -Lot ERR145). Retour arrière seulement proposé (Tools\Restore.ps1 -Lot ERR145). Les quatre fichiers ne touchent ni Room, ni PC, ni GI, ni Person, ni aucun asset.
4. Risques R#, questions Q#
4.1 L'option A reste-t-elle utile ? Non, elle devient inutile
- La vue ne lit plus aucun os sous
neckLower. L'ancre est l'os parent deneckLower, qui est visible donc gardé dans les os requis (SkeletalMeshComponent.cpp:2321-2332) et animé. Le gel des os masqués n'a plus d'effet visible : la tête est masquée, etFirstPersonSM, posé par Room sur les yeux figés, n'est plus la vue. - A n'apporterait plus qu'une chose : le rafraîchissement des os quand le maillage n'est pas rendu (
SkinnedMeshComponent.h:98-101;ACharacterest enAlwaysTickPose,Character.cpp:125cité par le DIAG). Son coût reste entier : deux recalculs des os requis par passage en C1, R4 deERR145-SPEC.md. - Recours seulement si T4 montre un saut au retour d'un regard au plafond (
rendu=0, R2).
4.2 Choix de l'ancre
- Os : parent de
neckLower, lu à l'exécution (GetParentBone,SkinnedMeshComponent.h:1263; racine →NAME_None,SkinnedMeshComponent.cpp:2506-2516).chestUpperest attendu sur Genesis 8, mais c'est une estimation : la hiérarchie n'a pas été relue dans les assets.neckLowerlui-même est exclu, car il est masqué donc figé. - Décalage :
- étalonné sur le maillage vivant, hors C1, debout et immobile, cou non masqué, maillage rendu. Il couvre homme, femme et proportions propres au maillage. Moyenne glissante sur des mesures espacées de 0,5 s ;
- repli sur la pose de référence (
GetRefPoseTransform, locale :SkinnedMeshComponent.h:2045-2053,.cpp:2727-2737), si la partie commence directement en C1. Le témoin écrit l'écart entre les deux.
- Repère : la rotation complète du buste. Penché pour manger, le point de vue avance avec le buste ; couché, il prolonge le buste. Un repère de lacet seul mettrait le point de vue à la verticale d'un buste couché.
- Lissage en espace composant : la marche et le déplacement du composant (assise, racine à 77-104 cm du composant selon le DIAG) ne créent aucun retard. Un saut de plus de 30 cm est recalé.
4.3 Risques
- R1 : l'étalonnage debout fige la tête de l'animation de repos au moment des mesures (regard un peu baissé, tête tournée). Erreur
attendue de quelques cm, moyennée ;
lp.C1EyeSource 1sert de repli. Témoin :ecart a la reference. - R2 : en
AlwaysTickPose, les os ne sont rafraîchis que si le maillage est rendu (SkinnedMeshComponent.h:100-101). Assis en C1, le regard au plafond peut sortir le corps de l'image : le buste reste figé en espace composant. La position de l'acteur reste juste, et l'erreur est petite au repos (estimé). Témoin :rendu. Recours : option A (§ 4.1). - R3 : le sous-système arme la vue stable sur l'état de l'image précédente (
LaunchEngineLoop.cpp:5859,:6103). À l'entrée en scène depuis C1, si la POV vise le joueur dans la même image, l'ancre peut s'appliquer une image en POV. Négligeable (estimé). Le désarmement suit à l'image suivante, au front deSexOn. - R4 : pendant un fondu de vue,
GetViewTarget()rend la cible en attente (PlayerCameraManager.cpp:249-256) et les deux cibles sont évaluées (:903,:916). L'ancre s'appliquerait alors aux deux, et le fondu deviendrait une coupe. Établi sans effet aujourd'hui : C1 est posée sans fondu (SetViewTargetWithBlend(…, 0, Linear),CAM-B1-IMPL.md:22; « fondu 0 partout »,CAM-AUDIT.md:44). Le garde-image (GFrameCounter) rend le même résultat au second appel. - R5 :
FirstPersonSM(posé par Room aux yeux figés) n'est plus à la place de la vue. Un Blueprint qui lirait sa position en C1 serait décalé. Côté C++, GrepFirstPersonSMsurSource/LifePlayne trouve que des lectures de témoin et la POV (l. 99, 801, 1209, 1643, 1675-1676). Les lecteurs Blueprint ne sont pas relus (incertain). Les clics à l'écran passent par la vue finale du gestionnaire de caméra, donc par l'ancre (estimé). Essai : T6. - R6 : l'alignement 0,5 s après une assise en C1 lit l'avant du corps à ce moment. Une transition plus longue donnerait un angle
faux (piste (a) du § 1.2). La souris corrige ;
lp.C1AlignYaw 0coupe. Le témoin écrit chaque alignement. - R7 : buste couché (plus de 72° de la verticale) : le lacet n'est pas aligné. Le repli sur les clavicules peut alors donner un angle
quelconque (
ComputeBodyYaw). C1 couché n'a pas été vu en essai : inconnu, à couvrir par T5. - R8 : l'étalonnage de l'avant du corps suppose que le maillage debout regarde l'avant de l'acteur (orientation vers la marche,
LPDirectControlSubsystem.cpp:1538-1544). Une animation de repos qui tourne le buste fausseraitC1CalForward(estimé faible). Le repli clavicules est mesuré à part par le témoin (bustecontreclaviculesselon la source). - R9 : compilation unitaire. Noms neufs uniques (
CVarLPC1*,FLPC1StableParams) ou membres de classe ; aucun espace anonyme (lp-build§ 8). - R10 : un
FTSTickertourne aussi au menu principal. SansPlayerCharacter, aucun paramètre n'est armé (Playernul).
4.4 Questions pour l'utilisateur
- Q4 : faut-il aussi aligner le lacet à l'entrée en C1 debout ? Proposition : non (
lp.C1AlignYaw 1). Debout, l'entrée garde le lacet de C2, comme aujourd'hui ;2est essayé en T6. - Q5 : un lissage par défaut ? Proposition : 0 (vue fixée au buste). Valeur retenue après T5 (
lp.C1Smooth 0.15), puis écrite dans le code ou dans[SystemSettings]deDefaultEngine.inipour la Shipping.
4.5 POV de scène (Q3 : oui, fiche à part après l'IMPL d'ERR-145)
- Effet de cette conception sur la POV : aucun. La vue stable n'est armée qu'avec
CamC1Onet horsSexOn(§ 2.4). Le libellé « POV » vient dePC.POVOnpendantSexOn(LPCameraModeSubsystem.cpp:2620-2625). Seule exception, une image au plus (R3). - Réutilisable tel quel :
ComputeHeadAnchor,ComputeBodyYawetComputeRefLocalEye: statiques, sans état, valables pour tout maillage de Person ;- le schéma du modificateur : branche avant le filtre des bornes, cible de vue relue à chaque image, garde-image, lissage en espace composant ;
- la règle « l'ancre est le parent de l'os masqué » ;
- le modèle de bascule (
lp.POVStable, par exemple) et le modèle du témoin (vue-ancre,yeux-ancre,ancre-buste,rendu).
- Particularités connues :
- Cible de vue :
SexPersons[SexCameraIndex](CAM-B1e-CART.md:88), parfois un PNJ. L'étalonnage de cette fiche ne couvre que le joueur. Pour la POV : pose de référence, ou étalonnage par maillage (une table par composant) pris avant la scène. - Masquage : le cou est masqué par PC sur
CurrentActor(CAM-B1e-CART.md:62-63).FirstPersonSMreste attaché àhead(CAM-B1e-CART.md:88-89). Un os sous l'os masqué donne donc le même gel que H1 : estimé, non observé. - Rotation : en POV, ce n'est pas le contrôleur qui tourne la vue. C'est
FirstPersonSM, parAddWorldRotationcôté PC (CAM-B1e-CART.md:89) et par ZQSD côté C++ (LPCameraModeSubsystem.cpp:1642-1643). La POV gardera la rotation du composant, ou la rebasera sur le repère du buste : choix de la fiche POV. - Torse : les poses de scène le bougent beaucoup (couché, à genoux). Le repère complet du buste est donc nécessaire, et le lissage sans doute plus utile qu'en C1.
- Entrée en scène :
ClearC1HiddenOnSexEntryremetC1Hiddenà faux (LPCameraModeSubsystem.cpp:750-764).
- Cible de vue :
5. Interdits
- Aucun changement de Room, PC, GI, Person ni d'asset ; aucun MCP ; aucun changement du plan proche global (
DefaultEngine.ini:50). - Ne jamais écrire
VisibilityBasedAnimTickOption, appelerRecalcRequiredBonesouRecalcRequiredCurves, ni écrire la position ou l'attache deFirstPersonSM(composant partagé avec la POV) : la vue C1 passe par le modificateur seul. - Ne toucher à aucun maillage, ni à aucun contrôleur hors du
GetOwningPlayerController()du gestionnaire de caméra ; aucune écriture sur un PNJ. - Ne pas retirer ni modifier
LPStringCompat,LPMeshCompat,LPNavigationHelperset voisins (lp-ue-compat§ 4). - Pas de compilation par l'IMPL (fiche BUILD à
lp-builder), pas de commande git qui écrit, modification par Edit uniquement.
6. Valeurs moteur lues dans le moteur installé
Moteur D:\Game\Launcher\Engine\UE_5.8\Engine\Source\Runtime\ (préfixe omis : Engine/ sauf mention).
- Ordre d'une image :
Launch/Private/LaunchEngineLoop.cpp:5575(FEngineLoop::Tick),:5859(GEngine->Tick),:6103(FTSTicker::GetCoreTicker().Tick, seul appel de la fonction, après le monde) ;Private/LevelTick.cpp:1750-1778(groupes d'acteurs),:1816(minuteries),:1821(FTickableGameObject::TickObjects),:1835-1847(caméras « after all actors have been ticked »),:1877(TG_PostUpdateWork, après les caméras).
- Caméra :
Private/PlayerCameraManager.cpp:244-261:GetViewTargetrend la cible en attente pendant un fondu ;:287-307:ApplyCameraModifiers, modificateur sauté s'il est désactivé, et arrêt siModifyCamerarend vrai ;:446-464: modificateurs appliqués aprèsUpdateViewTargetInternal, sauf caméras de débogage ;:881-916: deuxUpdateViewTargetpendant un fondu.Classes/Camera/PlayerCameraManager.h:552(GetOwningPlayerController),:701(GetCameraRotation),:705(GetCameraLocation).Classes/Camera/CameraModifier.h:51(CameraOwner),:95(ModifyCamera(float, FMinimalViewInfo&)).
- Contrôleur :
Classes/GameFramework/Controller.h:159(GetControlRotation),:163(SetControlRotation). - Maillage :
Classes/Components/SkinnedMeshComponent.h::98-101:AlwaysTickPoseAndRefreshBones/AlwaysTickPose(« Refresh BoneTransforms only when rendered ») ;:1156:GetBoneIndex;:1223:GetSkinnedAsset;:1263:GetParentBone;:1360:GetSocketTransform(FName, ERelativeTransformSpace);:1516:GetBoneTransform;:2036-2053:GetRefPosePosition/GetRefPoseTransform, en espace local ;:2171:IsBoneHiddenByName, non const.
Private/Components/SkinnedMeshComponent.cpp:2506-2516:GetParentBonerendNAME_Noneà la racine ;:2727-2737:GetRefPoseTransform=GetRefBonePose()[i].Private/Components/SkeletalMeshComponent.cpp:2321-2332: un os visible reste requis.:2358-2408:RecalcRequiredBonesremet la pose locale à la référence (:2369) et ne recalcule l'espace composant que des os requis (DIAG § 3.2).Classes/Components/PrimitiveComponent.h:999:WasRecentlyRendered(float Tolerance = 0.2f) const.Classes/Engine/SkinnedAsset.h:57:GetRefSkeleton.
- Cœur :
Core/Public/Math/TransformNonVectorized.h:472-473:C = A * Bapplique A puis B.:532,:536,:539,:546:TransformPosition,InverseTransformPosition,TransformVectorNoScale,InverseTransformVectorNoScale;Core/Public/CoreGlobals.h:532:GFrameCounter;Core/Public/HAL/IConsoleManager.h:2028:TAutoConsoleVariable(Name, DefaultValue, Help, Flags).
- Jeu, lu dans les sources du projet :
LPCameraModeSubsystem.cpp::733-735(tickers),:787-790et:811-828(tangage),:873-881(modificateur toujours installé),:970-979(retrait),:1196-1231(ancien témoin),:1258-1268(ordre par image),:2620-2635(libellés POV et C1) ;LPDirectControlSubsystem.cpp:556-569(FirstPersonView,OnSeat) et:1538-1548(lacet de l'acteur en C1, hors siège) ;LPRoomBoundsCameraModifier.cpp:40-88.
7. Critères de VAL (commande → attendu)
7.1 Contrôles de code et de build (lp-validator, après BUILD par lp-builder)
git -C "D:\Game\LifePlay-master 5.8" diff --stat→ les 4 fichiers du § 2, et eux seuls.- Grep
VisibilityBasedAnimTickOption|RecalcRequiredBones|PerspectiveNearClipPlane|AddAdditiveOffset|lp\.C1NearClip|lp\.C1Forward|lp\.C1RefreshBonessurSource/LifePlay→ 0. Greplp\.C1(Stable|AlignYaw|Smooth|EyeSource)→ 4 définitions, toutes dansLPCameraModeSubsystem.cpp. GrepSetControlRotation→ les deux d'avant (LPCameraModeSubsystem.cpp:790,:822) plus une dansLPRoomBoundsCameraModifier.cpp. .\Tools\Build.ps1 -Lot ERR145(LifePlayEditor) → code 0, moduleLifePlaycompilé, 0 C4996 nouveau, « autres lignes : 20 ». Puis une build Development empaquetée neuve (BuildDebug.ps1,-ArchiveDirneuf) pour l'utilisateur.- Journal de l'essai :
.\Tools\LogStats.ps1 -Log <LifePlay.log> -Pattern 'Temoin C1', 'C1 stable'. Attendu :- une ligne « C1 stable : maillage … buste chestUpper » au chargement (nom d'os à confirmer) ;
- des lignes « etalonnage 1 » puis « 20 » avant la première entrée en C1 ;
- une ligne « lacet de controle » par entrée assis ;
- des lignes « Temoin C1 » à 1 s d'écart en C1.
7.2 Cahier de tests pour l'utilisateur
Essai dans la build ERR145 ; console = touche ². Pour chaque ligne : « vue correcte / non », une capture si non, et trois lignes « Temoin C1 » recopiées.
- T1, recette du DIAG § 1 : debout, attendre 5 s hors C1 (étalonnage), puis C1, puis s'asseoir sans quitter C1, sur une chaise « Sit »
puis sur la chaise du repas (
have_a_meal_in). Regarder autour, en bas, à l'horizontale.- Attendu : vue à hauteur des yeux du corps assis, corps jamais vu de dehors ; 0,5 s après l'assise, le regard se tourne droit devant.
- Témoin :
stable=1 appliquee=1,vue-ancre 0.0,assis=1, une ligne « C1 stable : lacet de controle … (assise en C1) ».
- T2, preuve par bascule : même recette avec
lp.C1Stable 0. Le défaut d'origine revient (vue restée à hauteur debout), etvue-ancredépasse 10 cm. Puislp.C1Stable 1: la vue revient à l'image suivante, etvue-ancrerepasse à0.0. - T3, cycles C assis (faits 2 et 3) : sur chacune des deux chaises, trois cycles C2 → C3 → C1, dont un avec un long passage en C2 en
tournant la souris.
- Attendu : chaque retour en C1 est bien placé et regarde droit devant, dès le premier cycle.
- Témoin :
ecart vue-bustedans ±10° sur la première ligne. Noterlacet acteurcontrelacet buste, pour mesurer l'écart.
- T4, lever, marche, plafond : assis en C1, regarder au plafond 3 s puis revenir ; se lever (Z) ; marcher en C1 ; s'arrêter.
- Attendu : aucun saut de vue ; caméra aux yeux debout ; le corps se tourne vers le regard au lever.
- Témoin :
assis=0,vue-ancre 0.0. Noter les lignes oùrendu=0.
- T5, stabilité, lissage, source :
- assis 30 s sans souris, sur la chaise du repas : la vue ne suit aucun mouvement de tête ;
lp.C1Smooth 0.15, puis marche en C1 : noter si c'est plus agréable ; remettre à 0 ;lp.C1EyeSource 1, puis2: noter tout écart de hauteur de vue ; remettre à 0 ;- si un lit le permet en C1, se coucher (R7).
- T6, non-régression :
- C2, C3, Overview, studio ;
- scène lancée depuis C1, puis POV sur le joueur et sur le partenaire (comportement d'avant), puis retour en C1 après la scène ;
- clic sur un objet en C1 (interaction juste) ;
lp.C1AlignYaw 0(lacet d'avant), puis2(alignement aussi debout, Q4).
7.3 Effet attendu sur ERR-106, ERR-111 et ERR-145
- ERR-145 (
Registre_Erreurs.md:152) : position corrigée par construction, quel que soit l'ordre et à chaque cycle (T1, T2, T3) ; lacet recentré à l'entrée assis (T3). - ERR-111 (
Registre_Erreurs.md:118), volet « C1 à côté de la tête après un lever » : corrigé, car la vue ne lit plus les yeux figés (T4 ; estimé fort). Les volets orientation au retour de scène et sortie de CharGen ne sont pas visés. - ERR-106 (
Registre_Erreurs.md:113), résidu « décalage après un lever » : corrigé pour la même raison (estimé fort).
Fichiers à commiter :
A Docs/Migration/etat/REL/ERR145/ERR145-SPEC-02.md(dépôt du jeu, brancheue5.8)
Tours : 27 / 60
Constats
- Le journal T0 n'a aucune animation de repas pour le joueur (seulement
fnt_new048-f-0-sit), alors que le récit parle d'une chaise avec repas. La correspondance entre cycles et chaises est donc estimée (§ 1.1). T1 et T3 nomment les deux chaises. - À 06:22:11.092 (l. 2748), le libellé « Camera: Studio » apparaît une fois pendant le passage C3 → C1. Sans lien établi avec ERR-145.
- Le premier cycle mauvais (fait 2) n'est pas expliqué par le code lu (§ 1.2). La conception le rend sans objet, et
yeux-ancrele chiffrera dans la build ERR145 si on le veut. - Écart à la fiche : la fiche demandait d'« étudier un décalage calibré (homme / femme) ou une constante ». La SPEC retient un décalage étalonné par maillage à l'exécution, avec repli sur la pose de référence, plutôt que deux constantes. Ainsi aucune valeur n'est à mesurer à la main, et les proportions propres au maillage sont couvertes.
- Il reste à vérifier par MCP en lecture (hors fiche) : le parent de
neckLowersur Michael et Bridget, et l'existence delCollar/rCollar. Le journal de la build l'écrira de toute façon (ligne « C1 stable : maillage … buste … »). - La copie du journal lue est dans le dossier temporaire de la session : elle n'est pas un livrable. Le journal d'origine reste à son chemin.