UnityShader35:光晕光效

这篇具有很好参考价值的文章主要介绍了UnityShader35:光晕光效。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、光晕逻辑

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

UnityShader35:光晕光效

1.1 Unity URP 光晕

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

UnityShader35:光晕光效

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

1.2 光晕的可见性

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

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

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

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

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

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

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

UnityShader35:光晕光效

这个也很好解决,就是在中心点周围随机散点多次,分别采样计算是否被遮挡,然后拿得到的遮挡比率去乘上光晕颜色作为最终的贡献,由于这块是在顶点着色器中做的,而你光晕的贴片顶点很少,因此性能还算 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

UnityShader35:光晕光效

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

二、手机各平台兼容 

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

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

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

UnityShader35:光晕光效

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 功能,其中也说明

UnityShader35:光晕光效

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

不过也很奇怪,如果真的不支持整个 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

好了,搞定!文章来源地址https://www.toymoban.com/news/detail-428855.html

到了这里,关于UnityShader35:光晕光效的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 枪神8奥创失灵,键盘光效变成蓝色,无法修改键盘背光

    问题描述:买了枪神8后,某一天更新奥创后,光效变成蓝色,奥创中心无法识别键盘,或者无法控制键盘。 问题解决: 官方建议卸载奥创,然后重新安装,但是情况依然没有改变 后续:我发现是Win11动态光效和奥创驱动发生了冲突,Win11控制了键盘光效,覆盖了奥创的控制

    2024年02月22日
    浏览(207)
  • (数字逻辑笔记)用Verilog实现一个简单ALU(组合逻辑)

    实验描述: 输入 :两个4位二进制数,代表两个操作数A,B;一个3位控制信号operation,代表ALU要进行的运算。本实验中,ALU可以实现8种运算: 输出 :4位结果,1位进位 operation | F 000 | A + B 001 | A - B 010 | B + 1 011 | B - 1 100 | NOT A 101 | A XOR B 110 | A AND B 111 | A OR B 实现代码: TestBen

    2024年02月04日
    浏览(50)
  • Stable DIffusion 炫酷应用 | AI嵌入艺术字+光影光效

    目录  1 生成AI艺术字基本流程 1.1 生成黑白图 1.2 启用ControlNet 参数设置 1.3 选择大模型 写提示词 2 不同效果组合 2.1 更改提示词 2.2 更改ControlNet 2.2.1 更改模型或者预处理器 2.2.2 更改参数 3. 其他应用 3.1 AI光影字 本节需要用到ControlNet,可以查看之前博文 Stable Diffusion 系统教程

    2024年02月06日
    浏览(40)
  • vue 使用indexDB 简单完整逻辑

    1 npm 2 代码

    2024年02月12日
    浏览(23)
  • 学生成绩管理系统(逻辑清楚-简单实用)

    1.1、需求分析概述 需求分析是我们在软件开发中的重要环节,是软件开发的第一步也是最基础的环节,这将决定我们所实现的目标以及系统的各个组成部分、各部分的任务职能、以及使用到的数据结构、各个部门之间的组成关系和数据流程,为我们的系统设计打下基础。 1

    2024年02月09日
    浏览(41)
  • [FPGA 学习记录] 简单组合逻辑——多路选择器

    封面来源:Multiplexer 在本小节中,我们将使用 Verilog 语言描述一个具有多路选择器功能的电路,目的是学会使用 Verilog 语言实现简单的组合逻辑 本小节的主要内容分为两个部分:一个部分是理论学习,在这一部分我们会对本小节涉及到的理论知识做一个讲解;另一个部分是实

    2024年02月03日
    浏览(34)
  • 如何在 3ds Max 中使用 Mental Ray 制作逼真的草地和带有光晕的天空

    推荐: NSDT场景编辑器助你快速搭建可二次开发的3D应用场景   首先,您将创建一个 平面 对象,然后添加一个 噪点修改器。 在此之上应用毛 发和毛皮 修饰符 。  这将用于模拟逼真的草地。 我们用 日光系统 创造太阳和天空。为太阳添加 镜头 和 戒指 效果以及酷炫的逼真效

    2024年02月16日
    浏览(46)
  • Unity中敌人简单逻辑的实现(来回走动,攻击)2D

    unity自带一套自动巡航系统,但是目前应该先了解最基本的使用代码控制敌人实现逻辑(1来回走动,2发现玩家时追着玩家,3进入敌人攻击范围时进行攻击),一般来说这是最基本的敌人的功能 分析完敌人所具备的能力后,就将敌人的能力进行拆解,分别进行实现 一 来回走

    2024年02月12日
    浏览(43)
  • 若依框架快速开发项目(不涉及底层逻辑,只是简单使用)

    初衷: 若依框架使用及其普遍,是一个非常优秀的开源框架,框架本身的权限系统,字典设置以及相关封装,安全拦截相当完善,本人受益匪浅,学学到了许多,在这里,先向原创作者致敬! 本人刚刚接触这个框架的时候,很迷茫,几乎没有入手的地方,不知道怎么去开始

    2024年02月03日
    浏览(90)
  • 机器学习算法基础--逻辑回归简单处理mnist数据集项目

    目录 1.项目背景介绍 2.Mnist数据导入 3.数据标签提取且划分数据集 4.数据特征标准化 5.模型建立与训练 6.后验概率判断及预测 7.处理模型阈值及准确率 8.阈值分析的可视化绘图 9.模型精确性的评价标准

    2024年02月07日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包