WebGL ortografik 3D

Gregg Tavares
Gregg Tavares

WebGL Ortografik 3D

Bu yayın, WebGL ile ilgili bir dizi yayının devamıdır. İlki temel temellerle başladı, bir önceki ise 2D matrisler hakkında 2D matrisler hakkındaydı. Henüz okumadıysanız lütfen önce bu belgeleri görüntüleyin. Bir önceki gönderide, 2D matrislerin nasıl işlediğinden bahsettik. Çevirme, döndürme, ölçeklendirme ve hatta piksellerden klip alanına yansıtmanın bile tek matris ve bazı sihirli matris matematiği ile yapılabileceğinden bahsettik. 3D yapmak yalnızca bundan sonra atılacak küçük bir adımdır. Önceki 2D örneklerimizde 3x3'lük bir matrisle çarptığımız 2D noktalarımız (x, y) vardı. 3D yapmak için 3D noktalara (x, y, z) ve bir 4x4 matrisine ihtiyacımız vardır. Son örneğimizi alıp 3D olarak değiştirelim. Tekrar F kullanacağız, ancak bu kez 3D "F" olacaktır. Yapmamız gereken ilk şey, köşe gölgelendiricisini 3D'yi işleyecek şekilde değiştirmektir. İşte eski gölgelendirici.

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

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

Daha da basit hale geldi! 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);
}

Ardından, tüm matris fonksiyonlarını 2D'den 3D'ye dönüştürmemiz gerekiyor. MakeTranslation, makeRotation ve makeScale'in 2D (önceki) versiyonlarını burada 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
];
}

İşte 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,
];
}

Şimdi 3 döndürme işlevimiz olduğuna dikkat edin. 2D'de yalnızca bir tanesine ihtiyacımız vardı, çünkü etkin bir şekilde yalnızca Z ekseni etrafında dönüyorduk. Şimdi, 3D yapmak için x ve y ekseni etrafında da dönebilmek istiyoruz. İncelediğimizde hepsinin birbirine çok benzediğini görebiliyorsunuz. Bunları çözmüş olsaydık öncesi olduğu gibi basitleştirecek

Z döndürme

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

Y döndürme


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

X döndürme

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

Projeksiyon işlevini de güncellememiz gerekiyor. İşte 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
];
}

pikselden klip alanına dönüştürülür. 3D'ye genişletmeye yönelik ilk girişimimiz için,

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 gibi, z için de aynı şeyi yapmamız gerekir. Bu örnekte, Z boşluğu piksel birimlerini de yapıyorum. Derinlik için width benzeri bir değer ileteceğim. Böylece, alanımız 0 - genişlik piksel genişliğinde, 0 - yükseklik piksel yüksekliğinde olacak, ancak derinlik için -derinlik / 2 ile +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ı ve bu nedenle 3D'lerin görülmesini zorlaştırıyor. Bunu 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. Elimizde sayacak çok fazla şey var. 16 dikdörtgen x dikdörtgen başına 2 üçgen x üçgen başına 3 köşe, 96 köşedir. Tümünü görmek istiyorsanız kaynağı görüntüleyebilirsiniz. Daha fazla köşe çizmemiz gerekiyor.

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

Kaydırma çubuklarını hareket ettirdiğinizde, bunun 3D olduğunu anlamak oldukça zordur. Şimdi her dikdörtgeni farklı bir renge boyamaya çalışalım. Bunu yapmak için köşe gölgelendiricimize başka bir özellik ve köşe gölgelendiricisinden parça gölgelendiricisine geçirmek için de farklı bir özellik ekleyeceğiz. 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>

Parça gölgelendiricide bu rengi 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 için başka bir tampon 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? Bu 3D "F", ön, arka, yanlar vb. tüm farklı parçalarının Geometrimizde göründükleri sırayla çizildiği anlaşılıyor. Bazen arkadakiler öndekilerden sonra çizildiğinden, bu bize tam olarak istenen sonuçları vermez. WebGL'de üçgenler öne ve arkaya bakma konseptlerine sahiptir. Öne bakan üçgenin köşeleri saat yönünde hareket eder. Arkaya bakan üçgenin köşeleri saat yönünün tersi yönde hareket eder.

Üçgen kıvrım.

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

gl.enable(gl.CULL_FACE);

bunu programımızın başında, yalnızca bir kez yapıyoruz. Bu özellik açık olduğunda, WebGL varsayılan olarak arkaya bakan üçgenleri "ayrıştırır". Bu örnekte "Culling", "çizim değil" anlamına geliyor. WebGL söz konusu olduğunda, bir üçgenin saat yönünde mi yoksa saat yönünün tersine mi gittiğinin kabul edilip edilmeyeceği, üçgenin klip alanındaki köşelerine bağlıdır. Başka bir deyişle, WebGL üçgenin önde mi yoksa arkada mı olduğunu köşe gölgelendiricisindeki köşelere matematik uyguladıktan SONRA belirler. Diğer bir deyişle, X'te -1 ölçeklendirilen saat yönünde bir üçgen saat yönünün tersine bir üçgen veya X ya da Y ekseni etrafında 180 derece döndürülmüş 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ü(ön) hem de saat yönünün tersine(geri) üçgenleri görebiliriz. Artık bu özelliği etkinleştirdiğimize göre, öne bakan bir üçgen ölçekleme veya döndürme nedeniyle ya da herhangi bir nedenle etrafa döndüğünde WebGL bu üçgeni çizmez. Bu iyi bir şeydir, çünkü 3D'de bir şeyleri çevirdikçe genel olarak hangi üçgenlerin önünüze bakan olmasını istersiniz.

Merhaba! Bütün üçgenler nereye gitti? Ama bu akımların çoğunun yanlış yöne karşılaştığı anlaşılıyor. Döndürdüğünüzde diğer tarafa baktığınızda görürsünüz. Neyse ki, bu sorunu düzeltmek kolaydır. Hangilerinin geriye doğru olduğuna bakıp 2 köşe noktasını değiştiriyoruz. Örneğin, bir geriye dönük üçgen

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

ileri doğru yapmak için son 2 köşeyi çeviriyoruz.

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

Az kaldı ama hâlâ bir sorun var. Tüm üçgenler doğru yöne bakan ve arkaya bakanlar soyulmuş olsa bile, arkada olması gereken üçgenlerin önünde olması gereken üçgenlerin üzerinden çizildiği yerlerle karşı karşıyayız. DAĞITIM BUFFER'ı girin. Derinlik tamponu (bazen Z-Arabelleği olarak da adlandırılır), resmi oluşturmak için kullanılan her bir renk pikseli için bir derinlik pikseli olan depth piksellik bir dikdörtgendir. WebGL her bir renk pikselini çizerken bir derinlik pikseli de çizebilir. Bunu, Z için köşe gölgelendiriciden döndürdüğümüz değerlere dayanarak yapar. Tıpkı X ve Y için klip alanına dönüştürmemiz gerektiği gibi, Z de klip boşluğunda veya (-1'den +1'e) demektir. Bu değer daha sonra derinlik boşluğu değerine (0 ile +1 arasında) dönüştürülür. WebGL, bir renk pikseli çizmeden önce, karşılık gelen derinlik pikselini kontrol eder. Çizilecek 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ölgelendiricinizin rengiyle hem de yeni derinlik değeriyle birlikte derinlik pikselini de çizer. Bu, diğer piksellerin arkasındaki piksellerin çizilmeyeceği anlamına gelir. Bu özelliği, kayıttan çıkarmayı etkinleştirdiğimiz gibi,

gl.enable(gl.DEPTH_TEST);

Çizime başlamadan önce, derinlik arabelleğini tekrar 1,0 değerine çıkarmamı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 gönderide, bir perspektifi nasıl kullanabileceğinizi açıklayacağım.

Neden vec4 özelliği gl.vertexAttribPointer boyutu 3?

Ayrıntı odaklı olanlarınız için 2 özelliğimizi şu şekilde tanımladığımızı fark etmişsinizdir:

attribute vec4 a_position;
attribute vec4 a_color;

bunların her ikisi de 'vec4'tür, ancak WebGL'ye verileri arabelleklerimizden nasıl çıkaracağını bildirdiğimizde

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

Bunların her biri için "3" değeri, özellik başına yalnızca 3 değer alınmasını belirtir. Bu, köşe gölgelendiricisinde WebGL, sağlamadıklarınız için varsayılanlar sağladığından işe yarar. Varsayılan değerler, x = 0, y = 0, z = 0 ve w = 1 olacak şekilde 0, 0, 0, 1'dir. Bu nedenle, eski 2D köşe gölgelendiricimizde açıkça 1 tanesini sağlamak zorundaydık. x ve y'yi giriyorduk ve z için 1'e ihtiyacımız vardı ancak z'nin varsayılan değeri 0 olduğundan açık bir şekilde 1 sağlamamız gerekiyordu. 3D için de "w" sağlamasak da, matris matematiğinin çalışması için varsayılan olarak 1 değerini alırız.