React Three Fiber使用instanced mesh优化类Minecraft超大可点击地图
Instancing 运行逻辑
- 常规渲染流程中,每一个独立Mesh对应一次GPU Draw Call,CPU需要逐一向GPU传递模型的几何体、材质、位置、旋转、缩放、自定义属性等参数,物体数量过万后CPU会成为性能瓶颈,直接导致帧率暴跌
- InstancedMesh是Three.js对GPU实例化渲染的封装:所有复用同一个几何体、同一个材质的重复物体,仅需要1次Draw Call即可完成全部渲染
- 运行时CPU仅需要一次性向GPU传递所有实例的变换矩阵(位置/旋转/缩放)、自定义属性(颜色、特殊标记等),GPU侧并行完成所有实例的计算渲染,同性能下可承载的物体数量可提升几十至上百倍
- React Three Fiber(R3F)生态中已经完整封装了实例化能力,支持直接通过
<instancedMesh>组件实现,且原生支持实例级点击交互,可通过点击事件的instanceId属性定位到具体点击的是哪个实例,完全满足方块点击交互的需求
项目落地改造步骤
- 预转换布局数据:原代码中每次循环都用
filter遍历layout数组查询坐标对应的配置,时间复杂度O(n²),先将layout转换为哈希表,查询复杂度降到O(1) - 按类型分组创建InstancedMesh:普通地面方块、树木、岩石三类元素分别创建独立的InstancedMesh(因为三者的几何体、材质不同,不能合并到同一个实例化网格)
- 预计算所有实例的参数:提前生成所有方块的坐标、颜色、是否带树、是否带岩石的配置,统一写入对应的InstancedMesh的实例属性中
- 适配交互逻辑:给地面InstancedMesh绑定点击事件,通过
event.instanceId匹配对应方块的配置,触发原有的selectBox逻辑
改造后代码示例
import React, { Suspense, useRef, useMemo, useEffect } from "react"; import { BoxGeometry, MeshLambertMaterial, Object3D, Color } from "three"; export default function Ground({ layout, selectBox, selectedBox, position, }) { const size = 20; const totalBoxCount = size * size; const groundMeshRef = useRef(null); // 预存所有方块的配置数据 const boxConfigs = useMemo(() => { const configs = []; // 先转换layout为哈希表优化查询 const layoutMap = new Map(layout.map(item => [`${item.x},${item.y}`, item.color])); let id = 0; let hasTree = Math.random() > 0.99; for (let i = -(size / 2); i < size / 2; i++) { for (let j = -(size / 2); j < size / 2; j++) { const color = layoutMap.get(`${i},${j}`) ?? 0x61892f; const coords = { x: j * 5, y: 0, z: i * 5 }; const hasStone = Math.random() < 0.2; configs.push({ id, coords, color, hasTree, hasStone }); id++; if (hasTree) hasTree = Math.random() > 0.5; } if (Math.random() > 0.99) hasTree = true; } return configs; }, [layout]); // 初始化实例矩阵和颜色 useEffect(() => { if (!groundMeshRef.current) return; const mesh = groundMeshRef.current; const dummy = new Object3D(); boxConfigs.forEach((config, index) => { dummy.position.set(config.coords.x, config.coords.y, config.coords.z); dummy.updateMatrix(); mesh.setMatrixAt(index, dummy.matrix); mesh.setColorAt(index, new Color(config.color)); }); mesh.instanceMatrix.needsUpdate = true; mesh.instanceColor.needsUpdate = true; }, [boxConfigs]); // 点击交互处理 const handleBoxClick = (e) => { e.stopPropagation(); const clickedBox = boxConfigs[e.instanceId]; selectBox(clickedBox); }; return ( <group position={position ?? [0, 0, 0]}> <Suspense fallback={null}> {/* 地面方块实例化网格 */} <instancedMesh ref={groundMeshRef} args={[new BoxGeometry(5, 1, 5), new MeshLambertMaterial({ vertexColors: true }), totalBoxCount]} onClick={handleBoxClick} /> {/* 树木、岩石可参照上方逻辑分别创建对应的instancedMesh,仅渲染hasTree/hasStone为true的实例即可 */} </Suspense> </group> ); }
额外优化提示
- 如果方块类型较多(比如草地、泥土、石头等不同材质),按材质类型拆分多个InstancedMesh即可,不要把不同材质的方块塞到同一个实例化网格里
- 渲染距离较大时可以配合视锥剔除,动态更新InstancedMesh的实例数组,仅保留视野范围内的实例
- 树木、岩石等装饰元素如果是独立模型,单独创建对应的InstancedMesh即可,每类装饰仅占用1次Draw Call,性能开销极低
- 若需要实现选中高亮效果,可以单独维护选中实例的ID,在渲染循环中更新对应实例的颜色或缩放即可
内容的提问来源于stack exchange,提问作者Franciszek Job
相关产品推荐
相关产品推荐

