UE5制作六边形包裹球体效果

在科幻题材的视觉表现中,常出现由六边形与五边形组成的盾状结构,象征防御护甲等效果。本文将讲解如何在UE5中构建这样的球体护盾。

一.六边形/五边形 球体制作

1.因为纯六边形构建的球体在数学上不可能,因此这类球体都是由六边形与五边形组合构建。对于它的建模,你可以用DCC软件各显神通,也可以5分钟vibe coding搞定。我们用AI生成maya内的python脚本以完成该模型建模。

python 复制代码
# -*- coding: utf-8 -*-
import math
from collections import defaultdict
import maya.cmds as cmds

# =========================================================
# 纯 Python 手动实现向量数学工具(零第三方库依赖)
# =========================================================

def vec_len(v):
    return math.sqrt(v[0]**2 + v[1]**2 + v[2]**2)

def normalize(v):
    length = vec_len(v)
    if length > 0:
        return [v[0] / length, v[1] / length, v[2] / length]
    return v

def vec_dot(v1, v2):
    return v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2]

def vec_cross(v1, v2):
    return [
        v1[1]*v2[2] - v1[2]*v2[1],
        v1[2]*v2[0] - v1[0]*v2[2],
        v1[0]*v2[1] - v1[1]*v2[0]
    ]

def vec_sub(v1, v2):
    return [v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2]]

def poly_center(poly):
    num_verts = len(poly)
    return [
        sum(v[0] for v in poly) / num_verts,
        sum(v[1] for v in poly) / num_verts,
        sum(v[2] for v in poly) / num_verts
    ]

# =========================================================
# 几何生成核心(二十面体对偶转换算法)
# =========================================================

def create_icosahedron(radius=1.0):
    t = (1.0 + math.sqrt(5.0)) / 2.0
    raw_verts = [
        [-1,  t,  0], [ 1,  t,  0], [-1, -t,  0], [ 1, -t,  0],
        [ 0, -1,  t], [ 0,  1,  t], [ 0, -1, -t], [ 0,  1, -t],
        [ t,  0, -1], [ t,  0,  1], [-t,  0, -1], [-t,  0,  1]
    ]
    verts = [ [component * radius for component in normalize(v)] for v in raw_verts ]
    faces = [
        [0,11,5], [0,5,1], [0,1,7], [0,7,10], [0,10,11],
        [1,5,9], [5,11,4], [11,10,2], [10,7,6], [7,1,8],
        [3,9,4], [3,4,2], [3,2,6], [3,6,8], [3,8,9],
        [4,9,5], [2,4,11], [6,2,10], [8,6,7], [9,8,1]
    ]
    return verts, faces

def subdivide(verts, faces, level=1, radius=1.0):
    for _ in range(level):
        midpoint_cache = {}
        new_faces = []
        
        def get_midpoint(i1, i2):
            key = tuple(sorted((i1, i2)))
            if key in midpoint_cache:
                return midpoint_cache[key]
            v1, v2 = verts[i1], verts[i2]
            vm_raw = [(v1[0] + v2[0]) * 0.5, (v1[1] + v2[1]) * 0.5, (v1[2] + v2[2]) * 0.5]
            vm = [component * radius for component in normalize(vm_raw)]
            verts.append(vm)
            idx = len(verts) - 1
            midpoint_cache[key] = idx
            return idx

        for tri in faces:
            a, b, c = tri
            ab = get_midpoint(a, b)
            bc = get_midpoint(b, c)
            ca = get_midpoint(c, a)
            new_faces.extend([[a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca]])
        faces = new_faces
    return verts, faces

def create_dual_mesh(verts, faces, radius=1.0):
    face_centers = []
    for f in faces:
        raw_center = [
            (verts[f[0]][0] + verts[f[1]][0] + verts[f[2]][0]) / 3.0,
            (verts[f[0]][1] + verts[f[1]][1] + verts[f[2]][1]) / 3.0,
            (verts[f[0]][2] + verts[f[1]][2] + verts[f[2]][2]) / 3.0
        ]
        center = [component * radius for component in normalize(raw_center)]
        face_centers.append(center)

    vertex_faces = defaultdict(list)
    for fi, face in enumerate(faces):
        for v in face:
            vertex_faces[v].append(fi)

    polygons = []
    for vi, connected_faces in vertex_faces.items():
        center = verts[vi]
        normal = normalize(center)
        
        tangent = [1.0, 0.0, 0.0]
        if abs(vec_dot(tangent, normal)) > 0.9:
            tangent = [0.0, 1.0, 0.0]
            
        tangent = normalize(vec_cross(normal, tangent))
        bitangent = normalize(vec_cross(normal, tangent))

        angles = []
        for fi in connected_faces:
            p = face_centers[fi]
            d = vec_sub(p, center)
            x = vec_dot(d, tangent)
            y = vec_dot(d, bitangent)
            angles.append((math.atan2(y, x), fi))
            
        angles.sort()
        polygons.append([face_centers[fi] for _, fi in angles])
    return polygons

# =========================================================
# Maya 资产生成(对调通道色彩映射版)
# =========================================================

def build_honeycomb_mesh_in_maya(mesh_name, polygons, scale=100.0):

    if cmds.objExists(mesh_name):
        cmds.delete(mesh_name)
        
    created_facets = []
    pentagons = 0
    hexagons = 0
    
    # 暂时关闭撤销队列以提高拼装效率
    cmds.undoInfo(stateWithoutFlush=False)
    
    try:
        for poly in polygons:
            num_verts = len(poly)
            if num_verts == 5: pentagons += 1
            elif num_verts == 6: hexagons += 1
            
            # 计算该单元网格的中心点与单位方向
            center = poly_center(poly)
            normal = normalize(center)
            
            # 1. 新的 R 通道映射:基于 XZ 平面的方位角(经度),环绕一圈是 0-1
            angle = math.atan2(normal[2], normal[0]) 
            r_color = (angle + math.pi) / (2.0 * math.pi)
            r_color = max(0.0, min(1.0, r_color))
            
            # 2. 新的 G 通道映射:基于 Y 轴高度进行归一化映射 [-1, 1] -> [0, 1]
            g_color = normal[1] * 0.5 + 0.5
            g_color = max(0.0, min(1.0, g_color))
            
            # 拼装单元格片面顶点的世界坐标
            pts = [(v[0] * scale, v[1] * scale, v[2] * scale) for v in poly]
            facet = cmds.polyCreateFacet(point=pts, ch=False)[0]
            created_facets.append(facet)
            
            # 写入对调后的颜色数据
            cmds.polyColorPerVertex(facet, rgb=(r_color, g_color, 0.0), cdo=True)
            
        # 1. 执行网格大融拼
        combined_mesh = cmds.polyUnite(created_facets, ch=True, name=mesh_name)[0]
        
        # 2. 规范化颜色集命名
        current_sets = cmds.polyColorSet(combined_mesh, q=True, allColorSets=True)
        if current_sets:
            cmds.polyColorSet(combined_mesh, edit=True, colorSet=current_sets[0], newColorSet='colorSet1')
        
        # 3. 固化历史
        cmds.delete(combined_mesh, ch=True)
        
        # 4. 视口外观设置
        cmds.polySoftEdge(combined_mesh, angle=0, ch=False) # 全硬边保持结构
        cmds.toggle(combined_mesh, vertexColor=True)        # 开启视口顶点色数据显示
        cmds.select(combined_mesh)
        
        print("======================================================")
        print("  Channel Swapped! Maya Honeycomb Mesh Re-generated.  ")
        print("======================================================")
        print(f"Object Name : {combined_mesh}")
        print(f"Color Set   : colorSet1 (R: Circumference, G: Vertical)")
        print(f"Total Cells : {len(polygons)}")
        print("======================================================")
        
    finally:
        # 恢复撤销队列设置
        cmds.undoInfo(stateWithoutFlush=True)

# =========================================================
# 执行主入口
# =========================================================
if __name__ == "__main__":
    RADIUS = 1.0          
    SUBDIVISION = 3       
    SCALE_FACTOR = 100.0  

    MESH_NAME = "SM_ShieldSphere_Honeycomb"

    print("Executing channel-swapped geometry generation pipeline...")
    
    verts, faces = create_icosahedron(RADIUS)
    verts, faces = subdivide(verts, faces, level=SUBDIVISION, radius=RADIUS)
    polygons = create_dual_mesh(verts, faces, RADIUS)

    build_honeycomb_mesh_in_maya(MESH_NAME, polygons, scale=SCALE_FACTOR)

跑完之后效果长这样:

这个脚本用顶点色记录数据,每个cell是独立单元------R通道是绕周长映射的[0-1]区间,G通道是从下到上的映射,跟球面坐标那套逻辑一样。

2.但该球体无厚度,后序制作效果时会非常单薄,在MAYA中使用挤出命令设置厚度。

设置好后导出至到UE即可。

二.导入UE

3.导入UE后检查顶点色导入是否设置,如果未设置将该选项设置为Replace,点击Reimport Base Mesh重新导入。

4.选择Show - Vert Color预览顶点色。

5.加几个简单节点测试下,你会发现沿着法线做正弦运动时,效果一片混乱。

6.问题是因为挤出时法线并不一致,侧面的法线与正面法线朝向不同方向导致。但即使通过法线映射为一致,光照也会出现问题。

7.换个思路,之前在顶点色上缓存了球面坐标信息,可通过球面坐标反求法线。

Matrix Remap为方便使用的隐藏节点,需要通过复制粘贴命令得到:

cpp 复制代码
Begin Object Class=/Script/UnrealEd.MaterialGraphNode Name="MaterialGraphNode_0"
   Begin Object Class=/Script/InterchangeImport.MaterialExpressionRemap Name="MaterialExpressionRemap_0"
   End Object
   MaterialExpression=/Script/InterchangeImport.MaterialExpressionRemap'"MaterialExpressionRemap_0"'

之所以要进行Remap值域转换,是因为UE对sin,cos进行了值域的修改,最终CellNormal为输出法线。

8.测试下效果。

9.最后根据你的需求继续完成制作。



https://www.youtube.com/watch?v=dAk8TqzC0IY&t=1s

相关推荐
砍材农夫7 小时前
物联网 基于netty构建mqtt协议规范(遗嘱与保留消息)
java·开发语言·物联网·netty
DFT计算杂谈7 小时前
KPROJ编译教程
java·前端·python·算法·conda
froginwe117 小时前
Python3 迭代器与生成器
开发语言
xiaoshuaishuai87 小时前
C# 签名异常与Gas预估失败调试方案
开发语言·网络·tcp/ip·c#
xiaoshuaishuai87 小时前
C# Gemini 辅助网络安全漏洞分析
开发语言·web安全·c#
念恒123067 小时前
Python(循环中断)
开发语言·python
社交怪人7 小时前
【数字对调】信息学奥赛一本通C语言解法(题号2070)
c语言·开发语言
hef2887 小时前
C语言中char指针与数组的区别及应用
c语言·开发语言
风继续吹..7 小时前
C# 文件输入输出 精简理解
开发语言·c#