1993年有一款第一人称设计游戏《Doom》,是当时DOS系统下具有里程碑意义的第一人称设计游戏,虽然使用的是2.5D的技术,但成功的创造了一个具有深度干的三维空间,从游戏玩家的视角上看,Doom当时引入了一些新的游戏特性,比如高度差和地形差,墙壁再也不是吃豆豆那样垂直的了,所有的表面都有了材质贴图,以及光线的层次和阴影。
早期《Doom》对图形技术的贡献和影响:
- 光栅化技术:早期的《Doom》使用了一种称为光栅化的渲染技术,结合了2D精灵和3D空间中的平面。这种方法使得游戏能够在硬件有限的情况下实现相对复杂的3D环境。
- 软件渲染:游戏使用了软件渲染,计算图形效果(如阴影、光照)依赖于CPU而非GPU,这在当时是比较先进的做法。
- 分层技术:通过使用分层图层和贴图技术,《Doom》能够在不需要真正的3D模型的情况下创造出深度感和复杂性。这种设计为后来的图形引擎发展奠定了基础。
- 引擎创新:Doom引擎开创了很多后来的图形引擎设计理念,包括视锥体剔除、门和楼梯的实现等。这些技术为后续使用硬件加速和着色器的图形开发奠定了基础。
虽然早期的Doom没有直接使用GLSL(当时也没那个玩意),但在图形渲染和游戏引擎设计上为后来的发展铺平了道路,随着技术演变,最终变成了我们前端工程师用的现代图形编程中的着色器语言。这篇我们先只介绍在web端的webGL.
什么是WebGL
WebGL是一种3D绘图标准,这种绘图技术标准允许把JavaScript和OpenGL ES
结合在一起,通过增加OpenGL ES
的一个JavaScript绑定,WebGL可以为HTML5 Canvas提供硬件3D加速渲染(部分计算GPU),所以WebGL的本质就是:JavaScript操作OpenGL接口。
OpenGL ES 概念
既然上面已经说了,webGL是js操作OpenGL的接口,那我们就直接从OpenGL ES
开始,我们先介绍几个关于OpenGL中的概念,因为主要是在web中实践,所以主要基于OpenGL ES 3.0
版本接口,关于这个后面会说。
上下文(Context)
上下文是一个图形API(OpenGL、WebGL或DirectX)中的核心概念,代表着一组状态和资源,类似一个状态机(维护运行时)。上下文管理着GPU的状态,包括纹理、缓冲区、着色器程序等,是渲染操作的核心,包括:
- 状态信息:如深度测试、混合模式、剔除模式等。
- 资源管理:包括纹理、帧缓冲、顶点缓冲等对象的管理。
- 绘制命令:所有的绘制调用(如绘制三角形、线条等)都通过上下文来发起。
上下文可以视为与GPU
进行交互的接口,创建后需要在渲染过程中保持有效。
缓冲区(FrameBuffer)
帧缓冲区是用于存储渲染结果的内存区域,通常由多种附件组成,包括颜色缓冲区、深度缓冲区和模板缓冲区。帧缓冲区的类型包括:
- 默认帧缓冲区:系统提供的帧缓冲区,通常用于直接在屏幕上渲染。
- 离屏帧缓冲区:用户创建的帧缓冲区,允许在不直接显示到屏幕的情况下进行渲染,适用于
后处理
效果。
帧缓冲区的附件通常通过附着(Attachment)的方式来进行配置,以便在渲染时可以将不同的图像数据写入不同的缓冲区。
附着(Attachment)
附着是将图像数据(如颜色、深度、模板)附加到帧缓冲区的过程。常见的附着类型包括:
- 颜色附着:存储每个像素的颜色信息,通常使用一个或多个颜色缓冲区。
- 深度附着:存储每个像素的深度值,用于深度测试,确保遮挡关系正确。
- 模板附着:用于模板测试,允许进行复杂的遮挡效果,如阴影映射。
在OpenGL
中,附着操作通常使用glFramebufferTexture
或glFramebufferRenderbuffer
函数完成。
顶点数组(VertexArray)和顶点缓冲区(VertexBuffer)
- 顶点数组(VertexArray):在OpenGL中,顶点数组对象(VAO)用于封装顶点属性的状态,包括顶点缓冲区的绑定状态、顶点属性指针设置等。VAO使得开发者能够方便地管理顶点数据。
- 顶点缓冲区(VertexBuffer):用于实际存储顶点数据的对象,通常以GPU内存的形式存在。每个顶点缓冲区可以包含多种属性(如位置、法线、纹理坐标等),并通过
glBindBuffer
和glBufferData
等函数进行管理。
索引数组(ElementArray) 和索引缓冲区(ElementBuffer)
- 索引数组(ElementArray):用于定义图形的构建顺序,通过索引来指定顶点,减少数据冗余。可以使得同一个顶点被多次引用,节省内存。
- 索引缓冲区(ElementBuffer):实际存储索引数据的对象。索引缓冲区通过
glBindBuffer
和glBufferData
函数创建,并通过glDrawElements
调用时使用。
纹理(Texture)、渲染缓冲区(RenderBuffer)
- 纹理(Texture):是GPU用于存储图像数据的对象,允许在渲染过程中将图像映射到物体表面。纹理可以是2D、3D,甚至是立方体纹理,支持多种过滤和包裹模式。
- 渲染缓冲区(RenderBuffer):主要用于离屏渲染,不可以作为纹理使用。适用于存储深度和模板数据,通常具有更高的性能,适合快速的渲染操作。
着色器程序
着色器程序是GPU上的小型程序,负责在渲染过程中处理图形数据。常见的着色器包括:
1.顶点着色器:处理每个顶点的数据,主要任务包括:
- 接收顶点属性(如位置、法线等)。
- 进行变换(如模型、视图和投影变换)。
- 计算光照和阴影等信息。
- 将顶点的最终位置传递给光栅化阶段。
2.片元着色器:处理每个片元(即将要显示的像素),主要任务包括:
- 接收从顶点着色器传来的插值数据(如纹理坐标、颜色等)。
- 计算片元的最终颜色,包括纹理采样和光照计算。
- 进行各种效果的处理,如透明度、混合等。
图形管线(Graphics Pipeline)
图形管线是将输入数据(如顶点和纹理)转换为最终图像的一系列处理步骤。WebGL
的图形管线通常包括以下主要阶段:
- 顶点处理(Vertex Processing):在此阶段,顶点着色器被执行。每个顶点的属性(如位置、法线、颜色等)通过着色器进行处理,进行坐标变换(如模型变换、视图变换、投影变换),并生成屏幕坐标。
- 光栅化(Rasterization):将处理后的顶点转换为片元(
fragment
)。每个片元代表屏幕上的一个像素。光栅化还会决定片元的颜色、深度等属性。 - 片元处理(Fragment Processing):在此阶段,片元着色器被执行。每个片元的颜色和其他属性通过片元着色器进行计算,通常涉及
纹理采样
和光照计算
。 - 输出合并(Output Merging):将片元的最终颜色与帧缓冲区中的现有颜色进行合并,通常涉及深度测试和混合(
blending
)操作。
图形管线的意义在于将图形渲染的各个步骤模块化,允许开发者在特定阶段插入自定义的着色器代码,控制渲染过程的细节。也使得现代GPU能够高度并行化处理,因为每个阶段可以同时处理多个顶点和片元,大大提高了渲染性能。
WebGL渲染流程
上面有了基础概念之后,下面就是渲染流程,这里我们暂时只考虑webGL2哈:
1.创建WebGL上下文:
创建一个WebGL上下文,这个上下文用于与GPU交互。
// 创建WebGL上下文
const canvas = document.getElementById("glCanvas");
const gl = canvas.getContext("webgl");
if (!gl) {
console.error("无法初始化WebGL上下文");
}
2.设置视口和清除颜色:
- 使用
gl.viewport
设置视口的大小和位置。 - 使用
gl.clearColor
设置背景色,使用gl.clear
清空颜色缓冲区。
// 设置视口和清除颜色
gl.viewport(0, 0, canvas.width, canvas.height); // 设置视口大小
gl.clearColor(0.0, 0.0, 0.0, 1.0); // 设置清除颜色为黑色
gl.clear(gl.COLOR_BUFFER_BIT); // 清空颜色缓冲区
3.加载和编译着色器:
- 创建顶点着色器和片元着色器,并将其源代码传递给
WebGL
。 - 编译着色器并检查编译是否成功。
// 定义顶点着色器的源代码
const vsSource = `#version 300 es
in vec4 aVertexPosition; // 顶点位置属性
in vec4 aVertexColor; // 顶点颜色属性
out vec4 vColor; // 传递到片元着色器的颜色
void main(void) {
gl_Position = aVertexPosition; // 设置顶点位置
vColor = aVertexColor; // 传递颜色到片元着色器
}
`;
// 定义片元着色器的源代码
const fsSource = `#version 300 es
precision mediump float; // 设置浮点数精度
in vec4 vColor; // 从顶点着色器接收颜色
out vec4 fragColor; // 输出片元颜色
void main(void) {
fragColor = vColor; // 设置片元颜色
}
`;
// 创建着色器的函数
function createShader(gl, type, source) {
const shader = gl.createShader(type); // 创建着色器
gl.shaderSource(shader, source); // 传入着色器源代码
gl.compileShader(shader); // 编译着色器
// 检查着色器编译是否成功
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
return shader; // 返回编译成功的着色器
} else {
console.error(gl.getShaderInfoLog(shader)); // 输出错误信息
gl.deleteShader(shader); // 删除着色器
}
}
4.创建着色器程序:
- 将编译好的顶点着色器和片元着色器附加到一个程序对象上,并链接程序。
// 创建着色器程序
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource); // 创建顶点着色器
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource); // 创建片元着色器
// 创建着色器程序
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader); // 附加顶点着色器
gl.attachShader(shaderProgram, fragmentShader); // 附加片元着色器
gl.linkProgram(shaderProgram); // 链接程序
// 使用着色器程序
gl.useProgram(shaderProgram);
5.创建和绑定缓冲区:
- 创建顶点缓冲区(
Vertex Buffer
)和颜色缓冲区(Color Buffer
),并将顶点和颜色数据传入这些缓冲区。 - 绑定缓冲区,使其成为当前操作的缓冲区。
// 顶点
const vertices = new Float32Array([
0.0, 1.0,
-1.0, -1.0,
1.0, -1.0
]);
// 顶点缓冲区
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 绑定顶点缓冲区
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 将顶点数据传入缓冲区
// yanse
const colors = 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
]);
// 颜色缓冲区
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); // 绑定颜色缓冲区
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW); // 将颜色数据传入缓冲区
6.设置顶点属性指针:
- 获取顶点位置和颜色属性的位置,并指定如何从缓冲区中读取数据。
// 拿一下顶点位置属性指针地址
const positionLocation = gl.getAttribLocation(shaderProgram, "aVertexPosition");
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 绑定顶点缓冲区
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); // 指定顶点属性
gl.enableVertexAttribArray(positionLocation); // 启用顶点属性数组
7.绘制图形:
- 使用
gl.drawArrays
或gl.drawElements
发起绘制命令,告诉WebGL
如何使用顶点数据来绘制图形。
gl.clear(gl.COLOR_BUFFER_BIT); // 清空颜色缓冲区
gl.drawArrays(gl.TRIANGLES, 0, 3); // 浅浅画三个点
8.交换缓冲区(可选):
- 在某些情况下(如双缓冲),需要交换前后缓冲区,以显示渲染的结果。
// 使用帧缓冲区绘制
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
// 将结果绘制到屏幕
gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 绑定默认帧缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
纹理
在 WebGL 中,纹理是一种将图像应用于几何体表面的技术。它可以为物体增加细节和真实感。
纹理的基本概念
纹理类型:
- 2D 纹理:最常用的纹理类型,应用于平面或三维物体表面。
- 立方体纹理:用于创建环境映射和天空盒等效果,由六个面组成的立方体纹理。
- 3D 纹理:用于存储体积数据,适用于医学成像等应用。
纹理坐标:
- 纹理坐标通常用
[0.0, 0.0]
到[1.0, 1.0]
范围内的值表示,分别对应纹理的左下
角和右上
角。
纹理过滤:
- 放大过滤:当纹理比屏幕大时,使用
gl.TEXTURE_MAG_FILTER
设置过滤方式(如gl.LINEAR
、gl.NEAREST
)。 - 缩小过滤:当纹理比屏幕小时,使用
gl.TEXTURE_MIN_FILTER
设置过滤方式。
纹理包裹:
- 纹理的坐标可以超出
[0.0, 1.0]
范围,使用gl.TEXTURE_WRAP_S
和gl.TEXTURE_WRAP_T
设置包裹方式(如gl.CLAMP_TO_EDGE
、gl.REPEAT
)。
使用纹理步骤
- 创建纹理:使用
gl.createTexture()
创建纹理对象。 - 绑定纹理:使用
gl.bindTexture(gl.TEXTURE_2D, texture)
将纹理绑定到当前上下文。 - 定义纹理图像:使用
gl.texImage2D()
将图像数据传入纹理。 - 设置纹理参数:使用
gl.texParameteri()
设置过滤和包裹方式。 - 在着色器中使用纹理:在顶点着色器中传递纹理坐标,并在片元着色器中使用
texture()
函数采样纹理。
直接梭哈:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGL2 Texture Example</title>
<style>
canvas {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<canvas id="glCanvas"></canvas>
<script>
const canvas = document.getElementById("glCanvas");
const gl = canvas.getContext("webgl2");
if (!gl) {
console.error("无法初始化WebGL2上下文");
}
// 创建纹理对象
const texture = gl.createTexture();
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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// 创建图像并加载纹理
const image = new Image();
image.src = 'a.jpg'; // 替换为你的纹理图像URL
image.onload = () => {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D); // 生成 mipmaps
render(); // 纹理加载完成后开始渲染
};
// 顶点着色器和片元着色器代码
const vsSource = `
#version 300 es
in vec4 aVertexPosition;
in vec2 aTexCoord;
out vec2 vTexCoord;
void main(void) {
gl_Position = aVertexPosition;
vTexCoord = aTexCoord;
}
`;
const fsSource = `
#version 300 es
precision mediump float;
in vec2 vTexCoord;
out vec4 fragColor;
uniform sampler2D uSampler; // 纹理采样器
void main(void) {
fragColor = texture(uSampler, vTexCoord); // 采样纹理
}
`;
// 创建着色器
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
return shader;
} else {
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
}
// 创建着色器程序
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
gl.useProgram(shaderProgram);
// 定义矩形的顶点和纹理坐标
const vertices = new Float32Array([
-1.0, 1.0, 0.0, 0.0, // 左上
-1.0, -1.0, 0.0, 1.0, // 左下
1.0, 1.0, 1.0, 0.0, // 右上
1.0, -1.0, 1.0, 1.0 // 右下
]);
// 创建顶点缓冲区
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 设置顶点位置和纹理坐标属性
const positionLocation = gl.getAttribLocation(shaderProgram, "aVertexPosition");
const texCoordLocation = gl.getAttribLocation(shaderProgram, "aTexCoord");
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 16, 0);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 16, 8);
gl.enableVertexAttribArray(texCoordLocation);
// 主渲染函数
function render() {
gl.clear(gl.COLOR_BUFFER_BIT); // 清空画布
gl.bindTexture(gl.TEXTURE_2D, texture); // 绑定纹理
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); // 绘制矩形
}
</script>
</body>
</html>
参考代码: