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

如何使用ThreeJS和RTF创建带有相邻面不同图像的3D橄榄球模型

如何使用ThreeJS和React Three Fiber创建带有相邻面不同图像的3D橄榄球模型

嘿,我来帮你搞定这个橄榄球模型的材质问题!你现在的代码是把球体分成了上下两个半球,但你需要的是环绕橄榄球的四个独立面,每个面用不同的图对吧?咱们先理清楚现有代码的问题,再一步步改出符合需求的效果。

先说说你现有代码的小问题

你当前是基于顶点索引把球体切成了上下半球,但这种划分方式和你要的四个侧面完全不符;而且分组逻辑还有点小bug——第二个循环的条件i < sphereGeometry.index.count / 3会导致大量顶点没有被正确分组,所以另外的材质根本没法显示出来。

解决方案:按经度划分四个扇区

要实现四个相邻面各用不同纹理,咱们应该沿着橄榄球的长轴(你代码里是Y轴拉长的方向),把椭球按经度分成四个均等的扇区,每个扇区对应一个独立材质。这样旋转的时候就能看到四个不同的相邻面了!

修改后的完整代码

import React, { Suspense } from "react";
import { Canvas, useLoader } from "@react-three/fiber";
import {
  OrbitControls,
  useProgress,
  Html,
} from "@react-three/drei";
import * as THREE from "three";

const RugbyBall: React.FC = () => {
  // 加载四张纹理(替换成你的图片路径)
  const one = useLoader(THREE.TextureLoader, "/images/one.jpg");
  const two = useLoader(THREE.TextureLoader, "/images/two.jpg");
  const three = useLoader(THREE.TextureLoader, "/images/three.jpg");
  const four = useLoader(THREE.TextureLoader, "/images/four.jpg");
  const texture = useLoader(THREE.TextureLoader, "/images/texture.jpg");

  // 给每张纹理设置合适的包裹模式,避免边缘拉伸
  [one, two, three, four].forEach(t => {
    t.wrapS = THREE.ClampToEdgeWrapping;
    t.wrapT = THREE.ClampToEdgeWrapping;
    t.needsUpdate = true;
  });

  // 创建四个材质,每个对应一张图
  const materials = [
    new THREE.MeshPhysicalMaterial({
      map: one,
      bumpMap: texture,
      bumpScale: 1,
      roughness: 1,
      metalness: 0,
    }),
    new THREE.MeshPhysicalMaterial({
      map: two,
      bumpMap: texture,
      bumpScale: 1,
      roughness: 1,
      metalness: 0,
    }),
    new THREE.MeshPhysicalMaterial({
      map: three,
      bumpMap: texture,
      bumpScale: 1,
      roughness: 1,
      metalness: 0,
    }),
    new THREE.MeshPhysicalMaterial({
      map: four,
      bumpMap: texture,
      bumpScale: 1,
      roughness: 1,
      metalness: 0,
    }),
  ];

  // 创建球体几何并拉长为橄榄球形状
  const radius = 50;
  const widthSegments = 64; // 经度方向分段数,越高越精细
  const heightSegments = 64; // 纬度方向分段数,越高越精细
  const sphereGeometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
  sphereGeometry.scale(1, 1.8, 1); // 拉长Y轴变成橄榄球形状

  // 清空默认分组
  sphereGeometry.clearGroups();

  // 获取顶点位置数组
  const positions = sphereGeometry.attributes.position.array;
  // 计算总三角面数量
  const triangleCount = sphereGeometry.index ? sphereGeometry.index.count / 3 : positions.length / 9;

  // 遍历每个三角面,分配对应的材质组
  for (let i = 0; i < triangleCount; i++) {
    // 获取当前三角面的三个顶点索引
    let v1, v2, v3;
    if (sphereGeometry.index) {
      v1 = sphereGeometry.index.getX(i * 3) * 3;
      v2 = sphereGeometry.index.getX(i * 3 + 1) * 3;
      v3 = sphereGeometry.index.getX(i * 3 + 2) * 3;
    } else {
      v1 = i * 9;
      v2 = i * 9 + 3;
      v3 = i * 9 + 6;
    }

    // 计算当前三角面的中心位置
    const centerX = (positions[v1] + positions[v2] + positions[v3]) / 3;
    const centerZ = (positions[v1 + 2] + positions[v2 + 2] + positions[v3 + 2]) / 3;

    // 计算该中心的经度(绕Y轴的角度)
    const longitude = Math.atan2(centerZ, centerX);
    // 把经度转成0~2π的范围
    const normalizedLong = longitude < 0 ? longitude + 2 * Math.PI : longitude;
    // 分成4个均等扇区(每个扇区90度)
    const sector = Math.floor(normalizedLong / (Math.PI / 2));
    // 确保扇区索引在0-3之间
    const materialIndex = sector % 4;

    // 给当前三角面分配材质组
    sphereGeometry.addGroup(i * 3, 3, materialIndex);
  }

  // 创建网格并应用材质
  const rugbyBall = new THREE.Mesh(sphereGeometry, materials);

  return <primitive object={rugbyBall} position={[0, 0, 0]} scale={2.5} rotation={[-(Math.PI / 2), 0, 0]} />;
};

const Loader = () => {
  const { progress } = useProgress();
  return <Html center>{Math.round(progress)}% loaded</Html>;
};

// 主场景组件
const Scene = () => {
  return (
    <Canvas
      camera={{ position: [0, 0, 500], fov: 75 }}
      style={{
        width: "100vw",
        height: "30vh",
        backgroundColor: "red",
      }}
    >
      <Suspense fallback={<Loader />}>
        {/* 灯光设置 */}
        <ambientLight intensity={1} />
        <directionalLight position={[0, 1, 0]} intensity={2} />

        {/* 橄榄球模型 */}
        <RugbyBall />

        {/* 轨道控制器 */}
        <OrbitControls autoRotate autoRotateSpeed={2} enableDamping dampingFactor={0.1} />
      </Suspense>
    </Canvas>
  );
};

export default Scene;

关键部分说明

  1. 纹理与材质:加载四张纹理并分别创建MeshPhysicalMaterial,同时给每张纹理设置了边缘包裹模式,避免拉伸错位。
  2. 几何分组逻辑:遍历每个三角面,计算其中心位置的经度,将椭球按经度分成4个90度的扇区,每个扇区对应一个材质,完美匹配你要的四个相邻面效果。
  3. 形状调整:先创建球体再拉长为橄榄球形状,确保中心位置的计算完全准确。

小提醒

  • 确保四张图片的尺寸一致,这样纹理映射不会变形;
  • 如果纹理显示有拉伸,可以调整widthSegments和heightSegments的数值(越高越精细,64是性能和效果的平衡值);
  • 要是纹理出现翻转,可以给材质添加side: THREE.DoubleSide属性。

备注:内容来源于Stack Exchange,提问作者SiddAjmera

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 11:10:29