700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > 表面着色器(Surface Shader)的写法(一)

表面着色器(Surface Shader)的写法(一)

时间:2023-12-23 22:08:18

相关推荐

表面着色器(Surface Shader)的写法(一)

一、表面着色器的标准输出结构(Surface Output)

要书写Surface Shader,了解表面着色器的标准输出结构必不可少。此为表面着色器书写的第一个要素。

而定义一个“表面函数(surface function)”,需要输入相关的UV或数据信息,并在输出结构中填充SurfaceOutput。SurfaceOutput基本上描述了表面的特性(光照的颜色反射率、法线、散射、镜面等)。其实还是需要用CG或者HLSL编写此部分的代码。

我们其实是通过表面着色器(Surface Shader)来编译这段CG或者HLSL代码的,然后计算出需要填充输入什么,输出什么等相关信息,并产生真实的顶点(vertex)&像素(pixel)着色器,以及把渲染路径传递到正向或延时渲染路径。

说白了,还是那句话,Surface Shader是Unity微创新自创的一套着色器标准,是Unity自己发扬光大的一项使Shader的书写门槛降低和更易用的技术。

我们之前的文章中已经稍微了解过,表面着色器(Surface Shader)的标准输出结构是这样的:

[cpp]view plaincopystructSurfaceOutput { half3Albedo;//反射率,也就是纹理颜色值(r,g,b) half3Normal;//法线,法向量(x,y,z) half3Emission;//自发光颜色值(r,g,b) halfSpecular;//镜面反射度 halfGloss;//光泽度 halfAlpha;//透明度 };

而这个结构体的用法,其实就是对这些需要用到的成员变量在surf函数中赋一下值,比如说这样:

[cpp]view plaincopy//表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //反射率,也就是纹理颜色值赋为(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...指令,以声明这是一个表面着色器。指令的句法是:

#pragmasurface 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)。 比如说这样:

[cpp]view plaincopy#pragmasurfacesurfLambertfinalcolor: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 - 禁用正向渲染添加通道(Forwardrendering 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,就可以参与到光线运算中完成接下来的渲染工作了。

一个调用示例:

[cpp]view plaincopyo.Normal=UnpackNormal(tex2D(_BumpMap,IN.uv_BumpMap));

4.2saturate( )函数

saturate的字面解释是浸湿,浸透。其作用其实也就是将取值转化为[0,1]之内的一个值。

其可选的原型如下:

[cpp]view plaincopyfloatsaturate(floatx); float1saturate(float1x); float2saturate(float2x); float3saturate(float3x); float4saturate(float4x); halfsaturate(halfx); half1saturate(half1x); half2saturate(half2x); half3saturate(half3x); half4saturate(half4x); fixedsaturate(fixedx); fixed1saturate(fixed1x); fixed2saturate(fixed2x); fixed3saturate(fixed3x); fixed4saturate(fixed4x);

其唯一的一个参数x表示矢量或者标量的饱和值(Vector or scalar to saturate.),也就是将这个x转化为[0,1]之内的值。

其返回值:

如果x取值小于0,则返回值为0.如果x取值大于1,则返回值为1.若x在0到1之间,则直接返回x的值。

其代码实现大致如下:

[cpp]view plaincopyfloatsaturate(floatx) { returnmax(0,min(1,x)); }

一个调用示例:

[cpp]view plaincopyhalfrim=1.0-saturate(dot(normalize(IN.viewDir),o.Normal));

4.3dot( )函数

dot函数顾名思义,是高等数学中的点积操作,用于返回两个向量的标量积。

可选原型如下:

[cpp]view plaincopyfloatdot(floata,floatb); floatdot(float1a,float1b); floatdot(float2a,float2b); floatdot(float3a,float3b); floatdot(float4a,float4b); halfdot(halfa,halfb); halfdot(half1a,half1b); halfdot(half2a,half2b); halfdot(half3a,half3b); halfdot(half4a,half4b); fixeddot(fixeda,fixedb); fixeddot(fixed1a,fixed1b); fixeddot(fixed2a,fixed2b); fixeddot(fixed3a,fixed3b); fixeddot(fixed4a,fixed4b);

其代码实现大致是这样的:

[cpp]view plaincopyfloatdot(float4a,float4b) { returna.x*b.x+a.y*b.y+a.z*b.z+a.w*b.w; }

一个调用示例:

[cpp]view plaincopyfloatanswer=dot(normalize(IN.viewDir),o.Normal);

4.4tex2D( )函数

让我们看一看CG中用得比较多的用于2D纹理采样的tex2D函数的用法。其备选的原型也是非常之多:

[cpp]view plaincopyfloat4tex2D(sampler2Dsamp,float2s) float4tex2D(sampler2Dsamp,float2s,inttexelOff) float4tex2D(sampler2Dsamp,float3s) float4tex2D(sampler2Dsamp,float3s,inttexelOff) float4tex2D(sampler2Dsamp,float2s,float2dx,float2dy) float4tex2D(sampler2Dsamp,float2s,float2dx,float2dy,inttexelOff) float4tex2D(sampler2Dsamp,float3s,float2dx,float2dy) float4tex2D(sampler2Dsamp,float3s,float2dx,float2dy,inttexelOff) int4tex2D(isampler2Dsamp,float2s) int4tex2D(isampler2Dsamp,float2s,inttexelOff) int4tex2D(isampler2Dsamp,float2s,float2dx,float2dy) int4tex2D(isampler2Dsamp,float2s,float2dx,float2dy,inttexelOff) unsignedint4tex2D(usampler2Dsamp,float2s) unsignedint4tex2D(usampler2Dsamp,float2s,inttexelOff) unsignedint4tex2D(usampler2Dsamp,float2s,float2dx,float2dy) unsignedint4tex2D(usampler2Dsamp,float2s,float2dx,float2dy,inttexelOff)

参数简介:

samp-需要查找的采样对象,也就是填个纹理对象在这里。

s-需进行查找的纹理坐标。

dx-预计算的沿X轴方向的导数。

dy-预计算的沿Y轴方向的导数。

texelOff-添加给最终纹理的偏移量

而其返回值,自然是查找到的纹理。

最后,看一个综合了本次讲解的四个函数(UnpackNormal、saturate、tex2D、dot)的Surface Shader中surf函数的示例:

[cpp]view plaincopy//【2】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //从主纹理获取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应该怎么写:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/24.最基本的SurfaceShader" { //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式 #pragmasurfacesurfLambert //【2】输入结构 structInput { //四元素的颜色值(RGBA) float4color:COLOR; }; //【3】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //反射率 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结构体中需要用到的成员变量拿来赋值:

[cpp]view plaincopy//【2】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //反射率 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:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/25.颜色可调的SurfaceShader" { //--------------------------------【属性】--------------------------------------- Properties { _Color("【主颜色】MainColor",Color)=(0.1,0.3,0.9,1) } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式 #pragmasurfacesurfLambert //变量声明 float4_Color; //【2】输入结构 structInput { //四元素的颜色值(RGBA) float4color:COLOR; }; //【3】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //反射率 o.Albedo=_Color.rgb; //透明值 o.Alpha=_Color.a; } //-------------------结束CG着色器编程语言段------------------ ENDCG } //“备胎”为普通漫反射 FallBack"Diffuse" }

我们将此Shader编译后赋给材质,得到如下效果,和之前的固定功能Shader一样,可以自由调节颜色:

其在场景中的实测效果为:

3.基本纹理载入

再来看看如何实现一个基本的纹理载入Surface Shader:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/26.基本纹理载入" { //--------------------------------【属性】---------------------------------------- Properties { _MainTex("【主纹理】Texture",2D)="white"{} } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式 #pragmasurfacesurfLambert //【2】输入结构 structInput { //纹理的uv值 float2uv_MainTex; }; //变量声明 sampler2D_MainTex; //【3】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //从纹理获取rgb颜色值 o.Albedo=tex2D(_MainTex,IN.uv_MainTex).rgb; } //-------------------结束CG着色器编程语言段------------------ ENDCG } //“备胎”为普通漫反射 Fallback"Diffuse" }

我们将此Shader编译后赋给材质,得到如下效果:

场景中的实测效果图为:

4.凹凸纹理载入

让我们慢慢添加特性,使得到的Surface Shader的效果与功能越来越强大。接着来看看Surface Shader的凹凸纹理如何实现:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/27.凹凸纹理载入" { //--------------------------------【属性】---------------------------------------- Properties { _MainTex("【主纹理】Texture",2D)="white"{} _BumpMap("【凹凸纹理】Bumpmap",2D)="bump"{} } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式 #pragmasurfacesurfLambert //【2】输入结构 structInput { //主纹理的uv值 float2uv_MainTex; //凹凸纹理的uv值 float2uv_BumpMap; }; //变量声明 sampler2D_MainTex;//主纹理 sampler2D_BumpMap;//凹凸纹理 //【3】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //从主纹理获取rgb颜色值 o.Albedo=tex2D(_MainTex,IN.uv_MainTex).rgb; //从凹凸纹理获取法线值 o.Normal=UnpackNormal(tex2D(_BumpMap,IN.uv_BumpMap)); } //-------------------结束CG着色器编程语言段------------------ ENDCG } //“备胎”为普通漫反射 Fallback"Diffuse" }

我们将此Shader编译后赋给材质,得到如下效果:

场景中的实测效果图为:

5.纹理载入+颜色可调

接着看一看纹理如何通过一个finalcolor关键字自定义函数,来达到调色的目的:

[cpp]view plaincopyShader"浅墨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】光照模式声明:使用兰伯特光照模式+自定义颜色函数 #pragmasurfacesurfLambertfinalcolor:setcolor //【2】输入结构 structInput { //纹理的uv值 float2uv_MainTex; }; //变量声明 fixed4_ColorTint; sampler2D_MainTex; //【3】自定义颜色函数setcolor的编写 voidsetcolor(InputIN,SurfaceOutputo,inoutfixed4color) { //将自选的颜色值乘给color color*=_ColorTint; } //【4】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //从主纹理获取rgb颜色值 o.Albedo=tex2D(_MainTex,IN.uv_MainTex).rgb; } //-------------------结束CG着色器编程语言段------------------ ENDCG } //“备胎”为普通漫反射 Fallback"Diffuse" }

我们将此Shader编译后赋给材质,得到如下效果:

调些颜色玩一玩:

场景中的实测效果图为:

6. 凹凸纹理+边缘光照

在之前凹凸纹理的基础上让我们加上喜闻乐见的边缘光照:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/29.凹凸纹理+边缘光照" { //--------------------------------【属性】---------------------------------------- Properties { _MainTex("【主纹理】Texture",2D)="white"{} _BumpMap("【凹凸纹理】Bumpmap",2D)="bump"{} _RimColor("【边缘颜色】RimColor",Color)=(0.26,0.19,0.16,0.0) _RimPower("【边缘颜色强度】RimPower",Range(0.5,8.0))=3.0 } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数 #pragmasurfacesurfLambert //【2】输入结构 structInput { //主纹理的uv值 float2uv_MainTex; //凹凸纹理的uv值 float2uv_BumpMap; //当前坐标的视角方向 float3viewDir; }; //变量声明 sampler2D_MainTex;//主纹理 sampler2D_BumpMap;//凹凸纹理 float4_RimColor;//边缘颜色 float_RimPower;//边缘颜色强度 //【3】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //从主纹理获取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); } //-------------------结束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怎么写:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/30.凹凸纹理+颜色可调+边缘光照" { //--------------------------------【属性】---------------------------------------- Properties { _MainTex("【主纹理】Texture",2D)="white"{} _BumpMap("【凹凸纹理】Bumpmap",2D)="bump"{} _ColorTint("【色泽】Tint",Color)=(0.6,0.3,0.6,0.3) _RimColor("【边缘颜色】RimColor",Color)=(0.26,0.19,0.16,0.0) _RimPower("【边缘颜色强度】RimPower",Range(0.5,8.0))=3.0 } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数 #pragmasurfacesurfLambertfinalcolor:setcolor //【2】输入结构 structInput { //主纹理的uv值 float2uv_MainTex; //凹凸纹理的uv值 float2uv_BumpMap; //当前坐标的视角方向 float3viewDir; }; //变量声明 sampler2D_MainTex; sampler2D_BumpMap; fixed4_ColorTint; float4_RimColor; float_RimPower; //【3】自定义颜色函数setcolor的编写 voidsetcolor(InputIN,SurfaceOutputo,inoutfixed4color) { color*=_ColorTint; } //【4】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //从主纹理获取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); } //-------------------结束CG着色器编程语言段------------------ ENDCG } //“备胎”为普通漫反射 Fallback"Diffuse" }

我们将此Shader编译后赋给材质,得到如下非常赞的效果。除了载入纹理,还有色泽,边缘颜色和强度可供调节:

依然是调些效果玩一玩:

而在场景中的实测效果为:

8.细节纹理

接着我们来看一个在屏幕上显示纹理细节的Shader:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/31.细节纹理" { //--------------------------------【属性】---------------------------------------- Properties { _MainTex("【主纹理】Texture",2D)="white"{} _Detail("【细节纹理】Detail",2D)="gray"{} } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式 #pragmasurfacesurfLambert //【2】输入结构 structInput { //主纹理的uv值 float2uv_MainTex; //细节纹理的uv值 float2uv_Detail; }; //变量声明 sampler2D_MainTex; sampler2D_Detail; //【3】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //先从主纹理获取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:

[cpp]view plaincopyShader"浅墨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("【边缘颜色】RimColor",Color)=(0.26,0.19,0.16,0.0) _RimPower("【边缘颜色强度】RimPower",Range(0.5,8.0))=3.0 } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数 #pragmasurfacesurfLambertfinalcolor:setcolor //【2】输入结构 structInput { //主纹理的uv值 float2uv_MainTex; //凹凸纹理的uv值 float2uv_BumpMap; //细节纹理的uv值 float2uv_Detail; //当前坐标的视角方向 float3viewDir; }; //变量声明 sampler2D_MainTex; sampler2D_BumpMap; sampler2D_Detail; fixed4_ColorTint; float4_RimColor; float_RimPower; //【3】自定义颜色函数setcolor的编写 voidsetcolor(InputIN,SurfaceOutputo,inoutfixed4color) { color*=_ColorTint; } //【4】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //先从主纹理获取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参数获取自发光颜色 halfrim=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工程下载

之前的链接被屏蔽了,这边再分享一份:

链接:/s/1hsmbEAw 密码:7qzk

好的,本篇文章到这里就全部结束了。

一、表面着色器的标准输出结构(Surface Output)

要书写Surface Shader,了解表面着色器的标准输出结构必不可少。此为表面着色器书写的第一个要素。

而定义一个“表面函数(surface function)”,需要输入相关的UV或数据信息,并在输出结构中填充SurfaceOutput。SurfaceOutput基本上描述了表面的特性(光照的颜色反射率、法线、散射、镜面等)。其实还是需要用CG或者HLSL编写此部分的代码。

我们其实是通过表面着色器(Surface Shader)来编译这段CG或者HLSL代码的,然后计算出需要填充输入什么,输出什么等相关信息,并产生真实的顶点(vertex)&像素(pixel)着色器,以及把渲染路径传递到正向或延时渲染路径。

说白了,还是那句话,Surface Shader是Unity微创新自创的一套着色器标准,是Unity自己发扬光大的一项使Shader的书写门槛降低和更易用的技术。

我们之前的文章中已经稍微了解过,表面着色器(Surface Shader)的标准输出结构是这样的:

[cpp]view plaincopystructSurfaceOutput { half3Albedo;//反射率,也就是纹理颜色值(r,g,b) half3Normal;//法线,法向量(x,y,z) half3Emission;//自发光颜色值(r,g,b) halfSpecular;//镜面反射度 halfGloss;//光泽度 halfAlpha;//透明度 };

而这个结构体的用法,其实就是对这些需要用到的成员变量在surf函数中赋一下值,比如说这样:

[cpp]view plaincopy//表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //反射率,也就是纹理颜色值赋为(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...指令,以声明这是一个表面着色器。指令的句法是:

#pragmasurface 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)。 比如说这样:

[cpp]view plaincopy#pragmasurfacesurfLambertfinalcolor: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 - 禁用正向渲染添加通道(Forwardrendering 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,就可以参与到光线运算中完成接下来的渲染工作了。

一个调用示例:

[cpp]view plaincopyo.Normal=UnpackNormal(tex2D(_BumpMap,IN.uv_BumpMap));

4.2saturate( )函数

saturate的字面解释是浸湿,浸透。其作用其实也就是将取值转化为[0,1]之内的一个值。

其可选的原型如下:

[cpp]view plaincopyfloatsaturate(floatx); float1saturate(float1x); float2saturate(float2x); float3saturate(float3x); float4saturate(float4x); halfsaturate(halfx); half1saturate(half1x); half2saturate(half2x); half3saturate(half3x); half4saturate(half4x); fixedsaturate(fixedx); fixed1saturate(fixed1x); fixed2saturate(fixed2x); fixed3saturate(fixed3x); fixed4saturate(fixed4x);

其唯一的一个参数x表示矢量或者标量的饱和值(Vector or scalar to saturate.),也就是将这个x转化为[0,1]之内的值。

其返回值:

如果x取值小于0,则返回值为0.如果x取值大于1,则返回值为1.若x在0到1之间,则直接返回x的值。

其代码实现大致如下:

[cpp]view plaincopyfloatsaturate(floatx) { returnmax(0,min(1,x)); }

一个调用示例:

[cpp]view plaincopyhalfrim=1.0-saturate(dot(normalize(IN.viewDir),o.Normal));

4.3dot( )函数

dot函数顾名思义,是高等数学中的点积操作,用于返回两个向量的标量积。

可选原型如下:

[cpp]view plaincopyfloatdot(floata,floatb); floatdot(float1a,float1b); floatdot(float2a,float2b); floatdot(float3a,float3b); floatdot(float4a,float4b); halfdot(halfa,halfb); halfdot(half1a,half1b); halfdot(half2a,half2b); halfdot(half3a,half3b); halfdot(half4a,half4b); fixeddot(fixeda,fixedb); fixeddot(fixed1a,fixed1b); fixeddot(fixed2a,fixed2b); fixeddot(fixed3a,fixed3b); fixeddot(fixed4a,fixed4b);

其代码实现大致是这样的:

[cpp]view plaincopyfloatdot(float4a,float4b) { returna.x*b.x+a.y*b.y+a.z*b.z+a.w*b.w; }

一个调用示例:

[cpp]view plaincopyfloatanswer=dot(normalize(IN.viewDir),o.Normal);

4.4tex2D( )函数

让我们看一看CG中用得比较多的用于2D纹理采样的tex2D函数的用法。其备选的原型也是非常之多:

[cpp]view plaincopyfloat4tex2D(sampler2Dsamp,float2s) float4tex2D(sampler2Dsamp,float2s,inttexelOff) float4tex2D(sampler2Dsamp,float3s) float4tex2D(sampler2Dsamp,float3s,inttexelOff) float4tex2D(sampler2Dsamp,float2s,float2dx,float2dy) float4tex2D(sampler2Dsamp,float2s,float2dx,float2dy,inttexelOff) float4tex2D(sampler2Dsamp,float3s,float2dx,float2dy) float4tex2D(sampler2Dsamp,float3s,float2dx,float2dy,inttexelOff) int4tex2D(isampler2Dsamp,float2s) int4tex2D(isampler2Dsamp,float2s,inttexelOff) int4tex2D(isampler2Dsamp,float2s,float2dx,float2dy) int4tex2D(isampler2Dsamp,float2s,float2dx,float2dy,inttexelOff) unsignedint4tex2D(usampler2Dsamp,float2s) unsignedint4tex2D(usampler2Dsamp,float2s,inttexelOff) unsignedint4tex2D(usampler2Dsamp,float2s,float2dx,float2dy) unsignedint4tex2D(usampler2Dsamp,float2s,float2dx,float2dy,inttexelOff)

参数简介:

samp-需要查找的采样对象,也就是填个纹理对象在这里。

s-需进行查找的纹理坐标。

dx-预计算的沿X轴方向的导数。

dy-预计算的沿Y轴方向的导数。

texelOff-添加给最终纹理的偏移量

而其返回值,自然是查找到的纹理。

最后,看一个综合了本次讲解的四个函数(UnpackNormal、saturate、tex2D、dot)的Surface Shader中surf函数的示例:

[cpp]view plaincopy//【2】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //从主纹理获取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应该怎么写:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/24.最基本的SurfaceShader" { //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式 #pragmasurfacesurfLambert //【2】输入结构 structInput { //四元素的颜色值(RGBA) float4color:COLOR; }; //【3】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //反射率 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结构体中需要用到的成员变量拿来赋值:

[cpp]view plaincopy//【2】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //反射率 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:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/25.颜色可调的SurfaceShader" { //--------------------------------【属性】--------------------------------------- Properties { _Color("【主颜色】MainColor",Color)=(0.1,0.3,0.9,1) } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式 #pragmasurfacesurfLambert //变量声明 float4_Color; //【2】输入结构 structInput { //四元素的颜色值(RGBA) float4color:COLOR; }; //【3】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //反射率 o.Albedo=_Color.rgb; //透明值 o.Alpha=_Color.a; } //-------------------结束CG着色器编程语言段------------------ ENDCG } //“备胎”为普通漫反射 FallBack"Diffuse" }

我们将此Shader编译后赋给材质,得到如下效果,和之前的固定功能Shader一样,可以自由调节颜色:

其在场景中的实测效果为:

3.基本纹理载入

再来看看如何实现一个基本的纹理载入Surface Shader:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/26.基本纹理载入" { //--------------------------------【属性】---------------------------------------- Properties { _MainTex("【主纹理】Texture",2D)="white"{} } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式 #pragmasurfacesurfLambert //【2】输入结构 structInput { //纹理的uv值 float2uv_MainTex; }; //变量声明 sampler2D_MainTex; //【3】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //从纹理获取rgb颜色值 o.Albedo=tex2D(_MainTex,IN.uv_MainTex).rgb; } //-------------------结束CG着色器编程语言段------------------ ENDCG } //“备胎”为普通漫反射 Fallback"Diffuse" }

我们将此Shader编译后赋给材质,得到如下效果:

场景中的实测效果图为:

4.凹凸纹理载入

让我们慢慢添加特性,使得到的Surface Shader的效果与功能越来越强大。接着来看看Surface Shader的凹凸纹理如何实现:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/27.凹凸纹理载入" { //--------------------------------【属性】---------------------------------------- Properties { _MainTex("【主纹理】Texture",2D)="white"{} _BumpMap("【凹凸纹理】Bumpmap",2D)="bump"{} } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式 #pragmasurfacesurfLambert //【2】输入结构 structInput { //主纹理的uv值 float2uv_MainTex; //凹凸纹理的uv值 float2uv_BumpMap; }; //变量声明 sampler2D_MainTex;//主纹理 sampler2D_BumpMap;//凹凸纹理 //【3】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //从主纹理获取rgb颜色值 o.Albedo=tex2D(_MainTex,IN.uv_MainTex).rgb; //从凹凸纹理获取法线值 o.Normal=UnpackNormal(tex2D(_BumpMap,IN.uv_BumpMap)); } //-------------------结束CG着色器编程语言段------------------ ENDCG } //“备胎”为普通漫反射 Fallback"Diffuse" }

我们将此Shader编译后赋给材质,得到如下效果:

场景中的实测效果图为:

5.纹理载入+颜色可调

接着看一看纹理如何通过一个finalcolor关键字自定义函数,来达到调色的目的:

[cpp]view plaincopyShader"浅墨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】光照模式声明:使用兰伯特光照模式+自定义颜色函数 #pragmasurfacesurfLambertfinalcolor:setcolor //【2】输入结构 structInput { //纹理的uv值 float2uv_MainTex; }; //变量声明 fixed4_ColorTint; sampler2D_MainTex; //【3】自定义颜色函数setcolor的编写 voidsetcolor(InputIN,SurfaceOutputo,inoutfixed4color) { //将自选的颜色值乘给color color*=_ColorTint; } //【4】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //从主纹理获取rgb颜色值 o.Albedo=tex2D(_MainTex,IN.uv_MainTex).rgb; } //-------------------结束CG着色器编程语言段------------------ ENDCG } //“备胎”为普通漫反射 Fallback"Diffuse" }

我们将此Shader编译后赋给材质,得到如下效果:

调些颜色玩一玩:

场景中的实测效果图为:

6. 凹凸纹理+边缘光照

在之前凹凸纹理的基础上让我们加上喜闻乐见的边缘光照:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/29.凹凸纹理+边缘光照" { //--------------------------------【属性】---------------------------------------- Properties { _MainTex("【主纹理】Texture",2D)="white"{} _BumpMap("【凹凸纹理】Bumpmap",2D)="bump"{} _RimColor("【边缘颜色】RimColor",Color)=(0.26,0.19,0.16,0.0) _RimPower("【边缘颜色强度】RimPower",Range(0.5,8.0))=3.0 } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数 #pragmasurfacesurfLambert //【2】输入结构 structInput { //主纹理的uv值 float2uv_MainTex; //凹凸纹理的uv值 float2uv_BumpMap; //当前坐标的视角方向 float3viewDir; }; //变量声明 sampler2D_MainTex;//主纹理 sampler2D_BumpMap;//凹凸纹理 float4_RimColor;//边缘颜色 float_RimPower;//边缘颜色强度 //【3】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //从主纹理获取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); } //-------------------结束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怎么写:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/30.凹凸纹理+颜色可调+边缘光照" { //--------------------------------【属性】---------------------------------------- Properties { _MainTex("【主纹理】Texture",2D)="white"{} _BumpMap("【凹凸纹理】Bumpmap",2D)="bump"{} _ColorTint("【色泽】Tint",Color)=(0.6,0.3,0.6,0.3) _RimColor("【边缘颜色】RimColor",Color)=(0.26,0.19,0.16,0.0) _RimPower("【边缘颜色强度】RimPower",Range(0.5,8.0))=3.0 } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数 #pragmasurfacesurfLambertfinalcolor:setcolor //【2】输入结构 structInput { //主纹理的uv值 float2uv_MainTex; //凹凸纹理的uv值 float2uv_BumpMap; //当前坐标的视角方向 float3viewDir; }; //变量声明 sampler2D_MainTex; sampler2D_BumpMap; fixed4_ColorTint; float4_RimColor; float_RimPower; //【3】自定义颜色函数setcolor的编写 voidsetcolor(InputIN,SurfaceOutputo,inoutfixed4color) { color*=_ColorTint; } //【4】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //从主纹理获取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); } //-------------------结束CG着色器编程语言段------------------ ENDCG } //“备胎”为普通漫反射 Fallback"Diffuse" }

我们将此Shader编译后赋给材质,得到如下非常赞的效果。除了载入纹理,还有色泽,边缘颜色和强度可供调节:

依然是调些效果玩一玩:

而在场景中的实测效果为:

8.细节纹理

接着我们来看一个在屏幕上显示纹理细节的Shader:

[cpp]view plaincopyShader"浅墨Shader编程/Volume6/31.细节纹理" { //--------------------------------【属性】---------------------------------------- Properties { _MainTex("【主纹理】Texture",2D)="white"{} _Detail("【细节纹理】Detail",2D)="gray"{} } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式 #pragmasurfacesurfLambert //【2】输入结构 structInput { //主纹理的uv值 float2uv_MainTex; //细节纹理的uv值 float2uv_Detail; }; //变量声明 sampler2D_MainTex; sampler2D_Detail; //【3】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //先从主纹理获取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:

[cpp]view plaincopyShader"浅墨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("【边缘颜色】RimColor",Color)=(0.26,0.19,0.16,0.0) _RimPower("【边缘颜色强度】RimPower",Range(0.5,8.0))=3.0 } //--------------------------------【子着色器】---------------------------------- SubShader { //-----------子着色器标签---------- Tags{"RenderType"="Opaque"} //-------------------开始CG着色器编程语言段----------------- CGPROGRAM //【1】光照模式声明:使用兰伯特光照模式+自定义颜色函数 #pragmasurfacesurfLambertfinalcolor:setcolor //【2】输入结构 structInput { //主纹理的uv值 float2uv_MainTex; //凹凸纹理的uv值 float2uv_BumpMap; //细节纹理的uv值 float2uv_Detail; //当前坐标的视角方向 float3viewDir; }; //变量声明 sampler2D_MainTex; sampler2D_BumpMap; sampler2D_Detail; fixed4_ColorTint; float4_RimColor; float_RimPower; //【3】自定义颜色函数setcolor的编写 voidsetcolor(InputIN,SurfaceOutputo,inoutfixed4color) { color*=_ColorTint; } //【4】表面着色函数的编写 voidsurf(InputIN,inoutSurfaceOutputo) { //先从主纹理获取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参数获取自发光颜色 halfrim=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工程下载

之前的链接被屏蔽了,这边再分享一份:

链接:/s/1hsmbEAw 密码:7qzk

好的,本篇文章到这里就全部结束了。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。