WebGL2初识
WebGL渲染步骤 | WebGL渲染过程 | 具体代码片段 |
CPU端 | 1.准备顶点坐标(位置、颜色、法线)塞进GPU缓冲区 2.把“顶点/片元着色器源码”变成可执行文件 | |
GPU启动 | GPU按固定管线顺序执行后续所有阶段 | gl.drawArrays(gl.TRIANGLES,0,3); |
顶点着色器 | 执行顶点着色器文件,对每个顶点跑一遍:坐标变换-裁剪空间 | gl_Position=vec4(a_pos,0,1);代码只存在js内存里面 |
图元装配 | 顶点连起来后的“几何形状”(点线面) | 硬件自动,无代码 |
光栅化 | 在屏幕格子内部插值,生成一堆候选“片元”,还未写进屏幕 | 硬件自动,无代码 |
片元着色器 | 执行片元着色器文件,给每个片元上色、贴图、光照→输出颜色+深度 | fragColor=vec4(v_color,1);代码只存在js内存里面 |
测试与混合 | 深度测试/模板测试/混合→幸存下来的片元正式成为像素 | 默认开启深度测试 |
帧缓冲→显示控制器 | 写进帧缓存的真实屏幕点 | 浏览器+OS自动完成,无需代码 |
图形学是“先做再看”效率最高的学科,所以直接看效果和代码:
<script type="module">
/*** @fileoverview WebGL2 配置开关 - JSDoc 全参详解版* 每一行 gl.* 都带「参数含义 + 中文一句话 + 常见坑」* 执行顺序:① → ⑪ 保持原标记*//* ================= ① 获取 WebGL2 上下文 ================= */
/** @type {WebGL2RenderingContext} */
const gl = c.getContext('webgl2'); // 拿到 GPU 遥控器
if (!gl) throw '浏览器不支持 WebGL2'; // 保险:旧浏览器直接抛错/* ================= ② 玩家配置 ================= */
const CFG = {rotate: true, // 60 fps 旋转move: true, // 左右摆动rainbow: false, // 每帧换色texture: false, // 棋盘格纹理wireframe: true, // 线框模式cullFace: true, // 背面剔除depthTest: true, // 深度缓冲clearColor: [0, 0, 0, 1] // RGBA 0-1,黑色背景
};/* ================= ③ 实时绑定面板 ================= */
['rotate','move','rainbow','texture','wireframe','cullFace','depthTest'].forEach(k => document.getElementById(k).addEventListener('change', () => rebuild()));/* ================= ④ 动态生成着色器 ================= */
/*** 根据 CFG 返回 VS/FS 源码字符串* @returns {{vs:string, fs:string}}*/
function buildShader() {const vs = `#version 300 esin vec2 a_pos;in vec3 a_color;in vec2 a_uv;out vec3 v_color;out vec2 v_uv;uniform float u_time;uniform vec2 u_offset;void main(){vec2 p = a_pos;${CFG.rotate ? `float s = sin(u_time), c = cos(u_time); p = vec2(p.x*c - p.y*s, p.x*s + p.y*c);` : ''}${CFG.move ? `p += u_offset;` : ''}gl_Position = vec4(p, 0, 1);v_color = a_color;v_uv = a_uv;}`;const fs = `#version 300 esprecision highp float;in vec3 v_color;in vec2 v_uv;out vec4 fragColor;uniform float u_time;uniform sampler2D u_tex;void main(){${CFG.rainbow ? `fragColor = vec4(0.5 + 0.5*sin(u_time + vec3(0,2,4)), 1);` :CFG.texture ? `fragColor = texture(u_tex, v_uv);` :`fragColor = vec4(v_color, 1);`}}`;return {vs, fs};
}/* ================= ⑤ 生成棋盘格纹理 ================= */
/*** 创建 64×64 棋盘格纹理* @returns {WebGLTexture} 纹理句柄*/
function makeCheckerTexture() {const size = 64;const data = new Uint8Array(size * size * 4);for (let i = 0; i < size; ++i)for (let j = 0; j < size; ++j) {const c = ((i >> 3) + (j >> 3)) & 1 ? 255 : 0; // 8×8 黑白格const idx = (i * size + j) * 4;data[idx] = data[idx + 1] = data[idx + 2] = c; // RGBdata[idx + 3] = 255; // A}const tex = gl.createTexture(); // 新建纹理对象gl.bindTexture(gl.TEXTURE_2D, tex); // 绑定到纹理单元 0/*** 上传像素数据到 GPU* @param {GLenum} target - 固定写 gl.TEXTURE_2D* @param {GLint} level - mipmap 级别,0=基础图* @param {GLint} internalFormat - GPU 内部格式,如 gl.RGBA* @param {GLsizei} width - 图像宽(像素)* @param {GLsizei} height - 图像高(像素)* @param {GLint} border - 必须为 0(WebGL 限制)* @param {GLenum} format - 像素格式,如 gl.RGBA* @param {GLenum} type - 像素类型,如 gl.UNSIGNED_BYTE* @param {ArrayBufferView} pixels - 像素数据,null=空图*/gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size, size, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);gl.generateMipmap(gl.TEXTURE_2D); // 自动生成各级 mipmap// 纹理环绕模式:REPEAT = 重复平铺gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);// 纹理过滤:LINEAR_MIPMAP_LINEAR = 三线性过滤,最平滑gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);return tex;
}/* ================= ⑥ 顶点数据(含 UV) ================= */
/*** 顶点布局:x, y, r, g, b, u, v* UV 范围 0→1:左下 (0,0) 右上 (1,1)* @type {Float32Array}*/
const vertices = new Float32Array([0.0, 0.5, 1,0,0, 0.5, 1, // 上中-0.5, -0.5, 0,1,0, 0, 0, // 左下0.5, -0.5, 0,0,1, 1, 0 // 右下
]);
const buf = gl.createBuffer(); // 创建 GPU 缓冲对象
gl.bindBuffer(gl.ARRAY_BUFFER, buf); // 绑定到“数组缓冲”目标
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 拷进显存/* ================= ⑦ 重建管线(编译+链接+状态) ================= */
let program, tex, u_time, u_offset;
function rebuild() {const {vs, fs} = buildShader();const vsShader = gl.createShader(gl.VERTEX_SHADER);gl.shaderSource(vsShader, vs);gl.compileShader(vsShader);const fsShader = gl.createShader(gl.FRAGMENT_SHADER);gl.shaderSource(fsShader, fs);gl.compileShader(fsShader);program = gl.createProgram();gl.attachShader(program, vsShader);gl.attachShader(program, fsShader);gl.linkProgram(program);gl.useProgram(program); // 设为“当前要用的程序”// 获取 uniform 槽位u_time = gl.getUniformLocation(program, 'u_time');u_offset = gl.getUniformLocation(program, 'u_offset');// 属性绑定const locPos = gl.getAttribLocation(program, 'a_pos');const locColor = gl.getAttribLocation(program, 'a_color');const locUV = gl.getAttribLocation(program, 'a_uv');gl.enableVertexAttribArray(locPos);/*** 告诉 GPU 如何拆“快递包裹”里的顶点数据* @param {GLuint} index - 属性 location(槽位号)* @param {GLint} size - 每个顶点读几个分量(1-4)* @param {GLenum} type - 数据类型,gl.FLOAT 最常用* @param {GLboolean} normalized - 是否把整数归一化到 [-1,1] 或 [0,1]* @param {GLsizei} stride - **字节步长**,0=紧密排列* @param {GLintptr} offset - **字节偏移**,从缓冲区哪里开始读*/gl.vertexAttribPointer(locPos, 2, gl.FLOAT, false, 7*4, 0);gl.enableVertexAttribArray(locColor);gl.vertexAttribPointer(locColor, 3, gl.FLOAT, false, 7*4, 2*4);gl.enableVertexAttribArray(locUV);gl.vertexAttribPointer(locUV, 2, gl.FLOAT, false, 7*4, 5*4);// 纹理开关if (CFG.texture && !tex) tex = makeCheckerTexture();/*** 激活纹理单元(0-31 可用)* @param {GLenum} unit - gl.TEXTURE0 ~ gl.TEXTURE31*/gl.activeTexture(gl.TEXTURE0); // 激活纹理单元 0gl.bindTexture(gl.TEXTURE_2D, CFG.texture ? tex : null);/*** 把采样器绑定到指定纹理单元* @param {WebGLUniformLocation} location - uniform 槽位* @param {GLint} unit - 纹理单元号(0 对应 TEXTURE0)*/gl.uniform1i(gl.getUniformLocation(program, 'u_tex'), 0); // 采样器绑定到 0// 状态实时切换CFG.depthTest ? gl.enable(gl.DEPTH_TEST) : gl.disable(gl.DEPTH_TEST);CFG.cullFace ? gl.enable(gl.CULL_FACE) : gl.disable(gl.CULL_FACE);gl.cullFace(gl.BACK);gl.depthFunc(gl.LEQUAL);
}
rebuild(); // ⑧ 首次构建/* ================= ⑨ 动画循环 ================= */
let then = 0;
function frame(t) {const dt = (t - then) * 0.001; then = t;/*** 设置清屏颜色(RGBA 0-1)* @param {number} r - 红* @param {number} g - 绿* @param {number} b - 蓝* @param {number} a - 透明*/gl.clearColor(...CFG.clearColor);/*** 真正执行“清屏”操作* @param {GLbitfield} mask - 位掩码:gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT*/gl.clear(gl.COLOR_BUFFER_BIT | (CFG.depthTest ? gl.DEPTH_BUFFER_BIT : 0));// 更新动画 uniformgl.uniform1f(u_time, t * 0.001);/*** 设置 vec2 类型 uniform(摆动偏移)* @param {WebGLUniformLocation} location - uniform 槽位* @param {number} x - x 分量* @param {number} y - y 分量*/if (CFG.move) gl.uniform2f(u_offset, Math.sin(t * 0.001) * 0.3, 0);// 实时切换图元模式const mode = CFG.wireframe ? gl.LINE_LOOP : gl.TRIANGLES;/*** 核心 DrawCall:GPU 开始跑整条管线* @param {GLenum} mode - 图元类型* gl.POINTS 点* gl.LINES 每 2 顶点一线段* gl.LINE_STRIP 连续折线* gl.LINE_LOOP 闭合折线(本例线框用)* gl.TRIANGLES 每 3 顶点一三角形(默认填充)* gl.TRIANGLE_STRIP 共享边的三角带* gl.TRIANGLE_FAN 共享第一个顶点的扇形* @param {GLint} first - 从第几个顶点开始(索引)* @param {GLsizei} count - 共用多少个顶点*/gl.drawArrays(mode, 0, 3); // 画 1 个三角形(3 顶点)requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>