基于Three.js实现iPhone滚动效果的技术问题求助
问题:实现Exo风格iPhone滚动3D效果及模型渲染、代码优化问题
需求与现状
- 目标是用Three.js(推测基于react-three-fiber)实现某网站的iPhone滚动交互效果
- 搭建的两个沙箱均无法正常加载渲染iPhone模型
- 找到一个参考项目的动画效果接近需求,但手机方向与目标效果相反
当前核心问题
- 模型加载失败,无法正常渲染
- 代码包含冗余的状态管理依赖,需要清理以搭建独立沙箱
- 手机模型方向不符合需求
当前使用的PhoneModel组件代码
import { useGLTF } from '@react-three/drei'; import { useLayoutEffect, useMemo, useRef } from 'react'; import * as THREE from 'three'; import { MeshBasicMaterial } from 'three'; import { GLTF } from 'three-stdlib'; import { use3dStore } from '~/stores/3d/store'; type PhoneModelProps = { groupPos: [number, number, number]; groupRot: [number, number, number]; modelScale: number; visible: boolean; onLoadFn?: () => void; } & JSX.IntrinsicElements['group']; type GLTFResult = GLTF & { nodes: { M_Cameras: THREE.Mesh; M_Glass: THREE.Mesh; M_Metal_Rough: THREE.Mesh; M_Metal_Shiny: THREE.Mesh; M_Plastic: THREE.Mesh; M_Portal: THREE.Mesh; M_Screen: THREE.Mesh; M_Speakers: THREE.Mesh; M_USB: THREE.Mesh; }; materials: { cam: THREE.MeshStandardMaterial; ['glass.001']: THREE.MeshStandardMaterial; metal_rough: THREE.MeshStandardMaterial; metal_Shiny: THREE.MeshStandardMaterial; ['M_Base.001']: THREE.MeshStandardMaterial; Screen: THREE.MeshStandardMaterial; }; }; export function PhoneModel({ groupPos, groupRot, modelScale, visible, onLoadFn }: PhoneModelProps) { const { nodes, materials } = useGLTF('/assets/models/Iphone15.glb') as GLTFResult; const groupRef = useRef<THREE.Group>(null); const modelRef = useRef<THREE.Group>(null); const screenRef = useRef<THREE.Mesh>(null); const { setPhoneGroupRef, setPhoneModelRef, setPhoneScreenRef } = use3dStore([ 'setPhoneGroupRef', 'setPhoneModelRef', 'setPhoneScreenRef', ]); // Duplicate the screen material so we can modify it without impacting other instances const screenMaterial = useMemo(() => { const mat: MeshBasicMaterial = new MeshBasicMaterial().copy(materials.Screen); mat.toneMapped = false; return mat; }, [materials.Screen]); useLayoutEffect(() => { if (onLoadFn) { onLoadFn(); } }, [onLoadFn]); useLayoutEffect(() => { setPhoneGroupRef(groupRef); setPhoneModelRef(modelRef); setPhoneScreenRef(screenRef); }, [setPhoneGroupRef, setPhoneModelRef, setPhoneScreenRef]); return ( <group ref={groupRef} position={groupPos} rotation={groupRot} visible={visible}> <group ref={modelRef} dispose={null} scale={modelScale}> <group rotation={[Math.PI / 2, 0, 0]} scale={0.01}> <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 ref={screenRef} geometry={nodes.M_Screen.geometry} material={screenMaterial} /> <mesh geometry={nodes.M_Speakers.geometry} material={materials.metal_rough} /> <mesh geometry={nodes.M_USB.geometry} material={materials.metal_rough} /> </group> </group> </group> ); } useGLTF.preload('/assets/models/Iphone15.glb');
解决方案
一、修复模型渲染问题
- 资源路径验证:确认GLB模型放置路径与代码中
useGLTF的路径一致,沙箱环境中需将模型放在public目录下对应路径,或调整路径为沙箱内的正确相对路径 - 材质复制逻辑修正:原代码用
MeshBasicMaterial复制MeshStandardMaterial会丢失PBR材质特性,改为克隆原材质:
const screenMaterial = useMemo(() => { const mat = materials.Screen.clone(); mat.toneMapped = false; return mat; }, [materials.Screen]);
- 类型断言容错:若模型节点/材质结构与定义的
GLTFResult不匹配,会导致渲染失败,可先移除严格类型断言,打印模型结构确认:
const gltf = useGLTF('/assets/models/Iphone15.glb'); console.log('模型节点:', gltf.nodes, '模型材质:', gltf.materials);
二、代码清理(搭建独立沙箱)
- 移除状态管理依赖:删除
use3dStore相关导入与逻辑,仅保留必要的ref用于后续动画控制 - 简化组件结构:移除冗余的
modelRef(若无需单独控制该层级),合并重复的THREE导入 - 优化加载回调:用模型加载状态替代
useLayoutEffect触发回调:
if (gltf.isLoaded && onLoadFn) { onLoadFn(); }
三、调整手机模型方向
修改模型容器的rotation参数,原代码中[Math.PI / 2, 0, 0]是绕X轴顺时针转90度,可根据需求调整:
- 绕X轴逆时针转90度:
rotation={[-Math.PI / 2, 0, 0]} - 同时翻转Y轴方向:
rotation={[Math.PI / 2, Math.PI, 0]} - 可通过调整三个轴的旋转值,匹配目标效果的手机朝向
优化后的示例代码
import { useGLTF } from '@react-three/drei'; import { useMemo, useRef } from 'react'; import * as THREE from 'three'; type PhoneModelProps = { groupPos: [number, number, number]; groupRot: [number, number, number]; modelScale: number; visible: boolean; onLoadFn?: () => void; } & JSX.IntrinsicElements['group']; export function PhoneModel({ groupPos, groupRot, modelScale, visible, onLoadFn }: PhoneModelProps) { const gltf = useGLTF('/assets/models/Iphone15.glb'); const { nodes, materials } = gltf as { nodes: Record<string, THREE.Mesh>; materials: Record<string, THREE.MeshStandardMaterial>; }; const groupRef = useRef<THREE.Group>(null); const screenRef = useRef<THREE.Mesh>(null); // 正确克隆屏幕材质,保留PBR特性 const screenMaterial = useMemo(() => { const mat = materials.Screen.clone(); mat.toneMapped = false; return mat; }, [materials.Screen]); // 模型加载完成触发回调 if (gltf.isLoaded && onLoadFn) { onLoadFn(); } return ( <group ref={groupRef} position={groupPos} rotation={groupRot} visible={visible}> <group dispose={null} scale={modelScale}> {/* 调整rotation参数修正手机方向,此处示例为逆时针转90度 */} <group rotation={[-Math.PI / 2, 0, 0]} scale={0.01}> {Object.entries(nodes).map(([name, node]) => { if (name === 'M_Screen') { return <mesh key={name} ref={screenRef} geometry={node.geometry} material={screenMaterial} />; } // 匹配对应材质, fallback到第一个材质 const mat = materials[name] || Object.values(materials)[0]; return <mesh key={name} geometry={node.geometry} material={mat} />; })} </group> </group> </group> ); } useGLTF.preload('/assets/models/Iphone15.glb');
内容的提问来源于stack exchange,提问作者Rob
相关产品推荐
相关产品推荐

