前置知识(最下面有完整代码)
每个代码都有注释,零基础也能看懂中文官方文档教程
创建项目
创建空文件夹
执行如下命令初始化package.json
文件
js
npm init -y
安装threejs包
js
yarn add three
安装tween.js动画库,用于做动画tweenjs文档
js
yarn add @tweenjs/tween.js
安装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.js
在index.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()
}
}
})
})
效果图