原生js+WebGL实现3D图片效果
海外党玩F***book的时候可能有接触过这个酷炫的3d图片效果:
只要通过客户端的这个入口——
或者网页版的这个入口——
就能生成。不知道咋玩的请参考官方的帮助手册
。今天就教大家手撸出一个这样的功能,不要担心,所有代码加起来不超过200行并且不使用任何第三方库。虽然canvas2D也能做出这个效果,但是基于这种像素级操作的性能考虑,WebGL显然是更好的方案,我前面的有些教程也用到了WebGL,核心的API我就不做过多介绍,直接详细地注释在最终的代码里面了,代码仍然使用WebGL
1.0版本。
老规矩,还是先介绍原理,推荐有想法的读者略过教程,自己直接根据原理去撸出来,因为我还是秉持着话痨的特色,想到什么说什么,教程中掺杂一些自己的干货,对一些人来说可能过于啰嗦。夹,哈吉咩马修!(工地日语
_非死不可_客户端在上传图片的时候你有两种可选操作:
一种是上传带深度通道的图片,即图片的每个像素是RGB-D格式,如果你是苹果手机可能在相机里会有人像模式或景深模式,拍出来的照片在本地是heic格式的文件,一般这种就是带深度信息的(有兴趣的可以去维基了解下这种heif编码的图片,可以做到很多神奇的事)。通常有TOF镜头的手机都能拍出这种图片,但是不知道为啥F***book似乎只支持三星系列和自己发布的安卓机?
另一种办法就是上传两张图,一张普通的RGB像素的原图,一张灰度图,只要灰度图的文件名和原图一样,加上_depth的后缀即可。比如666.jpg和666_depth.jpg。这也是F***book网页版唯一支持的方式。这个灰度图的门道可就多了,也是我们后面代码实现的核心。开发过游戏的一定知道深度贴图,或者阴影贴图/光照贴图,其实都是类似的玩意,这种贴图存储了原图每个像素的深度信息,贴图的每个像素的R值就是原图的z轴偏移,因为一般深度贴图的R、G、B通道的值相同,所以表现出来的就是一张灰度图。
如何获取深度贴图呢?如果你有heic格式的带深度信息的照片,可以用PS抽取出z通道的信息(windows上的PS不支持),如果你啥都没有,我会在下个教程尝试“教”你一下如何在PS中绘制出深度贴图,或者使用谷歌提供的一个人工智能程序来生成,我也会写入下个教程,亲测匹配程度还是挺高的~
具体是怎么产生3D效果的呢?深度贴图中,颜色越浅(值越小)表示深度约低,通过深度贴图的深度值来对原图的采样位置进行偏移,比如当你把贴图往左偏移,然后使用偏移的距离乘上原图的某个坐标在贴图上的深度值得到的结果来对原图进行采样,就会得到不同的点在不同的深度偏移的大小不同的情况,距离越近的偏移越小,距离越远的偏移越大,是不是很符合我们生活中的常识?事实上,抛弃主观感知,从底层角度考虑,最终展现出来的效果其实就是一部分的像素点被压缩了,一部分的像素点被拉伸了。不知道大家有没有用过live2D或者Spine、龙骨等工具做出来的动画,就是这种:
刚刚所说的底层变化是不是和这种网格动画很像,其实都是对图片的变形来达到3D效果,就单张图的变化而言,他们的唯一区别就是蒙皮动画是手动key帧(或者是骨骼绑定——这个以后有机会谈谈),而3D图片是通过深度贴图自动生成。
废话终于说完,下面开始编码,先设置一下基础样式:
1 | * { |
然后引入glMatrix函数库用于操作矩阵(虽然,之前说好的不依赖第三方库,不过坐标换算确实挺烦免得程序太长还有写一堆注释
其实换算也不难,看过上一篇教程的应该自己实现问题也不大~~原谅我标题党 ಠᴗಠ)
1 | <script src="./gl-matrix-min.js"></script> |
我已经下载好了,想要消息了解这个函数库的可以去glMatrix官网,这个库非常小,未压缩前也就100多K。
顶点着色器(shader_vertex.vert)的代码:
1 | attribute vec2 a_pos; |
片元着色器的代码:
1 | precision highp float; |
直接贴上绘制静态图的代码:
1 | init() |
都是一些常规的操作,具体api的作用写入注释里就不做多解释了。
接下来把我们的深度贴图传入着色器,主要是这几个步骤:
①获取加载完成的图片对象:
1 | const depthImage = new Image() |
因为如果浏览器如果已经缓存了图片不一定会触发onload事件。所以我们先通过complete属性来判断图片的加载状态是否为已完成。
②修改片元着色器代码,通过深度贴图对原图来进行采样:
1 | precision highp float; |
获取贴图的R通道的值作为深度值
③通过另一个纹理单元(如1号纹理单元)将贴图传入片元着色器:
1 | // 同理,创建深度贴图的纹理 |
这时候看到最后效果没有任何变化,因为我们还没有对贴图进行偏移,u_offset默认值是vec(0.0,0.0)。
接下来可以给页面绑定mousemove事件,我这里限定了u,v最大的偏移量为0.05,把渲染循环函数放到事件回调中:
1 | const uOffset = gl.getUniformLocation(prg, 'u_offset') |
ok,大功告成,预览一下效果图——