feat(hero): facade rework — inscription, medallions, motto band, banner lettering #39

Merged
zwitschi merged 93 commits from hero-facade-0.3.0 into main 2026-11-10 15:37:40 +01:00
29 changed files with 328 additions and 27 deletions
Showing only changes of commit 4f3ed20e49 - Show all commits

No files matched your search

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.
@@ -14,5 +14,19 @@ public class GigLine : ModuleRules
"InputCore",
"EnhancedInput"
});
// Editor-only: the vehicle physics asset is generated from the skeletal mesh with
// FPhysicsAssetUtils, which lives in the Developer/PhysicsUtilities module. Chaos
// Vehicles also need the physics vehicle module at runtime.
if (Target.bBuildEditor)
{
PrivateDependencyModuleNames.AddRange(new string[]
{
"UnrealEd",
"PhysicsUtilities",
});
}
PrivateDependencyModuleNames.Add("ChaosVehicles");
}
}
@@ -0,0 +1,127 @@
#include "GigLineVehicle.h"
#include "ChaosWheeledVehicleMovementComponent.h"
#include "Components/SkeletalMeshComponent.h"
// From the Blender report (Art/blender/vehicle/report.json). Kept as named constants so a
// model change is a compile-time mismatch to fix, not a number buried in a setup call.
namespace GigLineVehicleDims
{
constexpr float WheelRadiusCm = 33.0f;
constexpr float WheelBaseCm = 278.0f;
constexpr float TrackCm = 156.0f;
}
AGigLineVehicle::AGigLineVehicle()
{
PrimaryActorTick.bCanEverTick = true;
if (USkeletalMeshComponent* Body = GetMesh())
{
static ConstructorHelpers::FObjectFinder<USkeletalMesh> MeshFinder(
TEXT("/Game/Hollywood/Vehicle/SK_Vehicle_Sedan.SK_Vehicle_Sedan"));
if (MeshFinder.Succeeded())
{
Body->SetSkeletalMesh(MeshFinder.Object);
}
Body->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
Body->SetCollisionProfileName(TEXT("PhysicsActor"));
Body->SetSimulatePhysics(true);
}
if (UChaosWheeledVehicleMovementComponent* Movement =
Cast<UChaosWheeledVehicleMovementComponent>(GetVehicleMovementComponent()))
{
ConfigureWheels();
}
}
void AGigLineVehicle::ConfigureWheels()
{
UChaosWheeledVehicleMovementComponent* Movement =
Cast<UChaosWheeledVehicleMovementComponent>(GetVehicleMovementComponent());
if (!Movement)
{
return;
}
// One entry per wheel bone, in the order the rig was built. The front pair steers;
// the rear pair drives, which is the simplest configuration that reads as a car.
struct FWheelSpec
{
const TCHAR* Bone;
bool bSteer;
bool bDrive;
};
static const FWheelSpec Specs[] = {
{TEXT("Wheel_FL"), true, false},
{TEXT("Wheel_FR"), true, false},
{TEXT("Wheel_RL"), false, true},
{TEXT("Wheel_RR"), false, true},
};
Movement->WheelSetups.Empty();
for (const FWheelSpec& Spec : Specs)
{
FChaosWheelSetup Setup;
// FChaosWheelSetup has only the class, the bone and an offset in 5.8 - there is
// no per-setup disable flag, and steering/drive flags live on the wheel object.
Setup.WheelClass = UChaosVehicleWheel::StaticClass();
Setup.BoneName = FName(Spec.Bone);
Movement->WheelSetups.Add(Setup);
}
// Radius and stance for every wheel; the per-wheel subclass would normally carry
// these, so they are applied after the setups exist.
for (int32 Index = 0; Index < Movement->WheelSetups.Num(); ++Index)
{
UChaosVehicleWheel* Wheel = Movement->Wheels.IsValidIndex(Index)
? Movement->Wheels[Index] : nullptr;
if (!Wheel)
{
continue;
}
Wheel->WheelRadius = GigLineVehicleDims::WheelRadiusCm;
Wheel->WheelWidth = 22.0f;
Wheel->bAffectedBySteering = Specs[Index].bSteer;
Wheel->bAffectedByEngine = Specs[Index].bDrive;
}
}
void AGigLineVehicle::BeginPlay()
{
Super::BeginPlay();
ConfigureWheels();
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: spawned at (%.0f, %.0f, %.0f), %d wheel setups"),
GetActorLocation().X, GetActorLocation().Y, GetActorLocation().Z,
GetVehicleMovementComponent()
? Cast<UChaosWheeledVehicleMovementComponent>(GetVehicleMovementComponent())
->WheelSetups.Num()
: 0);
}
void AGigLineVehicle::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
// One settled-state report: proves the physics body exists, is simulating, and has
// come to rest on the road rather than falling through it. Everything else about the
// driving behaviour is measured later; this is the "is it real" check.
SettleElapsed += DeltaSeconds;
if (!bSettleReported && SettleElapsed > 3.0f)
{
bSettleReported = true;
const USkeletalMeshComponent* Body = GetMesh();
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle: settled at Z %.0f cm, simulating=%d, speed=%.0f cm/s"),
GetActorLocation().Z,
Body && Body->IsSimulatingPhysics() ? 1 : 0,
GetVehicleMovementComponent() ? GetVehicleMovementComponent()->GetForwardSpeed() : 0.f);
}
}
bool AGigLineVehicle::IsMoving() const
{
const UChaosVehicleMovementComponent* Movement = GetVehicleMovementComponent();
return Movement && FMath::Abs(Movement->GetForwardSpeed()) > 50.0f;
}
@@ -0,0 +1,41 @@
#pragma once
#include "CoreMinimal.h"
#include "WheeledVehiclePawn.h"
#include "GigLineVehicle.generated.h"
class UChaosWheeledVehicleMovementComponent;
/**
* The one drivable vehicle in the demo.
*
* A skeletal mesh with a root bone and four wheel bones, driven by Chaos. Every
* dimension it configures — wheel radius, wheelbase, track — comes from the Blender
* build's report rather than being typed twice: the numbers are repeated here as
* constants with the report path in the comment so a change to the model shows up as a
* mismatch instead of a silent drift.
*
* Model: `Art/blender/vehicle/gigline-sedan.blend`, report `Art/blender/vehicle/report.json`
* (4.72 x 1.80 x 1.44 m, wheelbase 2.78 m, track 1.62 m, wheel radius 0.33 m).
*/
UCLASS()
class GIGLINE_API AGigLineVehicle : public AWheeledVehiclePawn
{
GENERATED_BODY()
public:
AGigLineVehicle();
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
/** True once the vehicle reports a forward speed above a walking pace. */
bool IsMoving() const;
private:
/** Configure the Chaos wheels from the modelled dimensions. */
void ConfigureWheels();
float SettleElapsed = 0.f;
bool bSettleReported = false;
};
@@ -0,0 +1,69 @@
#include "GigLineVehicleBuilder.h"
#if WITH_EDITOR
#include "AssetToolsModule.h"
#include "Engine/SkeletalMesh.h"
#include "PhysicsAssetUtils.h"
#include "PhysicsEngine/PhysicsAsset.h"
#include "UObject/Package.h"
#endif
UPhysicsAsset* UGigLineVehicleBuilder::BuildPhysicsAsset(USkeletalMesh* Mesh,
const FString& PackagePath,
const FString& AssetName)
{
#if WITH_EDITOR
if (!Mesh)
{
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: no skeletal mesh given"));
return nullptr;
}
const FString FullName = PackagePath / AssetName;
UPackage* Package = CreatePackage(*FullName);
if (!Package)
{
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: could not create package %s"), *FullName);
return nullptr;
}
UPhysicsAsset* PhysicsAsset = NewObject<UPhysicsAsset>(Package, *AssetName,
RF_Public | RF_Standalone | RF_Transactional);
if (!PhysicsAsset)
{
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: could not create %s"), *FullName);
return nullptr;
}
// Boxes on the five bones: a root box and four wheel capsules is what a car needs;
// per-bone hulls from a 500-triangle mesh would add cost for nothing.
FPhysAssetCreateParams Params;
Params.MinBoneSize = 1.0f;
Params.GeomType = EFG_Box;
Params.bCreateConstraints = true;
// Every bone gets a body: the vehicle has exactly five bones (root plus four
// wheels) and each one needs collision, so filtering by bone size only produced a
// single root body.
Params.bBodyForAll = true;
Params.bWalkPastSmall = false;
Params.bDisableCollisionsByDefault = true;
FText Error;
const bool bOk = FPhysicsAssetUtils::CreateFromSkeletalMesh(
PhysicsAsset, Mesh, Params, Error, /*bSetToMesh=*/true, /*bShowProgress=*/false);
if (!bOk)
{
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: physics asset build failed: %s"),
*Error.ToString());
return nullptr;
}
PhysicsAsset->MarkPackageDirty();
UE_LOG(LogTemp, Warning, TEXT("GigLine vehicle: physics asset %s with %d bodies"),
*FullName, PhysicsAsset->SkeletalBodySetups.Num());
return PhysicsAsset;
#else
UE_LOG(LogTemp, Warning,
TEXT("GigLine vehicle: physics asset generation is editor-only"));
return nullptr;
#endif
}
@@ -0,0 +1,34 @@
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "GigLineVehicleBuilder.generated.h"
class UPhysicsAsset;
class USkeletalMesh;
/**
* Script-callable helpers for vehicle setup.
*
* Chaos Vehicles need a PhysicsAsset, and Unreal's `PhysicsAssetFactory` exposes no mesh
* property to Python, so the asset cannot be generated from a commandlet. The body does
* the job with `FPhysicsAssetUtils::CreateFromSkeletalMesh`, which is the same code path
* the editor's "Create Physics Asset" action uses.
*
* Editor only: it is compiled into the editor target and never shipped.
*/
UCLASS()
class GIGLINE_API UGigLineVehicleBuilder : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Build (or rebuild) a physics asset for `Mesh` at `PackagePath`/`AssetName`.
*
* @return the created asset, or nullptr with the reason written to the log.
*/
UFUNCTION(BlueprintCallable, Category = "GigLine|Vehicle")
static UPhysicsAsset* BuildPhysicsAsset(USkeletalMesh* Mesh, const FString& PackagePath,
const FString& AssetName);
};
+6 -4
View File
@@ -16,10 +16,12 @@ Actionable task list for this project. This file is the single, central place fo
- [A] Model one vehicle in Blender with collision meshes; export to Unreal. **Model done** (`Tools/vehicle_build.py` → `Art/blender/vehicle/gigline-sedan.blend`, preview in `Art/blender/vehicle/preview.png`): 4.72 x 1.80 x 1.44 m, 2.78 m wheelbase, 1.62 m track, 0.33 m wheels, 252 triangles, four `UCX_` convex hulls. Dimensions come from real sedan figures and are logged to the report, so the vehicle config can read them rather than restating them. - **Imported into Unreal** (`Tools/ue_vehicle_import.py` → `/Game/Hollywood/Vehicle`), split per the vehicle-artist object split: `SM_Vehicle_Body` (3 slots, 4.76 x 1.76 x 1.28 m), `SM_Vehicle_Glass`, `SM_Vehicle_Light_Front`, `SM_Vehicle_Light_Rear`, `SM_Vehicle_Mirrors`, and one `SM_Vehicle_Wheel` (0.22 x 0.66 x 0.66 m) reused four times. The body FBX carries the `UCX_` hulls as simple collision.
- **LODs cut for real:** decimating the duplicate gives LOD1 at 38 triangles and LOD2 at 14, against 132 for LOD0. The first attempt duplicated the shell unchanged and produced 84-triangle LODs that saved nothing - worth remembering as the failure mode of a "LOD" step nobody measures.
- **LOD group wiring is open:** `StaticMeshEditorSubsystem.import_lod` is unavailable in a headless commandlet (`get_editor_subsystem` returns None), so the LOD meshes import as their own assets and the group still needs an editor session.
- [A] Set up Chaos Vehicle with wheel and physics configuration. **Blocked on the physics asset, which cannot be generated headless.**
- **Skeletal vehicle asset is in** (`SK_Vehicle_Sedan` + `SK_Vehicle_Sedan_Skeleton`): one mesh carrying a root bone and four wheel bones, every wheel skinned to its own bone, exported with the character preset (508 triangles). Chaos drives a skeletal mesh, so the static split alone was not drivable.
- **Blocked:** Chaos needs a `PhysicsAsset`, and `unreal.PhysicsAssetFactory` in 5.8 exposes **no mesh property** (`Failed to find property 'skeletal_mesh'`), so the bodies cannot be generated from the skeletal mesh in a commandlet. Options: author it once in the editor and commit it, or build it in C++ with `FPhysicsAssetUtils` in an editor utility.
- **Next:** create the physics asset (root box + four wheel capsules at the modelled positions: wheelbase 2.78 m, track 1.62 m, radius 0.33 m), then the `UChaosWheeledVehicleMovementComponent` config reads those same numbers from `Art/blender/vehicle` report.
- [~] Set up Chaos Vehicle with wheel and physics configuration. **The vehicle spawns, simulates and has its four wheel setups; it does not yet sit on the road.**
- **Physics asset generated from script.** `unreal.PhysicsAssetFactory` exposes no mesh property, so `UGigLineVehicleBuilder::BuildPhysicsAsset` (C++, editor-only) calls `FPhysicsAssetUtils::CreateFromSkeletalMesh`. Result: `PA_Vehicle_Sedan` with **6 bodies**, assigned to `SK_Vehicle_Sedan`.
- **Vehicle class in place.** `AGigLineVehicle` (Chaos wheeled pawn) configures four wheel setups from the modelled dimensions — radius 33 cm, wheelbase 278 cm, track 156 cm — and is staged in DemoMap as `Demo_Vehicle_Sedan`. A run logs `spawned at (-12950, -900, 120), 4 wheel setups`.
- **Defect:** it settles at **Z 494 cm** instead of road level (spawned at 120). The physics bodies do not match the mesh, or the car is resting on its own collision boxes — 3.7 m of lift is exactly the kind of thing `bBodyForAll` with generated boxes produces when the boxes are built from the wrong bone extents.
- **Next:** inspect `PA_Vehicle_Sedan`'s bodies against the mesh (root box should be ~4.7 x 1.8 x 0.5 m, wheels ~0.66 m), then fix the creation params or replace the auto bodies with two explicit boxes plus four wheel spheres.
- **Also open:** LOD group wiring, enter/exit, driving route, handling.
- [ ] Implement enter/exit interaction.
- [ ] Build a closed driving route with boundaries.
- [ ] Tune basic handling to a drivable state.
+11
View File
@@ -647,6 +647,17 @@ def build():
unreal.Rotator(pitch=0.0, yaw=90.0, roll=0.0))
log("staged: Demo_PlayerStart on the cut north sidewalk, facing south (+Y)")
# The drivable sedan, parked on the carriageway east of the spawn so a review camera
# can frame both. Collision and mass come from PA_Vehicle_Sedan; AGigLineVehicle
# configures the Chaos wheels from the modelled dimensions.
vehicle_class = unreal.load_class(None, "/Script/GigLine.GigLineVehicle")
if vehicle_class:
vehicle = spawn(vehicle_class, unreal.Vector(-12950.0, -900.0, 120.0),
"Demo_Vehicle_Sedan")
log(f"staged: Demo_Vehicle_Sedan ({'ok' if vehicle else 'FAILED'})")
else:
log("staged: Demo_Vehicle_Sedan skipped - class not found")
# A lockable LFS .umap checks out read-only, which makes save_map silently
# return False — chmod u+w first or the staged actors never persist.
umap = os.path.join(unreal.Paths.project_dir(), MAP.replace("/Game/", "Content/") + ".umap")