WebGLモードの新たな「描き順」問題


2018年02月26日
With
WebGLモードの新たな「描き順」問題 はコメントを受け付けていません。

クイズです。まず、こちらのコードを見てください。

function setup() {
	document.body.style.backgroundColor = '#222222';
	createCanvas(600, 600, WEBGL);
	ambientLight(80);
	directionalLight(255, 255, 255, 255, -1, 1, -1);
	pointLight(255, 255, 255, 255, -200, -200, 200);
	noStroke();
}

function draw() {
	background(0);

	push();
	translate(0, 0, 100);
	specularMaterial(80, 80, 255, 128);
	sphere(200, 200, 200);
	pop();

	push();
	translate(0, 0, -300);
	specularMaterial(255, 80, 80, 255);
	sphere(200, 200, 200);
	pop();
}

p5.js 0.6.0のWebGLモードで、「赤」と「青」の2つのsphere(球)を描くシンプルなコードです。青玉の座標は(0, 0, 100)で、specularMaterialでアルファ値を「128」の半透明に設定しています。赤玉の座標は(0, 0, -300)で、アルファ値は「255」の完全な不透明です。2つの球はいずれも「200」の同サイズです。p5.jsのWebGLモードでは、デフォルトでz軸の正方向は手前になるので、真上から見た場合の「赤玉」「半透明青玉」「カメラ」の位置関係は↓のような感じになります。

さて、このスケッチ、0.6.0で実際に走らせてみると、どのように見えるでしょうか?

答えは↓です。

…いや、おかしい。違うぞこれは(笑)。カメラから見て青玉より奥にあるはずの赤玉が小さくなって前に出てきてしまっているように見えます。

では、ここで第2問。↑のコードでdraw()の中で青玉を描いている「13~17行」と、赤玉を描いている「19~23行」の順番を入れ替えると、どうなるでしょうか。

(シンキングタイム♪)

正解は↓です。

…そうそう。これこれ。半透明の青玉の向こう側に、透けるような感じで赤玉が見えています。本来は、先ほどの場合も、こう見えてほしかったのです。

では次の問題です。玉の描き順を元のコードのものに戻して、青玉の「specularMaterial」の設定でアルファ値を「255」、つまり完全な不透明にすると、どう表示されるでしょうか?

正解はこれ。

青玉が不透明になったので、赤玉が完全に見えなくなりました。これは正しく表示されていますね。うーむ。…とりあえず「青玉の透明度設定」と「青玉と赤玉の描き順」の組み合わせで、表示がどうなるのかを確認してみました。

…つまり「マテリアルでアルファ値を255以外に設定している場合、カメラから見て奥にあるオブジェクトから順に描くようにしないと、正しく透過処理が行われない」ということのようです。

「透明度」と「描き順」。なんか、前にこれで苦労したことがあったような気がします(笑)。

p5.jsのWebGLモードで「グロー」を使おうとしてハマる
WebGLモードのスケッチを0.6.0用に書き直してみた(暫定)

以前「plane」と透明度を設定した「texture」の組み合わせで起こり、0.6.0で解消されていたかのように思えたオブジェクトの「描き順」問題、実は完全には直っていなかったのですね。

オブジェクトが前後方向に動かないスケッチであれば、コードを書くときの順番に気を付ければいいだけですが「複数の半透明オブジェクトがz軸方向も含めて移動する」ようなスケッチの場合はそうもいきません。不要になったと思っていた「Array.sort()」の再登板です。

●0.6.0で帰ってきた描き順問題(クリックすると別タブが開きます)

draw内に「Array.sort()」を入れ、フレームごとに「zOrder」でz座標を基準としてオブジェクトの配列内での順番を並び替えてやることにより「半透明のオブジェクト(青玉)より奥にあるはずの不透明オブジェクト(赤玉)が手前にあるように表示される」問題を回避しています。本当であれば、カメラ位置と各オブジェクトとの距離を「dist()」で出して比較してやったほうがより正しく描画されるはずですが、まぁ、このスケッチであればこれくらいで十分でしょう。

var sphereArr = [];
var spCount = 60;

function setup() {
    createCanvas(600, 600, WEBGL);
    document.body.style.backgroundColor = '#333377';
    ambientLight(40);
    directionalLight(255, 255, 255, 255, -1, 1, -1);
    pointLight(255, 255, 255, 255, -200, -500, 200);
    noStroke();
    for (var i = 0; i < spCount; i++) {
        sphereArr[i] = new Spr();
    }
}

function draw() {
    background(0);
    sphereArr.forEach(allSp => { allSp.updateMe(); });
    sphereArr.sort(zOrder);
    sphereArr.forEach(allSp => { allSp.drawMe(); });
}

function zOrder(obj1, obj2) {
    return obj1.z - obj2.z;
}


class Spr {
    constructor() {
        this.x = random(-100, 100);
        this.y = random(-100, 100);
        this.z = random(-1000, 100);
        this.r = 50;
        this.xmov = random(-5, 5);
        this.ymov = random(-5, 5);
        this.zmov = random(-5, 5);
        this.trans = floor(random(0, 2));
    }
    updateMe() {
        this.x += this.xmov;
        this.y += this.ymov;
        this.z += this.zmov;
        if (this.x < -300 || this.x > 300) { this.xmov *= -1; }
        if (this.y < -300 || this.y > 300) { this.ymov *= -1; }
        if (this.z < -2000 || this.z > 300) { this.zmov *= -1; }
    }
    drawMe() {
        push();
        translate(this.x, this.y, this.z);
        if (this.trans == 0) {
            specularMaterial(255, 100, 100, 255);
        } else {
            specularMaterial(100, 100, 255, 80);
        }
        sphere(this.r, 200, 200);
        pop();
    }
}

この「バグ」も将来のバージョンアップで修正されるかもしれませんが、それまではとりあえずこの方法でしのいでおこうと思います。