最近有个功能, 要渲染从主相机视角看到的另一个相机的可视范围和不可见范围, 大概如下图 :
简单来说就是主相机视野和观察者相机视野重合的地方, 能标记出观察者相机的可见和不可见, 实现原理就跟 ShadowMap 一样, 就是有关深度图, 世界坐标转换之类的, 每次有此类的功能都会很悲催, 虽然它的逻辑很简单, 可是用Unity3D做起来很麻烦...
原理 : 在观察者相机中获取深度贴图, 储存在某张 RenderTexture 中, 然后在主相机中每个片元都获取对应的世界坐标位置, 将世界坐标转换到观察者相机viewPort中, 如果在观察者相机视野内, 就继续把视口坐标转换为UV坐标然后取出储存在 RenderTexture 中的对应深度, 把深度转换为实际深度值后做比较, 如果深度小于等于深度图的深度, 就是观察者可见 否则就是不可见了.
先来看怎样获取深度图吧, 不管哪种渲染方式, 给相机添加获取深度图标记即可:
_cam = GetComponent<Camera>(); _cam.depthTextureMode |= DepthTextureMode.Depth;
然后深度图要精度高点的, 单通道图即可:
renderTexture = RenderTexture.GetTemporary(Screen.width, Screen.height, 0, RenderTextureFormat.RFloat); renderTexture.hideFlags = HideFlags.DontSave;
之后就直接把相机的贴图经过后处理把深度图渲染出来即可 :
private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (_material && _cam) { Shader.SetGlobalFloat("_cameraNear", _cam.nearClipPlane); Shader.SetGlobalFloat("_cameraFar", _cam.farClipPlane); Graphics.Blit(source, renderTexture, _material); } Graphics.Blit(source, destination); }
材质就是一个简单的获取深度的shader :
sampler2D _CameraDepthTexture; uniform float _cameraFar; uniform float _cameraNear; float DistanceToLinearDepth(float d, float near, float far) { float z = (d - near) / (far - near); return z; } fixed4 frag(v2f i) : SV_Target { float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); depth = LinearEyeDepth(depth); depth = DistanceToLinearDepth(depth, _cameraNear, _cameraFar); return float4(depth, depth, depth, 1); }
这里有点奇怪吧, 为什么返回的不是深度贴图的深度, 也不是 Linear01Depth(depth) 的归一化深度, 而是自己计算出来的一个深度?
这是因为缺少官方文档........其实看API里面有直接提供 RenderBuffer 给相机的方法 :
_cam.SetTargetBuffers(renderTexture.colorBuffer, renderTexture.depthBuffer);
可是没有文档也没有例子啊, 鬼知道你渲染出来我怎么用啊, 还有 renderTexture.depthBuffer 到底怎样作为 Texture 传给 shader 啊... 我之前尝试过它们之间通过 IntPtr 作的转换操作, 都是失败...
于是就只能用最稳妥的方法, 通过 Shader 渲染一张深度图出来吧, 然后通过 SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); 获取到的深度值, 应该是 NDC 坐标, 深度值的范围应该是[0, 1]之间吧(非线性), 如果在其它相机过程中获取实际深度的话, 就需要自己实现 LinearEyeDepth(float z) 这个方法:
inline float LinearEyeDepth( float z ) { return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w); }
而在不同平台, _ZBufferParams 里面的值是不一样的, 所以如果实现它的话会很麻烦, 并且经过我的测试, 结果是不对的......
double x, y; OpenGL would be this: x = (1.0 - m_FarClip / m_NearClip) / 2.0; y = (1.0 + m_FarClip / m_NearClip) / 2.0; D3D is this: x = 1.0 - m_FarClip / m_NearClip; y = m_FarClip / m_NearClip; _ZBufferParams = float4(x, y, x/m_FarClip, y/m_FarClip);
最后做出来的只是基本功能, 并没有对距离深度做样本估计, 所以会像硬阴影一样有锯齿边, 还有像没开各项异性的贴图一样有撕扯感, 这些都需要进行处理...