WebGL ortografik 3D

Gregg Tavares
Gregg Tavares

WebGL ortografik 3D

Bu yayın, WebGL ile ilgili yayın dizisinin devamıdır. İlkinde temel bilgilerden bahsettik, önceki videoda ise 2D matrisleri ele aldık. Henüz okumadıysanız lütfen önce bu makaleleri inceleyin. Son yazımızda 2 boyutlu matrislerin işleyiş şeklini incelemiştik. Öteleme, döndürme, ölçeklendirme ve hatta piksellerden kırpma alanına yansıtma işlemlerinin hepsinin tek bir matris ve bazı sihirli matris matematiğiyle yapılabileceğinden bahsettik. 3D çizim yapmak için sadece küçük bir adım daha atmak gerekir. Önceki 2D örneklerimizde, 3x3 matrisle çarptığımız 2D noktalar (x, y) vardı. 3D için 3D noktalara (x, y, z) ve 4x4 matrise ihtiyacımız var. Son örneğimizi 3D olarak değiştirelim. Yine "F" harfini kullanacağız ancak bu kez 3D "F" harfi olacak. İlk olarak, tepe gölgelendiriciyi 3D'yi işleyecek şekilde değiştirmemiz gerekir. Eski gölgelendiriciyi görüyorsunuz.

<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>

Yeni olan da bu

<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>

Artık daha da kolay. Ardından 3D verileri sağlamamı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);
}

Şimdi tüm matris işlevlerini 2D'den 3D'ye değiştirmemiz gerekiyor. makeTranslation, makeRotation ve makeScale'in 2D (önceki) sürümleri şunlardır:

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
];
}

Bunlar da güncellenmiş 3D sürümleri.

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 fonksiyonumuz var. 2D'de yalnızca Z ekseni etrafında döndüğümüz için tek bir açı yeterliydi. Ancak 3D için x ve y eksenleri etrafında da dönebilmek istiyoruz. Baktığınızda hepsinin birbirine çok benzediğini görebilirsiniz. Bunları çözmeye çalışırsak daha önce olduğu gibi basitleştiklerini görürsünüz.

Z ekseninde döndürme

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

Y ekseninde döndürme


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

X ekseninde döndürme

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

Yansıtma işlevini de güncellememiz gerekiyor. Eski sürüm burada

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
];
}

Bu değerler pikselden kırpma uzayına dönüştürülmüştür. Bunu 3D'ye genişletmeye yönelik ilk denememizde,

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 pikselleri kırpma uzayına dönüştürmemiz gerektiği gibi, Z için de aynı şeyi yapmamız gerekir. Bu durumda Z uzayını da piksel birimi yapıyorum. Derinlik için width değerine benzer bir değer gireceğim. Böylece alanımız 0 ila genişlik piksel genişliğinde, 0 ila yükseklik piksel yüksekliğinde, derinlik için ise -derinlik / 2 ila +derinlik / 2 olacak. Son olarak, matrisi hesaplayan kodu güncellememiz gerekiyor.

// 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);

İlk sorunumuz, geometrimizin düz bir F olması ve bu nedenle 3D olarak görmenin zor olmasıdır. Bunu düzeltmek için geometriyi 3D olarak genişletelim. Şu anki F harfimiz 3 dikdörtgenden oluşuyor ve her birinde 2 üçgen var. 3D yapmak için toplam 16 dikdörtgen gerekir. Burada listelemek için oldukça fazla. 16 dikdörtgen x dikdörtgen başına 2 üçgen x üçgen başına 3 köşe, 96 köşe eder. Tümünü görmek istiyorsanız örnekteki kaynağı görüntüleyin. Daha fazla köşe çizmemiz gerekiyor.

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

Kaydırıcıları hareket ettirirken 3D olduğunu anlamak oldukça zor. Her dikdörtgeni farklı bir renge boyayalım. Bunun için köşe gölgelendiricimize başka bir özellik ekleyeceğiz ve bunu köşe gölgelendiriciden parça gölgelendiriciye aktarmak için bir değişken kullanacağız. Yeni köşe 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ça gölgelendiricide kullanmamız gerekiyor.

<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 için başka bir arabellek ve özellik ayarlamamı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);
}

Eyvah, bu ne dağınıklık? 3D 'F'nin ön, arka, yanlar gibi tüm farklı parçaları geometrimizde göründükleri sırayla çizilir. Bu yöntem, bazen arkadaki nesneler öndekilerden sonra çizildiği için istediğimiz sonucu vermez. WebGL'deki üçgenlerde öne ve arkaya bakan yüz kavramı vardır. Öne bakan bir üçgenin köşeleri saat yönünde ilerler. Arka yüzey üçgeninin köşeleri saat yönünün tersine doğru gider.

Üçgen sarma.

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

gl.enable(gl.CULL_FACE);

Bu işlemi programımızın en başında yalnızca bir kez yaparız. Bu özellik etkinleştirildiğinde WebGL, varsayılan olarak arkaya bakan üçgenleri "ayıklayacak" şekilde ayarlanır. Bu bağlamda "ayıklama", "çizmeme" anlamına gelen havalı bir kelimedir. WebGL söz konusu olduğunda, bir üçgenin saat yönünde mi yoksa saat yönünün tersine mi gittiğinin, kırpma alanındaki köşelerine bağlı olduğunu unutmayın. Diğer bir deyişle WebGL, bir üçgenin ön mü yoksa arka mı olduğunu, köşe gölgelendiricideki köşelere matematik uyguladıktan SONRA belirler. Örneğin, X ekseninde -1 ölçekli 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ırakıldığı için hem saat yönündeki(ön) hem de saat yönünün tersindeki(arka) üçgenleri görebiliriz. Bu özelliği etkinleştirdiğimiz için, ölçeklendirme veya döndürme gibi nedenlerle ön yüzü arkaya dönen üçgenler WebGL tarafından çizilmeyecek. Bu iyi bir şeydir çünkü bir şeyi 3D olarak döndürdüğünüzde genellikle size bakan üçgenlerin ön yüz olarak kabul edilmesini istersiniz.

Merhaba! Tüm üçgenler nereye gitti? Birçoğunun yanlış yöne baktığı anlaşılıyor. Döndürdüğünüzde diğer tarafta göründüklerini göreceksiniz. Neyse ki bu sorunu çözmek kolaydır. Sadece hangilerinin ters olduğuna bakar ve bunların 2 köşesini değiştiririz. Örneğin, bir geri üçgen "geri" anlamına geliyorsa

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

Son iki köşeyi ters çevirerek ileriye doğru yönlendiririz.

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

Bu daha yakın ama hâlâ bir sorun daha var. Tüm üçgenler doğru yöne baksa ve arkaya bakanlar ayıklansa bile arkada olması gereken üçgenlerin önde olması gereken üçgenlerin üzerine çizildiği yerler hâlâ var. DERİNLİK TAMPONU'nu girin. Bazen Z arabelleği olarak da adlandırılan derinlik arabelleği, depth piksellik bir dikdörtgendir. Resmi oluşturmak için kullanılan her renk pikseli için bir derinlik pikseli vardır. WebGL her renk pikselini çizerken bir derinlik pikseli de çizebilir. Bunu, köşe gölgelendiriciden Z için döndürdüğümüz değerlere göre yapar. X ve Y için kırpma uzayına dönüştürmemiz gerektiği gibi Z de kırpma uzayında veya (-1 ila +1) arasındadır. Bu değer daha sonra derinlik uzayı değerine (0 ila +1) dönüştürülür. WebGL, renk pikseli çizmeden önce ilgili derinlik pikselini kontrol eder. Çizmek üzere olduğu pikselin derinlik değeri, karşılık gelen derinlik pikselinin değerinden büyükse WebGL yeni renk pikselini çizmez. Aksi takdirde, hem yeni renk pikselini parça gölgelendiricinizdeki renkle hem de derinlik pikselini yeni derinlik değeriyle çizer. Bu, diğer piksellerin arkasındaki piksellerin çizilmeyeceği anlamına gelir. Bu özelliği, ayıklamayı etkinleştirdiğimiz kadar kolay bir şekilde etkinleştirebiliriz.

gl.enable(gl.DEPTH_TEST);

Çizime başlamadan önce derinlik arabelleğini de 1,0'a sıfırlamamız 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 yazıda, perspektif ekleme konusunu ele alacağım.

Özellik vec4 iken gl.vertexAttribPointer boyutu neden 3?

Ayrıntılara dikkat edenleriniz, iki ö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 ancak WebGL'ye, kullandığımız arabelleklerden verileri 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 formüllerdeki "3" sayısı, her özellik için yalnızca 3 değer alınacağını belirtir. Bu, köşe gölgelendiricide WebGL'nin sağlamadığınız değerler için varsayılan değerler sağlaması nedeniyle çalışır. 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 gölgelendiricimizde 1'i açıkça belirtmemiz gerekiyordu. x ve y'yi iletiyorduk ve z için 1'e ihtiyacımız vardı ancak z'nin varsayılan değeri 0 olduğu için açıkça 1 değerini vermemiz gerekiyordu. Ancak 3D için "w" değeri sağlamasak bile varsayılan olarak 1 değerini alır. Bu da matris matematiğinin çalışması için ihtiyacımız olan değerdir.