You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 11:34:55