大白话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 里,每次绘制调用都需要一些额外的开销。如果能把多次绘制合并成一次,就能减少这些开销,提高性能。就好像你去超市买东西,一次多买点,少跑几趟,效率就高了。
javascript">// 初始化 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. 优化纹理使用
纹理是影响性能的一个重要因素。大尺寸的纹理会占用很多内存,加载和处理起来也慢。所以要尽量压缩纹理,并且按需加载。就像你搬家,东西能压缩就压缩,需要用的时候再拿出来。
javascript">// 初始化 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");// 激活纹理单元 0gl.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. 控制渲染循环
渲染循环就是不停地更新和绘制场景。如果帧率太高,会浪费性能;如果场景没变化,还一直渲染也没必要。所以要合理控制渲染循环,就像你跑步,不需要一直全速跑,该慢就慢,该停就停。
javascript">// 初始化 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();// 绑定