Blender|在材质中使用LUT进行昼夜循环
Thepoly原创 20976 0
实名

通过了实名认证的内容创造者

发布于 2023-7-10 17:15:58

您需要 登录 才可以下载或查看,没有账号?注册

x

Hello . 大家好
今天给大家带来外网的一篇用LUT做TOD的分享

      
fe56da19f224bf1d0a99bf4279776dcd.png
         
本文将介绍使用查找表LUT来进行昼夜过度或调整场景的感觉。示例里面使用的是Blender,并在unreal中进行了测试,你也可以用在unity中。
具体文件在Github:https://github.com/IRCSS/Day-Night-LUT-Shader
[micxp_wxonkey]wxv_2987673897328984068[/micxp_wxonkey]1背景
对于《Puzzling Places》(一个游戏),我们想尝试在玩家解谜过程中,地点会在白天和黑夜之间切换。我们当然可以有两种不同的纹理,一种用于白天,一种用于夜间。随着玩家的推进,我们可以在它们之间进行插值。然而,这意味着片段着色器中始终采样两个 8k 纹理。这既是巨大的内存成本,也是带宽压力。
在我们的方案中,不需要第二个纹理。你可以使用查找表来更改场景的外观和感觉。
查找表是转换图,它采用标准纹理颜色并将其转换为修改后的颜色空间。不是对颜色进行变换矩阵,而是在表格中烘焙了该变换,然后将变换矩阵转换为一系列样本,最后通过采样和插值重建原始变换函数。
从美术角度来说,你在表格中烘焙了一堆调整图层,该表格代表了每种颜色最终的外观。
由于我们在场景中进行的大多数颜色变换都是线性的且频率不高,因此我们可以使用分辨率非常低的 LUT。尽管我们的标准每通道 8 位颜色具有 256 种可能变化的深度,但我们可以使用少至 16 个样本来成功覆盖大多数颜色转换。对于不懂数学的人来说,可能已经被低频和线性颜色变换整蒙了,但希望文章最后能让你比较清晰。
首先,让我们先讨论一下美术面的问题,并介绍如何创建 LUT 本身,然后再继续讨论着色器部分。


2生成夜间场景
我们需要弄清楚如何改变场景的颜色,以便玩家体验与夜间相同的场景。
我们可能最开始的想法就是让一切变得更暗!这就是很多现在的电影也采用的办法,这是一个可怕的想法。我经常发现自己盯着电影的画面,很难弄清楚发生了什么,因为太黑了。
看看迪士尼的阿拉丁的这两个场景。

f7cceaef58b734cee9e7f9ca6579ec8b.png


显然夜间的场景更暗,但是也不是特别暗,还是一个清晰的夜间场景。我们来检查一下他们的明度数值




可以看到夜间版本和白天版本的明度值几乎相同。如果我们看一下直方图,这一点就变得更加明显。




第一张图片是夜间版本直方图,第二张图片是白天版本夜间版本没有很多明亮值(或亮点)。其总范围或多或少压缩到值范围的前半部分。但如果你查看分布曲线的中间,你会发现实际中位数仅移动了 10%!因此,图像平均仅暗 10%。更重要的是,大多数像素的亮度仍然高于 10%!还值得注意的是,几乎没有任何真正的暗像素,并且几乎没有纯黑色。
作为比较,这是电影《蝙蝠侠》中的一个场景。图像太暗,亮度中值可能是 5%。


更好的例子是《风之谷》的夜景。这些值与白天场景相当,但仍可作为夜间场景读取。


诀窍就是让场景变暗 10% 到 20%。但请确保只移动曲线的中间,这样就会使平均像素变暗,但不会将暗像素推得更暗!否则会将大量像素推向纯黑色,而这是很少需要的。
在存在浅色纹理的情况下通常会更加“清晰”。这意味着还有更多细节。这意味着表面上的微观对比度更高。通过降低高光和中等亮度,你还可以有效地降低对比度。
添加蓝色和紫色的深色调也很重要。
在夜间,我们获得的任何照明要么是天光,要么是月光。天光有很强的蓝色成分,我们感知到的月光也是蓝色的。只是说感知,因为月光实际上并不是蓝色的。在较暗的环境中,我们的颜色感受器会关闭,而杆状感受器对蓝光稍微更敏感。因此,在低光照条件下,我们感知到的月光是蓝色的。不过,我们仍然会使用蓝色作为夜间的指示符。毕竟,我们对世界的心理模型比事物的物理运作方式更重要。
一般我喜欢做的是使用Photoshop中的曲线调整图层来降低中间和顶部像素的亮度。但我还在曲线的下部放置了控制点,以确保较暗的区域不会变得太暗。然后,我使用“选择性颜色”向阴影添加大量蓝色(此处所有环境照明均为蓝色),向高光添加大量蓝色(我们假设主要光源是月亮),并降低红色和黄色的对比度。


在场景中,最终得到的是颜色变化,如下所示:
[micxp_wxonkey]wxv_2987675111680655364[/micxp_wxonkey]最后一步,是将这些颜色转换“烘焙”到 LUT 中,这是一项相当简单的任务。首先,你需要获取恒等 LUT,这是其中没有颜色转换的 LUT。


如果将此 LUT 应用于你的图像,就好像你将一个数字乘以 1。什么都不会改变。你可以在 Photoshop 中导入此 LUT,并将该图层放置在所有调整图层下方。像这样,你已经在默认颜色空间上烘焙了所有这些更改。然后,你只需将其导出为不应用压缩的格式即可。例如,视频中的场景是使用下面的 LUT 生成的。



修改后的LUT示例,视频中的液晶就是这样生成的
3在材质中使用
现在有了 LUT。接下来就是应用它。
我们需要的是一段代码,它将你的原始未修改的纹理作为输入,然后将其用作采样LUT的UV坐标。从LUT纹理采样出来的任何内容都是你的输出,这是原始纹理接收到相同颜色调整的结果,这些调整已经被烘焙到了你的LUT中。
因此,你需要将具有RGB值的纹理转换为用于采样LUT的UV坐标。你从3维转换为2维。LUT的设置方式是,红色和蓝色通道一起构成了UV坐标的水平U维度,而绿色通道构成了垂直V维度。你可以在LUT图像本身中看到这一点。当你垂直移动时,绿色的数量会改变,但如果你水平移动,取决于你所在的切片,红色和蓝色都可以改变。
关于用于采样的纹理设置的一些快速说明。现在让我们将采样设置为最近点采样(在某些环境中称为点采样),并将包裹模式设置为裁剪clip。


下面是blender中的整个材质



上面的节点有几个部分,我挨着解释一下:
1.将LUT从其所在的任何空间转换为线性空间。这可以是sRGB,但在构建LUT时由你决定。
2.使用原始纹理的绿色通道计算UV的V通道。
3.红色通道对UV的U通道计算的贡献。
4.你需要添加的偏移量以采样正确的像素。
5.蓝色通道对UV的U通道计算的贡献。
6.在U通道上两个蓝色切片之间进行自己的双线性过滤。现在我们可以逐一解释它们。第一步很简单,所以我会跳过它。
计算UV的V通道 我们可以从计算V(垂直分量)开始,因为它非常容易计算。如果你查看LUT,你会发现V分量实际上就是绿色通道。绿色通道的值在0-1之间,V的值也在0-1之间,因此你几乎可以直接使用它。


有一些需要注意的地方。首先,自然的LUT通常在左上角有纯黑色。在左上角,虽然V值为1,但RGB的绿色通道为0。你需要垂直翻转LUT图像,或者进行以下计算。
v_of_uv = 1. - INcolor.G;
第二个问题与采样方式有关。让我们将标准LUT图像作为纹理导入到Blender中,并编写一个着色器将LUT应用于自身。我们可以在原始LUT和应用LUT之间来回插值,以便在代码中发现逻辑错误。理想情况下,在LUT应用于自身之后,我们的自然LUT图像应该保持完全相同。以下是我们实现的效果:




请注意,V维度的顶部有一条黑色条纹。这是因为这是一张非常低分辨率的纹理,对于采样,你需要采样像素的中心。因此,如果你从像素0到像素15(你有16个像素),你需要在0.5、1.5、2.5、3.5等处进行采样。由于坐标是归一化的,因此应该是0.5/16到15.5/16。现在我们正在从0到1进行采样。由于我们使用的是点采样或最近过滤器,它会将其舍入到最近的像素中心。这意味着在1处实际上是在采样16.5个像素,而这个像素不存在。所以我们得到了黑色。
解决方法是将其归一化,使1映射到最后一个像素(对于16x16来说是15),并添加半个像素,以便始终采样正确的像素。这意味着:
v_of_uv = INColor.G * ((LUT_Dimension - 1) / LUT_Dimension ) + 0.5/LUT_Dimension ;
在上面的代码之前,你的绿色通道从0到1。这将从像素0到16进行采样。经过上述转换,你的0到1代码已经重新映射到0-15/16。你添加了半个像素偏移量0.5/16,这意味着现在你从0.5到15.5进行采样。
这就是计算LUT的V通道所需做的全部。在Blender的材质系统中,它看起来像这样:



4
计算UV中的U
这部分有点复杂。这个计算有两个部分。你需要取出颜色中的红色和蓝色,然后从这两个颜色中计算出U。首先让我们从蓝色开始。
计算蓝色的贡献:LUT是一个3D立方体。事实上,你可以将LUT作为3D纹理。但是我们的布局不是这样的。我们将立方体切成了N个部分,并将它们并排放置。因此,我们需要确定哪个切片与当前的InColor相关。蓝色的数量决定了这一点。换句话说,当你从左到右穿过这些不同的RG切片时,你会发现每个切片中的蓝色数量增加。
在自然的LUT中,最左边的切片的蓝色值为0。最右边的切片的蓝色值为1。因此,我们根据InColor.B的值来确定我们在哪个切片中,并从U维度的偏移量开始。


以4x4x4的LUT为例,如果蓝色为0,我们想要采样第一个切片。如果B为0.25-0.5,我们想要采样第二个切片,0.5-0.75为第三个切片,以此类推。
但是我们有一个问题,InColor.B是一个浮点数,因此它可以有任何值,但是我们的LUT由离散的NxN组成,因此我们需要找出我们在哪个单元格中读取值。这个数学计算非常简单:
U_of_UV = Offset_from_Blue + Contribution_From_Red;
Offset_from_Blue = Floor(InColor.B * LUT_Dimension) /LUT_Dimension
我们在这里使用了一个简单技巧。对于4x4x4,例如,如果我们的数字在0到0.25之间,将InColor.B乘以4,我们将得到0到1之间的数字,将其向下取整,我们将再次得到0。这意味着0到0.25之间的所有值都将得到0的偏移量,0.25到0.5之间的值将得到1的偏移量,0.5到0.75之间的值将得到2的偏移量,0.75到1之间的值将得到3的偏移量。当然,我们希望这个归一化,所以它将是0/4、1/4、2/4等。
实现中仍然存在一个问题。在4x4x4 LUT的情况下,我们永远不希望有一个偏移量为4,因为只有4个切片。记住,我们仍然要添加红色通道的贡献,当蓝色为1时,我们不应该得到4/4的偏移量。否则这意味着当我们添加红色贡献时,我们的U将变得大于1。
实际上,当我们归一化时,我们希望蓝色值为1时,它将给我们3的索引,因为这是我们添加红色偏移量以读取正确颜色的起始点(它是第4个正方形左下角)。
为了做到这一点,我们需要做一点改变:
Offset_from_Blue = Floor(InColor.B * (LUT_Dimension - 1)) /LUT_Dimension
这是节点的实现方式:


现在我们需要计算红色通道的贡献。为了得到U轴的最终坐标,需要将蓝色通道的偏移量和红色通道的偏移量相加。
在LUT空间中,红色通道的值在0-1之间。然而,由于有N个切片并排放置,红色实际上从0-1/N。因此,它从0/N到1/N,然后从1/N到2/N,然后从2/N到3/N,以此类推,直到N-1/N到N/N(即1)。这意味着我们需要做的第一件事是将红色通道除以N。
Contribution_From_Red = InColor.R / LUT_Dimension
如果我们将这个值加到蓝色偏移量中,理论上我们应该得到我们的U坐标。然而,就像在V坐标计算中一样,这将在右侧给我们一个黑色条纹。出于同样的原因,在红色值为1时,我们将采样第16个像素(对于16x16x16 LUT),在Closest采样滤镜中为16.5,这是一个不存在的像素。因此,我们还需要将其归一化,使得红色=1时,我们得到(LUT_Dimension-1)/LUT_Dimension。我们添加了16x16正方形的半个像素,以便采样第16个像素的中心,其坐标为15.5(归一化为0-1)。
最后一个要点是LUT纹理的U方向被拉伸了。因此,U通道的半个像素值比V通道拉伸了16倍(记住X方向是16倍长,或者是LUT_Dimension倍长)。因此,为了获得半个像素的值,我们需要做0.5/LUT_Dimension*LUT_Dimension。
Contribution_From_Red = (InColor.R / LUT_Dimension ) * ((LUT_Dimension -1)/ LUT_Dimension ) + (0.5/ pow(LUT_Dimension ,2);
这是节点的实现方式:




将所有内容放在一起:
U_of_UV = Offset_from_Blue + Contribution_From_Red;
或者
U_of_UV = Floor(InColor.B * (LUT_Dimension - 1)) /LUT_Dimension + (InColor.R / LUT_Dimension ) * ((LUT_Dimension -1)/ LUT_Dimension ) + (0.5/ pow(LUT_Dimension ,2);


5
Discrimination Artifact量化误差
这是使用我们的材质应用于场景的自然LUT的外观:


原始图像

应用了LUT以后,注意图像中存在条状伪影请注意,由于假的lighting,图像有点偏黄,但更重要的是请注意墙上的奇怪伪影和条纹。这是因为标准的RGB颜色每个通道有256个变化(即8位)。将其降低到16x16x16,你将每个通道的变化减少到16个(即4位)。因此,你的位深度减少了16倍,这会导致出现条纹。
一种解决方案当然是转向32x32x32 LUT,这将使精度加倍。但是,如果你想达到原本无条纹效果,你需要使用256x256x256 LUT,其像素数量与4k纹理相同。对于我们的情况,我们使用的是8K纹理,使用这样的LUT仍然比使用另一个具有颜色变化的8K纹理更好,但它仍然无法提供良好的性能。
幸运的是,我们还有一些事情没有做。我们没有插值我们的LUT读取的数值。虽然我们的LUT仅为16x16x16,但我们的输入仍然是每个通道256。由于在每个LUT像素之间,只有线性颜色变化,因此我们可以通过使用线性插值来基本上获得相同的结果。这将花费我们4个纹理读取,而不是单个点采样,但在16x16x16纹理上,并考虑到纹理缓存,还是非常便宜的。
对于美术师来说,我上面所说的类似于放大图像。你可以在Photoshop中增加16x16图像的分辨率,并且有不同的方法可以用于决定新像素的颜色值。如果你从16x16到32x32,你将有两倍于中间像素,你需要以某种方式决定其颜色。最简单的方法是最近或点过滤。这是我们到目前为止使用的蓝色通道以及红色和绿色的方法。在这种方法中,你只需取最近像素的颜色。但是还有双线性过滤。在这种情况下,你将获取两个最近像素的颜色,并根据它们与新像素之间的距离混合它们的两种颜色以获得其颜色。


双线性过滤可以通过在最近的两个像素之间进行线性插值来猜测中间颜色,根据所需位置与两个像素的距离来确定。      
通常,在着色器中对纹理进行采样时,硬件会为你处理过滤。但是,你无法在此处使用硬件插值,因为它可能会意外地采样相邻的切片,并且不知道如何插值蓝色通道。这是由于我们的LUT布局的独特方式。我们将这些切片放在一起,例如对于蓝色,相邻像素是一个完整的切片。当然,你可以使用3D LUT,并使用插值,但是在这里,我们将我们的LUT布置在2D中。
为了使我们的LUT不会导致条纹,所有通道都需要进行双线性插值。对于红色和绿色通道,将插值设置为Cubic就足够了。你会得到一些最小的伪影,但是对于我们目前的用例来说,这是足够好的,因为它几乎不可见。伪影来自U维度中不同切片之间的边界。对于非常接近1.0的InColor.R值,它将意外地采样相邻的切片。
在某些时候,值得正确解决这个问题,无论是查看如何重新映射UV,以便硬件插值起作用,还是手动完成这项工作。现在最重要的是设置蓝色通道插值,以便我们可以消除其中的条纹。
在进行以下计算来读取LUT的U之前:
U_of_UV = Offset_from_Blue + Contribution_From_Red;
Offset_from_Blue = Floor(InColor.B * (LUT_Dimension - 1)) /LUT_Dimension
现在我们想要做的是,在我们正在读取的切片上方也读取蓝色的值,并在它们之间进行插值。想象一下你有一个4x4x4的LUT。你的蓝色值为0.125。在我们当前的系统中,我们读取蓝色为0的LUT值。我们将其分配为最终颜色。这不好,因为0到0.25之间的每个蓝色值都将从LUT中读取相同的蓝色。我们想要做的是读取0处的蓝色,然后读取0.25处的蓝色,然后通过50%的插值在两者之间进行插值。当然,这是针对InColor.B为0.125的情况,它在0和0.25之间。因此,计算如下:
读取下端的蓝色值:
Offset_from_Blue_Lower = Floor(InColor.B * (LUT_Dimension - 1)) /LUT_Dimension
读取上端的蓝色值:
Offset_from_Blue_Higher = Ceil(InColor.B * (LUT_Dimension - 1)) /LUT_Dimension
然后根据InColor.B * (LUT_Dimension - 1)距离下端有多远将它们混合在一起:
Final_Color = Lerp(LUT_Read_At_Lower, LUT_Read_At_Higher, Frac(InColor.B * (LUT_Dimension - 1));
正如在材质中看到的那样,主要更改是我们将代码的蓝色贡献计算为两个部分,一个为下部,一个为上部。


然后我们进行两个不同的纹理读取,并根据Frac(InColor.B * (LUT_Dimension - 1))进行插值。      


这就是整个材质里面的内容。


6
Improvements改进
这几乎是完美的LUT着色器。剩下的最后一件事是对红色和绿色通道进行适当的插值,而不是依赖于硬件插值。如前所述,问题区域是不同切片之间的边界。


圈出的是有问题的地方对于我的项目来说,这效果已经足够好了。伪影只在红色值在15/16到16/16之间出现。因此,它在视觉上非常微小。
需要记住的一件事是,无论你在哪个环境中使用此代码,都应确保设置你的纹理设置。例如,在虚幻引擎中,你需要注意诸如压缩、Mipmap、颜色空间等事项。由于LUT是特殊的纹理,因此不能使用默认设置。在虚幻引擎中,你可以使用用于UI的压缩设置,这将应用正确的压缩设置。以下是我们在虚幻引擎中使用的设置。




  
7
About Lights关于灯光的处理
你可能已经注意到,在夜间版本中有一堆灯光。这是使用黑白mask制作的,该mask在原始纹理上应用不同的颜色转换LUT。此转换使用与生成夜间感觉相似的工作流程。也就是说,它基于我们对光如何影响物体的概念模型。平均亮度略高,更黄,更微小的对比度,更多的高光。
你可以通过在纹理上烘焙场景灯光来生成mask地图本身。或者你也可以手动绘制它。你可以在Photoshop中生成光如何影响场景颜色的颜色转换,并将其烘焙到LUT中,或者如果性能允许,则直接在着色器中完成。最后,我只是在着色器中完成了它,可参考上述Github
         

- End -



评分

参与人数 1元素币 +1 活跃度 +1 展开 理由
Shino + 1 + 1 楼主靠谱

查看全部评分

还没有设置签名!您可以在此展示你的链接,或者个人主页!
使用道具 <
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表