H5打造3d场景不完全攻略(二): Amazing CSS3D

353 查看

前言

对的,本文就是着重介绍如何使用CSS3中的3D变换打造出H5中的3D效果。灵感来源于造物节团队的3d引擎,因为使用方法比较复杂,也没有开源的API文档,于是想自己另外造个轮子,便开始了相关内容的学习和实践。
众所周知,目前市面上的H5 3D类库(如Three)、引擎(Egret)、构建工具(kpano、720云)都或存在体积太大、不开源、非免费、学习成本高等问题。对于我们较为熟悉的CSS3,为什么就不对它好好利用一把呢?诚然,CSS3存在我们比较清楚的短板,CSS对平面的渲染能力高,但是对3D建模方面便力不从心了。

我们知道3D的表现形式即让我们通过平面可从不同角度看到真实物体的展示效果。

在计算机世界里,3D世界是由点组成,两个点能够组成一条直线,三个不在一条直线上的点就能够组成一个三角形面,无数三角形面就能够组成各种形状的物体,如下图。

我们通常把这种网格模型叫做Mesh模型。给物体贴上皮肤,或者专业点就叫做纹理,那么这个物体就活灵活现了。最后无数的物体就组成了我们的3D世界。

Three中模型解析器的原理是将顶点数组将模型的顶点用数组储存起来,再利用three中的face函数取得定点数组中的三个或四个顶点的索引构成空间平面。如此反复,模型就被完整构造出来了。

于是,越复杂的物体就需要越多的网面拼接。而css中是不存在根据坐标建立空间平面的能力的。

(插个题外话,其实css有一个属性与坐标有关,那就是clip-path。这个属性的特性赋予了css3一定的建模能力。实现方法可参考这篇文章 纯clip-path打造的3D模型渲染器

CSS3实现3D全景

。上篇文章介绍了Web3D的一些表现形式,这里着重谈谈怎么以CSS3实现3D全景。下面会探索Three实现全景的方案,因为WebGL门槛和学习成本还是比较高的,不适于用于快速开发。造物节的CSS3d全景已有文章对其进行了技术探秘,但都未深入谈及具体实现方式。

要清晰理解实现方式,必须对CSS3的transform、perspective有一定的认识。
原理方面的东西我就不深入讲了,大家可以先看看这篇文章,对CSS3D有一个大致的概念。
玩轉 CSS 3D - 原理篇

CSS全景可通过建立柱形或者立方体再通过贴图方式实现。也许会有人问,球体行不行?实际上是不行的,球体模型由无数个极小的平面拼接构成连贯曲面,而CSS缺乏使平面扭曲的属性。球体模型我们可以使用上文提过的Clip-3d建造出,但是,贴图问题就解决不了了。

天空盒子

相信很多打造过或有了解过3d全景的同行们都知道这个概念。实际上Skybox就是一个立方体,通过给六个面贴上不同的,边缘可以无缝贴合的图片,再将视角伸入盒子内部。可以想象成我们自己站入了一个巨型立方体盒子内部,移动视角便能看到不同的场景。

1、贴图
来看一张天空盒子的贴图,剪头指向的边缘代表需要无缝贴合的边。

从上图可以看出只要相互贴合的两个面上的图像能够无缝拼接,那么再通过对各个面进行一定的旋转变换,天空盒子就能被打造出来了。

那么问题来了,怎么去拍摄制作这样的图片呢?这就需要通过一些专业软件了,比如pano2vr,max等。其实,需要用到这些专业工具打造的全景对画质和拼合度的要求都非常高了,而单纯依靠CSS3中的变化给不了它们很好的体验。

但我们今天讨论的是某些运营活动H5打造的全景,此全景不一定真实存在,或者是和真实场景有一定的比例差距。例如星空、海底。对于这类贴合度可人为改变的全景图的打造,我们可以采用现有的高清图片,再经由PS转换成六面全景图。
贴一篇文章 Create a Skybox From Photos
其实主要思想是
在一张大图上勾画出六个面的选取 >
选择大图中某个面的相邻面将其旋转到需要拼合的盒子的某个面上,使他们完美贴合 >
得到最合理的六面贴图后,观察有无创造出新的边缘,通过蒙版等工具使他们自然融合。

2、构造
贴图完成就可以创建立方体了。首先将创建好的六个面切割出来,以front、back、left、right…命名标记位置。

 .sence {
      -webkit-perspective: 1000px;
    }
    .cube {
      width: 500px;
      height: 500px;
      margin: 100px auto;
      transform-style: preserve-3d;
    }
    .cube img {
      width: 130px;
      height: 130px;
      position: absolute;
    }
    .cube img:nth-child(1) {

    }
    .cube img:nth-child(2) {
      transform:  rotateY(180deg);
    }
    .cube img:nth-child(3) {
      transform:  rotateY(90deg);
    }
    .cube img:nth-child(4) {
      transform:  rotateY(-90deg);
    }
    .cube img:nth-child(5) {
      transform:  rotateX(90deg);
    }
    .cube img:nth-child(6) {
      transform:  rotateX(-90deg);
    }
<div class="sence">
    <div class="cube">
      <img src="img/skybox/front.jpg" alt="" />
      <img src="img/skybox/back.jpg" alt="" />
      <img src="img/skybox/left.jpg" alt="" />
      <img src="img/skybox/right.jpg" alt="" />
      <img src="img/skybox/top.jpg" alt="" />
      <img src="img/skybox/bottom.jpg" alt="" />
    </div>
  </div>

准备好6个面,载入贴图。通过旋转,使得每个面旋转到相印的位置。如左边的面由原本面朝我们的图片绕Y轴逆时针旋转90°得到。(注意Y轴逆时针旋转是正数)

此时会得到下图这样的效果:

但是由于每个面的旋转中心都在其正中位置,因此还不能形成正方体。于是我们需要让每个面产生一定的位移。

贴一张坐标系图以助于大家理解。

现在首先让front位移到应该到的位置,由于全景图的镜头在立方体内部,因此,可以想象一下,我们需要将图片往后移动。移动距离很明显为立方体边长的一半。在这里是65px。得到下图结果。

.cube img:nth-child(1) {
      transform: translateZ(-65px);
    }

照这样看,是不是back位移为translateZ(65px),left为translateX(-65px),top translateY(-65px)呢?但结果并不是我们想要的。

重新看回上文空间坐标系的那张贴图,我们会发现,平面旋转后,其对应的三个轴的位置也改变了。如图片绕Y旋转后,Z轴指向为屏幕的水平方向。绕X旋转后,Z轴指向垂直方向。因此我们很容易发现,其实要将贴面移动到正确的位置,都只需要让他们translateZ(-width/2px)就可以了。

为了让大家容易理解,我这里设置了一个较大的perspective。要想得到全景的效果,我们将镜头拉近让它进入到box里面就可以了。

接下来绑定手势,就可以让它动起来啦。

部分代码:

viewer.on('touchstart', function(e) {
    x1 = e.targetTouches[0].pageX; - $(this).offset().left;
    y1 = e.targetTouches[0].pageY; - $(this).offset().top;
});

viewer.on('touchmove',function(){
    var dist_x = x2 - x1,
        dist_y = y2 - y1,
        deg_x = Math.atan2(dist_y, perspective) / Math.PI * 180,
        deg_y = -Math.atan2(dist_x, perspective) / Math.PI * 180,
        i,
        c_x_deg += deg_x;
        c_y_deg += deg_y;
        
    cube.css('transform', 'rotateX(' + deg_x + 'deg) rotateY(' + deg_y + 'deg)');
})

Math.atan2(y,x) 方法:得到从 x 轴到点 (x,y) 之间的角度。对于空间左边系比较难理解,大家可以想象成一张以空间Z轴为Y轴的平面绕X轴正方向旋转的角度即为cube绕空间Y轴旋转的角度。

柱形

柱形全景也不算复杂。关于圆柱形的打造方法,大家可以参考下这篇文章CSS3 3D transforms系列教程-3D旋转木马
有了这个基础,我们可以写一段函数快速构造柱形全景。

先来看下页面结构

<style>
  body {
    height: 100%;
    overflow: hidden;
  }
    .scene {
      width: 100%;
      height: 1170px;
      transform: translateX(-50%) translateY(-50%);
      top: 50%;
      left: 50%;
      position: absolute;
    }
    .cube {
      transform-style: preserve-3d;
      height: 100%;
      width: 100%;
      margin: 0px auto;
    }
    .cube_bg {
      transform-style: preserve-3d;
      height: 100%;
      width: 128px;
      margin: 0px auto;
    }
    .cube_bg div {
      height: 100%;
      
      /* 这里为圆柱形的每个面都设定了同样的背景图 那么在建造柱形时不再需要手动切图 */
      background-image: url("img/zao/zao.png");
      
      background-repeat: no-repeat;
      position: absolute;
      top: 0;
    }
</style>

<body>
  <div class="scene">
    <div class="cube">
      <div class="cube_bg">
        <!--
        这里是柱形全景背景贴图
        -->
      </div>
      <div class="cube_item">
        <!--
        这里是柱形全景中的小元件
        -->
      </div>
    </div>
  </div>
</body>
function creCylinder(lenZ,pieceWid,angle,slice){

    /* 
    pieceWid 表示单个柱形块状宽度
    angle表示柱形内角
    slice表示有多少个面拼接 
    slice越多,拼合的面越接近曲面
    */
    
  var l = pieceWid*slice; // 画布全长
  var ag = angle/slice // 旋转角度

  var html = '';

  /*
    设置每个面的旋转角度和位移 因为要分割成多个面,所以应该为每个面的背景图设置不同的`background-position`
    */

  for(var i=0,len=slice;i<len;i++){
    html+='<div style="transform: rotateY(-'+ag*i+'deg) '+
          'translateZ('+lenZ+'px);'+
          'width:'+(pieceWid)+'px;'+
          'background-position: -'+(i*pieceWid)+'px 0;'+
          'background-size: '+(l)+'px 100%;"></div>';
  }

    return html;
}

function renderPano(pieceWid,angle,slice){

    var vw = $(window).width();

    var RADIAN = 0.017453293; // 弧度制 将角度转成弧度

    var innerAngle = angle/(2*slice); //内角,用来计算translateZ

    // 这里的原理和上文旋转木马链接一致
    var lenZ = -(pieceWid/2)*Math.tan((90-innerAngle)*RADIAN);

    /*  
        因为默认是由画布的最左端开始旋转 所以处于我们面前的是画布的最左端和最右端及其连接处
        要想画布中央显示再我们面前,这里需要给cube_bg加上一定的绕Y旋转角度
    */
    var rotate = ((angle/slice)*(slice-1))/2,
        perspective = -lenZ-5;

    var cube_bg = $('.cube_bg'),
        scene = $('.scene');

    var cylinder = creCylinder(lenZ,pieceWid,angle,slice);

    cube_bg.html(cylinder).css('transform','rotateY('+rotate+'deg)');
    scence.css('-webkit-perspective',perspective+'px');
    
    //最后调用一下
    renderPano(128,360,20);

这里解释一下perspective为什么要设成 -lenZ-5
看一张图,上面的lenZtranslateZ值,为负值。
perspective为镜头到屏幕的距离,因为此时镜头在柱体内部,因此不能看到柱体后面的图像。
当perspective值为-lenZ值时,正好柱体back面能与镜头在同一平面上,为了避免它有一定的机率遮挡镜头,我们可以将镜头拉近一些。便设成了-lenZ-5。这个时候就能保证镜头处于柱体内部,同时也能更广角度地观察到柱体全景。

大家可以复制代码体验一下。这里的背景图我选用的是自己拼合成的造物节背景图。

优劣势对比

相信大家也有体会,天空盒制造起来会相对的简单,并且天空和地面都能被考虑进去。但是由于面面间的贴合角度太大,若物体正好处于相互贴合的两个面,会给人一种被拦腰折断的感觉。而柱形图对这种情况有了比较好的解决,但是天空和地面的贴图就比较困难了,一般情况下只能通过给scene添加背景图片模拟。

未完待续…