项目概述
使用 Houdini 开发的关卡生成器 HDA,并配套制作了一套模块化场景组件。在 Unreal Engine 中编辑曲线即可生成基于曲线的关卡布局,曲线控制关卡的整体结构和细节,包括墙体、地板的布置以及墙角的分布。这种方式加快了美术资产的迭代,也提高了关卡搭建效率。
模块化资产
关卡由墙体、墙角、地板、柱子、门、天花板和箱体道具等模块组成,墙体分为 4 米与 2 米两种规格,地板分为普通地板、管道地板与角落地板。
关卡布局与 HDA 参数
HDA 在 Unreal Engine 中暴露单元尺寸、天花板、墙体类型与随机种子、墙角类型与角度、翻转方向等参数,调整曲线或参数后布局即时更新。
曲线输入与墙体定位
从曲线创建平面并获取表面点的位置。基于原始点,用 VEX 筛选并补充必要的点作为墙体基础位置;同时利用顶点颜色识别表面上的凸起和凹陷区域,作为不同类型墙角的生成点,进一步优化墙体布局。
墙面与墙角生成
墙面脚本根据每个点的法线方向、侧向偏移量和随机种子,确定墙体类型(4 米或 2 米)及位置,并支持翻转墙体方向以适应不同场景。墙角脚本读取 measure_curvature 节点写入的颜色属性,区分凸起(绿色)与凹陷(红色)区域,生成对应类型的墙角,并用旋转矩阵调整法线。
地板生成
地板需要区分墙体交汇处、角落和其他区域。先去除对面墙之间的多余点,再检查点与墙体的关系:靠近墙体的点设为普通地板,其余设为管道地板;最后从墙角中获取角落点,去除多余点后设为角落地板。
道具分布
在 Unreal Engine 中制作了四种箱子蓝图,模型本身也是 Houdini 程序化资产。VEX 读取已有墙角点,按阈值去除部分点,用随机数分配箱子类型,并可通过外部输入的倍数调整;点位可沿法线方向偏移,形成稀疏而多样的箱子分布。
柱子与天花板
柱子脚本筛选 points_at_bottom 中的点,删除偶数索引的点,并按外部参数调整剩余点的位置,使柱子规律地分布在墙体之间。天花板读取用户设置的高度,将每个点向下偏移,法线统一朝下,再为每个点实例化网格。
门的生成
用包围盒限制两条基准曲线之间的距离,从位置接近的对面曲线中筛选出门的位置。调整包围盒大小即可增减门的数量,最后用脚本将筛选出的点实例化为门模型。
部分程序化资产
柱子、墙面和墙面组件同样由 HDA 生成,参数包括尺寸、面板深度、管道、柱帽与细节切割等,用于快速制作模块变体。
附:VEX 源码 · 22 段
Wall_Conver_Instance
读取红、绿颜色标记来区分凹角与凸角,为对应点设置实例资源、法线旋转和分组;随后清理高度超限的点与原有面。
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);
展示视频

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