Shader学习笔记 06 - 贴花
Mesh Decal
1. 获取与投影框相交的mesh
多种方法如:手动指定、遍历静态物体、碰撞检测、搜索等等。
2. 将Mesh的顶点、法线转换到投影框的模型坐标系内
主要是为裁剪做准备。
var mesh = meshFilter.sharedMesh; // 相交物体的 Mesh
List<Vector3> oriVertices = new List<Vector3>();
mesh.GetVertices(oriVertices);
List<Vector3> oriNormals = new List<Vector3>();
mesh.GetNormals(oriNormals);
List<Vector3> vertices = new List<Vector3>();
List<Vector3> normals = new List<Vector3>();
int vertexCount = mesh.vertexCount;
for (int i = 0; i < vertexCount; i++)
{
// meshFilter.transform.TransformPoint 从模型坐标转换到世界坐标
// 投影框的transform,transform.InverseTransformPoint,从世界坐标转换到投影框的模型坐标
positions.Add(transform.InverseTransformPoint(meshFilter.transform.TransformPoint(oriVertices[i])));
normals.Add(transform.InverseTransformDirection(meshFilter.transform.TransformDirection(oriNormals[i])));
}
3. 获取投影框内的三角网格
需要剔除投影框外的三角网格,保留投影框内的三角网格,裁剪部分投影框内三角网格,背部剔除(可选)。
for(遍历三角网格)
{
1. 背部剔除(可选),判断三角网格的法线朝向,背部则continue
2. 投影框内的顶点
3. 如果投影框内的顶点=3,则continue
4. 获取所有在三角网格内与投影框的边(8个角所组成的包围边)的交点
5. 获取三角网格的边与投影框面的交点(即裁剪)
// 我一开始的做法是将所有遍历所得的顶点保存,并使用三角剖分组成新的Mesh,大部分情况下没问题,但是对于复杂的模型(比如中空平面)会出错,中空的地方会被补上。
// 所以应该在原有三角网格的基础下衍生新的多个三角网格 //TODO 验证
6. 对当前循环内的所得顶点三角剖分,保存所有新的三角网格
}
- 保留投影框内的三角网格
三角网格的三个顶点xyz坐标是否都在投影框内,一般使用单位立方体作为投影框,即xyz范围[-0.5,0.5]
- 获取所有在三角网格内与投影框的边的交点
在投影框的模型坐标系下,投影框实际上是一个轴对称包围盒。
将三维的三角网格分别投影到xz,xy,yz轴平面上(eg:投影到xz轴平面上,直接将三角网格三个顶点的y值置为零即可)。得到的二维平面可能结果:
ABCD是投影框的投影,三角形是三角网格的投影,也可能是一条直线,并不影响计算结果。
判断ABCD是否在三角形内(重心法),如果是的话,则将对应点返回到三维空间内。
// eg: xz投影,求y
// (corner - triangle.A)*Normal = 0
float y = (
Vector3.Dot(triangle.Normal, triangle.A) -
triangle.Normal.x * corner.x - triangle.Normal.z * corner.y
) / triangle.Normal.y;
然后判断点是否在投影框内,是则表示这个点是三角网格内与投影框的边的交点。
- 获取三角网格的边与投影框面的交点
逐边裁剪(Sutherland Hodgman)。
for(遍历投影框6个面)
{
if(可遍历三角网格的边=0) break;
for(遍历三角网格的边)
{
if(边位于外侧即不可见一侧) 直接剔除这条边;
// 交点的某个坐标固定,通过这个值对开始、结束坐标插值,即可获取交点坐标
if(开始位于内侧,结束位于外侧 或 结束位于内侧,开始位于外侧) 获取交点,判断交点是否在投影框内,是则保存。
// 都位于内侧的情况,需要保存的结束点在上面的流程2已经保存。
}
}
4. 组成新的Mesh
新Mesh的uv取自顶点的xz坐标或者xy坐标(贴花的投射方向),或者根据Normal来动态改变,切线也需要重新计算。
Deferred Decal、Volume decals、Screen Space Decal
三者原理基本相同。Deferred Decal在延迟着色中使用。Screen Space Decal可以在向前渲染路径中使用。Volume decals同样在延迟着色中使用,作为无缝(seamless )贴花,投影到复杂几何上没有拉伸现象,在其他贴花方案通过法线动态修改贴图采样坐标也可以实现类似功能。
1. Shader配置
// 渲染顺序在贴花处物体渲染后面
// 关闭阴影投射
Tags { "Queue" = "Transparent+1" "ForceNoShadowCasting"="True" }
// 贴花半透明时需要指定混合方式
Blend SrcAlpha OneMinusSrcAlpha
2. 获取投影处
橙色为投影处
- 通过深度重构世界坐标
// 通过深度重构世界坐标(有两种方法,后处理、延迟光照中经常使用)
vert {
o.viewRay = UnityObjectToViewPos(v.vertex)*float3(-1,-1,1);
o.screenUV = ComputeScreenPos(o.vertex);
}
frag {
float3 ray = i.viewRay*(_ProjectionParams.z/i.viewRay.z);
float2 suv = i.screenUV.xy / i.screenUV.w;
// 通过screen uv作为采样坐标获取深度(即上图中投影框后物体坐标的深度)
float depth = Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, suv));
float4 vpos=fixed4(ray*depth,1);
float4 wpos=mul(unity_CameraToWorld,vpos);
}
- 将世界坐标转换到投影框的模型坐标系下,并判断是否在投影框内,裁剪掉在投影框外
// 投影框一般为单位正方体,只要判断转换后的坐标xyz是否小于边长的一半。
float3 opos=mul(unity_WorldToObject,wpos).xyz;
clip(0.5-abs(opos)); // 单位正方体边长为1
3. 获取法线
法线主要用于光照计算、解决非平面贴花的拉伸现象。向前渲染从_CameraDepthNormalsTexture取的深度、法线精度不够基本上没法使用,解决方案可以是单独渲一张精度足够的深度法线贴图,或者使用坐标偏导重新构建法线(面法线 face normal)。
// 使用坐标偏导构建法线
// ddx(wpos) 相当于三角网格的切线
float3 normal = normalize(cross(ddy(wpos), ddx(wpos)));
使用偏导数计算的切线结果,三角网格比较明显。向前渲染路径在没有顶点法线的情况下目前不知道如何平滑面法线。
纹理法线需要切线空间转换矩阵。
struct v2f
{
half3 oriSpace[3]:TEXCOORD5;
};
vert {
// 在顶点作色阶段根据投射方向,指定xyz轴
o.oriSpace[0] = mul((half3x3)unity_ObjectToWorld, half3(1, 0, 0));
o.oriSpace[1] = mul((half3x3)unity_ObjectToWorld, half3(0, 1, 0));
o.oriSpace[2] = mul((half3x3)unity_ObjectToWorld, half3(0, 0, 1));
}
frag {
float2 decalUV = opos.xz + 0.5;
fixed3 normal = UnpackNormal(tex2D(_NormalMap, decalUV));
half3x3 norMat = half3x3(i.oriSpace[0], i.oriSpace[2], i.oriSpace[1]);
normal = mul(normal, norMat);
}
4. 纹理采样
根据投射方向,使用模型坐标采样纹理(一般是按Y轴从上往下投射,所有使用模型坐标的xz分量)。角度变化明显(法线变化大),根据实际情况舍弃或者使用模型坐标其他分量采样纹理。