Overview
A level-generator HDA built in Houdini, together with a set of modular environment pieces. Editing curves in Unreal Engine generates a curve-based layout; the curves control both the overall structure and the details, such as wall and floor placement and corner distribution. This speeds up art iteration and level assembly.
Modular assets
The level is assembled from walls, corners, floors, pillars, doors, ceilings and crate props. Walls come in 4 m and 2 m sizes; floors include standard, pipe and corner variants.
Layout and HDA parameters
In Unreal Engine the HDA exposes cell size, ceiling, wall type and random seed, corner type and angle, and flip direction. The layout updates as soon as the curve or a parameter changes.
Curve input and wall placement
A surface is built from the curve and its point positions are extracted. VEX filters the original points and adds the ones needed as wall anchors, while vertex colors mark convex and concave areas that become spawn points for different corner types.
Walls and corners
The wall script uses each point’s normal, lateral offset and random seed to choose the wall type (4 m or 2 m) and position, with an option to flip wall direction. The corner script reads the color written by measure_curvature to tell convex (green) from concave (red) areas, spawns the matching corner type and adjusts normals with rotation matrices.
Floors
Floors distinguish wall junctions, corners and open areas. Extra points between opposite walls are removed first; points near a wall become standard floor and the rest become pipe floor. Corner points are then taken from the wall corners, cleaned up and set to corner floor.
Prop distribution
Four crate Blueprints were made in Unreal Engine, with meshes that are themselves Houdini procedural assets. VEX takes the existing corner points, removes some by threshold, assigns crate types with random values that can be scaled by an external multiplier, and offsets points along their normals for a sparse, varied distribution.
Pillars and ceiling
The pillar script filters points_at_bottom, removes even-indexed points and offsets the rest by parameter so pillars are spaced regularly between walls. The ceiling reads the user-defined height, offsets each point downward with a uniform downward normal, and instances a mesh on each point.
Doors
A bounding box limits the distance between two base curves, and door positions are picked from points on nearby opposite curves. Resizing the box increases or decreases the number of doors; a script then instances door meshes on the selected points.
Procedural asset HDAs
Pillars, wall panels and wall components are also HDAs, with parameters for size, panel depth, pipes, column caps and detail cuts, used to produce module variants quickly.
Appendix: VEX source · 22 snippets
Wall_Conver_Instance
Reads red and green markers to distinguish concave and convex corners, assigns instance assets, rotated normals and groups, then removes points above the height threshold and original primitives.
string corner_convexity = chs('corner_convexity');
string corner_concavity = chs('corner_concavity');
string instance = detailattrib(0, "instance_type", 0, 1);
// Initialize variables
vector color = {0, 0, 0}, normal = {0, 0, 0}, pos = {0, 0, 0}, axis = {0, 1, 0};
matrix3 rot_convexity = ident(), rot_concavity = ident();
float angle_convexity = radians(chf('angle_convexity'));
float angle_concavity = radians(chf('angle_concavity'));
// Apply rotations to generate rotation matrices
rotate(rot_concavity, angle_concavity, axis);
rotate(rot_convexity, angle_convexity, axis);
// Flip normal direction based on input
int flip = chi('flip_direction');
// Iterate through all points
for (int i = 0; i < npoints(0); i++) {
// Remove previous attributes
removepointattrib(0, 'convexity');
removepointattrib(0, 'concavity');
color = point(0, 'Cd', i); // Get color attribute
normal = point(0, 'N', i); // Get normal attribute
// Flip normal if necessary
if (flip == 1) {
normal *= -1;
setpointattrib(0, 'N', i, normal, "set");
}
// Handle concavity case (Red color)
if (color.r == 1) {
setpointattrib(0, 'concavity', i, 1, "set");
setpointattrib(0, instance, i, corner_concavity, 'set');
setpointattrib(0, 'N', i, normal * rot_concavity, "set"); // Set rotated normal
setpointgroup(0, 'concavity', i, 1, "set");
}
// Handle convexity case (Green color)
if (color.g == 1) {
setpointattrib(0, 'convexity', i, 1, "set");
setpointattrib(0, instance, i, corner_convexity, 'set');
setpointattrib(0, 'N', i, -normal * rot_convexity, "set"); // Set rotated normal
setpointgroup(0, 'convexity', i, 1, "set");
}
pos = point(0, 'P', i); // Get position attribute
// Remove points with Y > 0.1
if (pos.y > 0.1) removepoint(0, i);
// Remove color attribute
}
// Remove all primitives
for (int i = 0; i < nprimitives(0); i++)
removeprim(0, i, 0);
// Remove instance type attribute
removedetailattrib(0, 'instance_type');
Walltype_instance
int points[] = {};
string large_wall1 = chs('large_wall_type1');
string large_wall2 = chs('large_wall_type2');
string small_wall1 = chs('small_wall_type1');
string small_wall2 = chs('small_wall_type2');
string instance_type = detailattrib(0, 'instance_type', 0, 1);
float type_seed = 0.f; // 初始化随机种子
float small_wall_seed = chf('small_wall_random_seed');
float large_wall_seed = chf('large_wall_random_seed');
// 初始化所有点的 instance_type 属性为 small_wall
for (int i = 0; i < npoints(0); i++) {
setpointattrib(0, instance_type, i, small_wall1, "set");
}
vector pos = {0, 0, 0};
vector side = {0, 0, 0};
vector normal = (0, 0, 0);
float dist = chf('offset');
int flip = chi('flip_wall');
int pt_index = 0;
string small_top = chs('small_wall_top');
string large_top = chs('large_wall_top');
// 遍历每个原始(primitive),调整点的位置
for (int i = 0; i < nprimitives(0); i++) {
points = primpoints(0, i); // 获取当前原始的所有点
for (int n = 0; n < len(points); n++)
{
normal = normalize(point(0, 'N', points[n]));
side = point(0, 'side', points[n]); // 获取点的侧向向量
pos = point(0, 'P', points[n]); // 获取点的位置
type_seed = rand(points[n] + 12345); // 生成伪随机数种子
if (n == 0) { // 仅对索引为 0 的点分配墙类型
if (type_seed <= small_wall_seed) {
setpointattrib(0, instance_type, points[0], small_wall1, "set");
} else {
setpointattrib(0, instance_type, points[0], small_wall2, "set");
}
}
if (n > 0) { // 对索引大于 0 的点进行偏移
type_seed = rand(type_seed * 32145);
pos += side * dist; // 按侧向向量和偏移量更新位置
setpointattrib(0, 'P', points[n], pos, "set"); // 更新点的位置
// 根据伪随机种子分配 large_wall 类型
if (type_seed <= large_wall_seed) {
setpointattrib(0, instance_type, points[n], large_wall1, "set");
} else {
setpointattrib(0, instance_type, points[n], large_wall2, "set");
}
}
removepointattrib(0, 'side');
if(flip == 1) {
normal *= -1;
setpointattrib(0, 'N', points[n], normal, "set");
}
if (n % 2 != 0) {
removepoint(0, points[n]);
}
}
}
// 移除不必要的属性
removepointattrib(0, 'up');
removedetailattrib(0, 'instance_type');attribwrangle1
removeprim(0, @primnum, 0);
removepointgroup(0, 'concavity');
removepointgroup(0, 'convexity');
removepointattrib(0, 'Cd');attribwrangle2
string floor2x1 = chs("floor_2x1");
s@unreal_instance = floor2x1;
attribwrangle3
// 获取 door_mesh_instance 路径和 instance_type 属性
string door = chs("door_mesh_instance");
string instance = detailattrib(0, "instance_type", 0, 1);
// 遍历几何体的所有点
for (int i = 0; i < npoints(0); i++)
{
// 如果点不在点组 'door' 中,删除点
if (inpointgroup(0, "door", i) == 0)
removepoint(0, i);
else
// 如果点在点组 'door' 中,设置 unreal_instance 属性为 door 的值
setpointattrib(0, instance, i, door, "set");
removedetailattrib(0, 'instance_type');
removepointgroup(0, 'door');
}
// 遍历几何体的所有原语并删除
for (int i = 0; i < nprimitives(0); i++)
{
removeprim(0, i, 0);
removedetailattrib(0, 'restlength');
}
detailwrangle1
int pts[] = {}; // 初始化点数组
int pindex = 0;
float offset_dist = chf('pillar_position_offset'); // 获取柱子位置偏移量
float offset = chf('offset_amount'); // 获取偏移量
vector side = {0, 0, 0}, normal = {0, 0, 0}, pos = {0, 0, 0};
string instance_mesh = chs('pillar_mesh_path'); // 获取实例网格路径
string instance_type = detailattrib(0, 'instance_type', 0, 1); // 获取实例类型
// 遍历每个点,给它们赋值实例类型
for (int i = 0; i < npoints(0); i++) {
setpointattrib(0, instance_type, i, instance_mesh, "set");
}
// 遍历每个原始(primitive)
for (int i = 0; i < nprimitives(0); i++) {
pts = primpoints(0, i); // 获取当前原始的所有点
// 遍历原始中的每个点
for (int p = 0; p < len(pts); p++) {
if (p % 2 != 0) { // 删除偶数索引的点
removepoint(0, pts[p]);
} else {
pos = point(0, 'P', pts[p]); // 获取点的位置
side = point(0, 'side', pts[p]); // 获取点的侧向向量
normal = point(0, 'N', pts[p]); // 获取点的法向量
pos += normal * offset_dist; // 更新位置
pos -= side * offset; // 计算新位置
setpointattrib(0, 'P', pts[p], pos, "set"); // 更新点的位置
// 如果是第一个点,生成新的点并设置其属性
if (p == 0) {
pindex = addpoint(0, pos); // 添加新点
pos += side * offset * 2; // 移动新点位置
setpointattrib(0, "side", pindex, side, "set"); // 设置侧向向量
setpointattrib(0, 'N', pindex, normal, "set"); // 设置法向量
setpointattrib(0, 'P', pindex, pos, "set"); // 设置新点位置
setpointattrib(0, instance_type, pindex, instance_mesh, "set"); // 设置实例类型
}
}
}
// 移除不需要的原始
removeprim(0, i, 0);
}
// 移除不必要的属性
removepointattrib(0, 'side');
removedetailattrib(0, 'instance_type');
detailwrangle10
// 获取用户输入的 seed 参数(当前代码未使用)
float seed = chf('seed');
// 遍历几何体 0 的所有点
for (int i = 0; i < npoints(0); i++)
{
// 如果当前点不在 'convexity' 点组中,删除该点
if (inpointgroup(0, 'convexity', i) == 0)
{
removepoint(0, i);
}
}
detailwrangle2
// 获取用户参数
float height = chf('ceiling_height'); // 获取天花板的高度
vector normal = {0, -1, 0}; // 初始化法线方向向下
int instance = chi('unity'); // 获取实例类型,0为Unreal,1为Unity
string ceiling_mesh = chs('ceiling_mesh'); // 获取天花板网格路径
// 遍历每个点,设置实例类型和调整位置
for (int i = 0; i < npoints(0); i++) {
// 获取当前点的位置
vector pos = point(0, 'P', i);
// 向下偏移天花板高度
pos += -normal * height;
// 设置点的法线为统一方向
setpointattrib(0, 'N', i, normal, "set");
// 更新点的位置
setpointattrib(0, 'P', i, pos, "set");
// 根据实例类型设置不同的实例属性
if (instance == 0) {
setpointattrib(0, 'unreal_instance', i, ceiling_mesh, "set");
} else {
setpointattrib(0, 'unity_instance', i, ceiling_mesh, "set");
}
}
// 删除所有原始(primitives),采用反向循环以避免索引问题
int num_prims = nprimitives(0);
for (int i = num_prims - 1; i >= 0; i--) {
removeprim(0, i, 0); // 移除每个原始
}
detailwrangle3
string instance = detailattrib(0, 'instance_type', 0, 1);
string floor = chs('floor_mesh_2x2');
string floorAtSide = chs('floor_mesh_2x1');
// 遍历几何体 0 的所有点,设置点属性
for (int i = 0; i < npoints(0); i++)
{
setpointattrib(0, instance, i, floorAtSide, "set");
}
int pindex = 0;
int pts[] = {};
vector pos = {0, 0, 0};
// 遍历几何体 1 的所有点
for (int i = 0; i < npoints(1); i++)
{
pos = point(1, 'P', i); // 获取点的位置
pts = nearpoints(0, pos, 0.01); // 查找附近的点
// 如果没有找到附近的点,则新增点
if (len(pts) == 0)
{
pindex = addpoint(0, pos); // 新增点
setpointattrib(0, 'N', pindex, {0, 1, 0}, "set"); // 设置法线属性
setpointattrib(0, instance, pindex, floor, "set"); // 设置实例属性
}
}
detailwrangle4
int pts[] = {};
int type = 0;
vector pos = {0,0,0}, normal = {0,0,0};
string instance = detailattrib(0, "instance_type", 0, 1);
string floor_concavity = chs("floor_concavity");
string floor_convexity = chs("floor_convexity");
for (int i = 0; i < npoints(1); i++)
{
pos = point(1, 'P', i);
normal = point(1, 'N', i);
type = point(1, 'concavity', i);
pts = nearpoints(0, pos, 0.01, 1);
if (len(pts) > 0)
{
if (type == 1)
{
setpointattrib(0, instance, pts[0], floor_concavity, "set");
}
else
{
setpointattrib(0, instance, pts[0], floor_convexity, "set");
setpointattrib(0, 'N', pts[0], normal, "set");
}
}
}
removedetailattrib(0, 'instance_type');
removepointattrib(0, 'up');
removepointattrib(0, 'side');detailwrangle5
for (int i = 0; i < npoints(0); i++)
{
if (!inpointgroup(0, 'concavity', i)==0 && !inpointgroup(0, 'convexity', i)==0)
removepoint(0, i);
}
detailwrangle6
string floor_instance2x2 = chs("floor_2x2");
int pts[] = nearpoints(0, @P, 0.1);
if (len(pts) > 0)
{
removepoint(0, @ptnum);
}
s@unreal_instance = floor_instance2x2;
for (int i = 0; i < nprimitives(0); i++)
{
removeprim(0, i, 0);
}
detailwrangle7
string floor_instance2x2 = chs("floor_2x2");
int pts[] = nearpoints(0, @P, 0.1);
if (len(pts) > 0)
{
removepoint(0, @ptnum);
}
s@unreal_instance = floor_instance2x2;
for (int i = 0; i < nprimitives(0); i++)
{
removeprim(0, i, 0);
}
detailwrangle8
for (int i = 0; i < nprimitives(0); i++)
{
removeprim(0, i, 0);
}
removepointattrib(0, 'Cd');
string floor_concavity = chs("floor_concavity");
string instance = detailattrib(0, "instance_type", 0, 1);
// 遍历几何体 0 的所有点
for (int i = 0; i < npoints(0); i++)
{
// 设置每个点的 unreal_instance 属性为 floor_concavity
setpointattrib(0, "unreal_instance", i, floor_concavity, "set");
}detailwrangle9
for (int i = 0; i < nprimitives(0); i++)
{
removeprim(0, i, 0);
}
removepointattrib(0, 'Cd');
string floor_convexity = chs("floor_convexity");
string instance = detailattrib(0, "instance_type", 0, 1);
// 遍历几何体 0 的所有点
for (int i = 0; i < npoints(0); i++)
{
// 设置每个点的 unreal_instance 属性为 floor_convexity
setpointattrib(0, "unreal_instance", i, floor_convexity, "set");
}
instance_type
int unity = chi('unity'); // 获取一个整数参数 'unity' 的值。
// 该值通过用户界面或节点设置传入,决定逻辑分支。
string instance = 'instance_type'; // 定义一个字符串变量,用作属性名称。
// 条件判断:如果 unity 参数为 0,则设置 instance 属性为 'unreal_instance'。
// 否则,将 instance 属性设置为 'unity_instance'。
if (unity == 0)
setdetailattrib(0, instance, 'unreal_instance', 'set');
else
setdetailattrib(0, instance, 'unity_instance', 'set');
pointwrangle1
if(s@unreal_instance=="") removepoint(0,@ptnum);pointwrangle2
// 遍历几何体中的所有点
for (int i = npoints(0) - 1; i >= 0; i--) // 从后向前遍历以避免索引混乱
{
vector color = point(0, 'Cd', i); // 获取点的颜色属性
// 如果颜色的 R 属性不等于 1,则删除点
if (color.r != 1)
{
removepoint(0, i); // 删除当前点
}
}
pointwrangle3
// 遍历几何体中的所有点
for (int i = npoints(0) - 1; i >= 0; i--) // 从后向前遍历以避免索引混乱
{
vector color = point(0, 'Cd', i); // 获取点的颜色属性
// 如果颜色的 R 属性不等于 1,则删除点
if (color.g != 1)
{
removepoint(0, i); // 删除当前点
}
}
remove_pts_at_top
int bot_pts[] = {}; // 用于存储未被删除的点索引
int nearpt[] = {}; // 用于存储附近点的索引
vector pos = {0, 0, 0}; // 存储点的位置
// 遍历所有点
for (int i = 0; i < npoints(0); i++)
{
pos = point(0, 'P', i); // 获取当前点的位置
nearpt = nearpoints(0, pos, 0.1, 2); // 获取距离当前点0.1以内的最多2个点的索引
// 检查当前点是否需要删除
if (pos.y > 0.1 || len(nearpt) >= 2) // 如果点的 y 坐标大于0.1 或者附近点的数量 >= 2
removepoint(0, i, 0); // 删除当前点
else
append(bot_pts, i); // 否则将点索引加入 bot_pts 数组
}
// 初始化变量
int pts[] = {}; // 存储当前原始 (Primitive) 的点索引
vector up = {0, 1, 0}; // 上向量,用于计算法向量
vector side = {0, 0, 0}, normal = {0, 0, 0}; // 用于存储侧向向量和法向量
vector endpos1 = {0, 0, 0}, endpos2 = {0, 0, 0}; // 当前原始的两个端点位置
// 遍历几何体中的所有原始(Primitive)
for (int i = 0; i < nprimitives(0); i++)
{
pts = primpoints(0, i); // 获取当前原始的所有点索引
endpos1 = point(0, 'P', pts[0]); // 获取第一个点的位置
endpos2 = point(0, 'P', pts[1]); // 获取第二个点的位置
side = normalize(endpos1 - endpos2); // 计算端点之间的方向向量并归一化
normal = normalize(cross(up, side)); // 通过叉乘计算法向量并归一化
// 遍历当前原始中的所有点,设置属性
foreach (int p; pts)
{
setpointattrib(0, 'side', p, side, 'set'); // 为点设置侧向向量属性
setpointattrib(0, 'N', p, normal, 'set'); // 为点设置法向量属性
setpointattrib(0, 'up', p, up, 'set'); // 为点设置上向量属性
}
}
pointwrangle1
int pts[] = nearpoints(1, @P, 0.01, 5);
if(len(pts) == 0) removepoint(0, @ptnum);primitivewrangle1
if(@restlength > chi('rel_length')) removeprim(0, @primnum, 1);
Demonstration video

Technical content adapted from the author’s portfolio and project files.