纯CSS制作3D骰子
2015-03-21

前言

去年年末农闲,无聊中翻开一本古书(好吧,是双击打开一个PDF)。这本古书是AD&D(高级版龙与地下城)第二版的游戏手册。书中详述了创建游戏人物的具体规则。

创建游戏人物,最漫长的一步就是掷骰子决定人物的各项能力值,记忆一下子回到N年前,我偷偷在电脑上玩《博德之门II》,新建人物的时候,每次都要掷好多次骰子,不过游戏里并没有骰子的画面,只是告诉你一个数字。一般我都要掷它几百次,直到出现一个满意的数字,才保存下人物档案,进行下一步。

以人类和精灵为例,每一项能力值在3~18不等,当时光顾玩了没多想。如今终于从书中知道了答案:在真实的龙与地下城桌游里,是掷三个骰子,每个1~6点,三个加一起就是3~18。原来如此!于是我产生了一个冲动,想用3D动画的形式表现掷骰子的过程,应该会比较有趣。

作为一介码农,我忍不住要露一手,可是问题来了,之前都在二次元的世界里描描画画,从未涉足过三维领域,底气有些不足。不会就学呗!使出码农的惯用招数,谷歌百度一顿搜,得到的资料七零八碎含含混混,中文资料更是鲜有写得好的。然后我就想,正好以绘制3D筛子为例,把相关的知识全揪出来,逐个弄明白、讲清楚。于是就有了下面的文章。

三维坐标系

进入三次元世界之前,我们先来重新认识一下三维坐标系。高中立体几何学过,为何又要重新认识呢?幸亏你看了这篇文章,这里绝对是个大坑!立体几何的坐标系与计算机图形学的坐标系,两者是有区别滴。直接上图:

这是计算机世界的三维坐标系,只要记住两点,Y轴是指向下的,Z轴是指向观察者的。什么叫指向观察者呢?就是像要戳出屏幕迎面扎过来。坐标轴的正负方向至关重要,只有弄清了正负方向,做平移和旋转才不会糊涂。

平移 translate

简单理解,平移就是仅仅位置发生变化,而角度等都不发生变化。三维空间中任何一次平移,都可以分解成沿X轴Y轴Z轴各平移了多少。坐标轴有正负两个方向,向坐标轴的正方向移动,平移量就是正数。向坐标轴负方向移动,平移量就是负数。见下面代码:

  translateX: 10px; /* 沿X轴平移,向右移10像素 */
  translateY: 10px; /* 沿Y轴平移,向下移10像素,注意不是向上 */
  translateZ: 10px; /* 沿Z轴平移,拉近10像素,注意不是推远 */

  /* 正值是往坐标轴的正向移动,负值则是反方向移动 */
  translateX: -10px; /* 向左平移10像素 */
  translateY: -10px; /* 向上平移10像素 */
  translateZ: -10px; /* 推远10像素 */

  /* 还有一种组合写法 translate3d(tx, ty, tz) */
  translate3d(10px, -8px, 0); /* 右移10像素,上移8像素,Z轴方向上没动 */

旋转 rotate

平移是沿着坐标轴走,旋转则是绕着坐标轴转。旋转也有两个方向,顺时针转和逆时针转。注意坑来了,看自己戴的手表,表针是顺时针走没错。假如有个人站在你的正对面,从你的反方向看这块表,表针走的就是逆时针。那么问题来了,到底谁说了算?如何定义旋转的正负方向?

为此我从网上找了张图,姑且称之为左手法则:

用左手握住坐标轴,让大拇指指向坐标轴正方向,此时其余四指弯曲的方向就是旋转的正方向(底下蓝色箭头所示)。还可以这么理解,当坐标轴正对着刺入眼睛,此时你看到的顺时针方向,就是旋转的正方向。

考一考你,上图中手握的是XYZ哪个轴?Y轴?错!Y轴是朝下的!三根轴没有一条是朝上的,怎么样,一不小心又忘了吧。握Y轴应该是大拇指朝下,甄子丹版的《精武门》陈真打赢的时候嘚瑟起来就是这个手势。

来看下面的三个例子。这三个例子演示了空间中的矩形平面分别绕三个轴旋转的情形。

旋转 + 平移 = 任何你想要的平面

空间中的一个平面,对其同时运用旋转和平移,就可使其处于任何位置,摆出任何角度。

骰子是由六个正方形平面组成的,既然每个平面都可以任意控制位置和旋转角度,怎么把这六个平面拼成一个立方体呢?

首先我们让骰子的六个面初始值一样,即拥有同样的位置和角度,也就是叠在一起(参照下面例子可帮助理解)。

由初始位置出发,做六种不同的旋转和平移,就摆出了立方体的六个面。

例如,前方的正方形平面,是从初始位置往前平移了50像素,即沿Z轴正方向平移了50像素。

再如,上方的正方形平面,是先沿X轴旋转90度,然后沿Z轴平移50像素。嗯?怎么是Z轴呢,竖直的不是Y轴吗?啊哈,最大的坑出现了!请赶快擦亮眼睛。

不妨这样来想,屏幕里有一个统一的三维坐标系,又大又牢,不动如山(姑且称之为统一坐标系)。而每个DOM元素都有一个自己独立的三维坐标系(姑且称为独立坐标系),这个坐标系与元素紧紧绑定在一起,共缩放共旋转共进退。

有了上面的知识铺垫,再来看刚才那个问题。盖在立方体上方的那个平面,从原点开始,先沿着X轴旋转了正90度,因为是坐标系旋转,Z轴发生了90度偏移,Z轴从原先的垂直指向观察者转动到竖直指向上方(也就是原先Y轴的负方向),此时想向上平移50像素,就是沿Z轴正向平移50像素。

下面两个transform,做的是不同的动作,但最终的结果是一样的。

  /* 先沿X轴旋转90度,再沿Z轴平移50像素 */
  transform: rotateX(90deg) translateZ(50px);

  /* 先沿Y轴平移-50像素(Y指向下所以是向上移),再沿X轴旋转90度 */
  transform: translateY(-50px) rotateX(90deg);

立方体的六个面,transform分别如下:

.up.transform {
    transform: translateY(-50px) rotateX(90deg);
}

.bottom.transform {
    transform: translateY(50px) rotateX(-90deg);
}

.right.transform {
    transform: translateX(50px) rotateY(90deg);
}

.left.transform {
    transform: translateX(-50px) rotateY(-90deg);
}

.front.transform {
    transform: translateZ(50px);
}

.back.transform {
    transform: translateZ(-50px) rotateY(180deg);
}

perspective

perspective所代表的其实是perspective depth,指的是眼睛与屏幕的距离。用几何术语翻译一下,就是点与平面的距离,还原到现实世界,点就是眼睛,平面就是屏幕。这个距离的值,例如500px,就表示观察者的眼睛距离屏幕的垂直距离是500个像素。下图中,字母d所表示的就是perspective depth。

Z所表示的是目标在Z轴的偏移量(translateZ),当Z为正值,目标拉近,在屏幕上的投影(蓝色区域)变大。当Z为负值,目标推远,在屏幕上的投影变小,符合近大远小的常识。

如果不定义perspective,则不会有近大远小的效果。那么perspective定多少好呢?这就要看具体情况了。如果你要夸大透视的效果,就把值定小一些,反之就把值定大一些。举例来说,有一颗小骰子和一个大箱子,都放在地上与你差不多距离的位置,此时透视效果在大箱子身上很明显,在骰子身上却几乎看不出来。你把骰子举起来,慢慢拉到眼前,骰子越来越大,透视效果越来越明显。所以要根据情况来设置perspective的值,既和物体的大小有关又要和远近有关,总之目的是看起来逼真。

perspective-origin

平行透视是一个绘画术语,是说两根平行线如果向远方无限延伸,最终会相交于一点,这个点就是视点,也叫消失点。下面这两幅图就是视点在绘画中的应用。

CSS中用来表示视点的是 perspective-origin,它是一个二维坐标(注意是Y轴是指向下的所以原点在左上角)。它可以是像素值,也可以是百分比,默认值是{x:50%, y:50%}也就是正中心。

视点不同,所看到的景观当然也就不同。下面是几个例子,可以按底下的按钮来切换视点的位置,图中红色的小圆点就是视点。

CSS绘制3D的局限性

仅仅画一个骰子,就有这么多门道这么多坑。用DOM+CSS作图的一个局限性,就是只能画平面,画不了曲面。因为骰子是一个正六面体,恰好用平面可以拼出来,才侥幸得以实现。

因为要一个面一个面地去拼,制作稍微复杂一点的几何体都会变得很麻烦,而且在移动的时候,不同的平面偶尔会有不同步的现象,感觉物体在移动时肢解开了,停下后又还原回紧凑的整体,这体验实在不敢恭维。不过就CSS目前的情况来看,绘制简单的二维图形都很困难,没有现成方法,而是要借助各种hack手段,写出的代码可读性很差。所以在三维绘制上,就更不要对CSS有太多奢望了。前端命好苦。

END