Unity Shader 实现简单的贴花效果(二)

这个月进入了找不倒工作的焦虑状态,花了两周时间去学OpenGL,发现以前课上的讲的内容过于浅显,也加深了对渲染管线的了解也算是相当不错的吧。
话不多说,先上最初的效果图吧。
本效果考了该篇博客Unity Shader-Decal贴花
大佬的一篇博客的n个效果都够我研究n回写n篇博客了,请收下我的膝盖。
在这里插入图片描述
貌似跟上次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
		}
	}
}

好了,文章到此结束,感谢你的阅读,如有错误欢迎指正。

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值