WebGL图像处理
1 前言
上一节中,使用了varying绘制了彩色三角形,具体参考该文章:WebGL基础-变量控制与颜色修改。
但更多的场景中,不能使用简单的颜色来展示,具有复杂的图形展示需求,这个时候就需要使用纹理贴图来实现。
WebGL探索系列目录
:传送门
2 纹理基础
纹理映射是将一张图像映射到一个几何图形的表面,将一张真实世界的图片贴到一个由两个三角形组成的矩形上,矩形上看上去是这张图片。这张图片称为纹理图像
或纹理
。
纹理映射就是根据纹理,为光栅化后的图像图上合适的颜色,组成纹理图的像素又被称作纹素
。
纹理映射的步骤
1、准备好纹理。
2、为纹理图像配置映射方式
3、加载纹理图像,对其进行一定的配置,以在WebGL中使用。
4、在片元着色器中将相应的纹素从纹理中抽取出来,并将纹素的颜色赋给片元。
下面写一个程序,实现纹理的加载,主要为五个部分:
1、顶点着色器中接收顶点的纹理坐标,光栅化后传递给片元着色器。
2、片元着色器根据片元的纹理坐标,从纹理图像中抽取出纹素颜色,赋给当前片元。
3、设置顶点的纹理坐标。
4、准备待加载的纹理图像,令浏览器读取它。
5、监听纹理图像的加载时间,一旦加载完成,就在WebGL系统中使用纹理。
2.1 定义顶点着色器与片元着色器
// 顶点着色器
const vertexShaderSource = `
// 定义位置与a_texCoord
attribute vec2 a_position;
attribute vec2 a_texCoord;
// 定义canvas的分辨率
uniform vec2 u_resolution;
// 传递一个变量给片元着色器
varying vec2 v_texCoord;
void main() {
// 改变位置为0-1
vec2 zeroToOne = a_position / u_resolution;
// 改变 0->1 为 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// 改变 0->2 为 -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
// 将a_texCoord赋值给全局变量的v_texCoord
v_texCoord = a_texCoord;
}
`;
// 片元着色器
const fragmentShaderSource = `
// 开启高分辨率
precision highp float;
// 定义灰度图与图例
uniform sampler2D u_image;
// 顶着色器传入的v_texCoord
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_image, v_texCoord);
}
`;
2.2 加载图片
加载图片,该项目为vue,将图片资源放置于public
文件夹中,加载该图片,在图片加载完成后运行render函数,并将加载的图片传入进去。
// 加载图片
const image = new Image();
image.src = "./leaves.png";
image.onload = function() {
render(image);
};
2.3 获取变量并开启attribute的变量
获取顶点的变量和纹理坐标变量,获取分辨率和图片的全局变量
// 获取顶点着色器的attribute变量
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
const texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord");
// 获取全局变量
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
const imageLocation = gl.getUniformLocation(program, "u_image");
// 开启attribute属性
gl.enableVertexAttribArray(positionAttributeLocation);
gl.enableVertexAttribArray(texCoordAttributeLocation);
2.4 为纹理坐标赋值
// 提供矩形的纹理坐标。
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
1.0, 0.0,
1.0, 1.0,
]), gl.STATIC_DRAW);
gl.vertexAttribPointer(texCoordAttributeLocation, 2, gl.FLOAT, false, 0, 0);
2.5 创建纹理并将图像加载到纹理中
2.5.1 创建纹理
首先使用createTexture
创建一个texture
。随后将纹理绑定到目标,这里使用了TEXTURE_2D
,定义了二维纹理,还可以使用TEXTURE_CUBE_MAP
,表示立体映射纹理。
// 创建一个纹理
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
2.5.2 配置纹理参数
绑定完后使用gl.texParameteri
函数配置纹理参数。第一个参数有两个选择TEXTURE_2D
与TEXTURE_CUBE_MAP
。
第二个参数:有4个纹理参数。
(1)TEXTURE_MAX_FILTER
:放大方法默认: gl.LINEAR
(2)TEXTURE_MIN_FILTER
:缩小方法默认: gl.NEAREST_MIPMAP_LINEAR
(3)TEXTURE_WRAP_S
:水平填充方法默认: gl.REPEAT
(4)TEXTURE_WRAP_T
:垂直填充方法默认: gl.REPEAT
第三个参数,设置第二个参数的值。
gl.LINEAR
:使用距离新像素中心最近的四个像素的颜色值得加权值平均(图片质量好,但是开销大)
gl.NEAREST_MIPMAP_LINEAR
:使用原纹理上的距离映射后像素中心最近的那个像素的颜色值作为新像素的值
gl.rEPEAT
:平铺式的重复纹理
gl.REPEAT
:镜像对称式的重复纹理
gl.REPEAT
:使用纹理图像边缘值
// 设置参数,这样就不需要mips
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);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
2.5.3 配置纹理参数
再使用texImage2D
函数将图像加载到纹理中,该函数有六个参数。
第一个参数:指定纹理的绑定对象,有两个选择,TEXTURE_2D
与TEXTURE_CUBE_MAP
。
第二个参数:默认为0,0 级是基本图像等级,n 级是第 n 个金字塔简化级。
第三个参数:指定纹理中的颜色组件,可选gl.RGB(红绿蓝),gl.RGBA(红绿蓝透明度),gl.ALPHA(0.0,0.0,0.0,透明度),gl.LUMINANCE(流明),LUMINANCE_ALPHA(L,L,L,透明度)。
第四个参数:与第三个参数的值相同。
第五个参数:纹理的数据类型。UNSIGNED_BYTE
,每个颜色分量占据1字节。UNSIGNED_SHORT_5_6_5
:表示RGB,每一个分量分别占据5,6,5比特。UNSIGNED_SHORT_4_4_4_4
:表示RGBA,每一个分量分别占据4,4,4,4比特。UNSIGNED_SHORT_5_5_5_1
:表示RBGA,每一个分量分别占据5,5,5,1比特。
第六个参数:纹理图像的image图像。
// 将图像上载到纹理中。
const mipLevel = 0; // 最大的mip
const internalFormat = gl.RGBA; // 我们想要的纹理格式
const srcFormat = gl.RGBA; // 我们提供的数据格式
const srcType = gl.UNSIGNED_BYTE; // 我们提供的数据类型
gl.texImage2D(gl.TEXTURE_2D, mipLevel, internalFormat, srcFormat, srcType, image);
2.5.4 将0号纹理图像传递给着色器
告诉着色器从纹理单元0获取纹理,并激活纹理。
// 告诉着色器从纹理单元0获取纹理
gl.uniform1i(imageLocation, 0);
// 将每个纹理单位设置为使用特定纹理。
gl.activeTexture(gl.TEXTURE0);
2.6 为顶点坐标赋值
创建一个新的buffer,载入数据,设置顶点取值的方式。
// 创建buffer
const positionBuffer = gl.createBuffer();
// 将位置缓冲区绑定为将被调用的gl.bufferData
// 在setRectangle中,将数据放入位置缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// positionBuffer如何获取数据
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
// setRectangle的函数定义如下
function setRectangle(gl: WebGLRenderingContext, x: number, y: number, width: number, height: number) {
const x1 = x;
const x2 = x + width;
const y1 = y;
const y2 = y + height;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
x1, y1,
x2, y1,
x1, y2,
x1, y2,
x2, y1,
x2, y2,
]), gl.STATIC_DRAW);
}
2.7 使用gl.drawArrays绘制
最后使用gl.drawArrays
绘制即可。
// 绘制矩形
gl.drawArrays(gl.TRIANGLES, 0, 6);
2.8 运行与展示
该文件的代码综合为:
<template>
<canvas id="canvas"></canvas>
</template>
<script setup lang="ts">
import { initShaders, resizeCanvasToDisplaySize } from '@/utils/webgl/module/webgl-utils';
onMounted(() => {
const vertexShaderSource = `
// 定义位置与a_texCoord
attribute vec2 a_position;
attribute vec2 a_texCoord;
// 定义canvas的分辨率
uniform vec2 u_resolution;
// 传递一个变量给片元着色器
varying vec2 v_texCoord;
void main() {
// 改变位置为0-1
vec2 zeroToOne = a_position / u_resolution;
// 改变 0->1 为 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// 改变 0->2 为 -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
// 将a_texCoord赋值给全局变量的v_texCoord
v_texCoord = a_texCoord;
}
`;
const fragmentShaderSource = `
// 开启高分辨率
precision highp float;
// 定义灰度图与图例
uniform sampler2D u_image;
// 顶着色器传入的v_texCoord
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_image, v_texCoord);
}
`;
const image = new Image();
image.src = "./leaves.png"; // MUST BE SAME DOMAIN!!!
image.onload = function() {
render(image);
};
function render(image: HTMLImageElement) {
// 获取WebGL的context
const canvas = document.querySelector("#canvas") as HTMLCanvasElement
const gl = canvas.getContext("webgl");
if (!gl) return;
// 创建program
const program = initShaders(gl, vertexShaderSource, fragmentShaderSource)
if(!program) return
// 获取顶点着色器的attribute变量
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
const texCoordAttributeLocation = gl.getAttribLocation(program, "a_texCoord");
// 获取全局变量
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
const imageLocation = gl.getUniformLocation(program, "u_image");
// 开启attribute属性
gl.enableVertexAttribArray(positionAttributeLocation);
gl.enableVertexAttribArray(texCoordAttributeLocation);
// 提供矩形的纹理坐标。
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
1.0, 0.0,
1.0, 1.0,
]), gl.STATIC_DRAW);
gl.vertexAttribPointer(texCoordAttributeLocation, 2, gl.FLOAT, false, 0, 0);
// 创建一个纹理
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// 设置参数,这样就不需要mips
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);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// 将图像加载到纹理中。
const mipLevel = 0; // 最大的mip
const internalFormat = gl.RGBA; // 我们想要的纹理格式
const srcFormat = gl.RGBA; // 我们提供的数据格式
const srcType = gl.UNSIGNED_BYTE; // 我们提供的数据类型
gl.texImage2D(gl.TEXTURE_2D, mipLevel, internalFormat, srcFormat, srcType, image);
// 告诉着色器从纹理单元0获取纹理
gl.uniform1i(imageLocation, 0);
// 将每个纹理单位设置为使用特定纹理。
gl.activeTexture(gl.TEXTURE0);
// 调整画布的大小以匹配其显示的大小。
resizeCanvasToDisplaySize(gl.canvas);
// 告诉WebGL如何从剪辑空间转换为像素
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// 清空画布
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 传入画布分辨率,以便我们可以从
// 着色器中要剪裁的像素
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
// 创建buffer
const positionBuffer = gl.createBuffer();
// 将位置缓冲区绑定为将被调用的gl.bufferData
// 在setRectangle中,将数据放入位置缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// positionBuffer如何获取数据
gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0);
// 设置一个与图像大小相同的矩形。
setRectangle(gl, 0, 0, image.width, image.height);
// 绘制矩形
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
function setRectangle(gl: WebGLRenderingContext, x: number, y: number, width: number, height: number) {
const x1 = x;
const x2 = x + width;
const y1 = y;
const y2 = y + height;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
x1, y1,
x2, y1,
x1, y2,
x1, y2,
x2, y1,
x2, y2,
]), gl.STATIC_DRAW);
}
})
</script>
<style scoped lang="scss">
canvas {
width: 100%;
height: 100%;
display: block;
}
</style>
运行后得到的结果为: