在canvas 2D API下实现3D效果(3D版心形函数的绘制)

html5的canvas为我们提供了浏览器原生支持的绘图API。或者说,大多数浏览器已经为我们提供了原生的绘图API:HTML5的canvas
目前,这个API只提供2D context,并不支持3D绘图,但是web上从来就不缺牛人,各种canvas下绘制的3D效果层出不穷,令人吱吱称赞。(补充,后来有了WebGL)
有3D圣诞树:http://www.romancortes.com/blog/how-i-did-the-1kb-christmas-tree
有3D的FPS:使用 HTML 5 canvas 和光线投影算法创建伪 3D 游戏
还有3D俄罗斯:http://www.benjoffe.com/code/games/torus/
不胜枚举…

其实,无论canvas是否提供API,在我们目前这种二维显示设备下显示,势必都要将3维形状投影到2维平面坐标上。无论多炫的3D效果也只是二维平面上的投影。
对于此,读过《三体》特别是第三部《死神永生》的同学或许会大有感触吧。

人们总是喜欢用这样一个类比:想象生活在三维空间中的一张二维平面画中的扁片人,不管这幅画多么丰富多彩,其中的二维人只能看到周围世界的侧面,在他们眼中,周围的人和事物都是一些长短不一的线段而已。只有当一个二维扁片人从画中飘出来,进人三维空间,再回头看那幅画,才能看到画的全貌。
这个类比,其实也只是进一步描述了四维感觉的不可描述。

关于《三体——死神永生》里四维空间(不算时间维)的讨论,这里还有篇有意思的博文:四维世界的堵车问题

好了,言归正传。
下面提供一个三维到二维的投影算法(from www.benjoffe.com):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

var theta = 4.2; //转角
var eleva = 0.6; //仰角
function iso(x,y,z){
var dist = Math.sqrt(x*x+y*y);
var angle = (x==0 && y==0) ? 0 : Math.atan(y/x) + theta + ((x<0)? Math.PI : 0);

x=Math.cos(angle)*dist;
y=-Math.sin(angle)*dist;
var fact = (y*Math.cos(eleva) + z*Math.sin(eleva)+8)/8;
y=y*Math.sin(eleva) - z*Math.cos(eleva);
x*=fact;
y*=fact;

return {
x: x,
y: y
};
}

输入是x,y,z三个三维坐标下的值,输出是x,y两个二维坐标值。

我们应用一下:下面是一个3D球

 

以上代码是对球面的方程 x^2+y^2+z^2=r^2进行求解,将解(x,y,z)代入iso方法,最后根据输出二维坐标进行绘图。
对于这个球面方程的解法,也是各有各的写法。
(这里有个Functions 3D的应用,用来将方程式输出成3D图形:http://www.benjoffe.com/code/tools/functions3d/)

如果你看到过我这篇文章的话:笛卡尔情书的秘密——心形函数的绘制
我相信你也很可能知道网上还有一个3D版是心形函数:(x^2 + (9/4)y^2 + z^2 – 1)^3 – x^2z^3 – (9/80)y^2z^3 = 0
下面我将使用上面的iso方法在canvas中将其绘制出来,你可以拖到鼠标来看3D效果。

 

我们知道,三维空间下的坐标系不止直角坐标一种,还有 圆柱坐标系,球坐标系等等。
下面我们将iso方法转换一下,是输入使用球坐标系值(θ,Φ,r)——转角,仰角,球半径。
首先我们先要知道,三维直角坐标系于球坐标系的换算式:

1
2
3
4
x=rsinθcosφ  
y=rsinθsinφ   
z=rcosθ

呃哦,代入iso函数后我们发现iso变的更简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var theta = 4.2; //转角
var eleva = 0.6; //仰角
function iso(a,b,r){
var x,y,z
x=r*Math.cos(a+this.theta)*Math.sin(b);
y=r*Math.sin(a+this.theta)*Math.sin(b);
z=r*Math.cos(b);

var fact = (y*Math.cos(this.eleva) + z*Math.sin(this.eleva)+8)/8;
y=y*Math.sin(this.eleva) + z*Math.cos(this.eleva);
x*=fact;
y*=fact;
return {
x: x,
y: y
};
}

下面是我们使用球坐标系绘制的三维图形(三维投射到二维的图形)