Эффект
随着前端越来越卷,对前端的要求也越来越高,从视觉效果上不断的迈向3d
一直想学习threejs,迫于没有时间,正好空闲下来一周(记录一下入门threejs)
✨实现一个3d换色车以及场景动画✨
Архитектура знаний
├── Tweenjs 动画插件
└── Threejs 3d引擎
└── Scence 场景
└── WebGLRenderer 渲染器
└── Camera 相机(这里用到的是透视相机PerspectiveCamera)
└── Fog 雾化效果
└── Lights 灯光效果
└── OrbitControls 控制器
└── animate 动画函数
└── CubeTextureLoader 环境贴图
└── Mesh 网格对象
└── PlaneGeometry 创建矩形
└── MeshLambertMaterial 材质
└── FBXLoader fbx模型
└── GLTFLoader gltf/glb模型
└── Raycaster 模型拆分选中事件
Tweenjs
import TWEEN from '@tweenjs/tween.js'
setTweens(obj, newObj, duration = 1500) {
var ro = new TWEEN.Tween(obj) //创建tween动画实例
ro.to(newObj, duration) //变化后的对象以及动画持续时间
ro.easing(TWEEN.Easing.Sinusoidal.InOut) //动画缓动函数
ro.onUpdate(function() {}) //执行回调
ro.start()
},
Threejs
Документация Threejs
<template>
<div>
<div id="cart"></div>
<div class="main">
车身
<div class="flex">
<img style="width: 100px;height: 50px" src="/glb/cart.png" alt="">
<el-color-picker v-model="mainColor"></el-color-picker>
</div>
</div>
<div class="tirm">
内饰
<div class="flex">
<img style="width: 100px;height: 50px" src="/glb/trim.png" alt="">
<el-color-picker v-model="trimColor"></el-color-picker>
</div>
</div>
<div class="into" @click="intoCart">
上车
<img style="width: 50px;height: 50px" src="/glb/into.png" alt="">
</div>
<div class="out" @click.stop="outCart">
下车
<img style="width: 50px;height: 50px" src="/glb/out.png" alt="">
</div>
<div v-show="loding" v-loading.fullscreen.lock="loding" class="mask"></div>
<div
v-show="loding"
style="z-index: 9999;position: absolute;top: 53%;left: 50%;width: 300px;transform:translate(-50%,0)"
>
<el-progress :text-inside="true" :stroke-width="26" :percentage="this.percentage"></el-progress>
</div>
</div>
</template>
import * as THREE from '@/plugins/ys3d/threeLibs/three.module'
data() {
return {
loding: true, // 加载
mainColor: '', // 颜色
trimColor: '', // 颜色
marker: '', // 车门标记
selectedObjects: null, // 点击选中的模块
renderer: null,
main: null, // 车身
trim: null, // 内饰
cart: null, // 车模型
wheel: null, // 车轮
door: null, // 第一个车门
scene: null,
camera: null,
mixer: null, // 车子模型动画
mixer1: null, // 房子模型动画
isOpen: false, // 是否打开车门
controlsObj: null, // 操控画面对象
plane: null, // 平面模型
percentage: 0// 加载进度
}
}
async init() {
//创建场景
this.scene = new THREE.Scene()
//摄像头
this.camera = new THREE.PerspectiveCamera(70, 1, 1, 1000)// 摄像头
this.renderer = new THREE.WebGLRenderer({
antialias: true
})
this.renderer.setSize(window.innerWidth, window.innerHeight)
document.getElementById('cart').append(this.renderer.domElement)
this.renderer.setClearColor('#000000')
//添加雾化
this.scene.fog = new THREE.Fog(0xffffff, 200, 1000)
//创建光源
this.lights(this.scene)
//创建平面
this.loadPlan(this.scene)
//加载模型
await this.loadFbx(this.scene, this.camera)
await this.loadGlb(this.scene, this.camera)
//创建环境贴图
await this.initSky(this.scene)
//创建控制器
this.controls(this.scene, this.camera, this.renderer)
this.renderer.render(this.scene, this.camera)
}
Сцена сцены
const scene = new THREE.Scene() 场景可以理解为创建一个画布盒子
Рендерер WebGLRenderer
const renderer = new THREE.WebGLRenderer({
antialias: true 是否抗锯齿
... 相关属性查看threejs文档
})
камера
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000) 相机
camera.position.set(30, 10, 30) 相机位置
camera.lookAt(camera.position) 指相机看向三维中的某个位置
Распыление тумана
scene.fog = new THREE.Fog(0xffffff, 200, 1000) 远处添加边境线效果
Огни
new THREE.SpotLight 聚光灯
new THREE.PointLightHelper 灯光辅助容器
const sportLight = new THREE.SpotLight(0xffffff) 添加上方光源
sportLight.position.set(0, 1200, 0) 光源位置
scene.add(sportLight)
const sportLight1 = new THREE.SpotLight(0xffffff) 添加侧后方光源
sportLight1.position.set(-30, 20, 10)
scene.add(sportLight1)
const sportLight2 = new THREE.SpotLight(0xffffff) 添加后方光源
sportLight2.position.set(0, 0, 300)
scene.add(sportLight2)
const sportLight4 = new THREE.SpotLight(0xffffff) 添加侧前方光源
sportLight4.position.set(30, 0, 0)
scene.add(sportLight4)
// scene.add(new THREE.PointLightHelper(sportLight1, 1)) 添加灯光辅助容器(便于查看灯光位置)
Контроллер OrbitControls
new OrbitControls 创建控制器
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
// 动态阻尼系数 就是鼠标拖拽旋转灵敏度
// controls.dampingFactor = 0.5
// 是否可以缩放
controls.enableZoom = true
// 是否自动旋转
controls.autoRotate = true
controls.autoRotateSpeed = 0.3
// 设置相机距离原点的最远距离
controls.minDistance = 1
// 设置相机距离原点的最远距离
controls.maxDistance = 1200
// 是否开启右键拖拽
controls.enablePan = true
функция анимированной анимации
new THREE.Clock 时钟对象
clock.getDelta 当前的秒数
const that = this
const clock = new THREE.Clock() 时钟对象
function animate() {
// controls.update() 控制器更新
TWEEN.update() 动画更新
// 平面移动(路面行驶效果)
if (that.plane.position.z < -600) {
that.plane.position.set(0, that.plane.position.y, 0)
} else {
that.plane.position.set(that.plane.position.x, that.plane.position.y, that.plane.position.z - 2)
}
// 车门标记点opacity动画
if (that.marker.material.opacity >= 1) {
that.marker.material.opacity = 0.1
} else {
that.marker.material.opacity += 0.01
}
// 车轮转动
that.wheel.map(cell => {
cell.rotation.set(cell.rotation.x + 1000, -0.5 * Math.PI, cell.rotation.z)
})
renderer.render(scene, camera) //重新渲染
requestAnimationFrame(animate) //定时函数
const delta = clock.getDelta() //当前的秒数
if (that.mixer) {
that.mixer.update(delta)
}
if (that.mixer1) {
that.mixer1.update(delta)
}
if (that.mixer2) {
that.mixer2.update(delta)
}
}
animate()
Карта окружения CubeTextureLoader (скайбокс)
new THREE.CubeTextureLoader 创建天空盒子容器
initSky(scene) {
return new Promise((resolve, reject) => {
//加载正方体六个面的图片(p n正反面 xyz三维坐标轴)
scene.background = new THREE.CubeTextureLoader().setPath('glb/').load(
[
'nx.jpg',
'nz.jpg',
'py.jpg',
'DN.jpg',
'px.jpg',
'pz.jpg'
]
)
resolve()
})
}
Сетчатый объект сетки
THREE.ImageUtils.loadTexture 加载图片
new THREE.Mesh 创建网格对象
new THREE.PlaneGeometry 创建矩形
new THREE.MeshLambertMaterial 创建材质
loadPlan(scene) {
var texture = THREE.ImageUtils.loadTexture(
'/glb/grasslight-big.jpg',
null, function(t) {}
)
var texture1 = THREE.ImageUtils.loadTexture(
'/glb/water.jpg',
null, function(t) {}
)
this.plane = new THREE.Mesh(// 创建平面
new THREE.PlaneGeometry(300, 2000),
new THREE.MeshLambertMaterial({
map: texture,
side: THREE.DoubleSide,//双面材质
opacity: 0.5, // 透明度
transparent: true //是否透明
})
)
const plane = new THREE.Mesh(// 创建水面
new THREE.PlaneGeometry(2500, 2500),
new THREE.MeshLambertMaterial({
map: texture1,
side: THREE.DoubleSide
})
)
this.plane.rotation.x = -0.5 * Math.PI
this.plane.rotation.z = -1 * Math.PI
this.plane.position.set(0, -2.5, 0)
plane.rotation.x = -0.5 * Math.PI
plane.rotation.z = -1 * Math.PI
plane.position.set(0, -14, 0)
scene.add(this.plane)
scene.add(plane)
}
FBXLoader
Модель автомобиля
new FBXLoader fbx模型加载器
//小车模型
loadFbx(scene, camera) {
const that = this
const loader = new FBXLoader()
return new Promise(resolve => {
loader.load('/glb/MBSL.fbx', function(obj) {
obj.name = 'cart'//对象名称
that.cart = obj //小车对象
obj.scale.set(0.004, 0.008, 0.004)//大小
obj.position.set(0, 0, -4)//位置
obj.rotation.set(0, -41, 0)
that.wheel = obj.children[1].children[5].children//车轮模型组
that.wheel.map(cell => {//设置车轮位置
cell.rotation.set(cell.rotation.x, -0.5 * Math.PI, cell.rotation.z)
})
that.door = obj.children[1].children[3].children[0].children //车门模型组
//车身模型
that.main = [obj.children[1].children[0], ...obj.children[1].children[1].children, ...obj.children[1].children[3].children]
//内饰模型
that.trim = [...obj.children[1].children[2].children, ...obj.children[1].children[4].children, ...obj.children[1].children[6].children]
scene.add(this.sign())
that.domClick(camera, obj, that)//模型选中事件
})
resolve()
})
}
дверной маркер
THREE.ImageUtils.loadTexture 图片加载器
new THREE.Mesh 网格对象
new THREE.PlaneGeometry 矩形对象
new THREE.MeshLambertMaterial 材质
new THREE.Group 组合对象
sign(obj){
var texture = THREE.ImageUtils.loadTexture( //车门标记点
'/glb/marker3.png',
null, function(t) {
}
)
that.marker = new THREE.Mesh(// 创建网格对象
new THREE.PlaneGeometry(1, 1),//标记点
new THREE.MeshLambertMaterial({
map: texture,//标记点贴图
aoMapIntensity: 0,
side: THREE.DoubleSide,
opacity: 0.1,
transparent: true,
depthTest: false
})
)
that.marker.position.set(2.5, 3, 3)
that.marker.rotation.y = 0.5 * Math.PI
var group = new THREE.Group()//组合对象
group.add(obj)
group.add(marker)
return group
}
GLTFLoader
new GLTFLoader 加载gltf/glb模型
new DRACOLoader 对模型进行压缩
new THREE.AnimationMixer 提取执行模型动画
loadGlb(scene, camera) {
const loaderModule = new GLTFLoader()
const that = this
return new Promise(resolve => {
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('glb/draco/') //设置DRACOLoader文件路径public
loaderModule.setDRACOLoader(dracoLoader)
loaderModule.load('/glb/LittlestTokyo.glb', function(gltf) {
//房子模型
const object = gltf.scene
object.position.set(-300, 62, -500)
object.rotation.set(0, 0.5 * Math.PI, 0)
object.traverse(function(node) {
if (node.isMesh) node.castShadow = true
})
// get the animation
that.mixer1 = new THREE.AnimationMixer(object)
that.mixer1.clipAction(gltf.animations[0]).play()
object.scale.set(0.3, 0.3, 0.3)
// add object to scene
scene.add(object)
resolve()
}, this.onProgress)
loaderModule.load('/glb/Parrot.glb', function(gltf) {
//小鸟模型
const object = gltf.scene
object.position.set(-150, 60, -100)
object.traverse(function(node) {
if (node.isMesh) node.castShadow = true
})
// get the animation
that.mixer2 = new THREE.AnimationMixer(object)
that.mixer2.clipAction(gltf.animations[0]).play()
object.scale.set(0.1, 0.1, 0.1)
// add object to scene
scene.add(object)
})
})
}
Raycaster
new THREE.Raycaster 光线投射用于计算出鼠标经过哪些模型
//模型选中事件
domClick(camera, fbx, that) {
const raycaster = new THREE.Raycaster()
const mouse = new THREE.Vector2()
document.addEventListener('click', function(ev) {
mouse.x = (ev.clientX / window.innerWidth) * 2 - 1
mouse.y = -(ev.clientY / window.innerHeight) * 2 + 1
raycaster.setFromCamera(mouse, camera)
// 这里我们只检测模型的选中情况
const intersects = raycaster.intersectObjects(fbx.children, true)
if (intersects.length > 0) {
that.selectedObjects = intersects[0].object
console.log('selectedObjects:', that.selectedObjects)
switch (that.selectedObjects.name) {
case 'DLP_lak':
if (that.isOpen) { //关闭车门
that.isOpen = false
that.selectedObjects.parent.children.map(item => {
// 动画
that.setTweens(item.rotation, {
x: item.rotation.x,
y: item.rotation.y + 0.5 * Math.PI,
z: item.rotation.z
}, 1000)
})
} else {
that.isOpen = true
that.selectedObjects.parent.children.map(item => { //打开车门
that.setTweens(item.rotation, {
x: item.rotation.x,
y: item.rotation.y - 0.5 * Math.PI,
z: item.rotation.z
}, 1000)
})
}
break
}
}
}, false)
}
функция смены цвета
model 模型对象
value 颜色值
递归实现修改材质
setColor(model, value) {
model.map(cell => {
if (cell.children.length === 0) {
const material = cell.material.clone()
material.color = new THREE.Color(value)
cell.material = material
} else {
this.setColor(cell.children, value)
}
})
}
в машину
通过延迟实现动画的连贯性
intoCart() {
this.isOpen = true
this.controlsObj.reset()// 回到初始化视角
this.setTweens(this.camera.position, { x: 30, y: 10, z: 30 }, 1000)
this.door.map(item => { //打开门
this.setTweens(item.rotation, {
x: item.rotation.x,
y: item.rotation.y - 0.5 * Math.PI,
z: item.rotation.z
}, 1000)
})
setTimeout(() => {
this.setTweens(this.camera.position, { x: 35, y: 15, z: 30 }, 1000)//展开视角
setTimeout(() => {
this.setTweens(this.camera.position, { x: 0, y: 0, z: 1 }, 1000)//驾驶位视角
this.setTweens(this.plane.position, { x: this.plane.position.x, y: -10, z: this.plane.position.z }, 500)//平面下移
setTimeout(() => {
this.cart.position.set(1.5, -6, 5)//车位置改变
this.cart.rotation.set(0, 0, 0)
this.camera.lookAt(this.camera.position)//查看视角
this.isOpen = false
this.door.map(item => {//关闭车门
this.setTweens(item.rotation, {
x: item.rotation.x,
y: item.rotation.y + 0.5 * Math.PI,
z: item.rotation.z
}, 1000)
})
this.renderer.render(this.scene, this.camera)//重新渲染
}, 1000)
}, 1000)
}, 1000)
}
выйти из машины
outCart() {
this.isOpen = true
this.door.map(item => {//打开车门
this.setTweens(item.rotation, {
x: item.rotation.x,
y: item.rotation.y - 0.5 * Math.PI,
z: item.rotation.z
}, 1000)
})
setTimeout(() => {
this.setTweens(this.plane.position, { x: this.plane.position.x, y: -2.5, z: this.plane.position.z }, 500)//草坪回归原位
this.setTweens(this.camera.position, { x: 30, y: 10, z: 30 }, 1000)//回到原视角
this.cart.position.set(0, 0, -4)//小车回归原位
this.cart.rotation.set(0, -41, 0)
this.camera.lookAt(this.camera.position)
this.controlsObj.reset()// 回到初始化视角
setTimeout(() => {
this.door.map(item => {
this.setTweens(item.rotation, {//关闭车门
x: item.rotation.x,
y: item.rotation.y + 0.5 * Math.PI,
z: item.rotation.z
}, 1000)
this.renderer.render(this.scene, this.camera)
})
}, 1000)
}, 1000)
}