How to Export Houdini Point Data to Unity via JSON (Without Houdini Engine)
11 Aug 26 (1mo ago)
Procedural tools in Houdini are perfect for scattering and layout, but you don't always need to ship with the Houdini Engine in your loop. If you are scattering environment props like trees or rocks, bypassing the engine shifts your workflow from live synchronization to lightweight baking and exporting.
This JSON workflow keeps your Unity project clean. Instead of importing massive, cluttered meshes, you can use Houdini as an offline placement brain to spawn optimized, LOD-ready Unity prefabs.
Step 1: Export Point Data from Houdini (Python)
The easiest way to extract position, orientation, and scale is through a Python node in Houdini.
Coordinate Conversion: Houdini operates in a right-handed coordinate system, while Unity uses a left-handed one. The script below automatically handles this conversion by flipping the Z-axis for the position, and the Z and W axes for the orient quaternion.
Troubleshooting Tip: Ensure you are writing to a full file path (e.g.,
C:/exports/scatter.json), not just a folder, to avoid "Permission denied" errors. The script below creates the output directory automatically if it doesn't exist.
Create a Python node in Houdini, wire your scatter node into it, and paste this code:
import json
import os
import hou
node = hou.pwd()
geo = node.geometry()
data = {"points": []}
# Safety check: see if attributes exist
has_orient = geo.findPointAttrib("orient") is not None
has_pscale = geo.findPointAttrib("pscale") is not None
for pt in geo.points():
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 to match 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
data["points"].append(pt_data)
# Specify target file path inside Unity Assets folder
file_path = "C:/YourUnityProject/Assets/Data/houdini_points.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(data, f, indent=4)
Step 2: The JSON Structure & Unity's Parser
If you are used to parsing dynamic JSON data in web frameworks like Next.js, Unity's built-in JsonUtility might feel restrictive. Crucially, it does not support root arrays.
To bypass this limitation, the Python script wraps the array inside a dictionary key called "points". The resulting JSON file is perfectly structured for Unity's fast, native serialization:
{
"points": [
{
"x": 2.54,
"y": 0.0,
"z": -1.23,
"pscale": 1.5,
"qx": 0.0,
"qy": 0.707,
"qz": 0.0,
"qw": 0.707
}
]
}
Step 3: Import and Instance in Unity (C#)
In Unity, a C# script reads the JSON file, deserializes it, and instantiates a designated prefab at the correct coordinates.
Attach the HoudiniInstancer.cs script to an empty GameObject. You can trigger the generation manually in the Editor via the Inspector's Context Menu (right-click the component) to bake instances, or let it run automatically when pressing Play.
Troubleshooting Tip: Unity serializes Inspector fields. If you change code defaults after assigning a path in the Inspector, Unity keeps the old value. Always verify your JSON path in the Inspector to prevent missing file errors.
using UnityEngine;
using System.IO;
[System.Serializable]
public class PointData
{
public float x, y, z;
public float pscale;
public float qx, qy, qz, qw;
}
[System.Serializable]
public class PointList
{
public PointData[] points;
}
public class HoudiniInstancer : MonoBehaviour
{
[Header("Setup")]
[Tooltip("Path relative to the Assets folder, e.g., Data/houdini_points.json")]
public string jsonFileName = "Data/houdini_points.json";
public GameObject prefabToInstance;
[Header("Runtime Options")]
[Tooltip("If true, automatically spawns the points when you press Play.")]
public bool spawnOnStart = true;
void Start()
{
if (spawnOnStart) GenerateInstances();
}
[ContextMenu("Generate Instances")]
public void GenerateInstances()
{
ClearInstances();
if (prefabToInstance == null)
{
Debug.LogError("Error: No prefab assigned in the inspector.");
return;
}
string filePath = Path.Combine(Application.dataPath, jsonFileName);
if (!File.Exists(filePath))
{
Debug.LogError("Error: JSON file NOT found at: " + filePath);
return;
}
string jsonString = File.ReadAllText(filePath);
PointList pointList = JsonUtility.FromJson<PointList>(jsonString);
if (pointList?.points == null)
{
Debug.LogError("Error: JSON parsing failed.");
return;
}
foreach (PointData pt in pointList.points)
{
Vector3 spawnPosition = new Vector3(pt.x, pt.y, pt.z);
Quaternion spawnRotation = new Quaternion(pt.qx, pt.qy, pt.qz, pt.qw);
GameObject newInstance = Instantiate(prefabToInstance, spawnPosition, spawnRotation, transform);
newInstance.transform.localScale = Vector3.one * pt.pscale;
}
Debug.Log($"Successfully instanced {pointList.points.Length} objects.");
}
[ContextMenu("Clear Instances")]
public void ClearInstances()
{
for (int i = transform.childCount - 1; i >= 0; i--)
{
if (Application.isPlaying) Destroy(transform.GetChild(i).gameObject);
else DestroyImmediate(transform.GetChild(i).gameObject);
}
}
}