Custom SRP - Directional Lights

发布于:2025-07-31 ⋅ 阅读:(21) ⋅ 点赞:(0)

这是一篇学习笔记,原文地址:https://catlikecoding.com/unity/tutorials/custom-srp/directional-lights/

本节开始书写支持光照的 shader。

  • 利用法线计算光照

  • 支持最多4个方向光源

  • 应用BRDF

  • 支持半透明材质

  • 定制shader GUI

1. Lighting

光照是模拟光线与表面的交互的计算过程。

1.1 Lit Shader

从 UnlitPass.hlsl 复制一份,改名为 LitPass.hlsl。修改 include 保护 和 vertex fragment 函数名:

#ifndef CUSTOM_LIT_PASS_INCLUDED
#define CUSTOM_LIT_PASS_INCLUDED
…

Varyings LitPassVertex (Attributes input) { … }

float4 LitPassFragment (Varyings input) : SV_TARGET { … }

#endif

同样的,复制 Unlit.shader 并改名为 Lit.shader,并将文件内定义的顶点/像素着色程序,改为上面定义的函数.

Shader "Custom RP/Lit"
{
    Properties { ... }
    SubShader
    {
        Pass
        {
            ...
            #pragma vertex LitPassVertex
            #pragma fragment LitPassFragment
            #include "LitPass.hlsl"
            ...
        }
    }
}

我们要定义自己的光照方式,并在 Pass 中定义 light mode 为 CustomLit 来作为标识:

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

在 CameraRenderer 中,定义 shader 标识,并通过 DrawingSettings 进行配置:

...
static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");
static ShaderTagId litShaderTagId = new ShaderTagId("CustomLit");
...
    
void DrawVisibleGeometry(bool useDynamicBatching, bool useGPUInstancing)
{
    // 渲染不透明物体
    var sortingSettings = new SortingSettings(camera) { criteria = SortingCriteria.CommonOpaque };
    var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings)
    { enableDynamicBatching = useDynamicBatching, enableInstancing = useGPUInstancing };
    // 索引是 1,因为索引为 0 的通过构造函数将 unlitShaderTagId 设置了
    drawingSettings.SetShaderPassName(1, litShaderTagId);
    ...
}  

现在,可以创建 Lit 材质了

 

1.2 Normal Vectors

通过法线,定义光的入射方向和表面的相对关系,并基于该相对关系计算光照,因此法线十分重要。

首先为顶点着色器输入定义法线属性,同时光照是在像素着色器中计算的,因此需要传递给像素着色器:

struct Attributes
{
    float3 positionOS : POSITION;
    float3 normalOS : NORMAL;        // 法线
    float2 uv : TEXCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct Varyings
{
    float4 positionCS : SV_POSITION;
    float3 normalWS : VAR_NORMAL;    // 法线
    float2 uv : TEXTCOORD0;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

因为光照是在世界空间中计算的,因此在顶点着色器中,将顶点法线从模型空间变换到世界空间。

Varyings LitPassVertex(Attributes input)
{
    Varyings output;
    ...
    output.normalWS = TransformObjectToWorldNormal(input.normalOS);
    return output;
}

float4 LitPassFragment(Varyings input) : SV_TARGET
{
    return float4(input.normalWS, 1.0f);
}

在像素着色器中,我们先把法线作为颜色输出,看看是什么效果:

法线的分量有可能是负的,这部分值会被夹取为0.黑色的部分表示全是负的或0.

1.3 法线插值

虽然顶点法线都是归一化的,但是在顶点着色器中变换到世界空间,通过三角形进行线性插值后,它们就不再是归一化的了,通过修改像素着色器,我们可以看到效果:

因此,在像素着色器中使用前,要重新进行归一化:

float3 normalWS = normalize(input.normalWS);

1.4 Surface Properties

光照是模拟光线跟它击中的表面的相互作用,因此表面属性十分重要,常用。同时为了代码整洁,我们把表面属性作为一个结构体定义在 Surface.hlsl 中:

#ifndef UNITY_SURFACE_INCLUDED
#define UNITY_SURFACE_INCLUDED

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

#endif

 并在 LitPass.hlsl 中,在 Common.hlsl 之前包含它。

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

在像素着色器中定义 Surface 类型的变量,并进行填充:

float4 LitPassFragment(Varyings input) : SV_TARGET
{
    ...    
    Surface surface;
    surface.normal = normalize(input.normalWS);
    surface.color = base.rgb;
    surface.alpha = base.a; 
    return float4(surface.color, surface.alpha);
}

1.5 Calculating Lighting

我们创建一个计算光照的函数 GetLighting,并且以 Surface 作为参数.将这个函数放在 Lighting.hlsl 中,后面光照相关的函数,都放在该文件中.

#ifndef CUSTOM_LIGHTING_INCLUDED
#define CUSTOM_LIGHTING_INCLUDED

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

#endif

在 LitPass.hlsl 中,Surface.hlsl 之后,包含该文件.然后在像素着色器中调用该函数

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

...

float4 LitPassFragment(Varyings input) :SV_TARGET
{
    ...
    float3 color = GetLighting(surface);
    return float4(color, surface.alpha);
}

效果是这样的:

 

我们把表面法线的 Y 当作光照颜色返回了,因此在顶部 Y 是 1,然后向下到中间靠下时变成0,到底部最终变成 -1 . 小于 0 的值会被限制为0,因此我们看不到负值的颜色.这恰好匹配法线和 up vector 夹角的余弦.忽略负值的部分,看起来是垂直向下的方向光的效果.

表面的颜色也叫 Albedo ,我们将其应用到光照运算上:

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

2. Lights

要执行正确的光照运算,我们需要知道光的属性.当前教程中,我们仅实现方向光.

方向光表示的光源距离足够远,仅需要关心其方向,而忽略其位置.这种简化对于像照射到地球上的太阳/月光,以及那些单向(不发散)光来说足够了.

2.1 Light Structure

我们用一个包含颜色和方向的结构体来存储光源数据,并将其放入 Light.hlsl 中.将来其它类型的光源也会定义在该文件中.

同时定义函数,返回我们临时硬编码的方向光.注意我们的光源方向是指向光源的,而不是光前进的方向.

#ifndef LIGHT_INCLUDED
#define LIGHT_INCLUDED

struct Light{
    float3 color;
    float3 direction;
}

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

#endif

在 Lit.hlsl 中,Lighting.hlsl 之前包含该文件

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

2.2 Lighting Function

在 Lighting.hlsl 中定义光照计算函数,根据给定的表面和光源计算光照.我们用表面法线和光源方向的点积,乘以光的颜色来计算光照.

如果点积为负,表示表面方向与光的方向(指向光源)相反,不被照亮,因此通过 satureate 函数钳制为0.

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

完善光照函数应用我们刚刚定义的计算函数.同时提供一个仅以表面为参数的光照函数:

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

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

2.3 Sending Light Data to the GPU

运行时,光源在 CPU 端维持,并更新.需要在渲染时将光源数据上传到 GPU.

我们的管线最多支持4个方向光,因此需要定义常量.

首先在 Light.hlsl 中定义光源的 constant buffer

#define MAX_DIRECTIONAL_LIGHT_COUNT 4

CBUFFER_START(_CustomLights)
int _DirectionalLightCount;
float4 _DirectionalLightColors[MAX_DIRECTIONAL_LIGHT_COUNT];
float4 _DirectionalLightDirections[MAX_DIRECTIONAL_LIGHT_COUNT];
CBUFFER_END

int GetDirectionalLightCount(){
    return _DirectionalLightCount;
}

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

然后在光照函数中遍历所有光源,计算光照,并累加他们:

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

着色器中的光源数据来源于 CPU,需要我们上传到GPU. 我们把这部分逻辑独立到 Lighting.cs 中.

  • 同样定义最大方向光数量为4

  • 缓存 shader 常量 ID

  • 记录方向光数据

  • 创建 CommandBuffer 用来上传数据

const int MaxDirLightCount = 4;

static int dirLightCountId = Shader.PropertyToID("_DirectionalLightCount");
static int dirLightColorsId = Shader.PropertyToID("_DirectionalLightColors");
static int dirLightDirectionsId = Shader.PropertyToID("_DirectionalLightDirections");

static Vector4[] dirLightColors = new Vector4[MaxDirLightCount];
static Vector4[] dirLightDirections = new Vector4[MaxDirLightCount];

const string bufName = "Lighting";
CommandBuffer cmdBuf = new CommandBuffer(){ name = bufName };

光的颜色由 VisibleLight.finalColor 提供,它其实是由 light.color.linear * light.intensity 计算的来的.不过要使用 finalColor,需要在管线实例的构造函数中启用:

public CustomPipeline(...){
    ...
    GraphicsSettings.lightsUseLinearIntensity = true;
}

定义函数,将方向光数据缓存下来.为了避免参数传递导致的内存开销,我们将参数定义为 ref

// VisibleLight 是摄像机裁剪得到的光源
void SetupDirectionalLight(int index, ref VisibleLight light){
    dirLightColors[index] = light.finalColor;
    dirLightDirections[index] = -light.localToWorldMatrix.GetColumn(2);
}

定义接口函数,完成光源数据的上传.摄像机可见的光源同样在裁剪结果中,因此需要参数将 CullingResults 传进来.

光源数据混存好后,上传到 GPU

public void Setup(ScriptableRenderContext context, CullingResults cullingResults){
    Native<VisibleLight> visibleLights = cullingResults.visibleLights;
    int dirLightCount = 0;
    for(int i = 0; i < visibleLights.Length; ++i){
        VisibleLight light = visibleLights[i];
        if(light.lightType == LightType.Directional
            && dirLightCount < MaxDirLightCount) {
            SetupDirectionalLight(dirLightCount, ref light);
            dirLightCount++;
        }
    }
    
    cmdBuf.SetGlobalInt(dirLightCountId, dirLightCount);
    cmdBuf.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
    cmdBuf.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
}

下面的场景中创建了2个方向光,看看效果:

2.4 Shader Target Level

hlsl 代码中的循环,曾经代表低效代码,但是现代 GPU 都能够解决这个问题,尤其是当一个 draw call 的所有 fragment 都是以同样的循环次数计算时更不会成为问题。但是 OpenGL ES 2.0 和 WebGL 1.0 图形 API 不能高效的处理循环,因此我们的管线不对它们支持。

在 Lit.shader 中,加入目标版本定义来实现我们的目的:

HLSLPROGRAM
#pragma target 3.5
...

3. BRDF

前面用到的极简的光照,仅适用于完美的漫反射表面.下面我们引入效果更加真实的BRDF(bidirectional reflectance distribution function 双向反射分布函数).

3.1 incoming light 入射光

当一束光自顶向下照射到像素,这束光的所有能量都会作用到这个像素.简单起见,我们假定光束的宽度等于像素的宽度,那么这种情况下光线方向L和法线方向 N一致,他们的点积 dot(N,L)=1.当他们的方向不一致时,至少有部分光束不会照射到像素,因此作用到像素的能量变少,这部分能量是 dot(N,L).如果点积的结果是负的,意味着光束和表面的方向一定程度上是相反的,因此像素完全不受光束的影响.

3.2 outgoing light 出射光

我们并不能看到到达表面的光,而是看到从表面反射的进入我们眼镜(摄像机)的光.如果表面上一个绝对平面的镜子,那么光束以跟入射角度相同的出射角度,被反射出去,当摄像机方向与反射方向一致时,就能看到这部分光.这就是我们说的高光反射.

 当表面不是完美的平面时,光就会散射开,因为表面是由很多微小的表面构成,这些微表面的朝向各不相同,将光线分成方向不同的细小光线.即使观察方向与反射方向不完全一致,也能看到这一散射的光线.

 此外,还有部分光线会穿透进物质内部,部分被吸收,部分在内部经过几次反弹后,最终以随机的方向反射出来.如果情况极端一些,所有光线全部进入物质内部并反弹出来,那么就是完全的漫反射.

无论摄像机在哪儿,进入摄像机的漫反射光的量都是一样的.但这意味着我们看到的光远小于照射到表面的光.因此建议通过一个系数对入射光进行缩放.然而由于这个系数是固定的,因此将这个系数直接烘焙(存储到)光的颜色和强度.

3.3 Surface Properties

表面可以是完全漫反射,或完全镜面反射,或介于二者之间.有多种方式来控制这些效果,我们选择其中的金属度模型(metallic),因此需要向 Lit.shader 中加入两个属性:

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

将变量添加的 UnityPerMaterial 中去

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
...
float _Metallic;
float _Smoothness;
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

添加到 Surface 结构体中

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

并在 LitPassFragment 函数中,将参数从 instance 数据读取出来,复制到 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 也加入支持这两个属性

[DisallowMultipleComponent]
public class PerObjectMaterialProperties : MonoBehaviour
{
    static int baseColorId = Shader.PropertyToID("_BaseColor");
    static int metallicId= Shader.PropertyToID("_Metallic");
    static int smoothnessId = Shader.PropertyToID("_Smoothness");

    [SerializeField]
    private Color baseColor = Color.white;

    [SerializeField]
    private float metallic = 0.0f;

    [SerializeField]
    private float smoothness = 0.5f;

    static MaterialPropertyBlock matPropBlock;

    private void Awake()
    {
        OnValidate();
    }

    private void OnValidate()
    {
        if (matPropBlock == null)
            matPropBlock = new MaterialPropertyBlock();

        matPropBlock.SetColor(baseColorId, baseColor);
        matPropBlock.SetFloat(metallicId, metallic);
        matPropBlock.SetFloat(smoothnessId, smoothness);
        GetComponent<Renderer>().SetPropertyBlock(matPropBlock);

    }
}

3.4 BRDF Properties

通过表面属性计算 BRDF 公式,获得从表面反射出来的光,反射光由漫反射和高光反射组成。我们需要把表面颜色分成漫反射和高光反射两部分,同时需要知道表面的粗糙度。将这些数据定义到文件 BRDF.hlsl 中的 BRDF 结构体中

然后定义一个获取 BRDF 的函数。我们从完全漫反射开始,因此漫反射等于颜色值,而高光反射为0,粗糙度为1

#ifndef BRDF_INCLUDED
#define BRDF_INCLUDED

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

BRDF GetBRDF(Surface surface)
{
    BRDF brdf;
    brdf.diffuse = surface.color;
    brdf.specular = 0;
    brdf.roughness = 1;
}

#endif

将 BRDF.hlsl 包含进 LitPass.hlsl 

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

3.5 Reflectivity 反射率

表面的反射程度都是不同的,但是总的来说,金属通过高光反射反射所有光,所以漫反射为0.因此我们定义反射率等于表面的金属度。反射出去的光当然就不会再参与漫反射了,所以在 GetBRDF 中,漫反射的光用 1-metallic 进行缩放

现实中,绝缘体的表面也会反射一些光,带来一点高光。非金属的反射率各不相同,但平均值大概是 0.04,因此定义最小反射率为 0.04

#define MIN_REFLECTVITY 0.04

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

BRDF GetBRDF(Surface surface)
{
    BRDF brdf;
    brdf.diffuse = surface.color * OneMinusReflectivity(surface.metallic);
    brdf.specular = 0;
    brdf.roughness = 1;
    return brdf;
}
标题金属度分别为:0,0.25,0.5,0.75,1

3.6 Specular Color 高光颜色

在高光反射+漫反射中,光通过一种方式进行反射,就不能再参与另一种反射了,这叫能量守恒,也就是说反射光不能超出入射光。所以高光颜色等于 surface color - diffuse color。

同时,由于金属会影响高光反射的颜色,而非金属不会,且绝缘体的高光颜色是白色。通过利用金属度,在 MIN_REFLECTVITY 和surface.color 之间插值来达到这个效果。

BRDF GetBRDF(Surface surface)
{
    BRDF brdf;
    brdf.diffuse = surface.color * OneMinusReflectivity(surface.metallic);
    brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic);
    brdf.roughness = 1;
    return brdf;
}

3.7 Roughness 粗糙度

粗糙度正好与光滑度相反,因此简单通过1-smoothness 可以得到粗糙度。Core RP Library 中有实现该功能的函数 PerceptualSmoothnessToRoughness,我们直接使用该函数,以表明平滑度以及由此产生的粗糙度都是基于感知的。该函数符合迪士尼的光照模型,且使用该函数进行转换,在编辑材质时更符合直觉。

BRDF GetBRDF(Surface surface)
{
    BRDF brdf;
    brdf.diffuse = surface.color * OneMinusReflectivity(surface.metallic);
    brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic);
    float perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surface.smoothness);
    brdf.roughness = PerceptualRoughnessToRoughness(perceptualRoughness);
    return brdf;
}

两个转换函数定义在 CommonMaterial.hlsl 中,因此需要在我们的 Common.hlsl 中包含它 

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

3.8 View Direction 观察方向

计算观察方向需要知道摄像机位置,在 UnityInput.hlsl 中添加变量 float3 _WorldSpaceCameraPos, unity 渲染时会帮我们将该变量准备好.

float3 _WorldSpaceCameraPos

在着色器中计算观察方向,还需要将顶点的世界坐标传入像素着色器

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

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

在 LitPassFragment 中计算观察方向,并将其存储到 Surface 中去

Surface surface;
surface.normal = normalize(input.normalWS);
surface.viewDirection = normalize(_WorldSpaceCameraPos - input.positionWS);
surface.color = base.rgb;

3.9 Specular Strength 高光强度

高光反射到强度,跟观察方向和出射方向的一致性有关. URP 中使用了 Minimalist CookTorrance BRDF 的一种形式,我们也使用该公式.该公式多次用到了平方公式,因此在 common.hlsl 中定义给函数

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

然后在 BRDF.hlsl 中实现 SpecularStrength 函数

 

  • r 是粗糙度

  • 所有点积都要 saturate

  • N 是表面法线

  • L 光线方向

  • H = normalize(N+L), 是 N L 的半角

  • n = 4r + 2 用来规范化.

// 计算高光强度
float SpecularStrength(Surface surface, BRDF brdf, Light lihgt)
{
    float r2 = Square(brdf.roughness);
    float3 h = normalize(surface.normal + surface.viewDirection);
    float d = Square(dot(surface.normal, h))*(r2-1.0f) + 1.0001f;
    float d2 = Square(d);
    float lh2 = Square(dot(surface.normal, h));
    float n = 4 * brdf.roughness + 2;
    float strength = r2 / (d2*max(0.1,lh2) *n);
    return strength;
}

// 计算BRDF光照颜色 强度*高管颜色+漫反射颜色
float3 DirectBRDF(Surface surface, BRDF brdf, Light lihgt)
{
    return SpecularStrength(surface, brdf, lihgt) * brdf.specular + brdf.diffuse;
}

计算光照的函数: 入射光颜色 * BRDF 颜色 

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

自左到右,metallic: 0 0.25 0.5 0.75 1 . 自上而下,roughness: 0 0.22 0.5 0.75 0.95

我们现在获得了高光反射让表面看起来有高光.对于绝对粗糙的表面,高光跟漫反射差不多,表面越光滑,高光越集中.绝对光滑的表面,会有一个极小的高光光斑,得需要加一点点散射才能看到.

基于能量守恒,高光在平滑表面会非常亮,这是由于大多数到达表面像素的光都被集中反射了.当看到高光时,我们将看到远亮过漫反射的光.

通过降低颜色值,使其*0.05,可以明显看到,即使周围都完全没有漫反射光(黑色)了,高光的地方仍然很亮:

 将材质颜色改为蓝色,则可以看到,高光会影响颜色,而非高光不会.如下图,非高光部分依然是材质的蓝色,而高光部分则是白色.

 

现在我们有一个还不错的光照,但是由于没有考虑环境光反射,所以看起来有点暗,尤其是金属材质.

修改 MeshBall 脚本,加入金属度和光滑度参数,用 instance 进行渲染:

4. Transparency 透明

现在给材质加入透明的支持.对象依然是基于其 alpha 值淡出,只不过现在淡出的是反射的光.对于漫反射,由于部分光反射,而其余的穿过了表面,这看起来是合理的.

 

但是现在高光也变淡了,实际对于绝对干净的玻璃来说,光要么穿过,要么反射,高光反射不会变淡,现在的方法是错的.

4.1 Premultiplied Alpha 预乘 alpha

解决方案是仅将 diffuse 变淡,高光强度保持不变.修改混合模式, SrcBlend = One, DstBlend 不变,依然是 OnMinusSrcAlpha

 现在高光保持住了,但是漫反射也不会变淡了.我们通过将 surface alpha 乘到 diffuse 上来解决.

 

4.2 Premultiplication Toggle 预乘开关

预乘 alpha 让对象看起来像是玻璃,普通的 alpha blend 只是让对象部分可见,这两种情况都是需要支持的,所以我们要加入一个 shader feature ,并在材质面板增加开关

[Toggle(_PREMULTIPLY_ALPHA)]_PreMultiplyAlpha("Premultiply Alpha", Float) = 0
...
#pragma shader_feature _CLIPPING
#pragma shader_feature _PREMULTIPLY_ALPHA

然后修改像素着色器 

#if defined(_PREMULTIPLY_ALPHA)
    BRDF brdf = GetBRDF(surface, true);
#else 
    BRDF brdf = GetBRDF(surface, false);
#endif

下面是对比效果.左边预乘,右边没有预乘:

5. Shader GUI

我们现在支持多种渲染效果,每种都需要一些设置.为了简化在渲染模式之间的切换,现在向材质面板增加几个按钮,来应用我们预设的参数.

5.1 Custom Shader GUI

在 Lit.shader 中添加 CustomEditor "CustomeShaderGUI" 语句,通知 Unity 使用 CustomShaderGUI.cs 绘制该材质面板:

Shader "Custom RP/Lit"
{
    ...
    CustomEditor "CustomShaderGUI"
    ...
}

 在 Custom RP/Editor 目录下创建 CustomShaderGUI.cs.

public class CustomShaderGUI : ShaderGUI
{
    private MaterialEditor materialEditor;
    private Object[] materials;
    private MaterialProperty[] properties;
    
    public override void OnGUI(MaterialEditor matEditor, MaterialProperty[] props)
    {
        base.OnGUI(matEditor, props);
        
        // 缓存下需要的对象
        materialEditor = matEditor;
        materials = matEditor.targets;
        properties = props;

        // 绘制 peset 按钮
        OpaquePreset();
        ClipPreset();
        FadePreset();
        TransparentPreset();
    }

    // 设置属性的接口.如果没有制定的属性则什么也不做
    void SetProperty(string name, float value)
    {
        MaterialProperty prop = FindProperty(name, properties, false);
        if(prop != null)
            prop.floatValue = value;
    }

    // 启用/禁用 keyword
    void SetKeyword(string keyword, bool enable)
    {
        if (enable)
        {
            foreach(Material m in materials)
                m.EnableKeyword(keyword);
        }
        else
        {
            foreach (Material m in materials)
                m.DisableKeyword(keyword);
        }
    }

    // keyword 和一个 bool 属性关联,因此提供接口统一设置
    void SetProperty(string name, string keyword, bool v)
    {
        SetKeyword(keyword, v);
        SetProperty(name, v ? 1 : 0);
    }

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

    bool PremultiplyAlpha
    {
        set => SetProperty("_PreMultiplyAlpha", "_PREMULTIPLYALPHA", value);
    }

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

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

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

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

    bool HasProperty(string name)
    {
        return FindProperty(name, properties, false) != null;
    }

    // 通用的按钮接口
    private bool PresetButton(string name)
    {
        if (GUILayout.Button(name))
        {
            // 支持 undo
            materialEditor.RegisterPropertyChangeUndo(name);
            return true;
        }
        return false;
    }

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

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

    // 普通的半透明
    void FadePreset()
    {
        if (PresetButton("Fade"))
        {
            Clipping = false;
            PremultiplyAlpha = false;
            SrcBlend = BlendMode.SrcAlpha;
            DstBlend = BlendMode.OneMinusSrcAlpha;
            ZWrite = false;
            RenderQueue = RenderQueue.Transparent;
        }
    }

    // 带预乘 alpha 的半透明
    void TransparentPreset()
    {
        if (HasProperty("Transparent") && PresetButton("Transparent"))
        {
            Clipping = false;
            PremultiplyAlpha = true;
            SrcBlend = BlendMode.One;
            DstBlend = BlendMode.OneMinusSrcAlpha;
            ZWrite = false;
            RenderQueue = RenderQueue.Transparent;
        }
    }
}

在材质编辑界面,点击这些按钮,可以看到效果 

 

5.2 Presets for Unline

可以用该 shader gui 为 unlit.shader 通过预设按钮.

但是由于 unlit 没有预乘 alpha 的属性,因此效果看起来很怪,因此我们需要在 unlit 中不现实这个按钮,所以需要在显示按钮前,检查是否有该属性

bool HasProperty(string name)
{
    return FindProperty(name, properties, false) != null;
}

void TransparentPreset()
{
    if (HasProperty("Transparent") && PresetButton("Transparent"))
    {
        ...
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到