实现效果
使用新建网格的方式实现贴花效果,模拟被斩击的伤痕,再外加一个材质动画来控制伤痕褪色。想法来自最近玩鬼泣5的时候发现敌人被砍的伤痕都会有划痕。之前单单用shader试着实现,但是贴花不能稳定的跟随网格顶点。现在改为复制蒙皮的方式,划痕百分百贴合并跟随顶点。期间还试着加法线贴图让划痕立体一些,后来发现划痕太细,镜头稍远,加了法线贴图也看不出效果来。
贴花(Decal)方式
对于场景中静态物品(比如墙壁,地板),有Command Buffer和projector等方式来实现贴花,它们有个共通点是都是在一个“盒子”空间中来对准要贴的目标做“投影”。
但是,在带有蒙皮动画的角色身上做贴花,这种靠“盒子”的方式看起来效果是不理想的。上图可以看到当敌人身体扭动的时候,其身上的红线贴花没有稳定依附在蒙皮上,而是相对地位移和旋转了一小段距离,红线“伤痕”看上去反而像扫描仪的光线。在把贴花“投影”到蒙皮的时候,“摄影机”对准的不是硬实的画板,而是一张不稳定的可以扭曲的画布。在蒙皮上做贴花,仅仅把影像投影上去是不行的,要真正地“把油漆涂抹上去”,比如修改网格的贴图,在unity asset store中能找到UV Paint等插件,只有dll没有源码,但看演示估计应该就是动态修改贴图来达到UV Paint的效果。
至于我的实现方式,是从蒙皮网格上截取一块网格,对这块网格重新设置三角形和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); } }