How to select instant3Dhub-Geometry with LineTraces#
In this guide we will implement a C++ function that selects instant3Dhub geometry by tracing a ray against the world and then analyzing the hit result.
We assume there is an HubConnector that is already connected to a 3D space. Additionally, a start point (FVector RayStart) and end point (FVector RayEnd) of a ray is given that is used for the LineTrace.
Below is a function scaffold that will be filled with logic in the following steps:
void SelectGeometryWithLineTrace(AI3DHConnector* HubConnector,
FVector RayStart,
FVector RayEnd)
{
check(IsValid(HubConnector));
check(HubConnector->IsConnectedToHub());
// 1. Constrain the ray by clip planes
// 2. Trace and analyze the hit
// 3. Select the geometry that was hit
}
1. Constrain the ray by clip planes#
To prevent selection of geometry that is currently hidden we need to constrain the ray to the space that is not clipped. A utility function takes care of this:
// 1. Constrain the ray by clip planes
if (!UI3DHUtilities::ClipLineSegmentByClipPlanes(HubConnector, RayStart, RayEnd))
{
// The ray lies completely inside the clipped space – nothing to select.
return;
}
2. Trace and analyze the hit#
Next we trace a ray against the world using the
instant3Dhub Geometry Profile. The trace might hit any blocking, visible geometry in the world, so afterwards we pass the raw FHitResult to UI3DHUtilities::AnalyzeHitResult. The helper returns true and fills an FI3DHHitResult when the hit was produced by instant3Dhub geometry.
Make sure the trace result contains a valid face index by enabling bReturnFaceIndex. AnalyzeHitResult uses FHitResult::FaceIndex when it resolves the exact instant3Dhub detail node that was hit.
// 2. Trace and analyze the hit
FI3DHHitResult I3DHHit; // holds instant3Dhub‑specific hit data
UWorld* World = HubConnector->GetWorld();
FHitResult HitResult;
FCollisionQueryParams QueryParams;
QueryParams.bReturnFaceIndex = true;
if (!World->LineTraceSingleByProfile(HitResult,
RayStart,
RayEnd,
UInstant3DHubSettings::GetChecked().GeometryCollisionProfileName,
QueryParams))
{
return; // the trace didn't hit anything
}
if (!UI3DHUtilities::AnalyzeHitResult(HitResult, I3DHHit, true))
{
return; // an object was hit, but it is NOT instant3Dhub geometry
}
The third argument requests structure mapping for the hit geometry when it is not available yet. This request is asynchronous: the current call still returns immediately, and the exact DetailNodeId is only available in future traces after the mapping has been loaded. Until then, bHasStructureMapping is false and DetailNodeId falls back to StructureNodeId.
Tip
AnalyzeHitResult works with any method that produces an FHitResult. LineTraceSingleByChannel, SweepSingleByObjectType, etc. Feel free to use whatever suits your use case.
Note
Blueprint trace nodes return face indices by default, so no extra trace-parameter setup is needed there. Keep Project Settings > Physics > Optimization > Suppress Face Remap Table disabled; enabling it can prevent trace results from carrying usable face indices.
3. Select the geometry#
FI3DHHitResult contains everything we need to identify the node of the hit geometry. StructureNodeId is available as soon as the trace hits instant3Dhub geometry. DetailNodeId corresponds to the smallest addressable Node of the geometry that was hit only when bHasStructureMapping is true. Otherwise, DetailNodeId is set to StructureNodeId as a fallback.
The NodeId can now be added to the Selection. In this example we will first clear the active Selection. Once cleared, the NodeId will be added to the Selection, making it the only Node that is selected.
This example uses the fallback value, so the first trace can select the containing structure while the detail-node mapping is being requested. If your workflow must select only the exact leaf node, check I3DHHit.bHasStructureMapping before selecting and retry the trace once the mapping is available.
API calls that are synchronized via the 3D space typically expect an OnComplete Callback Function. The bound function is always called on the GameThread with an appropriate ErrorCode. In this example, we have a nested setup that first clears the selection and, once this operation is completed, selects the Geometry using its NodeId.
// 3. Select the geometry
const int32 NodeId = I3DHHit.DetailNodeId;
TSharedPtr<FI3DHConnectorAPI> API = HubConnector->GetAPI();
API->GetSelectionAPI()->ClearSelection([API, NodeId](EI3DHErrorCode ErrorCode)
{
API->GetSelectionAPI()->AddToSelection(NodeId,
[](const TArray<int32>& Selection, EI3DHErrorCode ErrorCode)
{
if (ErrorCode == EI3DHErrorCode::Success)
{
UE_LOG(LogTemp, Verbose, TEXT("Successfully selected NodeId: '%d'"), NodeId);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("%s – failed to select NodeId %d"),
*I3DHStringFromErrorCode(ErrorCode), NodeId);
}
});
});
Note
Both FI3DHSelectionAPI::ClearSelection and FI3DHSelectionAPI::AddToSelection are asynchronous and run on a background thread. The provided lambdas are executed on the game thread when the operation completes.
Final result#
Putting it all together, the updated SelectGeometryWithLineTrace looks like this:
void SelectGeometryWithLineTrace(AI3DHConnector* HubConnector,
FVector RayStart,
FVector RayEnd)
{
check(IsValid(HubConnector));
check(HubConnector->IsConnectedToHub());
// 1. Constrain the ray by clip planes
if (!UI3DHUtilities::ClipLineSegmentByClipPlanes(HubConnector, RayStart, RayEnd))
{
// The ray lies completely inside the clipped space – nothing to select.
return;
}
// 2. Trace and analyze hit
FI3DHHitResult I3DHHit;
FHitResult HitResult;
UWorld* World = HubConnector->GetWorld();
FCollisionQueryParams QueryParams;
QueryParams.bReturnFaceIndex = true;
if (!World->LineTraceSingleByProfile(HitResult,
RayStart,
RayEnd,
UInstant3DHubSettings::GetChecked().GeometryCollisionProfileName,
QueryParams))
{
return; // the trace didn't hit anything
}
if (!UI3DHUtilities::AnalyzeHitResult(HitResult, I3DHHit, true))
{
return; // an object was hit, but it is NOT instant3Dhub geometry
}
// 3. Select the geometry
const int32 NodeId = I3DHHit.DetailNodeId;
TSharedPtr<FI3DHConnectorAPI> API = HubConnector->GetAPI();
API->GetSelectionAPI()->ClearSelection([API, NodeId](EI3DHErrorCode ErrorCode)
{
API->GetSelectionAPI()->AddToSelection(NodeId,
[](const TArray<int32>& Selection, EI3DHErrorCode ErrorCode)
{
if (ErrorCode == EI3DHErrorCode::Success)
{
UE_LOG(LogTemp, Verbose, TEXT("Successfully selected NodeId: '%d'"), NodeId);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("%s – failed to select NodeId %d"),
*I3DHStringFromErrorCode(ErrorCode), NodeId);
}
});
});
}
Why not use AI3DHGeometry directly?#
You can bypass UI3DHUtilities::AnalyzeHitResult and grab the actor from the raw FHitResult yourself:
FHitResult HitResult;
if (World->LineTraceSingleByProfile(HitResult,
RayStart,
RayEnd,
UInstant3DHubSettings::GetChecked().GeometryCollisionProfileName))
{
AI3DHGeometry* GeometryActor = Cast<AI3DHGeometry>(HitResult.GetActor());
}
While this works, remember that AI3DHGeometry is marked experimental. Using AnalyzeHitResult and the resulting FI3DHHitResult keeps your code independent of the underlying actor implementation and therefore more resilient to future plugin changes.