WebGL笔记(一):起步
一、获取WebGL上下文
首先从DOM树里按传统方法获取canvas节点。类似以下大路货代码:
$ : function(id){
return typeof id == 'string'
? document.getElementById(id)
: id;
}
然后调用canvas的getContext方法获取WebGL对象:
var gl = canvas.getContext('experimental-webgl')
为了方便操作,也是一个个人习惯,我喜欢把各种常用工具封装起来。所以我写了一个WebGLHelper的对象,把上面两个操作整合在一起了。
var WebGLHelper = {
$ : function(id){
return typeof id == 'string'
? document.getElementById(id)
: id;
},
$$ : function(canvas){
if(!(canvas = this.$(canvas))
|| canvas.nodeType != 1
|| canvas.nodeName.toLowerCase() != 'canvas'
) return null;
try {
return canvas.getContext('experimental-webgl')
} catch(ex) { return null; }
},
...
二、获取Shaper
关于Shaper,找到一篇相关介绍的博文,引述一点相关知识:
http://blog.csdn.net/huanghuangyy/article/details/6954767
Shaders是用来将图形信息(shape data)转换为屏幕上的像素。当使用GLSL这种shader格式时,我们会用到两种不同的shaders。
Vertex shader 使用在被渲染的三角形的每个顶点(corner)上。这个shader会转换点信息,传入贴图对其信息并且使用每个三角形的normals来计算光照。GLSL提供给用户一个特殊的变量gl_Position来存储经过转换的顶点信息。WebGL使用三角形每个顶点存储的信息来生成并填充其他所有需要输出的像素。贴图对齐和光照信息通过varying变量传入。
所有Vertex shader的信息都会传递到fragment shader中,此shader会在每个被传入的经过转换的三角形的每个点上运行,从贴图得出对应的像素,调整光照并且输出。GLSL为此定义了一个专用的变量gl_FragColor,此变量存储的信息即为像素的颜色。
Shaper代码可以直接写在script标记中,ContentType为x-shader/x-vertex和x-shader/x-fragment两类,也是我们将在WebGL编程中用到的。script标记的type定义和代码类型的关联是严格的,书写的时候应该注意。
本文用到的Shader代码均来自Mozilla的MDN,由于比较简单,代码量也比较少,没有多做研究,仅仅是照搬照抄。下面是两段混写在HTML代码中的Shader程序。
<script id="shader-fs" type="x-shader/x-fragment">
void main(void){ gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); }
</script>
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
}
</script>
注意这里对两个script定义了id,这在后面的调用中是要用到的。下面我们写一段程序来获取这两个script标记中的代码。这段代码也封装在WebGLHelper中,需要调用上面已经提到$函数。
getCode : function(script){
if(!(script = this.$(script))) return null;
var source = '', child = script.firstChild;
while(!!child){
if(child.nodeType == 3){
source += child.textContent;
}
child = child.nextSibling;
}
child = script = null;
return source;
}
其实,就是将script标记中的文本节点——也就是代码段——组合成一个字符串。虽然MDN中讲到,为了方便扩展和改写,推荐将Shaper以script标记的方式混写在HTML中,但是本质上,最终还是以字符串方式来调用。换句话说,我们可以将既定的Shaper代码定义在js中,根据不同的运行需求来动态调用。例如:
var testVertexCode = '\
attribute vec3 aVertexPosition;\
uniform mat4 uMVMatrix;\
uniform mat4 uPMatrix;\
void main(void){\
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);\
}',
testFragmentCode = '\
void main(void){\
gl_FragColor=vec4(1.0,1.0,1.0,1.0);\
}';
这是题外话,继续说Shaper的调用。假设——混排或指定的方式——我们已获取了两段Shader代码,并赋值给两个变量testVertexCode和testFragmentCode,并有WebGL对象gl,我们需要首先得到两个相应的Shader对象:
var vertShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertShader, testVertexCode);
gl.compileShader(vertShader);
var fragShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragShader, testFragmentCode);
gl.compileShader(fragShader);
gl.VERTEX_SHADER和gl.FRAGMENT_SHADER是两个常量,分别对应35633和35632两个整数值。刚才提到,Fragment和Vertex类型代码要与这两个常量对应起来,否则会报错。调用这两个常量时,可以直接使用其对应的整数值。
三、绑定Shader
主要有三步:
- 创建一个编程对象作为Shader的容器;
- 将Shader附加到编程对象中;
- 让WebGL关联和引用该编程对象。
var program = gl.createProgram();
gl.attachShader(program, vertShader);
gl.attachShader(program, fragShader);
gl.linkProgram(program);
gl.useProgram(program);
以上关于Shader的使用就结束了,进一步的就是研究Shader编程本身的问题了。
四、加载绘制对象
下面我们绘制四个顶点来组成一个白色矩形。正如MDN相关文章中很友善的提示所说:虽然我们只绘制了一个平面图形,但是,我们确实是在一个三维空间中绘制的啊。哈哈!
首先在WebGL中建立一个缓存区来存储要绘制的图形:
var vertices = [
1.0, 1.0, 0.0,
-1.0, 1.0, 0.0,
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0
];
var squareVerticesBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
接下来关于所谓“顶点位置属性”的代码部分就不是很明了:
var vertexPosAttr = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(vertexPosAttr);
gl.vertexAttribPointer(vertexPosAttr, 3, gl.FLOAT, false, 0, 0);
上面第一行代码中的vertexPosAttr返回的是一个整数值。在测试中,仅发现0和1两个结果。参数aVertexPosition似乎也是既定值,尝试指定其他值时,虽然不报错,但是返回-1。vertexAttribPointer方法第二个参数3,似乎是通知gl数组vertices中每组值的长度,即3个为一组。
这里需要特别说明两点:
- gl.bufferData必须写在gl.bindBuffer之后;
- gl.vertexAttribPointer必须写在gl.bindBuffer之后。
其他方面还有待研究。
五、设置位置
Mozilla的MDN中提供了两段js代码,提供了一个Matrix对象来简化对绘制位置的设置。由于MDN的示例代码比较繁杂,个人感觉有点混乱,而且比较要命的是,整个示例中的变量命名于window域中,如果想要重新封装,改写是早晚的事情。所以我编写了MatrixHelper类,重新封装了Matrix,不污染window域。
function MatrixHelper(){ this.matrix = Matrix.I(4); }
MatrixHelper.prototype = {
/* makePerspective */
make : function(fovy, aspect, znear, zfar){
this.ppm = makePerspective(fovy, aspect, znear, zfar);
},
/* multMatrix */
mult : function(m){
this.matrix = this.matrix.x(m);
},
/* mvTranslate */
trans : function(v){
this.mult(Matrix.Translation($V([v[0], v[1], v[2]])).ensure4x4());
},
/* setMatrixUniforms */
set : function(gl, sProg){
if(!!this.ppm){
gl.uniformMatrix4fv(gl.getUniformLocation(sProg, "uPMatrix")
, false, new Float32Array(this.ppm.flatten()));
}
if(!!this.matrix){
gl.uniformMatrix4fv(gl.getUniformLocation(sProg, "uMVMatrix")
, false, new Float32Array(this.matrix.flatten()));
}
}
}
注释即为MDN中对应的原函数名称。原文在https://developer.mozilla.org/en/WebGL/Getting_started_with_WebGL,页尾附件为原始示例。
下面使用MatrixHelper对物体做位置调整:
var matrix = new MatrixHelper();
matrix.trans([0.0, 0.0, -5.0]);
matrix.make(45, 640 / 480, 0.1, 100.0);
matrix.set(gl, program);
trans方法设置位置信息(x,y,z);make方法第二个参数为视图比例,当该比例与canvas比例一致时,图形像素比为1:1;set方法即将位置信息附加到gl对象中。其他部分不甚明了。
六、绘制
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
如果不出意外,canvas区域应该会出现一个白色正方形。
drawArrays后面两个参数,与顶点数据分组有关:上面做过猜测,在
gl.vertexAttribPointer(vertexPosAttr, 3, gl.FLOAT, false, 0, 0)
中第2个参数3应该是顶点分组每组的个数,那么可知顶点为4个。推测drawArrays中的0应该是从第0个分组开始绘制,4应该是指从0开始绘制4组。可以自己动手改一下0或4或者vertices数组数量来测试一下。注意超出分组索引范围会导致绘图失败。
综述
与canvas的2d绘图相比,WebGL要麻烦很多,尤其是附加Shader部分,是以往没有遇到过的。在这两天的学习测试当中,注意力并没有放在理解WebGL原理或其他高级应用上,而是在归纳关键步骤,理清头绪。MDN的例子——个人感觉——有些混乱,而且不符合大部分js编程者的习惯。本文代码都出自MDN,并做了简化,结果可能也不那么令人兴奋,但是希望能归纳出一个正确的流程,让初学者——包括我自己——在进行WebGL程序编写中有章可循。
附本文代码:
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>WebGL Step 01</title>
<style type="text/css">
canvas{ background-color:#666; }
</style>
<script type="text/ecmascript" src="https://files.cnblogs.com/muse/sylvester.js"></script>
<script type="text/ecmascript" src="https://files.cnblogs.com/muse/glUtils.js"></script>
<script type="text/ecmascript">
function MatrixHelper(){ this.matrix = Matrix.I(4); }
MatrixHelper.prototype = {
/* makePerspective */
make : function(fovy, aspect, znear, zfar){
this.ppm = makePerspective(fovy, aspect, znear, zfar);
},
/* multMatrix */
mult : function(m){
this.matrix = this.matrix.x(m);
},
/* mvTranslate */
trans : function(v){
this.mult(Matrix.Translation($V([v[0], v[1], v[2]])).ensure4x4());
},
/* setMatrixUniforms */
set : function(gl, sProg){
if(!!this.ppm){
gl.uniformMatrix4fv(gl.getUniformLocation(sProg, "uPMatrix")
, false, new Float32Array(this.ppm.flatten()));
}
if(!!this.matrix){
gl.uniformMatrix4fv(gl.getUniformLocation(sProg, "uMVMatrix")
, false, new Float32Array(this.matrix.flatten()));
}
}
}
</script>
</head>
<body>
<canvas id="glcanvas" width="640" height="480">看来您的浏览器不支持<code><canvas></code>标记</canvas>
<script type="text/ecmascript">
var testVertexCode = '\
attribute vec3 aVertexPosition;\
uniform mat4 uMVMatrix;\
uniform mat4 uPMatrix;\
void main(void){\
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);\
}',
testFragmentCode = '\
void main(void){\
gl_FragColor=vec4(1.0,1.0,1.0,1.0);\
}';
var vertices = [
1.0, 1.0, 0.0,
-1.0, 1.0, 0.0,
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0
];
var canvas = document.getElementById('glcanvas');
var gl = canvas.getContext('experimental-webgl');
var vertShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertShader, testVertexCode);
gl.compileShader(vertShader);
var fragShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragShader, testFragmentCode);
gl.compileShader(fragShader);
var program = gl.createProgram();
gl.attachShader(program, vertShader);
gl.attachShader(program, fragShader);
gl.linkProgram(program);
gl.useProgram(program);
var squareVerticesBuffer = gl.createBuffer();
var vertexPosAttr = gl.getAttribLocation(program, 'aVertexPosition');
gl.bindBuffer(gl.ARRAY_BUFFER, squareVerticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.vertexAttribPointer(vertexPosAttr, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(vertexPosAttr);
var matrix = new MatrixHelper();
matrix.trans([0.0, 0.0, -5.0]);
matrix.make(45, 640 / 480, 0.1, 100.0);
matrix.set(gl, program);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
</script>
</body>
</html>