WebGL test

1. dot

jsx 复制代码
import React, { useEffect, useRef } from 'react';

const WebGL = () => {
  // 引用 canvas DOM,供 JS 操作绘图区域
  const canvasRef = useRef<HTMLCanvasElement>(null);

  // 编译单个着色器(顶点或片元),失败返回 null
  function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
    const shader = gl.createShader(type);
    if (!shader) return null;
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(shader));
      return null;
    }
    return shader;
  }

  // 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
  function initShaderProgram(
    gl: WebGL2RenderingContext,
    vertexShaderSource: string,
    fragmentShaderSource: string,
  ) {
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器

    const program = gl.createProgram(); // 创建程序对象
    if (!program) return null;

    gl.attachShader(program, vertexShader); // 附加顶点着色器
    gl.attachShader(program, fragmentShader); // 附加片元着色器
    gl.linkProgram(program); // 链接程序对象
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.error(gl.getProgramInfoLog(program));
      return null;
    }
    return program;
  }

  function initCanvas() {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }

    // 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
    const gl = canvas.getContext('webgl2');
    if (!gl) return;

    // 清屏:设背景色(黑)并清空颜色缓冲区
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 顶点着色器:vec4(x, y, z, w) 裁剪空间 [-1, 1],x 横 y 纵 z 深度,w 齐次;gl_PointSize 点大小(像素),默认值为 1.0
    // w 齐次:第 4 分量,GPU 显示前会把 x、y 除以 w 得到最终位置;2D 画图固定 w=1.0 即可
    const vertexShaderSource = `#version 300 es
      void main() {
        gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
        gl_PointSize = 10.0;
      }
    `;

    // 片元着色器:vec4(R, G, B, A),各分量取值 0.0~1.0,R 红 G 绿 B 蓝 A 透明度
    // WebGL2 无 gl_FragColor,需声明 out 变量作为颜色输出
    const fragmentShaderSource = `#version 300 es
      precision mediump float;
      out vec4 fragColor;
      void main() {
        fragColor = vec4(1.0, 0.0, 0.0, 1.0);
      }
    `;

    // 编译并链接着色器,得到 program(准备工作,此时还未绘制)
    const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
    if (!program) return;
    // 告诉 GPU 接下来使用这套着色器程序
    gl.useProgram(program);
    // 开始绘制:POINTS=画点,0=从第0个顶点开始,1=共画1个
    gl.drawArrays(gl.POINTS, 0, 1);
  }

  // 组件挂载后初始化 WebGL2 并绘制
  useEffect(() => {
    initCanvas();
  }, []);

  return (
    <div>
      {/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
      <canvas ref={canvasRef} width="200" height="100"></canvas>
    </div>
  );
};

export default WebGL;

2. dot-with-mousedown

tsx 复制代码
import React, { useEffect, useRef } from 'react';

const WebGL = () => {
  // 引用 canvas DOM,供 JS 操作绘图区域
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const glRef = useRef<WebGL2RenderingContext | null>(null);
  const programRef = useRef<WebGLProgram | null>(null);
  const positionsRef = useRef<[number, number, [number, number, number, number]][]>([]);

  // 编译单个着色器(顶点或片元),失败返回 null
  function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
    const shader = gl.createShader(type);
    if (!shader) return null;
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(shader));
      return null;
    }
    return shader;
  }

  // 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
  function initShaderProgram(
    gl: WebGL2RenderingContext,
    vertexShaderSource: string,
    fragmentShaderSource: string,
  ) {
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器

    const program = gl.createProgram(); // 创建程序对象
    if (!program) return null;

    gl.attachShader(program, vertexShader); // 附加顶点着色器
    gl.attachShader(program, fragmentShader); // 附加片元着色器
    gl.linkProgram(program); // 链接程序对象
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.error(gl.getProgramInfoLog(program));
      return null;
    }
    return program;
  }

  // 像素坐标 → 裁剪空间 [-1, 1](WebGL 原点在中心,y 轴向上)
  function pixelToClipSpace(x: number, y: number, width: number, height: number) {
    const clipX = (x / width) * 2 - 1;
    const clipY = 1 - (y / height) * 2;
    return [clipX, clipY] as const;
  }

  function getColor() {
    return [Math.random(), Math.random(), Math.random(), Math.random()] as [
      number,
      number,
      number,
      number,
    ];
  }

  function handleMouseDown(event: React.MouseEvent<HTMLCanvasElement>) {
    const gl = glRef.current;
    const program = programRef.current;
    const canvas = canvasRef.current;
    if (!gl || !program || !canvas) return;

    const x = event.nativeEvent.offsetX;
    const y = event.nativeEvent.offsetY;
    const [clipX, clipY] = pixelToClipSpace(x, y, canvas.width, canvas.height);
    const color = getColor();
    positionsRef.current.push([clipX, clipY, color]);

    gl.clearColor(0.0, 0.0, 0.0, 1.0); // 清屏:设背景色(黑)
    gl.clear(gl.COLOR_BUFFER_BIT); // 把颜色缓冲区里每个像素都重写成该颜色(整屏清空 / 清屏)

    // JS 只能写入顶点 attribute;片元着色器的数据须由顶点着色器 out 转发
    const aPosition = gl.getAttribLocation(program, 'a_Position');
    const aColor = gl.getAttribLocation(program, 'a_Color');
    positionsRef.current.forEach(([x, y, color]) => {
      gl.vertexAttrib4f(aColor, ...color); // → 顶点 in a_Color → out f_Color → 片元 in f_Color
      gl.vertexAttrib2f(aPosition, x, y);
      gl.drawArrays(gl.POINTS, 0, 1);
    });
  }

  function initCanvas() {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }

    // 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
    const gl = canvas.getContext('webgl2');
    if (!gl) return;

    // 清屏:设背景色(黑)并清空颜色缓冲区
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
    const vertexShaderSource = `#version 300 es
      in vec2 a_Position;  // 顶点位置(裁剪空间),JS 通过 vertexAttrib 写入
      in vec4 a_Color;     // 顶点颜色,JS 通过 vertexAttrib 写入
      out vec4 f_Color;    // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
      void main() {
        gl_Position = vec4(a_Position, 0.0, 1.0);
        gl_PointSize = 10.0;
        f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
      }
    `;

    // 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
    // vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
    const fragmentShaderSource = `#version 300 es
      precision mediump float;
      in vec4 f_Color;     // 与顶点 out f_Color 配对,由 GPU 在图元内插值后传入
      out vec4 fragColor;
      void main() {
        fragColor = f_Color;
      }
    `;

    // 编译并链接着色器,得到 program(准备工作,此时还未绘制)
    const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
    if (!program) return;
    gl.useProgram(program);

    glRef.current = gl;
    programRef.current = program;
  }

  // 组件挂载后初始化 WebGL2 并绘制
  useEffect(() => {
    initCanvas();
  }, []);

  return (
    <div>
      {/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
      <canvas ref={canvasRef} width="200" height="100" onMouseDown={handleMouseDown}></canvas>
    </div>
  );
};

export default WebGL;

3. triangle

tsx 复制代码
import React, { useEffect, useRef } from 'react';

const WebGL = () => {
  // 引用 canvas DOM,供 JS 操作绘图区域
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const glRef = useRef<WebGL2RenderingContext | null>(null);
  const programRef = useRef<WebGLProgram | null>(null);

  // 编译单个着色器(顶点或片元),失败返回 null
  function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
    const shader = gl.createShader(type);
    if (!shader) return null;
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(shader));
      return null;
    }
    return shader;
  }

  // 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
  function initShaderProgram(
    gl: WebGL2RenderingContext,
    vertexShaderSource: string,
    fragmentShaderSource: string,
  ) {
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器

    const program = gl.createProgram(); // 创建程序对象
    if (!program) return null;

    gl.attachShader(program, vertexShader); // 附加顶点着色器
    gl.attachShader(program, fragmentShader); // 附加片元着色器
    gl.linkProgram(program); // 链接程序对象
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.error(gl.getProgramInfoLog(program));
      return null;
    }
    return program;
  }

  function initCanvas() {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }

    // 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
    const gl = canvas.getContext('webgl2');
    if (!gl) return;

    // 清屏:设背景色(黑)并清空颜色缓冲区
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
    const vertexShaderSource = `#version 300 es
      in vec2 a_Position;  // 顶点位置(裁剪空间),JS 通过 buffer 写入
      in vec4 a_Color;     // 顶点颜色,JS 通过 buffer 写入
      out vec4 f_Color;    // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
      void main() {
        gl_Position = vec4(a_Position, 0.0, 1.0);
        f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
      }
    `;

    // 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
    // vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
    const fragmentShaderSource = `#version 300 es
      precision mediump float;
      in vec4 f_Color;     // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
      out vec4 fragColor;
      void main() {
        fragColor = f_Color;
      }
    `;

    // 编译并链接着色器,得到 program(准备工作,此时还未绘制)
    const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
    if (!program) return;
    gl.useProgram(program);

    // ── 顶点数据:buffer 路径(三角形一次 draw 需多个不同顶点) ──
    // WebGL 有两种向 attribute 供数方式:
    //   1) vertexAttrib*f:写入 GPU 状态,作为「常量 attribute」;适合 drawArrays(..., 1) 每次只画 1 个顶点(见 webgl-2-dot-with-mousedown)
    //   2) bufferData + vertexAttribPointer:vertices 在 JS 内存,bufferData 上传到 GPU 显存;适合一次 draw 多个不同顶点(本例 TRIANGLES, 3)
    // vertices 只是 CPU 侧定义,GPU 绘制时读不到 JS 变量,必须经过 createBuffer → bindBuffer → bufferData 上传。
    const vertices = new Float32Array([
      0.0,
      0.5,
      1.0,
      0.0,
      0.0,
      1.0, // 上顶点,红

      -0.5,
      -0.5,
      0.0,
      1.0,
      0.0,
      1.0, // 左下,绿

      0.5,
      -0.5,
      0.0,
      0.0,
      1.0,
      1.0, // 右下,蓝
    ]); // 内存布局:[x y r g b a | x y r g b a | ...],aPosition 读前 2 个,aColor 读后 4 个

    const buffer = gl.createBuffer(); // 在 GPU 侧创建空 buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer); // 绑定为当前顶点缓冲区,后续 bufferData / vertexAttribPointer 都作用于此
    // bufferData:把 vertices 从 JS 内存复制到 GPU 显存(「定义数据」≠「上传数据」,二者缺一不可)
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

    const stride = 6 * Float32Array.BYTES_PER_ELEMENT; // 步长:6 个 float = (x, y, r, g, b, a)
    const aPosition = gl.getAttribLocation(program, 'a_Position'); // 获取顶点着色器中的 a_Position 变量位置
    const aColor = gl.getAttribLocation(program, 'a_Color'); // 获取顶点着色器中的 a_Color 变量位置

    // vertexAttribPointer:告诉 GPU 如何从已绑定的 ARRAY_BUFFER 解析 attribute(不传输数据,只设读取规则)
    // 参数:location, 分量数, 类型, 是否归一化, stride, offset
    // 须配合 enableVertexAttribArray 启用「从 buffer 读」;若用 vertexAttrib*f 则无需 enable,且 attribute 对该次 draw 内所有顶点相同
    gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0); // offset 0:读 x,y → in a_Position
    gl.enableVertexAttribArray(aPosition);
    gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 2 * Float32Array.BYTES_PER_ELEMENT); // offset 8:跳过 x,y,读 r,g,b,a → in a_Color → out f_Color → in f_Color
    gl.enableVertexAttribArray(aColor);

    // 一次提交 3 个顶点;GPU 按 vertexAttribPointer 规则从 buffer 逐个读取 → 顶点着色器 → 片元插值 f_Color
    gl.drawArrays(gl.TRIANGLES, 0, 3);

    glRef.current = gl;
    programRef.current = program;
  }

  // 组件挂载后初始化 WebGL2 并绘制
  useEffect(() => {
    initCanvas();
  }, []);

  return (
    <div>
      {/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
      <canvas ref={canvasRef} width="200" height="100"></canvas>
    </div>
  );
};

export default WebGL;

4. auto-triangle

tsx 复制代码
import React, { useEffect, useRef } from 'react';

const WebGL = () => {
  // 引用 canvas DOM,供 JS 操作绘图区域
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const glRef = useRef<WebGL2RenderingContext | null>(null);
  const programRef = useRef<WebGLProgram | null>(null);
  // 累积所有点击顶点,每 3 次点击组成 1 个三角形;每次 mousedown 重传 buffer 并重绘全部几何
  const pointsRef = useRef<[number, number, number, number, number, number][]>([]);

  function handleMouseDown(event: React.MouseEvent<HTMLCanvasElement>) {
    // 坐标转换:offsetX/Y 相对 canvas 左上角(勿用 clientX/Y,canvas 不在页面原点时会偏移)
    // 裁剪空间 x∈[-1,1];y 需翻转(屏幕 Y 向下,WebGL Y 向上)
    const x = event.nativeEvent.offsetX;
    const y = event.nativeEvent.offsetY;
    pointsRef.current.push([
      (x / canvasRef.current.width) * 2 - 1,
      -(y / canvasRef.current.height) * 2 + 1,
      Math.random(),
      Math.random(),
      Math.random(),
      Math.random(),
    ]);
    const flatPoints = pointsRef.current.flat();
    const len = pointsRef.current.length;
    const remainder = len % 3; // 当前轮次未凑齐三角形的余数顶点数(0/1/2)
    const completeCount = len - remainder; // 可组成完整三角形的顶点数(3 的倍数)

    const gl = glRef.current;
    if (!gl) return;
    const program = programRef.current;
    if (!program) return;
    const aPosition = gl.getAttribLocation(program, 'a_Position');
    const aColor = gl.getAttribLocation(program, 'a_Color');

    // buffer 路径:CPU 侧 vertices 须经 createBuffer → bindBuffer → bufferData 上传,GPU 才能读取
    // 内存布局 [x y r g b a | x y r g b a | ...],stride=6 float;aPosition 读前 2 个,aColor 读后 4 个
    const buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(flatPoints), gl.STATIC_DRAW);
    const stride = 6 * Float32Array.BYTES_PER_ELEMENT;

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
    gl.enableVertexAttribArray(aPosition);
    gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 2 * Float32Array.BYTES_PER_ELEMENT);
    gl.enableVertexAttribArray(aColor);

    // drawArrays(mode, first, count):first=起始顶点索引,count=读取的顶点个数(不是三角形个数)
    // TRIANGLES 模式:顶点按顺序每 3 个一组构成一个三角形,一次 draw 可画 count/3 个三角形
    //   顶点 0,1,2 → △1 | 3,4,5 → △2 | 6,7,8 → △3 ...
    if (completeCount >= 3) {
      gl.drawArrays(gl.TRIANGLES, 0, completeCount);
    }

    // 未完成顶点的预览(first 必须从 buffer 末尾读,first=0 会永远画第一次点击的位置)
    //   len%3===1(第 1/4/7... 次点击)→ 画最新 1 个点
    //   len%3===2(第 2/5/8... 次点击)→ 画最新 2 点之间的线
    //   len%3===0(第 3/6/9... 次点击)→ 已凑齐三角形,无需预览
    if (remainder === 1) {
      gl.drawArrays(gl.POINTS, len - 1, 1);
    }
    if (remainder === 2) {
      gl.drawArrays(gl.LINES, len - 2, 2);
    }
  }

  // 编译单个着色器(顶点或片元),失败返回 null
  function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
    const shader = gl.createShader(type);
    if (!shader) return null;
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(shader));
      return null;
    }
    return shader;
  }

  // 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
  function initShaderProgram(
    gl: WebGL2RenderingContext,
    vertexShaderSource: string,
    fragmentShaderSource: string,
  ) {
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器

    const program = gl.createProgram(); // 创建程序对象
    if (!program) return null;

    gl.attachShader(program, vertexShader); // 附加顶点着色器
    gl.attachShader(program, fragmentShader); // 附加片元着色器
    gl.linkProgram(program); // 链接程序对象
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.error(gl.getProgramInfoLog(program));
      return null;
    }
    return program;
  }

  function initCanvas() {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }

    // 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
    const gl = canvas.getContext('webgl2');
    if (!gl) return;

    // 清屏:设背景色(黑)并清空颜色缓冲区
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
    const vertexShaderSource = `#version 300 es
      in vec2 a_Position;  // 顶点位置(裁剪空间),JS 通过 buffer 写入
      in vec4 a_Color;     // 顶点颜色,JS 通过 buffer 写入
      out vec4 f_Color;    // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
      void main() {
        gl_Position = vec4(a_Position, 0.0, 1.0);
        gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
        f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
      }
    `;

    // 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
    // vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
    const fragmentShaderSource = `#version 300 es
      precision mediump float;
      in vec4 f_Color;     // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
      out vec4 fragColor;
      void main() {
        fragColor = f_Color;
      }
    `;

    // 编译并链接着色器,得到 program(准备工作,此时还未绘制)
    const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
    if (!program) return;
    gl.useProgram(program);

    glRef.current = gl;
    programRef.current = program;
  }

  // 组件挂载后初始化 WebGL2 并绘制
  useEffect(() => {
    initCanvas();
  }, []);

  return (
    <div>
      {/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
      <canvas ref={canvasRef} width="600" height="400" onMouseDown={handleMouseDown}></canvas>
    </div>
  );
};

export default WebGL;

5. circle

tsx 复制代码
import React, { useEffect, useRef } from 'react';

const getX = (x: number, width: number) => {
  return (x / width) * 2 - 1;
};
const getY = (y: number, height: number) => {
  return -(y / height) * 2 + 1;
};

const WebGL = () => {
  // 引用 canvas DOM,供 JS 操作绘图区域
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const glRef = useRef<WebGL2RenderingContext | null>(null);
  const programRef = useRef<WebGLProgram | null>(null);
  // 编译单个着色器(顶点或片元),失败返回 null
  function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
    const shader = gl.createShader(type);
    if (!shader) return null;
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(shader));
      return null;
    }
    return shader;
  }

  // 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
  function initShaderProgram(
    gl: WebGL2RenderingContext,
    vertexShaderSource: string,
    fragmentShaderSource: string,
  ) {
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器

    const program = gl.createProgram(); // 创建程序对象
    if (!program) return null;

    gl.attachShader(program, vertexShader); // 附加顶点着色器
    gl.attachShader(program, fragmentShader); // 附加片元着色器
    gl.linkProgram(program); // 链接程序对象
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.error(gl.getProgramInfoLog(program));
      return null;
    }
    return program;
  }

  function initCanvas() {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }

    // 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
    const gl = canvas.getContext('webgl2');
    if (!gl) return;

    // 清屏:设背景色(黑)并清空颜色缓冲区
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
    const vertexShaderSource = `#version 300 es
      in vec2 a_Position;  // 顶点位置(裁剪空间),JS 通过 buffer 写入
      in vec4 a_Color;     // 顶点颜色,JS 通过 buffer 写入
      out vec4 f_Color;    // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
      void main() {
        gl_Position = vec4(a_Position, 0.0, 1.0);
        gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
        f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
      }
    `;

    // 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
    // vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
    const fragmentShaderSource = `#version 300 es
      precision mediump float;
      in vec4 f_Color;     // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
      out vec4 fragColor;
      void main() {
        fragColor = f_Color;
      }
    `;

    // 编译并链接着色器,得到 program(准备工作,此时还未绘制)
    const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
    if (!program) return;
    gl.useProgram(program);

    glRef.current = gl;
    programRef.current = program;

    renderCircle(300, 200, 100, 50);
  }

  function renderCircle(x: number, y: number, radius: number, segments: number) {
    const gl = glRef.current;
    if (!gl) return;
    const program = programRef.current;
    if (!program) return;
    const aPosition = gl.getAttribLocation(program, 'a_Position');
    const aColor = gl.getAttribLocation(program, 'a_Color');
    const color = [1.0, 0.0, 0.0, 1.0] as const;
    const points: number[] = [];
    for (let i = 0; i < segments; i++) {
      const angle = (i / segments) * 2 * Math.PI;
      points.push(
        getX(x + radius * Math.cos(angle), canvasRef.current.width),
        getY(y + radius * Math.sin(angle), canvasRef.current.height),
      );
    }
    const buffer = gl.createBuffer();
    points.push(points[0], points[1]);
    points.unshift(getX(x, canvasRef.current.width), getY(y, canvasRef.current.height));
    console.log(points);
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
    const stride = 2 * Float32Array.BYTES_PER_ELEMENT;
    gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
    gl.enableVertexAttribArray(aPosition);
    // 设置color为固定值
    gl.disableVertexAttribArray(aColor); // 关键
    gl.vertexAttrib4f(aColor, ...color);
    gl.drawArrays(gl.TRIANGLE_FAN, 0, points.length / 2);
  }

  // 组件挂载后初始化 WebGL2 并绘制
  useEffect(() => {
    initCanvas();
  }, []);

  return (
    <div>
      {/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
      <canvas ref={canvasRef} width="600" height="400"></canvas>
    </div>
  );
};

export default WebGL;

6. auto-circle

tsx 复制代码
import React, { useEffect, useRef } from 'react';

const getX = (x: number, width: number) => {
  return (x / width) * 2 - 1;
};
const getY = (y: number, height: number) => {
  return -(y / height) * 2 + 1;
};

/**
 * 随机半径:2-10
 */
const randomRadius = () => {
  return Math.ceil(Math.random() * 8) + 2;
};

const WebGL = () => {
  // 引用 canvas DOM,供 JS 操作绘图区域
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const glRef = useRef<WebGL2RenderingContext | null>(null);
  const programRef = useRef<WebGLProgram | null>(null);
  const circleRef = useRef<
    { points: number[]; radius: number; color: [number, number, number, number] }[]
  >([
    // { points: [], radius: 5, color: [1.0, 0.0, 0.0, 1.0] },
  ]);

  // 编译单个着色器(顶点或片元),失败返回 null
  function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
    const shader = gl.createShader(type);
    if (!shader) return null;
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(shader));
      return null;
    }
    return shader;
  }

  // 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
  function initShaderProgram(
    gl: WebGL2RenderingContext,
    vertexShaderSource: string,
    fragmentShaderSource: string,
  ) {
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器

    const program = gl.createProgram(); // 创建程序对象
    if (!program) return null;

    gl.attachShader(program, vertexShader); // 附加顶点着色器
    gl.attachShader(program, fragmentShader); // 附加片元着色器
    gl.linkProgram(program); // 链接程序对象
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.error(gl.getProgramInfoLog(program));
      return null;
    }
    return program;
  }

  function initCanvas() {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }

    // 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
    const gl = canvas.getContext('webgl2');
    if (!gl) return;

    // 清屏:设背景色(黑)并清空颜色缓冲区
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
    const vertexShaderSource = `#version 300 es
      in vec2 a_Position;  // 顶点位置(裁剪空间),JS 通过 buffer 写入
      in vec4 a_Color;     // 顶点颜色,JS 通过 buffer 写入
      out vec4 f_Color;    // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
      void main() {
        gl_Position = vec4(a_Position, 0.0, 1.0);
        gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
        f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
      }
    `;

    // 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
    // vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
    const fragmentShaderSource = `#version 300 es
      precision mediump float;
      in vec4 f_Color;     // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
      out vec4 fragColor;
      void main() {
        fragColor = f_Color;
      }
    `;

    // 编译并链接着色器,得到 program(准备工作,此时还未绘制)
    const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
    if (!program) return;
    gl.useProgram(program);

    glRef.current = gl;
    programRef.current = program;
  }

  function renderCircle(
    x: number,
    y: number,
    radius: number,
    segments: number,
    color: [number, number, number, number],
  ) {
    const gl = glRef.current;
    if (!gl) return;
    const program = programRef.current;
    if (!program) return;
    const aPosition = gl.getAttribLocation(program, 'a_Position');
    const aColor = gl.getAttribLocation(program, 'a_Color');
    const points: number[] = [];
    for (let i = 0; i < segments; i++) {
      const angle = (i / segments) * 2 * Math.PI;
      points.push(
        getX(x + radius * Math.cos(angle), canvasRef.current.width),
        getY(y + radius * Math.sin(angle), canvasRef.current.height),
      );
    }
    const buffer = gl.createBuffer();
    // 闭合路径
    points.push(points[0], points[1]);
    // 添加中心点
    points.unshift(getX(x, canvasRef.current.width), getY(y, canvasRef.current.height));
    // 绑定buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
    const stride = 2 * Float32Array.BYTES_PER_ELEMENT;
    gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
    gl.enableVertexAttribArray(aPosition);
    // a_Color 二选一:enable 从 buffer 读(需 stride 含 rgba);disable 则用 vertexAttrib4f 常量
    // buffer 仅有 x,y,须 disable,否则 vertexAttrib4f 被忽略,颜色/alpha 异常导致不可见
    gl.disableVertexAttribArray(aColor);
    gl.vertexAttrib4f(aColor, ...color);
    // 绘制
    gl.drawArrays(gl.TRIANGLE_FAN, 0, points.length / 2);
  }

  function handleMouseDown(event: React.MouseEvent<HTMLCanvasElement>) {
    // 坐标转换:offsetX/Y 相对 canvas 左上角(勿用 clientX/Y,canvas 不在页面原点时会偏移)
    // 裁剪空间 x∈[-1,1];y 需翻转(屏幕 Y 向下,WebGL Y 向上)
    const x = event.nativeEvent.offsetX;
    const y = event.nativeEvent.offsetY;
    circleRef.current.push({
      points: [x, y],
      radius: randomRadius(),
      color: [Math.random(), Math.random(), Math.random(), 1.0],
    });
    // 清屏:设背景色(黑)并清空颜色缓冲区
    const gl = glRef.current;
    if (!gl) return;
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    circleRef.current.forEach((circle) => {
      renderCircle(circle.points[0], circle.points[1], circle.radius, 50, circle.color);
    });
  }

  // 组件挂载后初始化 WebGL2 并绘制
  useEffect(() => {
    initCanvas();
  }, []);

  return (
    <div>
      {/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
      <canvas ref={canvasRef} width="600" height="400" onMouseDown={handleMouseDown}></canvas>
    </div>
  );
};

export default WebGL;

7. ring

tsx 复制代码
import React, { useEffect, useRef } from 'react';

const getX = (x: number, width: number) => {
  return (x / width) * 2 - 1;
};
const getY = (y: number, height: number) => {
  return -(y / height) * 2 + 1;
};

const WebGL = () => {
  // 引用 canvas DOM,供 JS 操作绘图区域
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const glRef = useRef<WebGL2RenderingContext | null>(null);
  const programRef = useRef<WebGLProgram | null>(null);
  // 编译单个着色器(顶点或片元),失败返回 null
  function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
    const shader = gl.createShader(type);
    if (!shader) return null;
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(shader));
      return null;
    }
    return shader;
  }

  // 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
  function initShaderProgram(
    gl: WebGL2RenderingContext,
    vertexShaderSource: string,
    fragmentShaderSource: string,
  ) {
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器

    const program = gl.createProgram(); // 创建程序对象
    if (!program) return null;

    gl.attachShader(program, vertexShader); // 附加顶点着色器
    gl.attachShader(program, fragmentShader); // 附加片元着色器
    gl.linkProgram(program); // 链接程序对象
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.error(gl.getProgramInfoLog(program));
      return null;
    }
    return program;
  }

  function initCanvas() {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }

    // 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
    const gl = canvas.getContext('webgl2');
    if (!gl) return;

    // 清屏:设背景色(黑)并清空颜色缓冲区
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
    const vertexShaderSource = `#version 300 es
      in vec2 a_Position;  // 顶点位置(裁剪空间),JS 通过 buffer 写入
      in vec4 a_Color;     // 顶点颜色,JS 通过 buffer 写入
      out vec4 f_Color;    // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
      void main() {
        gl_Position = vec4(a_Position, 0.0, 1.0);
        gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
        f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
      }
    `;

    // 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
    // vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
    const fragmentShaderSource = `#version 300 es
      precision mediump float;
      in vec4 f_Color;     // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
      out vec4 fragColor;
      void main() {
        fragColor = f_Color;
      }
    `;

    // 编译并链接着色器,得到 program(准备工作,此时还未绘制)
    const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
    if (!program) return;
    gl.useProgram(program);

    glRef.current = gl;
    programRef.current = program;

    renderCircle(300, 200, 100, 50);
  }

  function renderCircle(x: number, y: number, radius: number, segments: number) {
    const gl = glRef.current;
    if (!gl) return;
    const program = programRef.current;
    if (!program) return;
    const aPosition = gl.getAttribLocation(program, 'a_Position');
    const aColor = gl.getAttribLocation(program, 'a_Color');
    const color = [1.0, 0.0, 0.0, 1.0] as const;
    const points: number[] = [];
    for (let i = 0; i < segments; i++) {
      const angle = (i / segments) * 2 * Math.PI;
      points.push(
        getX(x + (radius / 2) * Math.cos(angle), canvasRef.current.width),
        getY(y + (radius / 2) * Math.sin(angle), canvasRef.current.height),
        getX(x + radius * Math.cos(angle), canvasRef.current.width),
        getY(y + radius * Math.sin(angle), canvasRef.current.height),
      );
    }
    const buffer = gl.createBuffer();
    points.push(points[0], points[1], points[2], points[3]);
    console.log(points);
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
    const stride = 2 * Float32Array.BYTES_PER_ELEMENT;
    gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
    gl.enableVertexAttribArray(aPosition);
    // 设置color为固定值
    gl.disableVertexAttribArray(aColor); // 关键
    gl.vertexAttrib4f(aColor, ...color);
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, points.length / 2);
  }

  // 组件挂载后初始化 WebGL2 并绘制
  useEffect(() => {
    initCanvas();
  }, []);

  return (
    <div>
      {/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
      <canvas ref={canvasRef} width="600" height="400"></canvas>
    </div>
  );
};

export default WebGL;

8. auto-ring

tsx 复制代码
import React, { useEffect, useRef } from 'react';

const getX = (x: number, width: number) => {
  return (x / width) * 2 - 1;
};
const getY = (y: number, height: number) => {
  return -(y / height) * 2 + 1;
};

/**
 * 随机半径:2-10
 */
const randomRadius = () => {
  return Math.ceil(Math.random() * 8) + 2;
};

const WebGL = () => {
  // 引用 canvas DOM,供 JS 操作绘图区域
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const glRef = useRef<WebGL2RenderingContext | null>(null);
  const programRef = useRef<WebGLProgram | null>(null);
  const circleRef = useRef<
    { points: number[]; radius: number; color: [number, number, number, number] }[]
  >([
    // { points: [], radius: 5, color: [1.0, 0.0, 0.0, 1.0] },
  ]);

  // 编译单个着色器(顶点或片元),失败返回 null
  function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
    const shader = gl.createShader(type);
    if (!shader) return null;
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(shader));
      return null;
    }
    return shader;
  }

  // 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
  function initShaderProgram(
    gl: WebGL2RenderingContext,
    vertexShaderSource: string,
    fragmentShaderSource: string,
  ) {
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器

    const program = gl.createProgram(); // 创建程序对象
    if (!program) return null;

    gl.attachShader(program, vertexShader); // 附加顶点着色器
    gl.attachShader(program, fragmentShader); // 附加片元着色器
    gl.linkProgram(program); // 链接程序对象
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.error(gl.getProgramInfoLog(program));
      return null;
    }
    return program;
  }

  function initCanvas() {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }

    // 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
    const gl = canvas.getContext('webgl2');
    if (!gl) return;

    // 清屏:设背景色(黑)并清空颜色缓冲区
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
    const vertexShaderSource = `#version 300 es
      in vec2 a_Position;  // 顶点位置(裁剪空间),JS 通过 buffer 写入
      in vec4 a_Color;     // 顶点颜色,JS 通过 buffer 写入
      out vec4 f_Color;    // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
      void main() {
        gl_Position = vec4(a_Position, 0.0, 1.0);
        gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
        f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
      }
    `;

    // 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
    // vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
    const fragmentShaderSource = `#version 300 es
      precision mediump float;
      in vec4 f_Color;     // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
      out vec4 fragColor;
      void main() {
        fragColor = f_Color;
      }
    `;

    // 编译并链接着色器,得到 program(准备工作,此时还未绘制)
    const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
    if (!program) return;
    gl.useProgram(program);

    glRef.current = gl;
    programRef.current = program;
  }

  function renderRing(
    x: number,
    y: number,
    radius: number,
    segments: number,
    color: [number, number, number, number],
  ) {
    const gl = glRef.current;
    if (!gl) return;
    const program = programRef.current;
    if (!program) return;
    const aPosition = gl.getAttribLocation(program, 'a_Position');
    const aColor = gl.getAttribLocation(program, 'a_Color');
    const points: number[] = [];
    for (let i = 0; i < segments; i++) {
      const angle = (i / segments) * 2 * Math.PI;
      points.push(
        getX(x + (radius / 2) * Math.cos(angle), canvasRef.current.width),
        getY(y + (radius / 2) * Math.sin(angle), canvasRef.current.height),
        getX(x + radius * Math.cos(angle), canvasRef.current.width),
        getY(y + radius * Math.sin(angle), canvasRef.current.height),
      );
    }
    const buffer = gl.createBuffer();
    // 闭合路径
    points.push(points[0], points[1], points[2], points[3]);
    // 绑定buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
    const stride = 2 * Float32Array.BYTES_PER_ELEMENT;
    gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
    gl.enableVertexAttribArray(aPosition);
    // a_Color 二选一:enable 从 buffer 读(需 stride 含 rgba);disable 则用 vertexAttrib4f 常量
    // buffer 仅有 x,y,须 disable,否则 vertexAttrib4f 被忽略,颜色/alpha 异常导致不可见
    gl.disableVertexAttribArray(aColor);
    gl.vertexAttrib4f(aColor, ...color);
    // 绘制
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, points.length / 2);
  }

  function handleMouseDown(event: React.MouseEvent<HTMLCanvasElement>) {
    // 坐标转换:offsetX/Y 相对 canvas 左上角(勿用 clientX/Y,canvas 不在页面原点时会偏移)
    // 裁剪空间 x∈[-1,1];y 需翻转(屏幕 Y 向下,WebGL Y 向上)
    const x = event.nativeEvent.offsetX;
    const y = event.nativeEvent.offsetY;
    circleRef.current.push({
      points: [x, y],
      radius: randomRadius(),
      color: [Math.random(), Math.random(), Math.random(), 1.0],
    });
    // 清屏:设背景色(黑)并清空颜色缓冲区
    const gl = glRef.current;
    if (!gl) return;
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    circleRef.current.forEach((circle) => {
      renderRing(circle.points[0], circle.points[1], circle.radius, 50, circle.color);
    });
  }

  // 组件挂载后初始化 WebGL2 并绘制
  useEffect(() => {
    initCanvas();
  }, []);

  return (
    <div>
      {/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
      <canvas ref={canvasRef} width="600" height="400" onMouseDown={handleMouseDown}></canvas>
    </div>
  );
};

export default WebGL;

9. rectangle

tsx 复制代码
import React, { useEffect, useRef } from 'react';

const getX = (x: number, width: number) => {
  return (x / width) * 2 - 1;
};
const getY = (y: number, height: number) => {
  return -(y / height) * 2 + 1;
};

const WebGL = () => {
  // 引用 canvas DOM,供 JS 操作绘图区域
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const glRef = useRef<WebGL2RenderingContext | null>(null);
  const programRef = useRef<WebGLProgram | null>(null);
  // 编译单个着色器(顶点或片元),失败返回 null
  function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
    const shader = gl.createShader(type);
    if (!shader) return null;
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(shader));
      return null;
    }
    return shader;
  }

  // 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
  function initShaderProgram(
    gl: WebGL2RenderingContext,
    vertexShaderSource: string,
    fragmentShaderSource: string,
  ) {
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器

    const program = gl.createProgram(); // 创建程序对象
    if (!program) return null;

    gl.attachShader(program, vertexShader); // 附加顶点着色器
    gl.attachShader(program, fragmentShader); // 附加片元着色器
    gl.linkProgram(program); // 链接程序对象
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.error(gl.getProgramInfoLog(program));
      return null;
    }
    return program;
  }

  function initCanvas() {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }

    // 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
    const gl = canvas.getContext('webgl2');
    if (!gl) return;

    // 清屏:设背景色(黑)并清空颜色缓冲区
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
    const vertexShaderSource = `#version 300 es
      in vec2 a_Position;  // 顶点位置(裁剪空间),JS 通过 buffer 写入
      in vec4 a_Color;     // 顶点颜色,JS 通过 buffer 写入
      out vec4 f_Color;    // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
      void main() {
        gl_Position = vec4(a_Position, 0.0, 1.0);
        gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
        f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
      }
    `;

    // 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
    // vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
    const fragmentShaderSource = `#version 300 es
      precision mediump float;
      in vec4 f_Color;     // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
      out vec4 fragColor;
      void main() {
        fragColor = f_Color;
      }
    `;

    // 编译并链接着色器,得到 program(准备工作,此时还未绘制)
    const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
    if (!program) return;
    gl.useProgram(program);

    glRef.current = gl;
    programRef.current = program;

    renderRectangle(300, 200, 100, 50);
  }

  function renderRectangle(x: number, y: number, width: number, height: number) {
    const gl = glRef.current;
    if (!gl) return;
    const program = programRef.current;
    if (!program) return;
    const aPosition = gl.getAttribLocation(program, 'a_Position');
    const aColor = gl.getAttribLocation(program, 'a_Color');
    const color = [1.0, 0.0, 0.0, 1.0] as const;
    const points: number[] = [];

    points.push(
      getX(x + width / 2, canvasRef.current.width),
      getY(y - height / 2, canvasRef.current.height),
      getX(x + width / 2, canvasRef.current.width),
      getY(y + height / 2, canvasRef.current.height),
      getX(x - width / 2, canvasRef.current.width),
      getY(y - height / 2, canvasRef.current.height),
      getX(x - width / 2, canvasRef.current.width),
      getY(y + height / 2, canvasRef.current.height),
    );
    const buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
    const stride = 2 * Float32Array.BYTES_PER_ELEMENT;
    gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
    gl.enableVertexAttribArray(aPosition);
    // 设置color为固定值
    gl.disableVertexAttribArray(aColor); // 关键
    gl.vertexAttrib4f(aColor, ...color);
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, points.length / 2);
  }

  // 组件挂载后初始化 WebGL2 并绘制
  useEffect(() => {
    initCanvas();
  }, []);

  return (
    <div>
      {/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
      <canvas ref={canvasRef} width="600" height="400"></canvas>
    </div>
  );
};

export default WebGL;

10. auto-rectangle

tsx 复制代码
import React, { useEffect, useRef } from 'react';

const getX = (x: number, width: number) => {
  return (x / width) * 2 - 1;
};
const getY = (y: number, height: number) => {
  return -(y / height) * 2 + 1;
};

/**
 * 随机宽度:5-13
 * 随机高度:5-13
 */
const randomWidthAndHeight = () => {
  return Math.ceil(Math.random() * 8) + 5;
};

const WebGL = () => {
  // 引用 canvas DOM,供 JS 操作绘图区域
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const glRef = useRef<WebGL2RenderingContext | null>(null);
  const programRef = useRef<WebGLProgram | null>(null);
  const rectangleRef = useRef<
    { points: number[]; width: number; height: number; color: [number, number, number, number] }[]
  >([
    // { points: [], width: 5, height: 5, color: [1.0, 0.0, 0.0, 1.0] },
  ]);

  // 编译单个着色器(顶点或片元),失败返回 null
  function loadShader(gl: WebGL2RenderingContext, type: number, source: string) {
    const shader = gl.createShader(type);
    if (!shader) return null;
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(shader));
      return null;
    }
    return shader;
  }

  // 将顶点 + 片元着色器链接成一个可执行的 GPU 程序
  function initShaderProgram(
    gl: WebGL2RenderingContext,
    vertexShaderSource: string,
    fragmentShaderSource: string,
  ) {
    const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertexShaderSource); // 创建顶点着色器
    const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建片元着色器

    const program = gl.createProgram(); // 创建程序对象
    if (!program) return null;

    gl.attachShader(program, vertexShader); // 附加顶点着色器
    gl.attachShader(program, fragmentShader); // 附加片元着色器
    gl.linkProgram(program); // 链接程序对象
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      console.error(gl.getProgramInfoLog(program));
      return null;
    }
    return program;
  }

  function initCanvas() {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }

    // 获取 WebGL2 上下文,后续所有绘制通过 gl 操作
    const gl = canvas.getContext('webgl2');
    if (!gl) return;

    // 清屏:设背景色(黑)并清空颜色缓冲区
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 顶点着色器:接收 JS 传入的 attribute,并负责把需交给片元的变量用 out 声明转发
    const vertexShaderSource = `#version 300 es
      in vec2 a_Position;  // 顶点位置(裁剪空间),JS 通过 buffer 写入
      in vec4 a_Color;     // 顶点颜色,JS 通过 buffer 写入
      out vec4 f_Color;    // 转发给片元着色器(片元侧同名 in 接收,不可跳过本阶段)
      void main() {
        gl_Position = vec4(a_Position, 0.0, 1.0);
        gl_PointSize = 5.0; // POINTS 模式必须设置,否则点可能只有 1 像素或不可见
        f_Color = a_Color; // 片元无法直接读 attribute,须经此处 out 传递
      }
    `;

    // 片元着色器:只能接收顶点 out 对应的 in,不能直接读 JS attribute
    // vec4(R,G,B,A) 各分量 0.0~1.0;WebGL2 无 gl_FragColor,需声明 out 输出最终颜色
    const fragmentShaderSource = `#version 300 es
      precision mediump float;
      in vec4 f_Color;     // 与顶点 out f_Color 配对,由 GPU 在三角形内插值后传入
      out vec4 fragColor;
      void main() {
        fragColor = f_Color;
      }
    `;

    // 编译并链接着色器,得到 program(准备工作,此时还未绘制)
    const program = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
    if (!program) return;
    gl.useProgram(program);

    glRef.current = gl;
    programRef.current = program;
  }

  function renderRectangle(
    x: number,
    y: number,
    width: number,
    height: number,
    color: [number, number, number, number],
  ) {
    const gl = glRef.current;
    if (!gl) return;
    const program = programRef.current;
    if (!program) return;
    const aPosition = gl.getAttribLocation(program, 'a_Position');
    const aColor = gl.getAttribLocation(program, 'a_Color');
    const points: number[] = [];

    points.push(
      getX(x + width / 2, canvasRef.current.width),
      getY(y - height / 2, canvasRef.current.height),
      getX(x + width / 2, canvasRef.current.width),
      getY(y + height / 2, canvasRef.current.height),
      getX(x - width / 2, canvasRef.current.width),
      getY(y - height / 2, canvasRef.current.height),
      getX(x - width / 2, canvasRef.current.width),
      getY(y + height / 2, canvasRef.current.height),
    );

    const buffer = gl.createBuffer();
    // 绑定buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
    const stride = 2 * Float32Array.BYTES_PER_ELEMENT;
    gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0);
    gl.enableVertexAttribArray(aPosition);
    // a_Color 二选一:enable 从 buffer 读(需 stride 含 rgba);disable 则用 vertexAttrib4f 常量
    // buffer 仅有 x,y,须 disable,否则 vertexAttrib4f 被忽略,颜色/alpha 异常导致不可见
    gl.disableVertexAttribArray(aColor);
    gl.vertexAttrib4f(aColor, ...color);
    // 绘制
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, points.length / 2);
  }

  function handleMouseDown(event: React.MouseEvent<HTMLCanvasElement>) {
    // 坐标转换:offsetX/Y 相对 canvas 左上角(勿用 clientX/Y,canvas 不在页面原点时会偏移)
    // 裁剪空间 x∈[-1,1];y 需翻转(屏幕 Y 向下,WebGL Y 向上)
    const x = event.nativeEvent.offsetX;
    const y = event.nativeEvent.offsetY;
    rectangleRef.current.push({
      points: [x, y],
      width: randomWidthAndHeight(),
      height: randomWidthAndHeight(),
      color: [Math.random(), Math.random(), Math.random(), 1.0],
    });
    // 清屏:设背景色(黑)并清空颜色缓冲区
    const gl = glRef.current;
    if (!gl) return;
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    rectangleRef.current.forEach((rectangle) => {
      renderRectangle(
        rectangle.points[0],
        rectangle.points[1],
        rectangle.width,
        rectangle.height,
        rectangle.color,
      );
    });
  }

  // 组件挂载后初始化 WebGL2 并绘制
  useEffect(() => {
    initCanvas();
  }, []);

  return (
    <div>
      {/* width/height 为画布像素尺寸,非 CSS 尺寸 */}
      <canvas ref={canvasRef} width="600" height="400" onMouseDown={handleMouseDown}></canvas>
    </div>
  );
};

export default WebGL;
相关推荐
程序员黑豆1 小时前
AI全栈开发系列开篇:从Java全栈到AI应用实战
前端·ai编程·全栈
yangyj1 小时前
从 PDR 到落地:用 Codex 完成一次 Rspack 升级
前端
程序员鱼皮1 小时前
提示词工程已死,Loop Engineering 称王!保姆级教程 + 项目实战
前端·后端·ai编程
小爷毛毛_卓寿杰2 小时前
给 Embedding 模型也加一块“游乐场“—— Xinference 是怎么把 vector 变成肉眼可见的体验的
前端
忆江南2 小时前
iOS 性能优化全面详解
前端
lichenyang4532 小时前
HAP / HAR / HSP 到底啥区别?顺带把「导入」那点疑惑讲清楚
前端
基德爆肝c语言2 小时前
MySQL表的操作
前端·数据库·mysql
秃头网友小李2 小时前
前端难点:Element Plus 样式覆盖 —— :deep()、CSS 变量与滚动状态类名
前端·vue.js
the_answer2 小时前
XSS 与 CSRF 深度解析
前端