一小时,带你入门 ThreeJS,实现一个 3D 展车的功能

开发 前端
咱们今天就花上 一个多小时 的时间,借助 ThreeJS的一个官方示例,来看下,对于我们前端开发者而言,如何快速的入门,并完成一个酷炫的 ThreeJS 项目。

Hello,大家好,我是 Sunday。

最近有很多同学特别关注 WebGL 和 ThreeJS 的知识,通过 ThreeJS 我们可以完成很多酷炫的 3D 效果。

所以,咱们今天就花上 一个多小时 的时间,借助 ThreeJS的一个官方示例,来看下,对于我们前端开发者而言,如何快速的入门,并完成一个酷炫的 ThreeJS 项目。

该项目可以直接通过 https://webgl.web.lgdsunday.club/ 进行访问:

图片图片

那么废话不多说,让我们开始吧~

1.canvas、OpenGL、WebGL 到底都是干嘛的

我们知道 canvas 本质上是一个 html 标签,它额外提供了一些 API 用来进行绘制。

canvas 的 API 主要用来绘制 2D 图形,但是在日常的开发中,除了 2D 图形之外,还存在一些 3D 的图形,那么想要绘制 3D 图形就需要使用到 WebGL。

WebGL 是一个  JavaScript API,可在任何兼容的 Web 浏览器中渲染高性能的交互式 3D 和 2D 图形,而无需使用插件。

注意: 有些浏览器是可能不支持 WebGL 的。我们可以通过这个 网站 来检测你的浏览器是否支持  WebGL 。

如果你使用的是  Chrome ,但是依然提示不支持,那么可以在 Chrome 中开启 硬件加速 以获得支持。

WebGL 中提供了很多的 API 方法,这些方法和 OpenGL 提供的方法是非常相似的。

OpenGL (Open Graphics Library) 是一套用来渲染 2D 和 3D 矢量图形的跨语言的、跨平台的应用程序接口 (API) . 这种接口通常用来与图形处理单元 (GPU) 交互,来达到 硬件加速渲染 的目的。

WebGL 正是利用了 浏览器中的图形加速硬件(如GPU)来进行图形渲染(这也是为什么我们必须要开启 浏览器硬件加速 的原因)。它通过JavaScript 与 HTML5的Canvas元素进行交互,并提供了在Web上展示复杂的3D图形和交互式体验的能力。

本章的内容,我们总结来说:

Canvas 提供了一个基本的2D渲染环境,OpenGL 是一个跨平台的图形编程接口规范,而 WebGL 是基于OpenGL ES的JavaScript API,用于在Web浏览器中进行高性能的3D图形渲染。WebGL可以看作是在Canvas上使用OpenGL ES进行3D渲染的一种实现方式。

2.WebGL 框架 three.js 初体验-理论篇

在上一小节中,我们初步了解了 canvas、OpenGL 和 WebGL 之间的关系,我们知道 WebGL 本质上是基于 OpenGL API 的一个 3D 图形渲染的实现方式。

但是,如果我们直接基于 WebGL 原生 API 进行项目开发的话,那么使用起来会比较复杂。所以在日常的企业开发中,我们都会 WebGL 框架来进行开发。

基于 WebGL 的框架其实非常多,这些框架在视频中,我们不一个一个介绍,大家感兴趣的话,可以看一下课程讲义:

  • Three.js: Three.js 是一个强大的 JavaScript 3D 渲染库,它封装了底层的 WebGL API,提供了简化的接口和功能,使得在 Web 上创建和展示 3D 场景变得更加容易。
  • Babylon.js : Babylon.js 是另一个功能强大的 WebGL 渲染引擎,它专注于游戏开发和实时图形应用。它提供了广泛的功能,包括高性能渲染、物理引擎、动画系统和粒子系统等。
  • A-Frame: A-Frame 是一个基于 Three.js 的 WebVR 框架,它使开发者能够轻松创建虚拟现实(VR)和增强现实(AR)的交互体验。A-Frame 使用 HTML 和实体-组件模式来定义场景和对象。
  • PlayCanvas: PlayCanvas 是一个面向游戏开发的 WebGL 引擎,它提供了可视化的场景编辑器和强大的脚本编辑器,使开发者能够创建高性能的 HTML5 游戏。

而我们接下来,主要要说的其实就是 Three.js。

threejs 是目前国内使用率最多的 JavaScript 3D 渲染库,它提供了 非常完善的文档 来帮助我们完成 3D 渲染的开发。

那么接下来,咱们就创建一个项目,然后在这个项目中为大家演示 threejs 的导入和最基础的用法。

  • 利用 vue-cli 创建项目:
vue create threejs-car
  • 配置项如下:
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, CSS Pre-processors
? Choose a version of Vue.js that you want to start the project with 3.x
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with dart-sass)
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
  • 安装 threejs 到项目:
cnpm install --save three@0.152.2

那么接下来,咱们就在 App.vue 中利用 threejs 绘制出一个基本的正方体。

图片图片

整个正方体的绘制,咱们将分成两个小节来说明:理论篇 和 实战篇。

这一小节,咱们先说理论。让大家对 threejs 中的一些基础概念有一个大概的认知。

首先,先导入  threejs:

// 导入整个 three.js核心库
import * as THREE from 'three'

接下来,我们先了解一些 threejs 中的基本概念:

  • 场景 Scene:它表示要绘制的内容,是整个 threejs 绘制的基础
  • 相机 camera:相机分为很多种,咱们这里主要使用 PerspectiveCamera 透视相机。它是一个最常用的相机模式,主要可以模拟人眼看到的效果
  • 渲染器 renderer:它的作用比较简单,主要用来渲染场景
  • 几何体 Geometry:要渲染的物体形状。物体形状有非常多,咱们这里主要使用立方体 BoxGeometry
  • 材质 material:为物体赋予一些材质,比如是光滑的镜面,还是粗糙的表面。咱们这里主要使用的是基础材质 MeshBasicMaterial
  • 网格基类 mesh:将几何体与材质合并成基类。最后可以加入到场景中
  • 渲染函数 renderer.render :利用渲染器的渲染函数,可以根据场景和摄像头进行渲染
  • 重绘函数 requestAnimationFrame:它与渲染函数配合,可以重复进行渲染,即:生成动画

最后汇总成一句话:

threejs 本质上是:利用 renderer.render 渲染函数,渲染指定的 scene 和 camera。scene中可以放置任何的几何体与材质。当渲染函数与 requestAnimationFrame 配合时,可以产生动画。

3.WebGL 框架 three.js 初体验-实战篇

在上一小节中,我们明确了 threejs 中的一些基本概念,那么接下来咱们就根据所学的概念,来完成一个基础的正方体渲染。

整个正方体渲染分为三大步进行:

  • 创建场景、相机、渲染器,并把生成的 canvas 添加到 body 中
  • 创建立方体,并把它加入到场景中
  • 渲染场景

以下是具体代码:

<template>
 <div></div>
</template>

<script setup>
// 导入整个 three.js核心库
import * as THREE from 'three'

// 第一大步:创建场景、相机、渲染器,并把生成的 canvas 添加到 body 中
// 1. 场景:渲染 threejs 内容的地方
const scene = new THREE.Scene()
// 2. 相机:用来模拟人眼所看到的的场景
/**
 * 参数:
 * 1. 视野垂直角度 fov — 能够看到的场景范围
 * 2. 长宽比 aspect ratio - 当前摄像机的长宽比
 * 3. 近截面 near - 比近截面近的,用户看不到
 * 4. 远截面 far - 比远截面远的,用户看不到
 */
const camera = new THREE.PerspectiveCamera(
 75,
 window.innerWidth / window.innerHeight,
 0.1,
 1000
)
// 为 camera 设置摄像纵深
camera.position.z = 2
// 3. 渲染器:用来渲染 3D 场景
const renderer = new THREE.WebGLRenderer()

// 4. webgl 的渲染会输出的 canvas 中,这里用来设置 canvas 的大小
renderer.setSize(window.innerWidth, window.innerHeight)
// 5. 将生成的 canvas 标签插入到 body 中
document.body.appendChild(renderer.domElement)

// 第二大步:创建立方体,并把它加入到场景中

// 1. BoxGeometry是四边形的原始几何类,三个参数分别为:
/**
 * width — X轴上面的宽度,默认值为1。
 * height — Y轴上面的高度,默认值为1。
 * depth — Z轴上面的深度,默认值为1。
 */
const geometry = new THREE.BoxGeometry(1, 1, 1)
// 2. material 一个具备简单着色的基础材质。表示为立方体的材质
const material = new THREE.MeshBasicMaterial({ color: 0x3f7b9d })
// 3. Mesh 一个网格类,将材质与四边形合并
const cube = new THREE.Mesh(geometry, material)
// 4. 将当前生成的立方体加入到场景中
scene.add(cube)

// 第三大步:渲染场景

// 1. 创建一个方法,通过 requestAnimationFrame 重复回调这个方法
function animate() {
 // 2. requestAnimationFrame:是 JS 原生 API,它会根据屏幕刷新率来回调指定方法
 requestAnimationFrame(animate)

 // 3. 不断修改 cube 的角度,产生旋转的效果
 cube.rotation.x += 0.01
 cube.rotation.y += 0.01

 // 4. 利用渲染器的 render 方法重复渲染场景
 renderer.render(scene, camera)
}
animate()
</script>

4.threejs 配置解码器载入 glb 格式 3D 模型

上一小节,咱们构建了一个基本的正方体,借助这个小案例咱们也大致了解了一些 threejs 的基本用法。

那么这一小节,咱们就导入一个 3D 模型,利用 threejs 进行渲染。同时构建出最终的 html 结构。

首先,咱们需要先导入一些资料(可以从源码中获取):

然后 复制 上一小节的 App.vue,重命名为 01:基础几何体渲染.vue 。然后在原有的 App.vue 中对代码进行重构:

首先是构建出对应的 html 结构:

<template>
 <div>
  <div id="info">
   <h2>
    请确保你的浏览器支持 WebGL,可以点击
    <a href="https://get.webgl.org/" target="_blank">这里</a>
    进行检测。如果不支持,可百度处理方案。
   </h2>
   <br /><br />
   <span class="colorPicker">
    <input id="body-color" type="color" value="#ff0000" />
    <br />
    车身
   </span>
   <span class="colorPicker">
    <input id="details-color" type="color" value="#ffffff" />
    <br />
    轮毂
   </span>
   <span class="colorPicker">
    <input id="glass-color" type="color" value="#ffffff" />
    <br />
    玻璃
   </span>
  </div>

  <div id="container"></div>
 </div>
</template>

<style lang="scss">
body {
 color: #bbbbbb;
 background: #333333;
 text-align: center;
 overflow: hidden;
}

a {
 color: #08f;
}

.colorPicker {
 display: inline-block;
 margin: 0 10px;
}
</style>

如果想要导入 glb 格式 的 3D 模型,那么我们需要借助两个东西:

  • 新的 webpack-loader:因为 webpack 默认只能处理 js 文件,所以想要处理 glb 文件的话,那么需要安装 file-loader,并进行配置:
cnpm i --save-dev file-loader@6.2.0

在 vue.config.js 中配置对应的 loader:

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
chainWebpack: (config) => {
    config.module
        .rule('glb')
        .test(/\.(glb|gltf|hdr)$/)
        .use('file-loader')
        .loader('file-loader')
        .options({
            name: 'assets/[name].[hash:8].[ext]'
        })
        .end()
}
})
  • 使用新的 threejs API ,包括 DRACOLoader  和 GLTFLoader 。

DRACOLoader:它是一个使用 Draco 库压缩的几何加载器 ,咱们需要借助它的 setDecoderPath 方法来指定 DRACO 解码器的路径

GLTFLoader:该 loader 是加载 gltf 文件的 loader。glTF(gl传输格式)是一种开放格式的规范 ,可以高效的加载 3D 内容。咱们资源中的 .glb 文件就属于 3D 内容 的一部分。

那么明确好了这两块内容之后,接下来咱们完成对应得代码:

<script setup>
// 导入Vue的onMounted钩子函数
import { onMounted } from 'vue'
// 导入Three.js库
import * as THREE from 'three'
// 导入GLTF模型加载器
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
// 导入DRACO解码器加载器
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'

// 定义相机、场景、渲染器对象
let camera, scene, renderer

function init() {
 // 获取容器元素
 const container = document.getElementById('container')

 // 创建场景对象
 scene = new THREE.Scene()
 // 设置场景的背景颜色为黑色
 scene.background = new THREE.Color(0x333333)

 // 创建透视相机对象
 camera = new THREE.PerspectiveCamera(
  40,
  window.innerWidth / window.innerHeight,
  0.1,
  100
 )
 // 为 camera 设置摄像纵深
 camera.position.set(1, 1, 10)

 // 创建WebGL渲染器对象,启用抗锯齿(更平滑)
 renderer = new THREE.WebGLRenderer({ antialias: true })
 // 设置渲染器的大小
 renderer.setSize(window.innerWidth, window.innerHeight)
 // 设置渲染循环函数(代替 requestAnimationFrame 的替代函数)
 renderer.setAnimationLoop(render)
 // 将渲染器的画布元素添加到容器中
 container.appendChild(renderer.domElement)

 // 创建DRACO解码器加载器对象
 const dracoLoader = new DRACOLoader()

 // 设置DRACO解码器的路径
 dracoLoader.setDecoderPath('decoder/')

 // 创建GLTF模型加载器对象
 const loader = new GLTFLoader()
 // 设置模型加载器的DRACO解码器
 loader.setDRACOLoader(dracoLoader)

 // 加载 3D 文件
 loader.load(require('@/assets/ferrari.glb').default, function (gltf) {
  // 获取加载的模型对象
  const carModel = gltf.scene.children[0]

  // 将车辆模型添加到场景中
  scene.add(carModel)
 })
}

/**
 * 渲染
 */
function render() {
  // 更新控制器
  controls.update()
  
 // 渲染场景
 renderer.render(scene, camera)
}

// 在挂载时调用初始化函数
onMounted(() => {
 init()
})
</script>

此时运行项目,我们可以看到一个黑色的车子轮廓。

图片图片

5.添加控制器,让模型支持 360°

上一小节,咱们已经导入了一个基本的 3D 模型,但是这个模型本身还比较 “抽象”。所以我们这一小节,希望可以调整车辆的角度,让模型支持 360° 旋转。

那么想要支持这样的功能,咱们需要借助另外一个 threejs API 叫做 OrbitControls 轨道控制器

具体代码如下:

// 导入轨道控制器OrbitControls
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

// 定义控制器对象
let controls

function init () {
  .....
  // 创建轨道控制器对象
 controls = new OrbitControls(camera, container)
 // 启用阻尼效果
 controls.enableDamping = true
 // 设置控制器能够缩放的最大距离
 controls.maxDistance = 9
 // 设置控制器的焦点位置
 controls.target.set(0, 0.5, 0)
 // 更新控制器
 controls.update()
  .....
}

6.添加材质,并基于 RGBELoader 为 3D 模型增加环境贴图

现在咱们的模型已经可以进行 360° 旋转了,但是它现在还比较难看,所以接下来,咱们就需要给它添加一些 “颜色”,让他变得更加漂亮。

那么想要达到这个目的,需要分成两步去做:

  • 通过 RGBELoader 添加环境贴图
  • 通过 Material 渲染物理材质

那么首先,咱们先做第一步: 通过 RGBELoader 添加环境贴图

RGBELoader 允许你从文件中加载HDR图像,并将其用作Three.js场景中的环境贴图。这对于创建逼真的照明效果非常有用,因为HDR图像能够捕捉到真实世界中的光照细节和环境反射。

// 导入HDR贴图加载器
  import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'

 // 加载HDR贴图作为场景的环境贴图
 scene.environment = new RGBELoader().load(
  require('@/assets/venice_sunset_1k.hdr').default
 )
 // 设置环境贴图的映射方式为球形映射
 scene.environment.mapping = THREE.EquirectangularReflectionMapping

现在车身应该具备一个基础颜色。

接下来我们来处理第二步,通过 Material 渲染物理材质, 让他具备反光效果:

  • 创建对应材质:
// 材料设置
// 创建车身材质对象,设置颜色和物理属性
const bodyMaterial = new THREE.MeshPhysicalMaterial({
    color: 0xff0000,
    metalness: 1.0,
    roughness: 0.5,
    clearcoat: 1.0,
    clearcoatRoughness: 0.03,
    sheen: 0.5
})

// 创建细节材质对象,设置颜色和物理属性
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 中,设置对应材质:
// 加载 3D 文件
loader.load(require('@/assets/ferrari.glb').default, function (gltf) {
    // 获取加载的模型对象
    const carModel = gltf.scene.children[0]

    // 设置车身部分的材质为车身材质
    carModel.getObjectByName('body').material = bodyMaterial

    // rim_xx 参考 glb 文件配置
    // 设置前左轮辋的材质为细节材质
    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)
})

这样,咱们的车辆模型就具备了一个基本的反光效果,会非常漂亮。

7.根据 GridHelper 设置网格动态效果

现在咱们的车辆本身已经非常好看的,那么这一小节,咱们就构建一个地面网格效果,并且让网格具备一个动态后移的动效。

想要构建网格效果,那么我们需要通过 THREE.GridHelper 类构建:

// 定义网格帮助器对象
let grid

function init () {
  // 创建网格帮助器对象,设置网格的大小和颜色
 grid = new THREE.GridHelper(20, 40, 0xffffff, 0xffffff)
 // 设置网格材质的透明度
 grid.material.opacity = 0.2
 // 禁用网格材质的深度写入
 grid.material.depthWrite = false
 // 设置网格材质为透明材质
 grid.material.transparent = true
 // 将网格帮助器添加到场景中
 scene.add(grid)

}

那么此时,咱们将具备一个地面网格。

我们可以调整 camera 的角度,让车辆位置更好看一些:

// 设置相机位置
camera.position.set(4.25, 1.4, -4.5)

最后,我们可以在被 持续回调 的 render 函数中,动态修改 z轴 的值:

/**
 * 渲染
 */
function render() {
 // 获取当前时间:https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/now
 const time = -performance.now() / 1000
 // 设置网格的位置
 grid.position.z = -time % 1

 // 渲染场景
 ......
}

这样,咱们可以得到一个持续后撤的地面效果。

8.构建车轮模型组,让车轮转起来

现在地面可以持续后撤了,但是车轮还是固定死的,所以这一小节,咱们就要让车轮可以转动起来。

在项目中,每一个车轮都是一个单独的模型,我们可以通过:

carModel.getObjectByName('wheel_fl')

获取 前左 车轮。

然后可以在 render 函数中 通过 车轮.rotation.x = time * Math.PI * 2 让它转动。

所以,我们如果想要让四个车轮全部转动,那么就需要构建一个数组,然后循环数组达到转动的效果:

// 定义一个数组用于存储车轮模型对象
const wheels = []

在 load 中:

// 将车辆的四个车轮模型对象添加到数组中
wheels.push(
 carModel.getObjectByName('wheel_fl'),
 carModel.getObjectByName('wheel_fr'),
 carModel.getObjectByName('wheel_rl'),
 carModel.getObjectByName('wheel_rr')
)

在 render 函数中:

// 设置车轮的旋转角度
for (let i = 0; i < wheels.length; i++) {
 wheels[i].rotation.x = time * Math.PI * 2
}

那么此时,就模拟出了一个车辆移动的效果。

9.监听选择器的变化,修改车身配置

这一小节,咱们来完成 修改车身颜色 的功能。

所谓的修改车身颜色,其实本质上就是利用 Material.color.set(色值) 方法重新设置色值。

明确了这一点之后,下面的实现就会非常简单了:

// 获取车身颜色输入框元素
 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)
 })

那么此时,咱们就可以直接修改车身配置了。

10.创建阴影贴图,让场景更加真实

现在咱们的功能其实已经大致完成了。

最后还剩下一个小功能,就是车身下的阴影效果:

图片图片

有了这个效果之后,会让整体变得更加立体。

而想要构建出这个效果,那么需要使用 THREE.TextureLoader ,加载阴影图:

// 车辆设置
// 加载车辆阴影贴图
const shadow = new THREE.TextureLoader().load(
 require('@/assets/ferrari_ao.png')
)

然后利用 mesh 构建出 **平面几何 PlaneGeometry**,并把它添加到 carModel 中:

// 阴影配置
// 创建阴影平面模型
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)

那么此时,我们就具备了阴影效果。

责任编辑:武晓燕 来源: 程序员Sunday
相关推荐

2022-03-18 14:11:05

安全事件安全分析威胁

2015-04-02 11:17:20

2017-08-08 15:55:31

戴尔

2017-04-05 11:32:36

环保戴尔地球一小时

2018-09-17 12:42:34

2013-03-21 15:20:14

搜狗

2013-06-04 13:43:53

2020-08-29 18:51:14

效能工具效率生产力

2013-08-09 09:41:04

2022-06-17 11:35:10

物联网

2021-06-26 07:15:25

网络攻击容器漏洞

2013-08-27 11:13:52

亚马逊宕机

2018-03-08 16:22:39

FlutterAndroid代码

2010-07-09 11:14:43

2012-07-04 14:14:39

Linux服务器云计算

2015-08-25 14:58:19

数据

2019-07-25 14:58:40

机器人厨师消费者

2022-11-21 18:01:24

CSSthree.js

2016-11-29 12:07:45

大数据思维大数据

2018-03-19 10:30:17

程序员永久断网
点赞
收藏

51CTO技术栈公众号