cocos进入3.x时代,我也努力跟进,实现了一个将页面映射到立方体上进行旋转查看的效果。
效果如下:
要点
为了这个效果,我主要实现了3个要点:
- 将页面准确映射到立方体上面,适配不同尺寸的手机屏幕。
- 对页面内容进行正常滑动、点击操作。
- 不同页面数量能够在立方体上滑动屏幕循环查看。
页面映射到立方体
创建立方体和页面
因为需要在4个面上显示不同的页面内容,引擎自带的立方体并不能简单地满足这个功能。这里我使用了4个quad拼凑成一个四面体,放在一个节点下,方便后面进行整体旋转。
四个quad节点的坐标分别是:前(0,0,0.5),右(0.5,0,0),后(0,0,-0.5),左(-0.5,0,0)
(图一:立方体拼凑示意)
页面就是普通的渲染节点。不用scrollview做页面滚动,之后统一用屏幕操作来处理。
页面没有铺满屏幕,在上方空出了导航栏的空间。
(图二:页面布局示意)
canvas的宽度尺寸是750,写有内容的页面的宽度是600(后面用pageWidth表示),居中。
屏幕宽适配,页面距离canvas顶部始终是250(后面用top表示) ,距离底部始终是60(后面用bottom表示) 。
页面节点加了mask组件所以只显示了一部分内容。
透视摄像机参数调整
做3d项目,首当其冲的是透视摄像机的引入。需要先了解它的各项参数的意思,才好完成一些3d功能。
我想实现的是:调整主摄像机的参数,可以让立方体的前quad模型在页面上呈现和“图二”一样的尺寸效果。
主要是调整主摄像机的位置坐标和fov值。(我想保持立方体不动)
x坐标影响quad左右显示,因为页面左右对称,所以x坐标为0 。
y坐标影响quad上下显示,值越大,页面越靠下。
y的值可以直接由:(top-bottom)/(2*pageWidth)得到。这里是:(250-60)/(2*600)=0.1583333
z坐标影响quad两边的空间,值越大,离quad越远,页面显示越小。
摄像机距离立方体距离和fov的大概关系如下:
(因为用了宽适配,不想fov影响宽度,所以fov axis使用了horizontal)
(图三:透视摄像机距离和fov关系,俯视图)
红色实线代表了前quad模型。宽度是1
绿色实线是近裁剪面。
BC比上AD的大小是:600/750 . 这里用ratio表示
AD尺寸:2*near*tan(fov/2)
可以算出DisZ和Fov的关系是:
DisZ=1/(2*tan(Fov/2)*ratio)
当Fov取60度时,DisZ是:1.0825 。那么主摄像机距离原点就是:1.5825(前quad距离立方体节点0.5)
调整z坐标后的效果如下。
(图四:调整主摄像机z坐标后的效果)
可以看到调整主摄像机的z值后,透视摄像机的前quad模型和正交摄像机的页面宽度保持了一致。
因为设备屏幕宽高比(aspect)不一致,所以quad的高度需要在启动后调节。
适配需求:页面距离canvas顶部始终是250 ,距离底部始终是60 。
quad高度和透视摄像机的关系如下图:
(图五:透视摄像机和立方体前quad的关系,左视图)
其中绿色实线为近裁剪面,红色实线为前quad模型
黄色虚线是xz平面,经过立方体中心。
蓝色虚线是摄像机xz平面,经过近裁剪面中心。
近裁剪面的宽度除以高度等于aspect,这个可以通过屏幕尺寸得到。
在cocos里可以通过camera.aspect得到
通过之前的公式,计算得到红色实线即前quad的高度为:
1/(ratio*aspect)-(top+bottom)/pageWidth
如果基于iphoneSE的屏幕,该值为:1.706666
调整好x,y,z和fov值后的效果:
(图六:调整完摄像机参数后的效果)
可以看到,2d页面和quad已经完全重合了!✌
立方体旋转
我直接监听了input的touchmove事件,然后根据delta来旋转立方体。
旋转的时候,使用到了四元数,四元数又是一个新词,不好理解,
但是cocos做了很好的封装,使用起来很方便。
旋转api用到了Quat.fromAxisAngle函数,代表了基于旋转轴和旋转角度的旋转。
旋转轴很容易确定,因为只是水平方向的旋转,那么就是绕着y轴旋转。即Vec3.UP
旋转角度做了一个限制,就是希望玩家即使从屏幕一侧滑到另一侧,也不会看到背面。
所以,最大旋转角度为90度。每次滑动屏幕距离是deltaX,那么每次的旋转角度是:90*deltaX/屏幕宽度
核心旋转代码是:node.rotate(quat)
设置后效果如下:
(图七:立方体旋转示意)
动图中,文字页面还没有映射到立方体上,所以为各个面加了基础颜色。
图七中的效果,仅是旋转跟随滑动。当松手后,应该需要像pageview那样,根据一个阈值,要么还原,要么旋转到下一个页面。
我使用了tween的方式,在松手后,直接旋转到原角度或者下一个页面的角度。
为了方便,在旋转的时候加了锁,只有tween完成后,才可以进行下一次滑动旋转。
这里还根据需要旋转的角度,算了一下时间,最长0.5秒,最短0.1秒。做一个线性插值。
最后的旋转效果如下:
(图八:立方体旋转示意,带自动旋转)
页面映射
我使用了rendertexture渲染贴图,为立方体的前,右,后,左各面片分别创建了对应的渲染贴图、材质、摄像机。材质绑定对应的渲染贴图。将材质赋值给各quad面片。为每个面设置一个layer。然后让对应的摄像机只拍摄这个layer。将渲染贴图赋值给对应的摄像机。
总之创建了:
4个渲染贴图,4个材质,4个摄像机。
都配置好后的效果如下:
(图九:配置渲染贴图后的效果)
现在会有2个问题:不同屏幕尺寸,内容会变形;内容没有铺满。
只要将各面的摄像机尺寸和主正交摄像机的orthoHeight设置成一样,
将各面的渲染贴图的尺寸设置成(aspect*orthoHeight,orthoHeight)
就能保证屏幕完整映射到立方体各面上。如下:
(图十:页面完整映射到立方体各面)
现在内容还是处于拉伸或者压缩的状态,因为页面的宽高比和屏幕的不一致。
只需要将渲染贴图的材质的tiling offset参数进行调整,
去掉黑色区域就可以还原内容的尺寸比例。
tilingOffset的x,y对应uv的xy缩放,tilingOffset的zw对应uv的xy偏移。
宽度是600/750=0.8;
所以tilingOffset的x设置为0.8 .
x方向应该采样0.1~0.9这个范围。所以将tilingOffset的z设置为0.1。
tilingOffset的y需要用公式:y=(screenHeight-top-bottom)/screenHeight来运算。
w需要用公式:z=top/screenHeight来运算。
最后用
material.setProperty('tilingOffset',new Vec4(0.8,(screenHeight-top-bottom)/screenHeight,0.1,top/screenHeight))
来设置最终的渲染效果,如下动图所示:
(图十一:对材质进行tilingoffset后的效果)
然后再加上一个背景图
调整一下各个摄像机的参数
主透视摄像机用depth only,priority 设置成最高
页面的正交摄像机用solid color,clear color用半透明白色, priority 设置成中等
主正交摄像机priority设置成最低,用于背景渲染。
将各页面材质的technique设置成1-transparent。
就实现了相对高级一点的效果:
(图十二:添加背景和透明度的效果)
对立方体各面内的页面进行长图滑动和点击
我的做法概要:
在滑动的第一帧,判断是想上下滑,还是想左右滑。
如果判断deltaY大于deltaX,则上下滑动,对当前页面进行content坐标处理。
如果无移动,则对当前页面进行点击处理。
页面点击是利用世界坐标进行的判断。
最后的效果如下:
(图十三:添加滑动和点击的效果)
因为不是3d内容范畴,所以写的比较简单。嘿嘿。
对不同数量的页面进行映射
我用过切换页面的layer,但是这样只能适用至少3个页面的情况。如果只有1、2个就不行了。
所以最后使用了切换摄像机的visibility来实现。
每次旋转结束,判断前,右,后,左四个页面,应该是哪个一。
然后设置摄像机的visibility来对应。
并且因为只有4个面片,所以layer也只需要用到1,2,4,8即可。
去掉一个页面后的效果如下(也是开头的效果)
(图十四:只有3个页面的效果)