Wrong transform of procedurally created mesh

I’m trying to implement basic dungeon generator and can’t get proper transforms of child meshes.

Here is what I do:

  • add USceneComponent as RootComponent
  • create static mesh subobjects, attach to RootComponent
  • set created mesh transform to previous connected mesh with additional translation based on prev mesh extent.

I’m printing resulting transform and it looks ok, i.e. (100.0, 0.0, 0.0…) when previous object size is 100.0.
And here is the resulting image:

111117-screenshot+2016-10-16+15.28.55.png

As you can see objects is spawned, but translation is too small (like 5.0 instead of 100.0).
I guess I’m doing something wrong in GenerateRoomSuitingNode method, please see code below.


Related code:

ADungeonGenerator::ADungeonGenerator() {
    PrimaryActorTick.bCanEverTick = true;
    
    RootComponent = CreateDefaultSubobject<USceneComponent>("SceneRoot");

    MaxDepth = 10;
    NodesCount = 0;

    Generate();

    SetActorEnableCollision(true);
}

void ADungeonGenerator::Generate() {
    Nodes = TArray<FDungeonNode>();
    FDungeonNode Root = FDungeonNode();
    GenerateRoomSuitingNode(Root, Root, EDungeonNodeSide::None, false); 
    Nodes.Add(Root); 
    TraverseAndSpawnNodesFrom(Root);
}

void ADungeonGenerator::GenerateRoomSuitingNode(FDungeonNode& Parent, FDungeonNode& Node, EDungeonNodeSide Side, bool CalcTransform) {
    static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeMeshAsset(TEXT("StaticMesh'/Game/Geometry/Meshes/Shape_Cube.Shape_Cube'"));
    if (CubeMeshAsset.Succeeded()) {
        NodesCount = NodesCount + 1;
        FString Id = "NodeMesh";
        Id.AppendInt(NodesCount);
        UStaticMeshComponent* Mesh = CreateDefaultSubobject<UStaticMeshComponent>(*Id);
        Mesh->SetStaticMesh(CubeMeshAsset.Object);
        Mesh->SetupAttachment(RootComponent);
        Node.Mesh = Mesh;        

        Mesh->SetMobility(EComponentMobility::Movable);
        if (CalcTransform) {
            Node.Transform = FTransform(Parent.TransformForNodeSide(Side));
            UE_LOG(LogTemp, Warning, TEXT("Transf %s"), *Node.Transform.ToString());
        }
        Mesh->SetRelativeTransform(Node.Transform);
        Mesh->UpdateComponentToWorld();
    }
}

void ADungeonGenerator::TraverseAndSpawnNodesFrom(FDungeonNode& Node) {
    TraverseAndSpawnNodeVia(Node, EDungeonNodeSide::North);
    TraverseAndSpawnNodeVia(Node, EDungeonNodeSide::East);
    TraverseAndSpawnNodeVia(Node, EDungeonNodeSide::South);
    TraverseAndSpawnNodeVia(Node, EDungeonNodeSide::West);
}

void ADungeonGenerator::TraverseAndSpawnNodeVia(FDungeonNode& Node, EDungeonNodeSide Side) {
    if (Node.GetTypeAt(Side) == EDungeonNodeSideType::Wall) return;
    if (Node.Depth + 1 > MaxDepth) return;    

    FDungeonNode NewNode = FDungeonNode(Node, Side, EDungeonNodeSideType::Wall, EDungeonNodeSideType::Wall,
                                                    EDungeonNodeSideType::Wall, EDungeonNodeSideType::Wall); 

    GenerateRoomSuitingNode(Node, NewNode, Side); 
    Nodes.Add(NewNode);
    TraverseAndSpawnNodesFrom(NewNode);
}

// FDungeonNode method 
FTransform TransformForNodeSide(EDungeonNodeSide Side) const {
    auto Extent = Mesh->CalcBounds(FTransform()).GetBox().GetExtent();
    float Dx = 0.0;
    float Dy = 0.0;
    switch (Side) {
        case EDungeonNodeSide::North:
            Dy = Extent.Y*2;
            break;
        case EDungeonNodeSide::East:
            Dx = Extent.X*2;
            break;
        case EDungeonNodeSide::South:
            Dy = -Extent.Y*2;
            break;
        case EDungeonNodeSide::West:
            Dx = -Extent.X*2;
            break;
    }
    auto NewTransform = FTransform(Transform);
    NewTransform.AddToTranslation(FVector(Dx, Dy, 0.0));
    return NewTransform;
}

In FDungeonNode::TransformForNodeSide you call CalcBounds on your mesh and pass it a new default FTransform. CalcBounds wants a LocalToWorld transform. Passing it a default transform will work for your root component if it’s spawned at (0, 0, 0), but any other component or root will not work properly. You can instead pass it Mesh->ComponentToWorld:

auto Extent = Mesh->CalcBounds(Mesh->ComponentToWorld).GetBox().GetExtent();

That might be your problem. All of your components are attached to the root, but you’re using a 0,0,0 relative origin for each one, rather than accumulating larger and larger transforms for each node on a branch. The first nodes on each branch from the root should still be working properly, however, if this is your only issue. Does FDungeonNode initialize its Tranform value?

In general, you might find this easier/more performant if you concentrate on only modifying the translation FVector and only passing that around rather than creating and passing multiple FTransforms - assuming that you’re only modifying translation and not doing any rotation or scale. You can use SetRelativeLocation rather than SetRelativeTransform. Notice you return a new FTransform from TransformForNodeSide, and then immediately rebuild the new FTransform with the returned value. You could instead do something like this:

 //within GenerateRoomSuitingNode
 ...
 
 if (CalcTransform) {
     Mesh->SetRelativeLocation(Parent.TranslationForNodeSide(Side));
     Node.Transform = FTransform(Mesh->RelativeRotation, Mesh->RelativeLocation, Mesh->RelativeScale3D);
     UE_LOG(LogTemp, Warning, TEXT("Transf %s"), *Node.Transform.ToString());
 }
 
 ...
 
 // FDungeonNode method 
 FVector TranslationForNodeSide(EDungeonNodeSide Side) const {
     auto Extent = Mesh->CalcBounds(Mesh->ComponentToWorld).GetBox().GetExtent();
     float Dx = 0.0;
     float Dy = 0.0;
     switch (Side) {
         case EDungeonNodeSide::North:
             Dy = Extent.Y*2;
             break;
         case EDungeonNodeSide::East:
             Dx = Extent.X*2;
             break;
         case EDungeonNodeSide::South:
             Dy = -Extent.Y*2;
             break;
         case EDungeonNodeSide::West:
             Dx = -Extent.X*2;
             break;
     }
     return RelativeLocation + FVector(Dx, Dy, 0.0f);
 }

It’s not a huge performance draw, but it might make things easier for you. You also can store the relative translation value instead of the transform in your node, or (since you’re holding a pointer to the mesh anyway) just use Mesh->RelativeLocation directly within the FDungeonNode and avoid redundant data storage. If you have large maps that’s might be worth the RAM savings.

Also, within TraverseAndSpawnNodeVia your line to call GenerateRoomSuitingNode looks like this:

GenerateRoomSuitingNode(Node, NewNode, Side);

I’m assuming GenerateRoomSuitingNode has a default value of “true” for CalcTransform, e.g.:

void GenerateRoomSuitingNode(FDungeonNode& Parent, FDungeonNode& Node, EDungeonNodeSide Side, bool CalcTransform = true);

Otherwise you have a problem there in that CalcTransform is never called; you mention that you are logging the new transform values so I have to assume that’s not the issue.

How many nodes have you been spawning along a branch for your tests? Do the second and third nodes show the same translation, or do they move progressively further? Is their spacing consistent?

Can you post the log output from your transforms?

Would you mind posting the FDungeonNode (FStruct?) source?

Thank you so much, @GigasightMedia! It works with RelativeLocation fix! Here is the source in case you interested: Discover gists · GitHub

@ - For some reason AnswerHub won’t let me post this as a reply to your comment, so I’m putting it here.

Glad it worked for you!

In looking at your code I noticed I may have introduced a redundancy.
Within GenerateRoomSuitingNode you currently have this:

if (CubeMeshAsset.Succeeded()) {
    ...
    if (CalcTransform) {
        Mesh->SetRelativeLocation(Parent.TranslationForNodeSide(Side));
        UE_LOG(LogTemp, Warning, TEXT("Relative location: %s"), *Mesh->RelativeLocation.ToString());
        Node.Transform = FTransform(Mesh->RelativeRotation, Mesh->RelativeLocation, Mesh->RelativeScale3D);
        UE_LOG(LogTemp, Warning, TEXT("Transform: %s"), *Node.Transform.ToString());
    }
    Mesh->SetRelativeTransform(Node.Transform);
    Mesh->UpdateComponentToWorld();

}

Calling “SetRelativeTransform” and “UpdateComponentToWorld” are redundant after calling “SetRelativeLocation” - as the local transform is updated by that method. Also, you can just retrieve the updated transform from your mesh rather than building a new one.

If you log the transform value before and after each line you’ll see that GetRelativeTransform returns the same FTransform each time after SetRelativeLocation.

UE_LOG(LogTemp, Warning, TEXT("Transform: %s"), *Mesh->GetRelativeTransform().ToString());
Mesh->SetRelativeTransform(Node.Transform);
UE_LOG(LogTemp, Warning, TEXT("Transform: %s"), *Mesh->GetRelativeTransform().ToString());
Mesh->UpdateComponentToWorld();
UE_LOG(LogTemp, Warning, TEXT("Transform: %s"), *Mesh->GetRelativeTransform().ToString());

This will be cleaner:

if (CubeMeshAsset.Succeeded()) {
    ...
    if (CalcTransform) {
        Mesh->SetRelativeLocation(Parent.TranslationForNodeSide(Side));
        Node.Transform = Mesh->GetRelativeTransform();

    }

}

Since you’re just assigning a default transform to Node.Transform, if you don’t calculate a new transform, there’s no reason to SetRelativeTransform if CalcTransform is false. You’re essentially calling Mesh->SetRelativeTransform(Mesh->GetRelativeTransform()). Do you even need FDungeonNode ::Transform at all now? You have a reference to the Mesh within FDungeonNode struct, you can always pull the current value via Mesh->GetRelativeTransform() rather than storing a duplicate. Depends on what you’re doing with the value elsewhere.

UpdateComponentToWorld is useful if you directly assign a value to RelativeLocation or RelativeRotation, rather than using the Set and Add methods, but otherwise isn’t necessary.

Thanks again! Yeah, have removed Node::Transform, it is useless now. At first I thought to separate Nodes logic and actual Mesh instances, but quickly realised I have to access Mesh bounds anyway so moved everything inside Node.