本系列文章由@浅墨_毛星云 出品,转载请注明出处。
文章链接: http://blog.csdn.net/poem_qianmo/article/details/42215079
作者:毛星云(浅墨) 微博:http://weibo.com/u/1723155442
本文主要讲解了Unity中SurfaceShader的具体写法,以及几个常用的CG函数的用法。
在这里先说明一下,表面着色器将分为两次讲解,本文介绍表面着色器的基本概念和一些写法,用内置的兰伯特光照模式来进行Surface Shader的书写,而下次将介绍Surface Shader+自定义的光照模式的写法。
PS:最近几天,在完美世界、腾讯互娱工作多年的几个朋友们问了浅墨一些表面着色器相关的Shader写法,浅墨当时回答他们的时候自己也是似懂非懂。通过这篇文章的书写,现在算是对这方面知识有了进一步的理解。所以说嘛,写作是总结自己所学的一种很好的方式~
OK,言归正传,依然是先来看看本文配套的游戏场景截图。
运行游戏,音乐响起,首先是一个欧式风格的集市映入眼帘:
雨淅沥沥、天空、晚霞、海平面:
天空中,一群飞鸟飞过:
峡谷:
暗黑城堡:
在小集市中逛荡:
城堡概况图:
OK,图先就上这么多。文章末尾有更多的运行截图,并提供了源工程的下载。可运行的exe下载在这里:
好的,我们正式开始。
一、表面着色器的标准输出结构(Surface Output)
要书写Surface Shader,了解表面着色器的标准输出结构必不可少。此为表面着色器书写的第一个要素。
而定义一个“表面函数(surface function)”,需要输入相关的UV或数据信息,并在输出结构中填充SurfaceOutput。SurfaceOutput基本上描述了表面的特性(光照的颜色反射率、法线、散射、镜面等)。其实还是需要用CG或者HLSL编写此部分的代码。
我们其实是通过表面着色器(Surface Shader)来编译这段CG或者HLSL代码的,然后计算出需要填充输入什么,输出什么等相关信息,并产生真实的顶点(vertex)&像素(pixel)着色器,以及把渲染路径传递到正向或延时渲染路径。
说白了,还是那句话,Surface Shader是Unity微创新自创的一套着色器标准,是Unity自己发扬光大的一项使Shader的书写门槛降低和更易用的技术。
我们之前的文章中已经稍微了解过,表面着色器(Surface Shader)的标准输出结构是这样的:
- struct SurfaceOutput
- {
- half3 Albedo; //反射率,也就是纹理颜色值(r,g,b)
- half3 Normal; //法线,法向量(x, y, z)
- half3 Emission; //自发光颜色值(r, g,b)
- half Specular; //镜面反射度
- half Gloss; //光泽度
- half Alpha; //透明度
- };
而这个结构体的用法,其实就是对这些需要用到的成员变量在surf函数中赋一下值,比如说这样:
- //表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //反射率,也就是纹理颜色值赋为(0.6, 0.6, 0.6)
- o.Albedo= 0.6;
- }
注意到Albedo是half3类型的。那么o.Albedo = 0.6和o.Albedo = float3(0.6,0.6,0.6)是等价的。
二、表面着色器的编译指令
表面着色器的编译指令为编写表面着色器的第二个要素。
表面着色器放在CGPROGRAM .. ENDCG块里面,就像其他的着色器一样。区别是:
其必须嵌在子着色器(SubShader)块里面。而不是Pass块里面。因为表面着色器( Surface shader)将在多重通道(multiple passes)内编译自己,而不是放在某个Pass中。
我们甚至可以这样说,如果你写表面着色器,用不到Pass代码块,一般直接在SubShader块中完成就行了。
使用的 #pragma surface...指令,以声明这是一个表面着色器。指令的句法是:
#pragma surface surfaceFunction lightModel[optionalparams]
所需参数的讲解:
- surfaceFunction - 表示指定名称的Cg函数中有表面着色器(surface shader)代码。这个函数的格式应该是这样:void surf (Input IN,inout SurfaceOutput o), 其中Input是我们自己定义的结构。Input结构中应该包含所需的纹理坐标(texture coordinates)和和表面函数(surfaceFunction)所需要的额外的必需变量。
- lightModel -使用的光照模式。内置的是Lambert (diffuse)和 BlinnPhong (specular)两种,一般习惯用Lambert,也就是兰伯特光照模式。而编写自己的光照模式我们将在下次更新中讲解。
可以根据自己的需要,进阶选这样的一些可选参数:
- alpha -透明( Alpha)混合模式。使用它可以写出半透明的着色器。
- alphatest:VariableName -透明( Alpha)测试模式。使用它可以写出 镂空效果的着色器。镂空大小的变量(VariableName)是一个float型的变量。
- vertex:VertexFunction - 自定义的顶点函数(vertex function)。相关写法可参考Unity内建的Shader:树皮着色器(Tree Bark shader),如Tree Creator Bark、Tree Soft Occlusion Bark这两个Shader。
- finalcolor:ColorFunction - 自定义的最终颜色函数(final color function)。 比如说这样:
- #pragma surfacesurf Lambert finalcolor:mycolor。
相关Shader示例可见下文Shader实战部分的第五个Shader(纹理载入+颜色可调)。
- exclude_path:prepass 或者 exclude_path:forward - 使用指定的渲染路径,不需要生成通道。
- addshadow - 添加阴影投射 & 收集通道(collector passes)。通常用自定义顶点修改,使阴影也能投射在任何程序的顶点动画上。
- dualforward - 在正向(forward)渲染路径中使用 双重光照贴图(dual lightmaps)。
- fullforwardshadows - 在正向(forward)渲染路径中支持所有阴影类型。
- decal:add - 添加贴图着色器(decal shader) (例如: terrain AddPass)。
- decal:blend - 混合半透明的贴图着色器(Semitransparent decal shader)。
- softvegetation - 使表面着色器(surface shader)仅能在Soft Vegetation打开时渲染。
- noambient - 不适用于任何环境光照(ambient lighting)或者球面调和光照(spherical harmonics lights)。
- novertexlights - 在正向渲染(Forward rendering)中不适用于球面调和光照(spherical harmonics lights)或者每个顶点光照(per-vertex lights)。
- nolightmap - 在这个着色器上禁用光照贴图(lightmap)。(适合写一些小着色器)
- nodirlightmap - 在这个着色器上禁用方向光照贴图(directional lightmaps)。 (适合写一些小着色器)。
- noforwardadd - 禁用正向渲染添加通道(Forward rendering additive pass)。 这会使这个着色器支持一个完整的方向光和所有光照的per-vertex/SH计算。(也是适合写一些小着色器).
- approxview - 着色器需要计算标准视图的每个顶点(per-vertex)方向而不是每个像索(per-pixel)方向。 这样更快,但是视图方向不完全是当前摄像机(camera) 所接近的表面。
- halfasview - 在光照函数(lighting function)中传递进来的是half-direction向量,而不是视图方向(view-direction)向量。 Half-direction会计算且会把每个顶点(per vertex)标准化。这样做会提高执行效率,但是准确率会打折扣。
此外,我们还可以在 CGPROGRA内编写 #pragma debug,然后表面编译器(surface compiler)会进行解释生成代码。
三、表面着色器输入结构(Input Structure)
表面着色器书写的第三个要素是指明表面输入结构(Input Structure)。
Input 这个输入结构通常拥有着色器需要的所有纹理坐标(texture coordinates)。纹理坐标(Texturecoordinates)必须被命名为“uv”后接纹理(texture)名字。(或者uv2开始,使用第二纹理坐标集)。
可以在输入结构中根据自己的需要,可选附加这样的一些候选值:
- float3 viewDir - 视图方向( view direction)值。为了计算视差效果(Parallax effects),边缘光照(rim lighting)等,需要包含视图方向( view direction)值。
- float4 with COLOR semantic -每个顶点(per-vertex)颜色的插值。
- float4 screenPos - 屏幕空间中的位置。 为了反射效果,需要包含屏幕空间中的位置信息。比如在Dark Unity中所使用的 WetStreet着色器。
- float3 worldPos - 世界空间中的位置。
- float3 worldRefl - 世界空间中的反射向量。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。 请参考这个例子:Reflect-Diffuse 着色器。
- float3 worldNormal - 世界空间中的法线向量(normal vector)。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。
- float3 worldRefl; INTERNAL_DATA - 世界空间中的反射向量。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。为了获得基于每个顶点法线贴图( per-pixel normal map)的反射向量(reflection vector)需要使用世界反射向量(WorldReflectionVector (IN, o.Normal))。请参考这个例子: Reflect-Bumped着色器。
- float3 worldNormal; INTERNAL_DATA -世界空间中的法线向量(normal vector)。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。为了获得基于每个顶点法线贴图( per-pixel normal map)的法线向量(normal vector)需要使用世界法线向量(WorldNormalVector (IN, o.Normal))。
四、一些本次写Shader用到的CG函数讲解
本次Shader书写用到了四个CG着色器编程语言中的函数——UnpackNormal、saturate、dot、tex2D。下面将分别对其进行讲解。
4.1UnpackNormal( )函数
UnpackNormal接受一个fixed4的输入,并将其转换为所对应的法线值(fixed3),并将其赋给输出的Normal,就可以参与到光线运算中完成接下来的渲染工作了。
一个调用示例:
- o.Normal = UnpackNormal (tex2D (_BumpMap,IN.uv_BumpMap));
4.2saturate( )函数
saturate的字面解释是浸湿,浸透。其作用其实也就是将取值转化为[0,1]之内的一个值。
其可选的原型如下:
- float saturate(float x);
- float1 saturate(float1 x);
- float2 saturate(float2 x);
- float3 saturate(float3 x);
- float4 saturate(float4 x);
- half saturate(half x);
- half1 saturate(half1 x);
- half2 saturate(half2 x);
- half3 saturate(half3 x);
- half4 saturate(half4 x);
- fixed saturate(fixed x);
- fixed1 saturate(fixed1 x);
- fixed2 saturate(fixed2 x);
- fixed3 saturate(fixed3 x);
- fixed4 saturate(fixed4 x);
其唯一的一个参数x表示矢量或者标量的饱和值(Vector or scalar to saturate.),也就是将这个x转化为[0,1]之内的值。
其返回值:
- 如果x取值小于0,则返回值为0.
- 如果x取值大于1,则返回值为1.
- 若x在0到1之间,则直接返回x的值。
其代码实现大致如下:
- float saturate(float x)
- {
- return max(0,min(1, x));
- }
一个调用示例:
- half rim = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));
4.3 dot( )函数
dot函数顾名思义,是高等数学中的点积操作,用于返回两个向量的标量积。
可选原型如下:
- float dot(float a, float b);
- float dot(float1 a, float1 b);
- float dot(float2 a, float2 b);
- float dot(float3 a, float3 b);
- float dot(float4 a, float4 b);
- half dot(half a, half b);
- half dot(half1 a, half1 b);
- half dot(half2 a, half2 b);
- half dot(half3 a, half3 b);
- half dot(half4 a, half4 b);
- fixed dot(fixed a, fixed b);
- fixed dot(fixed1 a, fixed1 b);
- fixed dot(fixed2 a, fixed2 b);
- fixed dot(fixed3 a, fixed3 b);
- fixed dot(fixed4 a, fixed4 b);
其代码实现大致是这样的:
- float dot(float4 a, float4 b)
- {
- return a.x*b.x +a.y*b.y + a.z*b.z + a.w*b.w;
- }
一个调用示例:
- float answer= dot (normalize(IN.viewDir),o.Normal);
4.4 tex2D( )函数
让我们看一看CG中用得比较多的用于2D纹理采样的tex2D函数的用法。其备选的原型也是非常之多:
- float4 tex2D(sampler2D samp, float2 s)
- float4 tex2D(sampler2D samp, float2 s, inttexelOff)
- float4 tex2D(sampler2D samp, float3 s)
- float4 tex2D(sampler2D samp, float3 s, inttexelOff)
- float4 tex2D(sampler2D samp, float2 s,float2 dx, float2 dy)
- float4 tex2D(sampler2D samp, float2 s,float2 dx, float2 dy, int texelOff)
- float4 tex2D(sampler2D samp, float3 s,float2 dx, float2 dy)
- float4 tex2D(sampler2D samp, float3 s,float2 dx, float2 dy, int texelOff)
- int4 tex2D(isampler2D samp, float2 s)
- int4 tex2D(isampler2D samp, float2 s, inttexelOff)
- int4 tex2D(isampler2D samp, float2 s,float2 dx, float2 dy)
- int4 tex2D(isampler2D samp, float2 s,float2 dx, float2 dy, int texelOff)
- unsigned int4 tex2D(usampler2D samp, float2s)
- unsigned int4 tex2D(usampler2D samp, float2s, int texelOff)
- unsigned int4 tex2D(usampler2D samp, float2s, float2 dx, float2 dy)
- unsigned int4 tex2D(usampler2D samp, float2s, float2 dx, float2 dy,int texelOff)
参数简介:
samp-需要查找的采样对象,也就是填个纹理对象在这里。
s-需进行查找的纹理坐标。
dx-预计算的沿X轴方向的导数。
dy-预计算的沿Y轴方向的导数。
texelOff-添加给最终纹理的偏移量
而其返回值,自然是查找到的纹理。
最后,看一个综合了本次讲解的四个函数(UnpackNormal、saturate、tex2D、dot)的Surface Shader中surf函数的示例:
- //【2】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //从主纹理获取rgb颜色值
- o.Albedo= tex2D (_MainTex, IN.uv_MainTex).rgb;
- //从凹凸纹理获取法线值
- o.Normal= UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
- //从_RimColor参数获取自发光颜色
- halfrim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
- o.Emission= _RimColor.rgb * pow (rim, _RimPower);
- }
五、写Shdaer实战
上面都是些概念,下面我们将进行一些实战的Shader书写,将学到的这些概念用到实际当中去。
本次我们将讲解9个表面SurfaceShader的写法,从最基本的Surface Shader,循序渐进,一点一点加功能,到最后的稍微有点复杂的“凹凸纹理+颜色可调+边缘光照+细节纹理“表面Shader的写法。本期的全部Shader的合照如下:
在材质界面菜单中的显示:
OK,下面开始讲解,从最基本的开始。
1.最基本的Surface Shader
先看一个使用内建光照模式的最基本的Surface Shader应该怎么写:
- Shader "浅墨Shader编程/Volume6/24.最基本的SurfaceShader"
- {
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式
- #pragma surface surf Lambert
- //【2】输入结构
- struct Input
- {
- //四元素的颜色值(RGBA)
- float4 color : COLOR;
- };
- //【3】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //反射率
- o.Albedo = float3(0.5,0.8,0.3);//(0.5,0.8,0.3)分别对应于RGB分量
- //而o.Albedo = 0.6;等效于写o.Albedo = float3(0.6,0.6,0.6);
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
可以发现,一个最基本的Surface Shader,至少需要有光照模式的声明、输入结构和表面着色函数的编写这三部分。
另外,主要注意其中的surf函数的写法,就是把上文讲到的Surface Output结构体中需要用到的成员变量拿来赋值:
- //【2】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //反射率
- o.Albedo= float3(0.5,0.8,0.3);//(0.5,0.8,0.3)分别对应于RGB分量
- //而o.Albedo = 0.6;等效于写o.Albedo =float3(0.6,0.6,0.6);
- }
注释中已经写得很明白,且之前也已经讲过,o.Albedo = 0.6;等效于写o.Albedo = float3(0.6,0.6,0.6);
来个举一反三,则o.Albedo =1;等效于写o.Albedo= float3(1,1,1);
我们将此Shader编译后赋给材质,得到如下效果:
而在场景中的实测效果为:
2.颜色可调
在最基本的Surface Shader的基础上,加上一点代码,就成了这里的可调颜色的Surface Shader:
- Shader "浅墨Shader编程/Volume6/25.颜色可调的SurfaceShader"
- {
- //--------------------------------【属性】---------------------------------------
- Properties
- {
- _Color ("【主颜色】Main Color", Color) = (0.1,0.3,0.9,1)
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType"="Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式
- #pragma surface surf Lambert
- //变量声明
- float4 _Color;
- //【2】输入结构
- struct Input
- {
- //四元素的颜色值(RGBA)
- float4 color : COLOR;
- };
- //【3】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //反射率
- o.Albedo = _Color.rgb;
- //透明值
- o.Alpha = _Color.a;
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- FallBack "Diffuse"
- }
我们将此Shader编译后赋给材质,得到如下效果,和之前的固定功能Shader一样,可以自由调节颜色:
其在场景中的实测效果为:
3.基本纹理载入
再来看看如何实现一个基本的纹理载入Surface Shader:
- Shader "浅墨Shader编程/Volume6/26.基本纹理载入"
- {
- //--------------------------------【属性】----------------------------------------
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式
- #pragma surface surf Lambert
- //【2】输入结构
- struct Input
- {
- //纹理的uv值
- float2 uv_MainTex;
- };
- //变量声明
- sampler2D _MainTex;
- //【3】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //从纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
我们将此Shader编译后赋给材质,得到如下效果:
场景中的实测效果图为:
4.凹凸纹理载入
让我们慢慢添加特性,使得到的Surface Shader的效果与功能越来越强大。接着来看看Surface Shader的凹凸纹理如何实现:
- Shader "浅墨Shader编程/Volume6/27.凹凸纹理载入"
- {
- //--------------------------------【属性】----------------------------------------
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- _BumpMap ("【凹凸纹理】Bumpmap", 2D) = "bump" {}
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式
- #pragma surface surf Lambert
- //【2】输入结构
- struct Input
- {
- //主纹理的uv值
- float2 uv_MainTex;
- //凹凸纹理的uv值
- float2 uv_BumpMap;
- };
- //变量声明
- sampler2D _MainTex;//主纹理
- sampler2D _BumpMap;//凹凸纹理
- //【3】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //从主纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- //从凹凸纹理获取法线值
- o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
我们将此Shader编译后赋给材质,得到如下效果:
场景中的实测效果图为:
5.纹理载入+颜色可调
接着看一看纹理如何通过一个finalcolor关键字自定义函数,来达到调色的目的:
- Shader "浅墨Shader编程/Volume6/28.纹理+颜色修改"
- {
- //--------------------------------【属性】----------------------------------------
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- _ColorTint ("【色泽】Tint", Color) = (0.6, 0.3, 0.6, 0.3)
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数
- #pragma surface surf Lambert finalcolor:setcolor
- //【2】输入结构
- struct Input
- {
- //纹理的uv值
- float2 uv_MainTex;
- };
- //变量声明
- fixed4 _ColorTint;
- sampler2D _MainTex;
- //【3】自定义颜色函数setcolor的编写
- void setcolor (Input IN, SurfaceOutput o, inout fixed4 color)
- {
- //将自选的颜色值乘给color
- color *= _ColorTint;
- }
- //【4】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //从主纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
我们将此Shader编译后赋给材质,得到如下效果:
调些颜色玩一玩:
场景中的实测效果图为:
6. 凹凸纹理+边缘光照
在之前凹凸纹理的基础上让我们加上喜闻乐见的边缘光照:
- Shader "浅墨Shader编程/Volume6/29.凹凸纹理+边缘光照"
- {
- //--------------------------------【属性】----------------------------------------
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- _BumpMap ("【凹凸纹理】Bumpmap", 2D) = "bump" {}
- _RimColor ("【边缘颜色】Rim Color", Color) = (0.26,0.19,0.16,0.0)
- _RimPower ("【边缘颜色强度】Rim Power", Range(0.5,8.0)) = 3.0
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数
- #pragma surface surf Lambert
- //【2】输入结构
- struct Input
- {
- //主纹理的uv值
- float2 uv_MainTex;
- //凹凸纹理的uv值
- float2 uv_BumpMap;
- //当前坐标的视角方向
- float3 viewDir;
- };
- //变量声明
- sampler2D _MainTex;//主纹理
- sampler2D _BumpMap;//凹凸纹理
- float4 _RimColor;//边缘颜色
- float _RimPower;//边缘颜色强度
- //【3】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //从主纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- //从凹凸纹理获取法线值
- o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
- //从_RimColor参数获取自发光颜色
- half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
- o.Emission = _RimColor.rgb * pow (rim, _RimPower);
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
其中的viewDir 意为WorldSpace View Direction,也就是当前坐标的视角方向:
关于surf中的两句新加的代码在这里也讲一下。
上面已经提到过,Normalize函数,用于获取到的viewDir坐标转成一个单位向量且方向不变,外面再与点的法线做点积。最外层再用 saturate算出一[0,1]之间的最靠近的值。这样算出一个rim边界。原理可以这样解释:
这里o.Normal就是单位向量。外加Normalize了viewDir。因此求得的点积就是夹角的cos值。因为cos值越大,夹角越小,所以,这时取反来。这样,夹角越大,所反射上的颜色就越多。于是就得到的两边发光的效果。哈哈这样明了吧。
这里再介绍一下这个half。CG里还有类似的float和fixed。half是一种低精度的float,但有时也会被选择成与float一样的精度。先就说这么多吧,后面还会遇到的,到时候再讲。
我们将此Shader编译后赋给材质,得到如下效果,除了凹凸纹理的选择之外,还有边缘发光颜色和强度可供自由定制:
依然是调一些颜色玩一玩:
![]()
![]()
场景中的实测截图为:
7.凹凸纹理+颜色可调
接下来我们看看凹凸纹理+颜色可调的Shader怎么写:
- Shader "浅墨Shader编程/Volume6/30.凹凸纹理+颜色可调+边缘光照"
- {
- //--------------------------------【属性】----------------------------------------
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- _BumpMap ("【凹凸纹理】Bumpmap", 2D) = "bump" {}
- _ColorTint ("【色泽】Tint", Color) = (0.6, 0.3, 0.6, 0.3)
- _RimColor ("【边缘颜色】Rim Color", Color) = (0.26,0.19,0.16,0.0)
- _RimPower ("【边缘颜色强度】Rim Power", Range(0.5,8.0)) = 3.0
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数
- #pragma surface surf Lambert finalcolor:setcolor
- //【2】输入结构
- struct Input
- {
- //主纹理的uv值
- float2 uv_MainTex;
- //凹凸纹理的uv值
- float2 uv_BumpMap;
- //当前坐标的视角方向
- float3 viewDir;
- };
- //变量声明
- sampler2D _MainTex;
- sampler2D _BumpMap;
- fixed4 _ColorTint;
- float4 _RimColor;
- float _RimPower;
- //【3】自定义颜色函数setcolor的编写
- void setcolor (Input IN, SurfaceOutput o, inout fixed4 color)
- {
- color *= _ColorTint;
- }
- //【4】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //从主纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- //从凹凸纹理获取法线值
- o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
- //从_RimColor参数获取自发光颜色
- half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
- o.Emission = _RimColor.rgb * pow (rim, _RimPower);
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
我们将此Shader编译后赋给材质,得到如下非常赞的效果。除了载入纹理,还有色泽,边缘颜色和强度可供调节:
依然是调些效果玩一玩:
![]()
![]()
![]()
而在场景中的实测效果为:
8.细节纹理
接着我们来看一个在屏幕上显示纹理细节的Shader:
- Shader "浅墨Shader编程/Volume6/31.细节纹理"
- {
- //--------------------------------【属性】----------------------------------------
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- _Detail ("【细节纹理】Detail", 2D) = "gray" {}
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式
- #pragma surface surf Lambert
- //【2】输入结构
- struct Input
- {
- //主纹理的uv值
- float2 uv_MainTex;
- //细节纹理的uv值
- float2 uv_Detail;
- };
- //变量声明
- sampler2D _MainTex;
- sampler2D _Detail;
- //【3】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //先从主纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- //设置细节纹理
- o.Albedo *= tex2D (_Detail, IN.uv_Detail).rgb * 2;
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
我们将此Shader编译后赋给材质,不加细节纹理如下:
加纹理细节的效果图如下:
而在场景中的实测效果为:
9.凹凸纹理+颜色可调+边缘光照+细节纹理
结合上面的8个Shader,我们可以完成本期文章这个结合了凹凸纹理+颜色可调+边缘光照+细节纹理的稍微复杂一点的Surface Shader:
- Shader "浅墨Shader编程/Volume6/32.凹凸纹理+颜色可调+边缘光照+细节纹理"
- {
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- _BumpMap ("【凹凸纹理】Bumpmap", 2D) = "bump" {}
- _Detail ("【细节纹理】Detail", 2D) = "gray" {}
- _ColorTint ("【色泽】Tint", Color) = (0.6, 0.3, 0.6, 0.3)
- _RimColor ("【边缘颜色】Rim Color", Color) = (0.26,0.19,0.16,0.0)
- _RimPower ("【边缘颜色强度】Rim Power", Range(0.5,8.0)) = 3.0
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数
- #pragma surface surf Lambert finalcolor:setcolor
- //【2】输入结构
- struct Input
- {
- //主纹理的uv值
- float2 uv_MainTex;
- //凹凸纹理的uv值
- float2 uv_BumpMap;
- //细节纹理的uv值
- float2 uv_Detail;
- //当前坐标的视角方向
- float3 viewDir;
- };
- //变量声明
- sampler2D _MainTex;
- sampler2D _BumpMap;
- sampler2D _Detail;
- fixed4 _ColorTint;
- float4 _RimColor;
- float _RimPower;
- //【3】自定义颜色函数setcolor的编写
- void setcolor (Input IN, SurfaceOutput o, inout fixed4 color)
- {
- color *= _ColorTint;
- }
- //【4】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //先从主纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- //设置细节纹理
- o.Albedo *= tex2D (_Detail, IN.uv_Detail).rgb * 2;
- //从凹凸纹理获取法线值
- o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
- //从_RimColor参数获取自发光颜色
- half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
- o.Emission = _RimColor.rgb * pow (rim, _RimPower);
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
我们将此Shader编译后赋给材质,得到如下效果:
依然是调一些效果玩一玩:
![]()
![]()
而在场景中的实测效果为:
六、场景搭建
以大师级美工鬼斧神工的场景作品为基础,浅墨调整了场景布局,加入了音乐,并加入了更多高级特效,于是便得到了如此这次非常炫酷的暗黑城堡场景。
运行游戏,史诗级音乐渐渐响起,雨淅沥沥地下,我们来到了神秘的暗黑城堡:
集市:
在集市中逛荡1:
在集市中逛荡2:
在集市中逛荡3:
通向远方的桥梁:
壮观的瀑布:
感受瀑布溅起的水花:
在瀑布旁看远方,雾气氤氲:
成群的飞鸟飞过:
远方:
一片荒凉:
孤岛:
透过栅栏远眺:
画面太美,简直乱真:
海面上倒映出晚霞:
城堡概况:
最后一张本次的Shader全家福:
OK,美图就放这么多。游戏场景可运行的exe可以在文章开头中提供的链接下载。而以下是源工程的下载链接。
本篇文章的示例程序源工程请点击此处下载:
【浅墨Unity3D Shader编程】之六 暗黑城堡篇配套Unity工程下载
好的,本篇文章到这里就全部结束了。
下周浅墨要出差,所以下周是否更新得看具体的行程安排。
OK,新的游戏编程之旅正在继续,我们下次再会~
本系列文章由@浅墨_毛星云 出品,转载请注明出处。
文章链接: http://blog.csdn.net/poem_qianmo/article/details/42215079
作者:毛星云(浅墨) 微博:http://weibo.com/u/1723155442
本文主要讲解了Unity中SurfaceShader的具体写法,以及几个常用的CG函数的用法。
在这里先说明一下,表面着色器将分为两次讲解,本文介绍表面着色器的基本概念和一些写法,用内置的兰伯特光照模式来进行Surface Shader的书写,而下次将介绍Surface Shader+自定义的光照模式的写法。
PS:最近几天,在完美世界、腾讯互娱工作多年的几个朋友们问了浅墨一些表面着色器相关的Shader写法,浅墨当时回答他们的时候自己也是似懂非懂。通过这篇文章的书写,现在算是对这方面知识有了进一步的理解。所以说嘛,写作是总结自己所学的一种很好的方式~
OK,言归正传,依然是先来看看本文配套的游戏场景截图。
运行游戏,音乐响起,首先是一个欧式风格的集市映入眼帘:
雨淅沥沥、天空、晚霞、海平面:
天空中,一群飞鸟飞过:
峡谷:
暗黑城堡:
在小集市中逛荡:
城堡概况图:
OK,图先就上这么多。文章末尾有更多的运行截图,并提供了源工程的下载。可运行的exe下载在这里:
好的,我们正式开始。
一、表面着色器的标准输出结构(Surface Output)
要书写Surface Shader,了解表面着色器的标准输出结构必不可少。此为表面着色器书写的第一个要素。
而定义一个“表面函数(surface function)”,需要输入相关的UV或数据信息,并在输出结构中填充SurfaceOutput。SurfaceOutput基本上描述了表面的特性(光照的颜色反射率、法线、散射、镜面等)。其实还是需要用CG或者HLSL编写此部分的代码。
我们其实是通过表面着色器(Surface Shader)来编译这段CG或者HLSL代码的,然后计算出需要填充输入什么,输出什么等相关信息,并产生真实的顶点(vertex)&像素(pixel)着色器,以及把渲染路径传递到正向或延时渲染路径。
说白了,还是那句话,Surface Shader是Unity微创新自创的一套着色器标准,是Unity自己发扬光大的一项使Shader的书写门槛降低和更易用的技术。
我们之前的文章中已经稍微了解过,表面着色器(Surface Shader)的标准输出结构是这样的:
- struct SurfaceOutput
- {
- half3 Albedo; //反射率,也就是纹理颜色值(r,g,b)
- half3 Normal; //法线,法向量(x, y, z)
- half3 Emission; //自发光颜色值(r, g,b)
- half Specular; //镜面反射度
- half Gloss; //光泽度
- half Alpha; //透明度
- };
而这个结构体的用法,其实就是对这些需要用到的成员变量在surf函数中赋一下值,比如说这样:
- //表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //反射率,也就是纹理颜色值赋为(0.6, 0.6, 0.6)
- o.Albedo= 0.6;
- }
注意到Albedo是half3类型的。那么o.Albedo = 0.6和o.Albedo = float3(0.6,0.6,0.6)是等价的。
二、表面着色器的编译指令
表面着色器的编译指令为编写表面着色器的第二个要素。
表面着色器放在CGPROGRAM .. ENDCG块里面,就像其他的着色器一样。区别是:
其必须嵌在子着色器(SubShader)块里面。而不是Pass块里面。因为表面着色器( Surface shader)将在多重通道(multiple passes)内编译自己,而不是放在某个Pass中。
我们甚至可以这样说,如果你写表面着色器,用不到Pass代码块,一般直接在SubShader块中完成就行了。
使用的 #pragma surface...指令,以声明这是一个表面着色器。指令的句法是:
#pragma surface surfaceFunction lightModel[optionalparams]
所需参数的讲解:
- surfaceFunction - 表示指定名称的Cg函数中有表面着色器(surface shader)代码。这个函数的格式应该是这样:void surf (Input IN,inout SurfaceOutput o), 其中Input是我们自己定义的结构。Input结构中应该包含所需的纹理坐标(texture coordinates)和和表面函数(surfaceFunction)所需要的额外的必需变量。
- lightModel -使用的光照模式。内置的是Lambert (diffuse)和 BlinnPhong (specular)两种,一般习惯用Lambert,也就是兰伯特光照模式。而编写自己的光照模式我们将在下次更新中讲解。
可以根据自己的需要,进阶选这样的一些可选参数:
- alpha -透明( Alpha)混合模式。使用它可以写出半透明的着色器。
- alphatest:VariableName -透明( Alpha)测试模式。使用它可以写出 镂空效果的着色器。镂空大小的变量(VariableName)是一个float型的变量。
- vertex:VertexFunction - 自定义的顶点函数(vertex function)。相关写法可参考Unity内建的Shader:树皮着色器(Tree Bark shader),如Tree Creator Bark、Tree Soft Occlusion Bark这两个Shader。
- finalcolor:ColorFunction - 自定义的最终颜色函数(final color function)。 比如说这样:
- #pragma surfacesurf Lambert finalcolor:mycolor。
相关Shader示例可见下文Shader实战部分的第五个Shader(纹理载入+颜色可调)。
- exclude_path:prepass 或者 exclude_path:forward - 使用指定的渲染路径,不需要生成通道。
- addshadow - 添加阴影投射 & 收集通道(collector passes)。通常用自定义顶点修改,使阴影也能投射在任何程序的顶点动画上。
- dualforward - 在正向(forward)渲染路径中使用 双重光照贴图(dual lightmaps)。
- fullforwardshadows - 在正向(forward)渲染路径中支持所有阴影类型。
- decal:add - 添加贴图着色器(decal shader) (例如: terrain AddPass)。
- decal:blend - 混合半透明的贴图着色器(Semitransparent decal shader)。
- softvegetation - 使表面着色器(surface shader)仅能在Soft Vegetation打开时渲染。
- noambient - 不适用于任何环境光照(ambient lighting)或者球面调和光照(spherical harmonics lights)。
- novertexlights - 在正向渲染(Forward rendering)中不适用于球面调和光照(spherical harmonics lights)或者每个顶点光照(per-vertex lights)。
- nolightmap - 在这个着色器上禁用光照贴图(lightmap)。(适合写一些小着色器)
- nodirlightmap - 在这个着色器上禁用方向光照贴图(directional lightmaps)。 (适合写一些小着色器)。
- noforwardadd - 禁用正向渲染添加通道(Forward rendering additive pass)。 这会使这个着色器支持一个完整的方向光和所有光照的per-vertex/SH计算。(也是适合写一些小着色器).
- approxview - 着色器需要计算标准视图的每个顶点(per-vertex)方向而不是每个像索(per-pixel)方向。 这样更快,但是视图方向不完全是当前摄像机(camera) 所接近的表面。
- halfasview - 在光照函数(lighting function)中传递进来的是half-direction向量,而不是视图方向(view-direction)向量。 Half-direction会计算且会把每个顶点(per vertex)标准化。这样做会提高执行效率,但是准确率会打折扣。
此外,我们还可以在 CGPROGRA内编写 #pragma debug,然后表面编译器(surface compiler)会进行解释生成代码。
三、表面着色器输入结构(Input Structure)
表面着色器书写的第三个要素是指明表面输入结构(Input Structure)。
Input 这个输入结构通常拥有着色器需要的所有纹理坐标(texture coordinates)。纹理坐标(Texturecoordinates)必须被命名为“uv”后接纹理(texture)名字。(或者uv2开始,使用第二纹理坐标集)。
可以在输入结构中根据自己的需要,可选附加这样的一些候选值:
- float3 viewDir - 视图方向( view direction)值。为了计算视差效果(Parallax effects),边缘光照(rim lighting)等,需要包含视图方向( view direction)值。
- float4 with COLOR semantic -每个顶点(per-vertex)颜色的插值。
- float4 screenPos - 屏幕空间中的位置。 为了反射效果,需要包含屏幕空间中的位置信息。比如在Dark Unity中所使用的 WetStreet着色器。
- float3 worldPos - 世界空间中的位置。
- float3 worldRefl - 世界空间中的反射向量。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。 请参考这个例子:Reflect-Diffuse 着色器。
- float3 worldNormal - 世界空间中的法线向量(normal vector)。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。
- float3 worldRefl; INTERNAL_DATA - 世界空间中的反射向量。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。为了获得基于每个顶点法线贴图( per-pixel normal map)的反射向量(reflection vector)需要使用世界反射向量(WorldReflectionVector (IN, o.Normal))。请参考这个例子: Reflect-Bumped着色器。
- float3 worldNormal; INTERNAL_DATA -世界空间中的法线向量(normal vector)。如果表面着色器(surface shader)不写入法线(o.Normal)参数,将包含这个参数。为了获得基于每个顶点法线贴图( per-pixel normal map)的法线向量(normal vector)需要使用世界法线向量(WorldNormalVector (IN, o.Normal))。
四、一些本次写Shader用到的CG函数讲解
本次Shader书写用到了四个CG着色器编程语言中的函数——UnpackNormal、saturate、dot、tex2D。下面将分别对其进行讲解。
4.1UnpackNormal( )函数
UnpackNormal接受一个fixed4的输入,并将其转换为所对应的法线值(fixed3),并将其赋给输出的Normal,就可以参与到光线运算中完成接下来的渲染工作了。
一个调用示例:
- o.Normal = UnpackNormal (tex2D (_BumpMap,IN.uv_BumpMap));
4.2saturate( )函数
saturate的字面解释是浸湿,浸透。其作用其实也就是将取值转化为[0,1]之内的一个值。
其可选的原型如下:
- float saturate(float x);
- float1 saturate(float1 x);
- float2 saturate(float2 x);
- float3 saturate(float3 x);
- float4 saturate(float4 x);
- half saturate(half x);
- half1 saturate(half1 x);
- half2 saturate(half2 x);
- half3 saturate(half3 x);
- half4 saturate(half4 x);
- fixed saturate(fixed x);
- fixed1 saturate(fixed1 x);
- fixed2 saturate(fixed2 x);
- fixed3 saturate(fixed3 x);
- fixed4 saturate(fixed4 x);
其唯一的一个参数x表示矢量或者标量的饱和值(Vector or scalar to saturate.),也就是将这个x转化为[0,1]之内的值。
其返回值:
- 如果x取值小于0,则返回值为0.
- 如果x取值大于1,则返回值为1.
- 若x在0到1之间,则直接返回x的值。
其代码实现大致如下:
- float saturate(float x)
- {
- return max(0,min(1, x));
- }
一个调用示例:
- half rim = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));
4.3 dot( )函数
dot函数顾名思义,是高等数学中的点积操作,用于返回两个向量的标量积。
可选原型如下:
- float dot(float a, float b);
- float dot(float1 a, float1 b);
- float dot(float2 a, float2 b);
- float dot(float3 a, float3 b);
- float dot(float4 a, float4 b);
- half dot(half a, half b);
- half dot(half1 a, half1 b);
- half dot(half2 a, half2 b);
- half dot(half3 a, half3 b);
- half dot(half4 a, half4 b);
- fixed dot(fixed a, fixed b);
- fixed dot(fixed1 a, fixed1 b);
- fixed dot(fixed2 a, fixed2 b);
- fixed dot(fixed3 a, fixed3 b);
- fixed dot(fixed4 a, fixed4 b);
其代码实现大致是这样的:
- float dot(float4 a, float4 b)
- {
- return a.x*b.x +a.y*b.y + a.z*b.z + a.w*b.w;
- }
一个调用示例:
- float answer= dot (normalize(IN.viewDir),o.Normal);
4.4 tex2D( )函数
让我们看一看CG中用得比较多的用于2D纹理采样的tex2D函数的用法。其备选的原型也是非常之多:
- float4 tex2D(sampler2D samp, float2 s)
- float4 tex2D(sampler2D samp, float2 s, inttexelOff)
- float4 tex2D(sampler2D samp, float3 s)
- float4 tex2D(sampler2D samp, float3 s, inttexelOff)
- float4 tex2D(sampler2D samp, float2 s,float2 dx, float2 dy)
- float4 tex2D(sampler2D samp, float2 s,float2 dx, float2 dy, int texelOff)
- float4 tex2D(sampler2D samp, float3 s,float2 dx, float2 dy)
- float4 tex2D(sampler2D samp, float3 s,float2 dx, float2 dy, int texelOff)
- int4 tex2D(isampler2D samp, float2 s)
- int4 tex2D(isampler2D samp, float2 s, inttexelOff)
- int4 tex2D(isampler2D samp, float2 s,float2 dx, float2 dy)
- int4 tex2D(isampler2D samp, float2 s,float2 dx, float2 dy, int texelOff)
- unsigned int4 tex2D(usampler2D samp, float2s)
- unsigned int4 tex2D(usampler2D samp, float2s, int texelOff)
- unsigned int4 tex2D(usampler2D samp, float2s, float2 dx, float2 dy)
- unsigned int4 tex2D(usampler2D samp, float2s, float2 dx, float2 dy,int texelOff)
参数简介:
samp-需要查找的采样对象,也就是填个纹理对象在这里。
s-需进行查找的纹理坐标。
dx-预计算的沿X轴方向的导数。
dy-预计算的沿Y轴方向的导数。
texelOff-添加给最终纹理的偏移量
而其返回值,自然是查找到的纹理。
最后,看一个综合了本次讲解的四个函数(UnpackNormal、saturate、tex2D、dot)的Surface Shader中surf函数的示例:
- //【2】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //从主纹理获取rgb颜色值
- o.Albedo= tex2D (_MainTex, IN.uv_MainTex).rgb;
- //从凹凸纹理获取法线值
- o.Normal= UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
- //从_RimColor参数获取自发光颜色
- halfrim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
- o.Emission= _RimColor.rgb * pow (rim, _RimPower);
- }
五、写Shdaer实战
上面都是些概念,下面我们将进行一些实战的Shader书写,将学到的这些概念用到实际当中去。
本次我们将讲解9个表面SurfaceShader的写法,从最基本的Surface Shader,循序渐进,一点一点加功能,到最后的稍微有点复杂的“凹凸纹理+颜色可调+边缘光照+细节纹理“表面Shader的写法。本期的全部Shader的合照如下:
在材质界面菜单中的显示:
OK,下面开始讲解,从最基本的开始。
1.最基本的Surface Shader
先看一个使用内建光照模式的最基本的Surface Shader应该怎么写:
- Shader "浅墨Shader编程/Volume6/24.最基本的SurfaceShader"
- {
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式
- #pragma surface surf Lambert
- //【2】输入结构
- struct Input
- {
- //四元素的颜色值(RGBA)
- float4 color : COLOR;
- };
- //【3】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //反射率
- o.Albedo = float3(0.5,0.8,0.3);//(0.5,0.8,0.3)分别对应于RGB分量
- //而o.Albedo = 0.6;等效于写o.Albedo = float3(0.6,0.6,0.6);
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
可以发现,一个最基本的Surface Shader,至少需要有光照模式的声明、输入结构和表面着色函数的编写这三部分。
另外,主要注意其中的surf函数的写法,就是把上文讲到的Surface Output结构体中需要用到的成员变量拿来赋值:
- //【2】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //反射率
- o.Albedo= float3(0.5,0.8,0.3);//(0.5,0.8,0.3)分别对应于RGB分量
- //而o.Albedo = 0.6;等效于写o.Albedo =float3(0.6,0.6,0.6);
- }
注释中已经写得很明白,且之前也已经讲过,o.Albedo = 0.6;等效于写o.Albedo = float3(0.6,0.6,0.6);
来个举一反三,则o.Albedo =1;等效于写o.Albedo= float3(1,1,1);
我们将此Shader编译后赋给材质,得到如下效果:
而在场景中的实测效果为:
2.颜色可调
在最基本的Surface Shader的基础上,加上一点代码,就成了这里的可调颜色的Surface Shader:
- Shader "浅墨Shader编程/Volume6/25.颜色可调的SurfaceShader"
- {
- //--------------------------------【属性】---------------------------------------
- Properties
- {
- _Color ("【主颜色】Main Color", Color) = (0.1,0.3,0.9,1)
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType"="Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式
- #pragma surface surf Lambert
- //变量声明
- float4 _Color;
- //【2】输入结构
- struct Input
- {
- //四元素的颜色值(RGBA)
- float4 color : COLOR;
- };
- //【3】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //反射率
- o.Albedo = _Color.rgb;
- //透明值
- o.Alpha = _Color.a;
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- FallBack "Diffuse"
- }
我们将此Shader编译后赋给材质,得到如下效果,和之前的固定功能Shader一样,可以自由调节颜色:
其在场景中的实测效果为:
3.基本纹理载入
再来看看如何实现一个基本的纹理载入Surface Shader:
- Shader "浅墨Shader编程/Volume6/26.基本纹理载入"
- {
- //--------------------------------【属性】----------------------------------------
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式
- #pragma surface surf Lambert
- //【2】输入结构
- struct Input
- {
- //纹理的uv值
- float2 uv_MainTex;
- };
- //变量声明
- sampler2D _MainTex;
- //【3】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //从纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
我们将此Shader编译后赋给材质,得到如下效果:
场景中的实测效果图为:
4.凹凸纹理载入
让我们慢慢添加特性,使得到的Surface Shader的效果与功能越来越强大。接着来看看Surface Shader的凹凸纹理如何实现:
- Shader "浅墨Shader编程/Volume6/27.凹凸纹理载入"
- {
- //--------------------------------【属性】----------------------------------------
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- _BumpMap ("【凹凸纹理】Bumpmap", 2D) = "bump" {}
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式
- #pragma surface surf Lambert
- //【2】输入结构
- struct Input
- {
- //主纹理的uv值
- float2 uv_MainTex;
- //凹凸纹理的uv值
- float2 uv_BumpMap;
- };
- //变量声明
- sampler2D _MainTex;//主纹理
- sampler2D _BumpMap;//凹凸纹理
- //【3】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //从主纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- //从凹凸纹理获取法线值
- o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
我们将此Shader编译后赋给材质,得到如下效果:
场景中的实测效果图为:
5.纹理载入+颜色可调
接着看一看纹理如何通过一个finalcolor关键字自定义函数,来达到调色的目的:
- Shader "浅墨Shader编程/Volume6/28.纹理+颜色修改"
- {
- //--------------------------------【属性】----------------------------------------
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- _ColorTint ("【色泽】Tint", Color) = (0.6, 0.3, 0.6, 0.3)
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数
- #pragma surface surf Lambert finalcolor:setcolor
- //【2】输入结构
- struct Input
- {
- //纹理的uv值
- float2 uv_MainTex;
- };
- //变量声明
- fixed4 _ColorTint;
- sampler2D _MainTex;
- //【3】自定义颜色函数setcolor的编写
- void setcolor (Input IN, SurfaceOutput o, inout fixed4 color)
- {
- //将自选的颜色值乘给color
- color *= _ColorTint;
- }
- //【4】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //从主纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
我们将此Shader编译后赋给材质,得到如下效果:
调些颜色玩一玩:
场景中的实测效果图为:
6. 凹凸纹理+边缘光照
在之前凹凸纹理的基础上让我们加上喜闻乐见的边缘光照:
- Shader "浅墨Shader编程/Volume6/29.凹凸纹理+边缘光照"
- {
- //--------------------------------【属性】----------------------------------------
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- _BumpMap ("【凹凸纹理】Bumpmap", 2D) = "bump" {}
- _RimColor ("【边缘颜色】Rim Color", Color) = (0.26,0.19,0.16,0.0)
- _RimPower ("【边缘颜色强度】Rim Power", Range(0.5,8.0)) = 3.0
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数
- #pragma surface surf Lambert
- //【2】输入结构
- struct Input
- {
- //主纹理的uv值
- float2 uv_MainTex;
- //凹凸纹理的uv值
- float2 uv_BumpMap;
- //当前坐标的视角方向
- float3 viewDir;
- };
- //变量声明
- sampler2D _MainTex;//主纹理
- sampler2D _BumpMap;//凹凸纹理
- float4 _RimColor;//边缘颜色
- float _RimPower;//边缘颜色强度
- //【3】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //从主纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- //从凹凸纹理获取法线值
- o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
- //从_RimColor参数获取自发光颜色
- half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
- o.Emission = _RimColor.rgb * pow (rim, _RimPower);
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
其中的viewDir 意为WorldSpace View Direction,也就是当前坐标的视角方向:
关于surf中的两句新加的代码在这里也讲一下。
上面已经提到过,Normalize函数,用于获取到的viewDir坐标转成一个单位向量且方向不变,外面再与点的法线做点积。最外层再用 saturate算出一[0,1]之间的最靠近的值。这样算出一个rim边界。原理可以这样解释:
这里o.Normal就是单位向量。外加Normalize了viewDir。因此求得的点积就是夹角的cos值。因为cos值越大,夹角越小,所以,这时取反来。这样,夹角越大,所反射上的颜色就越多。于是就得到的两边发光的效果。哈哈这样明了吧。
这里再介绍一下这个half。CG里还有类似的float和fixed。half是一种低精度的float,但有时也会被选择成与float一样的精度。先就说这么多吧,后面还会遇到的,到时候再讲。
我们将此Shader编译后赋给材质,得到如下效果,除了凹凸纹理的选择之外,还有边缘发光颜色和强度可供自由定制:
依然是调一些颜色玩一玩:
![]()
![]()
场景中的实测截图为:
7.凹凸纹理+颜色可调
接下来我们看看凹凸纹理+颜色可调的Shader怎么写:
- Shader "浅墨Shader编程/Volume6/30.凹凸纹理+颜色可调+边缘光照"
- {
- //--------------------------------【属性】----------------------------------------
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- _BumpMap ("【凹凸纹理】Bumpmap", 2D) = "bump" {}
- _ColorTint ("【色泽】Tint", Color) = (0.6, 0.3, 0.6, 0.3)
- _RimColor ("【边缘颜色】Rim Color", Color) = (0.26,0.19,0.16,0.0)
- _RimPower ("【边缘颜色强度】Rim Power", Range(0.5,8.0)) = 3.0
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数
- #pragma surface surf Lambert finalcolor:setcolor
- //【2】输入结构
- struct Input
- {
- //主纹理的uv值
- float2 uv_MainTex;
- //凹凸纹理的uv值
- float2 uv_BumpMap;
- //当前坐标的视角方向
- float3 viewDir;
- };
- //变量声明
- sampler2D _MainTex;
- sampler2D _BumpMap;
- fixed4 _ColorTint;
- float4 _RimColor;
- float _RimPower;
- //【3】自定义颜色函数setcolor的编写
- void setcolor (Input IN, SurfaceOutput o, inout fixed4 color)
- {
- color *= _ColorTint;
- }
- //【4】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //从主纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- //从凹凸纹理获取法线值
- o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
- //从_RimColor参数获取自发光颜色
- half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
- o.Emission = _RimColor.rgb * pow (rim, _RimPower);
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
我们将此Shader编译后赋给材质,得到如下非常赞的效果。除了载入纹理,还有色泽,边缘颜色和强度可供调节:
依然是调些效果玩一玩:
![]()
![]()
![]()
而在场景中的实测效果为:
8.细节纹理
接着我们来看一个在屏幕上显示纹理细节的Shader:
- Shader "浅墨Shader编程/Volume6/31.细节纹理"
- {
- //--------------------------------【属性】----------------------------------------
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- _Detail ("【细节纹理】Detail", 2D) = "gray" {}
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式
- #pragma surface surf Lambert
- //【2】输入结构
- struct Input
- {
- //主纹理的uv值
- float2 uv_MainTex;
- //细节纹理的uv值
- float2 uv_Detail;
- };
- //变量声明
- sampler2D _MainTex;
- sampler2D _Detail;
- //【3】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //先从主纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- //设置细节纹理
- o.Albedo *= tex2D (_Detail, IN.uv_Detail).rgb * 2;
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
我们将此Shader编译后赋给材质,不加细节纹理如下:
加纹理细节的效果图如下:
而在场景中的实测效果为:
9.凹凸纹理+颜色可调+边缘光照+细节纹理
结合上面的8个Shader,我们可以完成本期文章这个结合了凹凸纹理+颜色可调+边缘光照+细节纹理的稍微复杂一点的Surface Shader:
- Shader "浅墨Shader编程/Volume6/32.凹凸纹理+颜色可调+边缘光照+细节纹理"
- {
- Properties
- {
- _MainTex ("【主纹理】Texture", 2D) = "white" {}
- _BumpMap ("【凹凸纹理】Bumpmap", 2D) = "bump" {}
- _Detail ("【细节纹理】Detail", 2D) = "gray" {}
- _ColorTint ("【色泽】Tint", Color) = (0.6, 0.3, 0.6, 0.3)
- _RimColor ("【边缘颜色】Rim Color", Color) = (0.26,0.19,0.16,0.0)
- _RimPower ("【边缘颜色强度】Rim Power", Range(0.5,8.0)) = 3.0
- }
- //--------------------------------【子着色器】----------------------------------
- SubShader
- {
- //-----------子着色器标签----------
- Tags { "RenderType" = "Opaque" }
- //-------------------开始CG着色器编程语言段-----------------
- CGPROGRAM
- //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数
- #pragma surface surf Lambert finalcolor:setcolor
- //【2】输入结构
- struct Input
- {
- //主纹理的uv值
- float2 uv_MainTex;
- //凹凸纹理的uv值
- float2 uv_BumpMap;
- //细节纹理的uv值
- float2 uv_Detail;
- //当前坐标的视角方向
- float3 viewDir;
- };
- //变量声明
- sampler2D _MainTex;
- sampler2D _BumpMap;
- sampler2D _Detail;
- fixed4 _ColorTint;
- float4 _RimColor;
- float _RimPower;
- //【3】自定义颜色函数setcolor的编写
- void setcolor (Input IN, SurfaceOutput o, inout fixed4 color)
- {
- color *= _ColorTint;
- }
- //【4】表面着色函数的编写
- void surf (Input IN, inout SurfaceOutput o)
- {
- //先从主纹理获取rgb颜色值
- o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
- //设置细节纹理
- o.Albedo *= tex2D (_Detail, IN.uv_Detail).rgb * 2;
- //从凹凸纹理获取法线值
- o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
- //从_RimColor参数获取自发光颜色
- half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
- o.Emission = _RimColor.rgb * pow (rim, _RimPower);
- }
- //-------------------结束CG着色器编程语言段------------------
- ENDCG
- }
- //“备胎”为普通漫反射
- Fallback "Diffuse"
- }
我们将此Shader编译后赋给材质,得到如下效果:
依然是调一些效果玩一玩:
![]()
![]()
而在场景中的实测效果为:
六、场景搭建
以大师级美工鬼斧神工的场景作品为基础,浅墨调整了场景布局,加入了音乐,并加入了更多高级特效,于是便得到了如此这次非常炫酷的暗黑城堡场景。
运行游戏,史诗级音乐渐渐响起,雨淅沥沥地下,我们来到了神秘的暗黑城堡:
集市:
在集市中逛荡1:
在集市中逛荡2:
在集市中逛荡3:
通向远方的桥梁:
壮观的瀑布:
感受瀑布溅起的水花:
在瀑布旁看远方,雾气氤氲:
成群的飞鸟飞过:
远方:
一片荒凉:
孤岛:
透过栅栏远眺:
画面太美,简直乱真:
海面上倒映出晚霞:
城堡概况:
最后一张本次的Shader全家福:
OK,美图就放这么多。游戏场景可运行的exe可以在文章开头中提供的链接下载。而以下是源工程的下载链接。
本篇文章的示例程序源工程请点击此处下载:
【浅墨Unity3D Shader编程】之六 暗黑城堡篇配套Unity工程下载
好的,本篇文章到这里就全部结束了。
下周浅墨要出差,所以下周是否更新得看具体的行程安排。
OK,新的游戏编程之旅正在继续,我们下次再会~