注册
web

threejs3D汽车换肤实战

06-汽车动态换肤的案列


课程内容


一、环境的搭建


(1)搭建项目

threejs的每个版本都有一些差异,在api和threejs项目文件夹下面,本案列使用的版本


npm i three@0.153.0

项目的目录结构如下:


03-fulldemo
└───css
│───main.css

└───draco
│───gltf——存放Google Draco解码器插件

└───models——存放模型
│───ferrari.glb——模型文件,可以是glb也可以是gltf格式
│───ferrari_ao.png——模型贴图,这个图片是阴影效果

└───textures——纹理材质
│───venice_sunset_1k.hdr——将其用作场景的环境映射或者用来创建基于物理的材质


(2)代码基础结构搭建

创建对应的html文件并引入相应的环境


<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - materials - car</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="./css/main.css">
<style>
body {
color: #bbbbbb;
background: #333333;
}
a {
color: #08f;
}
.colorPicker {
display: inline-block;
margin: 0 10px
}
</style>
</head>

<body>
<!--设置三个按钮,用于切换车身、轮毂、玻璃的颜色-->
<div id="info">
<span class="colorPicker"><input id="body-color" type="color" value="#ff0000"></input><br/>Body</span>
<span class="colorPicker"><input id="details-color" type="color" value="#ffffff"></input><br/>Details</span>
<span class="colorPicker"><input id="glass-color" type="color" value="#ffffff"></input><br/>Glass</span>
</div>
<!--要渲染3D的容器-->
<div id="container"></div>
<script type="importmap">
{
"imports": {
"three": "./node_modules/three/build/three.module.js",
"three/addons/": "./node_modules/three/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
//用于显示屏幕渲染帧率的面板
import Stats from 'three/addons/libs/stats.module.js';
//相机控件OrbitControls实现旋转缩放预览效果。
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
//加载GLTF文件格式的加载器,用于加载外部为gltf的文件
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
//Draco是一个用于压缩和解压缩 3D 网格和点云的开源库
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
//RGBELoader可以将HDR图像加载到Three.js应用程序中
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';

//下面的代码就是JS渲染逻辑代码
</script>
</body>
</html>

在css/main.css文件中我们的代码如下


body {
margin: 0;
background-color: #000;
color: #fff;
font-family: Monospace;
font-size: 13px;
line-height: 24px;
overscroll-behavior: none;
}

a {
color: #ff0;
text-decoration: none;
}

a:hover {
text-decoration: underline;
}

button {
cursor: pointer;
text-transform: uppercase;
}

#info {
position: absolute;
top: 0px;
width: 100%;
padding: 10px;
box-sizing: border-box;
text-align: center;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
z-index: 1; /* TODO Solve this in HTML */
}

a, button, input, select {
pointer-events: auto;
}

.lil-gui {
z-index: 2 !important; /* TODO Solve this in HTML */
}

@media all and ( max-width: 640px ) {
.lil-gui.root {
right: auto;
top: auto;
max-height: 50%;
max-width: 80%;
bottom: 0;
left: 0;
}
}

#overlay {
position: absolute;
font-size: 16px;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: rgba(0,0,0,0.7);
}

#overlay button {
background: transparent;
border: 0;
border: 1px solid rgb(255, 255, 255);
border-radius: 4px;
color: #ffffff;
padding: 12px 18px;
text-transform: uppercase;
cursor: pointer;
}

#notSupported {
width: 50%;
margin: auto;
background-color: #f00;
margin-top: 20px;
padding: 10px;
}


效果如下图:


image-20230627175428242


二、进行3D场景的渲染


(1)进行初始化函数设计

在项目中我们添加一个carInit函数进行动画的初始化


...省略之前代码
//下面的代码就是JS渲染逻辑代码
let scene, renderer, grid, camera;
function initCar(){
//里面就开始进行3D场景的搭建
}

//执行初始化函数
initCar()

上面的函数设计用于执行我们所有3d业务代码。


(2)创建场景

/**
* (1)获取要渲染的容器
*/

const container = document.getElementById('container');

/**
* (2)创建场景对象Scene
*/

//创建一个场景对象,用来模拟3d世界
scene = new THREE.Scene();
//设置一个场景的背景颜色
scene.background = new THREE.Color(0x333333);
//这个类中的参数定义了线性雾。也就是说,雾的密度是随着距离线性增大的
scene.fog = new THREE.Fog("red", 10, 15);

background:这个属性用于设置我们场景的背景颜色,0x333333默认采用深灰来作为我们初始颜色


fog:定义了线性雾,类似于在背景指定位置设置雾化的效果,让背景看起来更加模糊,凸显空旷效果。


(3)坐标格辅助对象

/**
* (3)坐标格辅助对象. 坐标格实际上是2维线数组.
*/

//创建网格对象,参数1:大小,参数2:网格细分次数,参数3:网格中线颜色,参数4:网格线条颜色
grid = new THREE.GridHelper(40, 40, 0xffffff, 0xffffff);
//网格透明度
grid.material.opacity = 1;
grid.material.depthWrite = false;
grid.material.transparent = true;
scene.add(grid);

坐标格辅助对象GridHelper可以在3D场景中定义坐标格出现。后续我们会在坐标格上面放我们的模型进行展示


代码编写完毕后,最终渲染出来的坐标格效果如下:


image-20230628155733478


(4) 创建相机对象

/**
* (4)创建透视相机
* 参数一:摄像机视锥体垂直视野角度
* 参数二:摄像机视锥体长宽比
* 参数三:摄像机视锥体近端面
* 参数四:摄像机视锥体远端面
*/

camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.3, 100);
camera.position.set(0, 1.4, - 4.5);

任何一个3D渲染效果都需要相机来成像


这一投影模式被用来模拟人眼所看到的景象,它是3D场景的渲染中使用得最普遍的投影模式


透视相机最大的特点就是满足近大远小的效果。


(5)创建一个渲染器

/**
* (5)创建一个渲染器
*/

renderer = new THREE.WebGLRenderer({ antialias: true });
renderer = new THREE.WebGLRenderer({ antialias: true });
//设置设备像素比。通常用于避免HiDPI设备上绘图模糊
renderer.setPixelRatio(window.devicePixelRatio);
//设置渲染出来的画布范围
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
renderer.render(scene, camera);

有了场景、相机、坐标格辅助,我们想要让画面能够呈现出来,那就得有渲染器。


相当于你拍照需要将画面呈现到交卷上面。


其中renderer.render(scene, camera); 这段代码就是在进行渲染器的渲染。


如果render在指定频率内不断被调用,那就意味着可以不断拍照,不断渲染。可以实现动态切换效果


(6)效果渲染

当执行完上面的代码后,你需要确保调用了carInit这个函数,页面就可以渲染出对应的效果了


image-20230628161312404


说明:



  1. 场景的背景色为0x333333效果为深灰色。
  2. 我们设置的fog线性雾颜色为红色,所以你会发现在背景和网格之间会有一个过渡颜色。
  3. 网格的颜色采用的是0xffffff效果为灰色。

对应的各种参数,当你在学习的时候都都可以进行调整。一遍调整就能看懂参数和最终渲染的效果差异。


当你把fog的颜色调整为跟背景一样的时候,你会发现画面上就类似产生了迷雾效果,让3D背景更加立体


scene.fog = new THREE.Fog(0x333333, 10, 15);

效果如下:


image-20230628161707934


你也可以继续设置网格线条的透明度,让网格线不那么抢眼


grid.material.opacity = 0.3;

效果如下:


image-20230628161827094


是不是整个画面看起来3D立体效果会更强一些,背景看起来更深邃一些。


三、加载外部模型进行渲染


(1)添加轨道控制器

threejs官方给我们提供了一个类,OrbitControls(轨道控制器)可以使得相机围绕目标进行轨道运动。


换句话说,引入了OrbitControls后,我们可以操作鼠标来控制页面上动态效果。


比如:鼠标滚动、鼠标点击、鼠标左右滑动效果。


代码如下:


...省略了 【(5)创建一个渲染器】
/**
* (6)开启OrbitControls控件,可以支持鼠标操作图像
*/

controls = new OrbitControls(camera, container);
//你能够将相机向外移动多少(仅适用于PerspectiveCamera),其默认值为Infinity
controls.maxDistance = 9;
//你能够垂直旋转的角度的上限,范围是0到Math.PI,其默认值为Math.PI
controls.maxPolarAngle = THREE.MathUtils.degToRad(90);
controls.target.set(0, 0.5, 0);
controls.update();

加入上面代码后,我们还要继续优化代码


在carInit函数后面在添加一个render函数,用于执行渲染


function initCar(){

/**
* (5)创建一个渲染器
*/

renderer = new THREE.WebGLRenderer({ antialias: true });
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
//注释掉这句话
//renderer.render(scene, camera);
//调用一次render函数进行渲染
render()
}
function render(){
renderer.render(scene, camera);
requestAnimationFrame(render)
}

效果实现如下:



(2)加载汽车模型

既然要加载外部模型,那我们肯定需要通过模型软件来设计对应的模型。本案列不讲解如何设计模型,我使用threejs官方提供的模型来进行展示。


我们常用的模型格式如下:



  1. OBJ (Wavefront OBJ):

    OBJ 是一种常见的纯文本模型格式,支持存储模型的几何信息(顶点、面)和材质信息(纹理坐标、法线等)。可以通过OBJLoader来加载和解析OBJ格式的模型。


  2. FBX (Autodesk FBX):

    FBX 是由Autodesk开发的一种常用的二进制模型格式,支持存储模型的几何信息、材质、动画等。可以通过FBXLoader来加载和解析FBX格式的模型。


  3. GLTF (GL Transmission Format):

    GLTF 是一种基于JSON的开放标准,用于存储和传输三维模型和场景。GLTF格式支持几何信息、材质、骨骼动画、节点层次结构等,并且通常具有较小的文件大小。可以通过GLTFLoader来加载和解析GLTF格式的模型。


  4. STL (Stereolithography):

    STL 是一种常用的三维打印文件格式,用于存储模型的几何信息。STL 文件通常包含三角形面片的列表,用于定义模型的外观。可以通过STLLoader来加载和解析STL格式的模型。


  5. GLB:

    GLB是GL Transmission Format(gltf)的二进制版本,GLB格式将模型的几何信息、材质、骨骼动画、节点层次结构等存储在单个二进制文件中,通常具有较小的文件大小和更高的加载性能.



本案列采用glb格式来加载外部模型。


因为案列中使用glb模型数据采用了Draco来进行压缩,所以我们需要引入DRACOLoader来解析我们的模型


(1)引入DRACOLoader加载模型


/**
* (7)汽车模型相关的内容
* DRACOLoader 主要用于解析使用 Draco 压缩的 GLB 模型,而不是所有的 GLB 模型都使用了 Draco 压缩
*/

const dracoLoader = new DRACOLoader();
//配置加载器的位置,这个需要提前下载到项目中
dracoLoader.setDecoderPath('./draco/gltf/');
const loader = new GLTFLoader();
//设置GLTFLoader加载器使用DRACO来解析我们的模型数据
loader.setDRACOLoader(dracoLoader);


并不是所有的模型都需要Draco来进行加载,取决于你的模型在设计导出的时候是否用了Draco来进行压缩。



./draco/gltf/目录下面的文件如下:代码可以从gitee上面下载


image-20230629142933880


(3)加载glb模型数据

当你已经创建了`const loader = new GLTFLoader();这个类实例后,我们就可以加载模型了


/**
* (8)加载glb模型
*/

loader.load('./models/gltf/ferrari.glb', function (gltf) {
//获取到模型的数据
const carModel = gltf.scene.children[0];
//将模型添加到3D场景中
scene.add(carModel);
});
render()

加载的效果如下:


image-20230629143514305


模型已经加载成功了,但是你会发现他在整个背景中是黑色的。当然模型本身是有材质贴图的,车身默认是红色的。


之所以产生这个效果那是因为我们现在缺少一个非常重要的元素,那就是光照。


你试想一下,一个物体在没有任何光源的情况下,呈现出来的就是黑色的效果。如果你的场景背景也是黑色,那根本看不到效果。


(4)加载光影效果

我们设置光源的时候主要有两个部分



  1. 环境光:相当于天空的颜色,物体表面可以反射出对应的颜色。
  2. 点光源:相当于开启手电筒,照射到模型表面反射出来的颜色。

设置环境光


/**
* (9)添加光影效果
*/


//创建环境光
var ambient = new THREE.AmbientLight("blue");
scene.add(ambient);

环境光的颜色为blue,效果如下:


image-20230629145635074


环境光为blue的情况下,模型表面反射出来的颜色就是蓝色,一般金属材质和玻璃材质反射的效果更佳明显。所以轮毂和车辆挡风玻璃效果会更强烈一些。


设置点光源


/**
* (9)添加光影效果
*/


//创建环境光
var ambient = new THREE.AmbientLight("blue");
scene.add(ambient);

//创建点光源
var point = new THREE.PointLight("#fff");
//设置点光源位置
point.position.set(0, 300, 0);
//点光源添加到场景中
scene.add(point);

效果如下:


image-20230629145913328


此刻我们基本上完成了模型的渲染,环境光蓝色默认替换为黑色,这样车辆立体感会更强一些


//环境光
var ambient = new THREE.AmbientLight("#000");

效果如下:


image-20230629150241875


(5)加载hdr文件设置环境渲染

HDR(High Dynamic Range)文件是一种存储图像高动态范围信息的文件格式。


HDR可以理解成一张真实世界的图片或者设计者想要的灯光效果。


他的作用主要如下:



  1. HDR文件经常被用作环境贴图,用于模拟反射和光照环境。环境贴图是将场景的背景、反射和光照信息包装成一个纹理,然后将其应用到物体表面上。通过使用HDR文件作为环境贴图,可以更真实地模拟光线在场景中的反射和折射,增强渲染效果。
  2. HDR文件还可以用于模拟全局照明效果。全局照明是一种渲染技术,它考虑了场景中所有光源的组合对物体的影响,以获得更真实的照明效果。通过使用HDR文件提供的高动态范围和丰富的光照信息,可以在Three.js中实现更逼真的全局照明效果

也就说在本案列中如果我们想要获取更加真实的照明效果,我们可以使用设计师导出的hdr文件。将这个文件作为3D场景(Scene)的环境贴图


/**
* (2)创建场景对象Scene
*/

scene = new THREE.Scene();
scene.background = new THREE.Color(0x333333);
//通过RGBELoader加载hdr文件,它是一种图像格式,将其用作场景的环境映射或者用来创建基于物理的材质
scene.environment = new
RGBELoader().load('textures/equirectangular/venice_sunset_1k.hdr');
scene.environment.mapping = THREE.EquirectangularReflectionMapping;
scene.fog = new THREE.Fog(0x333333, 10, 15);

删除我们(9)添加光影效果中我们自己的光影效果


/**
* (9)添加光影效果
*/


//创建环境光
//var ambient = new THREE.AmbientLight("blue");
//scene.add(ambient);

//创建点光源
//var point = new THREE.PointLight("#fff");
//设置点光源位置
//point.position.set(0, 300, 0);
//点光源添加到场景中
//scene.add(point);

这样渲染下来我们物体在场景中显示的会更加自然


image-20230629152457227



不管你用hdr文件来作为环境贴图,还是采用光源设置来设计,我们都可以让模型在3D场景中更方便的显示出来。



四、汽车材质贴图


目前我们已经将模型渲染出来了,但是你会发现不管是车身、轮毂、还是玻璃材质跟我们想要的真实车辆材质是有区别的。比如你希望玻璃透明的、反光的。车身的漆面是可以反光的。模型在设计的时候使用默认材质。我们想要进行材质的替换。


(1)在步骤8中继续优化代码

/**
* (8)加载glb模型
* 并设置不同部位的材质。
*/

//物理网格材质(MeshPhysicalMaterial)
//车漆,碳纤,被水打湿的表面的材质需要在面上再增加一个透明的
const bodyMaterial = new THREE.MeshPhysicalMaterial({
color: 0xff0000, metalness: 1.0, roughness: 0.5, clearcoat: 1.0, clearcoatRoughness: 0.03
});

//汽车轮毂的材质,采用了标准网格材质,threejs解析gltf模型,会用两种材质PBR材质去解析
const detailsMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff, metalness: 1.0, roughness: 0.5
});

//汽车玻璃的材质
const glassMaterial = new THREE.MeshPhysicalMaterial({
color: 0xffffff, metalness: 0.25, roughness: 0, transmission: 1.0
});

loader.load('./models/gltf/ferrari.glb', function (gltf) {
//获取到模型的数据
const carModel = gltf.scene.children[0];
//将模型添加到3D场景中
scene.add(carModel);
});

材质创建了过后,接下来我们就可以将材质加载了到模型中了。


loader.load('./models/gltf/ferrari.glb', function (gltf) {
//获取到模型的数据
const carModel = gltf.scene.children[0];

//获取模型中指定的模块,将默认材质替换为我们自定义材质
carModel.getObjectByName('body').material = bodyMaterial;
//轮毂的材质替换
carModel.getObjectByName('rim_fl').material = detailsMaterial;
carModel.getObjectByName('rim_fr').material = detailsMaterial;
carModel.getObjectByName('rim_rr').material = detailsMaterial;
carModel.getObjectByName('rim_rl').material = detailsMaterial;
////座椅的材质
carModel.getObjectByName('trim').material = detailsMaterial;
//玻璃的材质替换
carModel.getObjectByName('glass').material = glassMaterial;

scene.add(carModel);
});

上面的代码分别是获取模型中车身区域(body),获取轮毂区域(rim_fl、rim_fr、rim_rr、rim_rl)、座椅区域(trim)、玻璃区域(glass)


将我们自己创建的材质拿去替换默认材质实现加载渲染。


效果如下:


image-20230629164136907


替换过后的模型,更有金属质感和玻璃质感。材质对应的颜色你们都可以自己进行替换。


(2)给车底盘添加阴影效果


车底盘是没有阴影效果的,我们可以使用图片来进行模型贴图,让底盘有阴影效果会更加立体。


贴图的图片为png,图片由设计师出的


效果如下:


ferrari_ao


创建一个材质对象,并使用这张图片作为贴图


loader.load('./models/gltf/ferrari.glb', function (gltf) {
//获取到模型的数据
const carModel = gltf.scene.children[0];

//获取模型中指定的模块,将默认材质替换为我们自定义材质
carModel.getObjectByName('body').material = bodyMaterial;
//轮毂的材质替换
carModel.getObjectByName('rim_fl').material = detailsMaterial;
carModel.getObjectByName('rim_fr').material = detailsMaterial;
carModel.getObjectByName('rim_rr').material = detailsMaterial;
carModel.getObjectByName('rim_rl').material = detailsMaterial;
//座椅的材质
carModel.getObjectByName('trim').material = detailsMaterial;
//玻璃的材质替换
carModel.getObjectByName('glass').material = glassMaterial;

// shadow阴影效果图片
const shadow = new THREE.TextureLoader().load( './models/gltf/ferrari_ao.png' );
// 创建一个材质模型
const mesh = new THREE.Mesh(
new THREE.PlaneGeometry(0.655 * 4, 1.3 * 4),
new THREE.MeshBasicMaterial({
map: shadow, blending: THREE.MultiplyBlending, toneMapped: false, transparent: true
})
);
mesh.rotation.x = - Math.PI / 2;
mesh.renderOrder = 2;
carModel.add(mesh);

scene.add(carModel);
});

效果如下:


image-20230629175934463


通过效果图能看出,车辆底部是有阴影效果的,让整个3D效果渲染更加立体。


五、设置动画效果


(1)获取轮毂的材质对象

轮毂和网格地板我们都要动画加载


网格需要进行平移,按照z的反方向进行移动。


轮毂需要按照x轴的方向进行旋转


代码如下:


let wheels = []
function initCar(){
loader.load('./models/gltf/ferrari.glb', function (gltf) {
//获取到模型的数据
const carModel = gltf.scene.children[0];

...省略代码
//将车轮的模块保存到数组中,后面可以设置动画效果
wheels.push(
carModel.getObjectByName('wheel_fl'),
carModel.getObjectByName('wheel_fr'),
carModel.getObjectByName('wheel_rl'),
carModel.getObjectByName('wheel_rr')
);

scene.add(carModel);
});
}

上面的代码将轮毂模块获取到过后,放入到wheels数组中。


(2)设置轮毂的动画效果

接下来在render函数中进行动画控制


function render() {
controls.update();
//performance.now()是一个用于测量代码执行时间的方法。它返回一个高精度的时间戳,表示自页面加载以来的毫秒数
const time = - performance.now() / 1000;
//控制车轮的动画效果
for (let i = 0; i < wheels.length; i++) {
wheels[i].rotation.x = time * Math.PI * 2;
}
//控制网格的z轴移动
grid.position.z = - (time) % 1;

renderer.render(scene, camera);
requestAnimationFrame(render)
}

通过上面的代码我们已经能够实现轮毂和网格的动画效果了


六、切换颜色


实现颜色切换就必须绑定js的事件。


三个按钮,我们都绑定点击事件,并获取对应的颜色


function initCar(){
...省略代码
/**
* (10)切换车身颜色
* 获取到指定的按钮,得到你选中的颜色,并将颜色设置给我们自己的模型对象
*/

const bodyColorInput = document.getElementById('body-color');
bodyColorInput.addEventListener('input', function () {
bodyMaterial.color.set(this.value);
});

const detailsColorInput = document.getElementById('details-color');
detailsColorInput.addEventListener('input', function () {
detailsMaterial.color.set(this.value);
});

const glassColorInput = document.getElementById('glass-color');
glassColorInput.addEventListener('input', function () {
glassMaterial.color.set(this.value);
});
}

当我们将上面的代码实现后,切换颜色就完成分了。


只要修改bodyMaterial材质对象的颜色,页面刷新的时候就可以应用成功。


课程小结


作者:无处安放的波澜
来源:juejin.cn/post/7277787934848204835

0 个评论

要回复文章请先登录注册