Breakdown 2 of 4

Houdini PDG and the stud algorithm

One PDG network builds all 945 parts with Stefan Müller's LDraw2Houdini, then my stud algorithm raycasts each one to find where other bricks connect.

Overview

The Houdini side has two jobs: generating a mesh for every part in the part list, and working out where each part can connect to another. The first is mostly Stefan Müller’s LDraw2Houdini (Müller, 2023). The second is my own work.

I left stud generation until last on purpose, since the tool already worked with studs set by hand on one test brick. Both jobs run in one PDG network, which suits working through a large set of parts.

Generating the meshes

LDraw2Houdini is a digital asset that parses the LDraw .dat files and builds the part geometry, which saved me writing my own parser. My mesh branch uses it mostly as it comes, plus scaling, a grid-aligned origin and UVs.

Houdini with the LDraw2Houdini node generating part 3001, a grey 2 x 4 brick in the viewport, and the node's parameters on the right showing the part number, material settings and a logo on studs option
LDraw2Houdini generating part 3001.

My first mesh generation wasn’t in PDG at all, and I only moved it over once the stud work started. The network makes one work item per entry in the part list, then splits in two. The right branch runs LDraw2Houdini’s full mesh asset through an HDA Processor and writes each part out as an FBX, and the left branch generates the stud positions.

The two branches of the TOP network side by side. On the left, generate_no_stud runs the ldraw_part_no_stud HDA, get_stud_points writes a bgeo per part, and save_stud_postion_to_json is a Python Script node. On the right, generate_mesh runs the ldraw_part_mesh HDA, import_mesh imports the geometry, and save_part_to_fbx writes SM_ToolBrick_ and the part number as an FBX
Studs on the left, meshes on the right.

Three unit systems

Before any of the stud work could line up, LDraw, Houdini and Unreal had to agree on size. LEGO’s grid isn’t uniform. A 1 x 1 brick is taller than it is wide, and a lot of building happens in plates, which are a third of a brick high (Shenoy, 2022).

Diagram of a red 1 x 2 plate and a yellow 1 x 1 brick with their measurements: a stud pitch of 0.8cm, a plate 0.32cm high (1 plate), and a brick 0.8cm wide and 0.96cm high (3 plates)
Brick and plate sizes (Shenoy, 2022).

I scaled everything up by 10 into Unreal units (millimetres to centimetres), then by 1.25 so it sits on Unreal’s own grid values, for a scale factor of 12.5 overall. A 1 x 1 brick ends up 10cm x 10cm x 12cm and a plate 4cm, so the snapping grid in Unreal is 10 x 10 x 4.

In Houdini the part still comes in at real size, where a plate is 3.2 units tall. That’s the number in the code below. The last node in the stud branch scales the points and the mesh by 1.25.

Finding the top studs

Finding the top studs is the simpler half. LDraw uses the same primitive for a top stud every time, and LDraw2Houdini keeps those positions in its cached version of a part before it adds the stud geometry, so I hijack that cache. The stud branch keeps the points marked as studs, averages each cluster into one point per stud and types it as SolidTopStud or HollowTopStud.

The first top stud then becomes the part’s origin. The geometry is moved so that stud sits at 0,0, and every part lines up with the grid the same way in the engine.

Finding the anti-studs

Anti-studs, where another part’s studs fit underneath, are harder, because they have to be found from the shape of the part itself.

My plan in October was to fire a ray up into the part from a random point underneath, then four rays sideways to check it was enclosed, counting three hits out of four as a valid position. The version I released keeps that idea but changes the details:

  1. A grid under the part. Points one brick apart, covering its bounds, with the first point at 0,0 so it lines up with the first top stud.
  2. Raycast up. Any point whose ray misses the part is deleted.
  3. Climb in plates. From the bottom of the part, step up one plate height at a time and fire eight rays outward. If six of the eight hit, a stud could sit there, so it’s a valid anti-stud.
  4. Clean up. A point that reaches the top without passing is deleted.

Step 3 is where most of the work happens. This is the wrangle as I released it:

// 8 directions: 4 cardinal + 4 diagonals
vector dirs[] = {
    { 1, 0,  0},   // +X
    {-1, 0,  0},   // -X
    { 0, 0,  1},   // +Z
    { 0, 0, -1},   // -Z
    { 1, 0,  1},   // +X+Z
    { 1, 0, -1},   // +X-Z
    {-1, 0, -1},   // -X-Z
    {-1, 0,  1}    // -X+Z
};
float plate = 3.2; // Snap to plate heights

// Start and stop positions for iteraation snapped
float minY = floor(@P.y / plate) * plate; ; 
float maxY = floor(detail(0, "hit_y", 0) / plate) * plate;
float testY = minY; // Iteration height
float foundY = -1e9; // If valid set to height

// Ray cast config
int required = 6; // x/8 amount of rays hit to count as valid space
float dist = 100.0; // Max check distance in directions

// Loop upwards in plate increments until we reach the hit height
for (; testY <= maxY + 1e-4; testY += plate) {
    int hitCount = 0;
    
    // Cast 8 dir check - ray is slight higher to stop grazing angle
    foreach (vector D; dirs) {
        vector origin = set(@P.x, testY + 0.1, @P.z);
        vector hitP;
        float u, v;

        int prim = intersect(1, origin, D * dist, hitP, u, v);
        // Hit mesh
        if (prim >= 0)
            hitCount++;}

    // If reaches theshold for valid snap, store the height
    if (hitCount >= required) {
        foundY = ceil(testY / plate) * plate; // Snap down
        break;}
}

// If it wasnt valid at any iteration, remove
if (foundY < -1e8){
    removepoint(0, @ptnum);
    return;}

// Set to anti stud height
@P.y = foundY;

s@type = "AntiStud";

Six out of eight is the same ratio as my original three out of four. The climb is the real change, since it means a part with a recess partway up still gets its anti-studs at the right height.

Besides the 2 x 4 brick, my other test mesh was part 58846, a large corner round brick with a cutout.

A quarter round LEGO part seen from underneath in Houdini, dark red, covered in blue rings where the underside tubes are, with bright green points marking positions between the rings across the whole curved shape
Anti-studs in green.
The SOP network inside the stud branch in Houdini. The incoming part splits three ways: a grid branch of transforms and point wrangles ending in an anti_studs null, a bounds branch, and a top stud branch ending in a blast node with the comment This will be the object orgin and also decide the grid offset. They merge, transfer attributes and go to the output
Inside the stud branch.

Writing it out for Unreal

Once both sets of points are merged, a Python Script TOP writes a JSON file per part with each stud’s type and its offset from the origin, swapping Y and Z on the way because Houdini is Y-up and Unreal is Z-up. Those files are merged back into the main part list, which the tool reads to spawn its snap handles.

This is the 2 x 4 brick in the released list, cut down to one top stud and one anti-stud. The top studs sit 10 apart and the anti-studs 12 below them, which is one stud and one brick height in Unreal units:

{
  "PartID": "3001",
  "Name": "Brick  2 x  4",
  "Keywords": "brick;2;x;4",
  "Studs": [
    {
      "ID": 1,
      "Type": "SolidTopStud",
      "PosOffset": { "X": -10, "Y": 0, "Z": 0 },
      "RotOffset": { "Pitch": 0, "Yaw": 0, "Roll": 0 }
    },
    {
      "ID": 8,
      "Type": "AntiStud",
      "PosOffset": { "X": -30, "Y": 0, "Z": -12 },
      "RotOffset": { "Pitch": 0, "Yaw": 0, "Roll": 0 }
    }
  ]
}

Reflections

  • Parts without top studs - Everything hangs off the first top stud, so a part without one (tiles, and a lot of curved and decorative pieces) has no origin on the grid, and the rays often miss its anti-studs too. That’s roughly a third of the library. Those parts sit off the grid, and moving them around means turning grid snapping off.
  • False positives - The raycast test can pass positions that aren’t really anti-studs. The fix I’d try is checking whether the stud geometry actually fits closely against the walls it found.
A curved green part in the Houdini viewport with red points inside it along its length, and a hand drawn red arrow pointing right to a line of small black points floating in empty space away from the part
Stray points on a curved part.
  • No side studs - The rotation offset is always zero. I’d like to add side studs, along with the more unusual stud formats.
  • Baking the whole library up front - Adding one more part means cooking the whole lot again. With Houdini Engine inside the editor, an artist could request a part and have it generated without leaving Unreal.

Sources

  • Müller, S. (2023). ldraw2houdini: Import LDraw Files Directly into Houdini. [online] GitHub. Available at: https://github.com/stefanmuller/ldraw2houdini [Accessed 23 Nov. 2025].

    Generates the part meshes, and its cache is where the top studs come from.

  • Shenoy, D. (2022). Isn't That Stud Supposed to Be on Top? No, It's SNOT - Building Sideways Using LEGO. [online] Brick Builder's Handbook. Available at: https://brickbuildershandbook.com/2022/01/20/ [Accessed 23 Nov. 2025].

    The brick and plate dimensions.