为控制渲染过程提供一层抽象,避免许多繁琐配置;用ShaderLab编写,使用一些嵌套在{}的语义;
定义了要显示材质所需的全部,不仅仅是着色器代码
Unity Shader != Shader(在同一个Unity Shader中定义顶点和片元着色器代码)
// 指定该shader名字
Shader "Unlit/Shader"
{
// 声明属性,方便以后调用
Properties
{
// Name:通常由下划线开始
// display name:在材质面板上的名字
// DefaultValue:Color和Vector形式为(x,y,z,w);2D/Cube/3D为 "DefaultValue"{}
Name("display name", PropertyType) = DefaultValue
}
// 给不同显卡用的子着色器,至少要有一个
SubShader
{
// 键值对 定义如何渲染该对象
Tags { "TagName1"="Value1" }
// 用来控制使用哪个SubShader,只有大于某个值时才会被使用(大的放上面)
LOD 100
// 可以在这里定义表面着色器(适合多光源)
// 本质上是对顶点/片元着色器的抽象,实际上会转化成一个含多Pass的顶点/片元着色器
...
#pragma surface surf Lambert
...
// 每个Pass定义了一次完整的渲染流程
Pass
{
// 可以通过UsePass命令复用(名字大写)
Name ""
// 顶点/片元着色器(更灵活)
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
#正交矩阵的转置=逆矩阵
#对方向矢量进行变换不用考虑有无平移(平移对方向矢量无影响);直接截取变换矩阵的前三行三列
#坐标空间的变换矩阵(A->B)
A空间在B空间中的原点+A在B中的X、Y、Z轴按列展开
#法线矩阵为原变换矩阵的逆转置矩阵(如果变换只包括旋转和统一缩放,则再逆缩放后可直接用原变换矩阵变换法线)
#Unity的API中的Matrix4x4为列优先;但CG中矩阵为行优先
#获得片元再屏幕上像素位置的两种方法
#顶点着色器到片元着色器经过一个插值过程(三角形三个顶点)
#投影空间非线性,不能在投影空间插值
// 编译指令
#pragma vertex vert
#pragma fragment frag
// 语义,定义输入输出值种类
// POSITION:模型空间的顶点坐标
// SV_POSITION: 裁剪空间的顶点坐标
float4 vert(float4 v: POSITION) : SV_POSITION
{
return mul(UNITY_MATRIX_MVP,v);
}
// SV_TARGET:渲染目标(默认帧缓存)
float4 farg(): SV_TARGET
{
return fixed4(1.0,1.0,1.0,1.0):
}
#Unity在每帧调用Draw Call时由Mesh Render组件将它负责渲染的模型数据发给Unity Shader
// 通过定义结构体并作为输入参数实现
// 应用和顶点着色器
struct a2v
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
// 顶点和片元
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
fixed3 color : COLOR0;
};
#尽管一般不会在片元着色器中用到,但a2v和v2f中的顶点信息(vertex)对frag来说是必须的
Properties
{
// 在材质面板添加着色器,控制颜色
_Color ("Color Tint", Color) = (1,1,1,1)
}
SubShader
{ ...
// 在CG代码段中,需定义一个与属性名称和类型都匹配 // 的变量
fixed4 _Color;
...
}
#uniform提供关于该变量初始化和存储的信息;与其他图形编程API的uniform不一样,可省略
实际上是赋给Shader输入和输出的字符串,表达该参数的含义
#一般不具有特殊含义,除了:
a2f中的TEXCOORD0,表示把模型的第一组纹理坐标存储在该变量中
系统数值语义(system-value semantics),以SV开头的;一般用来做输出,例如SV_POSITION、SV_TARGET
#一个语义用的寄存器只能处理4个float,如果想定义float4x4矩阵,可以拆成4个float4类型,这样省一点
屏幕空间:OpenGL的Y轴向上,原点在左下角;DirectX的Y轴向下,原点在左上角
#平时Unity会自动处理翻转,除了特殊情况(开启抗锯齿并使用渲染到纹理技术)
决定了Shader中各个特性(feature)的能力(capability),体现为能使用的运算指令数目、寄存器个数等
#慎用分支和循环语句,用clamp()代替
片元着色器 Phong shading 将顶点法线插值给像素,在像素上计算光照
计算量大 但效果较好
将 max(**n *** i, 0) 换成 α(n *** i) + β ,对结果进行一个α倍的缩放和β大小的偏移;通常α=β=0.5(α+β=1),将n *** i的结果从[-1,1]映射到[0,1]
改进:使模型背光面也有明暗变化
没有物理依据,仅仅是视觉加强技术
顶点着色器 Gouraud shading 在顶点计算光照,再插值输出成像素信息
计算量小 不适用非线性计算(Specular) 顶点颜色比片元内部亮(棱角)
经验模型 无法表现部分物理现象,如菲涅尔反射(斜着看车窗)
只能表现各向同性的材料(固定视角、光源,旋转视角得到结果一样,因为diffuse)
入射向量是由光源指向片元
先计算viewDIr和入射向量**(从顶点指向光源)**的半角向量(记得归一化),然后拿半角向量和法线点积
#需要在Pass中额外定义一个float4类型变量来定义纹理的属性;与属性名需和properties中声明一致类似,该属性名为_TexName_ST
Wrap Mode:超过纹理坐标的平铺方式
Filter Mode:纹理缩小下滤波模式(配合mipmapping)
存储强度值(intensity);颜色越浅越高
计算复杂,实时计算是要由灰度值计算的表面法线
存储表面法线方向;
采样映射:将法线[-1,1]映射到像素[0,1]
对法线纹理采样后还需对结果反映射,以得到原来法线方向
每个原点独有;原点是顶点本身,Z轴是法线方向,X轴是切线方向(t),Y轴是副切线由XZ叉积得(b)(右手系)
#在shader中,tangent用float4存储,需要额外一个tangent.w变量决定副切线的方向性
实质上存储着各自的法线扰动方向
优缺点(与模型空间法线映射相比):
缺:模型空间更平滑,同一坐标系下插值更连续
优:可复用;可进行UV动画实现凹凸移动;可压缩,只存储XY方向(Z方向恒为正);一般用切线空间法线映射
切线空间计算光照
将lightDir和viewDir变换到切线空间(在vert中完成 效率高);再传给frag计算光照
#可以用UnpackNormal()反映射;用TANGENT_SPACE_ROTATION构造模型到切线的变换矩阵
世界空间计算光照(通用性高)
将tangent变换到世界空间(在frag完成矩阵运算 效率低);在frag将Normal变换回世界空间
纹理类型选Normal Map之后可以用内置的UnpackNormal()来采样法线方向,以便针对不同平台选择不同压缩方式
例如DXT5nm压缩格式,只需存储法线的xy分量,z分量可以通过xy推导得出(法线为单位向量切切线空间中法线z分量恒为正)
Create from Grayscale:从高度图(相对高度中)生成法线纹理
控制漫反射光照结果,使物体轮廓线更明显,提供多种色调变化(卡通风格渲染)
计算diffuse时用渐变纹理的采样结果代替漫反射光照计算
#渐变纹理实际上时一维纹理,采样时可精简
#将Wrap Mode设为Clamp模式以避免浮点数精度造成的问题(1.0001截断整数后0.0001为黑色)
对物体的某些区域进行修改,可用于控制不同区域光照强弱、控制混合多张纹理等
采样得到mask texture的纹素值,然后用其中某个通道的值和某个表面属性相乘;实际开发中会充分利用RGBA四个通道以存储不同属性,如高光反射强度、边缘光照强度、自发光强度等
先渲染不透明物体,再从后往前渲染透明物体
可以用分割模型和网格来解决部分错误排序的问题
渲染队列(render queue)
整数索引号表示,索引号越小越先渲染;Unity提前定义好了5个渲染队列
// 透明度测试
Tags{"Queue" = "AlphaTest"
"RenderType"="Transparent" "IgnoreProjector"="True"}
// 透明度混合
Tags{"Queue" = "Transparent"
"RenderType"="Transparent" "IgnoreProjector"="True"}
...
ZWrite Off // 关闭深度写入
Blend SrcFactor DstFactor // 设置混合因子
...
透明度测试:只要一个片元不满足条件就丢弃
函数:void clip() 当参数的任何一个分量是负数时丢弃该像素颜色
例:clip(texColor.a - _Cutoff);
透明度混合:以透明度为系数,将当前片元颜色和存储在颜色缓冲中的颜色值进行混合;关闭了深度写入,但没关闭深度测试,不透明物体能正常遮挡透明物体
Blend命令语义
BlendOp BlendOperation:做一些其他的操作
在混合等式中用其他操作(减乘除最大最小)来代替默认的将混合后的原颜色和目标颜色相加
Blend SrcFactor DstFactor:开启混合并设置混合因子
D s t C o l o r n e w = S r c F a c t o r ∗ S r c C o l o r + D s t F a c t o r ∗ D s t C o l o r o l d DstColor_{new}=SrcFactor*SrcColor+DstFactor*DstColor_{old} DstColornew=SrcFactor∗SrcColor+DstFactor∗DstColorold
一般将SrcAlpha赋给***SrcFactor***,将OneMinusSrcAlpha(1-SrcAlpha)赋给***DstFactor***以得到适合的透明效果
Blend SrcFactor DstFactor SrcFactorA DstFactorA:与上面差不多,用后两个因子混合透明通道
为了解决模型内部的错误排序
用两个Pass渲染模型:第一个Pass只深度写入,不输出颜色(注意渲染顺序,不透明物体已经渲染,可以理解为该深度写入只针对不透明物体内部);第二个Pass进行透明度混合
函数:ColorMask RGB 设置颜色通道的写掩码,RGB即为写入RGB,0为不写入任何颜色通道
上述的方法看不到半透明物体的背部(在深度测试中被剔除)
函数:Cull Back|Front|Off 默认是Cull Back,即背面的片元不会被渲染
AlphaBlend中由于关闭了深度写入,无法保证片元从后往前渲染,因此要用两个Pass,第一个Pass只渲染背面,第二个Pass只渲染正面,保证正确的深度渲染关系(就把AlphaBlend的代码复制过去,然后在第一个Pass里写上Cull Front就行)
决定光照如何应用到Unity Shader中,如果有多个光源,则需要为每个Pass指定它使用的渲染路径
在Pass中用LightMode标签指定该Pass使用的渲染路径
原理:对物体上每个图元覆盖下的每个片元,先计算深度缓冲是否可见,若可见则更新颜色缓冲,再更新帧缓冲
物体在多个逐像素光源影响下需要执行多个Pass(N*M)
unity中三种处理光照方式:逐顶点、逐像素、球谐函数(SH)
由光源类型和渲染模式(在光源处设置是否Important)共同决定
Tags{"LightMode"=“ForwardBase”}
#pragma multi_compile_fwdbase
可计算一个逐像素的平行光以及所有逐顶点和SH光源;可实现环境光、自发光、阴影、光照纹理
可定义多个Base Pass(如双面渲染)但每个Base Pass只会执行一次
Tags{"LightMode"=“ForwardAdd”}
Blend One One
#pragma multi_compile_fwdadd
可计算其他影响该物体的逐像素光源,每个光源执行一次Pass
如果不开启混合,则会覆盖掉Base Pass的结果
实际计算中可以复制Base Pass的代码,只需去掉环境光、自发光、逐顶点和SH光;再加入对不同光源类型的支持,要用
#ifdef USING_DIRECTIONAL_LIGHT来判断当前处理光源的类型(是否为平行光);对点光源和聚光灯计算衰减时,Unity用光照纹理作为查找表(Lookup Table),用光源空间下坐标进行采样以得到衰减值,代替开根号等复杂光照计算
配置要求少、性能高、效果差
只用逐顶点方式计算光照,实际上是前向渲染的一个子集
使用额外缓冲区(G缓冲),存储表面的其他信息,如法线、位置、用于光照计算的材质属性
目的是解决前向渲染处理大量实时光源时性能下降的问题
原理:两个Pass。Pass1不进行光照计算,用深度测试判断片元是否可见,若可见则将表面信息存入G缓冲中;Pass2计算光照并更新帧缓冲
延迟缓冲的Pass通常为两个,与光源数目无关;换言之延迟渲染的效率不依赖场景复杂度,仅与接受光照的像素数有关
缺点:不支持抗锯齿;不能处理半透明物体;对显卡有要求
Unity内部使用光照纹理计算光照衰减,通常只关心_LightTexture0对角线上纹理颜色值
...
// 不然用unity_WorldToLight会报错
#include "AutoLight.cginc"
...
// 得到光源空间中位置
float3 lightCoord = mul(unity_WorldToLight,float4(i.worldPosition,1)).xyz;
// 用坐标的模的平方采样(避免开方),使用宏计算衰减纹理中
// 衰减值所在分量
fixed atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANEL;
阴影映射纹理:将摄像机放在光源处,看不到的地方就是阴影;本质上是深度图,记录了从光源触发能看到的场景中间距离它最近的深度信息
在shader中对shadow map采样,将采样结果乘到最终光照结果
用LightMode标签被设置为ShadowCaster的Pass将物体渲染到ShadowMap(只需深度信息,Base Pass和Additional Pass会浪费):变换位置,得到顶点在光源空间中位置信息
传统采样:使用xy分量对shadow map采样,得到该该位置深度信息;将该深度值和顶点深度值作比对,若小于,则处在阴影中
Screenspace Shadow Map:将摄像机深度纹理和shadow map中深度值比较,若大于,则处在阴影中
投射阴影:在Mesh Render组件中打开Cast Shadows;使用Fallback了VertexLit的shader(Diffuse和Specular都调用了VertexLit)
接受阴影:
struct v2f
{
...
// 声明用于对阴影纹理采样的坐标
// 参数为下一个可用的插值寄存器索引值
SHADOW_COORDS(2)
...
}
v2f vert(a2v v)
{
...
// 计算声明的阴影纹理坐标
TRANSFER_SHADOW(o);
...
}
fixed4 frag(v2f i)
{
...
// 用纹理坐标采样Shadow Map
fixed shadow = SHADOW_ATTENUATION(i);
...
}
#这些宏会用上下文变量计算,要保证v.vertex和a.pos正确定义
一般用的VertexLit的Pass中并没有进行透明度计算,需要用其他方法
// 用宏统一管理光照衰减和阴影
// 第一个参数存储计算结果 第二个参数是v2f,对ShadowMap采样
// 第三个参数用来计算光源空间坐标
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
fixed color = ... * atten;
好处:统一代码,不需要在Base Pass中处理阴影,不需要在Additional Pass中判断光源类型以处理光照衰减
包含六张图像,对应立方体六个面;需要用三位纹理坐标(从立方体中心出发的方向矢量,和立方体面交点即为采样结果)进行采样
缺点:引入新光源时需重新生成;不能模拟多次反射结果
Unity中的Skybox是在所有不透明物体之后渲染的
#场景的Skybox默认作用于所有摄像机,可以通过给摄像机添加Skybox组件覆盖场景默认Skybox
用来模拟金属材质
在场景指定位置生成立方体纹理:生成临时camera,使用Camera.RenderToCubemap()渲染到指定Cubemap中
反射
需要用reflect()函数计算反射方向,用它对采样Cubemap影响diffuse
折射
斯涅尔定律:η1 * sinθ1=η2 * sinθ2(η为介质折射率,θ为入射/折射方向与法线夹角)
函数:refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio) 第一个参数为入射方向;第三个参数为 入射介质/折射介质
菲涅尔反射
物体表面上被反射的光和进入物体内部被折射的光的比率关系随视角变化
Schlick菲涅尔近似等式
F S c h l i c k ( v , n ) = F 0 + ( 1 − F 0 ) ( 1 − v ∗ n ) 5 F_{Schlick}(v,n)=F_{0}+(1-F_{0})(1-v*n)^5 FSchlick(v,n)=F0+(1−F0)(1−v∗n)5
用计算出来的菲涅尔系数在diffuse和reflection中插值 lerp()
渲染目标纹理(Render Target Texture,RTT)
将场景渲染到帧缓冲和后备缓冲之外的中间缓冲
多重渲染目标(Multiple Render Target,MRT)
允许将场景同时渲染到多个渲染目标纹理中
1、将渲染纹理设置为摄像机的渲染目标,实时更新
2、用GrabPass命令或是OnRenderImage函数获取当前屏幕图像
利用方法一,将摄像机对准目标方向,水平反转渲染纹理显示在物体上
利用方法二,获取玻璃后面的屏幕图像,利用法线纹理偏移屏幕纹理坐标,再对屏幕图像采样近似模拟折射效果
Tags{ "Queue"="Transparent" "RenderType"="Opaque"}
// 可以不声明字符串,直接使用_GrabTexture,但每使用一次就专区一次屏幕,开销大
GrabPass{"_RefractionTex"}
v2f vert (av2 v)
{
...
// 获取被抓取屏幕图像的采样坐标
o.scrPos = ComputeGrabScreenPos(o.pos);
...
}
RenderType设置成Opaque是为了在使用着色器替换时该物体能正常渲染
#需要对scrPos进行透视除法得到真正的屏幕坐标
由计算机生成的图像,用来生成木头、石子纹理
用cs脚本编写;使用插件SetProperty保证在面板上修改属性后能更新程序纹理
函数:
Texture2D.SetPixel() 设置像素值
Texture2D.Apply() 将像素值写入纹理
Texture2D.SetTexture() 将纹理传给shader
代替粒子系统模拟动画效果
需要在每个时刻计算该时刻下应该播放的关键帧位置并采样