欢迎来到Shader的世界 Shader的学习之所以比学习C++、C#等编程语言难,一个原因就是因为Shader需要牵扯到对整个渲染流程的理解。
渲染流水线 应用阶段(Application Stage)、几何阶段(Geometry Stage)、光栅化阶段(Rasterizer Stage)
什么是shader
Unity Shader基础
Standard Surface Shader:包含标准关照迷行的shader;
Unlit Shader:不包含光效但包含雾效的基本shader;
Image Effect Shader:实现屏幕处理的shader;
Compute Shader:进行与渲染流水线无关的计算;
ShaderLab 基础结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Shader "ShaderName" { Properties { // 属性 } SubShader { // 显卡1 } SubShader { // 显卡2 } Fallback "VertexLit" }
shader名字:
1 Shader "Custom/MyShader"{} // 显示在Shader -> Custom -> MyShader
属性(Properties):
1 2 3 4 5 Properties { Name ("display name", PropertyType) = DefaultValue Name ("display name", PropertyType) = DefaultValue }
仿版在脚本与材质面板中方便的调整。
属性类型:Int, Float, Range(min, max), Color, Vector, 2D, 3D, Cube;
SubShader:
可以包含多个,但至少有一个。当加载这个shader的时候,unity会扫描所有SubShader语义块,选择第一个能够在对应平台运行的SubShader。如果都不支持,则使用Fallback指定的shader。
1 2 3 4 5 6 7 8 9 10 SubShader { // 可选的 [Tags] // 标签 // 可选的 [RenderSetup] // 状态 Pass {} // 其他的Pass }
SubShader中定义了一系列Pass,每个Pass中定义了一次完整的渲染流程。Pass数目过多会导致渲染性能的下降。状态和标签同样可以在Pass中声明,但有些标签设置SubShader和Pass不一样。在SubShader中进行这些设置将会作用于所有的Pass。
状态设置:渲染状态的设置指令。
状态名称
设置指令
解释
Cull
Cull Back | Front | Off
设置剔除模式:背面/正面/关闭
ZTest
ZTest Less Greater | LEqual |GEqual | Equal | NotEqual | Always
设置深度测试
ZWrite
ZWrite On | Off
开启关闭深度写入
Blend
Blend SrcFactor DstFactor
开启并设置混合模式
可以在Pass语句中单独设置。
标签:
1 Tags {"TagName1" = "Value1" "TagName2" = "Value2"}
支持的标签块类型:
Queue:渲染顺序,比如透明不透明之类的。
RenderType:着色器分类,透明不透明,可以被利用于着色器替换。
DisableBatching:是否采用批处理。
ForceNoShadowCasting:是否会投射阴影。
IgnoreProjector:是否受Projector的影响,通常用于半透明物体。
CanUseSpriteAtlas:如果shader用于Sprites的时候设置为false。
PreviewType:设置预览,比如Plane或SkyBox啥的。
这些标签只能在SubShader中声明,不能再Pass中声明。pass中也可以定义,但与这些不同。
Pass
1 2 3 4 5 6 7 Pass { [Name] [Tags] [RenderSetup] // 其他 }
Pass名称:
通过这个可以在别的shader中使用这个Pass:
1 UsePass "MyShader/MYPASSNAME" // 必须大写
Pass支持的标签类型:
LightMode:Pass在Unity渲染流水线中的角色。
RequireOptions:满足某些条件时才渲染这个Pass,SoftVegation。
UsePass:上面提到的一样。
GrabPass:抓取屏幕并将结果储存在纹理中,一遍后续的Pass处理。
Fallback:最低级的shader。
Unity Shader形式 表面着色器(Surface Shader) :Unity自己创造的一种着色器代码类型,本质上还是顶点和片元着色器。渲染代价比较大,Unity为我们处理了很多光照细节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Shader "Custom/Surface Shader" { SubShader { Tags {"RenderType" = "Opaque"} CGPROGRAM #pragma surface surf Lambert struct Input { float4 color : COLOR; }; void surf(Input IN, inout SurfaceOutput o) { o.Albedo = 1; } ENDCG } FallBack "Diffuse" }
表面着色器不需要定义Pass。
顶点/片元着色器(Vertex/Fragment 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 Shader "Custom/Simple VF Shader" { Subshader { Tags {} Pass { Tags{} CGPROGRAM #pragma vertex vert #pragma fragment frag float4 vert(float v : POSITION) : SV_POSITION { return mul(UNITY_MATRIX_MVP, v); } fixed4 frag() : SV_TARGET { return fixed4(1, 0, 0, 1); } ENDCG } } }
固定函数着色器(Fixed Function Shader) :对于较旧的设备,不支持可编程渲染管线。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Shader "Custom/Fixed Shader" { Properties { _Color ("Main Color", Color) = (1, 1, 1, 1) } SubShader { Pass { Material { Diffuse [_Color] } Lighting On } } }
学习Shader所需的数学基础 笛卡尔坐标系
笛卡尔坐标系(Cartesian Coordinate System)
基矢量(basis vector) :坐标轴.
正交基(orthogonal basis) :互相垂直的基矢量.
标准正交基(orthonormal basis) :互相垂直且长度为1的基矢量.
Unity使用Y轴向上的左手系。但在观察空间,也就是相机坐标是右手系,摄像机的前向是-z方向。
点和矢量 矩阵
正交矩阵(orthogonal matrix) :方阵M和它的转置矩阵相乘是单位矩阵的话。一组标准正交基可以满足这个条件。
矩阵的几何意义:变换 坐标空间 模型变换 -> 透视投影 -> 剪裁 -> 齐次除法 -> 屏幕变换
法线变换 p86 Unity Shader的内置变量(数学篇)p87 变换矩阵
变量名
描述
UNITY_MATRIX_MVP
MVP矩阵,用于将顶点和方向从模型空间转换到剪裁空间。
UNITY_MATRIX_MV
将顶点和方向从模型空间变换到观察空间
UNITY_MATRIX_V
将顶点和方向从世界空间变换到观察空间
UNITY_MATRIX_P
将顶点和方向从观察空间变换到剪裁空间
UNITY_MATRIX_VP
将顶点和方向从世界空间变换到剪裁空间
UNITY_MATRIX_T_MV
MV的转置矩阵
UNITY_MATRIX_IT_MV
MV的逆转置矩阵,可以转换法线,或者获得MV的逆矩阵
_Object2World
模型矩阵,模型空间到世界空间
_World2Object
从世界空间到模型空间
MVP -> UnityObjectToClipPos(*)
replaced ‘_World2Object’ with ‘unity_WorldToObject’
replaced ‘_Object2World’ with ‘unity_ObjectToWorld’
比如要将观察空间中的点或者方向变换到模型空间:
1 2 3 4 // 方法一:用转置矩阵 float4 modelPos = mul(transpose(UNITY_MATRIX_IT_MV), viewPos); // 方法二:交换相乘 float4 modelPos = mul(viewPos, UNITY_MATRIX_IT_MV);
摄像机和屏幕参数:
变量名
类型
描述
_WorldSpaceCameraPos
float3
该摄像机在世界空间中的位置
_ProjectionParams
float4
x = 1.0, y = Near, z = Far, w = 1.0 + 1.0/Far
_ScreenParams
float4
x = width, y = height, z = 1.0 + 1.0/width, w = 1.0 + 1.0/height
_ZBufferParams
float4
x = 1 - Far/Near, y = Far/Near, z = x/Far, w = y/Far
unity_OrthoParams
float4
x = width, y = height, z = null, w = 正交为1,透视为0
unity_CameraProjection
float4x4
该摄像机的投影矩阵
unity_CameraInvProjection
float4x4
该摄像机投影矩阵的逆矩阵
unity_CameraWorldClipPlanes[6]
flloat4
6个剪裁平面在世界空间下的等式:左右下上近远
填充矩阵行优先。不过Unity在脚本中提供了一种Matrix4x4,这个是列优先。
屏幕坐标:语义是VPOS或WPOS。
开始Unity 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 Shader "ShaderLearn/Simple VF Shader" { Properties { // 属性 } SubShader { // 针对显卡A的SubShader Pass { // 设置渲染状态和标签 // 开始CG代码片段 CGPROGRAM // 该代码片段的编译指令,比如: #pragma vertex vert #pragma fragment frag // CG代码写在这里 ENDCG // 其他设置 } // 其他需要的Pass } SubShader { // 针对显卡B的SubShader } // 上述SubShader都失败后回调的Shader FallBack "VertexLit" }
自定义结构体:
1 2 3 4 5 6 struct StructName { TypeName : Semantic; TypeName : Semantic; ... };
属性类型和变量类型对应:
Color, Vector -> float4, half4, fixed4
Range, Float -> float, half, fixed
2D -> sampler2D
Cube -> samplerCube
3D ->sampler3D
Unity提供的内置文件和变量 语义(semantics) 是不可省略的,告诉系统用户的需要哪些输入值,以及用户的输出是什么。
从app传到顶点着色器
语义
描述
POSITION
模型空间中的顶点位置,float4
NORMAL
顶点法线,float3
TANGENT
顶点切线,float4
TEXCOORDn
纹理坐标
COLOR
顶点颜色,fixed4或float4
顶点着色器传到片元着色器
语义
描述
SV_POSITION
裁剪空间中的顶点坐标
COLOR0
第一组顶点颜色,非必须
COLOR1
第二组顶点颜色,非必须
TEXCOORD1-7
输出纹理坐标,非必须
片元着色器输出,只有一个SV_TARGET语义。
渲染平台的差异 UNITY_INITIALIZE_OUTPUT(Input, o); 初始化输出。
DirectX 9/11不支持在顶点着色器中使用tex2D函数,可以使用tex2Dlod替代。
Shader整洁之道 三种精度:
float:最高精度浮点值,32位;
half:中等精度浮点值,16位,范围-60000 - 60000;
fixed:最低精度浮点值,11位,-2.0 - 2.0;
在现代桌面GPU中将所有都按float计算,所以无区别,只有移动平台有区别。且现代的移动平台GPU也将fixed和half当成同等精度对待。但不管怎么说,用尽可能低精度的。
尽量不在Shader中用分支语句。
Unity中的基础光照 标准光照模型 Phong shading:逐像素
Gouraud shading:逐顶点
在Unity Shader中实现漫反射光照模型 在只有一个平行光的情况下:
在Unity Shader中实现高光反射光照模型
使用Unity内置的函数 UnituCG.cginc中一些常用的函数:
float3 WorldSpaceViewDir (float4 v):输入模型空间的顶点位置,返回世界空间中的摄像机的观察方向。
float3 UnityWorldSpaceViewDir (float4 v):输入世界空间中的顶点位置,返回世界空间中的摄像机的观察方向。
float3 ObjSpaceViewDir (float4 v):输入模型空间中的顶点位置,返回模型空间中摄像机的观察方向。
float3 WorldSpaceLightDir (flaot4 v):仅可用于前向渲染 。输入模型空间中的顶点位置,返回世界空间中的光照方向。
float3 UnityWorldSpaceLightDir (float4 v):仅可用于前向渲染 。输入世界空间中的定点位置,返回世界空间中的光照方向,没有被归一化。
float3 ObjSpaceLightDir (float3 norm):仅可用于前向渲染 。输入模型空间中的顶点位置,返回模型空间中的光照方向,没有被归一化。
float3 UnityObjectToWorldNormal (float3 norm):将法线从模型空间转换到世界空间。
float3 UnityObjectToWorldDir (in float3 dir):把方向矢量从模型空间变换到世界空间。
float3 UnityWorldToObjectDir (float3 dir ):把方向矢量从世界空间变换到模型空间。
基础纹理 凹凸映射 切线空间计算
世界空间计算
渐变纹理 半兰伯特模型、Wrap Mode: Clamp
遮罩纹理 透明效果
透明度测试(Alpha Test) :开启深度测试,根据透明度舍弃一些片元。要不就完全透明,要不就完全不透明。
透明度混合(Alpha Blending) :根据透明度进行混合。
先渲染不透明物体,再渲染半透明物体
半透明物体按照距离摄像机的远近排序,从后往前渲染
Unity Shader的渲染顺序
名称
队列索引号
描述
Background
1000
最先被渲染,绘制在背景上的物体
Geometry
2000
默认渲染队列,不透明物体。
AlphaTest
2450
透明度测试队列
Transparent
3000
透明度混合队列
Overlay
4000
最后渲染,叠加效果
透明度测试 透明度混合 Blend:
Blend Off:关闭混合
Blend SrcFactor DstFactor:开启混合并设置混合因子。源颜色(该片元产生的颜色)会乘以SrcFactor,目标颜色(已经存在在缓冲区的颜色会乘以DstFactor),将两者相加然后再存入颜色缓冲。
Blend SrcFactor DstFactor, SrcFactorA DstFactorA:和上面几乎一样
BlendOp BlendOperation:并非简单混合,使用BlendOperation进行其他操作。
开启深度写入的半透明效果 使用两个Pass:第一个开启深度写入,不输出颜色只为了把魔性的深度值写入深度缓冲;第二个进行正常的透明度混合。
这种方法可以实现模型与背后背景的混合,但模型内部之间不会有任何真正的半透明效果。
ShaderLab混合命令 源颜色(source color) :片元着色器产生的颜色值;
目标颜色(destination color) :颜色缓冲中的值;
设置混合因子的命令:Blend SrcFactor DstFactor、Blend SrcFactor DstFactor, SrcFactorSA DstFactorA
混合因子:
One:1
Zero:0
SrcColor:源颜色
SrcAlpha:源颜色透明度值
DstColor:目标颜色
DstAlpha:目标颜色透明度值
OneMinusSrcColor:1 - 源颜色
OneMinusSrcAlpha:1 - 源颜色透明度值
OneMinusDstColor:1 - 目标颜色
OneMinusDstAlpha1 - 目标颜色透明度值
混合操作:BlendOp BlendOperation
Add:默认的相加
Sub:源 - 目标
RevSub:目标 - 混合
Min:各个分量较小的
Max:各个分量较大的
常见混合类型
正常:Blend SrcAlpha OneMinusSrcAlpha
柔和相加:Blend OneMinusDstColor One
正片叠底:Blend DstColor Zero
两倍相乘:Blend DstColor SrcColor
变暗:BlendOp Min Blend One One
变亮:BlendOp Max Blend One One
滤色:Blend OneMinusDstColor One
线性减淡:Blend One One
双面渲染的透明效果
透明度测试可以直接关闭Cull
透明度混合需要两个Pass,一个只渲染背面,一个只渲染正面
更复杂的光照 渲染路径
LightMode
Always:不管用什么渲染路径都会被渲染,不计算光照;
ForwardBase:前向渲染 。会计算环境光,最重要的平行光,逐顶点光源和Lightmaps;
ForwardAdd:前向渲染 。计算额外的逐顶点光源,每个Pass对应一个光源;
Deferred:延迟渲染 。会渲染G-buffer;
ShadowCaster:把物体的深度信息渲染到阴影映射纹理中。
PrepassBase:遗留的延迟渲染 ;
PrepassFinal:遗留的延迟渲染 ;
Vertex、VertexLMRGBM、VertexLM:遗留的顶点照明 ;
前向渲染 有三种处理光照的方式:逐顶点处理、逐像素处理、球谐函数(Spherical Harmonics,SH)处理
光源属于逐顶点还是逐像素取决于类型和渲染模式,类型指的是平行光或者其他类型,渲染模式指的是细节面板的重要性(important) 。
Base Pass :一个逐像素平行光以及所有逐顶点和SH光源
Tags {“LightMode” = “ForwardBase”}
#pragma multi_compile_fwdbase
Additional Pass :其他影响该物体的逐像素光源,每个光源执行一次此Pass
Tags {“LightMode” = “ForwardAdd”}
Blend One One
#pragma multi_compile_fwdadd
前向渲染内置的光照变量:
名称
类型
描述
_LightColor0
float4
该Pass处理的逐像素光源的颜色
_WorldSpaceLightPos0
float4
xyz是该Pass处理的逐像素光源的位置,如果是平行光则w = 0,其他w = 1
_LightMatrix0
float4x4
从世界空间到光源空间的变换矩阵。可用于采样cookie和光强衰减(attenuation)纹理
unity_4LightPosX0(Y0/Z0)
float4
仅用于Base Pass ,前4个非重要点光源在世界空间的位置
unity_4LightAtten0
float4
仅用于Base Pass ,前四个非重要点光源的衰减因子
unity_LightColor
half4[4]
仅用于Base Pass ,前四个非重要点光源的颜色
前向渲染内置的光照函数
函数名
描述
float3 WorldSpaceLightDir (float4 v)
输入一个模型空间中的顶点位置,返回世界空间中的光照方向,没有被归一化
float3 UnityWorldSpaceLightDir (float4 v)
输入一个世界空间中的顶点位置,返回世界空间中的光照方向,没有被归一化
float3 ObjSpaceLightDir (float4 v)
输入一个模型空间中的顶点位置,返回模型空间中的光照方向,没有被归一化
float3 Shaded4PointLights (…)
计算四个点光源的光照
顶点照明渲染路径 已被抛弃 p185
延迟渲染 更古老的渲染方法,由于前向渲染在灯光数量增加的时候回大幅度降低性能的瓶颈存在,近些年又流行起来。
原理:
主要包含两个Pass,第一个Pass中不进行光照计算,仅计算哪些片元是可见的(深度缓冲技术)。如果一个片元是可见的,就将其的相关信息储存到G-buffer中。在第二个Pass中,利用G-buffer中的各个片元信息(表面法线、漫反射系数等)进行光照计算。
优点:
光源数目多的时候使用;
每个光源都可以逐像素;
缺点:
不支持真正的抗锯齿功能;
不能处理半透明物体
对显卡有一定要求。显卡必须支持MRT(Multiple Render Targets)、Shader Mode 3.0及以上、深度纹理渲染以及双面的模板缓冲。
Pass1 :用于渲染G-buffer,对于每个物体,这个Pass只执行一次。
Pass2 :计算真正的光照模型。包含以下渲染纹理(Render Textrue)
RT0:ARGB32,RGB储存漫反射颜色,A通道没有使用。
RT1:ARGB32,RGB储存高光反射颜色,A通道用于储存高光反射颜色。
RT2:ARGB2101010,RGB储存法线,A没有使用。
RT3:ARGB32(非HDR)或ARGBHlaf(HDR),储存自发光 + Lightmap + 反射探针。
深度缓冲和模板缓冲。
延迟渲染路径的内置变量:
名称
类型
描述
_LightColor
float4
光源颜色
_LightMatrix
float4x4
从世界空间到光源空间的变换矩阵。用于采样cookie和光源衰减纹理
Unity的光源类型 平行光 :几何属性只有方向,没有位置和衰减;
点光源 :由球体定义。
聚光灯 :最复杂的一种,
前向渲染中处理多种光源 Base Pass:
#pragam multi_compile_fwdbase
环境色 + 漫反射 + 高光
Add Pass
#pragam multi_compile_fwdadd
判断灯光(#ifdef USING_DIRECTIONAL_LIGHT)
无环境光
Unity的光照衰减 使用_LightTexture0
如果使用了cookie,那么应该使用_LightTextureB0
Unity的阴影
如果想要接受阴影,就必须在shader中对阴影映射纹理进行采样,然后把采样结果与最后的光照结果相乘来产生阴影效果。
如果想要投射阴影,就必须把该物体加入到光源的阴影映射纹理计算中(使用LightMode为ShadeowCaster的Pass)。
不透明物体的阴影
#include “AutoLight.cginc”
SHADOW_COORDS TRANSFER_SHADOW SHADOW_ATTENUATION
命名规范: a2v结构体中顶点坐标变量名必须是vertex;顶点着色器输入变量必须被命名为v;v2f中顶点位置变量必须命名为pos。
统一管理光照衰减和阴影
UNITY_LIGHT_ATTENUATION:计算光照衰减和阴影的宏。
透明度物体的阴影
透明度测试:Fallback “Transparent/Cutout/Vertexlit” Cast Shadow设置为TwoSide。
透明度混合:半透明无阴影。
标准Unity Shader 高级纹理 立方体纹理
反射
1 2 3 o.worldRefl = reflect(-o.worldViewDir, o.worldNormal); ... fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb;
折射
1 2 3 o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio); ... fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.rgb;
菲涅尔效果
Schlick菲涅尔近似等式:F (v, n) = F0 + (1 - F0)(1 - v · n)^5
Empricial菲涅尔近似等式:F (v, n) = max (0, min(1, bias + scale * (1 - v · n)^power ))
1 fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldNormal, worldViewDir), 5);
渲染纹理
程序纹理
简单程序纹理
1 2 3 4 5 6 Texture2D proceduralTexture = new Texture2D(textureWidth, textureWidth); ··· proceduralTexture.SetPixel(w, h, pixel); ··· proceduralTexture.Apply(); material.SetTexture("_MainTex", proceduralTexture);
Unity的程序材质
配合Substance Designer使用
让画面动起来 Unity Shader中内置变量(时间)
名称
类型
描述
_Time
float4
自该场景加载所经过的时间,四个分量是(t / 20, t, 2t, 3t)
_SinTime
float4
时间的正弦值,四个分量是(t / 8, t / 4, t / 2, t)
_CosTime
float4
时间的余弦值,四个分量是(t / 8, t / 4, t / 2, t)
unity_DeltaTime
float4
时间增量,四个分量是(dt, 1 / dt, smoothDt, 1 / smoothDt)
纹理动画
序列帧动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 fixed4 frag (v2f i) : SV_Target { float time = floor(_Time.y * _Speed); float row = floor(time / _HorizontalAmount); float column = time - row * _HorizontalAmount; half2 uv = i.uv + half2(column, -row); uv.x /= _HorizontalAmount; uv.y /= _VerticalAmount; fixed4 color = tex2D(_MainTex, uv); color.rgb *= _Color; return color; }
滚动的背景
1 2 3 4 5 6 7 8 9 10 v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0) * _Time.y); o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0) * _Time.y); return o; }
顶点动画 1 "DisableBatching" = "True" // 禁用批处理
如果要正确投射阴影的话要加入Cast Shadow的Pass
屏幕后处理效果 建立一个基本的屏幕后处理脚本系统 1 MonoBehaviour.OnRenderImage (RenderTexture src, RenderTexture dest)
其中src是源纹理,dest是目标渲染纹理。
通常使用Graphics.Blit函数来完成对RenderTexture的处理。
Unity中实现屏幕后处理效果:
在摄像机上添加屏幕后处理脚本,实现OnRenderImage函数
调用Graphics.Blit函数,使用特定shader处理屏幕图像。
而且在进行屏幕后处理之前,需要检查一些列条件是否满足,例如当前平台是否支持渲染纹理和屏幕特效,是否支持当前使用的Unity Shader等。
后处理脚本的基类:
调整屏幕的亮度、饱和度和对比度 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 sampler2D _MainTex; float _Brightness; float _Saturation; float _Contrast; fixed4 frag (v2f i) : SV_Target { fixed4 renderTex = tex2D(_MainTex, i.uv); // 亮度 fixed3 finalColor = renderTex.rgb * _Brightness; // 饱和度 fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b; fixed3 luminanceColor = fixed3(luminance, luminance, luminance); finalColor = lerp(luminanceColor, finalColor, _Saturation); // 对比度 fixed3 avgColor = fixed3(0.5, 0.5, 0.5); finalColor = lerp(avgColor, finalColor, _Contrast); return fixed4(finalColor, renderTex.a); }
边缘检测 卷积
边缘检测常用的卷积核:Roberts Prewitt Sobel
使用Sobel实现:
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 struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv[9] : TEXCOORD0; float4 pos : SV_POSITION; }; ··· v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); half2 uv = v.uv; o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1); o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1); o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1); o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0); o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0); o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0); o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1); o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1); o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1); return o; } fixed Luminance(fixed4 color) { return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b; } half Sobel(v2f i) { // Sobel算子 const half gX[9] = {-1, -2, -1, 0, 0, 0, 1, 2, 1}; const half gY[9] = {-1, 0, 1, -2, 0, 2, -1, 0, 1}; half texColor; half edgeX = 0; half edgeY = 0; for(int it = 0; it <9; it++) { texColor = Luminance(tex2D(_MainTex, i.uv[it])); edgeX += texColor * gX[it]; edgeY += texColor * gY[it]; } half edge = 1 - abs(edgeX) - abs(edgeY); return edge; } fixed4 frag (v2f i) : SV_Target { half edge = Sobel(i); fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge); fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge); return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly); }
这个会导致阴影也被检测出边缘,下面还有更高级的边缘检测
高斯模糊
脚本:
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 private void OnRenderImage (RenderTexture src, RenderTexture dest ){ if (material) { int rtW = src.width / downSample; int rtH = src.height / downSample; RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0 ); buffer0.filterMode = FilterMode.Bilinear; Graphics.Blit(src, buffer0); for (int i = 0 ; i < iterations; i++) { material.SetFloat("_BlurSize" , 1 + i * blurSize); RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0 ); Graphics.Blit(buffer0, buffer1, material, 0 ); RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0 ); Graphics.Blit(buffer0, buffer1, material, 1 ); RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; } Graphics.Blit(buffer0, dest); RenderTexture.ReleaseTemporary(buffer0); } else { Graphics.Blit(src, dest); } }
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 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 CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; half4 _MainTex_TexelSize; float _BlurSize; struct v2f { float4 pos : SV_POSITION; half2 uv[5] : TEXCOORD0; }; v2f vertBlurVertical(appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); half2 uv = v.texcoord; o.uv[0] = uv; o.uv[1] = uv + float2(0, _MainTex_TexelSize.y * 1) * _BlurSize; o.uv[2] = uv - float2(0, _MainTex_TexelSize.y * 1) * _BlurSize; o.uv[3] = uv + float2(0, _MainTex_TexelSize.y * 2) * _BlurSize; o.uv[4] = uv - float2(0, _MainTex_TexelSize.y * 2) * _BlurSize; return o; } v2f vertBlurHorizontal(appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); half2 uv = v.texcoord; o.uv[0] = uv; o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1, 0) * _BlurSize; o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1, 0) * _BlurSize; o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2, 0) * _BlurSize; o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2, 0) * _BlurSize; return o; } fixed4 fragBlur (v2f i) : SV_TARGET { float weight[3] = {0.4026, 0.2442, 0.0545}; fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0]; for(int i1 = 1; i1 < 3; i1++) { sum += tex2D(_MainTex, i.uv[i1 * 2 - 1]).rgb * weight[i1]; sum += tex2D(_MainTex, i.uv[i1 * 2]).rgb * weight[i1]; } return fixed4(sum, 1); } ENDCG Cull Off ZWrite Off ZTest Always Pass { NAME "GAUSSIAN_BLUR_VERTICAL" CGPROGRAM #pragma vertex vertBlurVertical #pragma fragment fragBlur ENDCG } Pass { NAME "GAUSSIAN_BLUR_HORIZONTAL" CGPROGRAM #pragma vertex vertBlurHorizontal #pragma fragment fragBlur ENDCG }
泛光效果(Bloom) 原理:提取亮部,进行模糊,然后叠加在原图片
脚本:
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 private void OnRenderImage (RenderTexture src, RenderTexture dest ){ if (material) { material.SetFloat("_LuminanceThreshold" , luminanceThreshold); int rtW = src.width / downSample; int rtH = src.height / downSample; RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0 ); buffer0.filterMode = FilterMode.Bilinear; Graphics.Blit(src, buffer0, material, 0 ); for (int i = 0 ; i < iterations; i++) { material.SetFloat("_BlurSize" , 1 + i * blurSize); RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0 ); Graphics.Blit(buffer0, buffer1, material, 1 ); RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0 ); Graphics.Blit(buffer0, buffer1, material, 2 ); RenderTexture.ReleaseTemporary(buffer0); buffer0 = buffer1; } material.SetTexture("_Bloom" , buffer0); Graphics.Blit(src, dest, material, 3 ); RenderTexture.ReleaseTemporary(buffer0); } else { Graphics.Blit(src, dest); } }
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 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 CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; half4 _MainTex_TexelSize; sampler2D _Bloom; float _LuminanceThreshold; float _BlurSize; struct v2f { float4 pos : SV_POSITION; half2 uv : TEXCOORD0; }; v2f vertExtractBrightness(appdata_img v) { v2f o; o.pos = UnityViewToClipPos(v.vertex); o.uv = v.texcoord; return o; } fixed luminance (fixed4 color) { return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b; } fixed4 fragExtractBright (v2f i) : SV_Target { fixed4 color = tex2D(_MainTex, i.uv); fixed val = clamp(luminance(color) - _LuminanceThreshold, 0, 1); return color * val; } struct v2fBloom { float4 pos : SV_POSITION; half4 uv : TEXCOORD0; }; v2fBloom vertBloom (appdata_img v) { v2fBloom o; o.pos = UnityViewToClipPos(v.vertex); o.uv.xy = v.texcoord; o.uv.zw = v.texcoord; #if UNITY_UV_STARTS_AT_TOP if(_MainTex_TexelSize.y < 0) o.uv.w = 1 - o.uv.w; #endif return o; } fixed4 fragBloom (v2fBloom i) : SV_Target { return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw); } ENDCG Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vertExtractBrightness #pragma fragment fragExtractBright ENDCG } UsePass "Unity Shader Book/Chapter12/Gaussian Blur/GAUSSIAN_BLUR_VERTICAL" UsePass "Unity Shader Book/Chapter12/Gaussian Blur/GAUSSIAN_BLUR_HORIZONTAL" Pass { CGPROGRAM #pragma vertex vertBloom #pragma fragment fragBloom ENDCG }
运动模糊 实现方法:
累计缓存(accumulation buffer) :使用一块累计缓存混合多张连续图像,取平均值,性能消耗很大。
速度缓存(velocity buffer) :应用更加广泛,在速度缓存中储存各个像素的运动速度,然后利用这个值来确定模糊的方向和大小。
这次实现第一种方法。
脚本:
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 private void OnRenderImage (RenderTexture src, RenderTexture dest ){ if (material) { if (!accumulationTexture || accumulationTexture.width != src.width || accumulationTexture.height != src.height) { DestroyImmediate(accumulationTexture); accumulationTexture = new RenderTexture(src.width, src.height, 0 ); accumulationTexture.hideFlags = HideFlags.HideAndDontSave; Graphics.Blit(src, accumulationTexture); } accumulationTexture.MarkRestoreExpected(); material.SetFloat("_BlurAmount" , 1f - blurAmount); Graphics.Blit(src, accumulationTexture, material); Graphics.Blit(accumulationTexture, dest); } else { Graphics.Blit(src, dest); } }
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 45 46 47 48 49 50 51 CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; fixed _BlurAmount; struct v2f { float2 uv : TEXCOORD0; float4 pos : SV_POSITION; }; v2f vert(appdata_img v) { v2f o; o.pos = UnityViewToClipPos(v.vertex); o.uv = v.texcoord; return o; } fixed4 fragRGB (v2f i) : SV_Target { return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount); } half4 fragA (v2f i) : SV_Target { return tex2D(_MainTex, i.uv); } ENDCG Cull Off ZWrite Off ZTest Always Pass { Blend SrcAlpha OneMinusSrcAlpha ColorMask RGB CGPROGRAM #pragma vertex vert #pragma fragment fragRGB ENDCG } Pass { Blend One Zero ColorMask A CGPROGRAM #pragma vertex vert #pragma fragment fragA ENDCG }
不过这个效果一般,下面使用深度贴图还有一个[运动模糊 高级版](##运动模糊 高级版),使用速度映射图。
使用深度和法线纹理 获取深度和法线纹理 深度值是非线性的(由于透视投影导致)
深度纹理:要设置正确的RenderType标签
1 camera.depthTextureMode = DepthTextureMode.Depth;
然后在shader中声明**_CameraDepthTexture**变量即可访问。
深度法线纹理(法线存在RG通道,深度存在BA通道)
1 camera.depthTextureMode = DepthTextureMode.DepthNormals;
然后在Sader中声明**_CameraDepthNormalsTexture**变量即可访问。
组合这些模式(生成深度和深度法线纹理):
1 2 camera.depthTextureMode |= DepthTextureMode.Depth; camera.depthTextureMode |= DepthTextureMode.DepthNormals;
采样深度纹理:SAMPLE_DEPTH_TEXTURE宏
1 float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
将深度值转换为线性:
LinearEyeDepth :把深度纹理的采样结果转换到视角空间下的深度值
Linear01Depth :返回一个在[0, 1]的线性深度值。
对深度法线采样结果进行解码:DecodeDepthNormal (float4 enc, out float depth, out float3 normal)
运动模糊 高级版 储存两帧的深度图,通过深度图乘以逆矩阵获得每个像素世界位置的坐标,计算两帧位置差获得速度。可以在一个屏幕后处理步骤中完成整个效果的模拟,但是对性能有所影响。
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private void OnRenderImage (RenderTexture src, RenderTexture dest ){ if (material) { material.SetFloat("_BlurSize" , blurSize); material.SetMatrix("_PreviousViewProjectionMatrix" , m_previousViewProjectionMatrix); Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix; Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse; material.SetMatrix("_CurrentViewProjectionInverseMatrix" , currentViewProjectionInverseMatrix); m_previousViewProjectionMatrix = currentViewProjectionMatrix; Graphics.Blit(src, dest, material); } else { Graphics.Blit(src, dest); } }
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 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 CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; half4 _MainTex_TexelSize; sampler2D _CameraDepthTexture; float4x4 _CurrentViewProjectionInverseMatrix; float4x4 _PreviousViewProjectionMatrix; half _BlurSize; struct v2f { half2 uv : TEXCOORD0; half2 uvDepth : TEXCOORD1; float4 pos : SV_POSITION; }; v2f vert (appdata_img v) { v2f o; o.pos = UnityViewToClipPos(v.vertex); o.uv = v.texcoord; o.uvDepth = v.texcoord; #if UNITY_UV_STARTS_AT_TOP if(_MainTex_TexelSize.y < 0) o.uvDepth.y = 1 - o.uvDepth.y; #endif return o; } fixed4 frag(v2f i) : SV_Target { // 采样深度贴图 float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uvDepth); // 将屏幕位置从[0, 1]映射到[-1, 1] float4 h4 = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1); // 求出当前像素世界位置 float4 d4 = mul(_CurrentViewProjectionInverseMatrix, h4); float4 worldPos = d4 / d4.w; // 求出当前像素上一帧的屏幕位置 float4 currentPos = h4; float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos); previousPos /= previousPos.w; // 求出当前像素的速度 float2 velocity = (currentPos.xy - previousPos.xy) / 2; float2 uv = i.uv; float4 c = tex2D(_MainTex, uv); for(int it = 1; it < 5; it++) { uv += velocity * _BlurSize * it; float4 currentColor = tex2D(_MainTex, uv); c += currentColor; } c /= 5; return fixed4(c.rgb, 1); } ENDCG SubShader { Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag ENDCG }
这个只能用于摄像机运动(因为是使用的摄像机变换矩阵),对于物品运动没有效果。
全局雾效 Unity内置的雾效是基于距离的线性或者指数雾,每个 shader 都需要添加代码,这里讨论一种屏幕后处理全局雾效。
另一种高效的世界位置计算方法:
1 float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay
雾的系数 f:
1 float3 afterColor = f * fogColor + (1 - f) * origColor;
线性(Linear) 、指数(Exponential) 、指数平方(Exponential Squared)
这里使用比较简单的线性。
脚本
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 private void OnRenderImage (RenderTexture src, RenderTexture dest ){ if (material) { Matrix4x4 frustumCorners = Matrix4x4.identity; float fov = camera.fieldOfView; float near = camera.nearClipPlane; float far = camera.farClipPlane; float aspect = camera.aspect; float hlafHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad); Vector3 toRight = cameraTransform.right * hlafHeight * aspect; Vector3 toTop = cameraTransform.up * hlafHeight; Vector3 topLeft = cameraTransform.forward * near + toTop - toRight; float scale = topLeft.magnitude / near; topLeft.Normalize(); topLeft *= scale; Vector3 topRight = cameraTransform.forward * near + toRight + toTop; topRight.Normalize(); topRight *= scale; Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight; bottomLeft.Normalize(); bottomLeft *= scale; Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop; bottomRight.Normalize(); bottomRight *= scale; frustumCorners.SetRow(0 , bottomLeft); frustumCorners.SetRow(1 , bottomRight); frustumCorners.SetRow(2 , topRight); frustumCorners.SetRow(3 , topLeft); material.SetMatrix("_FrustumCornersRay" , frustumCorners); material.SetMatrix("_ViewProjectionInverseMatrix" , (camera.projectionMatrix * camera.worldToCameraMatrix).inverse); material.SetFloat("_FogDensity" , fogDensity); material.SetFloat("_FogStart" , fogStart); material.SetFloat("_FogEnd" , fogEnd); material.SetColor("_FogColor" , fogColor); Graphics.Blit(src, dest, material); } else { Graphics.Blit(src, dest); } }
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 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 CGINCLUDE #include "UnityCG.cginc" float4x4 _FrustumCornersRay; sampler2D _MainTex; half4 _MainTex_TexelSize; sampler2D _CameraDepthTexture; half _FogDensity; fixed4 _FogColor; float _FogStart; float _FogEnd; struct v2f { half2 uv : TEXCOORD0; float4 pos : SV_POSITION; half2 uvDepth : TEXCOORD1; float4 interpolatedRay : TEXCOORD2; }; v2f vert (appdata_img v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord; o.uvDepth = v.texcoord; #if UNITY_UV_STARTS_AT_TOP if (_MainTex_TexelSize.y < 0) o.uvDepth.y = 1 - o.uvDepth.y; #endif 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 = _FrustumCornersRay[index]; return o; } fixed4 frag (v2f i) : SV_Target { float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uvDepth)); float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz; 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 Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag ENDCG }
使用深度的边缘检测 使用Roberts算子
脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private void OnRenderImage (RenderTexture src, RenderTexture dest ){ if (material) { material.SetFloat("_EdgeOnly" ,edgesOnly); material.SetColor("_EdgeColor" , edgeColor); material.SetColor("_BackgroundColor" , backgroundColor); material.SetFloat("_SampleDistance" , sampleDistance); material.SetVector("_Sensitivity" , new Vector4(sensitivityNormals, sensitivityDepth, 0 , 0 )); Graphics.Blit(src, dest, material); } else { Graphics.Blit(src, dest); } }
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 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 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 = UnityViewToClipPos(v.vertex); half2 uv = v.texcoord; o.uv[0] = uv; #if UNITY_UV_STARTS_AT_TOP if(_MainTex_TexelSize.y < 0) 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; // 只会返回1或者0 return isSameNormal * isSameDepth ? 1 : 0; } fixed4 fragRobertsCrossDepthAndNormal(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; 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 SubShader { Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert #pragma fragment fragRobertsCrossDepthAndNormal ENDCG } }
非真实感渲染 卡通风格渲染 《Real Time Rendering, 3rd edition》中所介绍的渲染轮廓线的方法
基于观察角度和表面法线的轮廓线渲染。使用视角方向与法线点乘。这种方法简单快速,可以在一个pass内完成,但局限性大,效果一般。
过程式几何轮廓线渲染。使用两个pass,第一个渲染背面,并且使其轮廓可见,第二个pass再渲染正面。这种方法快速有效,但是不适合类似立方体这种较为平整的模型。
基于图像处理的轮廓线渲染。使用卷积算子进行边缘检测。优点为适用于任何模型,局限在于无法检测到法线和深度变化很小的轮廓,比如放在桌子上的纸。
基于轮廓边检测的轮廓线渲染。这种方法旨在于解决上述方法无法控制轮廓风格的问题,可以直接检测出轮廓边,并且直接渲染轮廓边。检测方法为检测与这条边相邻的两个三角形是否一个朝向正面,一个朝向背面。缺点在于由于是逐帧单独提取轮廓,所以会出现帧与帧之间发生跳变的情况。
最后一类混合了上述方法。
在这里直接使用过程式几何轮廓线渲染,将背面沿着法线扩张一段距离。
高光:为了防止高光边缘,可以使用smoothstep函数切换高光区域和非高光区域。
描边pass:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 v2f vert (a2v v) { v2f o; float4 pos = mul(UNITY_MATRIX_MV, v.vertex); float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal); normal.z = -0.5; pos = pos + float4(normalize(normal), 0) * _OutLine; o.pos = mul(UNITY_MATRIX_P, pos); return o; } fixed4 frag (v2f i) : SV_Target { return float4(_OutLineColor.rgb, 1); }
卡通着色Pass:
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 v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).rgb; TRANSFER_SHADOW(o); return o; } fixed4 frag(v2f i) : SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir); fixed4 c = tex2D(_MainTex, i.uv); fixed3 albedo = c.rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo; UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); fixed diff = dot(worldNormal, worldLightDir); diff = (diff * 0.5 + 0.5) * atten; fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp, float2(diff, diff)).rgb; fixed spec = dot(worldNormal, worldHalfDir); fixed w = fwidth(spec) * 2; fixed3 specular = _Specular.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1)) * step(0.0001, _SpecularScale); return fixed4(ambient + diffuse + specular, 1); }
素描风格渲染 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 v2f vert (appdata_base v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord.xy * _TileFactor; fixed3 worldLightDir = normalize(WorldSpaceLightDir(v.vertex)); fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); fixed diff = max(0, dot(worldLightDir, worldNormal)); o.hatchWeights0 = fixed3(0, 0, 0); o.hatchWeights1 = fixed3(0, 0, 0); float hatchFactor = diff * 7; if(hatchFactor > 6){} else if(hatchFactor > 5) { o.hatchWeights0.x = hatchFactor - 5; } else if(hatchFactor > 4) { o.hatchWeights0.x = hatchFactor - 4; o.hatchWeights0.y = 1 - o.hatchWeights0.x; } else if(hatchFactor > 3) { o.hatchWeights0.y = hatchFactor - 3; o.hatchWeights0.z = 1 - o.hatchWeights0.y; } else if(hatchFactor > 2) { o.hatchWeights0.z = hatchFactor - 2; o.hatchWeights1.x = 1 - o.hatchWeights0.z; } else if(hatchFactor > 1) { o.hatchWeights1.x = hatchFactor - 1; o.hatchWeights1.y = 1 - o.hatchWeights1.x; } else { o.hatchWeights1.y = hatchFactor; o.hatchWeights1.z = 1 - o.hatchWeights1.y; } o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; TRANSFER_SHADOW(o); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 hatchTex0 = tex2D(_Hatch0, i.uv) * i.hatchWeights0.x; fixed4 hatchTex1 = tex2D(_Hatch1, i.uv) * i.hatchWeights0.y; fixed4 hatchTex2 = tex2D(_Hatch2, i.uv) * i.hatchWeights0.z; fixed4 hatchTex3 = tex2D(_Hatch3, i.uv) * i.hatchWeights1.x; fixed4 hatchTex4 = tex2D(_Hatch4, i.uv) * i.hatchWeights1.y; fixed4 hatchTex5 = tex2D(_Hatch5, i.uv) * i.hatchWeights1.z; fixed4 whiteColor = fixed4(1, 1, 1, 1) * (1 - i.hatchWeights0.x - i.hatchWeights0.y - i.hatchWeights0.z - i.hatchWeights1.x - i.hatchWeights1.y - i.hatchWeights1.z); fixed4 hatchColor = hatchTex0 + hatchTex1 + hatchTex2 + hatchTex3 + hatchTex4 + hatchTex5 + whiteColor; UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); return fixed4(hatchColor.rgb * _Color.rgb * atten, 1); }
使用噪声 消融效果 通过一张噪点贴图实现消融
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 v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv_Main = TRANSFORM_TEX(v.texcoord, _MainTex); o.uv_Bump = TRANSFORM_TEX(v.texcoord, _BumpMap); o.uv_Burn = TRANSFORM_TEX(v.texcoord, _BumpMap); TANGENT_SPACE_ROTATION; o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz; o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; TRANSFER_SHADOW(o); return o; } fixed4 frag (v2f i) : SV_Target { fixed3 burn = tex2D(_BurnMap, i.uv_Burn).rgb; clip(burn.r - _BurnAmount); float3 tangentLightDir = normalize(i.lightDir); fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv_Bump)); fixed3 albedo = tex2D(_MainTex, i.uv_Main).rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir)); fixed t = 1 - smoothstep(0, _LineWidth, burn.r - _BurnAmount); fixed3 burnColor = lerp(_BurnFirstColor, _BurnSecondColor, t); burnColor = pow(burnColor, 5); UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); fixed3 finalColor = lerp(ambient + diffuse * atten, burnColor, t * step(0.0001, _BurnAmount)); return fixed4(finalColor, 1); }
在Shadow Caster Pass中也要记得clip,这样可以投正确的阴影。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 struct v2f { V2F_SHADOW_CASTER; float2 uvBurnMap : TEXCOORD1; }; fixed _BurnAmount; sampler2D _BurnMap; float4 _BurnMap_ST; v2f vert (appdata_base v) { v2f o; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o); o.uvBurnMap = TRANSFORM_TEX(v.texcoord, _BurnMap); return o; } fixed4 frag (v2f i) : SV_Target { fixed3 burn = tex2D(_BurnMap, i.uvBurnMap); clip(burn.r - _BurnAmount); SHADOW_CASTER_FRAGMENT(i); }
水波效果 给全局雾效添加噪声纹理 Unity中的渲染优化技术 移动平台的特点 影响性能的因素 CPU GPU
造成游戏性能瓶颈的原因
CPU
GPU
顶点处理(过多的顶点,过多的逐顶点计算)
片元处理(过多的片元,过多的逐片元计算)
带宽
使用了尺寸很大而且未经过压缩的纹理
分辨率过高的帧缓存。
CPU优化:
GPU优化:
减少顶点数目(优化几何体, 使用LOD, 使用遮挡剔除)
减少片元数目(控制绘制顺序,少用透明物体,减少实时光照。
降低计算复杂度(使用LOD, 代码优化)
Unity中的渲染分析工具
渲染统计窗口
信息名称
描述
每帧的时间和FPS
Batches
一帧中需要进行的批处理数目
Saved by batching
合并的批处理数目,标明了批处理为我们节省了多少draw call
Tris和Verts
需要绘制的三角形面片和顶点数目
Screen
屏幕大小以及其占用的内存大小
SetPass
渲染使用的Pass的数目,可能造成CPU瓶颈
Visible Skinned Meshes
渲染的蒙皮网格的数目
Animations
播放的动画数目
性能分析器的渲染区域
Window -> Profiler
帧调试器(Frame Debugger)
减少draw call数目 动态批处理:
静态批处理:勾选static
共享材质:参数也不能调
减少需要处理的顶点数目 优化几何体:减少顶点数目,减少不必要的硬边以及纹理衔接;
模型LOD技术:使用LOD Group组件;
遮挡剔除技术:
减少需要处理的片元数目 减少overdraw
控制绘制顺序:将更可能出现在前面的物体先画,更可能在最后的物体后画;
时刻小心透明物体:
减少实时光照和阴影:
节省带宽 减小纹理大小:长宽比最好是正方形,长宽值最好是2的整数幂;
利用分辨率缩放:
减少计算复杂度 shader的LOD技术:
代码方面的优化:尽量逐对象或逐顶点;使用低精度浮点数;尽量不使用全屏幕的后处理效果;
根据硬件条件进行更改:
Unity的表面着色器探秘 表面着色器(Surface Shader) 实际上就是在顶点片元着色器之上的一种抽象概念,使得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 SubShader { Tags { "RenderType" = "Opaque"} LOD 300 CGPROGRAM #pragma surface surf Lambert #pragma target 3.0 sampler2D _MainTex; sampler2D _NormalMap; fixed4 _Color; struct Input { float2 uv_MainTex; float2 uv_NormalMap; }; void surf (Input IN, inout SurfaceOutput o) { fixed4 tex = tex2D(_MainTex, IN.uv_MainTex); o.Albedo = tex.rgb * _Color.rgb; o.Alpha = tex.a * _Color.a; o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap)); } ENDCG }
编译指令 1 #pragma surface surfaceFunction lightMode [Optionalparams]
表面函数:
1 2 3 void surf (Input IN, inout SurfaceOutput o) void surf (Input IN, inout SurfaceOutputStandard o) void surf (Input IN, inout SurfaceOutputStandardSpecular o)
光照函数:
其他可选参数:
自定义修改函数:顶点修改函数(vertex:VertexFunction)和最后颜色修改函数(finalcolor:ColorFunction)
阴影:addshadow / fullforwardshadows / noshadow
透明度混合和透明度测试:alpha / alphatest:VariableName
光照:noambient / novertexlight / noforwardlight / nolightmap / nofog
控制代码的生成:exclude_path:deferred / forward / prepass
两个结构体
Input结构体
以uv或者uv2为前缀后面跟sampler名称,下划线隔开。
变量
描述
float3 viewDir
包含了视角方向,可用于计算边缘光照
使用Color语义定义float4变量
包含了插值后的逐顶点颜色
float4 screenPos
包含了屏幕空间坐标,可用于反射和屏幕特效
float3 worldPos
包含了世界空间下的位置
float3 worldRefl
包含了世界空间下的反射方向,前提是没有修改法线方向
float3 worldNormal
包含了世界空间的法线方向,前提是没有修改法线方向
如果更改了法线,则需要添加INTERNAL_DATA,然后使用WorldReflectionVector(IN, o.Normal) / WorldNormalVector(IN, o.Normal)来得到世界空间下的反射和法线方向。
SurfaceOutput / SurfaceOutputStandard / SurfaceOutputStandardSpecular结构体
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 struct SurfaceOutput { fixed3 Albedo; fixed3 Normal; fixed3 Emission; half Specular; fixed Gloss; fixed Alpha; }; struct SurfaceOutputStanard { fixed3 Albedo; fixed3 Normal; fixed3 Emission; half Metallic; half Snoothness; half Occlision; fixed Alpha; }; struct SurfaceOutputStandardSpecular { fixed3 Albedo; fixed3 Specular; fixed3 Normal; half3 Emission; half Smoothness; half Occlusion; fixed Alpha; };
Unity背后做了什么 将表面着色器编译成顶点和片元着色器
表面着色器实例分析 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 Tags { "RenderType"="Opaque" } LOD 300 CGPROGRAM #pragma surface surf CustomLambert vertex:myvert finalcolor:mycolor addshadow exclude_path:deferred exclude_path:prepass nometa #pragma target 3.0 fixed4 _ColorTint; sampler2D _MainTex; sampler2D _NormalMap; half _Amount; struct Input { float2 uv_MainTex; float2 uv_NormalMap; }; void myvert(inout appdata_full v) { v.vertex.xyz += v.normal * _Amount; } void surf (Input IN, inout SurfaceOutput o) { fixed4 tex = tex2D(_MainTex, IN.uv_MainTex); o.Albedo = tex.rgb; o.Alpha = tex.a; o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap)); } half4 LightingCustomLambert (SurfaceOutput s, half3 lightDir, half atten) { half NdotL = dot(s.Normal, lightDir); half4 c; c.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten); c.a = s.Alpha; return c; } void mycolor(Input IN, SurfaceOutput o, inout fixed4 color) { color *= _ColorTint; } ENDCG
addshadow 指令告诉unity生成一个对应顶点着色器的阴影投射pass
exclude_path:deferred 和 exclude_path:prepass 告诉unity不要为延迟渲染路径生成相应的pass
nometa 告诉unity取消生成提取元数据的pass
基于物理的渲染 PBS的理论和数学基础
Unity5的Standard Shader
金属材质
几乎没有漫反射,因为所有被吸收的光都会被自由电子立刻转化为其他形式的能量
有非常强烈的高光反射
高光反射通常是有颜色的
废金属材质
高光反射强度比较弱,但是菲涅尔现象比较强
高光反射颜色单一
漫反射的颜色多种多样
一个更加复杂的例子
设置光照环境
添加HDR的Skybox
设置间接光照反弹强度以及次数
放置反射探针
调整材质
线性颜色空间
Unity5更新了什么 场景更亮了 表面着色器更容易报错了 自己控制非统一缩放的网格 固定管线着色器逐渐退出舞台 还有更多内容吗