React Three Fiber中从中间状态启动Point模型动画及中途切换方案问询
在React-Three-Fiber中实现粒子从中间状态过渡的纹理切换动画
要解决路由变化时粒子从当前位置平滑过渡到新目标的问题,核心是维护粒子的当前状态,避免每次切换都从初始位置重新开始动画。以下是具体实现方案:
一、重构顶点着色器:跟踪当前与目标位置
修改顶点着色器,放弃单一的uProgress控制初始到最终位置的逻辑,改为通过currentPosition(当前位置)和targetPosition(目标位置)两个attribute,配合过渡进度uTransitionProgress实现插值动画:
// vert.js export const vertex = /* glsl */ ` uniform float uTransitionProgress; attribute vec3 currentPosition; attribute vec3 targetPosition; void main() { // 从当前位置平滑插值到目标位置 vec3 interpolatedPos = mix(currentPosition, targetPosition, uTransitionProgress); vec4 mvPosition = modelViewMatrix * vec4(interpolatedPos, 1.0); gl_Position = projectionMatrix * mvPosition; }`;
二、修改片段着色器:实现纹理混合
为了平滑切换纹理,增加纹理混合进度uTextureProgress,在两个纹理之间做插值:
// frag.js export const fragment = /* glsl */ ` uniform float uTextureProgress; uniform sampler2D uTexture1; uniform sampler2D uTexture2; varying vec2 vUv; void main() { vec4 texColor1 = texture2D(uTexture1, vUv); vec4 texColor2 = texture2D(uTexture2, vUv); // 混合两个纹理的颜色 gl_FragColor = mix(texColor1, texColor2, uTextureProgress); }`;
三、更新Material组件:处理路由切换与状态同步
在组件中监听NextJS路由变化事件,每次切换时先将粒子的当前位置同步为当前动画的插值结果,再设置新的目标位置,最后启动过渡动画。以下是使用GSAP的实现:
// Material.js import { useRef, useEffect } from 'react'; import { useGSAP } from '@gsap/react'; import gsap from 'gsap'; import { shaderMaterial } from '@react-three/drei'; import { useRouter } from 'next/router'; import * as THREE from 'three'; import { vertexShader } from './vert'; import { fragmentShader } from './frag'; export function Material({ initialTexture, textures }) { const materialRef = useRef(); const router = useRouter(); const currentTextureIdx = useRef(0); const initialPositions = useRef(null); // 初始化粒子位置:current和target都设为初始位置 useEffect(() => { if (!materialRef.current) return; const geometry = materialRef.current.parent; initialPositions.current = geometry.attributes.position.array.slice(); // 初始化currentPosition和targetPosition attribute geometry.setAttribute('currentPosition', new THREE.BufferAttribute(initialPositions.current, 3)); geometry.setAttribute('targetPosition', new THREE.BufferAttribute(initialPositions.current, 3)); }, []); // 初始动画:从初始位置到第一个目标位置 useGSAP(() => { if (!materialRef.current) return; // 这里替换成你实际的初始目标位置生成逻辑 const geometry = materialRef.current.parent; const targetPosArray = new Float32Array(initialPositions.current.length); for (let i = 0; i < targetPosArray.length; i += 3) { targetPosArray[i] = Math.random() * 10 - 5; targetPosArray[i+1] = Math.random() * 10 - 5; targetPosArray[i+2] = Math.random() * 10 - 5; } geometry.setAttribute('targetPosition', new THREE.BufferAttribute(targetPosArray, 3)); gsap.to(materialRef.current.uniforms.uTransitionProgress, { value: 1, duration: 15, ease: 'power4.out', onComplete: () => { // 动画结束后重置进度,同步currentPosition到目标位置 materialRef.current.uniforms.uTransitionProgress.value = 0; geometry.attributes.currentPosition.copyArray(targetPosArray); geometry.attributes.currentPosition.needsUpdate = true; } }); }, []); // 监听路由变化,触发纹理与位置过渡 useEffect(() => { const handleRouteChange = () => { if (!materialRef.current) return; const geometry = materialRef.current.parent; const uniforms = materialRef.current.uniforms; // 1. 计算当前粒子的实际位置(基于当前过渡进度) const currentProgress = uniforms.uTransitionProgress.value; const currentPosArray = geometry.attributes.currentPosition.array; const targetPosArray = geometry.attributes.targetPosition.array; for (let i = 0; i < currentPosArray.length; i += 3) { currentPosArray[i] = gsap.utils.interpolate(currentPosArray[i], targetPosArray[i], currentProgress); currentPosArray[i+1] = gsap.utils.interpolate(currentPosArray[i+1], targetPosArray[i+1], currentProgress); currentPosArray[i+2] = gsap.utils.interpolate(currentPosArray[i+2], targetPosArray[i+2], currentProgress); } geometry.attributes.currentPosition.needsUpdate = true; // 2. 更新目标位置(替换成你对应新纹理的位置逻辑) const newTargetPosArray = new Float32Array(initialPositions.current.length); for (let i = 0; i < newTargetPosArray.length; i += 3) { newTargetPosArray[i] = Math.random() * 10 - 5; newTargetPosArray[i+1] = Math.random() * 10 - 5; newTargetPosArray[i+2] = Math.random() * 10 - 5; } geometry.setAttribute('targetPosition', new THREE.BufferAttribute(newTargetPosArray, 3)); // 3. 切换纹理:将当前纹理移到uTexture1,新纹理设为uTexture2 currentTextureIdx.current = (currentTextureIdx.current + 1) % textures.length; const newTexture = textures[currentTextureIdx.current]; uniforms.uTexture1.value = uniforms.uTexture2.value; uniforms.uTexture2.value = newTexture; // 4. 启动位置与纹理的同步过渡动画 gsap.to(uniforms, { uTransitionProgress: 1, uTextureProgress: 1, duration: 2, ease: 'power4.out', onComplete: () => { // 重置进度,同步currentPosition到新目标位置 uniforms.uTransitionProgress.value = 0; uniforms.uTextureProgress.value = 0; geometry.attributes.currentPosition.copyArray(newTargetPosArray); geometry.attributes.currentPosition.needsUpdate = true; } }); }; router.events.on('routeChangeComplete', handleRouteChange); return () => router.events.off('routeChangeComplete', handleRouteChange); }, [router, textures]); return ( <shaderMaterial ref={materialRef} vertexShader={vertexShader} fragmentShader={fragmentShader} uniforms={{ uTransitionProgress: { value: 0 }, uTextureProgress: { value: 0 }, uTexture1: { value: initialTexture }, uTexture2: { value: initialTexture } }} transparent depthTest={false} depthWrite={false} /> ); }
四、替代方案:使用R3F的useFrame手动控制动画
如果不想用GSAP,也可以通过useFrame实现帧更新控制动画:
// 在Material组件中替换GSAP相关代码 import { useFrame } from '@react-three/fiber'; // 新增状态 const transitionState = useRef({ isActive: false, startTime: 0, duration: 2, progress: 0 }); useFrame((state) => { if (!transitionState.current.isActive) return; const elapsed = state.clock.getElapsedTime() - transitionState.current.startTime; transitionState.current.progress = Math.min(elapsed / transitionState.current.duration, 1); const uniforms = materialRef.current.uniforms; uniforms.uTransitionProgress.value = transitionState.current.progress; uniforms.uTextureProgress.value = transitionState.current.progress; if (transitionState.current.progress >= 1) { transitionState.current.isActive = false; transitionState.current.progress = 0; const geometry = materialRef.current.parent; geometry.attributes.currentPosition.copyArray(geometry.attributes.targetPosition.array); geometry.attributes.currentPosition.needsUpdate = true; uniforms.uTransitionProgress.value = 0; uniforms.uTextureProgress.value = 0; } }); // 路由变化时启动动画: const handleRouteChange = () => { // ... 前面的位置同步、目标位置设置、纹理切换逻辑 ... transitionState.current.isActive = true; transitionState.current.startTime = state.clock.getElapsedTime(); };
关键逻辑说明
- 状态保留:每次路由切换前,先根据当前过渡进度计算粒子的实际位置,更新
currentPosition,确保动画从当前状态开始 - 双纹理混合:通过
uTextureProgress在两个纹理间做平滑过渡,避免纹理切换的突兀感 - 动画重置:每次过渡结束后,重置进度值并同步
currentPosition到目标位置,为下一次切换做好准备
内容的提问来源于stack exchange,提问作者philolegein
相关产品推荐
相关产品推荐

