本系列文章由@浅墨_毛星云 出品,转载请注明出处。
文章链接: 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工程下载
之前的链接被屏蔽了,这边再分享一份:
链接:http://pan.baidu.com/s/1hsmbEAw 密码:7qzk
好的,本篇文章到这里就全部结束了。
下周浅墨要出差,所以下周是否更新得看具体的行程安排。
OK,新的游戏编程之旅正在继续,我们下次再会~