React Three Fiber竖向粘性轮播:多场景粘性失效与文本适配问题
基于Three.js + React Three Fiber的竖向粘性轮播问题

问题概述
我正在开发竖向粘性轮播,遇到以下问题:
- 堆叠粘性区域后,不知道如何添加与轮播风格匹配的联动文本;
- 为每个手机3D场景创建ref后,GSAP ScrollTrigger的粘性效果失效,不清楚粘性生效的控制逻辑,也无法控制每个场景的显示时长;
- 当前多手机场景可正常渲染,但粘性功能丢失,单手机场景的粘性功能则正常工作。
相关代码示例
多手机场景(粘性失效)
import React, { useRef, useEffect } from 'react' import { Canvas, useThree } from '@react-three/fiber' import { useGLTF, OrbitControls } from '@react-three/drei' import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' import * as THREE from 'three' gsap.registerPlugin(ScrollTrigger) const IphoneModel = (ref) => { const group = useRef() const { nodes, materials } = useGLTF('/Iphone15.glb') useEffect(() => { const video = document.createElement('video') video.src = 'https://cdn.pixabay.com/video/2024/07/14/221180_tiny.mp4' video.crossOrigin = 'anonymous' video.loop = true video.muted = true video.play() const videoTexture = new THREE.VideoTexture(video) videoTexture.minFilter = THREE.LinearFilter videoTexture.magFilter = THREE.LinearFilter videoTexture.encoding = THREE.sRGBEncoding materials.Screen.map = videoTexture materials.Screen.needsUpdate = true const tl = gsap.timeline({ scrollTrigger: { trigger: ref[0], scrub: 1, markers: true, pin: true, start: 'top top', end: 'bottom top' } }) tl.to(group.current.rotation, { z: -Math.PI / 8, duration: 2 }) }, [materials.Screen]) return ( <group ref={group} dispose={null} scale={0.2} rotation={[Math.PI / 2, 0, Math.PI / 8]}> <mesh geometry={nodes.M_Cameras.geometry} material={materials.cam} /> <mesh geometry={nodes.M_Glass.geometry} material={materials['glass.001']} /> <mesh geometry={nodes.M_Metal_Rough.geometry} material={materials.metal_rough} /> <mesh geometry={nodes.M_Metal_Shiny.geometry} material={materials.metal_Shiny} /> <mesh geometry={nodes.M_Plastic.geometry} material={materials.metal_rough} /> <mesh geometry={nodes.M_Portal.geometry} material={materials['M_Base.001']} /> <mesh geometry={nodes.M_Screen.geometry} material={materials.Screen} /> <mesh geometry={nodes.M_Speakers.geometry} material={materials.metal_rough} /> <mesh geometry={nodes.M_USB.geometry} material={materials.metal_rough} /> </group> ) } const Background = () => { const { scene } = useThree() useEffect(() => { scene.background = new THREE.Color('#555555') }, [scene]) return null } const TextSection = () => { const textRefs = useRef([]) useEffect(() => { gsap.fromTo( textRefs.current, { opacity: 0 }, { opacity: 1, stagger: 0.1, scrollTrigger: { trigger: '#text-trigger', start: 'top bottom', end: 'center center', scrub: 1, markers: false } } ) }, []) const texts = ['Ready 5', 'Ready 4', 'Ready 3', 'Ready 2', 'Ready 1'] return ( <div id="text-trigger" style={{ height: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', position: 'relative', top: '500px' }}> {texts.map((text, index) => ( <h1 key={index} ref={(el) => (textRefs.current[index] = el)} style={{ opacity: 0 }}> {text} </h1> ))} </div> ) } const ThreeScene = () => { const threeSceneGroup = useRef() return ( <div id="three-canvas-container" style={{ width: '100vw', height: '500px' }}> <div> <h2>header text</h2> <p>text text text</p> </div> <Canvas camera={{ position: [0, 0, 10], fov: 45 }} gl={{ antialias: true, alpha: false }}> <ambientLight intensity={0.4} /> <directionalLight position={[5, 10, 7.5]} intensity={1} /> <IphoneModel ref={threeSceneGroup} /> <OrbitControls enableZoom={false} /> <Background /> </Canvas> </div> ) } const App = () => ( <div style={{ display: 'flex', flexDirection: 'column', height: '400vh' }}> <div className="some-content" style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <h1>ACTION</h1> </div> <ThreeScene /> <ThreeScene /> <ThreeScene /> <ThreeScene /> <ThreeScene /> <TextSection /> </div> ) export default App
单手机场景(粘性正常)
import React, { useRef, useEffect } from 'react' import { Canvas, useThree } from '@react-three/fiber' import { useGLTF, OrbitControls } from '@react-three/drei' import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' import * as THREE from 'three' gsap.registerPlugin(ScrollTrigger) const IphoneModel = () => { const group = useRef() const { nodes, materials } = useGLTF('/Iphone15.glb') useEffect(() => { const video = document.createElement('video') video.src = 'https://cdn.pixabay.com/video/2024/07/14/221180_tiny.mp4' video.crossOrigin = 'anonymous' video.loop = true video.muted = true video.play() const videoTexture = new THREE.VideoTexture(video) videoTexture.minFilter = THREE.LinearFilter videoTexture.magFilter = THREE.LinearFilter videoTexture.encoding = THREE.sRGBEncoding materials.Screen.map = videoTexture materials.Screen.needsUpdate = true const tl = gsap.timeline({ scrollTrigger: { trigger: '#three-canvas-container', scrub: 1, markers: true, pin: true, start: 'top top', end: 'bottom top' } }) tl.to(group.current.rotation, { z: Math.PI * 2, duration: 2 }) }, [materials.Screen]) return ( <group ref={group} dispose={null} scale={0.2} rotation={[Math.PI / 2, 0, -Math.PI / 8]}> <mesh geometry={nodes.M_Cameras.geometry} material={materials.cam} /> <mesh geometry={nodes.M_Glass.geometry} material={materials['glass.001']} /> <mesh geometry={nodes.M_Metal_Rough.geometry} material={materials.metal_rough} /> <mesh geometry={nodes.M_Metal_Shiny.geometry} material={materials.metal_Shiny} /> <mesh geometry={nodes.M_Plastic.geometry} material={materials.metal_rough} /> <mesh geometry={nodes.M_Portal.geometry} material={materials['M_Base.001']} /> <mesh geometry={nodes.M_Screen.geometry} material={materials.Screen} /> <mesh geometry={nodes.M_Speakers.geometry} material={materials.metal_rough} /> <mesh geometry={nodes.M_USB.geometry} material={materials.metal_rough} /> </group> ) } const Background = () => { const { scene } = useThree() useEffect(() => { scene.background = new THREE.Color('#555555') }, [scene]) return null } const TextSection = () => { const textRefs = useRef([]) useEffect(() => { gsap.fromTo( textRefs.current, { opacity: 0 }, { opacity: 1, stagger: 0.1, scrollTrigger: { trigger: '#text-trigger', start: 'top bottom', end: 'center center', scrub: 1, markers: false } } ) }, []) const texts = ['Ready 5', 'Ready 4', 'Ready 3', 'Ready 2', 'Ready 1'] return ( <div id="text-trigger" style={{ height: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', position: 'relative', top: '500px' }}> {texts.map((text, index) => ( <h1 key={index} ref={(el) => (textRefs.current[index] = el)} style={{ opacity: 0 }}> {text} </h1> ))} </div> ) } const ThreeScene = () => ( <div id="three-canvas-container" style={{ width: '100vw', height: '500px' }}> <Canvas camera={{ position: [0, 0, 10], fov: 45 }} gl={{ antialias: true, alpha: false }}> <ambientLight intensity={0.4} /> <directionalLight position={[5, 10, 7.5]} intensity={1} /> <IphoneModel /> <OrbitControls enableZoom={false} /> <Background /> </Canvas> </div> ) const App = () => ( <div style={{ display: 'flex', flexDirection: 'column', height: '400vh' }}> <div className="some-content" style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <h1>ACTION</h1> </div> <ThreeScene /> <TextSection /> </div> ) export default App
图片翻转解决方案
通过THREE.Vector2设置纹理重复属性解决图片翻转问题,代码如下:
const imageTexture = new THREE.TextureLoader().load(image) imageTexture.wrapS = imageTexture.wrapT = THREE.RepeatWrapping imageTexture.anisotropy = 16 imageTexture.repeat = new THREE.Vector2(1, -1)
内容的提问来源于stack exchange,提问作者The Old County
相关产品推荐
相关产品推荐

