本专栏所有文章示例代码均可在我的git码云上获取,读者可自行下载:https://gitee.com/babyogl/learnWebGL,本文示例代码在chapter-02的color-triangle-01,引用文件都在libs文件夹下;在上一节中,笔者介绍了如何用WebGL绘制一个三角形:原生webgl学习(二) 用WebGL绘制一个三角形,那我们会想,画一个三角形有什么了不起的?的确,画一个三角形的确很一般,所以我们要最求更加高大上的骚操作,这就涉及到接下来笔者要讲的矩阵运算;可能学过计算机图形学的同学都知道,在计算机图形学中,一切运算皆矩阵,图形的平移、旋转和缩放都可以表示为图形顶点坐标与相关矩阵的运算。我们先假设在二维空间进行上述的变换操作,假设某个顶点v的坐标为(x, y, 1);那为什么二位的坐标看起来确实三维的?那是由于我们在进行平移运算的时候,坐标必须是其次坐标,否则容易出现错误;同理,如果是三维空间的坐标,在进行平移变换时,应写为(x, y, z, 1)。三维的情况本文先不涉及,先掌握二维,自然而然地就可以很容易扩展到三维空间。
1.平移矩阵
直观的理解,假设点v平移了在x方向平移看了tx,在y方向平移了ty, 则平移后v点的坐标可以表示(x+tx, y+ty),用矩阵表示这个运算过程如下:
2.旋转矩阵
如下图所示,旋转只需要用到和旋转角对应的正弦值和余弦值:
由上图得到旋转后的坐标:
将其表示为矩阵形式:
3.缩放变换
图形的缩放,无非就是顶点的缩放,假设存在系数s,对顶点v进行缩放变换后的坐标是(sx, sy),以矩阵的形式表达如下:
几种变换可以组合到一起运算,让平移矩阵、旋转矩阵和缩放矩阵相乘后再乘以顶点坐标即可。在下面笔者将带领大家实现一个带菜单栏工具的实时交互demo,界面如下图:
如果大家想更进一步了解三维空间中的平移、旋转和缩放变换可以参考: WebGL中的图形变换:旋转、平移和缩放,里面有更详细的介绍。
说了这么多理论的东西,接下来我们将它们变成代码吧,如果是刚学WebGL入门的,建议先参考:原生webgl学习(二) 用WebGL绘制一个三角形,学会构建整个页面后再学习本节的内容,会更容易上手。
我们首先构建点点着色器和片元着色器:
<script id="vertex-shader" type="x-shader/vertex">
attribute vec2 a_position;
uniform mat3 u_matrix;//用于变换矩阵的传入
varying vec4 v_color;//用于设置顶点的颜色
void main() {
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy * 0.8, 0, 1);
v_color = gl_Position * 0.5 + 0.5;//顶点的颜色随顶点位置变化
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
varying vec4 v_color;//用于设置顶点的颜色
void main() {
gl_FragColor = v_color;
}
</script>
我们创建一个canvas画布,并将相关的文件引用进来,同时给gui菜单开辟一块div,菜单栏工具主要使用dat.gui.min.js这个库来构建:
<canvas id="color-triangle" width="800" height="600"></canvas>
<div id="gui"></div>
<script type="text/javascript" src="../libs/webgl-utils.js"></script>
<script type="text/javascript" src="../libs/shader.js"></script>
<script type="text/javascript" src="../libs/m3.js"></script>
<script type="text/javascript" src="../libs/dat.gui.min.js"></script>
下面是主要的功能实现代码:
<script>
"use strict";
function main() {
let canvas = document.getElementById('color-triangle');
//判断浏览器是否支持webgl
let gl = canvas.getContext('webgl', {antialias: true, depth: false});
if (!gl) {
console.log("您的浏览器不支持webgl!");
}
//获取GLSL的文本
let vShaderSource = document.getElementById('vertex-shader').text;
let fShaderSource = document.getElementById('fragment-shader').text;
//创建着色器,加载着色器代码,编译着色器,链接着色器
let program = initShader(gl, vShaderSource, fShaderSource);
//在着色器中寻找attribute变量位置
let positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
//在着色器中找uniform变量位置
let matrixLocation = gl.getUniformLocation(program, 'u_matrix');
//创建缓冲区
let positionBuffer = gl.createBuffer();
//将它绑定到ARRAY_BUFFER(将其视为ARRAY_BUFFER = positionBuffer)
//绑定缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
let positions = [
0, -100,
150, 125,
-175, 100
];
setGeometry(gl, positions);
let translation = [200, 150];
let angleInRadians = 0;
let scale = [1, 1];
drawScene();
//菜单栏
let guiFiles = {
"平移X": 200,
"平移Y": 150,
"旋转角度": 0,
"缩放X方向": 1,
"缩放Y方向": 1
};
//新建一个菜单栏
let gui = new dat.GUI();
gui.add(guiFiles, "平移X", 0, gl.canvas.width).onChange(function (e) {
translation[0] = e;
drawScene();//改变参数后重新绘制
});
gui.add(guiFiles, "平移Y", 0, gl.canvas.width).onChange(function (e) {
translation[1] = e;
drawScene();//改变参数后重新绘制
});
gui.add(guiFiles, "旋转角度", -360, 360).onChange(function(e) {
angleInRadians = e * Math.PI / 180;
drawScene();//改变参数后重新绘制
});
gui.add(guiFiles, "缩放X方向", -5, 5).onChange(function(e) {
scale[0] = e;
drawScene();//改变参数后重新绘制
});
gui.add(guiFiles, "缩放Y方向",-5, 5).onChange(function(e) {
scale[1] = e;
drawScene();//改变参数后重新绘制
});
//获取div元素
let ui = document.getElementById('gui');
//将gui的dom元素加入div中显示
ui.appendChild(gui.domElement);
//绘制场景
function drawScene() {
//使画布的像素数和显示大小匹配
webglUtils.resizeCanvasToDisplaySize(gl.canvas);
//设置视口
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
//清除canvas
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
//应用着色器
gl.useProgram(program);
//建立着色器中attribute变量与缓冲区之间的连接
gl.enableVertexAttribArray(positionAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const size = 2;//2维坐标:每次迭代运行提取两个单位数据
const type = gl.FLOAT;//每个单位的数据类型是32位浮点型
const normalize = false;//不需要归一化数据
const stride = 0;//每次迭代前进大小* sizeof(类型)以获得下一个位置
const offset1 = 0;//从缓冲起始位置开始读取
//从缓冲区取出数据
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset1);
//计算矩阵
let matrix = m3.projection(gl.canvas.clientWidth, gl.canvas.clientHeight);
matrix = m3.translate(matrix, translation[0], translation[1]);
matrix = m3.rotate(matrix, angleInRadians);
matrix = m3.scale(matrix, scale[0], scale[1]);
gl.uniformMatrix3fv(matrixLocation, false, matrix);//将平移、旋转、缩放矩阵传给着色器中的u_matrix;
let primitiveType = gl.TRIANGLES;
const offset2 = 0;
const count = 3;
gl.drawArrays(primitiveType, offset2, count);//画图
}
}
//通过绑定点向缓冲区绑定数据
function setGeometry(gl, positions) {
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
}
main();
</script>
读者一定要仔细去体会JavaScript代码向着色器传送各种各样的数据的方法,这才是WebGL的核心灵魂所在,一旦搞懂了这个机制,那恭喜你,你可以随心所欲地用自己的数据去构建和渲染你想要的图形,笔者正在往这条道上前进,希望我们共同努力,共同进步!加上本节,我们已经画了两次三角形了,下从下一节开始,我将带领大家画出一个矩形。