rainyeve
  • 首页
  • 编程
  • 涂鸦
  • 其他
  • 关于
2019年8月21日
编程

【Unity网格编程】SkinnedMeshRenderer Decal 实现斩击伤痕贴花效果

【Unity网格编程】SkinnedMeshRenderer Decal 实现斩击伤痕贴花效果
2019年8月21日
编程

实现效果

使用新建网格的方式实现贴花效果,模拟被斩击的伤痕,再外加一个材质动画来控制伤痕褪色。想法来自最近玩鬼泣5的时候发现敌人被砍的伤痕都会有划痕。之前单单用shader试着实现,但是贴花不能稳定的跟随网格顶点。现在改为复制蒙皮的方式,划痕百分百贴合并跟随顶点。期间还试着加法线贴图让划痕立体一些,后来发现划痕太细,镜头稍远,加了法线贴图也看不出效果来。

decal preview gif
贴纸稳定依附在动态的蒙皮网格上

贴花(Decal)方式

对于场景中静态物品(比如墙壁,地板),有Command Buffer和projector等方式来实现贴花,它们有个共通点是都是在一个“盒子”空间中来对准要贴的目标做“投影”。

command buffer decal
Unity官方Manual上的关于Command Buffer实现贴花的示例
decal preview bad
贴花投影到蒙皮上位置不稳定

但是,在带有蒙皮动画的角色身上做贴花,这种靠“盒子”的方式看起来效果是不理想的。上图可以看到当敌人身体扭动的时候,其身上的红线贴花没有稳定依附在蒙皮上,而是相对地位移和旋转了一小段距离,红线“伤痕”看上去反而像扫描仪的光线。在把贴花“投影”到蒙皮的时候,“摄影机”对准的不是硬实的画板,而是一张不稳定的可以扭曲的画布。在蒙皮上做贴花,仅仅把影像投影上去是不行的,要真正地“把油漆涂抹上去”,比如修改网格的贴图,在unity asset store中能找到UV Paint等插件,只有dll没有源码,但看演示估计应该就是动态修改贴图来达到UV Paint的效果。

uv paint 插件
Asset Store 中的 UVPaint插件

至于我的实现方式,是从蒙皮网格上截取一块网格,对这块网格重新设置三角形和uv以及材质,然后放回原来的骨架中,整个过程中顶点是不做改动的,保证了蒙皮能够重新回到原骨架,贴图能稳定依附网格,但是缺点是每次生成贴花都要新复制一份蒙皮,对于顶点数多的模型会造成不小的内存开销,但我目前做的是lowpoly风格的游戏,模型顶点数是较少的。起初参考了CSDN的puppet_master大神的mesh decal方式,调试了一轮后发现并不适合用于蒙皮网格,该mesh decal方式是重新生成顶点,这样一来新的网格是无法用回原有的骨架的。

生成网格

首先复制一份网格,然后要调用SkinnedMeshRenderer.BakeMesh方法来对蒙皮网格做snapshot,以此来获取当前时刻的网格信息,仅用作截取时test用。如果是一般的网格可以仅用meshFilter来获取网格,而SkinnedMeshRenderer的sharedmesh始终是未做骨骼动画之前的状态,不调用BakeMesh是无法拿到实时的蒙皮网格信息的。

Mesh decalMesh = Mesh.Instantiate(skinnedMeshRenderer.sharedMesh);
Mesh bakeMesh = new Mesh();
skinnedMeshRenderer.BakeMesh(bakeMesh);
_bakedVertices = bakeMesh.vertices;
_bakedormals  = bakeMesh.normals;	
Vector2[] uvs = decalMesh.uv;

遍历全部三角形,凡是有顶点在平面内的三角形,都算作截取的目标,最后给新复制的网格设置新的三角形数组,覆盖掉原有的三角形

List triangleList = new List();
int[] triangles = decalMesh.GetTriangles(subMesh);
for (int i = 0; i < triangles.Length; i += 3)
{
if (isInsideFrustum(triangles[i], triangles[i + 1], triangles[i + 2], ref uvs))
{
    triangleList.Add(triangles[i]);
    triangleList.Add(triangles[i + 1]);
    triangleList.Add(triangles[i + 2]);
}
}
decalMesh.SetTriangles(triangleList.ToArray(), subMesh);

截取test和UV设置

组一个VP矩阵,用unity自带的工具GeometryUtility.CalculateFrustumPlanes生成6个用于test的平面

Matrix4x4 v = Matrix4x4.Inverse(Matrix4x4.TRS(originPoint, Quaternion.LookRotation(projectForward, projectUpword), new Vector3(1, 1, -1)));
Matrix4x4 p = Matrix4x4.Ortho(-sizeX, sizeX, -sizeY, sizeY, 0.0001f, depth);
_VP = p * v;
_testPlanes = GeometryUtility.CalculateFrustumPlanes(_VP);

test用的平面都是在世界坐标下,所以要先把顶点从模型坐标转到世界坐标再做test。为了优化效果还做了normal test,凡是顶点朝向偏差过大的都不通过test。

uv的设置要先在投影坐标系下进行,用MVP矩阵对模型空间的顶点做坐标系转换,然后做Remap把[-1,1]的投影空间转换到[0,1]的uv空间.

重新绑定骨架

SkinnedMeshRenderer的quality等属性都要保持一致,否则新旧蒙皮不能完美重合。接收阴影等开关则可以关闭掉以节约开销。

完整代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

//必须放在SkinnedMeshRenderer的最大的parent上,否则计算顶点的时候scale值会不正确
public class DecalSet : MonoBehaviour
{
	public BoxCollider debugBox;//仅用于调试
	public SkinnedMeshRenderer skinnedMeshRenderer;
	public Material decalMaterial;

	public int maxDecalCount = 10;
	public float normalFactor = 0f;
	public float fadeSpeed = 0.5f;

	List _curDecalList = new List();

	int _decalCount = 0;
	Vector3[] _bakedVertices;
	Vector3[] _bakedNormals;
	Matrix4x4 _VP;
	Vector3 _projectDir;
	float _uvRot;
	Plane[] _testPlanes;

	void Update() {
		for(int i=0; i<_curDecalList.Count; i++) { 
                        SkinnedMeshRenderer smr = _curDecalList[i]; 
                        if(smr==null) return; 
                        Material mat = smr.material; 
                        float emissionStrength = mat.GetFloat("_EmissionStrength");
                        if(emissionStrength>0) mat.SetFloat("_EmissionStrength", emissionStrength-Time.deltaTime*fadeSpeed);
		}
	}

	public void AddDecal(float offset, Vector3 originPoint, Vector3 projectForward, Vector3 projectUpword, float sizeX = 0.2f, float sizeY = 0.2f, float depth = 1, float rotation = 0)
	{
		_decalCount++;

		if (_decalCount > maxDecalCount)
		{
			Destroy(_curDecalList[0].gameObject);//这里是删除skinMeshRedenderer依附的gameObject,而不是单单删除skinMeshRenderer组件
			_curDecalList.RemoveAt(0);
			_decalCount--;
		}

		_uvRot = rotation;
		_projectDir = projectForward;

		originPoint -= _projectDir*offset;

		Matrix4x4 v = Matrix4x4.Inverse(Matrix4x4.TRS(originPoint, Quaternion.LookRotation(projectForward, projectUpword), new Vector3(1, 1, -1)));

		Matrix4x4 p = Matrix4x4.Ortho(-sizeX, sizeX, -sizeY, sizeY, 0.0001f, depth);

		_VP = p * v;

		_testPlanes = GeometryUtility.CalculateFrustumPlanes(_VP);

		Mesh decalMesh = Mesh.Instantiate(skinnedMeshRenderer.sharedMesh);

		Mesh bakeMesh = new Mesh();
		skinnedMeshRenderer.BakeMesh(bakeMesh);

		_bakedVertices = bakeMesh.vertices;
		_bakedNormals  = bakeMesh.normals;
	
		Vector2[] uvs = decalMesh.uv;

		for (int subMesh = 0; subMesh < decalMesh.subMeshCount; subMesh++)
		{
			List triangleList = new List();
			int[] triangles = decalMesh.GetTriangles(subMesh);

			for (int i = 0; i < triangles.Length; i += 3)
			{
				if (isInsideFrustum(triangles[i], triangles[i + 1], triangles[i + 2], ref uvs))
				{
					triangleList.Add(triangles[i]);
					triangleList.Add(triangles[i + 1]);
					triangleList.Add(triangles[i + 2]);
				}
			}

			if (triangleList.Count < 3)
				continue;

			decalMesh.SetTriangles(triangleList.ToArray(), subMesh);
		}
		decalMesh.uv = uvs;

		GameObject decalGO = new GameObject("decal");
		decalGO.transform.parent = skinnedMeshRenderer.transform;

		SkinnedMeshRenderer decalSkinRend = decalGO.AddComponent();
		decalSkinRend.quality = skinnedMeshRenderer.quality;
		decalSkinRend.shadowCastingMode = ShadowCastingMode.Off;
		decalSkinRend.sharedMesh = decalMesh;
		decalSkinRend.bones = skinnedMeshRenderer.bones;
		decalSkinRend.rootBone = skinnedMeshRenderer.rootBone;
		decalSkinRend.sharedMaterial = decalMaterial;
		decalSkinRend.lightProbeUsage = LightProbeUsage.Off;
		decalSkinRend.reflectionProbeUsage = ReflectionProbeUsage.Off;

		_curDecalList.Add(decalSkinRend);

		Destroy(bakeMesh);
	}

	bool isInsideFrustum(int t1, int t2, int t3, ref Vector2[] uvs)
	{
		Vector3 v1 = _bakedVertices[t1];
		Vector3 v2 = _bakedVertices[t2];
		Vector3 v3 = _bakedVertices[t3];

	        Matrix4x4 localToWorldMatrix = transform.localToWorldMatrix;
		
		for (int i = 0; i < 6; i++)
		{
			Vector3 world1 = localToWorldMatrix.MultiplyPoint(v1);
			Vector3 world2 = localToWorldMatrix.MultiplyPoint(v2);
			Vector3 world3 = localToWorldMatrix.MultiplyPoint(v3);

			// Debug.DrawLine(world1, world2, Color.red, 10f);
			// Debug.DrawLine(world2, world3, Color.red, 10f);

			if (!_testPlanes[i].GetSide(world1) && !_testPlanes[i].GetSide(world2) && !_testPlanes[i].GetSide(world3))
				return false;
			if (!FacingNormal(world1, world2, world3))
				return false;
		}

		//MVP矩阵把模型坐标系中的顶点转换到投影坐标系中,投影坐标系正是为了uv服务
		uvs[t1] = _VP * localToWorldMatrix * new Vector4(v1.x, v1.y, v1.z, 1);
		uvs[t2] = _VP * localToWorldMatrix * new Vector4(v2.x, v2.y, v2.z, 1);
		uvs[t3] = _VP * localToWorldMatrix * new Vector4(v3.x, v3.y, v3.z, 1);

		//从[-1,1]的投影坐标系Remap到[0,1]的uv坐标系,要乘以0.5然后加0.5
		uvs[t1] *= 0.5f;
		uvs[t2] *= 0.5f;
		uvs[t3] *= 0.5f;

		//允许贴图做z轴旋转
		Quaternion rot = Quaternion.Euler(0, 0, _uvRot);
		uvs[t1] = rot * uvs[t1];
		uvs[t2] = rot * uvs[t2];
		uvs[t3] = rot * uvs[t3];

		uvs[t1] += new Vector2(0.5f, 0.5f);
		uvs[t2] += new Vector2(0.5f, 0.5f);
		uvs[t3] += new Vector2(0.5f, 0.5f);


		return true;
	}

	bool FacingNormal(Vector3 v1, Vector3 v2, Vector3 v3)
	{
		Plane plane = new Plane(v1, v2, v3);

		if (Vector3.Dot(-_projectDir.normalized, plane.normal) < normalFactor)
			return false;

		return true;
	}

        [ContextMenu("Gen Decal")]
	public void GenDecal() {
		AddDecal(
			0f,
			debugBox.transform.position-debugBox.transform.forward*debugBox.size.z/2*debugBox.transform.localScale.z, 
			debugBox.transform.forward,
			debugBox.transform.up,
			debugBox.transform.localScale.x*debugBox.size.x/2,
			debugBox.transform.localScale.y*debugBox.size.y/2,
			debugBox.transform.localScale.z*debugBox.size.z,
			0);
	}

}
Post Views: 4,005

shader unity3d VFX 网格

上一篇【Unity Shader】顶点动画模拟衣物晃动效果shader实现顶点晃动效果下一篇 【Animation Rigging】Damped Transform Constraint实现头发晃动模拟AnimationRigging实现头发晃动

发表评论 取消回复

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

分类目录

  • 其他 (2)
  • 旅行 (1)
  • 涂鸦 (6)
  • 编程 (28)
Email: wuch441692@163.com