7-进入三维世界

一、学习要点

  1. 以用户视角而进入三维世界
  2. 控制三维可视空间
  3. 裁剪
  4. 处理物体的前后关系
  5. 绘制三维立方体

二、立方体由三角形构成

微信图片_20190426150340.png

12个三角形组成了一个立方体。既然三维物体由三角形组成,其实我们只需要像前面那样,逐个绘制组成物体的三角形就可以了吗?
三维物体与二维物体的一个显著区别:在二维中我们只需要考虑顶点的x y坐标而三维物体我们要考虑它的深度信息(depth information)。 为此我们要研究一下如何定义三维物体世界的观察者:

  • 在什么地方看
  • 朝着那边开
  • 视野有多宽
  • 能看多远

三、视点与视线

我们将观察者所处的位置称为视点,从视点触发沿着观察方向的射线称作视线。

视点、观察模板点和上方向

为了确定观察值的状态,你需要获取两项信息: 视点,即管这种的位置。观察目标点(look-at point),即被观察模板所在的点,他可以用来确定视线。此外 因为我们最后要把观察到的景象会知道屏幕上,还需要知道 上方向。

微信图片_20190426151511.png

  • 视点:观察者所在的三维空间中的位置,视线的起点。视点坐标用(eyeX,eyeY,eyeZ)表示。
  • 观察目标点:被观察目标所在的点。视线从视点触发,穿过观察目标点并继续延伸。坐标表示为(atX,atY,atZ).
  • 上方向:最终绘制在屏幕上的影响中的向上的方向。为了固定观察者防止它绕着视线为轴旋转。用(upX,upY,upZ)表示

微信图片_20190426151958.png

利用上面三个矢量可以创建一个视图矩阵(view matrix)。然后将该矩阵传递顶点着色器。视图矩阵可以表示观察者的状态。 观察者默认状态应该是

  • 视点位于坐标系统的原点(0,0,0)
  • 实现为Z轴负方向 观察点为(0,0,-1)
  • 上方向为Y轴的负方向 (0,1,0)

程序实例: 在三维空间中观察三角形

// 顶点着色器
var VSHANER_SOURCE =
    `
        attribute vec4 a_Position;
        attribute vec4 a_Color;
        uniform mat4 u_ViewMatrix;
        varying vec4 v_Color;
        void main() {
            gl_Position = u_ViewMatrix * a_Position;
            v_Color = a_Color;
        }
    `;
// 片元着色器
var FSHADER_SOURCE =
`
    precision highp float; // 需要声明浮点数精度否则报错
    varying vec4 v_Color;
    void main() {
        gl_FragColor = v_Color;
    }
`
function main() {
    let canvas = document.getElementById('webgl');
    var gl = getWebGLContext(canvas);
    if(!gl){
        console.log('error');
        return;
    }
    // 初始化着色器
    if(!initShaders(gl,VSHANER_SOURCE,FSHADER_SOURCE)){
        console.log('error');
        return;
    }

    var n = initVertexBuffers(gl);
    if(n<0){
        console.log("error");
        return;
    }
    var u_ViewMatrix = gl.getUniformLocation(gl.program,'u_ViewMatrix');
    // 设置视点 视线 和上方向
    var viewMatrix = new Matrix4();
    viewMatrix.setLookAt(0.28,0.25,0.25,0,0,0,0,1,0);
    // 将试图矩阵传给 u_ViewMatrix变量
    gl.uniformMatrix4fv(u_ViewMatrix,false,viewMatrix.elements);
    gl.clearColor(0.0,0.0,0.0,1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES,0,n);
}
function initVertexBuffers(gl) {
    var verticesColors = new Float32Array([
        // 顶点坐标和颜色
        0.0,0.5,-0.4,0.4,1.0,0.4,
        -0.5,-0.5,-0.4,0.4,1.0,0.4,
        0.5,-0.5,-0.4,1.0,0.4,0.4, // 绿色三角形

        0.5,0.4,-0.2,1.0,0.4,0.4,
        -0.5,0.4,-0.2,1.0,1.0,0.4,
        0.0,-0.6,-0.2,1.0,1.0,0.4, // 黄色三角形

        0.0,0.5,0.0,0.4,0.4,1.0, // 蓝色三角形
        -0.5,0.5,0.0,0.4,0.4,1.0,
        0.5,-0.5,0.0,1.0,0.4,0.4
    ])
    var n = 9;
    // 创建缓冲区对象
    var vertexColorbuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER,vertexColorbuffer);
    gl.bufferData(gl.ARRAY_BUFFER,verticesColors,gl.STATIC_DRAW);
    var FSIZE = verticesColors.BYTES_PER_ELEMENT;
    var a_Position = gl.getAttribLocation(gl.program,'a_Position');
    // 将缓冲区对象分配给 a_Position变量
    gl.vertexAttribPointer(a_Position,3,gl.FLOAT,false,FSIZE * 6,0);
    gl.enableVertexAttribArray(a_Position);
    var a_Color = gl.getAttribLocation(gl.program,'a_Color');
    gl.vertexAttribPointer(a_Color,3,gl.FLOAT,false,FSIZE * 6,FSIZE * 3);
    gl.enableVertexAttribArray(a_Color);
    return n;
}

视图矩阵与顶点矩阵相乘达到增加观察者的效果。 事实上 根据自定义的观察者状态 绘制观察者看到的景象 与 使用默认的观察状态,但是对三维对象进行评议 旋转等变化 在绘制观察者看到景象 这两种行为是等价的。

微信图片_20190426163510.png

视点移动的方向与被观察对象(也就是整个世界)移动的方向正好相反。对于视点的旋转也可以采用类似的方式。

四 从指定视点观察旋转后的三角形

我们需要两个矩阵: 旋转矩阵(表示三角形的旋转) 和 视图矩阵(表示观察世界的方式)。首先的问题是以怎么的顺序相乘两个矩阵。 我们需要先旋转三角形,然后从这个视点观察他。所以要先对三角形进行旋转变换,在对旋转后的三角形进行与“移动视点”等效的变化。我们按照上述顺序相乘矩阵。

  • <旋转后顶点坐标> = <旋转矩阵> x <原始顶点坐标>
  • <“从视点看上去” 的旋转后顶点坐标> = <视图矩阵> x <旋转后顶点坐标>

模型矩阵:使用平移 缩放等基本变换或他们的组合组成的矩阵

  • <“从视点看上去” 的旋转后顶点坐标> = <视图矩阵> x <模型矩阵> x <原始顶点坐标>
// 顶点着色器
var VSHANER_SOURCE =
    `
        attribute vec4 a_Position;
        attribute vec4 a_Color;
        uniform mat4 u_ViewMatrix;
        uniform mat4 u_ModelMatrix; // 增加了模型矩阵
        varying vec4 v_Color;
        void main() {
            gl_Position = u_ViewMatrix * u_ModelMatrix * a_Position;
            v_Color = a_Color;
        }
    `;
// 片元着色器
var FSHADER_SOURCE =
`
    precision highp float; // 需要声明浮点数精度否则报错
    varying vec4 v_Color;
    void main() {
        gl_FragColor = v_Color;
    }
`
function main() {
    let canvas = document.getElementById('webgl');
    var gl = getWebGLContext(canvas);
    if(!gl){
        console.log('error');
        return;
    }
    // 初始化着色器
    if(!initShaders(gl,VSHANER_SOURCE,FSHADER_SOURCE)){
        console.log('error');
        return;
    }

    var n = initVertexBuffers(gl);
    if(n<0){
        console.log("error");
        return;
    }
    var u_ViewMatrix = gl.getUniformLocation(gl.program,'u_ViewMatrix');
    // 设置视点 视线 和上方向
    var viewMatrix = new Matrix4();
    viewMatrix.setLookAt(0.28,0.25,0.25,0,0,0,0,1,0);
    // 将试图矩阵传给 u_ViewMatrix变量
    gl.uniformMatrix4fv(u_ViewMatrix,false,viewMatrix.elements);

    // 模型矩阵赋值
    var u_ModelMatrix = gl.getUniformLocation(gl.program,'u_ModelMatrix');
    var modelMatrix = new Matrix4();
    modelMatrix.setRotate(-10,0,0,1);
    gl.uniformMatrix4fv(u_ModelMatrix,false,modelMatrix.elements);


    gl.clearColor(0.0,0.0,0.0,1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES,0,n);
}

function initVertexBuffers(gl) {
    var verticesColors = new Float32Array([
        // 顶点坐标和颜色
        0.0,0.5,-0.4,0.4,1.0,0.4,
        -0.5,-0.5,-0.4,0.4,1.0,0.4,
        0.5,-0.5,-0.4,1.0,0.4,0.4, // 绿色三角形

        0.5,0.4,-0.2,1.0,0.4,0.4,
        -0.5,0.4,-0.2,1.0,1.0,0.4,
        0.0,-0.6,-0.2,1.0,1.0,0.4, // 黄色三角形

        0.0,0.5,0.0,0.4,0.4,1.0, // 蓝色三角形
        -0.5,0.5,0.0,0.4,0.4,1.0,
        0.5,-0.5,0.0,1.0,0.4,0.4
    ])

    var n = 9;
    // 创建缓冲区对象
    var vertexColorbuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER,vertexColorbuffer);
    gl.bufferData(gl.ARRAY_BUFFER,verticesColors,gl.STATIC_DRAW);
    var FSIZE = verticesColors.BYTES_PER_ELEMENT;
    var a_Position = gl.getAttribLocation(gl.program,'a_Position');
    // 将缓冲区对象分配给 a_Position变量
    gl.vertexAttribPointer(a_Position,3,gl.FLOAT,false,FSIZE * 6,0);
    gl.enableVertexAttribArray(a_Position);
    var a_Color = gl.getAttribLocation(gl.program,'a_Color');
    gl.vertexAttribPointer(a_Color,3,gl.FLOAT,false,FSIZE * 6,FSIZE * 3);
    gl.enableVertexAttribArray(a_Color);
    return n;
}

这样的写法 程序对于每个顶点都要计算 视图矩阵 x 模型矩阵 这是不必要的性能开销。我们可以现在外面吧这两个矩阵相乘结果算出来 在给顶点着色器 这两个矩阵相乘得到的结果成为 模型视图矩阵(model view matrix)

var  modelViewMatrix = viewMatrix.multiply(modelMatrix);

// 实际上只需要一行代码就可以计算出模型视图矩阵了
var modelViewMatrix = new Matrix4();
modelViewMatrix.setLookAt(0.20,0.25,0.25,0,0,0,0,1,0).rotate(-10,0,0,1);

五、利用键盘改变视点

// 顶点着色器
var VSHANER_SOURCE =
    `
        attribute vec4 a_Position;
        attribute vec4 a_Color;
        uniform mat4 u_ViewMatrix;
        varying vec4 v_Color;
        void main() {
            gl_Position = u_ViewMatrix * a_Position;
            v_Color = a_Color;
        }
    `;
// 片元着色器
var FSHADER_SOURCE =
`
    precision highp float; // 需要声明浮点数精度否则报错
    varying vec4 v_Color;
    void main() {
        gl_FragColor = v_Color;
    }
`
function main() {
    let canvas = document.getElementById('webgl');
    var gl = getWebGLContext(canvas);
    if(!gl){
        console.log('error');
        return;
    }
    // 初始化着色器
    if(!initShaders(gl,VSHANER_SOURCE,FSHADER_SOURCE)){
        console.log('error');
        return;
    }

    var n = initVertexBuffers(gl);
    if(n<0){
        console.log("error");
        return;
    }
    var u_ViewMatrix = gl.getUniformLocation(gl.program,'u_ViewMatrix');
    // 设置视点 视线 和上方向
    var viewMatrix = new Matrix4();
    // 注册键盘事件响应函数
    document.onkeydown = function (ev) {
        keydown(ev,gl,n,u_ViewMatrix,viewMatrix);
    };
    draw(gl,n,u_ViewMatrix,viewMatrix);
}

function initVertexBuffers(gl) {
    var verticesColors = new Float32Array([
        // 顶点坐标和颜色
        0.0,0.5,-0.4,0.4,1.0,0.4,
        -0.5,-0.5,-0.4,0.4,1.0,0.4,
        0.5,-0.5,-0.4,1.0,0.4,0.4, // 绿色三角形

        0.5,0.4,-0.2,1.0,0.4,0.4,
        -0.5,0.4,-0.2,1.0,1.0,0.4,
        0.0,-0.6,-0.2,1.0,1.0,0.4, // 黄色三角形

        0.0,0.5,0.0,0.4,0.4,1.0, // 蓝色三角形
        -0.5,0.5,0.0,0.4,0.4,1.0,
        0.5,-0.5,0.0,1.0,0.4,0.4
    ])

    var n = 9;
    // 创建缓冲区对象
    var vertexColorbuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER,vertexColorbuffer);
    gl.bufferData(gl.ARRAY_BUFFER,verticesColors,gl.STATIC_DRAW);
    var FSIZE = verticesColors.BYTES_PER_ELEMENT;
    var a_Position = gl.getAttribLocation(gl.program,'a_Position');
    // 将缓冲区对象分配给 a_Position变量
    gl.vertexAttribPointer(a_Position,3,gl.FLOAT,false,FSIZE * 6,0);
    gl.enableVertexAttribArray(a_Position);
    var a_Color = gl.getAttribLocation(gl.program,'a_Color');
    gl.vertexAttribPointer(a_Color,3,gl.FLOAT,false,FSIZE * 6,FSIZE * 3);
    gl.enableVertexAttribArray(a_Color);
    return n;
}


var g_eyeX = 0.20,g_eyeY=0.25,g_eyeZ=0.25;
function keydown(ev,gl,n,u_ViewMatrix,viewMatrix) {
    if(ev.keyCode == 39){
        // 按下右键
        g_eyeX += 0.01;
    }else if(ev.keyCode == 37){
        // 按下左键
        g_eyeX -= 0.01;
    }else{
        //按下其他键
        return ;
    }
    draw(gl,n,u_ViewMatrix,viewMatrix)
}


function draw(gl,n,u_ViewMatrix,viewMatrix) {
    // 设置视点和视线
    viewMatrix.setLookAt(g_eyeX,g_eyeY,g_eyeZ,0,0,0,0,1,0);
    // 将试图矩阵传给 u_ViewMatrix变量
    gl.uniformMatrix4fv(u_ViewMatrix,false,viewMatrix.elements);
    gl.clearColor(0.0,0.0,0.0,1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES,0,n);
}

六、可视范围(正射类型)

只有当三维物体卡再可视范围内时 webGL才会绘制它。

  • 可视空间分为两类
  1. 长方形可视空间,也成盒装空间,由正射投影(orthographic projection)产生。
  2. 四棱锥/ 金字塔可视空间,由透视投影(perspective projection)产生。

基于正射投影的盒装可视空间的工作原理:盒装可视空间的由前后两个矩形表面确定,分别称为近裁剪面和远裁剪面。

微信图片_20190426175430.png

定义盒装可视空间 cuon-matrix.js中提供了Matrix4.setOrtho()方法用来设置投影矩阵,定义盒装可视空间。 微信图片_20190426175701.png 通过示例程序展示正射投影矩阵的使用方法和效果。

    // 可以使用方向键操作投影盒子的近裁剪面 和 远裁剪面

    // 顶点着色器
var VSHANER_SOURCE =
    `
        attribute vec4 a_Position;
        attribute vec4 a_Color;
        uniform mat4 u_ProjMatrix;
        varying vec4 v_Color;
        void main() {
            gl_Position = u_ProjMatrix * a_Position;
            v_Color = a_Color;
        }
    `;
// 片元着色器
var FSHADER_SOURCE =
`
    precision highp float; // 需要声明浮点数精度否则报错
    varying vec4 v_Color;
    void main() {
        gl_FragColor = v_Color;
    }
`
function main() {
    let canvas = document.getElementById('webgl');
    // 获取nearFar元素
    var nf = document.getElementById('nearFar');
    var gl = getWebGLContext(canvas);
    if(!gl){
        console.log('error');
        return;
    }
    // 初始化着色器
    if(!initShaders(gl,VSHANER_SOURCE,FSHADER_SOURCE)){
        console.log('error');
        return;
    }

    var n = initVertexBuffers(gl);
    if(n<0){
        console.log("error");
        return;
    }
    var u_ProjMatrix = gl.getUniformLocation(gl.program,'u_ProjMatrix');
    // 设置视点 视线 和上方向
    var projMatrix = new Matrix4();
    // 注册键盘事件响应函数
    document.onkeydown = function (ev) {
        keydown(ev,gl,n,u_ProjMatrix,projMatrix,nf);
    };
    draw(gl,n,u_ProjMatrix,projMatrix,nf);
}
// 视点与 近 远裁剪面的距离
var g_near = 0.0, g_far = 0.5;
function keydown(ev,gl,n,u_ProjMatrix,projMatrix,nf) {
    switch (ev.keyCode){
        case 39: g_near +=0.01;break; //右方向
        case 37: g_near -=0.01;break; //左方向
        case 38: g_far +=0.01;break; //上方向
        case 40: g_far -=0.01;break; //下方向
        default: return;
    }
    draw(gl,n,u_ProjMatrix,projMatrix,nf);
}

function draw(gl,n,u_ProjMatrix,projMatrix,nf) {
    projMatrix.setOrtho(-1,1,-1,1,g_near,g_far);
    // 将投影矩阵传递给u_ProjMatrix变量
    gl.uniformMatrix4fv(u_ProjMatrix,false,projMatrix.elements);
    gl.clearColor(0.0,0.0,0.0,1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    nf.innerHTML = 'near:'+Math.round(g_near*100)/100 + ', far:'+Math.round(g_far*100)/100;
    gl.drawArrays(gl.TRIANGLES,0,n);
}

改变 projMatrix.setOrtho(-0.3,0.3,-1.0,1.0,g_near,g_far); 可以投影出不同的三角形

七、可视空间(透视投影)

使用透视空间可以使大的画出的图形具有深度感。从图上可以看出随着Z轴的变化 相同大小的三角形会越来越小 微信图片_20190430172702.png

透视投影可视空间的定义:

微信图片_20190430173011.png

微信图片_20190430173021.png

采用setPerspective()函数定义透视投影矩阵。 示例程序如下

// 顶点着色器
var VSHANER_SOURCE =
    `
        attribute vec4 a_Position;
        attribute vec4 a_Color;
        uniform mat4 u_ViewMatrix;
        uniform mat4 u_ProMatrix;
        varying vec4 v_Color;
        void main() {
            gl_Position =u_ProMatrix *  u_ViewMatrix * a_Position;
            v_Color = a_Color;
        }
    `;
// 片元着色器
var FSHADER_SOURCE =
`
    precision highp float; // 需要声明浮点数精度否则报错
    varying vec4 v_Color;
    void main() {
        gl_FragColor = v_Color;
    }
`
function main() {
    let canvas = document.getElementById('webgl');
    var gl = getWebGLContext(canvas);
    if(!gl){
        console.log('error');
        return;
    }
    // 初始化着色器
    if(!initShaders(gl,VSHANER_SOURCE,FSHADER_SOURCE)){
        console.log('error');
        return;
    }

    var n = initVertexBuffers(gl);
    if(n<0){
        console.log("error");
        return;
    }
    var u_ViewMatrix = gl.getUniformLocation(gl.program,'u_ViewMatrix');
    var u_ProMatrix = gl.getUniformLocation(gl.program,'u_ProMatrix');
    // 设置视点 视线 和上方向
    var viewMatrix = new Matrix4();
    // 投影矩阵
    var projMatrix = new Matrix4();
    // 计算视图矩阵和投影矩阵
    viewMatrix.setLookAt(0,0,5,0,0,-100,0,1,0);
    //第二个参数aspect宽高比应该与 canvas保持一致,我们根据canvas的 width和height属性来计算出该参数。从而使得canvas大小发生变化也不会导致显示出来的图变形
    projMatrix.setPerspective(30,canvas.width/canvas.height,1,100);
    // 将试图矩阵和投影矩阵传递给参数
    gl.uniformMatrix4fv(u_ViewMatrix,false,viewMatrix.elements);
    gl.uniformMatrix4fv(u_ProMatrix,false,projMatrix.elements);
    gl.clearColor(0.0,0.0,0.0,1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES,0,n);
}

function initVertexBuffers(gl) {
    var verticesColors = new Float32Array([
        // 右侧的3个三角形
        0.75,1.0,-4.0,0.4,1.0,0.4,
        0.25,-1.0,-4.0,0.4,1.0,0.4,
        1.25,-1.0,-4.0,1.0,0.4,0.4,

        0.75,1.0,-2.0,1.0,1.0,0.4,
        0.25,-1.0,-2.0,1.0,1.0,0.4,
        1.25,-1.0,-2.0,1.0,0.4,0.4,

        0.75,1.0,0.0,0.4,0.4,1.0,
        0.25,-1.0,0.0,0.4,0.4,1.0,
        1.25,-1.0,0.0,1.0,0.4,0.4,

        // 左侧的3个三角形
        -0.75,1.0,-4.0,0.4,1.0,0.4,
        -1.25,-1.0,-4.0,0.4,1.0,0.4,
        -0.25,-1.0,-4.0,1.0,0.4,0.4,

        -0.75,1.0,-2.0,1.0,1.0,0.4,
        -1.25,-1.0,-2.0,1.0,1.0,0.4,
        -0.25,-1.0,-2.0,1.0,0.4,0.4,

        -0.75,1.0,0.0,0.4,0.4,1.0,
        -1.25,-1.0,0.0,0.4,0.4,1.0,
        -0.25,-1.0,0.0,1.0,0.4,0.4,

    ])

    var n = 18;
    // 创建缓冲区对象
    var vertexColorbuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER,vertexColorbuffer);
    gl.bufferData(gl.ARRAY_BUFFER,verticesColors,gl.STATIC_DRAW);
    var FSIZE = verticesColors.BYTES_PER_ELEMENT;
    var a_Position = gl.getAttribLocation(gl.program,'a_Position');
    // 将缓冲区对象分配给 a_Position变量
    gl.vertexAttribPointer(a_Position,3,gl.FLOAT,false,FSIZE * 6,0);
    gl.enableVertexAttribArray(a_Position);
    var a_Color = gl.getAttribLocation(gl.program,'a_Color');
    gl.vertexAttribPointer(a_Color,3,gl.FLOAT,false,FSIZE * 6,FSIZE * 3);
    gl.enableVertexAttribArray(a_Color);
    return n;
}

投影矩阵的作用(矩阵为什么可以定义可是空间)

  • 我们看到三角形看上去变小了,也在被不同程度的想着中心新(视线)平移,使得看上去在视线的左右排成了两列。实际上这些三角形完全是相同的,透视矩阵对三小型进行了两次变换:
    (1) 根据三小型与视点的距离,按比例对三角形进行了缩小变换;
    (2) 对三角形进行偏移变换。

这表明可是空间的规范可以用一系列基本变换(如平移,缩放)来定义。

一个模型矩阵,视图矩阵和投影矩阵的综合例子

我们上面敲了六个三角形的顶点,真是太繁琐了,如何能比较高效的实现呢?

  1. 沿着Z轴准备三个三角形的顶点数据
  2. 将其沿着X轴正方向平移0.75单位,绘制这些三角形
  3. 将其沿着X轴负方向平移0.75单位,绘制这些三角形
// 顶点着色器
var VSHANER_SOURCE =
    `
        attribute vec4 a_Position;
        attribute vec4 a_Color;
        uniform mat4 u_ModelMatrix;
        uniform mat4 u_ViewMatrix;
        uniform mat4 u_ProMatrix;
        varying vec4 v_Color;
        void main() {
            gl_Position =u_ProMatrix *  u_ViewMatrix * u_ModelMatrix * a_Position;
            v_Color = a_Color;
        }
    `;
// 片元着色器
var FSHADER_SOURCE =
`
    precision highp float; // 需要声明浮点数精度否则报错
    varying vec4 v_Color;
    void main() {
        gl_FragColor = v_Color;
    }
`
function main() {
    let canvas = document.getElementById('webgl');
    var gl = getWebGLContext(canvas);
    if(!gl){
        console.log('error');
        return;
    }
    // 初始化着色器
    if(!initShaders(gl,VSHANER_SOURCE,FSHADER_SOURCE)){
        console.log('error');
        return;
    }

    var n = initVertexBuffers(gl);
    if(n<0){
        console.log("error");
        return;
    }
    var u_ModelMatrix = gl.getUniformLocation(gl.program,'u_ModelMatrix');
    var u_ViewMatrix = gl.getUniformLocation(gl.program,'u_ViewMatrix');
    var u_ProMatrix = gl.getUniformLocation(gl.program,'u_ProMatrix');
    // 模型矩阵
    var modelMatrix = new Matrix4();

    // 设置视点 视线 和上方向
    var viewMatrix = new Matrix4();
    // 投影矩阵
    var projMatrix = new Matrix4();
    // 计算视图矩阵和投影矩阵
    modelMatrix.setTranslate(0.75,0,0);// 平移0.75单位
    viewMatrix.setLookAt(0,0,5,0,0,-200,0,1,0);
    projMatrix.setPerspective(30,canvas.width/canvas.height,1,100);
    // 将试图矩阵和投影矩阵传递给参数
    gl.uniformMatrix4fv(u_ModelMatrix,false,modelMatrix.elements);
    gl.uniformMatrix4fv(u_ViewMatrix,false,viewMatrix.elements);
    gl.uniformMatrix4fv(u_ProMatrix,false,projMatrix.elements);
    gl.clearColor(0.0,0.0,0.0,1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES,0,n);

    modelMatrix.setTranslate(-0.75,0,0);
    gl.uniformMatrix4fv(u_ModelMatrix,false,modelMatrix.elements);
    gl.drawArrays(gl.TRIANGLES,0,n);

}

function initVertexBuffers(gl) {
    var verticesColors = new Float32Array([
        0.0,1.0,-4.0,0.4,1.0,0.4,
        -0.5,-1.0,-4.0,0.4,1.0,0.4,
        0.5,-1.0,-4.0,1.0,0.4,0.4,

        0.0,1.0,-2.0,1.0,1.0,0.4,
        -0.5,-1.0,-2.0,1.0,1.0,0.4,
        0.5,-1.0,-2.0,1.0,0.4,0.4,

        0.0,1.0,0.0,0.4,0.4,1.0,
        -0.5,-1.0,0.0,0.4,0.4,1.0,
        0.5,-1.0,0.0,1.0,0.4,0.4,

    ])

    var n = 9;
    // 创建缓冲区对象
    var vertexColorbuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER,vertexColorbuffer);
    gl.bufferData(gl.ARRAY_BUFFER,verticesColors,gl.STATIC_DRAW);
    var FSIZE = verticesColors.BYTES_PER_ELEMENT;
    var a_Position = gl.getAttribLocation(gl.program,'a_Position');
    // 将缓冲区对象分配给 a_Position变量
    gl.vertexAttribPointer(a_Position,3,gl.FLOAT,false,FSIZE * 6,0);
    gl.enableVertexAttribArray(a_Position);
    var a_Color = gl.getAttribLocation(gl.program,'a_Color');
    gl.vertexAttribPointer(a_Color,3,gl.FLOAT,false,FSIZE * 6,FSIZE * 3);
    gl.enableVertexAttribArray(a_Color);
    return n;
}

也可以在js里面 mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);用一个矩阵的相乘 提前就算出模型视图投影矩阵(model view projection matrix) 这样可以优化性能。

八、正确处理对象的前后关系

webgl 在默认的情况下按照缓冲区中的顺序绘制图形,而且后悔值的图形覆盖先绘制的图形,这是为了提高效率。如果视点不动的话提前定义图形的顺序是没有问题的,但是如果从不同的角度看物体,那就不能这样设定。

隐藏面消除(hidden surface removal)

为了解决这个问题,webgl提供了隐藏慢消除的功能。开启这一功能需要两步。

  1. 开始隐藏面消除功能 gl.enable(gl.DEPTH_TEST);
  2. 在绘制之前清除深度缓冲区 gl.clear(gl.DEPTH_BUFFER_BIT);

深度缓冲区的租用是 在片元着色器将图形绘制到颜色缓冲区上之前,如果开启了隐藏面消除,则要进行深度检测(判断每个像素的深度,来决定是否将这个像素画出来),深度缓冲区用来存储像素的深度信息。 (如果要同时清除深度缓冲区和颜色缓冲区 可以使用 ‘|’ 符号进行连接)

// 顶点着色器
var VSHANER_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;
        }
    `;
// 片元着色器
var FSHADER_SOURCE =
`
    precision highp float; // 需要声明浮点数精度否则报错
    varying vec4 v_Color;
    void main() {
        gl_FragColor = v_Color;
    }
`
function main() {
    let canvas = document.getElementById('webgl');
    var gl = getWebGLContext(canvas);
    if(!gl){
        console.log('error');
        return;
    }
    // 初始化着色器
    if(!initShaders(gl,VSHANER_SOURCE,FSHADER_SOURCE)){
        console.log('error');
        return;
    }

    var n = initVertexBuffers(gl);
    if(n<0){
        console.log("error");
        return;
    }
    var u_MvpMatrix = gl.getUniformLocation(gl.program,'u_MvpMatrix');

    // 模型矩阵
    var modelMatrix = new Matrix4();

    // 设置视点 视线 和上方向
    var viewMatrix = new Matrix4();
    // 投影矩阵
    var projMatrix = new Matrix4();
    // 混合投影
    var mvpMatrix = new Matrix4();


    // 计算视图矩阵和投影矩阵
    modelMatrix.setTranslate(0.75,0,0);// 平移0.75单位
    viewMatrix.setLookAt(0,0,5,0,0,-200,0,1,0);
    projMatrix.setPerspective(30,canvas.width/canvas.height,1,100);

    mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);

    // 将试图矩阵和投影矩阵传递给参数
    gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);

    gl.clearColor(0.0,0.0,0.0,1.0);
    // 开启隐藏面消除(注释掉这句话试试???)
    gl.enable(gl.DEPTH_TEST);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES,0,n);

    modelMatrix.setTranslate(-0.75,0,0);
    mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
    gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
    gl.drawArrays(gl.TRIANGLES,0,n);

}

function initVertexBuffers(gl) {
    var verticesColors = new Float32Array([
        0.0,1.0,0.0,0.4,1.0,0.4,
        -0.5,-1.0,0.0,0.4,1.0,0.4,
        0.5,-1.0,0.0,1.0,0.4,0.4,

        0.0,1.0,-2.0,1.0,1.0,0.4,
        -0.5,-1.0,-2.0,1.0,1.0,0.4,
        0.5,-1.0,-2.0,1.0,0.4,0.4,

        0.0,1.0,-4.0,0.4,0.4,1.0,
        -0.5,-1.0,-4.0,0.4,0.4,1.0,
        0.5,-1.0,-4.0,1.0,0.4,0.4,

    ])

    var n = 9;
    // 创建缓冲区对象
    var vertexColorbuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER,vertexColorbuffer);
    gl.bufferData(gl.ARRAY_BUFFER,verticesColors,gl.STATIC_DRAW);
    var FSIZE = verticesColors.BYTES_PER_ELEMENT;
    var a_Position = gl.getAttribLocation(gl.program,'a_Position');
    // 将缓冲区对象分配给 a_Position变量
    gl.vertexAttribPointer(a_Position,3,gl.FLOAT,false,FSIZE * 6,0);
    gl.enableVertexAttribArray(a_Position);
    var a_Color = gl.getAttribLocation(gl.program,'a_Color');
    gl.vertexAttribPointer(a_Color,3,gl.FLOAT,false,FSIZE * 6,FSIZE * 3);
    gl.enableVertexAttribArray(a_Color);
    return n;
}

深度冲突

当几何图形或者物体的两个表面极为接近时就会出现新的问题,表面看上去斑驳不堪,这种现行成为深度冲突(修改上面代码三角形的Z轴的值改成一样的 可以看到类似效果)。

  • webgl提供一种被成为多边形偏移(polygon offset)的机制来解决这个问题。该机制自动的在Z职上加一个偏移量,偏移量的值由物体表面相对于观察者实现的角度来确定。实现这一机制需要两步:
  1. 启用多边形偏移 gl.enable(gl.POLYGON_OFFSET_FILL);
  2. 在绘制执勤啊指定用来计算偏移量的参数 gl.polygonOffset(1.0,1.0);

有些浏览器会自动处理深度冲突问题

九、 立方体

之前我们都是通过绘制简单的三角形展示Webgl的诸多特性,下面我们来真正的进入立方体的绘制。之前我们使用gl.drawArrays()方法进行绘制,如果使用这个方法我们需要绘制两个三小型成为一个矩形表面,然后绘制出所有的面组成立方体。这种方法需要12个三角形 也就是36个顶点。但是实际上一个正方体只需要8个顶点。所以这种方法显然是低效的。webgl提供了一个新的函数 gl.drawElements(),来避免重复的定氮仪顶点,保持顶点数量最小。 微信图片_20190507100107.png

我们要将三角形列表传入到gl.ELEMENT_ARRAY_BUFFER中,它管理者具有素银结构的三维模型数据。 下图是正方体的索引结构的三维模型数据。

微信图片_20190507100625.png

  // 第一个立方体
  // 顶点着色器
var VSHANER_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;
        }
    `;
// 片元着色器
var FSHADER_SOURCE =
`
    precision highp float; // 需要声明浮点数精度否则报错
    varying vec4 v_Color;
    void main() {
        gl_FragColor = v_Color;
    }
`
function main() {
    let canvas = document.getElementById('webgl');
    var gl = getWebGLContext(canvas);
    if(!gl){
        console.log('error');
        return;
    }
    // 初始化着色器
    if(!initShaders(gl,VSHANER_SOURCE,FSHADER_SOURCE)){
        console.log('error');
        return;
    }

    var n = initVertexBuffers(gl);
    if(n<0){
        console.log("error");
        return;
    }
    gl.clearColor(0.0,0.0,0.0,1.0);
    // 开启隐藏面消除
    gl.enable(gl.DEPTH_TEST);

    var u_MvpMatrix = gl.getUniformLocation(gl.program,'u_MvpMatrix');
    // 混合投影
    var mvpMatrix = new Matrix4();
    // 设置视点 视线 和上方向
    var viewMatrix = new Matrix4();
    // 投影矩阵
    var projMatrix = new Matrix4();
    viewMatrix.setLookAt(3,3,7,0,0,0,0,1,0);
    projMatrix.setPerspective(30,canvas.width/canvas.height,1,100);
    mvpMatrix.set(projMatrix).multiply(viewMatrix);
    // 将试图矩阵和投影矩阵传递给参数
    gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
    // 开启偏移
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.drawElements(gl.TRIANGLES,n,gl.UNSIGNED_BYTE,0);

}

function initVertexBuffers(gl) {
    var verticesColors = new Float32Array([
        // 立方体的顶点坐标和颜色
        1.0,1.0,1.0,1.0,1.0,1.0,
        -1.0,1.0,1.0,1.0,0.0,1.0,
        -1.0,-1.0,1.0,1.0,0.0,0.0,
        1.0,-1.0,1.0,1.0,1.0,0.0,
        1.0,-1.0,-1.0,0.0,1.0,0.0,
        1.0,1.0,-1.0,0.0,1.0,1.0,
        -1.0,1.0,-1.0,0.0,0.0,1.0,
        -1.0,-1.0,-1.0,0.0,0.0,0.0
    ])

    // 顶点索引
    var indices = new Uint8Array([
        0,1,2,0,2,3, // 前
        0,3,4,0,4,5, // 右
        0,5,6,0,6,1, // 上
        1,6,7,1,7,2, // 左
        7,4,3,7,3,2, // 下
        4,7,6,4,6,5  // 后
    ])
    // 创建缓冲区对象
    var vertexColorbuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER,vertexColorbuffer);
    gl.bufferData(gl.ARRAY_BUFFER,verticesColors,gl.STATIC_DRAW);
    var FSIZE = verticesColors.BYTES_PER_ELEMENT;
    var a_Position = gl.getAttribLocation(gl.program,'a_Position');
    // 将缓冲区对象分配给 a_Position变量
    gl.vertexAttribPointer(a_Position,3,gl.FLOAT,false,FSIZE * 6,0);
    gl.enableVertexAttribArray(a_Position);
    var a_Color = gl.getAttribLocation(gl.program,'a_Color');
    gl.vertexAttribPointer(a_Color,3,gl.FLOAT,false,FSIZE * 6,FSIZE * 3);
    gl.enableVertexAttribArray(a_Color);
    // 主要区别点
     var indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW);
    return indices.length;
}

这个方法通过循环利用顶点信息,控制内存的开销,但是代价是你需要通过索引来简介的访问顶点,使得程序复杂化了。下图展示了使用索引缓冲区后webgl的内部状态。 微信图片_20190507105120.png

十、为每个面指定颜色

我们知道 如果想指定颜色,必须要将颜色定义为逐顶点信息,并传入到顶点着色器中,也就是说你想让一个面为蓝色,那这个面的四个顶点都必须为蓝色。但是我们的顶点是公用的,如果改变一个面的四个顶点为蓝色,那其他面也会染上蓝色。这不是我们想要。那如何解决呢

  • 解决方法:创建多个具有相同顶点坐标的顶点,其实就是允余的定义顶点。如下图 微信图片_20190507105826.png
    // 顶点着色器
var VSHANER_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;
        }
    `;
// 片元着色器
var FSHADER_SOURCE =
`
    precision highp float; // 需要声明浮点数精度否则报错
    varying vec4 v_Color;
    void main() {
        gl_FragColor = v_Color;
    }
`
function main() {
    let canvas = document.getElementById('webgl');
    var gl = getWebGLContext(canvas);
    if(!gl){
        console.log('error');
        return;
    }
    // 初始化着色器
    if(!initShaders(gl,VSHANER_SOURCE,FSHADER_SOURCE)){
        console.log('error');
        return;
    }

    var n = initVertexBuffers(gl);
    if(n<0){
        console.log("error");
        return;
    }
    gl.clearColor(0.0,0.0,0.0,1.0);
    // 开启隐藏面消除
    gl.enable(gl.DEPTH_TEST);

    var u_MvpMatrix = gl.getUniformLocation(gl.program,'u_MvpMatrix');
    // 混合投影
    var mvpMatrix = new Matrix4();
    // 设置视点 视线 和上方向
    var viewMatrix = new Matrix4();
    // 投影矩阵
    var projMatrix = new Matrix4();
    viewMatrix.setLookAt(3,3,7,0,0,0,0,1,0);
    projMatrix.setPerspective(30,canvas.width/canvas.height,1,100);
    mvpMatrix.set(projMatrix).multiply(viewMatrix);
    // 将试图矩阵和投影矩阵传递给参数
    gl.uniformMatrix4fv(u_MvpMatrix,false,mvpMatrix.elements);
    // 开启偏移
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.drawElements(gl.TRIANGLES,n,gl.UNSIGNED_BYTE,0);

}
function initVertexBuffers(gl) {
    let v0  = [1.0,1.0,1.0];
    let v1  = [-1.0,1.0,1.0];
    let v2  = [-1.0,-1.0,1.0];
    let v3  = [1.0,-1.0,1.0];
    let v4  = [1.0,-1.0,-1.0];
    let v5  = [1.0,1.0,-1.0];
    let v6  = [-1.0,1.0,-1.0];
    let v7  = [-1.0,-1.0,-1.0];
    // 这个坐标为什么是这样 对照上图慢慢理解一下
    var vertices = new Float32Array([
        // 立方体的顶点坐标和颜色
        ...v0,...v1,...v2,...v3,
        ...v0,...v3,...v4,...v5,
        ...v0,...v5,...v6,...v1,
        ...v1,...v6,...v7,...v2,
        ...v2,...v3,...v4,...v7,
        ...v4,...v7,...v6,...v5
    ])
    // 顶点索引
    var indices = new Uint8Array([
        0,1,2,0,2,3, // 前
        4,5,6,4,6,7, // 右
        8,9,10,8,10,11, // 上
        12,13,14,12,14,15, // 左
        16,17,18,16,18,19, // 下
        20,21,22,20,22,23 // 后
    ])
    var color = new Float32Array([
        0.4,0.4,1.0,0.4,0.4,1.0,0.4,0.4,1.0,0.4,0.4,1.0,
        0.4,1.0,0.4,0.4,1.0,0.4,0.4,1.0,0.4,0.4,1.0,0.4,
        1.0,0.4,0.4, 1.0,0.4,0.4, 1.0,0.4,0.4, 1.0,0.4,0.4,
        0.4,0.4,0.4,0.4,0.4,0.4,0.4,0.4,0.4,0.4,0.4,0.4,
        1.0,1.0,1.0, 1.0,1.0,1.0, 1.0,1.0,1.0, 1.0,1.0,1.0,
        0.4,1.0,1.0,0.4,1.0,1.0,0.4,1.0,1.0,0.4,1.0,1.0
    ])
    // 创建缓冲区对象
    var indexBuffer = gl.createBuffer();
    if(!initArrayBuffer(gl,vertices,3,gl.FLOAT,'a_Position')){
        return -1;
    }
    if(!initArrayBuffer(gl,color,3,gl.FLOAT,'a_Color')){
        return -1;
    }
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW);

    return indices.length;
}
function initArrayBuffer(gl,data,num,type,attribute) {
    // 创建缓冲区对象
    var buffer = gl.createBuffer();

    gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
    gl.bufferData(gl.ARRAY_BUFFER,data,gl.STATIC_DRAW);
    var a_attribute = gl.getAttribLocation(gl.program,attribute);
    // 将缓冲区对象分配给 a_Position变量
    gl.vertexAttribPointer(a_attribute,num,type,false,0,0);
    gl.enableVertexAttribArray(a_attribute);
    return true;
}

如果将所有的颜色改成白色会是神马效果呢? 自己试一下。 看不出是什么东西了,因为颜色没有差异后变得界限也就没有了,但是我们在现实生活中一个白色的盒子还是可以看出是一个正方体,那是因为有光照产生了颜色的细微差异。 下一章 我们学习如果运用三维场景中的光照。