手把手逆向Playcanvas天空盒编码(一次乌龙的任务)

文章背景:这是我刚入职公司时接到任务,对playcanvas不是很熟悉,公司前人以为playcanvas上传hdr生成天空盒dds文件,必须上playcanvas官方编辑器手动上传下载才可以,所以希望我用python实现这个效果。(后面接手playcanvas引擎后,发现其实在github issue里面就有这个算法解释,而且引擎源码里面就有对应的js代码),虽然是个乌龙,但我觉得当时的探索过程可以分享出来。

一、playcanvas天空盒的原始流程

步骤一:利用3D软件,将一张全景图(hdr),切割成6张hdr图片

步骤二:登录playcanvas官网,使用它提供的editor,创建一个场景,上传6张hdr,生成6张png图片(用于天空盒四周的纹理贴图)

步骤三:在editor上添加cubemap,贴上6张图的位置,下载生成后的dds(用于天空盒影响下模型反射)

步骤四:在playcanvas engine上,添加步骤二的6张png图 和 步骤三dds文件 的 代码到数据库,使用该天空盒。

二、自己实现天空盒的意义

从上面原始流程上看,要使用playcanvas的天空盒,前期必须在playcanvas生成png和dds的操作,再由开发人员添加代码,才可生成天空盒,对用户并不友好。

而我们的目标:在我们的服务上,用户只要上传一张全景图,后台自动处理后,直接生成天空盒

所以现在需要解决的问题点:

1、如何将一张全景图切割成6张立方体贴图

2、如何对这6张贴图进行编码(因为playcanvas engine在渲染时是有进行解码的,至于playcanvas为什么这样操作, 后面会详细说明)

3、如何利用6张贴图生成dds文件

三、将一张全景图切割成6张立方体贴图

按原来的探索流程,可以使用sphere2cube(https://github.com/Xyene/sphere2cube),它是基于blender处理的,所以在不同平台,不同版本的blender切出来的贴图是有一定的差异性的。

原来探索流程是直接使用sphere2cube将hdr直接切割成6张png,所以会导致hdr存在的亮度丢失(这也就是playcanvas会有编码和解码的原因),导致生成天空盒的光源随着亮度变暗并不会 缩小成一点。

所以这里python的imageio实现全景图切割,保留了hdr的亮度信息,对应文章:https://blog.csdn.net/u014494705/article/details/107413908

四、实现playcanvas贴图的encode算法

这里讲下playcanvas处理纹理贴图的原理(当时误以为官方并没解释,按前端给的shader和效果推测,其实这个算法是RGBM,有兴趣的可以Google下,主要是通过编码将亮度信息编码到a通道)

1、HDR格式

HDR,即高动态范围图像(High-Dynamic Range,简称HDR),相比普通的图像,可以提供更多的动态范围和图像细节。

HDR在代码中的表现形式:

        跟PNG类似,存在RGB三个通道,但PNG是8位,取值范围只能是(0-1 或 0-255)

        而在HDR中,正常的颜色值同PNG表现出来,但HDR使用32位,允许RGB的值高于1(或255),在肉眼中,表现为越来越亮。下面用个例子说明:

这是一个点光的场景,假设用RGB中的最大值来表示亮度,

如果用png存储,我们只能知道左下方一片都是“亮”的状态,亮度都是1,而不知道那里“更亮”,那里“最亮”

但hdr不同,由于他允许RGB大于1来表示,可以存储更多的亮度层次,我们可以看看把亮度划分区间(>1,>2,>10,>100)放入R值看看效果(左边是png,右边是hdr)

2、利用PNG的Aplha通道存储光照信息

playcanvas天空盒支持调节亮度,对于含有点光的场景,我们可以实现下图效果

在playcanvas引擎的源码里,实现调节亮度只是简简单单的 "rgb * intensity"(对hdr的rgb同时乘以intensity也可以达到这个效果哦),rgb同时变大时图片会变亮,超过1的部分全部显示为白色。

但对于原图,我们显然无法实现调节,因为rgb已经存储了颜色信息,而不能存储更多的光照信息(gl只支持解析rgb<=1)。

所以playcanvas这里使用了第四个通道,alpha通道,来存储亮度信息。那么我们调节函数可从"rgb * intensity" 变为 “"a * rgb * intensity",

那么a值越大的地方,乘以调节参数,rgb就越容易超过1,就越快变白,说明越亮

 

3、实现RGB通道的encode算法

通过playcanvas引擎的渲染源码,我们可以发现,里面存在的encode算法,这里用伪代码:

decode.rgb =  (encode.rgb * 8 * encode.a) *  (encode.rgb * 8 * encode.a) 

符合上面提到的利用a来存储亮度的想法,虽然多乘了8,然后进行平方

于是,我们encode算法可以写为

encode.rgb = sqrt(decode.rgb) / (8*encode.a)

而引擎渲染(decode)出来的图效果应该跟我们的原图一样,所以

encode.rgb = sqrt(src.rgb) / (8*encode.a)

但只根据decode算法,我们还无法逆向得出a通道的encode的算法,这时候,我们只能通过统计法来解决这个问题了。

4、利用统计拟合Alpha通道的encode函数

由于引擎的decode算法直接将alpha通道赋值为1,展示出来,所以playcanvas如何将hdr的亮度信息存储到alpha通道,是个黑盒子。

此时,我们需要利用统计的方式,看是否能拟合出alpha和原图的rgb有什么关系。

首先,通过playcanvas的平台,上传一张hdr,获得一张encode后的Png,画出png的a值 和 原图hdr的rgb的最大值的关系(不要问我为啥知道是取最大值,因为前面用了很多无法描述的手段)

从点的分布,貌似符合开方函数,所以我们取 a 值 和 sqrt(rgb)的函数分布试试

没错,我们可以得到一条完美的直线关系,即 encode_a = math.sqrt(max(src_r,src_g,src_b))

5、解决python的png临界像素问题

在得出上面关系后,用png做原图可以生成正常playcanvas贴图(无法调节亮度而已)。但用hdr生成的贴图,在一些含有光源处,正常图片会异常变黑,变成其他颜色。

在经过一系列实验之后,可得出下列结论:

python在存储png时,当RGB超过临界值,该值会显示成0,即(1.1,0.5,0.5)会显示成(0,0.5,0.5)。而在opengl,会处理为(1,0.5,0.5)

python在存储png时,当A超过临界值,整张图会被破坏,异常变黑

6、hdr和png转换的精度问题

在测试过程中,发现天空盒贴图若存在连续像素区域(类似天空),渲染出来后会有条纹感,如下图

这是因为hdr是32位精度,其rgb值也是32位精度,但存成png是8位精度

按我们第3步公式可以知道

encode.rgb (32位)=math. sqrt(src.rgb)(32位) / (8*encode.a)(32位)

而在渲染的时候,我们只能拿到encode.rgb (8位), (8*encode.a)(8位),都丢失了精度,反向算出来的math. sqrt(decode.rgb)可能会有一些临近像素点被合成 “块” 。

 

所以,在encode的,我们要先把src.rgb 和 encode.a变成 8位精度,算出8位encode.rgb ,后面逆回来的src.rgb就完全一致了

encode.rgb (8位)=math. sqrt(src.rgb)(8位) / (8*encode.a)(8位)

此时encode出来的中间图片有条纹感,decode后的图片就没条纹感了。至于为什么大家再理一理呗

五、使用cubemap合成dds

1、工具选择

playcanvas在模型反射时,并不是用周围的贴图进行映射,而是需要将6张贴图合成一个dds,利用dds来进行做贴图进行渲染

而在https://blog.playcanvas.com/the-making-of-seemore-webgl/ 这篇文章有提到,playcanvas使用的amd-cubemapgen对贴图进行合成

2、dds格式解析

dds是一种图片格式,是DirectDraw Surface的缩写,它是DirectX纹理压缩的产物。目前大部分3D游戏引擎都是选择用它进行渲染。

dds可以通过window下的photoshop安装插件进行查看,废话不多说,直接感受playcanvas合成的dds长什么样

playcanvas天空盒的dds是由6张128x128贴图合成,每张128x128都有对应的5张mipmap(分别是64x64,32x32,16x16.....)

3、模糊问题解决

在之前探索流程中,我们发现就算6张贴图是正确的,高清的,使用cubemapgen时,反射渲染出来会模糊。测试后发现修改了filter的方式即可

./ModifiedCubeMapGen.exe
	-importFaceXPos:dds_right.png
	-importFaceXNeg:dds_left.png
	-importFaceYPos:dds_top.png
	-importFaceYNeg:dds_bottom.png
	-importFaceZPos:dds_front.png
	-importFaceZNeg:dds_back.png


	-exportCubeDDS
	-exportFilename:disc.dds
	-exportMipChain
	# {Disc|Cone|Cosine|AngularGaussian|CosinePower}
	# 默认使用CosinePower,会变模糊,也可以通过ExcludeBase不渲染第一张128x128的问题
	-filterTech:Cosine


	-exit

4、合成的dds具有锯齿感

废话不多说,问题如图,右图为Playcanvas效果

原因:我们先对1024的hdr进行编码成png,在压缩成128png,用于合成dds,由于先编码再模糊压缩,再解码后,压缩造成的部分图像信息会形成锯齿感

所以先把1024hdr进行压缩128hdr,再编码成128png,那么合成dds解码后效果就跟128hdr一样,不会有锯齿感

不过由于上面提到sphere2cube切割的1024hdr会有模糊感,所以还需要同第三点一同解决

5、还存在的问题

生成的mip模糊含有方块感,需要再对其mip相关参数测试

(后续发现参数如何调试都无效,最后以找到playcanvas官方实现方案解决)

六、numpy加速

由于sphere2cube在切割hdr时,会造成图片一定程度的模糊,所以切割出来的6张hdr得尽量大,这里取1024x1024,在python中使用遍历处理像素,一张贴图需要16s左右,6张贴图用了将近2分钟的时间

而使用numpy的矩阵加速后,1张贴图均在几百毫秒,这里记录一下用到矩阵实现的奇淫怪巧

1、除数为0时,希望把结果设置为0:先取倒数,把inf作为0乘进去

inverse_array = 1 / alpha_array
inverse_array[np.isinf(inverse_array)] = 0
image_array = image_array * inverse_array

2、当a通道大于1时,rgba值全部需要设置为1:把(1024,1024,4)按第3维拆分4个(1024,1024,1),使用内置索引按a=1条件赋值,后在第三维合并

r,g,b,alpha_array = np.split(image_array,[1,2,3],axis=2)
r[alpha_array>1] = 1
g[alpha_array>1] = 1
b[alpha_array>1] = 1
alpha_array[alpha_array > 1] = 1

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值