第3章LitInput.hlsl LitInput.hlsl是ForwardLit Pass中的第一个包含文件,通过名字可以猜测出它是用来保存输入各种数据的,本章就对该文件及其包含的其他包含文件进行讲解。 建议用户在阅读本书的同时打开LitInput.hlsl文件阅读代码,该文件路径为Packages/Universal RP/Shaders/LitInput.hlsl。 3.1声明属性变量 UPR中声明纹理资源的方式与传统渲染流水线有一些不同,这也是本节需要重点讲解的内容,Shader代码如下: #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl" CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half4 _SpecColor; half4 _EmissionColor; half _Cutoff; half _Smoothness; half _Metallic; half _BumpScale; half _OcclusionStrength; CBUFFER_END TEXTURE2D(_OcclusionMap); SAMPLER(sampler_OcclusionMap); TEXTURE2D(_MetallicGlossMap); SAMPLER(sampler_MetallicGlossMap); TEXTURE2D(_SpecGlossMap); SAMPLER(sampler_SpecGlossMap); 解析: 文件在开始的位置将Core.hlsl、CommonMaterial.hlsl和SurfaceInput.hlsl这三个文件包含了进来,因为后面会用到这些文件中定义的函数和结构体。 接下来使用CBUFFER_START(UnityPerMaterial)…CBUFFER_END将声明的属性变量包含其中。CBuffer的全称为Constant Buffer(常量缓存区),该功能是在DirectX 10版本中引入的,在DirectX 9中并不存在CBuffer。 在DirectX 9中发送到GPU命令缓存区的数据大部分是着色器常量数据(Constant Data),而不同着色阶段访问的相同的数据是不能保存的,只能在同一阶段进行设置。例如,顶点着色器阶段与片段着色器阶段分别访问相同的常量但是必须设置两次。换句话说就是,常量缓存区出现之前,切换着色阶段需要重新设置常量。而常量缓冲区功能改变了这一现状,它是在GPU中单独划分出来一块用于存储常量的区域,可以绑定到不同的着色阶段,所以常量不再需要存储多次,如此一来便可以更高效地进行计算。 URP中纹理的声明方式与传统渲染流水线有很大的区别,Unity将声明纹理和定义采样器分离开了,首先使用TEXTURE2D(_textureName)指令声明纹理,然后使用SAMPLER(sampler_textureName)指令定义该纹理的采样器。如果需要用到纹理的Scale和Offset属性,还需要在CBuffer中声明一个float4类型的变量,名称为: 纹理名称_ST。 既然提到了采样器,下面再详细讲解。采样器可以定义纹理设置面板上的Wrap Mode(重复模式)与Filter Mode(过滤模式)选项,设置选项如图31所示。 图31纹理采样器的设置选项 采样器有3种定义方式: (1) SAMPLER(sampler_textureName): 这种方式表示使用textureName这个纹理在设置面板中设定的采样方式,这是最常用的定义方式。 (2) SAMPLER(filter_wrap): 使用自定义的采样器设置,自定义的采样器一定要同时包含过滤模式和重复模式的设置,例如SAMPLER(point_clamp)。 (3) SAMPLER(filter_wrapU_wrapV): 可以同时为U和V设置不同的重复模式,例如SAMPLER(linear_clampU_mirrorV)。 过滤模式可以设置的选项有: point、linear、triLinear。 重复模式可以设置的选项有: clamp、repeat、mirror、mirrorOnce。 细心的用户可能会发现,_BaseMap、_BumpMap和_EmissionMap这3个纹理变量并没有在当前文件中声明,这是因为它们已经在SurfaceInput.hlsl文件中声明了,关于该文件的内容会在第3.3节中详细讲解。 3.2纹理采样 3.2.1采样函数的宏定义 Shader代码如下: #ifdef _SPECULAR_SETUP #define SAMPLE_METALLICSPECULAR(uv) SAMPLE_TEXTURE2D(_SpecGlossMap, sampler_SpecGlossMap, uv) #else #define SAMPLE_METALLICSPECULAR(uv) SAMPLE_TEXTURE2D(_MetallicGlossMap, sampler_MetallicGlossMap, uv) #endif 解析: 这段代码定义了SAMPLE_METALLICSPECULAR(uv)在不同工作流中所进行的操作。当在材质面板中选择了Specular工作流,宏定义表示的是对_SpecGlossMap进行采样; 否则就是对_MetallicGlossMap采样。 在URP中,纹理使用SAMPLE_TEXTURE2D(_textureName, sampler, uv)宏定义进行采样,其中: (1) _textureName: 指需要采样的纹理。 (2) Sample: 3.1节中讲到的采样器。 (3) uv: 指纹理坐标。 3.2.2金属和高光采样函数 SampleMetallicSpecGloss()函数的代码如下: half4 SampleMetallicSpecGloss(float2 uv, half albedoAlpha) { half4 specGloss; #ifdef _METALLICSPECGLOSSMAP specGloss = SAMPLE_METALLICSPECULAR(uv); #ifdef _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A specGloss.a = albedoAlpha * _Smoothness; #else specGloss.a *= _Smoothness; #endif #else // _METALLICSPECGLOSSMAP #if _SPECULAR_SETUP specGloss.rgb = _SpecColor.rgb; #else specGloss.rgb = _Metallic.rrr; #endif #ifdef _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A specGloss.a = albedoAlpha * _Smoothness; #else specGloss.a = _Smoothness; #endif #endif return specGloss; } 解析: 这段代码定义了_MetallicGlossMap和_SpecGlossMap的采样函数,函数需要传入纹理坐标和Albedo纹理的a分量。函数对有无使用纹理这两种情况分别进行判断。 如果定义了_METALLICSPECGLOSSMAP,也就是材质使用了金属贴图或高光贴图,就使用第3.2.1节中讲解的SAMPLE_METALLICSPECULAR()宏定义对纹理进行采样,得到specGloss变量。 接下来继续进行判断,如果定义了_SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A,也就是材质的Source选项选择了Albedo Alpha,specGloss的a分量等于Albedo纹理的a分量乘以材质的光滑度属性,否则等于specGloss的a分量乘以光滑度参数。 如果材质没有使用金属贴图或高光贴图,下面再次进行判断,如果材质选择Specular工作流,specGloss的rgb分量等于材质的高光色属性,否则等于材质的金属度属性。接下来同样也根据材质的Source选项进行判断,判断结果与使用纹理的情况类似,这里不再赘述。 3.2.3AO采样函数 SampleOcclusion()函数的代码如下: half SampleOcclusion(float2 uv) { #ifdef _OCCLUSIONMAP // TODO: Controls things like these by exposing SHADER_QUALITY levels (low, medium, high) #if defined(SHADER_API_GLES) return SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv).g; #else half occ = SAMPLE_TEXTURE2D(_OcclusionMap, sampler_OcclusionMap, uv).g; return LerpWhiteTo(occ, _OcclusionStrength); #endif #else return 1.0; #endif } 解析: 这段代码定义了_OCCLUSIONMAP的采样函数,需要将纹理坐标传入函数。函数中对有无使用纹理这两种情况进行了判断。 如果定义了_OCCLUSIONMAP,也就是材质中使用了AO贴图,则继续进行判断,如果定义了SHADER_API_GLES,也就是目标平台是移动设备,为了性能优化,函数直接返回采样结果。否则,将采样结果在LerpWhiteTo()函数中做插值运算之后再返回,如此一来就可以通过_OcclusionStrength变量调节AO的强度。 如果材质中没有使用AO纹理,则返回数值1,表示物体完全受环境光的影响。 这里有必要单独讲解LerpWhiteTo()函数,该函数是在CommonMaterial.hlsl文件中定义的,代码如下: real LerpWhiteTo(real b, real t) { real oneMinusT = 1.0 - t; return oneMinusT + b * t; } real3 LerpWhiteTo(real3 b, real t) { real oneMinusT = 1.0 - t; return real3(oneMinusT, oneMinusT, oneMinusT) + b * t; } 函数中引入了一个新的数据类型real,这个类型是在Common.hlsl文件中定义的。函数一般使用float和half修饰数据的精度,当函数同时支持这两种精度的时候,则使用real修饰符。由于LitInput.hlsl先包含了Core.hlsl文件,并且Core.hlsl中已经包含了Common.hlsl文件,因此这个数据类型可以正常使用。 CommonMaterial.hlsl文件中定义了real和real3两个版本的LerpWhiteTo()函数,实现了重载功能,也就是说一维变量和三维变量都可以调用LerpWhiteTo()函数计算。函数名称已经非常清楚的说明了函数的作用,就是将数值1.0与第一个传入的参数进行插值计算,此时与lerp(1.0, b, t)的计算结果是一样的。 3.3SurfaceInput.hlsl 接下来原本应该讲解InitializeStandardLitSurfaceData()函数,但是由于该函数中会频繁用到了SurfaceInput.hlsl文件中定义的函数和结构体,因此不妨先来讲解SurfaceInput.hlsl文件中的代码。 3.3.1SurfaceData结构体 文件中声明了几个变量,定义了SurfaceData结构体,代码如下: #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Packing.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl" TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); TEXTURE2D(_BumpMap); SAMPLER(sampler_BumpMap); TEXTURE2D(_EmissionMap); SAMPLER(sampler_EmissionMap); // Must match Universal ShaderGraph master node struct SurfaceData { half3 albedo; half3 specular; half metallic; half smoothness; half3 normalTS; half3 emission; half occlusion; half alpha; }; 文件在开始的位置将Core.hlsl、Packing.hlsl和CommonMaterial.hlsl这三个文件包含了进来,并声明了_BaseMap、_BumpMap和_EmissionMap这三个纹理变量及其采样器,这也解释了为什么LitInput.hlsl中没有声明这三个纹理变量了。 接下来定义了SurfaceData结构体,可以看出,这些变量与传统渲染流水线中表面着色器(Surface Shader)的输出结构体中所定义的变量基本上是一样的,已经包含了PBR光照模型所需要的所有表面属性。 3.3.2透明度函数 Alpha()函数的代码如下: half Alpha(half albedoAlpha, half4 color, half cutoff) { #if !defined(_SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A) && !defined(_GLOSSINESS_FROM_BASE_ALPHA) half alpha = albedoAlpha * color.a; #else half alpha = color.a; #endif #if defined(_ALPHATEST_ON) clip(alpha - cutoff); #endif return alpha; } 解析: 本函数的主要目的是计算透明属性和透明裁切功能,函数需要传入Albedo纹理的alpha通道、基础色和裁切值3个参数。函数中先后进行了2个判断: _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A和_GLOSSINESS_FROM_BASE_ALPHA都是指材质面板上Source的Albedo Alpha选项,如果没有使用这一选项,alpha变量等于Albedo纹理的alpha通道乘上基础色的a分量,否则等于基础色的a分量。 接下来进行第二个判断,如果定义了_ALPHATEST_ON,也就是材质开启了Alpha Clipping开关,则调用clip()函数进行透明裁切。函数最后返回alpha变量。 3.3.3Albedo纹理采样函数 SampleAlbedoAlpha()函数的代码如下: half4 SampleAlbedoAlpha(float2 uv, TEXTURE2D_PARAM(albedoAlphaMap, sampler_albedoAlphaMap)) { return SAMPLE_TEXTURE2D(albedoAlphaMap, sampler_albedoAlphaMap, uv); } 解析: 函数实现了采样Albedo纹理的功能,在传入的参数中,有一个TEXTURE2D_PARAM()宏定义,该宏定义在不同图形库的API文件中都有定义,本书以D3D11为例进行讲解,文件路径为Packages/Core RP Library/ShaderLibrary/API/D3D11.hlsl,其他图形库的API文件也在该路径中。宏定义代码如下: #define TEXTURE2D_PARAM(textureName, samplerName) TEXTURE2D(textureName), SAMPLER(samplerName) 从代码中可以看出,该宏定义最终转换为一个纹理变量和一个采样器,因此SampleAlbedoAlpha()函数除了需要传入纹理坐标之外,其实还需要传入一个纹理变量及其采样器。 函数中使用SAMPLE_TEXTURE2D()宏定义对传入的纹理进行采样,并返回采样结果。 3.3.4法线贴图采样函数 SampleNormal()函数的代码如下: half3 SampleNormal(float2 uv, TEXTURE2D_PARAM(bumpMap, sampler_bumpMap), half scale = 1.0h) { #ifdef _NORMALMAP half4 n = SAMPLE_TEXTURE2D(bumpMap, sampler_bumpMap, uv); #if BUMP_SCALE_NOT_SUPPORTED return UnpackNormal(n); #else return UnpackNormalScale(n, scale); #endif #else return half3(0.0h, 0.0h, 1.0h); #endif } 解析: 函数实现了采样法线贴图的功能,传入的参数比第3.3.3节中讲解的SampleAlbedoAlpha()函数多了一个名称为scale的一维变量用于控制法线强度,并且该参数在声明的同时就已经被初始化为1。 在函数中先判断材质是否使用了法线贴图,如果是,则法线贴图采样得到变量n。接下来继续进行判断,当定义BUMP_SCALE_NOT_SUPPORTED时,也就是当前设备不支持调节法线强度,则调用UnpackNormal()函数将采样之后的法线贴图解包并返回; 否则,调用UnpackNormalScale()函数,并返回修改法线强度之后的结果。 如果材质中没有使用法线纹理,函数直接返回三维向量(0.0h, 0.0h, 1.0h),也就是切线空间法线向量的默认值。 函数中使用的UnpackNormal()函数和UnpackNormalScale()函数都是在Packing.hlsl文件中定义的,代码如下: real3 UnpackNormal(real4 packedNormal) { #if defined(UNITY_NO_DXT5nm) return UnpackNormalRGBNoScale(packedNormal); #else // Compiler will optimize the scale away return UnpackNormalmapRGorAG(packedNormal, 1.0); #endif } real3 UnpackNormalScale(real4 packedNormal, real bumpScale) { #if defined(UNITY_NO_DXT5nm) return UnpackNormalRGB(packedNormal, bumpScale); #else return UnpackNormalmapRGorAG(packedNormal, bumpScale); #endif } 这两个函数的结构非常相似,函数中通过判断结果分别调用两个不同的函数。当定义UNITY_NO_DXT5nm时,也就是说当前平台不使用 DXT5nm 压缩法线贴图(移动平台),UnpackNormal()函数和UnpackNormalScale()函数内部会分别调用UnpackNormalRGBNoScale()函数和UnpackNormalRGB()函数; 否则,这两个函数内部都调用UnpackNormalmapRGorAG()函数,只不过UnpackNormal()函数中的法线强度始终保持为1。 3.3.5自发光贴图采样函数 SampleEmission()函数的代码如下: half3 SampleEmission(float2 uv, half3 emissionColor, TEXTURE2D_PARAM(emissionMap, sampler_emissionMap)) { #ifndef _EMISSION return 0; #else return SAMPLE_TEXTURE2D(emissionMap, sampler_emissionMap, uv).rgb * emissionColor; #endif } 解析: 函数实现了对自发光贴图采样的功能,传入的参数比SampleAlbedoAlpha()函数多一个名称为emissionColor的三维变量,用于控制自发光颜色。 函数中只做了一个判断,当没有定义_EMISSION时,也就是材质没有开启自发光开关,函数返回0; 否则,函数中将自发光贴图采样之后与传入的自发光颜色相乘,最后返回乘积。 3.4表面数据初始化函数 InitializeStandardLitSurfaceData()函数的代码如下: inline void InitializeStandardLitSurfaceData(float2 uv, out SurfaceData outSurfaceData) { half4 albedoAlpha = SampleAlbedoAlpha(uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap)); outSurfaceData.alpha = Alpha(albedoAlpha.a, _BaseColor, _Cutoff); half4 specGloss = SampleMetallicSpecGloss(uv, albedoAlpha.a); outSurfaceData.albedo = albedoAlpha.rgb * _BaseColor.rgb; #if _SPECULAR_SETUP outSurfaceData.metallic = 1.0h; outSurfaceData.specular = specGloss.rgb; #else outSurfaceData.metallic = specGloss.r; outSurfaceData.specular = half3(0.0h, 0.0h, 0.0h); #endif outSurfaceData.smoothness = specGloss.a; outSurfaceData.normalTS = SampleNormal(uv, TEXTURE2D_ARGS(_BumpMap, sampler_BumpMap), _BumpScale); outSurfaceData.occlusion = SampleOcclusion(uv); outSurfaceData.emission = SampleEmission(uv, _EmissionColor.rgb, TEXTURE2D_ARGS(_EmissionMap, sampler_EmissionMap)); } 解析: 该函数实现的功能是初始化SurfaceData结构体中的变量并将其输出,函数需要传入纹理坐标,供内部调用的其他函数使用,而这些函数已经在本文件和SurfaceInput.hlsl文件中定义过了,下面开始详细讲解。 首先调用第3.3.3节中讲解的SampleAlbedoAlpha()函数对_BaseMap纹理采样得到albedoAlpha变量,函数中套用了TEXTURE2D_ARGS()宏定义,该宏定义也是在各个平台的API文件中定义的,以D3D11.hlsl文件为例,代码如下: #define TEXTURE2D_ARGS(textureName, samplerName) textureName, samplerName 可以看出,宏定义只是将纹理和采样器这两个参数合并在一起,并没有做其他操作。 接下来调用第3.3.2节讲解的Alpha()函数,并将结果保存到SurfaceData结构体的Alpha变量中。albedoAlpha与_BaseColor变量相乘的结果保存到结构体的albedo变量中。 然后调用第3.2.2节讲解的SampleMetallicSpecGloss()函数对金属贴图和高光贴图采样得到specGloss变量,然后进行如下判断。 如果材质选择了高光工作流,结构体中的metallic变量的值等于1,specular变量等于specGloss的rgb分量; 否则,结构体的metallic变量的值等于specGloss的r分量,specular变量为黑色。由于SampleMetallicSpecGloss()函数中已经对于光滑度相关的所有情况都进行了判断,并保存到a分量中,因此直接将specGloss的a分量保存到SurfaceData结构体的smoothness变量中。 后面的normalTS、occlusion和emission都是直接调用了之前定义好的函数,然后传入需要的参数中,这里就不再赘述了。 3.5函数和宏定义总结 到现在为止,LitInput.hlsl文件的代码全部讲解完,本章涉及的常用宏定义和函数在表31中进行了汇总。 表31第3章涉及的常用函数和宏定义 宏或函数说明 SAMPLE_TEXTURE2D(textureName, samplerName, coord2) 纹理采样的宏定义,需要传入纹理、采样器和纹理坐标 realLerpWhiteTo(real b, real t) 在1与b之间进行线性插值,一维向量函数 real3LerpWhiteTo(real3 b, real t) 在1与b之间进行线性插值,三维向量函数 real3UnpackNormal(real4 packedNormal) 将采样之后的法线向量解包 real3UnpackNormalScale(real4 packedNormal, real bumpScale) 将采样之后的法线向量解包,并通过bumpScale变量控制法相强度