
Bloom在游戏中是一种比较常见的效果,是通过后期处理的方式,来达到模拟现实生活中物体在强烈光源照射下,出现“光晕”的效果。本着学习,举一反三的精神,本文将探讨几个问题:
- Bloom的基本原理与效果
- Bloom与HDR的关系
- Bloom的美术意图
- Bloom在Unity中的基本实现
一、Bloom的基本原理与效果

拿我之前做的一个小恐龙作为本文的模特,这个就是最终的效果(Unity 的Post process stack 中的bloom),如果关闭bloom

两张图的最明显的地方,则在于上图阳光洒下的地方,会产生强烈的光晕,这个就是bloom效果,大家也常叫这种特效辉光。
接下来,动手在ps里面,拿一张图片,手动的模拟一下这个过程,感性的认识一下原理:
PS:这个过程只是可视化的展示一下原理,结果不准确
之前说了一组重点词,我提取出来:”强烈光源照射下,出现的效果“,”阳光洒下的地方“。我们干的第一件事情就是找出”阳光洒下的地方“也就是受光部分:
先通过色阶,找出图像中的受光(亮度更高的区域):

接着,将这部分作为选取,复制出这个选取的原图(不是这个调整过色阶的图)



通过选取复制出来之后,这就是基本上这张图上的受光情况,这就是我们需要bloom的区域,接下来,将这个图层做一次高斯模糊。

我这里随便拉一下,得到一张模糊后的图像,最后一步,将这个图像与原图相加即可(调整图层混合模式为 线性减淡(线性减淡就是与下层图像进行相加):

为了清晰,我把这里的名字重新做了梳理,从下至上的处理过程,其实Bloom的实现也是这么个流程。
最终我们的到了这样一张:


为了效果强烈一些,我多复制了一层并且透明度减少了50%,与原图做一个对比。这样,我们也得到了一个看起来略显廉价,但是也保留了主要特征的bloom,我们在与unity post process stack里面的bloom做一下对比:

导致这样的问题是因为 Unity 进行bloom特效时,所拿到的原始数据精度要远高于QQ截屏下来图像,请记住这个廉价的效果,后期会用到,不过再实际编码过程中处理的流程其实一样的。
梳理一下:bloom其实就是拿到一张图像,对期望区域进行模糊处理,再加回到原图,最后输出。
二、Bloom与HDR
刚做这行不久的时候,其实HDR(High Dynamic Range)这个名词总是会频频出现,尤其是与Bloom也常常绑定着一起出现。所以一直以来心中都有一个疑问,到底该怎样理解HDR,它与Bloom又有着什么样的关系?本章的重点是形象的理解,而非上来就是”高动态范围“这些初次看见就想骂娘的解释。我们挑战一下这个课题。
我们先做这么一个实验,在引擎里始终打开bloom,在HDR和非HDR的环境下观察效果。

我们看到左边的HDR使用的是Use Graphics Setting 由于我们的目标平台为PC所以在Graphics Setting里面默认是使用HDR的。(这里的版本是Unity 2019.2.4f1)

先来观察打开HDR的图像,是这样的:

我们再次关闭HDR,观察图像,是这样的:

Bloom始终打开,但在HDR与非HDR(LDR)的两者环境下的图像,受光部分出现了比较强的反差,没有HDR支持的小恐龙受光部分的光晕弱了很多,那是什么导致了这样的原因?还记得之前在PS里面处理的廉价版的Bloom图像吗?是不是很像?我拿出来做一下对比

所以我们可以得到一个初步的判断,非HDR下bloom所处理的图像,跟我们人手QQ截图下来送进ps里的图像,基本是一样的。那为什么HDR下的效果会如此强烈?是什么导致了这样的反差?上面也说过,是两者处理时使用的数据精度不一样所造成的。那要如何形象的理解这里的精度不一呢?我认为如果形象的理解了这个精度,那么对于得到这两个结果的不同就豁然开朗,如下图:

我们就先用一个亮度的概念来解释:HDR比LDR拥有着更多的亮度。也就是说,HDR跨过了LDR的最大存储容量(每通道8bit,共32bit的容量),这意味HDR能拥有更多的颜色,如果归一化解释,假设这个LDR下亮度最多到1,那么HDR则会突破这个限制超过1。
我们在ps里处理过程中,有一步是抠出受光面,这一步是通过色阶完成的,最终扣出来的那片区域,其实范围也是LDR的,因为我们模糊的图像,是基于QQ截图下来的,Unity内部进行模糊的数据是HDR范围下的,它拥有着比QQ截图更大的亮度,更多的颜色(更高的精度)。
为什么QQ截图是LDR的?因为虽然处理的是HDR数据,但还得是LDR下输出(我们显示器所限),而我们截取的屏幕,也是输出过的,因此截屏是不会得到HDR数据的。

我们假设红色部分是我们要模糊的那部分像素,LDR下,我们拥有的精度比HDR下少很多的信息,因此得到的结果也就比HDR下弱很多。
当我们拿到更高精度的数据去处理图像的时候,那么得到的也将是更高精度的结果。
总结一下:开启HDR后,ColorBuffer里则会存储更多的数据,当跟一些特殊的后效搭配,可以得到一些更高级的效果,如果把这个”高动态范围“解释为”高精度范围“或者美术更好理解一点的”高明度范围“会不会更好理解一点呢?
三、HDR&Bloom的美术意图
不同的项目或许风格迥异,但都希望在画面上获得更多的宽容度,拉高上限来获得更高自由度去创造虚拟世界,不管是明度对比,还是色彩对比,画面更多的层次、更丰富的变化,永远是美术人追求的目标。这就像是在地面有一个道具,策划往往希望玩家能很明显的观察到这个可交互物,这时就需要一些辅助手段,比如加个特效,换个OutlineShader等,这本质上就是一种拉开画面层次的手段,尽管这个做法并非基于画面美感去考虑的。美感的表达往往是在统一中寻求变化,在这个统一的框架下,变化越是丰富,画面也越经得住眼睛的长时间的考验。我也认为这也就是为什么2D美术即使像油画般精致,却还是被实时渲染的历史潮流无情碾压,实时渲染可以有着更丰富的即时变化,这些技术都是动态的,且还在不断地进步发展,是动态的,那就说明可以给画面提供更丰富的变化。 那么从美术上,肯定也是需要这种拉开美术关系,画面层次的手段。
HDR&bloom就是其中的一种手段,在提升画面层次,气氛的营造有很重要的作用,它可以让静谧的夜晚显得更神秘,正午的阳光更炙热。没图是说明不了问题的:

那如果我将这个场景的层次进一步拉开,我们打开bloom

意境在bloom加入之后得到了进一步提升。光晕让视觉中心得到了进一步的强化。也让”月光“这个看不见摸不着的东西,在画面中也能得到隐约的感受。
总结:更高的技术宽容度,即更高的表现宽容度
四 、在Unity中实现Bloom
本章将通过Unity给出的API OnImageRender下实现这个特效,探索以上所说的内部原理,并实现它,最终我们会得到一个与unity post process stack 里效果很接近的bloom。

首先我们捋一下大致处理的流程,以免跑偏:
- 获取当前图像
- 抠出期望区域
- 对期望区域做模糊
- 叠加回源图像
- 输出
1、获取当前图像
首先我们简单的搭一个场景:


然后新建一个在摄像机新建一个脚本,Bloom.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class Bloom : MonoBehaviour
{
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
Graphics.Blit(source, destination);
}
}
打开脚本,将自带的Start 与Update 方法删掉。为了确保我们的效果在非运行时下可以看到效果我们加一个Header
[ExecuteInEditMode]
为了让我们的Scene视图下也可以观察到效果,后面再跟一个
[ExecuteInEditMode,ImageEffectAllowedInSceneView]
OnRenderImage方法有两个类型为RenderTexture参数,source 指的是当前相机渲染的图像,destnation是目标图像, Graphics.Blit可以将source(当前摄像机图像)传递给 shader,通过一系列操作之后输出到destnation,Unity最后会将destnation写回backbuffer(你的屏幕)。当前这个方法什么都没做。
接下来为了更容易理解,先不着急往下走,继续做一些测试。
我们接着将文件夹做一些归类

然后再Shader文件里新建一个最基本ImageEffectShader:


这里我们就叫bloom,打开shader,看看里面都什么玩意:
Shader "Hidden/Bloom"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
// No culling or depth
Cull Off ZWrite Off ZTest Always
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _MainTex;
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
// just invert the colors
col.rgb = 1 - col.rgb;
return col;
}
ENDCG
}
}
}
shader很简单,不做介绍了,值得注意的是frag 方法里:
col.rgb = 1 - col.rgb;
return col;
这儿对颜色做了一个反转,这个有利于我们做测试,看看着色器是不是工作,如果工作,屏幕上应该会出现一个颜色反转(类似底片)的效果。
再次打开Bloom.cs:
...
[SerializeField] Material mat;
[SerializeField] Shader bloomShader;
const string m_bloomShaderID = "Hidden/Bloom";
private void Awake()
{
bloomShader = Shader.Find(m_bloomShaderID);
mat = new Material(bloomShader);
}
...
将材质跟shader在Awake里做初始化,然后返回编辑器,看看有没有得到正确结果:

正确,但目前我们并没有看到底底片效果,目前OnRenderImage()里还没有用到我们当前的这个shader做处理,接下来我们将代码稍作修改:
Graphics.Blit(source, destination,mat);
Graphics.Blit有很多重载,我们选择有Material类型的重载,然后将材质传递进去。

源数据通过材质指定的shader处理过后写到destnation里,最后得到了一张底片的图像,这个最终被写回backbuffer,变成你看到的图像。
还记得之前的公式 最终图像 = 原图像+模糊后的图像。
在模糊图像之前,再探讨一个问题:降采样。我们不会去对原图直接进行模糊,因为原图的尺寸比较大,且blur之后又是一张太清晰的低频图像,因此我们会再得到原图之后,进行一个降采样(更低分辨率)再进行模糊。模糊一般是对图像像素做卷积(术语是这么说的,我一直不太理解为什么叫卷积,一开始以为是春卷的一种衍生食品?),我所理解的卷积其实就是对一个像素上周围每一个像素,都分配一个权重,通过每个像素乘以权重最后加权,得到最终值。常见的方法有高斯过滤,盒式过滤,高斯过滤的好出就是效果比较好好,但是消耗也相对大一些,以至于常常把高斯操作分成两个pass进行单独的横向与纵向模糊最后合并起来。本章我们用效率较高的盒式过滤(Box Filter),直接使用这种方法的效果比较差,因此我们会配合一些技巧来提高图像质量。
首先我们来介绍一个概念,渐进式的降采样与上采样:一般我们为了拿到比屏幕分辨率小的图像时会这样做:
int width = screen.width/6;
int height = screen.height/6;
一个简单的实验:
int width = source.width/6;
int height = source.height/6;
RenderTexture tmp = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(source, tmp, mat);
Graphics.Blit(tmp, destination);

我们把原图缩小1/6,然后渲染出来,确实得到了正确的结果,但这个结果效果并不好。理论我们是可以拿着这个图去找GPU让它来帮我们处理,而我们打算弃用高斯滤波,这么粗糙的结果盒式过滤表示拯救不了你,而我们想在预处理阶段就得到一个总体满意的效果。
一步一步来:
所以我们需要渐进式的降低分辨率与提高分辨率:每一次降低一倍的分辨率,然后迭代N次,然后每次再提升一半分辨率,迭代N次。为什么要这样做,迭代两次跟直接除以10有啥区别?这其实是利用了引擎的双线性过滤对图像做一个间接的模糊,如果我们在每一次迭代中,再加入盒式滤波,得到的效果会非常好 我们通过PS来感性的认识一下,先把一张图直接缩小10倍,然后放回原本大小:

接着我们直接降低10倍,

注意:虽然PS的画布的尺寸改变了,但在引擎里,画布的大小是不可变的,我们改变的只是图像的分辨率。然后,我们要再把它放回原来大小:

当我们将一张低分辨率的图像填充回屏幕的时候,PS会有一个选项,对图像从新做一次采样,以平滑图像。


直接缩放10倍的前后对比,第一张已经比原先的图像要模糊很多。

当低分辨率图像填充回画布时,两个原始的像素位置(绿点)的最终回出现在两个橙点的位置上,那么绿点中间这些像素就会通过插值,来匹配它们最终的位置(线性过滤),当我们将小分辨率的图像填充回屏幕时,unity也会做同样的操作。
接下来。我们这样做,先将原始图像,每一次分辨率减半,然后重复这样的操作4次,然后每次放大图像一倍,再重复操作4次。
(4次是因为如果我们进行第5次时,分辨率宽度会从120下降为60,而直接除10,分辨率是120,为了保证结果的一致性,这里我们做四次,而不是5次)

最终,我们得到了这样一个结果,在这种情况下,在我们还没有添加其他额外操作,就已经得到了如此平滑的效果。
放大对比一下细节,

放大之后对比,左边为直接处理,右边为渐进处理,渐进处理的效果要明显好过直接处理。
接下来,我们用同样的原理,放进unity里面,我们会通过最笨,最直接的办法做测试,这样易于理解,关于如何聪明的包装这些代码,相信你比我能。
2.抠出期望区域
3.模糊图像
这两部分我们放在一起做,先处理模糊,再处理抠出期望区域(讲解顺序,非实际处理顺序)。
我们先将屏幕分辨率降低一半然后输出:
int width = source.width;
int height = source.height;
RenderTexture tmpSource = RenderTexture.GetTemporary(width, height, 0, source.format);
RenderTexture tmpDest;
Graphics.Blit(source, tmpSource);
tmpDest = RenderTexture.GetTemporary(tmpSource.width/2, tmpSource.height/2, 0, source.format);
Graphics.Blit(tmpSource, tmpDest);
RenderTexture.ReleaseTemporary(tmpSource);
Graphics.Blit(tmpDest, destination);
接着我们每次降低一半,重复四次:
width = source.width;
height = source.height;
RenderTexture tmpSource = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(source, tmpSource);
//分辨率降一半-------------------------第一次
width/=2;
height/=2;
RenderTexture tmpDest = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(tmpSource, tmpDest);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
//分辨率降一半-------------------------第二次
width/=2;
height/=2;
tmpDest = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(tmpSource, tmpDest);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
//分辨率降一半-------------------------第三次
width/=2;
height/=2;
tmpDest = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(tmpSource, tmpDest);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
//分辨率降一半-------------------------第四次
width/=2;
height/=2;
tmpDest = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(tmpSource, tmpDest);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
Graphics.Blit(tmpDest, destination);
RenderTexture.ReleaseTemporary(tmpDest);
注意释放Temporary的时机,就像将A盒子的东西放入B盒子里之前,需要先把B盒子里面的杂物拿出来一样。
Graphics.Blit(tmpSource, tmpDest);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;

这是我们得到的结果,进frame debug里面瞧瞧:

Drawcall 从第12开始到16,分辨率逐次降低,并在第17个Drawcall铺满画布。这么看来,渐进式的下采样就完成了,接下来我们沿用这种思想,逐次进行上采样:
//分辨提升一半-----------------------第一次
width*=2;
height*=2;
tmpDest = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(tmpSource, tmpDest);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
//分辨提升一半-----------------------第二次
width*=2;
height*=2;
tmpDest = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(tmpSource, tmpDest);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
//分辨提升一半-----------------------第三次
width*=2;
height*=2;
tmpDest = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(tmpSource, tmpDest);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
//分辨提升一半-----------------------第四次
width*=2;
height*=2;
tmpDest = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(tmpSource, tmpDest);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
Graphics.Blit(tmpDest, destination);
RenderTexture.ReleaseTemporary(tmpDest);

仅通过渐进式的下采样与下采样的技巧,已经可以得到让人接受的模糊效果。我们用直接处理的方法,做一个对比:

知道目前,代码中并没有让shader参与任何计算,接下来将在渐进采样的过程中为图像进行盒式过滤:
half3 Sample(float2 uv)
{
return tex2D(_MainTex,uv).rgb;
}
half3 BoxFilter(float2 uv)
{
half2 upL,upR,downL,downR;
upL =_MainTex_TexelSize.xy*half2(-1,1);
upR =_MainTex_TexelSize.xy*half2(1,1);
downL =_MainTex_TexelSize.xy*half2(-1,-1);
downR =_MainTex_TexelSize.xy*half2(1,-1);
half3 col =0;
col+=Sample(uv+upL)*0.25;
col+=Sample(uv+upR)*0.25;
col+=Sample(uv+downL)*0.25;
col+=Sample(uv+downR)*0.25;
return col;
}
_MainTex_TexelSize是每一个像素的纹理坐标,通过它我们得以找到每一颗像素的位置,然后以一颗像素为中心,他周围的像素应该会在他的 左上,右上,左下,右下。我们有4颗像素参与计算,因此每个像素的权重是1/4=0.25这样,最后再将他们加起来即可。
到目前为止我们的代码并无明确指定使用这个pass进行处理,所以加上:
Graphics.Blit(tmpSource, tmpDest,mat);

效果相当赞,并不比高斯过滤差。

抠出期望区域:
bloom只会在亮度很强的地方发生,因此,需要得到图像亮度峰值的区域。只需要再图像处理之前,将图像做一次预处理即可:
float _Theshold;
//亮度分布
half3 PreFilter(half3 c)
{
half brightness = max(c.r,max(c.g,c.b));
half contribution = max(0,brightness-_Theshold);
contribution/=max(brightness,0.00001);
return c*contribution;
}
如果直接用减法也可以,但不够精确,这里我们通过比较rgb三个通道最大值来得出亮度值,然后在shader里面公开一个参数,这个参数将通过C#控制图像的亮度阈值。
SubShader
{
// No culling or depth
Cull Off ZWrite Off ZTest Always
Pass//0 预处理
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 frag (v2f i) : SV_Target
{
return fixed4(PreFilter(BoxFilter(i.uv)),1);
}
ENDCG
}
Pass//1--盒式过滤
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//...
ENDCG
}
}
然后在C#里面
[Range(0,10)]
public float theshold=0f;
...
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
mat.SetFloat("_Theshold",theshold);
...
}


仔细观察,在非HDR的情况下,当阈值快到1的时候,整个图像无限接近黑色,在HDR环境下,因为物体材质的自发光有更高亮度的情况下,阈值就算超过1,图像也是有信息的。
最后我们创建一个合并的pass:
Pass//--合并
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 frag (v2f i) : SV_Target
{
fixed3 sourceColor = tex2D(_SourceTex,i.uv).rgb;
return fixed4(sourceColor+BoxFilter(i.uv,0.5),1);
}
ENDCG
}
_SourceTex是原始没有处理过的图像,我们现在将两者合并:
mat.SetTexture("_SourceTex",source);
Graphics.Blit(tmpDest, destination,mat,3);
RenderTexture.ReleaseTemporary(tmpDest);
然后观察scene view,得到这样的效果:

在到里,我们的bloom可以正常工作了,但是效果并不好,可为什么不好呢?当我们去辨识一个有色发光体时,真正决定光色相的不是发光源,而是光晕:

假设两个正方形,都做一个相同色相的辉光处理,虽然右侧方形是纯白,但我们还是会认为,这是一个橙黄发光体,并且亮度会比左侧更高,甚至左侧的东西都不像是在发光,因为真正的发光体跟右侧一样,光源中心的亮度,色相,纯度,都跟光晕有差别:

非常高亮的光源一般中心亮度很高高,纯度低,色相不易被察觉,而越往外,色彩倾向越明确,不仅亮度,纯度会发生改变,色相也会发生变化,将最外层的颜色处理为红色,我们依然认为,他是一个橙色发光源,且比上图在感官上有更丰富的变化,自然也更漂亮,甚至因为色相的改变,上图左侧不那么像光源的方形,在下图中更加像一个发光体了。
因此我们的效果与post process stack 中的bloom还有这最后一公里的差距,因为到目前为止,我们的光源与光晕的色相,纯度,都没有太多的变化,那么我们怎么做?
我们需要累计图像亮度与颜色,每一次模糊图像时,都期望与上一个模糊的图像做一个加法混合,而不只是单纯的缩放分辨率与模糊:
Pass//--盒式过滤
{
blend one one
... ...
}

虽然是个错误的效果,但我们也可以发现事情正在往好的方向发展:

从白色,到天蓝色,再到深蓝色,亮度,色相,纯度都有了变化,所以累计亮度是对的。
这个错误是因为每一个drawcall都与上一个的结果进行相加,不断累积了亮度。但第一我们不需要在降采样的时候累加,第二不是从backbuffer 中GetTemporary,而是在上采样的时候,累加之前下采样的结果。
我们的方法是,用一个数组,将之前下采样的结果都存起来,在上采样的时候,数据从数组中取出来,然后累加,我们也不需要再下采样的时候累加,这个时候我们需要再上采样时再用 blend one one的pass做累加:
Pass//2--盒式过滤+亮度累加
{
blend one one
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 frag (v2f i) : SV_Target
{
return fixed4(BoxFilter(i.uv,1),1);
}
ENDCG
}
然后我们再上采样的时候用这个pass 去处理rt:
RenderTexture[] textures =new RenderTexture[16];
RenderTexture tmpDest=textures[0] = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(source, tmpDest,mat,0);
RenderTexture tmpSource =tmpDest;
int i =1;
for (; i<7 ; i++)
{
width/=2;
height/=2;
tmpDest =textures[i] = RenderTexture.GetTemporary(width, height, 0, source.format);
Graphics.Blit(tmpSource, tmpDest,mat,1);
tmpSource = tmpDest;
}
for(i-=2;i>=0;i--)
{
tmpDest = textures[i];
textures[i] = null;
Graphics.Blit(tmpSource, tmpDest,mat,2);
RenderTexture.ReleaseTemporary(tmpSource);
tmpSource = tmpDest;
}
mat.SetTexture("_SourceTex",source);
Graphics.Blit(tmpDest, destination,mat,3);
RenderTexture.ReleaseTemporary(tmpSource);
现在我们迭代了7次,来对比一下之前跟之后的效果:


跟unity post process stack中效果非常接近。


至此,我们的HDR&bloom就到此讲完了,下一篇文章将讨论一些优化以及如何在LDR下也能得到跟HDR下相似结果的Tirck,虽然现在的机能(不论手机还是桌面)这点算力不算什么,但在移动AR/VR领域下还是很有用处的。
广告时间:本人所在的Ximmerse(广东虚拟现实)是一家AR硬件/软件/服务一体化的公司,近期推出了全息博物馆(HoloMuseum)面向全年龄,目前已经进入到试运营阶段,想想这样一群海龟从头上飘过是一种什么体验?
欢迎大家来抓龟 ^ ^:广东省深圳市南山区中心路宝能太古城北区负2层。