WebGL基础-变量控制与颜色修改
1 前言
前文已经完成了WebGL中Hello World
的实现,具体参考该文章:WebGL基础概念。也就是绘制一个三角形,这篇文章我们来修改其中的一些参数,改变三角形的颜色,修改三角形的位置等操作。
WebGL探索系列目录
:传送门
2 修改三角形颜色
还记得我们在片段着色器中给gl_FragColor
赋值了么,每个像素都将调用一次片段着色器,每次调用需要从你设置的特殊全局变量gl_FragColor中获取颜色信息。
gl_FragColor = vec4(1, 0, 0.5, 1); // 返回“红紫色”
如果想修改颜色,可以直接在这里修改,如修改为以下代码:
gl_FragColor = vec4(1, 1, 0.5, 1); // 返回黄色
当然,这样直接写死在片段着色器中的固定值不是我们想要的,可以通过传值的方式传入我们想要的值,一般传值的方式有三种
Uniforms
:全局变量
Textures
:纹理
Varyings
:可变量
由于纹理的内容比较多,所以我们单独开篇讲解,来看一下其他两个。
2.1 使用Uniforms修改颜色
全局变量在一次绘制过程中传递给着色器的值都一样,在下面的一个简单的例子中, 用全局变量给片段着色器添加了颜色。修改片段着色器的定义,加一个uniform
的变量u_frag
,并赋值给gl_FragColor
,这样我们在js代码中修改u_frag
即可
const fragmentShaderSource = `
precision mediump float;
uniform vec4 u_frag;
void main() {
gl_FragColor = u_frag;
}
`
首先使用getUniformLocation
获取一个uniform
的变量,注意和getAttribLocation
的区别。
const uFrag = gl.getUniformLocation(program, "u_frag");
随后对该变量进行赋值的操作,这样即可通过传递把值传给gl_FragColor
,可以实现在js中控制三角形的颜色。
gl.uniform4fv(uFrag, [1, 1, 0.5, 1])
要注意的是全局变量属于单个着色程序,如果多个着色程序有同名全局变量,需要找到每个全局变量并设置自己的值。 我们调用gl.uniform???
的时候只是设置了当前程序的全局变量,当前程序是传递给gl.useProgram
的最后一个程序。
gl.uniform1f (floatUniformLoc, v); // float
gl.uniform1fv(floatUniformLoc, [v]); // float 或 float array
gl.uniform2f (vec2UniformLoc, v0, v1); // vec2
gl.uniform2fv(vec2UniformLoc, [v0, v1]); // vec2 或 vec2 array
gl.uniform3f (vec3UniformLoc, v0, v1, v2); // vec3
gl.uniform3fv(vec3UniformLoc, [v0, v1, v2]); // vec3 或 vec3 array
gl.uniform4f (vec4UniformLoc, v0, v1, v2, v4); // vec4
gl.uniform4fv(vec4UniformLoc, [v0, v1, v2, v4]); // vec4 或 vec4 array
gl.uniformMatrix2fv(mat2UniformLoc, false, [ 4x element array ]) // mat2 或 mat2 array
gl.uniformMatrix3fv(mat3UniformLoc, false, [ 9x element array ]) // mat3 或 mat3 array
gl.uniformMatrix4fv(mat4UniformLoc, false, [ 16x element array ]) // mat4 或 mat4 array
gl.uniform1i (intUniformLoc, v); // int
gl.uniform1iv(intUniformLoc, [v]); // int 或 int array
gl.uniform2i (ivec2UniformLoc, v0, v1); // ivec2
gl.uniform2iv(ivec2UniformLoc, [v0, v1]); // ivec2 或 ivec2 array
gl.uniform3i (ivec3UniformLoc, v0, v1, v2); // ivec3
gl.uniform3iv(ivec3UniformLoc, [v0, v1, v2]); // ivec3 or ivec3 array
gl.uniform4i (ivec4UniformLoc, v0, v1, v2, v4); // ivec4
gl.uniform4iv(ivec4UniformLoc, [v0, v1, v2, v4]); // ivec4 或 ivec4 array
gl.uniform1i (sampler2DUniformLoc, v); // sampler2D (textures)
gl.uniform1iv(sampler2DUniformLoc, [v]); // sampler2D 或 sampler2D array
gl.uniform1i (samplerCubeUniformLoc, v); // samplerCube (textures)
gl.uniform1iv(samplerCubeUniformLoc, [v]); // samplerCube 或 samplerCube array
2.2 使用Varyings修改颜色
varying是一种可以从顶点着色器传值到片段着色器的“可变量”,在顶点着色器中定义一个变量v_color
,同样的,在片段着色器中定义同名varying变量,顶点着色器中v_color
的值会被传递给片段着色器。当然这不是直接的值传递,而是发生了一系列的内插过程,暂时不讨论这些过程。
const vertexShaderSource = `
// 一个属性变量,将会从缓冲中获取数据
attribute vec4 a_position;
varying vec4 v_color;
// 所有着色器都有一个main方法
void main() {
// gl_Position 是一个顶点着色器主要设置的变量
v_color = a_position;
gl_Position = a_position;
}
`
const fragmentShaderSource = `
// 片段着色器没有默认精度,所以我们需要设置一个精度
// mediump是一个不错的默认值,代表“medium precision”(中等精度)
precision mediump float;
varying vec4 v_color;
void main() {
// gl_FragColor是一个片段着色器主要设置的变量
gl_FragColor = v_color; // 返回一个颜色
}
`
顶点着色器中,将a_position
的值赋值给v_color
,在文中已经对a_position
的值进行了定义,则会传给顶点着色器的v_color
,同样也传给了片段着色器的v_color
,三个顶点的颜色被确定后,其中点颜色通过内插得到。
回顾一下,我们a_position
的赋值为
0, 0,
0, 0.5,
0.7, 0,
顶点的颜色被赋值后,是这样的
rgba(0, 0, 0, 255)
rgba(0, 127, 0, 255)
rgba(180, 0, 0, 255)
得出的三角形是一个多彩的三角形。
3 顶点坐标的传递过程
下面我们来聊一聊怎么传递顶点的过程
3.1 webgl坐标系
首先我们要先聊一下坐标系,webgl的坐标系是空间坐标系,使用的是右手坐标系,面对屏幕向上为y轴的正方向,向左为正x轴的正方向,垂直于屏幕向外的为z轴的正方向。原点位于屏幕的中心处,坐标抽范围为(-1,1)
所以我们看下数据,该数据一个点的长度为2,所以一行的两个数据代表两个点,第一个坐标为(0,0),第二个坐标为(0,0.5),第三个坐标为(0.7,0)
const positions = [
0, 0,
0, 0.5,
0.7, 0,
];
3.2 创建buffer并传入数据
那么如何让这样的一串数据传递给了顶点着色器中的变量呢,首先使用使用createBuffer
函数创建一个buffer,用来存储顶点或颜色等数据。绑定完数据后,GLSL
定义的变量就可以从缓冲区里面获取到想要的数据。
随后使用bindBuffer
函数将创建好的buffer
绑定到目标。这句代码是激活当前的buffer,因为可能会创建几个buffer,bindBuffer后,GLSL
会从该buffer中取数。
使用bufferData
函数可以向buffer中注入数据,这里是将positions
的数据注入了创建的buffer
中。
// 创建一个缓冲区,并在其中放置三个二维剪辑空间点
const positionBuffer = gl.createBuffer();
// 将其绑定到ARRAY_BUFFER
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positions = [
0, 0,
0, 0.5,
0.7, 0,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
3.3 将buffer的数据传入GLSL定义的变量中
数据已经准备好了,那么下面就开始取数了,首先要获取顶点着色器程序中我们定义的attribute
变量,使用getAttribLocation
函数可以实现。
// 从顶点着色程序中找到a_position属性
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
获取到了顶点的变量和buffer的数据,就可以开始给顶点变量赋值了。赋值操作主要使用vertexAttribPointer
函数。
该函数共有六个参数,第一个便是需要赋值的变量,我们这里的值为positionAttributeLocation
。第二个值是每次迭代运行提取的单位数据,在我们的项目中,相当于是一次取positions
数组的数量,赋值为2时,代表一个顶点有两个值是从positions
中获取的,vec4中的其他值会自动补全。positions
数组长度为6,则一共可以生成三个顶点。
第三个参数定义了数据类型,这里使用的是浮点型,所以使用了gl.FLOAT
,第四个参数是一个布尔值,是否需要归一化,我们的数据已经是[-1,-1]
之间的数据,所以不做归一化。
第五个参数是相邻两个顶点间的字节数,在我们的项目中,该值为0,因为所有的数据都是顶点的数据,没有在buffer中传入颜色的数据,所以顶点是紧挨着的,该值为0。第六个参数是指定缓冲对象的偏移量,我们这里是从数组的第一个值开始的,所以也是0。
// 告诉属性怎么从positionBuffer中读取数据 (ARRAY_BUFFER)
const size = 2; // 每次迭代运行提取两个单位数据
const type = gl.FLOAT; // 每个单位的数据类型是32位浮点型
const normalize = false; // 不需要归一化数据
const stride = 0; // 0 = 移动单位数量 * 每个单位占用内存(sizeof(type))
// 每次迭代运行运动多少内存到下一个数据开始点
const offset1 = 0; // 从缓冲起始位置开始读取
gl.vertexAttribPointer(
positionAttributeLocation, size, type, normalize, stride, offset1)
3.4 修改三角形的位置与形状
让我们修改当前三角形的位置与形状。直接修改positions即可
const positions = [
-1, -1,
0.4, 1,
0.7, 0,
];
4 使用offset在positions中取值
我们已经知道了如何在定义的数据中取值,但是目前顶点位置与颜色的值是一样的,我们来修改一下positions的值,使顶点位置与颜色值分开。
将positions添加几个参数。
const positions = [
// 顶点 颜色
0, 0, 1.0, 0, 1.0,
0, 0.5, 0, 1.0, 0,
0.7, 0, 1.0, 1.0, 0
];
const positionFloat = new Float32Array(positions)
前两个参数控制顶点的坐标,后三个参数控制片元的颜色。首先,在定义着色器时新增一个变量a_color
,用于控制颜色。
const vertexShaderSource = `
// 一个属性变量,将会从缓冲中获取数据
attribute vec4 a_position;
attribute vec4 a_color;
varying vec4 v_color;
// 所有着色器都有一个main方法
void main() {
// gl_Position 是一个顶点着色器主要设置的变量
v_color = a_color;
gl_Position = a_position;
}
`
const fragmentShaderSource = `
// 片段着色器没有默认精度,所以我们需要设置一个精度
// mediump是一个不错的默认值,代表“medium precision”(中等精度)
precision mediump float;
varying vec4 v_color;
void main() {
// gl_FragColor是一个片段着色器主要设置的变量
gl_FragColor = v_color; // 返回一个颜色
}
`
随后在获取属性时,获取a_color
的属性,并开启该属性。
const aColor = gl.getAttribLocation(program, "a_color");
gl.enableVertexAttribArray(aColor);
计算每个单位占用的内存,并分配数据。
// 计算这种数据类型占据的字节数
const FSIZE = positionFloat.BYTES_PER_ELEMENT
// 分配数据
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, FSIZE * 5, 0)
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2)
运行后的展示为:
这样就利用vertexAttribPointer
的stride
与offset
对同一块buffer数据进行了存取。