Threejs完成3D汽车动态换肤的案列
课程目标
- 基于案列实现对three核心理论剖析
- 实战为王、理论为纲。跟着实战一起快速进入3D世界
- 一天时间就可以搞定threejs的入门学习
课程内容
一、环境的搭建
(1)搭建项目
在前端的世界中3D是必不可少的一部分,尤其是现在产品多元化后,很多应用中都会涉及3D相关的技术开发。接下来我们的任务认识3D技术,开始借助threejs来帮助我们完成3D开发。
Three.js是基于原生WebGL封装运行的三维引擎,在所有WebGL引擎中,Three.js是国内文资料最多、使用最广泛的三维引擎。
你如果你要理解Three.js和WebGL的关系,那就相当于jQuery和原生js的关系。
threejs的每个版本都有一些差异,在api和threejs项目文件夹下面,本案列使用的版本
js
npm i three@0.153.0
项目的目录结构如下:
js
03-fulldemo
└───css
│───main.css
│
└───draco
│───gltf------存放Google Draco解码器插件
│
└───models------存放模型
│───ferrari.glb------模型文件,可以是glb也可以是gltf格式
│───ferrari_ao.png------模型贴图,这个图片是阴影效果
│
└───textures------纹理材质
│───venice_sunset_1k.hdr------将其用作场景的环境映射或者用来创建基于物理的材质
│
(2)代码基础结构搭建
创建对应的html文件并引入相应的环境
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文件中我们的代码如下
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;
}
效果如下图:
二、进行3D场景的渲染
(1)进行初始化函数设计
在项目中我们添加一个carInit函数进行动画的初始化
js
...省略之前代码
//下面的代码就是JS渲染逻辑代码
let scene, renderer, grid, camera;
function initCar(){
//里面就开始进行3D场景的搭建
}
//执行初始化函数
initCar()
上面的函数设计用于执行我们所有3d业务代码。
(2)创建场景
js
/**
* (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)坐标格辅助对象
js
/**
* (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场景中定义坐标格出现。后续我们会在坐标格上面放我们的模型进行展示
代码编写完毕后,最终渲染出来的坐标格效果如下:
(4) 创建相机对象
js
/**
* (4)创建透视相机
* 参数一:摄像机视锥体垂直视野角度
* 参数二:摄像机视锥体长宽比
* 参数三:摄像机视锥体近端面
* 参数四:摄像机视锥体远端面
*/
camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.3, 100);
camera.position.set(0, 1.4, - 4.5);
任何一个3D渲染效果都需要相机来成像
这一投影模式被用来模拟人眼所看到的景象,它是3D场景的渲染中使用得最普遍的投影模式
透视相机最大的特点就是满足近大远小的效果。
(5)创建一个渲染器
js
/**
* (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这个函数,页面就可以渲染出对应的效果了
说明:
- 场景的背景色为
0x333333
效果为深灰色。 - 我们设置的fog线性雾颜色为红色,所以你会发现在背景和网格之间会有一个过渡颜色。
- 网格的颜色采用的是
0xffffff
效果为灰色。
对应的各种参数,当你在学习的时候都都可以进行调整。一遍调整就能看懂参数和最终渲染的效果差异。
当你把fog的颜色调整为跟背景一样的时候,你会发现画面上就类似产生了迷雾效果,让3D背景更加立体
js
scene.fog = new THREE.Fog(0x333333, 10, 15);
效果如下:
你也可以继续设置网格线条的透明度,让网格线不那么抢眼
js
grid.material.opacity = 0.3;
效果如下:
是不是整个画面看起来3D立体效果会更强一些,背景看起来更深邃一些。
三、加载外部模型进行渲染
(1)添加轨道控制器
threejs官方给我们提供了一个类,OrbitControls(轨道控制器)可以使得相机围绕目标进行轨道运动。
换句话说,引入了OrbitControls后,我们可以操作鼠标来控制页面上动态效果。
比如:鼠标滚动、鼠标点击、鼠标左右滑动效果。
代码如下:
js
...省略了 【(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函数,用于执行渲染
js
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官方提供的模型来进行展示。
我们常用的模型格式如下:
-
OBJ (Wavefront OBJ):
OBJ 是一种常见的纯文本模型格式,支持存储模型的几何信息(顶点、面)和材质信息(纹理坐标、法线等)。可以通过OBJLoader来加载和解析OBJ格式的模型。
-
FBX (Autodesk FBX):
FBX 是由Autodesk开发的一种常用的二进制模型格式,支持存储模型的几何信息、材质、动画等。可以通过FBXLoader来加载和解析FBX格式的模型。
-
GLTF (GL Transmission Format):
GLTF 是一种基于JSON的开放标准,用于存储和传输三维模型和场景。GLTF格式支持几何信息、材质、骨骼动画、节点层次结构等,并且通常具有较小的文件大小。可以通过GLTFLoader来加载和解析GLTF格式的模型。
-
STL (Stereolithography):
STL 是一种常用的三维打印文件格式,用于存储模型的几何信息。STL 文件通常包含三角形面片的列表,用于定义模型的外观。可以通过STLLoader来加载和解析STL格式的模型。
-
GLB:
GLB是GL Transmission Format(gltf)的二进制版本,GLB格式将模型的几何信息、材质、骨骼动画、节点层次结构等存储在单个二进制文件中,通常具有较小的文件大小和更高的加载性能.
本案列采用glb格式来加载外部模型。
因为案列中使用glb模型数据采用了Draco来进行压缩,所以我们需要引入DRACOLoader来解析我们的模型
(1)引入DRACOLoader加载模型
js
/**
* (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上面下载
(3)加载glb模型数据
当你已经创建了`const loader = new GLTFLoader();这个类实例后,我们就可以加载模型了
js
/**
* (8)加载glb模型
*/
loader.load('./models/gltf/ferrari.glb', function (gltf) {
//获取到模型的数据
const carModel = gltf.scene.children[0];
//将模型添加到3D场景中
scene.add(carModel);
});
render()
加载的效果如下:
模型已经加载成功了,但是你会发现他在整个背景中是黑色的。当然模型本身是有材质贴图的,车身默认是红色的。
之所以产生这个效果那是因为我们现在缺少一个非常重要的元素,那就是光照。
你试想一下,一个物体在没有任何光源的情况下,呈现出来的就是黑色的效果。如果你的场景背景也是黑色,那根本看不到效果。
(4)加载光影效果
我们设置光源的时候主要有两个部分
- 环境光:相当于天空的颜色,物体表面可以反射出对应的颜色。
- 点光源:相当于开启手电筒,照射到模型表面反射出来的颜色。
设置环境光
js
/**
* (9)添加光影效果
*/
//创建环境光
var ambient = new THREE.AmbientLight("blue");
scene.add(ambient);
环境光的颜色为blue,效果如下:
环境光为blue的情况下,模型表面反射出来的颜色就是蓝色,一般金属材质和玻璃材质反射的效果更佳明显。所以轮毂和车辆挡风玻璃效果会更强烈一些。
设置点光源
js
/**
* (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);
效果如下:
此刻我们基本上完成了模型的渲染,环境光蓝色默认替换为黑色,这样车辆立体感会更强一些
js
//环境光
var ambient = new THREE.AmbientLight("#000");
效果如下:
(5)加载hdr文件设置环境渲染
HDR(High Dynamic Range)文件是一种存储图像高动态范围信息的文件格式。
HDR可以理解成一张真实世界的图片或者设计者想要的灯光效果。
他的作用主要如下:
- HDR文件经常被用作环境贴图,用于模拟反射和光照环境。环境贴图是将场景的背景、反射和光照信息包装成一个纹理,然后将其应用到物体表面上。通过使用HDR文件作为环境贴图,可以更真实地模拟光线在场景中的反射和折射,增强渲染效果。
- HDR文件还可以用于模拟全局照明效果。全局照明是一种渲染技术,它考虑了场景中所有光源的组合对物体的影响,以获得更真实的照明效果。通过使用HDR文件提供的高动态范围和丰富的光照信息,可以在Three.js中实现更逼真的全局照明效果
也就说在本案列中如果我们想要获取更加真实的照明效果,我们可以使用设计师导出的hdr文件。将这个文件作为3D场景(Scene)的环境贴图
js
/**
* (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)添加光影效果
中我们自己的光影效果
js
/**
* (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);
这样渲染下来我们物体在场景中显示的会更加自然
不管你用hdr文件来作为环境贴图,还是采用光源设置来设计,我们都可以让模型在3D场景中更方便的显示出来。
四、汽车材质贴图
目前我们已经将模型渲染出来了,但是你会发现不管是车身、轮毂、还是玻璃材质跟我们想要的真实车辆材质是有区别的。比如你希望玻璃透明的、反光的。车身的漆面是可以反光的。模型在设计的时候使用默认材质。我们想要进行材质的替换。
(1)在步骤8中继续优化代码
js
/**
* (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);
});
材质创建了过后,接下来我们就可以将材质加载了到模型中了。
js
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)
将我们自己创建的材质拿去替换默认材质实现加载渲染。
效果如下:
替换过后的模型,更有金属质感和玻璃质感。材质对应的颜色你们都可以自己进行替换。
(2)给车底盘添加阴影效果
车底盘是没有阴影效果的,我们可以使用图片来进行模型贴图,让底盘有阴影效果会更加立体。
贴图的图片为png,图片由设计师出的
效果如下:
创建一个材质对象,并使用这张图片作为贴图
js
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);
});
效果如下:
通过效果图能看出,车辆底部是有阴影效果的,让整个3D效果渲染更加立体。
五、设置动画效果
(1)获取轮毂的材质对象
轮毂和网格地板我们都要动画加载
网格需要进行平移,按照z的反方向进行移动。
轮毂需要按照x轴的方向进行旋转
代码如下:
js
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函数中进行动画控制
js
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的事件。
三个按钮,我们都绑定点击事件,并获取对应的颜色
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材质对象的颜色,页面刷新的时候就可以应用成功。
课程小结
threejs是WebGL的框架,可以快速帮助我们在项目开发过程中进行3D渲染
对于外部3D模型的加载,需要我们针对不同模型进行不同的解析,这个需要和建模师沟通协调好