Unity URP中根据深度重建世界坐标

通过深度值重建世界坐标,可以做出很多有意思的后处理效果,先实现下度值重建世界坐标这个功能。

一.验证重建效果

首先,得先找到一种证明反推回世界空间位置正确的方法。在相机前摆放几个物体,尽量使之在世界坐标下的位置小于1,方便判定颜色,然后将几个物体的shader换成如下的一个打印世界空间位置的shader:


//打印在世界空间位置
Shader "Universal Render Pipeline/Dejavu/ReconstructPositionWithDepth/WorldPosPrint"
{SubShader{Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "Opaque" }LOD 300ZWrite[_ZWrite]Cull Off ZWrite OnPass{Name "ForwardLit"Tags{"LightMode" = "UniversalForward"}HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"ENDHLSLHLSLPROGRAM#pragma vertex vert#pragma fragment fragstruct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float3 worldPos : TEXCOORD0;float4 vertex : SV_POSITION;};v2f vert(appdata v){v2f o;UNITY_SETUP_INSTANCE_ID(v);UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);o.vertex = TransformObjectToHClip(v.vertex);o.worldPos = mul(unity_ObjectToWorld, v.vertex);return o;}float4 frag(v2f i) : SV_Target{return float4(i.worldPos, 1.0);}ENDHLSL}Pass{Name "DepthOnly"Tags{"LightMode" = "DepthOnly"}ZWrite OnColorMask 0Cull[_Cull]HLSLPROGRAM#pragma exclude_renderers gles gles3 glcore#pragma target 4.5#pragma vertex DepthOnlyVertex#pragma fragment DepthOnlyFragment// -------------------------------------// Material Keywords#pragma shader_feature_local_fragment _ALPHATEST_ON#pragma shader_feature_local_fragment _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A//--------------------------------------// GPU Instancing#pragma multi_compile_instancing#pragma multi_compile _ DOTS_INSTANCING_ON#include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"#include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"ENDHLSL}}
}

 得到的效果如下图所示,颜色代表了世界坐标位置。要注意的是URP中想要渲染Depth,需要Tags{"LightMode" = "DepthOnly"}这个Pass

 在之后的重建世界坐标位置的后处理Shader开启后,如果摄像机显示的没有变化,则证明重建成功

二.逆矩阵方式重建

 世界坐标的重建有两种方法,最直观的一种就是通过VP逆矩阵,在fragment ShadeClip空间中的位置转换为世界坐标。该方法的核心步骤为:

1.通过uv和深度图中采样的深度信息,构建NDC中坐标位置,从而构建Clip空间中位置

2.NDC坐标乘以逆矩阵即可得到世界坐标。

fragment中的代码为:

//fragment shaderfloat4 frag(v2f i) : SV_Target{float sceneRawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.uv);float4 ndc = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, sceneRawDepth, 1);#if UNITY_UV_STARTS_AT_TOPndc.y *= -1;#endiffloat4 worldPos = mul(UNITY_MATRIX_I_VP, ndc);worldPos /= worldPos.w;return worldPos;       }

看起来比较简单,但是其中有一个/w的操作,如果按照正常思维来算,应该是先乘以w,然后进行逆变换,最后再把world中的w抛弃,即是最终的世界坐标,不过实际上投影变换是一个损失维度的变换,我们并不知道应该乘以哪个w,所以实际上上面的计算,并非按照理想的情况进行的计算,而是根据计算推导而来。具体推导:How to go from device coordinates back to worldspace in OpenGL (with explanation)

已知条件(M为VP矩阵,M^-1即为其逆矩阵,Clip为裁剪空间,ndc为标准设备空间,world为世界空间):

ndc = Clip.xyzw / Clip.w = Clip / Clip.w

world = M^-1 * Clip

二者结合得:

world = M ^-1 * ndc * Clip.w

我们已知M和ndc,然而还是不知道Clip.w,但是有一个特殊情况,是world的w坐标,经过变换后应该是1,即

1 = world.w = (M^-1 * ndc).w * Clip.w

进而得到Clip.w = 1 / (M^ -1 * ndc).w

带入上面等式得到:

world = (M ^ -1 * ndc) / (M ^ -1 * ndc).w

优化

上边的shader代码中,自己通过uv和深度构建了ndc坐标,但是实际上Unity URP提供了ComputeWorldSpacePosition函数可以直接调用(函数位置在core rp中的Common.hlsl):

float3 ComputeWorldSpacePosition(float2 positionNDC, float deviceDepth, float4x4 invViewProjMatrix)
{float4 positionCS  = ComputeClipSpacePosition(positionNDC, deviceDepth);float4 hpositionWS = mul(invViewProjMatrix, positionCS);return hpositionWS.xyz / hpositionWS.w;
}

其中的omputeClipSpacePosition函数也在core rp中的Common.hlsl中:

float4 ComputeClipSpacePosition(float2 positionNDC, float deviceDepth)
{float4 positionCS = float4(positionNDC * 2.0 - 1.0, deviceDepth, 1.0);#if UNITY_UV_STARTS_AT_TOP// Our world space, view space, screen space and NDC space are Y-up.// Our clip space is flipped upside-down due to poor legacy Unity design.// The flip is baked into the projection matrix, so we only have to flip// manually when going from CS to NDC and back.positionCS.y = -positionCS.y;
#endifreturn positionCS;
}

最终,我们的Shader只需要:

    //fragment shaderfloat4 frag(v2f i) : SV_Target{float sceneRawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.uv);float3 worldPos = ComputeWorldSpacePosition(i.uv, sceneRawDepth, UNITY_MATRIX_I_VP);return float4(worldPos, 1);}

代码立刻显得很清爽,ComputeWorldSpacePosition实际内容和自己上边写的shader是一致的

Bonus

以上的方法中,用到了UNITY_MATRIX_I_VP这个矩阵,在一些博主写的文章中,用的方式是从C#端将camera的VP逆矩阵传入shader中:

var vpMatrix = Camera.main.projectionMatrix * Camera.main.worldToCameraMatrix;

之后传入vpMatrix.inverse

在shader中使用该矩阵进行重建时,如果代码还按照上边的shader中写,是无法正确还原的。

纠其原因,是因为Camera.main.projectionMatrix * Camera.main.worldToCameraMatrix的逆矩阵并不和UNITY_MATRIX_I_VP一致,UNITY_MATRIX_I_VP是和平台无关的,而Camera相关的矩阵,是和Opengl还是directx等相关的,在opengl的模式下,重建还原代码如下,注意的是sceneRawDepth也进行了*2-1的操作,这是因为opengl的z范围是-1到1。

    //fragment shaderfloat4 frag(v2f i) : SV_Target{float sceneRawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.uv);#if defined(UNITY_REVERSED_Z)sceneRawDepth = 1 - sceneRawDepth;#endiffloat4 ndc = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, sceneRawDepth * 2 - 1, 1);float4 worldPos = mul(_InverseVPMatrix, ndc);worldPos /= worldPos.w;return worldPos;}

小结

1.这种方式重建世界坐标,性能比较差,一般来说,我们都是逐顶点地进行矩阵运算,毕竟定点数一般还是比较少的,但是全屏幕逐像素进行矩阵运算,这个计算量就不是一般的大了,性能堪忧。

2.能用Unity提供的API就用其提供的,Unity封装和处理了很多跨平台的情况,比如Z的范围,左手右手坐标系等,自己撸极容易出错。

三.射线方式重建

原理

这种方式的重建,可以参考Secrets of CryENGINE 3 Graphics Technology这个CryTech 2011年的PPT。借用一张图:

 然后偶再画个平面的图:

上图中,A为相机位置,G为空间中我们要重建的一点,那么该点的世界坐标为A(worldPos) + 向量AG,我们要做的就是求得向量AG即可。根据三角形相似的原理,三角形AGH相似于三角形AFC,则得到AH / AC = AG / AF。由于三角形相似就是比例关系,所以我们可以把AH / AC看做01区间的比值,那么AC就相当于远裁剪面距离,即为1,AH就是我们深度图采样后变换到01区间的深度值,即Linear01Depth的结果d。那么,AG = AF * d。所以下一步就是求AF,即求出相机到屏幕空间每个像素点对应的射线方向。

如何获得AF呢,在后处理Shader中,实际上就是绘制了一个Quad,对应整个屏幕。这个Quad的四个边界点刚好对应屏幕的四个边界点,uv是(0,1)区间的,刚好对应屏幕空间,我们通过*2 - 1将其转化到(-1,1)区间就可以得到四个边界对应NDC坐标系下的xy坐标了,在VertexShader中计算出这4个点对应的射线后,将其传入FragmentShader中,通过插值就会自动得到每个像素点对应的AF。

Frament中还原:

//fragment shaderfloat4 frag(v2f i) : SV_Target{float sceneRawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.uv);float linear01Depth = Linear01Depth(sceneRawDepth, _ZBufferParams);float3 worldPos = _WorldSpaceCameraPos.xyz + ( linear01Depth) * i.viewRayWorld ;return float4(worldPos, 1);}

VertexShader中计算射线:

方法1

        用ComputeWorldSpacePosition方法,计算四个顶点对应的世界坐标位置,减去摄像机位置后就是所求射线。这种方式还是很好理解的,每个顶点矩阵运算也只有一次,个人感觉是最优方案。

    //vertex shaderv2f vert(appdata v){v2f o;UNITY_SETUP_INSTANCE_ID(v);UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);o.positionCS = TransformObjectToHClip(v.positionOS.xyz);//方法1float sceneRawDepth = 1;#if defined(UNITY_REVERSED_Z)sceneRawDepth = 1 - sceneRawDepth;
#endiffloat3 worldPos = ComputeWorldSpacePosition(v.uv, sceneRawDepth, UNITY_MATRIX_I_VP);o.viewRayWorld = worldPos - _WorldSpaceCameraPos.xyz;o.uv = v.uv;return o;}

方法2

原理和方法1一样,只是先由clip空间转到View空间,再由View空间转到摄像机空间。其中_InverseVMatrix是由C#端传入的Camera.main.worldToCameraMatrix.inverse。

那么为什么不能直接使用Unity中的UNITY_MATRIX_I_V呢,因为后处理Shader中,Unity根本不会为你设置UNITY_MATRIX_I_VP,UNITY_MATRIX_I_VP是单位矩阵。

    v2f vert(appdata v){v2f o;UNITY_SETUP_INSTANCE_ID(v);UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);o.positionCS = TransformObjectToHClip(v.positionOS.xyz);float4 clipPos =  ComputeClipSpacePosition(v.uv, 0);float4 viewPos = mul(UNITY_MATRIX_I_P, clipPos);viewPos.xyz = viewPos.xyz / viewPos.w;float3 worldPos = mul(_InverseVMatrix, viewPos).xyz;o.viewRayWorld = worldPos - _WorldSpaceCameraPos.xyz;o.uv = v.uv;return o;}

方法3

这次我们不求得四个顶点在世界空间中的位置,而是在View空间中,以射线的方式做逆变换,因为在view空间中,camera的位置为0,所以viewpos就是viewRay的方向。射线的逆变换要注意的是,只需要3X3的3维矩阵,要排除掉平移变换的影响,因为射线无论如何平移,都是一样的射线

    v2f vert(appdata v){v2f o;UNITY_SETUP_INSTANCE_ID(v);UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);o.positionCS = TransformObjectToHClip(v.positionOS.xyz);//方法3 float4 clipPos =  ComputeClipSpacePosition(v.uv, 0);float4 viewPos = mul(UNITY_MATRIX_I_P, clipPos);float3 viewRay = viewPos.xyz / viewPos.w;o.viewRayWorld = mul((float3x3)_InverseVMatrix, viewRay);o.uv = v.uv;return o;}

最终结果

 无论用那种方式,开关后处理效果,都能发现场景是相同效果,证明重建成功!

完整工程代码

GitHub - Dejavu0709/StudyForShader  中的ReconstructPositionWithDepth文件夹

相关资料:感谢大佬们的无私分享

Reconstruct the world space positions of pixels from the depth texture | Universal RP | 10.5.1

Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果)_puppet_master的专栏-CSDN博客_shader深度

使用深度图重建世界坐标_小孔明的专栏-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/246054.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

dubbo yml配置_Spring boot 的profile功能如何实现多环境配置自动切换

通常服务端应用开发需要经过以下几个流程:开发 -> 测试 -> RC验证 -> 上线这就涉及到四个不同的环境,开发环境、测试环境、RC环境以及生产环境,为了避免不同环境之间相互干扰,通常需要独立部署数据库、缓存服务器等&…

Unity中的SystemInfo.deviceUniqueIdentifier 唯一ID

做游戏时可能经常使用SystemInfo.deviceUniqueIdentifier作为用户的唯一ID进行注册登录, 但是你会发现从谷歌商店上下载的自己游戏,和自己从Unity工程中直接打包出来的游戏账号竟然是不一致的! 这个坑还是很坑爹的,纠其原因是Sy…

sketchup生成面域插件_独家教程 | 快速抓取“高精准”场地信息,康石石教你生成不同“体量”地形...

无论建筑设计还是景观设计,同学们的设计项目都必须依托于场地来进行,通过分析场地的区位范围、地形地势,结合场地的局限性与可能性,才能进一步展开项目设计。可以说,获取场地信息是同学们在作品集创作中最重要的环节之…

Unity URP高度雾效果Shader

实现原理 见这篇文章Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果)_puppet_master的专栏-CSDN博客_shader深度…

无限重启_三星蓝光播放器出现无限自动重启BUG,涉及不少用户及不同型号

三星的蓝光播放器似乎遇到了一个挺严重的BUG,使得不少用户都开机后播放器会自动不停重启。从reddit、ZDNet以及三星技术支持论坛上面的情况来看,这次的问题波及不同型号的播放器,大部分用户遇到的问题都是不停重启。其中一位用户表示:“开机之…

python getattr_Python 内置方法和属性应用:反射和单例

1. 前言python除了丰富的第三方库外,本身也提供了一些内在的方法和底层的一些属性,大家比较常用的如dict、list、set、min、max、range、sorted等。笔者最近在做项目框架时涉及到一些不是很常用的方法和属性,在本文中和大家做下分享。2. 内置…

Unity URP世界空间后处理扫描圈效果Shader

实现原理 见这篇文章Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果)_puppet_master的专栏-CSDN博客_shader深度…

前端 重构时需要注意的事项_前端数据层落地实践

源宝导读:天际移动平台经过重构改版,近期正式发布了1.0版本,我们在低代码开发方面做了进一步增强。本文主要围绕前端Model、前端业务逻辑(领域模型)、数据层与视图层解耦(包装器模式)3个方面,给大家分享一下统一数据层方案的设计思…

postconstruct_@PostConstruct注解,你该好好看看

在最近的工作中,get到一个很实用的注解,分享给诸位。痛点做过微信或支付宝支付的童鞋,可能遇到过这种问题,就是填写支付结果回调,就是在支付成功之后,支付宝要根据我们给的地址给我们进行通知,通…

Unity URP运动模糊效果Shader

实现原理 见这篇文章Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果)_puppet_master的专栏-CSDN博客_shader深度…

线性系统的频率响应分析实验报告_动态系统的建模与分析

参考:DR_CAN1.介绍解决一个控制系统的问题:对研究对象进行分析控制器设计测试分析被控对象的物理特性及动态表现,在这个基础上建立数学模型,数学模型可以是动力学模型、热力学模型、流体力学模型和经济学模型等,然后在…

android 生命周期_Android生命周期组件 Lifecycle 源码详解(一)

在上篇文章:warmcheng:Android生命周期组件 Lifecycle 使用详解​zhuanlan.zhihu.com中,我们讲了 Lifecycle 的简单使用,本篇我们来研究下它的源码。 基础环境搭建首先,按照上篇文章所讲,快速搭建环境。添加…

Leetcode1143. 最长公共子序列(c#)

题解&#xff1a;力扣 public class Solution{public int LongestCommonSubsequence(string text1, string text2){int num1 text1.Length;int num2 text2.Length;int[,] dp new int[num1 1, num2 1];for(int i 0; i < num1; i){for(int j 0; j < num2; j){if(t…

telnet到设备里 php_PHP自动生成设备周检修计划

背景维修人员根据设备年度检修计划&#xff0c;然后制订周检修计划(设备年度计划包含设备一年需要维护几次等信息&#xff0c;根据年度计划分解到某一个周去执行)。在这个过程中&#xff0c;大量的excel复制粘贴工作&#xff0c;浪费人力并且容易出错。并且在审核过程中&#x…

通俗讲解:图像傅里叶变换

转自某乎&#xff1a;通俗讲解&#xff1a;图像傅里叶变换 - 知乎 这里我们主要要讲的是二维图像傅里叶变换&#xff0c;但是我们首先来看一张很厉害的一维傅里叶变换动图。 妈耶~厉害哇&#xff01;它把时域和频域解释的很清楚&#xff01; 什么&#xff01;你看不懂&#x…

数据库断线重连_干货分享—Niushop数据库配置

前几期阿牛ger主讲了代码编码规范&#xff0c;整洁规范的代码有利于我们查询和再次开发&#xff0c;也方便我们检测与修复bug&#xff01;这期&#xff0c;阿牛ger主要与大家分享数据库编码配置&#xff1a;数据库配置Niushop数据库配置方式与thinkphp相同&#xff0c;文件路径…

怎么将matlab滤波器系数导出_matlab与FPGA数字信号处理系列(1)——通过matlab工具箱设计FIR数字滤波器...

以99阶FIR低通滤波器为例&#xff0c;学习使用matlab的fdatool工具箱设计滤波器&#xff0c;并将滤波器系数导出到.coe文件&#xff0c;联合Vivado进行FPGA的FIR滤波器设计。本文滤波器参数为&#xff1a;低通FIR滤波器&#xff0c;窗函数设计&#xff0c;采用布莱克曼窗&#…

UGUI 合批原理

转自&#xff1a; UGUI合批原理笔记 - 赵青青 - 博客园 UGUI合批规则图解_时光不染-CSDN博客_ugui合批规则 合批的过程# 网格更新机制# Cavans.SendWillRenderCanvas m_LayoutRebuildQueuem_GraphicRebuildQueueCanvas.BuildBatch 更新所有DrawCall WaitingForJob 子线程网格…

vb.net 设置打印纸张与页边距_装订文档时不想让文字被挡住?在Excel中你可以这样设置打印!...

平时我们在打印文档的时候&#xff0c;通常会把文档左侧的页边距设置的大一点&#xff0c;这样在装订的时候显得美观一点。但如果我们进行双面打印时&#xff0c;文档左右两边的页边距刚好相反&#xff0c;装订时第2页的文本很容易被挡住&#xff0c;这样子反而更难装订了。那么…

CPU Cache原理与示例

转自这篇 CPU Cache&#xff0c;估计也没人看 基础知识 首先&#xff0c;我们都知道现在的 CPU 多核技术&#xff0c;都会有几级缓存&#xff0c;老的 CPU 会有两级内存&#xff08;L1 和 L2&#xff09;&#xff0c;新的CPU会有三级内存&#xff08;L1&#xff0c;L2&#x…