项目概述

在 Houdini 中开发的参数化程序化箱体 HDA。调整预设参数即可生成不同尺寸、装饰和配件的箱子,资产通过 PDG 工作流批量导出并附带相应贴图。在 Unreal Engine 中用这些贴图制作程序化材质,结合顶点颜色,可以批量生产不同材质和尺寸的三维资产,而无需为每个模型单独绘制 UV 贴图。

场景渲染。

HDA 参数

箱身、箱盖、底座、金属框架、把手与灯带分别由参数控制,可组合出多种箱体。

不同参数组合生成的箱体。

节点结构

整体网络按部件分组:底座、箱盖、顶部金属、把手、灯和箱体各自独立搭建,最后合并输出。

Houdini 节点总览。
底座、箱盖、顶部金属与把手的节点与结果。
灯与箱体的节点与结果。

通过 VEX 优化工作流

部分功能如果用 SOP 实现会比较繁琐或消耗较大,因此改用 VEX 编写了三个 HDA 工具,分别用于点筛选、平面上的点复制和选定面的移动。

三个 VEX 工具的节点与用法。

Plane Modifier

检测几何体的包围盒并识别最低点,按用户参数沿 X、Y、Z 轴调整点的位置。脚本根据点法线判断哪些点可以移动,并对最低组中的点单独处理,移动距离、方向和范围都可由参数控制。

Plane Modifier 节点与脚本。

Filter Points with Bounding Box

用包围盒过滤点云并分组。脚本读取包围盒尺寸和用户参数,遍历点并按条件筛选:可直接标记符合条件的点,也可统计出现次数高于或低于阈值的点,并支持反选,结果写入分组供后续使用。

Filter Points with Bounding Box 节点与脚本。

Make Copy Points from Prim

根据给定面生成复制点。先计算面的起止中点,再根据分段长度和生成样式确定点数并生成点,可选择连成折线;支持按方向和偏移量调整点位,包括反向移动。

Make Copy Points from Prim 节点与脚本。

PDG 资产管理与批处理

部件组装完成后,按后期材质计划为每个部件分配材质名称,用布尔运算合并命名模块并整理组结构。为导出不同参数的模型,复制多个箱体 HDA 并分别设置参数,再用 Python 脚本批量导出。之后按材质属性为部件分配顶点颜色,按面数要求生成高模并简化,同时传递 UV;最终导出模型、Mask、Position、ID 和 Object Normal 贴图。

批量导出与 Python 脚本。
顶点颜色分类与贴图导出。

Unreal Engine 程序化材质

Houdini 导出的贴图以虚拟纹理导入 UE5,并作为模型的顶点颜色使用。不同的程序化材质与顶点颜色结合,由顶点颜色区分模型的不同区域,在各个位置应用不同材质,而无需为每个模型单独分配 UV 贴图。

Mask 虚拟纹理、父材质与子材质函数。
附:VEX 源码 · 16 段
set_pts_pos

使用位置、法线与 lowest 点组区分底部和上部,将界面中的位移与缩放参数应用到切割平面点位。

set_pts_pos.parmVEX
// 定义变量
int mx = chi("mv_x");
float x_dist = chf("x_mv_dist");
int my = chi("mv_y");
float y_dist = chf("y_mv_dist");
int mz = chi("mv_z");
float z_dist = chf("z_mv_dist");

int mt = chi("mv_y_max");
float mt_dist = chf("mt_amount");
int mb = chi("mv_y_min");
float mb_dist = chf("mb_amount");

int st = chi("sc_y_max");
float st_dist = chf("sc_y_max_amount");
int sb = chi("sc_y_min");
float sb_dist = chf("sc_y_min_amount");

// 定义 SetValue 函数
float SetValue(float component, dist)
{
    float value = component;
    if (value > 0) 
        value += dist;
    else 
        value -= dist;
    return value;
}

// 定义方向向量
vector x = set(1, 0, 0);
vector y = set(0, 1, 0);
vector z = set(0, 0, 1);

// 获取当前点是否属于 'lowest' 组
int isLowest = i@group_lowest;

if (mx) 
    @P.x = SetValue(@P.x, x_dist);

if (mz) 
    @P.z = SetValue(@P.z, z_dist);

if (my)
{
    if (dot(@N, y) > 0.99 || dot(@N, -y) > 0.99)
    {
        // 如果法线接近 y 或 -y 方向,直接沿法线方向移动
        @P += @N * y_dist;
    }
    else
    {
        // 根据是否属于 'lowest' 组来决定 y 方向的移动
        if (isLowest) 
            @P.y = SetValue(@P.y, -y_dist);
        else 
            @P.y = SetValue(@P.y, y_dist);
    }
}

if (mt && (dot(@N, y) < 0.99 && dot(@N, -y) < 0.99))
{
    if (!isLowest)
        @P.y += mt_dist;
}

if (mb && (dot(@N, y) < 0.99 && dot(@N, -y) < 0.99))
{
    if (isLowest)
        @P.y -= mb_dist;
}

// 处理 X 轴缩放
if (st && (dot(@N, x) > 0.99 || dot(@N, -x) > 0.99))
{
    if (!isLowest) 
    {
        if (@P.z > 0)
            @P.z += st_dist;
        else 
            @P.z -= st_dist;
    }
}

if (sb && (dot(@N, x) > 0.99 || dot(@N, -x) > 0.99))
{
    if (isLowest)
    {
        if (@P.z > 0)
            @P.z += sb_dist;
        else 
            @P.z -= sb_dist;
    }
}

// 处理 Z 轴缩放
if (st && (dot(@N, z) > 0.99 || dot(@N, -z) > 0.99))
{
    if (!isLowest) 
    {
        if (@P.x > 0)
            @P.x += st_dist;
        else 
            @P.x -= st_dist;
    }
}

if (sb && (dot(@N, z) > 0.99 || dot(@N, -z) > 0.99))
{
    if (isLowest)
    {
        if (@P.x > 0)
            @P.x += sb_dist;
        else 
            @P.x -= sb_dist;
    }
}
set_pts_pos
set_pts_pos.parmVEX
// 定义变量
int mx = chi("mv_x");
float x_dist = chf("x_mv_dist");
int my = chi("mv_y");
float y_dist = chf("y_mv_dist");
int mz = chi("mv_z");
float z_dist = chf("z_mv_dist");

int mt = chi("mv_y_max");
float mt_dist = chf("mt_amount");
int mb = chi("mv_y_min");
float mb_dist = chf("mb_amount");

int st = chi("sc_y_max");
float st_dist = chf("sc_y_max_amount");
int sb = chi("sc_y_min");
float sb_dist = chf("sc_y_min_amount");

// 定义 SetValue 函数
float SetValue(float component, dist)
{
    float value = component;
    if (value > 0) 
        value += dist;
    else 
        value -= dist;
    return value;
}

// 定义方向向量
vector x = set(1, 0, 0);
vector y = set(0, 1, 0);
vector z = set(0, 0, 1);

// 获取当前点是否属于 'lowest' 组
int isLowest = i@group_lowest;

if (mx) 
    @P.x = SetValue(@P.x, x_dist);

if (mz) 
    @P.z = SetValue(@P.z, z_dist);

if (my)
{
    if (dot(@N, y) > 0.99 || dot(@N, -y) > 0.99)
    {
        // 如果法线接近 y 或 -y 方向,直接沿法线方向移动
        @P += @N * y_dist;
    }
    else
    {
        // 根据是否属于 'lowest' 组来决定 y 方向的移动
        if (isLowest) 
            @P.y = SetValue(@P.y, -y_dist);
        else 
            @P.y = SetValue(@P.y, y_dist);
    }
}

if (mt && (dot(@N, y) < 0.99 && dot(@N, -y) < 0.99))
{
    if (!isLowest)
        @P.y += mt_dist;
}

if (mb && (dot(@N, y) < 0.99 && dot(@N, -y) < 0.99))
{
    if (isLowest)
        @P.y -= mb_dist;
}

// 处理 X 轴缩放
if (st && (dot(@N, x) > 0.99 || dot(@N, -x) > 0.99))
{
    if (!isLowest) 
    {
        if (@P.z > 0)
            @P.z += st_dist;
        else 
            @P.z -= st_dist;
    }
}

if (sb && (dot(@N, x) > 0.99 || dot(@N, -x) > 0.99))
{
    if (isLowest)
    {
        if (@P.z > 0)
            @P.z += sb_dist;
        else 
            @P.z -= sb_dist;
    }
}

// 处理 Z 轴缩放
if (st && (dot(@N, z) > 0.99 || dot(@N, -z) > 0.99))
{
    if (!isLowest) 
    {
        if (@P.x > 0)
            @P.x += st_dist;
        else 
            @P.x -= st_dist;
    }
}

if (sb && (dot(@N, z) > 0.99 || dot(@N, -z) > 0.99))
{
    if (isLowest)
    {
        if (@P.x > 0)
            @P.x += sb_dist;
        else 
            @P.x -= sb_dist;
    }
}
set_pts_pos
set_pts_pos.parmVEX
// 定义变量
int mx = chi("mv_x");
float x_dist = chf("x_mv_dist");
int my = chi("mv_y");
float y_dist = chf("y_mv_dist");
int mz = chi("mv_z");
float z_dist = chf("z_mv_dist");

int mt = chi("mv_y_max");
float mt_dist = chf("mt_amount");
int mb = chi("mv_y_min");
float mb_dist = chf("mb_amount");

int st = chi("sc_y_max");
float st_dist = chf("sc_y_max_amount");
int sb = chi("sc_y_min");
float sb_dist = chf("sc_y_min_amount");

// 定义 SetValue 函数
float SetValue(float component, dist)
{
    float value = component;
    if (value > 0) 
        value += dist;
    else 
        value -= dist;
    return value;
}

// 定义方向向量
vector x = set(1, 0, 0);
vector y = set(0, 1, 0);
vector z = set(0, 0, 1);

// 获取当前点是否属于 'lowest' 组
int isLowest = i@group_lowest;

if (mx) 
    @P.x = SetValue(@P.x, x_dist);

if (mz) 
    @P.z = SetValue(@P.z, z_dist);

if (my)
{
    if (dot(@N, y) > 0.99 || dot(@N, -y) > 0.99)
    {
        // 如果法线接近 y 或 -y 方向,直接沿法线方向移动
        @P += @N * y_dist;
    }
    else
    {
        // 根据是否属于 'lowest' 组来决定 y 方向的移动
        if (isLowest) 
            @P.y = SetValue(@P.y, -y_dist);
        else 
            @P.y = SetValue(@P.y, y_dist);
    }
}

if (mt && (dot(@N, y) < 0.99 && dot(@N, -y) < 0.99))
{
    if (!isLowest)
        @P.y += mt_dist;
}

if (mb && (dot(@N, y) < 0.99 && dot(@N, -y) < 0.99))
{
    if (isLowest)
        @P.y -= mb_dist;
}

// 处理 X 轴缩放
if (st && (dot(@N, x) > 0.99 || dot(@N, -x) > 0.99))
{
    if (!isLowest) 
    {
        if (@P.z > 0)
            @P.z += st_dist;
        else 
            @P.z -= st_dist;
    }
}

if (sb && (dot(@N, x) > 0.99 || dot(@N, -x) > 0.99))
{
    if (isLowest)
    {
        if (@P.z > 0)
            @P.z += sb_dist;
        else 
            @P.z -= sb_dist;
    }
}

// 处理 Z 轴缩放
if (st && (dot(@N, z) > 0.99 || dot(@N, -z) > 0.99))
{
    if (!isLowest) 
    {
        if (@P.x > 0)
            @P.x += st_dist;
        else 
            @P.x -= st_dist;
    }
}

if (sb && (dot(@N, z) > 0.99 || dot(@N, -z) > 0.99))
{
    if (isLowest)
    {
        if (@P.x > 0)
            @P.x += sb_dist;
        else 
            @P.x -= sb_dist;
    }
}
attribwrangle2
attribwrangle2.parmVEX
int x_dir = chi("mv_x_direction");
int y_dir = chi("mv_y_direction");
int z_dir = chi("mv_z_direction");
int reverse = chi("reverse_normal_direction");
float amountX = chf("x_amount");
float amountY = chf("y_amount");
float amountZ = chf("z_amount");

vector x = {1, 0, 0};
vector y = {0, 1, 0};
vector z = {0, 0, 1};

if (reverse == 1)
{
    x *= -1;
    y *= -1;
    z *= -1;
}

if (x_dir) @P += x * amountX;
if (y_dir) @P += y * amountY;
if (z_dir) @P += z * amountZ;
copy_points
copy_points.parmVEX
// 获取几何体或节点1中的顶点位置并创建中间点
vector pos0 = point(1, "P", vertexpoint(1, 0));
vector pos1 = point(1, "P", vertexpoint(1, 1));
vector pos2 = point(1, "P", vertexpoint(1, 2));
vector pos3 = point(1, "P", vertexpoint(1, 3));

int dir = chi("change_direction");
vector p0, p1;

// 根据方向参数决定如何计算中间点
if (dir == 1)
{
    p0 = (pos0 + pos1) * 0.5;
    p1 = (pos2 + pos3) * 0.5;
}
else
{
    p0 = (pos1 + pos2) * 0.5;
    p1 = (pos0 + pos3) * 0.5;
}

// 起始点之间的距离
float pt_dist = distance(p0,p1);
//用户输入的线段resample长度
float usr_dif_len = chf("length");
//点的间隔距离
float segment_len =0;
// 点的总数
int total_pts=(int)ceil(pt_dist /usr_dif_len);
// 点移动的方向
vector line_dir =normalize(p1-p0);
segment_len=pt_dist/(total_pts-1);for(int i=0;i<total_pts; i++)
if(i !=0&& i<total_pts -1)
addpoint(0, p0+segment_len * i * line_dir);
attribwrangle1
attribwrangle1.parmVEX
@N = {0, 0, -1};
f@pscale = chf("size_scale");
attribwrangle2
attribwrangle2.parmVEX
removeprim(0, 0, 1);
@N = {1, 0, 0};
@pscale = chf("size_scale");
attribwrangle3
attribwrangle3.parmVEX
i@count = chi("brvel_Divisions");
if (@count > 0 && @count % 2 != 0)
   i@count += 1;
attribwrangle4
attribwrangle4.parmVEX
@Cd = 0;
@Cd.r = point(1, "convexity", @ptnum);
@Cd.g = point(1, "concavity", @ptnum);
@Cd.b = point(2, "mask", @ptnum);
set_lowest_group
set_lowest_group.parmVEX
// 获取当前几何体的最小包围盒坐标
vector bbmin = getbbox_min(0);

// 获取最低的 y 坐标
float lowest_y = bbmin.y;

// 创建一个数组来存储最低点的索引
int lowest[] = {};

// 遍历所有点
for (int i = 0; i < @numpt; i++) {
    // 获取点的位置
    vector pos = point(0, "P", i);
    
    // 检查点的 y 坐标是否等于最低 y 坐标
    if (pos.y == lowest_y) {
        // 将点的索引添加到数组中
        append(lowest, i);
    }
}

// 如果最低点数组的长度为2,则将这些点加入 "lowest" 组
if (len(lowest) == 2) {
    foreach (int pt; lowest) {
        setpointgroup(0, "lowest", pt, 1, "set");
    }
}
filter_points1
filter_points1.parmVEX
vector bbmax = getbbox_max(0);
vector bbmin = getbbox_min(0);

int methold = chi("filter_method");
int max_x = chi("x_max");
int max_y = chi("y_max");
int max_z = chi("z_max");
int min_x = chi("x_min");
int min_y = chi("y_min");
int min_z = chi("z_min");

int all_pts[] = {};

// 遍历所有顶点,根据条件收集符合要求的顶点编号
for (int i = 0; i < npoints(0); i++)
{
    vector pos = point(0, "P", i);

    if (max_x && pos.x == bbmax.x)
        append(all_pts, i);
    if (max_y && pos.y == bbmax.y)
        append(all_pts, i);
    if (max_z && pos.z == bbmax.z)
        append(all_pts, i);
    if (min_x && pos.x == bbmin.x)
        append(all_pts, i);
    if (min_y && pos.y == bbmin.y)
        append(all_pts, i);
    if (min_z && pos.z == bbmin.z)
        append(all_pts, i);
}

// 根据选择方法对顶点进行标记
if (methold == 1)
{
    // 标记出现次数大于等于2的顶点
    for (int i = 0; i < npoints(0); i++)
    {
        int count = 0;
        foreach (int pt; all_pts)
        {
            if (i == pt)
                count++;
        }
        if (count >= 2)
            setpointattrib(0, "selected", i, 1, "set");
    }
}
else if (methold == 0)
{
    // 直接标记所有收集到的顶点
    foreach (int pt; all_pts)
    {
        setpointattrib(0, "selected", pt, 1, "set");
    }
}
else if (methold == 2)
{
    // 标记出现次数小于2的顶点
    for (int i = 0; i < npoints(0); i++)
    {
        int count = 0;
        foreach (int pt; all_pts)
        {
            if (i == pt)
                count++;
        }
        if (count = 1)
            setpointattrib(0, "selected", i, 1, "set");
    }
}
filter_points
filter_points.parmVEX
vector bbmax = getbbox_max(0);
vector bbmin = getbbox_min(0);

int methold = chi("filter_method");
int reverse = chi("reserve_selection"); 
string groupName = chs("group_name"); 
int max_x = chi("x_max");
int max_y = chi("y_max");
int max_z = chi("z_max");
int min_x = chi("x_min");
int min_y = chi("y_min");
int min_z = chi("z_min");

int all_pts[] = {};
int makeGroup[] = {};



// 定义函数:根据类型决定是否添加顶点到数组
function void AddToArray(int Array[]; int pt; int type)
{
    if (type == 0 && find(Array, pt) < 0)
        append(Array, pt);
    else
        append(Array, pt);
}

// 遍历所有顶点,根据条件调用函数添加顶点编号到数组
for (int i = 0; i < npoints(0); i++)
{
    vector pos = point(0, "P", i);

    if (max_x && pos.x == bbmax.x)
        AddToArray(all_pts, i, methold);
    if (max_y && pos.y == bbmax.y)
        AddToArray(all_pts, i, methold);
    if (max_z && pos.z == bbmax.z)
        AddToArray(all_pts, i, methold);
    if (min_x && pos.x == bbmin.x)
        AddToArray(all_pts, i, methold);
    if (min_y && pos.y == bbmin.y)
        AddToArray(all_pts, i, methold);
    if (min_z && pos.z == bbmin.z)
        AddToArray(all_pts, i, methold);
}

// 根据选择方法对顶点进行标记
if (methold == 0)
{
    append(makeGroup, all_pts);
}
else if (methold == 1)
{
    // 标记出现次数大于等于2的顶点
    for (int i = 0; i < npoints(0); i++)
    {
        int count = 0;
        foreach (int pt; all_pts)
        {
            if (i == pt)
                count++;
        }
        if (count >= 2)
            append(makeGroup, i);
    }
}
else if (methold == 2)
{
    // 标记出现次数小于2的顶点
    for (int i = 0; i < npoints(0); i++)
    {
        int count = 0;
        foreach (int pt; all_pts)
        {
            if (i == pt)
                count++;
        }
        if (count < 2)
            append(makeGroup, i);
    }
}

// 根据 reverse 变量设置点组
if (!reverse)
{
    foreach (int p; makeGroup)
        setpointgroup(0, groupName, p, 1, "set");
}
else
{
    for (int p = 0; p < npoints(0); p++)
    {
        if (find(makeGroup, p) < 0)
            setpointgroup(0, groupName, p, 1, "set");
    }
}
set_lowest_group
set_lowest_group.parmVEX
// 获取当前几何体的最小包围盒坐标
vector bbmin = getbbox_min(0);

// 获取最低的 y 坐标
float lowest_y = bbmin.y;

// 创建一个数组来存储最低点的索引
int lowest[] = {};

// 遍历所有点
for (int i = 0; i < @numpt; i++) {
    // 获取点的位置
    vector pos = point(0, "P", i);
    
    // 检查点的 y 坐标是否等于最低 y 坐标
    if (pos.y == lowest_y) {
        // 将点的索引添加到数组中
        append(lowest, i);
    }
}

// 如果最低点数组的长度为2,则将这些点加入 "lowest" 组
if (len(lowest) == 2) {
    foreach (int pt; lowest) {
        setpointgroup(0, "lowest", pt, 1, "set");
    }
}
attribwrangle1
attribwrangle1.parmVEX
vector bbmax = getbbox_max(0);
if(@P.y == bbmax.y) i@group_topBevel = 1;
filter_points
filter_points.parmVEX
vector bbmax = getbbox_max(0);
vector bbmin = getbbox_min(0);

int methold = chi("filter_method");
int reverse = chi("reserve_selection"); 
string groupName = chs("group_name"); 
int max_x = chi("x_max");
int max_y = chi("y_max");
int max_z = chi("z_max");
int min_x = chi("x_min");
int min_y = chi("y_min");
int min_z = chi("z_min");

int all_pts[] = {};
int makeGroup[] = {};



// 定义函数:根据类型决定是否添加顶点到数组
function void AddToArray(int Array[]; int pt; int type)
{
    if (type == 0 && find(Array, pt) < 0)
        append(Array, pt);
    else
        append(Array, pt);
}

// 遍历所有顶点,根据条件调用函数添加顶点编号到数组
for (int i = 0; i < npoints(0); i++)
{
    vector pos = point(0, "P", i);

    if (max_x && pos.x == bbmax.x)
        AddToArray(all_pts, i, methold);
    if (max_y && pos.y == bbmax.y)
        AddToArray(all_pts, i, methold);
    if (max_z && pos.z == bbmax.z)
        AddToArray(all_pts, i, methold);
    if (min_x && pos.x == bbmin.x)
        AddToArray(all_pts, i, methold);
    if (min_y && pos.y == bbmin.y)
        AddToArray(all_pts, i, methold);
    if (min_z && pos.z == bbmin.z)
        AddToArray(all_pts, i, methold);
}

// 根据选择方法对顶点进行标记
if (methold == 0)
{
    append(makeGroup, all_pts);
}
else if (methold == 1)
{
    // 标记出现次数大于等于2的顶点
    for (int i = 0; i < npoints(0); i++)
    {
        int count = 0;
        foreach (int pt; all_pts)
        {
            if (i == pt)
                count++;
        }
        if (count >= 2)
            append(makeGroup, i);
    }
}
else if (methold == 2)
{
    // 标记出现次数小于2的顶点
    for (int i = 0; i < npoints(0); i++)
    {
        int count = 0;
        foreach (int pt; all_pts)
        {
            if (i == pt)
                count++;
        }
        if (count < 2)
            append(makeGroup, i);
    }
}

// 根据 reverse 变量设置点组
if (!reverse)
{
    foreach (int p; makeGroup)
        setpointgroup(0, groupName, p, 1, "set");
}
else
{
    for (int p = 0; p < npoints(0); p++)
    {
        if (find(makeGroup, p) < 0)
            setpointgroup(0, groupName, p, 1, "set");
    }
}
set_lowest_group
set_lowest_group.parmVEX
// 获取当前几何体的最小包围盒坐标
vector bbmin = getbbox_min(0);

// 获取最低的 y 坐标
float lowest_y = bbmin.y;

// 创建一个数组来存储最低点的索引
int lowest[] = {};

// 遍历所有点
for (int i = 0; i < @numpt; i++) {
    // 获取点的位置
    vector pos = point(0, "P", i);
    
    // 检查点的 y 坐标是否等于最低 y 坐标
    if (pos.y == lowest_y) {
        // 将点的索引添加到数组中
        append(lowest, i);
    }
}

// 如果最低点数组的长度为2,则将这些点加入 "lowest" 组
if (len(lowest) == 2) {
    foreach (int pt; lowest) {
        setpointgroup(0, "lowest", pt, 1, "set");
    }
}

展示视频

程序化作品合集

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

下一个项目Houdini × UE 道路生成