Z Buffer法


最大最小法 僅適用於曲面函式可以用顯函式y = f(x, z)來表示時,如果曲面函式非顯函式形式,則無法使用最大最小法來處理深度問題。

Z Buffer有些類似畫家演算法,都是以近景遮蓋遠景的方法來處理深度問題,所不同的是Z Buffer使用的是裁剪(culling)的方法,並以像素為處理的對象,Z Buffer將繪圖畫布內所有的座標當作一個深度緩衝區陣列zbuf[]的索引,每一個zbuf[]的元素記錄一個像素繪製時的Z深度資訊,可以使用它來 處理隱函式的圖形繪製。

假設畫布大小為600X400(X, Y),則zbuf[]的大小必須設定為600*400=240000(X*Y),一開始時所有zbuf[]元素的值設定為一個極小值,也就是所有的像素都 表示空間中一個極深的位置,開始繪製之後,必須在zbuf[]中記錄每一個像素的z值。

zbuf[]是個一維陣列,所以我們必須計算座標的索引值,如果以列為主的話,則(x, y)對應的zbuf[]元素值為:zbuf[x + y * 畫布高度];當然您也可以使用二維陣列zbuf[][]來直接對應。

如果後來要繪製的點之z值大於zbuf[]中記錄的值,表示此點在之前所繪點的前面,於是繪製此點來覆蓋之前所繪的點,並更新zbuf[] 中的z值為目前點的z值;如果後來要繪製的點之z值小於zbuf[]中記錄的值,表示此點在之前所繪點的後面,於是不用繪製此點,當然也不用更新zbuf []中的z值。

Z Buffer的深度處理方式無論從哪一個點開始繪製,都不會影響處理的結果;Z buffer的缺點就是使用大量的記憶體作為緩衝區,而由於它是以像素為處理的單位,所以需耗用相當大量的運算資源。

下面這個程式並不是一個很好的示範,因為我們並不是每個像素都考慮到,但可以讓您瞭解Z Buffer的演算法,如果要考慮所有的像素,這個程式要畫的好,最好加上陰影的效果,繪製的圖形之參數式如下,其中 a 表示圓的粗細:
x = (1+a*cosθ) * sinφ
y = a * sinθ
z = (1+a*cosθ)*cosφ
0 < a < 1, 0 <= θ <= 2π, 0 <= φ <= 2π

  • Demo.java
package cc.openhome;

import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JApplet;

import static java.lang.Math.*;

public class Demo extends JApplet {

private int orgX;
private int orgY;
private double[] zbuf;

public void init() {
super.init();
setBackground(Color.black);
setSize(640, 480);
orgX = getWidth() / 2;
orgY = getHeight() / 2;
zbuf = new double[getWidth() * getHeight()];
}

public void paint(Graphics g) {
g.setColor(Color.yellow);
// 從斜角繪製
// 繞 x 軸轉 30 度
double sinRotateX = sin(toRadians(30));
double cosRotateX = cos(toRadians(30));

double A = 0.3;
double K = 200.0;

for (int i = 0; i < zbuf.length; i++) {
zbuf[i] = -10000.0;
}

// 由於是單色,調整一下 j 與 i 可以看的明顯一些
for (double j = 0; j < 360; j += 0.2) {
for (double i = 0; i < 360; i += 0.1) {
double sinRadI = sin(toRadians(i));
double cosRadI = cos(toRadians(i));
double sinRadJ = sin(toRadians(j));
double cosRadJ = cos(toRadians(j));

double x = (1 + A * cosRadI) * sinRadJ;
double y = A * sinRadI;
double z = (1 + A * cosRadI) * cosRadJ;

// 立體旋轉,從斜角繪製,調整繪圖中心至視窗中心
double pointX = orgX + K * x;
double pointY = orgY - K * (y * cosRotateX - z * sinRotateX);

// Z buffer處理
int index = (int) (pointX + pointY * getWidth());

if (z > zbuf[index]) {
int px = (int) pointX;
int py = (int) pointY;
g.drawLine(px, py, px, py);
zbuf[index] = z;
}
}
}
}
}

以下是使用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;

var canvas1 = document.getElementById('canvas1');
var context = canvas1.getContext('2d');

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

var zbuf = new Array(canvas1.width * canvas1.height);

var sinRotateX = sin(toRadians(30));
var cosRotateX = cos(toRadians(30));

var A = 0.3;
var K = 200.0;

for(var i = 0; i < zbuf.length; i++) {
zbuf[i] = -10000.0;
}

context.beginPath();
var j = 0;
// 這個繪製很耗時間
// 在瀏覽器上會因Busy loop造成停止回應
// 因此使用setTimeout來作一毫秒的暫停
// 讓瀏覽器不至收不到程式回應而要求停止
setTimeout(function() {
if(j < 360) {
for(var i = 0; i < 360; i += 0.1) {
var sinRadI = sin(toRadians(i));
var cosRadI = cos(toRadians(i));
var sinRadJ = sin(toRadians(j));
var cosRadJ = cos(toRadians(j));

var x = (1 + A * cosRadI) * sinRadJ;
var y = A * sinRadI;
var z = (1 + A * cosRadI) * cosRadJ;

// 立體旋轉,從斜角繪製,調整繪圖中心至視窗中心
var pointX = orgX + K * x;
var pointY = orgY - K *
(y * cosRotateX - z * sinRotateX);

// Z buffer處理
var index = parseInt(
pointX + pointY * canvas1.width);

if(z > zbuf[index]) {
var px = parseInt(pointX);
var py = parseInt(pointY);
context.moveTo(pointX, pointY);
context.lineTo(pointX + 1, pointY + 1);
zbuf[index] = z;
}
}
j += 0.2;
setTimeout(arguments.callee, 1);
}
else {
context.stroke();
context.closePath();
}
}, 1);
};
</script>
</head>
<body>
<canvas id="canvas1" width="640" height="480"></canvas>
</body>
</html>

在Firefox上的結果如下: