- 获取当前绘制片段的世界坐标
- 绘制世界坐标
- 模型
- 重构深度缓存的世界坐标
- 先给相机添加OnRenderImage将深度值转到世界坐标当颜色值绘制
- ndc to world
- camera world position + frustum corner world space ray + linear01depth
- vert : ndcPos to clipPos, clipPos to viewPos, viewRay=viewPos.xyz, frag : viewRay \* linear01depth to world position
- 在正交相机下的深度纹理重构世界坐标
- CSharp
- Shader
- 效果
- 总结
- Project
- Graph
- References
在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.xyz都是 [-1~1]
- uv的范围是 [0~1]
- ndc.xy 可以表示为 uv * 2 - 1
- ndc.z 的范围 [-1~1] 但实际我们缓存只使用到 [0~1],可以从深度缓存中读取 0~1的数据(我记得好想DX是[0~1],OpenGL是[-1~1]),这里从_CameraDepthTexture.R读取的是ndc.z,但是_CameraDepthNormalsTexture.BA读取的是EyeZ/Far的一个比例值(Linear01Depth),具体可以查看之前写过的一篇:Unity Shader - 获取BuiltIn深度纹理和自定义深度纹理的数据
那么开始使用ndc来转换到世界坐标。
列出好几种方法,并简单介绍(下面几种方式在DX中没有问题,但在OpenGL中,第一种ndc to world显示有问题,因为我在android上跑,使用底层渲染API是OpenGL的,直接用第二种方式,性能最好,兼容性也好):
- ndc to world:frag中使用depth与screen.xy得到ndc,再使用_InvVP变换到world space。
- camera world position + frustum corner world space ray + linear01depth:frustum corner world space ray从vert传到frag,frag再用相机世界坐标 + 视锥体(截锥体)world space的角射线 * depth比例值。
- ndc to clip, clip to viewRay, viewRay * linear01depth to viewPos, viewPos to wordPos:vert中先是ndc space到clip space,再就是clip的远截面角落点转为view space的射线viewRay,frag中worldPos = cameraPos + viewRay * linear01depth。
下面所有的代码中的注释都非常详细,推荐看看,本文的正文部分没说的细节,可能在注释里有说明,因为我在写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();
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);
}
vert : ndcPos to clipPos, clipPos to viewPos, viewRay=viewPos.xyz, frag : viewRay * linear01depth to world position
这种方式是效率最差的方式。但这里做演示,都罗列一下。
- vertex shader中
ndc=float4(v.uv * 2 - 1, 1, 1)
,ndc转到clip
,再使用_InvP
将clip转到view
,view生成viewRay下的射线
(与之前的不同,之前的是world space下的射线,这里是view space下的射线),将下viewRay传到frag shader
; - frag shader直接拿到vert 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。
下面是我在正交相机下,实现获取深度的世界坐标(与透视的不一样):
CSharp// 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();
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我添加了一个分支,分别处理正交与透视的逻辑处理。 透视的内容不变,正交的与透视的区别在于:
- 透视构建frustum的角落点射线,主要思路是:
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);
}
正交相机的,我就只写一种方式吧(就上面那种写法),这种也是性能比较高的写法。
Projectbackup : UnityShader_GetWorldPosFromDepthTexTesting_2018.03.12
backup : UnityShader_GetWorldPosFromDepthTexTesting_IncludeOrthoTesting_2018.3.0f2
backup : Simplest_WPosFromDepth_2019_4_30f1
Graph- GetWorldPosFromDepth.ggb
- Unity Shader - 获取BuiltIn深度纹理和自定义深度纹理的数据
- Unity从深度缓冲重建世界空间位置
- Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果)
- Unity3D实现体积光
- 【Unity Shader】unity海边波浪效果的实现
- 屏幕后处理-无限大水面渲染
- Using _CameraDepthTexture with Orthographic Camera
- Reconstructing Position From Depth - 2020.04.07看到这位大神的博客也有一篇是从深度重构坐标的