赞
踩
在Unity中,获取当前绘制对象中的片段的世界坐标,可以按下列方式:
struct a2v { ... float4 pos : POSITION; ... }; struct v2f { ... float4 worldPos : TEXCOORD0; ... }; v2f vert(a2v v) { v2f o = (v2f)0; ... o.worldPos = mul(unity_ObjectToWorld, v.pos); ... return 0; } fixed4 frag(v2f i) : SV_Target { i.worldPos ...; // 该片段的世界坐标 }
OK,获取绘制的片段的世界坐标是如此的简单。
那么下面开始实践。
为了方便测试功能正确性,放一些模型,调整好他们的位置、缩放,尽量在unity的1 unit单位范围内。
因为我们要直接用xyz坐标当做颜色绘制出来。所以我们要控制要物体的xyz都尽量在0~1范围内。
先放置一个cube,scaleXYZ都是1,用于作为大小参考物
再放置三个quad,scaleXYZ都是1,三种颜色分别代表:红色:X轴,绿色:Y轴,蓝色:Z轴
再放一个,小Cube,scaleXYZ缩放都是0.1,因为要将它在0~1的坐标范围内移动,弄小一些就好。
再给这个小Cube弄上材质,材质的shader为下面的代码,用于显示世界坐标的。
// jave.lin 2020.03.10 - Draws the world position Shader "Custom/DrawWP" { SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; float4 wp : TEXCOORD0; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.wp = mul(unity_ObjectToWorld, v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { return i.wp; } ENDCG } } }
好了,那么移动小Cube看看坐标值作为颜色绘制的情况如何
将小Cube直接放大,看看他的各个角度下的片段颜色
OK,那么确定正确的世界坐标绘制颜色。
接下来需要对深度缓存中的值都还原到世界坐标。
深度缓存如何拿到啊?可以看看我上一篇的:Unity Shader - 获取BuiltIn深度纹理和自定义深度纹理的方法。
下面执行在后处理流程,在后处理获取深度缓存并转换每个像素到世界坐标。
先简单说明:
那么开始使用ndc来转换到世界坐标。
列出好几种方法,并简单介绍(下面几种方式在DX中没有问题,但在OpenGL中,第一种ndc to world显示有问题,因为我在android上跑,使用底层渲染API是OpenGL的,直接用第二种方式,性能最好,兼容性也好):
下面所有的代码中的注释都非常详细,推荐看看,本文的正文部分没说的细节,可能在注释里有说明,因为我在写demo时的代码,就将注释写上了,懒得在文章中又在写一遍。
首先挂个下面的脚本,设置好脚本的Camera cam
,这个就主相机就好了。
// jave.lin 2020.03.12 using UnityEngine; public class GetWPosFromDepthScript1 : MonoBehaviour { private Camera cam; public Material mat; // 这里之所以手动设置,并使用这些变量 // 而不是用unity内置的,是因为后处理中 // 部分的矩阵会给替换成一些只渲染一个布满屏幕Quad的正交矩阵信息 private int _InvVP_hash; // VP逆矩阵 private int _VP_hash; // V矩阵 private int _Ray_hash; // Frustum的角射线 private int _InvP_hash; // P的逆矩阵 private int _InvV_hash; // V的逆矩阵 private void Start() { cam = gameObject.GetComponent<Camera>(); cam.depthTextureMode |= DepthTextureMode.Depth; // _CameraDepthTexture与_CameraDepthNormalsTexture都测试 cam.depthTextureMode |= DepthTextureMode.DepthNormals; _InvVP_hash = Shader.PropertyToID("_InvVP"); _VP_hash = Shader.PropertyToID("_VP"); _Ray_hash = Shader.PropertyToID("_Ray"); _InvP_hash = Shader.PropertyToID("_InvP"); _InvV_hash = Shader.PropertyToID("_InvV"); } private void OnRenderImage(RenderTexture source, RenderTexture destination) { Graphics.Blit(source, destination, mat); } private void OnPreRender() { var aspect = cam.aspect; // 宽高比 var far = cam.farClipPlane; // 远截面距离长度 var rightDir = transform.right; // 相机的右边方向(单位向量) var upDir = transform.up; // 相机的顶部方向(单位向量) var forwardDir = transform.forward; // 相机的正前方(单位向量) // fov = field of view,就是相机的顶面与底面的连接相机作为点的夹角, // 我们取一半就好,与相机正前方方向的线段 * far就是到达远截面的位置(这条边当做下面的tan公式的邻边使用) // tan(a) = 对 比 邻 = 对/邻 // 邻边的长度是知道的,就是far值,加上fov * 0.5的角度,就可以求出高度(对边) // tan(a)=对/邻 // 对=tan(a)*邻 var halfOfHeight = Mathf.Tan(cam.fieldOfView * 0.5f * Mathf.Deg2Rad) * far; // 剩下要求宽度 // aspect = 宽高比 = 宽/高 // 宽 = aspect * 高 var halfOfWidth = aspect * halfOfHeight; // 前,上,右的角落偏移向量 var forwardVec = forwardDir * far; var upVec = upDir * halfOfHeight; var rightVec = rightDir * halfOfWidth; // 左下角 bottom left var bl = forwardVec - upVec - rightVec; // 左上角 top left var tl = forwardVec + upVec - rightVec; // 右上角 top right var tr = forwardVec + upVec + rightVec; // 右下角 bottom right var br = forwardVec - upVec + rightVec; // 视锥体远截面角落点的射线 var frustumFarCornersRay = Matrix4x4.identity; // 经shader中顶点颜色赋值后出入到屏幕,可以确定,第0是:左下角,1:左上角,2:右上角,3:右下角 frustumFarCornersRay.SetRow(0, bl); frustumFarCornersRay.SetRow(1, tl); frustumFarCornersRay.SetRow(2, tr); frustumFarCornersRay.SetRow(3, br); // 使用GL.GetGPUProjectionMatrix接口Unity底层会处理不同平台的投影矩阵的差异 // 第二个参数是相对RT来使用的,因为RT的UV.v在一些平台是反过来的,这里传false,因为不需要RT的UV变化兼容处理 Matrix4x4 p = GL.GetGPUProjectionMatrix(cam.projectionMatrix, false); Matrix4x4 v = cam.worldToCameraMatrix; Matrix4x4 vp = p * v; mat.SetMatrix(_InvVP_hash, vp.inverse); mat.SetMatrix(_VP_hash, vp); mat.SetMatrix(_Ray_hash, frustumFarCornersRay ); mat.SetMatrix(_InvP_hash, p.inverse); mat.SetMatrix(_InvV_hash, v.inverse); } }
脚本中,对viewPortRay每行向量,设置了一个射线,分别第几个射线对应哪个角落,可以在shader中,使用SV_VertexID
来取到顶点索引,在对索引绘制:0:红,1:绿,2:蓝,3:黄,shader如下:
struct appdata { float4 vertex : POSITION; uint vid : SV_VertexID; }; struct v2f { float4 vertex : SV_POSITION; fixed4 col : TEXCOORD2; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); const fixed4x4 vcol = { {1,0,0,1}, {0,1,0,1}, {0,0,1,1}, {1,1,0,1}, }; // 测试用的,用于辨别后处理的四个顶点 // 经过测试id:0在左下角,1:左上角,2:右上角,3:右下角 o.col = vcol[v.vid]; return o; } fixed4 frag (v2f i) : SV_Target { return i.col; }
运行效果:
经过测试id:0在左下角,1:左上角,2:右上角,3:右下角
所以在vert中要拿到对应frustum corner ray(视锥体角射线),直接取:_Ray[v.id]
就好了,下面一些使用方式中会用到。
在frag shader使用depth
与screenPos.xy
得到ndc
,再使用_InvVP
将ndc
变换到world space
。
详细的描述是:先在直接在片段着色器,获取深度值的ndcZ
,float4 fwp = mul(_InvVP, float4(i.uv * 2 - 1, ndcZ, 1));
,再fwp /= fwp.w;
后,fwp
就是深度片段对应的世界坐标了。
有多种写法:
第一种
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; }; float4x4 _InvVP; sampler2D _CameraDepthTexture; sampler2D _CameraDepthNormalsTexture; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; } fixed4 frag (v2f i) : SV_Target { // tex2D(_CameraDepthTexture, i.uv) 的是ndcZ float ndcZ = (SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv)); // ndc to world pos => worldPos = _InvVP * ndc; float4 fwp = mul(_InvVP, float4(i.uv * 2 - 1, ndcZ, 1)); fwp /= fwp.w; return fwp; }
这里为何要将fwp
世界坐标乘以它本身的w
分量。
可以参考:这篇和这个国外文章
原因:
看起来比较简单,但是其中有一个/w的操作,如果按照正常思维来算,应该是先乘以w,然后进行逆变换,最后再把world中的w抛弃,即是最终的世界坐标,不过实际上投影变换是一个损失维度的变换,我们并不知道应该乘以哪个w,所以实际上上面的计算,并非按照理想的情况进行的计算,而是根据计算推导而来。
已知条件( M M M为 V P VP VP矩阵, M − 1 M^{-1} M−1即为其逆矩阵, C l i p Clip Clip为裁剪空间, n d c ndc ndc为标准设备空间, w o r l d world world为世界空间):
n d c = C l i p . x y z w / C l i p . w = C l i p / C l i p . w ndc = Clip.xyzw / Clip.w = Clip / Clip.w ndc=Clip.xyzw/Clip.w=Clip/Clip.w
w o r l d = M − 1 ∗ C l i p world = M^{-1} * Clip world=M−1∗Clip
二者结合得:
w o r l d = M − 1 ∗ n d c ∗ C l i p . w world = M ^{-1} * ndc * Clip.w world=M−1∗ndc∗Clip.w
我们已知M和ndc,然而还是不知道Clip.w,但是有一个特殊情况,是world的w坐标,经过变换后应该是1,即
1 = w o r l d . w = ( M − 1 ∗ n d c ) . w ∗ C l i p . w 1 = world.w = (M^{-1} * ndc).w * Clip.w 1=world.w=(M−1∗ndc).w∗Clip.w
进而得到 C l i p . w = 1 / ( M − 1 ∗ n d c ) . w Clip.w = 1 / (M^{-1} * ndc).w Clip.w=1/(M−1∗ndc).w
带入上面等式得到:
w o r l d = ( M − 1 ∗ n d c ) / ( M − 1 ∗ n d c ) . w world = (M ^{-1} * ndc) / (M ^{-1} * ndc).w world=(M−1∗ndc)/(M−1∗ndc).w
所以,世界坐标就等于ndc进行VP逆变换之后再除以自身的w。
那么继续ndc to world的内容
第二种
不同的是,使用_CameraDepthNormalsTexture.BA
解码出来的线性linear01Depth
值,而不是ndc.z
,所以我们需要先将它转换到ndc.z
,ndc.z = (1/linear01Depth - _ZBufferParams.y) / _ZBufferParams.x
。
v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; } fixed4 frag (v2f i) : SV_Target { float depth; float3 normal; float4 cdn = tex2D(_CameraDepthNormalsTexture, i.uv); DecodeDepthNormal(cdn, depth, normal); //float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); //逆矩阵的方式使用的是1/z非线性深度,而_CameraDepthNormalsTexture中的是线性的,进行一步Linear01Depth的逆运算 /* Linear01Depth的逆运算 // Z buffer to linear 0..1 depth inline float Linear01Depth( float z ) { return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y); } Linear01Depth = 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y); (_ZBufferParams.x * z + _ZBufferParams.y) * Linear01Depth = 1 (_ZBufferParams.x * z + _ZBufferParams.y) = 1/Linear01Depth (_ZBufferParams.x * z) = 1/Linear01Depth - _ZBufferParams.y z = (1/Linear01Depth - _ZBufferParams.y) / _ZBufferParams.x */ // 此时的depth是ndcZ:ndc space z value(ndc空间下的z值) depth = (1.0/depth - _ZBufferParams.y) /_ZBufferParams.x ; // //自己操作深度的时候,需要注意Reverse_Z的情况 // #if defined(UNITY_REVERSED_Z) // depth = 1 - depth; // #endif // 从上面的反Linear01Depth运算,到正确的结果 // 说明中生成_CameraDepthNormalsTexture时 // 使用的o.depthNormals.w = COMPUTE_DEPTH_01;深度 // 就相当于处理了Linear01Depth,不然反运算是不能成功的 float4 ndc = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, depth, 1); float4 worldPos = mul(_InvVP, ndc); worldPos /= worldPos.w; return worldPos; }
运行效果:
但这种方式都有一个问题,那就是:都有在frag shader里执行float4 worldPos = mul(_InvVP, ndc);
,但在后处理的话,就意味着要执行screenW*H格frag shader,执行的片段是很多的。我们接着看看其他更优化的方式。
用相机世界坐标 + 视锥体(截锥体)world space的角射线 * depth比例值。
视锥体的角射线在CSharp脚本传入,视锥体射线:_Ray
,_Ray
有四个角落的射线,分别为:左下角,左上角,右上角,右下角,_Ray
在vert传入frag插值后使用。
这种方式会比ndc to world的方式要高效很多。
在vert中处理内容就只是索引查找:o.ray = _Ray[v.vid];
在frag中,有只有一次tex2D采样,一次加法,一次乘法(第二种一次乘法,第一种还多了个除法,这里是演示不同写法而已)
第一种写法:
主要思想是:CameraWorldPos + FrustumCornerRay * (EyeZ/Far)
放一张图的话,大概是这样的:
GIF来演示,四条FrustomCornerRay的0,1,2,3射线分别对应BLRay,TLRay,TRRay,BRRay
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; uint vid : SV_VertexID; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float3 ray : TEXCOORD1; }; float4x4 _Ray; sampler2D _CameraDepthTexture; sampler2D _CameraDepthNormalsTexture; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; o.ray = _Ray[v.vid]; } fixed4 frag (v2f i) : SV_Target { float eyeZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv)); // worldPos = cameraWorldPos + frustomConerRay * (eyeZ / far); // normalize(i.ray) * (eyeZ / far) == 射线 * (eyeZ / far) == 射线 * (Linear01Depth) // 所以我才使用了下面的eyeZ * _ProjectionParams.w的方式,因为_ProjectionParams.w == 1/far // float3 wp = _WorldSpaceCameraPos.xyz + normalize(i.ray) * eyeZ; // 与正确结果很相似,但肯定是不对的,因为eyeZ是视图空间下的,而i.ray是世界空间下的射线 // (eyeZ * _ProjectionParams.w) == Linear01Depth(tex2D(depthTex, i.uv).r) float3 wp = _WorldSpaceCameraPos.xyz + i.ray * (eyeZ * _ProjectionParams.w); return fixed4(wp, 1); }
第二种写法,也是目前罗列出来的写法中,效率是最高的写法。
都一样的思路,写法不一而已,作参考用,与第一种不同的是frag的内容
主要思想是:CameraWorldPos + FrustumCornerRay * linearEyeRate01Z
,用linearEyeRate01Z
替换了(EyeZ/Far)
fixed4 frag (v2f i) : SV_Target {
float linearEyeRate01Z = Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
// worldPos = cameraWorldPos + frustomConerRay * linearEyeRate01Z;
// 世界坐标 = 相机坐标 + 射线(注意不是方向,无归一化处理,所以向量模是很重要的) * 该深度与远截面的比例值
// 下面的链接:linear01Depth存的是什么,以及_CameraDepthTexture.r以及_CameraDepthNormalsTexture解码后的深度值有是什么都有说明
// https://blog.csdn.net/linjf520/article/details/104723859#t21
// linearEyeRate01Z就是链接中的AH / AC的比例值
// linearEyeRate01Z = AH / AC == Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv))
// Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv)) == DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), out float outLinear01Depth, out float3 normal)中的outLinear01Depth值
float3 wp = _WorldSpaceCameraPos.xyz + i.ray * linearEyeRate01Z;
return fixed4(wp, 1);
}
这种方式是效率最差的方式。但这里做演示,都罗列一下。
ndc=float4(v.uv * 2 - 1, 1, 1)
,ndc转到clip
,再使用_InvP
将clip转到view
,view生成viewRay下的射线
(与之前的不同,之前的是world space下的射线,这里是view space下的射线),将下viewRay传到frag shader
;viewRay
,再通过读取深度纹理得到ndc.z
,再Linear01Depth(ndc.z)
得到线性的深度比例值:linear01Depth
来对viewRay
做线性缩放,得到view space下的坐标viewPos = i.viewRay * linear01Depth
,通过float4 worldPos = mul(_InvV, viewPos);
。第一种写法:
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; uint vid : SV_VertexID; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float3 ray : TEXCOORD1; }; float4x4 _InvVP; float4x4 _InvP; float4x4 _InvV; sampler2D _CameraDepthTexture; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; float far = _ProjectionParams.z; float4 clipPos = float4(v.uv * 2 - 1.0, 1.0, 1.0) * far; // 远截面的ndc * far = clip float4 viewRay = mul(_InvP, clipPos); o.ray = viewRay.xyz; } fixed4 frag (v2f i) : SV_Target { float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); // i.ray == view space Ray, haven't normalized float3 viewPos = i.ray * Linear01Depth(depth); // 这里的_InvV原本为:UNITY_MATRIX_I_V,但是shader时运行在后处理时 // 所以MVP都被替换了,所以要用回原来主相机的MVP相关的矩阵都必须外部自己传进来 float4 worldPos = mul(_InvV, float4(viewPos, 1)); return worldPos; }
第二种写法,只有vertex shader不同:
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
// 两种方式获取(ndc.xy * 0.5 + 0.5)==screenPos
float4 screenPos = float4(v.uv, 1, 1); // 方法1
// float4 screenPos = ComputeScreenPos(o.vertex); // 方法2
// screenPos.xy /= screenPos.w;
float2 ndcPos = screenPos.xy * 2 -1;
float far = _ProjectionParams.z;
float3 clipVec = float3(ndcPos, 1) * far;
float3 viewVec = mul(_InvP, clipVec.xyzz).xyz;
o.ray = viewVec;
}
要注意的是,这种写的性能相对第二种来说比较差,因为vertex有矩阵乘法,fragment也有,虽然后处理顶点不多,一个Quad就四个点,但像素是全屏的数量:ScreenW*ScreenH。
最后添加了Timeline控制一下镜头旋转与后处理过渡背景的参数,看看效果
这个应该是unity 2018.3.0f2(我使用的unity版本的坑,后面版本unity应该会修复的)
具体有什么坑,可以看看下面我的shader代码注释里有写得很详细。
或是可以参考之前写的:Unity Shader - 获取BuiltIn深度纹理和自定义深度纹理的数据,只看部分内容:注意Unity正交相机中的深度纹理的编码。
关于正交相机(正交投影、矩阵)的相关知识,可以参考:Orthographic Projection。
下面是我在正交相机下,实现获取深度的世界坐标(与透视的不一样):
// jave.lin 2020.03.12 using UnityEngine; public class GetWPosFromDepthScript1 : MonoBehaviour { public enum ProjType { Perspective, Orthographic } public ProjType projType; [Range(0, 1)] public float alpha = 0.3f; public Material mat; private Camera cam; // 这里之所以手动设置,并使用这些变量 // 而不是用unity内置的,是因为后处理中 // 部分的矩阵会给替换成一些只渲染一个布满屏幕Quad的正交矩阵信息 private static int _InvVP_hash; // VP逆矩阵 private static int _VP_hash; // V矩阵 private static int _Ray_hash; // Frustum的角射线 private static int _InvP_hash; // P的逆矩阵 private static int _InvV_hash; // V的逆矩阵 private static int _Ortho_Ray_hash; // 正交相机的射线向量 private static int _Ortho_Ray_Oringin_hash; // 正交相机的射线起点 private static int _Alpha_hash; static GetWPosFromDepthScript1() { _InvVP_hash = Shader.PropertyToID("_InvVP"); _VP_hash = Shader.PropertyToID("_VP"); _Ray_hash = Shader.PropertyToID("_Ray"); _InvP_hash = Shader.PropertyToID("_InvP"); _InvV_hash = Shader.PropertyToID("_InvV"); _Ortho_Ray_hash = Shader.PropertyToID("_Ortho_Ray"); _Ortho_Ray_Oringin_hash = Shader.PropertyToID("_Ortho_Ray_Oringin"); _Alpha_hash = Shader.PropertyToID("_Alpha"); } private void Start() { cam = gameObject.GetComponent<Camera>(); cam.depthTextureMode |= DepthTextureMode.Depth; // _CameraDepthTexture與_CameraDepthNormalsTexture都测试 cam.depthTextureMode |= DepthTextureMode.DepthNormals; } private void OnRenderImage(RenderTexture source, RenderTexture destination) { Graphics.Blit(source, destination, mat); } private void OnPreRender() { cam.orthographic = projType == ProjType.Orthographic; if (cam.orthographic) { // 正交的处理 mat.EnableKeyword("_PROJ_METHOD_ORTHOGRAPHIC"); mat.DisableKeyword("_PROJ_METHOD_PERSPECTIVE"); // Camera's half-size when in orthographic mode. // https://docs.unity3d.com/ScriptReference/Camera-orthographicSize.html // The orthographicSize property defines the viewing volume of an orthographic Camera. In order to edit this size, // set the Camera to be orthographic first through script or in the Inspector. // The orthographicSize is half the size of the vertical viewing volume. The horizontal size of the viewing volume depends on the aspect ratio. // 由上面的官方API描述中,可得知,orthograpihcSize是控制 view volume,视图长方体的高度的一半的,orthographicSize = 5,那么view volume height = 10 // unity 1 unit == 100 pixels,所以view volume height = 10 unit == 1000 pixel var halfOfHeight = cam.orthographicSize; var halfOfWith = cam.aspect * halfOfHeight; //Debug.Log($"size:{cam.orthographicSize}, aspect:{cam.aspect}, hw:{halfOfWith}, hh:{halfOfHeight}"); // 了解orthographic的投影矩阵,更方便与对参数的应用:http://www.songho.ca/opengl/gl_projectionmatrix.html#ortho var upVec = cam.transform.up * halfOfHeight; var rightVec = cam.transform.right * halfOfWith; // 左下角 bottom left var bl = -upVec - rightVec; // 左上角 top left var tl = upVec - rightVec; // 右上角 top right var tr = upVec + rightVec; // 右下角 bottom right var br = -upVec + rightVec; // 正交相机的四个角落射线的起点 var orthographicCornersPos = Matrix4x4.identity; // 经shader中顶点颜色赋值后出入到屏幕,可以确定,第0是:左下角,1:左上角,2:右上角,3:右下角 orthographicCornersPos.SetRow(0, bl); orthographicCornersPos.SetRow(1, tl); orthographicCornersPos.SetRow(2, tr); orthographicCornersPos.SetRow(3, br); mat.SetVector(_Ortho_Ray_hash, transform.forward * cam.farClipPlane); mat.SetMatrix(_Ortho_Ray_Oringin_hash, orthographicCornersPos); } else { // 透视的处理 mat.EnableKeyword("_PROJ_METHOD_PERSPECTIVE"); mat.DisableKeyword("_PROJ_METHOD_ORTHOGRAPHIC"); var aspect = cam.aspect; // 宽高比 var far = cam.farClipPlane; // 远截面距离长度 var rightDir = transform.right; // 相机的右边方向(单位向量) var upDir = transform.up; // 相机的顶部方向(单位向量) var forwardDir = transform.forward; // 相机的正前方(单位向量) // fov = field of view,就是相机的顶面与底面的连接相机作为点的夹角, // 我们取一半就好,与相机正前方方向的线段 * far就是到达远截面的位置(这条边当做下面的tan公式的邻边使用) // tan(a) = 对 比 邻 = 对/邻 // 邻边的长度是知道的,就是far值,加上fov * 0.5的角度,就可以求出高度(对边) // tan(a)=对/邻 // 对=tan(a)*邻 var halfOfHeight = Mathf.Tan(cam.fieldOfView * 0.5f * Mathf.Deg2Rad) * far; // 剩下要求宽度 // aspect = 宽高比 = 宽/高 // 宽 = aspect * 高 var halfOfWidth = aspect * halfOfHeight; // 前,上,右的角落偏移向量 var forwardVec = forwardDir * far; var upVec = upDir * halfOfHeight; var rightVec = rightDir * halfOfWidth; // 左下角 bottom left var bl = forwardVec - upVec - rightVec; // 左上角 top left var tl = forwardVec + upVec - rightVec; // 右上角 top right var tr = forwardVec + upVec + rightVec; // 右下角 bottom right var br = forwardVec - upVec + rightVec; var frustumCornersRay = Matrix4x4.identity; // 经shader中顶点颜色赋值后出入到屏幕,可以确定,第0是:左下角,1:左上角,2:右上角,3:右下角 frustumCornersRay.SetRow(0, bl); frustumCornersRay.SetRow(1, tl); frustumCornersRay.SetRow(2, tr); frustumCornersRay.SetRow(3, br); mat.SetMatrix(_Ray_hash, frustumCornersRay); } // 公共属性 // 使用GL.GetGPUProjectionMatrix接口Unity底层会处理不同平台的投影矩阵的差异 // 第二个参数是相对RT来使用的,因为RT的UV.v在一些平台是反过来的,这里传false,因为不需要RT的UV变化兼容处理 Matrix4x4 p = GL.GetGPUProjectionMatrix(cam.projectionMatrix, false); Matrix4x4 v = cam.worldToCameraMatrix; Matrix4x4 vp = p * v; mat.SetMatrix(_InvVP_hash, vp.inverse); mat.SetMatrix(_VP_hash, vp); mat.SetMatrix(_InvP_hash, p.inverse); mat.SetMatrix(_InvV_hash, v.inverse); mat.SetFloat(_Alpha_hash, alpha); } }
可以看到,OnPreRender我添加了一个分支,分别处理正交与透视的逻辑处理。
透视的内容不变,正交的与透视的区别在于:
camWorldPos + frustumCornerRay * linear01depth;
。camWorldPos + viewVolumeCornerPos + camForwardDir * linear01depth;
。struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; uint vid : SV_VertexID; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float4 rayOrigin : TEXCOORD1; }; float _Alpha; sampler2D _MainTex; sampler2D _CameraDepthTexture; sampler2D _CameraDepthNormalsTexture; v2f vert_orthographic(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; o.rayOrigin = _Ortho_Ray_Oringin[v.vid]; return o; } fixed4 frag_orthographic(v2f i) { // 本人jave.lin 2020.03.14,下面代码运行在unity 2018.3.0f2测试结果,如果其他同学的没有这些问题,大概是unity版本不一致 #if _METHOD_T1 // 正交相机下,_CameraDepthTexture存储的是线性值,且:距离镜头远的物体,深度值小,距离镜头近的物体,深度值大,可以使用UNITY_REVERSED_Z宏做处理 // 透视相机下,_CameraDepthTexture存储的是ndc.z值,且:不是线性的。 float linear01depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); // return linear01depth; // 正交下的相机,_CameraDepthTexture纹理中存储的是线性比例值 #if defined(UNITY_REVERSED_Z) // 正交需要处理这个宏定义,透视不用,估计后面unity版本升级后会处理正交的这个宏定义处理吧 linear01depth = 1 - linear01depth; #endif // return linear01depth; float3 wp = _WorldSpaceCameraPos.xyz + i.rayOrigin.xyz + _Ortho_Ray.xyz * linear01depth; return lerp(tex2D(_MainTex, i.uv), float4(wp, 1), _Alpha); #endif #if _METHOD_T2 // _CameraDepthNormalsTexture的纹理,正交相机,与透视相机下都没问题一样这么使用 float linear01depth = DecodeFloatRG (tex2D(_CameraDepthNormalsTexture, i.uv).zw); // return linear01depth; // 正交下的相机,_CameraDepthTexture纹理中存储的是线性比例值 // #if defined(UNITY_REVERSED_Z) // 测试发现,即使正交模式下:使用_CameraDepthNormalsTexture的深度也是有处理UNITY_REVERSED_Z宏分支逻辑的,所以这里不需要Reverse Z // linear01depth = 1 - linear01depth; // #endif // return linear01depth; float3 wp = _WorldSpaceCameraPos.xyz + i.rayOrigin.xyz + _Ortho_Ray.xyz * linear01depth; return lerp(tex2D(_MainTex, i.uv), float4(wp, 1), _Alpha); #endif #if _METHOD_TCOLOR return i.col; #endif return tex2D(_CameraDepthTexture, i.uv).r; }
2020.03.15 更新,在实现其他深度相关的效果是,也法线Unity在SIGGRAPH2011有分享过一些基于使用深度实现的特效的内容,文档:SIGGRAPH2011 Special Effect with Depth.pdf
如果多年后,下载不了,链接无效了,可以点击这里(Passworld:cmte)下载(我收藏到网盘了)
他分享的也是用:深度世界坐标 = 相机世界坐标 + 世界坐标下的相机坐标指向远截面四个角落的射线 * 深度比例值01
透视相机的:还是使用第二种方式兼容性最好(DX,GL都没问题),性能最好。
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.ray = _Ray[v.vid];
}
fixed4 frag (v2f i) : SV_Target {
float linearEyeRate01Z = Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
float3 wp = _WorldSpaceCameraPos.xyz + i.ray * linearEyeRate01Z;
return fixed4(wp, 1);
}
正交相机的,我就只写一种方式吧(就上面那种写法),这种也是性能比较高的写法。
backup : UnityShader_GetWorldPosFromDepthTexTesting_2018.03.12
backup : UnityShader_GetWorldPosFromDepthTexTesting_IncludeOrthoTesting_2018.3.0f2
backup : Simplest_WPosFromDepth_2019_4_30f1
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。