实现效果
使用新建网格的方式实现贴花效果,模拟被斩击的伤痕,再外加一个材质动画来控制伤痕褪色。想法来自最近玩鬼泣5的时候发现敌人被砍的伤痕都会有划痕。之前单单用shader试着实现,但是贴花不能稳定的跟随网格顶点。现在改为复制蒙皮的方式,划痕百分百贴合并跟随顶点。期间还试着加法线贴图让划痕立体一些,后来发现划痕太细,镜头稍远,加了法线贴图也看不出效果来。
.gif)
贴花(Decal)方式
对于场景中静态物品(比如墙壁,地板),有Command Buffer和projector等方式来实现贴花,它们有个共通点是都是在一个“盒子”空间中来对准要贴的目标做“投影”。

.gif)
但是,在带有蒙皮动画的角色身上做贴花,这种靠“盒子”的方式看起来效果是不理想的。上图可以看到当敌人身体扭动的时候,其身上的红线贴花没有稳定依附在蒙皮上,而是相对地位移和旋转了一小段距离,红线“伤痕”看上去反而像扫描仪的光线。在把贴花“投影”到蒙皮的时候,“摄影机”对准的不是硬实的画板,而是一张不稳定的可以扭曲的画布。在蒙皮上做贴花,仅仅把影像投影上去是不行的,要真正地“把油漆涂抹上去”,比如修改网格的贴图,在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);
}
}


