Skip to content

API

初始化

const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl2");

画布

// 设置画布
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

缓冲区

创建缓冲区

const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, $顶点数据, gl.STATIC_DRAW);

为什么需要 gl.bindBuffer?

  1. gl.createBuffer:告诉 GPU 需要开辟一块区域来存储数据
  2. gl.ARRAY_BUFFER 表示了 WebGL 中 GPU 的一个插槽,gl.bindBuffer 的意思是将 buffer 挂载到这个插槽
  3. 当调用 gl.bufferData 向这个插槽注入数据时,其实也就是向 buffer 注入数据,通过总线将数据从 CPU 拷贝到 GPU 存储

为什么需要这么设计?

  1. 性能优化:避免频繁的传递数据
  2. 解耦:将数据和插槽解耦,插槽和数据(Buffer)可以任意匹配

VAO

VAO(Vertex Attribute Object)解决频繁调用 bindBuffer、vertexAttribPointer 带来的开销。VAO 记录了所有的配置状态,在后续使用时,可以一键激活(可以想象成一键预设)

const vao = gl.createVertexArray(); // 创建 VAO
// 绑定 VAO(后续操作会记录到当前 VAO)
gl.bindVertexArray(vao);
// 所有的 gl.bindBuffer(gl.ARRAY_BUFFER, ...)
// 所有的 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ...)
// 所有的 gl.vertexAttribPointer(...)
// 所有的 gl.enableVertexAttribArray(...)
// 解绑 VAO(配置完成)
gl.bindVertexArray(null);

清空缓冲区

gl.clearColor(0, 0, 0, 0); // 清除缓冲时,设置的颜色
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  • gl.COLOR_BUFFER_BIT:表示颜色缓冲区,也就是存储像素颜色值的缓冲区。
  • gl.DEPTH_BUFFER_BIT:表示深度缓冲区,它用于存储每个像素的深度信息,在进行深度测试时会用到。
  • gl.STENCIL_BUFFER_BIT:表示模板缓冲区,可用于实现一些特殊的渲染效果,如遮罩

绘制

gl.drawArrays(mode, first, count):void; // 该函数按照 顶点缓冲 区里顶点的顺序来绘制图元
gl.drawElements(mode, count, type, offset):void; // 此函数借助 索引缓冲区 来指定要使用的顶点
枚举值类型特点应用场景
gl.POINTS每个顶点绘制为一个点用于绘制离散点 散点图、星空、调试标记
gl.LINES线每两个顶点组成一条独立线段,线段相互独立网格线、物体轮廓
gl.LINE_STRIP线新顶点与前一顶点组成线段,形成折线连续的折线 折线图、路径
gl.LINE_LOOP线类似 gl.LINE_STRIP,最后连接首尾顶点形成封闭环封闭的折线
gl.TRIANGLES三角形每三个顶点组成一个三角形,三角形相互独立,顶点数据相对冗余,不相连的多边形或物体
gl.TRIANGLE_STRIP三角形从第三个顶点开始,每个新顶点和前两个顶点组成新三角形绘制圆柱体侧面、地形等
gl.TRIANGLE_FAN三角形第一个顶点为公共顶点,新顶点和第一个顶点及前一个顶点组成新三角形绘制扇形、圆形等

缓冲帧

WebGL 可以存在多个缓冲帧,可分为默认缓冲帧(画布)和自定义缓冲帧。自定义缓冲帧可以将输出另外存储起来

const fbo = gl.createFramebuffer(); // 创建缓冲帧
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); // 输出于当前绑定的缓冲帧
gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 输出于画布

场景

Scene(场景)是多图形的组织形式。Scene 本质是一个树

  1. 每次渲染时,由根节点遍历,渲染每个节点
  2. 子节点的位置、方向收到附父节点影响,即子节点的矩阵收到父节点矩阵影响

纹理

创建纹理

// 创建图片纹理
const texture = gl.createTexture();
gl.activeTexture(gl.TEXTURE0); // 此时就指定了纹理单元,可以指定多个纹理单元
gl.bindTexture(gl.TEXTURE_2D, texture);
// 此时可以生成mipMap
gl.generateMipmap(gl.TEXTURE_2D);
// 设置水平方向,超出的处理
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.uniform1i(uImage, 0); // 这里的0需要和上面的纹理的单元对应

纹理的超出时,有多种处理方式

枚举值描述
gl.REPEAT重复纹理(默认值)
gl.MIRRORED_REPEAT镜像重复纹理
gl.CLAMP_TO_EDGE延伸边缘像素

纹理的尺寸和需要绘制的图形尺寸不同时,有多种处理方式

枚举值描述
gl.NEAREST选择最近的像素(速度快,锯齿明显)
gl.LINEAR线性插值,线性过滤模式会对纹理进行双线性插值。当需要从纹理中采样颜色值时,它会选取距离采样点最近的 4 个纹素(纹理像素),并依据采样点到这 4 个纹素的距离进行加权平均
gl.NEAREST_MIPMAP_NEAREST选择最合适的贴图,然后从上面找到一个像素
gl.LINEAR_MIPMAP_NEAREST选择最合适的贴图,然后取出 4 个像素进行混合
gl.NEAREST_MIPMAP_LINEAR(默认值),选择最合适的两个贴图,从每个上面选择 1 个像素然后混合
gl.LINEAR_MIPMAP_LINEAR三线性过滤。选择最合适的两个贴图,从每个上选择 4 个像素然后混合

设置 UV

const uv = [0, 0, 1, 0, 0, 1, 1, 1];
const uvBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(uv), gl.STATIC_DRAW);
const aUVCoord = gl.getAttribLocation(program, "a_uvCoord");
gl.enableVertexAttribArray(aUVCoord);
gl.vertexAttribPointer(aUVCoord, 2, gl.FLOAT, false, 0, 0);

纹理投影

将纹理投影后,在做为别的图形的纹理。有如下场景

  1. 真正的投影纹理:从某个虚拟的投影仪,将一张纹理投影到场景中的任意物体上
  2. 先渲染到一个 Render To Texture,然后将这个渲染结果做为普通纹理铁道物体上

环境纹理

模拟物体表面反射/折射周围的环境,例如可以通过一个玻璃球看到周围的环境。可以按照如下步骤实现

  1. 环境贴图:可以使用立方体贴图,球形贴图,全景图
  2. 根据眼睛(相机)的法线,去算反射和折射。因为光路可以,则从眼睛发出的光可以打到物体上,则物体发出的光必然可以被眼睛看到
  3. 根据反射或者折射的方向,去映射到纹理坐标,并采集纹理
  4. 再根据光照和自身材质,算出最终的颜色

优缺点

  1. 计算效率高,效果较好
  2. 近似模拟
  3. 无法模拟靠近的动态物体

阴影

Shadow Map,场景需要渲染两次

  1. 第一次:根据光照,计算出阴影贴图
  2. 第二次:根据阴影贴图,计算出每个区域是否有阴影

拾取

颜色拾取

获取某一个像素的颜色

const pixelData = new Uint8Array(4); // RGBA
// 确保 FBO 仍是绑定的读缓冲 (或用 gl.bindFramebuffer(gl.READ_FRAMEBUFFER, pickingFBO))
gl.readPixels(
mouseX, // X coord of pixel to read
readY, // Y coord of pixel to read (adjusted)
1, // Width of area to read
1, // Height of area to read
gl.RGBA, // Format
gl.UNSIGNED_BYTE, // Type
pixelData, // Array to store result
);

射线拾取

获取鼠标和物体相交

  1. 获取鼠标的位置经过变换获得真实位置:【像素位置】 —> 裁剪坐标 —> 逆投影矩阵 —> 逆视图矩阵 —> 【真实位置】
  2. 从相机位置触发,沿着鼠标真实位置,绘制一条射线。
  3. 遍历场景的物体,谁可以与射线教相交(包围盒,包围球,三角形)
  4. 计算与与相交点的位置,取最近的一个

材质

PBR

说 PBR 材质理论前,先了解 Blinn Phone 的光照模型。BRP相较于Blinn-Phone能更好的计算高光,试图模拟光线与材质表面在微观层面的交互。是目前主流的游戏引擎(如 Unreal、Unity)和建模软件(如 Blender)采用的标准

特点

  1. 法线分布 (D)(关联粗糙度)

    我们在计算光照时,是按照每个像素去计算,虽然每个像素已经足够小了,但是每个像素中仍然存在很多起伏的结构,所以每个像素中是存在朝着各个方向的法线的,只是占比不同,所以使用单一的法线是无法完整的描述材质,所以引入法线分布来描述能造成高光的法线分布比例

  2. 菲涅耳效应 (F):按照物理来说

    1. 入射角越小,折射越多,反射越少;入射角越大,折射越小,反射越多
    2. 不同材质对反射和折射程度不同,例如水折射程度较高,而金属的反射程度较高
  3. 能力守恒:光照物理合理性

其他

处理 devicePixelRatio

devicePixelRatio 是设别像素比,公式为 devicePixelRatio=逻辑像素/物理像素devicePixelRatio = 逻辑像素 / 物理像素。本质上是物理像素和逻辑像素不同,从而导致在高分屏(例如 Retina)模糊。

如何处理

  1. 将画布的大小根据 devicePixelRatio 放大,在放大的画布上绘制
  2. 浏览器在自动缩放至指定的 CSS 像素大小

边界处理

  1. 多个显示器时,可能不同显示器 devicePixelRatio 不同
  2. 捕捉屏幕像素时,映射到画布时,需要计算缩放

屏幕空间环境光遮蔽

Screen-Space Ambient Occlusion(SSAO)

  1. 根据当前点和法线的方向半球内,取若干个采样点
  2. 根据选取的采样点,计算出该点的深度值,与当前点的深度(深度测试)进行比较
  3. 合并所有采样点的遮蔽值,即为当前点的遮蔽