WebGL-学习笔记(二)

WebGL学习笔记(二).png

构成三维模型的基本图形是三角形,所以接下来就从如何绘制一个三角形开始,之后涉及到图形的变换和动画。

1. 图形绘制

先回顾以下绘制单个点的方式:通过gl.getAttribLocati on()获得了GLSL中的Vertex着色器的属性值,并利用gl.vertexArrib[1234]f()方法簇给着色器属性赋值,并将值传递给GLSL的内置变量gl_Position,之后调用gl.drawArrays(gl.POINT, 0, 1)的方法绘制点。
如果要绘制多个点怎么办?当然,我们可以设置多次调用gl.vertexAttrib[1234]fgl.drawArrays(gl.POINT, 0, 1)的方式来实现绘制多个点,例如:

html:
  <canvas id="glCanvas" width="640" height="480"></canvas>
Javascript:
const canvas = document.querySelector('#glCanvas');
const gl = canvas.getContext('webgl');
// 着色器程序
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    attribute float a_PointSize;
    void main() {
        gl_Position = a_Position;
        gl_PointSize = a_PointSize;
    }
    `;
const FSHADER_SOURCE = `
    precision mediump float;
    uniform vec4 u_FragColor;
    void main() {
        gl_FragColor = u_FragColor;
    }
    `
let program = init(gl, VSHADER_SOURCE, FSHADER_SOURCE);
// 获取顶点位置的属性
let a_Position = gl.getAttribLocation(program, 'a_Position');
if (a_Position < 0) {
    console.log('Cant find the position');
    return;
}
// 设置顶点点的尺寸
let a_PointSize = gl.getAttribLocation(program, 'a_PointSize');
    if (a_PointSize < 0) {
        console.log('Cant find the pointsize');
        return;
    }
gl.vertexAttrib1f(a_PointSize, 10.0);
// 设置顶点颜色
let u_FragColor = gl.getUniformLocation(program, 'u_FragColor');
gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 设置并绘制多个顶点
drawPoint();
// 绘制多个点的方法
function drawPoint(gl, a_Position) {
    gl.vertexAttrib3f(a_Position, -0.5, -0.5, 0.0);
    gl.drawArrays(gl.POINT, 0, 1);
    gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0);
    gl.drawArrays(gl.POINT, 0, 1);
    gl.vertexAttrib3f(a_Position, 0.5, -0.5, 0.0);
    gl.drawArrays(gl.POINT, 0, 1);
}

这样反复调用绘制的方案明显效率低下(反复重绘整个画布),另外如果我们想绘制其他图形,建立点之间点连接关系,似乎就没有办法了(因为每次绘制的点都是独立的)。为了解决着两个问题,就需要用到WebGL提供的缓冲区来一次性存储多种信息(不仅仅存储顶点坐标,还可以一次性存入顶点颜色等信息,后面再讨论)

1.1 利用缓冲区绘制多个点

WebGL中要使用缓冲区,主要有以下五个步骤:

  1. 利用gl.createBuffer()创建缓冲区对象
  2. 利用gl.bindBuffer()将创建的缓冲区对象和WebGL中的内置对象gl.ARRAY_BUFFER进行绑定
  3. 利用gl.bufferData()gl.ARRAY_BUFFER内置对象传递数据(数据不能直接传递给缓冲区对象,要通过gl.ARRAY_BUFFER来进行)
  4. 利用gl.vertexAttribPointer()将缓冲区数据分配给GLSL中的变量
  5. 最后利用gl.enableVertexAttribArray()开启缓冲区对变量的使用,开启后gl.vertexAttrib[1234]f()方法簇的赋值将失效

完成上述操作后,调用gl.drawArrays()来就可以一次绘制缓冲区数据了,这时只要将第三个参数变为所需要绘制点的数目就可以了。根据前一个例子,这里将drawPoint()进行调整为使用缓冲区

function initPoint(gl, a_Position) {
    let pointData = new Float32Array([
        -0.5, -0.5,
        0, 0.5,
        0.5, -0.5
    ]);
    // 创建缓冲区对象
    let buffer = gl.createBuffer();
    // 绑定缓冲区对象
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    // 将数据传递到缓冲区
    gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);
    // 将缓冲区数据传递给顶点着色器attribute属性
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
    // 激活顶点和缓冲区到连接
    gl.enableVertexAttribArray(a_Position);
    // 一次性绘制多个点
    gl.drawArrays(gl.POINT, 0, 3);
}

PS:代码中使用了Float32Array来创建点的数据对象,它是Javascript提供的一种类型化数组,目的是为了说明数组中的所有数据都是同一种数据类型的特殊数组,使处理数组效率更快(可能不用做类型判断和转化了),其中类型化数组提供了很多方法和属性,尤其BYTES_PER_ELEMENT属性在之后会有很大用处的。

1.2 利用mode控制图形绘制

在使用缓冲区的基础上,绘制图形就很简单了,只需要改变gl.drawArrays()中的第一个参数就可以了
WebGL的第一个参数mode,提供了7种值:

  • gl.POINT: 绘制点
  • gl.LINES: 绘制线段
  • gl.LINE_STRIP:绘制连续线段,例如传入[A0, A1, A2, A3]四个坐标信息,那么绘制结果为[A0, A1], [A1, A2], [A2, A3]
  • gl.LINE_LOOP:首位两个点会连接起来
  • gl.TRIANGLES:绘制三角形
  • gl.TRIANGLE_STRIP:绘制一系列三角形,例如传入[A0, A1, A2, A3, A4, A5]五个坐标信息,那么绘制结果为[A0, A1, A2], [A2, A1, A3], [A2, A3, A4], [A4, A3, A5](webGL中绘制是按照逆时针方式进行绘制的)
  • gl.TRIANGLE_FAN:以第一个点为所有三角形顶点,绘制三角扇

2. 图形变换

我们经常会将绘制的图形进行平移,旋转,缩放的操作,这些操作统一被称为仿射变化。

2.1 基本变换

2.1.1 平移

平移要实现的其实是方向坐标上的位移:

x' = x + ux;
y' = y + uy;

因为GLSL中矢量可以直接进行加减运算,所以修改顶点着色器程序:

// 着色器程序
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    attribute float a_PointSize;
    void main() {
        // 矢量可以直接进行运算
        gl_Position = a_Position + vec4(0.1, 0.1, 0.0, 0.0); 
        gl_PointSize = a_PointSize;
    }
`;

如果想要自定义平移距离,那么可以在顶点着色器程序中新增一个uniform变量(使用uniform是因为变量本身与顶点无关)

// 着色器程序
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    attribute float a_PointSize;
    unifrom vec4 u_Translate;
    void main() {
        // 矢量可以直接进行运算
        gl_Position = a_Position + u_Translate; 
        gl_PointSize = a_PointSize;
    }
`;
// 设置平移距离
let u_Translate = gl.getUniformLocation(program, 'u_Translate');
gl.uniform4f(u_Translate, 0.1, 0.1, 0.0, 0.0);

PS:要注意因为是使用齐次坐标,所以最后一个变量值要传递为0.0(因为默认齐次坐标的第四个变量为1.0,矢量计算后最后一个变量仍然应该为1.0)

2.1.2 缩放

缩放要实现的是方向坐标上的比例变化

x' = ux;
y' = uy;

实现缩放的关键是要获取坐标在某些方向上的分量,可以通过a_Position.xa_Position.y来分别获取xy方向上的分量,修改着色器程序:

// 着色器程序
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    attribute float a_PointSize;
    void main() {
        // 分别设置四个方向上的分量信息
        gl_Position.x = a_Position.x * 0.5; 
        gl_Position.y = a_Position.y * 0.5; 
        gl_Position.z = a_Position.z;
        gl_Position.w = 1.0;
        gl_PointSize = a_PointSize;
    }
`;
2.1.3 旋转

旋转相比起来就复杂的多了,旋转必须指明三个要素,旋转轴,旋转方向和旋转角度。
WebGL中旋转正方向是逆时针方向,遵循右手旋转法则,也就是大拇指朝向轴的正方向,四指方向就是旋转正方向。
以仅绕z轴旋转为例,那么如果坐标系中的点的原角度为a(与x轴正方向角度),转动角度为b,我们可以利用三角函数得到

x = r*cos(a);
y = r*sin(a);
// 利用三角函数和角公式进行变化
x' = r*cos(a+b) = r*cos(a)*cos(b) - r*sin(a)*sin(b) = x*cos(b) - y* sin(b);
y' = r*sin(a+b) = r*cos(a)*sin(b) + r*sin(a)*cos(b) = x*sin(b) + y*cos(b)

于是同样通过获取分量的方式,将顶点着色器程序进行修改

// 着色器程序
const VSHADER_SOURCE = `
    attribute vec4 a_Position;
    attribute float a_PointSize;
    uniform float u_Cosb, u_Sinb;
    void main() {
        // 分别设置四个方向上的分量信息
        gl_Position.x = a_Position.x * u_Cosb - a_Position.y * u_Sinb
        gl_Position.y = a_Position.x * * u_Sinb + a_Position.y * u_Cosb; 
        gl_Position.z = a_Position.z;
        gl_Position.w = 1.0;
        gl_PointSize = a_PointSize;
    }
`;
// 转动30度
let rad = 90 / 180 * Math.PI;
let u_Sinb = gl.getUniformLocation(program, 'u_Sinb');
gl.uniform1f(u_Sinb, Math.sin(rad));
let u_Cosb = gl.getUniformLocation(program, 'u_Cosb');
gl.uniform1f(u_Cosb, Math.cos(rad));

2.2 矩阵变换

所有的变换其实都是平移,旋转,缩放的叠加,但是按照之前的方式来进行那么在变换叠加的时候,计算过程就变得麻烦了,例如,我们要先旋转后平移再缩放。。。由于WebGL支持矩阵运算,所以变换可以使用矩阵变化来处理。
具体矩阵运算可以参见矩阵(其实也就是将方程转换为了矩阵的方式)
在WebGL中,要使用变换矩阵主要要进行四步骤:

  1. 顶点着色器程序中增加矩阵变量uniform mat4 u_Matrix
  2. 顶点着色器程序中修改gl_Position = u_Matrix * a_Position(注意矩阵计算时的顺序)
  3. 创建矩阵变换的数组new Float32Array()
  4. 使用gl.unifromMatrix[1234]fv的方法设置矩阵变换uniform变量的值

以旋转变化为例,那么:

  // 着色器程序
  const VSHADER_SOURCE = `
      attribute vec4 a_Position;
      attribute float a_PointSize;
      uniform mat4 u_Matrix;
      void main() {
          gl_Position = u_Matrix * a_Position;
          gl_PointSize = a_PointSize;
      }
  `;
let rad = 90 / 180 * Math.PI;
let u_Matrix = gl.getUniformLocation(program, 'u_Matrix');
let matrix = new Float32Array([
    Math.cos(rad), Math.sin(rad), 0.0, 0.0,
    -Math.sin(rad), Math.cos(rad), 0.0, 0.0, 
    0.0, 0.0, 1.0, 0.0,
    0.0, 0.0, 0.0, 1.0
])
gl.uniformMatrix4fv(u_Matrix, false, matrix);

PS:特别注意,数组中存储二维数组有两种顺序,按列主序和按行主序,WebGL和OpenGL中都是按列主序,所以,注意数组中的数据和真是矩阵中数据位置存在转置(gl.uniformMatrix4fv的第二个参数可以实现矩阵转置,但是WebGL中并没有实现转置操作所以始终默认为false

3. 动画

3.1 利用requestAnimationFrame进行动画

动画基本上就是基于图形的各种变换的持续执行过程,于是在webGL中要实现动画,实际上就是在图形变换的基础上,调用了动画函数进行循环调用,不断重新绘制图形。可以使用setInterval函数也可以使用requestAnimationFrame来实现动画的循环调用,不过建议使用后者,因为后者只有在浏览器tab页激活的时候才会执行,而前者会一直执行。还是以旋转为例,下面是持续旋转的例子:

    // 着色器程序
    const VSHADER_SOURCE = `
        attribute vec4 a_Position;
        attribute float a_PointSize;
        uniform mat4 u_Matrix;
        void main() {
            gl_Position = u_Matrix * a_Position;
            gl_PointSize = a_PointSize;
        }
    `;
    let start = Date.now();
    let rad = 0;
    animation();
    // 旋转动画
    function animation() {
        let now = Date.now();
        let offsetTime = now - start;
        start = now;
        // 假设每秒钟转动30度
        rad += offsetTime * 30 / 360 / 1000 * Math.PI;
        let u_Matrix = gl.getUniformLocation(program, 'u_Matrix');
        let matrix = new Float32Array([
            Math.cos(rad), Math.sin(rad), 0.0, 0.0,
            -Math.sin(rad), Math.cos(rad), 0.0, 0.0, 
            0.0, 0.0, 1.0, 0.0,
            0.0, 0.0, 0.0, 1.0
        ])
        gl.uniformMatrix4fv(u_Matrix, false, matrix);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.LINE_LOOP, 0, 3);
        requestAnimationFrame(animation);
    }
}
  1. 总结
    一开始我总觉得自己在旋转的过程中图形形状发生了变化,很奇怪,最后花了很多事件才发现原来demo中的canvas不是正方形。。。所以在进行旋转操作的时候,x轴和y轴并不是相同的单位比例。
  2. 参考
    《WebGL编程指南》
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容