如何使用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;
关键部分说明
- 纹理与材质:加载四张纹理并分别创建MeshPhysicalMaterial,同时给每张纹理设置了边缘包裹模式,避免拉伸错位。
- 几何分组逻辑:遍历每个三角面,计算其中心位置的经度,将椭球按经度分成4个90度的扇区,每个扇区对应一个材质,完美匹配你要的四个相邻面效果。
- 形状调整:先创建球体再拉长为橄榄球形状,确保中心位置的计算完全准确。
小提醒
- 确保四张图片的尺寸一致,这样纹理映射不会变形;
- 如果纹理显示有拉伸,可以调整
widthSegments和heightSegments的数值(越高越精细,64是性能和效果的平衡值); - 要是纹理出现翻转,可以给材质添加
side: THREE.DoubleSide属性。
备注:内容来源于Stack Exchange,提问作者SiddAjmera
相关产品推荐
相关产品推荐

