项目概述

使用 Houdini 开发的关卡生成器 HDA,并配套制作了一套模块化场景组件。在 Unreal Engine 中编辑曲线即可生成基于曲线的关卡布局,曲线控制关卡的整体结构和细节,包括墙体、地板的布置以及墙角的分布。这种方式加快了美术资产的迭代,也提高了关卡搭建效率。

场景渲染。

模块化资产

关卡由墙体、墙角、地板、柱子、门、天花板和箱体道具等模块组成,墙体分为 4 米与 2 米两种规格,地板分为普通地板、管道地板与角落地板。

用于组合场景的模块化资产。

关卡布局与 HDA 参数

HDA 在 Unreal Engine 中暴露单元尺寸、天花板、墙体类型与随机种子、墙角类型与角度、翻转方向等参数,调整曲线或参数后布局即时更新。

HDA 在 Unreal Engine 中生成的关卡布局与参数面板。
Houdini 节点网络总览。

曲线输入与墙体定位

从曲线创建平面并获取表面点的位置。基于原始点,用 VEX 筛选并补充必要的点作为墙体基础位置;同时利用顶点颜色识别表面上的凸起和凹陷区域,作为不同类型墙角的生成点,进一步优化墙体布局。

由曲线生成平面与墙体基础点。

墙面与墙角生成

墙面脚本根据每个点的法线方向、侧向偏移量和随机种子,确定墙体类型(4 米或 2 米)及位置,并支持翻转墙体方向以适应不同场景。墙角脚本读取 measure_curvature 节点写入的颜色属性,区分凸起(绿色)与凹陷(红色)区域,生成对应类型的墙角,并用旋转矩阵调整法线。

墙面与墙角脚本及对应节点。

地板生成

地板需要区分墙体交汇处、角落和其他区域。先去除对面墙之间的多余点,再检查点与墙体的关系:靠近墙体的点设为普通地板,其余设为管道地板;最后从墙角中获取角落点,去除多余点后设为角落地板。

普通地板、角落地板与管道地板的生成。

道具分布

在 Unreal Engine 中制作了四种箱子蓝图,模型本身也是 Houdini 程序化资产。VEX 读取已有墙角点,按阈值去除部分点,用随机数分配箱子类型,并可通过外部输入的倍数调整;点位可沿法线方向偏移,形成稀疏而多样的箱子分布。

道具生成脚本与结果。

柱子与天花板

柱子脚本筛选 points_at_bottom 中的点,删除偶数索引的点,并按外部参数调整剩余点的位置,使柱子规律地分布在墙体之间。天花板读取用户设置的高度,将每个点向下偏移,法线统一朝下,再为每个点实例化网格。

柱子与天花板脚本。

门的生成

用包围盒限制两条基准曲线之间的距离,从位置接近的对面曲线中筛选出门的位置。调整包围盒大小即可增减门的数量,最后用脚本将筛选出的点实例化为门模型。

门位置的筛选与实例化。

部分程序化资产

柱子、墙面和墙面组件同样由 HDA 生成,参数包括尺寸、面板深度、管道、柱帽与细节切割等,用于快速制作模块变体。

柱子、墙面与墙面组件 HDA。
附:VEX 源码 · 22 段
Wall_Conver_Instance

读取红、绿颜色标记来区分凹角与凸角,为对应点设置实例资源、法线旋转和分组;随后清理高度超限的点与原有面。

Wall_Conver_Instance.parmVEX
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
Walltype_instance.parmVEX
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
attribwrangle1.parmVEX
removeprim(0, @primnum, 0);
removepointgroup(0, 'concavity');
removepointgroup(0, 'convexity');
removepointattrib(0, 'Cd');
attribwrangle2
attribwrangle2.parmVEX
string floor2x1 = chs("floor_2x1");

s@unreal_instance = floor2x1;
attribwrangle3
attribwrangle3.parmVEX
// 获取 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
detailwrangle1.parmVEX
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
detailwrangle10.parmVEX
// 获取用户输入的 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
detailwrangle2.parmVEX
// 获取用户参数
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
detailwrangle3.parmVEX
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
detailwrangle4.parmVEX
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
detailwrangle5.parmVEX
for (int i = 0; i < npoints(0); i++)
{
    if (!inpointgroup(0, 'concavity', i)==0 && !inpointgroup(0, 'convexity', i)==0)
    
        removepoint(0, i);
    
}
detailwrangle6
detailwrangle6.parmVEX

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
detailwrangle7.parmVEX

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
detailwrangle8.parmVEX
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
detailwrangle9.parmVEX
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
instance_type.parmVEX
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
pointwrangle1.parmVEX

if(s@unreal_instance=="") removepoint(0,@ptnum);
pointwrangle2
pointwrangle2.parmVEX
// 遍历几何体中的所有点
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
pointwrangle3.parmVEX
// 遍历几何体中的所有点
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
remove_pts_at_top.parmVEX
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
pointwrangle1.parmVEX
int pts[] = nearpoints(1, @P, 0.01, 5);
if(len(pts) == 0) removepoint(0, @ptnum);
primitivewrangle1
primitivewrangle1.parmVEX
if(@restlength > chi('rel_length')) removeprim(0, @primnum, 1);

展示视频

程序化作品合集

技术内容整理自作者作品集及项目文件。

下一个项目AI 模型磨损纹理生成