在利用缓冲区并在学会利用 mode
绘制图形动画以后,继续研究二维图形的颜色渲染以及纹理操作
1. 颜色渲染
1.1 利用步进和偏移拆分缓冲区数据
利用缓冲区可以一次存储点的多个信息,继续利用这一特性,并在利用vertexAttribPointer()
函数的时候,利用上stride
和offset
两个参数:
function main() {
const canvas = document.querySelector('#glCanvas');
const gl = canvas.getContext('webgl');
// 着色器程序
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute float a_PointSize;
void main() {
gl_Position = a_Position;
gl_PointSize = a_PointSize;
}
`;
const FSHADER_SOURCE = `
precision mediump float;
uniform vec4 u_FragColor;
void main() {
gl_FragColor = u_FragColor;
}
`
let program = init(gl, VSHADER_SOURCE, FSHADER_SOURCE);
let a_Position = gl.getAttribLocation(program, 'a_Position');
if (a_Position < 0) {
console.log('Cant find the position');
return;
}
let a_PointSize = gl.getAttribLocation(program, 'a_PointSize');
if (a_PointSize < 0) {
console.log('Cant find the pointsize');
return;
}
gl.vertexAttrib1f(a_PointSize, 10.0);
initPoint(gl, a_Position, a_PointSize);
let u_FragColor = gl.getUniformLocation(program, 'u_FragColor');
gl.uniform4f(u_FragColor, 1.0, 0.0, 0.0, 1.0);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 3);
}
function initPoint(gl, a_Position, a_PointSize) {
// 将点的坐标和尺寸信息放入一个类型化数组中
let pointData = new Float32Array([
-0.5, -0.5, 10.0,
0, 0.5, 20.0,
0.5, -0.5, 30.0
]);
// 利用类型化数组提供的内部属性,获取到元素的尺寸
let FSIZE = pointData.BYTES_PER_ELEMENT;
let buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);
// 设置第五个参数stride为3*FSIZE(每两个点之间有三个元素,也就是每三个数据进行一次拆分)
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 3*FSIZE, 0);
gl.enableVertexAttribArray(a_Position);
// 设置第六个参数offset偏移,将每组数据的第三个元素设置给a_PointSize属性
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 3*FSIZE, 2*FSIZE);
gl.enableVertexAttribArray(a_PointSize);
}
主要改变在于:
1.利用了类型化数组的
BYTE_PER_ELEMENT
属性,获取了每个元素的大小
- 利用
vertexAttribPointer()
的第五个参数,指定两个相邻顶点间的字节数,或者理解为每个点的信息由多少个元素组成。- 利用
vertexAttribPointer()
的第六个参数,指定缓冲区对象中每次读取时的偏移。
当然,要完成以上的操作,也可以将点的位置信息和尺寸信息进行分开放置:
function initPoint(gl, a_Position, a_PointSize) {
// 将类型化数组增加点的尺寸的参数
let pointData = new Float32Array([
-0.5, -0.5,
0, 0.5,
0.5, -0.5
]);
let pointSizeData = new Float32Array([10.0, 20.0, 30.0]);
// 设置点的位置信息
let buffer1 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer1);
gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
// 设置点的尺寸信息
let buffer2 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer2);
gl.bufferData(gl.ARRAY_BUFFER, pointSizeData, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_PointSize);
}
但是很明显操作多余而且复杂了
1.2 利用varying变量进行插值操作
想要从Vertex Shader
到Fragment Shader
进行数据传递,需要用到varying
声明的变量,为什么利用从Vertex Shader
到Fragment Shader
传递数据?
由于Fragment Shader
中是只能使用uniform
变量的,unifrom
信息是和顶点信息不相关联的,如果要单独对某个顶点的颜色进行设置,是没办法的,所以为了给每个顶点设置颜色,就要从Vertex Shader
传递数据到Fragment Shader
了。
如果绘制一个三角形,并将顶点颜色传递给Fragment Shader
:
// 修改着色器程序,增加varying
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;
void main() {
gl_Position = a_Position;
v_Color = a_Color;
}
`;
const FSHADER_SOURCE = `
precision mediump float;
varying vec4 v_Color;
void main() {
gl_FragColor = v_Color;
}
`
// 修改点的数据信息加载
function initPoint(gl, a_Position, a_Color) {
// 将点的坐标信息和颜色信息放到同一个类型化数组中
let pointData = new Float32Array([
-0.5, -0.5, 1.0, 0.0, 0.0,
0, 0.5, 0.0, 1.0, 0.0,
0.5, -0.5, 0.0, 0.0, 1.0
]);
// 利用类型化数组提供的方法,获取到元素的尺寸
let FSIZE = pointData.BYTES_PER_ELEMENT;
let buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 5*FSIZE, 0);
gl.enableVertexAttribArray(a_Position);
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, 5*FSIZE, 2*FSIZE);
gl.enableVertexAttribArray(a_Color);
}
varying
变量的作用其实不仅是进行数值的传递,如果将图形绘制的mode
修改为gl.TRANGLES
,可以发现绘制的三角形颜色是渐变的,造成这一结果是varying
的作用。
继续深入着色器过程,Vertex Shader
到Fragment Shader
要进行两步操作:
- 图形装配:将
Vertex Shader
的顶点信息,根据drawArray
的mode
配置在对应的几何图形顶点处- 光栅化:将装配好的图形分割为一个一个的片元(像素点),因此片元的数目就是像素点
varying
在赋值颜色给顶点的过程当中,会自动计算从一个顶点到另一个顶点变化的插值,并将每一个片元染成对应计算后的颜色,所以才看到一个从顶点颜色渐变的三角形,这一过程也可以被称为内插。
2. 纹理添加
除了使用单独的颜色渲染外,也可以直接将图片贴到几何图形的表面,该图片也就是纹理。图片是由像素点组成的,每个像素点就是纹素,是使用了RGB/RGBA的编码块
WebGL的给图形添加纹理很简单,但是要解决三个问题
2.1 浏览器跨域
由于WebGL的图片请求机制,所以如果直接请求本地图片的时候会出现跨域问题,为了使得开发和调试的方便,可以使用以下方式临时关闭浏览器的跨域验证,以Chrome为例:
Windows:
"C:\Program Files\Google\Chrome\Application\chrome.exe" --args --disable-web-security
MAC:
open -a "Google Chrome" --args --disable-web-security --user-data-dir
2.2 纹理坐标和图片坐标
WebGL中的纹理坐标Y轴正方向是朝上,其中坐标的四个角为左下(0.0, 0.0)
,右下(1.0, 0.0)
,右上(1.0, 1.0)
,左上(0.0, 1.0)
。
图片坐标和WebGL坐标的Y轴方向是相反的,正方向朝下,所以在进行纹理映射的时候,需要将坐标轴进行反转。
2.3 纹理添加
进行纹理映射最主要的有以下五步操作,以绘制一个矩形并贴图为例:
2.3.1. 将顶点坐标和纹理坐标都加入到类型化数组中
// 每一行前两个数据为顶点坐标,后两个数据为纹理坐标
let pointData = new Float32Array([
-0.5, 0.5, 0.0, 1.0,
-0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 1.0, 1.0,
0.5, -0.5, 1.0, 0.0
]);
2.3.2 创建纹理对象和图片加载
// 创建image对象和纹理对象
let texture = gl.createTexture();
let image = new Image();
// 给image执行onload方法的时候进行纹理渲染
image.onload = function() { initTexture() };
// 加载纹理图片
image.src = 'xxx.png';
PS:图片的加载过程是异步!!!
2.3.3 设置纹理
有6个主要的步骤,但是实际上去掉第2和6步也是可以运行的,目前对机制还不甚了解,之后会持续关注
// 1. y轴反转,因为图像坐标系y轴向下,而webgl坐标系y轴向上
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 2. 开启纹理单元(0号),一共有8个
gl.activeTexture(gl.TEXTURE0);
// 3. 绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture);
// 4. 配置纹理参数(对于非2的幂次尺寸的图片,需要设置图片拉伸方式)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// 如果图片尺寸为2的幂次,则可以使用以下方式
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 5. 将图像分配给纹理对象
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
// 6. 将0号纹理传递给着色器中的取样器变量
gl.uniform1i(u_Sampler, 0);
2.3.4 修改着色器程序
// 增加纹理坐标的数据接收
const VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
uniform mat4 u_Matrix;
void main() {
gl_Position = u_Matrix * a_Position;
v_TexCoord = a_TexCoord;
}
`;
// 给片元着色器增加 simlper2D的属性以及纹理坐标数据
const FSHADER_SOURCE = `
precision mediump float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main() {
gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
`
PS:一定要注意其中关于图片的选择,2.3.3 中关于配置纹理参数,一定要优先确定图片的长和宽是否为2的幂次,从而来决定使用哪种方案进行纹理的匹配,否则将导致纹理解析错误:
It maybe non-power-of-2 and have incompatible texture filtering.
当然目前在测试的过程当中,还存在一个浏览器警告提示没有解决
RENDER WARNING: there is no texture bound to the unit 0
目前虽然有警告,但是纹理渲染并没有问题,而且根据描述来说,我已经对纹理单元0进行了利用了,所以目前还不知道导致的原因,之后如果解决会在评论区补充原因说明。
4. 总结
基本上来说二维图形的绘制,渲染就大致这些内容,不过仍然存在很多细节需要自己去尝试摸索,接下来继续=学习进入真正的WebGL发挥实力的三维图形的领域!
5. 参考
《WebGL编程指南》