一、实例代码
<html>
<canvas id='c' width='480' height='320'></canvas>
<script type="x-shader/x-vertex" id="vertex-shader">
attribute vec4 a_Position;
attribute vec2 a_TextCoord;
varying vec2 v_TexCoord;
void main(){
gl_Position = a_Position;
v_TexCoord = a_TextCoord;
}
</script>
<script type="x-shader/x-fragment" id="fragment-shader">
//需要声明浮点数精度,否则报错No precision specified for (float)
precision mediump float;
// 纹理样本,被用来进行采样
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main(){
//设置颜色,从纹理中采样
gl_FragColor = texture2D(u_Sampler,v_TexCoord);
}
</script>
<script src="WebGLUtils.js"> </script>
<script>
var gl = createGLContext('c')
//创建shader program
var program = createProgramFromElementId(gl,'vertex-shader','fragment-shader');
gl.useProgram(program);
//-----1.将坐标数据复制到GPU缓冲区-----//
var vertices = new Float32Array([
-1.0, 1.0, 0.0, 1.0,//左上角
-1.0, -1.0, 0.0, 0.0,//左下角
1.0,1.0, 1.0,1.0,//右上角
0.5, -0.5, 1.0, 0.0//右下角
])
// 创建缓冲区,将数据从CPU复制到GPU
var vertexBuffer = gl.createBuffer()
// 绑定缓冲区到目标
gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer);
// 向缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW);
//-----2.向着色器传递坐标数据-----//
var FSIZE = vertices.BYTES_PER_ELEMENT;
// 1.传递顶点坐标
var a_Position = gl.getAttribLocation(program,"a_Position");
gl.enableVertexAttribArray(a_Position);
gl.vertexAttribPointer(a_Position,2,gl.FLOAT,false,4*FSIZE,0);
// 2.传递纹理坐标
var a_TextCoord = gl.getAttribLocation(program,"a_TextCoord");
gl.enableVertexAttribArray(a_TextCoord);
gl.vertexAttribPointer(a_TextCoord, 2, gl.FLOAT, false, FSIZE*4, FSIZE*2);
//-----向着色器传递纹理数据-----//
// 浏览器加载图片
var image = new Image();
image.src = "ttt.png";
image.onload = function(){
//-----3.将图像数据复制到GPU缓冲区的纹理中-----//
var texture = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
//-----4.向着色器传递纹理数据-----//
var u_Sampler = gl.getUniformLocation(program, 'u_Sampler');
gl.uniform1i(u_Sampler, 0);
// 5.绘制
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
</script>
</html>
二、代码分解
1. 编写着色器
我们使用了自定义的标签来放置vertex shader和fragment shader.
因为html5的标准规定,如果script标签里面的type不是text/javascript或者src=”…”之类的话,浏览器会把这段script标签里面的内容解析成文本。
因此,我们自定义两种type分别用来存放vertex shader和fragment shader:
2. 加载图片
我们需要加载一个图像,创建一个纹理然后将图像数据从CPU复制到GPU纹理中。 由于浏览器中的图片是异步加载的,所以我们需要重新组织一下代码, 等待纹理加载,一旦加载完成就开始绘制。
var image = new Image();
image.src = "test.png";
image.onload = function() {
render(image);
}
3. 复制图像数据到纹理
// 创建纹理
var texture = gl.createTexture();
// 激活纹理单元
gl.activeTexture(gl.TEXTURE0);
// 绑定纹理
gl.bindTexture(gl.TEXTURE_2D, texture);
// 图像Y轴翻转
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
// 复制图像数据到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
第一个参数指定了纹理目标(Target)。TEXTURE_2D
代表二维纹理,TEXTURE_CUBE_MAP
立方体纹理
第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
第三个参数图像的内部格式 ,有:gl.RGB(红绿蓝)、gl.RGBA(红绿蓝透明度)、gl.ALPHA(0.0,0.0,0.0,透明度)等
第四个参数是纹理的数据格式,必须与第三个参数相同。
第五个参数纹理数据格式
UNSIGNED_BYTE:表示无符号整形,每一个颜色分量占据1字节
UNSIGNED_SHORT_5_6_5:表示RGB,每一个分量分别占据占据5,6,5比特
UNSIGNED_SHORT_4_4_4_4:表示RGBA,每一个分量分别占据占据4,4,4,4比特
UNSIGNED_SHORT_5_5_5_1:表示RGBA,每一个分量分别占据占据5比特,A分量占据1比特
第六个参数是纹理图像的image对象
4. 纹理坐标
纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。
因为OpenGL要求y轴0.0坐标是在图片的底部的,但是图片(图片数据)的y轴0.0坐标通常在顶部。这里我们需要把纹理在Y轴翻转一下
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
5. 设置纹理参数
纹理环绕方式
纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像,但OpenGL提供了更多的选择:
gl.REPEAT | 对纹理的默认行为。重复纹理图像。 |
---|---|
gl.MIRRORED_REPEAT |
和GL_REPEAT一样,但每次重复图片是镜像放置的 |
gl.CLAMP_TO_EDGE |
纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
gl.CLAMP_TO_BORDER |
超出的坐标为用户指定的边缘颜色。 |
前面提到的每个选项都可以使用glTexParameter*函数对单独的一个坐标轴设置(s、t(如果是使用3D纹理那么还有一个r)它们和x、y、z是等价的)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);
纹理过滤
当进行放大(Magnify)和缩小(Minify)操作的时候可以设置纹理过滤的选项,比如你可以在纹理被缩小的时候使用邻近过滤,被放大时使用线性过滤。我们需要使用glTexParameter*函数为放大和缩小指定过滤方式。这段代码看起来会和纹理环绕方式的设置很相似:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
GL_NEAREST
(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。
GL_LINEAR
(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。
纹理单元
一个纹理的位置值通常称为一个纹理单元(Texture Unit)
纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。就像glBindTexture一样,我们可以使用glActiveTexture激活纹理单元,传入我们需要使用的纹理单元:
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
WebGL通过纹理单元的机制来同时使用多个纹理,gl.TEXTURE0~gl.TEXTURE7是管理纹理图像的8个纹理单元
6.向着色器传递纹理数据
var u_Sampler = gl.getUniformLocation(program, 'u_Sampler');
gl.uniform1i(u_Sampler, 0);
7.绘制
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
gl.TRIANGLES_STRIP:除了将前三个顶点之后的顶点当作第三个顶点与前两个顶点共同构成一个新三角形外,其他都与 gl.TRIANGLES 相同。例如,如果数组中包含 A、B、C、D 四个顶
点,则第一个三角形连接 ABC,而第二个三角形连接 BCD。
三、阶段性总结
通过对三角形的绘制和纹理的绘制,可以把绘制过程大体分为以下几个阶段:
- 准备顶点和片段着色器,编译着色器程序
- CPU中准备原始数据
- 数据从CPU到GPU缓存
- 向着色器中传递缓存数据
- 绘制