Unity3D CustomSRP[译].3.平行光[Directional Lights]

Directional Lights(平行光、方向光)

——直接照明


本节内容

  • 使用法向量来计算光照
  • 支持多达四个平行光
  • 应用双向反射分布函数(BRDF)
  • 制作有照明的透明材质
  • 创建一个自定义的着色器图形用户界面(Shader GUI)。



这是一个关于如何创建一个Custom SRP的系列教程的第三个部分,它添加了对多个平行光的支持。

这个教程使用的是Unity版本是2019.2.6f1.

由四个光照亮的各种球体

(ps:文章总被吞…最后偶然看到可能会被吞的一些词儿…尝试改了点但有些意思感觉不到位~)


1. 照明(Lighting)

如果我们想要创建一个更真实的场景,那么我们必须模拟光如何与物体的表面相互作用。这需要一个比我们目前的无光照的shader更复杂的shader。


1.1 照明着色器(Lit Shader)

复制UnlitPass.hlsl文件并将其重命名为LitPass.hlsl。调整引用保护定义以及顶点和片元函数名。稍后我们将添加光照计算。

#ifndef CUSTOM_LIT_PASS_INCLUDED
#define CUSTOM_LIT_PASS_INCLUDED

…

Varyings LitPassVertex (Attributes input) { … }

float4 LitPassFragment (Varyings input) : SV_TARGET { … }

#endif

也复制Unlit着色器,并将其重命名为Lit。更改其菜单名称、引用的文件、以及使用的函数。让我们同样也改变默认颜色为灰色,因为一个完全白色的表面在一个明亮的场景中会显得非常明亮。URP默认也使用灰色。

Shader "Custom RP/Lit" {
    Properties {
        _BaseMap("Texture", 2D) = "white" {}
        _BaseColor("Color", Color) = (0.5, 0.5, 0.5, 1.0)
        …
    }

    SubShader {
        Pass {
            …
            #pragma vertex LitPassVertex
            #pragma fragment LitPassFragment
            #include "LitPass.hlsl"
            ENDHLSL
        }
    }
}

我们将使用一个自定义照明方法,通过设置shader的照明模式为CustomLit。在Pass中添加一个Tags块,包含"LightMode" = "CustomLit"

Pass {
    Tags {
        "LightMode" = "CustomLit"
    }

    …
}

要渲染使用这个pass的对象,我们必须在CameraRenderer中包含它。首先为它添加一个shader标签标识符。

static ShaderTagId
    unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit"),
    litShaderTagId = new ShaderTagId("CustomLit");

然后将它添加到要在DrawVisibleGeometry中渲染的pass中,就像我们在DrawUnsupportedShaders中做的那样。

var drawingSettings = new DrawingSettings(
            unlitShaderTagId, sortingSettings
) {
    enableDynamicBatching = useDynamicBatching,
    enableInstancing = useGPUInstancing
};
drawingSettings.SetShaderPassName(1, litShaderTagId);

现在我们可以创建一个新的非透明的材质,尽管目前它产生的结果与无光照的材质相同。

默认的非透明材质


1.2 法向量(Normal Vectors)

一个物体被照亮的程度取决于多种因素,包括光与物体表面之间的相对角度。为了知道表面的方向,我们需要访问表面的法线,它是一个垂直于表面的单位长度的向量。这个向量是顶点数据的一部分,在对象空间中定义,就像位置一样。 所以把它添加到LitPassAttributes中。

struct Attributes {
    float3 positionOS : POSITION;
    float3 normalOS : NORMAL;
    float2 baseUV : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

光照是根据每个片元计算的,所以我们必须将法向量添加到Varyings中。我们将在世界空间中执行计算,因此将其命名为normalWS

struct Varyings {
    float4 positionCS : SV_POSITION;
    float3 normalWS : VAR_NORMAL;
    float2 baseUV : VAR_BASE_UV;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

我们可以使用来自SpaceTransforms.hlslTransformObjectToWorldNormal方法在LitPassVertex中将法线变换到世界空间中。

output.positionWS = TransformObjectToWorld(input.positionOS);
output.positionCS = TransformWorldToHClip(positionWS);
output.normalWS = TransformObjectToWorldNormal(input.normalOS);


TransformObjectToWorldNormal 是如何工作的?
.
检查代码时,您将看到它使用了两种方法之一,这取决于是否定义了 UNITY_ASSUME_UNIFORM_SCALING
.
UNITY_ASSUME_UNIFORM_SCALING被定义时,它调用 TransformObjectToWorldDir,这和 TransformObjectToWorld做的是一样的,除了它忽略了平移部分,因为我们处理的是方向向量而不是位置。但是这个向量也会被均匀缩放,所以之后应该被归一化。
·
在另一种情况下,不假设是均匀缩放。这是更复杂的,因为当一个物体因非均匀缩放而变形时,法向量必须反向缩放以匹配新的表面方向。这需要与转置的 UNITY_MATRIX_I_M矩阵相乘,并进行归一化。
·

不正确和正确的法线变换

使用 UNITY_ASSUME_UNIFORM_SCALING是一个轻微的优化,你可以通过自己定义它来启用。然而,当使用 GPU-Instancing时,这将更有意义。因为 UNITY_MATRIX_I_M矩阵数组必须发送给GPU,在不需要的时候避免这样做是值得的。你可以通过在着色器中添加 #pragma instancing_options assumeuniformscaling指令来启用它,但只有在你用统一缩放渲染对象时才这么做。


为了验证我们是否在LitPassFragment中得到了正确的法向量,我们可以使用它作为颜色输出。

base.rgb = input.normalWS;
return base;

世界空间的法向量

负值无法显示,所以它们被固定为零。


1.3 差值法线(Interpolated Normals)

虽然在顶点程序中,法向量是单位长度的,但三角形之间的线性插值会影响它们的长度。我们可以通过渲染1和向量的长度之间的差值来可视化误差,并将结果放大10倍,使其更明显。

base.rgb = abs(length(input.normalWS) - 1.0) * 10.0;

放大的法线差值的误差

我们可以通过对LitPassFragment中的法向量进行归一化来平滑插值失真。当只看法向量时,这种差异并不明显,但当用于照明时,这种差异就更明显了。

base.rgb = normalize(input.normalWS);

差值后的归一化


1.4 表面属性(Surface Properties)

在一个shader中产生照明需要模拟光照之间的交互作用,这意味着我们必须跟踪表面的属性。现在我们有一个法向量和一个基色。我们可以将后者分成两部分:RGB颜色和Alpha值。我们将在一些不同的地方使用这些数据,所以让我们定义一个方便的Surface结构体来包含所有相关数据。把这个结构体放在ShaderLibrary文件夹中的一个单独的Surface.hlsl文件中。

#ifndef CUSTOM_SURFACE_INCLUDED
#define CUSTOM_SURFACE_INCLUDED

struct Surface {
    float3 normal;
    float3 color;
    float alpha;
};

#endif


我们不应该把法线定义为 normalWS 吗?
.
可以,但是表面不关心法线是在什么空间定义的。光照计算可以在任何合适的3D空间中进行,所以我们不为法线定义这个空间限制。当填充数据时,我们只需要在所有地方使用相同的空间。我们将使用世界空间,但我们之后有可能会切换到另一个空间,一切仍将保持不变。


LitPassCommon之后引用它,这样我们就可以保持LitPass的简洁。从现在起,我们将把专用的代码放在它们自己的HLSL文件中,以便更容易地定位相关的功能。

#include "../ShaderLibrary/Common.hlsl"
#include "../ShaderLibrary/Surface.hlsl"

LitPassFragment中定义一个surface变量并填充它,结果变成表面的颜色和透明度。

Surface surface;
surface.normal = normalize(input.normalWS);
surface.color = base.rgb;
surface.alpha = base.a;

return float4(surface.color, surface.alpha);


这不是低效的代码吗?
.
这没有区别,因为着色器编译器将生成高度优化的程序,完全重写了我们的代码。结构体纯粹是为了方便我们使用。你可以通过在着色器面板的 Compile and show code按钮检查编译器的编译结果。


1.5 光照计算(Calculating Lighting)

为了计算实际的光照,我们将创建一个具有Surface参数的GetLighting函数。最初让它返回表面法线的Y分量。因为这是照明功能,我们将把它放在一个单独的Lighting.hlsl文件中。

#ifndef CUSTOM_LIGHTING_INCLUDED
#define CUSTOM_LIGHTING_INCLUDED

float3 GetLighting (Surface surface) {
    return surface.normal.y;
}

#endif

LitPass中引用Surface之后引用它,因为照明依赖于Surface.hlsl

#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"


为什么不在 Lighting.hlsl 中 引用 Surface.hlsl?
.
我们可以这样做,但最终的结果可能是多个文件依赖于多个其他文件,依赖关系会十分杂乱。相反,我选择将所有include语句放在一个地方,这样可以明确依赖关系。这也使得用一个文件替换另一个文件从而改变着色器的工作方式变得更加容易,只要新文件定义了其他文件依赖的相同功能。


现在我们可以在LitPassFragment中获得照明,并将其用于片元函数返回颜色的RGB部分。

    float3 color = GetLighting(surface);
    return float4(color, surface.alpha);

漫反射光照

现在,输出的是表面法线的Y分量,所以它在球体的顶部是1,在它的两侧是0。再往下结果变为负值,并在底部达到- 1。但我们观察不到负值,它等于法向量和上(up)向量夹角的余弦值。忽略负的部分,这在视觉上就好像一个漫反射的平行光从上垂直的向下照明。最后一步是在GetLighting中把表面颜色合并到结果中,将其诠释为表面反照率(Albedo)。

float3 GetLighting (Surface surface) {
    return surface.normal.y * surface.color;
}
应用反照率


反照率(Albedo)是什么意思?
.
反照率在拉丁语中是白色程度的意思。它代表着光被一个表面漫反射的程度。如果反照率不是全白,那么意味着部分光能被吸收而不是被反射。


2. 灯光(Lights)

为了表现合适的照明,我们还需要知道光源的属性。在本章节中,我们将只使用平行光。平行光代表着一个距离很远很远的光源,它的位置并不重要,重要的是它的方向。这是一种简化,但它足以模拟地球上的太阳光和其他单向光线的情况。


2.1 光照结构(Light Structure)

我们将使用一个结构体来存储光的数据。现在我们只需要一个颜色和一个方向就够了。将其放在单独的Light.hlsl文件中。同时定义一个GetDirectionalLight函数来返回一个配置好的平行光。使用白色和向上矢量初始化它,匹配我们目前使用的光照数据。请注意,光的方向的定义是,光线从哪里来,而不是它要到哪里去。

#ifndef CUSTOM_LIGHT_INCLUDED
#define CUSTOM_LIGHT_INCLUDED

struct Light {
    float3 color;
    float3 direction;
};

Light GetDirectionalLight () {
    Light light;
    light.color = 1.0;
    light.direction = float3(0.0, 1.0, 0.0);
    return light;
}

#endif

LitPass中引用Lighting.hlsl之前引用它。

#include "../ShaderLibrary/Light.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"


2.2 照明函数(Lighting Functions)

Lighting中添加一个IncomingLight函数,计算在给定一个表面和光源后有多少入射光。对于任意方向的光,我们必须取表面法线和光方向的点积。我们可以用点积函数。结果应该被光的颜色调节。

float3 IncomingLight (Surface surface, Light light) {
    return dot(surface.normal, light.direction) * light.color;
}


什么是点积?
.
两个向量的点积在几何学上的定义如下图:


这意味着它是两个向量夹角的余弦值,乘以它们的长度。 对于两个单位长度的向量 A·B=cos θ
在代数中,他被如下图这样定义:
·

可以通过将所有的分量相乘并相加来计算他们的值:
·

将点积具象化的话,这个操作是直接将一个矢量投射到另一个矢量上,就像在它投下阴影一样。通过这样做,你最终得到一个直角三角形,其底边的长度正是点积的结果。如果两个向量都是单位长度,那点积的结果就是它们夹角的余弦值。
·
点积


但这只在表面朝向光线时才正确。 当点积为负时,我们需要将它限制到零,这可以通过saturate函数来实现。

float3 IncomingLight (Surface surface, Light light) {
    return saturate(dot(surface.normal, light.direction)) * light.color;
}


saturate 做了什么?fffffffff
·
它将一个值限制于0和1之间。我们只需要指定一个最小值,因为点积永远不应该大于1。 saturate是一个非常常见的着色器操作。


添加另一个GetLighting函数,它返回表面和光源的最终照明。现在它返回的是入射光结果乘以表面颜色。在其他函数上面定义这个函数。

float3 GetLighting (Surface surface, Light light) {
    return IncomingLight(surface, light) * surface.color;
}

最后,调整GetLighting函数,它只有一个表面参数,所以它需要调用另一个同名函数来获取一个光源信息,使用GetDirectionalLight来提供平行光数据。

float3 GetLighting (Surface surface) {
    return GetLighting(surface, GetDirectionalLight());
}


2.3 向GPU发送光源数据(Sending Light Data to the GPU)

我们应该使用当前场景的光,而不是总是使用之前定义的白色的光。默认场景有一个平行光,代表太阳,它是淡黄色的——使用了十六进制的值fff4d6,并围绕X轴旋转50°,围绕Y轴旋转30°。如果这样的光不存在,那就创建一个。

为了在shader中访问光的数据,我们必须为它定义一个值,就像着色器属性一样。在这个例子中,我们将定义两个float3类型的向量:_DirectionalLightColor_DirectionalLightDirection。把它们放在一个定义在顶部的_CustomLight缓冲区中。

CBUFFER_START(_CustomLight)
    float3 _DirectionalLightColor;
    float3 _DirectionalLightDirection;
CBUFFER_END

GetDirectionalLight中使用这些值而不是常量。

Light GetDirectionalLight () {
    Light light;
    light.color = _DirectionalLightColor;
    light.direction = _DirectionalLightDirection;
    return light;
}

现在我们的渲染管线必须把光数据发送到GPU。我们将为此创建一个新的Lighting.cs类。它的工作原理类似于CameraRenderer,但是用于照明。给它一个带有context参数的公共方法Setup,在这个方法中它调用一个单独的SetupDirectionalLight方法。尽管不是严格必要的,但让我们也为它提供一个专用的命令缓冲区,以便在完成时执行,这样也便于调试。另一种方法是可以添加一个缓冲区参数。

using UnityEngine;
using UnityEngine.Rendering;

public class Lighting {

    const string bufferName = "Lighting";

    CommandBuffer buffer = new CommandBuffer {
        name = bufferName
    };
    
    public void Setup (ScriptableRenderContext context) {
        buffer.BeginSample(bufferName);
        SetupDirectionalLight();
        buffer.EndSample(bufferName);
        context.ExecuteCommandBuffer(buffer);
        buffer.Clear();
    }
    
    void SetupDirectionalLight () {}
}

跟踪着色器两个属性的标识符。

static int
        dirLightColorId = Shader.PropertyToID("_DirectionalLightColor"),
        dirLightDirectionId = Shader.PropertyToID("_DirectionalLightDirection");

我们可以通过RenderSettings.sun访问场景的主光源。这让我们默认情况下得到最重要的平行光,它也可以通过Window/Rendering/Lighting Settings设置。使用CommandBuffer.SetGlobalVector将光源数据发送到GPU。颜色是光在线性空间中的颜色,而方向是光源变换信息的正向量取反后的值。

void SetupDirectionalLight () {
    Light light = RenderSettings.sun;
    buffer.SetGlobalVector(dirLightColorId, light.color.linear);
    buffer.SetGlobalVector(dirLightDirectionId, -light.transform.forward);
}


SetGlobalVector 不是需要 Vector4 类型的参数吗?
·
是的,发送给GPU的向量总是有四个分量,即使我们以较少的分量定义它们,其余的分量会在shader中被隐式掩盖。同样,也有一个从Vector3到Vector4的隐式转换。



光的color属性是它的配置颜色,但光也有一个单独的强度因子。最后的颜色需要两者相乘。

buffer.SetGlobalVector(
    dirLightColorId, light.color.linear * light.intensity
);

CameraRenderer一个Lighting实例,并在绘制可见几何图形之前使用它来设置照明。

Lighting lighting = new Lighting();

public void Render (
    ScriptableRenderContext context, Camera camera,
    bool useDynamicBatching, bool useGPUInstancing
) {
        …

    Setup();
    lighting.Setup(context);
    DrawVisibleGeometry(useDynamicBatching, useGPUInstancing);
    DrawUnsupportedShaders();
    DrawGizmos();
    Submit();
}
被太阳光照亮


2.4 可见光(Visible Lights)

在进行裁剪时,Unity还会计算出哪些灯光会影响到对相机可见的空间。我们可以依靠这些信息,而不是单一的全局太阳光。为此,Lighting需要访问裁剪结果,所以为Setup添加一个参数,并将其存储在字段中。然后我们可以支持多个光源,所以用一个新的SetupLights方法替换掉SetupDirectionalLight的调用。

CullingResults cullingResults;

public void Setup (ScriptableRenderContext context, CullingResults cullingResults) {
    this.cullingResults = cullingResults;
    buffer.BeginSample(bufferName);
    //SetupDirectionalLight();
    SetupLights();
    …
}
    
void SetupLights () {}

CameraRenderer.Render中调用Setup时,添加裁剪结果作为参数。

    lighting.Setup(context, cullingResults);

现在Lighting.SetupLights可以通过裁剪结果的visibleLights属性获取所需的数据。它是一个Unity.Collections.NativeArray模板类结构,元素类型为VisibleLight

using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;

public class Lighting {
    …

    void SetupLights () {
        NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
    }

    …
}


什么是 NativeArray?
·
它是一个作用类似于数组的结构,但提供了到本机内存缓冲区的链接。这使得在托管的c#代码和原生的Unity引擎代码之间高效地共享数据成为可能。


2.5 多个平行光(Multiple Directional Lights)

使用可见光数据可以支持多个平行光,但我们必须将所有这些光的数据发送到GPU。所以我们不再使用仅仅一对向量,而是使用两个Vector4数组存储光的方向和颜色,另外加上一个整数作为光的计数。我们还将定义最大数量的平行光,我们可以使用它初始化两个数组字段来缓冲数据。让我们将最大值设置为4,这对大多数场景来说应该足够了。

const int maxDirLightCount = 4;

    static int
        //dirLightColorId = Shader.PropertyToID("_DirectionalLightColor"),
        //dirLightDirectionId = Shader.PropertyToID("_DirectionalLightDirection");
        dirLightCountId = Shader.PropertyToID("_DirectionalLightCount"),
        dirLightColorsId = Shader.PropertyToID("_DirectionalLightColors"),
        dirLightDirectionsId = Shader.PropertyToID("_DirectionalLightDirections");

    static Vector4[]
        dirLightColors = new Vector4[maxDirLightCount],
        dirLightDirections = new Vector4[maxDirLightCount];


为什么不使用结构化的缓冲区?
·
也许可能吧,但我不准备用,因为着色器对结构化的缓冲区的支持还不够好。它们要么根本不被支持或只在片元函数中存在,要么比常规数组性能更差。好消息是,数据在CPU和GPU之间如何传递的细节只影响少数地方,所以很容易改变。这是使用Light结构体的另一个好处。


SetupDirectionalLight添加一个索引和一个VisibleLight参数。让它根据提供的索引设置颜色和方向数据。在这种情况下,最终的颜色是通过调用VisibleLight.finalColor获得的
。可以通过VisibleLight.localToWorldMatrix获得前向量,它是矩阵的第三列,同样我们需要对它取反。

void SetupDirectionalLight (int index, VisibleLight visibleLight) {
    dirLightColors[index] = visibleLight.finalColor;
    dirLightDirections[index] = -visibleLight.localToWorldMatrix.GetColumn(2);
}

最终的颜色已经应用了光的强度,但默认情况下Unity不会将其转换到线性空间。我们必须设置GraphicsSettings. lightsUseLinearIntensity设置为true,我们可以在CustomRenderPipeline的构造函数中执行一次。

public CustomRenderPipeline (
        bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher
) {
    this.useDynamicBatching = useDynamicBatching;
    this.useGPUInstancing = useGPUInstancing;
    GraphicsSettings.useScriptableRenderPipelineBatching = useSRPBatcher;
    GraphicsSettings.lightsUseLinearIntensity = true;
}

接下来,在Lighting.SetupLights中循环所有可见光,并为每个元素调用SetupDirectionalLight。然后调用bufferSetGlobalIntSetGlobalVectorArray将数据发送给GPU。

NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
for (int i = 0; i < visibleLights.Length; i++) {
    VisibleLight visibleLight = visibleLights[i];
    SetupDirectionalLight(i, visibleLight);
}

buffer.SetGlobalInt(dirLightCountId, visibleLights.Length);
buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);

但是我们最多只支持四个方向灯,所以当我们达到这个最大值时,我们应该中止循环。让我们从循环的迭代器中单独跟踪平行光的索引。

int dirLightCount = 0;
for (int i = 0; i < visibleLights.Length; i++) {
    VisibleLight visibleLight = visibleLights[i];
    SetupDirectionalLight(dirLightCount++, visibleLight);
    if (dirLightCount >= maxDirLightCount) {
        break;
    }
}

buffer.SetGlobalInt(dirLightCountId, dirLightCount);

因为我们目前只支持平行光,所以我们应该忽略其他类型的灯。我们可以通过检查可见光的lightType属性是否等于LightType.directional来做到这一点。

VisibleLight visibleLight = visibleLights[i];
if (visibleLight.lightType == LightType.Directional) {
    SetupDirectionalLight(dirLightCount++, visibleLight);
    if (dirLightCount >= maxDirLightCount) {
        break;
    }
}

这是可行的,但VisibleLight的结构相当大。理想情况下,我们应该只从数组中检索它一次,而不是将它作为常规参数传递给SetupDirectionalLight,因为那样会复制它。我们可以使用Unity用于ScriptableRenderContext.DrawRenderers方法的技巧,通过ref传递参数。

SetupDirectionalLight(dirLightCount++, ref visibleLight);

这也要求我们将参数定义为引用。

void SetupDirectionalLight (int index, ref VisibleLight visibleLight) { … }


2.6 着色器循环(Shader Loop)

Light.hlsl中调整_CustomLight缓冲区,使其匹配我们的新数据。在本例中,我们将显式地使用float4作为数组类型。数组在着色器中需要有固定的大小,它们不能被调整大小。确保使用与我们在Lighting.cs中定义的相同的最大值。

#define MAX_DIRECTIONAL_LIGHT_COUNT 4

CBUFFER_START(_CustomLight)
    //float4 _DirectionalLightColor;
    //float4 _DirectionalLightDirection;
    int _DirectionalLightCount;
    float4 _DirectionalLightColors[MAX_DIRECTIONAL_LIGHT_COUNT];
    float4 _DirectionalLightDirections[MAX_DIRECTIONAL_LIGHT_COUNT];
CBUFFER_END

添加一个函数来获取平行光计数以及调整GetDirectionalLight函数,以便它可以获取特定的光索引的数据。

int GetDirectionalLightCount () {
    return _DirectionalLightCount;
}

Light GetDirectionalLight (int index) {
    Light light;
    light.color = _DirectionalLightColors[index].rgb;
    light.direction = _DirectionalLightDirections[index].xyz;
    return light;
}


rgb 和 xyz 之间有什么区别吗?
·
他们都是语义的别名,使用 rgbaxyzw是等价的。(译注:但通常来讲,我们使用 rgba 获取颜色,用 xyzw 获取向量和矢量等,便于功能上的区分和理解)


然后调整GetLighting函数,使用for循环来分别计算并累计每个平行光对于表面光照的贡献。

float3 GetLighting (Surface surface) {
    float3 color = 0.0;
    for (int i = 0; i < GetDirectionalLightCount(); i++) {
        color += GetLighting(surface, GetDirectionalLight(i));
    }
    return color;
}
四个平行光

现在我们的shader最多支持四个平行光。 通常只需要一个平行光来代表太阳或月亮,但也许在一个行星上有多个太阳的场景。平行光也可以用于模拟多个大型灯光平台,例如大型体育场的灯光。

如果你的游戏总是有一个单一的方向光,那么你可以摆脱循环,或制作多个shader变体。但在本教程中,我们将保持简单,坚持使用一个通用目的的循环。最好的性能总是需要通过删除不需要的内容来实现,尽管这样做并不总是会产生显著的差异。


2.7 着色器目标等级(Shader Target Level)

具有可变长度的循环曾经是shader的一个问题,但现代GPU可以毫无压力的处理它们,特别是当draw call的所有片元以相同的方式迭代相同的数据。然而,OpenGL ES 2.0和WebGL 1.0的图形api在默认情况下不能处理这样的循环。我们可以通过结合一个硬编码的最大值来让它工作,例如通过GetDirectionalLight返回min(_DirectionalLightCount, MAX_DIRECTIONAL_LIGHT_COUNT),这使得循环成为可能,将其转换为条件代码块序列。

不幸的是,这产生的shader代码是乱糟糟的,性能也下降很多。在非常老式的硬件上,所有的代码块都会被执行,它们是通过条件赋值来控制的。虽然我们可以使它工作,但它使代码更复杂,因为我们还必须进行其他更多的调整。

因此,为了简单起见,我选择忽略这些限制,在项目中取消对WebGL 1.0和OpenGL ES 2.0的支持,它们不支持线性照明。 我们也可以通过#pragma target 3.5指令,将我们的着色器的目标级别提升到3.5,从而避免为它们编译OpenGL ES 2.0的着色器变体。让我们对两个着色器都这样做。

HLSLPROGRAM
#pragma target 3.5
…
ENDHLSL


3. 双向反射分布函数(BRDF)

我们目前使用的是一个非常简单的光照模型,只适合完美的漫反射表面。我们可以通过应用双向反射分布函数(Bidirectional Reflectance Distribution Function),简称BRDF,来实现更丰富和真实的照明。有许多类型的BRDF函数。我们将使用URP所使用的相同的方法,这为了性能牺牲了一些更真实的表现。


3.1 入射光(Incoming Light)

当一束光迎面击中表面的片段时,它所有的能量都会影响到片段。为了简单起见,我们假定光束的宽度与表面片段的宽度相匹配。这就是光的方向L和表面法线N平行对齐的情况,即 N·L = 1。当他们没有平行对齐而是至少有一部分的光会偏离表面,所以影响表面的能量也就少了。影响表面的能量是 N·L。结果为负的话就表明表面远离了光入射的方向,所以它不会被光影响到。

入射光和表面


3.2 出射光(Outgoing Light)

我们看不到直接到达表面的光。我们只能看到从表面反射到相机或我们眼睛的那部分。如果表面是一个完美的平面镜,那么光线就会被反射出去,出射角与入射角相等。只有当相机与出射光一致时,我们才能看到这道光。这就是所谓的镜面反射。这是光与表面相互作用的简化,但对于我们当前来说已经足够了。

完美的镜面反射

但如果表面不是完全平坦,光线就会被散射,因为表面的片段实际上是由许多方向不同的小碎片组成的。这将光线分成不同方向的更细小的光束,这有效地模糊了镜面反射。我们最终可能会看到一些散射的光,即使没有与完美的反射方向对齐。

分散的镜面反射

除此之外,光也会穿透表面,在表面细碎的部位反弹,以不同的角度离开,以及还有一些其他我们目前不需要考虑的事情。在极端情况下,我们最终会得到一个完全漫反射的表面,将光线均匀地散射到所有可能的方向。这是我们当前在shader中计算的光照。

完全的漫反射

无论相机在哪里,表面接收到的漫射光的量是一样的,但这也意味着我们观测到的光能远远小于到达表面碎片的光能,这表明我们应该用另外一些因素来衡量入射光。然而,因为这个因素总是一样的,我们可以把它放入光的颜色和强度中。因此,我们使用的最终光颜色代表了从一个完美的白色漫反射表面片段,接受正面照明时观察到的数量,这只是实际发出的光总量的一小部分。还有其他配置光源的方法,例如指定光通量或辐照度,这样更容易配置真实的光源,但我们目前将坚持现在这种的方法。


3.3 表面属性(Surface Properties)

表面可以是完美的漫反射,或完美的高光反射(镜面反射),或者介于两者之间的任何东西。我们有很多方法可以控制它,目前我们将使用金属工作流,需要添加两个表面属性到Lit着色器中。

第一个属性用于表明一个表面是金属的还是非金属的,也被称为电介质。因为一个表面可以包含两者的混合,我们将为它添加一个范围0-1的滑动条,1表示它是完全金属的,默认为0。

第二个属性控制表面的光滑程度。我们还将为此使用范围0-1滑动条,0表示完全粗糙,1表示完全光滑。我们将使用0.5作为默认值。

_Metallic ("Metallic", Range(0, 1)) = 0
_Smoothness ("Smoothness", Range(0, 1)) = 0.5

具有金属度和光滑度属性滑动条的材质

将属性添加到UnityPerMaterial缓存区中。

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
    UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
    UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
    UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)
    UNITY_DEFINE_INSTANCED_PROP(float, _Metallic)
    UNITY_DEFINE_INSTANCED_PROP(float, _Smoothness)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

还有Surface结构体。

struct Surface {
    float3 normal;
    float3 color;
    float alpha;
    float metallic;
    float smoothness;
};

LitPassFragment中将它们复制到surface中。

    Surface surface;
    surface.normal = normalize(input.normalWS);
    surface.color = base.rgb;
    surface.alpha = base.a;
    surface.metallic = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Metallic);
    surface.smoothness =
        UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Smoothness);

并在PerObjectMaterialProperties.cs中添加对它们的支持。

    static int
        baseColorId = Shader.PropertyToID("_BaseColor"),
        cutoffId = Shader.PropertyToID("_Cutoff"),
        metallicId = Shader.PropertyToID("_Metallic"),
        smoothnessId = Shader.PropertyToID("_Smoothness");

    …

    [SerializeField, Range(0f, 1f)]
    float alphaCutoff = 0.5f, metallic = 0f, smoothness = 0.5f;

    …

    void OnValidate () {
        …
        block.SetFloat(metallicId, metallic);
        block.SetFloat(smoothnessId, smoothness);
        GetComponent<Renderer>().SetPropertyBlock(block);
    }


3.4 BRDF 属性(BRDF Properties)

我们将利用表面属性来计算BRDF方程。它告诉我们有多少光从一个表面反射回来,这是漫反射和高光反射两者的结合。我们需要在漫反射和高光反射部分分割表面颜色,我们还需要知道表面有多粗糙。让我们在一个BRDF结构体中存放和跟踪这三个值,放在一个单独的BRDF.hlsl文件中。

#ifndef CUSTOM_BRDF_INCLUDED
#define CUSTOM_BRDF_INCLUDED

struct BRDF {
    float3 diffuse;
    float3 specular;
    float roughness;
};

#endif

添加一个函数来获取给定表面的BRDF数据。让我们从一个完全漫反射的表面开始,因此漫反射部分应该相当于表面颜色,而高光部分则是黑色,粗糙度为1。

BRDF GetBRDF (Surface surface) {
    BRDF brdf;
    brdf.diffuse = surface.color;
    brdf.specular = 0.0;
    brdf.roughness = 1.0;
    return brdf;
}

在引用Light.hlslLighting.hlsl之间引用BRDF.HLSL

#include "../ShaderLibrary/Common.hlsl"
#include "../ShaderLibrary/Surface.hlsl"
#include "../ShaderLibrary/Light.hlsl"
#include "../ShaderLibrary/BRDF.hlsl"
#include "../ShaderLibrary/Lighting.hlsl"

为两个GetLighting函数添加一个BRDF参数,然后将入射光与漫反射部分相乘,而不是整个表面颜色。

float3 GetLighting (Surface surface, BRDF brdf, Light light) {
    return IncomingLight(surface, light) * brdf.diffuse;
}

float3 GetLighting (Surface surface, BRDF brdf) {
    float3 color = 0.0;
    for (int i = 0; i < GetDirectionalLightCount(); i++) {
        color += GetLighting(surface, brdf, GetDirectionalLight(i));
    }
    return color;
}

最后,在LitPassFragment中获取BRDF数据,并将其传递给GetLighting

BRDF brdf = GetBRDF(surface);
float3 color = GetLighting(surface, brdf);


3.5 反射率(Reflectivity)

一个表面如何反射是可变的,但一般来说,金属表面通过镜面反射反射所有的光,而其漫反射为零。所以我们声明反射率等于金属表面的属性。被反射的光不会被漫反射,所以我们应该在GetBRDF中将漫反射的比例调整为1-反射率

float oneMinusReflectivity = 1.0 - surface.metallic;

brdf.diffuse = surface.color * oneMinusReflectivity;
金属度依次为 0,0.25,0.5,0.75,1

在现实中,一些光也会被非金属表面反射,这就给了它们高光。非金属的反射率各不相同,但平均约为0.04。让我们将其定义为最小反射率,并添加一个OneMinusReflectivity函数,将0-1范围调整到0-0.96。此范围调整与URP使用的方法相同。

#define MIN_REFLECTIVITY 0.04

float OneMinusReflectivity (float metallic) {
    float range = 1.0 - MIN_REFLECTIVITY;
    return range - metallic * range;
}

GetBRDF中使用该函数限制一个最小值。当只渲染漫反射时,这种差异很难被注意到,但当我们添加高光反射后,这种差异会变得很明显。没有它,非金属就不会有高光。

float oneMinusReflectivity = OneMinusReflectivity(surface.metallic);


3.6 高光反射的颜色(Specular Color)

以一种方式反射的光不能以另一种方式反射,这就是所谓的能量守恒,这意味着出射光的能量不能超过入射光的能量,这也表明高光部分的颜色应该等于表面颜色减去漫反射颜色。

brdf.diffuse = surface.color * oneMinusReflectivity;
brdf.specular = surface.color - brdf.diffuse;

然而,这忽略了一个事实,金属会影响高光反射的颜色,而非金属则不会。非金属表面的高光部分的颜色应该是白色的,我们可以通过使用金属度在最小反射率和表面颜色之间进行插值来实现。

brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic);


3.7 粗糙度(Roughness)

粗糙度是光滑度的反义词,所以我们可以简单地用1减去平滑度。Core RP库中有一个名为PerceptualSmoothnessToPerceptualRoughness的函数,我们将使用这个函数,来定义光滑度和粗糙度是“感知上的”。我们可以通过PerceptualRoughnessToRoughness函数转换为实际的粗糙度值,该函数对“感知的值”进行平方,这种方法与迪士尼的光照模型相匹配。之所以这样做,是因为在编辑材质时调整“感知的值”更为直观。

    float perceptualRoughness =
        PerceptualSmoothnessToPerceptualRoughness(surface.smoothness);
    brdf.roughness = PerceptualRoughnessToRoughness(perceptualRoughness);

这些函数在Core RP库的CommonMaterial.hlsl文件中定义。在我们自己的Common.hlsl文件中引用Common.hlsl之后已用它。

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
#include "UnityInput.hlsl"


3.8 视图方向(View Direction)

为了确定相机与完美反射方向的对齐情况,我们需要知道相机的位置。Unity通过float3 _WorldSpaceCameraPos使这些数据可访问,所以将其添加到UnityInput.hlsl中。

float3 _WorldSpaceCameraPos;

为了在LitPassFragment中获得视图方向,即从表面到相机的方向,我们需要在Varyings中添加表面在世界空间的位置。

struct Varyings {
    float4 positionCS : SV_POSITION;
    float3 positionWS : VAR_POSITION;
    …
};

Varyings LitPassVertex (Attributes input) {
    …
    output.positionWS = TransformObjectToWorld(input.positionOS);
    output.positionCS = TransformWorldToHClip(output.positionWS);
    …
}

我们将视图方向视为表面数据的一部分,因此将其添加到Surface中。

struct Surface {
    float3 normal;
    float3 viewDirection;
    float3 color;
    float alpha;
    float metallic;
    float smoothness;
};

LitPassFragment中对它赋值,它等于相机坐标减去片元坐标,并进行归一化。

    surface.normal = normalize(input.normalWS);
    surface.viewDirection = normalize(_WorldSpaceCameraPos - input.positionWS);


3.9 高光强度(Specular Strength)

我们观察到的高光反射的强度取决于我们的视角方向与完美反射方向的匹配程度。我们将使用URP中使用的相同公式,它是极简化的Cook-Torrance BRDF的变体。 这个公式包含一些平方操作,所以让我们先在Common.hlsl中添加一个方便的Square函数。

float Square (float v) {
    return v * v;
}

然后在BRDF中添加一个带有SurfaceBRDF数据和Light参数的SpecularStrength函数,这个函数应该计算如下图的公式:

其中d代表饱和度,N表示表面法线,L是光的方向,H表示归一化后的L + V,是一个光的方向和视图方向中间的向量。使用SafeNormalize方法让向量归一化,来避免向量相反的情况下除以0,。最后,n = 4r + 2 是一个标准化的项。

float SpecularStrength (Surface surface, BRDF brdf, Light light) {
    float3 h = SafeNormalize(light.direction + surface.viewDirection);
    float nh2 = Square(saturate(dot(surface.normal, h)));
    float lh2 = Square(saturate(dot(light.direction, h)));
    float r2 = Square(brdf.roughness);
    float d2 = Square(nh2 * (r2 - 1.0) + 1.00001);
    float normalization = brdf.roughness * 4.0 + 2.0;
    return r2 / (d2 * max(0.1, lh2) * normalization);
}


这个方法是如何工作的?
·
BRDF 理论太复杂了,不能简单地解释清楚,也不是本教程的重点。 您可以查看 URPLighting.hlsl文件获得一些代码文档和参考资料。


接下来,添加一个DirectBRDF方法,它将返回通过直接照明获得的颜色。给定一个表面、BRDF数据和光数据。结果是高光颜色由高光强度控制,再加上漫反射颜色。

float3 DirectBRDF (Surface surface, BRDF brdf, Light light) {
    return SpecularStrength(surface, brdf, light) * brdf.specular + brdf.diffuse;
}

GetLighting则必须将入射光乘以该函数的输出。

float3 GetLighting (Surface surface, BRDF brdf, Light light) {
    return IncomingLight(surface, light) * DirectBRDF(surface, brdf, light);
}

光滑度从上到下为0、0.25、0.5、0.75、0.95

我们现在实现了高光反射反射,它为我们的表面添加了高光部分。对于完全粗糙的表面,高光模拟漫反射。光滑的表面得到更集中的亮光。一个完全光滑的表面会有一个我们看不到的无限小的高光。需要一些散射才能使它可见。

由于能量守恒,对于光滑的表面,因为大部分到达表面的光都聚焦了,高光可以变得非常明亮,因此,我们才会看到更多的光,而不是由于高光部分可见的漫反射。你可以通过大幅缩小最终输出的颜色来验证这一点。

最终颜色除以100

你也可以通过使用白色以外的基色,来验证金属材质会影响高光部分的颜色,而非金属材质不会。

蓝色基色

我们现在有了可靠的直接照明的功能,尽管目前的效果还太暗,尤其是对金属材质,因为我们还不支持环境反射等。在这一点上,一个标准的黑色环境会比默认的天空框更真实,但这会让我们的对象更难以观察到。添加更多的灯光也可以做到。

四个光


3.10 网格球体(Mesh Ball)

让我们也为MeshBall.cs添加对不同金属度和平滑度属性的支持。这需要添加两个浮点数组。

static int
        baseColorId = Shader.PropertyToID("_BaseColor"),
        metallicId = Shader.PropertyToID("_Metallic"),
        smoothnessId = Shader.PropertyToID("_Smoothness");

    …
    float[]
        metallic = new float[1023],
        smoothness = new float[1023];

    …

    void Update () {
        if (block == null) {
            block = new MaterialPropertyBlock();
            block.SetVectorArray(baseColorId, baseColors);
            block.SetFloatArray(metallicId, metallic);
            block.SetFloatArray(smoothnessId, smoothness);
        }
        Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1023, block);
    }

让我们在Awake中,把25%的实例设置为金属质感,光滑度在0.05到0.95之间随机。

baseColors[i] =
                new Vector4(
                    Random.value, Random.value, Random.value,
                    Random.Range(0.5f, 1f)
                );
            metallic[i] = Random.value < 0.25f ? 1f : 0f;
            smoothness[i] = Random.Range(0.05f, 0.95f);

然后让网格球体使用一个照明材质。

照亮网格球


4. 透明(Transparency)

让我们再次考虑透明度。物体仍然会根据它们的alpha值变淡,但现在是反射光也变淡了。这对于漫反射是有意义的,因为只有一部分光被反射,而其余的光穿过表面。

变淡的球体

然而,高光反射也会减弱。在全透的玻璃情况下,光要么通过,要么被反射。高光反射部分不会减弱。我们目前的方法不能表现出这一点。


4.1 预乘透明度(Premultiplied Alpha)

解决的办法是只淡化漫反射光,同时保持高光反射的全部强度。Src Blend模式目前不适用于我们的需求,让我们将其设置为One,而目标混合模式仍然使用OneMinusSrcAlpha

更改混合模式

这恢复了高光反射的强度,但漫反射没有减弱。我们通过将使用表面透明度来减弱漫反射颜色来解决这个问题。因此我们用透明度对漫反射进行预乘,而不是稍后依靠GPU去混合。这种方法被称为预乘透明度混合。在GetBRDF里这样做。

brdf.diffuse = surface.color * oneMinusReflectivity;
brdf.diffuse *= surface.alpha;

预乘后的漫反射


4.2 预乘开关(Premultiplication Toggle)

将透明度和漫反射预乘有效地把对象变成像玻璃的材质,而一成不变的透明度混合模式使对象总是只存在部分。让我们同时支持两种方法,通过在GetBRDF中添加一个布尔参数来控制是否对透明度进行预乘,默认设置为false。

BRDF GetBRDF (inout Surface surface, bool applyAlphaToDiffuse = false) {
    …
    if (applyAlphaToDiffuse) {
        brdf.diffuse *= surface.alpha;
    }

    …
}

我们可以使用一个_PREMULTIPLY_ALPHA关键字来决定在LitPassFragment中使用哪种方法,类似于我们之前如何控制透明度裁剪。

#if defined(_PREMULTIPLY_ALPHA)
    BRDF brdf = GetBRDF(surface, true);
#else
    BRDF brdf = GetBRDF(surface);
#endif
float3 color = GetLighting(surface, brdf);
return float4(color, surface.alpha);

Lit的Pass添加一个着色器特性(shader feature)。

#pragma shader_feature _CLIPPING
#pragma shader_feature _PREMULTIPLY_ALPHA

并添加一个开关属性。

[Toggle(_PREMULTIPLY_ALPHA)] _PremulAlpha ("Premultiply Alpha", Float) = 0

预乘透明度开关


5. 着色器的图形用户界面(Shader GUI)

我们现在支持多种渲染模式,每种模式都需要特定的设置。为了使模式之间的切换更容易,让我们在材质面板中添加一些按钮来应用预设配置。


5.1 自定义着色器GUI(Custom Shader GUI)

添加一个CustomEditor "CustomShaderGUI"语句到灯光着色器的主块。

Shader "Custom RP/Lit" {
    …

    CustomEditor "CustomShaderGUI"
}

这指示Unity编辑器使用CustomShaderGUI类的一个实例来绘制使用Lit着色器的材质的面板。为该类创建一个脚本资源,并将其放入一个新的Custom RP/Editor文件夹中。

我们需要使用UnityEditorUnityEngineUnityEngine.Rendering名称空间。这个类必须扩展ShaderGUI并重写公共的OnGUI方法,它有一个MaterialEditor和一个MaterialProperty数组参数。让它调用基类方法,这样我们就得到了默认的显示面板。

using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;

public class CustomShaderGUI : ShaderGUI {

    public override void OnGUI (
        MaterialEditor materialEditor, MaterialProperty[] properties
    ) {
        base.OnGUI(materialEditor, properties);
    }
}


5.2 设置属性和关键字(Setting Properties and Keywords)

为了完成我们的工作,我们需要访问三个东西,我们将它们存储在字段中。首先是材质编辑器,它是底层的编辑器对象,负责显示和编辑材质。第二个是对正在编辑的材质的引用,我们可以通过编辑器的targets属性来访问它,它被定义为一个Object数组,因为targets是通用Editor类的一个属性。第三个是可以编辑的属性数组。

    MaterialEditor editor;
    Object[] materials;
    MaterialProperty[] properties;

    public override void OnGUI (
        MaterialEditor materialEditor, MaterialProperty[] properties
    ) {
        base.OnGUI(materialEditor, properties);
        editor = materialEditor;
        materials = materialEditor.targets;
        this.properties = properties;
    }


为什么会有多种材质?
·
使用相同着色器的多个材质可以同时被编辑,就像你可以选择和编辑多个游戏对象一样。


为了设置一个属性,我们首先必须在数组中找到它,为此我们可以使用ShaderGUI.FindProperty方法,传递给它一个名称和属性数组。然后,我们可以通过给它的floatValue属性赋值来调整它的值。 用一个字符串名称和一个float值参数将其封装在一个方便的SetProperty方法中。

void SetProperty (string name, float value) {
    FindProperty(name, properties).floatValue = value;
}

设置关键字稍微有点复杂。我们将为此创建一个SetKeyword方法,带有一个字符串和一个布尔参数,以指示该关键字是应启用还是禁用。我们必须在所有材质上调用EnableKeywordDisableKeyword,给它们传递关键字名称。

    void SetKeyword (string keyword, bool enabled) {
        if (enabled) {
            foreach (Material m in materials) {
                m.EnableKeyword(keyword);
            }
        }
        else {
            foreach (Material m in materials) {
                m.DisableKeyword(keyword);
            }
        }
    }

让我们再创建一个SetProperty变体,用于同时开关"属性-关键字"。

void SetProperty (string name, string keyword, bool value) {
    SetProperty(name, value ? 1f : 0f);
    SetKeyword(keyword, value);
}

然后定义一些属性的访问器。

bool Clipping {
    set => SetProperty("_Clipping", "_CLIPPING", value);
}

bool PremultiplyAlpha {
    set => SetProperty("_PremulAlpha", "_PREMULTIPLY_ALPHA", value);
}

BlendMode SrcBlend {
    set => SetProperty("_SrcBlend", (float)value);
}

BlendMode DstBlend {
    set => SetProperty("_DstBlend", (float)value);
}

bool ZWrite {
    set => SetProperty("_ZWrite", value ? 1f : 0f);
}

最后,渲染队列通过分配所有材质的RenderQueue属性来设置。

RenderQueue RenderQueue {
    set {
        foreach (Material m in materials) {
            m.renderQueue = (int)value;
        }
    }
}


5.3 预设按钮(Preset Buttons)

可以通过GUILayout.Button方法创建按钮。传递给它一个标签,这将是一个预设的名称。 如果该方法返回true,则代表它被按下。在应用预设之前,我们应该向编辑器注册一个撤销步骤,这可以通过调用RegisterPropertyChangeUndo来完成。 因为这段代码对所有预设都是相同的,所以把它放在PresetButton方法中。

    bool PresetButton (string name) {
        if (GUILayout.Button(name)) {
            editor.RegisterPropertyChangeUndo(name);
            return true;
        }
        return false;
    }

我们将为每个预设创建一个单独的方法,从默认的Opaque模式开始,在激活时恰当地设置属性。

void OpaquePreset () {
        if (PresetButton("Opaque")) {
            Clipping = false;
            PremultiplyAlpha = false;
            SrcBlend = BlendMode.One;
            DstBlend = BlendMode.Zero;
            ZWrite = true;
            RenderQueue = RenderQueue.Geometry;
        }
    }

第二个预设是Clipping

void ClipPreset () {
        if (PresetButton("Clip")) {
            Clipping = true;
            PremultiplyAlpha = false;
            SrcBlend = BlendMode.One;
            DstBlend = BlendMode.Zero;
            ZWrite = true;
            RenderQueue = RenderQueue.AlphaTest;
        }
    }

第三个预设是标准的透明,它会淡出对象,所以我们将其命名为Fade

    void FadePreset () {
        if (PresetButton("Fade")) {
            Clipping = false;
            PremultiplyAlpha = false;
            SrcBlend = BlendMode.SrcAlpha;
            DstBlend = BlendMode.OneMinusSrcAlpha;
            ZWrite = false;
            RenderQueue = RenderQueue.Transparent;
        }
    }

第四种预设是Fade的变体,应用了预乘的透明度混合。 我们将它命名为Transparent

void TransparentPreset () {
        if (PresetButton("Transparent")) {
            Clipping = false;
            PremultiplyAlpha = true;
            SrcBlend = BlendMode.One;
            DstBlend = BlendMode.OneMinusSrcAlpha;
            ZWrite = false;
            RenderQueue = RenderQueue.Transparent;
        }
    }

OnGUI结束时调用预设方法,这样它们就会显示在默认材质面板下面。

public override void OnGUI (
        MaterialEditor materialEditor, MaterialProperty[] properties
    ) {
        …

        OpaquePreset();
        ClipPreset();
        FadePreset();
        TransparentPreset();
    }

预设按钮

预设按钮不会经常使用,所以让我们把它们放在一个折叠标签中。这是通过调用[EditorGUILayout](http://docs.unity3d.com/Documentation/ScriptReference/EditorGUILayout.html).Foldout来完成的。它返回新的折叠状态,我们应该将其存储在一个字段中,只有当折叠打开时才绘制按钮。

bool showPresets;

    …

    public override void OnGUI (
        MaterialEditor materialEditor, MaterialProperty[] properties
    ) {
        …

        EditorGUILayout.Space();
        showPresets = EditorGUILayout.Foldout(showPresets, "Presets", true);
        if (showPresets) {
            OpaquePreset();
            ClipPreset();
            FadePreset();
            TransparentPreset();
        }
    }

可以折叠的预设按钮


5.4 无光照着色器的预设(Presets for Unlit)

我们也可以为我们的无光照着色器使用自定义着色器GUI。

Shader "Custom RP/Unlit" {
    …

    CustomEditor "CustomShaderGUI"
}

然而,激活预设将导致一个错误,因为我们试图设置一个着色器没有的属性。我们可以通过调整SetProperty来防止这种情况。 让它调用FindProperty,并将false作为附加参数,表示如果没有找到该属性,它不应该报告错误。结果将为null,所以只有在不为null的情况下才设置值。

bool SetProperty (string name, float value) {
    MaterialProperty property = FindProperty(name, properties, false);
    if (property != null) {
        property.floatValue = value;
        return true;
    }
    return false;
}

然后调整SetProperty,使它只在相关属性存在的情况下设置关键字。

void SetProperty (string name, string keyword, bool value) {
    if (SetProperty(name, value ? 1f : 0f)) {
        SetKeyword(keyword, value);
    }
}


5.5 非透明(No Transparency)

现在预设也适用于使用无光照着色器的材质,尽管透明模式在这种情况下没有多大意义,因为相关属性不存在。 让我们在不相关的时候隐藏这个预设。

首先,添加一个返回属性是否存在的HasProperty方法。

bool HasProperty (string name) =>
        FindProperty(name, properties, false) != null;

其次,创建一个方便的属性来检查_PremultiplyAlpha是否存在。

    bool HasPremultiplyAlpha => HasProperty("_PremulAlpha");

最后,通过在TransparentPreset中检查,使所有的Transparent相关预设都以该属性为条件。

    if (HasPremultiplyAlpha && PresetButton("Transparent")) { … }

没有透明预设的无光照材质


下一个章节是 平行光阴影(Directional Shadows)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容