WebGL或OpenGL關於模型視圖投影變換的設置技巧

1. 具體實例

看了很多的關於WebGL/OpenGL的資料,筆者發現這些資料在講解圖形變換的時候都講了不少的原理,而後舉出一個特別簡單的實例(座標是1.0,0.5的那種)來說解。確實一看就懂,但用到實際的場景之中就一臉懵逼了(好比地形的三維座標都是很大的數字)。因此筆者這裏結合一個具體的實例,總結下WebGL/OpenGL中,關於模型變換、視圖變換、投影變換的設置技巧。web

繪製任何複雜的場景以前,均可以先繪製出其包圍盒,能應用於包圍盒的圖形變換,基本上就能用於該場景了,所以,筆者這裏繪製一幅地形的包圍盒。它的最大最小範圍爲:chrome

//包圍盒範圍
var minX = 399589.072;
var maxX = 400469.072;
var minY = 3995118.062;
var maxY = 3997558.062;
var minZ = 732;
var maxZ = 1268;

2. 解決方案

WebGL是OpenGL的子集,所以我這裏直接用WebGL的例子,可是各類接口函數跟OpenGL是很是相似的,尤爲是圖形變換的函數。編程

1) Cube.html

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="utf-8" />
    <title>Hello cube</title>
  </head>

  <body onload="main()">
    <canvas id="webgl" width="600" height="600">
    Please use a browser that supports "canvas"
    </canvas>

    <script src="lib/webgl-utils.js"></script>
    <script src="lib/webgl-debug.js"></script>
    <script src="lib/cuon-utils.js"></script>
    <script src="lib/cuon-matrix.js"></script>
    <script src="Cube.js"></script>
  </body>
</html>

2) Cube.js

// Vertex shader program
var VSHADER_SOURCE =
    'attribute vec4 a_Position;\n' +
    'attribute vec4 a_Color;\n' +
    'uniform mat4 u_MvpMatrix;\n' +
    'varying vec4 v_Color;\n' +
    'void main() {\n' +
    '  gl_Position = u_MvpMatrix * a_Position;\n' +
    '  v_Color = a_Color;\n' +
    '}\n';

// Fragment shader program
var FSHADER_SOURCE =
    '#ifdef GL_ES\n' +
    'precision mediump float;\n' +
    '#endif\n' +
    'varying vec4 v_Color;\n' +
    'void main() {\n' +
    '  gl_FragColor = v_Color;\n' +
    '}\n';

//包圍盒範圍
var minX = 399589.072;
var maxX = 400469.072;
var minY = 3995118.062;
var maxY = 3997558.062;
var minZ = 732;
var maxZ = 1268;

//包圍盒中心
var cx = (minX + maxX) / 2.0;
var cy = (minY + maxY) / 2.0;
var cz = (minZ + maxZ) / 2.0;

//當前lookAt()函數初始視點的高度
var eyeHight = 2000.0;

//根據視點高度算出setPerspective()函數的合理角度
var fovy = (maxY - minY) / 2.0 / eyeHight;
fovy = 180.0 / Math.PI * Math.atan(fovy) * 2;

//setPerspective()遠截面
var far = 3000;

//
function main() {
    // Retrieve <canvas> element
    var canvas = document.getElementById('webgl');

    // Get the rendering context for WebGL
    var gl = getWebGLContext(canvas);
    if (!gl) {
        console.log('Failed to get the rendering context for WebGL');
        return;
    }

    // Initialize shaders
    if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
        console.log('Failed to intialize shaders.');
        return;
    }

    // Set the vertex coordinates and color
    var n = initVertexBuffers(gl);
    if (n < 0) {
        console.log('Failed to set the vertex information');
        return;
    }

    // Get the storage location of u_MvpMatrix
    var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
    if (!u_MvpMatrix) {
        console.log('Failed to get the storage location of u_MvpMatrix');
        return;
    }

    // Register the event handler
    var currentAngle = [0.0, 0.0]; // Current rotation angle ([x-axis, y-axis] degrees)
    initEventHandlers(canvas, currentAngle);

    // Set clear color and enable hidden surface removal
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);

    // Start drawing
    var tick = function () {

        //setPerspective()寬高比
        var aspect = canvas.width / canvas.height;

        //
        draw(gl, n, aspect, u_MvpMatrix, currentAngle);
        requestAnimationFrame(tick, canvas);
    };
    tick();
}

function initEventHandlers(canvas, currentAngle) {
    var dragging = false;         // Dragging or not
    var lastX = -1, lastY = -1;   // Last position of the mouse

    // Mouse is pressed
    canvas.onmousedown = function (ev) {
        var x = ev.clientX;
        var y = ev.clientY;
        // Start dragging if a moue is in <canvas>
        var rect = ev.target.getBoundingClientRect();
        if (rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom) {
            lastX = x;
            lastY = y;
            dragging = true;
        }
    };

    //鼠標離開時
    canvas.onmouseleave = function (ev) {
        dragging = false;
    };

    // Mouse is released
    canvas.onmouseup = function (ev) {
        dragging = false;
    };

    // Mouse is moved
    canvas.onmousemove = function (ev) {
        var x = ev.clientX;
        var y = ev.clientY;
        if (dragging) {
            var factor = 100 / canvas.height; // The rotation ratio
            var dx = factor * (x - lastX);
            var dy = factor * (y - lastY);
            // Limit x-axis rotation angle to -90 to 90 degrees
            //currentAngle[0] = Math.max(Math.min(currentAngle[0] + dy, 90.0), -90.0);
            currentAngle[0] = currentAngle[0] + dy;
            currentAngle[1] = currentAngle[1] + dx;
        }
        lastX = x, lastY = y;
    };

    //鼠標縮放
    canvas.onmousewheel = function (event) {
        var lastHeight = eyeHight;
        if (event.wheelDelta > 0) {
            eyeHight = Math.max(1, eyeHight - 80);
        } else {
            eyeHight = eyeHight + 80;
        }

        far = far + eyeHight - lastHeight;
    };
}

function draw(gl, n, aspect, u_MvpMatrix, currentAngle) {
    //模型矩陣
    var modelMatrix = new Matrix4();
    modelMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0); // Rotation around x-axis 
    modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); // Rotation around y-axis    
    modelMatrix.translate(-cx, -cy, -cz);

    //視圖矩陣
    var viewMatrix = new Matrix4();
    viewMatrix.lookAt(0, 0, eyeHight, 0, 0, 0, 0, 1, 0);

    //投影矩陣
    var projMatrix = new Matrix4();
    projMatrix.setPerspective(fovy, aspect, 10, far);

    //模型視圖投影矩陣
    var mvpMatrix = new Matrix4();
    mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);

    // Pass the model view projection matrix to u_MvpMatrix
    gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

    // Clear color and depth buffer
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    // Draw the cube
    gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

function initVertexBuffers(gl) {
    // Create a cube
    //    v6----- v5
    //   /|      /|
    //  v1------v0|
    //  | |     | |
    //  | |v7---|-|v4
    //  |/      |/
    //  v2------v3

    var verticesColors = new Float32Array([
        // Vertex coordinates and color
        maxX, maxY, maxZ, 1.0, 1.0, 1.0,  // v0 White
        minX, maxY, maxZ, 1.0, 0.0, 1.0,  // v1 Magenta
        minX, minY, maxZ, 1.0, 0.0, 0.0,  // v2 Red
        maxX, minY, maxZ, 1.0, 1.0, 0.0,  // v3 Yellow
        maxX, minY, minZ, 0.0, 1.0, 0.0,  // v4 Green
        maxX, maxY, minZ, 0.0, 1.0, 1.0,  // v5 Cyan
        minX, maxY, minZ, 0.0, 0.0, 1.0,  // v6 Blue
        minX, minY, minZ, 1.0, 0.0, 1.0   // v7 Black
    ]);

    // Indices of the vertices
    var indices = new Uint8Array([
        0, 1, 2, 0, 2, 3,    // front
        0, 3, 4, 0, 4, 5,    // right
        0, 5, 6, 0, 6, 1,    // up
        1, 6, 7, 1, 7, 2,    // left
        7, 4, 3, 7, 3, 2,    // down
        4, 7, 6, 4, 6, 5     // back
    ]);

    // Create a buffer object
    var vertexColorBuffer = gl.createBuffer();
    var indexBuffer = gl.createBuffer();
    if (!vertexColorBuffer || !indexBuffer) {
        return -1;
    }

    // Write the vertex coordinates and color to the buffer object
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

    var FSIZE = verticesColors.BYTES_PER_ELEMENT;
    // Assign the buffer object to a_Position and enable the assignment
    var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    if (a_Position < 0) {
        console.log('Failed to get the storage location of a_Position');
        return -1;
    }
    gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
    gl.enableVertexAttribArray(a_Position);
    // Assign the buffer object to a_Color and enable the assignment
    var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
    if (a_Color < 0) {
        console.log('Failed to get the storage location of a_Color');
        return -1;
    }
    gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
    gl.enableVertexAttribArray(a_Color);

    // Write the indices to the buffer object
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

    return indices.length;
}

3) 運行結果

這份代碼改進《WebGL編程指南》一書裏面繪製一個簡單立方體的例子,引用的幾個JS-lib也是該書提供。本例所有源代碼地址連接爲:https://share.weiyun.com/52XmsFv ,密碼:h1lbay。
用chrome打開Cube.html,會出現一個長方體的包圍盒,還能夠用鼠標左鍵旋轉,鼠標滾輪縮放:
canvas

3. 詳細講解

本例的思路是經過JS的requestAnimationFrame()函數不停的調用繪製函數draw(),同時將一些變量關聯到鼠標操做事件和draw(),達到頁面圖形變換的效果。這裏筆者就不講原理,重點講一講設置三個圖形變換的具體過程,網上已經有很是多的原理介紹了。數組

1) 模型變換

在draw()函數中設置模型矩陣:函數

//模型矩陣
var modelMatrix = new Matrix4();
modelMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0); // Rotation around x-axis 
modelMatrix.rotate(currentAngle[1], 0.0, 1.0, 0.0); // Rotation around y-axis    
modelMatrix.translate(-cx, -cy, -cz);

因爲這個包圍盒(長方體)的座標值都很是大,因此第一步須要對其作平移變換translate(-cx, -cy, -cz),cx,cy,cz就是包圍盒的中心:webgl

//包圍盒中心
var cx = (minX + maxX) / 2.0;
var cy = (minY + maxY) / 2.0;
var cz = (minZ + maxZ) / 2.0;

接下來是旋轉變換,數組currentAngle記錄了繞X軸和Y軸旋轉的角度,初始值爲0。配合onmousedown,onmouseup,onmousemove三個鼠標事件,將頁面鼠標X、Y方向的移動,轉換成繞X軸,Y軸的角度值,累計到currentAngle中,從而實現了三維模型隨鼠標旋轉。.net

// Mouse is moved
canvas.onmousemove = function (ev) {
    var x = ev.clientX;
    var y = ev.clientY;
    if (dragging) {
        var factor = 100 / canvas.height; // The rotation ratio
        var dx = factor * (x - lastX);
        var dy = factor * (y - lastY);
        // Limit x-axis rotation angle to -90 to 90 degrees
        //currentAngle[0] = Math.max(Math.min(currentAngle[0] + dy, 90.0), -90.0);
        currentAngle[0] = currentAngle[0] + dy;
        currentAngle[1] = currentAngle[1] + dx;
    }
    lastX = x, lastY = y;
};

注意模型矩陣的平移變換要放後面,須要把座標軸換到包圍盒中心,才能繞三維模型自轉。debug

2) 視圖變換

經過lookAt()函數設置視圖矩陣:

//當前lookAt()函數初始視點的高度
var eyeHight = 2000.0;

// …

//視圖矩陣
var viewMatrix = new Matrix4();
viewMatrix.lookAt(0, 0, eyeHight, 0, 0, 0, 0, 1, 0);

視圖變換調整的是觀察者的狀態,lookAt()函數分別設置了視點、目標觀察點以及上方向。雖然能夠在任何位置去觀察三維場景的點,從而獲得渲染結果。但在實際的應用當中,這個函數設置的結果很不可思議,因此筆者設置成,觀察者站在包圍盒中心上方的位置,對準座標系原點(注意這個時候通過模型變換,包圍盒的中心點已是座標系原點了),常見的Y軸做爲上方向。這樣,視圖內不管如何都是可見的。
這裏將視點的高度設置成變量eyeHight,初始值爲2000,是一個大於0的經驗值。同時經過鼠標的滾輪事件onmousewheel()調整該值,從而實現三維模型的縮放的:

//鼠標縮放
 canvas.onmousewheel = function (event) {
     var lastHeight = eyeHight;
     if (event.wheelDelta > 0) {
         eyeHight = Math.max(1, eyeHight - 80);
     } else {
         eyeHight = eyeHight + 80;
     } 
 };

3) 投影變換

經過setPerspective()來設置投影變換:

//根據視點高度算出setPerspective()函數的合理角度
var fovy = (maxY - minY) / 2.0 / eyeHight;
fovy = 180.0 / Math.PI * Math.atan(fovy) * 2;

//setPerspective()遠截面
var far = 3000;

//setPerspective()寬高比
var aspect = canvas.width / canvas.height;

//...

//投影矩陣
var projMatrix = new Matrix4();
projMatrix.setPerspective(fovy, aspect, 10, far);

前面的視圖變換已經論述了,這個模型是在中心點上方去觀察中心點,至關於視線垂直到前界面near的表面,那麼setPerspective()就能夠肯定其角度fovy了,示意圖以下:

很明顯的看出,當光線射到包圍盒的中心,包圍盒Y方向長度的一半,除以視點高,就是fovy通常的正切值。

寬高比aspect便是頁面canvas元素的寬高比。

近界面near通常設置成較近的值,可是不能太近(好比小於1),不然會影響深度判斷的精度形成頁面閃爍。《OpenGL繪製紋理,縮放相機致使紋理閃爍的解決方法gluPerspective ()》論述了這個問題。

而遠界面far也是須要跟着鼠標滾輪一塊兒變換的,不然當eyeHight變大,三維物體會逐漸離開透視變換的視錐體:

//鼠標縮放
canvas.onmousewheel = function (event) {
    var lastHeight = eyeHight;
    if (event.wheelDelta > 0) {
        eyeHight = Math.max(1, eyeHight - 80);
    } else {
        eyeHight = eyeHight + 80;
    }

    far = far + eyeHight - lastHeight;
};

4) 模型視圖投影矩陣

將三個矩陣都應用起來,就獲得最終的模型視圖投影矩陣。注意計算式是:投影矩陣 * 視圖矩陣 * 模型矩陣:

//模型視圖投影矩陣
var mvpMatrix = new Matrix4();
mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);

4. 存在問題

本例中的三維物體隨着鼠標旋轉,是把鼠標X、Y方向的移動距離轉換成繞X軸,Y軸方向的角度來實現的。可是如何用鼠標實現繞Z軸(第三軸)旋轉呢?例如像OSG這樣的渲染引擎,是能夠用鼠標繞第三個軸旋轉的(固然操做有點費力)。這裏但願你們能批評指正下。

相關文章
相關標籤/搜索