Unity URP 渲染管线着色器编程 104 之 镜头光晕(lensflare)的实现

2023-10-29

在SRP管线中是不支持Unity原有的LensFlare配置的,也就是说如果在URP或者HDRP管线下要使用Lens Flare,需要自己实现改功能。

本节介绍一个HDRP的官方DEMO工程 FontainebleauDemo 中使用到的LensFlare功能。该LensFlare模块只需少量的修改就可以在URP管线下使用,但是如果要适配移动端设备,需要做一些修改,同时也可以进行一些性能优化。

当然LensFlare的实现不止这一种思路,也可以在屏幕后处理里得到同样的效果,但是屏幕后处理对于移动端设备来说过于昂贵,不建议这样做。


最终效果如下:
在这里插入图片描述


设计思路

镜头光晕效果本质上是由多个面片,按照设定的顺序,大小叠加出来的效果。
如下图所示

  • 使用的原始贴图
    在这里插入图片描述

  • 多个面片,按照不同方位大小进行排列
    在这里插入图片描述

  • 对应的效果
    在这里插入图片描述

有了这个思路,我们的LensFlare就需要如下几个功能:
  1. 一个CSharp脚本用来根据需要(LensFlare的设置)生成指定数量的面片(Quad),并且每个面片要有所需的大小,角度等。
  2. 用来渲染面片的着色器,使用类似粒子特效的透明材质,可选Alpha Blend的模式(Additive,Premultiply,DefaultTransparent)。
  3. 要实现遮罩,避免太阳被遮挡时,依然能看到镜头光晕。
对于这三个功能,我们出于性能考虑做一点点变动。
  • 因为镜头光晕会根据摄像机的朝向进行移动,面片的大小和旋转以及位置都通过GPU在顶点着色器中进行处理,避免在CPU中频繁创建生成mesh。
  • 遮罩的实现,使用在顶点着色器中使用均匀的采样点对深度图进行多次采样,来得到遮罩比例。

优化

对于原始的实现有如下优化策略或者移动端的适配可以使用:

  • 目前的实现使用了多个独立的subMesh来绘制多个面片,但是可以使用分UV的方式,把多张flare贴图合成一张图,并且在一个mesh上进行绘制,可以减少到一次drawcall。原始的Builtin管线里的lensflare便是如此的实现。
  • 部分移动端低端设备不支持在顶点着色器里采样深度贴图,可以改为在像素着色器中进行采样,只是如此会带来额外的开销。
  • 目前的实现,把一些配置参数通过mesh的额外uv1,uv2,uv3传入shader,而且使用了float4 格式的uv,但是float4格式的uv在移动端可能不支持,因此需要调整为float2格式,而这样有可能导致uv坐标不够的情况,mesh只支持最多四套uv(uv0,uv1,uv2,uv3)。因此要优化掉部分通过uv传递的餐宿
    1. 一些参数如:lensflare在世界空间下的中心坐标,可以省略掉(可以在着色器里通过把模型空间下的(0,0,0)坐标转换到世界坐标来得到(0,0,0)对应的世界坐标)。
    2. 另外一些参数,因为是对所有面片通用的,例如 Near Start Distance /Near End Distance / Far Start Distance / Far End Distance这四个参数可以直接在shader里使用硬编码或者放在constant buffer里,避免占用额外的UV坐标。

上述优化并不包含在下面的代码里,如果把该功能应用到实际的移动端项目里,需要自己执行这些优化操作

完整代码如下

CSharp脚本代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class LensFlare : MonoBehaviour
{
  [SerializeField, HideInInspector]
  MeshRenderer m_MeshRenderer;
  [SerializeField, HideInInspector]
  MeshFilter m_MeshFilter;
  [SerializeField]
  Light m_Light;
  
  [Header("Global Settings")]
  public float OcclusionRadius = 1.0f;
  public float NearFadeStartDistance = 1.0f;
  public float NearFadeEndDistance = 3.0f;
  public float FarFadeStartDistance = 10.0f;
  public float FarFadeEndDistance = 50.0f;
  
  [Header("Flare Element Settings")]
  [SerializeField]
  public List<FlareSettings> Flares;
  
  void Awake()
        {
            if (m_MeshFilter == null)
                m_MeshFilter = GetComponent<MeshFilter>();
            if (m_MeshRenderer == null)
                m_MeshRenderer = GetComponent<MeshRenderer>();

            m_Light = GetComponent<Light>();

            m_MeshFilter.hideFlags = HideFlags.None;
            m_MeshRenderer.hideFlags = HideFlags.None;

            if (Flares == null)
                Flares = new List<FlareSettings>();

            m_MeshFilter.mesh = InitMesh();
        }

        void OnEnable()
        {
            UpdateGeometry();
        }


        // Use this for initialization
        void Start ()
        {
            m_Light = GetComponent<Light>();
        }

        void OnValidate()
        {
            UpdateGeometry();
            UpdateMaterials();
        }

        // Update is called once per frame
        void Update ()
        {
            // Lazy!
            UpdateVaryingAttributes();
        }

        Mesh InitMesh()
        {
            Mesh m = new Mesh();
            m.MarkDynamic();
            return m;
        }

        void UpdateMaterials()
        {
            Material[] mats = new Material[Flares.Count];

            int i = 0;
            foreach(FlareSettings f in Flares)
            {
                mats[i] = f.Material;
                i++;
            }
            m_MeshRenderer.sharedMaterials = mats;
        }

        void UpdateGeometry()
        {
            Mesh m = m_MeshFilter.sharedMesh;

            // Positions
            List<Vector3> vertices = new List<Vector3>();
            foreach (FlareSettings s in Flares)
            {
                vertices.Add(new Vector3(-1, -1, 0));
                vertices.Add(new Vector3(1, -1, 0));
                vertices.Add(new Vector3(1, 1, 0));
                vertices.Add(new Vector3(-1, 1, 0));
            }
            m.SetVertices(vertices);

            // UVs
            List<Vector2> uvs = new List<Vector2>();
            foreach (FlareSettings s in Flares)
            {
                uvs.Add(new Vector2(0, 1));
                uvs.Add(new Vector2(1, 1));
                uvs.Add(new Vector2(1, 0));
                uvs.Add(new Vector2(0, 0));
            }
            m.SetUVs(0, uvs);

            // Variable Data
            m.SetColors(GetLensFlareColor());
            m.SetUVs(1, GetLensFlareData());
            m.SetUVs(2, GetWorldPositionAndRadius());
            m.SetUVs(3, GetDistanceFadeData());

            m.subMeshCount = Flares.Count;

            // Tris
            for (int i = 0; i < Flares.Count; i++)
            {
                int[] tris = new int[6];
                tris[0] = (i * 4) + 0;
                tris[1] = (i * 4) + 1;
                tris[2] = (i * 4) + 2;
                tris[3] = (i * 4) + 2;
                tris[4] = (i * 4) + 3;
                tris[5] = (i * 4) + 0;
                m.SetTriangles(tris, i);
            }

            Bounds b = m.bounds;
            b.extents = new Vector3(OcclusionRadius, OcclusionRadius, OcclusionRadius);
            m.bounds = b;
            m.UploadMeshData(false);
        }

        void UpdateVaryingAttributes()
        {
            Mesh m = m_MeshFilter.sharedMesh;

            m.SetColors(GetLensFlareColor());
            m.SetUVs(1, GetLensFlareData());
            m.SetUVs(2, GetWorldPositionAndRadius());
            m.SetUVs(3, GetDistanceFadeData());

            Bounds b = m.bounds;
            b.extents = new Vector3(OcclusionRadius, OcclusionRadius, OcclusionRadius);
            m.bounds = b;
            m.name = "LensFlare (" + gameObject.name + ")";
        }

        List<Color> GetLensFlareColor()
        {
            List<Color> colors = new List<Color>();
            foreach (FlareSettings s in Flares)
            {
                Color c = (s.MultiplyByLightColor && m_Light != null)? s.Color * m_Light.color * m_Light.intensity : s.Color;

                colors.Add(c);
                colors.Add(c);
                colors.Add(c);
                colors.Add(c);
            }
            return colors;
        }

        List<Vector4> GetLensFlareData()
        {
            List<Vector4> lfData = new List<Vector4>();

            foreach(FlareSettings s in Flares)
            {
                Vector4 data = new Vector4(s.RayPosition, s.AutoRotate? -1 : Mathf.Abs(s.Rotation), s.Size.x, s.Size.y);
                lfData.Add(data); lfData.Add(data); lfData.Add(data); lfData.Add(data);
            }
            return lfData;
        }
        List<Vector4> GetDistanceFadeData()
        {
            List<Vector4> fadeData = new List<Vector4>();

            foreach (FlareSettings s in Flares)
            {
                Vector4 data = new Vector4(NearFadeStartDistance,NearFadeEndDistance, FarFadeStartDistance, FarFadeEndDistance);
                fadeData.Add(data); fadeData.Add(data); fadeData.Add(data); fadeData.Add(data);
            }
            return fadeData;
        }
    

        List<Vector4> GetWorldPositionAndRadius()
        {
            List<Vector4> worldPos = new List<Vector4>();
            Vector3 pos = transform.position;
            Vector4 value = new Vector4(pos.x,pos.y,pos.z, OcclusionRadius);
            foreach (FlareSettings s in Flares)
            {
                worldPos.Add(value); worldPos.Add(value); worldPos.Add(value); worldPos.Add(value);
            }

            return worldPos;
        }

        void OnDrawGizmosSelected()
        {
            Gizmos.color = new Color(1, 0, 0, 0.3f);
            Gizmos.DrawSphere(transform.position, OcclusionRadius);
            Gizmos.color = Color.red;
            Gizmos.DrawWireSphere(transform.position, OcclusionRadius);
        }

        [System.Serializable]
        public class FlareSettings
        {
            public float RayPosition;
            public Material Material;
            [ColorUsage(true,true)]
            public Color Color;
            public bool MultiplyByLightColor;
            public Vector2 Size;
            public float Rotation;
            public bool AutoRotate;
        
            public FlareSettings()
            {
                RayPosition = 0.0f;
                Color = Color.white;
                MultiplyByLightColor = true;
                Size = new Vector2(0.3f, 0.3f);
                Rotation = 0.0f;
                AutoRotate = false;
            }
        }
}

着色器代码如下:
着色器代码由两部分构成:

  • LensFlareCommon.hlsl,包含这里用到的一些通用功能
  • LensFlareAdditive.shader/LensFlareLerp.shader/LensFlarePremultiplied.shader 分别应用了不同的Blend模式,其他代码一致

LensFlareCommon.hlsl代码如下

struct appdata
{
	float4 vertex : POSITION;
	float2 uv : TEXCOORD0;
	float4 color : COLOR;

	// LensFlare Data : 
	//		* X = RayPos 
	//		* Y = Rotation (< 0 = Auto)
	//		* ZW = Size (Width, Height) in Screen Height Ratio
	nointerpolation float4 lensflare_data : TEXCOORD1;
	// World Position (XYZ) and Radius(W) : 
	nointerpolation float4 worldPosRadius : TEXCOORD2;
	// LensFlare FadeData : 
	//		* X = Near Start Distance
	//		* Y = Near End Distance
	//		* Z = Far Start Distance
	//		* W = Far End Distance
	nointerpolation float4 lensflare_fadeData : TEXCOORD3;
};

struct v2f
{
	float2 uv : TEXCOORD0;
	float4 vertex : SV_POSITION;
	float4 color : COLOR;
};

TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
TEXTURE2D(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
float _OccludedSizeScale;

// thanks, internets
static const uint DEPTH_SAMPLE_COUNT = 32;
static float2 samples[DEPTH_SAMPLE_COUNT] = {
	float2(0.658752441406,-0.0977704077959),
	float2(0.505380451679,-0.862896621227),
	float2(-0.678673446178,0.120453640819),
	float2(-0.429447203875,-0.501827657223),
	float2(-0.239791020751,0.577527523041),
	float2(-0.666824519634,-0.745214760303),
	float2(0.147858589888,-0.304675519466),
	float2(0.0334240831435,0.263438135386),
	float2(-0.164710089564,-0.17076793313),
	float2(0.289210408926,0.0226817727089),
	float2(0.109557107091,-0.993980526924),
	float2(-0.999996423721,-0.00266989553347),
	float2(0.804284930229,0.594243884087),
	float2(0.240315377712,-0.653567194939),
	float2(-0.313934922218,0.94944447279),
	float2(0.386928111315,0.480902403593),
	float2(0.979771316051,-0.200120285153),
	float2(0.505873680115,-0.407543361187),
	float2(0.617167234421,0.247610524297),
	float2(-0.672138273716,0.740425646305),
	float2(-0.305256098509,-0.952270269394),
	float2(0.493631094694,0.869671344757),
	float2(0.0982239097357,0.995164275169),
	float2(0.976404249668,0.21595069766),
	float2(-0.308868765831,0.150203511119),
	float2(-0.586166858673,-0.19671548903),
	float2(-0.912466347218,-0.409151613712),
	float2(0.0959918648005,0.666364192963),
	float2(0.813257217407,-0.581904232502),
	float2(-0.914829492569,0.403840065002),
	float2(-0.542099535465,0.432246923447),
	float2(-0.106764614582,-0.618209302425)
};

float GetOcclusion(float2 screenPos, float depth, float radius, float ratio)
{
	float contrib = 0.0f;
	float sample_Contrib = 1.0 / DEPTH_SAMPLE_COUNT;
	float2 ratioScale = float2(1 / ratio, 1.0);
	for (uint i = 0; i < DEPTH_SAMPLE_COUNT; i++)
	{
		float2 pos = screenPos + (samples[i] * radius * ratioScale);
		pos = pos * 0.5 + 0.5;
		pos.y = 1 - pos.y;
		if (pos.x >= 0 && pos.x <= 1 && pos.y >= 0 && pos.y <= 1)
		{
			float sampledDepth = LinearEyeDepth(SAMPLE_TEXTURE2D_LOD(_CameraDepthTexture, sampler_CameraDepthTexture, pos, 0).r, _ZBufferParams);
			//float sampledDepth = LinearEyeDepth(SampleCameraDepth(pos), _ZBufferParams);
			
			if (sampledDepth >= depth)
				contrib += sample_Contrib;
		}
	}
	return contrib;
}

v2f vert(appdata v)
{
	v2f o;

    
	float4 clip = TransformWorldToHClip(GetCameraRelativePositionWS(v.worldPosRadius.xyz));
	float depth = clip.w; // 这里的w实际上等于相机空间下的坐标z值,也就相机空间下距离相机的距离。因为世界空间到相机空间的矩阵没有缩放操作,因此这里的距离等价于世界空间下距离相机的距离。

	float3 cameraUp = normalize(mul(UNITY_MATRIX_V, float4(0, 1, 0, 0))).xyz;

	float4 extent = TransformWorldToHClip(GetCameraRelativePositionWS(v.worldPosRadius.xyz + cameraUp * v.worldPosRadius.w));
    
    // 这里是得到太阳中心或者lensflare中心的坐标
	float2 screenPos = clip.xy / clip.w; // 齐次除,得到坐标区间[-1,1]
	// 这里是lensflare上边缘中心点的坐标
	float2 extentPos = extent.xy / extent.w; // 齐次除,得到坐标区间[-1,1]

	float radius = distance(screenPos, extentPos);

    // 屏幕长宽比 w/h
	float ratio = _ScreenParams.x / _ScreenParams.y; // screenWidth/screenHeight
	// 计算遮罩比例 
	float occlusion = GetOcclusion(screenPos, depth - v.worldPosRadius.w, radius, ratio);

	// 根据设置的距离,淡入淡出镜头光晕
	float4 d = v.lensflare_fadeData;
	float distanceFade = saturate((depth - d.x) / (d.y - d.x));
	distanceFade *= 1.0f - saturate((depth - d.z) / (d.w - d.z));

	// position and rotate
	float angle = v.lensflare_data.y;
	if (angle < 0) // Automatic
	{
		float2 dir = normalize(screenPos);
		angle = atan2(dir.y, dir.x) + 1.57079632675; // arbitrary, we need V to face the source, not U;
	}

    // 根据遮罩比例和设置的面片大小,计算最终的面片大小
    float2 quad_size = lerp(_OccludedSizeScale, 1.0f, occlusion) * v.lensflare_data.zw;
    if (distanceFade * occlusion == 0.0f) // if either one or other is zeroed
        quad_size = float2(0, 0); // clip

	float2 local = v.vertex.xy * quad_size;

// 旋转面片
// 这里实际上是float2x2 旋转矩阵R
// { 
//   cos(θ),-sin(θ)
//   sin(θ),cos(θ) 
// }
// 乘上顶点的屏幕空间坐标
	local = float2(
		local.x * cos(angle) + local.y * (-sin(angle)),
		local.x * sin(angle) + local.y * cos(angle));

	// adjust to correct ratio
	local.x /= ratio; // 应用对应屏幕比例的缩放,否则结果会被拉伸

	float2 rayOffset = -screenPos * v.lensflare_data.x;
	o.vertex.w = v.vertex.w;
	o.vertex.xy = screenPos + local + rayOffset;

	o.vertex.z = 1;
	o.uv = v.uv;

	o.color = v.color * occlusion * distanceFade * saturate(length(screenPos * 2));
	return o;
}

LensFlareAdditive.shader代码如下

Shader "zhangguangmu/tutorial/lensflare/LensFlareAdditive"
{
    Properties
    {
        _MainTex("Texture",2D)="white"
        _OccludedSizeScale("Cooluded Size scale",Float)=1.0
    }
    SubShader
    {
        Pass
        {
            Tags{"RenderQueue"="Transparent"}
            Blend One One
            ColorMask RGB
            ZWrite Off
            Cull Off
            ZTest Always
            HLSLPROGRAM
            #pragma target 5.0
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #include "LensFlareCommon.hlsl"
            real4 frag(v2f i):SV_Target
            {
                float4 col=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.uv);
                return col*i.color;
            }
            ENDHLSL
        }
    }
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Unity URP 渲染管线着色器编程 104 之 镜头光晕(lensflare)的实现 的相关文章

  • Ubuntu建立nfs和tftp环境

    nfs apt安装 sudo apt get install nfs kernel server 编辑配置文件 sudo vi etc exports 在文件末尾加入红框所示内容 其中蓝框内写入nfs工作目录 要传输的文件放在这个目录下 开

随机推荐

  • MATLAB入门教程

    1 MATLAB的基本知识 1 1 基本运算与函数 在MATLAB下进行基本数学运算 只需将运算式直接打入提示号 gt gt 之後 并按入Enter键即可 例如 gt gt 5 2 1 3 0 8 10 25 ans 4 2000 MATL
  • 算法学习——递归

    引言 从这个专栏开始 我们将会一起来学习算法知识 首先我们要一起来学习的算法便是递归 为什么呢 因为这个算法是我很难理解的算法 我希望通过写这些算法博客 来加深自己对于递归算法的理解和运用 当然 学习算法最快的方式便是通过刷题 但是今天这篇
  • jwt 的 token 被获取怎么办

    jwt 签发后 每次请求会续期 如果 token 被抓包后 别人得到后 有没有好的方案解决身份窃取问抗投诉服务器题 签发 token 的时候加入一些验证信息 比如 IP 如果当前 request IP 和签发时候的 IP 不一致就加 bla
  • 1.Python 基本概念

    一 Python 源程序的基本概念 Python源程序就是一个特殊格式的文本文件 可以使用任意文本编辑软件做Python的开发 Python程序的文件扩展名 通常是 py 文件 二 Python 2 x 与 Python 3 x 版本介绍
  • 多线程算法(完整版)

    多线程算法 完整版 算法导论第3版新增第27章 ThomasH Cormen Charles E Leiserson Ronald L Rivest Clifford Stein 邓辉 译 原文 http software intel co
  • 排序(Sort)

    排序 1 排序的基本知识 2 插入类排序 2 1 直接插入排序 2 2 折半插入排序 2 3 希尔排序 3 交换类排序 3 1 冒泡排序 3 2 快速排序 4 选择类排序 4 1 简单选择排序 4 2 堆排序 5 归并排序 6 基数排序 7
  • 基于python开发一个Django博客网站项目

    基于Python和Django框架的简单博客平台 该平台提供了一个用户友好的界面 使用户能够轻松地创建和管理博客文章 评论和标签 前期环境 需要准备的环境 python3以上 创建一个虚拟环境 以兼容不同的Django版本 创建一个文件夹来
  • 图像频谱图-直方图三维可视化 python

    图像频谱图 直方图三维可视化 python代码 目录 1 条纹噪声图像 频谱图3D可视化 2 图像二维直方图3D可视化 1 条纹噪声图像 频谱图3D可视化 频谱图三维可视化思路 将图像经过傅里叶变换 中心化 取log 再3D可视化 代码 i
  • Quick Search —— 快速匹配字符串

    注 正确性有待考察 因为没有题试试水 转载 https blog csdn net superhackerzhang article details 6432559 算法说明 令模式串为p p 0 p 1 p m 1 长度为m 文本串为T
  • ELK-日志服务【kafka-配置使用】

    kafka 01 10 0 0 21 kafka 02 10 0 0 22 kafka 03 10 0 0 23 1 安装zk集群 配置 root es 01 yum y install java maven root es 01 tar
  • Geoserver 重启后引起的事故

    1 geoserver作用来由 geoserver有两种 一种作为单独一个程序来运行 另一种使用geoserver war放到容器中启动使用 Geoserver是用来发布图层 其他的服务使用链接将图层与地图嵌套可以得到想要的数据的直观页面比
  • Android 混淆使用及其字典混淆(Proguard)

    1 使用背景 ProGuard能够通过压缩 优化 混淆 预检等操作 检测并删除未使用的类 字段 方法和属性 分析和优化字节码 使用简短无意义的名称来重命名类 字段和方法 从而使代码更小 更高效 更难进行逆向工程 Android代码混淆 又称
  • 5 款逆向工具,7 款代码分析工具,11 项优化建议

    本文作者 小木箱 原文发布于 小木箱成长营 小木箱成长营 包体积优化系列文章 包体积优化 实战论 怎么做包体积优化 做好能晋升吗 能涨多少钱 包体积优化 方法论 揭开包体积优化神秘面纱 1 引言 Hello 我是小木箱 欢迎来到小木箱成长营
  • Android Jetpack组件DataStore之Proto与Preferences存储详解与使用

    一 介绍 Jetpack DataStore 是一种数据存储解决方案 允许您使用协议缓冲区存储键值对或类型化对象 DataStore 使用 Kotlin 协程和 Flow 以异步 一致的事务方式存储数据 如果您当前在使用 SharedPre
  • CentOS下GitLab的安装部署

    转载来源 https mp weixin qq com s kUwZja0xK1IfqGU6R2f1EA 一 GitLab Server的搭建 参考 https about gitlab com install 1 准备工作 以centos
  • DRRN(Image Super-Resolution via Deep Recursive Residual Network)超分辨网络-详细分析

    Contents 1 Introduction References 1 Introduction 这篇文章可以在一定程度上看做是DRCN的改良版 保留了DRCN的全局跳层连接和循环块提升网络深度但限制参数量的思想 增加了ResNet的局部
  • WiFi的Channel是个啥玩意

    今天下载了一个监控周围WiFi状态的app WiFi Explorer 打开app首页 如下图 其中有一栏名为Channel 看了下自己所用WiFi的Channel为1 是里面数值最低的 这是不是就代表通道很少 所以很卡呢 查阅资料后发现这
  • Unity用相机实现的镜子效果

    首先登场 场景中的元素 mirror是镜子 挂着我们的脚本 Quad是一个面片 Camera是用来生成RenderTexture给面片的 里面的test1是我用来调试位置的球 镜子size是大小 x是 2 为了反转一下贴图 相机直接可以禁用
  • centos7桥接模式,ip突然消失,ping不通电脑

    方法一 百度了很多方法都不行 然后尝试将路由器重启 得以解决
  • Unity URP 渲染管线着色器编程 104 之 镜头光晕(lensflare)的实现

    在SRP管线中是不支持Unity原有的LensFlare配置的 也就是说如果在URP或者HDRP管线下要使用Lens Flare 需要自己实现改功能 本节介绍一个HDRP的官方DEMO工程 FontainebleauDemo 中使用到的Le