無論是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]];
      
      
      
 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函式庫時,通常可以自行選擇使用哪一種頂點記錄方式,而且包裝成物件之後,您也無須親自處理索引的細節。

