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 (Grep `fnt_|-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 sur `FirstPersonSM` avant `SetViewTargetWithBlend` (`CAM-B2s-IMPL.md:138`, `:124`). La minuterie fait de même à chaque passage (`:137`). - Côté C++, `ResetC1PitchOnExit` ne remet que le tangage (`LPCameraModeSubsystem.cpp:811-828`), et `TickC1Pitch` n'écrit que le tangage (`:787-790`). - É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 `CircleCamera` et `LeftRight`, à 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 de `CircleCamera` exige la vue `CamRoom` ou `CamC1On` (même source). Une entrée en C1 hérite donc du lacet laissé par C2. - Établi : assis, l'acteur n'est pas tourné par la marche. `Walk` ne pose le lacet de l'acteur que hors siège (`LPDirectControlSubsystem.cpp:1545-1547`). Le commentaire `:564` dit « son lacet est celui de la place ». Le joueur n'est pas possédé : `SetControlRotation` ne 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 `_35` est 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 buste` contre `lacet 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) 1. **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émoin `vue-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). 2. **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. 3. **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-buste` dans ±10° sur la première ligne qui suit ; l'utilisateur regarde droit devant sans toucher la souris. 4. **Rotation** : en C1, la rotation de la vue est celle du contrôleur seul (tangage, lacet), roulis nul ; jamais celle de la tête. 5. **Repli** : `lp.C1Stable 0` à la console rend la vue d'avant dès l'image suivante. 6. **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`), dans `FEngineLoop::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, dont `UpdateRoomCam` de Room (`:1816`), puis les caméras, « after all actors have been ticked » (`:1835-1847`) ; - dans la caméra, `UpdateViewTarget` appelle `CalcCamera` (vue de `FirstPersonSM`), puis `ApplyCameraModifiers` (`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. - Voies écartées : - (i) un `FTickableGameObject` (`LevelTick.cpp:1821`, entre les minuteries et les caméras) qui écrirait `FirstPersonSM` : 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:1877` contre `:1847`), donc avec une image de retard ; - (iii) attacher `FirstPersonSM` au 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++. - 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ée `class USkeletalMeshComponent;`. Avant `UCLASS()` (l. 15), une structure C++ simple, hors réflexion : ```cpp /** 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 Player; TWeakObjectPtr 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) : ```cpp /** 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 : ```cpp 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 rend `false` pour toute vue non bornée, C1 comprise : ```cpp // 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 : ```cpp 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) : ```cpp /** 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 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;` et `const float C1CalMaxSpeed = 10.f;` ; - retirer `C1Interval`, `C1MaxEyeDistance`, `C1MaxPitch`, `C1MaxYawGap` (l. 104, 106-108). Grep : ils ne servent qu'à l'ancien `WitnessC1` (l. 1203, 1220-1221) ; - garder `C1LogInterval` (l. 105), `C1PitchMin` / `C1PitchMax` (l. 132-133, utilisés par `TickC1Pitch`), `LeftEyeSocket` / `RightEyeSocket` (l. 102-103). - Après la fermeture de l'espace de noms (l. 728) : ```cpp // ERR145-SPEC-02 : bascules de l'essai en jeu (console, touche ² ou ~, DefaultInput.ini:166-168), sans recompiler. static TAutoConsoleVariable 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 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 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 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 suivent `TickC1Pitch` (l. 800-801) : ```cpp // 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(Manager->FindCameraModifierByClass(ULPRoomBoundsCameraModifier::StaticClass())) : nullptr; ACharacter* Player = Cast(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). ```cpp // 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(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIPlayerCharacter)); USkeletalMeshComponent* Mesh = IsValid(Player) ? Player->GetMesh() : nullptr; const USceneComponent* Camera = IsValid(Player) ? Cast(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(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 de `VisibilityBasedAnimTickOption`) : 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). - `BodyForward` par les clavicules seules : remplacé par `ComputeBodyYaw` (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 0` garde la position stable sans toucher au lacet ; - `lp.C1EyeSource 1` force 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 de `neckLower`, 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, et `FirstPersonSM`, 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` ; `ACharacter` est en `AlwaysTickPose`, `Character.cpp:125` cité par le DIAG). Son coût reste entier : deux recalculs des os requis par passage en C1, R4 de `ERR145-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`). `chestUpper` est attendu sur Genesis 8, mais c'est une **estimation** : la hiérarchie n'a pas été relue dans les assets. `neckLower` lui-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 1` sert 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 de `SexOn`. - **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++, Grep `FirstPersonSM` sur `Source/LifePlay` ne 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 0` coupe. 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 fausserait `C1CalForward` (estimé faible). Le repli clavicules est mesuré à part par le témoin (`buste` contre `clavicules` selon la source). - **R9** : compilation unitaire. Noms neufs uniques (`CVarLPC1*`, `FLPC1StableParams`) ou membres de classe ; aucun espace anonyme (`lp-build` § 8). - **R10** : un `FTSTicker` tourne aussi au menu principal. Sans `PlayerCharacter`, aucun paramètre n'est armé (`Player` nul). ### 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 ; `2` est 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]` de `DefaultEngine.ini` pour 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 `CamC1On` et hors `SexOn` (§ 2.4). Le libellé « POV » vient de `PC.POVOn` pendant `SexOn` (`LPCameraModeSubsystem.cpp:2620-2625`). Seule exception, une image au plus (R3). - **Réutilisable tel quel** : - `ComputeHeadAnchor`, `ComputeBodyYaw` et `ComputeRefLocalEye` : 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`). `FirstPersonSM` reste **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`, par `AddWorldRotation` cô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** : `ClearC1HiddenOnSexEntry` remet `C1Hidden` à faux (`LPCameraModeSubsystem.cpp:750-764`). ## 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`, appeler `RecalcRequiredBones` ou `RecalcRequiredCurves`, ni écrire la position ou l'attache de `FirstPersonSM` (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`, `LPNavigationHelpers` et 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` : `GetViewTarget` rend la cible en attente pendant un fondu ; - `:287-307` : `ApplyCameraModifiers`, modificateur sauté s'il est désactivé, et arrêt si `ModifyCamera` rend vrai ; - `:446-464` : modificateurs appliqués après `UpdateViewTargetInternal`, sauf caméras de débogage ; - `:881-916` : deux `UpdateViewTarget` pendant 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` : `GetParentBone` rend `NAME_None` à la racine ; `:2727-2737` : `GetRefPoseTransform` = `GetRefBonePose()[i]`. - `Private/Components/SkeletalMeshComponent.cpp:2321-2332` : un os visible reste requis. `:2358-2408` : `RecalcRequiredBones` remet 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 * B` applique 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-790` et `: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`) 1. `git -C "D:\Game\LifePlay-master 5.8" diff --stat` → les 4 fichiers du § 2, et eux seuls. 2. Grep `VisibilityBasedAnimTickOption|RecalcRequiredBones|PerspectiveNearClipPlane|AddAdditiveOffset|lp\.C1NearClip|lp\.C1Forward|lp\.C1RefreshBones` sur `Source/LifePlay` → 0. Grep `lp\.C1(Stable|AlignYaw|Smooth|EyeSource)` → 4 définitions, toutes dans `LPCameraModeSubsystem.cpp`. Grep `SetControlRotation` → les deux d'avant (`LPCameraModeSubsystem.cpp:790`, `:822`) plus une dans `LPRoomBoundsCameraModifier.cpp`. 3. `.\Tools\Build.ps1 -Lot ERR145` (LifePlayEditor) → code 0, module `LifePlay` compilé, 0 C4996 nouveau, « autres lignes : 20 ». Puis une build Development empaquetée neuve (`BuildDebug.ps1`, `-ArchiveDir` neuf) pour l'utilisateur. 4. Journal de l'essai : `.\Tools\LogStats.ps1 -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), et `vue-ancre` dépasse 10 cm. Puis `lp.C1Stable 1` : la vue revient à l'image suivante, et `vue-ancre` repasse à `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-buste` dans ±10° sur la première ligne. Noter `lacet acteur` contre `lacet 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`, puis `2` : 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), puis `2` (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, branche `ue5.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-ancre` le 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 `neckLower` sur Michael et Bridget, et l'existence de `lCollar` / `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.