【图片完整效果代码位于文章末】
物体的点击事件与外发光教程可见往期文章:
引言
本文讲解如何使用Three.js库创建一个动态的3D标注系统,用于高亮和追踪场景中的特定物体。这段代码的核心功能是定义两个函数:showObjectLocation 和 locationAnimate,分别用于创建并显示标注物以及为其添加动画效果。
物体实现跳跃原理
物体的上下浮动实际是使用了正弦函数y=sinx,通过不断增加x轴的值,让y在[-1,1]区间内不断变化,用y的值来改变物体在垂直方向上的位置即可模拟跳跃或者振动效果。通过改变函数的公式y=a*sinx中a的值,即可调整跳跃的幅度。
具体实现步骤如下所示:
1.初始化变量与声明函数
// 初始化一个变量,用于保存当前正在显示的标注物
let currentLocationObj = null;
// 定义一个函数,用于在三维场景中显示目标物体的位置
function showObjectLocation(obj) {
// ...
}
// 定义一个函数,用于给当前标注物添加旋转和上下浮动的动画效果
function locationAnimate(obj) {
// ...
}
2.创建与更新标注物(showObjectLocation函数详解)
2.1创建圆锥形标注物
使用THREE.ConeGeometry创建一个底部半径为0.25、高度为0.5、由4个径向片段构成的圆锥几何体。然后,将圆锥体沿X轴旋转180度,使其尖端向下。给圆锥体赋予一种基础材质,颜色设为浅蓝色(0x66ccff)。
const geometry = new THREE.ConeGeometry(0.25, 0.5, 4);
geometry.rotateX(Math.PI);
const locationMaterial = new THREE.MeshBasicMaterial({
color: 0x66ccff,
});
const pyramid = new THREE.Mesh(geometry, locationMaterial);
2.2设置标注物的位置
pyramid.position.set(obj.position.x, 0, obj.position.z) // 设置x、z坐标,并让y坐标略高于地面
2.3.添加标注物到场景
将新的标注物添加到场景中,并更新currentLocationObj为当前标注物。
scene.add(pyramid);
currentLocationObj = pyramid;
2.4.启动动画
调用locationAnimate函数为标注物添加动态效果。
locationAnimate(pyramid);
3.实现标注物动画(locationAnimate函数详解)
locationAnimate函数实现了标注物的持续动画效果:
3.1.定义内部动画计时器
创建一个内部变量time用于跟踪动画的时间进度,防止多个动画之间互相干扰。
let time = 0;
3.2.定义递归动画函数
animate函数利用requestAnimationFrame不断调用自身,形成一个动画循环。
function animate() {
const animationHandle = requestAnimationFrame(() => animate());
// 更新旋转和位置属性...
// ...
}
3.3旋转与浮动动画效果
在每次动画帧中,标注物绕Y轴旋转,并根据正弦函数在其垂直位置上产生上下浮动效果。
obj.rotation.y += 0.03; // 旋转动画
obj.position.y = Math.sin(time) * 0.2 + 1.5; // 浮动动画
time += 0.07; // 更新时间变量
3.4管理动画句柄
将当前动画请求的句柄存储在标注物对象上,以便后续停止动画。
obj.animationHandle = animationHandle;
3.5启动动画
调用animate()函数启动动画循环。
animate();
3.6可选地,提供停止动画的方法
返回一个包含stop方法的对象,调用该方法时会取消正在进行的动画。
return {
stop: function () {
cancelAnimationFrame(obj.animationHandle);
},
};
4.完整实现效果代码如下所示
<template>
<div
style="
font-size: 24px;
color: #ffffff;
text-align: center;
position: absolute;
top: 20%;
left: 50%;
transform: translate(-50%, -50%);
"
>
{{ msg }}
</div>
</template>
<script setup>
import * as THREE from 'three'
import { onMounted, ref } from 'vue'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
const msg = ref('')
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
)
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
const controls = new OrbitControls(camera, renderer.domElement)
onMounted(() => {
init()
})
function init() {
camera.position.set(0, 0, 5)
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material1 = new THREE.MeshBasicMaterial({
color: 0xff00a2d7,
transparent: true,
opacity: 0.5,
})
const material2 = new THREE.MeshBasicMaterial({
color: 0xffd3e3fd,
transparent: true,
opacity: 0.5,
})
const cube1 = new THREE.Mesh(geometry, material1)
const cube2 = new THREE.Mesh(geometry, material2)
scene.add(cube1, cube2)
cube1.position.set(0, 0, 0)
cube1.name = '方块1'
cube2.position.set(2, 0, 0)
cube2.name = '方块2'
cube1.position.x = -2
controls.update()
function animate() {
requestAnimationFrame(animate)
controls.update()
cube1.rotation.y += 0.01
cube2.rotation.y -= 0.01
renderer.render(scene, camera)
}
animate()
outlineanimate()
}
// 创建射线投射器
const raycaster = new THREE.Raycaster()
// 鼠标位置
const mouse = new THREE.Vector2()
// 记录上一个被点击的对象
let lastSelectedObject = null
let outlineComposer = new EffectComposer(renderer) // 轮廓渲染器
//物体发光通道
let outlinePass = null
let renderPass = new RenderPass(scene, camera)
// 新建一个场景通道 为了覆盖到原理来的场景上
outlineComposer.addPass(renderPass)
// 鼠标点击事件监听
window.addEventListener('click', mouseClick, false)
function mouseClick(event) {
// 将鼠标坐标归一化
mouse.x = (event.clientX / window.innerWidth) * 2 - 1
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
// 设置射线起点为鼠标位置,射线的方向为相机视角方向
raycaster.setFromCamera(mouse, camera)
// 计算射线相交
const intersects = raycaster.intersectObjects(scene.children, true)
if (intersects.length > 0) {
// 如果之前有选中的物体,将其颜色恢复为初始状态
if (lastSelectedObject) {
lastSelectedObject.material.color.set(
lastSelectedObject.initialColor
)
}
// 选中物体
const selectedObject = intersects[0].object
console.log('点击事件', selectedObject.name)
msg.value = `标注${selectedObject.name}`
// 记录当前选中物体的状态
selectedObject.initialColor = selectedObject.material.color.clone()
lastSelectedObject = selectedObject
selectedObject.material.color.set(0xff62e258)
outlineObj([selectedObject])
showObjectLocation(selectedObject)
} else {
// 如果没有新的物体被选中,恢复上一个选中物体的颜色(如果存在的话)
if (lastSelectedObject) {
lastSelectedObject.material.color.set(
lastSelectedObject.initialColor
)
msg.value = ''
}
if (outlinePass) {
outlineComposer.removePass(outlinePass)
outlinePass = null
}
if (currentLocationObj) {
// 如果之前有标注物,则先移除
scene.remove(currentLocationObj)
}
}
}
// 绘制轮廓线
function outlineObj(selectedObjects) {
if (outlinePass) {
outlineComposer.removePass(outlinePass)
outlinePass = null
}
// 物体边缘发光通道
outlinePass = new OutlinePass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
scene,
camera
)
outlinePass.edgeStrength = 5 // 边框的亮度
outlinePass.edgeGlow = 1 // 光晕[0,1]
outlinePass.edgeThickness = 5 // 边框宽度
outlinePass.pulsePeriod = 3 // 呼吸闪烁的速度
outlinePass.visibleEdgeColor.set(0xff4ecfff)
outlinePass.hiddenEdgeColor.set(0x00ffff)
outlinePass.selectedObjects = selectedObjects
outlineComposer.addPass(outlinePass)
// 自定义的着色器通道 作为参数
}
// 渲染循环
function outlineanimate() {
requestAnimationFrame(outlineanimate)
outlineComposer.render()
}
let currentLocationObj = null // 当前的标注物
// 定义显示物体位置的函数
function showObjectLocation(obj) {
if (currentLocationObj) {
// 如果之前有标注物,则先移除
scene.remove(currentLocationObj)
}
// 创建圆锥几何体作为标注物
const geometry = new THREE.ConeGeometry(0.25, 0.5, 4) // 创建一个底部半径为0.25、高度为0.5、由4个径向片段组成的圆锥几何体
geometry.rotateX(Math.PI) // 将圆锥体绕X轴旋转180度,使得尖端朝下
const locationMaterial1 = new THREE.MeshBasicMaterial({
color: 0x66ccff,
}) // 创建材质
const pyramid = new THREE.Mesh(geometry, locationMaterial1)
pyramid.name = '坐标标注'
// 设置标注物的位置与传入物体的位置一致
pyramid.position.set(obj.position.x, 0, obj.position.z) // 设置x、z坐标,并让y坐标略高于地面
// 将标注物加入到场景中
scene.add(pyramid)
currentLocationObj = pyramid // 记录当前标注物
locationAnimate(pyramid) // 调用动画函数
}
// 坐标标注旋转动画
function locationAnimate(obj) {
let time = 0 //使用各自内部的time 防止多个动画相互影响
function animate() {
const animationHandle = requestAnimationFrame(() => animate())
obj.rotation.y += 0.03
// 上下浮动
obj.position.y = Math.sin(time) * 0.2 + 1.5 // 按照正弦函数进行浮动
time += 0.07
obj.animationHandle = animationHandle
}
animate()
// 可选地,返回停止动画的方法
return {
stop: function () {
cancelAnimationFrame(obj.animationHandle)
},
}
}
</script>