先说结论:把"碎石形状"做成三个旋钮——圆度、棱角、长宽比。拧一下,一条命令生成一箱石头:2D 出多边形,3D 出三角网格,产物是纯 ASCII 的 .dat,跑一次 PFC 就得到 gravel_1..N 这一组 geometry set。文末有命令。
PFC 自带的成样命令,生出来的是圆盘(2D)和球(3D)。
用圆盘做砂土、做堆石,最大的问题是滚动性太强——颗粒之间没有咬合,抗剪强度全靠摩擦系数硬撑,剪胀也出不来。
真实碎石不是圆的。棱角会互相卡住;长条形的会定向排列;扁平的在压力下会翻面。这些都会改变配位数、剪胀量和峰值强度。
那手搓形状行不行?画 20 个不重样的石头轮廓,画完还得算面积、算球度、检查有没有自交、确保能塞进模具。画完一批,参数一改又得重来。
所以要参数化生成。
图1 2D 成品:24 颗碎石多边形,一条命令生成。颗粒大小是随机的(半径 0.35–0.65),形状也是随机的,但整体风格由参数统一控制。
图2 3D 成品:12 颗碎石三角网格,每颗 320 个面。它和图1 是同一套形貌参数——2D 与 3D 是同一个抽象的两个载体。
产物是一个纯 ASCII 的 .dat 文件,每个形状的顶点/面片坐标都已经算好、烘进去了。丢给 PFC 控制台跑一遍,模型里就多出 gravel_1 到 gravel_N 一组 geometry set。
拿来干什么:形状库、墙体的几何来源、DFN 的母体、clump 模板的底座,或者纯粹当个底图。
原理一句话:圆的半径处处相等,那就让半径随角度变一变。
r(θ) = R · (1 + Σ a_k · cos(k·θ + φ_k))公式里的 k 是"这层波浪有几个波峰"。k=2 把圆拉成椭圆,k=3 出三角状起伏,k 越大越细碎。
图3 2D 原理:同一组系数,从只有 k=2 一路加到 k=6,轮廓从椭圆变成碎石。虚线是原始基圆。
实际生成时,每一阶的幅值 a_k 和相位 φ_k 都是随机抽的。相位决定"鼓包鼓在哪一边",所以每颗石头的朝向和凹凸位置都不一样。
抽的规则决定了手感:
3D 的思路一模一样,只是"角度"从一个变成球面上的两个,"波浪"换成球谐函数。
但有个细节值得单说:球面网格怎么铺。
最直觉的做法是经纬网格,像地球仪。问题是两极会挤成一堆退化的三角形,顶点分布极不均匀,球谐采样会出现方向偏差——南北极附近的形状容易失真。
所以用的是细分二十面体:从一个正二十面体起步,每个三角面切成四个,重复几轮。
图4 3D 原理:正二十面体(20 面)→ 细分一次(80 面)→ 细分两次(320 面,默认)→ 再把每个顶点的半径按球谐函数揉乱,就成了碎石。
好处是顶点在球面上分布均匀,没有极点问题。PFC 自带的 geometry generate sphere 也是这么干的。
图5 生成器自带一个零依赖的 HTML 预览器:拖拽旋转、滚轮缩放、切线框,双击就能开,不用装任何东西。批量生成前先看一眼形状对不对,比跑完 PFC 再发现问题省事。
图6 三个形貌旋钮,每个扫四个取值。注意一件事:每一行的四个形状是同一颗石头——参数变了,石头没换。这个性质为什么成立,后面单独讲。
怎么选:
3D 的默认值(圆度 0.4 / 棱角 0.6)是个"安全默认",球度高达 0.94,看着像打磨过的卵石。要碎石感就把棱角和谐波阶数都拉上去。
下面三条是固定 seed、单因子扫描的实测,2D 与 3D 用同一套参数。球度是"离正球有多远"的量化指标,1.0 就是正球。
图7 圆度是总开关:2D 球度 0.93→0.96,3D 0.94→0.98,两边单调变圆。
图8 棱角几乎不动球度:3D 从 0.949 到 0.943,基本是一条平线。这不是"没效果"——棱角把细节塞进高频,总幅值预算没变,所以球度不动,但形状明显更方更尖(对比图6第二行)。
图9 长宽比才是球度的真正主控。上图:从 1.0 拉到 2.0,2D 球度从 0.98 掉到 0.83。下图:拧多少出来多少,2D 基本是 1:1 ——输入 1.6,实测 1.62。
下图中 3D 那条线系统性低于 1:1,这是口径差异不是形状差异:3D 量的是任意方向的三轴包围盒,不是旋转对齐后的主轴。同样的形状换把尺子,数就低了。
这一条是最近测试才确认的,我觉得比上面所有参数都值钱。
固定 seed 之后,改圆度 / 棱角 / 长宽比,形状不会被重新抽签。
图10 同一个 seed,圆度从 0.2 调到 0.4、0.8。变的只是"平滑度",这三组轮廓和网格是同一颗石头。
换句话说是:参数动的是"化妆",不是"换人"。
为什么?随机数只在决定每一阶谐波的幅值和相位时消耗。而圆度只改总幅值预算、棱角只改高频权重、长宽比只改 k=2 那一阶的幅值——三个都作用在系数上,不碰随机数流。
这件事的意义是可以做配对试验。以前扫参只能比统计量(平均球度、平均长宽比),因为每一档的形状都是新抽的,两组之间的差异里混着随机噪声,样本量不够就看不出小效应。现在固定 seed 逐颗对比,同一颗石头的"前后照"摆在一起看。
有个例外:改 --count 会洗牌。因为布局和形状生成共用同一条随机流,而布局先跑。想连 count 也不洗牌,用 --layout grid 布局。
生成形状时要往域里摆位置。摆不下怎么办?
grid 布局是等距排,间距按最大半径算,但长宽比会把颗粒沿长轴拉出去,两者不联动;random 布局则是尝试若干次之后强行放下去。
结果就是形状会真的叠在一起,而且什么都不报。 生成器不报,PFC 也不报。
图11 A 是摆得开的:24 颗,最紧的地方还剩 0.033 的缝(约颗粒半径的 7%)。B 是 40 颗硬塞进 4×4 域:158 对真的糊在一起。
拿这种重叠的形状去做 clump 模板或者墙体,问题要到模型里才暴露——而那时候你已经不知道是形状的锅还是接触参数的锅。
所以现在生成器自己会查:算完所有形状两两之间的最短间距,发现接触就打一行 [warn],同时写进 .dat 的头部注释。看到输出里是 placement ok 才算过关。3D 那边用的是顶点到三角面的距离,比包围盒判断准得多。
顺带一提,文档里原来的示例命令(random 布局 + 30 颗 + 长宽比 1.6)本身就产出 2 对重叠,也是这次测试发现的,已经改成 grid 了。
跑完看两件事:生成器最后一行是不是 placement ok;PFC 日志里 geometry list 的行数对不对得上 --count。
这些东西不复杂:2D 用傅里叶描述符,3D 用球谐加细分二十面体,都是几十年前就有的数学。
价值在于打包成"三个旋钮加一条命令",让做 DEM 的人不用自己去推公式、写网格、查自交。形状参数化之后,圆度、棱角、长宽比这些能对应到地质描述的量就可以直接扫参了——磨圆度不同的砂、棱角不同的碎石,都能拿同一套流程做出来对比。
VibePFC 社区 · 一起把 AI 玩进 PFC
我成立了一个 VibePFC 社群,专门聊 AI 怎么落地到 PFC / 数值模拟——让大模型读懂模型、自己改脚本、自动排查"看不懂"的问题。收一个 50 元的门槛费。
想进群的同学,加 QQ 763388012,备注"vibePFC"即可。
pfc-gravel-shape:2D 傅里叶描述符 + 3D 球谐与细分二十面体,产物为纯 ASCII 的 geometry 脚本随机形状技能测试/随机形状技能测试报告.html