前言
【关于作业的狡辩】
本周内容与前几周相比,难度提高了一些,本章的shader需要配合相应的脚本使用。脚本基本上承担两个作用:设置着色器的参数,以及隐式创建承载着色器的材质。因为脚本功能高度相似,可以抽象出一个父类进行统一编写(我的基类脚本为RenderImageBase)。
本周的学习为主,创新之处不多。在阅读案例的过程中,碰到了一些没见过的方法,在这里做简单记录。
【关于debug】
试敲过程中,因为多加了脚本这一层操作,可能不太方便调试,屏幕中没有效果,不知道从哪儿开始找漏洞。有时可能是没有设置编辑模式显示,有时是忘记挂上着色器了,有时是脚本编写过程中有疏漏覆盖了正确结果。
对于这种问题,建议在片元着色器内先返回一个明显的颜色(如255红),可以确保该着色器确实起效;或者在Camera上挂载已经编写成功的特效,可以确保屏幕特效可以运行。 排除这些问题之后,再在着色器内进行下一步排错。
脚本基类
在基类中需要执行的操作:
- 进行 shader 和 material 变量的声明
- 对 material 进行 get 操作,创建用于承载 shader 的自建材质
- 在 Start() 中对 shader 的可用性进行检测
- [易忘] 对 material 进行删除
【HideFlags.HideAndDontSave】
HideFlags 枚举. 隐藏并不可编辑 —— 保证该材质不在层级面板也不在场景中,完全通过创建此材质的脚本对其进行操作。
myMaterial.hideFlags = HideFlags.HideAndDontSave;
【DestroyImmediate】
DestroyImmediate 和 Destory 相比,后者实际上是对物体进行了一个标注,提供异步删除方式,即在运行的下一帧进行删除操作。而前者的作用是立即删除并移除内存。
if(myMaterial) DestroyImmediate(myMaterial);
【关于语句 “myShader. isSupported” 报错】
运行中可能会出现“not Support”的日志报错(Start 中设置的),问题可能在于脚本中没有能够支持使用的SubPass。原因之一可能是使用了UsePass命令却寻址不正确(是个十分低级但是卡了我很久的错误),详细内容在下文景深部分。
详细基类cs脚本:
using UnityEngine;
[ExecuteInEditMode]//保证在编辑模式下也能够运行
public class RenderImageBase : MonoBehaviour
{
//----------------------------变量声明---------------------------------
#region Varieables
public Shader myShader;//需要外部设置的shader
private Material myMaterial;//自己建立的材质
#endregion
//----------------------------材质的get---------------------------------
#region Properties
protected Material material{//保护级别,可以提供给子类继承
get{
//shader可用性检测
if(!myShader||!myShader.isSupported){
enabled = false;//如果shader不可用,则不启用脚本
return null;
}
//建立一个承载着色器的材质
if(myMaterial == null){
myMaterial = new Material(myShader);//建立一个用于承载着色器的材质
myMaterial.hideFlags = HideFlags.HideAndDontSave;//设置材质是否保存以及其可见性
}
return myMaterial;//将设置好的myMaterial返回
}
}
#endregion
//----------------[Start函数] 对shader的可用性进行汇报--------------------
private void Start() {
if(!myShader||!myShader.isSupported){
//shader可用性检测并报告
enabled = false;
Debug.Log("not Support");//输出日志 报告不支持
}
}
//----------------[On Disable函数] 对自建的material进行清理----------------
private void OnDisable() {
//手动删除变量
if(myMaterial) DestroyImmediate(myMaterial);//立即销毁物体并移除内存
}
}
景深效果
主要思想:使用模糊和清晰的两张图片进行筛选显示,利用 Unity 自带的像素深度信息作依据,深度信息通过筛选测试的像素显示清晰图片的信息,否则显示模糊信息。
下图所示两张图片的灰色区域是需要模糊信息的像素。
脚本部分
类似于单纯的深度显示,需要在Update中开启摄像机的深度检测模式,深度信息则送到着色器中的 _CameraDepthTexture 变量进行使用。
Camera.main.depthTextureMode = DepthTextureMode.Depth;
完整C#代码:
using System.Diagnostics;
using UnityEngine;
public class DepthOfField : RenderImageBase
{
//----------------------------变量声明---------------------------------
#region Variables
[Range(0, 3)]
public float blurSize = .6f;//取样尺寸-用于模糊
[Range(0, 3)]
public int iterations = 3;//迭代层数-用于模糊
[Range(1, 20)]
public int downSample = 2;//像素化强度-用于模糊
[Range(-.02f, 1.02f)]
public float focusDis = 2;//聚焦距离
#endregion
//------------------[Update函数] 开启深度模式--------------------------
private void Update() {
//Shader中_CameraDepthTexture信息
Camera.main.depthTextureMode = DepthTextureMode.Depth;
}
//------------------内置[OnRenderImage函数] 抓取纹理 传给着色器--------------------------
private void OnRenderImage(RenderTexture src, RenderTexture dest) {
//若在基类中成功创建材质 则进入条件
if(material){
//设置聚焦平面的距离
material.SetFloat("_FocusDis", focusDis);
//设置不因长宽比而压缩的屏幕像素化效果
var w = src.width / downSample;
var h = src.height / downSample;
//获取指定像素内容的临时渲染纹理 将像素化后的结果存入buffer0
//将获取到的Texture设置为Bilinear过滤模式
var buffer0 = RenderTexture.GetTemporary(w, h, 0);
buffer0.filterMode = FilterMode.Bilinear;
//使用着色器将源纹理scr复制到目标纹理buffer0 绘制全屏四边形
Graphics.Blit(src, buffer0);
//将像素化的纹理结果进行高斯模糊处理(具体内容见着色器)
for(int i = 0; i < iterations; i++){
material.SetFloat("_BlurSize", blurSize * i + 1.0f);
var buffer1 = RenderTexture.GetTemporary(w, h, 0);
buffer1.filterMode = FilterMode.Bilinear;
Graphics.Blit(buffer0, buffer1, material, 0);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = RenderTexture.GetTemporary(w, h, 0);
Graphics.Blit(buffer1, buffer0, material, 1);
RenderTexture.ReleaseTemporary(buffer1);
}
material.SetTexture("_BlurTex", buffer0);
Graphics.Blit(src, dest, material, 2);//仅对材质中的pass2应用,即原图通道
RenderTexture.ReleaseTemporary(buffer0);
}
else
Graphics.Blit(src, dest);//不做处理直接绘制
}
}
着色器部分
可以通过UsePass来使用对应地址的pass,但是需要注意如果pass在块内再次命名,需要包含完整的路径,包含文件起始处的位置加上重新命名(如果有)的名字。如果引用不完全可能会报着色器没有可以使用的pass的错。(!shader.isSupported)
引用处:
UsePass "Lesson/PostShader/GaussianBlur/GaussianBlur_h"
UsePass "Lesson/PostShader/GaussianBlur/GaussianBlur_v"
被引用处:
(很妙的一点在于编写过程中可以单独选择需要的着色器进行封装)
例子中的高斯模糊计算也很聪明,使用了矩阵相乘的方式,是我粗暴枚举的优化版本。
原链接的着色器和未修改的基类配合使用总是会报错,严重时会溢栈,导致无法打开unity。从资源管理器中把出问题的脚本删除掉后仍然不太清楚导致崩溃的原因。经修改后变成了如下样子(至少能跑了)
景深shader:
Shader "Lesson/PostShader/DepthOfField"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
ZTest Always
Cull Off
ZWrite Off
UsePass "Lesson/PostShader/GaussianBlur/GaussianBlur_v"
UsePass "Lesson/PostShader/GaussianBlur/GaussianBlur_h"
Pass{
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 uv : TEXCOORD0;
float2 depth_uv : TEXCOORD1;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex; //清晰的图片
sampler2D _BlurTex; //模糊处理后的图片
half4 _MainTex_TexelSize; //清晰图片的像素尺寸(4位)
sampler2D _CameraDepthTexture; //像素深度信息
float _FocusDis; //聚焦距离
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.uv;
o.uv.zw = v.uv;
o.depth_uv = v.uv;
//差异化处理:可以适用于其他一些坐标从上到下的平台
#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0){
o.uv.w = 1.0 - o.uv.w;
o.depth_uv.y = 1.0 - o.depth_uv.y;
}
#endif
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv.xy);//清晰图片
fixed4 blurCol = tex2D(_BlurTex, i.uv.zw);//模糊图片
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.depth_uv);//深度信息
depth = Linear01Depth(depth);//确保深度值在0~1之间
fixed bVa = abs(depth - _FocusDis);
return lerp(col, blurCol, bVa);
//return fixed4(1, 0, 0, 1); //可以用于调试
}
ENDCG
}
}
//Fallback "Diffuse"//可以用于调试
}
【附】引用的高斯模糊shader:
Shader "Lesson/PostShader/GaussianBlur"
{
Properties
{
_MainTex ("Main Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
CGINCLUDE
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv[5] : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float2 _MainTex_TexelSize;
float _BlurSize;
//纵向模糊
v2f vert_v (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
UNITY_TRANSFER_FOG(o,o.vertex);
float2 uv = v.uv;
o.uv[0] = v.uv;
o.uv[1] = v.uv + float2(0, _MainTex_TexelSize.y * 1.0f) * _BlurSize;
o.uv[2] = v.uv - float2(0, _MainTex_TexelSize.y * 1.0f) * _BlurSize;
o.uv[3] = v.uv + float2(0, _MainTex_TexelSize.y * 2.0f) * _BlurSize;
o.uv[4] = v.uv - float2(0, _MainTex_TexelSize.y * 2.0f) * _BlurSize;
return o;
}
//水平模糊
v2f vert_h(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
UNITY_TRANSFER_FOG(o, o.vertex);
o.uv[0] = v.uv;
o.uv[1] = v.uv + float2(_MainTex_TexelSize.x * 1.0f, 0) * _BlurSize;
o.uv[2] = v.uv - float2(_MainTex_TexelSize.x * 1.0f, 0) * _BlurSize;
o.uv[3] = v.uv + float2(_MainTex_TexelSize.x * 2.0f, 0) * _BlurSize;
o.uv[4] = v.uv - float2(_MainTex_TexelSize.x * 2.0f, 0) * _BlurSize;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 mianTex = tex2D(_MainTex, i.uv[0]);
float3 weights = {0.4026, 0.2442, 0.0545};//卷积部分权重
fixed3 finalColor = mianTex * weights[0];
finalColor += tex2D(_MainTex, i.uv[1]) * weights[1];
finalColor += tex2D(_MainTex, i.uv[2]) * weights[1];
finalColor += tex2D(_MainTex, i.uv[3]) * weights[2];
finalColor += tex2D(_MainTex, i.uv[4]) * weights[2];
fixed4 finalColorCA = fixed4(finalColor, 1);//finalColor Contant a
return finalColorCA;
}
ENDCG
ZTest Always
Cull Off
ZWrite Off
//vertical blur
Pass
{
NAME "GaussianBlur_v"
CGPROGRAM
#pragma vertex vert_v
#pragma fragment frag
ENDCG
}
//horizontal blur
Pass
{
NAME "GaussianBlur_h"
CGPROGRAM
#pragma vertex vert_h
#pragma fragment frag
ENDCG
}
}
Fallback Off
}
碎屏效果
和景深部分相比简单了很多,而且有些类似于上周的抓屏畸变内容,可以配合多个法线图进行使用。
脚本仅需要设置贴图和scale,内容较少:
private void OnRenderImage(RenderTexture src, RenderTexture dest) {
if(myShader != null){
if(!material)
return;
material.SetFloat("_scale", breakScale);
material.SetTexture("_BrokeTex", breakTex);
Graphics.Blit(src, dest, material);
}
else Graphics.Blit(src, dest);
}
着色器核心代码:
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);//清晰图片
o.uv.zw = TRANSFORM_TEX(v.uv, _BrokeTex);//扰动贴图
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//确保扰动图片使用的法线图片颜色得到正确采样
fixed3 normal = UnpackNormal(tex2D(_BrokeTex, i.uv.zw));
//构造扰动因子
float2 offset = normal.xy * _scale;
//在采样时加入扰动因子
fixed3 col = tex2D(_MainTex, i.uv.xy+ offset).rgb;
//通过使其变暗的方式来区分裂开的部分
fixed luminance = (col.r + col.g + col.b) / 5;
//通过lerp构造根据条件上色命令
fixed3 finalColor = lerp(fixed3(luminance,luminance,luminance),col,0.3);
return fixed4(finalColor, 1);
}
动态模糊
可以配合键盘产生一些类似于加速的效果。(motionScript是用于控制屏幕特效的脚本)
var v = Input.GetAxis("Vertical");
//一些移动控制
//....
//模糊程度控制
var time = 16 * v + 1;//最大层数 * v
motionScript.blurTime = (int)time;
var size = 1 - 0.4f * v;//最大间隔 * v, 使v与偏移单位大小呈反比
motionScript.blurSize = size;
但是在敲码过程中,发现这种方式的单位和层数缩放速度无法很好匹配,会有一种“先大后小”的感觉,仍需要继续迭代。
脚本部分同样比较简单:
private void OnRenderImage(RenderTexture src, RenderTexture dest) {
if(myShader != null){
material.SetInt("_blurNumber", blurTime);
material.SetFloat("_size", blurSize);
Graphics.Blit(src, dest, material);
}
else
Graphics.Blit(src, dest);
}
shader部分核心代码:
fixed4 frag (v2f i) : SV_Target
{
float2 center = float2(.5, .7);//透视中心
float2 uv = i.uv;
uv -= center;//相对于中心的uv
fixed4 finalColor = fixed4(0, 0, 0, 0);
float scaleUnit = 1;
for(int i = 0; i < _blurNumber; i++)
{
//在finalColor上逐层叠加动效残影
float shadow = pow(0.8, _blurNumber + 1);
//在原有颜色上叠加每一次偏移的内容
finalColor += tex2D(_MainTex, uv * _size * scaleUnit * shadow + center);
scaleUnit += 1;
}
//将叠加颜色的过曝效果抵消
finalColor /= _blurNumber;
return finalColor;
}