React Fiber与Three.js:同时更新颜色和位置属性时几何体渲染异常
问题分析与修复方案
核心问题
- BufferAttribute 数组复用错误:初始化
attrPosSphere时错误使用了newColorSphere数组,导致球体的位置和颜色属性共享同一块内存空间,更新其中一个属性时会覆盖另一个的数据,这就是同时更新时其中一个失效的根本原因。 - 冗余的
setAttribute调用:在useFrame中每次重新调用setAttribute会重置属性引用,干扰Three.js的属性更新逻辑,直接修改现有BufferAttribute的数组并标记needsUpdate即可。 - 顶点位置计算逻辑缺陷:当前更新球体位置时,每次基于已经修改过的顶点数据计算,会导致顶点持续偏移,应该基于原始的顶点初始值进行计算。
修复后的代码
import { useFrame } from '@react-three/fiber' import React, { useRef, useEffect } from 'react' import { createNoise3D, createNoise4D } from 'simplex-noise' import alea from 'alea'; import * as THREE from 'three' function Background(props) { const refPlane = useRef() const refSphere = useRef() const prng = alea('seed') const noise3D = createNoise3D(prng) const noise4D = createNoise4D(prng) let posPlane, posSphere; // 保存球体原始顶点数据,用于位置计算 let originalSpherePositions = null; // 初始化各属性的数组,确保互不共享 let newColorPlane = new Float32Array(12) let newColorSphere = new Float32Array(12675) let newPosSphere = new Float32Array(12675) // 修正attrPosSphere的初始化,使用newPosSphere数组 let attrColorSphere = new THREE.BufferAttribute(newColorSphere, 3) let attrColorPlane = new THREE.BufferAttribute(newColorPlane, 3) let attrPosSphere = new THREE.BufferAttribute(newPosSphere, 3) function mapRange(value, a, b, c, d) { value = (value - a) / (b - a); return c + value * (d - c); } useEffect(() => { posPlane = refPlane.current.geometry.getAttribute('position') posSphere = refSphere.current.geometry.getAttribute('position') // 保存球体原始顶点数据的副本,避免后续修改影响计算 originalSpherePositions = Float32Array.from(posSphere.array) // 初始化几何体的color属性 refPlane.current.geometry.setAttribute('color', attrColorPlane) refSphere.current.geometry.setAttribute('color', attrColorSphere) refSphere.current.geometry.setAttribute('position', attrPosSphere) }, []); // 修正函数名:该函数实际更新平面的颜色,而非位置 function updateColorPlane(positionPlane, time) { for (let i = 0; i < (positionPlane.count * 3); i += 3) { const x = positionPlane.array[i]; const y = positionPlane.array[i + 1]; const noise = noise3D(x, y, time / 2); const hue = mapRange(noise, -1, 1, 0, 1); const color = new THREE.Color(); color.setHSL(hue, 0.9, 0.25); newColorPlane[i] = color.r; newColorPlane[i + 1] = color.g; newColorPlane[i + 2] = color.b; } } function updateColorSphere(time) { for (let i = 0; i < originalSpherePositions.length; i += 3) { const x = originalSpherePositions[i]; const y = originalSpherePositions[i + 1]; const z = originalSpherePositions[i + 2]; const noise = noise4D(x / 5, y / 5, z / 5, time / 10); const hue = mapRange(noise, -1, 1, 0, 1); const color = new THREE.Color(); color.setHSL(hue, 0.9, 0.25); newColorSphere[i] = color.r; newColorSphere[i + 1] = color.g; newColorSphere[i + 2] = color.b; } } function updatePosSphere(time) { for (let i = 0; i < originalSpherePositions.length; i += 3) { const x = originalSpherePositions[i]; const y = originalSpherePositions[i + 1]; const z = originalSpherePositions[i + 2]; const noise = noise4D(x / 5, y / 5, z / 5, time); const vec = new THREE.Vector3(x, y, z); // 基于原始顶点计算偏移,避免持续累积 const newVec = vec.addScaledVector(vec, noise / 2); newPosSphere[i] = newVec.x; newPosSphere[i + 1] = newVec.y; newPosSphere[i + 2] = newVec.z; } } useFrame(({ clock }) => { const a = clock.getElapsedTime() // 更新平面颜色 updateColorPlane(posPlane, a) attrColorPlane.needsUpdate = true // 更新球体位置和颜色 updatePosSphere(a) updateColorSphere(a) attrPosSphere.needsUpdate = true attrColorSphere.needsUpdate = true refSphere.current.rotation.y = a }) return ( <> <mesh {...props} ref={refPlane} position={[0, 0, 0]} scale={[20, 20, 0]}> <planeGeometry /> <meshBasicMaterial color='white' vertexColors /> </mesh> <mesh {...props} ref={refSphere} position={[0, 0, 0]}> <sphereGeometry args={[2, 64, 64]} /> <meshBasicMaterial vertexColors /> </mesh> </> ) } export default Background
关键修改说明
- 修正BufferAttribute数组复用:将
attrPosSphere的初始化改为使用newPosSphere数组,确保位置和颜色属性的内存空间完全独立。 - 保存原始顶点数据:在
useEffect中保存球体原始顶点的副本,后续位置计算基于原始值,避免顶点偏移持续累积。 - 移除冗余的
setAttribute调用:在useEffect中一次性初始化几何体的color和position属性,useFrame中仅更新数组并标记needsUpdate,避免属性引用被频繁重置。 - 修正函数命名:将
updatePosPlane改为updateColorPlane,使其语义与功能匹配,提升代码可读性。 - 移除Material上的冗余
needsUpdate:needsUpdate是BufferAttribute的属性,Material上不需要设置该属性。
内容的提问来源于stack exchange,提问作者Luca Ponzanelli
相关产品推荐
相关产品推荐

