Exporting Grouped Houdini Point Data to Unity via JSON (Without Houdini Engine)
02 Oct 26 (Today)
Using Houdini as an offline layout engine avoids the overhead of runtime Houdini Engine sessions while allowing you to scatter complex environments with LODs and prefabs.
Rather than exporting a single flat array or baking static geometry, this workflow groups point data dynamically by an attribute (like name) in Python, exports it into clean JSON, and uses a native-friendly Unity importer to instantiate matching prefabs automatically.
Houdini Setup: Grouped Python Export
In Houdini, assign a name string attribute to your scattered points (e.g., "Tree_Oak", "Rock_Large", "Debris"). A Python SOP then groups the transforms by this identifier and converts coordinates to match Unity.
Coordinate Conversion
- Position: Houdini uses right-handed ($Y$-up, $Z$-out), while Unity uses left-handed ($Y$-up, $Z$-forward). Invert the $Z$ position (
-pos[2]). - Rotation: Invert the $Z$ and $W$ components of the
orientquaternion (-orient[2],-orient[3]).
Wire your point geometry into a Python SOP and paste the following script:
import json
import os
import hou
node = hou.pwd()
geo = node.geometry()
# Root container wrapping grouped data
# Structure: {"groups": [ {"name": "Tree_Oak", "points": [...]}, ... ]}
grouped_dict = {}
has_name = geo.findPointAttrib("name") is not None
has_orient = geo.findPointAttrib("orient") is not None
has_pscale = geo.findPointAttrib("pscale") is not None
for pt in geo.points():
group_name = pt.stringAttribValue("name") if has_name else "Default"
if group_name not in grouped_dict:
grouped_dict[group_name] = []
pos = pt.position()
# 1. Position (Flip Z for Unity)
pt_data = {
"x": pos[0],
"y": pos[1],
"z": -pos[2]
}
# 2. Scale
pt_data["pscale"] = pt.attribValue("pscale") if has_pscale else 1.0
# 3. Rotation (Flip Z and W for Unity)
if has_orient:
orient = pt.attribValue("orient")
pt_data["qx"] = orient[0]
pt_data["qy"] = orient[1]
pt_data["qz"] = -orient[2]
pt_data["qw"] = -orient[3]
else:
pt_data["qx"], pt_data["qy"], pt_data["qz"], pt_data["qw"] = 0.0, 0.0, 0.0, 1.0
grouped_dict[group_name].append(pt_data)
# Pack into a structure Unity's native JsonUtility can read without third-party plugins
export_data = {
"groups": [
{"name": key, "points": points}
for key, points in grouped_dict.items()
]
}
# Target file path inside your Unity project
file_path = "C:/YourUnityProject/Assets/Data/houdini_layout.json"
directory = os.path.dirname(file_path)
if not os.path.exists(directory):
os.makedirs(directory)
with open(file_path, 'w') as f:
json.dump(export_data, f, indent=4)
The JSON Structure
By nesting the groups inside a top-level list, the JSON remains 100% compatible with Unity's fast, built-in JsonUtility without needing external libraries like Newtonsoft.Json:
{
"groups": [
{
"name": "Tree_Oak",
"points": [
{
"x": 1.25,
"y": 0.0,
"z": -5.42,
"pscale": 1.2,
"qx": 0.0,
"qy": 0.38,
"qz": -0.0,
"qw": -0.92
}
]
},
{
"name": "Rock_Large",
"points": [
{
"x": 8.5,
"y": 1.0,
"z": 3.2,
"pscale": 0.8,
"qx": 0.0,
"qy": 0.0,
"qz": 0.0,
"qw": 1.0
}
]
}
]
}
Unity Importer & Instancer (C#)
Save this script as HoudiniMultiInstancer.cs and attach it to an empty GameObject in Unity.
It maps group names directly to assigned prefabs in the Inspector, supports baking directly in Edit Mode via context menus, and correctly cleans up spawned instances.
using UnityEngine;
using System.IO;
using System.Collections.Generic;
[System.Serializable]
public class PointData
{
public float x, y, z;
public float pscale;
public float qx, qy, qz, qw;
}
[System.Serializable]
public class PointGroup
{
public string name;
public PointData[] points;
}
[System.Serializable]
public class LayoutPayload
{
public PointGroup[] groups;
}
public class HoudiniMultiInstancer : MonoBehaviour
{
[System.Serializable]
public struct PrefabMapping
{
[Tooltip("Matches the 'name' attribute from Houdini (e.g. Tree_Oak)")]
public string attributeName;
public GameObject prefab;
}
[Header("File Settings")]
[Tooltip("Path relative to Assets/ folder, e.g. Data/houdini_layout.json")]
public string jsonRelativePath = "Data/houdini_layout.json";
[Header("Prefab Mapping")]
public List<PrefabMapping> prefabMappings = new List<PrefabMapping>();
[Header("Runtime")]
public bool spawnOnStart = false;
void Start()
{
if (spawnOnStart)
{
GenerateInstances();
}
}
[ContextMenu("Generate Instances")]
public void GenerateInstances()
{
ClearInstances();
string fullPath = Path.Combine(Application.dataPath, jsonRelativePath);
if (!File.Exists(fullPath))
{
Debug.LogError($"[HoudiniInstancer] JSON not found at: {fullPath}");
return;
}
// Build fast lookup dictionary for prefabs
Dictionary<string, GameObject> prefabMap = new Dictionary<string, GameObject>();
foreach (var mapping in prefabMappings)
{
if (!string.IsNullOrEmpty(mapping.attributeName) && mapping.prefab != null)
{
prefabMap[mapping.attributeName] = mapping.prefab;
}
}
string rawJson = File.ReadAllText(fullPath);
LayoutPayload payload = JsonUtility.FromJson<LayoutPayload>(rawJson);
if (payload?.groups == null)
{
Debug.LogError("[HoudiniInstancer] Failed to parse JSON or no groups found.");
return;
}
int totalInstantiated = 0;
foreach (PointGroup group in payload.groups)
{
if (!prefabMap.TryGetValue(group.name, out GameObject targetPrefab))
{
Debug.LogWarning($"[HoudiniInstancer] No prefab mapped for attribute name '{group.name}'. Skipping group.");
continue;
}
// Create a parent GameObject per group to keep hierarchy organized
Transform groupRoot = new GameObject($"Group_{group.name}").transform;
groupRoot.SetParent(transform, false);
foreach (PointData pt in group.points)
{
Vector3 position = new Vector3(pt.x, pt.y, pt.z);
Quaternion rotation = new Quaternion(pt.qx, pt.qy, pt.qz, pt.qw);
GameObject instance = Instantiate(targetPrefab, position, rotation, groupRoot);
instance.transform.localScale = Vector3.one * (pt.pscale > 0f ? pt.pscale : 1f);
totalInstantiated++;
}
}
Debug.Log($"[HoudiniInstancer] Successfully spawned {totalInstantiated} instances across {payload.groups.Length} groups.");
}
[ContextMenu("Clear Instances")]
public void ClearInstances()
{
for (int i = transform.childCount - 1; i >= 0; i--)
{
Transform child = transform.GetChild(i);
if (Application.isPlaying)
{
Destroy(child.gameObject);
}
else
{
DestroyImmediate(child.gameObject);
}
}
}
}
Workflow in Action
- In Houdini: Scatter your points, use an
Attribute WrangleorEnumerate SOPto sets@name(e.g.,s@name = "Tree_Oak";), and trigger the Python SOP. - In Unity: Create an empty GameObject, add
HoudiniMultiInstancer, set the file path, and add your mapping entries matching thenameattributes. - Right-click the component in the Inspector and select Generate Instances to bake directly into your scene or leave
spawnOnStartchecked for runtime population.