webGL球体绘制

webGL球体

我们知道立方体是由多个多边形组成的,所以我们通过将多边形分成一个个的三角形来绘制多边形,然后再将多边形组合成一个立方体。绘制球体的原理也是类似的,我们需要绘制一系列的三角形来逼近球体。这就要求我们要有一个分割球体的方案,将球体分成一个个的三角形,通过传入这些三角形的顶点坐标和顶点索引来绘制球体。这里介绍一个最简单的分割球体的方法,虽然方法简单,但是效果很不错。

分割原理很简单,我们在球体上画出经线和纬线,将球体分成了一个个的小区间。然后我们连接每个区间的对角线,将区间分成两个三角形。如下图所示,做完分割之后,我们只要计算出所有顶点的坐标就可以将球体绘制出来。

在知道怎么分割球体之后,我们需要知道怎么计算这些顶点的坐标。首先我们将球体放在坐标系的原点,沿着X轴和Y轴平面将球体切开,得到如下图所示的圆形,这个圆形的半径和球体的半径是一样的,我们假设是1。图中一条条的线是不同的纬线,一共有10条,我们假设从上往下数第三条纬线与圆心的连线与Y轴的夹角为θ,那么纬线与圆的交点的X坐标是sin(θ),Y坐标是cos(θ),这条纬线所在的圆的半径是sin(θ)。

因为每两条相邻纬线之间的球面距离都是相等的,我们可以根据θ的值来计算出每条纬线的半径。每个半圆的弧度是π,所以θ的取值应该是从0、π/10、2π/10、 3π/10……一直到10π/10,第n条纬线对应的Y坐标是cos(nπ / 10)。

纬线上的点,无论经度是多少,他们的Y坐标都是一样的。接来下我们需要知道这些点的X和Z坐标。我们将球体沿着第n条纬线切开,得到一个圆形,半径是sin(nπ / 10)。假设某条经线与这个圆形的交点与圆心的连线与X轴成φ角,则这个点对应的X坐标为sin(nπ / 10)cos(φ),Z坐标为sin(nπ / 10)sin(φ),相应的,我们也会有10条经线,所以φ的取值为0、2π/10、4π/10……,到20π/10。

总结一下,对于一个半径为r的球体,有m个纬线带和n个经线带,我们把从0到π的区间平均分成m等份就可以得到θ的取值范围,把0到2π的区间平均分成n等份就可以得到φ的取值范围,从而计算出坐标x,y,z的值。

x = r sinθ cosφ
y = r cosθ
z = r sinθ sinφ

完成了三角形的分割之后,最后一步是确定顶点索引。针对每个经纬线划分出来的小区间,如下图所示,按照A-B-C,B-D-C的顺序绘制三角形。

月亮的demo

这是一个使用了一张月亮纹理的demo,可以使用鼠标滑动来旋转月亮。

webGL代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

var moonVertexPositionBuffer;
var moonVertexNormalBuffer;
var moonVertexTextureCoordBuffer;
var moonVertexIndexBuffer;

function initBuffers() {
//使用三十条纬线和三十条经线划分球体
var latitudeBands = 30;
var longitudeBands = 30;
//球体半径
var radius = 2;
//存储顶点坐标
var vertexPositionData = [];
//存储纹理坐标
var textureCoordData = [];
//从纬线开始遍历
for (var latNumber=0; latNumber <= latitudeBands; latNumber++) {
//计算θ角度
var theta = latNumber * Math.PI / latitudeBands;
var sinTheta = Math.sin(theta);
var cosTheta = Math.cos(theta);

for (var longNumber=0; longNumber <= longitudeBands; longNumber++) {
//计算φ角度
var phi = longNumber * 2 * Math.PI / longitudeBands;
var sinPhi = Math.sin(phi);
var cosPhi = Math.cos(phi);
//计算顶点的x,y,z坐标
var x = radius * cosPhi * sinTheta;
var y = radius * cosTheta;
var z = radius * sinPhi * sinTheta;
//贴图是矩形的,我们将贴图在X轴上按照经线划分,在Y轴上按照纬线划分,来计算顶点对应的贴图U,V坐标
var u = longNumber / longitudeBands;
var v = latNumber / latitudeBands;


textureCoordData.push(u);
textureCoordData.push(v);
vertexPositionData.push(x);
vertexPositionData.push(y);
vertexPositionData.push(z);
}
}
//存储顶点索引
var indexData = [];
for (var latNumber=0; latNumber < latitudeBands; latNumber++) {
for (var longNumber=0; longNumber < longitudeBands; longNumber++) {
var A = (latNumber * (longitudeBands + 1)) + longNumber;
var B = A + longitudeBands + 1;
var C = A + 1;
var D = B + 1;
indexData.push(A);
indexData.push(B);
indexData.push(C);

indexData.push(B);
indexData.push(D);
indexData.push(C);
}
}

moonVertexTextureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, moonVertexTextureCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordData), gl.STATIC_DRAW);
moonVertexTextureCoordBuffer.itemSize = 2;
moonVertexTextureCoordBuffer.numItems = textureCoordData.length / 2;

moonVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, moonVertexPositionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexPositionData), gl.STATIC_DRAW);
moonVertexPositionBuffer.itemSize = 3;
moonVertexPositionBuffer.numItems = vertexPositionData.length / 3;

moonVertexIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, moonVertexIndexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indexData), gl.STATIC_DRAW);
moonVertexIndexBuffer.itemSize = 1;
moonVertexIndexBuffer.numItems = indexData.length;
}