概述
在实现自定义阴影的过程中需要构建把物体从世界空间转到方向光的投影空间,和常规的M V P 变换思路一样,只不过这里的 V空间 和 P空间 变成了以 方向光为坐标原点的空间,而常规的V P空间是以摄像机为坐标原点。
下面主要说明的是 变换到方向光空间的view矩阵,常用的有四种方式。
Camera.worldToCameraMatrix 方式
实现逻辑如下:
- 建立一个Camera物体,放到和Directional Light物体位置一样的地方(建议座位Directional Light的子物体,然后设置Camera的rotation和localPosition都为Vector3.zero)
- 用该Camera物体在渲染所有Opaque物体前使用指定的shader渲染一遍场景,该shader只输出深度值, 渲染前给shader设置好VP变换矩阵(
GL.GetGPUProjectionMatrix(lightCam.projectionMatrix, false) * lightCam.worldToCameraMatrix
) - 正常渲染场景,使用第2步中的VP变换矩阵把物体从世界空间变换到方向光的投影空间,计算深度,
float depthInLS = clipPos.z * 0.5 + 0.5
- 计算读取shadowmap的uv,
float2 uv = clipPos.xy * 0.5 + 0.5
- 读取shadowmap,
float depthInSM = tex2D(_ShadowMap, uv)
- 比较
depthInLS
和depthInSM
的值,决定当前像素是否处于阴影中
此种方式使用一个dummy camera放到了方向光物体的位置,并设置旋转角度和方向光一致,好处是非常方便,可以直接使用Camera类的worldToCameraMatrix方法,此时变换到Camera空间就是变换到方向光的空间。而投影矩阵也可以使用现成的方法 Camera.projectionMatrix, 此时View 和Projection矩阵都具备了,要做的就是把两个矩阵乘起来即可,代码如下:
Matrix4x4 GetMatrix_VP()
{
Matrix4x4 projectionMatrix = GL.GetGPUProjectionMatrix(lightCam.projectionMatrix, false);
return projectionMatrix * lightCam.worldToCameraMatrix;
}
需要注意的是这里需要用 GL.GetGPUProjectionMatrix
方法来处理一下投影矩阵,以使投影矩阵能正确的变换坐标。
Camera.worldToLocalMatrix 方式
由于一下几种方式中投影矩阵的构建部分没有变化,因此只详细说明view矩阵部分。
代码如下:
Matrix4x4 view = Matrix4x4.Scale(new Vector3(1, 1, -1)) * Matrix4x4.Translate(-lightPos) * lightTrans.worldToLocalMatrix;
乘以 Matrix4x4.Translate(-lightPos)
是因为需要进行灯光位置平移的逆运算,
乘以 Matrix4x4.Scale(new Vector3(1, 1, -1))
是因为摄像机看向的是view空间Z轴的负方向,因为view空间使用右手坐标系而模型和世界空间使用的是左手坐标系
Matrix4x4.TRS + Quaternion.LookRotation 方式
// view矩阵, Matrix4x4.TRS + Quaternion.LookRotation 方式 //
Matrix4x4 GetViewMatrix_TRS_LookRotation(Vector3 lookAtPos, Vector3 camForward)
{
Vector3 lightPos = lookAtPos - camForward * distance;
Quaternion rot = Quaternion.LookRotation(camForward, Vector3.up);
return Matrix4x4.Inverse(Matrix4x4.TRS(lightPos, rot, new Vector3(1, 1, -1)));
}
Matrix4x4.TRS + Quaternion.Euler 方式
// view矩阵, Matrix4x4.TRS + Quaternion.Euler 方式 //
Matrix4x4 GetViewMatrix_TRS_Euler(Vector3 lookAtPos, Transform lightTrans)
{
Vector3 lightPos = lookAtPos - lightTrans.forward * distance;
Quaternion rot = Quaternion.Euler(lightTrans.eulerAngles);
return Matrix4x4.Inverse(Matrix4x4.TRS(lightPos, rot, new Vector3(1, 1, -1)));
}
basicAxis * translate 方式
这种方式代码稍微多一些,思想是先求出方向光空间的三个基准轴向量,由这三个基准向量可以构成一个旋转矩阵,然后再由方向光的世界空间位置计算出平移矩阵,把两个矩阵相乘即可得到view矩阵。这种构建view矩阵的方式比较通用,常规的view矩阵也可以使用这种方式构建。
// view矩阵, basicAxis * translate 方式 //
Matrix4x4 GetViewMatrix_AxisMulTrans(Vector3 lookAtPos, Vector3 camForward)
{
Vector3 forward = Vector3.Normalize(-camForward);
Vector3 up = new Vector3(0, 1, 0);
Vector3 right = Vector3.Normalize(Vector3.Cross(up, forward));
up = Vector3.Normalize(Vector3.Cross(forward, right));
// translate //
Vector3 lightPos = lookAtPos + forward * distance;
Matrix4x4 translate = Matrix4x4.identity;
// 也可以用 Matrix4x4.Translate(-translatePos); 构建平移矩阵 //
translate.SetColumn(3, new Vector4(-lightPos.x, -lightPos.y, -lightPos.z, 1));
//translate.SetColumn(3, -lightPos);
// basicAxis //
Matrix4x4 basicAxis = Matrix4x4.identity;
basicAxis.SetRow(0, new Vector4(right.x, right.y, right.z, 0));
basicAxis.SetRow(1, new Vector4(up.x, up.y, up.z, 0));
basicAxis.SetRow(2, new Vector4(forward.x, forward.y, forward.z, 0));
// 先平移,再计算投影在基向量的长度,即新空间的坐标 //
return basicAxis * translate;
}
总结
前三种方式都比较方便,但是因为使用了Unity内置的一些变量,导致具体的构建细节被隐藏,不能很好地理解view矩阵是怎么计算得出的,最后一种方式则是以view矩阵的原理为基础,使用的unity内置变量也最少,而且不需要使用dummy camera物体,完全根据参数即可构建出view矩阵,相对比较独立、可控,因此推荐使用最后一种方法。