React 驱动的顶点数据缓冲:探究如何将 React 状态变更转化为 WebGL 缓冲区(VBO)的增量更新

女士们,先生们,欢迎来到这场关于“React 状态与 WebGL 缓冲区之爱恨情仇”的深度技术讲座。

我是你们今天的讲师,一个在 React 的虚拟世界里摸爬滚打,又在 WebGL 的像素海洋里差点溺水的高级工程师。

今天,我们不聊那些花里胡哨的 UI 组件,不聊那些让你头秃的 CSS Flexbox。今天,我们要聊聊 WebGL 的硬核内脏——顶点数据缓冲,以及 React 这个“唠叨”的家长是如何试图控制它的。我们将探讨一个极其性感且极具挑战性的话题:如何将 React 的声明式状态变更,转化为 WebGL 的命令式增量更新。

准备好了吗?让我们开始这场“内存拷贝”的狂欢。


第一章:当 React 遇到 WebGL —— 确认过眼神,是两个世界的人

首先,我们要认清现实。React 和 WebGL,这俩货就像是两个不同星球的居民。

React 是个典型的“乖宝宝”。它讲究声明式编程。你告诉它“我想让这个数字变成 100”,它就会乖乖地去修改虚拟 DOM,然后通过 Diffing 算法找出最小的变化路径,最后把那一点点 DOM 节点更新到屏幕上。它很聪明,但也很“慢”(相对于直接操作内存来说)。

WebGL 则是个暴躁的“老技工”。它是个命令式的怪兽。它不在乎你的数据结构是否优雅,它只在乎指令是否精准。你想画个三角形?行,给我 gl.drawArrays。你想改个顶点?行,给我 gl.bufferData。它就像一个只认死理的将军,你命令它,它才动。如果你不给它命令,它就在那儿干瞪眼,或者更糟,它正在后台疯狂地清空内存、重写数据。

痛点来了:
如果你在 React 的 useEffect 里,每当 state 变化就调用一次 gl.bufferData,会发生什么?

想象一下,你的 React 组件渲染了 10 次(因为父组件传了个新 prop),每次渲染都触发 useEffect,然后 gl.bufferData 就被调用 10 次。这意味着什么?意味着你的 GPU 被迫 10 次重新分配内存,10 次把数据从 CPU 拷贝到 GPU 显存。这就像你明明只需要给老板发一封邮件,结果你每次都把电脑重启一遍再发。

这不仅是浪费,这是对硬件的亵渎。

第二章:打破同步 —— useRef 的救赎

要解决这个问题,我们要做的第一件事就是打破同步

React 的状态更新是同步的,它会触发组件重新渲染。而 WebGL 的操作必须在特定的生命周期中(比如 useEffect 的回调里)进行。我们不能在渲染函数里直接操作 WebGL,否则 React 会报错(或者更糟,性能会像蜗牛一样爬)。

我们需要一个“离屏”的地方来存放 WebGL 的上下文和当前的数据。

代码示例 1:建立 React 和 WebGL 的安全通道

import React, { useRef, useEffect, useState } from 'react';

const WebGLBufferComponent = () => {
  // 1. useRef 是我们的秘密基地。它不会引起 React 的重新渲染。
  const glRef = useRef(null);
  const bufferRef = useRef(null);

  // 模拟我们的顶点数据
  const [vertices, setVertices] = useState(new Float32Array([0, 0, 0, 1, 1, 0]));

  useEffect(() => {
    const canvas = document.getElementById('glCanvas');
    const gl = canvas.getContext('webgl');

    if (!gl) return;

    // 初始化 WebGL 上下文
    glRef.current = gl;

    // 创建 VBO (顶点缓冲对象)
    const buffer = gl.createBuffer();
    bufferRef.current = buffer;

    // 第一次:把数据塞进去
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

    // 渲染一帧
    render(gl);

    return () => {
      // 清理工作(别忘的!)
      gl.deleteBuffer(buffer);
    };
  }, []);

  const render = (gl) => {
    // ... 简单的着色器和绘制代码 ...
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    // 这里省略了 shader program 的 setup 和 draw call 的细节
  };

  const handleClick = () => {
    // React 状态更新
    setVertices(new Float32Array([1, 1, 1, 0, 0, 0, 0.5, 0.5, 0.5]));
  };

  return (
    <div>
      <button onClick={handleClick}>更新数据</button>
      <canvas id="glCanvas" width={500} height={500} />
    </div>
  );
};

上面的代码是安全的,但它是低效的。每次点击按钮,vertices 状态改变,React 重新渲染,useEffect 再次执行,gl.bufferData 把整个数组从头到尾重写一遍。

第三章:数据结构 —— TypedArrays 的魔法

在深入增量更新之前,我们必须聊聊数据结构。这是 React 和 WebGL 的另一个冲突点。

React 喜欢用普通的 JavaScript 数组或对象来管理状态。
WebGL 喜欢用 TypedArrays(如 Float32Array, Uint16Array)。

普通的 JS 数组是动态的,可以随意 push,占用内存大,遍历慢。而 Float32Array 就像是内存里的一块预定义的“积木”,速度快,内存紧凑。

React 的痛点: 你想修改数组里的第 100 个元素。在 JS 数组里,你只能 arr[100] = newValue,这会触发 React 的 Diff 算法,导致整个组件树重新计算。

WebGL 的痛点: gl.bufferDatagl.bufferSubData 都是基于字节偏移量 的。

所以,我们的策略是:

  1. 在 React 状态里存一个 Float32Array(或者通过 useMemo 转换)。
  2. useEffect 里,我们只处理 WebGL 的“脏活累活”。

第四章:增量更新的核心 —— gl.bufferSubData

这是今天的重头戏。我们不想每次都 bufferData(重置),我们想用 bufferSubData(增量)。

gl.bufferData:清空缓冲区,然后写入新数据。开销大。
gl.bufferSubData:从缓冲区的某个偏移量开始,写入新数据。开销小。

那么,React 的状态变了,我们怎么知道 WebGL 的缓冲区哪里变了呢?

我们需要一个“守门员”。每次 React 状态更新时,我们都要比对“旧数据”和“新数据”。如果数据长度变了,我们就只能老老实实地 bufferData。如果长度没变,我们就遍历数组,找到变化的那个索引,然后只更新那个索引。

代码示例 2:实现增量更新逻辑

const updateBuffer = (gl, buffer, oldData, newData) => {
  // 1. 长度检查:如果长度变了,那就没办法了,只能全量替换
  if (oldData.length !== newData.length) {
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, newData, gl.DYNAMIC_DRAW);
    return newData; // 返回新数据作为下一次的旧数据
  }

  // 2. 长度未变:开始寻找变化
  // 注意:这是 CPU 端的遍历,非常快,因为 TypedArray 的遍历比 JS 数组快得多
  let hasChanges = false;
  let firstChangeIndex = -1;

  // 我们只比较 Float32Array 的每个元素
  for (let i = 0; i < newData.length; i++) {
    if (newData[i] !== oldData[i]) {
      hasChanges = true;
      firstChangeIndex = i;
      break; // 找到第一个变化就停止,因为 WebGL 的 bufferSubData 只能从 offset 开始写
    }
  }

  if (hasChanges) {
    // 计算字节偏移量:每个 float 是 4 个字节
    const byteOffset = firstChangeIndex * 4; 

    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    // 呼叫 bufferSubData,只更新变化的这一小段
    gl.bufferSubData(gl.ARRAY_BUFFER, byteOffset, newData.subarray(firstChangeIndex));
  }

  return newData;
};

这段代码的逻辑非常优雅:
它捕捉了 React 状态变化的“本质”。React 的状态更新通常是局部的(比如用户改了一个数字),我们利用这个局部性,只告诉 GPU 更新那一小块内存。

但是,上面的代码有个小瑕疵。如果数组很大,比如 10 万个顶点,而我们只改了第一个顶点,那么上面的 for 循环虽然只 break 了,但它是从 0 开始遍历的。对于大数据量,这其实还是有点浪费的。

不过,在现代 JS 引擎的优化下,对于几百到几千个顶点的数组,这种遍历是微不足道的。真正的性能瓶颈在于 gl.bindBuffer 和数据拷贝。

第五章:性能陷阱 —— bindBuffer 的隐形杀手

在 WebGL 中,gl.bindBuffer 是个昂贵的操作。它就像是去图书馆借书,每次都要去柜台排队。

每次我们调用 gl.bindBuffer(gl.ARRAY_BUFFER, buffer),GPU 都要暂停当前的任务,去处理这个请求。

在 React 的世界里,我们可能在一个渲染周期里多次修改状态,或者父组件频繁传递 props 导致子组件频繁重渲染。

优化策略:保持绑定状态。

一旦我们绑定了一个缓冲区,如果在下一次更新中我们还是更新这个缓冲区,我们就不要再次调用 bindBuffer

const updateBufferOptimized = (gl, buffer, oldData, newData) => {
  // ... 前面的 length 检查逻辑 ...

  if (hasChanges) {
    // 只有当 buffer 还没有绑定,或者绑定的不是这个 buffer 时,才去 bind
    // 这里需要维护一个额外的 state 来记录当前绑定的 buffer id,或者利用 gl 的状态
    // 简单起见,假设我们通过某种方式知道当前是否已绑定
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 

    const byteOffset = firstChangeIndex * 4;
    gl.bufferSubData(gl.ARRAY_BUFFER, byteOffset, newData.subarray(firstChangeIndex));
  }
};

但这在 React 中很难完美做到,因为 React 的渲染周期是异步且不确定的。我们不知道上一次 useEffect 执行完后,中间有没有其他地方调用了 gl.bindBuffer

这就引出了更高级的技术:Buffer Mapping(缓冲区映射)

第六章:终极奥义 —— gl.mapBufferRange (MapBufferRange)

如果你觉得 bufferSubData 还是太慢,因为每次都要把数据从 CPU 拷贝到显存,那你就需要了解 gl.mapBufferRange

mapBufferRange 允许我们直接在 CPU 内存中修改 WebGL 缓冲区的内容,而不需要显式地调用 bufferSubData。这就像是直接拿到了 GPU 显存的一块物理内存地址,你改内存里的值,GPU 就会自动看到。

警告: 这是一个双刃剑。如果你修改了 GPU 正在使用的内存,画面就会崩坏。

代码示例 3:使用 MapBufferRange 进行零拷贝更新

const updateBufferWithMapping = (gl, buffer, oldData, newData) => {
  // 1. 尝试映射缓冲区,只映射我们要修改的那一部分
  // gl.UNSIGNED_SHORT_BIT | gl.UNSIGNED_INT_BIT 表示我们要写入的是整数类型
  // gl.PERSISTENT_BIT | gl.WRITE_INVALIDATE_BIT 是关键
  const pointer = gl.mapBufferRange(gl.ARRAY_BUFFER, 0, newData.byteLength, 
    gl.MAP_WRITE_BIT | gl.MAP_INVALIDATE_BUFFER_BIT | gl.MAP_UNSYNCHRONIZED_BIT);

  if (!pointer) return; // 映射失败

  // 2. 直接写入内存
  // pointer 是一个 ArrayBufferView,可以直接赋值
  const typedPointer = new Float32Array(pointer);
  typedPointer.set(newData);

  // 3. 必须调用 unmap 告诉 GPU 内存已经就绪
  // 在 React 的 useEffect 中,我们通常不需要手动 unmap,因为 useEffect 结束时上下文会被清理
  // 但为了保险,这里演示手动 unmap 的逻辑
  gl.unmapBuffer(gl.ARRAY_BUFFER);
};

这里发生了什么?
gl.MAP_INVALIDATE_BUFFER_BIT 告诉 GPU:“你手里的旧数据我不要了,我直接覆盖你。”
gl.MAP_UNSYNCHRONIZED_BIT 告诉 GPU:“别管我,直接改,就算你正在用这块内存也没关系(虽然这很危险,但在 React 这种单线程环境下通常是安全的)。”

通过这种方式,我们将 CPU 到 GPU 的数据传输从“拷贝”变成了“赋值”,性能提升是惊人的。

第七章:封装的艺术 —— 抽象出 React 风格的 WebGL Buffer

既然手动管理 gl.bufferDatagl.bufferSubDatamapBufferRange 这么痛苦,而且容易出错,为什么不把它们封装起来,让 React 看起来像是在直接操作 Buffer 呢?

我们需要一个自定义的 Hook:useWebGLBuffer

这个 Hook 应该具备以下能力:

  1. 接收 React 的 data(通常是一个 Float32Array)。
  2. 监听 data 的变化。
  3. 自动选择最佳更新策略(全量更新 vs 增量更新 vs MapBuffer)。
  4. 返回 WebGL 的 Buffer 对象和渲染函数。

代码示例 4:封装后的 Hook

import { useState, useEffect, useRef, useMemo } from 'react';

const useWebGLBuffer = (data, drawCallback) => {
  const glRef = useRef(null);
  const bufferRef = useRef(null);
  const isMountedRef = useRef(false);

  // 记录上一次的数据,用于 Diffing
  const prevDataRef = useRef(null);

  // 初始化 WebGL 资源
  useEffect(() => {
    const canvas = document.getElementById('glCanvas');
    const gl = canvas.getContext('webgl');
    if (!gl) return;

    glRef.current = gl;
    isMountedRef.current = true;

    const buffer = gl.createBuffer();
    bufferRef.current = buffer;

    // 初始化:全量上传
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

    return () => {
      gl.deleteBuffer(buffer);
      isMountedRef.current = false;
    };
  }, []);

  // 处理数据更新
  useEffect(() => {
    if (!glRef.current || !bufferRef.current || !isMountedRef.current) return;

    const gl = glRef.current;
    const buffer = bufferRef.current;
    const prevData = prevDataRef.current;

    // 如果没有旧数据(初始化阶段),直接上传
    if (!prevData) {
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
      gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
      prevDataRef.current = data;
      return;
    }

    // 策略选择逻辑
    const needsFullUpdate = prevData.length !== data.length;

    if (needsFullUpdate) {
      // 策略 A:长度变了,只能重置
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
      gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
    } else {
      // 策略 B:长度没变,尝试增量更新
      // 这里我们简化逻辑,直接使用 bufferSubData 全量替换(对于少量数据通常比遍历快)
      // 或者实现上面的增量遍历逻辑
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
      gl.bufferSubData(gl.ARRAY_BUFFER, 0, data);
    }

    prevDataRef.current = data;

  }, [data]);

  // 渲染函数
  const render = () => {
    if (!glRef.current) return;
    const gl = glRef.current;

    // 简单的渲染循环
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES, 0, data.length / 3); // 假设每个顶点是3个float
  };

  return { buffer: bufferRef.current, render };
};

// --- 使用示例 ---

const MyComponent = () => {
  const [vertices, setVertices] = useState(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]));

  const { render } = useWebGLBuffer(vertices);

  // 偶尔更新数据
  const randomize = () => {
    const newData = new Float32Array(9);
    for(let i=0; i<9; i++) newData[i] = Math.random();
    setVertices(newData);
  };

  return (
    <div>
      <button onClick={randomize}>随机化顶点</button>
      <canvas id="glCanvas" width={500} height={500} />
      <button onClick={render}>手动重绘</button>
    </div>
  );
};

第八章:深入剖析 —— 为什么我们这么做?

通过上面的封装,我们成功地让 React 看起来像是直接控制了 WebGL Buffer。但实际上,我们在中间层做了一层非常复杂的“翻译”。

让我们回顾一下性能的时间线

  1. React 渲染周期: 用户点击按钮 -> setVertices -> React 重新渲染 -> vertices 变量更新。耗时:极短(微秒级)
  2. React Effect 触发: useEffect 检测到 vertices 依赖变化。耗时:极短
  3. 数据 Diff: 我们比较了新旧数组。耗时:取决于数组长度
  4. WebGL 命令执行: gl.bindBuffer -> gl.bufferSubData耗时:取决于显存带宽

关键点在于:React 的渲染周期通常比 WebGL 的数据传输要快得多。 如果我们在 React 里频繁地 setState,React 会帮我们做批处理。如果我们不把 React 的频繁更新“节流”或者“合并”,那么即使我们在 WebGL 层面做了优化,CPU 的负担也会很重。

所以,React 驱动的顶点更新,不仅仅是关于 WebGL 的 API 调用,更是关于数据流的管理

第九章:高级话题 —— 多 Buffer 与 绑定状态管理

在实际的大型项目中,我们不仅仅有一个 Buffer。我们可能有位置 Buffer (POSITION)、颜色 Buffer (COLOR)、索引 Buffer (INDEX)。

当 React 更新颜色数据时,我们需要确保我们绑定的还是 COLOR_BUFFER,而不是 POSITION_BUFFER。如果绑错了,颜色数据就会写进位置缓冲区,导致图形扭曲。

解决方案:
不要在每次更新时都去 gl.bindBuffer。我们应该维护一个全局的“绑定状态机”。

// 简单的绑定状态机
const glState = {
  boundBuffer: null,
  boundProgram: null
};

const bindBufferSafely = (gl, target, buffer) => {
  if (glState.boundBuffer !== buffer) {
    gl.bindBuffer(target, buffer);
    glState.boundBuffer = buffer;
  }
};

在 React 的 useEffect 中,我们只需要调用这个安全的绑定函数。这能极大地减少 WebGL 的状态切换开销。

第十章:实战中的坑与调试

当你开始这样写代码时,你会发现几个非常坑爹的问题:

  1. 类型不匹配: React 传过来的是 Float32Array,你写 gl.bufferData 时传错了类型,或者 Shader 期望的是 int 但你传了 float。结果:渲染一片黑。
  2. 生命周期错乱:useEffect 里设置了 Buffer,但组件卸载了。如果你再次挂载同一个组件,WebGL Context 可能会丢失或者报错。
  3. 内存泄漏: React 的 useRef 里的数据如果不小心引用了外部对象,可能导致内存无法释放。

调试技巧:
不要相信你的眼睛。屏幕上显示的可能是旧的 Buffer 数据,因为 GPU 是异步渲染的。
使用 Chrome DevTools 的 WebGL Inspector 或者 RenderDoc。它们可以让你看到当前 Buffer 里到底存了什么数据。当你怀疑 React 的 setVertices 没生效时,用这个工具一抓,真相大白。

第十一章:未来的展望 —— React 19 与 WebGL

随着 React 19 的发布,它的并发模式和 Server Components 带来了更多的可能性。

未来的 React 可能会更好地处理“副作用”的时机。也许我们不需要再手动在 useEffect 里写 WebGL 的初始化逻辑了。也许我们会看到像 useFrame (来自 Three.js) 那样的概念被 React 原生支持,或者更激进一点,React 能直接识别 WebGL 的 Buffer 并提供类似 useMemo 的缓存机制。

但在此之前,掌握“如何将 React 状态转化为 WebGL 命令”依然是我们作为前端图形学工程师的必修课。

结语:保持敬畏,保持性能

好了,同学们,今天的讲座就到这里。

我们回顾了从最初的“同步地狱”到现在的“增量优化”的全过程。我们学会了使用 useRef 来隔离 React 的渲染周期,学会了使用 TypedArrays 来优化内存,学会了使用 bufferSubDatamapBufferRange 来实现高性能的更新。

记住,React 负责告诉“世界发生了什么变化”,而 WebGL 负责以最高的效率去执行这些变化。不要让 React 的“虚拟”特性拖累了 WebGL 的“实体”性能。

现在,拿起你的代码,去优化你的 Buffer 吧!别让你的 GPU 因为你的低效代码而哭泣。下课!


附录:完整示例代码 (混合了 React 和 WebGL 的最佳实践)

import React, { useState, useEffect, useRef } from 'react';

const AdvancedWebGLBuffer = () => {
  const canvasRef = useRef(null);
  const glRef = useRef(null);
  const bufferRef = useRef(null);

  // 维护一个引用,用于在 effect 之间传递旧数据
  const prevDataRef = useRef(null);
  // 维护一个引用,用于追踪 buffer 的绑定状态,避免无效的 bind 调用
  const boundBufferRef = useRef(null);

  const [vertices, setVertices] = useState(new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]));

  // 初始化 WebGL 上下文和 Buffer
  useEffect(() => {
    const canvas = canvasRef.current;
    const gl = canvas.getContext('webgl', { preserveDrawingBuffer: true }); // preserveDrawingBuffer 允许截图

    if (!gl) return;

    glRef.current = gl;

    const buffer = gl.createBuffer();
    bufferRef.current = buffer;

    // 初始上传
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

    boundBufferRef.current = buffer;

    return () => {
      gl.deleteBuffer(buffer);
    };
  }, []);

  // 核心更新逻辑
  useEffect(() => {
    if (!glRef.current || !bufferRef.current) return;

    const gl = glRef.current;
    const buffer = bufferRef.current;
    const prevData = prevDataRef.current;
    const currentData = vertices;

    // 1. 检查是否需要重新创建 Buffer (长度变化)
    if (prevData && prevData.length !== currentData.length) {
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
      gl.bufferData(gl.ARRAY_BUFFER, currentData, gl.DYNAMIC_DRAW);
    } else {
      // 2. 如果长度没变,尝试增量更新
      // 简单实现:全量 bufferSubData。对于复杂场景,可以在这里实现 Diff 算法
      // 只有当 buffer 确实被绑定了(或者是当前绑定的 buffer)时才 bind
      if (boundBufferRef.current !== buffer) {
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        boundBufferRef.current = buffer;
      }
      gl.bufferSubData(gl.ARRAY_BUFFER, 0, currentData);
    }

    // 更新引用
    prevDataRef.current = currentData;

    // 3. 立即重绘
    renderScene(gl, buffer);
  }, [vertices]);

  const renderScene = (gl, buffer) => {
    if (!gl) return;

    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    gl.clearColor(0.1, 0.1, 0.1, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 简单的着色器程序
    // Vertex Shader
    const vsSource = `
      attribute vec4 aVertexPosition;
      void main() {
        gl_Position = aVertexPosition;
      }
    `;
    // Fragment Shader
    const fsSource = `
      void main() {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
      }
    `;

    // 编译 Shader (为了演示简单,这里省略了复杂的 shader 编译逻辑,实际项目应缓存 shader program)
    const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
    const programInfo = {
      program: shaderProgram,
      attribLocations: {
        vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
      },
    };

    gl.useProgram(programInfo.program);

    // 绑定 Buffer
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.vertexAttribPointer(
      programInfo.attribLocations.vertexPosition,
      3, // numComponents (x, y, z)
      gl.FLOAT,
      false,
      0,
      0
    );
    gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);

    gl.drawArrays(gl.TRIANGLES, 0, 3);
  };

  // 辅助函数:编译 Shader (为了代码完整性)
  const initShaderProgram = (gl, vsSource, fsSource) => {
    // ... 省略 ...
    return null; 
  };

  const handleClick = () => {
    // 模拟一个简单的动画效果:旋转三角形
    const newData = new Float32Array([
      Math.random(), Math.random(), Math.random(), // x, y, z
      Math.random(), Math.random(), Math.random(),
      Math.random(), Math.random(), Math.random()
    ]);
    setVertices(newData);
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h2>React 驱动的 WebGL Buffer 增量更新</h2>
      <div style={{ display: 'flex', gap: '20px' }}>
        <canvas ref={canvasRef} width={500} height={500} style={{ border: '1px solid #ccc' }} />
        <div>
          <button onClick={handleClick} style={{ padding: '10px 20px', cursor: 'pointer' }}>
            随机更新顶点
          </button>
          <p>观察控制台或 Canvas,你应该能看到顶点实时变化。</p>
          <p>注意:这种更新方式避免了不必要的 gl.bufferData 调用。</p>
        </div>
      </div>
    </div>
  );
};

export default AdvancedWebGLBuffer;

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注