在Unity 2019.2中扩展Shader Graph,实现自定义光照

随着Unity 2019.1的发布,Shader Graph着色器视图资源包正式脱离预览阶段。在Unity 2019.2中,Shader Graph着色器视图加入了更多新功能。

Unity 2019.2中Shader Graph新功能

自定义函数和子视图更新

为了在Shader Graph着色器视图中使用自定义代码,你可以使用全新的Custom Function节点自定义输入和输出,对其重新排序,并将自定义函数直接插入节点中,或者引用外部文件。

Sub Graphs子视图功能也进行了更新,你可以基于不同类型,可自定义名称和可重新排序的端口,为子视图自定义想要的输出数据。此外,子视图的Blackboard面板现在支持主视图支持的所有数据类型。

颜色模式和精度模式

你可以使用Shader Graph着色器视图轻松创建出强大而优化的着色器。

在Unity 2019.2中,你可以在视图中手动设置计算的精度,既可以设置整个视图的计算精度,也可以设置单个节点的计算精度。新的Color Modes颜色模式可以轻松快捷地可视化精度流,节点分类以及为特定用途显示自定义颜色。

了解更多关于新功能的信息,请访问Shader Graph着色器视图文档:
https://docs.unity3d.com/Packages/com.unity.shadergraph@6.9/manual/index.html

示例项目

为了帮助你熟悉使用新的自定义函数工作流程,我们提供了一个示例项目。该示例项目将向你展示如何使用Custom Function节点,以及如何为轻量级渲染管线LWRP编写自定义光照着色器。


请访问GitHub下载示例项目:
https://github.com/Unity-Technologies/ShaderGraph-Custom-Lighting

温馨提醒:打开示例项目,请确保使用Unity 2019.2和LWRP资源包6.9.1或更高版本。

从主光源获取数据

首先,我们需要从场景中的主光源获取信息。点击Create > Shader >> Unlit Graph,创建新的Unlit Shader Graph着色器视图。在Create Node菜单中,找到新的Custom Function节点,单击右上角齿轮按钮,打开节点菜单。

在节点菜单中,我们可以添加输入和输出数据。我们添加二个输出接口,它们分别是Direction和Color,接口类型为Vector 3 。如果遇到“undeclared identifier”(未声明标识符)警告提示,不必担心,在我们添加代码后,警告会自动消失。

在Type下拉菜单中,选择String,把函数名称改为MainLight。现在,我们可以开始在文本框添加自定义代码。


首先,我们要使用#ifdef SHADERGRAPH_PREVIEW标识。

由于节点上的预览方框无法访问光线数据,所以我们需要告诉节点在视图内的预览框显示什么内容。#ifdef会让编译器在不同情况下使用不同代码。首先定义输出接口的回退值。

#if SHADERGRAPH_PREVIEW

       Direction = half3(0.5, 0.5, 0);

       Color = 1;

接下来,我们使用#else告诉编译器不在预览框内的时候要做什么。

我们会在此获取光线数据,使用LWRP资源包的内置函数GetMainLight()。我们可以使用获取到的信息来指定Direction和Color输出。

自定义函数代码如下。

#if SHADERGRAPH_PREVIEW

       Direction = half3(0.5, 0.5, 0);

       Color = 1;

#else

       Light light = GetMainLight();

       Direction = light.direction;

       Color = light.color;

#endif

现在,我们可以把该节点添加到节点分组中,从而标记它的行为。

右键单击节点,选择Create Group from Selection,然后重命名分组标题来表示节点的行为。我们在分组标题输入Get Main Light。


得到光线数据后,我们可以计算着色效果。我们打算实现标准朗伯光照,所以首先要获取世界法线向量和光线方向的点积。

我们将点积结果传入Saturate节点,使它和光线颜色相乘,然后连接到Unlit Master节点的Color接口,我们预览图应该会更新一些自定义着色。


使用自定义函数的文件模式

我们了解了如何使用Custom Function节点获取光线数据,下面扩展我们的函数。该函数接下来会从主光源获取衰减值,方向和颜色。

由于这个函数更为复杂,因此我们要切换为文件模式,使用HLSL包含文件。这样可以在代码编辑器编写更复杂的函数,然后再加入到视图中,这也意味着我们有合适的位置来调试代码。

首先,我们在项目的Assets > Include文件夹中,打开CustomLighting包含文件。

现在我们只关注MainLight_half函数,代码如下。

void MainLight_half(float3 WorldPos, out half3 Direction, out half3 Color, out half DistanceAtten, out half ShadowAtten)

{

#if SHADERGRAPH_PREVIEW

   Direction = half3(0.5, 0.5, 0);

   Color = 1;

   DistanceAtten = 1;

   ShadowAtten = 1;

#else

#if SHADOWS_SCREEN

   half4 clipPos = TransformWorldToHClip(WorldPos);

   half4 shadowCoord = ComputeScreenPos(clipPos);

#else

   half4 shadowCoord = TransformWorldToShadowCoord(WorldPos);

#endif

   Light mainLight = GetMainLight(shadowCoord);

   Direction = mainLight.direction;

   Color = mainLight.color;

   DistanceAtten = mainLight.distanceAttenuation;

   ShadowAtten = mainLight.shadowAttenuation;

#endif

}

MainLight_half函数有新的输入和输出数据,因此我们要回到Custom Function节点,添加相应数据。我们添加二个新输出数据:一个是DistanceAtten,表示距离衰减;另一个是ShadowAtten,表示阴影衰减。添加新的输入数据WorldPos,它表示世界位置。

有了相应的输入和输出数据后,我们可以引用之前的包含文件,把Type下拉菜单设为File。在Source部分,找到之前的包含文件,选中并引用该文件。现在我们需要告诉节点要使用哪个函数。在Name方框中,输入MainLight。


我们发现该包含文件的函数名结尾有_half,但我们的名称选项中却没有_half。这是因为Shader Graph编译器会把精度格式附加给每个函数名。

由于我们正在定义函数,我们需要通过源代码,告诉编译器我们的函数使用什么精度格式。但是在节点中,我们只需要引用主要函数名称即可。我们可以创建函数的副本,让它使用float值,从而在float精度模式下编译。

“精度”的颜色模式允许我们轻松跟踪视图中每个节点设置的精度,蓝色表示float浮点值,红色表示half半精度值。

我们可能会在其它位置使用该函数,让Custom Function节点可以重用的最简单方法,是把它包装到Sub Graph子视图中。选中节点和其分组,单击右键,选择Convert to Sub-graph。

我们把该子视图命名为Get Main Light。在子视图中,我们把需要的输出接口添加到子视图的输出节点,把节点的输出部分连接到子视图的输出部分。然后添加世界位置节点,把它连接到输入部分。


保存子视图,回到Unlit着色器视图。我们要添加二个Multiply节点到现有的视图中。

首先,把二个衰减输出相乘,把乘积的输出结果再乘以光线颜色,把前面的结果乘以NdotL节点分组的结果,从而计算出基本着色中的衰减。


创建直接镜面着色器

我们制作的着色器适合无光泽对象,但如果我们想要光泽效果,应该怎么做?我们可以给着色器添加镜面计算。

我们会使用另一个Custom Function节点,把它包装到名称为Direct Specular的子视图。再次查看CustomLighting包含文件,我们现在要引用该文件的另一个函数。

void DirectSpecular_half(half3 Specular, half Smoothness, half3 Direction, half3 Color, half3 WorldNormal, half3 WorldView, out half3 Out)

{

#if SHADERGRAPH_PREVIEW

   Out = 0;

#else

   Smoothness = exp2(10 * Smoothness + 1);

   WorldNormal = normalize(WorldNormal);

   WorldView = SafeNormalize(WorldView);

   Out = LightingSpecular(Color, Direction, WorldNormal, WorldView, half4(Specular, 0), Smoothness);

#endif

}

该函数会执行简单的镜面计算,该函数的子视图也包含Blackboard上的输入数据。


要确保新节点的输入和输出接口符合函数的输入和输出数据。给Blackboard添加属性的方法很简单:单击面板右上方的加号(+)图标,选择数据类型即可。

双击显示名称的椭圆框来重命名输入数据,把椭圆框拖到视图中,从而把它添加到视图。最后,更新子视图的输出接口,保存整个视图。

现在镜面计算已经设置好,我们可以回到Unlit着色器视图,通过Create Node菜单添加该功能。把Attenuation节点分组的输出连接到Direct Specular子视图的Color输入。

然后,把Get Main Light函数的Direction输出连接到镜面子视图。把NdotL和Attenuation的输出进行相乘,把乘积结果加上Direct Specular子视图的输出,然后把相加结果连接到Unlit Master节点的Color输出。


处理多个光源

LWRP的主光源是对物体来说最亮的定向光,通常这种光是阳光。为了提升低端硬件的性能,LWRP会分别计算主光源和其它光源。

要确保着色器正确计算场景中的所有光线,而不仅仅计算最亮的定向光,我们需要在函数创建一个循环。添加额外光线数据,我们会使用新的子视图来包装新的Custom Function节点,现在查看CustomLighting包含文件的AdditionalLight_float函数。

void AdditionalLights_hlaf(half3 SpecColor, half Smoothness, half3 WorldPosition, half3 WorldNormal, half3 WorldView, out half3 Diffuse, out half3 Specular)

{
   half3 diffuseColor = 0;

   half3 specularColor = 0;

#ifndef SHADERGRAPH_PREVIEW

   Smoothness = exp2(10 * Smoothness + 1);

   WorldNormal = normalize(WorldNormal);

   WorldView = SafeNormalize(WorldView);

   int pixelLightCount = GetAdditionalLightsCount();

   for (int i = 0; i < pixelLightCount; ++i)

   {

       Light light = GetAdditionalLight(i, WorldPosition);

       half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);

       diffuseColor += LightingLambert(attenuatedLightColor, light.direction, WorldNormal);

       specularColor += LightingSpecular(attenuatedLightColor, light.direction, WorldNormal, WorldView, half4(SpecColor, 0), Smoothness);

   }
#endif

   Diffuse = diffuseColor;

   Specular = specularColor;

}

和前面一样,我们要在Custom Function节点的文件引用部分使用AdditionalLights函数,并创建所有对应的输入和输出数据。在子视图中包装这个节点,并在该子视图的Blackboard面板公开Specular Color和Specular Smoothness。

我们把Position节点、Normal Vector节点和View Direction节点分别连接到子视图的World Position、World Normal和World Space View Direction。

在设置好函数后,我们要使用函数。首先,打开之前的Unlit主视图,把它折叠为子视图。选中视图的所有节点,右键单击Convert to Sub-graph。

移除最后一个Add节点,把输出连接到子视图的输出接口。建议同时创建Specular和Smoothness的输入属性。


现在,我们可以结合主光源和其它光源的计算结果。在Unlit主视图中,为Additional Light的计算创建一个新节点,使该节点的计算和Main Light的计算同步进行。

把Main Light的Diffuse输出和Additional Lights的Diffuse输出相加,把它们各自的Specular输出也进行相加,最后把二个结果再次相加。


创建简单的卡通着色器

我们已经知道如何在LWRP项目中,从场景的所有光线获取数据,我们如何利用这些知识呢?对于着色器的自定义光照,最常见的一个用法是实现经典的卡通着色器。如果拥有所有光线数据,创建卡通着色器的过程会很简单。

首先,获取已完成的所有光线计算,把它们包装到子视图中。这样可以提高最终着色器的可读性。别忘了移除最后的Add节点,把Diffuse和Specular用作子视图输出节点的独立输出接口。

我们有很多方法创建卡通着色效果,但在本示例中,我们会使用光线强度查询渐变纹理的颜色,该方法通常称为渐变光照(Ramp Lighting)。


我们在示例项目中加入了渐变光照所需的示例纹理资源,也可以通过采样渐变,在渐变光照中使用动态渐变效果。

第一步是把Diffuse和Specular的强度从RGB数值转换为HSV数值。这让我们可以通过使用光线颜色的强度即HSV数值,决定着色器上的亮度,并且可以帮助我们沿着资源的水平轴,采样不同位置的纹理。

在UV的Y通道使用静态数值,决定图像中从上到下哪个位置要进行采样。我们可以使用该静态值作为索引,在一个纹理资源中引用项目的多个光照渐变。


设置UV数值后,使用Sample Texture 2D LOD节点来采样渐变纹理。Sample Texture 2D LOD节点很重要,如果我们使用常见的Sample Texture 2D节点,渐变将自动在场景中mimapped,这样较远的对象会有不同的光照行为。

使用Sample Texture 2D LOD节点可以手动确定mip等级。此外,由于渐变纹理的高度只有2个像素,我们要为纹理创建自定义Sampler State采样器状态。

为了确保正确地采样纹理,我们把Filter设为Point,把Wrap设为Clamp。我把这些设置作为属性公开在Blackboard面板,使用户可以在纹理资源发生变化时修改设置。


最后,我们把漫反射计算出的渐变采样乘以颜色属性Diffuse,从而改变对象的颜色。把镜面计算出的渐变采样加上Diffuse输出,然后把最终颜色连接到Master节点。


扩展自定义光照

我们可以扩展这个简单的自定义光照设置,把它应用到各种场景的不同用例。在示例项目中,我们加入了完整的场景,该场景通过使用自定义光照设置的着色器进行配置。

该着色器也包含顶点动画,简单的次表面散射估算,以及使用深度的折射和着色效果。你可以下载示例项目,查看示例资源,从而了解更多高级方法。


小结

如果想要讨论Shader Graph着色器视图和使用该功能制作的着色器,欢迎访问官方论坛:https://forum.unity.com/forums/shader-graph.346/

本文转自: Unity官方平台(Unity-GreaterChina),转载此文目的在于传递更多信息,版权归原作者所有。

推荐阅读