求知若饥,虚心若愚
获取深度和法线纹理
背后的原理
深度纹理实际就是一张渲染纹理,只不过它里面存储的像素值不是颜色值,而是一个高精度的深度值。深度纹理里的深度值范围是[0,1],而且通常是非线性分布(和透视矫正插值 有关,同时变换的投影矩阵确实非线性的)。总体来说,这些深度值来自于顶点变换后得到的归一化的设备坐标(Normalized Device Coordinates, NDC) 。
在透视投影中,投影矩阵首先对顶点进行了缩放。在经过齐次除法后,透视投影的裁剪空间会变换到一个立方体。图中标注了4个关键点经过投影矩阵变换后的结果
在正交投影中,投影矩阵对顶点进行了缩放。在经过齐次除法后,正交投影的裁剪空间会变换到一个立方体。图中标注了4个关键点经过投影矩阵变换后的结果
坐标变换到NDC后z分量的值就是深度值,其范围是[-1,1],为了将其映射到能存储在一张图像中,需要进行
d是深度纹理中的像素值, 是NDC坐标中的z分量。
Unity怎么得到这样一张深度纹理 ?
延迟渲染路径,深度信息在G-Buffer中,可以访问到
不能直接访问到时,使用着色器替换技术,根据选择特定的渲染类型以及那些渲染队列≤2500(Background/Geometry/AlphaTest)的物体(通常还需要支持ShadowCaster的Pass),使用单独一个Pass(按理说渲染阴影的时候已经渲染一次深度图了,官方给出的解释是有些平台获取不到,以及全屏和非全屏的问题)渲染到深度纹理中。在Shader中必须设置正确的RenderType标签(通常是Opaque)
可以选择让相机生成一张深度纹理或一张深度 +法线纹理:
仅深度纹理:精度通常是24位或16位
深度+法线纹理:会创建一张和屏幕分辨率相同、精度为32位(RGBA8)的纹理,RG通道是观察空间下的法线信息,BA通道是深度信息
如何获取
通过在脚本中设置相机的depthTextureMode,设置好后可以在Shader中通过声明_CameraDepthTexture来访问。
1 2 3 4 5 6 7 8 9 camera.depthTextureMode = DepthTextureMode.Depth; // 获取深度纹理 camera.depthTextureMode = DepthTextureMode.DepthNormals; // 获取深度+法线纹理 camera.depthTextureMode |= DepthTextureMode.Depth; camera.depthTextureMode |= DepthTextureMode.DepthNormals; // 同时产生一张深度和深度+法线纹理 // 应该是还原设置 camera.depthTextureMode &= ~DepthTextureMode.Depth; camera.depthTextureMode &= ~DepthTextureMode.DepthNormals;
使用SAMPLE_DEPTH_TEXTURE宏对深度纹理进行采样,处理由于平台差异(绝大多数使用tex2D即可,但诸如PS2就需要特殊处理)造成的问题:
1 float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
其中,i.uv是一个float2类型的变量,对应了当前像素的纹理坐标。类似的宏还有SAMPLE_DEPTH_TEXTURE_PROJ和SAMPLE_DEPTH_TEXTURE_LOD。SAMPLE_DEPTH_TEXTURE_PROJ宏同样接受两个参数——深度纹理和一个float3或float4类型的纹理坐标,它的内部使用了tex2Dproj这样的函数进行投影纹理采样,纹理坐标的前两个分量首先会除以最后一个分量,再进行纹理采样。如果提供了第四个分量,还会进行一次比较,通常用于阴影的实现中。SAMPLE_DEPTH_TEXTURE_PROJ的第二个参数通常是由顶点着色器输出插值而得的屏幕坐标,例如:
1 float d = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.srcPos));
其中,i.scrPos是在顶点着色器中通过调用ComputerScreenPos(o.pos)得到的屏幕坐标。区别
因为深度值往往是非线性的,而我们需要线性的深度值,这需要倒推顶点变换的过程, 可结合这篇文章 和书中内容反复查阅。
使用Unity辅助函数Linear01Depth来得到映射到01范围的线性深度值,以及使用LinearEyeDepty可以得到视角空间,范围[Near,Far]的深度值。
使用tex2D对_CameraDepthNormalsTexture进行采样,最后使用DecodeDepthNormal对采样结果进行解码,得到深度值和法线方向(depth是[0,1]的线性深度值,法线是视角空间下的法线方向),它在UnityCG.cginc中的定义如下:
1 2 3 4 5 inline void DecodeDepthNormal(float4 enc, out float depth, out float3 normal) { depth=DecodeFloatRG(enc.zw); normal=DecodeViewNormalStereo(enc); }
查看深度和法线纹理
设置了相机的depthTextureMode即可在帧调试器中查看对应的纹理。
使用Frame Debugger查看深度纹理(左)和深度+法线纹理(右)。如果当前摄像机需要生成深度和法线纹理,帧调试器的面板中就会出现相应的渲染事件。只要单击对应的事件就可以查看得到的深度和法线纹理
但此时是非线性的深度值,使用shader在片元着色器中输出转换或解码后的深度和法线值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _DepthNormalValue("Depth Normal Value", int) = 0 } SubShader{ Pass{ CGPROGRAM #include "UnityCG.cginc" #pragma vertex vert #pragma fragment frag sampler2D _MainTex; fixed _DepthNormalValue; sampler2D _CameraDepthTexture; sampler2D _CameraDepthNormalsTexture; struct v2f{ float4 pos:SV_POSITION; half2 uv:TEXCOORD0; }; v2f vert(appdata_img v){ v2f o; o.pos=UnityObjectToClipPos(v.vertex); o.uv=v.texcoord; return o; } fixed4 frag(v2f i):SV_Target{ float depth=SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv); float linearDepth=Linear01Depth(depth); fixed3 normal=DecodeViewNormalStereo(tex2D(_CameraDepthNormalsTexture,i.uv)); fixed4 linearDepthColor=fixed4(linearDepth,linearDepth,linearDepth,1.0); fixed4 normalColor=fixed4(normal*0.5+0.5,1.0); return lerp(linearDepthColor,normalColor,_DepthNormalValue); } ENDCG } } Fallback Off
再谈运动模糊
之前是使用多张屏幕图像来模拟运动模糊,另一种应用更加广泛的技术是使用速度映射图。
在片元着色器中为每个像素计算其在世界空间下的位置再使用前一帧的视角*投影矩阵变换得到前一帧的NDC坐标,计算前一帧和当前帧的位置差,生成该像素的速度。优点是可以在一个屏幕后处理步骤中完成整个效果的模拟,缺点是在片元着色器中进行了两次矩阵乘法的操作,对性能有影响。(该方法出自《GPU Gems3》27章)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 using System; using System.Collections; using System.Collections.Generic; using Unity_Shaders_Learn; using UnityEngine; public class MotionBlurWithDepthTexture : PostEffectsBase { public Shader motionBlurShader; private Material _motionBlurMaterial = null; public Material MotionBlurMaterial { get { _motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, _motionBlurMaterial); return _motionBlurMaterial; } } [Header("模糊图像使用的大小"), Range(0.0f, 1.0f)] public float blurSize = 0.5f; // 可以放PostEffectsBase里 private Camera _myCamera; public Camera MyCamera { get { if (_myCamera == null) { _myCamera = GetComponent<Camera>(); } return _myCamera; } } private Matrix4x4 _previousViewProjectionMatrix; private void OnEnable() { MyCamera.depthTextureMode |= DepthTextureMode.Depth; } private void OnDisable() { MyCamera.depthTextureMode &= ~DepthTextureMode.Depth; } private void OnRenderImage(RenderTexture src, RenderTexture dest) { if (MotionBlurMaterial == null) { Graphics.Blit(src, dest); return; } MotionBlurMaterial.SetFloat("_BlurSize", blurSize); MotionBlurMaterial.SetMatrix("_PreviousViewProjectionMatrix", _previousViewProjectionMatrix); // 计算当前帧的视角*投影矩阵的逆矩阵,以便在片元中求到该像素的世界坐标 Matrix4x4 currentViewProjectionMatrix = MyCamera.projectionMatrix * MyCamera.worldToCameraMatrix; Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse; MotionBlurMaterial.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix); // 记录上一帧的视角*投影矩阵,通过世界坐标来计算上一帧的NDC坐标 _previousViewProjectionMatrix = currentViewProjectionMatrix; Graphics.Blit(src, dest, MotionBlurMaterial); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 Shader "Unity Shaders Learn/Chapter13/MotionBlurWithDepthTexture" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _BlurSize ("Blur Size", Float) = 1.0 } SubShader { CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; float4 _MainTex_TexelSize; sampler2D _CameraDepthTexture; float4x4 _PreviousViewProjectionMatrix; float4x4 _CurrentViewProjectionInverseMatrix; half _BlurSize; struct v2f { float4 pos : SV_POSITION; half2 uv : TEXCOORD0; half2 uv_depth : TEXCOORD1; }; v2f vert(appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord; o.uv_depth = v.texcoord; #if UNITY_UV_STARTS_AT_TOP if (_MainTex_TexelSize.y < 0) { o.uv_depth.y = 1 - o.uv_depth.y; } #endif return o; } fixed4 frag(v2f i) : SV_Target { // 获取像素的深度 float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth); // 这个像素视角方向的奇次裁剪坐标 float4 H = fixed4(2 * i.uv.x - 1, 2 * i.uv.y - 1, 2 * d - 1, 1); // 使用视角、投影逆矩阵变换 float4 D = mul(_CurrentViewProjectionInverseMatrix, H); // 得到世界坐标 float4 worldPos = D/D.w; float4 currentPos = H; float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos); previousPos /= previousPos.w; // 前一帧到当前帧的速度方向 float2 velocity = (currentPos.xy - previousPos.xy) / 2.0f; float2 uv = i.uv; float4 c = tex2D(_MainTex, uv); uv += velocity * _BlurSize; for (int it = 1; it < 3; it++, uv += velocity * _BlurSize) { float4 currentColor = tex2D(_MainTex, uv); c += currentColor; } c /= 3; return fixed4(c.rgb, 1.0); } ENDCG Pass { Cull Off ZWrite Off ZTest Always CGPROGRAM #pragma vertex vert; #pragma fragment frag; ENDCG } } Fallback Off }
效果总觉得会抖一下
以上是片元着色器中使用逆矩阵来重建每个像素在世界空间下的位置的做法,这种做法往往会影响性能。在Unity自带的ImageEffect包有更多运动模糊的实现方法。
全局雾效
雾效(Fog) 是游戏里经常使用的一种效果。
Unity内置的雾效可以产生基于距离的线性或指数雾效。但如果要在自己编写的着色器实现这些雾效,需要在shader中添加#pragma multi_compile_fog指令,并配合UNITY_FOG_COORDS、UNITY_TRABSFER_FOG、UNITY_APPLY_FOG等内置宏。 使用内置雾效缺点有:需要为场景相关物体添加这些渲染代码,且不能个性化操作,比如不能实现基于高度的雾效等。
这里学习一种经过一次屏幕后处理(不需要逐物体修改)的全局雾效。自由度高,可以模拟均匀的雾、基于距离的线性/指数雾、基于高度的雾等。实现的关键在于根据深度纹理来重建每个像素在世界空间下的位置。
重建世界坐标
知道相机在世界空间下的位置,以及世界空间下该像素相对于相机的偏移量,把它们相加就可以得到该像素的世界坐标:
1 2 3 4 // interpolatedRay是由顶点着色器输出并插值后得到的射线 // 因为屏幕后处理实际是渲染到只有四个顶点的面片,所以只需要计算几个顶点的这个射线向量,所有像素的都能通过插值生成 // 包含了该像素到相机的方向和距离信息 float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;
interpolatedRay的推导过程见书和这篇文章
计算interpolatedRay
采样得到的深度值并非是点到摄像机的欧式距离
雾的计算
需要计算一个雾效系数f,作为混合原始颜色和雾的颜色的混合系数
1 float3 afterFog = f * fogColor + (1 - f) * origColor;
雾效系数f的一些计算方法:
线性(Linear): , 和 表示受雾影响的最小和最大距离
指数(Exponential): ,d是控制雾浓度的参数
指数的平方(Exponential Squared): ,d是控制雾浓度的参数
基于高度的线性雾计算: , 和 表示受雾影响的起始和终止高度
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 using System; using System.Collections; using System.Collections.Generic; using Unity_Shaders_Learn; using UnityEngine; public class FogWithDepthTexture : PostEffectsBase { public Shader fogShader; private Material _fogMaterial = null; public Material FogMaterial { get { _fogMaterial = CheckShaderAndCreateMaterial(fogShader, _fogMaterial); return _fogMaterial; } } [Header("雾的浓度"), Range(0.0f, 3.0f)] public float fogDensity = 1.0f; [Header("雾的颜色")] public Color fogColor = Color.white; [Header("雾的起始高度")] public float fogStart = 0.0f; [Header("雾的终止高度")] public float fogEnd = 2.0f; // 可以放PostEffectsBase里 private Camera _myCamera; public Camera MyCamera { get { if (_myCamera == null) { _myCamera = GetComponent<Camera>(); } return _myCamera; } } private Transform _myCameraTrans; public Transform MyCameraTrans { get { if (_myCameraTrans == null) { _myCameraTrans = MyCamera.transform; } return _myCameraTrans; } } private void OnEnable() { MyCamera.depthTextureMode |= DepthTextureMode.Depth; } private void OnDisable() { MyCamera.depthTextureMode &= ~DepthTextureMode.Depth; } private void OnRenderImage(RenderTexture src, RenderTexture dest) { if (FogMaterial == null) { Graphics.Blit(src, dest); return; } Matrix4x4 frustumCorners = Matrix4x4.identity; float fov = MyCamera.fieldOfView; float near = MyCamera.nearClipPlane; float far = MyCamera.farClipPlane; float aspect = MyCamera.aspect; // 为计算相机到四个角的向量做准备 float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad); Vector3 toRight = MyCameraTrans.right * halfHeight * aspect; Vector3 toTop = MyCameraTrans.up * halfHeight; Vector3 cameraForward = MyCameraTrans.forward; // 先计算相机到近平面左上角向量,再根据相似三角形得到投影前坐标的向量 Vector3 topLeft = cameraForward * near + toTop - toRight; float scale = topLeft.magnitude / near; topLeft.Normalize(); topLeft *= scale; Vector3 topRight = cameraForward * near + toTop + toRight; topRight.Normalize(); topRight *= scale; Vector3 bottomLeft = cameraForward * near - toTop - toRight; bottomLeft.Normalize(); bottomLeft *= scale; Vector3 bottomRight = cameraForward * near - toTop + toRight; bottomRight.Normalize(); bottomRight *= scale; // 注意顺序,要和shader中相对应 frustumCorners.SetRow(0, bottomLeft); frustumCorners.SetRow(1, bottomRight); frustumCorners.SetRow(2, topRight); frustumCorners.SetRow(3, topLeft); FogMaterial.SetMatrix("_FrustumCorners",frustumCorners); FogMaterial.SetFloat("_FogDensity", fogDensity); FogMaterial.SetColor("_FogColor", fogColor); FogMaterial.SetFloat("_FogStart", fogStart); FogMaterial.SetFloat("_FogEnd", fogEnd); Graphics.Blit(src, dest, FogMaterial); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 Shader "Unity Shaders Learn/Chapter13/FogWithDepthTexture" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _FogDensity ("Fog Density", Float) = 1.0 _FogColor ("Fog Color", Color) = (1,1,1,1) _FogStart ("Fog Start", Float) = 0.0 _FogEnd ("Fog End", Float) = 1.0 } SubShader { CGINCLUDE #include "UnityCG.cginc" float4x4 _FrustumCorners; sampler2D _MainTex; half4 _MainTex_TexelSize; sampler2D _CameraDepthTexture; half _FogDensity; fixed4 _FogColor; float _FogStart; float _FogEnd; struct v2f { float4 pos : SV_POSITION; half2 uv : TEXCOORD0; half2 uv_depth : TEXCOORD1; // 在顶点着色器中计算顶点(面片只有四个顶点)到相机(视角)位置的偏移向量,通过插值从而在片元着色器也能访问 float4 interpolatedRay : TEXCOORD2; }; v2f vert(appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord; o.uv_depth = v.texcoord; #if UNITY_UV_STARTS_AT_TOP if (_MainTex_TexelSize.y < 0) { o.uv_depth.y = 1 - o.uv_depth.y; } #endif // 虽然用了很多if,但考虑到只有四个顶点,可以忽略不计 int index = 0; if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5) { index = 0; }else if(v.texcoord.x > 0.5 && v.texcoord.y < 0.5) { index = 1; }else if(v.texcoord.x > 0.5 && v.texcoord.y > 0.5) { index = 2; }else { index = 3; } #if UNITY_UV_STARTS_AT_TOP if (_MainTex_TexelSize.y < 0) { index = 3 - index; } #endif o.interpolatedRay = _FrustumCorners[index]; return o; } fixed4 frag(v2f i) : SV_Target { float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth)); float3 worldPos = _WorldSpaceCameraPos + i.interpolatedRay.xyz * linearDepth; // 套线性公式计算 float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart); fogDensity = saturate(fogDensity * _FogDensity); fixed4 finalColor = tex2D(_MainTex, i.uv); finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity); return finalColor; } ENDCG Pass { Cull Off ZWrite Off ZTest Always CGPROGRAM #pragma vertex vert #pragma fragment frag ENDCG } } Fallback Off }
再谈边缘检测
使用Sobel算子对屏幕图像进行边缘检测的描边效果,由于是直接使用颜色信息会得到很多不希望的边缘线,例如阴影或者一些地板缝隙也被描上了黑边。接下来学习使用深度和法线纹理的边缘检测,不受纹理和光照的影响。
这里会使用Roberts算子,Roberts算子的本质是计算左上角和右下角的差值,乘以右上角和左下角的差值,作为评估边缘的依据。这里我们采样深度值或法线到这个算子进行卷积
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 using System; using System.Collections; using System.Collections.Generic; using Unity_Shaders_Learn; using UnityEngine; public class EdgeDetectNormalAndDepth : PostEffectsBase { public Shader edgeDetectShader; private Material _edgeDetectMaterial = null; public Material EdgeDetectMaterial { get { _edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, _edgeDetectMaterial); return _edgeDetectMaterial; } } [Header("是否只显示描边"), Range(0.0f, 1.0f)] public float edgesOnly = 1.0f; [Header("描边的颜色")] public Color edgeColor = Color.black; [Header("背景颜色")] public Color backgroundColor = Color.white; [Header("采样距离")] public float sampleDistance = 1.0f; [Header("深度采样灵敏度")] public float sensitivityDepth = 1.0f; [Header("法线采样灵敏度")] public float sensitivityNormals = 1.0f; // 可以放PostEffectsBase里 private Camera _myCamera; public Camera MyCamera { get { if (_myCamera == null) { _myCamera = GetComponent<Camera>(); } return _myCamera; } } private void OnEnable() { MyCamera.depthTextureMode |= DepthTextureMode.DepthNormals; } private void OnDisable() { MyCamera.depthTextureMode &= ~DepthTextureMode.DepthNormals; } private void OnRenderImage(RenderTexture src, RenderTexture dest) { if (EdgeDetectMaterial == null) { Graphics.Blit(src, dest); return; } EdgeDetectMaterial.SetFloat("_EdgeOnly", edgesOnly); EdgeDetectMaterial.SetColor("_EdgeColor", edgeColor); EdgeDetectMaterial.SetColor("_BackgroundColor", backgroundColor); EdgeDetectMaterial.SetFloat("_SampleDistance", sampleDistance); EdgeDetectMaterial.SetVector("_Sensitivity", new Vector4(sensitivityDepth, sensitivityNormals, 0.0f,0.0f)); Graphics.Blit(src, dest, EdgeDetectMaterial); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 Shader "Unity Shaders Learn/Chapter13/EdgeDetectNormalAndDepth" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _EdgeOnly ("Edge Only", Float) = 1.0 _EdgeColor ("Edge Color", Color) = (0,0,0,1) _BackgroundColor ("Background Color", Color) = (1,1,1,1) _SampleDistance ("Sample Distance", Float) = 1.0 _Sensitivity ("Sensitivity", Vector) = (1,1,1,1) } SubShader { CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; half4 _MainTex_TexelSize; fixed _EdgeOnly; fixed4 _EdgeColor; fixed4 _BackgroundColor; float _SampleDistance; half4 _Sensitivity; sampler2D _CameraDepthNormalsTexture; struct v2f { float4 pos : SV_POSITION; half2 uv[5] : TEXCOORD0; }; v2f vert(appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); half2 uv = v.texcoord; o.uv[0] = uv; #if UNITY_UV_STARTS_AT_TOP if (_MainTex_TexelSize.y < 0.0f) { uv.y = 1 - uv.y; } #endif o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1, 1) * _SampleDistance; o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1, -1) * _SampleDistance; o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 1) * _SampleDistance; o.uv[4] = uv + _MainTex_TexelSize.xy * half2(1, -1) * _SampleDistance; return o; } half CheckSame(half4 center, half4 sample) { half2 centerNormal = center.xy; float centerDepth = DecodeFloatRG(center.zw); half2 sampleNormal = sample.xy; float sampleDepth = DecodeFloatRG(sample.zw); half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x; int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1; float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y; int isSameDepth = diffDepth < 0.1 * centerDepth;//这里为什么还要乘centerDepth return isSameNormal * isSameDepth ? 1.0 : 0.0; } fixed4 frag(v2f i) : SV_Target { // 这里不需要知道真正的法线值,所以没用解码函数 half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1]); half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2]); half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3]); half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4]); half edge = 1.0f; edge *= CheckSame(sample1, sample2); edge *= CheckSame(sample3, sample4); fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge); fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge); return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly); } ENDCG Pass { Cull Off ZWrite Off ZTest Always CGPROGRAM #pragma vertex vert #pragma fragment frag ENDCG } } Fallback Off }
如果需要只对特定物体进行描边,可以使用Unity提供的Graphics.DrawMesh或Graphics.DrawMeshNow把需要描边的物体再次渲染一遍,然后使用本节的边缘检测算法计算深度或发现纹理中每个像素的梯度值,使用Shader中的Clip函数进行剔除,从而显示出原来的物体颜色。
扩展阅读
Unity再2011年SIGGRAPH(计算机图形学的顶级会议)上一个关于深度纹理实现各种特效的演讲(利用深度纹理来实现特定物体的描边、角色护盾、相交线的高光模拟等效果):https://blogs.unity3d.com/2011/09/08/special-effects-with-depth-talk-at-siggraph/
ImageEffect实现屏幕空间环境遮挡(Screen Space Ambient Occlusion, SSAO):https://docs.unity3d.com/550/Documentation/Manual/comp-ImageEffects.html