fix(collision): terrain bbox proxy buried the whole street

The player never fell through - it stood on an invisible 8 m box. The
imported Terrain_DEM carried only a bounding-box simple collision proxy,
so every downward trace in the block hit Demo_Terrain_DEM at Z 819 (the
mesh bbox max) and the road/sidewalk underneath were unreachable. The
view from up there looked plausible, which is why it read as working.

- add UGigLineAssetTools (C++): set complex-as-simple collision on a
  static mesh; UE 5.8 exposes ECollisionTraceFlag, not
  bUseComplexAsSimpleCollision, and Python cannot set it at all.
- builder forces + saves the terrain asset as complex-as-simple (the flag
  lives on the asset, so saving only the .umap loses it).
- spawn (-13336, -1150, 600): inside the carved corridor; lands on the
  road at Z 456 (feet 368 on road top 366).
- HighResShot 1280x720, not "1280 720" (that is multiplier 1280).
- keep SIM + single ground-probe diagnostics (TEMP QA).
This commit is contained in:
zwitschi committed 2026-11-08 11:30:41 +01:00
1 parent 2bab0fb0cd
commit 2bb1e74814
14 files changed
+157 -21

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.
@@ -0,0 +1,65 @@
#include "GigLineAssetTools.h"
#include "Engine/StaticMesh.h"
#include "PhysicsEngine/BodySetup.h"
#include "StaticMeshResources.h"
void UGigLineAssetTools::SetComplexCollisionAsSimple(UStaticMesh* Mesh, bool bEnable)
{
if (!Mesh)
{
return;
}
UBodySetup* BodySetup = Mesh->GetBodySetup();
if (!BodySetup)
{
return;
}
BodySetup->CollisionTraceFlag = bEnable ? CTF_UseComplexAsSimple : CTF_UseSimpleAndComplex;
BodySetup->InvalidatePhysicsData();
BodySetup->CreatePhysicsMeshes();
Mesh->MarkPackageDirty();
Mesh->PostEditChange();
}
void UGigLineAssetTools::ClearSimpleCollision(UStaticMesh* Mesh)
{
if (!Mesh)
{
return;
}
UBodySetup* BodySetup = Mesh->GetBodySetup();
if (!BodySetup)
{
return;
}
BodySetup->AggGeom.EmptyElements();
BodySetup->InvalidatePhysicsData();
BodySetup->CreatePhysicsMeshes();
Mesh->MarkPackageDirty();
Mesh->PostEditChange();
}
FString UGigLineAssetTools::GetCollisionComplexity(UStaticMesh* Mesh)
{
if (!Mesh)
{
return TEXT("no mesh");
}
const UBodySetup* BodySetup = Mesh->GetBodySetup();
if (!BodySetup)
{
return TEXT("no body setup");
}
const int32 Flag = static_cast<int32>(BodySetup->CollisionTraceFlag.GetValue());
if (Flag == CTF_UseComplexAsSimple) { return TEXT("UseComplexAsSimple"); }
if (Flag == CTF_UseSimpleAsComplex) { return TEXT("UseSimpleAsComplex"); }
if (Flag == CTF_UseSimpleAndComplex) { return TEXT("UseSimpleAndComplex"); }
return TEXT("ProjectDefault");
}
@@ -0,0 +1,34 @@
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "GigLineAssetTools.generated.h"
class UStaticMesh;
/**
* Small editor-side helpers for the headless asset pipeline scripts.
*
* Exposed to Python so tools can fix up imported meshes that the Interchange
* importer gives unusable default collision. The terrain is the motivating
* case: without complex-as-simple it gets a bounding-box proxy, so the whole
* street sits under an invisible 8 m box and the player walks on the box lid.
*/
UCLASS()
class GIGLINE_API UGigLineAssetTools : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/** Make collision trace against the mesh triangles instead of a proxy hull. */
UFUNCTION(BlueprintCallable, Category = "GigLine|Assets")
static void SetComplexCollisionAsSimple(UStaticMesh* Mesh, bool bEnable = true);
/** Drop any generated simple collision (hulls/boxes) from the mesh. */
UFUNCTION(BlueprintCallable, Category = "GigLine|Assets")
static void ClearSimpleCollision(UStaticMesh* Mesh);
/** Report the current collision complexity: "SimpleAndComplex", "Simple", ... */
UFUNCTION(BlueprintCallable, Category = "GigLine|Assets")
static FString GetCollisionComplexity(UStaticMesh* Mesh);
};
@@ -134,7 +134,7 @@ void AGigLineCharacter::BeginPlay()
{
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
PC->ConsoleCommand(TEXT("HighResShot 1280 720"));
PC->ConsoleCommand(TEXT("HighResShot 1280x720"));
}
}, 1.5f, false);
@@ -146,15 +146,26 @@ void AGigLineCharacter::BeginPlay()
{
UCharacterMovementComponent* M = GetCharacterMovement();
UE_LOG(LogTemp, Log,
TEXT("GigLine SIM: t=%.2f Z=%.0f grav=%.1f fall=%d local=%d role=%d mode=%d ctrl=%s"),
TEXT("GigLine SIM: t=%.2f XYZ=(%.0f,%.0f,%.0f) grav=%.1f fall=%d local=%d role=%d mode=%d ctrl=%s"),
GetWorld() ? GetWorld()->GetTimeSeconds() : -1.f,
GetActorLocation().Z,
GetActorLocation().X, GetActorLocation().Y, GetActorLocation().Z,
M ? M->GetGravityZ() : 0.f,
(M && M->IsFalling()) ? 1 : 0,
IsLocallyControlled() ? 1 : 0,
(int)GetLocalRole(),
M ? (int)M->MovementMode : -1,
GetController() ? *GetController()->GetName() : TEXT("NONE"));
if (UWorld* W = GetWorld())
{
const FVector L = GetActorLocation();
FHitResult Hit;
FCollisionQueryParams Q(TEXT("GigLineGroundProbe"), false, this);
const bool bHit = W->LineTraceSingleByChannel(Hit,
FVector(L.X, L.Y, 3000.f), FVector(L.X, L.Y, -3000.f), ECC_Visibility, Q);
UE_LOG(LogTemp, Log, TEXT("GigLine GROUND: top=%.0f actor=%s"),
bHit ? Hit.ImpactPoint.Z : -9999.f,
bHit && Hit.GetActor() ? *Hit.GetActor()->GetActorLabel() : TEXT("none"));
}
}, 1.0f, true);
}
+10
View File
@@ -6,6 +6,16 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
---
## 2026-11-08 — The fall-through was an 8 m invisible box (QA branch)
- **Symptom:** player "spawned above ground but never fell", sat at Z 909, `fall=0`, `mode=1` (Walking), no fall-through, view looked plausible so it was mistaken for working.
- **Cause:** `Terrain_DEM` had only a **bounding-box simple collision proxy** (Interchange default). A line trace down at *every* Y through the block hit `Demo_Terrain_DEM` at exactly **819** — the mesh bbox max Z. The whole street was under an invisible lid; the pawn stood on the box, 8 m above the road.
- **Proof:** a debug `LineTraceSingleByChannel` profile at Y −1912…+1447 reported `top=819 actor=Demo_Terrain_DEM` for the entire street. After the fix it reports `Sidewalk_North 395`, `Road_Hollywood 366`, `Sidewalk_South 317`.
- **Fix:** force complex-as-simple collision on the terrain. Python **cannot** set it (`StaticMesh`/`BodySetup` expose no such property; `dir()` shows only `lod_for_collision`). Added `UGigLineAssetTools::SetComplexCollisionAsSimple` in the C++ module (`BodySetup->CollisionTraceFlag = CTF_UseComplexAsSimple`, UE 5.8 no longer has `bUseComplexAsSimpleCollision`).
- **Second gotcha:** the flag lives on the **asset**, not the map. Setting it in a builder session and saving only the `.umap` loses it — `EditorAssetLibrary.save_loaded_asset()` is required, otherwise the next run reloads the bbox proxy.
- **Third gotcha:** `HighResShot 1280 720` means **multiplier 1280** (UE warns "the high resolution screenshot multiplier is too large for your system"). The correct form is `HighResShot 1280x720`.
- Resulting spawn: `(-13336, -1150, 600)` — inside the carved corridor, lands on the road at Z 456 (feet 368 on road top 366).
## 2026-11-07 — Publication audit
- MPFB 2.0.18's installed `blender_manifest.toml` identifies its code as GPL-3.0-or-later; its versioned `LICENSE.md` explicitly puts bundled mesh, texture, and rig assets under CC0 1.0. The earlier CC0 claim for the add-on code was wrong.
+23 -7
View File
@@ -301,6 +301,19 @@ def build():
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
if mesh is None:
continue
# The imported terrain carries a bounding-box simple collision proxy,
# which puts an invisible lid ~8 m over the whole block: the player
# walks on that box instead of the street. Force trace against the real
# triangles so the carved street corridor is actually open.
if "Terrain" in name:
asset = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
if asset:
unreal.GigLineAssetTools.set_complex_collision_as_simple(asset, True)
# The flag lives on the asset, not the map: persist it or the
# next session reloads the bbox proxy and the street is buried.
saved_asset = unreal.EditorAssetLibrary.save_loaded_asset(asset)
log(f"terrain collision: {unreal.GigLineAssetTools.get_collision_complexity(asset)} "
f"(saved asset: {saved_asset})")
actor = spawn(unreal.StaticMeshActor, unreal.Vector(0, 0, 0), f"Demo_{name}")
if actor:
comp = actor.static_mesh_component
@@ -372,14 +385,17 @@ def build():
# sidewalk is at Y -1912..-1053 and the SOUTH sidewalk at Y +718..+1447.
# The hero facade is on the SOUTH side at Y +1307. Stand on the north
# sidewalk and face south (+Y, yaw 90) across the road at it.
# Z: the terrain here is ~Z 99-138. The capsule half-height is 87.5 cm, so
# feet must start ABOVE the surface or the point is embedded and UE rejects
# the PlayerStart ("NO PLAYERSTART with positive rating"), dropping the pawn
# at the world origin to fall through. Cap feet ~80 cm above grade: centre
# ~138+80+87 = 305, so Z 300 lets it drop cleanly and gravity settles it.
spawn(unreal.PlayerStart, unreal.Vector(-13336.0, -1700.0, 300.0), "Demo_PlayerStart",
# CRITICAL: the terrain is only carved inside the street corridor, and
# terrain_from_dem.py --corridor 10.7 is a HALF-width around the road
# centreline (Y ~= -167), so the cut band is Y -1237..+902. A PlayerStart at
# Y -1700 sits OUTSIDE it, under ~8 m of uncut terrain: the capsule spawns
# embedded, depenetration pops it to Z ~909 (terrain top 819 + half-height
# 87.5) and it never touches the sidewalk. Y -1150 is on the north sidewalk
# (Y -1912..-1053) AND inside the cut, so the pavement is the ground.
# Z 600 is clear air; the capsule drops onto the pavement and settles.
spawn(unreal.PlayerStart, unreal.Vector(-13336.0, -1150.0, 600.0), "Demo_PlayerStart",
unreal.Rotator(pitch=0.0, yaw=90.0, roll=0.0))
log("staged: Demo_PlayerStart on the north sidewalk (cap above grade), facing south (+Y)")
log("staged: Demo_PlayerStart on the cut north sidewalk, facing south (+Y)")
# 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.