Notifications
Article
【重现】天涯明月刀中分形闪电的Unity实现
Updated a year ago
916
0
【重现】天涯明月刀中分形闪电的Unity实现
知乎地址:https://zhuanlan.zhihu.com/p/38785982

我把闪电的制作拆分成以下阶段:
  1. 闪电路径的制作
  2. 网格的生成
  3. 渲染
  4. 后处理

路径

在如何评价腾讯的 QuicksilverX 游戏引擎?这个回答中提到了,【(天涯明月刀的闪电)是运用分形法实时生成,在计算闪电的枝杈与扭曲程度时引入概率和随机值】。
那么分形的概念,在此不再赘述。通俗地来讲,分形就是其每一部分与其整体拥有自相似性的性质。分子的布朗运动、花菜的形态,都具有类似的性质。事实上,数学家们还能利用分形理论,人为地创造出非常瑰丽的图案。鉴于这些图案极有可能引发密集恐惧症者的不适,我建议大家自己去搜搜看。
这里计算闪电路径使用的是中点位移法[1][2],也就是对线段取中点后计算出一个随机的偏移值令其位移。我们可以将闪电划分成无数Z型和H型片段的集合,所谓Z型和H型,就是闪电路径拐点处有无分岔。而H型片段仅仅是比Z型多了一个顶点而已。
写出生成片段的函数,随后递归调用,我们就得到了随机化的闪电路径。
private void GetFractcalLightning(LightningSegment lightningSegment, int fractalTime, int maxFractalTime, float baseAttenuation, Vector2 offsetRange, Vector3 direction, Vector3 forward, Vector3 right, float branchChance = 0f) { if (!lightningSegment.isLeaf() || fractalTime > maxFractalTime) return; //制作Z型片段 float zOffset = Mathf.Exp(-baseAttenuation * fractalTime) * Random.Range(offsetRange.x, offsetRange.y); float xOffset = Mathf.Exp(-baseAttenuation * fractalTime) * Random.Range(offsetRange.x, offsetRange.y); Vector3 mid = lightningSegment.Middle() + xOffset * right + zOffset * forward; lightningSegment.LeftChild = new LightningSegment(lightningSegment.start, mid); lightningSegment.RightChild = new LightningSegment(mid, lightningSegment.end); GetFractcalLightning(lightningSegment.LeftChild, fractalTime + 1, maxFractalTime, baseAttenuation, offsetRange, direction, forward, right, branchChance); GetFractcalLightning(lightningSegment.RightChild, fractalTime + 1, maxFractalTime, baseAttenuation, offsetRange, direction, forward, right, branchChance); if (Random.Range(0f, 1f) < branchChance) { //制作H形片段 zOffset = Mathf.Exp(-baseAttenuation * fractalTime) * Random.Range(offsetRange.x, offsetRange.y); xOffset = Mathf.Exp(-baseAttenuation * fractalTime) * Random.Range(offsetRange.x, offsetRange.y); float yOffset = Mathf.Exp(-baseAttenuation * fractalTime) * Random.Range(offsetRange.x, offsetRange.y); Vector3 branchEnd = lightningSegment.Middle() + xOffset * right + zOffset * forward + yOffset * direction; lightningSegment.Branch = new LightningSegment(mid, branchEnd); GetFractcalLightning(lightningSegment.Branch, fractalTime + 1, maxFractalTime, baseAttenuation, offsetRange, direction, forward, right, branchChance); } }
之后我们使用Gizmos.DrawLine方法将闪电画出来。

网格

网格这块之前钻了牛角尖,一直想着怎么把整个闪电做成一个整体的Mesh,后来硬着头皮将每个线段单独做成一个面片,发现效果很好,仔细一想,原来我把闪电递归分划了10次,每个片段已经非常小了,因此做成面片之后,其不连续的部分就被掩盖了。
private void SegmentToMesh(LightningSegment lightningSegment, List<Vector3> vertices, List<int> triangle, List<Vector2> uv, Vector3 obPos, float radius, float attenuation, ref int idx) { if (lightningSegment.isLeaf()) { Vector3 zMid = lightningSegment.Middle(); Vector3 zNormal = Vector3.Cross(zMid - obPos, lightningSegment.end - lightningSegment.start).normalized; var finalR = radius * attenuation; if (lightningSegment.Branch == null) { //Z型片段的Mesh生成 vertices.Add(lightningSegment.start + finalR * zNormal); vertices.Add(lightningSegment.start - finalR * zNormal); vertices.Add(lightningSegment.end + finalR * zNormal); vertices.Add(lightningSegment.end - finalR * zNormal); triangle.Add(idx + 1); triangle.Add(idx); triangle.Add(idx + 2); triangle.Add(idx + 1); triangle.Add(idx + 2); triangle.Add(idx + 3); idx += 4; } } else { SegmentToMesh(lightningSegment.LeftChild, vertices, triangle, uv, obPos, radius, attenuation, ref idx); SegmentToMesh(lightningSegment.RightChild, vertices, triangle, uv, obPos, radius, attenuation, ref idx); if (lightningSegment.Branch != null) SegmentToMesh(lightningSegment.Branch, vertices, triangle, uv, obPos, radius * attenuation, attenuation, ref idx); }

渲染

为了让闪电呈现出一种动态蔓延的感觉,我们需要使用一张贴图指示闪电中每个面片的透明度门槛,未达到该门槛的统一Clip掉。

然后为Mesh布UV的时候也要注意,因为某些分支有可能出现向上的情况,这种情况下如果仅仅使用距离终点的距离去判断,会出现分支闪电末端出现于分岔之前的问题。即便我们修改Offset令其只能向下,那么蔓延的效果也会大打折扣,看起来就像是用刷子刷出来的一样。

我这里采用的是递归的时候判断一下当前Segment在整个闪电中的位置,这样分叉末端无论位置如何,永远在分叉起点处之后展现。为了节省大量【* 0.5f】的操作,我专门使用一个数组储存了0.5的n次幂。
…… lightningSegment.LeftChild = new LightningSegment(lightningSegment.start, mid); lightningSegment.LeftChild.fractal = fractalTime + 1; lightningSegment.LeftChild.uv = lightningSegment.uv - LightningCreator.arrayForUV[lightningSegment.LeftChild.fractal]; lightningSegment.RightChild = new LightningSegment(mid, lightningSegment.end); lightningSegment.RightChild.fractal = fractalTime + 1; lightningSegment.RightChild.uv = lightningSegment.uv + LightningCreator.arrayForUV[lightningSegment.RightChild.fractal]; …… Vector3 branchEnd = lightningSegment.Middle() + xOffset * right + zOffset * forward + yOffset * direction; lightningSegment.Branch = new LightningSegment(mid, branchEnd); lightningSegment.Branch.fractal = fractalTime + 1; lightningSegment.Branch.uv = lightningSegment.uv + LightningCreator.arrayForUV[lightningSegment.Branch.fractal];

后处理

众所周知,闪电降临的时候必然有着耀眼的光芒。一般来说,这种耀眼在视觉上体现为其亮度扩散到周围的景物上。
这里我直接践行拿来主义,用冯乐乐的《Unity Shader入门精要》的Bloom特效[3],然后经过魔幻调参——啊,顺带一提,此书精妙绝伦,深入浅出,实乃Shader学习之必备,居家旅行,老少咸宜……

结语

本文实际上是对我接触到的技术的一个总结,值得一提的是学习了中点位移法,此法会在今后进行过程化生成技术的时候有很多用途。
本文的实现方法并不完美,主要的消耗在随机数和递归上。针对这两种原因,我个人认为有以下方法提升性能:现在闪电是3D的,如果对距离不敏感,可以将闪电转换为2D,此时可以省去一个方向的计算;自定义自己的伪随机数系统。
Github:https://github.com/noobdawn/Fractal-Lightning-Unity

参考文献

  1. https://blog.csdn.net/u012945598/article/details/18862091
  2. http://kns.cnki.net/KCMS/detail/detail.aspx?dbcode=CJFQ&dbname=CJFD2011&filename=SJSJ201110067&v=MTE3Mjg3RGgxVDNxVHJXTTFGckNVUkxLZllPWnRGeW5nVnJ2Sk5pZllaTEc0SDlETnI0OURZNFI4ZVgxTHV4WVM=
  3. https://github.com/candycat1992/Unity_Shaders_Book
N
Noobdawn
3
Comments