├── .gitignore ├── Content └── Example │ ├── HoudiniTrafficDemo.umap │ └── he_example_zone_shape_output.uasset ├── HoudiniMassTranslator.uplugin ├── LICENSE ├── README.md └── Source └── HoudiniMassTranslator ├── HoudiniMassTranslator.Build.cs ├── Private ├── HoudiniInputZoneShape.cpp ├── HoudiniMassCommands.cpp ├── HoudiniMassTranslator.cpp └── HoudiniOutputZoneShape.cpp └── Public ├── HoudiniInputZoneShape.h ├── HoudiniMassCommands.h ├── HoudiniMassCommon.h ├── HoudiniMassTranslator.h └── HoudiniOutputZoneShape.h /.gitignore: -------------------------------------------------------------------------------- 1 | Intermediate/ 2 | Binaries/ 3 | __pycache__/ 4 | backup/ -------------------------------------------------------------------------------- /Content/Example/HoudiniTrafficDemo.umap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianPanGithub/HoudiniMassTranslator/f4f3085fdb8c78bdcc60df5c08c22643d9c1e59f/Content/Example/HoudiniTrafficDemo.umap -------------------------------------------------------------------------------- /Content/Example/he_example_zone_shape_output.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianPanGithub/HoudiniMassTranslator/f4f3085fdb8c78bdcc60df5c08c22643d9c1e59f/Content/Example/he_example_zone_shape_output.uasset -------------------------------------------------------------------------------- /HoudiniMassTranslator.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "FriendlyName": "HoudiniMassTranslator", 6 | "Description": "", 7 | "Category": "Other", 8 | "CreatedBy": "", 9 | "CreatedByURL": "", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "SupportedTargetPlatforms": [ 15 | "Win64", "Mac", "Linux" 16 | ], 17 | "IsBetaVersion": false, 18 | "IsExperimentalVersion": false, 19 | "Installed": false, 20 | "Modules": [ 21 | { 22 | "Name": "HoudiniMassTranslator", 23 | "Type": "Editor", 24 | "LoadingPhase": "Default", 25 | "PlatformAllowList": [ 26 | "Win64", "Mac", "Linux" 27 | ] 28 | } 29 | ], 30 | "Plugins": [ 31 | { 32 | "Name": "HoudiniEngine", 33 | "Enabled": true 34 | }, 35 | { 36 | "Name": "ZoneGraph", 37 | "Enabled": true 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Yuzhe Pan (childadrianpan@gmail.com). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Houdini Mass Translator 2 | 3 | Welcome to the [repository](https://github.com/AdrianPanGithub/HoudiniMassTranslator) for the Houdini Mass Translator For Unreal. 4 | 5 | This plug-in provides seamless integration between Houdini Engine and Zone Graph, allow zone shape input and output. 6 | Also see [Tutorial](https://youtu.be/HAM8_OP_Fyc?si=K_1HXkTBwF1rLYVB) 7 | 8 | # Compatibility 9 | 10 | This plug-in relies on **my custom** [HoudiniEngineForUnreal](https://github.com/AdrianPanGithub/HoudiniEngineForUnreal), so same [compatibility](https://github.com/AdrianPanGithub/HoudiniEngineForUnreal#compatibility) as the Houdini Engine. (Examples only run with >= 5.4) 11 | 12 | # Installation 13 | 01. In this GitHub [repository](https://github.com/AdrianPanGithub/HoudiniMassTranslator), click [Releases](https://github.com/AdrianPanGithub/HoudiniMassTranslator/releases) on the right side. 14 | 02. Download the Houdini Mass Translator zip file that matches your Unreal Engine Version. 15 | 03. Extract the **HoudiniMassTranslator** and **HoudiniEngine** to the **Plugins** of your Unreal Project Directory. 16 | 17 | e.g. `C:/Unreal Projects/MyGameProject/Plugins/HoudiniMassTranslator` and `C:/Unreal Projects/MyGameProject/Plugins/HoudiniEngine` 18 | 19 | # Tutorial/Example 20 | [Tutorial](https://youtu.be/HAM8_OP_Fyc?si=K_1HXkTBwF1rLYVB) Require Unreal Engine >= 5.4 to run the example: 21 | 01. Download the [CitySample](https://www.fab.com/listings/4898e707-7855-404b-af0e-a505ee690e68) project (Unreal Engine >= 5.4) from Fab Store. 22 | 02. Put these two plug-ins into **CitySample/Plugins folder**. 23 | 24 | 03. Open `/HoudiniMassTranslator/Example/HoudiniTrafficDemo` in the content of this plug-in, and simulate. 25 | 26 | Also see what can be achieved by Only using your HDAs and these two unreal plugins: [City Toolchains](https://youtu.be/5Vp5nAFq1X8?si=IGSDG4cUdsefwn5x) 27 | 28 | # Usage Brief 29 | 30 | Support both input and output of mass ai zone shapes 31 | 32 | Many properties on zone shape and zone shape points can be set by @**unreal_uproperty_***, such as i@unreal_uproperty_**PolygonRoutingType**, f@unreal_uproperty_**InnerTurnRadius**. 33 | 34 | Here are some specific attributes for zone shape input and output: 35 | 36 | i@**unreal_output_zone_shape** 37 | 38 | = 1 on detail, all curves will output as zone shapes 39 | i@**unreal_zone_shape_type** / s@**unreal_zone_shape_type** 40 | 41 | define whether a curve is represented as zone shape spline or polygon (intersection). 42 | s[]@**unreal_zone_shape_tags** 43 | 44 | specify zone graph tags on zone shape. 45 | d[]@**unreal_zone_lane_profile** 46 | 47 | Represent Lanes. Will find or create lane profiles based on this attribute when output. could be both on point and prim. Please click menu "Build/Clean Up Houdini Lane Profiles" at last. 48 | s@**unreal_zone_lane_profile_name** 49 | 50 | Will find lane profiles based on this attribute, could be both on point and prim at same time. 51 | p@**rot** 52 | 53 | Specify polygon zone shape point directions. -------------------------------------------------------------------------------- /Source/HoudiniMassTranslator/HoudiniMassTranslator.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) <2025> Yuzhe Pan (childadrianpan@gmail.com). All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class HoudiniMassTranslator : ModuleRules 6 | { 7 | public HoudiniMassTranslator(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicIncludePaths.AddRange( 12 | new string[] { 13 | 14 | } 15 | ); 16 | 17 | 18 | PrivateIncludePaths.AddRange( 19 | new string[] { 20 | 21 | } 22 | ); 23 | 24 | 25 | PublicDependencyModuleNames.AddRange( 26 | new string[] 27 | { 28 | "Core", 29 | } 30 | ); 31 | 32 | 33 | PrivateDependencyModuleNames.AddRange( 34 | new string[] 35 | { 36 | "CoreUObject", 37 | "Engine", 38 | "Slate", 39 | "SlateCore", 40 | "Json", 41 | "HoudiniEngine", 42 | "ZoneGraph", 43 | "ToolMenus", 44 | "UnrealEd", 45 | "DeveloperToolSettings", 46 | } 47 | ); 48 | 49 | 50 | DynamicallyLoadedModuleNames.AddRange( 51 | new string[] 52 | { 53 | // ... add any modules that your module loads dynamically here ... 54 | } 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Source/HoudiniMassTranslator/Private/HoudiniInputZoneShape.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) <2025> Yuzhe Pan (childadrianpan@gmail.com). All Rights Reserved. 2 | 3 | #include "HoudiniInputZoneShape.h" 4 | 5 | #include "ZoneGraphSettings.h" 6 | #include "ZoneShapeComponent.h" 7 | 8 | #include "HoudiniApi.h" 9 | #include "HoudiniEngine.h" 10 | #include "HoudiniEngineUtils.h" 11 | 12 | #include "HoudiniMassCommon.h" 13 | 14 | 15 | bool FHoudiniZoneShapeComponentInput::HapiDestroy(UHoudiniInput* Input) const // Will then delete this, so we need NOT to reset node ids to -1 16 | { 17 | if (NodeId >= 0) 18 | { 19 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::DeleteNode(FHoudiniEngine::Get().GetSession(), NodeId)); 20 | Input->NotifyMergedNodeDestroyed(); 21 | } 22 | 23 | return true; 24 | } 25 | 26 | bool FHoudiniZoneShapeComponentInputBuilder::IsValidInput(const UActorComponent* Component) 27 | { 28 | return IsValid(Component) && Component->IsA(); 29 | } 30 | 31 | uint32 GetTypeHash(const FZoneLaneDesc& Lane) 32 | { 33 | return GetTypeHash(TArray((uint8*)&Lane, sizeof(FZoneLaneDesc))); 34 | } 35 | 36 | static const char* ConvertLaneToJsonStr(const FZoneLaneDesc& Lane, const UZoneGraphSettings* ZoneGraphSettings, TMap>& InOutLaneDictStrMap) 37 | { 38 | if (const TSharedPtr* FoundStrPtr = InOutLaneDictStrMap.Find(Lane)) 39 | return (*FoundStrPtr)->c_str(); 40 | 41 | FString TagsStr = TEXT("["); 42 | for (const FZoneGraphTagInfo& Tag : ZoneGraphSettings->GetTagInfos()) 43 | { 44 | if (Lane.Tags.Contains(Tag.Tag)) 45 | TagsStr += TEXT("\"") + Tag.Name.ToString() + TEXT("\","); 46 | } 47 | TagsStr.RemoveFromEnd(TEXT(",")); 48 | TagsStr += TEXT("]"); 49 | 50 | return InOutLaneDictStrMap.Add(Lane, MakeShared(TCHAR_TO_UTF8(*FString::Printf(TEXT("{\"Width\":%f,\"Direction\":%d,\"Tags\":%s}"), 51 | Lane.Width * POSITION_SCALE_TO_HOUDINI, int32(Lane.Direction), *TagsStr))))->c_str(); 52 | } 53 | 54 | static const char* ConvertNameToJsonStr(const FName& Name, TMap>& InOutNameStrMap) 55 | { 56 | if (const TSharedPtr* FoundStrPtr = InOutNameStrMap.Find(Name)) 57 | return (*FoundStrPtr)->c_str(); 58 | 59 | return InOutNameStrMap.Add(Name, MakeShared(Name.IsNone() ? "" : TCHAR_TO_UTF8(*Name.ToString())))->c_str(); 60 | } 61 | 62 | bool FHoudiniZoneShapeComponentInputBuilder::HapiUpload(UHoudiniInput* Input, const bool& bIsSingleComponent, // Is there only one single valid component in the whole blueprint/actor 63 | const TArray& Components, const TArray& Transforms, const TArray& ComponentIndices, // Components and Transforms are all of the components in blueprint/actor, and ComponentIndices are ref the valid indices from IsValidInput 64 | int32& InOutInstancerNodeId, TArray>& InOutComponentInputs, TArray& InOutPoints) 65 | { 66 | TSharedPtr ZSCInput; 67 | if (InOutComponentInputs.IsValidIndex(0)) 68 | ZSCInput = StaticCastSharedPtr(InOutComponentInputs[0]); 69 | else 70 | { 71 | ZSCInput = MakeShared(); 72 | InOutComponentInputs.Add(ZSCInput); 73 | } 74 | 75 | int32& NodeId = ZSCInput->NodeId; 76 | const bool bCreateNewNode = (NodeId < 0); 77 | if (bCreateNewNode) 78 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::CreateNode(FHoudiniEngine::Get().GetSession(), Input->GetGeoNodeId(), "null", 79 | TCHAR_TO_UTF8(*FString::Printf(TEXT("%s_zone_shape_%08X"), *(Components[ComponentIndices[0]]->GetOuter()->GetName()), FPlatformTime::Cycles())), 80 | false, &NodeId)); 81 | 82 | HAPI_PartInfo PartInfo; 83 | FHoudiniApi::PartInfo_Init(&PartInfo); 84 | PartInfo.type = HAPI_PARTTYPE_CURVE; 85 | PartInfo.faceCount = ComponentIndices.Num(); 86 | 87 | const UZoneGraphSettings* ZoneGraphSettings = GetDefault(); 88 | 89 | TArray VertexCounts; 90 | TArray ZoneShapeTypes; 91 | TArray Positions; 92 | TArray Rotations; 93 | 94 | // s@unreal_zone_lane_profile_name 95 | TMap> LaneProfileNameStrMap; 96 | TArray PointLaneProfileNames; 97 | TArray SplineLaneProfileNames; 98 | SplineLaneProfileNames.Reserve(PartInfo.faceCount); 99 | 100 | // d[]@unreal_zone_lane_profile 101 | TMap> LaneDictStrMap; 102 | TArray PointLanes; 103 | TArray PointLaneCounts; 104 | TArray SplineLanes; 105 | TArray SplineLaneCounts; 106 | SplineLaneCounts.Reserve(PartInfo.faceCount); 107 | 108 | bool bHasPolygon = false; 109 | bool bHasSpline = false; 110 | for (const int32& CompIdx : ComponentIndices) 111 | { 112 | const UZoneShapeComponent* ZSC = Cast(Components[CompIdx]); 113 | const FTransform& Transform = Transforms[CompIdx]; 114 | ZoneShapeTypes.Add((int32)ZSC->GetShapeType()); 115 | VertexCounts.Add(ZSC->GetPoints().Num()); 116 | 117 | const int32 NumPoints = ZSC->GetNumPoints(); 118 | if (ZSC->GetShapeType() == FZoneShapeType::Spline) 119 | { 120 | bHasSpline = true; 121 | 122 | // Spline LaneProfile 123 | FZoneLaneProfile LaneProfile; 124 | ZSC->GetSplineLaneProfile(LaneProfile); 125 | SplineLaneProfileNames.Add(ConvertNameToJsonStr(LaneProfile.Name, LaneProfileNameStrMap)); 126 | 127 | SplineLaneCounts.Add(LaneProfile.Lanes.Num()); 128 | for (const FZoneLaneDesc& Lane : LaneProfile.Lanes) 129 | SplineLanes.Add(ConvertLaneToJsonStr(Lane, ZoneGraphSettings, LaneDictStrMap)); 130 | 131 | // Point LaneProfiles 132 | for (int32 PointIdx = 0; PointIdx < NumPoints; ++PointIdx) 133 | { 134 | PointLaneProfileNames.Add(""); 135 | PointLaneCounts.Add(0); 136 | } 137 | } 138 | else 139 | { 140 | bHasPolygon = true; 141 | 142 | // Point LaneProfiles 143 | TArray LaneProfiles; 144 | ZSC->GetPolygonLaneProfiles(LaneProfiles); 145 | for (const FZoneLaneProfile& LaneProfile : LaneProfiles) 146 | { 147 | PointLaneProfileNames.Add(ConvertNameToJsonStr(LaneProfile.Name, LaneProfileNameStrMap)); 148 | 149 | PointLaneCounts.Add(LaneProfile.Lanes.Num()); 150 | for (const FZoneLaneDesc& Lane : LaneProfile.Lanes) 151 | PointLanes.Add(ConvertLaneToJsonStr(Lane, ZoneGraphSettings, LaneDictStrMap)); 152 | } 153 | 154 | // Spline LaneProfile 155 | SplineLaneProfileNames.Add(""); 156 | SplineLaneCounts.Add(0); 157 | } 158 | 159 | 160 | PartInfo.pointCount += NumPoints; 161 | for (const FZoneShapePoint& Point : ZSC->GetPoints()) 162 | { 163 | const FVector3f Pos = FVector3f(Transform.TransformPosition(Point.Position) * POSITION_SCALE_TO_HOUDINI); 164 | Positions.Add(Pos.X); 165 | Positions.Add(Pos.Z); 166 | Positions.Add(Pos.Y); 167 | 168 | const FQuat4f Rot = (FQuat4f)Transform.TransformRotation(Point.Rotation.Quaternion()); 169 | Rotations.Add(Rot.X); 170 | Rotations.Add(Rot.Z); 171 | Rotations.Add(Rot.Y); 172 | Rotations.Add(-Rot.W); 173 | } 174 | } 175 | PartInfo.vertexCount = PartInfo.pointCount; 176 | 177 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::SetPartInfo(FHoudiniEngine::Get().GetSession(), NodeId, 0, &PartInfo)); 178 | 179 | HAPI_AttributeInfo AttributeInfo; 180 | FHoudiniApi::AttributeInfo_Init(&AttributeInfo); 181 | AttributeInfo.exists = true; 182 | AttributeInfo.originalOwner = HAPI_ATTROWNER_INVALID; 183 | 184 | { 185 | // @P 186 | AttributeInfo.count = PartInfo.pointCount; 187 | AttributeInfo.tupleSize = 3; 188 | AttributeInfo.owner = HAPI_ATTROWNER_POINT; 189 | AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT; 190 | 191 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), NodeId, 0, 192 | HAPI_ATTRIB_POSITION, &AttributeInfo)); 193 | 194 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::SetAttributeFloatData(FHoudiniEngine::Get().GetSession(), NodeId, 0, 195 | HAPI_ATTRIB_POSITION, &AttributeInfo, Positions.GetData(), 0, AttributeInfo.count)); 196 | } 197 | 198 | { 199 | HAPI_CurveInfo CurveInfo; 200 | FHoudiniApi::CurveInfo_Init(&CurveInfo); 201 | CurveInfo.curveType = HAPI_CURVETYPE_LINEAR; 202 | CurveInfo.curveCount = PartInfo.faceCount; 203 | CurveInfo.vertexCount = PartInfo.vertexCount; 204 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::SetCurveInfo(FHoudiniEngine::Get().GetSession(), NodeId, 0, &CurveInfo)); 205 | 206 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::SetCurveCounts( 207 | FHoudiniEngine::Get().GetSession(), NodeId, 0, VertexCounts.GetData(), 0, PartInfo.faceCount)); 208 | } 209 | 210 | { 211 | // p@rot 212 | AttributeInfo.count = PartInfo.pointCount; 213 | AttributeInfo.tupleSize = 4; 214 | AttributeInfo.owner = HAPI_ATTROWNER_POINT; 215 | AttributeInfo.storage = HAPI_STORAGETYPE_FLOAT; 216 | 217 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), NodeId, 0, 218 | HAPI_ATTRIB_ROT, &AttributeInfo)); 219 | 220 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::SetAttributeFloatData(FHoudiniEngine::Get().GetSession(), NodeId, 0, 221 | HAPI_ATTRIB_ROT, &AttributeInfo, Rotations.GetData(), 0, AttributeInfo.count)); 222 | } 223 | 224 | { 225 | // i@unreal_zone_shape_type 226 | AttributeInfo.count = PartInfo.faceCount; 227 | AttributeInfo.tupleSize = 1; 228 | AttributeInfo.owner = HAPI_ATTROWNER_PRIM; 229 | AttributeInfo.storage = HAPI_STORAGETYPE_INT; 230 | 231 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), NodeId, 0, 232 | HAPI_ATTRIB_UNREAL_ZONE_SHAPE_TYPE, &AttributeInfo)); 233 | 234 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::SetAttributeIntData(FHoudiniEngine::Get().GetSession(), NodeId, 0, 235 | HAPI_ATTRIB_UNREAL_ZONE_SHAPE_TYPE, &AttributeInfo, ZoneShapeTypes.GetData(), 0, AttributeInfo.count)); 236 | } 237 | 238 | static const char* SpareStr = ""; 239 | 240 | auto HapiSetLaneProfileLambda = [&](const int32& ElemCount, const HAPI_AttributeOwner& Owner, 241 | TArray& LaneProfileNames, TArray& Lanes, const TArray& LaneCounts) -> bool 242 | { 243 | AttributeInfo.count = ElemCount; 244 | AttributeInfo.tupleSize = 1; 245 | AttributeInfo.owner = Owner; 246 | 247 | // s@unreal_zone_lane_profile_name 248 | AttributeInfo.storage = HAPI_STORAGETYPE_STRING; 249 | 250 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), NodeId, 0, 251 | HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE_NAME, &AttributeInfo)); 252 | 253 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::SetAttributeStringData(FHoudiniEngine::Get().GetSession(), NodeId, 0, 254 | HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE_NAME, &AttributeInfo, LaneProfileNames.GetData(), 0, AttributeInfo.count)); 255 | 256 | // s@unreal_zone_lane_profile 257 | AttributeInfo.storage = HAPI_STORAGETYPE_DICTIONARY_ARRAY; 258 | 259 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::AddAttribute(FHoudiniEngine::Get().GetSession(), NodeId, 0, 260 | HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE, &AttributeInfo)); 261 | 262 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::SetAttributeDictionaryArrayData(FHoudiniEngine::Get().GetSession(), NodeId, 0, 263 | HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE, &AttributeInfo, 264 | (Lanes.IsEmpty() ? &SpareStr : Lanes.GetData()), Lanes.Num(), LaneCounts.GetData(), 0, AttributeInfo.count)); 265 | 266 | return true; 267 | }; 268 | 269 | if (bHasPolygon) 270 | HOUDINI_FAIL_RETURN(HapiSetLaneProfileLambda(PartInfo.pointCount, HAPI_ATTROWNER_POINT, PointLaneProfileNames, PointLanes, PointLaneCounts)); 271 | 272 | if (bHasSpline) 273 | HOUDINI_FAIL_RETURN(HapiSetLaneProfileLambda(PartInfo.faceCount, HAPI_ATTROWNER_PRIM, SplineLaneProfileNames, SplineLanes, SplineLaneCounts)); 274 | 275 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::CommitGeo(FHoudiniEngine::Get().GetSession(), NodeId)); 276 | 277 | if (bCreateNewNode) 278 | HOUDINI_FAIL_RETURN(Input->HapiConnectToMergeNode(NodeId)); 279 | 280 | return true; 281 | } 282 | 283 | void FHoudiniZoneShapeComponentInputBuilder::AppendInfo(const TArray& Components, const TArray& Transforms, const TArray& ComponentIndices, // See the comment upon 284 | const TSharedPtr& JsonObject) // Append object info to JsonObject, keys are instance refs, values are JsonObjects that contain transoforms and meta data 285 | { 286 | // TODO: 287 | } 288 | -------------------------------------------------------------------------------- /Source/HoudiniMassTranslator/Private/HoudiniMassCommands.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) <2025> Yuzhe Pan (childadrianpan@gmail.com). All Rights Reserved. 2 | 3 | #include "HoudiniMassCommands.h" 4 | 5 | #include "ZoneGraphSettings.h" 6 | #include "ZoneShapeComponent.h" 7 | 8 | #include "HoudiniMassCommon.h" 9 | 10 | 11 | #define LOCTEXT_NAMESPACE "HoudiniMassTranslator" 12 | 13 | FHoudiniMassCommands::FHoudiniMassCommands() 14 | : TCommands(TEXT("HoudiniMassTranslator"), NSLOCTEXT("Contexts", "HoudiniMassTranslator", "Houdini Mass Translator Plugin"), NAME_None, FAppStyle::GetAppStyleSetName()) 15 | { 16 | } 17 | 18 | void FHoudiniMassCommands::RegisterCommands() 19 | { 20 | UI_COMMAND(CleanupLaneProfiles, "Clean Up Houdini Lane Profiles", "Clean Up Useless Houdini Engine Generated Lane Profiles (Starts With \"LP_HE_\")", EUserInterfaceActionType::Button, FInputChord()); 21 | UI_COMMAND(RemoveAllLaneProfiles, "Remove All Houdini Lane Profiles", "Remove All Houdini Engine Generated Lane Profiles (Starts With \"LP_HE_\")", EUserInterfaceActionType::Button, FInputChord()); 22 | } 23 | 24 | void FHoudiniMassCommands::OnCleanupLaneProfiles() 25 | { 26 | UZoneGraphSettings* ZoneGraphSettings = GetMutableDefault(); 27 | TSet UsedLaneProfileIDs; 28 | for (FThreadSafeObjectIterator Iter(UZoneShapeComponent::StaticClass()); Iter; ++Iter) 29 | { 30 | UZoneShapeComponent* ZSC = Cast(*Iter); 31 | if (!IsValid(ZSC)) 32 | continue; 33 | 34 | if (ZSC->GetShapeType() == FZoneShapeType::Spline) 35 | { 36 | FZoneLaneProfile LaneProfile; 37 | ZSC->GetSplineLaneProfile(LaneProfile); 38 | UsedLaneProfileIDs.Add(LaneProfile.ID); 39 | } 40 | else 41 | { 42 | for (const FZoneShapePoint& Point : ZSC->GetMutablePoints()) 43 | { 44 | if (Point.LaneProfile == FZoneShapePoint::InheritLaneProfile) 45 | { 46 | FZoneLaneProfile LaneProfile; 47 | ZSC->GetSplineLaneProfile(LaneProfile); 48 | UsedLaneProfileIDs.Add(LaneProfile.ID); 49 | } 50 | else if (ZSC->GetPerPointLaneProfiles().IsValidIndex(Point.LaneProfile)) 51 | UsedLaneProfileIDs.Add(ZSC->GetPerPointLaneProfiles()[Point.LaneProfile].ID); 52 | } 53 | } 54 | } 55 | 56 | ((TArray*)&ZoneGraphSettings->GetLaneProfiles())->RemoveAll([UsedLaneProfileIDs](const FZoneLaneProfile& LaneProfile) 57 | { 58 | FString Name = LaneProfile.Name.ToString(); 59 | if (Name.StartsWith(HOUDINI_LANE_PROFILE_PREFIX) && Name.Len() >= 7) 60 | return !UsedLaneProfileIDs.Contains(LaneProfile.ID); 61 | 62 | return false; 63 | }); 64 | ZoneGraphSettings->TryUpdateDefaultConfigFile(); 65 | } 66 | 67 | void FHoudiniMassCommands::OnRemoveAllLaneProfiles() 68 | { 69 | UZoneGraphSettings* ZoneGraphSettings = GetMutableDefault(); 70 | ((TArray*) & ZoneGraphSettings->GetLaneProfiles())->RemoveAll([](const FZoneLaneProfile& LaneProfile) 71 | { 72 | return LaneProfile.Name.ToString().StartsWith(HOUDINI_LANE_PROFILE_PREFIX); 73 | }); 74 | ZoneGraphSettings->TryUpdateDefaultConfigFile(); 75 | } 76 | 77 | #undef LOCTEXT_NAMESPACE 78 | -------------------------------------------------------------------------------- /Source/HoudiniMassTranslator/Private/HoudiniMassTranslator.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) <2025> Yuzhe Pan (childadrianpan@gmail.com). All Rights Reserved. 2 | 3 | #include "HoudiniMassTranslator.h" 4 | 5 | #include "Editor.h" 6 | #include "ToolMenus.h" 7 | #include "Widgets/Notifications/SNotificationList.h" 8 | #include "Framework/Notifications/NotificationManager.h" 9 | #include "Settings/ProjectPackagingSettings.h" 10 | #include "ZoneGraphDelegates.h" 11 | 12 | #include "HoudiniEngine.h" 13 | #include "HoudiniInputZoneShape.h" 14 | #include "HoudiniOutputZoneShape.h" 15 | #include "HoudiniMassCommands.h" 16 | 17 | 18 | #define LOCTEXT_NAMESPACE "FHoudiniMassTranslatorModule" 19 | 20 | FHoudiniMassTranslator* FHoudiniMassTranslator::HoudiniMassTranslatorInstance = nullptr; 21 | 22 | void FHoudiniMassTranslator::StartupModule() 23 | { 24 | HoudiniMassTranslatorInstance = this; 25 | 26 | FHoudiniEngine& HoudiniEngine = FHoudiniEngine::IsLoaded() ? FHoudiniEngine::Get() : 27 | FModuleManager::LoadModuleChecked("HoudiniEngine"); 28 | 29 | ComponentInputBuilder = MakeShared(); 30 | HoudiniEngine.RegisterInputBuilder(ComponentInputBuilder); 31 | 32 | OutputBuilder = MakeShared(); 33 | HoudiniEngine.RegisterOutputBuilder(OutputBuilder); 34 | 35 | FHoudiniMassCommands::Register(); 36 | 37 | Commands = MakeShareable(new FUICommandList); 38 | 39 | Commands->MapAction(FHoudiniMassCommands::Get().CleanupLaneProfiles, 40 | FExecuteAction::CreateStatic(&FHoudiniMassCommands::OnCleanupLaneProfiles), 41 | FCanExecuteAction()); 42 | 43 | Commands->MapAction(FHoudiniMassCommands::Get().RemoveAllLaneProfiles, 44 | FExecuteAction::CreateStatic(&FHoudiniMassCommands::OnRemoveAllLaneProfiles), 45 | FCanExecuteAction()); 46 | 47 | // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner 48 | FToolMenuOwnerScoped OwnerScoped(this); 49 | 50 | if (UToolMenu* BuildMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Build")) 51 | { 52 | FToolMenuSection& Section = BuildMenu->FindOrAddSection("LevelEditorNavigation"); 53 | Section.AddMenuEntryWithCommandList(FHoudiniMassCommands::Get().CleanupLaneProfiles, Commands); 54 | Section.AddMenuEntryWithCommandList(FHoudiniMassCommands::Get().RemoveAllLaneProfiles, Commands); 55 | } 56 | 57 | UE::ZoneGraphDelegates::OnZoneGraphDataBuildDone.AddRaw(this, &FHoudiniMassTranslator::OnZoneGraphBuildDone); 58 | FEditorDelegates::BeginPIE.AddRaw(this, &FHoudiniMassTranslator::OnZoneGraphBuildCancel); 59 | 60 | // We need to ignore this plugin's content while unreal cooking 61 | UProjectPackagingSettings* PackagingSettings = GetMutableDefault(); 62 | if (!PackagingSettings->DirectoriesToNeverCook.ContainsByPredicate( 63 | [](const FDirectoryPath& Directory) { return Directory.Path == TEXT("/HoudiniMassTranslator/Example"); })) 64 | { 65 | PackagingSettings->DirectoriesToNeverCook.Add({ TEXT("/HoudiniMassTranslator/Example") }); 66 | PackagingSettings->TryUpdateDefaultConfigFile(); 67 | } 68 | } 69 | 70 | void FHoudiniMassTranslator::OnZoneShapeOutputFinish() 71 | { 72 | if (!Notification.IsValid()) 73 | { 74 | FNotificationInfo Info(LOCTEXT("HoudiniZoneShapeOutputFinish", "Houdini Zone Shape Output Finished\nPlease Build Zone Graph")); 75 | Info.bFireAndForget = false; 76 | Info.FadeInDuration = 0.05f; 77 | Info.ExpireDuration = 0.2f; 78 | Info.FadeOutDuration = 0.3f; 79 | Info.ButtonDetails.Add(FNotificationButtonInfo(LOCTEXT("HoudiniZoneGraphBuild", "Build Zone Graph"), FText::GetEmpty(), 80 | FSimpleDelegate::CreateLambda([]() { UE::ZoneGraphDelegates::OnZoneGraphRequestRebuild.Broadcast(); }), SNotificationItem::CS_None)); 81 | Info.ButtonDetails.Add(FNotificationButtonInfo(LOCTEXT("HoudiniZoneGraphBuildCancel", "Cancel"), FText::GetEmpty(), 82 | FSimpleDelegate::CreateLambda([]() { FHoudiniMassTranslator::Get().OnZoneGraphBuildCancel(false); }), SNotificationItem::CS_None)); 83 | Info.bUseSuccessFailIcons = true; 84 | Notification = FSlateNotificationManager::Get().AddNotification(Info); 85 | //Notification.Pin()->SetCompletionState(SNotificationItem::CS_Pending); 86 | } 87 | } 88 | 89 | void FHoudiniMassTranslator::OnZoneGraphBuildDone(const FZoneGraphBuildData&) 90 | { 91 | if (Notification.IsValid()) 92 | { 93 | Notification.Pin()->SetCompletionState(SNotificationItem::CS_Success); 94 | Notification.Pin()->ExpireAndFadeout(); 95 | Notification.Reset(); 96 | } 97 | } 98 | 99 | void FHoudiniMassTranslator::OnZoneGraphBuildCancel(const bool) 100 | { 101 | if (Notification.IsValid()) 102 | { 103 | Notification.Pin()->SetText(LOCTEXT("HoudiniZoneGraphBuildCanceled", "Houdini Zone Graph Build Canceled")); 104 | Notification.Pin()->SetCompletionState(SNotificationItem::CS_Fail); 105 | Notification.Pin()->SetExpireDuration(0.5f); 106 | Notification.Pin()->SetFadeOutDuration(1.0f); 107 | Notification.Pin()->ExpireAndFadeout(); 108 | Notification.Reset(); 109 | } 110 | } 111 | 112 | void FHoudiniMassTranslator::ShutdownModule() 113 | { 114 | if (FHoudiniEngine::IsLoaded()) 115 | { 116 | FHoudiniEngine::Get().UnregisterInputBuilder(ComponentInputBuilder); 117 | FHoudiniEngine::Get().UnregisterOutputBuilder(OutputBuilder); 118 | } 119 | 120 | UE::ZoneGraphDelegates::OnZoneGraphDataBuildDone.RemoveAll(this); 121 | FEditorDelegates::BeginPIE.RemoveAll(this); 122 | 123 | HoudiniMassTranslatorInstance = nullptr; 124 | } 125 | 126 | #undef LOCTEXT_NAMESPACE 127 | 128 | IMPLEMENT_MODULE(FHoudiniMassTranslator, HoudiniMassTranslator) -------------------------------------------------------------------------------- /Source/HoudiniMassTranslator/Private/HoudiniOutputZoneShape.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) <2025> Yuzhe Pan (childadrianpan@gmail.com). All Rights Reserved. 2 | 3 | #include "HoudiniOutputZoneShape.h" 4 | 5 | #include "ZoneGraphSettings.h" 6 | #include "ZoneShapeComponent.h" 7 | 8 | #include "HoudiniApi.h" 9 | #include "HoudiniEngine.h" 10 | #include "HoudiniAttribute.h" 11 | #include "HoudiniEngineUtils.h" 12 | #include "HoudiniOutputUtils.h" 13 | 14 | #include "HoudiniMassTranslator.h" 15 | #include "HoudiniMassCommon.h" 16 | 17 | 18 | bool FHoudiniZoneShapeOutputBuilder::HapiIsPartValid(const int32& NodeId, const HAPI_PartInfo& PartInfo, bool& bOutIsValid, bool& bOutShouldHoldByOutput) 19 | { 20 | bOutShouldHoldByOutput = true; 21 | bOutIsValid = false; 22 | 23 | if (PartInfo.type == HAPI_PARTTYPE_CURVE) // ZoneShape is curve-liked, so we should retrieve them from houdini curves 24 | { 25 | const int32& PartId = PartInfo.id; 26 | 27 | HAPI_AttributeInfo AttribInfo; 28 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), NodeId, PartId, 29 | HAPI_ATTRIB_UNREAL_OUTPUT_ZONE_SHAPE, HAPI_ATTROWNER_DETAIL, &AttribInfo)); 30 | 31 | if (AttribInfo.exists && !FHoudiniEngineUtils::IsArray(AttribInfo.storage) && 32 | FHoudiniEngineUtils::ConvertStorageType(AttribInfo.storage) == EHoudiniStorageType::Int) // Currently only support i@unreal_output_zone_shape = 1 on detail 33 | { 34 | int bIsZoneShape = 0; 35 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeIntData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, 36 | HAPI_ATTRIB_UNREAL_OUTPUT_ZONE_SHAPE, &AttribInfo, 1, &bIsZoneShape, 0, 1)); 37 | 38 | bOutIsValid = bool(bIsZoneShape); 39 | return true; 40 | } 41 | } 42 | 43 | return true; 44 | } 45 | 46 | 47 | UZoneShapeComponent* FHoudiniZoneShapeOutput::Find(const AHoudiniNode* Node) const 48 | { 49 | return Find_Internal(Component, Node); 50 | } 51 | 52 | UZoneShapeComponent* FHoudiniZoneShapeOutput::CreateOrUpdate(AHoudiniNode* Node, const FString& InSplitValue, const bool& bInSplitActor) 53 | { 54 | return CreateOrUpdate_Internal(Component, Node, InSplitValue, bInSplitActor); 55 | } 56 | 57 | void FHoudiniZoneShapeOutput::Destroy(const AHoudiniNode* Node) const 58 | { 59 | DestroyComponent(Node, Component, false); 60 | Component.Reset(); 61 | } 62 | 63 | namespace HoudiniZoneShapeOutputUtils 64 | { 65 | static FZoneGraphTagMask FindOrCreateZoneGraphTag(UZoneGraphSettings* ZoneGraphSettings, const FName& TagName, bool& bZoneGraphSettingsModified); 66 | 67 | static bool HapiGetOrCreateTags(const int32& NodeId, const int32& PartId, UZoneGraphSettings* ZoneGraphSettings, 68 | HAPI_AttributeOwner& InOutOwner, TArray& OutTags, bool& bZoneGraphSettingsModified); 69 | 70 | FORCEINLINE static uint32 GetLaneProfileHash(const TArray& Lanes) 71 | { 72 | return GetTypeHash(TArray((uint8*)Lanes.GetData(), Lanes.GetTypeSize() * Lanes.Num())); 73 | } 74 | 75 | static FString GetLaneProfileString(const TArray& Lanes); 76 | 77 | static bool HapiGetOrCreateLaneProfiles(const int32& NodeId, const int32& PartId, UZoneGraphSettings* ZoneGraphSettings, 78 | const HAPI_AttributeOwner& NameOwner, const HAPI_AttributeOwner& LanesOwner, TMap& InOutHashProfileIdxMap, TArray& OutLaneProfileIndices, bool& bZoneGraphSettingsModified); 79 | 80 | static bool HapiGetOrCreateLaneProfiles(const int32& NodeId, const int32& PartId, const TArray& AttribNames, const int AttribCounts[HAPI_ATTROWNER_MAX], 81 | UZoneGraphSettings* ZoneGraphSettings, const bool bIsOnPoints, 82 | HAPI_AttributeOwner& OutLaneProfileOwner, TMap& InOutHashProfileIdxMap, TArray& OutLaneProfileIndices, bool& bZoneGraphSettingsModified); 83 | } 84 | 85 | FZoneGraphTagMask HoudiniZoneShapeOutputUtils::FindOrCreateZoneGraphTag(UZoneGraphSettings* ZoneGraphSettings, const FName& TagName, bool& bZoneGraphSettingsModified) 86 | { 87 | TConstArrayView ConstTagInfos = ZoneGraphSettings->GetTagInfos(); 88 | TArrayView TagInfos = *((TArrayView*)&ConstTagInfos); 89 | 90 | // First, try find the tag by name 91 | const int32 FoundTagIdx = TagInfos.IndexOfByPredicate([&TagName](const FZoneGraphTagInfo& TagInfo) { return TagInfo.Name == TagName; }); 92 | if (TagInfos.IsValidIndex(FoundTagIdx)) 93 | return TagInfos[FoundTagIdx].Tag; 94 | 95 | // Second, try find tag that is invalid (name is none) and set name, as a created tag 96 | for (FZoneGraphTagInfo& TagInfo : TagInfos) 97 | { 98 | if (!TagInfo.IsValid()) 99 | { 100 | TagInfo.Name = TagName; 101 | bZoneGraphSettingsModified = true; 102 | return TagInfo.Tag; 103 | } 104 | } 105 | 106 | UE_LOG(LogHoudiniEngine, Error, TEXT("Cannot create zone graph tag: %s"), *TagName.ToString()); 107 | return FZoneGraphTagMask(1); 108 | } 109 | 110 | bool HoudiniZoneShapeOutputUtils::HapiGetOrCreateTags(const int32& NodeId, const int32& PartId, UZoneGraphSettings* ZoneGraphSettings, 111 | HAPI_AttributeOwner& InOutOwner, TArray& OutTags, bool& bZoneGraphSettingsModified) 112 | { 113 | if (InOutOwner == HAPI_ATTROWNER_INVALID) 114 | return true; 115 | 116 | HAPI_AttributeInfo AttribInfo; 117 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), 118 | NodeId, PartId, HAPI_ATTRIB_UNREAL_ZONE_SHAPE_TAGS, InOutOwner, &AttribInfo)); 119 | 120 | TArray Counts; 121 | TArray SHs; 122 | if (AttribInfo.storage == HAPI_STORAGETYPE_STRING) 123 | { 124 | SHs.SetNumUninitialized(AttribInfo.count); 125 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeStringData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, 126 | HAPI_ATTRIB_UNREAL_ZONE_SHAPE_TAGS, &AttribInfo, SHs.GetData(), 0, AttribInfo.count)); 127 | } 128 | else if (AttribInfo.storage == HAPI_STORAGETYPE_STRING_ARRAY) 129 | { 130 | Counts.SetNumUninitialized(AttribInfo.count); 131 | SHs.SetNumUninitialized(AttribInfo.totalArrayElements); 132 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeStringArrayData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, 133 | HAPI_ATTRIB_UNREAL_ZONE_SHAPE_TAGS, &AttribInfo, SHs.GetData(), AttribInfo.totalArrayElements, Counts.GetData(), 0, AttribInfo.count)); 134 | } 135 | else 136 | return true; 137 | 138 | TMap SHTagMap; 139 | { 140 | TArray TagSHs = TSet(SHs).Array(); 141 | TArray TagNames; 142 | HOUDINI_FAIL_RETURN(FHoudiniEngineUtils::HapiConvertStringHandles(TagSHs, TagNames)); 143 | for (int32 TagIdx = 0; TagIdx < TagSHs.Num(); ++TagIdx) 144 | SHTagMap.Add(TagSHs[TagIdx], TagNames[TagIdx].empty() ? FZoneGraphTagMask() : FindOrCreateZoneGraphTag(ZoneGraphSettings, TagNames[TagIdx].c_str(), bZoneGraphSettingsModified)); 145 | } 146 | 147 | if (AttribInfo.storage == HAPI_STORAGETYPE_STRING) 148 | { 149 | OutTags.SetNumUninitialized(AttribInfo.count); 150 | for (int32 ElemIdx = 0; ElemIdx < AttribInfo.count; ++ElemIdx) 151 | OutTags[ElemIdx] = SHTagMap[SHs[ElemIdx]]; 152 | } 153 | else if (AttribInfo.storage == HAPI_STORAGETYPE_STRING_ARRAY) 154 | { 155 | OutTags.SetNum(AttribInfo.count); 156 | int32 ArrayElemIdx = 0; 157 | for (int32 ElemIdx = 0; ElemIdx < AttribInfo.count; ++ElemIdx) 158 | { 159 | for (int32 ArrayIdx = 0; ArrayIdx < Counts[ElemIdx]; ++ArrayIdx) 160 | { 161 | OutTags[ElemIdx].Add(SHTagMap[SHs[ArrayElemIdx]]); 162 | ++ArrayElemIdx; 163 | } 164 | } 165 | } 166 | 167 | return true; 168 | } 169 | 170 | FString HoudiniZoneShapeOutputUtils::GetLaneProfileString(const TArray& Lanes) 171 | { 172 | FString ProfileStr; 173 | for (int32 LaneIdx = Lanes.Num() - 1; LaneIdx >= 0; --LaneIdx) 174 | { 175 | switch (Lanes[LaneIdx].Direction) 176 | { 177 | case EZoneLaneDirection::Forward: ProfileStr += TEXT("\u2191"); break; 178 | case EZoneLaneDirection::Backward: ProfileStr += TEXT("\u2193"); break; 179 | case EZoneLaneDirection::None: ProfileStr += TEXT("X"); break; 180 | } 181 | } 182 | 183 | return ProfileStr; 184 | } 185 | 186 | bool HoudiniZoneShapeOutputUtils::HapiGetOrCreateLaneProfiles(const int32& NodeId, const int32& PartId, UZoneGraphSettings* ZoneGraphSettings, 187 | const HAPI_AttributeOwner& NameOwner, const HAPI_AttributeOwner& LanesOwner, TMap& InOutHashProfileIdxMap, TArray& OutLaneProfileIndices, bool& bZoneGraphSettingsModified) 188 | { 189 | HAPI_AttributeInfo AttribInfo; 190 | 191 | TArray LaneProfileNames; 192 | if (NameOwner != HAPI_ATTROWNER_INVALID) 193 | { 194 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), 195 | NodeId, PartId, HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE_NAME, NameOwner, &AttribInfo)); 196 | 197 | if (AttribInfo.storage == HAPI_STORAGETYPE_STRING) // If is string, then means this is the name of a lane profile name 198 | { 199 | TArray SHs; 200 | SHs.SetNumUninitialized(AttribInfo.count); 201 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeStringData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, 202 | HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE, &AttribInfo, SHs.GetData(), 0, AttribInfo.count)); 203 | 204 | TMap SHNameMap; 205 | { 206 | TArray UniqueSHs = TSet(SHs).Array(); 207 | TArray UniqueNames; 208 | HOUDINI_FAIL_RETURN(FHoudiniEngineUtils::HapiConvertUniqueStringHandles(UniqueSHs, UniqueNames)); 209 | for (int32 UniqueIdx = 0; UniqueIdx < UniqueSHs.Num(); ++UniqueIdx) 210 | SHNameMap.Add(UniqueSHs[UniqueIdx], *UniqueNames[UniqueIdx]); 211 | } 212 | LaneProfileNames.SetNum(AttribInfo.count); 213 | for (int32 ElemIdx = 0; ElemIdx < AttribInfo.count; ++ElemIdx) 214 | LaneProfileNames[ElemIdx] = SHNameMap[SHs[ElemIdx]]; 215 | } 216 | } 217 | 218 | if (LanesOwner != HAPI_ATTROWNER_INVALID) 219 | { 220 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), 221 | NodeId, PartId, HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE, LanesOwner, &AttribInfo)); 222 | 223 | auto ConvertJsonToLaneLambda = [&](const TSharedPtr& JsonLane, FZoneLaneDesc& Lane) -> void 224 | { 225 | float LaneWidth = 0.0; 226 | if (JsonLane->TryGetNumberField(TEXT("Width"), LaneWidth)) 227 | Lane.Width = LaneWidth * POSITION_SCALE_TO_UNREAL_F; 228 | 229 | int32 LaneDirInt = 0; 230 | if (JsonLane->TryGetNumberField(TEXT("Direction"), LaneDirInt)) 231 | Lane.Direction = EZoneLaneDirection(LaneDirInt); 232 | else 233 | { 234 | FString LaneDirStr; 235 | if (JsonLane->TryGetStringField(TEXT("Direction"), LaneDirStr)) 236 | { 237 | if (LaneDirStr == TEXT("None")) 238 | Lane.Direction = EZoneLaneDirection::None; 239 | else if (LaneDirStr == TEXT("Backward")) 240 | Lane.Direction = EZoneLaneDirection::Backward; 241 | } 242 | } 243 | 244 | const TArray>* JsonTagNames; 245 | if (JsonLane->TryGetArrayField(TEXT("Tags"), JsonTagNames)) 246 | { 247 | Lane.Tags = FZoneGraphTagMask(0); 248 | for (const TSharedPtr& JsonTagName : *JsonTagNames) 249 | { 250 | FString TagName; 251 | if (JsonTagName->TryGetString(TagName)) 252 | Lane.Tags.Add(FindOrCreateZoneGraphTag(ZoneGraphSettings, *TagName, bZoneGraphSettingsModified)); 253 | } 254 | if (Lane.Tags == FZoneGraphTagMask(0)) 255 | Lane.Tags = FZoneGraphTagMask(1); 256 | } 257 | else 258 | { 259 | FString TagName; 260 | if (JsonLane->TryGetStringField(TEXT("Tag"), TagName)) 261 | Lane.Tags = FindOrCreateZoneGraphTag(ZoneGraphSettings, *TagName, bZoneGraphSettingsModified); 262 | } 263 | }; 264 | 265 | auto FindOrAddLaneProfileLambda = [&](const int32& ElemIdx, const FName& LaneProfileName, const uint32& HashValue, const TArray& Lanes) 266 | { 267 | if (const int32* FoundProfileIdxPtr = InOutHashProfileIdxMap.Find(HashValue)) 268 | OutLaneProfileIndices[ElemIdx] = *FoundProfileIdxPtr; 269 | else // Create a new lane profile 270 | { 271 | FZoneLaneProfile NewLaneProfile; 272 | NewLaneProfile.Name = LaneProfileName.IsNone() ? 273 | FName(HOUDINI_LANE_PROFILE_PREFIX + GetLaneProfileString(Lanes), FMath::Abs(int32(HashValue))) : LaneProfileName; 274 | NewLaneProfile.Lanes = Lanes; 275 | 276 | const int32 NewProfileIdx = ((TArray*) & ZoneGraphSettings->GetLaneProfiles())->Add(NewLaneProfile); 277 | bZoneGraphSettingsModified = true; 278 | 279 | InOutHashProfileIdxMap.Add(HashValue, NewProfileIdx); 280 | OutLaneProfileIndices[ElemIdx] = NewProfileIdx; 281 | } 282 | }; 283 | 284 | if (AttribInfo.storage == HAPI_STORAGETYPE_DICTIONARY_ARRAY) // Means we should find or create a lane profile 285 | { 286 | TArray Counts; 287 | Counts.SetNumUninitialized(AttribInfo.count); 288 | TArray SHs; 289 | SHs.SetNumUninitialized(AttribInfo.totalArrayElements); 290 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeDictionaryArrayData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, 291 | HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE, &AttribInfo, SHs.GetData(), AttribInfo.totalArrayElements, Counts.GetData(), 0, AttribInfo.count)); 292 | 293 | // HAPI BUG: GetAttributeDictionaryArrayData will get all sh unique, we could only find unique strs in unreal 294 | TArray LaneDictStrs; 295 | HOUDINI_FAIL_RETURN(FHoudiniEngineUtils::HapiConvertUniqueStringHandles(SHs, LaneDictStrs)); 296 | TMap SHLaneMap; 297 | for (int32 ArrayIdx = 0; ArrayIdx < SHs.Num(); ++ArrayIdx) 298 | { 299 | if (SHLaneMap.Contains(LaneDictStrs[ArrayIdx])) 300 | continue; 301 | 302 | FZoneLaneDesc Lane = FZoneLaneDesc(); 303 | TSharedRef> JsonReader = TJsonReaderFactory::Create(LaneDictStrs[ArrayIdx]); 304 | TSharedPtr JsonLane; 305 | if (FJsonSerializer::Deserialize(JsonReader, JsonLane)) 306 | ConvertJsonToLaneLambda(JsonLane, Lane); 307 | 308 | SHLaneMap.Add(LaneDictStrs[ArrayIdx], Lane); 309 | } 310 | 311 | OutLaneProfileIndices.SetNumUninitialized(AttribInfo.count); 312 | int32 AccumulatedCount = 0; 313 | for (int32 ElemIdx = 0; ElemIdx < AttribInfo.count; ++ElemIdx) 314 | { 315 | const FName LaneProfileName = LaneProfileNames.IsEmpty() ? NAME_None : LaneProfileNames[NameOwner == HAPI_ATTROWNER_DETAIL ? 0 : ElemIdx]; 316 | const int32& Count = Counts[ElemIdx]; 317 | if (Count <= 0) // Fallback to try to find lane profile by name 318 | { 319 | OutLaneProfileIndices[ElemIdx] = LaneProfileNames.IsEmpty() ? -1 : ZoneGraphSettings->GetLaneProfiles().IndexOfByPredicate( 320 | [LaneProfileName](const FZoneLaneProfile& LaneProfile) { return LaneProfile.Name == LaneProfileName; }); 321 | continue; 322 | } 323 | 324 | TArray Lanes; 325 | for (int32 ArrayIdx = AccumulatedCount; ArrayIdx < AccumulatedCount + Count; ++ArrayIdx) 326 | Lanes.Add(SHLaneMap[LaneDictStrs[ArrayIdx]]); 327 | AccumulatedCount += Count; 328 | 329 | FindOrAddLaneProfileLambda(ElemIdx, LaneProfileName, GetLaneProfileHash(Lanes), Lanes); 330 | } 331 | } 332 | else if (AttribInfo.storage == HAPI_STORAGETYPE_STRING) // Warning: Temporarily, will remove this method if HAPI fix the bug 333 | { 334 | TArray SHs; 335 | SHs.SetNumUninitialized(AttribInfo.count); 336 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeStringData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, 337 | HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE, &AttribInfo, SHs.GetData(), 0, AttribInfo.count)); 338 | 339 | TMap>> SHLanesMap; 340 | { 341 | TArray UniqueSHs = TSet(SHs).Array(); 342 | TArray UniqueStrs; 343 | HOUDINI_FAIL_RETURN(FHoudiniEngineUtils::HapiConvertUniqueStringHandles(UniqueSHs, UniqueStrs)); 344 | for (int32 UniqueIdx = 0; UniqueIdx < UniqueSHs.Num(); ++UniqueIdx) 345 | { 346 | if (UniqueStrs[UniqueIdx].IsEmpty()) 347 | continue; 348 | 349 | TSharedRef> JsonReader = TJsonReaderFactory::Create(UniqueStrs[UniqueIdx]); 350 | TSharedPtr JsonLanes; 351 | if (FJsonSerializer::Deserialize(JsonReader, JsonLanes)) 352 | { 353 | const TArray>* JsonLanesPtr = nullptr; 354 | if (JsonLanes->TryGetArrayField(TEXT("Lanes"), JsonLanesPtr)) 355 | { 356 | TArray Lanes; 357 | for (const TSharedPtr& JsonLane : *JsonLanesPtr) 358 | { 359 | const TSharedPtr* JsonLanePtr = nullptr; 360 | FZoneLaneDesc Lane = FZoneLaneDesc(); 361 | if (JsonLane->TryGetObject(JsonLanePtr)) 362 | ConvertJsonToLaneLambda(*JsonLanePtr, Lane); 363 | Lanes.Add(Lane); 364 | } 365 | SHLanesMap.Add(UniqueSHs[UniqueIdx], TPair>(GetLaneProfileHash(Lanes), Lanes)); 366 | } 367 | } 368 | } 369 | } 370 | OutLaneProfileIndices.SetNumUninitialized(AttribInfo.count); 371 | for (int32 ElemIdx = 0; ElemIdx < AttribInfo.count; ++ElemIdx) 372 | { 373 | const FName LaneProfileName = LaneProfileNames.IsEmpty() ? NAME_None : LaneProfileNames[NameOwner == HAPI_ATTROWNER_DETAIL ? 0 : ElemIdx]; 374 | const TPair>* HashLanesPtr = SHLanesMap.Find(SHs[ElemIdx]); 375 | if (!HashLanesPtr) // Fallback to try to find lane profile by name 376 | { 377 | OutLaneProfileIndices[ElemIdx] = LaneProfileNames.IsEmpty() ? -1 : ZoneGraphSettings->GetLaneProfiles().IndexOfByPredicate( 378 | [LaneProfileName](const FZoneLaneProfile& LaneProfile) { return LaneProfile.Name == LaneProfileName; }); 379 | continue; 380 | } 381 | 382 | FindOrAddLaneProfileLambda(ElemIdx, LaneProfileName, HashLanesPtr->Key, HashLanesPtr->Value); 383 | } 384 | } 385 | } 386 | 387 | if (OutLaneProfileIndices.IsEmpty() && !LaneProfileNames.IsEmpty()) // Fallback to try to find lane profile by name 388 | { 389 | OutLaneProfileIndices.SetNumUninitialized(LaneProfileNames.Num()); 390 | TMap NameIdxMap; 391 | for (int32 ElemIdx = 0; ElemIdx < LaneProfileNames.Num(); ++ElemIdx) 392 | { 393 | const FName LaneProfileName = LaneProfileNames[ElemIdx]; 394 | if (LaneProfileName.IsNone()) 395 | { 396 | OutLaneProfileIndices[ElemIdx] = -1; 397 | continue; 398 | } 399 | 400 | if (const int32* IdxPtr = NameIdxMap.Find(LaneProfileName)) 401 | OutLaneProfileIndices[ElemIdx] = *IdxPtr; 402 | else 403 | { 404 | const int32 Idx = ZoneGraphSettings->GetLaneProfiles().IndexOfByPredicate( 405 | [LaneProfileName](const FZoneLaneProfile& LaneProfile) { return LaneProfile.Name == LaneProfileName; }); 406 | NameIdxMap.Add(LaneProfileName, Idx); 407 | OutLaneProfileIndices[ElemIdx] = Idx; 408 | } 409 | } 410 | } 411 | 412 | return true; 413 | } 414 | 415 | bool HoudiniZoneShapeOutputUtils::HapiGetOrCreateLaneProfiles(const int32& NodeId, const int32& PartId, const TArray& AttribNames, const int AttribCounts[HAPI_ATTROWNER_MAX], 416 | UZoneGraphSettings* ZoneGraphSettings, const bool bIsOnPoints, 417 | HAPI_AttributeOwner& OutLaneProfileOwner, TMap& InOutHashProfileIdxMap, TArray& OutLaneProfileIndices, bool& bZoneGraphSettingsModified) 418 | { 419 | OutLaneProfileOwner = HAPI_ATTROWNER_INVALID; 420 | 421 | const HAPI_AttributeOwner Owner0 = bIsOnPoints ? HAPI_ATTROWNER_VERTEX : HAPI_ATTROWNER_PRIM; 422 | const HAPI_AttributeOwner Owner1 = bIsOnPoints ? HAPI_ATTROWNER_POINT : HAPI_ATTROWNER_DETAIL; 423 | 424 | HAPI_AttributeOwner LaneProfileNameOwner = HAPI_ATTROWNER_INVALID; 425 | if (FHoudiniEngineUtils::IsAttributeExists(AttribNames, AttribCounts, 426 | HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE_NAME, Owner0)) 427 | LaneProfileNameOwner = Owner0; 428 | else if (FHoudiniEngineUtils::IsAttributeExists(AttribNames, AttribCounts, 429 | HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE_NAME, Owner1)) 430 | LaneProfileNameOwner = Owner1; 431 | 432 | if (FHoudiniEngineUtils::IsAttributeExists(AttribNames, AttribCounts, 433 | HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE, Owner0)) 434 | OutLaneProfileOwner = Owner0; 435 | else if (FHoudiniEngineUtils::IsAttributeExists(AttribNames, AttribCounts, 436 | HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE, Owner1)) 437 | OutLaneProfileOwner = Owner1; 438 | 439 | if ((LaneProfileNameOwner != HAPI_ATTROWNER_INVALID) || (OutLaneProfileOwner != HAPI_ATTROWNER_INVALID)) 440 | { 441 | HOUDINI_FAIL_RETURN(HapiGetOrCreateLaneProfiles(NodeId, PartId, 442 | ZoneGraphSettings, LaneProfileNameOwner, OutLaneProfileOwner, InOutHashProfileIdxMap, OutLaneProfileIndices, bZoneGraphSettingsModified)); 443 | 444 | if (OutLaneProfileOwner == HAPI_ATTROWNER_INVALID) 445 | OutLaneProfileOwner = LaneProfileNameOwner; 446 | } 447 | 448 | return true; 449 | } 450 | 451 | using namespace HoudiniZoneShapeOutputUtils; 452 | 453 | 454 | bool UHoudiniOutputZoneShape::HapiUpdate(const HAPI_GeoInfo& GeoInfo, const TArray& PartInfos) 455 | { 456 | TRACE_CPUPROFILER_EVENT_SCOPE(HoudiniOutputZoneShape); 457 | 458 | const AHoudiniNode* Node = GetNode(); 459 | 460 | 461 | struct FHoudiniCurveIndicesHolder 462 | { 463 | FHoudiniCurveIndicesHolder(const int8& UpdateMode, const FString& InSplitValue) : 464 | PartialOutputMode(UpdateMode), SplitValue(InSplitValue) {} 465 | 466 | int8 PartialOutputMode = 0; 467 | FString SplitValue; 468 | 469 | TArray CurveIndices; 470 | }; 471 | 472 | struct FHoudiniCurvesPart 473 | { 474 | FHoudiniCurvesPart(const HAPI_PartInfo& PartInfo) : Info(PartInfo) {} 475 | 476 | HAPI_PartInfo Info; 477 | TArray AttribNames; 478 | TArray VertexIndices; // Accumulate append CurveCounts 479 | TMap SplitCurvesMap; 480 | }; 481 | 482 | 483 | const int32& NodeId = GeoInfo.nodeId; 484 | 485 | 486 | // -------- Retrieve all part data -------- 487 | TArray Parts; 488 | for (const HAPI_PartInfo& PartInfo : PartInfos) 489 | Parts.Add(FHoudiniCurvesPart(PartInfo)); 490 | 491 | bool bPartialUpdate = false; 492 | 493 | for (FHoudiniCurvesPart& Part : Parts) 494 | { 495 | const HAPI_PartInfo& PartInfo = Part.Info; 496 | const HAPI_PartId& PartId = PartInfo.id; 497 | 498 | 499 | // -------- Retrieve attrib and group names -------- 500 | HOUDINI_FAIL_RETURN(FHoudiniEngineUtils::HapiGetAttributeNames(NodeId, PartId, PartInfo.attributeCounts, Part.AttribNames)); 501 | const TArray& AttribNames = Part.AttribNames; 502 | 503 | 504 | // -------- Retrieve split values and partial output modes if exists -------- 505 | TArray SplitKeys; // Maybe int or HAPI_StringHandle 506 | HAPI_AttributeOwner SplitAttribOwner = HAPI_ATTROWNER_PRIM; // Prefer on prim 507 | TMap SplitValueMap; 508 | FHoudiniOutputUtils::HapiGetSplitValues(NodeId, PartId, AttribNames, PartInfo.attributeCounts, 509 | SplitKeys, SplitValueMap, SplitAttribOwner); 510 | const bool bHasSplitValues = !SplitKeys.IsEmpty(); 511 | 512 | HAPI_AttributeOwner PartialOutputModeOwner = bHasSplitValues ? FHoudiniEngineUtils::QueryAttributeOwner(AttribNames, 513 | PartInfo.attributeCounts, HAPI_ATTRIB_PARTIAL_OUTPUT_MODE) : HAPI_ATTROWNER_INVALID; 514 | TArray PartialOutputModes; 515 | HOUDINI_FAIL_RETURN(FHoudiniEngineUtils::HapiGetEnumAttributeData(NodeId, PartId, 516 | HAPI_ATTRIB_PARTIAL_OUTPUT_MODE, PartialOutputModes, PartialOutputModeOwner)); 517 | 518 | 519 | // -------- Retrieve vertex list -------- 520 | TArray CurveCounts; 521 | CurveCounts.SetNumUninitialized(PartInfo.faceCount); 522 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetCurveCounts(FHoudiniEngine::Get().GetSession(), NodeId, PartId, 523 | CurveCounts.GetData(), 0, PartInfo.faceCount)); 524 | 525 | 526 | // -------- Split curves -------- 527 | TMap& SplitMap = Part.SplitCurvesMap; 528 | FHoudiniCurveIndicesHolder AllCurves(HAPI_PARTIAL_OUTPUT_MODE_REPLACE, FString()); 529 | int32 VertexIdx = 0; // The first vertex/point index of this curve, will accumulate each CurveCount 530 | for (int32 CurveIdx = 0; CurveIdx < PartInfo.faceCount; ++CurveIdx) 531 | { 532 | // Same as HoudiniOutputMesh 533 | // Judge PartialOutputMode, if remove && previous NOT set, then we will NOT parse the GroupIdx 534 | const int32 SplitKey = bHasSplitValues ? 535 | SplitKeys[FHoudiniOutputUtils::CurveAttributeEntryIdx(SplitAttribOwner, VertexIdx, CurveIdx)] : 0; 536 | FHoudiniCurveIndicesHolder* FoundHolderPtr = bHasSplitValues ? SplitMap.Find(SplitKey) : nullptr; 537 | 538 | const int8 PartialOutputMode = FMath::Clamp(PartialOutputModes.IsEmpty() ? HAPI_PARTIAL_OUTPUT_MODE_REPLACE : 539 | PartialOutputModes[FHoudiniOutputUtils::CurveAttributeEntryIdx(PartialOutputModeOwner, VertexIdx, CurveIdx)], 540 | HAPI_PARTIAL_OUTPUT_MODE_REPLACE, HAPI_PARTIAL_OUTPUT_MODE_REMOVE); 541 | 542 | if (PartialOutputMode == HAPI_PARTIAL_OUTPUT_MODE_MODIFY) 543 | bPartialUpdate = true; 544 | else if (PartialOutputMode == HAPI_PARTIAL_OUTPUT_MODE_REMOVE) // If has PartialOutputModes, then must also HasSplitValues 545 | { 546 | bPartialUpdate = true; 547 | if (FoundHolderPtr) // If previous triangles has identified PartialOutputMode, We should NOT change it 548 | { 549 | if (FoundHolderPtr->PartialOutputMode == HAPI_PARTIAL_OUTPUT_MODE_REMOVE) 550 | continue; 551 | } 552 | else 553 | { 554 | SplitMap.Add(SplitKey, FHoudiniCurveIndicesHolder(HAPI_PARTIAL_OUTPUT_MODE_REMOVE, GET_SPLIT_VALUE_STR)); 555 | continue; 556 | } 557 | } 558 | 559 | if (bHasSplitValues) 560 | { 561 | if (!FoundHolderPtr) 562 | FoundHolderPtr = &SplitMap.Add(SplitKey, FHoudiniCurveIndicesHolder(PartialOutputMode, GET_SPLIT_VALUE_STR)); 563 | FoundHolderPtr->CurveIndices.Add(CurveIdx); 564 | } 565 | else 566 | AllCurves.CurveIndices.Add(CurveIdx); 567 | 568 | VertexIdx += CurveCounts[CurveIdx]; 569 | Part.VertexIndices.Add(VertexIdx); 570 | } 571 | 572 | if (!bHasSplitValues) 573 | SplitMap.Add(0, AllCurves); // We just add AllLodTrianglesMap to SplitMeshMap 574 | } 575 | 576 | TDoubleLinkedList OldZoneShapeOutputs; 577 | TArray NewZoneShapeOutputs; 578 | 579 | if (bPartialUpdate) 580 | { 581 | TSet ModifySplitValues; 582 | TSet RemoveSplitValues; 583 | for (FHoudiniCurvesPart& Part : Parts) 584 | { 585 | for (TMap::TIterator SplitIter(Part.SplitCurvesMap); SplitIter; ++SplitIter) 586 | { 587 | if (SplitIter->Value.PartialOutputMode >= HAPI_PARTIAL_OUTPUT_MODE_REMOVE) 588 | { 589 | RemoveSplitValues.FindOrAdd(SplitIter->Value.SplitValue); 590 | SplitIter.RemoveCurrent(); 591 | } 592 | else 593 | ModifySplitValues.FindOrAdd(SplitIter->Value.SplitValue); 594 | } 595 | } 596 | 597 | FHoudiniOutputUtils::UpdateSplittableOutputHolders(ModifySplitValues, RemoveSplitValues, ZoneShapeOutputs, 598 | [Node](const FHoudiniZoneShapeOutput& OldZSOutput) { return IsValid(OldZSOutput.Find(Node)); }, OldZoneShapeOutputs, NewZoneShapeOutputs); 599 | } 600 | else // Collect valid old output holders for reuse 601 | FHoudiniOutputUtils::UpdateOutputHolders(ZoneShapeOutputs, 602 | [Node](const FHoudiniZoneShapeOutput& OldZSOutput) { return IsValid(OldZSOutput.Find(Node)); }, OldZoneShapeOutputs); 603 | 604 | FHoudiniEngine::Get().FinishHoudiniMainTaskMessage(); // Avoid RHI crash 605 | 606 | UZoneGraphSettings* ZoneGraphSettings = GetMutableDefault(); 607 | 608 | TMap HashProfileIdxMap; // Find all unique lane profiles 609 | for (int32 ProfileIdx = 0; ProfileIdx < ZoneGraphSettings->GetLaneProfiles().Num(); ++ProfileIdx) 610 | HashProfileIdxMap.FindOrAdd(GetLaneProfileHash(ZoneGraphSettings->GetLaneProfiles()[ProfileIdx].Lanes), ProfileIdx); 611 | 612 | FArrayProperty* ShapeConnectorsProp = CastField(UZoneShapeComponent::StaticClass()->FindPropertyByName("ShapeConnectors")); 613 | FArrayProperty* ConnectedShapesProp = CastField(UZoneShapeComponent::StaticClass()->FindPropertyByName("ConnectedShapes")); 614 | 615 | bool bZoneGraphSettingsModified = false; 616 | TMap> ActorPropertyNamesMap; // Use to avoid Set the same property in same SplitActor twice 617 | HAPI_AttributeInfo AttribInfo; 618 | TArray ChangedZSCs; 619 | for (const FHoudiniCurvesPart& Part : Parts) 620 | { 621 | if (Part.SplitCurvesMap.IsEmpty()) 622 | continue; 623 | 624 | const HAPI_PartInfo& PartInfo = Part.Info; 625 | const HAPI_PartId& PartId = PartInfo.id; 626 | const TArray& AttribNames = Part.AttribNames; 627 | 628 | // -------- Transforms -------- 629 | TArray PositionData; 630 | PositionData.SetNumUninitialized(PartInfo.pointCount * 3); 631 | 632 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), NodeId, PartId, 633 | HAPI_ATTRIB_POSITION, HAPI_ATTROWNER_POINT, &AttribInfo)); 634 | 635 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeFloatData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, 636 | HAPI_ATTRIB_POSITION, &AttribInfo, -1, PositionData.GetData(), 0, PartInfo.pointCount)); 637 | 638 | HAPI_AttributeOwner RotOwner = FHoudiniEngineUtils::QueryAttributeOwner(AttribNames, PartInfo.attributeCounts, HAPI_ATTRIB_ROT); 639 | TArray Rots; 640 | if (RotOwner != HAPI_ATTROWNER_INVALID) 641 | { 642 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeInfo(FHoudiniEngine::Get().GetSession(), NodeId, PartId, 643 | HAPI_ATTRIB_ROT, RotOwner, &AttribInfo)); 644 | 645 | if (((AttribInfo.storage == HAPI_STORAGETYPE_FLOAT) || (AttribInfo.storage == HAPI_STORAGETYPE_FLOAT64)) && 646 | ((AttribInfo.tupleSize == 3) || (AttribInfo.tupleSize == 4))) 647 | { 648 | TArray RotData; 649 | RotData.SetNumUninitialized(AttribInfo.count * AttribInfo.tupleSize); 650 | 651 | HAPI_SESSION_FAIL_RETURN(FHoudiniApi::GetAttributeFloatData(FHoudiniEngine::Get().GetSession(), NodeId, PartId, 652 | HAPI_ATTRIB_ROT, &AttribInfo, -1, RotData.GetData(), 0, AttribInfo.count)); 653 | 654 | Rots.SetNumUninitialized(AttribInfo.count); 655 | if (AttribInfo.tupleSize == 4) 656 | { 657 | for (int32 ElemIdx = 0; ElemIdx < AttribInfo.count; ++ElemIdx) 658 | Rots[ElemIdx] = FQuat(RotData[ElemIdx * 4], RotData[ElemIdx * 4 + 2], RotData[ElemIdx * 4 + 1], -RotData[ElemIdx * 4 + 3]).Rotator(); 659 | } 660 | else 661 | { 662 | for (int32 ElemIdx = 0; ElemIdx < AttribInfo.count; ++ElemIdx) 663 | Rots[ElemIdx] = FRotator(FMath::RadiansToDegrees(RotData[ElemIdx * 3]), FMath::RadiansToDegrees(RotData[ElemIdx * 3 + 2]), FMath::RadiansToDegrees(RotData[ElemIdx * 3 + 1])); 664 | } 665 | } 666 | else 667 | RotOwner = HAPI_ATTROWNER_INVALID; 668 | } 669 | 670 | HAPI_AttributeOwner ZoneShapeTypeOwner = FHoudiniEngineUtils::QueryAttributeOwner(AttribNames, PartInfo.attributeCounts, HAPI_ATTRIB_UNREAL_ZONE_SHAPE_TYPE); 671 | TArray ZoneShapeTypes; 672 | HOUDINI_FAIL_RETURN(FHoudiniEngineUtils::HapiGetEnumAttributeData(NodeId, PartId, 673 | HAPI_ATTRIB_UNREAL_ZONE_SHAPE_TYPE, [](const FUtf8StringView& AttribValue) 674 | { 675 | if ((UE::String::FindFirst(AttribValue, "polygon", ESearchCase::IgnoreCase) != INDEX_NONE)) 676 | return 1; 677 | return 0; 678 | }, ZoneShapeTypes, ZoneShapeTypeOwner)); 679 | 680 | // Lane Profile 681 | TArray PointLaneProfileIndices; 682 | HAPI_AttributeOwner LaneProfileOwner = HAPI_ATTROWNER_INVALID; // For Curve, maybe on prim or detail 683 | TArray LaneProfileIndices; // For Curve, maybe on prim or detail 684 | { 685 | // We should check whether vertex or point has lane profile attrib 686 | HAPI_AttributeOwner PointLaneProfileOwner; 687 | HOUDINI_FAIL_RETURN(HapiGetOrCreateLaneProfiles(NodeId, PartId, AttribNames, PartInfo.attributeCounts, 688 | ZoneGraphSettings, true, PointLaneProfileOwner, HashProfileIdxMap, PointLaneProfileIndices, bZoneGraphSettingsModified)); 689 | 690 | // We should also check whether prim or detail has lane profile attrib 691 | HOUDINI_FAIL_RETURN(HapiGetOrCreateLaneProfiles(NodeId, PartId, AttribNames, PartInfo.attributeCounts, 692 | ZoneGraphSettings, false, LaneProfileOwner, HashProfileIdxMap, LaneProfileIndices, bZoneGraphSettingsModified)); 693 | } 694 | 695 | // Zone Shape Tags 696 | HAPI_AttributeOwner ZoneGraphTagOwner = FHoudiniEngineUtils::IsAttributeExists(AttribNames, PartInfo.attributeCounts, HAPI_ATTRIB_UNREAL_ZONE_SHAPE_TAGS, HAPI_ATTROWNER_PRIM) ? 697 | HAPI_ATTROWNER_PRIM : FHoudiniEngineUtils::QueryAttributeOwner(AttribNames, PartInfo.attributeCounts, HAPI_ATTRIB_UNREAL_ZONE_SHAPE_TAGS); 698 | TArray ZoneGraphTags; 699 | HOUDINI_FAIL_RETURN(HapiGetOrCreateTags(NodeId, PartId, ZoneGraphSettings, ZoneGraphTagOwner, ZoneGraphTags, bZoneGraphSettingsModified)); 700 | 701 | // Common 702 | HAPI_AttributeOwner SplitActorsOwner = FHoudiniEngineUtils::QueryAttributeOwner(AttribNames, PartInfo.attributeCounts, HAPI_ATTRIB_UNREAL_SPLIT_ACTORS); 703 | TArray bSplitActors; 704 | HOUDINI_FAIL_RETURN(FHoudiniEngineUtils::HapiGetEnumAttributeData(NodeId, PartId, 705 | HAPI_ATTRIB_UNREAL_SPLIT_ACTORS, bSplitActors, SplitActorsOwner)); 706 | 707 | TArray> PropAttribs; 708 | HOUDINI_FAIL_RETURN(FHoudiniAttribute::HapiRetrieveAttributes(NodeId, PartId, AttribNames, PartInfo.attributeCounts, 709 | HAPI_ATTRIB_PREFIX_UNREAL_UPROPERTY, PropAttribs)); 710 | 711 | const TArray& VertexIndices = Part.VertexIndices; 712 | for (const auto& SplitCurves : Part.SplitCurvesMap) 713 | { 714 | const FString& SplitValue = SplitCurves.Value.SplitValue; 715 | 716 | for (const int32& CurveIdx : SplitCurves.Value.CurveIndices) 717 | { 718 | const int32 MainVertexIdx = (CurveIdx == 0) ? 0 : VertexIndices[CurveIdx - 1]; 719 | 720 | bool bSplitActor = false; 721 | if (!bSplitActors.IsEmpty()) 722 | bSplitActor = bSplitActors[FHoudiniOutputUtils::CurveAttributeEntryIdx(SplitActorsOwner, MainVertexIdx, CurveIdx)] >= 1; 723 | 724 | FHoudiniZoneShapeOutput NewZSOutput; 725 | if (FHoudiniZoneShapeOutput* FoundZSOutput = FHoudiniOutputUtils::FindOutputHolder(OldZoneShapeOutputs, 726 | [&](FHoudiniZoneShapeOutput* OldZSOutput) { return OldZSOutput->CanReuse(SplitValue, bSplitActor); })) 727 | NewZSOutput = *FoundZSOutput; 728 | 729 | UZoneShapeComponent* ZSC = NewZSOutput.CreateOrUpdate(GetNode(), SplitValue, bSplitActor); 730 | 731 | // We should judge ZoneShapeType first, if is Polygon, then points should have LaneProfile 732 | if (!ZoneShapeTypes.IsEmpty()) 733 | ZSC->SetShapeType((FZoneShapeType)ZoneShapeTypes[FHoudiniOutputUtils::CurveAttributeEntryIdx(ZoneShapeTypeOwner, MainVertexIdx, CurveIdx)]); 734 | 735 | if (!ZoneGraphTags.IsEmpty()) 736 | ZSC->SetTags(ZoneGraphTags[FHoudiniOutputUtils::CurveAttributeEntryIdx(ZoneShapeTypeOwner, MainVertexIdx, CurveIdx)]); 737 | 738 | if (!LaneProfileIndices.IsEmpty()) 739 | { 740 | const int32 LaneProfileIdx = LaneProfileIndices[FHoudiniOutputUtils::CurveAttributeEntryIdx(LaneProfileOwner, MainVertexIdx, CurveIdx)]; 741 | if (ZoneGraphSettings->GetLaneProfiles().IsValidIndex(LaneProfileIdx)) 742 | ZSC->SetCommonLaneProfile(ZoneGraphSettings->GetLaneProfiles()[LaneProfileIdx]); 743 | } 744 | 745 | TArray& Points = ZSC->GetMutablePoints(); 746 | 747 | const int32 StartVertexIdx = ((CurveIdx == 0) ? 0 : VertexIndices[CurveIdx - 1]); 748 | const int32 NumCurvePoints = VertexIndices[CurveIdx] - StartVertexIdx; 749 | Points.SetNum(NumCurvePoints); 750 | 751 | ZSC->ClearPerPointLaneProfiles(); 752 | 753 | for (int32 PointIdx = 0; PointIdx < NumCurvePoints; ++PointIdx) 754 | { 755 | FZoneShapePoint& Point = Points[PointIdx]; 756 | Point = FZoneShapePoint(); // Reset 757 | const int32 GlobalPointIdx = PointIdx + StartVertexIdx; 758 | Point.Position = FVector(PositionData[GlobalPointIdx * 3], PositionData[GlobalPointIdx * 3 + 2], PositionData[GlobalPointIdx * 3 + 1]) * POSITION_SCALE_TO_UNREAL; 759 | if (!Rots.IsEmpty()) 760 | Point.Rotation = Rots[FHoudiniOutputUtils::CurveAttributeEntryIdx(RotOwner, GlobalPointIdx, CurveIdx)]; 761 | 762 | Point.LaneProfile = FZoneShapePoint::InheritLaneProfile; 763 | if (!PointLaneProfileIndices.IsEmpty() && ZSC->GetShapeType() == FZoneShapeType::Polygon) 764 | { 765 | Point.Type = FZoneShapePointType::LaneProfile; 766 | const int32& LaneProfileIdx = PointLaneProfileIndices[GlobalPointIdx]; 767 | if (ZoneGraphSettings->GetLaneProfiles().IsValidIndex(LaneProfileIdx)) 768 | Point.LaneProfile = ZSC->AddUniquePerPointLaneProfile(ZoneGraphSettings->GetLaneProfiles()[LaneProfileIdx]); 769 | } 770 | 771 | for (const TSharedPtr& PropAttrib : PropAttribs) 772 | { 773 | const HAPI_AttributeOwner& PropAttribOwner = PropAttrib->GetOwner(); 774 | if ((PropAttribOwner == HAPI_ATTROWNER_VERTEX) || (PropAttribOwner == HAPI_ATTROWNER_POINT)) 775 | PropAttrib->SetStructPropertyValues(&Point, FZoneShapePoint::StaticStruct(), 776 | FHoudiniOutputUtils::CurveAttributeEntryIdx(PropAttribOwner, GlobalPointIdx, CurveIdx)); 777 | } 778 | } 779 | 780 | // Set UProperties 781 | for (const TSharedPtr& PropAttrib : PropAttribs) 782 | { 783 | const HAPI_AttributeOwner& PropAttribOwner = PropAttrib->GetOwner(); 784 | if ((PropAttribOwner == HAPI_ATTROWNER_PRIM) || (PropAttribOwner == HAPI_ATTROWNER_DETAIL)) 785 | PropAttrib->SetObjectPropertyValues(ZSC, FHoudiniOutputUtils::CurveAttributeEntryIdx(PropAttribOwner, MainVertexIdx, CurveIdx)); 786 | } 787 | SET_SPLIT_ACTOR_UPROPERTIES(NewZSOutput, FHoudiniOutputUtils::CurveAttributeEntryIdx(PropAttribOwner, MainVertexIdx, CurveIdx), false); 788 | 789 | // Avoid Crash when ZSC create scene proxy 790 | if (ShapeConnectorsProp && ConnectedShapesProp) 791 | { 792 | FScriptArrayHelper_InContainer ArrayHelper(ShapeConnectorsProp, ZSC); 793 | ArrayHelper.EmptyValues(); 794 | ArrayHelper = FScriptArrayHelper_InContainer(ConnectedShapesProp, ZSC); 795 | ArrayHelper.EmptyValues(); 796 | } 797 | 798 | ChangedZSCs.Add(ZSC); 799 | 800 | NewZoneShapeOutputs.Add(NewZSOutput); 801 | } 802 | } 803 | } 804 | 805 | // -------- Post-processing -------- 806 | if (bZoneGraphSettingsModified) 807 | ZoneGraphSettings->TryUpdateDefaultConfigFile(); 808 | 809 | // Destroy old outputs, like this->Destroy() 810 | for (const FHoudiniZoneShapeOutput* OldZSOutput : OldZoneShapeOutputs) 811 | OldZSOutput->Destroy(Node); 812 | OldZoneShapeOutputs.Empty(); 813 | 814 | // Update output holders 815 | ZoneShapeOutputs = NewZoneShapeOutputs; 816 | 817 | // We should update shapes after useless ZSCs has been destroyed 818 | for (UZoneShapeComponent* ZSC : ChangedZSCs) 819 | { 820 | ZSC->UpdateShape(); 821 | ZSC->Modify(); 822 | } 823 | 824 | if (!ChangedZSCs.IsEmpty()) 825 | AsyncTask(ENamedThreads::GameThread, [] { FHoudiniMassTranslator::Get().OnZoneShapeOutputFinish(); }); // After all outputs finished 826 | 827 | return true; 828 | } 829 | 830 | void UHoudiniOutputZoneShape::Destroy() const 831 | { 832 | for (const FHoudiniZoneShapeOutput& OldZoneShapeOutput : ZoneShapeOutputs) 833 | OldZoneShapeOutput.Destroy(GetNode()); 834 | } 835 | 836 | void UHoudiniOutputZoneShape::CollectActorSplitValues(TSet& InOutSplitValues, TSet& InOutEditableSplitValues) const 837 | { 838 | for (const FHoudiniZoneShapeOutput& ZoneShapeOutput : ZoneShapeOutputs) 839 | { 840 | if (ZoneShapeOutput.IsSplitActor()) 841 | InOutSplitValues.FindOrAdd(ZoneShapeOutput.GetSplitValue()); 842 | } 843 | } 844 | -------------------------------------------------------------------------------- /Source/HoudiniMassTranslator/Public/HoudiniInputZoneShape.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) <2025> Yuzhe Pan (childadrianpan@gmail.com). All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "HoudiniInput.h" 6 | 7 | 8 | class HOUDINIMASSTRANSLATOR_API FHoudiniZoneShapeComponentInput : public FHoudiniComponentInput 9 | { 10 | public: 11 | int32 NodeId = -1; 12 | 13 | virtual void Invalidate() const override {} // Will then delete this, so we need NOT to reset node ids to -1 14 | 15 | virtual bool HapiDestroy(UHoudiniInput* Input) const override; // Will then delete this, so we need NOT to reset node ids to -1 16 | }; 17 | 18 | class HOUDINIMASSTRANSLATOR_API FHoudiniZoneShapeComponentInputBuilder : public IHoudiniComponentInputBuilder 19 | { 20 | public: 21 | virtual bool IsValidInput(const UActorComponent* Component) override; 22 | 23 | virtual bool HapiUpload(UHoudiniInput* Input, const bool& bIsSingleComponent, // Is there only one single valid component in the whole blueprint/actor 24 | const TArray& Components, const TArray& Transforms, const TArray& ComponentIndices, // Components and Transforms are all of the components in blueprint/actor, and ComponentIndices are ref the valid indices from IsValidInput 25 | int32& InOutInstancerNodeId, TArray>& InOutComponentInputs, TArray& InOutPoints) override; 26 | 27 | virtual void AppendInfo(const TArray& Components, const TArray& Transforms, const TArray& ComponentIndices, // See the comment upon 28 | const TSharedPtr& JsonObject) override; // Append object info to JsonObject, keys are instance refs, values are JsonObjects that contain transoforms and meta data 29 | }; 30 | -------------------------------------------------------------------------------- /Source/HoudiniMassTranslator/Public/HoudiniMassCommands.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) <2025> Yuzhe Pan (childadrianpan@gmail.com). All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "Framework/Commands/Commands.h" 6 | 7 | 8 | class FHoudiniMassCommands : public TCommands 9 | { 10 | public: 11 | FHoudiniMassCommands(); 12 | 13 | TSharedPtr CleanupLaneProfiles; 14 | 15 | TSharedPtr RemoveAllLaneProfiles; 16 | 17 | // TCommand<> interface 18 | virtual void RegisterCommands() override; 19 | 20 | static void OnCleanupLaneProfiles(); 21 | 22 | static void OnRemoveAllLaneProfiles(); 23 | }; -------------------------------------------------------------------------------- /Source/HoudiniMassTranslator/Public/HoudiniMassCommon.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) <2025> Yuzhe Pan (childadrianpan@gmail.com). All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #define HOUDINI_LANE_PROFILE_PREFIX TEXT("LP_HE_") 6 | #define HAPI_ATTRIB_UNREAL_OUTPUT_ZONE_SHAPE "unreal_output_zone_shape" 7 | #define HAPI_ATTRIB_UNREAL_ZONE_SHAPE_TYPE "unreal_zone_shape_type" // both int and string are supported 8 | #define HAPI_ATTRIB_UNREAL_ZONE_SHAPE_TAGS "unreal_zone_shape_tags" 9 | #define HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE "unreal_zone_lane_profile" // Define lanes, use d[]@unreal_zone_lane_profile to find or create LaneProfiles 10 | #define HAPI_ATTRIB_UNREAL_ZONE_LANE_PROFILE_NAME "unreal_zone_lane_profile_name" // use s@unreal_zone_lane_profile_name to specify exists LaneProfiles, or name the created LaneProfiles 11 | -------------------------------------------------------------------------------- /Source/HoudiniMassTranslator/Public/HoudiniMassTranslator.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) <2025> Yuzhe Pan (childadrianpan@gmail.com). All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Modules/ModuleManager.h" 7 | 8 | 9 | struct FZoneGraphBuildData; 10 | class SNotificationItem; 11 | 12 | class FHoudiniZoneShapeComponentInputBuilder; 13 | class FHoudiniZoneShapeOutputBuilder; 14 | 15 | class FHoudiniMassTranslator : public IModuleInterface 16 | { 17 | public: 18 | /** IModuleInterface implementation */ 19 | virtual void StartupModule() override; 20 | virtual void ShutdownModule() override; 21 | 22 | FORCEINLINE static FHoudiniMassTranslator& Get() { return *HoudiniMassTranslatorInstance; } 23 | 24 | void OnZoneShapeOutputFinish(); 25 | 26 | protected: 27 | static FHoudiniMassTranslator* HoudiniMassTranslatorInstance; 28 | 29 | TSharedPtr ComponentInputBuilder; 30 | 31 | TSharedPtr OutputBuilder; 32 | 33 | TSharedPtr Commands; 34 | 35 | TWeakPtr Notification; 36 | 37 | void OnZoneGraphBuildDone(const FZoneGraphBuildData&); 38 | 39 | void OnZoneGraphBuildCancel(const bool); 40 | }; 41 | -------------------------------------------------------------------------------- /Source/HoudiniMassTranslator/Public/HoudiniOutputZoneShape.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) <2025> Yuzhe Pan (childadrianpan@gmail.com). All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "HoudiniOutput.h" 6 | 7 | #include "HoudiniOutputZoneShape.generated.h" 8 | 9 | 10 | class UZoneShapeComponent; 11 | 12 | USTRUCT() 13 | struct HOUDINIMASSTRANSLATOR_API FHoudiniZoneShapeOutput : public FHoudiniComponentOutput 14 | { 15 | GENERATED_BODY() 16 | 17 | protected: 18 | mutable TWeakObjectPtr Component; 19 | 20 | public: 21 | UZoneShapeComponent* Find(const AHoudiniNode* Node) const; 22 | 23 | UZoneShapeComponent* CreateOrUpdate(AHoudiniNode* Node, const FString& InSplitValue, const bool& bSplitToActors); 24 | 25 | void Destroy(const AHoudiniNode* Node) const; 26 | }; 27 | 28 | UCLASS() 29 | class HOUDINIMASSTRANSLATOR_API UHoudiniOutputZoneShape : public UHoudiniOutput 30 | { 31 | GENERATED_BODY() 32 | 33 | protected: 34 | UPROPERTY() 35 | TArray ZoneShapeOutputs; 36 | 37 | public: 38 | virtual bool HapiUpdate(const HAPI_GeoInfo& GeoInfo, const TArray& PartInfos) override; 39 | 40 | virtual void Destroy() const override; 41 | 42 | virtual void CollectActorSplitValues(TSet& InOutSplitValues, TSet& InOutEditableSplitValues) const override; 43 | }; 44 | 45 | 46 | class HOUDINIMASSTRANSLATOR_API FHoudiniZoneShapeOutputBuilder : public IHoudiniOutputBuilder 47 | { 48 | public: 49 | virtual bool HapiIsPartValid(const int32& NodeId, const HAPI_PartInfo& PartInfo, bool& bOutIsValid, bool& bOutShouldHoldByOutput) override; 50 | 51 | virtual TSubclassOf GetClass() const override { return UHoudiniOutputZoneShape::StaticClass(); } 52 | }; --------------------------------------------------------------------------------