在上一节中,回顾一下,我们学习了如何利用WebGL来绘制一个三角形。
本节,我们来深入了解一下着色器相关的知识。
在前篇,我们已经接触过了 顶点着色器 (vertexShader)和 片断着色器(fragmentShader)。
片断着色器也被称做:像素着色器。我觉得这个更好!在以后的文章中将被统一这么叫,大家注意了。
本节要点:这次会升级上一篇的三角形,让它的颜色通过程序随着时间的流逝,不断的进行进行变化。
【1】首先认识一下我们的两个着色器:
(a)顶点着色器
(b)像素着色器
它们分工明确,显卡的执行速度很快,比CPU快多了。因为显卡的工作很单一,就干这一件事,所以效率超高。
一般几万次顶点处理对于显卡来说,轻轻松松,这就是它最擅长的工作。
【2】学习着色器代码程序的三个关键字变量:
一个着色器程序,和我们的C语言比较相似。它由一个main() 函数做为入口。
GLGL ES中有几种变量关键字定义方法,常见的有attribute,uniform,varying 这三个关键字。
(1)attribute 关键字:
通过方法getAttribLocation()获取顶点数据的索引位置, 然后利用方法vertexAttribPointer()
可以把类型数组创建的顶点数据传递给顶点着色器,然后逐顶点处理计算。
(2)uniform 关键字:
uniform关键字和attribute共同的作用是可以接收数据,不同点是接收的数据不同,
attribute关键字声明顶点数据,这些数据会逐顶点处理,uniform关键字声明的数据不是顶点数据,
这些数据往往都会重复利用,每个顶点对应的数据会有坐标、颜色、法向量等,
但是光线的颜色和方向对于所有的顶点都是通用的。
(3)varying 关键字:
主要作用是在顶点着色器向像素着色器中传递数据中。且顺序不可逆。
GLSL ES是强类型语言,不像JavaScript使用var关键字来声明所有变量。GLSL ES要求你具体地指明变量的数据类型。
【3】默认精度修饰符:precision precision-qualifier type;
precision可以用来确定默认精度修饰符。type可以是int或float或采样器类型,precision-qualifier可以是lowp, mediump, 或者highp。任何其他类型和修饰符都会引起错误。如果type是float类型,那么该精度(precision-qualifier)将适用于所有无精度修饰符的浮点数声明(标量,向量,矩阵)。如果type是int类型,那么该精度(precision-qualifier)将适用于所有无精度修饰符的整型数声明(标量,向量)。包括全局变量声明,函数返回值声明,函数参数声明,和本地变量声明等。没有声明精度修饰符的变量将使用和它最近的precision语句中的精度。
在顶点着色器中有如下预定义的全局默认精度语句:
precision highp float;
precision highp int;
precision lowp sampler2D;
precision lowp samplerCube;
在像素着色器中有如下预定义的全局默认精度语句:
precision mediump int;
precision lowp sampler2D;
precision lowp samplerCube;
像素着色器没有默认的浮点数精度修饰符。因此,对于浮点数,浮点数向量和矩阵变量声明,
要么声明必须包含一个精度修饰符,要不默认的精度修饰符在之前已经被声明过了。
着色器还有许多知识将在以后逐步讲解,下面来开始改动程序,让三角形闪起来!
【4】更加高效的帧渲染循环(推荐!!):
我们想要三角形不停的变换颜色,首先需要一个不停执行的方法或循环。
在Web中推荐使用帧渲染模式的循环方法requestAnimationFrame,代码如下:
//帧循环
var animate: any = function (time: number) {
//要执行的程序。。。。。。
//启用帧循环
window.requestAnimationFrame(animate);
}
animate(0);
window.requestAnimationFrame() 将告知浏览器你马上要开始动画效果了,后者需要在下次动画前调用相应方法来更新画面。这个方法就是传递给window.requestAnimationFrame()的回调函数。
这个方法原理其实也就跟setTimeout/setInterval差不多,通过递归调用同一方法来不断更新画面以达到动起来的效果,但它优于setTimeout/setInterval的地方在于它是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。
【5】更加准确的时间计时(推荐!!):
图形程序和游戏通常会跟踪一个时间差(Deltatime)变量,它储存了渲染上一帧所用的时间。我们把所有速度都去乘以deltaTime值。结果就是,如果我们的deltaTime很大,就意味着上一帧的渲染花费了更多时间,所以这一帧的速度需要变得更高来平衡渲染所花去的时间。使用这种方法时,无论你的电脑快还是慢,通过时间差计算的速度或颜色变换都会相应平衡,这样每个用户的体验就都一样了。不会发生有的电脑运行相同的代码,有的变化快,有的变化慢了。
var timeValue: number = 0.0;// 获取程序运行的秒数
var deltaTime: number = 0.0;// 当前帧与上一帧的时间差
var lastFrame: number = 0.0;// 上一帧的时间
//帧循环
var animate: any = function (time: number) {
//计算当前帧时间
var currentFrame: number = new Date().getTime();
deltaTime = currentFrame - lastFrame;
timeValue = timeValue + deltaTime / 1000;
lastFrame = currentFrame;
//要执行的程序。。。。。。
//启用帧循环
window.requestAnimationFrame(animate);
}
animate(0);
【6】实时渲染并更新像素着色器的颜色:
首先我们更改像素着色器的代码,增加一个uniform vec4 ourColor;用于接收颜色。
vec4 是一个包含4个值的数据float类型,可以用来表示 坐标值(x,y,z,w),向量值,颜色值(r,g,b,a)等等。
//片段着色器
var fragmentShaderSource: string = `
precision mediump float;
uniform vec4 ourColor;
void main()
{
gl_FragColor = ourColor;
}
`;
然后,在渲染前,给这个ourColor属性赋值。以下是赋值过程。
//不停的更新着色器颜色
var greenValue: number = Math.sin(timeValue) / 2.0 + 0.5;
//获取像素着色器中ourColor变量的位置
var vertexColorLocation: WebGLUniformLocation | null = gl.getUniformLocation(shaderProgram, "ourColor");
//给ourColor赋值变化的颜色
gl.uniform4f(vertexColorLocation, 0.0, greenValue, 0.0, 1.0);
运行你的代码,你会发现三角形的颜色,开始不停的闪烁渐变了。非常的酷,对吗?
以下是 HelloTriangles.ts 的全部内容:
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;
uniform vec4 ourColor;
void main()
{
gl_FragColor = ourColor;
}
`;
// 生成并编译顶点着色器和片段着色器
// =========================================================================
// 首先是创建和编译顶点着色器
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);//激活
var timeValue: number = 0.0;// 获取程序运行的秒数
var deltaTime: number = 0.0;// 当前帧与上一帧的时间差
var lastFrame: number = 0.0;// 上一帧的时间
//帧循环
var animate: any = function (time: number) {
//计算当前帧时间
var currentFrame: number = new Date().getTime();
deltaTime = currentFrame - lastFrame;
timeValue = timeValue + deltaTime / 1000;
lastFrame = currentFrame;
//渲染代码开始
// =========================================================================
//清空指定<canvas>的颜色
gl.clearColor(0.2, 0.3, 0.3, 1.0);
//清空<canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
// 使用着色器程序
gl.useProgram(shaderProgram);
// 绑定VBO
gl.bindBuffer(gl.ARRAY_BUFFER, VBO);
//不停的更新着色器颜色
var greenValue: number = Math.sin(timeValue) / 2.0 + 0.5;
//获取像素着色器中ourColor变量的位置
var vertexColorLocation: WebGLUniformLocation | null = gl.getUniformLocation(shaderProgram, "ourColor");
//给ourColor赋值变化的颜色
gl.uniform4f(vertexColorLocation, 0.0, greenValue, 0.0, 1.0);
// 画出三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);
// =========================================================================
//渲染代码结束
//启用帧循环
window.requestAnimationFrame(animate);
}
animate(0);
}
//获取一个兼容的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 !
欢迎关注或留言~