WebGL ortografik 3D

Gregg Tavares
Gregg Tavares

WebGL Orthographic 3D

Bu yayın, WebGL ile ilgili bir dizi yayının devamı niteliğindedir. İlki temel bilgilerle başladı, önceki ise 2D matrislerle ilgiliydi 2D matrisler hakkında. Henüz okumadıysanız lütfen önce bunları inceleyin. Son yayınımızda 2D matrislerin işleyiş şeklini ele aldık. Taşıma, döndürme, ölçeklendirme ve hatta piksellerden klip alanına projeksiyon yapma işlemlerinin hepsinin 1 matris ve bazı sihirli matris matematikleriyle yapılabileceğinden bahsettik. 3D işleme geçmek için yalnızca küçük bir adım atmanız gerekir. Önceki 2D örneklerimizde, 3x3 matrisle çarptığımız 2D noktalara (x, y) sahiptik. 3D işlemek için 3D noktalara (x, y, z) ve 4x4 matrise ihtiyacımız vardır. Son örneğimizi 3D olarak değiştirelim. Yine F harfini kullanacağız ancak bu sefer 3D bir "F" harfi. Yapmamız gereken ilk şey, 3D'yi işleyebilecek şekilde köşe düğümü gölgelendiriciyi değiştirmektir. Eski gölgelendiriciyi burada bulabilirsiniz.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform mat3 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
</script>

İşte yenisi

<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;

uniform mat4 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
}
</script>

İşlem artık daha da kolay. Ardından 3D verileri sağlamanız gerekir.

...

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

...

// Fill the buffer with the values that define a letter 'F'.
function setGeometry(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
        // left column
        0,   0,  0,
        30,   0,  0,
        0, 150,  0,
        0, 150,  0,
        30,   0,  0,
        30, 150,  0,

        // top rung
        30,   0,  0,
        100,   0,  0,
        30,  30,  0,
        30,  30,  0,
        100,   0,  0,
        100,  30,  0,

        // middle rung
        30,  60,  0,
        67,  60,  0,
        30,  90,  0,
        30,  90,  0,
        67,  60,  0,
        67,  90,  0]),
    gl.STATIC_DRAW);
}

Ardından tüm matris işlevlerini 2D'den 3D'ye değiştirmemiz gerekiyor. makeTranslation, makeRotation ve makeScale işlevlerinin 2D (önceki) sürümlerini aşağıda bulabilirsiniz.

function makeTranslation(tx, ty) {
return [
1, 0, 0,
0, 1, 0,
tx, ty, 1
];
}

function makeRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c,-s, 0,
s, c, 0,
0, 0, 1
];
}

function makeScale(sx, sy) {
return [
sx, 0, 0,
0, sy, 0,
0, 0, 1
];
}

Güncellenen 3D sürümler aşağıda verilmiştir.

function makeTranslation(tx, ty, tz) {
return [
    1,  0,  0,  0,
    0,  1,  0,  0,
    0,  0,  1,  0,
    tx, ty, tz, 1
];
}

function makeXRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
};

function makeYRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
];
};

function makeZRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
    c, s, 0, 0,
-s, c, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1,
];
}

function makeScale(sx, sy, sz) {
return [
sx, 0,  0,  0,
0, sy,  0,  0,
0,  0, sz,  0,
0,  0,  0,  1,
];
}

Artık 3 rotasyon işlevimiz olduğunu fark edin. Yalnızca Z ekseni etrafında döndüğümüz için 2D'de yalnızca bir tane modele ihtiyacımız vardı. Ancak 3D yapmak için x ekseni ve y ekseni etrafında da döndürebilmek isteriz. Bu resimlere baktığınızda hepsinin birbirine çok benzediğini görebilirsiniz. Bunları çözecek olursak, daha önce olduğu gibi basitleştiğini görürsünüz.

Z dönüşümü

newX = x * c + y * s;
newY = x * -s + y * c;

Y ekseni dönme


newX = x * c + z * s;
newZ = x * -s + z * c;

X rotasyonu

newY = y * c + z * s;
newZ = y * -s + z * c;

Ayrıca projeksiyon işlevini de güncellememiz gerekiyor. Eskisi

function make2DProjection(width, height) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
}

piksel olarak dönüştürülür. İlk 3D denememizde şunu deneyelim

function make2DProjection(width, height, depth) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
    2 / width, 0, 0, 0,
    0, -2 / height, 0, 0,
    0, 0, 2 / depth, 0,
-1, 1, 0, 1,
];
}

x ve y için piksellerden klip alanına dönüştürmemiz gerektiği gibi, z için de aynı işlemi yapmamız gerekir. Bu durumda Z alanı piksel birimlerini de oluşturuyorum. Derinlik için width'e benzer bir değer ileteceğim. Böylece alanımız 0 ila genişlik pikseli genişliğinde, 0 ila yükseklik pikseli yüksekliğinde olacak ancak derinlik için -derinlik / 2 ila +derinlik / 2 olacak. Son olarak, matrisi hesaplayan kodu güncellememiz gerekir.

// Compute the matrices
var projectionMatrix =
    make2DProjection(canvas.width, canvas.height, canvas.width);
var translationMatrix =
    makeTranslation(translation[0], translation[1], translation[2]);
var rotationXMatrix = makeXRotation(rotation[0]);
var rotationYMatrix = makeYRotation(rotation[1]);
var rotationZMatrix = makeZRotation(rotation[2]);
var scaleMatrix = makeScale(scale[0], scale[1], scale[2]);

// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationZMatrix);
matrix = matrixMultiply(matrix, rotationYMatrix);
matrix = matrixMultiply(matrix, rotationXMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
matrix = matrixMultiply(matrix, projectionMatrix);

// Set the matrix.
gl.uniformMatrix4fv(matrixLocation, false, matrix);

Karşılaştığımız ilk sorun, geometrimizin düz bir F olması. Bu da 3D'yi görmeyi zorlaştırıyor. Bu sorunu düzeltmek için geometriyi 3D olarak genişletelim. Mevcut F'miz, her biri 2 üçgen olmak üzere 3 dikdörtgenden oluşur. 3D yapmak için toplam 16 dikdörtgen gerekir. Burada listelememiz gereken çok fazla özellik var. 16 dikdörtgen x dikdörtgen başına 2 üçgen x üçgen başına 3 köşe = 96 köşe. Bunların tümünü görmek istiyorsanız örnekte kaynağı görüntüleyin. Daha fazla köşe çizmemiz gerekiyor.

// Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);

Kaydırma çubuklarını hareket ettirirken bunun 3D olduğunu anlamak oldukça zor. Her dikdörtgeni farklı bir renkle boyamayı deneyelim. Bunu yapmak için köşe düğümü gölgeleme programımıza başka bir özellik ve bu özelliği köşe düğümü gölgeleme programından parçacık gölgeleme programına aktarmak için bir değişken ekleriz. Yeni köşe düğümü gölgelendirici

<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
attribute vec4 a_color;

uniform mat4 u_matrix;

varying vec4 v_color;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;

// Pass the color to the fragment shader.
v_color = a_color;
}
</script>

Bu rengi, parçacık gölgelendiricide kullanmamız gerekir.

<script id="3d-vertex-shader" type="x-shader/x-fragment">
precision mediump float;

// Passed in from the vertex shader.
varying vec4 v_color;

void main() {
gl_FragColor = v_color;
}
</script>

Renkleri sağlamak için konumu aramamız, ardından renkleri vermek üzere başka bir arabellek ve özellik oluşturmamız gerekir.

...
var colorLocation = gl.getAttribLocation(program, "a_color");

...
// Create a buffer for colors.
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(colorLocation);

// We'll supply RGB as bytes.
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

// Set Colors.
setColors(gl);

...
// Fill the buffer with colors for the 'F'.

function setColors(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Uint8Array([
        // left column front
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,

        // top rung front
    200,  70, 120,
    200,  70, 120,
    ...
    ...
    gl.STATIC_DRAW);
}

Bu ne? Bu 3D "F"nin ön, arka, yan vb. tüm farklı parçalarının geometrimizde göründükleri sırayla çizildiği ortaya çıkıyor. Bazen arkadakiler öndekilerden sonra çizildiği için bu yöntemle istediğimiz sonuçları elde edemiyoruz. WebGL'deki üçgenlerde ön ve arka yüz kavramları vardır. Öne bakan bir üçgenin köşeleri saat yönünde gider. Arkaya bakan bir üçgenin köşeleri saat yönünün tersine gider.

Üçgen sarma.

WebGL yalnızca ileri veya arkaya bakan üçgenler çizebilir. Bu özelliği

gl.enable(gl.CULL_FACE);

Bu işlemi programımızın en başında bir kez yaparız. Bu özellik etkinleştirildiğinde WebGL, arkaya bakan üçgenleri varsayılan olarak "ayıklar". Bu durumda "ayıklama", "çizmeme" anlamına gelen havalı bir kelimedir. WebGL ile ilgili olarak, bir üçgenin saat yönünde mi yoksa saat yönünün tersinde mi gittiğinin, bu üçgenin klip alanındaki köşelerine bağlı olduğunu unutmayın. Diğer bir deyişle, WebGL bir üçgenin önden mi yoksa arkadan mı görüneceğini, tepe gölgelendiricisindeki tepe noktalarına matematiksel işlemler uyguladıktan SONRA belirler. Yani örneğin, X ekseninde -1 ölçeğindeki saat yönünde bir üçgen, saat yönünün tersine bir üçgen olur veya X ya da Y ekseni etrafında 180 derece döndürülen saat yönünde bir üçgen, saat yönünün tersine bir üçgen olur. CULL_FACE'ı devre dışı bıraktığımız için hem saat yönünde(ön) hem de saat yönünün tersine(arka) bakan üçgenleri görebiliriz. Bu ayarı etkinleştirdiğimizde, ölçeklendirme veya döndürme ya da başka bir nedenden dolayı ön tarafa bakan bir üçgen her zaman ters döndüğünde WebGL bunu çizmez. Bu, bir nesneyi 3D olarak döndürdüğünüzde genellikle size bakan üçgenlerin ön yüz olarak kabul edilmesini istediğiniz için iyi bir şeydir.

Merhaba! Tüm üçgenler nereye gitti? Ancak bunların çoğunun yanlış yöne baktığı ortaya çıktı. Döndürdüğünüzde diğer tarafa baktığınızda bunları görürsünüz. Neyse ki bu sorunu kolayca çözebilirsiniz. Yalnızca hangilerinin ters yöne baktığını kontrol edip 2 köşesini değiştiririz. Örneğin, geriye dönük bir üçgen

1,   2,   3,
40,  50,  60,
700, 800, 900,

İleri doğru olmasını sağlamak için son 2 köşeyi çeviririz.

1,   2,   3,
700, 800, 900,
40,  50,  60,

Bu daha doğru ancak hâlâ bir sorun var. Tüm üçgenler doğru yönde baksa ve arkaya bakan üçgenler ayıklanmış olsa bile arkada olması gereken üçgenlerin öndeki üçgenlerin üzerine çizildiği yerler var. DEPTH BUFFER'ı girin. Derinlik arabelleği (Z-arabelleği olarak da bilinir), görüntüyü oluşturmak için kullanılan her renk pikseli için bir derinlik pikseli olan depthpikseli dikdörtgendir. WebGL her renk pikseli çizerken derinlik pikseli de çizebilir. Bunu, Z için köşe düğümü gölgelendiricisinden döndürdüğümüz değerlere göre yapar. X ve Y için klip alanına dönüştürmemiz gerektiği gibi, Z de klip alanında veya (-1 ila +1) arasındadır. Bu değer daha sonra derinlik alanı değerine (0 ila +1) dönüştürülür. WebGL, bir renk pikseli çizmeden önce ilgili derinlik pikseli kontrol eder. Çizmek üzere olduğu pikselin derinlik değeri, ilgili derinlik pikselin değerinden büyükse WebGL yeni renk pikseli çizmez. Aksi takdirde hem yeni renk pikseli, parçacık gölgelendiricinizden gelen renkle hem de derinlik pikseli, yeni derinlik değeriyle çizilir. Bu, diğer piksellerin arkasındaki piksellerin çizilmeyeceği anlamına gelir. Bu özelliği, ayıklama özelliğini etkinleştirdiğimiz kadar kolay bir şekilde etkinleştirebiliriz.

gl.enable(gl.DEPTH_TEST);

Ayrıca, çizmeye başlamadan önce derinlik arabelleğini 1,0 değerine geri temizlememiz gerekir.

// Draw the scene.
function drawScene() {
// Clear the canvas AND the depth buffer.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
...

Bir sonraki gönderide, perspektif ekleme hakkında bilgi vereceğim.

Özellik neden vec4 ancak gl.vertexAttribPointer boyutu 3?

Ayrıntılara önem verenler, 2 özelliğimizi şu şekilde tanımladığımızı fark etmiş olabilir:

attribute vec4 a_position;
attribute vec4 a_color;

Her ikisi de "vec4" türündedir ancak WebGL'ye verilerimizi arabelleklerimizden nasıl alacağını söylediğimizde

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

Bu değerlerin her birindeki "3", özellik başına yalnızca 3 değer alınacağını belirtir. Bu, WebGL'in, sağlamadığınız değerler için varsayılan değerler sağlaması nedeniyle işe yarar. Varsayılan değerler 0, 0, 0, 1'dir. Burada x = 0, y = 0, z = 0 ve w = 1'dir. Bu nedenle eski 2D köşe üstü gölgelendiricimizde 1 değerini açıkça belirtmemiz gerekiyordu. x ve y parametrelerini iletiyorduk ve z için 1 değerine ihtiyacımız vardı ancak z için varsayılan değer 0 olduğu için açıkça 1 değerini sağlamamız gerekiyordu. Ancak 3D için "w" sağlamasak da varsayılan olarak 1 olur. Bu da matris matematiğinin çalışması için ihtiyaç duyduğumuz değerdir.