草庐IT

UnityShader35:光晕光效

Jaihk662 2023-11-18 原文

一、光晕逻辑

光晕的逻辑很简单,就是在屏幕上画上一个一个方形的 Mesh,然后采样带 Alpha 通道的光晕贴图,效果就出来了,其中方形 Mesh 的大小、位置、纹理表现全部都由美术配置,因此效果好坏主要取决于光晕贴图以及是否有一套很好的参数/配置

1.1 Unity URP 光晕

其实 Unity 是支持光晕的,有自带的光晕组件,不过很可惜的是它不能很好的支持 URP:

默认的组件不行的话,就只能自己去实现,不过好在网上已经有人在 HDRP 上实现过了(来源于一个 HDRP Demo):毕竟效果大同小异,直接抄就完事,略微改下就能用,后面的内容也都是在这个基础之上做的分析和优化

1.2 光晕的可见性

光晕(halation)是指在曝光拍摄过程中,强光投射到胶片上时,透过胶片乳剂中在片基表面进行反射,从而致使图像发晕的现象

想要一个科学的光晕效果,需要满足两个条件:

  1. 场景中的太阳可视时,会出现镜头光晕效果
  2. 光晕在屏幕上的位置分布与场景中的太阳方位,和当前视角都有一定关系

场景中的太阳作为主要的平行光源,我们往往只需要其方向,但若想要实现这两个需求,还是要拿到太阳的具体位置,很好办,直接无脑将场景太阳方向乘上一个非常大的值就 OK 了:

half3 D = _ROCLightDir1;
float4 clip = TransformWorldToHClip(GetCameraRelativePositionWS(D * 10000));
float depth = LinearEyeDepth(0, _ZBufferParams);

这一部分是自己做的扩展,原方案的光晕位置是跟着挂载组件的 GameObject 走的

然后就是遮挡判断,需要计算太阳的屏幕空间坐标,并且和当前的摄像机深度缓冲做对比,不过这还有一个问题就是:真实的太阳它不会是一个点,因此我们不能只拿一个世界坐标去进行判断

这个也很好解决,就是在中心点周围随机散点多次,分别采样计算是否被遮挡,然后拿得到的遮挡比率去乘上光晕颜色作为最终的贡献,由于这块是在顶点着色器中做的,而你光晕的贴片顶点很少,因此性能还算 OK 的,当然对于很低端的移动设备不支持在顶点着色器中采样深度贴图的话就不要开光晕了,片段着色器做这个性能爆炸

//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),
    //…… 32 组随机数,略
};

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);
            if (sampledDepth >= depth)
                contrib += sample_Contrib;
        }
    }
    return contrib;
}

vert()
{
    float2 screenPos = clip.xy / clip.w;
    float ratio = _ScreenParams.x / _ScreenParams.y;
    float radius = v.worldPosRadius.w;
    float occlusion = GetOcclusion(screenPos, depth, radius, ratio);
}

1.3 Mesh 生成与着色

这一块完全根据配置决定,几乎没有计算量,太简单了,可以直接看代码

不过还有一点要提:原方案由于可以调节的参数比较多,它们要传入 shader,都需要通过 mesh 的额外通道 uv1、uv2、uv3,并且格式要开 float4,这样就可能会出现通道不够用的情况,特别是有的设备可能都不支持额外的 uv 数据使用 float4,因此有些参数就考虑不要了

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;
};

很明显,如果我们确保光晕的方向一定跟着场景光源方向走,那么我们就不太需要去传递世界坐标到 shader 中,只需要按照前面的方案无脑拿平行光方向乘上一个很大的数就可以了,这样第二组参数就可以省下来

除此之外,第三个参数用于实现光源距离越近,光晕效果越强的渐进效果,其实也可以不要,目前来看效果是 OK 的,毕竟我们完全可以把太阳当作在无限远的位置,给个固定值就可以了

这样就能砍掉一半的顶点数据

1.4 后续性能优化方案

目前实现是用了多个独立的 subMesh 来绘制多个面片,但其实也可以使用分 UV 的方式,把多张 Flare 贴图合成一张图,并且在一个 Mesh 上进行绘制,这样可以减少到一次 Drawcall

除此之外,目前光晕和画质无关,所有画质下都是默认开启的,理论上低配中配可以不给开

二、手机各平台兼容 

很可惜,如果你和我一样是基于前面 HDRP Demo 的光晕效果,在此基础之上优化修改的,那么它还有一个问题,那就是其不能很好的支持的 Andriod 平台(当然如果你不考虑除了 Window 以外的其它平台就当我没说)

问题就出现在文章中的 2.2 光晕光效的可见性部分,当主光源被遮挡时,就不会有光晕的现象,这个判断很简单,因为太阳的位置可以被视作无限远,因此我们只要以太阳中心为圆心,多次随机采样周围屏幕坐标对应的深度信息中有没有被写入数据就可以了,如果有就说明这个屏幕点不可视

但是在 Andriod 平台上,这个深度检测它貌似失效了:即怎么检测都通过,因此无论光源是否可见,光晕效果永远存在,这明显是不对的

2.1 是否当前的手机平台不支持顶点着色器采样纹理

查了下资料,OpenGL 3.0 及以上,DX9 以上都支持顶点着色器中采样纹理,而事实上,现在极大多数设备平台都满足这个要求,因此不是这个原因

  • 但仍然需要注意的是:Unity 若要在顶点着色器中采样纹理,需要指定 mipmap 等级,这就意味着不能使用常规的 API 例如 SAMPLE_TEXTURE2D(…),需要用 SAMPLE_TEXTURE2D_LOD(…, 0) 代替,这是因为 mipmap 的级别的选择需要在片段着色阶段才能获取

2.2 是否是因为指定的着色器模型,默认编译目标设置过高

Shader 中设置的默认编译目标为 5.0,这基本上是能设置的最高等级了:

#pragma target 5.0

后来查了下 Unity3D 对应的官方手册:着色器编译:针对着色器模型和 GPU 功能,其中也说明

如果真如此,那么显然目前几乎所有的主流设备都不支持编译这么高的版本,必然有问题

不过也很奇怪,如果真的不支持整个 shader 都应该不生效才对,不应该只是深度测试有问题,因此后面又查了下资料,确保 Unity 在这块的设置是默认向后兼容的,而且你没有用像曲面细分着色器这种必须高版本才支持的功能/特性的话,编译目标版本确实低一些也没关系

后面修改编译目标,打包测试了下,排除了这个原因

2.3 疑似屏幕坐标计算错误

参考其它 shader:通过屏幕坐标采样 depthTexture,都会对 clipPos 做一步 ComputeScreenPos 的操作,再除一个 w,而出现问题的光晕着色器中,并没有这一步,最终我们用于采样的坐标是手算的,所以怀疑是否有算错

o.pos = TransformObjectToHClip(v.pos.xyz);
o.screenUV = ComputeScreenPos(o.pos);
half2 screenUV = i.screenUV.xy / i.screenUV.w;
viewSpaceDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_PointClamp, screenUV);

需要排查这个原因,需要先知道 Unity 的 ComputeScreenPos 到底为我们做了什么事

2.3.1 ComputeScreenPos

直接看源码:

float4 ComputeScreenPos(float4 positionCS)
{
    float4 o = positionCS * 0.5f;
    o.xy = float2(o.x, o.y * _ProjectionParams.x) + o.w;
    o.zw = positionCS.zw;
    return o;
}

对于我们的输入 positionCS,也就是经过 TransformObjectToHClip 转换后的坐标是处于裁剪空间中的坐标,其 x, y 分量的范围为 ,假设我们最终屏幕的宽高分别为 width 和 height,那么真实屏幕空间坐标的计算方法应该是:

screenUV.x = ((pos.x / w) * 0.5 + 0.5) * width;
screenUV.y = ((pos.y / w) * 0.5 + 0.5) * height;

这就是一个标准的  映射到 [0, 1] 范围的操作,再乘上我们的宽和高

而此时你再看 ComputeScreenPos 方法内的操作,你就会发现,它的主体其实就是上面的代码乘上 w(先不考虑其中的 _ProjectionParams.x),也就是将  映射到  范围而非 [0, 1]

Unity 为什么要这样做呢?即为什么不直接帮我们直接映射到 [0, 1]?

  1. 考虑 tex2Dproj 指令,其会在对纹理采样前帮我们把参数除上一个 w 分量,这样你在 ComputeScreenPos 之后直接套用这个方法就是正确的了,只不过我们很少用这个 tex2Dproj 指令
  2. 如果你要在片段着色器中采样,非常不建议在顶点着色器中就提前除以 w 分量,此会导致经过插值到片段着色器后,得到的插值结果不准确,因此最好是先插值再归一化,这是因为投影空间不是线性空间

而对于 _ProjectionParams.x,则是判断我们的投影矩阵是否为翻转矩阵,如果使用了翻转投影矩阵,那么我们同时也要翻转 y 的坐标值,关于是否使用翻转投影矩阵,这个就和平台有关

  • 如果是 Direct3D-like 平台(UNITY_UV_STARTS_AT_TOP = 1),就需要进行翻转
  • 如果是 OpenGL-like 平台(UNITY_UV_STARTS_AT_TOP = 0),则无需翻转

因此使用 ComputeScreenPos 就不需要关心这一部分平台差异了

2.4 Reversed-Z

在做深度测试的时候,我们把太阳的位置当作一个无限远的位置,但是不同平台下,深度最值不一样,这个是个很重要的问题,之前有篇文章也专门介绍过 Reversed-Z

看下原始代码,逻辑中给太阳设定的深度值如下:

float depth = LinearEyeDepth(0, _ZBufferParams);

关于 LinearEyeDepth,即传入深度纹理中的深度值以计算出实际的深度值,这就不详细介绍

很明显,只有在深度发生反转的平台上(DirectX),最值深度才为0,OpenGL 平台没有 Reversed-Z,其深度范围为 [0, 1],最值为1,因此正确的考虑了平台差异的代码应如下:

#if UNITY_REVERSED_Z
    float depth = LinearEyeDepth(0, _ZBufferParams);
#else
    float depth = LinearEyeDepth(1, _ZBufferParams);
#endif

好了,搞定!

有关UnityShader35:光晕光效的更多相关文章

  1. ruby-on-rails - 我日志中的 [1m[35m] 是什么,我该如何让它消失? - 2

    如果这个问题已经得到回答,我提前道歉。我一直在尝试在Google和StackOverflow上搜索此内容,但由于我的搜索查询中包含标点符号,因此搜索引擎往往会对其进行修改并给出无意义的结果。在我的rails应用程序(rails3.2.11,ruby1.9.3)中,我的日志经常是这样的:StartedGET"/apply/contact"for127.0.0.1at2013-01-2917:35:21-0600ProcessingbyJobApplicationsController#showasHTMLParameters:{"id"=>"contact"}[1m[36mJobAppl

  2. UnityShader(六)透明效果 - 2

    一、如何实现透明效果在Unity中实现透明效果的方式有两种,其一是透明度测试,其二是透明度混合。透明度测试:这种方式不需要关闭深度写入,且实现机制非常简单粗暴。只要一个片元的透明度不满足条件(比如小于某个值),则该片元会被直接舍弃,否则就按照不透明物体的处理方式来处理。它产生的效果要么是完全不透明,要么是完全透明,并不是真正的半透明效果。透明度混合:这种方式会使用当前片元的透明度作为混合因子,与颜色缓冲中的颜色进行混合。这就需要关闭深度写入。而关闭深度写入意味着我们需要非常小心物体的渲染顺序,否则可能出现渲染问题。为什么要关注渲染顺序在之前的Shader中我们并没有关心过物体渲染顺序的问题。这

  3. javascript - 使用带有 Protractor 的 Firefox 35 导致错误 - 2

    使用chrome运行我的Angular应用程序场景场景运行成功,但在firefox新版本35.0b6时发生停止。任何人都请帮助我提前谢谢。我使用的是Protractor1.4.0。我的场景:describe('99ccse2etesting',function(){it('checkithaveatitle99CCS',function(){browser.get('http://99ccs.com/ccsnew/#/login');//itchecksthe"http://99ccs.com/ccsnew/"pagecontainsatitle"99CCS"expect(browse

  4. javascript - 无法在 Firefox 35 插件中创建内联 Web Worker - 2

    我正在开发一个基于XUL的Firefox扩展。我正在尝试使用BLOB创建一个内联WebWorker。该代码曾在Firefox33中运行,但在更新到Firefox35后出现错误。这是一个代码示例:try{varblob=newBlob(["functionf(){}"],{type:"application/javascript"});varurl=window.URL.createObjectURL(blob);//blob:null/371e34bd-1fbf-4f66-89cc-24d0c1c7bad5returnnewWorker(url);}catch(e){console.e

  5. 我的NBIOT学习——BC35-G用AT指令通过CoAP协议连接华为云 - 2

    一、首先在华为云物联网平台上的创建产品与设备模型定义、插件开发这就不赘述了 注:我尝试用了不加密的方式,但是无法连接;选择加密的注册方式,可以连接注:NBIOT的密钥(PSK)仅支持32个16进制数(0-f),        例如:a22aaa699997ff90fbc1ac89aab94a99二、通过AT指令使NBIOT连接上华为云1.设备初始化:1AT2AT+CFUN=03AT+QSETPSK=,        注:pskid填0,psk即上面自己写的密钥4AT+NCDP=,        注:ip获取方法,端口号填5684  5AT+QSECSWT=12.开始连接iot平台:1AT+CF

  6. 华为员工年龄曝光,35岁职业危机不存在? - 2

    一直以来,互联网界都有着“程序员是吃青春饭”的说法,这一年龄危机甚至逐渐演变为“45岁退休,35岁换人”的段子。并且在“华为大力清洗34岁+的老员工”这则新闻出来后这个话题更加的被愈演愈烈。近日,华为在官网上发布了有关员工年龄层的相关数据,30岁以下员工仅占28%。这个数据自然引发网友的质疑,相关评论迅速占据热评榜首。35岁到底是不是程序员职业寿命的上限?这个问题再度被热议。为什么都认为程序员是吃青春饭?大家都知道中国IT行业起步于九十年代,有经验的老程序员因为业务的需要,很多已经转岗了。而市场上的程序员人才以初级、中级居多,这导致了目前公司里40+的程序员寥寥无几。国内互联网公司996模式,

  7. flink学习35:flinkSQL查询mysql - 2

    总览:   importorg.apache.flink.streaming.api.scala._importorg.apache.flink.streaming.api.scala.StreamExecutionEnvironmentimportorg.apache.flink.table.api.EnvironmentSettingsimportorg.apache.flink.table.api.bridge.scala.{StreamTableEnvironment,tableConversions}objectsqlQueryTable{ defmain(args:Array[St

  8. windows - 使用 Python35 在 Windows 10 中安装 Tensorflow - 2

    我想在Windows10中使用Tensorflow(仅支持CPU)。我尝试了以下命令来使用pip安装但没有成功。知道如何解决这个问题吗?C:/Python35/Scripts/pipinstall--upgradetensorflow在CommanPrompt中运行它,我收到以下错误:CollectingtensorflowCouldnotfindaversionthatsatisfiestherequirementtensorflow(fromversions:)Nomatchingdistributionfoundfortensorflow我也尝试过使用pip3或URL安装:C:\

  9. 「UnityShader笔记」12.Unity中的前向渲染(Forward Base) - 2

    Part1.Unity前向渲染的介绍1.1前向渲染的基本原理前向渲染的主要特点是针对每个物体,对于每个光源都会分别进行一次光照计算,最后的颜色值是由所有光源的光照结果混合而成的,比如场景中有M个物体,N个光源,则渲染整个场景需要N×M个Pass,可以看到如果光源数目多,前向渲染的开销是非常巨大的为了解决这个开销问题,选让引擎常常会限制在每个物体上进行逐像素光照的数目,Unity引擎也是这样做的1.2Unity中前向渲染的实现原理Unity的前向渲染中,实现光照有三种方式:逐像素处理、逐顶点处理、球谐函数(SH),它们的开销是依次递减的Unity中,我们可以手动设置光照的重要度模式,有三种可选:

  10. 工作7年了,从“功能测试”到了现在的“测试开发”,年薪35W+,分享下我的心得 - 2

    前言时光飞逝,转眼间从事软件测试已经是第7个年头了。从最开始的毛头小子到现在的独当一面经历了太多,也学习了太多知识,所幸最后结果是好的,目前在上海工作从事测试开发岗位,年薪35W+,曾就职于美团测试开发框架组,搭建过美团platuo测试框架,thrift测试框架,自动化测试平台,熟悉python3,java,vue,在多家公司从0到1搭建过自动化测试框架,保障过亿级流量服务的质量工作。今天就分享分享我的心得和我的学习路线以及我整理的学习资源选择测试的原因  我大学学的是计算机专业,对于IT互联网行业,那也算是正统科班出身吧,大四那年就进了一家还挺大的软件公司实习,开发公司的自主产品,一个线上管

随机推荐