
简介这份小巧的程序包面向计算机图形学、游戏开发及物理模拟方向的开发者通过DBM介电击穿模型与DLA扩散限制凝聚模型的Matlab实现解决从零模拟逼真闪电分支结构的问题适合特效制作、科研教学与算法学习场景使用。资源共包含5个Matlab脚本文件压缩包整体仅5KB其中THUNDER_DBM.m负责电场击穿主干路径计算THUNDER_DLA.m结合randwalk.m与DifPoss.m生成分形分支isadjacent.m用于判断通道连通性脚本按功能拆分清晰。该程序包目前已有1635人学习适合希望深入理解两种物理模型并快速上手闪电效果模拟的开发者。通过调整模型参数可以控制闪电主干与分支的形态、延伸速度及亮度表现同时借助两个模型的组合逻辑能够直观对比DBM与DLA在随机分形生成上的差异为后续扩展气象预报或游戏特效提供可运行的参考实现。 前阵子写了一个挺有意思的小程序核心目标就一个在屏幕上复现那种“咔嚓”一下的闪电分叉。方案上我同时用了两种经典算法一个是DBM介电击穿模型另一个是DLA扩散限制凝聚模型。如果你也想用代码模拟闪电、研究分形图案或者只是想找点漂亮的算法图形练手这篇文章就是为你准备的。我会把两条技术路线各自的原理、参数、Python实现和踩坑经验都摊开来说保证你能照着复现并且真的能跑出像模像样的闪电。1. 用模型捕捉闪电的“骨架”DBM与DLA的基本思路1.1 为什么闪电能被二维网格模拟出来闪电在视觉上最明显的特征是树枝状分叉这本质上是电介质击穿过程中电场尖端不断选择新的薄弱点继续放电的结果。把空间离散成二维网格每一个格子要么是“已击穿”导电要么是“未击穿”绝缘然后让击穿路径按概率逐步延伸就能得到一棵不断生长、不断分叉的“树”。这就是DBM的核心思想也是最常被用来生成Lichtenberg图案和电树的老牌算法。DLA虽然最初是研究悬浮粒子凝聚的模型但它同样会产生类似的多分支结构粒子从远处随机游走到凝聚体表面并粘附随着粒子越堆越多枝杈向外延伸形态上很像闪电的骨架。我一开始其实先用DLA做了一版因为实现起来最简单后来为了追求更接近真实放电的细节才又补了DBM版本。两版程序跑出来的图放一起对比很有意思DLA的枝杈偏圆润、饱满像铅树DBM的枝杈则更尖锐、锐利更像高压放电的瞬间形态。1.2 两个模型的共同点与核心分歧两个模型都建立在一个底层的“生长博弈”之上某个点是否被选中成为新的结构取决于一个概率分布而这个概率分布又由局部的物理量决定。DLA用粒子碰撞的高频事件表达“哪里更可能接住粒子”DBM用局部电场强度决定“哪里更容易打穿绝缘层”。它们的分歧也很明显。DLA没有真正的“场”存在随机游走粒子只受接触到的固体影响属于短程相互作用DBM则需要求解整个空间的电势场每个边界点的生长概率和该点的电场强度η次幂成正比属于长程相互作用。这个“η”是可调的它直接决定了分叉的稀疏度、细长度和整体形态后面我会专门讲参数怎么选。对比维度DBMDLA物理背景电介质击穿、雷电先导扩散限制凝聚、胶体沉积核心机制拉普拉斯场 概率生长随机游走 表面粘附计算开销每步需迭代求解电势耗时较高每步只需移动粒子非常快形态特征分支窄、锐利、稀疏分支宽、圆润、致密关键参数η电场指数、网格尺寸、边界条件释放半径、步长、粒子数实际写程序的时候我强烈建议先把DLA跑通再用DBM去打磨细节。毕竟DLA的代码量很少3分钟就能看到一棵分形树DBM的代码量和调试成本都高一个量级但产出效果更惊艳尤其是渲染成动画时那种一步步往前跳的火花感特别逼真。2. DBM核心细节与实战配置2.1 数学建模拉普拉斯方程 概率生长DBM的模型基础是拉普拉斯方程。把空间看作一个二维导体场已击穿区域视为等电势导体未击穿区域则是一个阻性介质。简化处理时我用了一个很常见的做法顶部边界固定为高电位1底部边界固定为0左右边界设为绝缘或周期边界都可以初始种子点放在顶部中心它一旦出现就整根通道都变成高电位。接下来每轮做两件事求解当前未击穿区域内的电势分布我用的是高斯-赛德尔松弛迭代公式很简单φ[i][j] 0.25 * (φ[i1][j] φ[i-1][j] φ[i][j1] φ[i][j-1])遍历所有未击穿但紧邻已击穿区域的“候选点”计算它们的局部电场。电场近似取作相邻格点电势差的绝对值通常在二维离散网格里用E |φ[neighbor] - φ[seed]|因为种子区域电势接近高压所以可以用“未击穿点自己与周围四个点的最大电势差”来简化。然后每个候选点的生长概率为p_i (E_i^eta) / sum(E_j^eta)按照这个概率加权随机选一个点把它标记为击穿加入通道刷新边界集合再重新求解电势。反复迭代直到通道长度达到设定值或者放电到达底部边界。可以想得更直观一点把大楼的电路想象成一块橡皮泥尖端上“电力”越强越容易被选中。η越大高电场的尖端优势被进一步放大分支于是越少、越细长。η接近0时所有候选点的概率几乎拉平生长出来的图案就会变得比较蓬松、密实甚至像一坨花菜。2.2 网格初始化与边界条件我最初用的网格是200x200初始种子点设置在顶部中间约(1, 100)底部整行是接地电位。这里有个细节如果顶部和底部之间距离太小闪电还没怎么分叉就打到底了图像很呆。所以想让分叉舒展开起止位置间距要留够我最后调整到300x300跑到大约一半高度就停下效果立刻好了很多。边界条件可以简单处理成上边界固定为1下边界固定为0左右边界用对称边界也就是φ[0][j] φ[1][j]和φ[n-1][j] φ[n-2][j]。至于种子点其实不需要非得放在顶边模拟从云层尖端放电就觉得放在顶边下一点更好比如(2, 150)这样上方还有一小段介质区看起来更像云层中孕育出来的。注意每次击穿一个新点后整个通道都被视为高电位所以松弛迭代的时候要把已击穿点的电势固定住不能在迭代中更新。这个约束很容易漏漏了就会导致生长路径“跳”得很诡异。2.3 参数η如何“指挥”闪电分支η是DBM最灵魂的参数也是新手最容易调到怀疑人生的地方。我实测下来的经验是η1.2 ~ 1.5分支细而长像高压线上的放电痕迹也最接近照片中的闪电分支。η0.5 ~ 1.0分支变多整体更密集像植物根系。η0.5图形会变得胖胖的、云团感很强失去“闪电感”。η2分支极少有时直接一根主干直插到底分叉率低到像小木棍。为什么会有这种规律因为η控制着“尖端效应”的反馈强度。高η让已经突出的尖端获得更高的选择概率于是它更倾向于继续往前冲而不是向侧面开岔。低η则消除了这种优势各方向机会均等图案就蓬松了。真实闪电的流光机制里尖端优先发展确实普遍存在所以取η在1.2到1.8之间通常能获得最具“雷电味”的画面。实际运行中我还会加一个很小幅度的随机扰动到E上比如E E 1e-6用来避免多个候选点电场为0导致概率分母为0。3. DLA核心细节与实战配置3.1 从随机游走到凝聚DLA过程非常“机械派”先在中心放一个种子点然后从某个半径处释放一个粒子粒子执行布朗运动也就是每一步随机往四个方向挪一格直到碰到凝聚体的某个相邻格粒子就固定下来成为凝聚体的一部分之后再释放下一个粒子。为了控制计算量粒子如果游走到超过某个距离比如最大凝聚半径的5倍就丢弃重来。我一开始直接照搬教科书实现但立刻发现一个问题生成的图案完全没有任何方向性四面均匀向外长最后出来一个圆形毛球。虽然也是分形结构但总不能骗自己说那是闪电。解决方法是给随机游走加一个“漂移偏置”粒子每个轴向移动的概率不再均分而是给向下方向更高权重比如down: up: left: right 0.4 : 0.25 : 0.175 : 0.175。这样粒子整体上会向下漂移凝聚体也被“拉”成纵向延展的形状更像一道从天而降的闪电主通道。3.2 粒子释放半径与步长的平衡DLA的效率瓶颈主要在线粒子的漫长游走过程。粒子老远释放、乱逛很久还没碰到凝聚体时间就白白浪费了。我建议将释放半径设成maxRadius 5其中maxRadius是当前凝聚体距离种子点的最大距离每加进去一个新粒子都实时更新它。游走的上限区域可以设为释放半径 50这个范围够了再大只会拖慢速度。步长也可以加速粒子离凝聚体很远时可以让它一次跳多格。我实测把远距离步长变成8近距离距离边界5格以内再一步步走速度提升非常明显结果形态没有可感知差异。这个方法也适用于DBM算是通用优化技巧。代码也不复杂核心循环就是一个模拟粒子移动、判断邻接、决定粘附或丢弃的过程。后面放心我会给一个可以直接跑的版本。4. 实际运行代码、加速技巧与可视化4.1 一份可运行的DBM模拟核心代码我直接用Python写了个演示程序依赖numpy和matplotlib。为了简化这里只放出核心生长逻辑做渲染的时候稍加修饰即可。import numpy as np import random def dbm_grow(size150, eta1.4, max_len1200, seed(2, size//2)): # 0: 未击穿, 1: 已击穿通道 grid np.zeros((size, size)) grid[seed] 1 channel [seed] top_potential 1.0 bottom_potential 0.0 def solve_potential(grid): phi np.zeros_like(grid, dtypefloat) phi[0, :] top_potential phi[-1, :] bottom_potential # 初始化中间随便给个初值 phi[1:-1, 1:-1] 0.5 cond grid.astype(bool) phi[cond] top_potential # 高斯-赛德尔迭代 for _ in range(300): new_phi phi.copy() for i in range(1, size-1): for j in range(1, size-1): if grid[i, j] 1: continue new_phi[i, j] 0.25 * (phi[i1, j] phi[i-1, j] phi[i, j1] phi[i, j-1]) phi new_phi phi[cond] top_potential phi[0, :] top_potential phi[-1, :] bottom_potential return phi while len(channel) max_len: phi solve_potential(grid) frontier set() for (i, j) in channel: for di, dj in ((1,0),(-1,0),(0,1),(0,-1)): ni, nj idi, jdj if 0 ni size and 0 nj size and grid[ni, nj] 0: frontier.add((ni, nj)) candidates [] weights [] for (i, j) in frontier: e max(abs(phi[i, j] - phi[i1, j]), abs(phi[i, j] - phi[i-1, j]), abs(phi[i, j] - phi[i, j1]), abs(phi[i, j] - phi[i, j-1])) e max(e, 1e-9) candidates.append((i, j)) weights.append(e ** eta) weights np.array(weights) probs weights / weights.sum() chosen candidates[np.random.choice(len(candidates), pprobs)] grid[chosen] 1 channel.append(chosen) return grid这个实现为了清晰牺牲了一点性能每次击穿一个新点后都完整迭代300次电势。如果网格是200x200跑起来会有明显卡顿我通常会把迭代次数压缩到80到100次因为局部电势已经足够稳定误差对概率分布的影响肉眼基本看不出来。4.2 一份可运行的DLA模拟核心代码DLA的代码就轻快多了。下面的版本带垂直漂移能生成偏纵向的“类闪电”树。import numpy as np def dla_grow(size200, max_particles1500, seed(size//2, size//2), bias(0.25, 0.4, 0.175, 0.175)): # up, down, left, right grid np.zeros((size, size), dtypebool) grid[seed] True particles [seed] max_r 0 for _ in range(max_particles): # 计算最大半径并确定释放点 if particles: max_r max(np.hypot(x-seed[0], y-seed[1]) for (x, y) in particles) release_r int(max_r 5) theta np.random.uniform(0, 2 * np.pi) pos (seed[0] int(release_r * np.cos(theta)), seed[1] int(release_r * np.sin(theta))) stuck False for _ in range(100000): # 越界或飘太远就丢弃 if not (0 pos[0] size-1 and 0 pos[1] size-1): break if pos[0]**2 pos[1]**2 (max_r 50)**2: break r np.random.rand() if r bias[0]: step (-1, 0) elif r bias[0] bias[1]: step (1, 0) elif r bias[0] bias[1] bias[2]: step (0, -1) else: step (0, 1) new_pos (pos[0] step[0], pos[1] step[1]) if not (0 new_pos[0] size-1 and 0 new_pos[1] size-1): break pos new_pos # 判断是否与凝聚体相邻 touched False for di, dj in ((1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)): ni, nj pos[0]di, pos[1]dj if 0 ni size and 0 nj size and grid[ni, nj]: touched True break if touched: grid[pos] True particles.append(pos) stuck True break if not stuck: continue return grid代码里有个关键细节判断邻接时用8邻域这样更容易形成较粗的分支形态往往比四邻域更接近真实的闪电主干。缺点是需要多写两条对角线判断但换来的是更好的视觉效果很划算。4.3 渲染成“闪电”的小技巧模型跑出来只是一张二值图直接拿imshow展示就像一张黑白电路板完全没有闪电的冲击力。我在渲染上做了几步处理效果立刻不一样给通道加光晕用scipy.ndimage.gaussian_filter对二值图模糊一下然后把原图与模糊图像叠加。核心是暗底、亮线、外圈弥散光三者在视觉上会产生“发光”感。颜色映射不要用默认的binary色带改成plt.cm.hot或plt.cm.inferno亮的部分是白/黄暗底是深蓝黑瞬间就有雷电主题了。给主干增加白核把二值图再腐蚀一层只剩最中心的通道涂成纯白外围用颜色渐晕就会产生“中心放电”的感觉。这三招成本极低完全不需要额外库但视觉收益极高也是整个项目里最让我满意的一步。5. 常见问题与避坑经验5.1 DBM慢/不收敛DBM最容易被卡死的点是电势迭代。我用过最暴力的方法是每轮迭代500次一个300x300的网格跑到2000步能让你等出一杯咖啡的时间。后来我做了三个改良只在已击穿通道周围的局部区域计算电势远处固定为合理初值省掉大量无效迭代。将迭代误差阈值放宽到1e-4DBM对电场绝对值不敏感它只敏感于电场之间的相对大小所以粗一点完全没问题。改用“批量生长”连续选若干候选点一起击穿而不是一次一个点。这个会稍微改变形态但视觉差异很小速度能再提三分之一。5.2 DLA吸附不上DLA粒子经常在凝聚体周围晃半天就是不粘上或是直接跑飞。我遇到过最大的坑是释放半径太小粒子一出生就粘在凸出尖端上最后长成一个尖刺丛不够自然。解决办法是每次更新释放半径时保留一个凸出因子比如release_r max_r * 1.1 5让粒子有一定空间绕到比较深的位置再触发粘附。还有一种情况是网格边界判断写错粒子在边界处被默默丢弃尤其是使用8邻域时ni,nj很容易出界。务必在判断前加上边界检查否则你会看到一个缺了大半边脸的“半拉闪电”。5.3 形态不像闪电怎么办如果DBM分支太乱先降η如果分支太少像个棍子就升η。如果DLA看起来太圆就加强垂直方向的漂移权重或者加上重力方向的随机扰动。这里我可以分享一个额外的小技巧在DBM生长时给down方向的候选点额外加一个微小的权重加成比如weights * 1.0但i seed[0]的点weights * 1.05这样模拟的闪电会更倾向于向下蔓延更符合“从云到地”的直觉。这一加不会破坏分形性质但造型立刻正向了很多。5.4 衡量结果分形维度测量模拟闪电到底像不像有一个客观指标分形维度。真实闪电的二维投影分形维度大致在1.2到1.5之间。盒子计数法是最简便的度量方式把图缩放到不同盒子尺寸s统计需要多少个边长为s的格子才能覆盖通道然后对log(N)和log(1/s)做线性回归斜率就是分形维度。我测过自己的结果DLA版本算出来约1.55DBM版本在η1.4时约1.35确实落在真实闪电的区间。这个指标极大的方便了我调参因为光凭肉眼容易自我欺骗算一个数字出来心里才有底。如果要追求更进一步的“真实感”可以考虑把DBM扩展到三维或者引入时间相关的先导步进模型。但作为模拟闪电的入门方案DBM和DLA已经足够给你提供丰富的枝干结构之后再叠加噪声、形变和色彩就能做出很唬人的视觉作品。我自己在实际操作中体会最深的一点是不要一上来就追求复杂的物理模型。先用DLA把整个渲染管线和参数面板打通再用DBM替换核心生长逻辑调试成本会大大降低。最后再分享一个小技巧生成闪电时可以把每张图当成一帧连续保存后用少量插值帧组合成动态闪烁效果视觉震撼力远超静态图。这一步虽然有额外工作量但看到屏幕上自己算出来的“闪电”跳出的那一刻你会觉得之前的折腾全值了。本文还有配套的精品资源点击获取