WebGL图形编程实战【2】:动态着色 × 纹理贴图技术揭秘

发布于:2025-03-24 ⋅ 阅读:(25) ⋅ 点赞:(0)

上一篇文章: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 传递变量需要以下五个步骤、

创建缓冲区对象
绑定Buffer
将数据写入到缓冲区对象
将缓冲区对象分配给attribute变量
开启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 值
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 二维数组纹理
  • 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 轴翻转图像。 truefalse
UNPACK_PREMULTIPLY_ALPHA_WEBGL 指定在解包图像数据时是否将颜色值与 Alpha 值预乘。 truefalse
UNPACK_COLORSPACE_CONVERSION_WEBGL 指定在解包图像数据时是否进行颜色空间转换(如从 sRGB 到线性 RGB)。 gl.BROWSER_DEFAULT_WEBGLgl.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方法的时候要先让两个纹理都加载完了之后再调用。
在这里插入图片描述