Skip to content

前置知识(最下面有完整代码)

每个代码都有注释,零基础也能看懂中文官方文档教程

创建项目

创建空文件夹

执行如下命令初始化package.json文件

js
npm init -y

安装threejs包

js
yarn add three

安装tween.js动画库,用于做动画tweenjs文档

js
yarn add @tweenjs/tween.js

安装gui调试工具

lil-gui官方文档

js
yarn add lil-gui

使用parcel工具打包执行项目

官方网站

js
yarn global add parcel-bundler

根目录下创建src/index.html

这个链接的资料,点击下载到scr文件夹内部,它是个public文件夹,将其下载下来放到scr文件夹内部

初始化html结构

js
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
</html>

package.json里面配置启动命令(后面的是启动文件路径)

增加type:module

js
{
  "name": "demo",
  "version": "1.0.0",
  "description": "",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "dev": "parcel src/index.html",
    "build": "parcel build src/index.html"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "three": "^0.166.1"
  }
}

src下新建app.jsindex.html里面引入一下

js
<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <title>My First Parcel App</title>
  <style>
    * {
      padding: 0;
      margin: 0;
    }
  </style>
</head>

<body>
  <script src="./app.js"></script>
</body>

</html>

使用yarn run dev启动项目

双击启动后的网址,查看页面

在这里插入图片描述

能访问到网页就说明配置正常

案例一(设置几何体及其材质)

案例代码

后续所有操作都在app.js内部写

一切物体都身处一个场景内,即使3D页面也是如此

js
// 引入three.js
import * as THREE from 'three'
// 引入轨道控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// 引入材质纹理图片,用于设置正方体纹理
import pkq from './public/pkq1.jpg'
// 引入动画库插件
import TWEEN from '@tweenjs/tween.js'

// 全局变量
let scene, camera, renderer, controls, cube

// 初始化场景
function initScene () {
  // 创建场景
  scene = new THREE.Scene()
}

// 初始化相机
function initCamera () {
  // 参数一:夹角,参数二:宽高比,参数三:近裁剪面,参数四:远裁剪面
  camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000)
  // 设置相机位置(x,y,z)
  // camera.position.set(0, 0, 10);
  // 也可以
  // camera.position.z = 10
  // 设置相机方向(指向的场景)
  camera.lookAt(scene.position)
}

// 初始化渲染器
function initWebGL () {
  // 设置渲染器
  renderer = new THREE.WebGLRenderer()
  // 设置渲染器的尺寸
  renderer.setSize(window.innerWidth, window.innerHeight)
  // 将渲染器添加到body
  document.body.appendChild(renderer.domElement)
}

// 添加坐标系
function initAxis () {
  // 参数一:场景,参数二:坐标系长度
  let axis = new THREE.AxesHelper(3)
  // 将坐标系添加到场景中
  scene.add(axis)
}

// 添加轨道控制器(可以滑动屏幕和缩放屏幕)
function initTrackballControls () {
  // 参数一:相机,参数二:渲染器
  let trackballControls = new OrbitControls(camera, renderer.domElement)
  // 添加控制器
  scene.add(trackballControls)
}

// 创建立方体
function initCube () {
  // 创建几何体形状
  let geometry = new THREE.BoxGeometry(1, 1, 1)
  // 设置图片纹理
  let texture = new THREE.TextureLoader().load(pkq)
  // 创建材质颜色
  let material = new THREE.MeshBasicMaterial({
    color: 'yellow',
    map: texture
  })
  // 创建网格
  cube = new THREE.Mesh(geometry, material)
  // 将网格添加到场景中
  scene.add(cube)
}

// 主函数
function main () {
  initScene()
  initCamera()
  initWebGL()
  initAxis()
  initTrackballControls()
  initCube()


  // 加入动画库插件
  const coords = { x: 0 }
  const tween = new TWEEN.Tween(coords)
    .to({ x: 3 }, 1000) // 从起始x为0的位置到x为3的位置,动画持续时间为1000ms
    .easing(TWEEN.Easing.Quadratic.Out) // 运动模式
    .onUpdate((that) => {
      // 设置正方体的x坐标位置
      cube.position.x = that.x
    }).start()
}

// 启动
main()

// 渲染(动画)
function render (time) {
  // 将三维页面转换为二维渲染到页面上
  renderer.render(scene, camera)
  requestAnimationFrame(render)
  if (!time) return
  // 动画库插件调用
  TWEEN.update(time)
}

// 启动动画函数
render()

// 自适应视口大小
window.addEventListener('resize', function () {
  // 设置相机的宽高比
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()

  // 设置渲染器的宽高比
  renderer.setSize(window.innerWidth, window.innerHeight)
})

案例二(3D汽车展厅效果编写)

配置glb模型解析

根目录下创建.parcelrc文件,内容如下:

js
{
  "extends": "@parcel/config-default",
  "transformers": {
    "*.{gltf,glb,png,jpg,obj,exr}": [
      "@parcel/transformer-raw"
    ]
  }
}

查看模型各模块的名称

点击进入官方的模型编辑器,推荐谷歌浏览器打开

点击左上角文件->导入->导入汽车模型glb文件

点击左上角添加->光源->平行光->可以显示模型完整的样子当我们需要知道模型的某个部位的名称,只需要点击一下相关部位,右侧就会在场景中定位到选中的部位并在下方显示出对应的信息,例如当前模块的名称,如下图所示:

在这里插入图片描述下面示例中需要用到模型名称,做如开关门动作等

案例代码

项目代码:

js
// 引入three.js
import * as THREE from 'three'
// 引入轨道控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// 引入模型加载器
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
// 引入模型
import Lamborghini from './public/Lamborghini.glb'
// 引入gui
import { GUI } from 'lil-gui'
// 引入动画库插件
import TWEEN from '@tweenjs/tween.js'
// 引入聚光灯所用的图片
import messi from './public/messi.JPG'

// 全局变量
let scene, camera, renderer, trackballControls

// 存储门的集合
let doors = []

// 车门状态
let carStatus

// 车身材质
let bodyMaterial = new THREE.MeshPhysicalMaterial({
  color: "#6e2121",
  metalness: 1, // 材质的金属度
  roughness: 0.5, // 材质的粗糙度
  clearcoat: 1.0, // 材质的清晰度
  clearcoatRoughness: 0.03, // 材质的粗糙度
})


// 玻璃材质
let glassMaterial = new THREE.MeshPhysicalMaterial({
  color: "#793e3e",
  metalness: 0.25, // 材质的金属度
  roughness: 0, // 材质的粗糙度
  transmission: 1.0 //透光性.transmission属性可以让一些很薄的透明表面,例如玻璃,变得更真实一些。
})

// 初始化场景
function initScene () {
  // 创建场景
  scene = new THREE.Scene()
}

// 初始化相机
function initCamera () {
  // 参数一:夹角,参数二:宽高比,参数三:近裁剪面,参数四:远裁剪面
  camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000)
  // 设置相机位置(x,y,z)
  camera.position.set(4.25, 1.4, -4.5)
  // 也可以
  // camera.position.z = 100
  // 设置相机方向(指向的场景)
  camera.lookAt(scene.position)
}

// 初始化渲染器
function initWebGL () {
  // 设置渲染器
  renderer = new THREE.WebGLRenderer()
  // 设置渲染器的尺寸
  renderer.setSize(window.innerWidth, window.innerHeight)
  // 设置渲染器支持阴影
  renderer.shadowMap.enabled = true
  // 将渲染器添加到body
  document.body.appendChild(renderer.domElement)
}

// 添加坐标系
function initAxis () {
  // 参数一:场景,参数二:坐标系长度
  let axis = new THREE.AxesHelper(3)
  // 将坐标系添加到场景中
  scene.add(axis)
}

// 添加轨道控制器(可以滑动屏幕和缩放屏幕)
// (需要在动画渲染函数中进行更新)
function initTrackballControls () {
  // 参数一:相机,参数二:渲染器
  trackballControls = new OrbitControls(camera, renderer.domElement)
  // 添加阻尼效果
  trackballControls.enableDamping = true
  // 添加阻尼惯性
  trackballControls.dampingFactor = 0.1

  // 设置最大最小缩放(防止用户缩放出场景外)
  trackballControls.minDistance = 1  // 最小缩放距离
  trackballControls.maxDistance = 9  // 最大缩放距离

  // 设置上下滑动的最大最小角度
  trackballControls.minPolarAngle = 0 // 最小角度
  trackballControls.maxPolarAngle = 80 / 360 * 2 * Math.PI // 最大角度

  // 设置水平滑动的最大最小角度为0-360度
  trackballControls.minAzimuthAngle = -Infinity
  trackballControls.maxAzimuthAngle = Infinity
}

// 加载小汽车模型
function initCar () {
  new GLTFLoader().load(Lamborghini, function (gltf) {
    // 获取模型场景
    const object = gltf.scene
    // 让小车绕y轴旋转180度
    object.rotation.y = Math.PI

    // 获取小车信息
    object.traverse(obj => {
      // 让车模型产生阴影效果
      obj.castShadow = true
      // 获取小车材质
      if (obj.name === 'Object_103' || obj.name == 'Object_64' || obj.name == 'Object_77') {
        // 定义车身材质
        obj.material = bodyMaterial
      } else if (obj.name === 'Object_90') {
        // 定义玻璃材质
        obj.material = glassMaterial
      } else if (obj.name === 'Empty001_16' || obj.name === 'Empty002_20') {
        // 门
        doors.push(obj)
      } else { }
    })

    // 将小汽车模型添加到场景中
    scene.add(object)
  })
}

// 做一个地板
function initFloor () {
  // 参数为地板的长宽
  const foolGeometry = new THREE.PlaneGeometry(100, 100)
  const floorMaterial = new THREE.MeshStandardMaterial({
    // 设置双面显示
    side: THREE.DoubleSide,
    // 设置地板颜色
    color: 0x808080,
    // 设置金属材质
    metalness: 0, // 金属度(0-1 0 完全不金属 1完全金属)
    roughness: 0.1 // 粗糙度(0-1 0 完全光滑 1完全粗糙)
  })
  const mesh = new THREE.Mesh(foolGeometry, floorMaterial)
  // 沿着y轴旋转180度
  mesh.rotation.x = -Math.PI / 2
  // 设置地板可以接收阴影
  mesh.receiveShadow = true
  // 添加到场景
  scene.add(mesh)
}
// 添加环境光让小车亮起来
function initLight () {
  const light = new THREE.AmbientLight(0xffffff, 0.5)
  scene.add(light)
}

// 添加聚光灯
function initSpotLight () {
  // 参数一:灯光颜色,参数二:灯光强度
  const spotLight = new THREE.SpotLight(0xffffff, 100)
  // 散射角度,跟水平线夹角
  spotLight.angle = Math.PI / 8
  // 半影衰减百分比
  spotLight.penumbra = 0.2
  // 沿光照距离衰减量
  spotLight.decay = 2
  // 聚光灯距离
  spotLight.distance = 30
  // 灯光阴影半径
  spotLight.shadow.radius = 10
  // 阴影映射宽度,阴影映射高度
  spotLight.shadow.mapSize.set(4096, 4096)
  // 设置聚光灯位置
  spotLight.position.set(-5, 10, 1)
  // 光照射的方向
  spotLight.target.position.set(0, 0, 0)
  // 灯光是否开启阴影
  spotLight.castShadow = true
  // 将聚光灯添加到场景中
  scene.add(spotLight)
}

// 添加圆柱体包裹模拟展厅效果
function initCylinder () {
  // 参数一:底面半径,参数二:顶面半径,参数三:高度,参数四:圆周分割数,参数五:顶面分割数
  const cylinderGeometry = new THREE.CylinderGeometry(10, 10, 20, 50, 50)
  // 材质
  const cylinderMaterial = new THREE.MeshStandardMaterial({
    // 设置双面显示
    side: THREE.DoubleSide,
    color: 'gary'
  })
  // 创建网格模型
  const mesh = new THREE.Mesh(cylinderGeometry, cylinderMaterial)
  // 将网格模型添加到场景中
  scene.add(mesh)
}

// 开关门动画
function setAnimationDoor (start, end, mesh) {
  // 在最下面的render函数内需要调用动画库插件,否则不生效
  const tween = new TWEEN.Tween(start).to(end, 1000).easing(TWEEN.Easing.Quadratic.Out)

  tween.onUpdate((that) => {
    // 修改车门位置
    mesh.rotation.x = that.x
  })
  tween.start()
}

// 车内车外效果转换
function setAnimationCamera (start, end) {
  // 参数c开头的是相机起始位置,o开头的是轨道控制器起始位置
  const tween = new TWEEN.Tween(start).to(end, 3000).easing(TWEEN.Easing.Quadratic.Out)
  tween.onUpdate((that) => {
    //  camera.postition  和 controls.target 一起使用
    // 修改摄像机位置和轨道控制器位置
    camera.position.set(that.cx, that.cy, that.cz)
    trackballControls.target.set(that.ox, that.oy, that.oz)
  })
  tween.start()
}

// 打开车门
function carOpen () {
  // 控制开门
  carStatus = 'open'
  // 遍历车门
  for (let i = 0; i < doors.length; i++) {
    // 加载开门的动画(兰博基尼都是剪刀门,因此是往上抬的)
    setAnimationDoor({ x: 0 }, { x: Math.PI / 3 }, doors[i])
  }
}

// 关闭车门
function carClose () {
  // 关闭车门
  carStatus = 'close'
  for (let i = 0; i < doors.length; i++) {
    setAnimationDoor({ x: Math.PI / 3 }, { x: 0 }, doors[i])
  }
}

// 初始化GUI调试工具
function initGUI () {
  let obj = {
    bodyColor: bodyMaterial.color.getHex(), // 车身颜色
    glassColor: glassMaterial.color.getHex(), // 玻璃颜色
    carOpen,
    carClose,
    carIn: function () {
      // 车内视角
      setAnimationCamera({ cx: 4.25, cy: 1.4, cz: -4.5, ox: 0, oy: 0.5, oz: 0 }, { cx: -0.27, cy: 0.83, cz: 0.60, ox: 0, oy: 0.5, oz: -3 })
    },
    carOut: function () {
      // 退出车内
      setAnimationCamera({ cx: -0.27, cy: 0.83, cz: 0.6, ox: 0, oy: 0.5, oz: -3 }, { cx: 4.25, cy: 1.4, cz: -4.5, ox: 0, oy: 0.5, oz: 0 })
    }
  }
  // 创建一个GUI
  const gui = new GUI()
  // 创建一个文件夹
  const folder = gui.addFolder('小车参数')
  // 添加一个颜色选择
  // 使用obj对象的属性进行更改重新设置车身和玻璃颜色
  folder.addColor(obj, 'bodyColor').name('车身颜色方法一').onChange((value) => {
    bodyMaterial.color.set(value)
  })
  folder.addColor(obj, 'glassColor').name('玻璃颜色方法一').onChange((value) => {
    bodyMaterial.color.set(value)
  })

  // 因为材质本质也是一个对象,里面有color属性,可以相当于直接修改里面的color属性了
  folder.addColor(bodyMaterial, 'color').name('车身颜色方法二')
  folder.addColor(glassMaterial, 'color').name('玻璃颜色方法二')

  folder.add(obj, 'carOpen').name('打开车门')
  folder.add(obj, 'carClose').name('关闭车门')
  folder.add(obj, 'carIn').name('进入车内')
  folder.add(obj, 'carOut').name('退出车内')
}

// 创建聚光灯的函数
function createSpotlight(color) {
  // 创建聚光灯(颜色,强度)
  const newObj = new THREE.SpotLight(color, 20);
  // 聚光灯支持阴影
  newObj.castShadow = true;
  // 聚光灯的锥体角度
  newObj.angle = Math.PI / 6;
  // 聚光灯的边缘
  newObj.penumbra = 0.2;
  // 聚光灯的衰减
  newObj.decay = 2;
  // 聚光灯的距离
  newObj.distance = 50;
  return newObj;
}
// 加一个投影仪效果,里面投影一张图片
function initCreateSpotLight () {
  // 创建聚光灯
  const spotLight1 = createSpotlight('#ffffff');
  // 创建纹理
  const texture = new THREE.TextureLoader().load(messi)
  // 聚光灯的位置
  spotLight1.position.set(0, 3, 0);
  // 聚光灯的照射方向
  spotLight1.target.position.set(-10, 3, 10)
  // 聚光灯的投影
  spotLight1.map = texture
  // 聚光灯的辅助线
  new THREE.SpotLightHelper(spotLight1);
  // 添加到场景中
  scene.add(spotLight1);
}
// 主函数
function main () {
  // 初始化场景
  initScene()
  // 初始化相机
  initCamera()
  // 初始化渲染器
  initWebGL()
  // 初始化坐标轴
  initAxis()
  // 初始化控制器
  initTrackballControls()
  // 初始化小车
  initCar()
  // 初始化环境光
  initLight()
  // 初始化地板
  initFloor()
  // 初始化聚光灯
  initSpotLight()
  // 初始化圆柱体
  initCylinder()
  // 初始化GUI
  initGUI()
  // 投影仪效果
  initCreateSpotLight()
}

// 启动
main()

// 渲染(动画)
function render (time) {
  // 将三维页面转换为二维渲染到页面上
  renderer.render(scene, camera)
  requestAnimationFrame(render)
  // 更新轨道控制器
  trackballControls.update()

  // 动画库插件调用
  TWEEN.update(time)
}

// 启动动画函数
render()

// 自适应视口大小
window.addEventListener('resize', function () {
  // 设置相机的宽高比
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()

  // 设置渲染器的宽高比
  renderer.setSize(window.innerWidth, window.innerHeight)
})

// 监听鼠标点击事件,可以支持点击车门就打开车门等
window.addEventListener('click', function (event) {
  // 获取鼠标点击位置(下面转换是将canvas坐标系转换为世界坐标系的坐标,固定写法)
  let pointer = {}
  pointer.x = (event.clientX / window.innerWidth) * 2 - 1
  pointer.y = - (event.clientY / window.innerHeight) * 2 + 1

  // 创建一个向量
  var vector = new THREE.Vector2(pointer.x, pointer.y)
  // 创建光线投射,用于鼠标交互,可以捕获到穿过了什么物体
  var raycaster = new THREE.Raycaster()
  // 设置射线投射的起始位置和方向
  raycaster.setFromCamera(vector, camera)
  // 获取场景中所有物体
  let intersects = raycaster.intersectObjects(scene.children)

  // 遍历所有物体
  intersects.forEach((item) => {
    // 根据点击的物体名称判断,下面这个是两边车门的名称
    if (item.object.name === 'Object_64' || item.object.name === 'Object_77') {
      // 如果是打开车或者关闭车
      if (!carStatus || carStatus === 'close') {
        carOpen()
      } else {
        carClose()
      }
    }
  })
})

效果图

在这里插入图片描述