Fiche ERR145-SPEC — 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 : C1 assis, os masqués figés — option A, témoin C1 et réglages de vue (2026-10-21) Ordonnateur : Big Boss (local_104ff22d). Étude seule : aucun code modifié, aucune compilation, ni copie UE, ni MCP, ni commit. Seul fichier écrit : ce livrable. Amont : `ERR145-DIAG.md` (H1), confirmation de l'utilisateur du 2026-10-21 07:11, puis fait nouveau relayé par Big Boss (capture 34 : le cycle C assis ne corrige pas la vue). ## 1. Objectif mesurable ### 1.1 Ce qui est établi avant d'écrire - **H1 (mécanisme)** : Room masque `neckLower` sur `PlayerCharacter.Mesh` (`etat/CAM/CAM-B1-IMPL.md:87`) ; un os masqué sort des os requis sauf si `VisibilityBasedAnimTickOption == AlwaysTickPoseAndRefreshBones` (`SkeletalMeshComponent.cpp:2321-2332`) ; `lEye` / `rEye` (sous `head`, sous `neckLower`) gardent leur pose en espace composant ; `UpdateRoomCam` pose la caméra au milieu de ces os à chaque passage (`CAM-B2k-IMPL.md:75`, nœud `SWL_512`). Confirmé en jeu par l'utilisateur à 07:11 (debout → C1 → assis). - **Journal de l'essai du 2026-10-21** (`D:\Game\LifePlay_Build_5.8_Development_COM23\Windows\LifePlay\Saved\Logs\LifePlay.log`, heure UTC, locale = UTC + 2) : - l. 1725 (05:08:56) : entrée en C1 debout ; l. 1736 : action « Sit » (`have_a_meal_in`) ; l. 1755-1757 (05:09:13) : assis au meuble `_196`, « racine Genesis8Female à 77 cm du bassin, à 9 cm du composant » ; l. 1758-1768 : C1 assis, tangage -11 à -57, lacet de contrôle de -175 à +187 (vue décapitée, cas ERR-145) ; - l. 1781 (05:11:21) C1 → C2 (`C1Hidden=0`, donc `RestoreC1` passé) ; l. 1784 (05:12:10) → C3 ; l. 1787-1790 (05:12:14.978) → C1, toujours assis (aucun `unOccupyFnt` entre l. 1757 et l. 1809) ; - l. 1791-1792 (05:12:15 à 05:12:16) : **tangage -0,1 puis -2,1**, lacet de contrôle +133 puis +86 ; plus aucune ligne « C1 souris » jusqu'à la sortie de C1 en l. 1800 (05:14:01). `TickC1Pitch` n'écrit qu'au mouvement vertical de la souris (`LPCameraModeSubsystem.cpp:782-786`) et c'est le seul code qui change le tangage hors sortie (`:790`, `:811-828`). - **Capture 34** (description de Big Boss) : C1 assis après le cycle, surface de peau cylindrique collée à gauche, haut violet et collier vus de très près à droite. Elle tombe dans la fenêtre 05:12:15 → 05:14:01 (07:12 → 07:14 locales), seule fenêtre « C1 assis après un cycle » du journal (estimé : l'heure exacte de la capture n'est pas dans le journal). ### 1.2 Conséquences du fait nouveau (établi / estimé) - **Établi** : après le cycle, les yeux ont été relus dans la pose assise du moment (os démasqués en C2 et C3, puis remasqués à l'entrée en C1 ; la voie C1 lit les yeux avant le masquage, `CAM-B1-IMPL.md:104-107`). C'est l'état que l'option A maintient en permanence pour une pose immobile. La vue reste mauvaise : **l'option A seule n'est pas prouvée suffisante**. - **Établi** : le tangage valait environ -2° pendant la capture. « Borner le tangage » ne traite pas ce cas, et l'hypothèse H2 du DIAG (regard vers le bas sur la coupe du cou) n'explique pas la capture 34. - **Estimé** (géométrie) : la largeur horizontale vue à 90° (FOV de `FirstPersonSM`, `CAM-B1e-CART.md:79`) ne couvre verticalement qu'environ ±29° en 16:9. Pour qu'une épaule ou un cou remplisse la gauche de l'image à un tangage de -2°, il faut que le point de vue soit **à hauteur d'épaule ou de cou**, pas à hauteur des yeux d'une tête droite. Deux lectures restent ouvertes : - **L1, gel à une phase basse** : la pose « repas » plonge peut-être la tête vers l'assiette en boucle. Le remasquage a figé les yeux à un instant où la tête était baissée. A corrige le gel, mais la caméra suivra alors la tête qui plonge et remonte. - **L2, point de vue trop près du corps visible**, yeux vivants : tête très penchée, regard de côté (lacet de contrôle +86, orientation du buste inconnue), ou sommets du cou à poids mixtes étirés (voir R8). Le tout avec un plan proche à **1 cm** (`Config/DefaultEngine.ini:50`, contre 10 cm par défaut dans `BaseEngine.ini:361`) : toute peau à plus de 1 cm est dessinée. - **Inconnu** : le contenu de l'animation `fnt_new017-f-0-use` (l. 1748) ; l'orientation du buste pendant la capture ; la distance réelle entre la caméra et la peau. ### 1.3 Objectifs (critères chiffrés au § 7) 1. **A** : en C1, les os masqués du joueur restent requis et animés. Le témoin montre « yeux-buste » dans la plage debout, assis comme debout, et la recette du DIAG § 1 ne montre plus le corps décapité vu de dehors. 2. **Témoin** : une ligne par seconde en C1. Elle mesure la vue contre un os **visible** (parent de `neckLower`) et contre l'avant du buste. Elle donne aussi les yeux, `assis=`, l'écart racine ↔ composant, l'option d'animation et l'état du masquage. 3. **Réglages de vue C1** (partie C, sans effet par défaut) : plan proche propre à la vue C1 et décalage du point de vue vers l'avant du buste, réglables à la console. Ils servent à trancher L1 / L2 dans la même build, sans second aller-retour. 4. PNJ, POV de sexe, C2, C3, Overview, studio : comportement inchangé. ## 2. Fichiers touchés, ordre Ordre : (1) `LPCameraModeSubsystem.h`, (2) `LPCameraModeSubsystem.cpp`, (3) `LPRoomBoundsCameraModifier.h`, (4) `LPRoomBoundsCameraModifier.cpp` (tous sous `D:\Game\LifePlay-master 5.8\Source\LifePlay\`). Les parties 3 et 4 portent seulement la partie C. Elles débordent du périmètre de la fiche, qui ne nommait que le sous-système (écart, voir Constats). Si l'orchestrateur ne retient pas C, on retire § 2.4, les appels à `SetC1View` et `CVarLPC1NearClip` / `CVarLPC1Forward` ; le témoin écrit alors `plan proche 0.0`. Volume estimé : partie A ≈ 45 lignes ; témoin ≈ 60 lignes, dont 36 remplacées ; partie C ≈ 35 lignes. Total ≈ 140 lignes touchées (≈ +115 / −30), aucune écriture d'asset, une compilation. ### 2.1 `LPCameraModeSubsystem.h` - Ajouter `class USkeletalMeshComponent;` aux déclarations avancées (l. 17-25). - Remplacer le commentaire du témoin C1 (l. 71) par : « Témoin C1 (ERR145) : une ligne par seconde en C1, vue mesurée contre le parent de neckLower (os visible) et l'avant du buste ». - Après `float C1Cooldown = 0.f;` (l. 74) : ```cpp /** ERR145 (option A) : en C1, les os masques par Room restent requis et animes sur le maillage du joueur * (AlwaysTickPoseAndRefreshBones) ; option d'origine rendue des que le libelle quitte C1 ou que le joueur change. * Transmet aussi les reglages de vue C1 au modificateur (partie C). Appele a chaque image depuis TickSwitchKey. */ void TickC1BoneRefresh(bool bC1Now); /** Maillage dont l'option a ete changee, et sa valeur d'origine (EVisibilityBasedAnimTickOption). */ TWeakObjectPtr C1RefreshMesh; uint8 C1SavedTickOption = 0; ``` ### 2.2 `LPCameraModeSubsystem.cpp` : partie A - Include : `#include "HAL/IConsoleManager.h"` (`TAutoConsoleVariable`, `IConsoleManager.h:2028`). - Dans `namespace LPCameraModeLocal`, après la l. 108 : ```cpp // ERR145 : os masque par Room en C1 (CAM-B1c) ; son parent (chestUpper attendu sur Genesis 8, lu a l'execution) est le // repere visible du temoin ; les clavicules donnent l'avant du buste. const FName NeckLowerBone(TEXT("neckLower")); const FName LeftCollarBone(TEXT("lCollar")); const FName RightCollarBone(TEXT("rCollar")); // Avant horizontal du buste : perpendiculaire a la ligne des clavicules (UE : X avant, Y droite, Z haut, donc // (droite - gauche) x haut = avant) ; a defaut, avant de l'acteur. OutFrom nomme la source pour le temoin. FVector BodyForward(const ACharacter* Player, const USkeletalMeshComponent* Mesh, const TCHAR*& OutFrom) { OutFrom = TEXT("acteur"); FVector Forward = Player->GetActorForwardVector(); if (Mesh->GetBoneIndex(LeftCollarBone) != INDEX_NONE && Mesh->GetBoneIndex(RightCollarBone) != INDEX_NONE) { const FVector Across = Mesh->GetSocketLocation(RightCollarBone) - Mesh->GetSocketLocation(LeftCollarBone); const FVector Candidate = FVector::CrossProduct(Across, FVector::UpVector).GetSafeNormal2D(); if (!Candidate.IsNearlyZero()) { Forward = Candidate; OutFrom = TEXT("clavicules"); } } return Forward.GetSafeNormal2D(); } ``` `BodyForward` doit être défini après les includes de `GameFramework/Character.h` et `Components/SkeletalMeshComponent.h` (déjà en l. 20 et 26). Il reste dans l'espace nommé, avec appel qualifié (règle C2374, `lp-build` § 8). - Après la fermeture de l'espace de noms (l. 728) : ```cpp // ERR145 : bascules de l'essai en jeu (console, touche ² ou ~, DefaultInput.ini:166-168), sans recompiler. static TAutoConsoleVariable CVarLPC1RefreshBones(TEXT("lp.C1RefreshBones"), 1, TEXT("ERR145 : 1 = en C1, les os masques du joueur restent animes (option A) ; 0 = comportement d'avant (os figes).")); static TAutoConsoleVariable CVarLPC1NearClip(TEXT("lp.C1NearClip"), 0.f, TEXT("ERR145 : plan proche de la vue C1, en cm (0 = plan global du projet, 1 cm).")); static TAutoConsoleVariable CVarLPC1Forward(TEXT("lp.C1Forward"), 0.f, TEXT("ERR145 : avance du point de vue C1 vers l'avant du buste, en cm (0 = aucune).")); ``` - Nouvelle méthode, placée après `ResetC1PitchOnExit` (l. 828) : ```cpp // ERR145 (option A, ERR145-SPEC § 2.2) : Room masque neckLower en C1 ; un os masque sort des os requis et sa pose reste // figee (SkeletalMeshComponent.cpp:2321-2332), or UpdateRoomCam pose la camera sur lEye / rEye, sous neckLower. En C1, // le maillage du joueur passe en AlwaysTickPoseAndRefreshBones : les os masques restent requis et animes, le rendu les // garde masques (SkeletalRender.cpp:516-520). Le drapeau bRequiredBonesUpToDate = faux fait recalculer les os requis au // prochain passage d'animation (SkeletalMeshComponent.cpp:1707-1712, 2879-2884), comme HideBone (:3950). void ULPCameraModeSubsystem::TickC1BoneRefresh(bool bC1Now) { UGameInstance* GI = GetGameInstance(); ACharacter* Player = bC1Now ? Cast(LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIPlayerCharacter)) : nullptr; if (!IsValid(Player)) { Player = nullptr; } USkeletalMeshComponent* Mesh = (Player && CVarLPC1RefreshBones.GetValueOnGameThread() != 0) ? Player->GetMesh() : nullptr; USkeletalMeshComponent* Applied = C1RefreshMesh.Get(); if (Applied && Applied != Mesh) { Applied->VisibilityBasedAnimTickOption = static_cast(C1SavedTickOption); Applied->bRequiredBonesUpToDate = false; UE_LOG(LogLifePlay, Log, TEXT("C1 : os du joueur rendus a l'option %d (sortie de C1, scene, joueur change ou lp.C1RefreshBones 0)"), C1SavedTickOption); Applied = nullptr; } if (!Applied) { C1RefreshMesh.Reset(); } if (Mesh && !Applied) { C1SavedTickOption = static_cast(Mesh->VisibilityBasedAnimTickOption); Mesh->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::AlwaysTickPoseAndRefreshBones; Mesh->bRequiredBonesUpToDate = false; C1RefreshMesh = Mesh; UE_LOG(LogLifePlay, Log, TEXT("C1 : os masques du joueur gardes animes (option %d -> %d ; neckLower masque=%d)"), C1SavedTickOption, static_cast(EVisibilityBasedAnimTickOption::AlwaysTickPoseAndRefreshBones), Mesh->IsBoneHiddenByName(LPCameraModeLocal::NeckLowerBone) ? 1 : 0); } // Partie C (§ 2.4) : reglages de vue C1 transmis au modificateur a chaque image ; nul hors C1. 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; if (Modifier) { FVector Offset = FVector::ZeroVector; const float Forward = CVarLPC1Forward.GetValueOnGameThread(); if (Player && Player->GetMesh() && Forward != 0.f) { const TCHAR* From = nullptr; Offset = LPCameraModeLocal::BodyForward(Player, Player->GetMesh(), From) * Forward; } Modifier->SetC1View(Player, CVarLPC1NearClip.GetValueOnGameThread(), Offset); } } ``` - `TickSwitchKey`, après la l. 1264 (`ResetC1PitchOnExit(...)`) : `TickC1BoneRefresh(FrameLabel == LPCameraModeLocal::LabelC1);`. Le libellé est calculé à chaque image (l. 1261). Il vaut C1 seulement si la vue est `PlayerCharacter` et `CamC1On` (l. 2632). Pendant une scène, il vaut « Scene » ou « POV » (l. 2620-2627). L'entrée en scène rend donc l'option dans la même image ; aucun autre point d'appel n'est nécessaire. Un menu ouvert en C1 garde le libellé C1 et l'option : voulu, car la vue reste C1 derrière le menu. - `Deinitialize`, avant `RemoveViewBounds` (l. 744) : `TickC1BoneRefresh(false);` (rend l'option si le jeu se ferme en C1). ### 2.3 `LPCameraModeSubsystem.cpp` : témoin C1 (remplace `WitnessC1`, l. 1196-1231) ```cpp // ERR145 : une ligne par seconde en C1, sans condition. L'ancien ecart aux yeux etait aveugle : Room pose la camera sur ces // memes yeux (ERR145-DIAG § 3.3). Reperes : parent de neckLower (os visible) et avant du buste (clavicules). void ULPCameraModeSubsystem::WitnessC1(float DeltaTime) { C1Cooldown = FMath::Max(0.f, C1Cooldown - DeltaTime); if (C1Cooldown > 0.f) { return; } C1Cooldown = LPCameraModeLocal::C1LogInterval; const UGameInstance* GI = GetGameInstance(); const UWorld* World = GI ? GI->GetWorld() : nullptr; const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; const 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; const FName Chest = Mesh ? Mesh->GetParentBone(LPCameraModeLocal::NeckLowerBone) : NAME_None; if (!PC || !Mesh || !IsValid(Camera) || Chest.IsNone()) { return; } const FVector ChestAt = Mesh->GetSocketLocation(Chest); const FVector Eyes = (Mesh->GetSocketLocation(LPCameraModeLocal::LeftEyeSocket) + Mesh->GetSocketLocation(LPCameraModeLocal::RightEyeSocket)) * 0.5f; const FVector CameraAt = Camera->GetComponentLocation(); const FVector ViewAt = PC->PlayerCameraManager ? PC->PlayerCameraManager->GetCameraLocation() : CameraAt; const TCHAR* From = nullptr; const FVector Forward = LPCameraModeLocal::BodyForward(Player, Mesh, From); const FVector Right = FVector::CrossProduct(FVector::UpVector, Forward); const FVector Rel = ViewAt - ChestAt; const FRotator CameraRotation = Camera->GetComponentRotation(); const FVector Root = Mesh->GetBoneLocation(Mesh->GetBoneName(0)); const UObject* RoomObject = LPCameraModeLocal::ReadObject(GI, LPCameraModeLocal::GIRoom); UE_LOG(LogLifePlay, Log, TEXT("Temoin C1 : vue-%s %.0f cm (avant %+.0f, droite %+.0f, haut %+.0f) ; yeux-buste %.0f cm ; camera-yeux %.0f cm ; vue-camera %.0f cm ; lacet vue-buste %+.0f (%s) ; tangage %+.0f ; assis=%d ; racine-composant %.0f cm ; option os %d ; cou masque=%d ; plan proche %.1f ; minuterie=%d"), *Chest.ToString(), Rel.Size(), FVector::DotProduct(Rel, Forward), FVector::DotProduct(Rel, Right), Rel.Z, FVector::Dist(Eyes, ChestAt), FVector::Dist(CameraAt, Eyes), FVector::Dist(ViewAt, CameraAt), FRotator::NormalizeAxis(CameraRotation.Yaw - Forward.Rotation().Yaw), From, FRotator::NormalizeAxis(CameraRotation.Pitch), IsValid(LPCameraModeLocal::ReadObject(Player, LPCameraModeLocal::PlayerOccupyingFnt)) ? 1 : 0, FVector::Dist(Root, Mesh->GetComponentLocation()), static_cast(Mesh->VisibilityBasedAnimTickOption), Mesh->IsBoneHiddenByName(LPCameraModeLocal::NeckLowerBone) ? 1 : 0, CVarLPC1NearClip.GetValueOnGameThread(), LPCameraModeLocal::TimerActive(IsValid(RoomObject) ? RoomObject : nullptr, LPCameraModeLocal::RoomCamTimer, World) ? 1 : 0); } ``` - Constantes devenues orphelines (`C1Interval`, `C1MaxEyeDistance`, `C1MaxPitch`, `C1MaxYawGap`, l. 104-108) : l'IMPL les cherche par Grep et retire celles qui ne servent plus. Le préfixe « Temoin C1 : » est gardé pour les recherches de journal. - **Plages attendues** (estimées sur les proportions Genesis 8, non mesurées ; la VAL les cale sur la première ligne debout) : | Champ | Debout, A actif | Assis, A actif | A coupé (`lp.C1RefreshBones 0`) après debout → assis | |---|---|---|---| | `yeux-buste` | 20 à 35 cm (référence R0) | R0 ± 5 cm (la chaîne du cou est rigide) | s'écarte de R0 de plus de 10 cm (COM17 : racine à 104 cm du composant) | | `camera-yeux` | 0 à 3 cm (minuterie de 0,016 s, une image de retard) | 0 à 3 cm | 0 à 3 cm (la caméra suit les yeux figés : aveugle, comme l'ancien témoin) | | `vue-buste` haut / avant | +15 à +30 / 0 à +15 cm | idem si la tête est droite ; haut < +10 = tête baissée (L1 / L2) | quelconque | | `vue-camera` | 0 (= `lp.C1Forward` si réglé) | idem | idem | | `lacet vue-buste` | libre (souris X) ; au-delà de ±90 = regard par-dessus l'épaule | idem | idem | | `option os` / `cou masque` | 0 / 1 | 0 / 1 | 1 / 1 (valeur d'`ACharacter`, `Character.cpp:125` cité par le DIAG) | ### 2.4 Partie C : réglages de vue C1 (`LPRoomBoundsCameraModifier.h/.cpp`) - `.h`, section publique, à côté de `SetBounds` (l. 28) : ```cpp /** ERR145-SPEC § 2.4 : vue C1 (cible de vue = joueur) : plan proche propre en cm (0 = plan global) et decalage du point * de vue en coordonnees monde. Joueur nul = rien. Rafraichi a chaque image par LPCameraModeSubsystem. */ void SetC1View(AActor* InPlayer, float InNearClip, const FVector& InOffset); ``` Section privée : `TWeakObjectPtr C1View; float C1NearClip = 0.f; FVector C1Offset = FVector::ZeroVector;` - `.cpp` : `SetC1View` recopie les trois valeurs. Dans `ModifyCamera`, après le bloc Studio (l. 42-47) et **avant** le filtre des l. 50-54 (qui rend `false` pour toute vue non bornée, C1 comprise) : ```cpp // ERR145 : vue C1 seulement (le joueur est aussi cible de vue en POV : SetC1View recoit nul hors C1). const AActor* C1 = C1View.Get(); if (C1 && CameraOwner && CameraOwner->GetViewTarget() == C1) { if (C1NearClip > 0.f) { InOutPOV.PerspectiveNearClipPlane = C1NearClip; } InOutPOV.Location += C1Offset; } ``` - Pourquoi un modificateur et pas `FirstPersonSM` : `UCameraComponent` n'a pas de plan proche en perspective en 5.8, seulement `OrthoNearClipPlane` (`CameraComponent.h:84-86`). Son décalage additif (`:221`, `:311`, `:330-334`) resterait posé sur un composant partagé avec la POV de sexe (`CAM-B1e-CART.md:89`). Le champ `FMinimalViewInfo::PerspectiveNearClipPlane` (`CameraTypes.h:92-94`) est pris par le calcul de projection (`CameraStackTypes.cpp:342`, appelé par `LocalPlayer.cpp:1285`). Il ne touche que la vue, pas le plan global. Le modificateur est toujours installé, quel que soit le libellé (`LPCameraModeSubsystem.cpp:873-881`). ## 3. Repli - **Sans build** : `lp.C1RefreshBones 0` à la console rend l'option d'origine dès l'image suivante (§ 2.2) ; `lp.C1NearClip 0` et `lp.C1Forward 0` (valeurs par défaut) annulent la partie C. En Shipping, sans console, ces variables gardent leur défaut. - **Code** : instantané pris 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 Person, ni aucun asset. ## 4. Risques R#, questions Q# ### 4.1 L'option A suffit-elle ? (réponse à Big Boss) **Non prouvé, et la capture 34 dit plutôt non pour la pose « repas ».** A est **nécessaire** : le mécanisme est établi et confirmé en jeu, et c'est lui qui donne la vue décapitée de dehors (COM17 ; COM23, l. 1757-1768). Mais le cycle C a produit, pour cette pose, l'état que A maintient, et la vue est restée mauvaise à un tangage de -2°. Mesures proposées par Big Boss, jugées sur le code : | Mesure | Effet sur la capture 34 | Verdict | |---|---|---| | Borner le tangage | aucun : tangage -0,1 / -2,1 (l. 1791-1792), borne actuelle -60 (`LPCameraModeSubsystem.cpp:132`) jamais atteinte dans la fenêtre | écartée (et contraire à la décision A1-R « tous les axes », `:766`) | | Masquer plus haut (`neckUpper`) | laisse `neckLower` visible, donc de la peau **plus près** des yeux | écartée (aggrave, estimé) | | Masquer plus bas (`chestUpper`) | masque aussi les clavicules et les bras, s'ils descendent de `chestUpper` (estimé, hiérarchie Genesis 8 non relue) ; c'est un changement de Room (Blueprint), pas du C++ | écartée | | Plan proche propre à C1 (C, `lp.C1NearClip`) | retire toute peau à moins de N cm du point de vue (projet : 1 cm, `DefaultEngine.ini:50`) ; ne déplace rien | **1er complément**, réglage en jeu | | Décalage vers l'avant (C, `lp.C1Forward`) | sort le point de vue du volume du buste quel que soit le lacet, s'il suit l'avant **du buste** ; s'il suivait le regard, il pousserait la vue dans l'épaule en regardant de côté | **2e complément**, réglage en jeu | Recommandation : livrer A, le témoin et C (C sans effet par défaut) dans la **même build**. On choisit les valeurs par défaut de C d'après les essais T0 (sans build) et T6 (§ 7.2). Décision par les chiffres du témoin, A actif, assis, vue mauvaise : - `yeux-buste` hors de R0 ± 5 → A n'agit pas (option non posée, voir `option os`) : retour à l'IMPL ; - `haut` < +10 (tête baissée, L1 ou L2) → `lp.C1Forward` 10 à 20, ou plan proche 10 à 15 ; - `haut` ≥ +15 et |lacet| > 90 → regard par-dessus l'épaule : plan proche ; une borne de lacet relative au buste serait une mesure nouvelle, hors de cette fiche ; - `haut` ≥ +15, |lacet| ≤ 60, vue mauvaise → peau étirée du cou (R8) : plan proche ; - vue bonne → A suffit ; C reste à 0. ### 4.2 Risques - **R1** : A seul ne corrige pas la capture 34 (§ 4.1) ; couvert par C et le témoin. - **R2** : avec A, la caméra suit la tête animée : plongée vers l'assiette si la pose « repas » en a une (L1), petits mouvements de tête des poses de repos. Gêne possible ; réponse par `lp.C1Forward` ou plus tard un lissage (Q2). - **R3** : 1 à 3 images de gel à l'entrée en C1. Room masque, attend une image, puis change la vue (`CAM-B1-IMPL.md:104-105`) ; le libellé C1 n'est vu qu'après ce changement (l. 2632). Les yeux viennent d'être lus : écart négligeable (estimé). - **R4** : A demande deux recalculs des os requis par passage en C1, un à l'entrée et un à la sortie, le même chemin que `HideBone` / `UnHideBone` (`SkeletalMeshComponent.cpp:3950`, `:3983`). `RecalcRequiredBones` remet la pose locale à la référence (`:2369`) avant l'évaluation de la même image (`:1707-1712` puis `:2879-2884`). Pas d'image en pose de référence attendue (estimé). C'est la raison pour laquelle on n'appelle **pas** `RecalcRequiredBones` hors du passage d'animation. - **R5** : coût : les os de la tête (visage compris) redeviennent évalués en C1, comme hors C1. `AlwaysTickPoseAndRefreshBones` rafraîchit même sans rendu (`SkinnedMeshComponent.cpp:1805-1808`), mais le corps est rendu en C1 : surcoût nul attendu (estimé). - **R6** : avec un plan proche au-dessus de 1 cm, un mur à moins de N cm du point de vue est traversé du regard. Les passes qui liraient `GNearClippingPlane` directement plutôt que la vue ne sont pas relues (incertain). Le risque n'existe que si `lp.C1NearClip` > 0. - **R7** : la POV de sexe n'est pas couverte. PC masque aussi `neckLower` sur `CurrentActor` (`CAM-B1e-CART.md:62-63`) et y attache `FirstPersonSM` à `head`, un os sous l'os masqué : même gel possible (estimé, non observé). Voir Q3. - **R8** : les sommets à poids mixtes du cou (`chestUpper` et `neckLower`) sont dessinés entre leur place et l'**origine** de la matrice du parent. En effet `ApplyScale(0)` = `ScaleMatrix * M` annule les trois axes et garde la ligne de translation (`Matrix.inl:897-905` ; `SkeletalRender.cpp:486`, `:519`). Ce point est solidaire de `chestUpper`, à l'endroit où se trouvait l'origine du modèle en pose de liaison, donc loin sous le buste. D'où un « tube » de peau étiré (les « lambeaux » de `CAM-B1e-CART.md:72`). A ne le change pas, C ne le cache que près de la vue. Candidat (estimé) pour la « surface de peau cylindrique » de la capture 34. - **R9** : sans os `lCollar` / `rCollar`, `BodyForward` prend l'avant de l'acteur ; le témoin l'écrit (`acteur`). Une pose de meuble qui tourne le corps par rapport à l'acteur fausserait alors `avant` et `lacet` : sans effet sur A. - **R10** : compilation unitaire. Les noms neufs sont uniques (`CVarLPC1*`) ou dans l'espace nommé `LPCameraModeLocal` (`lp-build` § 8). ### 4.3 Questions pour l'utilisateur - **Q1** : quelles valeurs par défaut pour `lp.C1NearClip` et `lp.C1Forward` dans la build d'essai ? Proposition : 0 (aucun effet). Réglage à la console pendant T6, puis valeur retenue écrite dans le code, ou dans `[SystemSettings]` de `DefaultEngine.ini`, pour la Shipping. - **Q2** : la vue C1 doit-elle suivre les mouvements de tête des animations, au risque de plonger avec la tête dans la pose « repas » ? Si non, la position doit venir d'un os du buste plus un décalage fixe (option B du DIAG, voir `vue-buste` du témoin). - **Q3** : étendre A à la POV de sexe (personne filmée = joueur ou PNJ) dans une fiche à part ? ## 5. Interdits - Aucun changement de Room, PC, GI, Person ni d'asset ; aucun MCP ; aucun changement du plan proche global (`DefaultEngine.ini:50`). - Ne toucher à aucun maillage autre que `GI.PlayerCharacter->GetMesh()`. Les PNJ gardent leur option : la propriété est par instance de composant (`SkinnedMeshComponent.h:793-794`). - Pas d'appel direct à `RecalcRequiredBones` ni à `RecalcRequiredCurves` (R4) ; pas de décalage additif sur `FirstPersonSM` (§ 2.4). - 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). - `Classes/Components/SkinnedMeshComponent.h:94-113` : `EVisibilityBasedAnimTickOption` ; `AlwaysTickPoseAndRefreshBones` = « Always Tick and Refresh BoneTransforms whether rendered or not ». Lignes `:787-794` : `VisibilityBasedAnimTickOption` est publique, `UPROPERTY(… BlueprintReadWrite, Config …)`, sans setter (Grep sur `Classes/Components` : seulement `:794`, `:797-798`, et des commentaires). Lignes `:1156`, `:1165` : `GetBoneIndex`, `GetBoneName` ; `:1263` : `GetParentBone` ; `:2014` : `GetBoneLocation` ; `:2153`, `:2161`, `:2171` : `HideBoneByName`, `UnHideBoneByName`, `IsBoneHiddenByName` (non const) ; `:1759-1772` : états de visibilité. - `Classes/Components/SkeletalMeshComponent.h:894-896` : `bRequiredBonesUpToDate` est public et transitoire ; à faux, les os requis sont recalculés au prochain passage. Lignes `:1862-1868` : `RecalcRequiredBones` (public) ; `:1885` : `ExcludeHiddenBones` ; `:1898` : `RecalcRequiredCurves`. Aucune API `MarkRequiredCurvesDirty` à appeler : `RecalcRequiredBones` marque les courbes à jour (`SkeletalMeshComponent.cpp:2400`) après le recalcul des instances d'animation (`:2383-2396`). - `Private/Components/SkeletalMeshComponent.cpp:1707-1718` (passage d'animation) et `:2879-2890` (`RefreshBoneTransforms`) : recalcul si le drapeau est faux. Lignes `:2246-2258` : les os demandés par les suiveurs sont ajoutés, puis les os masqués purgés. Lignes `:2296-2336` : `ExcludeHiddenBones` garde un os s'il est `BVS_Visible` **ou** si l'option vaut `AlwaysTickPoseAndRefreshBones` (`:2321-2322`). Lignes `:2358-2408` : `RecalcRequiredBones`. Lignes `:3933-3965` et `:3967-3983` : `HideBone` / `UnHideBone` mettent le drapeau à faux, sauf sur un suiveur (`:3942-3945`, `:3976-3979`). - `Private/Components/SkinnedMeshComponent.cpp:2119-2123` : un suiveur ne met pas à jour sa visibilité. Lignes `:2124-2163` : propagation `BVS_HiddenByParent`. Lignes `:4424-4448`, `:4482-4500` : masquage par nom et par index. Lignes `:2413-2434` et `:2468-2474` : position d'un os = transformée en espace composant (celle du leader pour un suiveur) × `LocalToWorld`. Lignes `:1805-1808`, `:1881-1897` : rafraîchissement selon le rendu ou l'option. - `Private/SkeletalRender.cpp:453`, `:458-466` : le rendu parcourt les os actifs de la LOD (`ActiveBoneIndices`), **pas** les os requis. Lignes `:483-487` (suiveur, états du leader) et `:516-520` (sans leader) : un os non visible reçoit la matrice du parent à l'échelle 0. **Un os masqué mais requis reste invisible au rendu.** Établi. - `Core/Public/Math/Matrix.inl:897-905` : `ApplyScale` = `ScaleMatrix * M` ; la ligne de translation de M est gardée (R8). - `Classes/GameFramework/Character.h:487` : `GetMesh() const` rend un pointeur non const. `Classes/Camera/PlayerCameraManager.h:705` : `GetCameraLocation() const`. `Classes/Camera/CameraModifier.h:95` : `ModifyCamera(float, FMinimalViewInfo&)`. - `Classes/Camera/CameraTypes.h:92-94`, `:178`, `:216-219` : `PerspectiveNearClipPlane` (négatif = plan global, -1 par défaut). `Private/Camera/CameraStackTypes.cpp:342` (et `:210`) : pris par la projection. `Private/LocalPlayer.cpp:1285` : appel de la vue du joueur. `Classes/Camera/CameraComponent.h:84-86` : seul un plan proche orthographique existe sur la caméra ; `Private/Camera/CameraComponent.cpp:448-461` : décalage additif. - `Private/UnrealEngine.cpp:19090-19102` : commande console `r.SetNearClipPlane `, minimum 1, 20 sans argument, réglage **global**. Aucune garde de compilation lue entre les l. 19050 et 19102 ; présence en Development estimée. - `Core/Public/HAL/IConsoleManager.h:2028` : `TAutoConsoleVariable(const TCHAR* Name, const T& DefaultValue, const TCHAR* Help, uint32 Flags = ECVF_Default)`. - Configuration : `Engine/Config/BaseEngine.ini:361` `NearClipPlane=10.0` ; projet `Config/DefaultEngine.ini:50` `NearClipPlane=1.000000` ; console `Config/DefaultInput.ini:166-168` (`Tilde`, `²`). - Leader et suiveurs dans le jeu : Room masque sur `PlayerCharacter.Mesh` = `CharacterMesh0` (`CAM-B1-IMPL.md:87` ; `CAM-B1e-CART.md:48`, `:61`). Ce maillage est le leader de `HairSM`, `PenisSM` et des tenues (`SetLeaderPoseComponent(Mesh)`, `CAM-B1e-CART.md:52-57`). Les suiveurs lisent les transformées et la visibilité du leader : A ne demande rien sur eux. ## 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 seulement, ou 2 si C n'est pas retenue. 2. Grep `VisibilityBasedAnimTickOption` sur `Source/` → occurrences dans `LPCameraModeSubsystem.cpp` seulement. Grep `RecalcRequiredBones|ApplyScale|AddAdditiveOffset` sur `Source/LifePlay` → 0. 3. `.\Tools\Build.ps1 -Lot ERR145` (LifePlayEditor) → code 0, module `LifePlay` compilé, 0 C4996 nouveau, « autres lignes : 20 ». Puis 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 : os'` → une ligne « gardes animes » par entrée en C1, une ligne « rendus » par sortie, et des lignes « Temoin C1 » à 1 s d'écart en C1. ### 7.2 Cahier de tests pour l'utilisateur Essai dans une build donnée ; console = touche ² ; chaque ligne rapporte « vue correcte / non », plus une capture si non. - **T0, sans build, sur COM23, maintenant** (oriente le défaut de C, Q1) : - T0a : s'asseoir à la même chaise « repas », en C1. Faire trois cycles C (C2 → C3 → C1) espacés de 5 s, en regardant à l'horizontale. La vue change d'un cycle à l'autre (plus ou moins basse) → L1 (gel à une phase de la boucle) ; toujours la même → L2. - T0b : dans la vue mauvaise, `r.SetNearClipPlane 10`, puis `r.SetNearClipPlane 20` ; noter si la peau proche disparaît. Remettre ensuite `r.SetNearClipPlane 1` (valeur du projet), ou relancer le jeu. - T0c : même essai sur une chaise sans repas (autre action « Sit »). - **T1, recette du DIAG § 1** (build ERR145) : debout → C1 → s'asseoir sans quitter C1 → regarder autour, en bas, à l'horizontale. Attendu : vue depuis les yeux, plus de corps décapité vu de dehors. Témoin : `yeux-buste` = R0 ± 5, `option os 0`, `cou masque=1`. - **T2, preuve par bascule** : même recette avec `lp.C1RefreshBones 0` → le défaut d'origine revient, et `yeux-buste` s'écarte de R0 de plus de 10 cm. Puis `lp.C1RefreshBones 1` → la vue revient aux yeux dans la seconde. - **T3, lever en C1** (ERR-111, ERR-106) : assis en C1, se lever (Z) sans quitter C1. Attendu : caméra aux yeux, pas à côté de la tête ; témoin : `yeux-buste` = R0 ± 5 et `assis=0`. - **T4, cycle C** debout et assis : aucun écart ; une ligne « C1 : os … rendus » à chaque sortie de C1. - **T5, PNJ et scènes inchangés** : en C2, PNJ assis et en marche (tête visible, aucun changement) ; lancer une scène depuis C1 (ligne « rendus » à l'entrée), POV sur le partenaire (tête normale), retour en C1 après la scène. - **T6, compléments** (si T1 assis reste mauvais, pose « repas ») : `lp.C1NearClip 10`, puis 15 ; remettre à 0 ; `lp.C1Forward 10`, puis 20 ; remettre à 0. Noter le réglage qui donne une vue correcte et recopier trois lignes « Temoin C1 » de chaque réglage. ### 7.3 Effet attendu sur ERR-106 et ERR-111 - **ERR-111** (`Registre_Erreurs.md:118`). Volet « C1 à côté de la tête après un lever … un cycle C corrige » : même mécanisme dans le sens inverse (yeux figés assis, corps levé). **Corrigé par A** (estimé fort), preuve par T3. Volets « vue au plafond au retour d'une scène » (orientation, B2s) et « à-coup à la sortie de CharGen » : sans effet attendu de A. - **ERR-106** (`Registre_Erreurs.md:113`). Résidu « décalage après un lever » : **corrigé par A** (estimé fort). Le recalage de B2k à chaque passage de minuterie lit enfin des yeux vivants. La partie principale (caméra posée une seule fois) était déjà corrigée par B2k. - **ERR-145** (`Registre_Erreurs.md:152`) : volet « entré debout puis assis » corrigé par A (T1, T2) ; volet « capture 34 » (assis, après cycle) : A plus un complément C, selon T0 et T6. Fichiers à commiter : - `A Docs/Migration/etat/REL/ERR145/ERR145-SPEC.md` (dépôt du jeu, branche `ue5.8`) Tours : 30 / 60 ## Constats - Écart au périmètre de la fiche : la partie C touche `LPRoomBoundsCameraModifier.h/.cpp`, non nommés par la fiche. Raison : aucun plan proche par vue n'existe sur `UCameraComponent` en 5.8 (§ 2.4). Partie séparable, décision de l'orchestrateur. - Écart au DIAG : la plage du témoin « 15 à 40 cm » (`ERR145-DIAG.md:103`) devient « R0 ± 5 », calée sur la première ligne debout de la build. Les 20 à 35 cm sont une estimation Genesis 8 non mesurée. - Le journal COM23 contient aussi, à l'action « Sit », trois `Divide by zero: Divide_DoubleDouble` de GI (l. 1740-1745) : sans lien établi avec ERR-145. - Il reste à vérifier, par MCP en lecture (hors fiche) : la hiérarchie Genesis 8 sous `chestUpper` (`get_bone_children`), et l'existence de `lCollar` / `rCollar` sur Michael et Bridget. Le témoin écrit aussi `clavicules` ou `acteur`.