您需要 登录 才可以下载或查看,没有账号?注册
x
上一篇地址
还是写在前面我自己的废话,昨天只看了一节,我并没有HLSL或者CG语言的基础,大量的基础对于我来说有些吃力,
但是怎么说呢,路是一步一步走的,我的本子上记满了各种英文以及翻译,
其实我上学时候英语都很少及格,
我只能记住,路既然是自己选的,无论前方的路是如何都要披荆斩棘,遇山开山,遇河渡河,,与君共勉。。。。。
这节的基础单词更多了,还是分2章发,
下面开始是正文
【浅墨Unity3D Shader编程】之六 暗黑城堡篇: 表面着色器(Surface Shader)的写法(一)
本文主要讲解了Unity中SurfaceShader的具体写法,以及几个常用的CG函数的用法。
在这里先说明一下,表面着色器将分为两次讲解,本文介绍表面着色器的基本概念和一些写法,用内置的兰伯特光照模式来进行Surface Shader的书写,而下次将介绍Surface Shader+自定义的光照模式的写法。
PS:最近几天,在完美世界、腾讯互娱工作多年的几个朋友们问了浅墨一些表面着色器相关的Shader写法,浅墨当时回答他们的时候自己也是似懂非懂。通过这篇文章的书写,现在算是对这方面知识有了进一步的理解。所以说嘛,写作是总结自己所学的一种很好的方式~
OK,言归正传,依然是先来看看本文配套的游戏场景截图。
运行游戏,音乐响起,首先是一个欧式风格的集市映入眼帘:
雨淅沥沥、天空、晚霞、海平面:
天空中,一群飞鸟飞过:
峡谷:
暗黑城堡:
在小集市中逛荡:
城堡概况图:
OK,图先就上这么多。文章末尾有更多的运行截图,并提供了源工程的下载。可运行的exe下载在这里:
【可运行的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)的标准输出结构是这样的:
[cpp] view plain copy
- struct SurfaceOutput
- {
- half3 Albedo; //反射率,也就是纹理颜色值(r,g,b)
- half3 Normal; //法线,法向量(x, y, z)
- half3 Emission; //自发光颜色值(r, g,b)
- half Specular; //镜面反射度
- half Gloss; //光泽度
- half Alpha; //透明度
- };
而这个结构体的用法,其实就是对这些需要用到的成员变量在surf函数中赋一下值,比如说这样:
[cpp] view plain copy
- //表面着色函数的编写
- 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)。 比如说这样:
[cpp] view plain copy
- #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,就可以参与到光线运算中完成接下来的渲染工作了。
一个调用示例:
[cpp] view plain copy
- o.Normal = UnpackNormal (tex2D (_BumpMap,IN.uv_BumpMap));
4.2saturate( )函数
saturate的字面解释是浸湿,浸透。其作用其实也就是将取值转化为[0,1]之内的一个值。
其可选的原型如下:
[cpp] view plain copy
- 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的值。
其代码实现大致如下:
[cpp] view plain copy
- float saturate(float x)
- {
- return max(0,min(1, x));
- }
一个调用示例:
[cpp] view plain copy
- half rim = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));
4.3 dot( )函数
dot函数顾名思义,是高等数学中的点积操作,用于返回两个向量的标量积。
可选原型如下:
[cpp] view plain copy
- 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);
其代码实现大致是这样的:
[cpp] view plain copy
- float dot(float4 a, float4 b)
- {
- return a.x*b.x +a.y*b.y + a.z*b.z + a.w*b.w;
- }
一个调用示例:
[cpp] view plain copy
- float answer= dot (normalize(IN.viewDir),o.Normal);
4.4 tex2D( )函数
让我们看一看CG中用得比较多的用于2D纹理采样的tex2D函数的用法。其备选的原型也是非常之多:
[cpp] view plain copy
- 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函数的示例:
[cpp] view plain copy
- //【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);
- }
下一部分是实例
|