找回密码
 立即注册
查看: 255|回复: 0

[简易教程] Unity SRP从零搭建一套图形渲染管线 实践3

[复制链接]
发表于 2023-3-6 11:01 | 显示全部楼层 |阅读模式
参照 UWA上的一个教程:Unity SRP从零搭建一套图形渲染管线_UWA学堂 (uwa4d.com)
这是第三章节
以下图文均来自上面的教程,一些具体步骤做了简略,参考教程即可,这里仅记录一些知识点。
方向光


1.光照

之前的Shader是不受光照的,本节加上光照交互。
1.1 受光照影响的Shader

复制上一节的Shader、HLSL文件,修改为对应的Lit.Shader,LitPass.hlsl文件,并修改对应的方法名




光照改为自定义照明


增加代码中的处理(该Pass的标识符)
static ShaderTagId litShaderTagId = new ShaderTagId("CustomLit");
drawingSettings.SetShaderPassName(1,litShaderTagId);
1.2 法线向量

表面法线,是顶点数据的一部分,我们在项点输入结构体中定义表面法线。照明是逐片元计算的,且往往是在世界空间中计算,我们在片元输入结构体中定义世界空间的法线。
VAR_BASE_UV ,VAR_NORMAL 不是Unity中的语义,是作者随便定义的


顶点着色器增加法线计算


1.3 表面属性

定义一个Surface.hlsl存储表面相关数据,并引入


#include "../ShaderLibrary/Surface.hlsl"
片元函数中存储表面数据


1.4 光照计算

新建Lighting.hlsl用于光照计算,此处用法线的Y值作为光照结果。


LitPass.hlsl包含进来


修改片元着色器获取光照结果作为输出颜色




2.灯光

本节只考虑方向光。
2.1灯光的属性

新建一个Light.hlsl文件来专门存储灯光的数据如下。


引入Light.hlsl,放在引入Lighting.hlsl之前


2.2 光照函数

Lighing.hlsl中加入计算入射光照方法,得到最终照明方法


调整之前的GetLighting方法,使其调用一个重载


2.3 向GPU发送灯光数据

1.接下来我们在Shader中获取场景中默认的那盏方向光的灯光数据


2.编写代码将灯光数据发送给GPU
public class Lighting
{
  const string bufferName = "Lighting";

  CommandBuffer buffer = new CommandBuffer
  {
     name = bufferName
  };

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

    public void Setup(ScriptableRenderContext context)
    {
        buffer.BeginSample(bufferName);
        //发送光源数据
        SetupDirectionalLight();
        buffer.EndSample(bufferName);
        context.ExecuteCommandBuffer(buffer);
        buffer.Clear();
    }

    void SetupDirectionalLight()
    {
        Light light = RenderSettings.sun;
        //灯光的颜色我们再乘以光强作为最终颜色
        buffer.SetGlobalVector(dirLightColorId,light.color.linear * light.intensity);
        buffer.SetGlobalVector(dirLightDirectionId,-light.transform.forward);
    }

}
在CameraRender的Render方法里调用


这里注意被引用的方法要写在前面,否则会报找不到这个方法。感觉也是类似lua那种编译方式


结果小球接收了方向光的照明


2.4 可见光

Unity会在剔除阶段找到哪些光源会影响相机的可见空间,我们在Lighting脚本中获取相机的剔除结果并定义一个字段进行后续追踪。后续我们要支持多个光源,定义一个SetupLights方法来设置和发送多个光源的数据。


2.5 支持多个方向光

1.我们已经获取到了场景中所有的可见光,现在要将这些可见光数据全部发送到GPU。先定义CPU端


2.我们改造SetupDirectionalLight方法。


可见光的finalColor属性已经应用了光照强度,但默认情况下Unity不会将其转换为线性空间,需要如下代码:


把可见光传递给GPU


调整_CustomLight 缓冲区定义,修改GetDirectionalLight 方法,得到对应灯光数据


调整Lighting.hlsl文件中的GetLighting万法,使用for循环对每个可见方向光的照明结果进行累
加,作为最终的照明结果。


如下图,不同朝向的方向光的叠加效果


在Pass中将着色器编译目标级别设置为3.5,该级别越高,允许使用现代GPU的功能越多。如果不设置,Unity默认将着色器编译目标级别设为2.5,介于DirectX着色器模型2.0和3.0之间。但OpenGLES 2.0和WebGL 1.0的图形API是不能处理可变长度的循环的,也不支持线性空间。所以我们在工程构建时可以关闭对OpenGL ES 2.0和WebGL 1.0的支持。


3.BRDF

现在我们的光照模型比较简单,只适用于完全散射的表面,接下来我们使用BRDF (双向反射分布函数)实现更加真实的光照效果,在这里我们将使用和URP一样的BRDF模型。
3.1Metallic 和Smoothnes

在Unity的内置渲染管线中支持两种流行的基于物理的工作流程: 金属工作流和高光反射工作流。其中金属工作流是默认的工作流程,对应的Shader为Standard Shader。如果想要使用高光反射工作流,需要在材质的Shader下拉框选择Standard (Specular setup) 。需要注意的是,使用不同的工作流可以实现相同的效果,只是它们使用的参数不同而已。
1.这里将使用金属工作流,需要为Lit.shader添加两个属性,Metallic和Smoothness。其中Metallic定义了该物体表面看起来是否更像金属或非金属,如果把材质的Metallic值设为1,表明该物体几乎完全是一个金属材质,若设置为0表明该物体几乎没有任何金属特性。Smoothness是Metallic的附属值,定义了从视觉上看该表面的光滑程度,1代表完全光滑,镜面反射最明显,0代表完全粗糙。






片元函数中存储表面的金属度和光滑度


在PerObiectMaterialProperties 脚本中也可以定义这些属性


3.2 BRDF属性

我们将使用表面的属性计算BRDF,它告诉我们最终有多少光从物体的表面反射出去,这是漫反射和镜面反射的组合。我们需要将表面颜色分成漫反射部分和镜面反射部分,还需要知道表面的粗糙度。新建一个BRDF.hlsl


使用#include "../ShaderLibrary/BRDF.hlsl" 引入
修改GetLighting方法,增加BRDF参数


修改片元着色器


3.3 反射率 Reflectivity

1.当使用金属工作流时,物体表面对光线的反射率 (Reflectivity) 会受到Metallic (金属度)的影响,物体的Metallic越大,其自身反照率 (Albedo) 颜色越不明显,对周围环境景象的反射就越清晰,达到最大时就完全反射显示了周围的环境景象。我们调整BRDF的GetBRDF方法,用1减去金属度得到的不反射的值,然后跟表面颜色相乘得到BRDF的漫反射部分。
2.实际上一些电介质(通常不导电物质),如玻璃、塑料等非金属物体,还会有一点光从表面反射出来,平均约为0.04,这给了它们亮点。它将作为我们的最小反射率。


3.4 粗糙度

粗糙度和光滑度相反,只需要使用1减去光滑度即可


引入#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
3.5 视角方向

UnityInput.hlsl 中定义相机位置
float3 _WorldSpaceCameraPos;
顶点函数中存储顶点在世界空间的位置


片元函数中得到视角方向
//得到视角方向
   surface.viewDirection = normalize(_WorldSpaceCameraPos - input.positionWS);
3.6 镜面反射强度

1.镜面反射强度取决于视角方向和完美反射方向的对齐程度,我们使用URP中相同的公式这是简化版Cook-Torrance模型的一种变体.
镜面反射强度的计算公式如下,我们通过表面数据,BRDF数据和光照来计算它


r代表粗糙度,N代表表面法线,L代表光照方向,V代表视角方向,H代表归一化的L+V,它是光和视角方向的中间对角线向量,为了做一个保护,使用SafeNormalize方法进行归一化,避免两个向量在相反的情况下被零除。n代表4r+2,是一个归一化项。
2.接下来可以套用上面的公式进行计算并得到镜面反射强度


3.修改Lighting.hlsl文件的GetLighting方法


此时已经可以看到高光了


4.透明度

当我们调整小球的Alpha值时,小球会渐渐透明化,但镜面反射也会慢慢消失。在实际情况下,比如透明的玻璃,光线会穿过它或者反射出来,镜面反射并不会消失,我们现在还不能做到这一点


4.1 Premultiplied (预乘) Alpha

先说说什么是 Premultiplied Alpha。常见的像素格式为RGBA8888即(r,g,b,a) ,每个通道8位范围在 [0,255] 之间。比如红色50%的透明度可以表示为 (255,0,0,127),PremultipliedAlpha是把RGB的通道也乘上透明度比例,这就是 (r*a,g*a,b*a,a) ,那么红色50%透明度则变成了(127,0,0,127)。使用它的好处是可以让两个像素之间线性插值后颜色结果更合理,使得带透明通道图片的纹理可以进行正常的线性插值。
实现预乘,增加关键字,增加shader开关。










5.ShaderGUI

我们的材质现在支持多种渲染模式,不过切换起来比较麻烦,需要单独配置和进行一些参数调节,我们使用ShaderGUI来对材质面板进行一些扩展,可以很方便的切换各种渲染模式,来一键进行参数配置。
扩展材质面板
参考:ShaderLab: CustomEditor - 简书 (jianshu.com)
使用CustomEditor扩展面板


此处直接贴出代码
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
/// <summary>
/// 扩展材质面板
/// </summary>
public class CustomShaderGUI : ShaderGUI
{
    MaterialEditor editor;
    Object[] materials;
    MaterialProperty[] properties;
    bool showPresets;
    public override void OnGUI(
        MaterialEditor materialEditor, MaterialProperty[] properties
    )
    {
        base.OnGUI(materialEditor, properties);
        editor = materialEditor;
        materials = materialEditor.targets;
        this.properties = properties;

        EditorGUILayout.Space();
        showPresets = EditorGUILayout.Foldout(showPresets, "Presets", true);
        if (showPresets)
        {
            OpaquePreset();
            ClipPreset();
            FadePreset();
            TransparentPreset();
        }
    }
    /// <summary>
    /// 设置材质属性
    /// </summary>
    /// <param name="name"></param>
    /// <param name="value"></param>
    /// <returns></returns>
    bool SetProperty(string name, float value)
    {
        MaterialProperty property = FindProperty(name, properties, false);
        if (property != null)
        {
            property.floatValue = value;
            return true;
        }
        return false;
    }
    /// <summary>
    /// 设置关键字状态
    /// </summary>
    /// <param name="keyword"></param>
    /// <param name="enabled"></param>
    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);
            }
        }
    }
    /// <summary>
    /// 相关属性存在时可以设置关键字开关
    /// </summary>
    /// <param name="name"></param>
    /// <param name="keyword"></param>
    /// <param name="value"></param>
    void SetProperty(string name, string keyword, bool value)
    {
        if (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
    {
        set
        {
            foreach (Material m in materials)
            {
                m.renderQueue = (int)value;
            }
        }
    }
    bool PresetButton(string name)
    {
        if (GUILayout.Button(name))
        {
           
            editor.RegisterPropertyChangeUndo(name);
            return true;
        }
        return false;
    }
    /// <summary>
    /// 不透明材质默认设置
    /// </summary>
    void OpaquePreset()
    {
        if (PresetButton("Opaque"))
        {
            Clipping = false;
            PremultiplyAlpha = false;
            SrcBlend = BlendMode.One;
            DstBlend = BlendMode.Zero;
            ZWrite = true;
            RenderQueue = RenderQueue.Geometry;
        }
    }
    /// <summary>
    /// 裁切材质默认设置
    /// </summary>
    void ClipPreset()
    {
        if (PresetButton("Clip"))
        {
            Clipping = true;
            PremultiplyAlpha = false;
            SrcBlend = BlendMode.One;
            DstBlend = BlendMode.Zero;
            ZWrite = true;
            RenderQueue = RenderQueue.AlphaTest;
        }
    }
    /// <summary>
    /// 标准透明材质默认设置
    /// </summary>
    void FadePreset()
    {
        if (PresetButton("Fade"))
        {
            Clipping = false;
            PremultiplyAlpha = false;
            SrcBlend = BlendMode.SrcAlpha;
            DstBlend = BlendMode.OneMinusSrcAlpha;
            ZWrite = false;
            RenderQueue = RenderQueue.Transparent;
        }
    }
    //如果shader的预乘属性不存在,不需要显示该渲染模式的按钮
    bool HasProperty(string name) => FindProperty(name, properties, false) != null;
    bool HasPremultiplyAlpha => HasProperty("_PremulAlpha");
    /// <summary>
    /// 受光正确的透明材质默认设置
    /// </summary>
    void TransparentPreset()
    {
        if (HasPremultiplyAlpha && PresetButton("Transparent"))
        {
            Clipping = false;
            PremultiplyAlpha = true;
            SrcBlend = BlendMode.One;
            DstBlend = BlendMode.OneMinusSrcAlpha;
            ZWrite = false;
            RenderQueue = RenderQueue.Transparent;
        }
    }
}
效果

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-4-28 08:06 , Processed in 0.440217 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表