写点什么

小米 su7 or 保时捷怎么选?使用 Three 实现 3D 汽车展示平台比比看

作者:战场小包
  • 2024-12-19
    湖北
  • 本文字数:9516 字

    阅读完需:约 31 分钟

小米su7 or 保时捷怎么选?使用 Three 实现 3D 汽车展示平台比比看

前言

Threejs 可以实现特别多炫酷的 3D 效果,作为当下极度内卷的前端时代的前端 er,需要不断的提升自己,但是苦于 Three 的高质量资料较少,学起来有几分困难。


从去年最开始写 Threejs 专栏开始,就想写一个汽车展示平台,一直没能够很完善的实现,这次在 AI 的协助下,写一个 su7 和保时捷的展示 &对比平台,咱们就来较个真,看看到底咋好看?


通过本文,你能学到:


  • 打造一个 SU7 和 保时捷的 3D 展示平台

  • 初始化 Three 项目的必备要素

  • Three 开发常用辅助手段

  • 如何下载、加载和预览模型

  • 如何实现颜色、贴膜的替换

  • 实现动画效果

  • 学习如何使用 MarsCode 和豆包进行辅助编程


观看本文前,推荐先阅读:Threejs 从入门到实战专栏,建立起对 Three 的基本概念

准备工作

在实现一个 3D 展示平台之前,首先需要一些准备工作

获取模型

这里比较推荐 sketchfab,从里面下载一下,su7 的模型比较多, porsche_taycan 模型相对少一些,而且还不够精美



其他 3D 模型获取方式:free3d cgmodel cgtrade ...


模型有很多种格式,本文选用 GLTF 格式,根据扩展名可以分为两种


  • gltf: gltf 格式文件没有被压缩,会附带额外的 bin 数据文件,在文本编辑器中容易阅读,方便进行调试。

  • glb: 所有数据都包含在二进制文件中,文件小,推荐使用,本文选用


创建项目

建议使用 vue 或者 react 进行实现,避免引入 Threejs 有一堆莫名其妙的问题。


// 创建项目npm create vite@latest// 安装依赖npm i// 安装three.jsnpm i --save-dev three// 启动项目npm run dev
复制代码

搭建 Three 项目

搭建一个基础的 Three 演示代码,Threejs 中存在核心三要素


  • scene:渲染物体的场景

  • camera:相机,决定场景中哪些部分能看到

  • renderer:渲染器,将场景渲染到画布 canvas 上这部分可以参考官方的案例 Creating a scene,也可以直接使用 MarsCode 生成一份。


输入:在本仓库的基础上,使用 vue 写一个 threejs 演示代码。由于当前为 vue 仓库,输入指令限定一下范围,避免生成原生 javascript 代码


<template>  <div id="container"></div></template>
<script>import * as THREE from "three";
export default { name: "ThreejsDemo", mounted() { this.initThreejs(); }, methods: { initThreejs() { const width = window.innerWidth; const height = window.innerHeight;
const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height); document.getElementById("container").appendChild(renderer.domElement);
const geometry = new THREE.BoxGeometry(); const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
const animate = () => { requestAnimationFrame(animate);
cube.rotation.x += 0.01; cube.rotation.y += 0.01;
renderer.render(scene, camera); };
animate(); }, },};</script>
<style>#container { width: 100%; height: 100%;}</style>
复制代码


重新运行程序,就可以实现一个转动的绿色正方体。另外,我比较习惯将 scene、renderer、camera 等变量定义为全局变量,方便后续使用,这里按需使用。



在 Threejs 开发中,在开发的过程中,官方提供的某些辅助功能,非常方便,这里也添加一下,简单的建议还是使用 MarsCode 直接生成就好。


输入:添加轨道控制器 control 和辅助线和窗口自适应


import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
initThree() { // 创建坐标辅助线 const axesHelper = new THREE.AxesHelper(5); scene.add(axesHelper); // 创建轨道控制器 controls = new OrbitControls(camera, renderer.domElement); // 窗口自适应 window.addEventListener("resize", () => { const width = window.innerWidth / 2; const height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height); });}
复制代码


  • 轨道控制器:允许用户通过鼠标操作来控制相机,实现围绕场景中的目标物体进行旋转、缩放和平移。通俗来讲就是一个可以控制相机的工具。

  • 坐标辅助线:可以创建一个坐标系,注意轴的颜色,黄色代表 X 轴,绿色代表 Y 轴,蓝色代表 Z 轴,不小心就被坑了,将 Z 轴和 Y 轴混淆

  • 窗口自适应


有可能还有一些更深层次的辅助需求,比如想看相机的位置,那可以启用相机位置指示器


// 创建相机位置指示器cameraHelper = new THREE.CameraHelper(camera);scene.add(cameraHelper);
复制代码


据我体验,只是在页面中多了一个灰色的坐标系,交叉点是相机指向的方向,目前没有感觉出什么用。



想获取相机的位置,因为在后续操作中,可以根据相机的位置进行一些复杂交互。


输入:能不能渲染出相机的位置,并可视化渲染的区域。


此次 MarsCode 没有给出满意的结果啊,反复问了几次都没有成功,依旧推荐 CameraHelper,遇到这种情形,不要头铁,换一种描述来做:



输入:尝试通过一个物体来模拟相机的位置


MarsCode 生成的代码如下:


camera.position.z = 5;
const markerGeometry = new THREE.SphereGeometry(0.1, 16, 16);const markerMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });marker = new THREE.Mesh(markerGeometry, markerMaterial);// 将标记物体的位置设置为相机的位置marker.position.copy(camera.position);// 将标记物体添加到场景scene.add(marker);
const animate = () => { requestAnimationFrame(animate);
// 更新标记物体的位置为相机的位置 marker.position.copy(camera.position); cube.rotation.x += 0.01; cube.rotation.y += 0.01;
renderer.render(scene, camera);};
复制代码


非常有创意的实现方式,相当于在场景中创建了一个物体,物体的位置和相机重合,从而实现相机的可视化,注意物体的创建要在相机设定位置之后。而且,要删除掉 animate 中的 marker.position.copy(camera.position); 否则看不到物体。这也提醒了,使用 MarsCode 生成的代码,可能需要根据场景做一些动态调整。



到这里基本工作就完成了,下面开始核心部分开发。

核心开发

要实现一个 su7 vs 保时捷的 3D 汽车展示平台,要实现以下内容


  • 加载 glb 模型

  • 渲染展示场景

  • 添加灯光

  • 车体颜色修改

  • 车体贴膜修改

  • 场景切换

  • 更多功能...


定义一个 Car 类,用于实例汽车。推荐借助 MarsCode 实现一个基础,然后再手动进行完善


// 定义 Car 类import * as THREE from "three";
export default class Car { constructor(model, scene, camera, controls, color, name) { this.model = model; this.scene = scene; this.camera = camera; this.controls = controls; this.color = color;
// 初始化方法 this.init(); }
init() { // 将汽车模型添加到场景中 this.scene.add(this.model); } // 修改颜色 changeColor(color) {} // 修改粗糙度 changeRoughness(roughness) {} // 修改金属度 changeMetalness(metalness) {}}
复制代码

加载模型

使用 MarsCode 帮助写一段加载 glb 模型的方法:定义一个 GLTF 模型加载的函数


// 引入 Three.js 和 GLTF 加载器import * as THREE from "three";import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
export default { name: "GLTFModelViewer", data() { return { model: null, }; }, methods: { loadGLTFModel() { const loader = new GLTFLoader(); loader.load( // 模型文件的路径 "/path/to/your/model.gltf", // 加载成功后的回调函数 (gltf) => { this.model = gltf.scene; this.scene.add(this.model); }, // 加载过程中的回调函数 (xhr) => { console.log((xhr.loaded / xhr.total) * 100 + "% loaded"); }, // 加载失败后的回调函数 (error) => { console.error(error); } ); }, }, mounted() { this.loadGLTFModel(); },};
复制代码



MarsCode 写的太完善了,给简化一下


import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";import Car from "../model/Car";
export default { mounted() { this.initThree(); this.loadModel(); }, methods: { loadModel() { this.loader = new GLTFLoader(); // 分别传入两个汽车模型的 path this.loader.load(this.modelPath, (gltf) => { this.car = new Car(gltf.scene, scene, camera, controls, this.carColor); // 传递初始颜色 }); }, },};
复制代码



保时捷模型完全看不到,小米 su7 只能模模糊糊的看到一些轮廓。


代码开发中遇到未知的情形,还是推荐多问一些 MarsCode 或者豆包,问题是:需要设定光源才能实现预览

添加光源

输入:期望添加来自一个四面八方的光源


const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);directionalLight.position.set(1, 1, 1);scene.add(directionalLight);
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.8);directionalLight2.position.set(-1, -1, -1);scene.add(directionalLight2);
const directionalLight3 = new THREE.DirectionalLight(0xffffff, 0.8);directionalLight3.position.set(-1, 1, -1);scene.add(directionalLight3);
const directionalLight4 = new THREE.DirectionalLight(0xffffff, 0.8);directionalLight4.position.set(1, -1, 1);scene.add(directionalLight4);
复制代码


使用了一组四个方向的平行光 DirectionalLight 和一个环境光 AmbientLight,就可以成功看到 su7 和保时捷了


调整模型清晰度

两个车有点偏像素风,非常模糊,继续询问一下 MarsCode



设定一下抗锯齿和设备像素比


renderer = new THREE.WebGLRenderer({ antialias: true });renderer.setPixelRatio(window.devicePixelRatio);
复制代码



相较于 su7,保时捷显得有点小,这种情况修改一下相机位置,拉紧一些保时捷的相机,拉远一些 su7 的。


// su7camera.position.set(5, 5, 5);// 保时捷camera.position.set(-3, 3, 3);
复制代码


添加背景

输入:现在需要生成一个具有科技感的背景用于汽车模型的展示


上面给出的 prompt 就有些过于笼统了,导致这里和 MarsCode 拉扯了一会,后续补充了多组进一步提示,才实现展示背景。


createBackground() {  // 创建背景网格  const geometry = new THREE.PlaneGeometry(100, 100);
// 使用 MeshPhysicalMaterial 实现金属效果 const material = new THREE.MeshPhysicalMaterial({ color: 0xcccccc, // 起始颜色:浅灰色 metalness: 0.5, // 金属度 roughness: 0.2, // 粗糙度 envMapIntensity: 1.0, // 环境贴图强度 });
// 加载环境贴图 // const loader = new THREE.CubeTextureLoader(); // const envMap = loader.load([ // 'path_to_positive_x.jpg', // 'path_to_negative_x.jpg', // 'path_to_positive_y.jpg', // 'path_to_negative_y.jpg', // 'path_to_positive_z.jpg', // 'path_to_negative_z.jpg', // ]);
// 将环境贴图应用到材质上 // material.envMap = envMap;
const plane = new THREE.Mesh(geometry, material); plane.rotation.x = -Math.PI / 2; scene.add(plane);
// 增加环境光的强度 const ambientLight = new THREE.AmbientLight(0xffffff, 1.5); // 增加到1.5 scene.add(ambientLight);
// 调整光源位置和强度 const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); directionalLight.position.set(1, 1, 1); scene.add(directionalLight);
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.5); directionalLight2.position.set(-1, -1, -1); scene.add(directionalLight2);
const directionalLight3 = new THREE.DirectionalLight(0xffffff, 0.5); directionalLight3.position.set(-1, 1, -1); scene.add(directionalLight3);
const directionalLight4 = new THREE.DirectionalLight(0xffffff, 0.5); directionalLight4.position.set(1, -1, 1); scene.add(directionalLight4);},
复制代码


推荐使用环境贴图,一时没有找到合适的,就没有添加,本质上就是创建一个平面,使用 MeshPhysicalMaterial 进行渲染,效果如下:



环境贴图获取方式 MarsCode 也提供了一些,有需要可以参考一下


颜色修改

车颜色可以说就相当于 QQ 飞车的皮肤,好看的颜色,那简直炫酷极了。


输入:定义一串颜色选择按钮,点击相应按钮,汽车模型颜色替换为该颜色


// Car.jsclass Car {  // ...  changeColor(color) {    this.color = color;    this.model.material.color.set(color); // 更新颜色  }}
// Car.vue
export default { //... methods: { changeCarColor(color) { this.carColor = color; // 更新汽车模型的颜色 this.car.changeColor(color); }, },};
复制代码


上述实现是存在问题的,this.model.material.color.set(color) 抛出了错误。


既然 MarsCode 没有给出解决方案,先尝试一下豆包:现在在开发一个 three 汽车展示程序,已经成功加载了 glb 模型,如何实现模型的颜色变换


豆包给出了一种非常彻底的实现方式:


// 假设model是加载后的glb模型model.traverse((child) => {  if (child.isMesh) {    // 获取网格的材质    const material = child.material;    // 设置新的颜色,这里以红色为例(0xff0000是红色的十六进制表示)    material.color.set(0xff0000);  }});
复制代码


由于只有 Mesh 节点支持颜色修改,遍历汽车模型的所有子节点,如果为 Mesh 节点,替换颜色。


颜色的替换实现了,现实世界中不可能整车是一个颜色吧,可以沿着豆包提供的思路进行思考:是否有方法可以只修改其中某几个部分的颜色?


在遍历的过程中,可以打印一下各个子 Mesh 节点,可以获取到汽车模型的各个组成部分,每个 Mesh 都存在 name 属性,通过 name 进行进一步约束,实现局部的颜色替换。



新的问题来了,如何获取车身的 name 那,总不能挨着去尝试吧?Threejs 非常任性的提供了一个在线编辑平台,支持各类模型的导入,同时可以详细的看到模型的组成。


在线编辑平台:https://threejs.org/editor/




以小米 su7 为例,选中车身,右侧场景部分展示详细信息,其中有一个 Car_body,操作箭头,可以将该部分从汽车整体中拉出。Car_body 即车身,也就是需要替换颜色的部分,但是注意该部分的名字叫 Object_18。同样方法获取保时捷的车身 name 为 Body_Textures_Body_1_0。



下面附上使用的颜色数组


const color= [{  name: '土豪金',  color: '#ff9900',  isSelected: true,},{  name: '传奇黑',  color: '#343a40',  isSelected: false,},{  name: '海蓝',  color: '#409EFF',  isSelected: false,},{  name: '玫瑰紫',  color: '#6600ff',  isSelected: false,},{  name: '银灰色',  color: '#DCDFE6',  isSelected: false,}],
复制代码

贴膜替换

贴膜替换本质上与颜色替换相同,只不过修改的是材质的金属度和粗糙度,因此借助 MarsCode 生成一组修改粗糙度和金属度的按钮。


输入:类比颜色变换,生成一组按钮来控制汽车模型的金属度和粗糙度变换


生成的核心代码:


<button  v-for="(metalness, index) in metalnesses"  :key="metalness.name"  :style="{ backgroundColor: metalness.color, left: index * 100 + 'px' }"  @click="changeCarMetalness(metalness.value)">  {{ metalness.name }}</button><button  v-for="(roughness, index) in roughnesses"  :key="roughness.name"  :style="{ backgroundColor: roughness.color, left: index * 100 + 'px' }"  @click="changeCarRoughness(roughness.value)">  {{ roughness.name }}</button>
复制代码


{metalnesses: [    {      name: '低金属度',      value: 0.2,      color: '#888888',    },    {      name: '中金属度',      value: 0.5,      color: '#aaaaaa',    },    {      name: '高金属度',      value: 0.8,      color: '#cccccc',    },  ],  roughnesses: [    {      name: '低粗糙度',      value: 0.2,      color: '#888888',    },    {      name: '中粗糙度',      value: 0.5,      color: '#aaaaaa',    },    {      name: '高粗糙度',      value: 0.8,      color: '#cccccc',    },  ],}
复制代码


按钮的只渲染了 left 位置,没有写 bottom 会发生重合,手动调整一下。


其实代码遗漏了修改 Car 的金属度和粗糙度的代码,进一步交互一下:在 Car.js 中如何修改模型的粗糙度和金属度


// 假设你已经导入了必要的Three.js模块import * as THREE from 'three';
// 在Car类中添加方法来更改粗糙度和金属度changeRoughness(roughness) { this.model.traverse((child) => { if (child.isMesh) { const material = child.material; if (material instanceof THREE.MeshStandardMaterial || material instanceof THREE.MeshPhysicalMaterial) { material.roughness = roughness; } } });}
changeMetalness(metalness) { this.model.traverse((child) => { if (child.isMesh) { const material = child.material; if (material instanceof THREE.MeshStandardMaterial || material instanceof THREE.MeshPhysicalMaterial) { material.metalness = metalness; } } });}
复制代码


根据 color 部分的实现,可以添加上 name 的约束,避免修改整体汽车模型的粗糙度和金属度。


到这里,就可以实现基于粗糙度、金属度和颜色来实现车身的多种变化,具体效果如下:


驾驶位视角

最后实现一个复杂的效果,动态平滑切换到主驾驶位置,具体原理见下图:



可以想象一下驾车时候的感觉,然后将人的感觉转变成相机即可,也就是将相机位置固定在主驾驶处,相机朝向外面。


这一部分比较难通过 MarsCode 来实现,除非描述清楚具体的车辆位置、大小,这有些繁琐,只需要 MarsCode 提供大致怎么写,再陆续调整就行。



定义一个从内部看的函数,当该函数触发时,调整相机位置和指向


viewFromInside() {  camera.position.set(-0.2, 0.9, -0.6);  // 设置主驾驶视角目标点,假设看向汽车前方中心位置,比如(0.5, 0, 0.8)  const targetPoint = new THREE.Vector3(2.8, 1.2, -0.4);  camera.lookAt(targetPoint);}
复制代码



视角的转换非常突然,尝试添加一下动画效果,更圆滑的实现转变。动画也没必要重复造轮子,询问一下



gsap,一定注意时 gsap,总是习惯打成 gasp。


需要动态的修改相机的位置和相机的朝向,相机的位置可以通过 camera.position 修改,lookAt 是个瞬间操作,无法通过动画来实现。


输入:现在需要实现一个从汽车模型外部到内部的 gsap 动画切换效果,在修改 camera.position 的同时,同步修改相机指向的位置,这里需要怎么实现


// 假设你已经导入了必要的Three.js模块和GSAP库import * as THREE from 'three';import gsap from 'gsap';
// 在Car类中添加方法来实现视角切换动画viewFromOutsideToInside() { // 设置初始相机位置和目标点(外部视角) const initialCameraPosition = new THREE.Vector3(-5, 5, 5); const initialTargetPoint = new THREE.Vector3(0, 0, 0);
// 设置最终相机位置和目标点(内部视角) const finalCameraPosition = new THREE.Vector3(-0.2, 0.9, -0.6); const finalTargetPoint = new THREE.Vector3(2.8, 1.2, -0.4);
// 使用GSAP的to方法来动画相机位置 gsap.to(camera.position, { x: finalCameraPosition.x, y: finalCameraPosition.y, z: finalCameraPosition.z, duration: 2, ease: 'power2.inOut', onUpdate: () => { // 在动画过程中更新相机的目标点 camera.lookAt(initialTargetPoint.lerp(finalTargetPoint, gsap.utils.clamp(0, 1, gsap.getProperty(camera.position, 'x') / finalCameraPosition.x))); } });}
复制代码


效果很奇怪,跟预想的不同,尝试调整一下



当时尝试了很多提示词,都没能实现比较满意的效果,主要不知道如何描述所需场景。


后面问了豆包,OrbitControls 是用于围绕目标对象进行旋转、缩放和平移操作,存在一个 target 属性,定义了相机围绕点。动态地改变 target 属性的值,可以实现相机焦点的平滑转移。


那么将原来的 lookAt 位置设置为 target 的位置,就能实现相机位置修改的同时,围绕点也在旋转,代码如下:


viewFromInside() {  // 设置相机位置到汽车内部  gsap.to(camera.position, {    x: -0.2,    y: 0.9,    z: -0.4,    duration: 2,    ease: "power2.inOut",    onUpdate: () => {      // 在动画过程中更新相机的视角      camera.lookAt(controls.target);    },  });
gsap.to(controls.target, { x: 2.8, y: 0.9, z: -0.4, duration: 2, ease: "power2.inOut", });},
复制代码


更多操作

上面实践了模型内部的遍历 traverse,通过 isMesh 和 name 锁定车身,借助 gsap 实现动画效果,这几个操作任意组合,能实现各式各样的效果。


例如去 https://threejs.org/editor/ 定位到车门所在 Mesh 为 Object_52。



添加一个按钮,点击按钮时,旋转车门


openCarDoor() {  this.model.traverse((child) => {    if (child.isMesh && child.name === 'Object_52') {      gsap.to(child.rotation, {        x: -Math.PI / 4,        y: -Math.PI / 4,        duration: 1,        ease: 'power2.out'      });    }  })}
复制代码



诸如此类,整个模型就可以随意折腾了。

总结

本文实现了一个 su7 & 保时捷模型的展示,包括背景、颜色、金属度、粗糙度、视角切换等,从中可以吸收到非常多


  • 初始化一个 Threejs 项目必备因素:scene、camera、renderer、animate、自适应、抗锯齿 antialias、设备像素比

  • 辅助手段:轨道控制器 OrbitControls、坐标辅助线 AxesHelper、相机位置指示器 CameraHelper(通过渲染物体来模拟)

  • 通过sketchfab下载模型和加载模型


loadModel() {  this.loader = new GLTFLoader();  this.loader.load(this.modelPath, (gltf) => {    // xxx  });},
复制代码


  • 在线预览模型,查看模型组成部分

  • 实现颜色、金属度、粗糙度改变

  • 借助 gsap 实现各类动画


另外,还是要反复提到 MarsCode 和豆包,作为开发的辅助,还是非常 nice 的,尤其是对于一些简单代码逻辑和开发所遇问题,是解答利器。


用好这两个东西,还是存在一定的难度,需求描述一定要清晰,步骤分解合理,循序渐进的进行描述。

源码

最后,附上粗糙的项目源码,有兴趣可以体验一下。

发布于: 刚刚阅读数: 5
用户头像

战场小包

关注

成长中的小前端,一起努力,一起进步 2021-09-23 加入

掘金签约作者、InfoQ签约作者、阿里云社区签约作者。公众号: 小包学前端

评论

发布
暂无评论
小米su7 or 保时捷怎么选?使用 Three 实现 3D 汽车展示平台比比看_Vue_战场小包_InfoQ写作社区