頂點索引陣列


無論是2D或是3D繪製,頂點的使用是一個重要的課題,頂點的使用與座標息息相關,這也就是為何之前一直在談論座標系統的原因。

頂點的記錄方式有許多種,不同的繪圖目的應搭配不同的頂點資料結構,這邊介紹最簡單的幾個立體物件頂點記錄方式。

假設有一個正立方體,其中心位於原點,則可以如下圖先定出頂點的座標:


紙上作業與程式規劃所不同的是,如何使用這些頂點來繪製一個立方體,基本上必須以四個頂點為一個單位,使用繪製多邊型的函式來繪製一個四邊形,然後以較方 便的方式選擇四個頂點,通常會使用迴圈,但為了能使用迴圈,頂點資料結構必須有可重複索引的性質,在這邊介紹兩種規劃方式。

其中一個規劃方式是使用6*4=24個元素的陣列,每個面使用掉四個頂點,如下圖所示:


如此就可以使用迴圈取出頂點資訊,這個方法的好處是簡單,但由於頂點會有重複,因而會耗用大量的記憶體,對於複雜圖形並不適用。

可以使用頂點索引來解決頂點重複的問題,首先必須先將頂點編號,如下圖所示:


通常為了具有判別法向量的作用,頂點編號時使用右手定則,以逆時針的順序來編號同一個面的頂點;頂點編號完畢後,使用一個頂點索引陣列來記錄每個面所使用到的頂點編號,如下所示:
// Java
int V_INDEX[][] = {{0, 1, 2, 3}, {0, 7, 6, 1}, {4, 5, 6, 7},

                   {2, 5, 4, 3}, {0, 3, 4, 7}, {1, 6, 5, 2}};

// JavaScript
var V_INDEX = [
[0, 1, 2, 3], [0, 7, 6, 1], [4, 5, 6, 7],
               [2, 5, 4, 3], [0, 3, 4, 7], [1, 6, 5, 2]];
 

使用索引陣列的好處是減少記憶體使用量,雖然額外使用了一個索引陣列,但對於頂點越多時,記憶體的減少使用會更顯著,但缺點就是必須額外耗用一些運算是處理頂點資訊。VetexArray.htm

下面是使用頂點索引陣列來繪製正立方體的Java Applet程式,您可以參考我們是如何處理頂點資訊的:
  • Demo.java
package cc.openhome;

import java.awt.*;
import java.applet.*;

import static java.lang.Math.*;

class Point {
final int x, y, z;
Point(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
}

public class Demo extends Applet {
private final static int L = 100;
// 索引陣列
private final static int V_INDEX[][] = {
{0, 1, 2, 3}, {0, 7, 6, 1}, {4, 5, 6, 7},
{2, 5, 4, 3}, {0, 3, 4, 7}, {1, 6, 5, 2}
};
// 立方體頂點
private final static Point[] VETEX = {
new Point(L, L, L), new Point(L, -L, L),
new Point(L, -L, -L), new Point(L, L, -L),
new Point(-L, L, -L), new Point(-L, -L, -L),
new Point(-L, -L, L), new Point(-L, L, L)
};
// 視窗中心
private int orgX;
private int orgY;
public void init() {
super.init();
setBackground(Color.black);
setSize(640, 480);
orgX = getWidth() / 2;
orgY = getHeight() / 2;
}

public void paint(Graphics g) {
g.setColor(Color.yellow);
// 旋轉以斜角繪製圖形
double ax = toRadians(30);
double ay = toRadians(-30);

double sinAx = sin(ax);
double cosAx = cos(ax);
double sinAy = sin(ay);
double cosAy = cos(ay);

int[] px = new int[4];
int[] py = new int[4];
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 4; j++) {
// 利用索引陣列取出正確的頂點
Point v = VETEX[V_INDEX[i][j]];
// 旋轉以斜角繪製圖形
px[j] = (int) (v.x * cosAy + v.z * sinAy + orgX);
py[j] = (int) (v.y * cosAx -
(-v.x * sinAy + v.z * cosAy) * sinAx + orgY);
}
g.drawPolyline(px, py, 4);
}
}
}

以下是使用HTML5 Canvas的方式(如果瀏覽器支援HTML5 Canvas,例如最新版的Firexfox、Chrome、IE9等,可以直接將下面的內容存為HTML或按下檔名連結,直接載入瀏覽器執行觀看結果:
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=Big5" http-equiv="content-type">
<script type="text/javascript">
window.onload = function() {
function toRadians(angle) {
return angle * Math.PI / 180;
}

var sin = Math.sin;
var cos = Math.cos;
var sqrt = Math.sqrt;

function Point(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}

var V_INDEX = [
[0, 1, 2, 3], [0, 7, 6, 1], [4, 5, 6, 7],
[2, 5, 4, 3], [0, 3, 4, 7], [1, 6, 5, 2]
];

var L = 100;
var VETEX = [
new Point(L, L, L), new Point(L, -L, L),
new Point(L, -L, -L), new Point(L, L, -L),
new Point(-L, L, -L), new Point(-L, -L, -L),
new Point(-L, -L, L), new Point(-L, L, L)
];

// 旋轉以斜角繪製圖形
var ax = toRadians(30);
var ay = toRadians(-30);

var sinAx = sin(ax);
var cosAx = cos(ax);
var sinAy = sin(ay);
var cosAy = cos(ay);

var px = [];
var py = [];

var canvas1 = document.getElementById('canvas1');

var orgX = canvas1.width / 2;
var orgY = canvas1.height / 2;

var context = canvas1.getContext('2d');
context.beginPath();
for(var i = 0; i < 6; i++) {
for (var j = 0; j < 4; j++) {
// 利用索引陣列取出正確的頂點
var v = VETEX[V_INDEX[i][j]];
// 旋轉以斜角繪製圖形
px[j] = parseInt(v.x * cosAy + v.z * sinAy + orgX);
py[j] = parseInt(v.y * cosAx -
(-v.x * sinAy + v.z * cosAy) * sinAx + orgY);
}
context.moveTo(px[0], py[0]);
for(var k = 1; k < 4; k++) {
context.lineTo(px[k], py[k]);
}
}
context.stroke();
context.closePath();
};
</script>
</head>
<body>
<canvas id="canvas1" width="640" height="480"></canvas>
</body>
</html>

在Firefox下的效果如下:


如果您使用3D函式庫時,通常可以自行選擇使用哪一種頂點記錄方式,而且包裝成物件之後,您也無須親自處理索引的細節。