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

基于Three.js实现iPhone滚动效果的技术问题求助

问题:实现Exo风格iPhone滚动3D效果及模型渲染、代码优化问题

需求与现状

  • 目标是用Three.js(推测基于react-three-fiber)实现某网站的iPhone滚动交互效果
  • 搭建的两个沙箱均无法正常加载渲染iPhone模型
  • 找到一个参考项目的动画效果接近需求,但手机方向与目标效果相反

当前核心问题

  1. 模型加载失败,无法正常渲染
  2. 代码包含冗余的状态管理依赖,需要清理以搭建独立沙箱
  3. 手机模型方向不符合需求

当前使用的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');

解决方案

一、修复模型渲染问题

  1. 资源路径验证:确认GLB模型放置路径与代码中useGLTF的路径一致,沙箱环境中需将模型放在public目录下对应路径,或调整路径为沙箱内的正确相对路径
  2. 材质复制逻辑修正:原代码用MeshBasicMaterial复制MeshStandardMaterial会丢失PBR材质特性,改为克隆原材质:
const screenMaterial = useMemo(() => {
  const mat = materials.Screen.clone();
  mat.toneMapped = false;
  return mat;
}, [materials.Screen]);
  1. 类型断言容错:若模型节点/材质结构与定义的GLTFResult不匹配,会导致渲染失败,可先移除严格类型断言,打印模型结构确认:
const gltf = useGLTF('/assets/models/Iphone15.glb');
console.log('模型节点:', gltf.nodes, '模型材质:', gltf.materials);

二、代码清理(搭建独立沙箱)

  1. 移除状态管理依赖:删除use3dStore相关导入与逻辑,仅保留必要的ref用于后续动画控制
  2. 简化组件结构:移除冗余的modelRef(若无需单独控制该层级),合并重复的THREE导入
  3. 优化加载回调:用模型加载状态替代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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 21:30:59