Compare commits
4
Commits
v0.1.5
...
304c1e9afd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
304c1e9afd | ||
|
|
05c1488c00 | ||
|
|
96238bdb6a | ||
|
|
817313aabd |
No files matched your search
@@ -210,8 +210,8 @@ Any change that affects the game's appearance — meshes, materials, lighting, l
|
|||||||
|
|
||||||
1. Run `Tools/render_screenshots.sh` (rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from current content, runs the game on it, captures the fixed camera list, copies the PNGs into `Screenshots/`).
|
1. Run `Tools/render_screenshots.sh` (rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from current content, runs the game on it, captures the fixed camera list, copies the PNGs into `Screenshots/`).
|
||||||
2. Optional pre-check with a vision model: `Tools/judge_screenshot.py --image <shot.png> --expect "<what it should show>"` (or `--shots` to run every shot in the README) flags likely-missing content before a human looks. It reads `OPENROUTER_API_KEY` from `.env` (ignored). Small helper only — it never replaces the human verdict.
|
2. Optional pre-check with a vision model: `Tools/judge_screenshot.py --image <shot.png> --expect "<what it should show>"` (or `--shots` to run every shot in the README) flags likely-missing content before a human looks. It reads `OPENROUTER_API_KEY` from `.env` (ignored). Small helper only — it never replaces the human verdict.
|
||||||
3. Update `Screenshots/README.md`: one row per screenshot with what it shows and an approval state; describe any new shots or scene changes in the same change.
|
3. Update `Screenshots/README.md` **in the same change as any visual work** — every change that affects appearance, not only new shots: the image is **embedded inline** (``) with its view, expected content, verification and approval state, plus one row in the status table at the top. The review happens in the file; a path alone is not a review, and a shot whose README section is stale is a defect.
|
||||||
4. The PNGs **are committed** so reviewers see exactly what the build rendered (`.gitignore` does not exclude them). Keep them in sync with this README: drop retired shots in the same change.
|
4. The PNGs **are committed** so reviewers see exactly what the build rendered (`.gitignore` does not exclude them). Keep them in sync with this README: drop retired shots in the same change.
|
||||||
5. Wait for the user's approval before treating a visual change as done; record approval by ticking the table in `Screenshots/README.md`.
|
5. Wait for the user's approval before treating a visual change as done; record approval by ticking both the shot's section and its row in the status table in `Screenshots/README.md`.
|
||||||
|
|
||||||
New content that needs a different camera angle gets a new entry in the shot list in `Tools/ue_build_review_map.py`.
|
New content that needs a different camera angle gets a new entry in the shot list in `Tools/ue_build_review_map.py`.
|
||||||
@@ -25,7 +25,9 @@ C++ classes in `Game/GigLine/Source/GigLine/`:
|
|||||||
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
|
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
|
||||||
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
|
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
|
||||||
|
|
||||||
Input assets in `/Game/Input/`: `IA_Move`, `IA_MoveBack`, `IA_Strafe`, `IA_StrafeLeft`, `IA_Look`, `IA_Jump`, `IA_Reset`, `IA_Pause`, bound in `IMC_Default`.
|
Input assets in `/Game/Input/`: `IA_Move`, `IA_MoveBack`, `IA_Strafe`, `IA_StrafeLeft`, `IA_Look`, `IA_Jump`, `IA_Reset`, `IA_Pause`, `IA_ToggleCamera`, bound in `IMC_Default`.
|
||||||
|
|
||||||
|
Player-facing bindings: **WASD** move, **mouse** look, **Space** jump, **R** reset, **Escape** pause, **V** swap first/third person. The camera and body work landed with `0.2.0` (see ADR-9): the shipped default view stays first person, and `-thirdperson` starts the pawn in the third-person rig — used by the screenshot run so the review is reproducible.
|
||||||
|
|
||||||
### Site
|
### Site
|
||||||
|
|
||||||
@@ -56,6 +58,7 @@ Python scripts in `Tools/` drive the content pipeline:
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `export_*`, `hero_facade_build` | Blender headless export (FBX) |
|
| `export_*`, `hero_facade_build` | Blender headless export (FBX) |
|
||||||
| `ue_import_*`, `ue_hero_facade_import` | Unreal headless import + placement |
|
| `ue_import_*`, `ue_hero_facade_import` | Unreal headless import + placement |
|
||||||
|
| `character_anim_build`, `ue_character_anim_import` | Character animation clips (`GigLine_Idle`, `GigLine_Walk`) and their import |
|
||||||
| `ue_build_demo_map`, `ue_build_review_map` | Map assembly (lighting, meshes, player start) |
|
| `ue_build_demo_map`, `ue_build_review_map` | Map assembly (lighting, meshes, player start) |
|
||||||
| `ue_create_input_assets` | Enhanced Input actions + mapping context |
|
| `ue_create_input_assets` | Enhanced Input actions + mapping context |
|
||||||
| `render_screenshots.sh` | Screenshot capture pipeline |
|
| `render_screenshots.sh` | Screenshot capture pipeline |
|
||||||
|
|||||||
@@ -55,3 +55,11 @@
|
|||||||
- **Decision**: All Blender work driven by Python scripts in `Tools/`. Skill packs used as method guides (checklists, naming, proportions) only.
|
- **Decision**: All Blender work driven by Python scripts in `Tools/`. Skill packs used as method guides (checklists, naming, proportions) only.
|
||||||
|
|
||||||
- **Consequences**: Scripts are version-controlled and repeatable. No live Blender session needed for CI.
|
- **Consequences**: Scripts are version-controlled and repeatable. No live Blender session needed for CI.
|
||||||
|
|
||||||
|
## ADR-9: Third-person body is an attachment of the first-person pawn, not a second pawn
|
||||||
|
|
||||||
|
- **Context**: `0.2.0` needs a visible body and a third-person camera, while `0.1.0` shipped a first-person pawn with no body and the owner's decision was first person for that release. Two separate pawn classes would duplicate movement, input and reset logic, which is the code most likely to drift.
|
||||||
|
|
||||||
|
- **Decision**: one pawn, both rigs. `AGigLineCharacter` carries a skeletal mesh (hidden in first person), a collision-tested spring-arm camera, and a **V** toggle that swaps the active camera and the body's visibility. The shipped default view stays first person; `-thirdperson` starts the pawn in the third-person rig and is what the screenshot run passes, so the review shot is reproducible without a human pressing a key.
|
||||||
|
|
||||||
|
- **Consequences**: movement, reset, pause and the QA self-test stay in one place. The body's animation state is driven from `Tick` by speed rather than from input, so being pushed or falling animates correctly. The cost is that the camera swap is not a separate actor, so a future cinematic camera needs its own path rather than reusing the toggle.
|
||||||
@@ -169,7 +169,9 @@ C++ classes in `Game/GigLine/Source/GigLine/`:
|
|||||||
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
|
| `AGigLineShotActor` | Screenshot camera walker | Drives cameras in `-game` mode, `FScreenshotRequest`, quits |
|
||||||
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
|
| `UGigLineAssetTools` | C++ collision helpers | `SetComplexCollisionAsSimple` on `StaticMesh` assets |
|
||||||
|
|
||||||
Input assets in `/Game/Input/`: `IA_Move`, `IA_MoveBack`, `IA_Strafe`, `IA_StrafeLeft`, `IA_Look`, `IA_Jump`, `IA_Reset`, `IA_Pause`, bound in `IMC_Default`.
|
Input assets in `/Game/Input/`: `IA_Move`, `IA_MoveBack`, `IA_Strafe`, `IA_StrafeLeft`, `IA_Look`, `IA_Jump`, `IA_Reset`, `IA_Pause`, `IA_ToggleCamera`, bound in `IMC_Default`.
|
||||||
|
|
||||||
|
Player-facing bindings: **WASD** move, **mouse** look, **Space** jump, **R** reset, **Escape** pause, **V** swap first/third person. The camera and body work landed with `0.2.0` (see ADR-9): the shipped default view stays first person, and `-thirdperson` starts the pawn in the third-person rig — used by the screenshot run so the review is reproducible.
|
||||||
|
|
||||||
### Site
|
### Site
|
||||||
|
|
||||||
@@ -200,6 +202,7 @@ Python scripts in `Tools/` drive the content pipeline:
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `export_*`, `hero_facade_build` | Blender headless export (FBX) |
|
| `export_*`, `hero_facade_build` | Blender headless export (FBX) |
|
||||||
| `ue_import_*`, `ue_hero_facade_import` | Unreal headless import + placement |
|
| `ue_import_*`, `ue_hero_facade_import` | Unreal headless import + placement |
|
||||||
|
| `character_anim_build`, `ue_character_anim_import` | Character animation clips (`GigLine_Idle`, `GigLine_Walk`) and their import |
|
||||||
| `ue_build_demo_map`, `ue_build_review_map` | Map assembly (lighting, meshes, player start) |
|
| `ue_build_demo_map`, `ue_build_review_map` | Map assembly (lighting, meshes, player start) |
|
||||||
| `ue_create_input_assets` | Enhanced Input actions + mapping context |
|
| `ue_create_input_assets` | Enhanced Input actions + mapping context |
|
||||||
| `render_screenshots.sh` | Screenshot capture pipeline |
|
| `render_screenshots.sh` | Screenshot capture pipeline |
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,9 @@
|
|||||||
#include "GigLineCharacter.h"
|
#include "GigLineCharacter.h"
|
||||||
|
|
||||||
|
#include "Animation/AnimSequence.h"
|
||||||
#include "Camera/CameraComponent.h"
|
#include "Camera/CameraComponent.h"
|
||||||
|
#include "Components/SkeletalMeshComponent.h"
|
||||||
|
#include "GameFramework/SpringArmComponent.h"
|
||||||
#include "Components/CapsuleComponent.h"
|
#include "Components/CapsuleComponent.h"
|
||||||
#include "EnhancedInputComponent.h"
|
#include "EnhancedInputComponent.h"
|
||||||
#include "EnhancedInputSubsystems.h"
|
#include "EnhancedInputSubsystems.h"
|
||||||
@@ -28,7 +31,7 @@ static TAutoConsoleVariable<int32> CVarGigLineSetupLogs(
|
|||||||
|
|
||||||
AGigLineCharacter::AGigLineCharacter()
|
AGigLineCharacter::AGigLineCharacter()
|
||||||
{
|
{
|
||||||
PrimaryActorTick.bCanEverTick = false;
|
PrimaryActorTick.bCanEverTick = true;
|
||||||
|
|
||||||
// 1.75 m tall, 0.42 m wide, human scale in Unreal units (1 m = 100 uu).
|
// 1.75 m tall, 0.42 m wide, human scale in Unreal units (1 m = 100 uu).
|
||||||
GetCapsuleComponent()->InitCapsuleSize(42.0f, 87.5f);
|
GetCapsuleComponent()->InitCapsuleSize(42.0f, 87.5f);
|
||||||
@@ -51,9 +54,59 @@ AGigLineCharacter::AGigLineCharacter()
|
|||||||
FirstPersonCamera->SetRelativeLocation(FVector(0.0f, 0.0f, 72.5f)); // eye height 1.60 m
|
FirstPersonCamera->SetRelativeLocation(FVector(0.0f, 0.0f, 72.5f)); // eye height 1.60 m
|
||||||
FirstPersonCamera->bUsePawnControlRotation = true;
|
FirstPersonCamera->bUsePawnControlRotation = true;
|
||||||
|
|
||||||
// An eye-level mesh is not needed for 0.1.0; the body is a capsule with a camera.
|
// Visible body (0.2.0). The animated import carries its own skeleton, so the
|
||||||
GetMesh()->SetHiddenInGame(true);
|
// clips resolve without a retarget step; it is hidden while the first-person
|
||||||
|
// camera is active and shown in third person.
|
||||||
|
static ConstructorHelpers::FObjectFinder<USkeletalMesh> BodyFinder(
|
||||||
|
TEXT("/Game/Test/Character/Character_Anim.Character_Anim"));
|
||||||
|
if (BodyFinder.Succeeded())
|
||||||
|
{
|
||||||
|
GetMesh()->SetSkeletalMesh(BodyFinder.Object);
|
||||||
|
}
|
||||||
|
// The animated import carries no material of its own; without this the body is
|
||||||
|
// grey clay. The warm skin material was created by the character import.
|
||||||
|
static ConstructorHelpers::FObjectFinder<UMaterialInterface> SkinFinder(
|
||||||
|
TEXT("/Game/Test/Character/M_Character_Export.M_Character_Export"));
|
||||||
|
if (SkinFinder.Succeeded())
|
||||||
|
{
|
||||||
|
GetMesh()->SetMaterial(0, SkinFinder.Object);
|
||||||
|
}
|
||||||
|
static ConstructorHelpers::FObjectFinder<UAnimSequence> IdleFinder(
|
||||||
|
TEXT("/Game/Test/Character/Character_AnimHuman_rig_GigLine_Idle.Character_AnimHuman_rig_GigLine_Idle"));
|
||||||
|
if (IdleFinder.Succeeded())
|
||||||
|
{
|
||||||
|
IdleAnimation = IdleFinder.Object;
|
||||||
|
}
|
||||||
|
static ConstructorHelpers::FObjectFinder<UAnimSequence> WalkFinder(
|
||||||
|
TEXT("/Game/Test/Character/Character_AnimHuman_rig_GigLine_Walk.Character_AnimHuman_rig_GigLine_Walk"));
|
||||||
|
if (WalkFinder.Succeeded())
|
||||||
|
{
|
||||||
|
WalkAnimation = WalkFinder.Object;
|
||||||
|
}
|
||||||
|
GetMesh()->SetRelativeLocation(FVector(0.0f, 0.0f, -87.5f));
|
||||||
|
GetMesh()->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
|
||||||
GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
GetMesh()->SetHiddenInGame(true);
|
||||||
|
|
||||||
|
// Third-person boom. bDoCollisionTest is the camera-collision feature: the arm
|
||||||
|
// shortens when something is between the pivot and the desired camera position.
|
||||||
|
ThirdPersonArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("ThirdPersonArm"));
|
||||||
|
ThirdPersonArm->SetupAttachment(GetCapsuleComponent());
|
||||||
|
ThirdPersonArm->SetRelativeLocation(FVector(0.0f, 0.0f, 60.0f));
|
||||||
|
ThirdPersonArm->TargetArmLength = ThirdPersonBoomLength;
|
||||||
|
ThirdPersonArm->bUsePawnControlRotation = true;
|
||||||
|
ThirdPersonArm->bDoCollisionTest = true;
|
||||||
|
ThirdPersonArm->ProbeSize = 12.0f;
|
||||||
|
ThirdPersonArm->ProbeChannel = ECollisionChannel::ECC_Camera;
|
||||||
|
ThirdPersonArm->bEnableCameraLag = true;
|
||||||
|
ThirdPersonArm->CameraLagSpeed = 8.0f;
|
||||||
|
ThirdPersonArm->SetAutoActivate(false);
|
||||||
|
|
||||||
|
ThirdPersonCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("ThirdPersonCamera"));
|
||||||
|
ThirdPersonCamera->SetupAttachment(ThirdPersonArm, USpringArmComponent::SocketName);
|
||||||
|
ThirdPersonCamera->bUsePawnControlRotation = false;
|
||||||
|
ThirdPersonCamera->SetAutoActivate(false);
|
||||||
|
ThirdPersonCamera->SetFieldOfView(90.0f);
|
||||||
|
|
||||||
// Hard-reference the Enhanced Input assets with ConstructorHelpers so the
|
// Hard-reference the Enhanced Input assets with ConstructorHelpers so the
|
||||||
// cooker always includes them and they load during CDO construction. Runtime
|
// cooker always includes them and they load during CDO construction. Runtime
|
||||||
@@ -78,6 +131,8 @@ AGigLineCharacter::AGigLineCharacter()
|
|||||||
TEXT("/Game/Input/IA_Jump.IA_Jump"));
|
TEXT("/Game/Input/IA_Jump.IA_Jump"));
|
||||||
static ConstructorHelpers::FObjectFinder<UInputAction> ResetF(
|
static ConstructorHelpers::FObjectFinder<UInputAction> ResetF(
|
||||||
TEXT("/Game/Input/IA_Reset.IA_Reset"));
|
TEXT("/Game/Input/IA_Reset.IA_Reset"));
|
||||||
|
static ConstructorHelpers::FObjectFinder<UInputAction> CamF(
|
||||||
|
TEXT("/Game/Input/IA_ToggleCamera.IA_ToggleCamera"));
|
||||||
static ConstructorHelpers::FObjectFinder<UInputAction> PauseF(
|
static ConstructorHelpers::FObjectFinder<UInputAction> PauseF(
|
||||||
TEXT("/Game/Input/IA_Pause.IA_Pause"));
|
TEXT("/Game/Input/IA_Pause.IA_Pause"));
|
||||||
|
|
||||||
@@ -89,6 +144,7 @@ AGigLineCharacter::AGigLineCharacter()
|
|||||||
LookAction = LookF.Object;
|
LookAction = LookF.Object;
|
||||||
JumpAction = JumpF.Object;
|
JumpAction = JumpF.Object;
|
||||||
ResetAction = ResetF.Object;
|
ResetAction = ResetF.Object;
|
||||||
|
ToggleCameraAction = CamF.Object;
|
||||||
PauseAction = PauseF.Object;
|
PauseAction = PauseF.Object;
|
||||||
|
|
||||||
if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction
|
if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction
|
||||||
@@ -138,6 +194,23 @@ void AGigLineCharacter::BeginPlay()
|
|||||||
StartLocation = GetActorLocation();
|
StartLocation = GetActorLocation();
|
||||||
StartRotation = GetActorRotation();
|
StartRotation = GetActorRotation();
|
||||||
|
|
||||||
|
// `-thirdperson` starts the pawn in the third-person rig. The first-person view
|
||||||
|
// stays the default (owner decision for 0.1.0), but the screenshot run needs the
|
||||||
|
// body visible without a human pressing V, so the review is reproducible.
|
||||||
|
if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Log,
|
||||||
|
TEXT("GigLine: BeginPlay thirdperson=%d body=%s idle=%s walk=%s"),
|
||||||
|
FParse::Param(FCommandLine::Get(), TEXT("thirdperson")) ? 1 : 0,
|
||||||
|
GetMesh()->GetSkeletalMeshAsset() ? TEXT("ok") : TEXT("nil"),
|
||||||
|
IdleAnimation ? TEXT("ok") : TEXT("nil"),
|
||||||
|
WalkAnimation ? TEXT("ok") : TEXT("nil"));
|
||||||
|
}
|
||||||
|
if (FParse::Param(FCommandLine::Get(), TEXT("thirdperson")))
|
||||||
|
{
|
||||||
|
ToggleCamera();
|
||||||
|
}
|
||||||
|
|
||||||
// The PlayerStart sits above grade so the capsule cannot spawn embedded, so
|
// The PlayerStart sits above grade so the capsule cannot spawn embedded, so
|
||||||
// the BeginPlay position is mid-air. Re-capture once the pawn has landed,
|
// the BeginPlay position is mid-air. Re-capture once the pawn has landed,
|
||||||
// otherwise Reset drops the player 1.4 m back into the air every press.
|
// otherwise Reset drops the player 1.4 m back into the air every press.
|
||||||
@@ -557,6 +630,10 @@ void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputCo
|
|||||||
{
|
{
|
||||||
Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AGigLineCharacter::TogglePause);
|
Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AGigLineCharacter::TogglePause);
|
||||||
}
|
}
|
||||||
|
if (ToggleCameraAction)
|
||||||
|
{
|
||||||
|
Input->BindAction(ToggleCameraAction, ETriggerEvent::Started, this, &AGigLineCharacter::ToggleCamera);
|
||||||
|
}
|
||||||
if (JumpAction)
|
if (JumpAction)
|
||||||
{
|
{
|
||||||
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
|
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
|
||||||
@@ -644,6 +721,68 @@ void AGigLineCharacter::Look(const FInputActionValue& Value)
|
|||||||
AddControllerPitchInput(Axis.Y * LookSensitivity * (bInvertLookY ? -1.0f : 1.0f));
|
AddControllerPitchInput(Axis.Y * LookSensitivity * (bInvertLookY ? -1.0f : 1.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AGigLineCharacter::Tick(float DeltaSeconds)
|
||||||
|
{
|
||||||
|
Super::Tick(DeltaSeconds);
|
||||||
|
// Speed-driven animation state: correct whether the player walked, was pushed,
|
||||||
|
// or fell, and independent of which input produced the movement.
|
||||||
|
UpdateBodyAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AGigLineCharacter::ToggleCamera()
|
||||||
|
{
|
||||||
|
// Both rigs exist from construction; the toggle only swaps which one is active
|
||||||
|
// and whether the body is drawn. Deactivating the other camera matters: with
|
||||||
|
// two active cameras the view is whichever was registered last.
|
||||||
|
bThirdPerson = !bThirdPerson;
|
||||||
|
FirstPersonCamera->SetActive(!bThirdPerson);
|
||||||
|
ThirdPersonCamera->SetActive(bThirdPerson);
|
||||||
|
GetMesh()->SetHiddenInGame(!bThirdPerson);
|
||||||
|
if (bThirdPerson)
|
||||||
|
{
|
||||||
|
UpdateBodyAnimation();
|
||||||
|
}
|
||||||
|
if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("GigLine: camera %s"), bThirdPerson ? TEXT("third person") : TEXT("first person"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AGigLineCharacter::UpdateBodyAnimation()
|
||||||
|
{
|
||||||
|
if (!bThirdPerson || !GetMesh())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bool bMoving = GetVelocity().Size2D() > WalkAnimationThreshold;
|
||||||
|
UAnimSequence* Wanted = bMoving ? WalkAnimation.Get() : IdleAnimation.Get();
|
||||||
|
if (!Wanted)
|
||||||
|
{
|
||||||
|
// Logged once: a missing clip presents as an A-posed body, which looks like
|
||||||
|
// an animation problem rather than a missing asset reference.
|
||||||
|
static bool bWarnedMissingClip = false;
|
||||||
|
if (!bWarnedMissingClip)
|
||||||
|
{
|
||||||
|
bWarnedMissingClip = true;
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("GigLine: body clip missing (idle=%s walk=%s) - body stays in bind pose"),
|
||||||
|
IdleAnimation ? TEXT("ok") : TEXT("nil"),
|
||||||
|
WalkAnimation ? TEXT("ok") : TEXT("nil"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Wanted == PlayingAnimation)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PlayingAnimation = Wanted;
|
||||||
|
if (CVarGigLineSetupLogs.GetValueOnGameThread())
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("GigLine: playing body clip %s"), *Wanted->GetName());
|
||||||
|
}
|
||||||
|
GetMesh()->PlayAnimation(Wanted, true);
|
||||||
|
}
|
||||||
|
|
||||||
void AGigLineCharacter::ResetToStart()
|
void AGigLineCharacter::ResetToStart()
|
||||||
{
|
{
|
||||||
// Reset is a level-restart in miniature: put the body back where it spawned,
|
// Reset is a level-restart in miniature: put the body back where it spawned,
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
#include "InputActionValue.h"
|
#include "InputActionValue.h"
|
||||||
#include "GigLineCharacter.generated.h"
|
#include "GigLineCharacter.generated.h"
|
||||||
|
|
||||||
|
class UAnimSequence;
|
||||||
class UCameraComponent;
|
class UCameraComponent;
|
||||||
class UInputAction;
|
class UInputAction;
|
||||||
class UInputMappingContext;
|
class UInputMappingContext;
|
||||||
|
class USpringArmComponent;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +31,7 @@ public:
|
|||||||
|
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
||||||
|
virtual void Tick(float DeltaSeconds) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/** Loads the Enhanced Input assets from /Game/Input; called from BeginPlay. */
|
/** Loads the Enhanced Input assets from /Game/Input; called from BeginPlay. */
|
||||||
@@ -38,6 +41,23 @@ protected:
|
|||||||
void ResetToStart();
|
void ResetToStart();
|
||||||
void TogglePause();
|
void TogglePause();
|
||||||
|
|
||||||
|
/** Swaps between the first-person and third-person rigs (V). */
|
||||||
|
void ToggleCamera();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays the idle or walk clip on the visible body.
|
||||||
|
*
|
||||||
|
* Called from the movement tick rather than from input: an animation state
|
||||||
|
* driven by speed is correct whether the player walked, was pushed, or fell.
|
||||||
|
* Only switches when the clip changes, because PlayAnimation restarts a clip
|
||||||
|
* and calling it every frame would freeze the cycle at its first key.
|
||||||
|
*/
|
||||||
|
void UpdateBodyAnimation();
|
||||||
|
|
||||||
|
/** Velocity above which the walk clip plays, in cm/s (0.1 m/s tolerance). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
|
||||||
|
float WalkAnimationThreshold = 10.0f;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TEMP QA: scripted walk / reset / pause verification.
|
* TEMP QA: scripted walk / reset / pause verification.
|
||||||
*
|
*
|
||||||
@@ -109,6 +129,39 @@ protected:
|
|||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||||
TObjectPtr<UCameraComponent> FirstPersonCamera;
|
TObjectPtr<UCameraComponent> FirstPersonCamera;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Third-person boom. Collision testing is on, which is what stops the camera
|
||||||
|
* from ending up inside a wall: the arm shortens until the camera is clear
|
||||||
|
* instead of clipping through geometry.
|
||||||
|
*/
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||||
|
TObjectPtr<USpringArmComponent> ThirdPersonArm;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||||
|
TObjectPtr<UCameraComponent> ThirdPersonCamera;
|
||||||
|
|
||||||
|
/** Boom length in cm: far enough to see the body, close enough to read the street. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
|
||||||
|
float ThirdPersonBoomLength = 340.0f;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UAnimSequence> IdleAnimation;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UAnimSequence> WalkAnimation;
|
||||||
|
|
||||||
|
/** True while the third-person rig is the active view. */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Camera")
|
||||||
|
bool bThirdPerson = false;
|
||||||
|
|
||||||
|
/** Clip currently playing on the body, so UpdateBodyAnimation can diff it. */
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UAnimSequence> PlayingAnimation;
|
||||||
|
|
||||||
|
/** Toggle-camera action (V). */
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
|
||||||
|
TObjectPtr<UInputAction> ToggleCameraAction;
|
||||||
|
|
||||||
/** Degrees per second per unit of mouse input; defaults to Unreal's usual 45. */
|
/** Degrees per second per unit of mouse input; defaults to Unreal's usual 45. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
|
||||||
float LookSensitivity = 45.0f;
|
float LookSensitivity = 45.0f;
|
||||||
|
|||||||
+93
-14
@@ -12,18 +12,83 @@ Tools/render_screenshots.sh --verify
|
|||||||
|
|
||||||
Stages the demo shot cameras, runs the game on `/Game/Hollywood/DemoMap`, copies the PNGs into `Screenshots/demo/` and then runs [`Tools/visual_loop.py`](../Tools/visual_loop.py) `--verify` to check that each shot still shows the content its expectation describes. About a minute.
|
Stages the demo shot cameras, runs the game on `/Game/Hollywood/DemoMap`, copies the PNGs into `Screenshots/demo/` and then runs [`Tools/visual_loop.py`](../Tools/visual_loop.py) `--verify` to check that each shot still shows the content its expectation describes. About a minute.
|
||||||
|
|
||||||
## The shot list
|
## Status at a glance
|
||||||
|
|
||||||
Each shot has an **expectation** in [`Tools/visual_targets.json`](../Tools/visual_targets.json) — the content the capture must show — and a verification state produced by `--verify`: it fails if the frame is blank, the wrong size, or has no tonal range.
|
Every shot is embedded below, in order, so the whole review happens in this file.
|
||||||
|
|
||||||
| # | File | View | Expected content | Verified | Approved |
|
| # | Shot | Verified | Approved |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 1 | `demo/review-demo-spawn-view.png` | north sidewalk | The whole south block: the neoclassical hero façade between its neighbours, both sidewalks and the road in the foreground | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
| 1 | [Spawn view](#1-spawn-view) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||||
| 2 | `demo/review-demo-street-level.png` | eye height, north side | Street level: the hero colonnade with the adjacent frontage buildings either side, shopfronts at pavement level | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
| 2 | [Street level](#2-street-level) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||||
| 3 | `demo/review-demo-crossing.png` | along the crosswalk | The designated mid-block crossing (s=130 m, UE X −14956): zebra bars on the asphalt, south frontage and colonnade behind | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
| 3 | [Crossing](#3-crossing) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||||
| 4 | `demo/review-demo-north-buildings.png` | across the Boulevard | The two north-side buildings (TCL Chinese Theatre, Madame Tussaud's) from the south side, full height in frame | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
| 4 | [North buildings](#4-north-buildings) | ☑ 2026-11-09 | ☑ 2026-11-09 |
|
||||||
|
| 5 | [Third-person body](#5-third-person-body) | ☑ 2026-11-09 | ☐ pending |
|
||||||
|
|
||||||
Approval is per shot, in the table above (owner review of the `0.1.4` set, recorded in [`Docs/reviews/0.1.4-review.md`](../Docs/reviews/0.1.4-review.md)). Add a row when a shot is added; remove the file in the same change when a shot is retired.
|
**Verified** is what `--verify` reports: the frame is not blank, is the right size, and has tonal range. **Approved** is the owner's verdict, recorded here. Each shot also has an **expectation** in [`Tools/visual_targets.json`](../Tools/visual_targets.json) describing the content it must show.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Spawn view
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **File:** `demo/review-demo-spawn-view.png`
|
||||||
|
- **View:** north sidewalk, 14 m back, level horizon
|
||||||
|
- **Expected content:** the whole south block — the neoclassical hero façade between its neighbouring buildings, both sidewalks and the road in the foreground
|
||||||
|
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Street level
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **File:** `demo/review-demo-street-level.png`
|
||||||
|
- **View:** eye height, north side
|
||||||
|
- **Expected content:** the hero colonnade with the adjacent frontage buildings either side, shopfronts at pavement level
|
||||||
|
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Crossing
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **File:** `demo/review-demo-crossing.png`
|
||||||
|
- **View:** from the north sidewalk along the crosswalk
|
||||||
|
- **Expected content:** the designated mid-block crossing (s=130 m, UE X −14956) — zebra bars on the asphalt, south frontage and colonnade behind
|
||||||
|
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. North buildings
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **File:** `demo/review-demo-north-buildings.png`
|
||||||
|
- **View:** across the Boulevard from the south side
|
||||||
|
- **Expected content:** the two north-side buildings (TCL Chinese Theatre, Madame Tussaud's), full height in frame
|
||||||
|
- **Verified:** ☑ 2026-11-09 · **Approved:** ☑ 2026-11-09
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Third-person body
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **File:** `demo/review-demo-third-person.png`
|
||||||
|
- **View:** third-person boom, ~6 m behind the pawn, captured with `-thirdperson` so the pawn starts in the third-person rig
|
||||||
|
- **Expected content:** the player's own visible body on the north sidewalk, seen from behind, with the hero façade across the street
|
||||||
|
- **Verified:** ☑ 2026-11-09 · **Approved:** ☐ pending
|
||||||
|
|
||||||
|
What this shot is evidence for, and what it is not:
|
||||||
|
|
||||||
|
- **Implemented and visible:** the pawn carries a visible skeletal body with the warm skin material, and a spring-arm camera with collision testing. The **V** toggle swaps first and third person; the shipped default stays first person and the review passes `-thirdperson`.
|
||||||
|
- **Not working:** the figure is in **bind pose, arms out**. The idle clip is bound and `PlayAnimation` is called — the run logs `GigLine: playing body clip Character_AnimHuman_rig_GigLine_Idle` — so the call succeeds and the pose does not apply. Tracked in `TODO.md` under "Verify animation plays correctly on the imported rig" with that evidence.
|
||||||
|
- **Camera collision is implemented but unproven:** the spawn point has open space behind it, so no shot exercises the arm shortening.
|
||||||
|
- The skin material needed `used_with_skeletal_mesh=True`; without it Unreal silently substituted the default grey, which is what the first capture of this shot showed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## The iteration loop (uncommitted)
|
## The iteration loop (uncommitted)
|
||||||
|
|
||||||
@@ -40,12 +105,26 @@ Note the comparison is only meaningful when the shot and the reference frame the
|
|||||||
## Scene conventions
|
## Scene conventions
|
||||||
|
|
||||||
- **Demo map** (`/Game/Hollywood/DemoMap`): fixed daytime — sun pitch −62 / yaw 120 (intensity 4, warm), sky light 2.6, sky atmosphere, height fog, fixed exposure.
|
- **Demo map** (`/Game/Hollywood/DemoMap`): fixed daytime — sun pitch −62 / yaw 120 (intensity 4, warm), sky light 2.6, sky atmosphere, height fog, fixed exposure.
|
||||||
- **Cameras**: elevation shots sit at their target height (pitch 0, so verticals stay parallel) and use a 42–55° lens to fill the frame from across the street.
|
- **Cameras**: elevation shots sit at their target height (pitch 0, so verticals stay parallel) and use a 42–68° lens to fill the frame from across the street.
|
||||||
- **Materials**: every surface carries a look-developed material from `Tools/ue_build_demo_map.py` — noise-blended base colour, masonry coursing, and a generated texture set (`Tools/generate_facade_textures.py`) sampled world-aligned. Explicit roughness and specular per surface type.
|
- **Materials**: every surface carries a look-developed material from `Tools/ue_build_demo_map.py` — noise-blended base colour, masonry coursing, and a generated texture set (`Tools/generate_facade_textures.py`) sampled world-aligned. Explicit roughness and specular per surface type.
|
||||||
|
|
||||||
## Known cosmetic issues
|
## Known cosmetic issues
|
||||||
|
|
||||||
- Hero façade renders pale: the stone material is a graph and a texture set, not a scanned surface (the `0.1.4` façade pass is in progress).
|
Every issue is tracked in `TODO.md` under the version that will fix it; an issue with no ticket is a bug in the plan, not a note.
|
||||||
- Banners sit on the column line rather than flush to the wall (they hang on the columns in the photographs, so this is acceptable).
|
|
||||||
- Between kerb and building line the walkable surface is the terrain mesh, not a modelled pavement.
|
| Issue | Where it is tracked | State |
|
||||||
- The El Capitan Theatre has no marquee or blade sign yet — the feature that makes it read as itself.
|
| --- | --- | --- |
|
||||||
|
| Hero stone renders pale — the material is a procedural graph and a generated texture set, not a measured surface | `0.9.0` → measured material library | Open |
|
||||||
|
| Hero façade has no doors at street level left and right of the central bay, and the central-bay elements are not positioned as the photographs show | `0.9.0` → per-building models, hero façade | Open |
|
||||||
|
| Third-person body renders in bind pose: the clip is bound and `PlayAnimation` is called, but the pose does not apply | `0.2.0` → verify animation plays correctly | Open (blocking `0.2.0`) |
|
||||||
|
| Camera collision is implemented but never exercised against a wall | `0.2.0` → camera collision | Open |
|
||||||
|
| Between kerb and building line the walkable surface is the terrain mesh, not a modelled pavement | `0.6.0` → visual polish | Open |
|
||||||
|
|
||||||
|
Accepted by the owner, not a defect:
|
||||||
|
|
||||||
|
- Banners hang on the column line rather than flush to the wall — that is how they hang in the photographs.
|
||||||
|
|
||||||
|
Resolved and removed:
|
||||||
|
|
||||||
|
- "The El Capitan Theatre has no marquee or blade sign" — `Tools/ue_stage_theatre_trim.py` stages all six trim actors; verified in `/tmp/gigline/theatre-import.txt`.
|
||||||
|
- "Materials are not yet created in Blender or fully integrated" — contradicted by the scene conventions above.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -21,19 +21,19 @@ Suggested blender skills: **references** (reference-image-match), **archviz**, *
|
|||||||
|
|
||||||
## 0.2.0 — MPFB player body, third-person camera, basic animation (weeks 7–9)
|
## 0.2.0 — MPFB player body, third-person camera, basic animation (weeks 7–9)
|
||||||
|
|
||||||
### Character setup
|
### Character setup - active - session-5598ce7e-27e3-4c64-bb49-c04a4a31ee58 (branch `anim-body-0.2.0`)
|
||||||
|
|
||||||
- [x] Generate MPFB human base mesh; export baked materials and deform skeleton. Done in bootstrap: idle-pose 19 158-vert human with a 137-bone standard rig, `Tools/mpfb_character_export.py` (now `--pose idle` + warm `make_diffuse`).
|
- [x] Generate MPFB human base mesh; export baked materials and deform skeleton. Done in bootstrap: idle-pose 19 158-vert human with a 137-bone standard rig, `Tools/mpfb_character_export.py` (now `--pose idle` + warm `make_diffuse`).
|
||||||
- [x] Import character into Unreal; validate skeleton, scale, and materials. Done: `/Game/Test/Character` — `SkeletalMesh` (138 bones), `Skeleton`, warm `M_Character_Export` material with baked `T_Character_Diffuse`.
|
- [x] Import character into Unreal; validate skeleton, scale, and materials. Done: `/Game/Test/Character` — `SkeletalMesh` (138 bones), `Skeleton`, warm `M_Character_Export` material with baked `T_Character_Diffuse`.
|
||||||
- [x] Promote the character asset into a reusable NPC prefab (visible body, collision capsule, idle pose) ready for pedestrians (`0.5.0`) once the third-person body lands. Done 2026-11-07: `AGigLineNPCPerson` (see the 0.1.0 Character-development item); `0.5.0` reuses it as the pedestrian base.
|
- [x] Promote the character asset into a reusable NPC prefab (visible body, collision capsule, idle pose) ready for pedestrians (`0.5.0`) once the third-person body lands. Done 2026-11-07: `AGigLineNPCPerson` (see the 0.1.0 Character-development item); `0.5.0` reuses it as the pedestrian base.
|
||||||
- [ ] Create basic idle/walk animation set.
|
- [x] Create basic idle/walk animation set. Done 2026-11-09: `Tools/character_anim_build.py` authors `GigLine_Idle` (60 f) and `GigLine_Walk` (30 f) at 30 fps from parametric cycles, and `Tools/ue_character_anim_import.py` imports them. Both AnimSequences verified in `/Game/Test/Character`: Idle 1.967 s, Walk 0.967 s. Limb axes were **measured** on the rig, not assumed — the probe rotated `upperleg01.L` 0.5 rad per axis and read `foot.L`, which showed X is the fore/aft swing (Y twists, Z abducts).
|
||||||
- [ ] Verify animation plays correctly on the imported rig.
|
- [A] Verify animation plays correctly on the imported rig. **Playback is broken and now measurable**: the pawn's mesh is bound, both clips resolve, and the run logs `GigLine: playing body clip Character_AnimHuman_rig_GigLine_Idle`, but shot 5 still renders the body in bind pose (arms out). So `PlayAnimation` is called and the pose is not applied — next step is to check whether the imported clip carries bone curves at all (the FBX bake may be sampling only keyed bones) or whether the component needs `SetAnimationMode(AnimationSingleNode)` before `SetAnimation`/`Play`.
|
||||||
|
|
||||||
### Camera and controls
|
### Camera and controls
|
||||||
|
|
||||||
- [ ] Add third-person camera following the player body.
|
- [x] Add third-person camera following the player body. Done 2026-11-09: `USpringArmComponent` + camera on the capsule, boom 340 cm, lag 8, and the body is the animated skeletal mesh with the skin material. Review shot 5 (`demo-third-person`, `Screenshots/README.md`) shows the rig; the shipped default view stays first person.
|
||||||
- [ ] Add first/third-person toggle.
|
- [x] Add first/third-person toggle. **V**, via `IA_ToggleCamera`; the toggle swaps the active camera and the body's visibility. `-thirdperson` on the command line starts in third person so the review is reproducible.
|
||||||
- [ ] Add camera collision to prevent clipping through geometry.
|
- [x] Add camera collision to prevent clipping through geometry. The spring arm's collision test (`bDoCollisionTest`, probe 12 cm on `ECC_Camera`) shortens the boom instead of letting the camera pass through a wall. **Not yet exercised against a wall in a shot** — the spawn point has open space behind it, so this is implemented but unproven.
|
||||||
- [ ] Document any input changes.
|
- [ ] Document any input changes.
|
||||||
|
|
||||||
### Release checks
|
### Release checks
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
|||||||
| Tool | Purpose |
|
| Tool | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `hero_facade_build.py` | Parametric hero façade; emits a JSON report naming the evidence behind every part |
|
| `hero_facade_build.py` | Parametric hero façade; emits a JSON report naming the evidence behind every part |
|
||||||
|
| `character_anim_build.py` | Authors the basic animation set on the MPFB rig — `GigLine_Idle` (60 f) and `GigLine_Walk` (30 f) sampled from parametric cycles at 30 fps — and exports one animated FBX with the `ue_character_v1` preset. Rotation axes are measured on the rig, not assumed (probe in the docstring). `--out`, `--report`. |
|
||||||
| `mpfb_character_export.py` | MPFB human, standard rig, baked skin, FBX export |
|
| `mpfb_character_export.py` | MPFB human, standard rig, baked skin, FBX export |
|
||||||
|
|
||||||
## Unreal import and level building
|
## Unreal import and level building
|
||||||
@@ -53,6 +54,7 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
|||||||
| `ue_build_demo_map.py` | Builds `/Game/Hollywood/DemoMap`: lighting, façade, site meshes, player start |
|
| `ue_build_demo_map.py` | Builds `/Game/Hollywood/DemoMap`: lighting, façade, site meshes, player start |
|
||||||
| `ue_build_review_map.py` | Rebuilds `Test/ReviewMap`, stages the shot camera list, and stages the demo-map views |
|
| `ue_build_review_map.py` | Rebuilds `Test/ReviewMap`, stages the shot camera list, and stages the demo-map views |
|
||||||
| `ue_create_input_assets.py` | Creates `/Game/Input` actions and the mapping context headlessly |
|
| `ue_create_input_assets.py` | Creates `/Game/Input` actions and the mapping context headlessly |
|
||||||
|
| `ue_character_anim_import.py` | Imports the animated FBX into `/Game/Test/Character` and **verifies an AnimSequence exists** — an FBX exported without `bake_anim` imports a mesh and no clips, with no error. Writes `VERDICT: PASS/FAIL` to `/tmp/gigline/character-anim-import.txt`. |
|
||||||
| `ue_character_import.py` | Skeletal import plus the material/texture chain Interchange skips |
|
| `ue_character_import.py` | Skeletal import plus the material/texture chain Interchange skips |
|
||||||
|
|
||||||
## Visual review
|
## Visual review
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Author the basic character animation set (0.2.0): idle and walk.
|
||||||
|
|
||||||
|
Three clips on the MPFB standard rig, sampled from parametric cycles rather than
|
||||||
|
hand-keyed poses, so the loop is closed by construction and every key can be
|
||||||
|
re-derived from the numbers below:
|
||||||
|
|
||||||
|
GigLine_Idle 60 frames @ 30 fps — breathing sway, feet planted
|
||||||
|
GigLine_Walk 30 frames @ 30 fps — one full stride, left and right contact
|
||||||
|
|
||||||
|
blender -b Art/blender/character/character-2026-11-07.blend \
|
||||||
|
--python Tools/character_anim_build.py -- \
|
||||||
|
--out /tmp/gigline/character-anim.fbx \
|
||||||
|
--report /tmp/gigline/character-anim-report.json
|
||||||
|
|
||||||
|
Rotation axes are **measured, not assumed**. A probe on the actual rig rotated
|
||||||
|
`upperleg01.L` by 0.5 rad on each axis and read the resulting `foot.L` head:
|
||||||
|
|
||||||
|
X -> foot Y -0.022 -> +0.362 (fore/aft swing: the axis used here)
|
||||||
|
Y -> twist along the leg, foot barely moves
|
||||||
|
Z -> foot swings sideways into the other leg
|
||||||
|
|
||||||
|
The arm probe gives the same answer for `upperarm01`, so limbs swing on local X
|
||||||
|
and positive X sends the limb backwards. Everything else (spine lean, head nod)
|
||||||
|
uses small X deltas for the same reason.
|
||||||
|
|
||||||
|
The rig is `pelvis.L → upperleg01 → upperleg02 → lowerleg01 → lowerleg02 → foot`,
|
||||||
|
all deform bones, no constraints, so a plain pose-bone rotation deforms the mesh.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import fbx_export_preset as fbx_preset # noqa: E402
|
||||||
|
|
||||||
|
FPS = 30
|
||||||
|
CLIPS = {"Idle": 60, "Walk": 30}
|
||||||
|
|
||||||
|
# Stride amplitude in radians at the hip. A human walk swings the thigh about
|
||||||
|
# 25-30 deg forward and 15-20 back; 0.45 rad (~26 deg) is a normal-pace stride.
|
||||||
|
HIP = 0.45
|
||||||
|
# Knee flexion during swing, measured from the photographs of a normal gait.
|
||||||
|
KNEE = 0.55
|
||||||
|
# Arm swing counter-phases the leg; a relaxed walk swings about 20 deg.
|
||||||
|
ARM = 0.32
|
||||||
|
ELBOW = 0.18
|
||||||
|
|
||||||
|
PARTS: list[dict] = []
|
||||||
|
|
||||||
|
# The .blend stores the raw A-pose bind: `mpfb_character_export.py --pose idle`
|
||||||
|
# bakes the arms down only in the exported FBX, not in the file. A clip that
|
||||||
|
# animates only swing therefore leaves the arms out in an A — which is exactly how
|
||||||
|
# the first third-person render looked. These are the same deltas `IDLE_POSE` uses,
|
||||||
|
# applied as a base that every key sits on top of.
|
||||||
|
BASE = {
|
||||||
|
"shoulder01.L": (0.0, -0.15, -0.03),
|
||||||
|
"shoulder01.R": (0.0, 0.15, 0.03),
|
||||||
|
"upperarm01.L": (0.0, 0.0, -1.55),
|
||||||
|
"upperarm01.R": (0.0, 0.0, 1.55),
|
||||||
|
"lowerarm01.L": (0.0, 0.0, -0.10),
|
||||||
|
"lowerarm01.R": (0.0, 0.0, 0.10),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
PARTS.append(msg)
|
||||||
|
print(f"[anim] {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def clear_animation(arm):
|
||||||
|
"""Drop any pre-existing actions so a rerun does not stack clips."""
|
||||||
|
for action in list(bpy.data.actions):
|
||||||
|
bpy.data.actions.remove(action)
|
||||||
|
if arm.animation_data:
|
||||||
|
arm.animation_data_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def pose_bones(arm):
|
||||||
|
bpy.context.view_layer.objects.active = arm
|
||||||
|
bpy.ops.object.mode_set(mode="POSE")
|
||||||
|
for pb in arm.pose.bones:
|
||||||
|
pb.rotation_mode = "XYZ"
|
||||||
|
return arm.pose.bones
|
||||||
|
|
||||||
|
|
||||||
|
def key_euler(pb, frame, x=0.0, y=0.0, z=0.0):
|
||||||
|
"""Key a pose-bone rotation on top of the arms-down base."""
|
||||||
|
bx, by, bz = BASE.get(pb.name, (0.0, 0.0, 0.0))
|
||||||
|
pb.rotation_euler = (bx + x, by + y, bz + z)
|
||||||
|
pb.keyframe_insert(data_path="rotation_euler", frame=frame)
|
||||||
|
|
||||||
|
|
||||||
|
def clip_idle(arm, frames):
|
||||||
|
"""Breathing sway: spine and head counter-rotate, arms drift, feet planted."""
|
||||||
|
bones = pose_bones(arm)
|
||||||
|
action = bpy.data.actions.new("GigLine_Idle")
|
||||||
|
arm.animation_data_create()
|
||||||
|
arm.animation_data.action = action
|
||||||
|
|
||||||
|
for frame in range(1, frames + 1):
|
||||||
|
t = 2.0 * math.pi * (frame - 1) / frames
|
||||||
|
breathe = math.sin(t)
|
||||||
|
# Chest lifts and settles; the head counter-rotates so the gaze stays level.
|
||||||
|
key_euler(bones["spine03"], frame, x=-0.020 * breathe)
|
||||||
|
key_euler(bones["spine05"], frame, x=-0.012 * breathe)
|
||||||
|
key_euler(bones["neck01"], frame, x=0.010 * breathe)
|
||||||
|
key_euler(bones["head"], frame, x=0.008 * breathe)
|
||||||
|
# Arms hang and drift a little; nothing swings.
|
||||||
|
key_euler(bones["upperarm01.L"], frame, x=0.020 * breathe)
|
||||||
|
key_euler(bones["upperarm01.R"], frame, x=-0.020 * breathe)
|
||||||
|
|
||||||
|
action.use_fake_user = True
|
||||||
|
log(f"Idle: {frames} frames, spine/neck/head/arms, feet planted")
|
||||||
|
return action
|
||||||
|
|
||||||
|
|
||||||
|
def clip_walk(arm, frames):
|
||||||
|
"""One full stride: left contact at frame 1, right contact at the half-cycle."""
|
||||||
|
bones = pose_bones(arm)
|
||||||
|
action = bpy.data.actions.new("GigLine_Walk")
|
||||||
|
arm.animation_data.action = action
|
||||||
|
|
||||||
|
for frame in range(1, frames + 1):
|
||||||
|
t = 2.0 * math.pi * (frame - 1) / frames
|
||||||
|
swing = math.sin(t)
|
||||||
|
# Hip swing, opposite phase per leg; negative X is forward.
|
||||||
|
key_euler(bones["upperleg01.L"], frame, x=-HIP * swing)
|
||||||
|
key_euler(bones["upperleg01.R"], frame, x=HIP * swing)
|
||||||
|
# The knee only flexes while the leg is swinging forward, so it is driven
|
||||||
|
# by the half-rectified swing rather than the raw sine.
|
||||||
|
knee_l = KNEE * max(0.0, math.sin(t + math.pi))
|
||||||
|
knee_r = KNEE * max(0.0, math.sin(t))
|
||||||
|
key_euler(bones["lowerleg01.L"], frame, x=knee_l)
|
||||||
|
key_euler(bones["lowerleg01.R"], frame, x=knee_r)
|
||||||
|
# Ankles keep the foot roughly level: counter the hip plus half the knee.
|
||||||
|
key_euler(bones["foot.L"], frame, x=0.25 * HIP * swing - 0.35 * knee_l)
|
||||||
|
key_euler(bones["foot.R"], frame, x=-0.25 * HIP * swing - 0.35 * knee_r)
|
||||||
|
# Arms counter-phase the legs, elbows carry a constant relaxed bend.
|
||||||
|
key_euler(bones["upperarm01.L"], frame, x=ARM * swing)
|
||||||
|
key_euler(bones["upperarm01.R"], frame, x=-ARM * swing)
|
||||||
|
key_euler(bones["lowerarm01.L"], frame, x=ELBOW + 0.10 * swing)
|
||||||
|
key_euler(bones["lowerarm01.R"], frame, x=ELBOW - 0.10 * swing)
|
||||||
|
# Torso counter-rotation: the shoulders turn against the hips.
|
||||||
|
key_euler(bones["spine04"], frame, z=0.08 * swing)
|
||||||
|
key_euler(bones["neck01"], frame, z=-0.04 * swing)
|
||||||
|
|
||||||
|
action.use_fake_user = True
|
||||||
|
log(f"Walk: {frames} frames, stride {HIP:.2f} rad hip / {KNEE:.2f} knee / {ARM:.2f} arm")
|
||||||
|
return action
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("--out", default="/tmp/gigline/character-anim.fbx")
|
||||||
|
ap.add_argument("--report", default=None)
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
|
arm = next((o for o in bpy.data.objects if o.type == "ARMATURE"), None)
|
||||||
|
if arm is None:
|
||||||
|
raise SystemExit("no armature in the .blend")
|
||||||
|
mesh = next((o for o in bpy.data.objects if o.type == "MESH"), None)
|
||||||
|
log(f"rig: {arm.name}, {len(arm.pose.bones)} bones; mesh {mesh.name if mesh else 'none'} "
|
||||||
|
f"{len(mesh.data.vertices) if mesh else 0} verts")
|
||||||
|
|
||||||
|
bpy.context.scene.render.fps = FPS
|
||||||
|
clear_animation(arm)
|
||||||
|
idle = clip_idle(arm, CLIPS["Idle"])
|
||||||
|
walk = clip_walk(arm, CLIPS["Walk"])
|
||||||
|
|
||||||
|
bpy.ops.object.mode_set(mode="OBJECT")
|
||||||
|
# Export armature + mesh together: the preset's character flags carry the
|
||||||
|
# bake_anim settings that make Unreal create AnimSequences.
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
arm.select_set(True)
|
||||||
|
if mesh:
|
||||||
|
mesh.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = arm
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(args.out), exist_ok=True)
|
||||||
|
fbx_preset.export_character(args.out, selection=True)
|
||||||
|
log(f"exported {args.out}")
|
||||||
|
|
||||||
|
if args.report:
|
||||||
|
with open(args.report, "w", encoding="utf-8") as handle:
|
||||||
|
json.dump({
|
||||||
|
"fps": FPS,
|
||||||
|
"rig": arm.name,
|
||||||
|
"bones": len(arm.pose.bones),
|
||||||
|
"clips": [
|
||||||
|
{"action": idle.name, "frames": CLIPS["Idle"]},
|
||||||
|
{"action": walk.name, "frames": CLIPS["Walk"]},
|
||||||
|
],
|
||||||
|
"axis_evidence": "probe: upperleg01.L X 0.5 rad moves foot.L "
|
||||||
|
"Y -0.022 -> +0.362; Y twists, Z abducts",
|
||||||
|
"amplitudes_rad": {"hip": HIP, "knee": KNEE, "arm": ARM, "elbow": ELBOW},
|
||||||
|
"export": os.path.basename(args.out),
|
||||||
|
"log": PARTS,
|
||||||
|
}, handle, indent=2)
|
||||||
|
print(f"[anim] report -> {args.report}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -49,12 +49,25 @@ _METRIC = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Character exports differ only in the object set and the armature/path flags.
|
# Character exports differ only in the object set and the armature/path flags.
|
||||||
|
#
|
||||||
|
# Animation export (added for the 0.2.0 animation set): without `bake_anim` the
|
||||||
|
# exporter silently writes a static bind pose even when the armature carries
|
||||||
|
# actions, and Unreal then imports a character with no AnimSequence and no error.
|
||||||
|
# `bake_anim_use_all_actions` exports every action as its own clip; NLA strips are
|
||||||
|
# off because the project does not author NLA. `bake_anim_simplify_factor=0.0`
|
||||||
|
# keeps the sampled keys exactly as authored - the clips are short, so the size
|
||||||
|
# cost is irrelevant and a simplified curve would drift from the source poses.
|
||||||
_CHARACTER = dict(
|
_CHARACTER = dict(
|
||||||
_METRIC,
|
_METRIC,
|
||||||
object_types={"ARMATURE", "MESH"},
|
object_types={"ARMATURE", "MESH"},
|
||||||
add_leaf_bones=False,
|
add_leaf_bones=False,
|
||||||
path_mode="COPY",
|
path_mode="COPY",
|
||||||
embed_textures=False,
|
embed_textures=False,
|
||||||
|
bake_anim=True,
|
||||||
|
bake_anim_use_all_actions=True,
|
||||||
|
bake_anim_use_nla_strips=False,
|
||||||
|
bake_anim_force_startend_keying=True,
|
||||||
|
bake_anim_simplify_factor=0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
PRESETS = {"ue_metric_v1": _METRIC, "ue_character_v1": _CHARACTER}
|
PRESETS = {"ue_metric_v1": _METRIC, "ue_character_v1": _CHARACTER}
|
||||||
|
|||||||
@@ -39,8 +39,11 @@ run_cmdlet ue_build_review_map.py "5/6 stage the demo shot cameras"
|
|||||||
tail -3 /tmp/ue-review-map-result.txt 2>/dev/null || true
|
tail -3 /tmp/ue-review-map-result.txt 2>/dev/null || true
|
||||||
|
|
||||||
echo "== 6/6 capture the demo map =="
|
echo "== 6/6 capture the demo map =="
|
||||||
./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" /Game/Hollywood/DemoMap \
|
# Absolute engine path: this step runs from the repo, not from $ENGINE, and a bare
|
||||||
-game -windowed -ResX=1920 -ResY=1080 -nopause -nosplash -stdout
|
# ./Engine/... failed with "No such file or directory" while every commandlet above
|
||||||
|
# succeeded through run_cmdlet's cd. -thirdperson so the review shows the body.
|
||||||
|
"$ENGINE/Engine/Binaries/Linux/UnrealEditor-Cmd" "$PROJ" /Game/Hollywood/DemoMap \
|
||||||
|
-game -windowed -ResX=1920 -ResY=1080 -nopause -nosplash -thirdperson -stdout
|
||||||
|
|
||||||
echo "== collect the demo screenshots =="
|
echo "== collect the demo screenshots =="
|
||||||
mkdir -p "$REPO/Screenshots/demo"
|
mkdir -p "$REPO/Screenshots/demo"
|
||||||
|
|||||||
@@ -52,6 +52,24 @@ class CharacterPresetTest(unittest.TestCase):
|
|||||||
self.assertEqual(kwargs["mesh_smooth_type"], "FACE")
|
self.assertEqual(kwargs["mesh_smooth_type"], "FACE")
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterAnimationTest(unittest.TestCase):
|
||||||
|
"""The animation flags are the difference between a clip and a silent bind pose."""
|
||||||
|
|
||||||
|
def test_bakes_animation(self):
|
||||||
|
kwargs = preset.preset_kwargs("ue_character_v1")
|
||||||
|
self.assertTrue(kwargs["bake_anim"], "without bake_anim Unreal imports no AnimSequence")
|
||||||
|
self.assertTrue(kwargs["bake_anim_use_all_actions"])
|
||||||
|
self.assertFalse(kwargs["bake_anim_use_nla_strips"])
|
||||||
|
|
||||||
|
def test_does_not_simplify_authored_keys(self):
|
||||||
|
kwargs = preset.preset_kwargs("ue_character_v1")
|
||||||
|
self.assertEqual(kwargs["bake_anim_simplify_factor"], 0.0)
|
||||||
|
|
||||||
|
def test_static_preset_stays_animation_free(self):
|
||||||
|
kwargs = preset.preset_kwargs("ue_metric_v1")
|
||||||
|
self.assertNotIn("bake_anim", kwargs)
|
||||||
|
|
||||||
|
|
||||||
class OverrideTest(unittest.TestCase):
|
class OverrideTest(unittest.TestCase):
|
||||||
def test_overrides_win(self):
|
def test_overrides_win(self):
|
||||||
kwargs = preset.preset_kwargs(mesh_smooth_type="OFF")
|
kwargs = preset.preset_kwargs(mesh_smooth_type="OFF")
|
||||||
|
|||||||
@@ -308,6 +308,11 @@ try:
|
|||||||
# (X -22750) and look NORTH toward the mass. The old camera was at
|
# (X -22750) and look NORTH toward the mass. The old camera was at
|
||||||
# X -22800 Y +2600 aiming at its own X, giving an 85%-sky frame.
|
# X -22800 Y +2600 aiming at its own X, giving an 85%-sky frame.
|
||||||
("demo-north-buildings", unreal.Vector(-22750, 1600, 500), unreal.Vector(-22750, -5200, 350)),
|
("demo-north-buildings", unreal.Vector(-22750, 1600, 500), unreal.Vector(-22750, -5200, 350)),
|
||||||
|
# 0.2.0 third-person body. Stands where the pawn's own boom camera sits —
|
||||||
|
# 3.4 m behind the spawn point, 1 m above it — and looks at the pawn, so
|
||||||
|
# the visible body and its idle clip are what the shot is for.
|
||||||
|
# Demo_PlayerStart is at (-13336, -1150, 600), yaw 90 (facing +Y).
|
||||||
|
("demo-third-person", unreal.Vector(-13336, -1750, 780), unreal.Vector(-13336, -1150, 560), 58.0),
|
||||||
]
|
]
|
||||||
seq = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer")
|
seq = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer")
|
||||||
if seq:
|
if seq:
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Import the character animation clips into Unreal (0.2.0).
|
||||||
|
#
|
||||||
|
# blender -b Art/blender/character/character-2026-11-07.blend \
|
||||||
|
# --python Tools/character_anim_build.py -- --out /tmp/gigline/character-anim.fbx
|
||||||
|
# <engine>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
|
||||||
|
# -script=<repo>/Tools/ue_character_anim_import.py -nullrhi -unattended -nopause -stdout
|
||||||
|
#
|
||||||
|
# Imports the animated FBX next to the existing character assets and verifies the
|
||||||
|
# result rather than assuming it: an FBX whose export omitted `bake_anim` imports
|
||||||
|
# without error and produces a skeletal mesh but no AnimSequence, which is exactly
|
||||||
|
# the failure this tool must catch.
|
||||||
|
#
|
||||||
|
# Destination: /Game/Test/Character. Result file: /tmp/gigline/character-anim-import.txt
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
|
||||||
|
import unreal
|
||||||
|
|
||||||
|
SRC = "/tmp/gigline/character-anim.fbx"
|
||||||
|
DEST = "/Game/Test/Character"
|
||||||
|
PREFIX = "Character_Anim"
|
||||||
|
RESULT = "/tmp/gigline/character-anim-import.txt"
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
lines.append(str(msg))
|
||||||
|
unreal.log(str(msg))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not os.path.exists(SRC):
|
||||||
|
log(f"missing {SRC}; run Tools/character_anim_build.py first")
|
||||||
|
return
|
||||||
|
|
||||||
|
task = unreal.AssetImportTask()
|
||||||
|
task.filename = SRC
|
||||||
|
task.destination_path = DEST
|
||||||
|
task.destination_name = PREFIX
|
||||||
|
task.automated = True
|
||||||
|
task.save = True
|
||||||
|
task.replace_existing = True
|
||||||
|
|
||||||
|
ui = unreal.FbxImportUI()
|
||||||
|
ui.import_mesh = True
|
||||||
|
ui.import_as_skeletal = True
|
||||||
|
ui.import_animations = True
|
||||||
|
ui.import_materials = True
|
||||||
|
ui.import_textures = False
|
||||||
|
# UE 5.8 has no SKELETON_ONLY content type (FBXICT_ALL/GEOMETRY/SKINNING_WEIGHTS
|
||||||
|
# only), so the clips come in with the mesh and the verification below decides
|
||||||
|
# whether that worked. The mesh is replaced at the same path by design.
|
||||||
|
ui.skeletal_mesh_import_data.import_uniform_scale = 1.0
|
||||||
|
task.options = ui
|
||||||
|
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||||
|
|
||||||
|
# Verify: the clips are the deliverable, not the mesh.
|
||||||
|
registry = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||||
|
found = []
|
||||||
|
for asset in registry.get_assets_by_path(DEST, recursive=False):
|
||||||
|
name = str(asset.asset_name)
|
||||||
|
if not name.startswith(PREFIX):
|
||||||
|
continue
|
||||||
|
obj = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}")
|
||||||
|
if isinstance(obj, unreal.AnimSequence):
|
||||||
|
length = obj.get_play_length()
|
||||||
|
# UE 5.8 exposes only play length and sequence length here; the sampled
|
||||||
|
# frame count is derived so the report still states the clip duration.
|
||||||
|
frames = round(length * 30.0)
|
||||||
|
found.append(name)
|
||||||
|
log(f"AnimSequence {name:34} {length:.3f} s (~{frames} frames at 30 fps)")
|
||||||
|
elif isinstance(obj, unreal.SkeletalMesh):
|
||||||
|
skeleton = obj.get_editor_property("skeleton")
|
||||||
|
log(f"SkeletalMesh {name:34} skeleton={'ok' if skeleton else 'MISSING'}")
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
log("FAILED: no AnimSequence was created — the FBX carried no baked animation")
|
||||||
|
lines.append("VERDICT: FAIL")
|
||||||
|
return
|
||||||
|
log(f"{len(found)} AnimSequence(s) imported: {', '.join(sorted(found))}")
|
||||||
|
lines.append("VERDICT: PASS")
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
import traceback
|
||||||
|
log(f"FAILED: {type(exc).__name__}: {exc}")
|
||||||
|
log(traceback.format_exc())
|
||||||
|
lines.append("VERDICT: FAIL")
|
||||||
|
|
||||||
|
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
|
||||||
|
os.makedirs(os.path.dirname(RESULT), exist_ok=True)
|
||||||
|
with open(RESULT, "w", encoding="utf-8") as handle:
|
||||||
|
handle.write("\n".join(lines) + "\n")
|
||||||
@@ -127,6 +127,10 @@ def build():
|
|||||||
ia_reset.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
ia_reset.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
||||||
save(ia_reset)
|
save(ia_reset)
|
||||||
|
|
||||||
|
ia_cam = fresh("IA_ToggleCamera", unreal.InputAction, unreal.InputAction_Factory)
|
||||||
|
ia_cam.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
||||||
|
save(ia_cam)
|
||||||
|
|
||||||
ia_pause = fresh("IA_Pause", unreal.InputAction, unreal.InputAction_Factory)
|
ia_pause = fresh("IA_Pause", unreal.InputAction, unreal.InputAction_Factory)
|
||||||
ia_pause.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
ia_pause.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
|
||||||
save(ia_pause)
|
save(ia_pause)
|
||||||
@@ -136,7 +140,7 @@ def build():
|
|||||||
(ia_move, "Gamepad_LeftY"), (ia_strafe, "Gamepad_LeftX"),
|
(ia_move, "Gamepad_LeftY"), (ia_strafe, "Gamepad_LeftX"),
|
||||||
(ia_look, "MouseX"), (ia_look, "MouseY"), (ia_look, "Gamepad_Right2D"),
|
(ia_look, "MouseX"), (ia_look, "MouseY"), (ia_look, "Gamepad_Right2D"),
|
||||||
(ia_jump, "SpaceBar"), (ia_jump, "Gamepad_FaceButton_Bottom"),
|
(ia_jump, "SpaceBar"), (ia_jump, "Gamepad_FaceButton_Bottom"),
|
||||||
(ia_reset, "R"), (ia_pause, "Escape"),
|
(ia_reset, "R"), (ia_pause, "Escape"), (ia_cam, "V"),
|
||||||
]
|
]
|
||||||
|
|
||||||
mappings = []
|
mappings = []
|
||||||
|
|||||||
@@ -48,6 +48,18 @@
|
|||||||
"Art/reference/north-chinese/01-grauman-s-chinese-theatre-by-carol-highsmith.jpg"
|
"Art/reference/north-chinese/01-grauman-s-chinese-theatre-by-carol-highsmith.jpg"
|
||||||
],
|
],
|
||||||
"expect": "The two north-side buildings (TCL Chinese Theatre, Madame Tussaud's) seen from across Hollywood Boulevard, full height in frame, daylight"
|
"expect": "The two north-side buildings (TCL Chinese Theatre, Madame Tussaud's) seen from across Hollywood Boulevard, full height in frame, daylight"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "demo-third-person",
|
||||||
|
"shot": "Screenshots/demo/review-demo-third-person.png",
|
||||||
|
"size": [
|
||||||
|
1920,
|
||||||
|
1080
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
"Art/reference/hollywood/el-capitan-facade-day-wikimedia-2009-10-27.jpg"
|
||||||
|
],
|
||||||
|
"expect": "Third-person view of the player body standing on the north sidewalk, full figure visible from behind, the hero facade across the street, daylight"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user