前言
该章节将通过以下三个问题展开讨论
1、 将顶点的其它数据——如颜色等——传入顶点着色器
2、发生在顶点着色器和片元着色器之间的从图形到片元的转化,又称为图元光栅化
3、将图像(或称纹理)映射到图形或三维对象的表面上
1 绑定多个缓冲区
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
#canvas {
width: 600px;
height: 600px;
position: absolute;
top: calc(50% - 300px);
left: calc(50% - 300px);
background-color: black;
}
</style>
<body>
<canvas id="canvas"></canvas>
<script>
// 通过元素获取二维图形的绘图上下文
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl");
// 定义顶点着色器
const vertexShaderSource = `
attribute vec4 a_Position;
attribute float a_PointSize;
void main() {
gl_Position = a_Position; // 设置点的坐标
gl_PointSize = a_PointSize; // 设置点的大小
}
`;
// 定义片段着色器
const fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 设置点的颜色为红色
}
`;
// 编译着色器函数
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
// 创建和链接着色器程序
const vertexShader = createShader(
gl,
gl.VERTEX_SHADER,
vertexShaderSource
);
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
fragmentShaderSource
);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
// 设置顶点数据
const vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]);
// 创建缓冲区,給a_Position使用
const buffer1 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer1);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 连接顶点着色器的a_Position变量
// 获取a_Position变量存储位置
const a_Position = gl.getAttribLocation(program, "a_Position");
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
// 创建缓冲区,給gl_PointSize使用
const buffer2 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer2);
// 设置顶点数据
const sizes = new Float32Array([
10.0,
20.0,
30.0, // 在画布中心绘制一个点
]);
gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STATIC_DRAW);
// 连接顶点着色器的gl_PointSize变量
// 获取gl_PointSize变量存储位置
const a_PointSize = gl.getAttribLocation(program, "a_PointSize");
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_PointSize);
// 设置背景颜色并清空
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制点 这里只获取一个顶点
gl.drawArrays(gl.POINTS, 0, 3);
</script>
</body>
</html>
效果图
顶点着色器是如何执行缓冲区数据的,存在多个缓冲区如何指定?
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0)将指向当前绑定的缓冲区,之前的缓冲区会被暂存起来,当调用drawArrays时,就会缓冲区中获取数据。
利用两个缓冲区来分别为顶的顶点a_Position和大小a_PointSize分配数据,有带你麻烦,可以将a_Position和大小a_PointSize的数据写入同一个缓冲区:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
#canvas {
width: 600px;
height: 600px;
position: absolute;
top: calc(50% - 300px);
left: calc(50% - 300px);
background-color: black;
}
</style>
<body>
<canvas id="canvas"></canvas>
<script>
// 通过元素获取二维图形的绘图上下文
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl");
// 定义顶点着色器
const vertexShaderSource = `
attribute vec4 a_Position;
attribute float a_PointSize;
void main() {
gl_Position = a_Position; // 设置点的坐标
gl_PointSize = a_PointSize; // 设置点的大小
}
`;
// 定义片段着色器
const fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 设置点的颜色为红色
}
`;
// 编译着色器函数
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
// 创建和链接着色器程序
const vertexShader = createShader(
gl,
gl.VERTEX_SHADER,
vertexShaderSource
);
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
fragmentShaderSource
);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
// 设置顶点数据
const vertices = new Float32Array([
0.0, 0.5, 10.0, -0.5, -0.5, 20.0, 0.5, -0.5, 30.0,
]);
// 创建缓冲区,給a_Position使用
const buffer1 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer1);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 连接顶点着色器的a_Position变量
// 获取a_Position变量存储位置
const a_Position = gl.getAttribLocation(program, "a_Position");
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 3 * 4, 0);
gl.enableVertexAttribArray(a_Position);
// 连接顶点着色器的gl_PointSize变量
// 获取gl_PointSize变量存储位置
const a_PointSize = gl.getAttribLocation(program, "a_PointSize");
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 3 * 4, 2 * 4);
gl.enableVertexAttribArray(a_PointSize);
// 设置背景颜色并清空
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制点 这里只获取一个顶点
gl.drawArrays(gl.POINTS, 0, 3);
</script>
</body>
</html>
gl.vertexAttribPointer
| location | 变量存储位置 |
|---|---|
| size | 变量分量个数,示例中点的分量为2 |
| type | 数据格式 |
| normalize | 是否将非浮点数的数据归一化到[0, 1]或者[-1, 1] |
| stride | 指定相邻顶点之间的字节数(Float32Array为4字节) |
| offset | 从缓冲区的何处开始 |
2 彩色三角形
这里先抛出一个问题:前面的三角形如何被决定的,为什么是红色?
前面绘制三角形的实例, gl.drawArrays(gl.TRIANGLES, 0, 3);声明了绘制一个三角形,用到了三个顶点,那么在顶点着色器中会陆续冲缓冲区中获取三个顶点数据,有这三个顶点组成一个三角形?在片元着色器中,我们指定了rgb红色给gl_FragColor,他是怎么填充三角形的?
顶点坐标、图元装配、光栅化、执行片元着色器过程
顶点着色器和片段着色器之间有这两个步骤
图形装配过程:这一步的任务就是将孤立的顶点坐标装配成几何图形。集合图形的类型由drawArrays参数指定;
光栅化过程:这一步的任务就是将装配好的几何图形转化为片元;
当执行顶点着色器的时候,每从缓冲区获取到一个顶点,就会将顶点存在装配区,获取三次,就会将三个顶点数据存在装配区,由装配区的顶点装配出一个三角形。
光栅化结束之后,程序开始逐片元调用着色器,有多少个片元就会调用多少次。对于每个片元片元着色器计算出该片元的颜色,并写入颜色缓冲区。
光栅化过程生成的片元都是带有坐标信息的,调用片元着色器时,这些坐标信息也随着片元传递过去。
代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
#canvas {
width: 600px;
height: 600px;
position: absolute;
top: calc(50% - 300px);
left: calc(50% - 300px);
background-color: black;
}
</style>
<body>
<canvas id="canvas"></canvas>
<script>
// 通过元素获取二维图形的绘图上下文
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl");
// 定义顶点着色器
var VSHADER_SOURCE =
"attribute vec4 a_Position;\n" +
"void main() {\n" +
" gl_Position = a_Position;\n" +
"}\n";
// 定义片段着色器
var FSHADER_SOURCE =
"precision mediump float;\n" +
"uniform float u_Width;\n" +
"uniform float u_Height;\n" +
"void main() {\n" +
" gl_FragColor = vec4(gl_FragCoord.x/u_Width, 0.0, gl_FragCoord.y/u_Height, 1.0);\n" +
"}\n";
// 编译着色器函数
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
// 创建和链接着色器程序
const vertexShader = createShader(gl, gl.VERTEX_SHADER, VSHADER_SOURCE);
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
FSHADER_SOURCE
);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
// 设置顶点数据
const vertices = new Float32Array([0, 0.5, -0.5, -0.5, 0.5, -0.5]);
// 创建缓冲区,給a_Position使用
const buffer1 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer1);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 连接顶点着色器的a_Position变量
// 获取a_Position变量存储位置
const a_Position = gl.getAttribLocation(program, "a_Position");
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
var u_Width = gl.getUniformLocation(program, "u_Width");
var u_Height = gl.getUniformLocation(program, "u_Height");
// 颜色缓冲区宽度
gl.uniform1f(u_Width, gl.drawingBufferWidth);
// 颜色缓冲区高度
gl.uniform1f(u_Height, gl.drawingBufferHeight);
// 设置背景颜色并清空
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制点 这里只获取一个顶点
gl.drawArrays(gl.TRIANGLES, 0, 3);
</script>
</body>
</html>
效果图
2.1 varying变量的作用和内插过程
代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
#canvas {
width: 600px;
height: 600px;
position: absolute;
top: calc(50% - 300px);
left: calc(50% - 300px);
background-color: black;
}
</style>
<body>
<canvas id="canvas"></canvas>
<script>
// 通过元素获取二维图形的绘图上下文
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl");
// 定义顶点着色器
var VSHADER_SOURCE =
"attribute vec4 a_Position;\n" +
"attribute vec4 a_Color;\n" +
"varying vec4 v_Color;\n" +
"void main() {\n" +
" gl_Position = a_Position;\n" +
" v_Color = a_Color;\n" +
"}\n";
// Fragment shader program
var FSHADER_SOURCE =
"precision mediump float;\n" +
"varying vec4 v_Color;\n" +
"void main() {\n" +
" gl_FragColor = v_Color;\n" +
"}\n";
// 编译着色器函数
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
// 创建和链接着色器程序
const vertexShader = createShader(gl, gl.VERTEX_SHADER, VSHADER_SOURCE);
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
FSHADER_SOURCE
);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
// 设置顶点数据
const vertices = new Float32Array([
// 顶点数据和color
0.0, 0.5, 1.0, 0.0, 0.0, -0.5, -0.5, 0.0, 1.0, 0.0, 0.5, -0.5, 0.0, 0.0,
1.0,
]);
// 创建缓冲区,給a_Position使用
const buffer1 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer1);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 连接顶点着色器的a_Position变量
// 获取a_Position变量存储位置
const a_Position = gl.getAttribLocation(program, "a_Position");
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 4 * 5, 0);
gl.enableVertexAttribArray(a_Position);
var a_Color = gl.getAttribLocation(program, "a_Color");
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, 4 * 5, 4 * 2);
gl.enableVertexAttribArray(a_Color);
// 设置背景颜色并清空
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制点 这里只获取一个顶点
gl.drawArrays(gl.TRIANGLES, 0, 3);
</script>
</body>
</html>
效果
为什么只是给三个顶点指定了颜色,最后会生成一个彩色三角形?
事实上,我们把顶点的颜色复制给了顶点着色器的varying变量v_Color,它的值被传给了片元着色器中的同名、同类型的变量(既片元着色器中也有verying变量v_Color),准确的说,顶点着色器中的变量v_Color在传入片元着色器之前经过了内插过程,所以片元着色器中的v_Color和顶点着色器中的v_Color并不是一回事,彩色三角形各个片元的颜色都是通过顶点颜色内插出来的。
考虑一条线段
RGBA的R值从1.0降低到0.0,而这个B的值从0.0上升到1.0,线上的所有片元的颜色都被计算出来,这个过程就被称为内插过程
2.1.1 varying变量的作用
在 WebGL 中,varying 类型的变量用于在 顶点着色器 和 片段着色器 之间传递数据。具体来说,varying 变量从顶点着色器输出,然后在光栅化过程中,它的值会在顶点之间进行插值,最终传递给片段着色器中的对应变量,用于片段级别的操作。
变量的作用和工作机制
- 在顶点着色器中定义输出:在顶点着色器中,变量通常用于存储与每个顶点相关联的属性值(如颜色、纹理坐标、法线等),并将其作为输出传递给片段着色器。
- 插值过程:当顶点着色器将顶点传递给光栅化阶段时,WebGL 会对变量进行插值,从而为三角形内的每个片段(像素)生成一个基于顶点间插值的值。例如,如果顶点着色器输出的颜色是顶点 A、B、C 的颜色,那么在片段着色器中,三角形内的每个像素的颜色会是根据顶点颜色计算出来的插值结果。
- 在片段着色器中使用插值结果:片段着色器中使用相同名称的变量来接收光栅化阶段插值后的值,进而对每个片段进行进一步的颜色或光照计算。
变量的定义
- 顶点着色器变量在顶点着色器中用于输出。例如,输出颜色或纹理坐标。
- 片段着色器变量在片段着色器中用于输入,用以获取经过插值后的数据。
当顶点着色器与片断着色器中有类型和命名都相同的varying变量,顶点着色器赋值给该变量的值就会自动被传入片断着色器。
2.1.2 顶点着色器到片段着色器的插值概述
在 WebGL(以及大部分基于 GPU 的图形渲染管线中),着色器分为多个阶段:
- 顶点着色器:处理每个顶点的属性(如位置、颜色等),输出顶点的变换后位置及相关属性。
- 光栅化阶段:将顶点组成的几何体(如三角形)划分为片段(Fragment),并对顶点属性(如颜色、法线、纹理坐标等)进行插值。
- 片段着色器:处理每个片段的属性,并计算出每个片段的最终颜色。
在三角形光栅化过程中,顶点着色器为三角形的每个顶点输出属性(如颜色),而在片段着色器中,这些属性会根据片段(像素)在三角形内部的位置,通过插值算法计算出来。
插值过程的核心
最常用的插值方式是 透视校正线性插值(Perspective-correct interpolation),它能够在透视投影下正确地插值片段数据。对于每一个片段,颜色值会基于三角形三个顶点的颜色进行加权平均。
假设三角形的三个顶点分别是 v0、v1、v2,它们对应的颜色分别是 C0、C1、C2。在三角形内部任意一点(片段)的位置可以用 重心坐标(Barycentric Coordinates)表示为 λ0, λ1, λ2,这些坐标是通过片段与三个顶点的相对位置计算出来的。
片段颜色 CCC 可以通过以下公式计算:
C=λ0C0+λ1C1+λ2C2C
这里:
- λ0+λ1+λ2=1 (重心坐标的性质)
- λ0, λ1, λ2 是片段到每个顶点的权重,决定了颜色的插值方式。
透视校正插值 vs 线性插值
- 线性插值(Linear Interpolation)
在没有透视投影的情况下,WebGL 默认会对颜色属性进行 线性插值。即,片段的颜色值是直接基于屏幕空间中顶点颜色的线性权重计算的。在这种情况下,插值过程不考虑投影矩阵的影响。
- 透视校正插值(Perspective-Correct Interpolation)
在透视投影的场景下,直接进行线性插值会导致视觉错误,例如颜色变形或失真。为了解决这个问题,WebGL 采用了 透视校正插值。它会先将属性值除以顶点的齐次坐标 w 分量(从投影矩阵计算得到),再进行插值,然后在片段着色器中将插值后的结果重新乘以片段的 w。
这是因为透视投影会导致距离远的对象在屏幕空间中看起来更小,因此片段的插值必须考虑这种透视效果,否则就会出现视觉上的错误。
3 在矩形表面贴上图像
纹理映射:将一张图片贴在一个几何图形表面
在WebGL中,进行纹理映射,需要进行以下几步
- 准备图像
- 为几何图形配置纹理映射方式
- 加载纹理图像,对齐进行一些配置,以在WebGL中使用
- 在片段着色器中将相应的纹素从纹理中抽取出来,并将纹素的颜色赋给片元
3.1 纹理坐标
纹理坐标是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色,在WebGL中使用s和t或者u和v来描述纹理坐标。纹理图像坐标由(-1,-1)到(1,1)。
u_Sampler意为取样器,纹理像素是有大小的,取样处的纹理坐标很可能并不是落在某个像素中心,所以取样通常并不是直接取纹理图像某个像素的颜色,而是通过附件的若干个像素共同计算得到。
代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
#canvas {
width: 600px;
height: 600px;
position: absolute;
top: calc(50% - 300px);
left: calc(50% - 300px);
background-color: black;
}
</style>
<body>
<canvas id="canvas"></canvas>
<script>
// 定义顶点着色器
var VSHADER_SOURCE =
"attribute vec4 a_Position;\n" +
"attribute vec2 a_TexCoord;\n" +
"varying vec2 v_TexCoord;\n" +
"void main() {\n" +
" gl_Position = a_Position;\n" +
" v_TexCoord = a_TexCoord;\n" +
"}\n";
// Fragment shader program
var FSHADER_SOURCE =
"#ifdef GL_ES\n" +
"precision mediump float;\n" +
"#endif\n" +
"uniform sampler2D u_Sampler;\n" +
"varying vec2 v_TexCoord;\n" +
"void main() {\n" +
" gl_FragColor = texture2D(u_Sampler, v_TexCoord);\n" +
"}\n";
function main() {
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl");
// 初始化着色器
initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);
// 初始化buffer
var n = initVertexBuffers(gl);
// 清除canvas
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 设置纹理
initTextures(gl, n);
}
function createProgram(gl, vshader, fshader) {
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
return program;
}
function initShaders(gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
gl.useProgram(program);
gl.program = program;
return true;
}
function loadShader(gl, type, source) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
return shader;
}
function initVertexBuffers(gl) {
var verticesTexCoords = 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,
]);
var n = 4;
var vertexTexCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);
var FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;
var a_Position = gl.getAttribLocation(gl.program, "a_Position");
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
gl.enableVertexAttribArray(a_Position);
// 将纹理坐标分配给a_TexCoord,并开启他
var a_TexCoord = gl.getAttribLocation(gl.program, "a_TexCoord");
gl.vertexAttribPointer(
a_TexCoord,
2,
gl.FLOAT,
false,
FSIZE * 4,
FSIZE * 2
);
gl.enableVertexAttribArray(a_TexCoord);
return n;
}
function initTextures(gl, n) {
var texture = gl.createTexture();
// 获取u_Sampler存储位置
var u_Sampler = gl.getUniformLocation(gl.program, "u_Sampler");
var image = new Image();
// 注册图像加载时间的响应函数
image.onload = function () {
loadTexture(gl, n, texture, u_Sampler, image);
};
// 浏览器开始加载图像
image.src = "./asset/OIP.jpg";
return true;
}
function loadTexture(gl, n, texture, u_Sampler, image) {
// 对纹理图像进行y轴反转:图片跟纹理y轴方向不一样
// pixelStorei 用于描述像素存储模式的函数
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 开启 0号纹理单元
// WebGL通过一种纹理单元的机制来同时使用多个纹理,每个纹理都有一个单眼编号来管理一张纹理图像,默认支持8个纹理单元
gl.activeTexture(gl.TEXTURE0);
// 向target绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture);
// 配置纹理参数
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.LINEAR);
// 将纹理图像分配给纹理对象
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
image
);
// 将0号纹理传递给着色器
gl.uniform1i(u_Sampler, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);
}
main();
</script>
</body>
</html>
效果图
在 WebGL 中,当你使用纹理时,纹理的坐标 (s, t)(也称为 UV 坐标)通常位于 [0, 1] 的范围内。但在某些情况下,纹理坐标可能超出这个范围,需要指定纹理如何处理这些坐标。WebGL 提供了三种常用的纹理重复模式,它们是通过方法设置的。具体来说,它们控制了当纹理坐标超出 [0, 1] 范围时,纹理是如何被映射的。
注意: WebGL加载图像时,由于没有开启本地服务器,检测不到域名,会判断为跨域,因此需要开启本地开发服务器,可以直接采用vite脚手架搭建一个本地开发服务器,将index.html复制过去。
3.2 超出纹理坐标[0, 1]的情况
WebGL支持2 的幂次方纹理,比如256,当使用非 2 的幂次方纹理时,WebGL 会强制应用 gl.CLAMP_TO_EDGE,即使你设置了 gl.REPEAT。因此在这种情况下,纹理会按照 gl.CLAMP_TO_EDGE 的行为处理,而不会重复显示。
3.2.1 纹理映射时手动设置的坐标超过 [0, 1]
在 WebGL 中,纹理坐标(通常称为 UV 坐标)被定义在 [0, 1] 的范围内。然而,如果程序在定义顶点或片段时给定的纹理坐标超过了这个范围,就会导致坐标超出。常见的情况如下:
- 纹理平铺效果: 为了让纹理重复,程序员可能故意将纹理坐标设置为超过 [0, 1],例如设置为 (1.5, 1.5) 或者 (-0.5, -0.5),以使纹理在某个表面上重复多次。
- 手动设置的 UV 坐标:
var verticesTexCoords = new Float32Array([
-0.5, 1.5, // 顶点1的纹理坐标
1.5, -0.5, // 顶点2的纹理坐标
]);
3.2.2 模型或几何体的大小比例导致纹理坐标超出
当几何体被缩放、旋转或移动时,应用于该几何体的纹理坐标可能会超出 [0, 1] 范围。例如,假设你有一个二维的矩形,你将其缩放或拉伸到比原始尺寸更大。此时,尽管原始的纹理坐标在 [0, 1] 之间,但由于几何体的变化,纹理坐标映射的区域会超出这个范围。
- 几何体放大: 当几何体被拉伸时,纹理坐标会跟随几何体变化,从而可能超过 [0, 1]。
- 例如在缩放的矩形上使用纹理:
var scalingMatrix = mat4.create();
mat4.scale(scalingMatrix, scalingMatrix, [2.0, 2.0, 1.0]); // 放大 2 倍
3.2.3 程序对纹理进行平铺操作
在许多情况下,程序需要让纹理在大面积表面上重复出现,比如地板、墙壁等。为了实现这个效果,程序员可以手动增大纹理坐标,使其超出 [0, 1],以便在整个表面上均匀分布。例如,地面上的地砖图案需要重复数次。
- 地板平铺纹理: 在地面或墙面上进行平铺时,纹理坐标可能会被设置为大于 1,以便重复多次。
var verticesTexCoords = new Float32Array([
0, 0, // 左下角的纹理坐标
3, 0, // 右下角的纹理坐标,重复3次
0, 3, // 左上角的纹理坐标,重复3次
3, 3, // 右上角的纹理坐标,重复3次
]);
3.2.4 纹理坐标插值产生的超出
在 WebGL 中,顶点着色器和片段着色器之间会对顶点属性进行插值。如果顶点着色器中给定的纹理坐标在 [0, 1] 范围内,但由于几何形状或插值的原因,在片段着色器阶段的纹理坐标可能超出 [0, 1]。
- 插值超出: 例如,如果四个顶点的纹理坐标为 (0, 0), (2, 0), (0, 2), (2, 2),在插值过程中,中间的坐标可能会超出 [0, 1]。
- 插值示例:
varying vec2 v_TexCoord;
// 顶点着色器中,给某个顶点分配大于1的纹理坐标
v_TexCoord = a_TexCoord;
3.2.5 应用某种变换(旋转、缩放等)
在 WebGL 中应用几何变换,如旋转、缩放、平移等操作时,纹理坐标可能会超出 [0, 1]。例如,当你将一个四边形旋转到某个角度时,原本在 [0, 1] 之间的纹理坐标可能被旋转到超出这个范围。
- 旋转变换: 旋转几何体会导致部分纹理坐标超出。
var rotationMatrix = mat4.create();
mat4.rotateZ(rotationMatrix, rotationMatrix, Math.PI / 4); // 旋转 45 度
3.2.6 纹理坐标自动生成或采样时
当 WebGL 进行一些高级的纹理操作时,比如通过计算法线贴图或进行环境映射,纹理坐标有时会自动生成,并可能超出 [0, 1] 的范围。
- 自动生成的纹理坐标: 在环境映射或其他高级纹理映射技术中,程序会自动生成纹理坐标,这些坐标经常会超过 [0, 1]。
3.2.7 总结
纹理坐标会在以下情况超出 [0, 1]:
- 手动设置的纹理坐标超过 [0, 1]。
- 模型几何体的缩放或平铺导致坐标超过 [0, 1]。
- 进行插值计算时,插值结果可能超出 [0, 1]。
- 对几何体应用变换(如缩放、旋转等)。
- 通过某些自动生成的方式获得的纹理坐标。
这些超出的纹理坐标在 WebGL 中通过 gl.REPEAT、gl.MIRRORED_REPEAT和gl.CLAMP_TO_EDGE等纹理包装模式来处理。
3.3 gl.texParameteri参数详解
3.3.1 参数描述
| target | gl.TEXTURE_2D或者gl.TEXTURE_CUBE_MAP |
|---|---|
| pname(纹理参数) | 接受四种类型参数 gl.TEXTURE_WRAP_S gl.TEXTURE_WRAP_T gl.TEXTURE_MAG_FILTER gl.TEXTURE_MIN_FILTER |
| param(纹理参数值) | 参数值 |
可以分配给 gl.TEXTURE_MAG_FILTER 和 gl.TEXTURE_MIN_FILTER 的值
gl.NEAREST和gl.LINEAR
可以分配给 gl.TEXTURE_WRAP_S 和 gl.TEXTURE_WRAP_T 的值
gl.REPEAT、gl.MIRRORED_REPEA和gl.CLAMP_TO_EDGE
在WebGL 中,当你使用纹理时,纹理的坐标 (s, t)(也称为 UV 坐标)通常位于 [0, 1] 的范围内。但在某些情况下,纹理坐标可能超出这个范围,需要指定纹理如何处理这些坐标。WebGL 提供了三种常用的纹理重复模式,它们是通过 gl.texParameteri() 方法设置的。具体来说,它们控制了当纹理坐标超出 [0, 1] 范围时,纹理是如何被映射的。
3.3.2. gl.REPEAT
- 描述: 当纹理坐标超出 [0, 1] 时,纹理将重复平铺。这个模式会忽略超出的整数部分,仅保留小数部分。这种方式可以让纹理不断重复,从而创建无缝平铺的效果。
- 适用场景: 当你希望纹理能够在多个方向上无限重复时使用,比如在地面、墙壁等大面积表面平铺纹理。
- 示例: 如果纹理坐标
s = 1.5,实际采样的坐标将是s = 0.5,因为1.5 - 1 = 0.5,即忽略了整数部分。 - 效果示意:
0 1 2
────┼───┼───→ s
[image] [image] [image]
- 代码设置:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); // s方向重复
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); // t方向重复
3.3.3 gl.MIRRORED_REPEAT
- 描述: 当纹理坐标超出 [0, 1] 时,纹理会以镜像形式重复平铺。奇数次超出的部分会反转显示,类似镜像效果。
- 适用场景: 当需要平铺纹理但希望边界部分产生镜像效果时,可以使用该模式,例如在生成地形时避免接缝过于突兀。
- 示例: 如果纹理坐标
s = 1.5,实际采样的坐标将是s = 0.5,但如果s = 2.5,实际采样的坐标将是s = 0.5,因为2表示一次完整的重复,剩余部分根据镜像规则处理。 - 效果示意:
0 1 2
────┼───┼───→ s
[image] [mirrored-image] [image]
- 代码设置:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); // s方向镜像重复
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); // t方向镜像重复
3.3.4. gl.CLAMP_TO_EDGE
- 描述: 当纹理坐标超出 [0, 1] 时,WebGL 会使用纹理的边缘颜色来填充,而不是重复纹理。这意味着所有超过 1 的坐标会使用纹理右边缘的颜色,小于 0 的坐标会使用纹理左边缘的颜色。这个模式会阻止纹理的重复或镜像效果。
- 适用场景: 该模式在需要纹理平滑过渡到背景时很有用,比如天空盒或需要避免边界重复的场景。
- 示例: 如果纹理坐标
s = 1.2,实际采样的坐标将被夹到s = 1.0,也就是使用纹理的最右边缘部分。 - 效果示意:
0 1
────┼───→ s
[image][edge color]
- 代码设置:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); // s方向夹取边缘
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // t方向夹取边缘
3.3.5 gl.NEAREST(最近点采样)
gl.NEAREST 是最简单的纹理过滤模式。它选择与当前像素最接近的纹理像素(texel)作为结果,不进行插值。
- 特点:
- 直接选择最接近的纹素。
- 无需进行任何颜色混合或插值。
- 效果: 图像会显得比较块状或像素化,特别是在放大时明显,因为纹理直接使用最近的纹素颜色。
- 适用场景:
- 使用像素艺术风格时,保留清晰的像素边缘。
- 对性能要求较高的场景,因为
gl.NEAREST计算速度快,不需要额外插值计算。
- 示例:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
- 示例效果:
- 在放大图像时,纹理看起来有明显的像素块,边缘锐利。
- 在缩小时,图像看起来可能有跳跃感或明显失真。
3.3.6 gl.LINEAR(双线性插值)
gl.LINEAR 使用双线性插值进行纹理采样。它会考虑附近四个纹理像素(texel),并对它们的颜色进行加权平均,以计算最终颜色值。
- 特点:
- 对周围的多个纹素进行插值计算,最终生成平滑的颜色过渡。
- 效果: 图像的边缘会显得更加平滑,减少了像素化效果,但可能会导致模糊。
- 适用场景:
- 场景要求较高的视觉质量,特别是在纹理被放大或缩小时。
- 渲染较复杂的纹理,如照片或高清图像时,
gl.LINEAR能提供更好的视觉效果。
- 示例:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
- 示例效果:
- 在放大图像时,纹理会显得更柔和,没有明显的像素块,边缘平滑。
- 在缩小时,图像不会出现跳跃,但由于插值,会显得有点模糊。
3.3.6.1 对比 gl.NEAREST 和 gl.LINEAR
| 过滤模式 | 描述 | 优点 | 缺点 | 应用场景 |
|---|---|---|---|---|
| gl.NEAREST | 最近点采样 | 快速、适合像素风格 | 图像像素化、边缘生硬 | 像素艺术、低分辨率图形、高性能需求 |
| gl.LINEAR | 双线性插值 | 边缘平滑,过渡自然 | 图像可能模糊,性能稍差 | 高清图像、需要平滑过渡的场景 |
3.3.6.2 纹理过滤应用场景
- 放大过滤(Magnification Filter):当一个纹理在屏幕上被放大时,如何决定屏幕像素颜色。可以设置为
gl.NEAREST或gl.LINEAR。 - 缩小过滤(Minification Filter):当一个纹理在屏幕上被缩小时,如何决定屏幕像素颜色。对于缩小过滤,还可以结合使用多重贴图(Mipmap),即预先存储多个不同缩放级别的纹理图像。
示例代码
// 放大过滤使用 gl.LINEAR,缩小过滤使用 gl.NEAREST
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
3.3.6.3 总结
gl.NEAREST:最近邻采样,图像效果锐利但可能显得粗糙、像素化,适合像素风格或高性能要求的场景。gl.LINEAR:双线性插值采样,图像效果平滑、柔和,适合高清纹理或对图像质量要求较高的场景。
在实际应用中,可以根据具体的图像效果需求和性能要求,选择合适的过滤模式。
4 使用多幅纹理
WebGL支持多幅纹理,纹理单元就是为了这个目的设计的。
代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
#canvas {
width: 600px;
height: 600px;
position: absolute;
top: calc(50% - 300px);
left: calc(50% - 300px);
background-color: black;
}
</style>
<body>
<canvas id="canvas"></canvas>
<script>
// 定义顶点着色器
var VSHADER_SOURCE =
"attribute vec4 a_Position;\n" +
"attribute vec2 a_TexCoord;\n" +
"varying vec2 v_TexCoord;\n" +
"void main() {\n" +
" gl_Position = a_Position;\n" +
" v_TexCoord = a_TexCoord;\n" +
"}\n";
// 片段着色器
var FSHADER_SOURCE =
"#ifdef GL_ES\n" +
"precision mediump float;\n" +
"#endif\n" +
"uniform sampler2D u_Sampler0;\n" +
"uniform sampler2D u_Sampler1;\n" +
"varying vec2 v_TexCoord;\n" +
"void main() {\n" +
" vec4 color0 = texture2D(u_Sampler0, v_TexCoord);\n" +
" vec4 color1 = texture2D(u_Sampler1, v_TexCoord);\n" +
" gl_FragColor = color0 * color1;\n" +
"}\n";
function main() {
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl");
// 初始化着色器
initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);
// 初始化buffer
var n = initVertexBuffers(gl);
// 清除canvas
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 设置纹理
initTextures(gl, n);
}
function createProgram(gl, vshader, fshader) {
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
var program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
return program;
}
function initShaders(gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
gl.useProgram(program);
gl.program = program;
return true;
}
function loadShader(gl, type, source) {
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
return shader;
}
function initVertexBuffers(gl) {
var verticesTexCoords = 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,
]);
var n = 4;
var vertexTexCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);
var FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;
var a_Position = gl.getAttribLocation(gl.program, "a_Position");
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
gl.enableVertexAttribArray(a_Position);
// 将纹理坐标分配给a_TexCoord,并开启他
var a_TexCoord = gl.getAttribLocation(gl.program, "a_TexCoord");
gl.vertexAttribPointer(
a_TexCoord,
2,
gl.FLOAT,
false,
FSIZE * 4,
FSIZE * 2
);
gl.enableVertexAttribArray(a_TexCoord);
return n;
}
let g_texUnit0 = false;
let g_texUnit1 = false;
function initTextures(gl, n) {
var texture0 = gl.createTexture();
var texture1 = gl.createTexture();
// 获取u_Sampler存储位置
var u_Sampler0 = gl.getUniformLocation(gl.program, "u_Sampler0");
var u_Sampler1 = gl.getUniformLocation(gl.program, "u_Sampler1");
var image0 = new Image();
var image1 = new Image();
// 注册图像加载时间的响应函数
image0.onload = function () {
loadTexture(gl, n, texture0, u_Sampler0, image0, 0);
};
image1.onload = function () {
loadTexture(gl, n, texture1, u_Sampler1, image1, 1);
};
// 浏览器开始加载图像
image0.src = "./asset/OIP.jpg";
image1.src = "./asset/OIP1.jpg";
return true;
}
function loadTexture(gl, n, texture, u_Sampler, image, texUnit) {
// 对纹理图像进行y轴反转:图片跟纹理y轴方向不一样
// pixelStorei 用于描述像素存储模式的函数
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 开启 0号纹理单元
// WebGL通过一种纹理单元的机制来同时使用多个纹理,每个纹理都有一个单眼编号来管理一张纹理图像,默认支持8个纹理单元
if (texUnit == 0) {
gl.activeTexture(gl.TEXTURE0);
g_texUnit0 = true;
} else {
gl.activeTexture(gl.TEXTURE1);
g_texUnit1 = true;
}
// 向target绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture);
// 配置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 将纹理图像分配给纹理对象
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
image
);
// 将0号纹理传递给着色器
gl.uniform1i(u_Sampler, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 图片的加载是异步的,所以这里提供了两个全局变量来记录图片加载状态,待所有图片加载完毕之后再渲染视图
if (g_texUnit0 && g_texUnit1) {
gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);
}
}
main();
</script>
</body>
</html>
效果图