Appearance
多物体选择
按“Z"来锁住/解锁视角
点击运行
<template>
<div>
按“Z"来锁住/解锁视角
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<div v-if="isRunning" style="position: relative;">
<div id="rpgMultiObjectSelection" class="stage"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
Scene,
PerspectiveCamera,
WebGLRenderer,
Color,
AmbientLight,
BoxGeometry,
MeshLambertMaterial,
Mesh,
Vector3,
Texture,
Frustum
} from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
const requestID = ref<any>()
const isRunning = ref(false)
let sceneResources
let allBox: any = []
let collectionSelect: any = []
let center = new Vector3()
let controls: any = null
let createDiv: any = null
let isLockScreen = false
let isPointerDown = false
let startX = 0
let startY = 0
let endX = 0
let endY = 0
const startPoint = {
x: 0,
y: 0,
z: 0.5
}
const endPoint = {
x: 0,
y: 0,
z: 0.5
}
const frustum = new Frustum()
const vecNear = new Vector3()
const vecTopLeft = new Vector3()
const vecTopRight = new Vector3()
const vecBottomLeft = new Vector3()
const vecBottomRight = new Vector3()
const vecFar1 = new Vector3()
const vecFar2 = new Vector3()
const vecFar3 = new Vector3()
const deep = 2000
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
sceneResources = await initScene()
} else {
isRunning.value = false
destroy()
}
}
const initScene = () => {
const ele = document.getElementById('rpgMultiObjectSelection') as HTMLElement
const width = Number(window.getComputedStyle(ele).width.split('px')[0])
const height = Number(window.getComputedStyle(ele).height.split('px')[0])
const scene = new Scene()
const camera: any = new PerspectiveCamera(45, width / height, 1, 10000)
camera.position.set(0, 0, 1000)
camera.rotation.set(0, 0, 1)
scene.add(camera)
const renderer: any = new WebGLRenderer({
antialias: true,
powerPreference: 'high-performance',
alpha: true
})
renderer.setSize(width, height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.setClearColor(new Color('#32373E'), 1)
ele.appendChild(renderer.domElement)
// 添加 OrbitControls
const createOrbitControls = () => {
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.25
controls.enableZoom = true
}
// 光
const createLight = () => {
const ambient = new AmbientLight(0x444444)
scene.add(ambient)
}
const createBox = () => {
for (let i = 0; i < 10; i++) {
const geometry = new BoxGeometry(20, 20, 20)
const material = new MeshLambertMaterial({
color: 0x00ff00
})
const cube = new Mesh(geometry, material)
const x = Math.random() * 200
const y = Math.random() * 200
const z = Math.random() * 200
cube.position.set(x, y, z)
allBox.push(cube)
scene.add(cube)
}
}
const createEle = () => {
createDiv = document.createElement('div')
createDiv.style.pointerEvents = 'none'
createDiv.style.border = '1px solid red'
createDiv.style.background = 'rgba(222, 160, 131, 0.2)'
createDiv.style.position = 'absolute'
createDiv.style.display = 'none'
createDiv.style.left = '0'
createDiv.style.top = '0'
createDiv.style.zIndex = '10'
createDiv.style.transformOrigin = '0 0'
renderer.domElement.parentElement.appendChild(createDiv)
return createDiv
}
const numb = n =>{
return Number(n.toFixed(4))
}
const runAnimate = () => {
requestID.value = requestAnimationFrame(runAnimate)
renderer.render(scene, camera)
}
renderer.domElement.addEventListener('pointerdown', e => {
if (isLockScreen) {
isPointerDown = true
startX = numb(e.offsetX)
startY = numb(e.offsetY)
createDiv.style.transform = `translate3d(${startX}px, ${startY}px, 0) rotate3d(0, 0, 0, 180deg)`
startPoint.x = numb((startX / width) * 2 - 1)
startPoint.y = numb(-(startY / height) * 2 + 1)
for (let i = 0; i < collectionSelect.length; i++) {
collectionSelect[i].material.emissive.set(0x000000)
}
}
})
renderer.domElement.addEventListener('pointermove', e => {
if (isLockScreen && isPointerDown) {
for (let i = 0; i < collectionSelect.length; i++) {
collectionSelect[i].material.emissive.set(0x000000)
}
endX = numb(e.offsetX)
endY = numb(e.offsetY)
const w = endX - startX
const h = endY - startY
let rX = 0
let rY = 0
let rZ = 0
if (w < 0 && h > 0) {
rY = 1
} else if (w > 0 && h < 0) {
rX = 1
} else if (w < 0 && h < 0) {
rZ = 1
}
createDiv.style.display = w === 0 || h === 0 ? 'none' : 'block'
createDiv.style.transform = `translate3d(${startX}px, ${startY}px, 0) rotate3d(${rX}, ${rY}, ${rZ}, 180deg)`
createDiv.style.width = Math.abs(w) + 'px'
createDiv.style.height = Math.abs(h) + 'px'
endPoint.x = numb((endX / width) * 2 - 1)
endPoint.y = numb(-(endY / height) * 2 + 1)
getSelectThings()
for (let i = 0; i < collectionSelect.length; i++) {
collectionSelect[i].material.emissive.set(0xffffff)
}
}
})
renderer.domElement.addEventListener('pointerup', event => {
if (isLockScreen) {
createDiv.style.width = 0 + 'px'
createDiv.style.height = 0 + 'px'
createDiv.style.display = 'none'
isPointerDown = false
endPoint.x = numb((numb(event.offsetX) / width) * 2 - 1)
endPoint.y = numb(-(numb(event.offsetY) / height) * 2 + 1)
getSelectThings()
for (let i = 0; i < collectionSelect.length; i++) {
collectionSelect[i].material.emissive.set(0xffffff)
}
}
})
const createFrustum = () => {
/**
* 为什么sP和eP这样弄,画个图“回”,因为从左往右的顺序,和从右往左的顺序,是不一样的(sX,sY,eX,eY)
*/
const sP = {
x: Math.min(startPoint.x, endPoint.x),
y: Math.max(startPoint.y, endPoint.y),
z: 0.5
}
const eP = {
x: Math.max(startPoint.x, endPoint.x),
y: Math.min(startPoint.y, endPoint.y),
z: 0.5
}
console.log(sP, eP)
/**
* vecNear就直接取相机的position
* vecTopLeft、vecTopRight、vecBottomLeft、vecBottomRight则重新赋值,并unproject
*/
vecNear.setFromMatrixPosition(camera.matrixWorld)
vecTopLeft.copy(sP)
vecTopRight.set(eP.x, sP.y, 0.5)
vecBottomLeft.set(sP.x, eP.y, 0.5)
vecBottomRight.copy(eP)
/**
* unproject方法主要用于将屏幕上的点击位置转换为Three.js场景中的三维坐标
* 换句话说,它能够将二维的屏幕坐标转换为三维场景中的坐标
*/
vecTopLeft.unproject(camera)
vecTopRight.unproject(camera)
vecBottomLeft.unproject(camera)
vecBottomRight.unproject(camera)
vecFar1.copy(vecTopLeft).sub(vecNear).normalize()
vecFar2.copy(vecTopRight).sub(vecNear).normalize()
vecFar3.copy(vecBottomRight).sub(vecNear).normalize()
vecFar1.multiplyScalar(deep)
vecFar2.multiplyScalar(deep)
vecFar3.multiplyScalar(deep)
vecFar1.add(vecNear)
vecFar2.add(vecNear)
vecFar3.add(vecNear)
const planes = frustum.planes
/**
* 六个面,可以看public/tips/isPerspectiveCamera.webp这个图
*/
planes[0].setFromCoplanarPoints(vecNear, vecTopLeft, vecTopRight)
planes[1].setFromCoplanarPoints(vecNear, vecTopRight, vecBottomRight)
planes[2].setFromCoplanarPoints(vecBottomRight, vecBottomLeft, vecNear)
planes[3].setFromCoplanarPoints(vecBottomLeft, vecTopLeft, vecNear)
planes[4].setFromCoplanarPoints(vecTopRight, vecBottomRight, vecBottomLeft)
planes[5].setFromCoplanarPoints(vecFar3, vecFar2, vecFar1)
/**
* normal.multiplyScalar(-1)是一个将向量的每个分量乘以-1的操作
* 这实际上是将向量的方向反转
* 因此,frustum.planes[5].normal.multiplyScalar(-1)的意思是反转视锥体下平面的法线方向,使其从指向下方变为指向上方
* 在某些情况下,开发者可能想要反转这个法线方向,即将其从指向下方改为指向上方
* 这可能是因为开发者想要以一种不同的方式使用这个平面,例如在进行自定义的物体与视锥体的相交检测时。
*/
/**
* 这种操作在某些情况下可能是有用的
* 比如当想计算一个点与视锥体的关系时,但希望以相反的方向来考虑下平面
* 通常,视锥体的下平面是用来确定哪些物体在相机下方,因此不应该被渲染的
* 但如果反转了下平面的法线,那么它将用来确定哪些物体在相机的上方
*/
/**
* 这个去掉好像也没有影响??
*/
planes[5].normal.multiplyScalar(-1)
}
const getSelectThings = () => {
collectionSelect = []
if (startPoint.x === endPoint.x || startPoint.y === endPoint.y) {
return
}
camera.updateProjectionMatrix()
camera.updateMatrixWorld()
createFrustum()
scene.children.forEach((child: any) => {
if (child.isMesh && child.isObject3D) {
center.copy(child.geometry.boundingSphere.center)
/**
* 当Vector3表示一个顶点坐标时,applyMatrix4方法可以通过矩阵对顶点坐标进行矩阵变换,如平移、旋转、缩放等
* 例如,如果有一个表示空间中某点坐标的Vector3对象
* 并且有一个Matrix4对象表示一个变换矩阵
* 可以通过调用applyMatrix4方法来应用这个变换矩阵到向量上,从而得到变换后的新坐标
*/
center.applyMatrix4(child.matrixWorld)
if (frustum.containsPoint(center)) {
collectionSelect.push(child)
}
}
})
console.log(collectionSelect)
}
createLight()
createBox()
createEle()
runAnimate()
createOrbitControls()
return {
renderer,
scene,
}
}
const lockScreen = (e) => {
if (e.key.toLocaleLowerCase() === 'z' && !isLockScreen) {
isLockScreen = true
controls.enabled = false
} else if (e.key === 'z' && isLockScreen) {
isLockScreen = false
controls.enabled = true
}
}
const destroy = () => {
if (sceneResources) {
sceneResources.scene.clear()
sceneResources.scene.traverse((child) => {
if (child.geometry) child.geometry?.dispose()
if (child.material) {
if (child.material.map) child.material.map?.dispose()
child.material?.dispose()
}
})
if (sceneResources.scene.background) {
if (sceneResources.scene.background instanceof Texture) {
sceneResources.scene.background?.dispose()
}
}
sceneResources.renderer?.dispose()
sceneResources.renderer.forceContextLoss()
sceneResources.controls?.dispose()
cancelAnimationFrame(requestID.value)
sceneResources = null
controls = null
createDiv = null
allBox = []
collectionSelect = []
}
}
onMounted(async() => {
await nextTick()
window.addEventListener('keydown', lockScreen)
})
onUnmounted(() => {
destroy()
window.removeEventListener('keydown', lockScreen)
})
</script>