Unity有两种方式编写Shader,顶点-片段着色器与表面着色器 在顶点片段着色器中,可以使用CG HLSL GLSL等语言编写,在新建的shader中,可以看到被包裹的CG代码片段,因此可以说SHaderLab只是起到一个组织结构的作用,真正生效的是其中包裹的CG语言 因为CG是由NVIDIA开发的跨平台语言,而GLSL基于OpenGl(跨平台),HLSL基于Directx(微软,只支持windows) 除此之外,还有Vulkan(图形API) unity暂时不涉及就不多介绍, 查看unity手册可以知道,Unity官方主要是用Cg/HLSL编写Shader程序片段,Unity官方手册也说明对于Cg/HLSL程序进行扩展也可以使用GLSL,不过Unity官方建议使用原生的GLSL进行编写和测试。如果不使用原生GLSL,你就需要知道你的平台必须是Mac OS X、OpenGL ES 2.0以上的移动设备或者是Linux。在一般情况下Unity会把Cg/HLSL交叉编译成优化过的GLSL。因此我们有多种选择,我们既可以考虑使用Cg/HLSL,也可以使用GLSL。不过由于Cg/HLSL更好的跨平台性,更倾向于使用Cg/HLSL编写Shader程序,因此大多数情况下是使用CG即可
目录
CG
编译
1.编译 使用方法 第一步设置编译指令
Pass{
Tag{//}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.0
//CG代码
ENDCG
}
编译指令第一条设置着色器,顶点/片段 第二条设置 编译目标,越高,则拥有越多特性,具体见unity手册 也可以直接声明特定功能 具体有多少功能,需要那些功能,属于经验问题
渲染平台
unity会对所有平台都编译一份Shader程序,很占空间和内存,所以可以通过编译指令指定编译或者不变异哪些平台(有手机端和DX基本够用了) 提问shader变体是啥? #pragma only_renderers PlatformName #pragma exclude_renderers PlatformName
着色器函数
有返回值函数 typr name(in 参数) { return 返回值; } 无返回值函数(通过out输出) void name(in 参数,out 参数) { } 在着色器语言中支持的类型
语义
4.语义 表面上看shader的语法和普通的编程语言没有区别,函数亦是和C#类似,但是语义是着色器语言区别于其他编程语言的最大区别 着色器语言定义了函数,但是哪里调用呢?答案是没有,我们处理的只是渲染流水线所暴露出来的一部分,语义就是数据流入流出的接口。语义表示需要传递的数据信息 每一个参数都有对应语义,传进来的参数是我们需要处理的数据,传出/返回的参数则是需要传走交给渲染流水线的下一个环节的参数 有返回值函数 typr name(in 参数:语义):语义 { return 返回值; } 无返回值函数(通过out输出) void name(in 参数:语义,out 参数:语义) { } 常见语义汇总
顶点着色器最重要的一个任务就是输出顶点在 剪裁空间中的坐标 空间变换总结: 模型空间(模型的顶点数据的坐标) 世界空间(将模型的顶点数据的坐标变换到虚拟的世界空间中,自定义坐标) 摄像机空间(在虚拟世界中假设一个摄像机) 剪裁空间(摄像机能够看到的全部内容) 屏幕空间(将能够看到的全部内容,投影到2D平面) 具体的空间变换参考图形学的MVP变换 顶点着色器输出之后,片段着色器输入,即顶点着色器的输出可以在片段着色器的输入使用 顶点着色器输出和片段着色器输入常用的语义
其中SV_POSITION 默认输入到片段着色器中 更多语义 基于经验 与查看unity 与HLSL 内容,CG是基于HLSL的语言
参数
在Shaderlab 声明的参数,需要在CG中再次声明
与别的数据不同的是,2D 贴图类型,需要额外声明一个float4 存储 平铺(缩放)与偏移 folat4 {TextureName} _ST; XYZW 分别对应Tiling XY OffsetXY
贴图使用方法
先在顶点坐标计算 位置,通过TEXCOORD0 语义传第一套UV(模型UV在3D软件展开),然后计算贴图与传过来的uv坐标 最后将计算好的坐标(贴图uv与模型uv计算完毕的坐标),传出 out 接下来在片段着色器中接收顶点坐标传过来的UV坐标,采样着色(text2D函数) 代码参考
Shader "Custom/NewSurfaceShader"
{
Properties
{
_MainColor("MainColor",color)=(1,1,1,1)
_MyTexture("Texture",2D)="white"{}
}
SubShader
{
pass
{
CGPROGRAM
#pragma vertex vert;
#pragma fragment frag;
fixed4 _MainColor;
sampler2D _MyTexture;
float4 _MyTexture_ST;
void vert(in float4 vert:POSITION,in float2 uv:TEXCOORD0,out float4 positon:SV_POSITION,out float2 texcoord:TEXCOORD0)
{
positon=UnityObjectToClipPos(vert);
texcoord=uv*_MyTexture_ST.xy+uv*_MyTexture_ST.zw;
}
void frag(out fixed4 color:SV_TARGET0,in float4 positon:SV_POSITION,in float2 texcoord:TEXCOORD0)
{
color=tex2D(_MyTexture,texcoord)*_MainColor;
}
ENDCG
}
}
}
结构体
当一次太多的参数需要传入传出,函数会变得很长,而且不易于阅读,所以可以将参数分类,组合成一个结构体 struct Type { float3 positon:SV_POSITION; type name:语义 变量类型 变量名:语义 } 以结构体传出传出,只需要 in out 即可 void vert(in struct1 ins,out struct2 ots) { }
unity的包含文件
相当于C++的库文件,里面有常用的辅助函数 语法(include很有C++味): #include "UnityCG.cginc" 常见结构体
顶点坐标变换
向量变换
灯光
灯光(Forward 前项渲染路径类型)
提问:什么是前项渲染路径类型 渲染类型与光照相关,与物体是否透明相关 Unity中光照:逐顶点、逐像素、球谐光照。 逐像素光照:按照每个像素的颜色被计算,看起来比较平滑; 逐顶点光照:每个顶点上做光照,其他地方插值,效果粗糙。 但是逐像素光照中,每个光源会使每个光源光照范围内的物体增加一个渲染批次。延迟渲染可以解决逐像素光照对光源造成的性能影响。 球谐光照:球谐光照实际上就是将周围的环境光采样成几个系数,然后渲染的时候用这几个系数来对光照进行还原,这种过程可以看做是对周围环境光的简化 延迟渲染:先不进行光照运算,对每个像素生成一组数据(G-buffer),包括位置,法线,高光等,然后用这些数据将每个光源以2D后处理的方式施加在最后图像上(屏幕空间)。复杂度:屏幕的分辨率*光源个数。 前向渲染:先渲染一遍物体,把法线和高光存在ARGB32的渲染纹理中(法线用rgb通道,高光用a通道),存在了z buffer里;然后通过深度信息,法线和高光信息计算光照(屏幕空间),光照信息缓存在Render Texture中;最后混合。*(每个几何对象都一步步的从顶点着色器到像素着色器最终送到屏幕渲染处理)。如果是逐像素的,复杂度:片段的个数*光照的个数。 综上:大量灯光时选延迟渲染。但是缺点:不允许渲染半透明物体(原因:无法记录半透明物体的深度和法线信息);缺乏抗锯齿;硬件不支持;性能差。
其余函数和宏,包含文件
还有更多文件和函数请参考unity的手册,能否正确合理的使用这些函数和宏,这是个经验问题
光照模型
Lambert光照模型
C_{diffuse}=(C_{light}·M_{diffuse})saturate(n·l) 灯光函数
参考代码:
Shader "Custom/NewSurfaceShader"
{
Properties
{
_MainColor("MainColor",color)=(1,1,1,1)
}
SubShader
{
pass
{
CGPROGRAM
#pragma vertex vert2;
#pragma fragment frag;
fixed4 _MainColor;
#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"
struct v2f
{
float4 pos:SV_POSITION;
fixed4 dif:COLOR;
};
v2f vert2(appdata_base v)//appdata_base是头文件声明过的结构体
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
//法线向量
float3 n=UnityObjectToWorldNormal(v.normal);
n=normalize(n);
//灯光方向向量
fixed3 lignt=normalize(_WorldSpaceLightPos0.xyz);
//漫反射公式
fixed ndotLight=dot(n,lignt);
o.dif=_LightColor0*_MainColor*saturate(ndotLight);//saturate截取,避免取负
return o;
}
fixed4 frag(v2f i):SV_TARGET
{
return i.dif;
}
ENDCG
}
}
}
漫反射模型,利用灯光与模型法线的夹角得到一个数值,从而能够知道模型那些地方亮,哪些地方暗(被灯光直射的部分亮,与灯光越平行,越暗淡) 漫反射模型 可以看做给与了3D物体形状,在2D美术中,也是通过明暗关系表现物体的形态,在Lambert模型之前,物体所有地方都是一个颜色,看不出3D的感觉
Half_Lambert光照模型
在Lambert模型中,因为物体背面无法被照亮(被截取到0-1之间),所以提出不直接截取,而是先*0.5将区间缩小到【-0.5,0.5】 为什么是-0.5到0.5呢,因为向量点乘的数值在【-1,1】 为什么点乘数值是-1,1呢?因为A·B=|A|·|B|·cos<A,B> 为什么|A|·|B|·cos<A,B>就是-1,1呢? 因为lambert模型中顶点法向量(模为1),而灯光也是方向向量(模为1),所以只需要计算两者夹角,而cos夹角的范围是-1,1之间 为什么xx 法向量是数学定义,垂直于一个平面的单位向量,单位向量,方向向量就是模为1的向量,
C_{diffuse}=(C_{light}·M_{diffuse})[0.5(n·l)+0.5]
参考代码:修改漫反射公式
o.dif=_LightColor0*_MainColor*(0.5*ndotLight+0.5);//
Phong光照模型
Lambert漫反射模型,可以模拟粗糙物体,但是如果是光滑的物体,具有光的反射,折射效果,就不能很好地模拟了,所以出现了Phong,在物体漫反射基础上增加镜面反射 SurfaceColor=C_{Ambient}+C_{Diffuse}+C_{Specular} C_{Ambient}为环境光;C_{Diffuse}为漫反射;C_{Specular}为镜面反射。 镜面反射公式
C_{light}为灯光亮度;M_{specular}为物体材质的镜面反射颜色;v为视角方向(由顶点指向摄像机);r为光线的反射方向;M_{shininess}为物体材质的光泽度(指数)。
其中v,r,可以看做是人眼距离反射光线的接近程度(一面镜子,一个手电,越接近反射角度看过去,越刺眼,因为大部分光都是反射角度射出)
实现phong模型,环境光部分调整Lighting 设置中的Environment-> Environment Light ->Source为Gradient ,因为使用Gradient 环境光(能否使用skybox?)
代码参考:
Shader "Custom/NewSurfaceShader"
{
Properties
{
_MainColor("MainColor",Color)=(1,1,1,1)
_SpecularColor("SpecularColor",Color)=(0,0,0,0)
_Shininess("Shininess",Range(1,100))=1
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert;
#pragma fragment frag;
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f
{
float4 pos:SV_POSITION;
fixed4 color:COLOR0;
};
fixed4 _MainColor;
fixed4 _SpecularColor;
half _Shininess;
v2f vert(appdata_base v)//appdata_base是头文件声明过的结构体
{
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
//顶点法线向量
float3 n=UnityObjectToWorldNormal(v.normal);//计算顶点的世界空间法向量,因为要在世界空间计算镜面反射
n=normalize(n);//归一化,只需要向量方向,wo:感觉不用不到,法向量本身就是单位向量了,实测
//灯光方向向量 顶点->灯光,所有Wordspace取到的都是顶点为起点的向量(如下面的顶点->视角方向)
fixed3 light=normalize(_WorldSpaceLightPos0.xyz);
//漫反射公式
fixed ndotLight=saturate(dot(n,light));//saturate截取,避免取负
fixed4 dif=_LightColor0*_MainColor*ndotLight;
//计算镜面反射视角方向(顶点->人眼,摄像机方向向量)
fixed3 view=normalize(WorldSpaceViewDir(v.vertex));
//计算光线反射向量
float3 ref=reflect(-light,n);//参数1:入射方向,参数2:法向量,函数结果:根据入射向量和法向量,计算反射向量(反射向量到法线量与入射向量到法向量的夹角相等)。
//取-是因为入射是 灯光->顶点,所以取反
ref=normalize(ref);
// //镜面反射公式
fixed rdotv=saturate(dot(ref,view));
fixed4 spec=_LightColor0*_SpecularColor*pow(rdotv,_Shininess);
//Phong公式
o.color=unity_AmbientSky+dif+spec;
//o.color=unity_AmbientSky;
//o.color=dif;
//o.color=spec;
return o;
}
fixed4 frag(v2f i):SV_TARGET
{
return i.color;
}
ENDCG
}
}
}
效果:设置高光颜色为白色,调节Shininess 可以看到很明显的高光
Blinn-Phong光照模型
在Phong的基础上改进镜面反射的算法
Blinn-Phong光照模型不再使用反射向量r计算镜面反射,而是使用半角向量h代替r,h为表视角方向v和灯光方向l的角平分线方向。
h=normalize(v+l)//入射和视角方向的半角求法,向量相加,平行四边形法则,然后归一化即是半角向量
代码参考:将phong中的镜面反射公式替换即可
//镜面反射公式 Blinn-phong
fixed3 h=normalize(light+view);//求半角向量
fixed ndoth=saturate(dot(n,h));
fixed4 spec=_LightColor0*_SpecularColor*pow(ndoth,_Shininess);
可以看到Blinn-phong 更加细腻 从性能上来说Blinn-phong 更好,Phong多计算一步,根据顶点向量计算光线反射向量 当观察者和灯光离被照射物体非常远时,Blinn-Phong的计算效率要高于Phong。因为h是取决于视角方向以及灯光方向的,两者都很远时h可以被认为是常量,跟位置以及表面的曲率没关系,因此可以大大减少计算量。而Phong却要根据表面曲率去逐顶点或逐像素计算反射向量r,相对而言计算量比较大。 总结:光照模型,就是光线(带颜色的向量)如何在数学上模拟与物体进行的各种照射效果(利用向量来计算)