目录
· 1 光照
· 1.1 受光着色器
· 1.2 法线向量
· 1.3 法线插值
· 1.4 表面属性
· 1.5 计算光照
· 2 灯光
· 2.1 灯光结构
· 2.2 光照函数
· 2.3 发送灯光数据给GPU
· 2.4 可见光
· 2.5 多方向光
· 2.6 Shader 循环
· 2.7 Shader 目标级别
· 3 BRDF
· 3.1 入射光
· 3.2 出射光
· 3.3 表面属性
· 3.4 BRDF 属性
· 3.5 反射率
· 3.6 镜面颜色
· 3.7 粗糙度
· 3.8 视角方向
· 3.9 镜面强度
· 3.10 Mesh Ball
· 4 透明度
· 4.1 预乘 Alpha
· 4.2 预乘切换
· 5 Shader GUI
· 5.1 自定义 ShaderGUI
· 5.2 设置属性和关键字
· 5.3 预设按钮
· 5.4 UnLit预设
· 5.5 不透明
本文重点: 1、使用法线向量计算光照 2、支持4个方向光 3、应用BRDF 4、制作受光的透明材质 5、使用预设创建自定义着色GUI
这是自定义可编程渲染管线系列的第三篇,让着色器支持多个方向光。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。
教程是用Unity 2019.2.12f1 完成的。
(各式各样受到4个光照的球体)
1 光照
如果要创建一个更加真实的场景,我们就需要模拟光和物体表面的交互。这比我们之前制作的不受光的着色器要复杂的多。
1.1 受光着色器
复制UnlitPass HLSL文件,并将其重命名为LitPass。调整包含保护的定义以及顶点和片段函数名称以匹配响应的修改,并在稍后添加光照计算。
同时复制“Unlit ”着色器,并将其重命名为“Lit”。更改其菜单名称,包含的文件及其使用的功能。将默认颜色更改为灰色,因为在光线充足的场景中全白色的表面可能显得过于明亮。默认情况下,通用管道也使用灰色。
我们将使用一种自定义的光照方法,通过将着色器的照明模式设置为CustomLit来进行说明。向Pass里添加一个Tag块,其中包含“ LightMode” =“ CustomLit”。
要渲染使用此pass的对象,必须将其包含在CameraRenderer中。首先为其添加一个着色器标签标识符。
然后将其添加到要在DrawVisibleGeometry中渲染的过程中,就像在DrawUnsupportedShaders中所做的那样。
现在,创建一个新的不透明材质,到现在为止,它产生的结果与unlit 材质还没有区别。
(默认的不透明材质)
1.2 法线向量
物体的光照程度取决于多个因素,比如灯光和表面之间的相对角度。要知道表面的方向,就需要访问表面的法线,该法线是一个单位长度的向量,指向远离它的方向。该向量是顶点数据的一部分,就像位置在对象空间中的定义一样。因此,将其添加到LitPass中的“Attributes”中。
照明是按每个片段计算的,因此我们也必须将法向矢量添加到Varyings中。因为是在世界空间中执行计算,因此将其命名为normalWS。
我们可以使用SpaceTransforms中的TransformObjectToWorldNormal在LitPassVertex中将法线转换到世界空间。
TransformObjectToWorldNormal如何工作? 如果你去查看代码,你会看到它使用两种方法之一,基于是否定义了UNITY_ASSUME_UNIFORM_SCALING。 定义UNITY_ASSUME_UNIFORM_SCALING时,它将调用TransformObjectToWorldDir,该函数与TransformObjectToWorld相同,但它忽略平移部分,因为我们正在处理方向矢量而不是位置。但是矢量也会得到均匀缩放,因此应在之后进行归一化。 在另一种情况下,则不假定均匀缩放。这会更加复杂,因为当对象因不均匀缩放而变形时,法向矢量必须反向缩放以匹配新的表面方向。这需要与转置的UNITY_MATRIX_I_M矩阵相乘,再进行归一化。 使用UNITY_ASSUME_UNIFORM_SCALING是一个轻微的优化,可以通过自己定义来启用。但是,在使用GPU实例化时,它的作用更大,因为必须将一组UNITY_MATRIX_I_M矩阵发送到GPU。在不需要时避免多余的计算是值得的。你可以通过向着色器添加#pragma instancing_options假定uniform scaleing指令来启用它,但仅在以专门以uniform scale为渲染对象时才这么做。
(不正确和正确的法线转换)
为了验证是否在LitPassFragment中获得正确的法线向量,我们可以将其用作颜色看看。
(世界空间的法线向量)
负值无法显示,因此将其限制为零。
1.3 法线插值
尽管法线向量在顶点程序中为单位长,但跨三角形的线性插值会影响其长度。我们可以通过渲染一个和向量长度之间的差(放大十倍以使其更明显)来可视化该错误。
(放大了插值误差)
可以通过标准化LitPassFragment中的法线向量来平滑插值,减少失真。仅查看法线矢量时,这种差异并不十分明显,但用于照明时会更明显。
(插值后的法线向量)
1.4 表面属性
在着色器的照明是模拟光击中一个表面之后的相互作用,这意味着我们需要追踪表面的属性。现在我们有一个法向量和一个基色。可以将后者(基色)分为两部分:RGB颜色和alpha值。后面会在几个地方使用这些数据,所让先定义一个方便的Surface结构来包含所有相关的数据。将它放在ShaderLibrary文件夹中的一个单独的SurfaceHLSL文件中。
应该把normal定义为normalWS吗? 可以,但是表面不在乎法线的定义空间。它可以在任何适当的3D空间中执行光照计算。因此,我们保留了定义的空间不填。填充数据时,我们仅需在各处使用相同的空间即可。现在使用的是世界空间,但是稍后我们可以切换到另一个空间,并且一切任然正常进行。
在Common之后,将其包含在LitPass中。这样,我们可以使LitPass简短。从现在开始,我们将专用代码放入其自己的HLSL文件中,以使查找相关功能更加容易。
在LitPassFragment中定义一个surface变量并填充它。然后,最终结果将成为表面的颜色和Alpha。
这种写法会有性能问题吗? 没关系,因为着色器编译器会生成高度优化的程序,从而完全重写我们的代码。该结构纯粹是为了我们阅读和理解的方便。你可以通过着色器检视面板中的“Compile and show code ”按钮来检查编译器的工作。
1.5 计算光照
为了计算实际的照明,我们将创建一个具有Surface参数的GetLighting函数。最初使它返回表面法线的Y分量。由于这是照明功能,因此我们将其放在单独的Lighting HLSL文件中。
在LitPass里包含它,然后包含Surface,因为计算光照需要依赖它。
为什么不把Surface直接写在Light里? 可以这么做,但最终我们会有多少个文件取决于它们依赖多少个文件。之所以我选择把它们分来是因为分离的文件比糅合在一起的更加清晰。这也能让我们更加轻松的通过替换带有相同函数的文件来修改着色器的功能修改。
现在,我们可以在LitPassFragment中获取光照并将其用于片段的RGB部分了。
(来自上方的漫反射光照)
在球体的每一个点上,结果都是是表面法线的Y分量,因此它在球体的顶部为1,在侧面为零。再下面,结果为负,在底部达到-1,但我们看不到负值。它与法线向量和向上矢量之间的角度的余弦匹配。忽略负的部分,这在视觉上与指向下方的方向光的漫反射光相匹配。最终的结果就是GetLighting函数中看到的surface color的影响因素,可以将其称之为表面反照率(albedo)。
(应用了albedo)
2 灯光
为了计算合适的光照,我们还需要了解灯光的属性。在本教程中,我们将仅限于定向光。定向光源代表的是非常远的光源,以至于其位置无关紧要,仅取决于其方向。这是一个简化,但是足以模拟太阳光以及其他或多或少是单向入射光的情况。
2.1 灯光结构
我们会用一个结构来存储灯光数据。但现在,我们只需要满足颜色和方向的要求。将其放在单独的Light HLSL文件中。再定义一个GetDirectionalLight函数,该函数返回已配置的定向光。一开始我们先使用白色和向上向量,来匹配我们当前正在使用的光照数据。请注意,光线的方向定义为光线的来源方向,而不是光线照射方向方向。 在Lighting前将文件包含在LitPass中。
2.2 光照函数
在Lighting 中添加GetIncomingLight函数,以计算给定的表面和光的入射数量。对于任意的方向的光,我们都需要用表面的法线和方向进行点乘(可以使用dot函数)。把结果和灯光的颜色进行混合。
dot会产生什么? 两个向量之间的点积在几何上定义为A⋅B = | | A | | | | B | | cosθA⋅B= || A || || B || cosθ。这意味着它是矢量之间的角度的余弦值乘以它们的长度。因此,在两个单位长度矢量的情况下,A⋅B = cosθA⋅B=cosθ。代数定义为
这意味着你可以通过将所有组件对相乘并求和来计算它。 float dotProduct = a.x b.x + a.y b.y + a.z * b.z; 在视觉上,此操作将一个向量直接向下投影到另一个向量,就像在其上投射阴影一样。这样,你最终得到一个直角三角形,其底边的长度是点积的结果。而且,如果两个向量均为单位长度,则为它们角度的余弦值。
但这仅在表面朝光源时才是正确的。当点积为负时,我们需要将其限制为零,这可以通过saturate函数来实现。
saturate干了什么? 它会限制值,使其介于零到一之间。我们只需要指定一个最小值,因为点积绝不会大于1,但是饱和度是一种常见的着色器操作,因此通常是自由操作修饰符。
添加另一个GetLighting函数,该函数返回表面和灯光的最终照明。现在,它是入射光乘以表面颜色。在其他函数上面定义它。
最后,调整仅具有表面参数的GetLighting函数,以便使用GetDirectionalLight提供灯光数据来调用另一个参数。
2.3 发送灯光数据给GPU
与其始终使用上面的白光,不如使用当前场景的光。默认场景带有代表太阳的定向光,颜色略微偏黄(FFF4D6十六进制),并且绕X轴旋转50°,绕Y轴旋转-30°。如果当前场景不存在的话,可以自己手动创建一个。
为了使光源的数据可在着色器中访问,我们需要为其创建uniform 的值,就像着色器属性一样。在这种情况下,我们定义两个float3向量:_DirectionalLightColor和_DirectionalLightDirection。将它们放在Light顶部定义的_CustomLight缓冲区中。
使用这些值代替GetDirectionalLight中的常量
现在,我们的RP需要将灯光数据发送给GPU。可以为它创建一个新的Lighting类。它的工作方式与CameraRenderer相似,但只用于灯光。给它提供一个带有上下文参数的公共Setup方法,在该方法中它调用一个单独的SetupDirectionalLight方法。虽然不是必须的,但我们还是为它提供一个专用的命令缓冲区,该缓冲区在完成后执行,可以很方便地进行调试。另一种方法是添加一个缓冲区参数。
追踪两个着色器属性的标识符。
可以通过RenderSettings.sun来访问场景的主光源。默认情况下,它会成为最重要的方向光源,还可以通过“Window / Rendering / Lighting Settings”显式配置它。使用CommandBuffer.SetGlobalVector将灯光数据发送到GPU。颜色是灯光在线性空间中的颜色,而方向是灯光变换的正向向量取反。
SetGlobalVector不是要求Vector4类型吗? 是的,即使我们定义的向量更少,发送到GPU的向量也始终具有四个分量。额外的分量在着色器中被隐式屏蔽。同样,从Vector3到Vector4会有一个隐式转换。
灯光的颜色属性是其配置的颜色,但是灯光也具有单独的强度因子。最终颜色都相乘。
为CameraRenderer提供一个Lighting实例,并在绘制可见的几何图形之前使用它来设置灯光。
(接受“太阳”光)
2.4 可见光
当剔除时,Unity也会找出哪些光线会影响相机可见的空间。我们可以依靠这些信息而不是全局的光参数。为此,Lighting需要访问剔除结果,为Setup添加一个参数,并将其存储在字段中以方便使用。然后,我们可以支持多个光源,因此请使用新的SetupLights方法替换对SetupDirectionalLight的调用。
在CameraRenderer.Render中调用Setup时,将剔除结果添加为参数。
现在Lighting.SetupLights可以通过剔除结果的visibleLights属性检索所需的数据。它以具有VisibleLight元素类型的Unity.Collections.NativeArray形式提供。
2.5 多方向光
使用可见光数据可以支持多个定向光,但是我们必须将所有这些光的数据发送到GPU。因此,我们将使用两个Vector4数组,而不是两个Vector,并为光计数加上一个整数。我们还将定义最大数量的定向光,可以使用它来初始化两个数组字段以缓冲数据。暂时将最大值设置为四个,这对于大多数场景来说应该足够了。
buffer为什么为什么不使用结构体? 这是可以的的,但我不用,因为着色器对结构体Buffer的支持还不够好。要么根本不支持它们,要么仅在片段程序中支持它们,要么它们的性能比常规数组差。但好消息是,如何在CPU和GPU之间传递数据的细节仅在几个地方很重要,因此很容易修改。那也是使用Light结构的好处。
将索引和VisibleLight参数添加到SetupDirectionalLight。用提供的索引设置颜色和方向元素。在这种情况下,最终颜色是通过VisibleLight.finalColor属性提供的。可以通过VisibleLight.localToWorldMatrix属性找到前向矢量。它是矩阵的第三列,必须再次取反。
最终颜色已经应用了光源的强度,但是默认情况下Unity不会将其转换为线性空间。我们必须将GraphicsSettings.lightsUseLinearIntensity设置为true,这可以在CustomRenderPipeline的构造函数中执行一次。
接下来,遍历Lighting.SetupLights中的所有可见光,并为每个元素调用SetupDirectionalLight。然后在缓冲区上调用SetGlobalInt和SetGlobalVectorArray以将数据发送到GPU。
因为我们最多只支持四个方向灯,因此当达到最大值时,应该中止循环。让我们跟踪与循环的迭代器分开的方向光索引。
因为我们仅支持定向光源,所以我们应该忽略其他光源类型。我们可以通过检查可见光的lightType属性是否等于LightType.Directional来实现。
这已经起作用了,但是VisibleLight结构相当大。理想情况下,我们仅从本机数组中检索一次,并且也不要将其作为常规参数传递给SetupDirectionalLight,因为那样会复制它。我们可以使用Unity用于ScriptableRenderContext.DrawRenderers方法的相同技巧,该方法通过引用传递参数。
这要求我们也将参数定义为引用。
2.6 Shader 循环
在Light中调整_CustomLight缓冲区,使其与我们的新数据格式匹配。这时候,我们将显式使用float4作为数组类型。着色器中的数组大小固定,无法调整大小。确保使用与Lighting中定义的最大值相同的最大值。
添加一个函数以获取定向光计数并调整GetDirectionalLight,以便它检索特定光索引的数据。
然后调整表面的GetLight,使其使用for循环来累积所有定向光的贡献度。
现在,我们的着色器最多支持四个定向光。通常只需要一个定向光来表示太阳或月球,但是某些行星上可能有多个太阳。定向灯也可以用于近似多个大型的照明设备,例如大型体育场的照明设备。
如果你的游戏始终只有一个定向光,那么就可以去掉循环,或者制作多一个着色器变体。但是对于本教程,为了保持简单,会坚持使用一个通用循环。最好的性能总是通过剔除不需要的内容来实现的,并且它不一定会带来很大的不同。
2.7 Shader 目标级别
对于着色器来说,可变长度的循环曾经是一个问题,但是现代GPU可以毫无问题地处理它们,尤其是在绘制的所有片段调用以相同方式迭代相同数据时。但是,默认情况下,OpenGL ES 2.0和WebGL 1.0图形API不能处理此类循环。我们可以通过合并一个硬编码的最大值(例如,使GetDirectionalLight返回min(_DirectionalLightCount,MAX_DIRECTIONAL_LIGHT_COUNT))来使其工作。这样就可以展开循环,将其变成一系列条件代码块。不幸的是,这会让生成的着色器代码一团糟,性能下降得很快。在非常老式的硬件上,所有代码块都将始终执行,它们的贡献可通过条件分配来控制。尽管我们可以进行这项工作,但它会使代码更加复杂,因为我们还必须进行其他调整。因此,为了简化起见,我选择忽略这些限制并在构建中关闭WebGL 1.0和OpenGL ES 2.0支持。他们同样也不支持线性空间。我们还可以通过#pragma target 3.5指令将着色器传递的目标级别提高到3.5,从而避免为它们编译OpenGL ES 2.0着色器变体。为了使效果保持一致,我们为两个着色器执行此操作。
3 BRDF
目前,我们使用的照明模型非常简单,仅适用于完全散射的表面。通过应用双向反射率分布函数BRDF,我们可以实现更加多样化和逼真的照明。我们将使用与Universal RP相同的模型,该模型以某种真实感来换取性能。
3.1 入射光
当光束正面撞击表面片段时,其所有能量都会影响片段。为简单起见,我们假设光束的宽度与片段的宽度匹配。这是光方向L 和表面法线N 对齐的情况,因此N⋅L = 1。当它们不对齐时,至少有一部分光束会错过表面片段,因此较少的能量会影响片段。影响片段的能量部分为N⋅L。负结果表示该表面的方向远离光线,因此它不会受到光线的影响。
(入射光线模型)
3.2 出射光
人的眼睛看不到直接到达表面的光。我们仅看到从表面反弹并到达相机或眼睛的部分。如果表面是一个完美的平面镜,则光线会反射出去,出射角等于入射角。如果相机与之对齐,我们将只能看到该灯光。这称为镜面反射。这是光交互的简化,但足以满足我们的目的。
(完美的镜面反射)
但是,如果表面不是完全平坦,则光线会散射,因为片段实际上是由许多具有不同方向的较小片段组成。这会将光束分成较小的光束,并朝不同的方向传播,从而有效地使镜面反射模糊。即使不与完美的反射方向对齐,我们最终还是会看到一些散射光。
(散乱的镜面反射)
除此之外,光还可以穿透表面,反弹并以不同的角度出射,以及其他我们不需要考虑的事物。极端地讲,我们最终得到了一个完美扩散的表面,该表面在所有可能的方向上均匀地散射光。这就是我们当前在着色器中计算的灯光。
无论照相机在哪里,从表面接收到的散射光量都是相同的。但这意味着我们观察到的光能远小于到达表面片段的光能。这表明我们应该按一定比例缩放入射光。但是,由于该因子始终相同,我们可以将其烘焙到灯光的颜色和强度中。因此,我们使用的最终光色代表从正面照亮的完美白色漫射表面片段反射时观察到的光量。这只是实际发出的光总量的一小部分。还有其他配置光源的方法,例如通过指定lumen 或lux,可以更轻松地配置真实的光源,但是这里将继续使用当前的方法。
3.3 表面属性
表面可以是完美的漫反射,完美的镜子或两者之间的任何物体。我们可以通过多种方式来控制它。这里使用metallic 工作流,这需要我们向Lit着色器添加两个表面属性。 第一个属性是告知表面是金属的还是非金属的,也称为电介质。因为一个表面可以包含这两者的混合,所以我们将为其添加一个范围为0~1的滑块,其中1表示它是完全金属的。默认为全绝缘。
第二个属性控制表面的光滑程度。为此,我们还将使用范围为0~1的滑块,其中0完全粗糙,而1完全光滑。我们将使用0.5作为默认值。
(金属和光滑的滑动条)
将属性添加到UnityPerMaterial缓冲区。
以及Surface结构。
将它们复制到LitPassFragment中的表面。
以及PerObjectMaterialProperties中。
3.4 BRDF 属性
我们将使用表面属性来计算BRDF方程。它告诉我们最终看到多少光从表面反射,这是漫反射和镜面反射的组合。我们需要将表面颜色分为漫反射和镜面反射部分,还需要知道表面的粗糙度。让我们在放在单独的BRDF HLSL文件中的BRDF结构中跟踪这三个值。
添加一个函数以获取给定表面的BRDF数据。从完美的漫反射表面开始,因此漫反射部分等于表面颜色,而镜面反射为黑色,粗糙度为1
在Light 之后和照明Lighting包括BRDF。
在两个GetLighting函数中都添加一个BRDF参数,然后将入射光与漫反射部分而不是整个表面颜色相乘。
最后,在LitPassFragment中获取BRDF数据,并将其传递给GetLighting。
3.5 反射率
不同的表面,反射的方式不同,但通常金属会通过镜面反射反射所有光,并且漫反射为零。因此,我们将声明反射率等于金属表面属性。被反射的光不会扩散,因此我们应将扩散色的缩放比例减去GetBRDF中的反射率一倍。
(白色球体,metallic分别为0,0.25,0.5,0.75,1)
实际上,一些光还会从电介质表面反射回来,从而使其具有亮点。非金属的反射率有所不同,但平均约为0.04。让我们将其定义为最小反射率,并添加一个OneMinusReflectivity函数,该函数将范围从0~1调整为0~0.96。此范围调整与Universal RP的方法匹配。
在GetBRDF中使用该函数可以强制执行最小值。仅渲染漫反射时,这种差异几乎不会引起注意,但是当我们添加镜面反射时,差异将非常重要。没有它,非金属将不会获得镜面反射高光。
3.6 镜面颜色
以一种方式反射的光,不能全部以另一种方式反射。这称为能量转换,意味着出射光的量不能超过入射光的量。这表明镜面反射颜色应等于表面颜色减去漫反射颜色。
这忽略了金属会影响镜面反射的颜色而非金属不会影响镜面反射的颜色这一事实。介电表面的镜面颜色应为白色,这可以通过使用金属属性在最小反射率和表面颜色之间进行插值来实现。
3.7 粗糙度
粗糙度与平滑度相反,因此我们只需减去一个平滑度即可。核心RP库具有一个执行此功能的函数,名为PerceptualSmoothnessToPerceptualRoughness。我们使用此功能,以明确将平滑度和粗糙度定义为可感知的。可以通过PerceptualRoughnessToRoughness函数将其转换为实际粗糙度值,该函数将感知值平方。这与迪士尼照明模型匹配。这样做是因为在编辑资料时调整感知版本更加直观。
这些功能在Core RP Libary的CommonMaterial HLSL文件中定义。在包含核心的Common之后,将其包含在我们的Common文件中。
3.8 视角方向
为了确定相机与完美反射方向对齐的程度,我们需要知道相机的位置。Unity通过float3 _WorldSpaceCameraPos使这些数据可用,因此将其添加到UnityInput。
为了获得视角方向(从表面到相机的方向),在LitPassFragment中,我们需要将世界空间表面位置添加到Varyings中。
我们将视角方向视为表面数据的一部分,因此将其添加到Surface。
在LitPassFragment中分配它。它等于相机位置减去片段位置(归一化)。
3.9 镜面强度
我们观察到的镜面反射的强度取决于我们的观察方向与完美反射方向的匹配程度。使用与Universal RP相同的公式,它是Minimalist CookTorrance BRDF的一种变体。该公式包含几个正方形,因此让我们首先向Common添加一个便捷的Square函数。
然后,以表面,BRDF数据和光照为参数,向BRDF添加SpecularStrength函数。它应该计算出
,其中r 是粗糙度,所有点积都应应用饱和。此外
,N是表面法线,L是光的方向,H = L + V归一化,这是光和视角方向之间的中途向量。使用SafeNormalize函数对矢量进行归一化,以防在矢量相对的情况下被零除。最后,n = 4 r+2 ,是一个归一化项。
接下来,添加DirectBRDF,返回通过直接照明获得的颜色(给定表面,BRDF和灯光)。结果是由镜面反射强度调制的镜面反射颜色加上漫射颜色。
然后,GetLighting必须将入射光乘以该函数的结果。
(光滑程度从上到下0,0.25,0.5,0.75,0.95)
现在,我们得到镜面反射,这在我们的表面上添加了高光。对于完美的粗糙表面,高光模仿了漫反射。较光滑的表面可获得更集中的亮点。完美光滑的表面会得到无限的高光,但我们看不到的。需要一些散射才能使其可见。
由于能量转换,高光会在光滑的表面上变得非常明亮,因为到达表面片段的大部分光线都被聚焦了。因此,我们最终看到的光要比由于可见高光的漫反射所导致的光要多得多。你可以通过大量缩小最终渲染的颜色来验证这一点。
(最终颜色除以100)
你还可以通过使用白色以外的基本颜色来验证金属是否会影响镜面反射的颜色而非金属不会影响镜面反射的颜色。
(base color 为blue)
现在,我们可以使用功能齐全的直接光照了,但目前结果还是太暗了(尤其是对于金属而言),因为我们还没有支持环境反射。此时,Unity的黑色环境将比默认的天空盒更为逼真,但这会使我们的对象难以看清。也可以通过添加更多的灯光。
(4盏灯)
3.10 Mesh Ball
我们也为MeshBall添加对各种金属和平滑度属性的支持吧。这需要添加两个浮点数组。
让我们将25%的实例金属化,并在Awake中将平滑度从0.05更改为0.95。
然后让MeshBall使用lit材质吧。
(lit材质下的MeshBall)
4 透明度
这里需要再次考虑透明度。对象仍会根据其Alpha值淡入,但是现在是反射光就消失了。这对于漫反射是有意义的,因为只有一部分光被反射,而其余的光则穿过了表面。
(融合的球体)
但是,镜面反射也同样会消失。如果是完全透明的玻璃,则光线会穿过或反射。镜面反射不会消失。我们不能用我们目前的方法来呈现这一点。
4.1 预乘 Alpha
解决方案是仅让diffuse 光褪色,同时使specular 反射保持全强度。由于源混合模式适用于所有我们无法使用的模式,因此我们将其设置为1,同时仍将目标混合模式使用one-minus-source-alpha。
(源混合设置在一起)
这样可以恢复镜面反射,但是漫反射不再消失。通过将表面Alpha分解为漫反射颜色来解决此问题。因此,将Alpha预先乘以diffuse,而不是以后依赖GPU混合。这种方法称为预乘alpha混合。在GetBRDF中进行。
(预乘漫反射)
4.2 预乘切换
将Alpha与diffuse 进行预乘可有效地将对象变成玻璃,而常规Alpha混合可使对象实际上仅部分存在。通过为GetBRDF添加一个布尔参数来控制是否预乘alpha,默认情况下将其设置为false来支持这两种方法。
我们可以使用_PREMULTIPLY_ALPHA关键字来决定在LitPassFragment中使用哪种方法,类似于我们如何控制alpha裁剪一样。
给着色器的新功能添加关键字到Lit的Pass里。
向材质球添加一个toggle属性。
(预乘alpha的开关)
5 Shader GUI
现在,我们支持多种渲染模式,每种模式都需要特定的设置。为了使切换模式更加容易,让我们在材料检查器中添加一些按钮以应用预设配置。
5.1 自定义 ShaderGUI
在Lit着色器的主块中添加一个CustomEditor“ CustomShaderGUI”语句。
这告诉Unity编辑器使用CustomShaderGUI类的实例来绘制使用Lit着色器的材质的检查器。为该类创建脚本资产,并将其放入新的Custom RP / Editor文件夹中。
我们需要使用UnityEditor,UnityEngine和UnityEngine.Rendering命名空间。该类必须扩展ShaderGUI并覆盖公共的OnGUI方法,该方法具有MaterialEditor和MaterialProperty数组参数。让它调用基本方法,因此我们得到了默认的检查器。
5.2 设置属性和关键字
要完成任务,我们需要访问三项内容,并将其存储在字段中。首先是材质编辑器,它是负责显示和编辑材质的基础编辑器对象。其次是对正在编辑的材质的引用,我们可以通过编辑器的targets属性来检索它们。因为target是通用Editor类的属性,所以将其定义为Object数组。第三是可以编辑的属性数组。
要设置属性,我们首先必须在数组中找到它,为此我们可以使用ShaderGUI.FindPropery方法,并为其传递一个名称和属性数组。然后,通过分配其floatValue属性来调整其值。使用名称和值参数将其封装在方便的SetProperty方法中。
设置关键字要稍微复杂一些。我们将为此创建一个SetKeyword方法,该方法具有一个名称和一个布尔参数,以指示是否应启用或禁用该关键字。还必须在所有材质上调用EnableKeyword或DisableKeyword,并向它们传递关键字名称。
再创建一个SetProperty变体,以切换属性-关键字组合。
现在,我们可以定义简单的Clipping,PremultiplyAlpha,SrcBlend,DstBlend和ZWrite setter属性。
最后,通过分配所有材质的RenderQueue属性来设置渲染队列。我们可以为此使用RenderQueue枚举。
5.3 预设按钮
可以通过GUILayout.Button方法创建按钮,并为其传递标签,该标签将成为预设的名称。如果该方法返回true,则将其按下。在应用预设之前,我们应该在编辑器中注册一个撤消步骤,可以通过在名称上调用RegisterPropertyChangeUndo来完成。由于此代码对于所有预设都是相同的,因此请将其放在PresetButton方法中,该方法返回是否应应用预设。
从默认的不透明模式开始为每个预设创建一个单独的方法。设置适当激活后属性。
第二个预设是Clip,它是Opaque的副本,其中裁剪已打开并且队列设置为AlphaTest。
第三个预设是用于标准透明度的,可以淡化对象,因此我们将其命名为“Fade”。它是Opaque的另一个副本,具有调整的混合模式和队列,并且没有深度写入。
第四个预设是Fade的变体,它应用了预乘alpha混合。我们将其命名为“Transparent ”,因为它用于具有正确照明的半透明表面。
在OnGUI的末尾调用预设方法,使它们显示在默认检查器下方。
(预设按钮)
预设按钮不会经常使用,因此让我们将其放入默认的折叠中。这是通过调用具有当前折叠状态,标签和EditorGUILayout.Foldout为true来完成的,前面小的箭头指示,单击它可以切换其状态。因为它会返回新的折叠状态,所以应该将其存储在字段中。仅在折页打开时才绘制按钮。
(预设折叠)
5.4 UnLit预设
还可以将自定义着色器GUI用于Unlit着色器。
但是,如果激活预设会导致错误,因为我们正在尝试设置着色器没有的属性。可以通过调整SetProperty来防止这种情况。让它使用false作为附加参数调用FindProperty,指示如果找不到该属性,则不应记录错误。结果将为空,只有在检测到的时候才设置该值。当然还返回属性是否存在。
然后调整SetProperty的关键字版本,以便仅在相关属性存在时设置关键字。
5.5 不透明
现在,这些预设也适用于使用“Unlit”着色器的材质了,但“Transparent ”模式在这个Shader下没有意义,因为相关属性根本不存在。我们再改造下让它把无关的预设隐藏。
首先,添加一个HasProperty方法,该方法返回属性是否存在。
其次,创建一个方便的属性来检查_PremultiplyAlpha是否存在。
最后,通过对TransparentPreset进行检查,以便决定Transparent预设的显示情况。
(Unlit 材质隐藏了Transparent预设)
欢迎扫描二维码,查看更多精彩内容。点击 阅读原文 可以跳转原教程。
本文翻译自 Jasper Flick的系列教程
原文地址:
https://catlikecoding.com/unity/tutorials