WebGL2初识

发布于:2025-09-08 ⋅ 阅读:(20) ⋅ 点赞:(0)
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 es
  in 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 es
  precision 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; // RGB
      data[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); // 激活纹理单元 0
  gl.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));

  // 更新动画 uniform
  gl.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>


网站公告

今日签到

点亮在社区的每一天
去签到