WebGL学习(1) - 三角形

原文地址:WebGL学习(1) - 三角形
还记得第一次看到canvas的粒子特效的时候,真的把我给惊艳到了,原来在浏览器也能做出这么棒的效果。结合《HTML5 Canvas核心技术》和网上的教程,经过半年断断续续的学习,对canvas的学习终于完结,对常用的canvas特效基本能做到信手拈来的。canvas特效请看:样例列表

众所周知,canvas是2D绘图技术,虽然可以通过坐标变换,位置计算也能做到3D的效果。但3D场景数据量毕竟比2D要高一个数量级的,纯粹用canvas的话,不管是性能和开发的复杂度会成为一个瓶颈。

这也是webGL出现的原因,解决web端3D渲染的场景。webGL会调用到GPU,处理大量重复的3D场景数据时,性能非常有优势。同时webGL是基于openGL ES 2.0, 因此它处理3D场景是非常成熟的。但为什么不直接学习three.js呢?因为本人对图形学感兴趣,只是希望做一些自己喜欢的效果的同时深入了解计算机图形学,没指望通过它做商业项目。

为了让学习更有动力和目的性,我们以实例为导向学习webGL,再从中展开到需要学习哪些知识点。这次我们来实现如下的动画,该教程参考了《WebGL编程指南》

实际效果请看:旋转的三角形

triangle

webGL渲染流程

webGL的渲染流程如下,其中第2,3,4步是重点,里面细节比较多。接着我们就按这个流程一步一步解决问题

  1. 获取webGL绘图上下文
  2. 初始化着色器
  3. 创建、绑定缓冲区对象
  4. 向顶点着色器和片元着色器写入数据
  5. 设置canvas背景色,清空canvas
  6. 绘制

webGL绘图上下文

webGL是canvas基础之上的3D绘图技术,只是上下文不同,get3DContext函数作用就是依次降级获取上下文。

var canvas = document.getElementById("canvas"),
    gl = get3DContext(canvas, true);
function get3DContext(canvas, opt) {
    var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
    var context = null;
    for (var i = 0, len = names.length; i < len; i++) {
        try {
            context = canvas.getContext(names[i], opt);
        } catch (e) {}
        if (context) {
            break;
        }
    }
    return context;
}

着色器

着色器就是嵌入到js中的webGL代码,是由GLSL语言编写的,可以把着色器看成是js代码连接webGL的中间件。顶点着色器和片元着色器分别用于操作顶点和颜色光照,《WebGL编程指南》中是把着色器写成字符串,但从可维护性考虑,还是写在script标签中比较好。GLSL语言与C语言非常像,只要熟悉了GLSL特有的部分,其实还是比较简单的。

限定符
限定符只能用于全局变量,有3种类型:

  • attribute用于表示顶点信息
  • uniform用于表示除顶点外的其他信息,可以是除结构体和数组之外的任意类型
  • varying用于顶点着色器向片元着色器传输数据

GLSL特有的数据类型

  1. 向量:

    vec2, vec3, vec4 : 表示有2,3,4个浮点数的向量
    ivec2, ivec3, ivec4 : 表示有2,3,4个整形的向量
    bvec2, bvec3, bvec4 : 表示有2,3,4个布尔值的向量

  2. 矩阵:
    mat2, mat3, mat4 : 表示有2x2,3x3,4x4的浮点数的矩阵

顶点着色器

<script type="x-shader/x-vertex" id="vs">
attribute vec4 a_Position; //顶点,4个浮点的矢量,attribute变量传输与顶点有关的数据,表示逐顶点的信息
uniform mat4 u_xformMatrix; //变换矩阵,4*4浮点矩阵, uniform变量传输的是所有顶点都相同的数据
void main() { 
        gl_Position=u_xformMatrix*a_Position;
} 
</script>

片元着色器

<script type="x-shader/x-fragment" id="fs">
precision mediump float; // 精度限定
uniform vec4 u_FragColor;  // 颜色
void main() {
        gl_FragColor = u_FragColor;
}
</script>

接着就是创建着色器了,首先从页面script标签取出着色器代码,初始化着色器;接着创建程序对象,最后连接程序对象。中间的步骤其实非常的啰嗦,已经把这几个步骤封装,我们只需要调用createShaders就可以了。

/**
 * 根据script id创建着色器
 * @param  {Object} gl  context
 * @param  {String} vid script id
 * @param  {String} fid script id
 * @return {Boolen}
 */
function createShaders(gl, vid, fid) {
    var vshader, fshader, element, program;
    [vid, fid].forEach(function(id) {
        element = document.getElementById(id);
        if (element) {
            switch (element.type) {
                // 顶点着色器的时候
                case "x-shader/x-vertex":
                    vshader = element.text;
                    break;
                // 片段着色器的时候
                case "x-shader/x-fragment":
                    fshader = element.text;
                    break;
                default:
                    break;
            }
        }
    });
    if (!vshader) {
        console.log("VERTEX_SHADER String not exist");
        return false;
    }
    if (!fshader) {
        console.log("FRAGMENT_SHADER String not exist");
        return false;
    }
    program = createProgram(gl, vshader, fshader);
    if (!program) {
        console.log("Failed to create program");
        return false;
    }

    gl.useProgram(program);
    gl.program = program;
    return true;
}

/**
 * 创建连接程序对象
 * @param  {Object} gl       上下文
 * @param  {String} vshader  顶点着色器代码
 * @param  {String} fshader  片元着色器代码
 * @return {Object}
 */
function createProgram(gl, vshader, fshader) {
    // 创建着色器对象
    var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
    var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
    if (!vertexShader || !fragmentShader) {
        return null;
    }

    // 创建程序对象
    var program = gl.createProgram();
    if (!program) {
        return null;
    }

    // 连接着色器对象
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);

    // 连接程序对象
    gl.linkProgram(program);

    // 检查连接结果
    var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (!linked) {
        var error = gl.getProgramInfoLog(program);
        console.log("Failed to link program: " + error);
        gl.deleteProgram(program);
        gl.deleteShader(fragmentShader);
        gl.deleteShader(vertexShader);
        return null;
    }
    return program;
}

/**
 * 加载着色器
 * @param  {Object} gl     上下文
 * @param  {Object} type   类型
 * @param  {String} source 代码字符串
 * @return {Object}
 */
function loadShader(gl, type, source) {
    // 创建着色器对象
    var shader = gl.createShader(type);
    if (shader == null) {
        console.log("unable to create shader");
        return null;
    }

    // 设置着色器程序
    gl.shaderSource(shader, source);

    // 编译着色器
    gl.compileShader(shader);

    // 检查编译结果
    var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (!compiled) {
        var error = gl.getShaderInfoLog(shader);
        console.log("Failed to compile shader: " + error);
        gl.deleteShader(shader);
        return null;
    }

    return shader;
}

缓冲区

创建好缓冲区对象后,需要把它分配给变量,然后使它生效。注意顶点数组使用的是类型化数组Float32Array,这样更加高效。vertexAttribPointer方法这里指定了每个顶点分量的个数为2,因为我们目前只定义x,y坐标,z坐标使用系统默认。

/**
 * 创建缓冲区
 * @param  {Array} data
 * @param  {Object} bufferType
 * @return {Object}
 */
function createBuffer(data, bufferType) {
    // 生成缓存对象
    var buffer = gl.createBuffer();
    if (!buffer) {
        console.log("Failed to create the buffer object");
        return null;
    }
    // 绑定缓存(gl.ARRAY_BUFFER<顶点>||gl.ELEMENT_ARRAY_BUFFER<顶点索引>)
    gl.bindBuffer(bufferType || gl.ARRAY_BUFFER, buffer);

    // 向缓存中写入数据
    gl.bufferData(bufferType || gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

    // 将绑定的缓存设为无效
    // gl.bindBuffer(gl.ARRAY_BUFFER, null);

    // 返回生成的buffer
    return buffer;
}

// 创建缓冲区并传人顶点
var vertices = new Float32Array([-0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5]);
if (!createBuffer(vertices)) return;

// 分配缓冲区对象给a_Position变量
// (地址,每个顶点分量的个数<1-4>,数据类型<整形,符点等>,是否归一化,指定相邻两个顶点间字节数<默认0>,指定缓冲区对象偏移量<默认0>)
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

// 启动
gl.enableVertexAttribArray(a_Position);

写入数据

首先要获取变量的地址,然后再给变量赋值,感觉挺麻烦的。attribute标记的变量使用getAttribLocation获取,同理uniform标记的变量使用getUniformLocation获取。

我们的动画要使图形绕坐标原点旋转,那么这就需要用到矩阵的变换,矩阵相关的知识就不详细说明了。要注意webGL使用的是列主序的矩阵,计算好变换矩阵后,把值赋予变量就ok。

// 获取 u_FragColor变量的存储地址并赋值
var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
if (!u_FragColor) return;
//颜色模式为rgba,值范围0~1
gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);

// 绕z轴旋转
var deg=Math.PI/180*(angle++),
    cos=Math.cos(deg),
    sin=Math.sin(deg);

//  webgl中是按列主序 旋转加位移
var xformMatrix=new Float32Array([
    cos,sin,0.0,0.0,
    -sin,cos,0.0,0.0,
    0.0,0.0,1.0,0.0,
    0.3,0.0,0.0,1.0
]);

// v表示可以向着色器传输多个数值(地址变量,webgl中必须false,矩阵)
gl.uniformMatrix4fv(u_xformMatrix,false,xformMatrix);

背景操作

每次执行动画前进行清屏,和canvas中的设置fillStyle,执行clearRect,效果一样。

// 设置清屏颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 清屏
gl.clear(gl.COLOR_BUFFER_BIT);

绘制

最后渲染图形,注意第一个参数,指定不同的值,它就渲染为不同的图形,大家可以用不同的值试试效果。

  • POINTS 点
  • LINES 线段
  • LINE_STRIP 线条
  • LINE_LOOP 回路
  • TRIANGLES 三角形
  • TRIANGLE_STRIP 三角带
  • TRIANGLE_FAN 三角扇
// (基本图形,第几个顶点,执行几次),修改基本图形项可以生成点,线,三角形,矩形,扇形等
gl.drawArrays(gl.TRIANGLES, 0, 3);

最后主体代码如下:

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 vertices = new Float32Array([ -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5 ]);
    if (!createBuffer(vertices)) {
        console.log("Failed to create the buffer object");
        return;
    }

    // 获取顶点位置
    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;
    }

    // 分配缓冲区对象给a_Position变量
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(a_Position);

    // 获取 u_FragColor变量的存储地址并赋值
    var u_FragColor = gl.getUniformLocation(gl.program, "u_FragColor");
    if (!u_FragColor) {
        console.log("Failed to get the storage location of u_FragColor");
        return;
    }
    gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);

    // 获取矩阵变量
    var u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix");
    if (!u_xformMatrix) {
        console.log("Failed to get the storage location of u_xformMatrix");
        return;
    }

    var xformMatrix,
        angle = 0;
    // 设置清屏颜色
    gl.clearColor(0.0, 0.0, 0.0, 1.0);

    // 执行动画
    (function animate() {
        var deg = (Math.PI / 180) * angle++,
            cos = Math.cos(deg),
            sin = Math.sin(deg);

        // 旋转加位移
        xformMatrix = new Float32Array([ 
            cos, sin, 0.0, 0.0,
            -sin, cos, 0.0, 0.0,
            0.0, 0.0, 1.0, 0.0,
            0.3, 0.0, 0.0, 1.0
        ]);

        // v表示可以向着色器传输多个数值(地址变量,webgl中必须false,矩阵)
        gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);

        gl.clear(gl.COLOR_BUFFER_BIT);

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

        requestAnimationFrame(animate);
    })();
}

main();

总结

相比canvas,webGL的api要原始得多,涉及到很多底层的openGL细节,但经过封装后,我们可以把那部分细节看成一个黑箱。大部分的操作都是基于矩阵变换,尽管有很多方便的第三方矩阵库,但有牢固的线性代数基础还是大有裨益的,GLSL编程语言也是一样需要熟练掌握。

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

推荐阅读更多精彩内容

  • WebGL从2012年开始接触,后面因为开始专注前端其他方面的事情,慢慢地就把它给遗忘。最近前端开始又流行起绘画制...
    我不是传哥阅读 4,085评论 1 22
  • 你好,三角形 图形渲染管线(Pipeline) 3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Pi...
    IceMJ阅读 7,422评论 2 13
  • 在OpenGL中,大量的数据在着色器中传递,数据通过Buffer和Texture两种形式组织。 1 缓存(Buff...
    RichardJieChen阅读 7,986评论 1 10
  • 1 前言 一直想沿着图像处理这条线建立一套完整的理论知识体系,同时积累实际应用经验。因此有了从使用AVFounda...
    RichardJieChen阅读 5,649评论 5 12
  • 转自http://m.blog.csdn.net/qq_31518167/article/details/5198...
    柚子ziheLiu阅读 1,894评论 0 0