• WebGL或OpenGL关于模型视图投影变换的设置技巧


    1. 具体实例

    看了不少的关于WebGL/OpenGL的资料,笔者发现这些资料在讲解图形变换的时候都讲了很多的原理,然后举出一个特别简单的实例(坐标是1.0,0.5的那种)来讲解。确实一看就懂,但用到实际的场景之中就一脸懵逼了(比如地形的三维坐标都是很大的数字)。所以笔者这里结合一个具体的实例,总结下WebGL/OpenGL中,关于模型变换、视图变换、投影变换的设置技巧。

    绘制任何复杂的场景之前,都可以先绘制出其包围盒,能应用于包围盒的图形变换,基本上就能用于该场景了,因此,笔者这里绘制一幅地形的包围盒。它的最大最小范围为:

    //包围盒范围
    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;
    ' +
        'attribute vec4 a_Color;
    ' +
        'uniform mat4 u_MvpMatrix;
    ' +
        'varying vec4 v_Color;
    ' +
        'void main() {
    ' +
        '  gl_Position = u_MvpMatrix * a_Position;
    ' +
        '  v_Color = a_Color;
    ' +
        '}
    ';
    
    // Fragment shader program
    var FSHADER_SOURCE =
        '#ifdef GL_ES
    ' +
        'precision mediump float;
    ' +
        '#endif
    ' +
        'varying vec4 v_Color;
    ' +
        'void main() {
    ' +
        '  gl_FragColor = v_Color;
    ' +
        '}
    ';
    
    //包围盒范围
    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,会出现一个长方体的包围盒,还可以用鼠标左键旋转,鼠标滚轮缩放:

    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就是包围盒的中心:

    //包围盒中心
    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中,从而实现了三维模型随鼠标旋转。

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

    注意模型矩阵的平移变换要放后面,需要把坐标轴换到包围盒中心,才能绕三维模型自转。

    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这样的渲染引擎,是可以用鼠标绕第三个轴旋转的(当然操作有点费力)。这里希望大家能批评指正下。

  • 相关阅读:
    车标知识学习网页开发,与Flask通过base64展示二进制图片 #华为云·寻找黑马程序员#
    大型情感剧集Selenium:3_元素定位 #华为云·寻找黑马程序员#
    大型情感剧集Selenium:2_options设置 #华为云·寻找黑马程序员#
    【nodejs原理&源码赏析(9)】用node-ssh实现轻量级自动化部署
    大型情感剧集Selenium:1_介绍 #华为云·寻找黑马程序员#
    使用Python开发小说下载器,不再为下载小说而发愁 #华为云·寻找黑马程序员#
    #华为云·寻找黑马程序员#【代码重构之路】如何“消除”if/else
    #华为云·寻找黑马程序员#【代码重构之路】如何优雅的关闭外部资源
    走近深度学习,认识MoXing:初识华为云ModelArts的王牌利器 — MoXing
    开启 J2EE(一)—‘全明星队伍’
  • 原文地址:https://www.cnblogs.com/charlee44/p/10393227.html
Copyright © 2020-2023  润新知