【重现】天涯明月刀中分形闪电的Unity实现

战棋的剧情和对话系统又走错路了,我试图按照传统GAL的方法,单纯通过对话之间的联结来管理整个剧情,事实证明这样不符合我的要求。所以“蒸汽机车”这边继续开坑,搞一个《重现》系列的坑,试图在Unity中重现其他游戏中一些比较惊艳的表现。


我把闪电的制作拆分成以下阶段:

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方法将闪电画出来。

《【重现】天涯明月刀中分形闪电的Unity实现》
当然,反复调参不可免

网格

网格这块之前钻了牛角尖,一直想着怎么把整个闪电做成一个整体的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);
}
《【重现】天涯明月刀中分形闪电的Unity实现》

渲染

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

《【重现】天涯明月刀中分形闪电的Unity实现》
用到的AlphaTex

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

《【重现】天涯明月刀中分形闪电的Unity实现》
红色分支逆势而行

我这里采用的是递归的时候判断一下当前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实现》
断续的地方是因为我开了摄像机的正交模式

后处理

众所周知,闪电降临的时候必然有着耀眼的光芒。一般来说,这种耀眼在视觉上体现为其亮度扩散到周围的景物上。

这里我直接践行拿来主义,用冯乐乐的《Unity Shader入门精要》的Bloom特效[3],然后经过魔幻调参——啊,顺带一提,此书精妙绝伦,深入浅出,实乃Shader学习之必备,居家旅行,老少咸宜……

《【重现】天涯明月刀中分形闪电的Unity实现》
闪电周围的光晕

结语

最终效果如下:

超级武器·闪电风暴已就绪!

本文实际上是对我接触到的技术的一个总结,值得一提的是学习了中点位移法,此法会在今后进行过程化生成技术的时候有很多用途。

本文的实现方法并不完美,主要的消耗在随机数和递归上。针对这两种原因,我个人认为有以下方法提升性能:现在闪电是3D的,如果对距离不敏感,可以将闪电转换为2D,此时可以省去一个方向的计算;自定义自己的伪随机数系统。

顺带一提,从本文开始,文章亦会同步到Unity官方中文论坛,有兴趣的开发者可以关注一哈。

Github:

noobdawn/Fractal-Lightning-Unity​github.com

参考文献

[1]Cocos2d-x教程(20)-闪电效果 – CSDN博客

[2]基于多重细分的闪电仿真方法 – 中国知网

[3]candycat1992/Unity_Shaders_Book

 

from zhihu 破晓

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注