大白话react第十七章React 与 WebGL 项目进阶优化及拓展
大白话react第十七章React 与 WebGL 项目进阶优化及拓展
1. 引入物理引擎
在 React 和 WebGL 结合的项目里,加入物理引擎能让 3D 场景更真实,就像在现实世界里物体有重力、碰撞等效果一样。这里我们用 cannon-es
这个物理引擎库。
// 引入 React 的 useEffect 和 useRef 钩子
import React, { useEffect, useRef } from'react';
// 引入 three.js 用于创建 3D 场景
import * as THREE from 'three';
// 引入 cannon-es 物理引擎库
import * as CANNON from 'cannon-es';
const PhysicsComponent = () => {
// 创建一个 ref 用于引用存放 3D 场景的 DOM 元素
const containerRef = useRef(null);
useEffect(() => {
// 创建 three.js 的场景
const scene = new THREE.Scene();
// 创建透视相机,设置视角、宽高比、近裁剪面和远裁剪面
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// 创建 WebGL 渲染器
const renderer = new THREE.WebGLRenderer();
// 设置渲染器的大小为窗口大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 将渲染器的 DOM 元素添加到 ref 对应的 DOM 元素中
containerRef.current.appendChild(renderer.domElement);
// 创建 cannon-es 的物理世界
const world = new CANNON.World();
// 设置物理世界的重力,这里模拟向下的重力
world.gravity.set(0, -9.82, 0);
// 创建一个地面的物理材质
const groundMaterial = new CANNON.Material('groundMaterial');
// 创建地面的形状,这里是平面
const groundShape = new CANNON.Plane();
// 创建地面的刚体
const groundBody = new CANNON.Body({
mass: 0, // 质量为 0 表示静止物体
shape: groundShape,
material: groundMaterial
});
// 旋转地面使其水平
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
// 将地面刚体添加到物理世界
world.addBody(groundBody);
// 创建一个立方体的物理材质
const boxMaterial = new CANNON.Material('boxMaterial');
// 创建立方体的形状
const boxShape = new CANNON.Box(new CANNON.Vec3(1, 1, 1));
// 创建立方体的刚体,设置质量和位置
const boxBody = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(0, 10, 0),
shape: boxShape,
material: boxMaterial
});
// 将立方体刚体添加到物理世界
world.addBody(boxBody);
// 创建地面的 3D 模型
const groundGeometry = new THREE.PlaneGeometry(10, 10);
const groundMaterialThree = new THREE.MeshBasicMaterial({ color: 0x808080, side: THREE.DoubleSide });
const groundMesh = new THREE.Mesh(groundGeometry, groundMaterialThree);
groundMesh.rotation.x = -Math.PI / 2;
scene.add(groundMesh);
// 创建立方体的 3D 模型
const boxGeometry = new THREE.BoxGeometry(2, 2, 2);
const boxMaterialThree = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const boxMesh = new THREE.Mesh(boxGeometry, boxMaterialThree);
scene.add(boxMesh);
// 设置相机位置
camera.position.z = 5;
// 定义渲染函数
const animate = () => {
// 请求下一帧动画
requestAnimationFrame(animate);
// 更新物理世界
world.step(1 / 60);
// 将立方体刚体的位置和旋转同步到 3D 模型上
boxMesh.position.copy(boxBody.position);
boxMesh.quaternion.copy(boxBody.quaternion);
// 渲染场景
renderer.render(scene, camera);
};
// 开始动画循环
animate();
// 组件卸载时清理资源
return () => {
containerRef.current.removeChild(renderer.domElement);
};
}, []);
return (
// 创建一个 div 用于存放 3D 场景
<div ref={containerRef} />
);
};
export default PhysicsComponent;
2. 实现光照与阴影效果
光照和阴影能让 3D 场景更有立体感和真实感。在 three.js 里可以很方便地实现不同类型的光照和阴影效果。
// 引入 React 的 useEffect 和 useRef 钩子
import React, { useEffect, useRef } from'react';
// 引入 three.js 用于创建 3D 场景
import * as THREE from 'three';
const LightingAndShadowsComponent = () => {
// 创建一个 ref 用于引用存放 3D 场景的 DOM 元素
const containerRef = useRef(null);
useEffect(() => {
// 创建 three.js 的场景
const scene = new THREE.Scene();
// 创建透视相机,设置视角、宽高比、近裁剪面和远裁剪面
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// 创建 WebGL 渲染器
const renderer = new THREE.WebGLRenderer();
// 设置渲染器的大小为窗口大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启渲染器的阴影支持
renderer.shadowMap.enabled = true;
// 将渲染器的 DOM 元素添加到 ref 对应的 DOM 元素中
containerRef.current.appendChild(renderer.domElement);
// 创建一个平面几何体
const planeGeometry = new THREE.PlaneGeometry(10, 10);
// 创建平面的材质
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0x808080 });
// 创建平面的 3D 模型
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2;
// 设置平面接收阴影
plane.receiveShadow = true;
scene.add(plane);
// 创建一个立方体几何体
const cubeGeometry = new THREE.BoxGeometry(2, 2, 2);
// 创建立方体的材质
const cubeMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
// 创建立方体的 3D 模型
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.y = 1;
// 设置立方体投射阴影
cube.castShadow = true;
scene.add(cube);
// 创建一个点光源,设置颜色和强度
const pointLight = new THREE.PointLight(0xffffff, 1);
pointLight.position.set(2, 5, 2);
// 开启点光源的阴影投射
pointLight.castShadow = true;
scene.add(pointLight);
// 设置相机位置
camera.position.z = 5;
// 定义渲染函数
const animate = () => {
// 请求下一帧动画
requestAnimationFrame(animate);
// 旋转立方体
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
// 渲染场景
renderer.render(scene, camera);
};
// 开始动画循环
animate();
// 组件卸载时清理资源
return () => {
containerRef.current.removeChild(renderer.domElement);
};
}, []);
return (
// 创建一个 div 用于存放 3D 场景
<div ref={containerRef} />
);
};
export default LightingAndShadowsComponent;
3. 支持用户交互
让用户可以和 3D 场景进行交互,比如点击、拖动等操作,能提升用户体验。这里我们用 three.js
的 Raycaster
来实现点击检测。
// 引入 React 的 useEffect、useRef 和 useState 钩子
import React, { useEffect, useRef, useState } from'react';
// 引入 three.js 用于创建 3D 场景
import * as THREE from 'three';
const InteractionComponent = () => {
// 创建一个 ref 用于引用存放 3D 场景的 DOM 元素
const containerRef = useRef(null);
// 创建一个状态来存储点击的物体
const [clickedObject, setClickedObject] = useState(null);
useEffect(() => {
// 创建 three.js 的场景
const scene = new THREE.Scene();
// 创建透视相机,设置视角、宽高比、近裁剪面和远裁剪面
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// 创建 WebGL 渲染器
const renderer = new THREE.WebGLRenderer();
// 设置渲染器的大小为窗口大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 将渲染器的 DOM 元素添加到 ref 对应的 DOM 元素中
containerRef.current.appendChild(renderer.domElement);
// 创建一个立方体几何体
const cubeGeometry = new THREE.BoxGeometry(2, 2, 2);
// 创建立方体的材质
const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
// 创建立方体的 3D 模型
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
scene.add(cube);
// 设置相机位置
camera.position.z = 5;
// 创建一个射线投射器,用于检测点击
const raycaster = new THREE.Raycaster();
// 创建一个二维向量,用于存储鼠标位置
const mouse = new THREE.Vector2();
// 定义鼠标点击事件处理函数
const onMouseClick = (event) => {
// 计算鼠标在标准化设备坐标中的位置
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 通过鼠标位置更新射线投射器
raycaster.setFromCamera(mouse, camera);
// 获取射线与场景中物体的交点
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
// 如果有交点,设置点击的物体状态
setClickedObject(intersects[0].object);
} else {
// 没有交点则清空点击的物体状态
setClickedObject(null);
}
};
// 监听鼠标点击事件
window.addEventListener('click', onMouseClick);
// 定义渲染函数
const animate = () => {
// 请求下一帧动画
requestAnimationFrame(animate);
// 渲染场景
renderer.render(scene, camera);
};
// 开始动画循环
animate();
// 组件卸载时清理资源
return () => {
containerRef.current.removeChild(renderer.domElement);
// 移除鼠标点击事件监听
window.removeEventListener('click', onMouseClick);
};
}, []);
return (
<div>
{/* 创建一个 div 用于存放 3D 场景 */}
<div ref={containerRef} />
{clickedObject && (
// 如果有点击的物体,显示提示信息
<p>你点击了一个物体!</p>
)}
</div>
);
};
export default InteractionComponent;
通过这些进阶的优化和拓展,你的 React 与 WebGL 项目会变得更加丰富、真实和有趣,用户体验也会大大提升。
如何优化WebGL的渲染性能?
优化 WebGL 渲染性能的方法
1. 减少绘制调用次数
在 WebGL 里,每次绘制调用都需要一些额外的开销。如果能把多次绘制合并成一次,就能减少这些开销,提高性能。就好像你去超市买东西,一次多买点,少跑几趟,效率就高了。
// 初始化 WebGL 上下文
function initWebGL(canvas) {
try {
// 尝试获取 WebGL 上下文
return canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
} catch (e) {
// 如果获取失败,输出错误信息
console.error("无法获取 WebGL 上下文", e);
return null;
}
}
// 合并几何体的函数
function mergeGeometries(geometries) {
let positions = [];
let indices = [];
let indexOffset = 0;
// 遍历所有几何体
geometries.forEach(geometry => {
// 将当前几何体的顶点位置添加到总的顶点位置数组中
positions = positions.concat(geometry.positions);
// 将当前几何体的索引添加到总的索引数组中,并根据偏移量调整索引值
indices = indices.concat(geometry.indices.map(index => index + indexOffset));
// 更新索引偏移量
indexOffset += geometry.positions.length / 3;
});
return {
positions: positions,
indices: indices
};
}
// 创建一个简单的几何体
function createGeometry() {
// 定义顶点位置
const positions = [
-0.5, -0.5, 0,
0.5, -0.5, 0,
0.5, 0.5, 0,
-0.5, 0.5, 0
];
// 定义索引
const indices = [
0, 1, 2,
2, 3, 0
];
return {
positions: positions,
indices: indices
};
}
// 主函数
function main() {
// 获取 canvas 元素
const canvas = document.getElementById("glCanvas");
// 初始化 WebGL 上下文
const gl = initWebGL(canvas);
if (!gl) {
// 如果上下文获取失败,输出错误信息并返回
console.error("无法初始化 WebGL");
return;
}
// 创建两个几何体
const geometry1 = createGeometry();
const geometry2 = createGeometry();
// 合并这两个几何体
const mergedGeometry = mergeGeometries([geometry1, geometry2]);
// 创建顶点缓冲区
const positionBuffer = gl.createBuffer();
// 绑定顶点缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 将合并后的顶点位置数据写入缓冲区
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(mergedGeometry.positions), gl.STATIC_DRAW);
// 创建索引缓冲区
const indexBuffer = gl.createBuffer();
// 绑定索引缓冲区
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// 将合并后的索引数据写入缓冲区
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(mergedGeometry.indices), gl.STATIC_DRAW);
// 顶点着色器代码
const vertexShaderSource = `
attribute vec3 a_position;
void main() {
gl_Position = vec4(a_position, 1.0);
}
`;
// 片段着色器代码
const fragmentShaderSource = `
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
// 创建顶点着色器对象
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
// 将顶点着色器代码放入着色器对象
gl.shaderSource(vertexShader, vertexShaderSource);
// 编译顶点着色器
gl.compileShader(vertexShader);
// 创建片段着色器对象
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
// 将片段着色器代码放入着色器对象
gl.shaderSource(fragmentShader, fragmentShaderSource);
// 编译片段着色器
gl.compileShader(fragmentShader);
// 创建着色器程序对象
const shaderProgram = gl.createProgram();
// 将顶点着色器附加到着色器程序
gl.attachShader(shaderProgram, vertexShader);
// 将片段着色器附加到着色器程序
gl.attachShader(shaderProgram, fragmentShader);
// 链接着色器程序
gl.linkProgram(shaderProgram);
// 使用着色器程序
gl.useProgram(shaderProgram);
// 获取顶点属性的位置
const positionAttributeLocation = gl.getAttribLocation(shaderProgram, "a_position");
// 启用顶点属性
gl.enableVertexAttribArray(positionAttributeLocation);
// 绑定顶点缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 设置顶点属性指针
gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);
// 设置视口大小
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);
// 绘制合并后的几何体
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.drawElements(gl.TRIANGLES, mergedGeometry.indices.length, gl.UNSIGNED_SHORT, 0);
}
// 调用主函数
main();
2. 优化纹理使用
纹理是影响性能的一个重要因素。大尺寸的纹理会占用很多内存,加载和处理起来也慢。所以要尽量压缩纹理,并且按需加载。就像你搬家,东西能压缩就压缩,需要用的时候再拿出来。
// 初始化 WebGL 上下文
function initWebGL(canvas) {
try {
// 尝试获取 WebGL 上下文
return canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
} catch (e) {
// 如果获取失败,输出错误信息
console.error("无法获取 WebGL 上下文", e);
return null;
}
}
// 加载纹理的函数
function loadTexture(gl, url) {
// 创建纹理对象
const texture = gl.createTexture();
// 绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture);
// 设置纹理的初始参数
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
new Uint8Array([0, 0, 255, 255]));
// 创建图像对象
const image = new Image();
image.onload = function () {
// 图像加载完成后,绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture);
// 将图像数据上传到纹理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
// 检查图像尺寸是否是 2 的幂次方
if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
// 如果是 2 的幂次方,生成多级纹理
gl.generateMipmap(gl.TEXTURE_2D);
} else {
// 如果不是 2 的幂次方,设置纹理过滤模式
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.LINEAR);
}
};
// 设置图像的源地址
image.src = url;
return texture;
}
// 判断一个数是否是 2 的幂次方
function isPowerOf2(value) {
return (value & (value - 1)) === 0;
}
// 主函数
function main() {
// 获取 canvas 元素
const canvas = document.getElementById("glCanvas");
// 初始化 WebGL 上下文
const gl = initWebGL(canvas);
if (!gl) {
// 如果上下文获取失败,输出错误信息并返回
console.error("无法初始化 WebGL");
return;
}
// 加载纹理
const texture = loadTexture(gl, "compressed_texture.jpg");
// 顶点着色器代码
const vertexShaderSource = `
attribute vec3 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 1.0);
v_texCoord = a_texCoord;
}
`;
// 片段着色器代码
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_texture, v_texCoord);
}
`;
// 创建顶点着色器对象
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
// 将顶点着色器代码放入着色器对象
gl.shaderSource(vertexShader, vertexShaderSource);
// 编译顶点着色器
gl.compileShader(vertexShader);
// 创建片段着色器对象
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
// 将片段着色器代码放入着色器对象
gl.shaderSource(fragmentShader, fragmentShaderSource);
// 编译片段着色器
gl.compileShader(fragmentShader);
// 创建着色器程序对象
const shaderProgram = gl.createProgram();
// 将顶点着色器附加到着色器程序
gl.attachShader(shaderProgram, vertexShader);
// 将片段着色器附加到着色器程序
gl.attachShader(shaderProgram, fragmentShader);
// 链接着色器程序
gl.linkProgram(shaderProgram);
// 使用着色器程序
gl.useProgram(shaderProgram);
// 顶点位置数据
const positions = [
-0.5, -0.5, 0,
0.5, -0.5, 0,
0.5, 0.5, 0,
-0.5, 0.5, 0
];
// 纹理坐标数据
const texCoords = [
0, 0,
1, 0,
1, 1,
0, 1
];
// 索引数据
const indices = [
0, 1, 2,
2, 3, 0
];
// 创建顶点缓冲区
const positionBuffer = gl.createBuffer();
// 绑定顶点缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 将顶点位置数据写入缓冲区
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// 获取顶点属性的位置
const positionAttributeLocation = gl.getAttribLocation(shaderProgram, "a_position");
// 启用顶点属性
gl.enableVertexAttribArray(positionAttributeLocation);
// 设置顶点属性指针
gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);
// 创建纹理坐标缓冲区
const texCoordBuffer = gl.createBuffer();
// 绑定纹理坐标缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
// 将纹理坐标数据写入缓冲区
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW);
// 获取纹理坐标属性的位置
const texCoordAttributeLocation = gl.getAttribLocation(shaderProgram, "a_texCoord");
// 启用纹理坐标属性
gl.enableVertexAttribArray(texCoordAttributeLocation);
// 设置纹理坐标属性指针
gl.vertexAttribPointer(texCoordAttributeLocation, 2, gl.FLOAT, false, 0, 0);
// 创建索引缓冲区
const indexBuffer = gl.createBuffer();
// 绑定索引缓冲区
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// 将索引数据写入缓冲区
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
// 获取纹理统一变量的位置
const textureUniformLocation = gl.getUniformLocation(shaderProgram, "u_texture");
// 激活纹理单元 0
gl.activeTexture(gl.TEXTURE0);
// 绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture);
// 设置纹理统一变量的值
gl.uniform1i(textureUniformLocation, 0);
// 设置视口大小
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);
// 绘制几何体
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
}
// 调用主函数
main();
3. 控制渲染循环
渲染循环就是不停地更新和绘制场景。如果帧率太高,会浪费性能;如果场景没变化,还一直渲染也没必要。所以要合理控制渲染循环,就像你跑步,不需要一直全速跑,该慢就慢,该停就停。
// 初始化 WebGL 上下文
function initWebGL(canvas) {
try {
// 尝试获取 WebGL 上下文
return canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
} catch (e) {
// 如果获取失败,输出错误信息
console.error("无法获取 WebGL 上下文", e);
return null;
}
}
// 主函数
function main() {
// 获取 canvas 元素
const canvas = document.getElementById("glCanvas");
// 初始化 WebGL 上下文
const gl = initWebGL(canvas);
if (!gl) {
// 如果上下文获取失败,输出错误信息并返回
console.error("无法初始化 WebGL");
return;
}
// 顶点着色器代码
const vertexShaderSource = `
attribute vec3 a_position;
void main() {
gl_Position = vec4(a_position, 1.0);
}
`;
// 片段着色器代码
const fragmentShaderSource = `
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
// 创建顶点着色器对象
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
// 将顶点着色器代码放入着色器对象
gl.shaderSource(vertexShader, vertexShaderSource);
// 编译顶点着色器
gl.compileShader(vertexShader);
// 创建片段着色器对象
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
// 将片段着色器代码放入着色器对象
gl.shaderSource(fragmentShader, fragmentShaderSource);
// 编译片段着色器
gl.compileShader(fragmentShader);
// 创建着色器程序对象
const shaderProgram = gl.createProgram();
// 将顶点着色器附加到着色器程序
gl.attachShader(shaderProgram, vertexShader);
// 将片段着色器附加到着色器程序
gl.attachShader(shaderProgram, fragmentShader);
// 链接着色器程序
gl.linkProgram(shaderProgram);
// 使用着色器程序
gl.useProgram(shaderProgram);
// 顶点位置数据
const positions = [
-0.5, -0.5, 0,
0.5, -0.5, 0,
0.5, 0.5, 0,
-0.5, 0.5, 0
];
// 索引数据
const indices = [
0, 1, 2,
2, 3, 0
];
// 创建顶点缓冲区
const positionBuffer = gl.createBuffer();
// 绑定顶点缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// 将顶点位置数据写入缓冲区
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
// 获取顶点属性的位置
const positionAttributeLocation = gl.getAttribLocation(shaderProgram, "a_position");
// 启用顶点属性
gl.enableVertexAttribArray(positionAttributeLocation);
// 设置顶点属性指针
gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);
// 创建索引缓冲区
const indexBuffer = gl.createBuffer();
// 绑定