在上一篇,我们贴出了一套用WebGL绘制三角形的示例,包含了大量的代码。
初学者可能会看得天晕地转,如同天书一般。本篇正是为了解决您的烦恼而写。
【过程分解】化复杂为简单!
可以把整个过程,分为四个阶段或者四个步骤。
(1)获取接口阶段:通过Canvas来获取WebGL接口。
(2)着色器准备阶段:准备顶点和片断着色器,编译并链接到着色器程序。
(3)顶点准备阶段:准备三角形的顶点数据,并关联到着色器中,激活。
(4)渲染阶段:清空缓冲区,使用着色器程序和VBO,绘制三角形。
【第一步详解】获取接口阶段:
这一步是通过Canvas来获取WebGL的接口,在前面几篇都已经重点介绍过,不明白的可以看前几篇:认识Canvas。
//通过元素id来获取对象
var canvas: HTMLCanvasElement = <HTMLCanvasElement>document.getElementById('my-canvas');
if (!canvas) {
console.log("错误:无法获取到 Canvas 元素!");
return;
}
//获取一个3d绘图上下文,表明我们将使用webgl相关的api
var gl: WebGLRenderingContext = this.create3DContext(canvas);
if (gl == null) {
console.log("错误:无法获取到 WebGL 上下文!");
return;
}
【第二步详解】着色器准备阶段:
着色器分成两种:顶点着色器(vertexShader)和片断着色器(fragmentShader)。为什么会有两种?
举个例子,想像我们平时在画一个三角形的时候,是不是要先确定三角形的位置和尺寸,然后再填充涂上颜色呢。
(a)如何确定顶点的位置等信息,由[ 顶点着色器(vertexShader ) ]来负责。结果由 gl_Position 输出
//顶点着色器
var vertexShaderSource: string = `
attribute vec4 a_Position;
void main()
{
gl_Position = vec4(a_Position.x, a_Position.y, a_Position.z, 1.0);
}
`;
(b)如何确定填充的颜色等信息,由[ 片断着色器(fragmentShader) ]来负责。结果由 gl_FragColor 输出
//片段着色器
var fragmentShaderSource: string = `
precision mediump float;
void main()
{
gl_FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
`;
至于如何编写GLGL ES着色器代码,将在后面的连载中讲,这里可以先忽略。
// 生成并编译顶点着色器和片段着色器
// =========================================================================
// 首先是创建和编译顶点着色器
var vertexShader: WebGLShader | null = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);//编译
// 检查着色器代码是否发生编译错误
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
var log: string | null = gl.getShaderInfoLog(vertexShader);
gl.deleteShader(vertexShader);
console.log("错误:编译vertex顶点着色器发生错误:" + log);
return;
}
然后是分别编译顶点着色器和片断着色器。如果你的着色器代码编写有误,在编译时会获取错误日志Log信息。
// 链接到着色器
var shaderProgram: WebGLProgram | null = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);//导入顶点着色器
gl.attachShader(shaderProgram, fragmentShader);//导入片断着色器
gl.linkProgram(shaderProgram);//链接到着色器
这里是把上面两边编译的着色器代码,链接到着色器程序中WebGLProgram。
到这里,着色器部份就全部完成了。有关着色器的知识,以后会重点讲解。这里先搞懂流程就好了。
【第三步详解】顶点准备阶段:
我们知道,想要绘制一个三角形,首先要提供三个顶点位置信息:一个3D顶点由(x,y,z)三个坐标组成。
我们先来看一下WebGL中的“屏幕坐标系”,原点(x=0,y=0,z=0)在屏幕正中心处。
这个“屏幕坐标系”打引号的原因,因为还涉及投影,这里科学的叫法应该叫做NDC:
标准化设备坐标(Normalized Device Coordinates, NDC)
从这张图可以看到,原点在正中心,X的正轴往右,Y的正轴往上。Z轴先不管!
Z轴的方向取决于你所使用的坐标系(左手坐标系或右手坐标系)这里不解释,没用到,以后会重点讲。
// 创建一个三角形的顶点数据
// =========================================================================
var vertices: number[] = [
-0.5, -0.5, 0.0, // left
0.5, -0.5, 0.0, // right
0.0, 0.5, 0.0 // top
];
可以看到,图上所画的三角形的坐标,和我们代码中定义的是一样的,因为在标准化设置坐标NDC中,所以Z=0是可以的。
WebGL有很多缓冲对象类型,顶点缓冲对象的缓冲类型是ARRAY_BUFFER。
// 创建并绑定一个VBO对象
var VBO: WebGLBuffer | null = gl.createBuffer();//创建一个VBO顶点Buffer
gl.bindBuffer(gl.ARRAY_BUFFER, VBO);//绑定
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);//填充数据
WebGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。
我们可以使用gl.bindBuffer函数把新创建的缓冲绑定到ARRAY_BUFFER目标上:
gl.bufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。
它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到ARRAY_BUFFER目标上。
第二个参数是我们希望发送的实际数据。
第三个参数指定了我们希望显卡如何管理给定的数据。它有两种形式:
STATIC_DRAW :数据不会或几乎不会改变。
DYNAMIC_DRAW:数据会被改变很多。
三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是STATIC_DRAW。
如果,比如说一个缓冲中的数据将频繁被改变,那么使用的类型就是GL_DYNAMIC_DRAW,
这样就能确保显卡把数据放在能够高速写入的内存部分。
现在我们已经把顶点数据储存在显卡的内存中,用VBO这个顶点缓冲对象管理。
【链接顶点属性】目的是把顶点数据结构告诉给GPU着色器程序。
顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,
它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。
所以,我们必须在渲染前指定WebGL该如何解释顶点数据。我们的顶点缓冲数据会被解析为下面这样子:
有了这些信息我们就可以使用glVertexAttribPointer函数告诉WebGL该如何解析顶点数据(应用到逐个顶点属性上)了:
//给指定的着色器变量a_Position赋值
var a_Position = gl.getAttribLocation(shaderProgram, 'a_Position');//获取地址
//参数:着色器中的位置,顶点大小,数据类型,是否标准化,步长,偏移量
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);//激活
现在我们已经定义了WebGL该如何解释顶点数据,我们现在应该使用gl.enableVertexAttribArray,
以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。
自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,
建立了一个顶点和一个片段着色器,并告诉了WebGL如何把顶点数据链接到顶点着色器的顶点属性上。
【第四步详解】渲染阶段:
要想绘制我们想要的物体,WebGL给我们提供了drawArrays函数,
它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据来绘制图元。
// 画出三角形
gl.useProgram(shaderProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, VBO);
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.drawArrays函数第一个参数是我们打算绘制的WebGL图元的类型。
由于我们在一开始时说过,我们希望绘制的是一个三角形,这里传递gl.TRIANGLES给它。
第二个参数指定了顶点数组的起始索引,我们这里填0。
最后一个参数指定我们打算绘制多少个顶点,这里是3(我们只从我们的数据中渲染一个三角形,它只有3个顶点长)。
如果以上步骤都没有发生错误,你应该会看到以下结果,如果出现在错误,请仔细检查你所有的代码。
好了,本篇做为上一节代码的补充,如果你的输出和这个看起来不一样,你可能做错了什么。
去查看一下源码,检查你是否遗漏了什么东西,或者你也可以在评论区提问。
完整代码:
namespace sunnyboxs {//namespace命名空间
export class HelloTriangles { //类名
constructor() //构造函数
{
//通过元素id来获取对象
var canvas: HTMLCanvasElement = <HTMLCanvasElement>document.getElementById('my-canvas');
if (!canvas) {
console.log("错误:无法获取到 Canvas 元素!");
return;
}
//获取一个3d绘图上下文,表明我们将使用webgl相关的api
var gl: WebGLRenderingContext = this.create3DContext(canvas);
if (gl == null) {
console.log("错误:无法获取到 WebGL 上下文!");
return;
}
//设置WebGL渲染区域尺寸
gl.viewport(0, 0, canvas.clientWidth, canvas.clientHeight);
//顶点着色器
var vertexShaderSource: string = `
attribute vec4 a_Position;
void main()
{
gl_Position = vec4(a_Position.x, a_Position.y, a_Position.z, 1.0);
}
`;
//片段着色器
var fragmentShaderSource: string = `
precision mediump float;
void main()
{
gl_FragColor = vec4(1.0, 0.5, 0.2, 1.0);
}
`;
// 生成并编译顶点着色器和片段着色器
// =========================================================================
// 首先是创建和编译顶点着色器
var vertexShader: WebGLShader | null = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);//编译
// 检查着色器代码是否发生编译错误
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
var log: string | null = gl.getShaderInfoLog(vertexShader);
gl.deleteShader(vertexShader);
console.log("错误:编译vertex顶点着色器发生错误:" + log);
return;
}
// 片段着色器
var fragmentShader: WebGLShader | null = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);//编译
// 检查着色器代码是否发生编译错误
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
var log: string | null = gl.getShaderInfoLog(fragmentShader);
gl.deleteShader(fragmentShader);
console.log("错误:编译fragment片段着色器发生错误:" + log);
return;
}
// 链接到着色器
var shaderProgram: WebGLProgram | null = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);//导入顶点着色器
gl.attachShader(shaderProgram, fragmentShader);//导入片断着色器
gl.linkProgram(shaderProgram);//链接到着色器
// 检查链接时,是否发生错误
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
var log: string | null = gl.getProgramInfoLog(shaderProgram);
console.log("错误:链接到着色器时发生错误:" + log);
return;
}
//链接完成后可以释放源
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
// 创建一个三角形的顶点数据
// =========================================================================
var vertices: number[] = [
-0.5, -0.5, 0.0, // left
0.5, -0.5, 0.0, // right
0.0, 0.5, 0.0 // top
];
// 创建并绑定一个VBO对象
var VBO: WebGLBuffer | null = gl.createBuffer();//创建一个VBO顶点Buffer
gl.bindBuffer(gl.ARRAY_BUFFER, VBO);//绑定
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);//填充数据
//给指定的着色器变量a_Position赋值
var a_Position = gl.getAttribLocation(shaderProgram, 'a_Position');//获取地址
//参数:着色器中的位置,顶点大小,数据类型,是否标准化,步长,偏移量
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);//激活
//开始渲染
// =========================================================================
//清空指定<canvas>的颜色
gl.clearColor(0.2, 0.3, 0.3, 1.0);
//清空<canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
// 画出三角形
gl.useProgram(shaderProgram);
gl.bindBuffer(gl.ARRAY_BUFFER, VBO);
gl.drawArrays(gl.TRIANGLES, 0, 3);
//渲染结束
}
//获取一个兼容的webgl上下文
private create3DContext(canvas: HTMLCanvasElement): WebGLRenderingContext | any {
var names: string[] = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
for (var i = 0; i < names.length; i++) {
try {
var context: WebGLRenderingContext = <WebGLRenderingContext>canvas.getContext(names[i]);
if (context) {
return context;
}
} catch (e) { }
}
return null;
}
}
}
下一节,我们继续连载来学习WebGL!
欢迎关注~