OpenGl:www.opengl.org
WebGL:www.learningwebgl.com
WebGL是针对Canvas的3D上下文;OpenGL等是3D图形语言;
类型化数组
类型化数组也是数组,只不过其元素被设置为特定类型的值。
数组缓冲器ArrayBuffer
类型和byteLength
属性
类型化数组的核心就是一个名为
ArrayBuffer
的类型。每个ArrayBuffer对象表示的只是内存中指定的字节数,但不会指定这些字节用于保存什么类型的数据。通过ArrayBuffer能做的,就是为了将来使用而分配一定数量的字节。
如:
var buffer = new ArrayBuffer(20); //在内存中分配20B
属性
byteLength
返回它包含的字节数
如:
var buffer = new ArrayBuffer(20);
console.log(buffer.byteLength); //20
数组缓冲器视图
DataView
数组缓冲器视图
使用ArrayBuffer(数组缓冲器类型)的一种特别的方式就是用它来创建数组缓冲器视图。其中,最常见的视图是
DataView
,通过它可以选择ArrayBuffer中的一小段字节。为此,可在创建DataView实例的时候传入一个ArrayBuffer、一个可选的字节偏移量(从该字节开始选择)和一个可选的要选择的字节数。
如:
var view = new DataView(buffer); //新的视图
var view = new DataView(buffer, 6); //开始于字节6的新视图
var view = new DataView(buffer, 6, 9); //开始于字节6,结束于字节9的新视图
DataView
的属性byteOffset
和byteLength
DataView对象会把字节偏移量以及字符长度信息保存在
byteOffset
byteLength
两个属性中:
var view = new DataView(buffer, 6, 9); //开始于字节6,结束于字节9的新视图
console.log(view.byteOffset); //6 字节偏移量为6
console.log(view.byteLength); //9 字节长度为9
buffer属性也可以取得数组缓冲器;
getter
和setter
读写方法
读取和写入DataView的时候,要根据实际操作的数据类型,选择相应的
getter
setter
如下,列出了DataView支持的数据类型以及相应的读写方法:
getter:
getInt8(byteOffset)
方法: 在相对于视图开始处的指定字节偏移量位置处获取 Int8 值。getUint8(byteOffset)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处获取 Uint8 值。getInt16(byteOffset,littleEndian)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处获取 Int16 值。getUint16(byteOffset,littleEndian)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处获取 Uint16 值。getInt32(byteOffset,littleEndian)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处获取 Int32 值。getUint32(byteOffset,littleEndian)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处获取 Uint32 值。getFloat32(byteOffset,littleEndian)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处获取 Float32 值。getFloat64(byteOffset,littleEndian)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处获取 Float64 值。
setter:
setInt8(byteOffset,value)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处存储 Int8 值。setUint8(byteOffset,value)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处存储 Uint8 值。setInt16(byteOffset,value,littleEndian)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处存储 Int16 值。setUint16(byteOffset,value,littleEndian)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处存储 Uint16 值。setInt32(byteOffset,value,littleEndian)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处存储 Int32 值。setUint32(byteOffset,value,littleEndian)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处存储 Uint32 值。setFloat32(byteOffset,value,littleEndian)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处存储 Float32 值。setFloat64(byteOffset,value,littleEndian)
方法 (DataView): 在相对于视图开始处的指定字节偏移量位置处存储 Float64 值。
如:
var buffer = new ArrayBuffer(20);
var view = new DataView(buffer);
view.setUint16(0,25); //0000000000001001
var value = view.getUint8(0); //00000000
console.log(value); //0
类型化视图在读写数组缓冲器中更加便利:
类型化视图(类型化数组)
类型化视图一般也被称为类型化数组,因为它们除了元素必须是某种特定的数据类型外,与常规的数组无异。类型化视图也分几种,而且它们都继承了DataView。
Int8Array
:表示8为二补整数。Uint8Array
:表示8位无符号整数。Int16Array
:表示16位二补整数。Uint16Array
:表示16位无符号整数。Int32Array
:表示32为二补整数。Uint32Array
:表示32位无符号整数。Float32Array
:表示32位IEEE浮点值。Float64Array
:表示64位IEEE浮点值。
需要三个参数,只有第一个是必须的:ArrayBuffer对象、字节偏移量、要包含的字节数,如:
var buffer = new ArrayBuffer(20);
var int8s = new Int8Array(buffer);
注意:20B的ArrayBuffer可以保存20个Int8Array或Uint8Array,或者10个Int16Array或Uint16Array,或者5个Int32Array或Uint32Array或Float32Array,或者2个Float64Array。
var buffer = new ArrayBuffer(20);
var int8s = new Int8Array(buffer); //创建一个新数组,使用整个缓冲器
var int16s = new Int16Array(buffer, 9); //只使用从字节9开始的缓冲器
var uint16s = new Uint16Array(buffer, 9, 10); //只使用从字节9到字节10的缓冲器
能够指定缓冲器中可用的字节段,意味着能在同一个缓冲器中保存不同类型的数值,如下面的代码就是在缓冲器的开头保存8位整数,而在其他字节中保存16位整数:
var buffer = new ArrayBuffer(30); //缓冲器中有30个字节
var int8s = new Int8Array(buffer, 0, 10); //前面10个字节存储10个8位整数
var int16s = new Int16Array(buffer, 10, 10); //后面还有20个字节,2个字节存储1个16位整数,所以只能存储10个
另外,每个视图构造函数都有一个名为
BYTES_PER_ELEMENT
表示类型化数组的每个元素需要多少字节:
console.log(Float64Array.BYTES_PER_ELEMENT) //8
这样就可以利用这个属性来辅助初始化:
var buffer = new ArrayBuffer(20);
var int8s = new Int8Array(buffer, 0, 10 * Int8Array.BYTES_PER_ELEMENT);
var int16s = new Int16Array(buffer, int8s.byteOffset + int8s.byteLength, (10 / Int16Array.BYTES_PER_ELEMENT));
另外,还可以不用首先创建ArrayBuffer对象,只要传入希望数组保存的元素数,相应的构造函数就可以自动创建一个包含足够字节数的ArrayBuffer对象:
var int16s = new Int16Array(10); //创建一个数组保存10个16位整数(10字节)
var int32s = new Int32Array(1); //创建一个数组保存1个32位整数(4字节)
另外还可以把常规数组转换为类型化视图:
var int8s = new Int8Array([1,2,3,4]);
var view = new DataView(int8s.buffer);
console.log(int8s.toString()); //1234
console.log(view.byteLength); //4
对类型化视图的迭代:
for (var i = 0; i < int8s.length; i++) {
console.log(int8s[i]);
};
也可以使用方括号语法为类型化视图的元素赋值:
var uint16s = new Uint16Array(10);
uint16s[0] = 65537;
console.log(uint16s[0]); //1
另外可以通过
subarray()
方法基于底层数组缓冲器的子集创建一个新视图,接收两个参数:开始元素的索引,可选的结束元素的索引:
如:
var uint16s = new Uint16Array(10),
sub = uint16s.subarray(2, 5);
WebGL上下文
目前,在支持的浏览器中,WebGL的名字叫做“experimental-webgl”,这是因为WebGL规范仍然未制定完成。制定完成后,这个上下文的名字就会变成简单的“webgl”。如果浏览器不支持WebGL,那么取得该上下文时会返回null。
var drawing = document.getElementById("drawing");
if (drawing.getContext) {
var gl = drawing.getContext("experimental-webgl");
if (gl) {
//[...]
}
}
通过给getContext()传递第二个参数,可以为WebGL上下文设置一些选项。这个参数本身是一个对象,可以包含下列属性:
* `alpha`:值为true,表示为上下文创建一个Alpha通道缓冲区;默认值为true;
* `depth`:值为true,表示可以使用16位深缓冲区;默认值为true;
* `stencil`:值为true,表示可以使用8位模板缓冲区;默认值为false;
* `antialias`:值为true,表示将使用默认机制执行抗锯齿操作;默认值为true。
* `premultipliedAlpha`:值为true,表示绘图缓冲区有预乘Alpha值;默认为true;
* `preserveDrawingBuffer`:值为true;表示在绘图完成后保留绘图缓冲区;默认值为false。
传递这个选项对象的方式如下:
var drawing = document.getElementById("drawing");
if (drawing.getContext) {
var gl = drawing.getContext("experimental-webgl", {
alpha: false
});
if (gl) {
//[...]
}
}
大多数情况下不用开启,因为可能影响到性能,而且默认值一般都能满足我们需求。
如果getContext()无法创建WebGL上下文,浏览器可能会报错。所以应该把它封装到try-catch块中:
var drawing = document.getElementById("drawing");
if (drawing.getContext) {
try {
var gl = drawing.getContext("experimental-webgl");
} catch (e) {}
if (gl) {
//[...]
}
}
常量
在WebGL中,保存在上下文对象中的这些常量都没有GL_前缀。
方法命名
方法名的后缀会包含参数个数(1到4),和接收的数据类型(f为浮点数,i为整数),如:gl.uniform4f()意味着要接收4个浮点数;另外还有很多方法接收数组参数而非一个个单独的参数,这样的方法中名字包含字母v,如:gl.uniform3iv()可以接收一个包含3个值的整数数组。
准备绘图
在实际操作WebGL上下文之前,一般都要使用某种实色清除canvas元素,为绘图做好准备。为此,首先必须使用:
clearColor()
方法来指定要使用的颜色值,这个方法接收4个参数:红、绿、蓝和透明度。每个参数必须是一个0到1之间的数值,表示每种分量在最终颜色中的强度。
如:
var drawing = document.getElementById("drawing");
if (drawing.getContext) {
try {
var gl = drawing.getContext("experimental-webgl");
} catch (e) {}
if (gl) {
gl.clearColor(0,0,0,1); //把清理缓冲区的值设置为黑色
gl.clear(gl.COLOR_BUFFER_BIT); //调用clear方法,传入参数gl.COLOR_BUFFER_BIT告诉WebGL使用之前定义的颜色来填充相应区域。
}
}
视口与坐标
开始绘图之前,通常要先定义WebGL的视口(viewport)。默认情况下,视口可以使用整个canavs区域。要改变视口大小,可以调用
viewport()
方法并传入4个参数:(视口相对于canvas元素的)x、y坐标、宽度和高度。
视口坐标的原点(0,0)在canvas元素的左下角,x轴和y轴的正方向分别是向右和向上,可以定义为(width-1,height-1)。
如:
var drawing = document.getElementById("drawing");
if (drawing.getContext) {
try {
var gl = drawing.getContext("experimental-webgl");
} catch (e) {}
if (gl) {
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// gl.viewport(0, 0, drawing.width / 2, drawing.height / 2); //视口在画布的左下角四分之一区域
gl.viewport(drawing.width / 2, 0, drawing.width / 2, drawing.height / 2); //视口在画布的右下角四分之一区域
}
}
视口内部的坐标系与定义视口的坐标系也不一样。在视口内部,坐标原点(0,0)是视口的中心点,因此视口左下角坐标为(-1,-1),而右上角坐标为(1,1)。
缓冲区
顶点信息保存在JavaScript的类型化数组中,使用之前必须转换到WebGL的缓冲区。要创建缓冲区,可以调用
gl.createBuffer()
,然后使用gl.bindBuffer()
绑定到WebGL上下文。这两步做完以后,就可以用数据来填充缓冲区了。
如:
var drawing = document.getElementById("drawing");
if (drawing.getContext) {
try {
var gl = drawing.getContext("experimental-webgl");
} catch (e) {}
if (gl) {
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.viewport(drawing.width / 2, 0, drawing.width / 2, drawing.height / 2);
var buffer = gl.createBuffer(); //创建缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, buffer); //绑定到上下文
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0.5, 1]), gl.STATIC_DRAW); //使用Float32Array中的数据初始化buffer
}
}
gl.bufferData()
最后一个参数主要有:
gl.STATIC_DRAW
:数据只加载一次,在多次绘图中使用;gl.STREAM_DRAW
:数据只加载一次,在几次绘图中使用;gl.DYNAMIC_DRAW
:数据动态改变,在多次绘图中使用;
一般来说gl.STATIC_DRAW
够用了;
在包含缓冲区的页面重载之前,缓冲区始终保留在内存中。如果你不想要某个缓冲区了,可以直接调用
gl.deleteBuffer()
释放内存。
错误
JavaScript与WebGL之间的一个最大区别在于,WebGL操作一般不会抛出错误。为了知道是否有错误发生,必须在调用某个可能出错的方法后,手工调用
gl.getError()
方法。这个方法返回一个表示错误类型的常量。
可能的错误常量如下:
gl.NO_ERROR
:上一次操作没有发生错误(值为0)。gl.INVALID_ENUM
:应该给方法传入WebGL常量,但却传错了参数。gl.INVALID_VALUE
:在需要无符号数的地方传入了负值。gl.INVALID_OPERATION
:在当前状态下不能完成操作。gl.OUT_OF_MEMORY
:没有足够的内存完成操作。gl.CONTEXT_LOST_WEBGL
:由于外部事件(如设备断电)干扰丢失了当前WebGL的上下文。
如果发生了多个错误,需要反复调用gl.getError()直到返回gl.NO_ERROR:
var errorCode = gl.getError();
while (errorCode) {
console.log(errorCode);
errorCode = gl.getError();
}
着色器
着色器(shader)是OpenGL 中的另一个概念。WebGL中有两种着色器:定点着色器和片段(或像素)着色器。顶点着色器用于将3D顶点转换为需要渲染的2D点。片段着色器用于准确计算要绘制的每个像素的颜色。WebGL的着色器是使用GLSL(OpenGL Shading Language,OpenGL着色器)写的,GLSL是一种与C和JavaScript完全不同的语言。
编写着色器
GLSL是一种类C语言,专门用于编写OpenGL着色器。因为WebGL是OpenGL ES 2.0的实现,所以OpenGL中使用的着色器可以直接在WebGL中使用。
每个着色器都有一个
main()
方法,该方法在绘图期间会重复执行。
为着色器传递数据的方式有两种:
Attribute
和Uniform
。通过Attribute可以向顶点着色器传入顶点信息,通过Uniform可以向任何着色器传入常量值。
Attribute和Uniform在main()方法外部定义,分别使用关键字attribute和uniform。
如Attribute顶点着色器:
void main() {
gl_Position = vec4(aVertexPosition, 0.0, 1.0);
}
又如Uniform片段着色器:
void main() {
gl_FragColor = uColor;
}
编写着色器程序
浏览器不能理解GLSL程序,因此必须准备好字符串形式的GLSL程序,以便编译并链接到着色器程序。
为着色器传入值
前面定义的着色器必须接收一个值才能工作。为了给着色器传入这个值,必须先找到要接收这个值的变量。
调试着色器和程序
与着色器的其他操作一样,着色器操作也可能会失败,而且也是静默失败。如果你想找到着色器或程序执行中是否发生了错误,必须亲自询问WebGL上下文。
绘图
WebGL只能绘制三种形状:点、线和三角。其他所有形状都是由这三种基本形状合成之后,再绘制到三维空间中的。执行绘图操作要调用gl.drawArrays()或gl.drawElements()方法,前者用于数组缓冲区,后置用于元素数组缓冲区。
纹理
WebGL的纹理可以使用DOM中的图像。要创建一个新纹理,可以调用gl.createTexture(),然后再将一副图像绑定到该纹理。如果图像尚未加载到内存中,可能需要创建一个Image对象的实例,以便动态加载图像。图像加载完成之前,纹理不会初始化,因此,必须在load事件触发后才能设置纹理。
读取像素
与2D上下文类似,通过WebGL上下文也能读取像素值。读取像素值的方法readPixels()与OpenGL中的同名方法只有一点不同,即最后一个参数必须是类型化数组。像素信息是从帧缓冲区读取的,然后保存在类型化数组中。readPixels()方法的参数有:x、y、宽度、高度、图像格式、数据类型和类型化数组。前4个参数指定读取哪个区域中的像素。图像格式参数几乎总是gl.RGBA。数据类型用于指定保存在类型化数组中的数据类型,但有以下限制。
如果类型是gl.UNSIGNED_BYTE,则类型化数组必须是Unit8Array。
如果类型是gl.UNSIGNED_SHORT_5_6_5、gl.UNSIGNED_SHORT_4_4_4_4、或gl.UNSIGNED_SHORT_5_5_5_1,则类型化数组必须是Unit16Array。
15.3.3 支持
Firefox4+和Chrome都实现了WebGL API。Safari5.1也实现了WebGL,但默认是禁用的。