本系列文章由@浅墨_毛星云 出品,转载请注明出处。
文章链接: http://blog.csdn.net/poem_qianmo/article/details/50812177
作者:毛星云(浅墨) 微博:http://weibo.com/u/1723155442
本文工程使用的Unity3D版本: 5.2.1
好久不见。
从这篇文章开始, Shader系列博文将继续开始更新。且在这次重启,这个系列文章会更多专注于实际Shader的书写,力求推出更多具有特色和实用性的Shader,概念性的东西在原则上上是不会再多讲的。
作为可编程Shader系列的第一篇文章,本文将从最简化的可编程Shader开始,逐步变换与实现一个漫反射(也就是实现Lambert光照模型)顶点&片段Shader,可以说,是在讲述一个可编程Shader的进化史。
依然是先放出游戏场景的exe和运行截图。
【可运行的本文配套exe游戏场景请点击这里下载】
一、可编程Shader的书写初步
我们知道,在Unity中,Shader可以分成如下三种基本类型:
1.固定功能着色器(FixedFunction Shader)
2.表面着色器(SurfaceShader)
3.顶点着色器&片段着色器(Vertex Shader & Fragment Shader)
我们也知道,可编程Shader是Unity Shader之中功能最强大、最自由的形态。就让我们开始一个最简单的单色可编程Shader的书写。
1.单色Shader的书写
单色Shader算是比较精简的可编程Shader。直接上注释好的代码:
//单色顶点&片段着色器 Shader "浅墨Shader编程/Volume12/1.SimpleShader" { //------------------------------------【唯一的子着色器】------------------------------------ SubShader { //--------------------------------唯一的通道------------------------------- Pass { //===========开启CG着色器语言编写模块============ CGPROGRAM //编译指令:告知编译器顶点和片段着色函数的名称 #pragma vertex vert #pragma fragment frag //--------------------------------【顶点着色函数】----------------------------- // 输入:POSITION语义(坐标位置) // 输出:SV_POSITION语义(像素位置) //--------------------------------------------------------------------------------- float4 vert(float4 vertexPos : POSITION) : SV_POSITION { //坐标系变换 //输出的顶点位置(像素位置)为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口 return mul(UNITY_MATRIX_MVP,vertexPos); } //--------------------------------【片段着色函数】----------------------------- // 输入:无 // 输出:COLOR语义(颜色值) //--------------------------------------------------------------------------------- float4 frag(void): COLOR { //返回单色 return float4(0.0, 0.6, 0.8, 1.0); } //===========结束CG着色器语言编写模块=========== ENDCG } } }
这个Shader也就是先指定一下顶点和片段着色器的名称,然后在顶点着色器中进行一下坐标空间的转换,在片段着色器中直接返回一个固定的蓝色,仅此而已。
将其施用于材质之上的效果如下:
2.单色可调Shader的书写
仅仅Hard encoding硬编码颜色怎么可以,我们想要可调节,可玩的Shader。这不,来一个Properties属性块,里面定义一个Color属性,然后在片段着色器里替换一下float4型的固定颜色不就好了。所以,代码如下:
//单色可调顶点&片段着色器 Shader "浅墨Shader编程/Volume12/2.ColorChange" { //------------------------------------【属性值】------------------------------------ Properties { //主纹理 _Color("Color", Color) = (1,1,1,1) } //------------------------------------【唯一的子着色器】------------------------------------ SubShader { //--------------------------------唯一的通道------------------------------- Pass { //===========开启CG着色器语言编写模块============ CGPROGRAM //指定顶点与片段着色器名称 #pragma vertex vert #pragma fragment frag //--------------------------------【顶点着色函数】----------------------------- // 输入:POSITION语义(坐标位置) // 输出:SV_POSITION语义(像素位置) //--------------------------------------------------------------------------------- float4 vert(float4 vertexPos : POSITION) : SV_POSITION { //坐标系变换 //输出的顶点位置(像素位置)为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口 return mul(UNITY_MATRIX_MVP, vertexPos); } uniform float4 _Color; //--------------------------------【片段着色函数】----------------------------- // 输入:无 // 输出:COLOR语义(颜色值) //--------------------------------------------------------------------------------- float4 frag(void) : COLOR { //返回单色 return _Color; } //===========结束CG着色器语言编写模块=========== ENDCG } } }将其施用于材质之上的效果如下:
于是,我们便可以用这边的调色板对此Shader进行颜色的调节。
接下来,看点更有意思的Shader。
3.RGB Cube Shader
这个是Unity旧版官方文档中出现的一个Shader示例,先上原版RGB Cube的Shader代码,稍后对其进行改造升级:
//RGB立方体 Shader "浅墨Shader编程/Volume12/3.RGB cube" { //------------------------------------【唯一的子着色器】------------------------------------ SubShader { //--------------------------------唯一的通道------------------------------- Pass { //===========开启CG着色器语言编写模块============ CGPROGRAM //编译指令:告知编译器顶点和片段着色函数的名称 #pragma vertex vert #pragma fragment frag //顶点着色器输出结构 struct vertexOutput { float4 positon : SV_POSITION;//空间位置 float4 color : TEXCOORD0;//0级纹理坐标 }; //--------------------------------【顶点着色函数】----------------------------- // 输入:POSITION语义 // 输出:顶点输出结构体 //--------------------------------------------------------------------------------- vertexOutput vert(float4 vertexPos : POSITION) { //实例化一个vertexOutput输出结构 vertexOutput output; //坐标系变换:将三维空间中的坐标投影到二维窗口 output.positon = mul(UNITY_MATRIX_MVP, vertexPos); //输出颜色为顶点位置加上一个颜色偏移量 output.color = vertexPos + float4(0.2, 0.2, 0.2, 0.0); //返回最终的值 return output; } //--------------------------------【片段着色函数】----------------------------- // 输入:vertexOutput结构体 // 输出:COLOR语义(颜色值) //--------------------------------------------------------------------------------- float4 frag(vertexOutput input) : COLOR { //直接返回输入的颜色值 return input.color; } //===========结束CG着色器语言编写模块=========== ENDCG } } }将其施用于材质之上的效果如下:
4.颜色单项可调的RGB Cube
我们在上述Shader的基础上,加上一个可调节的属性,代码如下:
//RGB立方体单项可调 Shader "浅墨Shader编程/Volume12/4.RGB cube v2" { //------------------------------------【属性值】------------------------------------ Properties { //单项颜色调节变量 _ColorValue("Color", Range(0.0, 1.0)) = 0.6 } //------------------------------------【唯一的子着色器】------------------------------------ SubShader { //--------------------------------唯一的通道------------------------------- Pass { //===========开启CG着色器语言编写模块============ CGPROGRAM //编译指令:告知编译器顶点和片段着色函数的名称 #pragma vertex vert #pragma fragment frag //顶点着色器输出结构 struct vertexOutput { float4 positon : SV_POSITION; float4 color : TEXCOORD0; }; //变量声明 uniform float _ColorValue; //--------------------------------【顶点着色函数】----------------------------- // 输入:POSITION语义 // 输出:顶点输出结构体 //--------------------------------------------------------------------------------- vertexOutput vert(float4 vertexPos : POSITION) { //实例化一个vertexOutput输出结构 vertexOutput output; //坐标系变换:将三维空间中的坐标投影到二维窗口 output.positon = mul(UNITY_MATRIX_MVP, vertexPos); //输出颜色为顶点位置加上一个颜色偏移量 output.color = vertexPos + float4(_ColorValue, _ColorValue, _ColorValue, 0.0); //返回最终的值 return output; } //--------------------------------【片段着色函数】----------------------------- // 输入:vertexOutput结构体 // 输出:COLOR语义(颜色值) //--------------------------------------------------------------------------------- float4 frag(vertexOutput input) : COLOR { //直接返回输入的颜色值 return input.color; } //===========结束CG着色器语言编写模块=========== ENDCG } } }将其施用于材质之上的效果如下:
5.三色分量可调的RGB Cube
在上述Shader的基础上,加上三个可调节的属性,分别代表RGB三色的分布情况。那么Shader代码就变成了:
//RGB立方体三色可调 Shader "浅墨Shader编程/Volume12/5.RGB cube v3" { //------------------------------------【属性值】------------------------------------ Properties { //红色 _ColorValueRed("ColorRed", Range(0.0, 1.0)) = 0.2 //绿色 _ColorValueGreen("ColorGreen", Range(0.0, 1.0)) = 0.5 //蓝色 _ColorValueBlue("ColorBlue", Range(0.0, 1.0)) = 0.6 } //------------------------------------【唯一的子着色器】------------------------------------ SubShader { //--------------------------------唯一的通道------------------------------- Pass { //===========开启CG着色器语言编写模块============ CGPROGRAM //编译指令:告知编译器顶点和片段着色函数的名称 #pragma vertex vert #pragma fragment frag //顶点着色器输出结构 struct vertexOutput { float4 positon : SV_POSITION; float4 color : TEXCOORD0; }; //变量声明 uniform float _ColorValueRed; uniform float _ColorValueGreen; uniform float _ColorValueBlue; //--------------------------------【顶点着色函数】----------------------------- // 输入:POSITION语义 // 输出:顶点输出结构体 //--------------------------------------------------------------------------------- vertexOutput vert(float4 vertexPos : POSITION) { //实例化一个vertexOutput输出结构 vertexOutput output; //坐标系变换:将三维空间中的坐标投影到二维窗口 output.positon = mul(UNITY_MATRIX_MVP, vertexPos); //输出颜色为顶点位置加上一个颜色偏移量 output.color = vertexPos + float4(_ColorValueRed, _ColorValueGreen, _ColorValueBlue, 0.0); //返回最终的值 return output; } //--------------------------------【片段着色函数】----------------------------- // 输入:vertexOutput结构体 // 输出:COLOR语义(颜色值) //--------------------------------------------------------------------------------- float4 frag(vertexOutput input) : COLOR { //直接返回输入的颜色值 return input.color; } //===========结束CG着色器语言编写模块=========== ENDCG } } }将其施用于材质之上的效果如下:
OK,热身差不多就这么多,接下来看看漫反射的原理与Shader实现。
二、漫反射的了解与Shader实现
了解Diffuse Reflection
漫反射(Diffuse Reflection),又称Lambert反射,是投射在粗糙表面上的光向各个方向反射的现象。当一束平行的入射光线射到粗糙的表面时,表面会把光线向着四面八方反射,所以入射线虽然互相平行,由于各点的法线方向不一致,造成反射光线向不同的方向无规则地反射。
在生活中,我们看到的月球的光近乎完全是漫反射。粉笔或者磨砂纸也是漫反射。事实上,任何表面的漫反射看起来都是类似于磨砂表面的效果。
而在完美的漫反射的情况下,所观察到的反射光的强度取决于在表面法线矢量和入射光的光线之间的角度的余弦。如图所示,经过归一化的物体表面法线向量N与物体表面正交,光源照射到物体表面的光线方向为L。
也就是说,我们可以使用表面的法线矢量N和入射光方向矢量L,计算漫反射。
想要计算出眼睛观察到的漫反射光,需要计算归一化的表面法向量N和归一化的方向光源向量L之间夹角的余弦值,即点积运算N·L。因为任何两个向量a和b的点积运算可以表示为:
而对于已经经过归一化的向量,|a| 和|b|都是1。
如果点积运算N·L为负,那么光源方向就是在表面内部照射过来的,这是错误的。这种情况下,我们就将反射光设为0即可。这可以通过代码max(0, N·L)来实现,这样就确保了在点积结果为负时,得到的结果为0。此外,漫反射光还取决于入射光light和材质的漫反射系数 。而对于一个黑色的表面,材质漫反射系数 为0,而对于白色的表面,材质的漫反射强度就为1.
根据如上的表述,漫反射强度的方程如下:
需注意,此公式适用于任何单一的颜色分量(如红、绿、蓝),也适用于颜色分量混合颜色的入射光。
OK,理论部分先说这么多。
实现Diffuse Reflection Shader
关于漫反射的可编程Shader实现,总结一下吧:
- 核心代码可以在顶点着色器中实现,也可以在片段着色函数中实现。
- 需在世界空间中进行实现,因为世界空间中Unity提供了光源方向。
- 关于如何获取参数,总结如下:
- 通过属性properties中指定后传进来。
- 世界空间的光源方向由unity内置变量_WorldSpaceLightPos0给出。
- 光源颜色由unity内置变量_LightColor0给出。
- 环境光颜色通过内置变量UNITY_LIGHTMODEL_AMBIENT给出。
- 用Tags {"LightMode" = "ForwardBase"}确保上述内置变量的值处于正确的状态。
- 世界空间下的物体表面的法线向量,可以通过输出参数语义NORMAL来获得物体空间下的表面的法线向量,然后将此向量从物体空间转到世界空间中获得。
OK,下面就开始Shader的书写吧。
6.单色可调的漫反射光照Shader书写
本节首先实现了一个单一光照条件下,可调颜色的漫反射可编程Shader。代码如下。
Shader "浅墨Shader编程/Volume12/6.Diffuse(Lambert) Shader" { //------------------------------------【属性值】------------------------------------ Properties { //颜色值 _Color("Main Color", Color) = (1, 1, 1, 1) } //------------------------------------【唯一的子着色器】------------------------------------ SubShader { //渲染类型设置:不透明 Tags{ "RenderType" = "Opaque" } //设置光照模式:ForwardBase Tags{ "LightingMode" = "ForwardBase" } //细节层次设为:200 LOD 200 //--------------------------------唯一的通道------------------------------- Pass { //===========开启CG着色器语言编写模块=========== CGPROGRAM //编译指令:告知编译器顶点和片段着色函数的名称 #pragma vertex vert #pragma fragment frag //包含头文件 #include "UnityCG.cginc" //顶点着色器输入结构 struct appdata { float4 vertex : POSITION;//顶点位置 float3 normal : NORMAL;//法线向量坐标 }; //顶点着色器输出结构 struct v2f { float4 position : SV_POSITION;//像素位置 float3 normal : NORMAL;//法线向量坐标 }; //变量声明 float4 _LightColor0; float4 _Color; //--------------------------------【顶点着色函数】----------------------------- // 输入:顶点输入结构体 // 输出:顶点输出结构体 //--------------------------------------------------------------------------------- v2f vert(appdata input) { //【1】声明一个输出结构对象 v2f output; //【2】填充此输出结构 //输出的顶点位置为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口 output.position = mul(UNITY_MATRIX_MVP, input.vertex); //获取顶点在世界空间中的法线向量坐标 output.normal = mul(float4(input.normal, 0.0), _World2Object).xyz; //【3】返回此输出结构对象 return output; } //--------------------------------【片段着色函数】----------------------------- // 输入:顶点输出结构体 // 输出:float4型的像素颜色值 //--------------------------------------------------------------------------------- fixed4 frag(v2f input) : COLOR { //【1】先准备好需要的参数 //获取法线的方向 float3 normalDirection = normalize(input.normal); //获取入射光线的值与方向 float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz); //【2】计算出漫反射颜色值 Diffuse=LightColor * MainColor * max(0,dot(N,L)) float3 diffuse = _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)); //【3】合并漫反射颜色值与环境光颜色值 float4 DiffuseAmbient = float4(diffuse, 1.0) + UNITY_LIGHTMODEL_AMBIENT; //【4】将漫反射-环境光颜色值乘上纹理颜色,并返回 return DiffuseAmbient; } //===========结束CG着色器语言编写模块=========== ENDCG } } }将其施用于材质之上的效果如下:
7.可调颜色和自定义纹理的漫反射光照Shader
接下来,让我们进一步实现一个可以自定义纹理的漫反射光照的顶点&片段着色器。也就是在Properties属性中加上一项纹理,然后在最终的漫反射颜色计算完成之后,乘上纹理即可。
Shader "浅墨Shader编程/Volume12/7.Diffuse(Lambert) Shader with Texture" { //------------------------------------【属性值】------------------------------------ Properties { //主纹理 _MainTex("Texture", 2D) = "white"{} //主颜色值 _Color("Main Color", Color) = (1, 1, 1, 1) } //------------------------------------【唯一的子着色器】------------------------------------ SubShader { //渲染类型设置:不透明 Tags{ "RenderType" = "Opaque" } //设置光照模式:ForwardBase Tags{ "LightingMode" = "ForwardBase" } //细节层次设为:200 LOD 200 //--------------------------------唯一的通道------------------------------- Pass { //===========开启CG着色器语言编写模块=========== CGPROGRAM //编译指令:告知编译器顶点和片段着色函数的名称 #pragma vertex vert #pragma fragment frag //包含头文件 #include "UnityCG.cginc" //顶点着色器输入结构 struct appdata { float4 vertex : POSITION;//顶点位置 float3 normal : NORMAL;//法线向量坐标 float2 texcoord : TEXCOORD0;//纹理坐标 }; //顶点着色器输出结构 struct v2f { float4 positon : SV_POSITION;//像素位置 float3 normal : NORMAL;//法线向量坐标 float2 texcoord : TEXCOORD0;//纹理坐标 }; //变量声明 float4 _LightColor0; float4 _Color; sampler2D _MainTex; //--------------------------------【顶点着色函数】----------------------------- // 输入:顶点输入结构体 // 输出:顶点输出结构体 //--------------------------------------------------------------------------------- v2f vert(appdata input) { //【1】声明一个输出结构对象 v2f output; //【2】填充此输出结构 //输出的顶点位置为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口 output.positon = mul(UNITY_MATRIX_MVP, input.vertex); //获取顶点在世界空间中的法线向量坐标 output.normal = mul(float4(input.normal, 0.0), _World2Object).xyz; //输出的纹理坐标也就是输入的纹理坐标 output.texcoord = input.texcoord; //【3】返回此输出结构对象 return output; } //--------------------------------【片段着色函数】----------------------------- // 输入:顶点输出结构体 // 输出:float4型的像素颜色值 //--------------------------------------------------------------------------------- fixed4 frag(v2f input) : COLOR { //【1】先准备好需要的参数 //获取纹理颜色 float4 texColor = tex2D(_MainTex, input.texcoord); //获取法线的方向 float3 normalDirection = normalize(input.normal); //获取入射光线的值与方向 float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz); //【2】计算出漫反射颜色值 Diffuse=LightColor * MainColor * max(0,dot(N,L)) float3 diffuse = _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection)); //【3】合并漫反射颜色值与环境光颜色值 float4 DiffuseAmbient = float4(diffuse, 1.0) + UNITY_LIGHTMODEL_AMBIENT; //【4】将漫反射-环境光颜色值乘上纹理颜色,并返回 return DiffuseAmbient* texColor; } //===========结束CG着色器语言编写模块=========== ENDCG } } }
将其施用于材质之上的效果如下:
下图是此漫反射Shader使用到皮卡丘模型上的效果图。
今天写的Shader的全家福:
最后放几张加上镜头特效的场景效果图:
OK,本篇的内容大致如此,下次更新见。
附1: 本博文相关资源下载链接清单
【百度云】博文游戏场景exe下载
【百度云】博文示例场景资源和源码工程下载 ( PS:工程所用Unity版本为5.2.1)
【Github】本文全部Shader源码
附2:Reference
[1] http://docs.unity3d.com/Manual/SL-Reference.html
[2] https://en.wikibooks.org/wiki/Cg_Programming