这个月进入了找不倒工作的焦虑状态,花了两周时间去学OpenGL,发现以前课上的讲的内容过于浅显,也加深了对渲染管线的了解也算是相当不错的吧。
话不多说,先上最初的效果图吧。
貌似跟上次MeshDecal的效果差不多(祖传贴图拉长还在),但是这次只用了一个Shader就实现了,真是tql,“还要啥自行车!”。
我先来讲讲思路吧,原文章就写了个三角形相似,我想了许久才知道什么原理,我个人感觉还是比较绕而且不太好理解。
我们就是要通过B点求D点,C点(该死的试用水印把C点挡住了,见谅)求E点,图画得不太好也请见谅。明显可以看到我们可以构建等比三角形OCa和OEb,其实我个人认为也可以把OC与OE理解为两组向量,我们可以通过比例用OC求出OE。
那怎么求呢?说了是用比例,那么用的是哪个比例?我们能用的大概也就是Z坐标了,我们获取深度缓冲里的深度值,并将它还原到视角空间下的Z值(即上图D或E的值),然后将B或C转换到视角空间,这样通过他们z值的比例我们就可以通过B,C求出D,E。
上图出现了三种情况,一是A点对应的点不在圆柱上,二是D在圆柱上且在正方形内,三是E点在圆柱上但是不在正方形上,我们把一三情况的片元剔除掉,只保留第二种情况,就造成一种视觉假象是图片贴到了圆柱上。
以下是代码:
Shader "Copy/DepthDecal"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags {"Queue"="Transparent+100"}
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 screenPos : TEXCOORD1;
float3 ray : TEXCOORD2;
};
sampler2D _MainTex;
sampler2D_float _CameraDepthTexture;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.screenPos = ComputeScreenPos(o.vertex);
o.ray =UnityObjectToViewPos(v.vertex)*float3(-1,-1,1);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//深度重建视空间坐标
float2 screenuv = i.screenPos.xy / i.screenPos.w;//透视除法
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenuv);//内置宏对深度纹理采样
//当通过纹理采样SAMPLE_DEPTH_TEXTURE之后,得到的深度值往往是非线性的。
float viewDepth = Linear01Depth(depth) * _ProjectionParams.z;//乘以远裁剪平面,恢复原来的值
//Linear01Depth返回一个范围在【0,1】的线性深度值
float3 viewPos = i.ray* (viewDepth / i.ray.z);
//转化到世界空间坐标
float4 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1.0));
//转化为物体空间坐标
float3 objectPos = mul(unity_WorldToObject, worldPos);
//剔除掉在立方体外面的内容
clip(float3(0.5, 0.5, 0.5) - abs(objectPos)); //abs计算输入值的绝对值。
//使用物体空间坐标的xz坐标作为采样uv
float2 uv = objectPos.xz+0.5;
fixed4 col = tex2D(_MainTex, uv);
return col;
}
ENDCG
}
}
}
ComputeScreenPos(o.vertex); 该函数输入的是一个其次裁剪空间坐标,返回的是归一化的标准坐标(NDC),但是切记
这个坐标没有经经过透视除法,所以在片元着色器中才有了这段代码:
float2 screenuv = i.screenPos.xy / i.screenPos.w;
下面的这段代码是我唯一没有搞懂的地方:
o.ray =UnityObjectToViewPos(v.vertex)*float3(-1,-1,1);
为什么要乘以float3(-1,-1,1)?按字面理解是将x、y轴的方向翻过来,但是为什么?开始我想的是unity的视角空间是
右手坐标系然后我把float3(-1,-1,1)换成float3(1,1,-1)也没有问题,但是当我在片元着色器这样做时渲染效果却不
正确
float3 viewPos = i.ray*float3(1,1,-1)* (viewDepth / i.ray.z);
但是这样却是正确的
float3 viewPos = i.ray*float3(-1,-1,1)* (viewDepth / i.ray.z);
我就百思不得其解,果然还是太菜了吗......
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenuv);
当通过纹理采样SAMPLE_DEPTH_TEXTURE之后,得到的深度值往往是非线性的。为什么说是往往?我也不知道,我只知道
深度缓冲的值是非线性的。所以我们要用Linear01Depth(depth)获取线性的深度值,方便我们计算。
float viewDepth = Linear01Depth(depth) * _ProjectionParams.z;
这样行代码是通过线性的深度值还原视角空间未进行过处理的z坐标
有关_ProjectionParams
//x = 1,如果投影翻转则x = -1
//y是camera近裁剪平面
//z是camera远裁剪平面
//w是1/远裁剪平面
//Linear01Depth返回一个范围在【0,1】的线性深度值
float3 viewPos = i.ray* (viewDepth / i.ray.z);
这行代码是整个shader的精髓,也是关键,我们通过三角形相似的原理,用正方体上的片元视角空间坐标反求圆柱的视角空
间片元坐标,随后我们进行了两次坐标系变换(以判断圆柱是否在正方体内),不得不说虽然代码简单效果也不错,但是在片
元着色器里进行两次矩阵运算对性能是一个较大的损耗。
float2 uv = objectPos.xz+0.5;
xz范围在[-0.5,0.5],所以要加0.5。
关于为什么深度缓冲为什么是非线性的,同这也是一个非常好的学习OpenGL的网站深度测试
接下来是两个优化效果,都是针对祖传贴图拉长的。
警告:该优化只在game窗口有正确的渲染效果,scene窗口十分鬼畜,且都需要DepthTextureMode.DepthNormals!
以下是添加到摄像机的脚本:
using UnityEngine;
[ExecuteInEditMode]
public class CameraDepthNormalHelper : MonoBehaviour {
public DepthTextureMode depthMode = DepthTextureMode.DepthNormals;
void OnEnable()
{
GetComponent<Camera>().depthTextureMode = depthMode;
}
void OnDisable()
{
GetComponent<Camera>().depthTextureMode = DepthTextureMode.None;
}
}
第一:根据原文的指导思想,不对的就不要渲染出来,好主意Ψ( ̄∀ ̄)Ψ!!
思路就是获取视角空间下的法线坐标,通过与转换到视角空间下的模型空间的y轴进行点乘,把大于等于90度的片元统统丢掉。
以下是效果图:
Scene窗口:
Game窗口:
接下来是代码:
Shader "NewStart/DepthDecalClip"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Threshold("Threshold",Range(0,0.3))=0.1
}
SubShader
{
Tags {"Queue"="Transparent+100"}
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 screenPos : TEXCOORD1;
float3 ray : TEXCOORD2;
float3 yDir:TEXCOORD3;
};
sampler2D _MainTex;
sampler2D_float _CameraDepthNormalsTexture;
float _Threshold;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.screenPos = ComputeScreenPos(o.vertex);//该函数返回的是齐次坐标下的未经过透视除法的屏幕坐标值
o.ray =UnityObjectToViewPos(v.vertex)*float3(-1,-1,1);//将一个点从object空间转换为view空间。
o.yDir=UnityObjectToViewPos(float3(0,1,0));
//将模型的Y轴转换到视角空间
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//深度重建视空间坐标
float2 screenuv = i.screenPos.xy / i.screenPos.w;//透视除法
float depth;
float3 viewNormal;
// 返回一个视空间深度值 和 一个视空间法线 //
// 该深度值是一个线性深度值, 范围是0到1, 精度小于使用SAMPLE_DEPTH_TEXTURE方法获得的深度值 //
// 因为DecodeDepthNormal方法中的深度值用16位存储,而SAMPLE_DEPTH_TEXTURE中的深度值用32位存储 //
float4 cdn=tex2D(_CameraDepthNormalsTexture,screenuv);
DecodeDepthNormal(cdn,depth,viewNormal);
float viewDepth = depth * _ProjectionParams.z;//乘以远裁剪平面,恢复原来的值
float3 viewPos = i.ray* (viewDepth / i.ray.z);
//转化到世界空间坐标
float4 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1.0));
//转化为物体空间坐标
float3 objectPos = mul(unity_WorldToObject, worldPos);
//剔除掉在立方体外面的内容
clip(float3(0.5, 0.5, 0.5) - abs(objectPos)); //abs计算输入值的绝对值。
viewNormal=normalize(viewNormal);
float3 yDir=normalize(i.yDir);
clip(dot(yDir,viewNormal)-_Threshold);
//使用物体空间坐标的xz坐标作为采样uv
float2 uv = objectPos.xz+0.5;
fixed4 col = tex2D(_MainTex, uv);
return col;
}
ENDCG
}
}
}
代码没什么可说的,无非就是采样的纹理对象不一样了,并且多了一个法线,看得懂第一个这个肯定也没问题
第二:对y轴的的影响也考虑进UV采样中,参考Unity Decal 贴花效果测试 效果:
Scene:
Game:
Shader "NewStart/DepthDecalClip"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Threshold("Threshold",Range(0,0.3))=0.1
}
SubShader
{
Tags {"Queue"="Transparent+100"}
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 screenPos : TEXCOORD1;
float3 ray : TEXCOORD2;
float3 yDir:TEXCOORD3;
};
sampler2D _MainTex;
sampler2D_float _CameraDepthNormalsTexture;
float _Threshold;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.screenPos = ComputeScreenPos(o.vertex);//该函数返回的是齐次坐标下的未经过透视除法的屏幕坐标值
o.ray =UnityObjectToViewPos(v.vertex)*float3(-1,-1,1);//将一个点从object空间转换为view空间。
o.yDir=UnityObjectToViewPos(float3(0,1,0));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//深度重建视空间坐标
float2 screenuv = i.screenPos.xy / i.screenPos.w;//透视除法
float depth;
float3 viewNormal;
// 返回一个视空间深度值 和 一个视空间法线 //
// 该深度值是一个线性深度值, 范围是0到1, 精度小于使用SAMPLE_DEPTH_TEXTURE方法获得的深度值 //
// 因为DecodeDepthNormal方法中的深度值用16位存储,而SAMPLE_DEPTH_TEXTURE中的深度值用32位存储 //
float4 cdn=tex2D(_CameraDepthNormalsTexture,screenuv);
DecodeDepthNormal(cdn,depth,viewNormal);
float viewDepth = depth * _ProjectionParams.z;//乘以远裁剪平面,恢复原来的值
float3 viewPos = i.ray* (viewDepth / i.ray.z);
//转化到世界空间坐标
float4 worldPos = mul(unity_CameraToWorld, float4(viewPos, 1.0));
//转化为物体空间坐标
float3 objectPos = mul(unity_WorldToObject, worldPos);
//剔除掉在立方体外面的内容
clip(float3(0.5, 0.5, 0.5) - abs(objectPos)); //abs计算输入值的绝对值。
viewNormal=normalize(viewNormal);
float3 yDir=normalize(i.yDir);
float nodt=dot(yDir,viewNormal);
float offset=1-nodt;
float2 uv = objectPos.xz+0.5+offset*float2(0,objectPos.y);
fixed4 col = tex2D(_MainTex, uv);
return col;
}
ENDCG
}
}
}