上一篇文章:WebGL初体验【1】:绘制图形与变换技巧
仓库地址:github…、gitee…
传递非坐标数据到顶点着色器
在前面的例子中,我们传递了顶点坐标到顶点着色器,然后由顶点着色器计算出顶点在屏幕上的位置。然后例子当中也有一个大小的值。下面用来实现传递大小值给点
修改顶点着色器代码
let vertexString = `
attribute vec4 a_position;
attribute float size1;
uniform float size2;
void main(){
gl_Position = a_position;
gl_PointSize = size1;
}`;
这里可以用 attribute 和 uniform 定义大小的值,然后传递给顶点着色器,那么对应他的 js 赋值的代码如下:
利用 attribute 传递变量需要以下五个步骤、
如果你用的 web storm 或者 idea 来写 md,发现 mermaid 预览不了,可以在插件 plugin 里面搜索 mermaid 进行安装
如果是 vscode,可以安装插件:Markdown Preview Mermaid Support
而同时使用 uniform 传递变量就可以直接获取进行赋值效果是一样的,都可以把值传递进去(但是这里 uniform
传值设置的是随机数,但是设置的所有顶点的值都是同一个)
// attribute
const sizeArray = new Float32Array([60, 100, 80, 30]);
let sizeBuffer = webGL.createBuffer();
webGL.bindBuffer(webGL.ARRAY_BUFFER, sizeBuffer);
webGL.bufferData(webGL.ARRAY_BUFFER, sizeArray, webGL.STATIC_DRAW);
let aSize = webGL.getAttribLocation(program, "size");
// 参数说明:attribute变量,传递值个数、数据类型,是否要归一化,跨度,偏移量
webGL.vertexAttribPointer(aSize, 1, webGL.FLOAT, false, 4, 0);
webGL.enableVertexAttribArray(aSize);
// uniform
let uSize = webGL.getUniformLocation(program, "size2");
webGL.uniform1f(uSize, Math.random() * 100);
拓展 attribute 和 uniform 的使用
特性 | attribute | uniform |
---|---|---|
作用范围 | 逐顶点(每个顶点不同) | 全局(所有顶点共享) |
数据来源 | 顶点缓冲区(如顶点坐标数组) | 直接通过 JavaScript 设置 |
更新频率 | 每个顶点处理时更新 | 一次绘制调用中保持不变 |
典型用途 | 顶点位置、颜色、纹理坐标 | 变换矩阵、全局参数 |
WebGL | 设置方法 gl.vertexAttribPointer | gl.uniform* 系列函数 |
如何选择?
- 用 attribute:当数据需要为每个顶点单独指定时(例如顶点坐标、颜色)
- 用 uniform:当数据对所有顶点一致时(例如变换矩阵、全局光照参数)
修改颜色
现在已经知道了将其他非坐标数据传递给顶点着色器了,同样的方法可以将颜色数据传递过去,但是处理颜色是在片元着色器当中,接下来看怎么将数据从顶点着色器传到片元着色器
先搞点传递给顶点着色器(在顶点着色器里面定义一个 attribute 的 a_color 值)
const colorArray = new Float32Array([
1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0,
]);
let aColor = webGL.getAttribLocation(program, "a_color");
let colorBuffer = webGL.createBuffer();
webGL.bindBuffer(webGL.ARRAY_BUFFER, colorBuffer);
webGL.bufferData(webGL.ARRAY_BUFFER, colorArray, webGL.STATIC_DRAW);
webGL.vertexAttribPointer(aColor, 4, webGL.FLOAT, false, 4 * 4, 0);
webGL.enableVertexAttribArray(aColor);
将顶点着色器的值共享给片元着色器只需要有一个 varying 修饰的相同变量即可,这个时候的顶点和片元的代码如下:表示将 js 当中赋值的
a_color 给到 v_color,
同时由于 v_color 是 varying 修饰的所以可以共享 v_color 给片元着色器
// 顶点着色器
const vertexString = `
attribute vec4 a_position;
uniform mat4 proj;
attribute float size;
attribute vec4 a_color;
varying vec4 v_color;
void main(){
gl_Position = proj * a_position;
gl_PointSize = size;
v_color = a_color;
}`;
// 片元着色器
const fragmentString = `
varying vec4 v_color;
void main(){
gl_FragColor = v_color;
}`;
注:报错:ERROR: 0:2: ‘’ : No precision specified for (float)
原因是:片元着色器中未声明浮点型(float)变量的默认精度。GLSL ES 规范要求片元着色器必须显式定义浮点类型的精度,否则编译器会报错
解决:在片元着色器中声明精度,如:precision mediump float;
拓展 precision mediump float
precision:用于声明着色器中浮点数或整数的计算精度
为什么片元着色器必须声明?
- 顶点着色器默认支持 highp。顶点着色器中的 float 默认是 highp,无需显式声明
- 片元着色器无默认精度 。片元着色器中的 float 精度必须手动指定,否则编译器报错(No precision specified)
精度对性能和效果的影响
精度等级 | 性能 | 适用场景 | 典型问题 |
---|---|---|---|
float | 高 | 一般计算(如颜色、阴影、纹理采样) | 移动设备可能不支持或性能差 |
highp | 低 | 需要高精度的计算(如复杂光照、抗锯齿) | 移动设备可能不支持或性能差 |
mediump | 中 | 大多数颜色计算(纹理采样、颜色混合) | 极小数(如 < 0.0001)可能被截断 |
lowp | 高 | 简单颜色计算(如纯色、低精度渐变) | 颜色过渡可能出现断层 |
varying 变量的作用和内插过程
顶点的颜色赋值给了顶点着色器中的 varying 变量 v_color,它的值被传给片元着色器中的同名、同类型变量(即片元着色器中的 varying
变量 v_color),
但是,更准确地说,顶点着色器中的 v_Co1or 变量在传人片元着色器之前经过了内插过程。所以,片元着色器中的 v_color 变量和顶点着色器中的
v_color 变量实际上并不是一回事,这也正是我们将这种变量称为“varying”(变化的)变量的原因
渐变三角形
在顶点着色器和片元着色器之间还有两个步骤
- 图形装配过程:将孤立的顶点坐标装配成几何图形。几何图形 的类别由 gl.drawArrays()函数的第一个参数决定
- 光栅化:装配好的几何图形转化为片元
在光栅化过程生成的片元都是带有坐标信息的,调用片元着色器时这些坐标信息也随着片元传了进去,我们可以通过片元着色器中的内置变量来访问片元的坐标
在前面已经有一个创建三角形的案例,然后也知道怎么把颜色赋值进去了,修改片元着色器
vec4 gl_FragCoord 该内置变量的第 1 个和第 2 个分量表示片元在坐标系统(窗口坐标系统)中的坐标值
const fragmentString = `
precision mediump float;
uniform float u_width;
uniform float u_height;
void main(){
gl_FragColor = vec4(gl_FragCoord.x / u_width, 0.0, gl_FragCoord.y / u_height, 1.0);
}`;
然后在 js 当中给片元着色器传递一个 u_width 和 u_height 的值,然后就可以得到一个渐变的三角形了(传递的值就是 canvas 的宽高)
let uniformWidth = webGL.getUniformLocation(program, "u_width");
webGL.uniform1f(uniformWidth, 1024.0);
let uniformHeight = webGL.getUniformLocation(program, "u_height");
webGL.uniform1f(uniformHeight, 768.0);
补充:片元着色器的内置变量
变量名 | 类型/结构 | 读写权限 | 含义与用途 | 注意事项 |
---|---|---|---|---|
gl_FragCoord | vec4 | 只读 | 片元在屏幕空间的位置(x,y 原点在视口左下角,z 深度值,w 透视校正的倒数) | 与 Canvas 坐标系不同,常用于屏幕空间特效 |
gl_FrontFacing | bool | 只读 | 判断片元是否属于图元正面(true 为正面,false 为背面) | 用于双面材质(如正反面不同颜色) |
gl_FragColor | vec4 | 只写 | 片元的最终颜色(RGBA) | WebGL 2.0 已废弃,改用 out 变量(如 out vec4 color;) |
gl_FragData | vec4[] | 只写 | 多渲染目标(MRT)时,输出到不同颜色附件的颜色数组 | WebGL 1.0 支持有限,WebGL 2.0 需通过 layout(location=N) 指定自定义输出变量 |
gl_PointCoord | vec2 | 只读 | 绘制 GL_POINTS 时,片元在点精灵内的纹理坐标(范围 [0.0, 1.0]) | 原点在点精灵左下角,用于粒子效果或纹理贴图的点 |
gl_DepthRange | struct | 只读 | 深度缓冲区参数(包含 near, far, diff 三个字段) | 通常由 GPU 自动处理,手动计算深度时参考 |
gl_LastFragData | vec4[](扩展) | 只读 | 保留前一次渲染的片元颜色数据(需启用扩展) | 用于高级混合或延迟渲染技术,仅限 WebGL 2.0 扩展 |
纹理贴图
纹理映射
- 说明:将一张图像(就像一张贴纸)映射(贴)到一个几何图形的表面上去。将一张真实世界的图片贴到一个由两个三角形组成的矩形上,这样矩形表面看上去就是这张图片。此时,这张图片又可以称为纹理图像(texture
image)或纹理(texture) - 作用:就是根据纹理图像,为之前光栅化后的每个片元涂上合适的颜色。组成纹理图像的像素又被称为纹素(texels,texture elements)
,每一个纹素的颜色都使用 RGB 或 RGBA 格式编码
在 webGL 当中,要进行纹理映射有以下四个步骤:
- 准备好映射到几何图形上的纹理图像
- 为几何图形配置映射方式
- 加载纹理图形,对其进行配置,以便使用
- 在片元着色器中将相应的纹素从纹理中抽取出来,并将纹素的颜色赋给片元
点精灵贴图
在前面例子的基础上进行调整,先添加纹理到片元着色器中,
let fragmentString = `
precision mediump float;
uniform sampler2D texture;
void main(){
vec4 color = texture2D(texture, gl_PointCoord);
if(color.a < 0.1){
discard;
}
gl_FragColor = color;
}`;
在通用方法 initBuffer 当中添加,其中 uTexture 作为全局变量需要提前定义。
- enable(webGL.BLEND):激活片元的颜色融合计算
- blendFunc()
定义了一个用于混合像素算法的函数 参数说明如下:参数可选见 API- @param 为源混合因子指定一个乘数。默认值是
gl.ONE
- @param 为源目标合因子指定一个乘数。默认值是
gl.ZERO
- SRC_ALPHA:将所有颜色乘以源 alpha 值
- ONE_MINUS_SRC_ALPHA:将所有颜色乘以 1 减去源 alpha 值
- @param 为源混合因子指定一个乘数。默认值是
uTexture = webGL.getUniformLocation(program, "texture");
webGL.enable(webGL.BLEND);
webGL.blendFunc(webGL.SRC_ALPHA, webGL.ONE_MINUS_SRC_ALPHA);
initTexture();
引入图片作为纹理贴图
- createTexture 创建纹理对象
- 然后加载一张本地 png 图片作为纹理贴图
- 在加载完成后会触发 onload 方法,这个时候调用一个自定义的方法进行纹理配置
function initTexture() {
let textureHandle = webGL.createTexture();
textureHandle.image = new Image();
textureHandle.image.src = "../assets/image/point64.png";
textureHandle.image.onload = () => {
handleLoadedTexture(textureHandle);
};
}
进行纹理贴图配置
bindTexture(target, texture) 将纹理对象绑定到目标上
- @param target
- TEXTURE_2D 二维纹理
- TEXTURE_CUBE_MAP 立方体映射纹理
- TEXTURE_3D 三维纹理
- TEXTURE_2D_ARRAY 二维数组纹理
- @param target
texImage2D()
方法指定了二维纹理图像 API 详细说明…- @param target TEXTURE_2D 二维纹理
- @param level 指定详细级别。0 级是基本图像等级,n 级是第 n 个金字塔简化级
- @param 指定纹理中的颜色
- @param 和第三个参数保持一致
- @param type UNSIGNED_BYTE,RGBA 每个通道 8 位
- @param pixels 纹理的像素源(image)
texParameteri(target, pname, param)
设置纹理参数 API 详细说明…- @param target 同上 bindTexture 的 target
function handleLoadedTexture(texture) {
webGL.bindTexture(webGL.TEXTURE_2D, texture);
webGL.texImage2D(
webGL.TEXTURE_2D,
0,
webGL.RGBA,
webGL.RGBA,
webGL.UNSIGNED_BYTE,
texture.image
);
webGL.texParameteri(webGL.TEXTURE_2D, webGL.TEXTURE_MAG_FILTER, webGL.LINEAR);
webGL.texParameteri(webGL.TEXTURE_2D, webGL.TEXTURE_MIN_FILTER, webGL.LINEAR);
webGL.texParameteri(webGL.TEXTURE_2D, webGL.TEXTURE_WRAP_S, webGL.REPEAT);
webGL.texParameteri(webGL.TEXTURE_2D, webGL.TEXTURE_WRAP_T, webGL.REPEAT);
webGL.uniform1i(uTexture, 0);
}
到这里就将图片作为纹理贴图渲染到目标上。
其中 texParameteri 方法第二个参数为要设置的纹理参数
名称 | 描述 |
---|---|
TEXTURE_MAG_FILTER | 纹理放大滤波器 |
TEXTURE_MIN_FILTER | 纹理缩小滤波器 |
TEXTURE_WRAP_S | 纹理坐标水平填充 |
TEXTURE_WRAP_T | 纹理坐标垂直填充 |
第三个参数为展示的算法(先看 TEXTURE_MAG_FILTER 和 TEXTURE_MIN_FILTER 的取值)
模式 | 描述 | 特点 | 适用场景 |
---|---|---|---|
NEAREST | 最近邻过滤 | 性能高,锯齿明显 | 性能优先,画质要求低 |
LINEAR | 线性过滤 | 平滑,计算开销略高 | 需要平滑效果 |
NEAREST_MIPMAP_NEAREST | 最近邻 mipmap 过滤 | 性能高,锯齿或模糊 | 性能优先,mipmap 支持 |
LINEAR_MIPMAP_NEAREST | 线性 mipmap 过滤 | 平滑,性能适中 | 平衡性能和画质 |
NEAREST_MIPMAP_LINEAR | 双线性 mipmap 过滤 | 更平滑,性能较好 | 平衡性能和画质 |
LINEAR_MIPMAP_LINEAR | 三线性过滤 | 最佳平滑效果,计算开销最高 | 对画质要求高的场景 |
而后是 TEXTURE_WRAP_S 和 TEXTURE_WRAP_T 的取值
模式 | 描述 | 特点 | 适用场景 |
---|---|---|---|
REPEAT | 纹理以平铺的方式重复 | 无缝拼接,适合重复图案 | 如砖墙、草地等需要重复的场景 |
CLAMP_TO_EDGE | 超出范围的坐标被截断到最近的边界值 | 边缘拉伸,无重复或镜像效果 | 单张图片或背景 |
MIRRORED_REPEAT | 纹理以镜像的方式重复 | 每次重复都会翻转方向,减少割裂感 | 自然平铺效果,如地板、水面等 |
使用 LINEAR 和 NEAREST 的区别【锯齿效果对比】
纹理坐标
纹理坐标是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色。WebGL系统中的纹理坐标系统是二维的,WebGL使用s和t命名纹理坐标(st坐标系统)
纹理图像四个角的坐标为左下、右下、右上、和左上。纹理坐标很通用,因为坐标值与图像自身的尺寸无关,
不管是128×128还是128×256的图像,其右上角的纹理坐标始终是(1.0,1.0)。
单纹理贴图
上一步说明了怎么给点添加纹理,这一步就是给平面来添加纹理贴图。首先来个案例,绘制一个正方形,这个正方形就是到时候要贴图的块,拿两个三角形给他拼接成一个正方形,他的坐标就是这样:这里不管你是用的
webGL 原本-1 到 1 的坐标系,还是转换成 canvas 的坐标系,后续只要修改 triangleSize 变量即可
const triangleSize = 500;
// let triangleArray = [
// 0, 0, 0, 1.0, 0, 0,
// 0, triangleSize, 0, 1.0, 0, 1,
// triangleSize, 0, 0, 1.0, 1, 0,
// triangleSize, 0, 0, 1.0, 1, 0,
// 0, triangleSize, 0, 1.0, 0, 1,
// triangleSize, triangleSize, 0, 1.0, 1, 1
// ];
其他的代码我就不一一贴在这了,可以参考上一篇文章绘制三角形的代码 WebGL 初体验:绘制图形与变换技巧
之后就是添加纹理了,修改顶点着色器和片元着色器的代码,也就是下面这个,好,现在心想那他不就是和上一步的点精灵纹理一样嘛?那我就按照这样的步骤去实现,最后会发现纹理贴图并不会贴上来,取而代之的反而是一片纯色,因为纹理贴图没有被正确绘制,所以就出现了问题。
// 顶点着色器
let vertexString = `
attribute vec4 a_position;
uniform mat4 proj;
void main(){
gl_Position = proj * a_position;
}`;
// 片元着色器
let fragmentString = `
precision mediump float;
uniform sampler2D texture;
void main(){
vec4 color = texture2D(texture, gl_PointCoord);
gl_FragColor = color;
}`;
那这个着色的代码就不对,需要修改一下,要指定纹理的坐标,纹理坐标是会变化的,所以还是先通过 attribute 传递给顶点着色器,在通过
varying 传给片元着色器,并且在片元着色器当中 texture2D 的第二个参数设置为传递来的纹理坐标
// 顶点着色器
let vertexString = `
attribute vec4 a_position;
attribute vec2 a_texture_coord;
varying vec2 v_texture_coord;
uniform mat4 proj;
void main(){
gl_Position = proj * a_position;
v_texture_coord = a_texture_coord;
}`;
// 片元着色器
let fragmentString = `
precision mediump float;
uniform sampler2D texture;
varying vec2 v_texture_coord;
void main(){
vec4 color = texture2D(texture, v_texture_coord);
gl_FragColor = color;
}`;
调整坐标,每一行都是一个点的位置,其中六个坐标分别代表 x,y,z,纹理坐标 u,v,其中 u 和 v 的范围是 0~1,所以需要把 x,y,z
坐标除以纹理大小,得到纹理坐标
// 格式化的时候总把这个数组弄乱,我注释了先
// let triangleArray = [
// 0, 0, 0, 1.0, 0, 0,
// 0, triangleSize, 0, 1.0, 0, 1,
// triangleSize, 0, 0, 1.0, 1, 0,
// triangleSize, 0, 0, 1.0, 1, 0,
// 0, triangleSize, 0, 1.0, 0, 1,
// triangleSize, triangleSize, 0, 1.0, 1, 1
// ];
然后就是参数传递,这里的参数传递不同的就是每个参数都要跳过 4 个,因为这里的顶点着色器传递的顶点坐标是 vec4,所以要跳过 4 个
let aTexCoord = webGL.getAttribLocation(program, "a_texture_coord");
webGL.enableVertexAttribArray(aTexCoord);
webGL.vertexAttribPointer(aTexCoord, 2, webGL.FLOAT, false, 6 * 4, 4 * 4);
最后加载图片绘制纹理就是相同的代码了,这里看一下效果
图片反转
现在是将 webGL 的坐标转换成了 canvas 的坐标系,但是如果用的是 webGL 的坐标系进行添加纹理贴图的时候就会发现图片是反的,这个时候就需要对图片进行反转,如下
- pixelStorei(pname, parmas)是用于图像预处理的函数
webGL.pixelStorei(webGL.UNPACK_FLIP_Y_WEBGL, true);
参数名 (pname ) |
描述 | 可选值 (param ) |
---|---|---|
PACK_ALIGNMENT | 指定打包(读取)像素数据时的字节对齐方式。 | 1, 2, 4, 8 |
UNPACK_ALIGNMENT | 指定解包(写入)像素数据时的字节对齐方式。 | 1, 2, 4, 8 |
UNPACK_FLIP_Y_WEBGL | 指定在解包图像数据时是否沿 Y 轴翻转图像。 | true 或 false |
UNPACK_PREMULTIPLY_ALPHA_WEBGL | 指定在解包图像数据时是否将颜色值与 Alpha 值预乘。 | true 或 false |
UNPACK_COLORSPACE_CONVERSION_WEBGL | 指定在解包图像数据时是否进行颜色空间转换(如从 sRGB 到线性 RGB)。 | gl.BROWSER_DEFAULT_WEBGL 或 gl.NONE |
让纹理动起来
在了解了纹理贴图之后,思考一下,如果让纹理动起来,比如让纹理滚动或者旋转,那应该如何实现呢?那就是改变纹理的坐标就可以实现纹理的运动效果。实现起来很简单的,修改片元着色器
let fragmentString = `
precision mediump float;
uniform sampler2D texture;
varying vec2 v_texture_coord;
uniform float u_translateX;
void main(){
vec4 color = texture2D(texture, vec2(v_texture_coord.x + u_translateX,v_texture_coord.y));
gl_FragColor = color;
}`;
// 缩放对应的变量和取值
`uniform float u_scale;
vec4 color = texture2D(texture, vec2(v_texture_coord.x * u_scale, v_texture_coord.y * u_scale));`;
在片元着色器里面,加了一个 uniform 变量 u_translateX,然后通过 uniform 传递给片元着色器,在 texture2D
中创建贴图的时候去修改其二维坐标的一个位置,最后在 js 当中修改传递进来的值。
当然其他的旋转、缩放效果也是一样的。去修改片元着色器的纹理坐标,然后通过 uniform 传递
let count = 0;
let scale = 1;
let maxScale = 4;
function textureAnimate() {
count += 0.005;
if (scale < maxScale) {
scale += 0.005;
}
if (scale >= maxScale) {
scale = 1;
}
let uTranslateX = webGL.getUniformLocation(program, "u_translateX");
webGL.uniform1f(uTranslateX, count);
let uScale = webGL.getUniformLocation(program, "u_scale");
webGL.uniform1f(uScale, scale);
draw();
requestAnimationFrame(textureAnimate);
}
多重纹理
既然单层纹理已经搞定了,那么多层纹理是不是往里面再加一个纹理,再通过 js 赋值就完事了呢?照着这个思路来改代码,先调整片元着色器,往里面再加一个
texture
let fragmentString = `
precision mediump float;
uniform sampler2D texture1;
uniform sampler2D texture2;
varying vec2 v_texture_coord;
void main(){
vec4 color1 = texture2D(texture1, v_texture_coord);
vec4 color2 = texture2D(texture2, v_texture_coord);
gl_FragColor = color1+color2;
}`;
找到加载图片作为纹理的入口,把 texture1 和 texture2 赋值给 uTexture1 和 uTexture2,这几个变量都定义了全局变量,我这省去了
uTexture1 = webGL.getUniformLocation(program, 'texture1');
uTexture2 = webGL.getUniformLocation(program, 'texture2');
texture1 = initTexture('../assets/image/send.png');
texture2 = initTexture('../assets/image/man.png');
最后调用draw方法,这里的draw方法也要进行调整。
- 通过activeTexture激活纹理单元,
- 然后绑定纹理对象,
- 最后再把纹理单元传递给片元着色器。
- uniform1i传递的第二个参数(0和1)分别表示纹理单元的索引
function draw() {
webGL.clearColor(0, 0, 1, 1);
webGL.clear(webGL.COLOR_BUFFER_BIT);
webGL.enable(webGL.DEPTH_TEST);
webGL.activeTexture(webGL.TEXTURE0);
webGL.bindTexture(webGL.TEXTURE_2D, texture1);
webGL.uniform1i(uTexture1, 0);
webGL.activeTexture(webGL.TEXTURE1);
webGL.bindTexture(webGL.TEXTURE_2D, texture2);
webGL.uniform1i(uTexture2, 1);
webGL.drawArrays(webGL.TRIANGLES, 0, triangleArray.length / 6);
requestAnimationFrame(draw);
}
注意:在调用draw方法的时候要先让两个纹理都加载完了之后再调用。