一、工具目标与整体思路
这个脚本的目标是:
在 3ds Max 中随机生成一个类似碳纳米结构的三维模型,包括原子球和原子之间的键柱,并允许用户通过 UI 调整原子数、键长、键角、半径、随机种子等参数。
它并不是严格意义上的物理化学模拟,而是一个程序化建模工具:
用“原子”表示顶点;
用“键”表示边;
通过随机生长算法不断添加新原子;
对键角、键长、原子间距做约束;
最后用 Sphere 表示原子,用 Cylinder 表示键;
用颜色区分生长深度、随机颜色或统一颜色。
核心模块包括:
CarbonParams:参数结构体;工具函数:高斯随机、向量归一化、随机单位向量、六元环种子等;
makeBondMatrix:计算圆柱体从 p1 到 p2 的变换矩阵;generateCarbonStructure:主生成函数;carbonUI: rollout 界面;createDialog:创建窗口。
二、工具函数解析
这个工具我们将使用一些算法,这些算法封装成函数:
1. 高斯随机数
fn gaussRandom mean sigma = ( local u1 = (random 0.0001 1.0) local u2 = (random 0.0 1.0) mean + sigma * (sqrt(-2.0 * log u1) * cos (360.0 * u2)) )这里用 Box-Muller 变换生成近似正态分布随机数。
用途:
键长不是完全固定,而是围绕
bondLength波动;让结构更自然,不那么机械。
注意:random 0.0001 1.0避免log 0。
2. 向量归一化
fn vecNormalize v = ( local L = length v if L < 1e-6 then [0,0,0] else v / L )避免除以零。这个函数在计算方向、键角、圆柱朝向时都会用到。
3. 随机单位向量
fn randomUnitVector = ( local theta = random 0.0 360.0 local phi = acos (random -1.0 1.0) [sin(phi)*cos(theta), sin(phi)*sin(theta), cos(phi)] )这是球面均匀采样:
theta是方位角;phi是极角;acos(random -1 1)保证球面均匀,而不是立方体均匀。
用途:为新原子选择随机生长方向。
4. 六元环种子
fn makeHexSeed center r = ( local pts = #() for k = 0 to 5 do ( local a = k * 60.0 append pts (center + [r*cos(a), r*sin(a), 0]) ) pts )生成一个平面正六边形,作为初始“类石墨/富勒烯”种子。
为什么用六元环?
碳材料常见结构是六元环;
比单原子种子更容易长出有组织的结构;
初始就有 6 个原子和 6 条键,生长更稳定。
三、键柱变换矩阵:makeBondMatrix
fn makeBondMatrix p1 p2 = ( local zAxis = vecNormalize (p2 - p1) local xAxis, yAxis if abs(zAxis.z) > 0.9 then xAxis = vecNormalize (cross [1,0,0] zAxis) else xAxis = vecNormalize (cross [0,0,1] zAxis) yAxis = cross zAxis xAxis matrix3 xAxis yAxis zAxis p1 )这是整个脚本里非常关键的一段。
3ds Max 的 Cylinder 默认轴心在底面中心,高度沿局部 Z 轴。我们要让圆柱:
从
p1延伸到p2;局部 Z 轴指向
p2 - p1;局部原点位于
p1。
所以矩阵构造为:
zAxis = normalize(p2 - p1);选一个辅助轴与
zAxis叉乘得到xAxis;yAxis = cross zAxis xAxis;位置为
p1。
为什么要判断abs(zAxis.z) > 0.9?
因为如果zAxis和[0,0,1]几乎平行,叉乘会退化。这时改用[1,0,0]做叉乘,避免零向量。
这个函数是“把任意两点连成圆柱”的通用套路,值得记住。
四、主生成函数:generateCarbonStructure
这是脚本的核心。
1. 初始化
seed p.seed local atoms = #() local bonds = #() local depths = #()atoms:存原子坐标;bonds:存键,每项是[i, j];depths:存每个原子的生长深度。
2. 种子阶段
if p.useRingSeed then ( local hexR = p.bondLength local hex = makeHexSeed [0,0,0] hexR for i = 1 to 6 do ( append atoms hex[i] append depths 0 ) for i = 1 to 6 do append bonds [i, (mod i 6) + 1] ) else ( append atoms [0,0,0] append depths 0 )如果使用六元环种子:
生成 6 个原子;
深度都为 0;
连接成环:1-2, 2-3, ..., 6-1。
否则只放一个中心原子。
3. 生长循环
while atoms.count < p.nAtoms and attempts < maxAttempts do ( attempts += 1 local parentIdx = random 1 atoms.count local parentPos = atoms[parentIdx] local bestDir = undefined local bestScore = -1e9 for trial = 1 to 12 do ( local dir = randomUnitVector() local ok = true for b in bonds do ( local nb = 0 if b[1] == parentIdx then nb = b[2] else if b[2] == parentIdx then nb = b[1] if nb > 0 then ( local nd = vecNormalize (atoms[nb] - parentPos) local c = dot dir nd if c > cosMinAngle then ( ok = false; exit ) ) ) if not ok do continue local candidatePos = parentPos + dir * p.bondLength local minD = 1e9 for a in atoms do ( local d = dist a candidatePos if d < minD do minD = d ) if minD > bestScore then ( bestScore = minD bestDir = dir ) ) if bestDir == undefined do continue local bl = gaussRandom p.bondLength (p.bondLength * p.bondJitter) if bl < p.bondLength * 0.5 do bl = p.bondLength * 0.5 local newPos = parentPos + bestDir * bl local tooClose = false for a in atoms do if (dist a newPos) < p.bondLength * 0.55 do (tooClose = true; exit) if tooClose do continue append atoms newPos append bonds [parentIdx, atoms.count] append depths (depths[parentIdx] + 1) )逻辑可以概括为:
随机选一个已存在的原子作为父节点;
尝试 12 个随机方向;
对每个方向,检查它和父节点已有键的方向是否夹角太小;
如果夹角小于
minAngleDeg,就放弃;计算候选新原子位置;
找候选点到所有已存在原子的最小距离;
选“最小距离最大”的那个方向,也就是最不拥挤的方向;
用高斯扰动生成实际键长;
再检查新原子是否离已有原子太近;
如果没问题,加入原子、键、深度。
这个算法不是分子动力学,而是带约束的随机贪心生长。
优点是:
实现简单;
能生成比较自然的网状结构;
通过最小键角和最小距离避免原子重叠。
缺点是:
没有真正化学键价态约束;
没有环化检测;
可能生成悬空键较多的结构;
对大规模原子数效率一般。
五、建模阶段
1. 原子球
for i = 1 to atoms.count do ( local s = Sphere radius:p.atomRadius segs:16 smooth:true s.pos = atoms[i] s.name = "C_Atom_" + (i as string) case p.colorMode of ( #byDepth: ( local f = (depths[i] as float) / (maxDepth + 1e-6) s.wireColor = color (255*(1-f)) (80 + 120*f) (255*f) ) #random: s.wireColor = color (random 60 255) (random 60 255) (random 60 255) default: s.wireColor = color 80 80 80 ) append atomObjs s )每个原子是一个 Sphere。
着色模式:
#byDepth:按生长深度从红到蓝渐变;#random:随机颜色;#uniform:统一灰色。
深度归一化:
local f = (depths[i] as float) / (maxDepth + 1e-6)+1e-6防止除以零。
2. 键柱
local shrink = 0.0 for b in bonds do ( local p1 = atoms[b[1]] local p2 = atoms[b[2]] local L = dist p1 p2 if L < 1e-3 do continue local Lvis = L - shrink if Lvis < 1e-3 do Lvis = 1e-3 local m = makeBondMatrix p1 p2 local c = Cylinder radius:p.bondRadius height:Lvis heightsegs:1 sides:12 c.transform = m c.wireColor = color 200 200 200 append bondObjs c )每条键是一个 Cylinder:
高度是两点距离;
通过
makeBondMatrix对齐到 p1->p2;shrink目前为 0,表示圆柱从 p1 到 p2 完整连接;如果想让键不要插进球里,可以把
shrink设为atomRadius * 0.4左右。
注意脚本里 UI 有spnShrink,但主函数里shrink是局部变量,没有真正从 UI 传入。这是一个可以改进的点。
六、UI 封装
设计一个ui,将关键参数与ui绑定,例如:
点击“生成结构”时:
根据下拉框选择颜色模式;
构造
CarbonParams;把缩进值放到全局变量
gShrink;调用
generateCarbonStructure p;选中生成的对象。
七、如何运行
打开 3ds Max;
打开 Scripting -> New Script;
粘贴代码;
按 Ctrl+E 执行;
会弹出
RandomCarbonNanostructure v0.1.0面板;调整参数;
点击“生成结构”。
建议测试顺序:
先用默认参数生成;
改原子数到 200、500;
改最小键角到 90、100;
改键长扰动到 0.3;
改随机种子,观察不同结构。
八、这个脚本的优点
结构清晰:参数、工具函数、生成函数、UI 分离。
可复现:通过 seed 控制随机。
约束合理:最小键角 + 最小距离,避免严重重叠。
建模直接:Sphere + Cylinder,容易理解。
可扩展:颜色模式、种子方式、参数都可以继续加。
九、完整开发流程总结
如果你要自己开发类似工具,可以按这个流程:
定义参数结构体:把所有可调参数集中。
写工具函数:随机、向量、矩阵。
设计生成算法:
初始种子;
生长规则;
约束条件;
终止条件。
把数据转成几何:
点 -> Sphere;
边 -> Cylinder;
矩阵对齐。
写 UI:
spinner 调数值;
checkbox 调开关;
dropdownlist 调模式;
button 触发生成。
测试与调参:
小规模测试;
检查重叠、断裂、性能。
迭代:
加材质;
加分组;
加环检测;
加导出。
这份《随机碳纳米结构生成工具》脚本,本质上是一个用 MaxScript 把“随机算法 + 几何约束 + 程序化建模”串起来的完整小项目。它从参数结构体出发,经过种子生成、随机生长、几何建模,最后落到 UI 交互,形成了一个可运行、可调参、可复现的 3ds Max 工具雏形。
它不算复杂,但麻雀虽小,五脏俱全:有数据组织,有算法逻辑,有矩阵变换,有界面封装。对学习 MaxScript 来说,这是一个很好的练手案例;对做程序化建模来说,它也是一个可以继续生长的起点。你可以沿着“修复缩进参数 → 启用生长半径 → 增加键数限制 → 加入环检测 → 优化性能 → 增加材质与分组”这条路线,把它逐步打磨成一个真正实用的碳纳米结构生成器。
程序化建模的乐趣就在于此:你写的不是死模型,而是一套能不断生成新形态的规则。希望这份教程能帮你理解这套规则,也能启发你去写出属于自己的生成工具。
(下载该工具请查看本文上方的本站下载链接)