LifePlay-Revival/Docs/Migration/etat/REL/ERR145/ERR145-SPEC.md
2026-10-21 09:36:04 +02:00

35 KiB
Raw Blame History

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) :
	/** 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<USkeletalMeshComponent> 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 :
	// 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) :
// ERR145 : bascules de l'essai en jeu (console, touche ² ou ~, DefaultInput.ini:166-168), sans recompiler.
static TAutoConsoleVariable<int32> 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<float> 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<float> 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) :
// 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<ACharacter>(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<EVisibilityBasedAnimTickOption>(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<uint8>(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<int32>(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<ULPRoomBoundsCameraModifier>(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)

// 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<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;
	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<int32>(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) :
	/** 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<AActor> 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) :
	// 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 <cm>, 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 <LifePlay.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.