概述
通过详细分析一个旋转彩色立方体的WebGL程序,串联WebGL基础概念和几何对象的建立及旋转。
WebGL 基础概念,WebGL 工作原理, WebGL着色器和GLSL,WebGL二维矩阵,WebGL 三维正射投影,交互式计算机图形学4.6内容的整合。
效果:
shader准备
GLSL代码
- 属性(Attributes)和缓冲缓冲是发送到GPU的一些二进制数据序列,通常情况下缓冲数据包括位置,法向量,纹理坐标,顶点颜色值等。 你可以存储任何数据。属性用来指明怎么从缓冲中获取所需数据并将它提供给顶点着色器。 例如你可能在缓冲中用三个32位的浮点型数据存储一个位置值。 对于一个确切的属性你需要告诉它从哪个缓冲中获取数据,获取什么类型的数据(三个32位的浮点数据), 起始偏移值是多少,到下一个位置的字节数是多少。缓冲不是随意读取的。事实上顶点着色器运行的次数是一个指定的确切数字, 每一次运行属性会从指定的缓冲中按照指定规则依次获取下一个值。
- 全局变量(Uniforms)全局变量在着色程序运行前赋值,在运行过程中全局有效。
- 可变量(Varyings)可变量是一种顶点着色器给片断着色器传值的方式,依照渲染的图元是点, 线还是三角形,顶点着色器中设置的可变量会在片断着色器运行中获取不同的插值。
ini
<script id="vertex-shader" type="x-shader/x-vertex">
// 属性用来指明怎么从缓冲中获取所需数据并将它提供给顶点着色器
attribute vec4 vPosition;
attribute vec4 vColor;
varying vec4 fColor;
uniform vec3 theta;
void main()
{
// Compute the sines and cosines of theta for each of
// the three axes in one computation.
vec3 angles = radians( theta );
vec3 c = cos( angles );
vec3 s = sin( angles );
// 列主序矩阵
// 按 x 轴旋转矩阵
mat4 rx = mat4( 1.0, 0.0, 0.0, 0.0,
0.0, c.x, s.x, 0.0,
0.0, -s.x, c.x, 0.0,
0.0, 0.0, 0.0, 1.0 );
// 按 y 轴旋转矩阵
mat4 ry = mat4( c.y, 0.0, -s.y, 0.0,
0.0, 1.0, 0.0, 0.0,
s.y, 0.0, c.y, 0.0,
0.0, 0.0, 0.0, 1.0 );
// 按 z 轴旋转矩阵
mat4 rz = mat4( c.z, s.z, 0.0, 0.0,
-s.z, c.z, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0 );
fColor = vColor;
// vPosition从JS里传入
gl_Position = rz * ry * rx * vPosition;
gl_Position.z = -gl_Position.z;
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
varying vec4 fColor;
void
main()
{
gl_FragColor = fColor;
}
</script>
shader编译链接
ini
//
// initShaders.js
//
function initShaders( gl, vertexShaderId, fragmentShaderId )
{
var vertShdr;
var fragShdr;
// 获取顶点着色器GLSL代码
var vertElem = document.getElementById( vertexShaderId );
if ( !vertElem ) {
alert( "Unable to load vertex shader " + vertexShaderId );
return -1;
}
else {
// 创建顶点着色器对象
vertShdr = gl.createShader( gl.VERTEX_SHADER );
// 提供数据源
gl.shaderSource( vertShdr, vertElem.text );
// 编译 -> 生成顶点着色器
gl.compileShader( vertShdr );
// 如果编译失败
if ( !gl.getShaderParameter(vertShdr, gl.COMPILE_STATUS) ) {
var msg = "Vertex shader failed to compile. The error log is:"
+ "<pre>" + gl.getShaderInfoLog( vertShdr ) + "</pre>";
alert( msg );
return -1;
}
}
// 获取片元着色器GLSL代码
var fragElem = document.getElementById( fragmentShaderId );
if ( !fragElem ) {
alert( "Unable to load vertex shader " + fragmentShaderId );
return -1;
}
else {
// 创建片元着色器对象
fragShdr = gl.createShader( gl.FRAGMENT_SHADER );
// 提供数据源
gl.shaderSource( fragShdr, fragElem.text );
// 编译 -> 生成片元着色器
gl.compileShader( fragShdr );
if ( !gl.getShaderParameter(fragShdr, gl.COMPILE_STATUS) ) {
// 如果编译失败
var msg = "Fragment shader failed to compile. The error log is:"
+ "<pre>" + gl.getShaderInfoLog( fragShdr ) + "</pre>";
alert( msg );
return -1;
}
}
// 将两个着色器链接到一个程序
var program = gl.createProgram();
gl.attachShader( program, vertShdr );
gl.attachShader( program, fragShdr );
gl.linkProgram( program );
if ( !gl.getProgramParameter(program, gl.LINK_STATUS) ) {
var msg = "Shader program failed to link. The error log is:"
+ "<pre>" + gl.getProgramInfoLog( program ) + "</pre>";
alert( msg );
return -1;
}
// 返回该程序
return program;
}
WebGL准备
ini
"use strict";
var canvas;
var gl;
var NumVertices = 36;
var points = [];
var colors = [];
var xAxis = 0;
var yAxis = 1;
var zAxis = 2;
var axis = 0;
var theta = [0, 0, 0];
var thetaLoc;
window.onload = function init() {
canvas = document.getElementById("gl-canvas");
// 创建 WebGL 上下文
gl = WebGLUtils.setupWebGL(canvas);
if (!gl) {
alert("WebGL isn't available");
}
// 建模彩色立方体(详细代码见下文)
colorCube();
// 告诉WebGL裁剪空间的 -1 -> +1 分别对应到x轴的 0 -> gl.canvas.width 和y轴的 0 -> gl.canvas.height。
// webgl裁剪空间使用笛卡尔坐标系
gl.viewport(0, 0, canvas.width, canvas.height);
// 清除颜色缓存
gl.clearColor(1.0, 1.0, 1.0, 1.0);
// 开启深度信息
gl.enable(gl.DEPTH_TEST);
//
// Load shaders and initialize attribute buffers
//
// 创建着色程序
var program = initShaders(gl, "vertex-shader", "fragment-shader");
// 调用这个着色器程序
gl.useProgram(program);
var cBuffer = gl.createBuffer(); // 创建缓冲
// 绑定数据源到绑定点 ARRAY_BUFFER
gl.bindBuffer(gl.ARRAY_BUFFER, cBuffer);
// gl.STATIC_DRAW 表示变动频率低
gl.bufferData(gl.ARRAY_BUFFER, flatten(colors), gl.STATIC_DRAW);
// 通过 创建的着色器程序获得vColor的地址
var vColor = gl.getAttribLocation(program, "vColor");
// 告诉 WebGL 怎样从缓冲中获取数据传递给属性
var numComponents = 4; // (x, y, z, w)
var type = gl.FLOAT; // 32位浮点数据
var normalize = false; // 不标准化
var offset = 0; // 从缓冲起始位置开始获取
var stride = 0; // 到下一个数据跳多少位内存
// 0 = 使用当前的单位个数和单位长度 ( 3 * Float32Array.BYTES_PER_ELEMENT )
gl.vertexAttribPointer(
vColor,
numComponents,
type,
normalize,
stride,
offset
);
// 开启从缓冲中获得数据
gl.enableVertexAttribArray(vColor);
var vBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
gl.bufferData(gl.ARRAY_BUFFER, flatten(points), gl.STATIC_DRAW);
var vPosition = gl.getAttribLocation(program, "vPosition");
gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(vPosition);
// 获取theta属性地址
thetaLoc = gl.getUniformLocation(program, "theta");
//绑定点击事件修改偏转角度
document.getElementById("xButton").onclick = function () {
axis = xAxis;
};
document.getElementById("yButton").onclick = function () {
axis = yAxis;
};
document.getElementById("zButton").onclick = function () {
axis = zAxis;
};
render();
};
- 一个JavaScript序列positions 。 然而WebGL需要强类型数据,所以new Float32Array(positions)创建了32位浮点型数据序列, 并从positions中复制数据到序列中
- gl.bufferData复制这些数据到GPU的positionBuffer对象上。
- 最终传递到positionBuffer上是因为在前一步中我们我们将它绑定到了ARRAY_BUFFER(也就是绑定点)上。
建模一个彩色立方体
scss
function colorCube() {
quad(1, 0, 3, 2);
quad(2, 3, 7, 6);
quad(3, 0, 4, 7);
quad(6, 5, 1, 2);
quad(4, 5, 6, 7);
quad(5, 4, 0, 1);
}
function quad(a, b, c, d) {
var vertices = [ vec4(-0.5, -0.5, 0.5, 1.0), vec4(-0.5, 0.5, 0.5, 1.0), vec4(0.5, 0.5, 0.5, 1.0), vec4(0.5, -0.5, 0.5, 1.0), vec4(-0.5, -0.5, -0.5, 1.0), vec4(-0.5, 0.5, -0.5, 1.0), vec4(0.5, 0.5, -0.5, 1.0), vec4(0.5, -0.5, -0.5, 1.0), ];
var vertexColors = [ [0.0, 0.0, 0.0, 1.0], // black
[1.0, 0.0, 0.0, 1.0], // red
[1.0, 1.0, 0.0, 1.0], // yellow
[0.0, 1.0, 0.0, 1.0], // green
[0.0, 0.0, 1.0, 1.0], // blue
[1.0, 0.0, 1.0, 1.0], // magenta
[0.0, 1.0, 1.0, 1.0], // cyan
[1.0, 1.0, 1.0, 1.0], // white
];
// We need to parition the quad into two triangles in order for
// WebGL to be able to render it. In this case, we create two
// triangles from the quad indices
//vertex color assigned by the index of the vertex
var indices = [a, b, c, a, c, d];
for (var i = 0; i < indices.length; ++i) {
points.push(vertices[indices[i]]);
colors.push( vertexColors[indices[i]] );
// for solid colored faces use
colors.push(vertexColors[a]);
}
}
建模立方体的面
我们使用齐次坐标表示顶点,因此可以通过下列代码来定义立方体顶点数组
ini
var vertices = [
vec4(-0.5, -0.5, 0.5, 1.0),
vec4(-0.5, 0.5, 0.5, 1.0),
vec4(0.5, 0.5, 0.5, 1.0),
vec4(0.5, -0.5, 0.5, 1.0),
vec4(-0.5, -0.5, -0.5, 1.0),
vec4(-0.5, 0.5, -0.5, 1.0),
vec4(0.5, 0.5, -0.5, 1.0),
vec4(0.5, -0.5, -0.5, 1.0),
];
接下来可以用一个顶点序列来定义立方体的面,如0,3,2,1
向内和向外的面
0,3,2,1和 0,1,2,3 是不同的,是同一个多边形的两个面。在定义一个三维多边形时,必须注意顶点被指定的顺序。如果从外部观看一个对象的表面时,其顶点是按照逆时针遍历的,则这个表面称为是向外的,这个规则称为右手规则(right-hand rule),因为如果让右手四指指向遍历顶点的方向,那么拇指指向对象表面的外部。
通过仔细指定正面和背面,可以去掉和剔除不可见的面。
使用顶点列表来表示立方体
正方体有6个面,每个面4个点,共24个,但是这些顶点有重复的,使用上图表示方式可以只列出8个顶点。
彩色立方体
使用顶点列表来定义一个彩色立方体,用quad函数将立方体每个面的顶点位置及其对应的颜色存储在points, colors两个数组中,quad函数的4个输入参数是立方体某个面上4个顶点的索引值,注意4个顶点对应外向面。
ini
var NumVertices = 36;
var points = [];
var colors = [];
由于只能显示三角形,quad函数必须为每个面生成两个三角形,因此每个面有6个顶点,如果希望每个顶点都有自己的颜色,则共需要36个顶点和36种颜色。通过quad函数指定这个彩色立方体。
scss
function colorCube() {
quad(1, 0, 3, 2);
quad(2, 3, 7, 6);
quad(3, 0, 4, 7);
quad(6, 5, 1, 2);
quad(4, 5, 6, 7);
quad(5, 4, 0, 1);
}
vertexColors数组中存储的是立方体8个顶点的RGBA颜色。
quad函数使用输入的前三个顶点定义一个三角形,并用1,3,4个顶点来定义第二个三角形。
scss
function quad(a, b, c, d) {
// ...
// We need to parition the quad into two triangles in order for
// WebGL to be able to render it. In this case, we create two
// triangles from the quad indices
//vertex color assigned by the index of the vertex
var indices = [a, b, c, a, c, d];
for (var i = 0; i < indices.length; ++i) {
points.push(vertices[indices[i]]);
colors.push( vertexColors[indices[i]] );
// for solid colored faces use
// colors.push(vertexColors[a]);
}
颜色插值
现在指定了每个顶点的颜色,图形系统必须利用这个信息来确定多边形内部每个点的颜色。
基于质心坐标的插值
先在这条边上做线性插值,参数方程如下
得到的颜色,现在利用线性插值得到线段上的颜色。
显示立方体
使用基本的正交投影
矩阵把平移旋转缩放放在一个矩阵里
正面背面
可以通过这样一行代码去掉所有背面三角形
ini
gl.enable(gl.CULL_FACE);
本例中仔细设置,全部是正面三角形,所以没有使用这行代码。
接触DEPTH BUFFER(深度缓冲)
depth buffer 有时也称为 Z-Buffer,是一个存储像素深度的矩形,一个深度像素对应一个着色像素,在绘制图像时组合使用。当WebGL绘制每个着色像素时,也会写入深度像素,它的值基于顶点着色器返回的Z值,就像我们将X和Y转换到裁剪空间一样,Z也在裁剪空间(-1到+1)。这个值会被转换到深度空间(0 到 +1),WebGL绘制一个着色像素之前会检查对应的深度像素,如果对应的深度像素中的深度值小于当前像素的深度值,WebGL就不会绘制新的颜色。反之它会绘制片段着色器体提供的心颜色并更新深度像素中的深度值。这意味着在其他像素后的像素不会被绘制。
开启方式:
ini
gl.enable(gl.DEPTH_TEST);
开始绘制之前需要清除深度缓冲为1.0
ini
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
旋转
把角度、正弦余弦计算、旋转矩阵写在GLSL中
scss
// Compute the sines and cosines of theta for each of
// the three axes in one computation.
vec3 angles = radians( theta );
vec3 c = cos( angles );
vec3 s = sin( angles );
// Remeber: thse matrices are column-major
mat4 rx = mat4( 1.0, 0.0, 0.0, 0.0,
0.0, c.x, s.x, 0.0,
0.0, -s.x, c.x, 0.0,
0.0, 0.0, 0.0, 1.0 );
mat4 ry = mat4( c.y, 0.0, -s.y, 0.0,
0.0, 1.0, 0.0, 0.0,
s.y, 0.0, c.y, 0.0,
0.0, 0.0, 0.0, 1.0 );
mat4 rz = mat4( c.z, s.z, 0.0, 0.0,
-s.z, c.z, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0 );
通过unifrom变量传递角度值
- GLSL中初始化变量theta
ini
uniform vec3 theta;
- 获得uniform变量 theta 的地址
ini
thetaLoc = gl.getUniformLocation(program, "theta");
- 更新角度值
ini
gl.uniform3fv(thetaLoc, theta);
- GLSL中通过theta计算angle
ini
vec3 angles = radians( theta );
x、y、z轴旋转矩阵
推导参见WebGL 三维正射投影,此处直接使用。
pipeline
可以直观看一下程序逐行代码执行 state-diagram
几乎整个WebGL API都是关于如何设置这些成对方法的状态值以及运行它们。 对于想要绘制的每一个对象,都需要先设置一系列状态值,然后通过调用 gl.drawArrays 或 gl.drawElements 运行一个着色方法对,使得你的着色器对能够在GPU上运行。
数据传递
JS中创建数组
JS中用gl.createBuffer创建缓冲,如
ini
var vBuffer = gl.createBuffer();
GLSL中初始化一个attribute变量,如vPosition。
ini
attribute vec4 vPosition;
JS向GLSL传递数据步骤如下:
- 绑定数据源到绑定点 ARRAY_BUFFER
- 通过绑定点把数据存到缓冲
- 获取attribute变量vPosition的地址
- 指定传递数据的方式
- 按指定方式成功把points中的数据传给vPosition
scss
// 1.绑定数据源到绑定点 ARRAY_BUFFER
gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
// 2.通过绑定点向缓冲中放入数据,gl.STATIC_DRAW 表示变动频率低
gl.bufferData(gl.ARRAY_BUFFER, flatten(points), gl.STATIC_DRAW);
// 3.通过 创建的着色器程序获得vPosition的地址
var vPosition = gl.getAttribLocation(program, "vPosition");
// 4.告诉 WebGL 怎样从缓冲中获取数据传递给属性
gl.vertexAttribPointer(vPosition, 4, gl.FLOAT, false, 0, 0);
// 5.开启从缓冲中获得数据
gl.enableVertexAttribArray(vPosition);
此处points是顶点信息,在顶点着色器中即可使用,如果是颜色信息则需要通过varying变量传递给片元着色器。
渲染
WebGL应用程序和着色器之间传递数据
ini
function render() {
// 清空深度信息和数据缓冲信息
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 每次点击按钮,该角度数值加 2.0
theta[axis] += 2.0;
// 把更新后的 theta 传到 thetaLoc属性所在的位置
gl.uniform3fv(thetaLoc, theta);
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = NumVertices;
// 运行着色对,使得着色器对能在GPU上运行。
gl.drawArrays(primitiveType, offset, count);
requestAnimFrame(render);
}
参考资料
交互式计算机图形学4.6