1.1 前言

在SRP中C++提供了最底层的渲染接口,URP和HDRP根据底层渲染接口构建出各自的渲染管线。如下图所示,整个帧渲染的每个Pass都是在C#中完成,只需要打开URP的源码就可以轻松进行调试,这在Built-in管线中是不可能做到的。管线开源还有个好处就是我们可以进一步优化性能,URP为了兼容性默认会经过4次RT拷贝,但其实完全可以节约掉这部分性能,只需要改改源码就可以实现。

Unity目前摄像机动态分辨率支持的不佳,只有在iOS和Android的Vulkan环境下才支持,由于目前大部分设备还只是OpenGL ES,所以无法做到3D降分辨率,UI不降分辨率。有了URP我们就可以通过修改源码的方式实现这种需求。如下图所示, 我们将3D摄像机的分辨率改成了0.5,这样渲染的压力大大减小。

如下图所示,后面UI是高清分辨率直接写到FrameBuffer中,从而实现性能与效果的双重兼容性。

本篇文章会提供一些经典案例,比如URP优化RT拷贝次数、UI一部分背景+3D模糊效果、干掉FinalBlit优化性能,3D部分与UI部分不同分辨率提高性能与效果。这些功能都离不开源码的定制以及修改,所以对URP源码我们一定要了然于胸。

1.2 渲染管线与渲染技术

渲染管线和渲染技术可以说是两个完全不同的概念,渲染技术和平台引擎是无关的,学术界的大佬将渲染公式研究出来,才慢慢应用在工业界。

比如Phong光照模型,Bui Tuong Phong(裴祥风)1975年出版的博士论文 《Illumination for computer generated pictures(计算机生成图片光照)》中的计算光的反射向量有一定开销,Jim Blinn(吉姆·布林)就基于Phong光照模型的基础上于1977年提出使用光的向量+视向量算出中向量(计算中向量的效率高于计算反射向量),中向量与法向量做点乘计算高光强度,这就是我们现在手绘贴图最常用的Blinn–Phong光照模型。

现在手游中,PBR光照模型几乎已经成为标配,早在上世纪80年代由康奈尔大学就开始研究物理正确的渲染理论基础。迪士尼(Disney) 在总结前人经验的基础上,于2012年首先提出实时渲染的PBR方案。论文中提到,原本他们可以实现多个物理光照模型,让艺术家们选择和组合它们,但是无法避免参数过多的情况。所以他们将实时基于物理的渲染整合成一个模型。该光照模型由Epic Games首先应用于自家Unreal Engine的实时渲染中,上一章我们提到Unity也根据这个模型提出了一套精简版拟合方案。

如果大家对PBR的理论基础感兴趣,推荐阅读出版于2016年的这本旷世巨作《Physically Based Rendering(基于物理的渲染)》,作者是三位巨佬:包括Matt Pharr(NVIDIA的杰出研究科学家)、Wenzel Jakob(EPFL计算机与通信科学学院的助理教授)和Greg Humphreys(FanDuel的工程总监,之前曾在Google的Chrome图形团队和NVIDIA的OptiX GPU光线追踪引擎工作)。

这本书提供了在线免费阅读,推荐大家一定要看《Physically Based Rendering(基于物理的渲染)》。

通过Phong光照模型和PBR光照模型我们可以看出,渲染技术与渲染引擎是无关的,时至今日渲染技术还有很多,实时图形渲染背后的都是学术界的大佬,并不受限于Unity引擎或者Unreal Engine引擎。再回到工业界游戏引擎会根据学术界的公式适当做一些拓展或者优化,并且引入一些通用渲染效果加入引擎中。目前比较火的岗位包括图形程序员和技术美术。我认为顶级的图形程序员并不仅仅是渲染公式的搬运工,一定要搞懂背后的原理,只有完全搞懂公式的原理才能有目的性地修改渲染公式,实现项目风格化的图形渲染。(在这条路上我愿与君共勉。)

1.2.1 普通光照计算

内置管线中将光照都封装在Surface Shaders中,不利于学习与修改,现在URP将Blinn-Phong和PBR的光照都封装在Lighting.hlsl中,直接引用即可。如果某些地方想修改也可以自己写一个Shader文件,引用一个自定义的Lighting.hlsl:

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

计算光照必须要得到每盏光的光方向、光颜色、光衰减、视线方向和着色点法线方向。

1. Blinn-Phong

在URP中直接使用SimpleLit.shader就可以直接实现Blinn-Phong光照,在顶点着色器中先得到视线方向:

Varyings LitPassVertexSimple(Attributes input)
{Varyings output = (Varyings)0;//...略output.viewDir = GetWorldSpaceViewDir(vertexInput.positionWS);//...略return output;
}

在片元着色器中计算光照颜色:

half4 LitPassFragmentSimple(Varyings input) : SV_Target
{//...略InputData inputData;InitializeInputData(input, normalTS, inputData);//...略half4 color = UniversalFragmentBlinnPhong(inputData, diffuse, specular, smoothness, emission, alpha);return color;
}

最终结果:

half4 UniversalFragmentBlinnPhong(InputData inputData, half3 diffuse, half4 specularGloss, half smoothness, half3 emission, half alpha)
{//主光Light mainLight = GetMainLight(inputData.shadowCoord, inputData.positionWS, shadowMask);half3 attenuatedLightColor = mainLight.color * (mainLight.distanceAttenuation * mainLight.shadowAttenuation);half3 diffuseColor = inputData.bakedGI + LightingLambert(attenuatedLightColor, mainLight.direction, inputData.normalWS);half3 specularColor = LightingSpecular(attenuatedLightColor, mainLight.direction, inputData.normalWS, inputData.viewDirectionWS, specularGloss, smoothness);//点光
#ifdef _ADDITIONAL_LIGHTSuint pixelLightCount = GetAdditionalLightsCount();for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex){Light light = GetAdditionalLight(lightIndex, inputData.positionWS, shadowMask);#if defined(_SCREEN_SPACE_OCCLUSION)light.color *= aoFactor.directAmbientOcclusion;#endifhalf3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);diffuseColor += LightingLambert(attenuatedLightColor, light.direction, inputData.normalWS);specularColor += LightingSpecular(attenuatedLightColor, light.direction, inputData.normalWS, inputData.viewDirectionWS, specularGloss, smoothness);}
#endif//最终结果 环境光 + 漫反射 + 高光half3 finalColor = diffuseColor * diffuse + emission + specularColor;return half4(finalColor, alpha);
}

1.2.2 PBR光照计算

接着就是处理主光阴影的CBUFFER_START(MainLightShadows),URP的C#代码将阴影级联与阴影偏移等参数传递到GPU中,Shader根据这些参数就可以采样正确的阴影信息了。

Shadows.hlsl

#ifndef SHADER_API_GLES3
CBUFFER_START(MainLightShadows)
#endif
// Last cascade is initialized with a no-op matrix. It always transforms
// shadow coord to half3(0, 0, NEAR_PLANE). We use this trick to avoid
// branching since ComputeCascadeIndex can return cascade index = MAX_SHADOW_CASCADES
float4x4    _MainLightWorldToShadow[MAX_SHADOW_CASCADES + 1];
float4      _CascadeShadowSplitSpheres0;
float4      _CascadeShadowSplitSpheres1;
float4      _CascadeShadowSplitSpheres2;
float4      _CascadeShadowSplitSpheres3;
float4      _CascadeShadowSplitSphereRadii;
half4       _MainLightShadowOffset0;
half4       _MainLightShadowOffset1;
half4       _MainLightShadowOffset2;
half4       _MainLightShadowOffset3;
half4       _MainLightShadowParams;  // (x: shadowStrength, y: 1.0 if soft shadows, 0.0 otherwise, z: oneOverFadeDist, w: minusStartFade)
float4      _MainLightShadowmapSize; // (xy: 1/width and 1/height, zw: width and height)
#ifndef SHADER_API_GLES3
CBUFFER_END
#endif

URP的C#部分代码是如何传递以上信息的,请大家仔细阅读MainLightShadowCasterPass.cs。

非主光阴影参数CBUFFER_START(AdditionalLightShadows)同样是由C#传递进来的。

Shadows.hlsl

#ifndef SHADER_API_GLES3
CBUFFER_START(AdditionalLightShadows)
#endif
float4x4    _AdditionalLightsWorldToShadow[MAX_VISIBLE_LIGHTS];
half4       _AdditionalShadowParams[MAX_VISIBLE_LIGHTS];
half4       _AdditionalShadowOffset0;
half4       _AdditionalShadowOffset1;
half4       _AdditionalShadowOffset2;
half4       _AdditionalShadowOffset3;
float4      _AdditionalShadowmapSize; // (xy: 1/width and 1/height, zw: width and height)
#ifndef SHADER_API_GLES3
CBUFFER_END
#endif
#endif

URP的C#部分代码是如何传递以上信息的,请大家仔细阅读AdditionalLightsShadowCasterPass.cs。

有了阴影的参数还不够,还需要将1盏主光和8盏非主光的颜色、矩阵、LightProbe和灯光的衰减从URP的C#代码传入GPU中,这样Shader就能着色灯光和阴影了。如以下代码所示,每个物体都需要引用Input.hlsl并且C#中传递主光的信息。

Input.hlsl

float4 _MainLightPosition;
half4 _MainLightColor;
half4 _MainLightOcclusionProbes;

接着再传递点光的信息,它们保存在CBUFFER_START(AdditionalLights)中:

Input.hlsl

#ifndef SHADER_API_GLES3CBUFFER_START(AdditionalLights)
#endif
float4 _AdditionalLightsPosition[MAX_VISIBLE_LIGHTS];
half4 _AdditionalLightsColor[MAX_VISIBLE_LIGHTS];
half4 _AdditionalLightsAttenuation[MAX_VISIBLE_LIGHTS];
half4 _AdditionalLightsSpotDir[MAX_VISIBLE_LIGHTS];
half4 _AdditionalLightsOcclusionProbes[MAX_VISIBLE_LIGHTS];
#ifndef SHADER_API_GLES3
CBUFFER_END
#endif
#endif

如下图所示,C#代码中请大家仔细阅读URP中ForwardLights.cs类文件,可以找到实际传递的地方。

光的数据已经全部传递到Shader中了,接着就可以计算光照了。请大家仔细阅读Lighting.hlsl文件,这里包含了物体表面主光和点光源的颜色计算。

Light mainLight = GetMainLight(inputData.shadowCoord, inputData.positionWS, shadowMask);
//..部分略,mainLight计算物体表面的主光颜色
color += LightingPhysicallyBased(brdfData, brdfDataClearCoat,mainLight,inputData.normalWS, inputData.viewDirectionWS,surfaceData.clearCoatMask, specularHighlightsOff);#ifdef _ADDITIONAL_LIGHTSuint pixelLightCount = GetAdditionalLightsCount();for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex){Light light = GetAdditionalLight(lightIndex, inputData.positionWS, shadowMask);#if defined(_SCREEN_SPACE_OCCLUSION)light.color *= aoFactor.directAmbientOcclusion;#endif//light计算物体表明点光的颜色,最多8盏颜色进行相加color += LightingPhysicallyBased(brdfData, brdfDataClearCoat,light,inputData.normalWS, inputData.viewDirectionWS,surfaceData.clearCoatMask, specularHighlightsOff);}
#endif

最终物体表面颜色由BRDF乘以辐射率得出:

half3 LightingPhysicallyBased(BRDFData brdfData, BRDFData brdfDataClearCoat,half3 lightColor, half3 lightDirectionWS, half lightAttenuation,half3 normalWS, half3 viewDirectionWS,half clearCoatMask, bool specularHighlightsOff)
{//..部分代略half NdotL = saturate(dot(normalWS, lightDirectionWS));//请注意这里,光源颜色*光源衰减*(法线方向点乘光的方向)half3 radiance = lightColor * (lightAttenuation * NdotL);half3 brdf = brdfData.diffuse;brdf += brdfData.specular * DirectBRDFSpecular(brdfData, normalWS, lightDirectionWS, viewDirectionWS);//最终将计算的辐射率*BRDFreturn brdf * radiance;
}

1.2.3 BRDF优化

Unity的工程师Renaldas Zioma在2015年发表了一篇在移动端优化PBR的论文《See "Optimizing PBR for Mobile" from Siggraph 2015 moving mobile graphics course》(强烈建议大家看看),对BRDF高光感兴趣的同学可以继续看这段代码。

half DirectBRDFSpecular(BRDFData brdfData, half3 normalWS, half3 lightDirectionWS, half3 viewDirectionWS)
{float3 halfDir = SafeNormalize(float3(lightDirectionWS) + float3(viewDirectionWS));float NoH = saturate(dot(normalWS, halfDir));half LoH = saturate(dot(lightDirectionWS, halfDir));// GGX Distribution multiplied by combined approximation of Visibility and Fresnel// BRDFspec = (D * V * F) / 4.0// D = roughness^2 / ( NoH^2 * (roughness^2 - 1) + 1 )^2// V * F = 1.0 / ( LoH^2 * (roughness + 0.5) )// See "Optimizing PBR for Mobile" from Siggraph 2015 moving mobile graphics course// https://community.arm.com/events/1155// Final BRDFspec = roughness^2 / ( NoH^2 * (roughness^2 - 1) + 1 )^2 * (LoH^2 * (roughness + 0.5) * 4.0)// We further optimize a few light invariant terms// brdfData.normalizationTerm = (roughness + 0.5) * 4.0 rewritten as roughness * 4.0 + 2.0 to a fit a MAD.float d = NoH * NoH * brdfData.roughness2MinusOne + 1.00001f;half LoH2 = LoH * LoH;half specularTerm = brdfData.roughness2 / ((d * d) * max(0.1h, LoH2) * brdfData.normalizationTerm);// On platforms where half actually means something, the denominator has a risk of overflow// clamp below was added specifically to "fix" that, but dx compiler (we convert bytecode to metal/gles)// sees that specularTerm have only non-negative terms, so it skips max(0,..) in clamp (leaving only min(100,...))
#if defined (SHADER_API_MOBILE) || defined (SHADER_API_SWITCH)specularTerm = specularTerm - HALF_MIN;specularTerm = clamp(specularTerm, 0.0, 100.0); // Prevent FP16 overflow on mobiles
#endifreturn specularTerm;
}

代码中的注释可以清晰地看到Unity是如何进行优化的,首先看看完整的BRDF公式。

D:微表面分部函数(注意这里的D就是GGX);

G:遮挡可见性函数(注意这里的G并不是GGX);

F:菲涅尔函数;

首先Unity先将G项进行拟合称为V项遮挡可见性函数,如下图所示,拟合结果 V=G/(4(N⋅V)(N⋅L)):

接着对V*F进行拟合:

最终BRDF = V * F * D (前面说到D就是GGX):

如下图所示,按照上面的公式大家可以自己乘一下看看最终是不是正确。

大可以按照这个公式再对比一下前面函数DirectBRDFSpecular计算的是否正确。

URP从原理到应用——进阶篇相关推荐

  1. ViewDragHelper(二)- 源码及原理解读(进阶篇)

    声明:本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 本篇为该系列的第二篇,侧重讲解ViewDragHelper 的实现原理和源码逻辑,以及它所提供的Callback. 目录 Vi ...

  2. Kafka核心设计与实践原理总结:进阶篇

    作者:未完成交响曲,资深Java工程师!目前在某一线互联网公司任职,架构师社区合伙人! kafka作为当前热门的分布式消息队列,具有高性能.持久化.多副本备份.横向扩展能力.我学习了<深入理解K ...

  3. [安全攻防进阶篇] 七.恶意样本检测之编写代码自动提取IAT表、字符串及时间戳溯源

    系统安全绕不开PE文件,PE文件又与恶意样本检测及分析紧密相关.前文作者带领大家逆向分析两个CrackMe程序,包括逆向分析和源码还原.这篇文章主要介绍了PE文件基础知识及恶意样本检测的三种处理知识, ...

  4. 编码原则实例------c++程序设计原理与实践(进阶篇)

    编码原则: 一般原则 预处理原则 命名和布局原则 类原则 函数和表达式原则 硬实时原则 关键系统原则 (硬实时原则.关键系统原则仅用于硬实时和关键系统程序设计) (严格原则都用一个大写字母R及其编号标 ...

  5. Docker 数据卷之进阶篇

    Docker 数据卷之进阶篇 原文:Docker 数据卷之进阶篇 笔者在<Docker 基础 : 数据管理>一文中介绍了 docker 数据卷(volume) 的基本用法.随着使用的深入, ...

  6. Kafka核心设计与实践原理总结:基础篇

    作者:未完成交响曲,资深Java工程师!目前在某一线互联网公司任职,架构师社区合伙人! 一.基本概念 1.体系架构 Producer:生产者 Consumber:消费者 Broker:服务代理节点(k ...

  7. 计算机编程书籍-笨办法学Python 3:基础篇+进阶篇

    编辑推荐: 适读人群 :本书适合所有已经开始使用Python的技术人员,包括初级开发人员和已经升级到Python 3.6版本以上的经验丰富的Python程序员. "笨办法学"系列, ...

  8. 最快让你上手ReactiveCocoa之进阶篇

    前言 由于时间的问题,暂且只更新这么多了,后续还会持续更新本文<最快让你上手ReactiveCocoa之进阶篇>,目前只是简短的介绍了些RAC核心的一些方法,后续还需要加上MVVM+Rea ...

  9. SQL Server调优系列进阶篇(如何维护数据库索引)

    前言 上一篇我们研究了如何利用索引在数据库里面调优,简要的介绍了索引的原理,更重要的分析了如何选择索引以及索引的利弊项,有兴趣的可以点击查看. 本篇延续上一篇的内容,继续分析索引这块,侧重索引项的日常 ...

最新文章

  1. oracle 19c补丁下载,rpm 安装oracle 19c,通过补丁升级到19.8
  2. 学习笔记(十六)——MySQL(约束与关系)
  3. 尾递归对时间与空间复杂度的影响(上)
  4. html语言制作网页,HTML语言的网页制作技巧与方法
  5. layui 单选框、复选框、下拉菜单 不显示问题 记录
  6. JAVA记录-Servlet介绍
  7. 我说我了解集合类,面试官竟然问我为啥HashMap的负载因子不设置成1!?
  8. hduoj 2602Bone Collector
  9. php exif 扩展
  10. 信号与系统郭宝龙版 第二章 连续系统的时域分析 思维导图
  11. 虚拟机(VMware)中windows2003系统服务器的IE无法打开搜索网页
  12. 《挑战不可能之加油中国》中越边境广西段扫雷队整装亮相
  13. 根据关系图非常简单的求出三种关系闭包(自反闭包、对称闭包、传递闭包)附练习题
  14. 安全测试(五)Android APK软件安全 APP应用安全 手机软件安全 apk安全 apk反编译 应用日志窃取 apk漏洞 应用软件本身功能漏洞 高危权限泄密风险等 移动应用常规安全讲解
  15. 华硕ezflash3找不到u盘_通过华硕BIOS Tools - EZ Flash 2 更新主板BIOS
  16. ckplayer超酷flv网页播放器
  17. 【技巧】EXCEL如何按行找出最大三个数并标记
  18. [渝粤教育] 深圳信息职业技术学院 《新理念英语》English For You 参考 资料
  19. 【C语言程序】已知一个长方体的高,通过输入长方体的长和宽,计算出长方体的体积
  20. 【Python】Pyyaml和ruamel.yaml

热门文章

  1. uplink端口与普通接口区别_工业显示器和普通液晶显示器的区别
  2. 上接星辰,下抵炊烟:一颗中国AI定海针的故事
  3. 椅子能在不平的地面上放稳吗
  4. Mac Catalyst 初步体验+排坑
  5. 【spark】windows spark 环境搭建
  6. RTK基站加入3D扼流圈天线,对卫星导航有什么助益?
  7. 深度学习案例分享 | 房价预测 - PyTorch 实现
  8. 如何自动生成接口自动化测试用例
  9. Python-容器类型
  10. HRBU 2021暑期训练解题报告阶段三Day2