当前位置: 首页 > news >正文

Custom SRP - LOD and Reflections

1 LOD Groups

场景中对象越多,场景就越丰富,但是过多的对象,也会增加 CPU 和 GPU 的负担.同时如果对象最终渲染在屏幕上后覆盖的像素太少,就会产生模糊不清的像素点/噪点.如果能够不渲染这些过小的对象,就能解决噪点问题,同时释放 CPU GPU,去处理更重要的对象.

裁剪掉这些对象,可能会导致对象突然消失/出现的问题,因此,可以基于对象在屏幕上的大小,定义一系列子对象,根据对象到摄像机的距离,选择一个子对象进行渲染

这些逻辑,都可以有 LOD Group 组件来实现.

1.1 LOD Group Component

组件创建后,默认有4个LOD级别, LOD 0,1,2 和culled(裁剪掉,不渲染).

组件上的百分数,代表对象在屏幕上,渲染的高度与屏幕高度的比值(一般都是这样),叫做对象的屏占比.如上图,它表示如果屏占比大于60%则用 LOD 0 渲染,以此类推,知道屏占比小于10% 时就会被裁剪掉

在 Quality Settings 中,可以配置 LOD Bias,默认是 2, 会将对象屏占比乘以2.在上面的配置中,意味着屏占比大于30%时渲染 LOD 0.

对于一个 LOD 对象,我们一般会创建一个对象,添加 LOD Group 组件,然后为其创建子对象,这些子对象被LOD驱动,渲染当前 LOD 级别配置的 Renderer.

选择一个 LOD 级别,点击 + 号,创建一个 Renderer 项, 然后就可以将 LOD 子对象拖动到 Renderers 列表中,表示该LOD会渲染它.

一个对象可以配置到多个 LOD 中.比如有A,B,C三个子对象,LOD0时,ABC都渲染,LOD1时渲染AB,LOD2时渲染A.这样随着距离增加,按照重要程度(ABC),三个子对象会一次消失.

1.2 LOD Transition

LOD之间的切换可能会过于突兀, 在LOD Group 的 Fade Mode 选项中,选择 cross fade,则切换时旧的 LOD 会有淡出过程,新的也会有淡入.但是目前该选项不会带来任何效果,因为这需要我们的 shader 的支持.

切换时两个LOD 的 Renderer 都会渲染, shader 中需要对他们进行混合.

Unity 用 LOD_FADE_CROSSFADE 来定义支持混合的 shader 变体.需要在 CustomLit 和 ShadowCaster 两个 pass 中定义 shader keyword:

#pragma multi_compile _ LOD_FADE_CROSSFADE

Fade 控制参数由 per draw buffer 中的 unity_LODFade 提供.其 x 时淡出参数.该参数在淡出和淡入时的取值是不同的:

  • 淡出时,1 ~ 0
  • 淡入时,-1 ~ 0 (一定是负的,但是是不是 0 ~ -1 ?)

后面实现 fade 效果时,会针对性处理.

1.3 Dithering fade 效果

我们通过 clip 来实现淡入淡出效果.

定义 ClipLOD 方法,并在像素着色器开始时调用

// 执行 lod fade 裁剪
void ClipLOD(float2 positionCS, float fade)
{
#if defined(LOD_FADE_CROSSFADE)//// 在垂直方向上划分条纹的效果//float dither = (positionCS.y % 32)/32;// unity dither 生成函数float dither = InterleavedGradientNoise(positionCS.xy, 0);// 淡入时,fade 时负的,因此需要 + ditherclip(fade + (fade < 0 ? dither : -dither));
#endif
}float4 LitPassFragment(Varyings input) : SV_TARGET
{UNITY_SETUP_INSTANCE_ID(input);ClipLOD(input.positionCS, unity_LODFade.x);...
}

可以看到我们自定义的 dither 的 fade 效果

lightmap 过度时,我们用到了 unity 提供的 dither 生成函数 InterleavedGradientNoise,这里可以换成同样的函数看效果

1.4 Animated cross-fading

开启后, cross fade 会在一定时间内完成,默认时 0.5 秒.这个值可以在代码中,修改 LODGroup.crossFadeAnimationDuration 的值来改变.注意这个值是静态变量,会影响所有 LOD Group

2 Reflections

目前我们的材质还不支持高光反射,导致我们的 metallic 材质是黑色的.

首先向 baked scene 中,创建几个球,配置其材质为 metallic > 0.8,可以发现是黑色的

2.1 Indirect BRDF

首先增加 IndirectBRDF 函数

// BRDF.hlsl
float3 IndirectBRDF(Surface surface, BRDF brdf, float3 diffuse, float3 specular)
{float3 reflection = brdf.specular * specular;// 粗糙表面会散射光线,因此除以粗糙度的平方+1reflection /= (brdf.roughness * brdf.roughness + 1.0);float3 diff = brdf.diffuse * diffuse;return diff + reflection;
}

粗糙度除了降低反射强度,还会让反射变得模糊.我们可以通过采样 cubemap 的低级别 mipmap 来实现这样的效果.Unity 在 Core RP Library 中的 ImageBasedLighting.hlsl 中定义了基于 perceptual roughness(感知粗糙度)来获取mipmap level,因此需要在 BRDF 结构体中定义该数据

// BRDF.hlsl
struct BRDF
{float3 specular;float3 diffuse;float roughness;float perceptualRoughness;
};
...BRDF GetBRDF(Surface surface, bool premultiplyAlpha)
{BRDF brdf;float oneMinusReflectivity = OneMinusReflectivity(surface.metallic);oneMinusReflectivity = 1.0 - surface.metallic;brdf.diffuse = surface.color * oneMinusReflectivity;if (premultiplyAlpha)brdf.diffuse *= surface.alpha;brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic);brdf.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surface.smoothness);brdf.roughness = PerceptualRoughnessToRoughness(brdf.perceptualRoughness);return brdf;
}

unity 通过 unity_SpecCube0 提供环境贴图,在GI.hlsl 中声明相应的贴图和采样器,然后定义采样函数.在GI结构体中增加 specular 用来传递反射

// GI.hlsl#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/EntityLighting.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/ImageBasedLighting.hlsl"
...
TEXTURECUBE(unity_SpecCube0);
SAMPLER(samplerunity_SpecCube0);
...
struct GI
{float3 diffuse;ShadowMask shadowMask;float3 specular;
};
...
// 采样环境贴图
float3 SampleEnvironment(Surface surfaceWS, BRDF brdf)
{// 采样环境贴图需要一个方向,由表面法线和表面的观察方向计算反射方向.float3 uvw = reflect(-surfaceWS.viewDirection, surfaceWS.normal);// 基于“感知粗糙度”,计算采样时的 mipmap levelfloat mip = PerceptualRoughnessToMipmapLevel(brdf.perceptualRoughness);float4 environment = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, uvw, 0.0);return environment.rgb;
}
..
GI GetGI (float2 lightMapUV, Surface surfaceWS) 
{GI gi;gi.diffuse = SampleLightMap(lightMapUV) + SampleLightProbe(surfaceWS);// 采样环境图gi.specular = SampleEnvironment(surfaceWS);...return gi;
}

修改Lighting 中的 GetLighting 函数,调用 IndirectBRDF

float3 GetLighting(Surface surfaceWS, BRDF brdf, GI gi)
{ShadowData shadowData = GetShadowData(surfaceWS);shadowData.shadowMask = gi.shadowMask;// 临时返回以查看数据//return gi.shadowMask.shadows.rgb;//float3 color = gi.diffuse * brdf.diffuse;float3 color = IndirectBRDF(surfaceWS, brdf, gi.diffuse, 1.0);for(int i = 0; i < GetDirectionalLightCount(); ++i){Light light = GetDirectionalLight(i, surfaceWS, shadowData);color += GetLighting(surfaceWS, brdf, light);}return color;
}

如下图,反射了天空盒

2.2 Fresnel Reflection

当视线掠过表面(观察方向与表面法线接近90度)时,表面更像镜子,会反射更多,这种现象就是 菲涅尔反射.模拟这种现象非常复杂,我们参考 URP 中采用的 Schlick approximation(石里克近似) 法.

我们将菲涅尔反射定义为白色,而粗糙表面会降低菲涅尔效应,因此我们基于粗糙度计算菲涅尔颜色(本质上灰度).然后基于观察方向和表面法线计算菲涅尔强度,并在菲涅尔颜色和反射之间插值.

// BRDF.hlslstruct BRDF
{float3 specular;          // 高光反射float3 diffuse;          // 漫反射float roughness;          // 粗糙度float perceptualRoughness; // 感知粗糙度float fresnel; // 菲涅尔颜色,因为是灰度,rgb都相等,因此只需要一个浮点数
};...BRDF GetBRDF(Surface surface, bool premultiplyAlpha)
{BRDF brdf;float oneMinusReflectivity = OneMinusReflectivity(surface.metallic);oneMinusReflectivity = 1.0 - surface.metallic;brdf.diffuse = surface.color * oneMinusReflectivity;if (premultiplyAlpha)brdf.diffuse *= surface.alpha;brdf.specular = lerp(MIN_REFLECTIVITY, surface.color, surface.metallic);brdf.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surface.smoothness);brdf.roughness = PerceptualRoughnessToRoughness(brdf.perceptualRoughness);// 计算菲涅尔灰度颜色brdf.fresnel = saturate(surface.smoothness + 1.0 - oneMinusReflectivity);return brdf;
}float3 IndirectBRDF(Surface surface, BRDF brdf, float3 diffuse, float3 specular)
{// 计算菲涅尔强度float fresnelStrength = Pow4(1.0 - saturate(dot(surface.normal, surface.viewDirection)));// 反射是根据菲尼尔强度,在高光反射和菲涅尔反射之间插值float3 reflection = specular + lerp(brdf.specular, brdf.fresnel, fresnelStrength);// 粗糙表面会散射光线,因此除以粗糙度的平方+1reflection /= (brdf.roughness * brdf.roughness + 1.0);float3 diff = brdf.diffuse * diffuse;return diff + reflection;
}

2.3 Fresnel Slider

菲涅尔反射主要出现在几何体的边缘.当环境图与物体背后的颜色匹配时,效果很好.但是如果颜色不匹配,就会显得怪异.

降低光滑度可以降低菲涅尔反射,但会让整个表面变暗.同时,在某些情况下,近似的菲涅尔反射可能不合适,如水下.因此,需要加一个参数来控制菲涅尔强度.

// Lit.shader 中定义材质参数
_Smoothness("Smoothness", Range(0,1)) = 0.51
_Fresnel("Fresnel", Range(0,1)) = 1// LitInput.hlsl 中,定义 per material 变量,并定义查询函数
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
...
float _Smoothness;
float _Fresnel;
...
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)...float GetFresnel (float2 baseUV)
{return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Fresnel);
}// UnlitInput.hlsl 中,定义返回0的函数
float GetFresnel (float2 baseUV)
{return 0.0;
}// Surface.hlsl 中,为Surface结构体增加 fresnelStrength
struct Surface
{...float smoothness;float fresnelStrength;...
};// LitPass.hlsl 中,为 Surface.fresnelStrength 赋值
float4 LitPassFragment(Varyings input) : SV_TARGET
{...surface.smoothness = GetSmoothness(input.uv);surface.fresnelStrength = GetFresnel(input.uv);...
}// BRDF.hlsl 中,应用该强度
float3 IndirectBRDF(Surface surface, BRDF brdf, float3 diffuse, float3 specular)
{// 计算菲涅尔强度float fresnelStrength = surface.fresnelStrength * Pow4(1.0 - saturate(dot(surface.normal, surface.viewDirection)));// 反射是根据菲尼尔强度,在高光反射和菲涅尔反射之间插值float3 reflection = specular + lerp(brdf.specular, brdf.fresnel, fresnelStrength);// 粗糙表面会散射光线,因此除以粗糙度的平方+1reflection /= (brdf.roughness * brdf.roughness + 1.0);float3 diff = brdf.diffuse * diffuse;return diff + reflection;
}

2.4 Reflection Probes

除了反射天空,还可以创建当前场景.通过GameObject / Light / Reflection Probe创建 Reflection Probe,该组件会在其位置拍摄6方向并生成立方体贴图.属性 Box Size 用来确定影响范围,配合 Importance 重要性,该范围内的对象会使用该 Reflection Probe.

Cube map 可以离线生成,也可以实时生成.实时生成需要渲染6个画面,因此消耗比较大,不建议.

通过 Anchor Override, Renderer 可以调整 Reflection Probe 选择,即使对象本身超出了范围,但是可以指定该属性的位置在 Reflection Probe 范围内,来优化选择. 使用场景中的 Light Probe 会打断合批.同时,DrawMeshInstanced 接口渲染时,不支持指定 Reflection Probe.

Renderer 的 Reflection Probe 选项中:

  • 默认是 Blend Probes, Unity会选择两个 Reflection Probe 并进行插值,该模式与 SRP Batcher 不兼容,因此我们不能用.
  • off 表示使用天空盒的 cube map
  • Simple 表示使用最近,最重要的 Reflection Probe

Cube map 可能是 HDR 或 LDR 的,我们需要正确解码采样结果.该解码参数以 unity_SpecCube0_HDR 变量提供.

// UnityInput.hlsl 中
CBUFFER_START(UnityPerDraw)
...
float4 unity_ProbeVolumeMin;
float4 unity_SpecCube0_HDR; // 如何解码 reflection cube map
CBUFFER_END// GI.hlsl 中
// 采样环境贴图
float3 SampleEnvironment(Surface surfaceWS, BRDF brdf)
{// 采样环境贴图需要一个方向,由表面法线和表面的观察方向计算反射方向.float3 uvw = reflect(-surfaceWS.viewDirection, surfaceWS.normal);// 基于“感知粗糙度”,计算采样时的 mipmap levelfloat mip = PerceptualRoughnessToMipmapLevel(brdf.perceptualRoughness);float4 environment = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, uvw, 0.0);// unity_SpecCube0_HDR 参数可以确定 cube map 是 HDR/LDR的,通过下面的方法正确解码return DecodeHDREnvironment(environment, unity_SpecCube0_HDR);
}

http://www.xdnf.cn/news/1391131.html

相关文章:

  • 【AI】常见8大LLM大语言模型地址
  • SPSA为什么要求三阶可导
  • 事务和锁(进阶)
  • 对接连连支付(七)-- 退款查询
  • C++ 线程安全初始化机制详解与实践
  • Elasticsearch核心配置与性能优化
  • 从零开始的python学习——常量与变量
  • 复杂保单信息如何自动提取
  • 【新启航】3D 逆向抄数的工具技术与核心能力:基于点云处理的扫描设备操作及模型重建方法论
  • Java面试现场:Spring Boot+Redis+MySQL在电商场景下的技术深度剖析
  • Shell 编程基础(续):流程控制与实践
  • Python Imaging Library (PIL) 全面指南:PIL图像处理异常处理与优化
  • 数据结构:选择排序 (Selection Sort)
  • JavaScript 中,判断一个数组是否包含特定值
  • 【完整源码+数据集+部署教程】停车位状态检测系统源码和数据集:改进yolo11-DCNV2-Dynamic
  • 机器学习入门,从线性规划开始
  • 基于 Selenium 和 BeautifulSoup 的动态网页爬虫:一次对百度地图 POI 数据的深度模块化剖析
  • el-table实现双击编辑-el-select选择框+输入框限制非负两位小数
  • SQL知识
  • Python的一次实际应用:利用Python操作Word文档的页码
  • 打造高效外贸网站:美国服务器的战略价值
  • ASCM使用手册
  • 从零开始构建卷积神经网络(CNN)进行MNIST手写数字识别
  • 彻底弄清URI、URL、URN的关系
  • BGP路由协议(二):报文的类型和格式
  • OpenAI宣布正式推出Realtime API
  • 网络_协议
  • Qt事件_xiaozuo
  • 快速深入理解zookeeper特性及核心基本原理
  • Replay – AI音乐伴奏分离工具,自动分析音频内容、提取主唱、人声和伴奏等音轨