WebGL学习(2) - 3D场景

原文地址:WebGL学习(2) - 3D场景
经过前面的学习,我们已经掌握了webGL的基础知识,也已经能够画出最基本的图形,比如点,线,三角形,矩形等。有了2D绘图的基础,现在终于可以进入精彩的3D世界了,来看一下这一节要实现的3D的效果吧。

实际效果:webGL3D场景

it

webGL渲染流程

重温一下webGL的渲染流程,这一节在第3、4、5、6步骤需要学习新的内容。其中写入数据交叉存放缓冲区,设置隐藏面消除,清空深度缓冲都是比较简单的部分。重点和难点是在3D变换的环节,在理解了矩阵的原理基础上,这次使用了《WebGL编程指南》提供的矩阵操作库。

  1. 获取webGL绘图上下文
  2. 初始化着色器
  3. 创建、绑定缓冲区对象
  4. 3D变换
  5. 向顶点着色器和片元着色器写入数据(数据交叉存放缓冲区)
  6. 设置canvas背景色,设置隐藏面消除
  7. 清空canvas|清空深度缓冲
  8. 绘制

着色器

着色器代码修改为下面,我们现在需要为每个顶点都使用不同的颜色,所以使用到了varying限定符的变量,这个变量目的就是连接顶点和片元着色器,把顶点信息和颜色信息结合起来。看到顶点着色器和片元着色器都有的v_color变量了吗?其实就是通过全局变量传递。
顶点着色器

<script type="x-shader/x-vertex" id="vs">
attribute vec4 a_Position; //顶点
uniform mat4 u_MvpMatrix;//模型视点投影矩阵
attribute vec4 a_Color;
varying vec4 v_color;// 连接片元着色器
void main() { 
    gl_Position = u_MvpMatrix * a_Position;
    v_color=a_Color;//传递给片元着色器变量
} 
</script>

片元着色器

<script type="x-shader/x-fragment" id="fs">
precision mediump float; // 精度限定
varying vec4 v_color; //从顶点着色器接收
void main() {
    gl_FragColor = v_color;
}
</script>

3D坐标系

第1、2、3步骤前面文章已经介绍,现在我们直接进入3D的环节。3D比2D主要就是多了深度信息,用坐标系来描述就是,除xy轴外,还多了z轴。webGL的坐标系跟我们web的坐标系是不一样的,首先它原点不是在左上角而是位于中间,xyz方向也不同。


it

视点和视线

接着引入一个概念,视点,也就是定义观察者的位置,观察者能看多远,观察者的方向,直接看图吧

it

上方向就是观测者从哪个方向看,(0,1,0)是正常的Y轴正方向,(1,0,0)就相当于物体向左旋90度,等于我们把头打横看物体。通过定义视点矩阵,我们看到的图形的形状会产生变化的,就和我们实际环境不同的角度位置观察同一物体是一样一样的。我们调用矩阵库中的方法,会产生出一个4X4的矩阵,具体中间的产生过程,可以看源代码。

// (视点,观察目标点,上方向)
setLookAt(eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ)

投影可视空间

只有指定了可视空间,webGL才会去绘制图形,有两种类型:
一是盒状可视空间,由正射投影产生,它产生的图形,前后物体没有大小区别,都是一样高宽。

it

调用矩阵库的setOrtho方法,产生矩阵

// 正视投影 (left,right,bottom,top,near,far), 组成一个正方体的可视空间 
setOrtho(left, right, bottom, top, near, far);

二是四棱锥可视空间,由透视投影产生,透视投影产生的3D场景更加真实自然,它产生的图形具有近大远小的透视效果,当然性能消耗相对正射投影高一些。

it

调用setPerspective方法,同理生成矩阵

// 投影矩阵(fov可视空间底面和顶面夹角<大于0>,近裁截面宽高比,近裁截面位置<大于0>,远裁截面位置<大于0> )
setPerspective(fovy, aspect, near, far)

数据交叉存放缓冲区

我们既可以给不同的信息分别创建单独缓冲区,也可以给不同的信息创建同一块合用的缓冲区,前者适合数据量小的情况,我们现在实现第二种情况:给不同的信息创建一块缓冲区,并交叉存放。首先用一个数组同时存放顶点信息和顶点对应的颜色信息,接着创建缓冲区后调用gl.vertexAttribPointer(),该方法有定义每个分量的个数,每一行的个数以及偏移数,当然相邻顶点数和偏移量要乘以单位字节,具体看代码的注释。

/**
 * 混合缓冲区(包括顶点,颜色),每一行前3个是顶点信息,后3个是颜色信息
 */
var verticeColors=new Float32Array([
        0.0,  1.0,  -2.0,  0.3,  1.0,  0.4, 
    -0.5, -1.0,  -2.0,  0.3,  1.0,  0.4,
        0.5, -1.0,  -2.0,  1.0,  0.4,  0.4, 

        0.0,  1.0,  -1.0,  1.0,  1.0,  0.4, 
    -0.5, -1.0,  -1.0,  1.0,  1.0,  0.4,
        0.5, -1.0,  -1.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, 
]);
// 创建缓冲区
if(!createBuffer(verticeColors)){
    console.log('Failed to create the buffer object');
    return;
}

// 每个元素的字节
var FSIZE = verticeColors.BYTES_PER_ELEMENT;
// 获取顶点位置
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
// (地址,每个顶点分量的个数<1-4>,数据类型<整形,符点等>,是否归一化,指定相邻两个顶点间字节数<默认0>,指定缓冲区对象偏移字节数量<默认0>)
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 6*FSIZE, 0);
// Enable the assignment to a_Position variable
gl.enableVertexAttribArray(a_Position);

// 获取a_Color变量的存储地址并赋值
var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, 6*FSIZE, 2*FSIZE);
gl.enableVertexAttribArray(a_Color);

3D相关的其他设置

开启隐藏面消除可以减少渲染量,提高性能,同时还可以避免顺序不一致时,后面的图形盖住前面的图形。而多边形偏移,可以避免深度很接近的两个图形产生冲突。当然每次重新渲染的时候,在清屏的同时清除深度缓冲,具体实现请看代码。

// 开启隐藏面消除
gl.enable(gl.DEPTH_TEST);
//清屏|清深度缓冲
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 启用多边形偏移,避免深度冲突
gl.enable(gl.POLYGON_OFFSET_FILL);
//设置偏移量
gl.polygonOffset(1.0, 1.0);

执行动画

来看一下我们执行动画的部分,首先设置好用于位移旋转的模型矩阵,然后依次产生视点矩阵,投影矩阵,接着把它们相乘产生出mvp矩阵,然后传入变量,最后绘图。在绘制完第一组图形的时候,将前面的mvp矩阵再左移2个单位,再绘制一遍,于是就产生出了第二组图形。具体的逻辑情况代码注释。

var angle=0;
// 执行动画
(function animate(){
    // 旋转位移 等于绕原点Y旋转
    modelMatrix.setRotate((angle++)%360,0,1,0);
    modelMatrix.translate(1, 0, 1);
    // (视点,观察目标点,上方向)
    viewMatrix.setLookAt(-0.25, -0.25, 5, 0, 0, -100, 0, 1, 0);
    // 投影矩阵(fov可视空间底面和顶面夹角<大于0>,近裁截面宽高比,近裁截面位置<大于0>,远裁截面位置<大于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.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    // 启用多边形偏移,避免深度冲突
    gl.enable(gl.POLYGON_OFFSET_FILL);

    // (基本图形,第几个顶点,执行几次),修改基本图形项可以生成点,线,三角形,矩形,扇形等
    gl.drawArrays(gl.TRIANGLES, 0, 9);

    //位移后,再将前面3个三角形重新绘制
    modelMatrix.translate(-2, 0, 0);
    mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
    gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

    //设置偏移量
    gl.polygonOffset(1.0, 1.0);
    gl.drawArrays(gl.TRIANGLES, 0, 9); 

    requestAnimationFrame(animate);
}());

总结

学习完3D场景后,我们又再一次领略到了线性代数中矩阵在图形学中的重要作用。3D的矩阵转换才是需要空间思维和深入理解的部分,其他地方说实话就是学习如何调用api。
最后,献上主体的全部代码

var canvas=document.getElementById('canvas'),
    gl=get3DContext(canvas,true);

function main() {
    if (!gl) {
        console.log('Failed to get the rendering context for WebGL');
        return;
    }

    if (!createShaders(gl, 'fs', 'vs')) {
        console.log('Failed to intialize shaders.');
        return;
    }

    /**
     * 混合缓冲区(包括顶点,颜色)
     */
    var verticeColors=new Float32Array([
            0.0,  1.0,  -2.0,  0.3,  1.0,  0.4,
        -0.5, -1.0,  -2.0,  0.3,  1.0,  0.4,
            0.5, -1.0,  -2.0,  1.0,  0.4,  0.4, 

            0.0,  1.0,  -1.0,  1.0,  1.0,  0.4,
        -0.5, -1.0,  -1.0,  1.0,  1.0,  0.4,
            0.5, -1.0,  -1.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, 
    ]);
    // 创建缓冲区
    if(!createBuffer(verticeColors)){
        console.log('Failed to create the buffer object');
        return;
    }

    // 每个元素的字节
    var FSIZE = verticeColors.BYTES_PER_ELEMENT;
    // 获取顶点位置
    var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    // (地址,每个顶点分量的个数<1-4>,数据类型<整形,符点等>,是否归一化,指定相邻两个顶点间字节数<默认0>,指定缓冲区对象偏移字节数量<默认0>)
    gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 6*FSIZE, 0);
    // Enable the assignment to a_Position variable
    gl.enableVertexAttribArray(a_Position);

    // 获取a_Color变量的存储地址并赋值
    var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
    gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, 6*FSIZE, 2*FSIZE);
    gl.enableVertexAttribArray(a_Color);

    var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
    if(!u_MvpMatrix) { 
        console.log('Failed to get the storage location of u_MvpMatrix');
        return;
    }
    // 设置背景颜色
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    // 开启隐藏面消除
    gl.enable(gl.DEPTH_TEST);

    var modelMatrix = new Matrix4(); // 模型矩阵
    var viewMatrix = new Matrix4();  // 视点矩阵
    var projMatrix = new Matrix4();  // 投影矩阵
    var mvpMatrix = new Matrix4();   // 用于相乘用
    var angle=0;
    // 执行动画
    (function animate(){
        // 旋转位移 等于绕原点Y旋转
        modelMatrix.setRotate((angle++)%360,0,1,0);
        modelMatrix.translate(1, 0, 1);
        // (视点,观察目标点,上方向)
        viewMatrix.setLookAt(-0.25, -0.25, 5, 0, 0, -100, 0, 1, 0);
        // 投影矩阵(fov可视空间底面和顶面夹角<大于0>,近裁截面宽高比,近裁截面位置<大于0>,远裁截面位置<大于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.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        // 启用多边形偏移,避免深度冲突
        gl.enable(gl.POLYGON_OFFSET_FILL);

        // (基本图形,第几个顶点,执行几次),修改基本图形项可以生成点,线,三角形,矩形,扇形等
        gl.drawArrays(gl.TRIANGLES, 0, 9);


        //位移后,再将前面3个三角形重新绘制
        modelMatrix.translate(-2, 0, 0);
        mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
        gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

        //设置偏移量
        gl.polygonOffset(1.0, 1.0);
        gl.drawArrays(gl.TRIANGLES, 0, 9);

        requestAnimationFrame(animate);
    }());
}

main();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352