想象你坐在奶奶身边学织毛衣。一块布不是一整片的,而是由千万个毛线结——我们叫它“针脚”——和连接它们的纱线织成的。你扯一下任何一个针脚,纱线就会把劲儿传给旁边的针脚,一传十、十传百,整块布跟着变形。程序里的布料模拟,就是织这样一张“会动的毛衣”:先设计编织图样,告诉织娘每个针脚往哪连;再给每根纱线定好“回弹脾气”,拉长就想缩回;接着一帧一帧踩织布机的踏板,让时间走起来;遇到衣领翘边,就压一道“不许翘”的规矩;裙摆两层叠在一起,还要防止它们粘成一块。最后,请来一千个织娘同时开工,几千个针脚眨眼间就能全部织好,一块布就在屏幕上迎风飘起来了。

岗位关联

学懂这块知识,你就能理解游戏和影视里披风、裙摆随风飘动背后的物理原理,这是你往游戏物理与动画方向进阶、从事 AI 引擎开发与项目落地的坚实一步。

目录
  1. 可视化演示
  2. 一、弹簧网络拓扑:先画好编织图样
  3. 二、质点弹簧模型:每根纱线都在较劲
  4. 三、数值积分方法:踩准织布的节拍
  5. 四、弯曲约束:压住翘边的衣领
  6. 五、PBD约束求解:直接用手抚平褶皱
  7. 六、自碰撞处理:别让两层布粘成一块
  8. 七、GPU并行加速:请来一千个织娘
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

交互展示质点弹簧网格结构、布料重力变形动态及PBD位置约束求解迭代过程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

弹簧网格:三类约束拓扑 结构约束(水平垂直)维持网格基本形状 剪切约束(对角)抵抗拉伸变形 F = -k(|x| - L₀) · x̂ 质点弹簧模型通过三种约束弹簧网络实现布料物理仿真

一、弹簧网络拓扑:先画好编织图样

织毛衣的第一步从来不是拿针,而是看图纸。图纸上画着针脚往哪儿下、线往哪儿走:横着走几针、竖着走几针、斜着加几道。布料的软硬、垂感、褶皱风格,早在图纸阶段就定下了大半。布料模拟的第一步同样是“画图纸”——设计弹簧网络拓扑。你甚至可以把图纸放大挂在墙上:每个针脚的位置、每根纱线的走向,就是布料未来所有动作的总纲。

在程序里,这张图纸变成了一张网格。网格的每个交叉点是一个「质点」,对应毛衣上的一个针脚;质点之间的连线是「弹簧」,对应一根纱线。一张 8×6 的布料,就是 48 个质点和上百根弹簧组成的受力网络。问题随之而来:弹簧到底应该连哪些质点?连错了,布料要么像渔网一样稀松,要么像钢板一样僵硬,所以这个“连法”必须仔细设计。

答案是三类弹簧分头负责。结构弹簧连接上下左右的近邻,管布料的拉伸强度,是最基础的“经线纬线”;剪切弹簧连接斜对角邻居,管布料在斜向拉扯时不变形;弯曲弹簧跨越一个质点去连接“隔一个”的邻居,管布料抗折叠。程序里通常把二维网格压成一维数组,用编号公式定位每个质点,避免二维索引带来的寻址开销。

i=r×C+c\displaystyle i = r \times C + c

1. 行号定位:行号 r 表示针脚在布料中从上往下的第几行,它是计算编号的起点。

r\displaystyle r

2. 行宽与偏移:C 是每行的质点数量,用行号乘以行宽,得到这个针脚之前一共有多少个质点。

r×C\displaystyle r \times C

3. 列号补位:再加上列号 c,就能得到该针脚在一维数组中的唯一下标。

i=r×C+c\displaystyle i = r \times C + c

图纸画好,织娘才知道往哪儿下针;同理,弹簧网络连通之后,后续的受力计算、约束求解、碰撞检测才有“网”可依。拓扑设计的质量,几乎决定了布料最终的垂坠感和稳定性——松一点像渔网,紧一点像钢板,三类弹簧之间的取舍,就是布料模拟的第一道风味。

二、质点弹簧模型:每根纱线都在较劲

织毛衣用的线,其实是一根“很有主见的橡皮筋”:你把它拉长,它想缩回去;你把它压短,它想弹开。而且拉得越狠,反抗越凶。这个“越用力越挣扎”的规律,就是物理学里的胡克定律。生活中的毛衣、渔网、蹦床,全都靠这种“较劲”维持形状,布料模拟只是把这种较劲搬进了代码里。

动画里的每个质点身边都牵着好几根弹簧,每根弹簧都在按自己的“主见”拉扯这个质点。把这几股力加在一起,再加上向下的重力,就得到质点受到的合力。合力除以质量得到加速度,加速度再推动质点移动——布料就这么动起来了。看起来复杂的裙摆飘动,拆到底就是“每个针脚听谁的话”的问题。

弹簧的“主见”由两个参数决定:自然长度 L0 和刚度 k。自然长度就是纱线没被拉扯时的原始长度;刚度 k 是“脾气系数”,k 越大,同样形变产生的回弹力越大,布就越硬挺。弹力方向永远指向把弹簧恢复到自然长度的方向,所以弹簧两端的质点是互相拽着的。

F=k×(ℓ−L0)\displaystyle F = k \times (\ell - L_0)

1. 当前长度:ℓ 是这根弹簧此刻被拉伸或压缩后的瞬时长度,随每帧模拟不断变化。

ℓ\displaystyle \ell

2. 形变量:用当前长度减去自然长度,结果是正数代表拉长了,负数代表被压短了。

ℓ−L0\displaystyle \ell - L_0

3. 回弹力:刚度 k 乘以形变量,得到弹簧试图恢复原状的回弹力,方向沿弹簧指向恢复方向。

k×(ℓ−L0)\displaystyle k \times (\ell - L_0)

真实的毛衣线之间还有摩擦和阻尼,否则整块布会像琴弦一样振荡个不停。阻尼项把这些振荡慢慢“吸”掉,让裙摆在风中飘动时能安静地垂下。线在较劲,布在权衡,最终达到的平衡状态,就是布料自然下垂的质感。

三、数值积分方法:踩准织布的节拍

看电影时画面一帧帧闪过,物理模拟也一样:把时间切成一帧一帧,每帧推进一小步 Δt。织布时织娘要一梭一梭地踩踏板,步子稳,布面才平;步子迈太大,梭子会飞出去。这个比喻放在物理模拟里毫不夸张——时间步迈得太大,质点真的会“飞”出屏幕,我们管这叫数值发散。

迈步子有三种典型姿势。显式欧拉是“先用旧速度走一步再说”,代码简单但步子一大就容易扯到筋;隐式欧拉是“反复思考每一步的后果”,极其稳定但每帧要解大型方程组,贵得吓人;半隐式欧拉折中——先更新速度,再用新速度更新位置,既稳又快。游戏引擎默认选它,就像老织娘总有自己的省力节奏。

半隐式欧拉是布料模拟的事实标准,因为它把“先算速度后算位置”的顺序优化到了极致。重力、弹簧力先在速度上结算,再带着全新速度去推进位置,这样的耦合方式大幅削弱了数值爆炸的风险。它也是后面 PBD 方法理解惯性项的基础。

vt+1=vt+a Δt,xt+1=xt+vt+1 Δt\displaystyle \mathbf{v}_{t+1} = \mathbf{v}_t + \mathbf{a}\,\Delta t,\qquad \mathbf{x}_{t+1} = \mathbf{x}_t + \mathbf{v}_{t+1}\,\Delta t

1. 速度更新:把当前速度加上“加速度乘以时间步”,得到新速度,加速度来自合力除以质量。

vt+1=vt+a Δt\displaystyle \mathbf{v}_{t+1} = \mathbf{v}_t + \mathbf{a}\,\Delta t

2. 位置更新:把当前位置加上“新速度乘以时间步”,得到新位置,注意用的是新速度。

xt+1=xt+vt+1 Δt\displaystyle \mathbf{x}_{t+1} = \mathbf{x}_t + \mathbf{v}_{t+1}\,\Delta t

Δt 就像织布的步幅:迈得太大,织物会散架;迈得太小,一帧画面要算很多步,机器累到冒烟。游戏里常见的做法是固定 Δt,比如 1/60 秒,保证节奏稳定;如果连 1/60 秒都嫌大,就拆成两三个子步去积分。踩准节拍,布才能织得又快又稳。

四、弯曲约束:压住翘边的衣领

毛衣平铺在桌上时,最不听话的是领口和袖口——明明桌面是平的,它们偏要翘起来。原因很朴素:横竖的纱线只能抗“拉伸”,几乎不能抗“折”。纯靠结构弹簧的布,被掰弯之后完全不知道要恢复原状,整个衣领塌得像块抹布。要让布料记住自己的“平整姿态”,就必须额外立规矩。

这个规矩就是弯曲约束。程序里的布料表面通常被离散成一张三角网格,两个相邻的三角形共用一条边,它们之间的“张开角度”叫二面角。布平铺时二面角接近 180°,折叠时二面角变小。弯曲约束的任务就是盯着二面角,一旦偏离原始角度,就施加一个往回掰的恢复效果。

实现上常用弯曲弹簧或弯曲能量项:角度偏离越大,惩罚越大。正因为有这一项,西装领子才能挺括地立起来,裙摆才能在转身时甩出利落的折痕,而不是软塌塌地贴在腿上。抗弯刚度 kb 就是这个约束的“力气”。

Ebend=12 kb (θ−θ0)2\displaystyle E_{\text{bend}} = \frac{1}{2}\,k_b\,(\theta - \theta_0)^2

1. 二面角:θ 是两个相邻三角形面片的二面角,即布料在该处“折”了多少度。

θ\displaystyle \theta

2. 角度偏差:θ0 是原始角度(布料摊平时通常接近 180°),两者相减得到偏离量。

θ−θ0\displaystyle \theta - \theta_0

3. 平方惩罚:偏差的平方保证能量恒为正,再乘上抗弯刚度 kb 的一半,就得到弯曲能量。

12 kb (θ−θ0)2\displaystyle \frac{1}{2}\,k_b\,(\theta - \theta_0)^2

kb 是“衣领的脾气”:西装领子挺括,kb 得大;棉布 T 恤软塌,kb 调小。调好 kb,衣摆既不僵硬也不软得像抹布,褶皱被压在该在的位置上,一件衣服的“性格”就出来了。

五、PBD约束求解:直接用手抚平褶皱

想象妈妈铺桌布:桌布皱了,她不会去计算“这块布每根线的受力”,而是直接用手把褶皱推到该在的位置。2007 年提出的 Position Based Dynamics(PBD)就是这个思路——不绕弯子去求弹力,直接用“位置投影”把越界的质点拉回约束面。这个思路粗糙却极其有效,游戏物理里到处是它的身影。

PBD 的世界里没有“力”这个抽象概念,只有“约束”:距离约束要求两个质点的距离保持为固定值。每帧先让重力把质点往下推一把,然后检查哪些质点对的距离不符合要求,发现不对就强行掰回正确距离。就像桌布皱了,直接用手一捋,布就平了。

关键在于“掰”的分配:两个质点都要动,谁轻谁多动一点,谁重谁少动一点,用逆质量加权来分配。一轮掰完再掰下一轮,通常迭代 3~5 次就能收敛得相当好。迭代次数越多,布料越接近“刚硬”,这种直观的调参方式让美术同学也能轻松上手。

xi←xi−wiwi+wj (∥xj−xi∥−L0) xj−xi∥xj−xi∥\displaystyle \mathbf{x}_i \leftarrow \mathbf{x}_i - \frac{w_i}{w_i + w_j}\,\big(\lVert \mathbf{x}_j - \mathbf{x}_i \rVert - L_0\big)\,\frac{\mathbf{x}_j - \mathbf{x}_i}{\lVert \mathbf{x}_j - \mathbf{x}_i \rVert}

1. 距离误差:计算两个质点当前距离与目标距离 L0 的差值,这就是需要修正的错位量。

∥xj−xi∥−L0\displaystyle \lVert \mathbf{x}_j - \mathbf{x}_i \rVert - L_0

2. 修正方向:用两点的单位方向向量指出该往哪个方向推。

xj−xi∥xj−xi∥\displaystyle \frac{\mathbf{x}_j - \mathbf{x}_i}{\lVert \mathbf{x}_j - \mathbf{x}_i \rVert}

3. 质量加权:按逆质量比例 wi/(wi+wj) 分配修正量,轻的质点多动、重的质点少动。

wiwi+wj\displaystyle \frac{w_i}{w_i + w_j}

PBD 的妙处是“永远不炸”:即使时间步很大,位置投影也能保证布料不被扯裂。这正是游戏里随风狂舞的披风、角色衣角的首选方案——不追求物理绝对精确,只求观感稳定、手感可控。

六、自碰撞处理:别让两层布粘成一块

裙子转起来,裙摆内外两层布会擦肩而过。如果程序不管,它们会直接互相穿透,像两张凉席强行叠在一起,物理上荒谬、视觉上穿帮。布不仅要撞墙撞地面,更要防着“自己撞自己”——这叫自碰撞。现实中两块布叠在一起会自然滑开,模拟里必须显式地处理这件事。

最笨的办法是每对质点都做一次距离检测,n 个质点就是 O(n²) 的组合数量。布料动辄几千个质点,n² 直接变成上千万次计算,机器当场死机。聪明人的做法是把空间切成小格子——空间哈希。就像整理衣柜时把衣服按格子分类,找某件衣服不用翻遍整个衣柜,只看那一个格子就行。

每个质点先算出自己落在哪个格子,然后只需要检查“同一个格子 + 周围一圈格子”里的邻居,碰撞候选数量从 n² 骤降到常数级。找到候选对之后,如果距离过近,就用和 PBD 一样的投影把它俩推开,必要时再补一点摩擦,让两层布相对滑动时更自然。

g(p)=⌊px/h⌋+⌊py/h⌋×H\displaystyle g(p) = \lfloor p_x / h \rfloor + \lfloor p_y / h \rfloor \times H

1. 网格坐标:把质点的 x 坐标除以格宽 h 后向下取整,得到它所在列的格子编号。

⌊px/h⌋\displaystyle \lfloor p_x / h \rfloor

2. 行偏移:再把 y 方向的格子编号乘以网格总列数 H,作为行的偏移量。

⌊py/h⌋×H\displaystyle \lfloor p_y / h \rfloor \times H

3. 合并成键:两者相加得到一个唯一格子 ID,每个质点的邻居只在邻近 3×3 格子范围里找。

g(p)=⌊px/h⌋+⌊py/h⌋×H\displaystyle g(p) = \lfloor p_x / h \rfloor + \lfloor p_y / h \rfloor \times H

查完格子,邻居是谁一目了然,裙摆各走各的路。自碰撞处理得当,舞蹈角色的裙摆才能飞出蓬松自然的弧线,而不是两片玻璃一样交错穿帮。

七、GPU并行加速:请来一千个织娘

一台织布机只有一个梭子,织一面旗要织到天黑。但如果请来一千个织娘,每人负责一小块布面同时开工,眨眼间就能织完。GPU 就是这一千个织娘——布料的几千个质点,彼此之间的弹簧计算天然独立,正是大规模并行最爱的负载。这也是实时布料渲染成为可能的根本原因。

每隔一帧,每个质点都要计算它身上所有弹簧的合力、更新位置、处理碰撞。这些计算互不依赖,一个线程管几个质点,弹簧的力在一个并行阶段里就能全部算完。把质点数据交给 GPU 的成百上千个线程,CPU 只需要做最后的汇总和渲染调用,整块布的物理刷新就完成了。

工程上要注意数据布局:把“每个质点的 (x, y, z)”拆成“所有 x 的数组、所有 y 的数组”,让相邻线程访问连续内存,避免访存打架。同时 GPU 加速并非无限,Amdahl 定律画出了天花板——总有一部分串行工作(如约束迭代间的依赖整理、碰撞对的合并)无法并行。

S=1(1−f)+f/N\displaystyle S = \frac{1}{(1 - f) + f / N}

1. 串行部分:1-f 是无法并行的计算比例,比如拓扑构建、碰撞数据整理等,比例越少越好。

1−f\displaystyle 1 - f

2. 并行部分:f/N 表示可并行比例 f 被 N 个线程均分后的耗时,线程越多耗时越短。

fN\displaystyle \frac{f}{N}

3. 总加速比:总耗时是串行部分加并行部分,用原来的 1 除以它,就得到加速比 S。

S=1(1−f)+f/N\displaystyle S = \frac{1}{(1 - f) + f / N}

正是这些细节,让游戏里角色的披风、裙摆能在手机上实时飘动。一千个织娘各守一段纱,合力织出一整片会呼吸的布——这就是 GPU 布料模拟的浪漫。

AI 应用场景

虚拟试衣间的实时布料模拟

阶段:数据生成 / 实时仿真

解决的问题:传统线下试衣成本高、无法个性化;虚拟试衣需要实时模拟衣物在人体上的垂坠、褶皱与碰撞,还要根据用户体型动态调整版型。布料模拟提供物理可信的衣物形变,AI 再根据体态参数驱动仿真,让用户旋转身体时衣物实时跟随,避免“衣服是糊在身上的贴图”的违和感。

具体案例:电商 App 的 AR 试衣功能:摄像头捕捉用户 15 个骨骼点,驱动一件连衣裙的质点网格;裙摆随转身摆动,弯腰时布料自然堆叠——背后是 PBD 约束 + GPU 并行实时求解 4000 个质点,每帧耗时控制在 2ms 内。

机器人叠衣服的仿真训练数据

阶段:强化学习训练

解决的问题:机器人叠衣服最难的是“摸不到布料”——真实数据采集又慢又贵。布料模拟可以在虚拟环境中生成海量布料形变数据,供强化学习智能体在仿真里反复试错,学会抓取角点、对折、再对折。模仿学习与 RL 都需要在模拟器里跑成千上万次,布料模拟的稳定性决定了训练能否收敛。

具体案例:研究团队在 GPU 布料模拟环境里训练机器人夹爪:每个 episode 随机把毛巾丢在桌上,模拟器实时计算毛巾的褶皱和自碰撞,智能体用视觉观察 + 物理状态作为输入,训练 10 万步后学会把毛巾叠成方块,再迁移到真实夹爪。

游戏引擎中的布料物理调参

阶段:实时物理系统·体验调优

解决的问题:游戏角色的披风、裙摆、头发都需要布料模拟,但手机性能有限。工程师需要用 GPU 并行 + 分片网格,让布料在离镜头近时精细、远处退化为简单弹簧,用 AI 驱动 LOD 选择,自动判断哪些布料需要全精度模拟。

具体案例:某开放世界 MMORPG 中,主角披风的 2000 个质点被烘到 Compute Shader 中,每帧 4 次 PBD 迭代 + 空间哈希自碰撞,整体耗时控制在 1.5ms 以内;镜头拉远时自动切换到 500 质点的简化网格。

Python 演示

python80 行
# 用 PBD 实现一块 8x6 的二维布料, 输出每帧的最大形变误差import math class Cloth:    """PBD 布料模拟: 质点 + 距离约束网络"""     def __init__(self, cols=8, rows=6, spacing=0.5):        self.cols = cols        self.rows = rows        self.spacing = spacing        self.points = []        for r in range(rows):            for c in range(cols):                x = c * spacing                y = r * spacing                # [x, y, px, py, vx, vy, fixed]                self.points.append([x, y, x, y, 0.0, 0.0, r == 0])        self.constraints = []        for r in range(rows):            for c in range(cols):                i = r * cols + c                if c < cols - 1:                    self.constraints.append((i, i + 1, spacing, 1.0))                if r < rows - 1:                    self.constraints.append((i, i + cols, spacing, 1.0))                if c < cols - 1 and r < rows - 1:                    rest = spacing * math.sqrt(2.0)                    self.constraints.append((i, i + cols + 1, rest, 0.5))     def simulate(self, gravity=0.15, iterations=4, damping=0.99):        # 1. 重力 + 速度推进        for p in self.points:            if p[6]:                continue            p[2], p[3] = p[0], p[1]            p[5] += gravity            p[0] += p[4]            p[1] += p[5]        # 2. 迭代求解距离约束        for _ in range(iterations):            for i, j, rest, w in self.constraints:                a = self.points[i]                b = self.points[j]                dx = b[0] - a[0]                dy = b[1] - a[1]                dist = math.sqrt(dx * dx + dy * dy)                if dist < 1e-6:                    continue                diff = (dist - rest) / dist * 0.5 * w                if not a[6]:                    a[0] += dx * diff                    a[1] += dy * diff                if not b[6]:                    b[0] -= dx * diff                    b[1] -= dy * diff        # 3. 由位置差反推速度 + 阻尼        for p in self.points:            if p[6]:                continue            p[4] = (p[0] - p[2]) * damping            p[5] = (p[1] - p[3]) * damping     def max_strain(self):        worst = 0.0        for i, j, rest, w in self.constraints:            a = self.points[i]            b = self.points[j]            dist = math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2)            worst = max(worst, abs(dist - rest))        return worst  if __name__ == '__main__':    cloth = Cloth(cols=8, rows=6, spacing=0.5)    print('质点数量:', len(cloth.points))    print('约束数量:', len(cloth.constraints))    for frame in range(25):        cloth.simulate()        if frame % 5 == 0:            print('帧 %2d 最大形变误差: %.4f' % (frame, cloth.max_strain()))
目的:用纯 Python 实现 PBD 布料核心循环,打印每帧所有距离约束的最大形变误差,直观看到约束迭代如何维持布料形状。
  1. self.points.append([x, y, x, y, 0.0, 0.0, r == 0])每个质点同时保存当前坐标与上一帧坐标,最后一位标记固定点(第一行像晾衣夹一样被夹住)。
  2. self.constraints.append((i, i + 1, spacing, 1.0))结构弹簧,把左右相邻质点拴在一起,权重 1.0 表示它俩几乎平分修正。
  3. rest = spacing * math.sqrt(2.0)斜对角弹簧的自然长度等于格子对角线长度,而不是间距。
  4. p[5] += gravity每帧给垂直方向的速度叠加重力,布料开始往下坠。
  5. for _ in range(iterations)约束迭代 4 次,把一次修正的影响在网络里传播开,迭代越多布料越“硬”。
  6. diff = (dist - rest) / dist * 0.5 * w算出每个质点需要被拉回的比例:相对误差、对半分、按权重缩放。
  7. p[4] = (p[0] - p[2]) * damping用“现在坐标减上一帧坐标”反推速度,再乘阻尼让运动逐渐平稳。

常见误区

误区我总觉得,想让披风更硬挺,就得多加弹簧。反正弹簧越多织物越结实,受力网络越密,衣服肯定更笔直,对吧?

正解布的软硬主要取决于每根弹簧的刚度(权重)和约束求解的迭代次数,而不是弹簧数量。弹簧数量决定的是“受力网络的分辨率”——就像毛衣的针脚密度只决定织物细腻度,真正决定挺括感的是纱线本身的粗细和张力。弹簧再多,如果每根的刚度都很小、迭代次数很少,布照样软趴趴。

为何常见:直观上“线多=结实”,但在数值模拟里,硬度是力的响应强度,和拓扑密度没有直接关系。

误区显式欧拉代码最少、最好写,为什么游戏里不默认用它?我觉得它简单又够用。

正解显式欧拉对时间步极其敏感,Δt 稍大,弹簧网络就会数值爆炸——质点直接飞出屏幕。游戏里 1/60 秒的步长对显式欧拉往往不够,必须用半隐式欧拉或隐式方法。显式欧拉只适合教学 Demo,工程里默认是半隐式欧拉。

为何常见:教材总是先讲显式欧拉,容易给人“入门即最优”的错觉,忽略了它在刚性方程下的稳定性灾难。

误区PBD 不也是质点和弹簧吗?把弹簧力换成位置修正,本质还是质点弹簧模型那一套吧。

正解两者本质不同:质点弹簧法是“力驱动”——先算力、再积分速度、最后更新位置,刚度参数没有直观的视觉含义,调参像玄学;PBD 是“约束驱动”——直接操作位置保证约束成立,迭代次数直接对应布料硬度,稳定性强、不易穿透。它俩是两套并行的世界观,不能混为一谈。

为何常见:因为 PBD 也常画成弹簧网格,表面上都是质点和线,容易误以为是同一算法换了求解器。

误区布料跟地面、墙壁碰撞就够了,衣服自己和自己怎么会撞到?我这个布料没有自碰撞需求。

正解裙摆、披风、宽袖在运动时内外层极易互相穿透,这是布料模拟最头疼的问题之一。不加自碰撞处理,角色一转身,裙摆就会像两片玻璃一样交错穿帮。必须用空间哈希 + 碰撞响应来保证布料层之间的合理滑动。

为何常见:日常经验里“物体穿过物体”很少见,但布料薄、层数多、运动快,自碰撞几乎必然发生,只是肉眼容易忽略。

误区GPU 有几千个线程,布料模拟里把线程数拉满,帧率肯定直接翻倍,无脑堆就行。

正解并行加速受 Amdahl 定律限制:总有一部分串行工作(约束依赖整理、碰撞对合并)不能并行,线程再多也要等它们。而且显存带宽有限,数据在显存间搬运可能比计算更贵。正确的做法是控制线程块大小、优化内存布局,而不是无脑开线程。

为何常见:人们容易把“核数多”等同于“无限加速”,忽略了串行瓶颈和访存带宽这两堵墙。

面试题

Q1. 布料模拟中,负责连接网格上下左右近邻的弹簧是哪一种?

单选题 · easy

  • A.结构弹簧← 正确答案
  • B.剪切弹簧
  • C.弯曲弹簧
  • D.碰撞弹簧

答案:A。结构弹簧只连接上下左右四个近邻,是布料最基础的“经线纬线”;剪切弹簧连斜对角,弯曲弹簧跨一个质点抗折叠;“碰撞弹簧”并非网络拓扑里的弹簧类型。

Q2. 根据胡克定律,弹簧弹力大小与哪个量成正比?

单选题 · easy

  • A.弹簧自身原本的长度
  • B.形变量即与原长的差值← 正确答案
  • C.所有质点的总质量
  • D.质点当前的瞬时速度

答案:B。胡克定律说弹力与形变量成正比——拉得越长,回弹越猛。原长本身、质量、速度都不是弹力的来源,弹力只与“被改变了多少”有关。

Q3. 半隐式欧拉与显式欧拉相比,关键区别在哪一步?

单选题 · mid

  • A.用更新后的速度推进位置← 正确答案
  • B.用上一帧的老速度推进位置
  • C.同时更新速度与位置两个量
  • D.先推位置再回头更新速度

答案:A。半隐式欧拉先用加速度更新速度,再用新速度更新位置;显式欧拉用旧速度更新位置,所以对时间步更敏感。这“先更新速度”的一步正是稳定性的关键。

Q4. 布料模拟中,用来抑制衣领翘边、防止布料过度折叠的机制是?

单选题 · mid

  • A.弯曲约束← 正确答案
  • B.剪切约束
  • C.拉伸约束
  • D.碰撞约束

答案:A。衣领翘边源于折叠过度,弯曲约束盯住二面角,角度偏了就给回拉力;剪切约束管斜拉,拉伸约束管长短,碰撞约束管穿插,都不直接管折叠。

Q5. PBD 方法区别于质点弹簧法的核心求解思路是?

单选题 · hard

  • A.对弹力积分再推位置
  • B.直接投影位置满足约束← 正确答案
  • C.用神经网络预测位置
  • D.完全不施加任何约束

答案:B。PBD 的灵魂是“位置投影”——直接把不满足约束的质点拉到正确位置,而不是先算力再积分。正因如此它稳定性强,适合实时游戏。

Q6. 布料自碰撞检测中,空间哈希的主要作用是?

单选题 · mid

  • A.缩小候选碰撞对的搜索范围← 正确答案
  • B.调整布料表面的颜色
  • C.替代弹簧网络的更新
  • D.加快贴图纹理的加载

答案:A。空间哈希把空间切成格子,每个质点只需查同格与近邻格,候选对被大幅裁剪,从 O(n²) 降到常数级。它跟颜色、弹簧更新、贴图无关。

Q7. 关于 GPU 布料模拟,下列说法正确的是?

单选题 · hard

  • A.弹力计算天然适合并行加速← 正确答案
  • B.GPU 只能渲染不能做物理
  • C.单线程运行反而速度最快
  • D.并行时没有任何通信开销

答案:A。质点弹簧计算彼此独立,GPU 天然适合;GPU 早就能做通用并行计算,单线程远慢于成百上千线程,并行也有带宽和同步开销。

Q8. 关于时间步长 Δt 的说法,正确的有哪些?

多选题 · mid

  • A.时间步过大会导致模拟失稳← 正确答案
  • B.时间步越小计算开销越大← 正确答案
  • C.时间步对稳定性毫无影响
  • D.显式方法更依赖小时间步← 正确答案

答案:ABD。Δt 过大时力还没来得及收敛就飞出去了,显式方法尤其严重;Δt 越小每帧要算的子步越多,开销越大;稳定性和 Δt 强相关,C 说“毫无影响”是错的。

Q9. PBD 相比经典质点弹簧法的优点包括?

多选题 · hard

  • A.稳定性好且不容易发散← 正确答案
  • B.约束直观便于实时调参← 正确答案
  • C.迭代求解可以完全省略
  • D.非常适合游戏实时应用← 正确答案

答案:ABD。PBD 稳定、直观、实时友好;但它仍然需要迭代求解来让约束误差在网络中传播,“完全省略迭代”会让约束形同虚设。

Q10. 布料模拟网络中常见的弹簧类型包括?

多选题 · easy

  • A.结构弹簧← 正确答案
  • B.剪切弹簧← 正确答案
  • C.弯曲弹簧← 正确答案
  • D.重力弹簧

答案:ABC。结构、剪切、弯曲是布料网络三大弹簧类型;“重力弹簧”不是拓扑元素,重力属于外力,不是质点间的连接关系。