Files
three-demos/src/views/three/10/index.vue
2025-11-21 11:35:47 +08:00

438 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
* @Description: 模型与Web的交互
* @Autor: 相卿
* @Date: 2025-11-17 16:07:33
* @LastEditors: 相卿
* @LastEditTime: 2025-11-21 10:04:12
-->
<script lang="ts" setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
// 物体信息类型
interface ObjectInfo {
name: string
material: string
position: THREE.Vector3
mesh: THREE.Mesh
}
const threeRef = shallowRef<HTMLDivElement>()
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let animationId: number
let raycaster: THREE.Raycaster
let mouse: THREE.Vector2
let model: THREE.Group
// 后期处理相关
let composer: EffectComposer
let outlinePass: OutlinePass
// 交互状态
const hoveredObject = shallowRef<THREE.Mesh | null>(null)
const selectedObject = ref<ObjectInfo | null>(null)
const labelPosition = ref({ x: 0, y: 0 })
// 辅助线
let axesHelper: THREE.AxesHelper | null = null
function init() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
scene = new THREE.Scene()
scene.background = new THREE.Color(0x1A1A2E)
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.set(0, 5, 15)
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.setPixelRatio(window.devicePixelRatio)
threeRef.value!.appendChild(renderer.domElement)
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
// 射线检测器
raycaster = new THREE.Raycaster()
mouse = new THREE.Vector2()
// 添加灯光 - 增强照明
// 环境光 - 提供基础亮度
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 5)
scene.add(ambientLight)
// 主方向光 - 从右上方照射
const mainLight = new THREE.DirectionalLight(0xFFFFFF, 1.5)
mainLight.position.set(10, 15, 10)
mainLight.castShadow = true
scene.add(mainLight)
// 辅助方向光 - 从左侧照射,补充阴影区域
const fillLight = new THREE.DirectionalLight(0xFFFFFF, 0.8)
fillLight.position.set(-10, 10, -10)
scene.add(fillLight)
// 背光 - 从后方照射,增加轮廓感
const backLight = new THREE.DirectionalLight(0xFFFFFF, 0.6)
backLight.position.set(0, 5, -15)
scene.add(backLight)
// 点光源 - 增加局部亮度
const pointLight = new THREE.PointLight(0xFFFFFF, 1, 50)
pointLight.position.set(0, 10, 0)
scene.add(pointLight)
// 添加地面
const groundGeometry = new THREE.PlaneGeometry(100, 100)
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x555555,
roughness: 0.8,
metalness: 0.2,
})
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
ground.rotation.x = -Math.PI / 2
ground.position.y = 0
scene.add(ground)
// 设置后期处理
setupPostProcessing()
// 加载模型
const loader = new GLTFLoader()
loader.load('/mzjc_bansw.glb', (gltf) => {
model = gltf.scene
model.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.userData.objectType = '建筑构件'
child.userData.objectName = child.name || '未命名物体'
}
})
scene.add(model)
})
// 事件监听
renderer.domElement.addEventListener('mousemove', onMouseMove)
renderer.domElement.addEventListener('click', onCanvasClick)
window.addEventListener('resize', onResize)
animate()
}
// 设置后期处理(轮廓描边)
function setupPostProcessing() {
composer = new EffectComposer(renderer)
const renderPass = new RenderPass(scene, camera)
composer.addPass(renderPass)
outlinePass = new OutlinePass(
new THREE.Vector2(threeRef.value!.clientWidth, threeRef.value!.clientHeight),
scene,
camera,
)
outlinePass.edgeStrength = 3
outlinePass.edgeGlow = 0.5
outlinePass.edgeThickness = 2
outlinePass.pulsePeriod = 0
outlinePass.visibleEdgeColor.set('#00ff00')
outlinePass.hiddenEdgeColor.set('#00ff00')
composer.addPass(outlinePass)
}
function animate() {
animationId = requestAnimationFrame(animate)
controls.update()
// 更新标签位置
if (selectedObject.value) {
updateLabelPosition()
}
composer.render()
}
// 更新标签位置
function updateLabelPosition() {
if (!selectedObject.value)
return
const position = selectedObject.value.position.clone()
// 获取物体的包围盒来计算高度
const box = new THREE.Box3().setFromObject(selectedObject.value.mesh)
const height = box.max.y - box.min.y
position.y = box.max.y + height * 0.3
position.project(camera)
labelPosition.value = {
x: (position.x * 0.5 + 0.5) * threeRef.value!.clientWidth,
y: (-(position.y * 0.5) + 0.5) * threeRef.value!.clientHeight,
}
}
// 鼠标移动事件
function onMouseMove(event: MouseEvent) {
const rect = renderer.domElement.getBoundingClientRect()
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
raycaster.setFromCamera(mouse, camera)
if (model) {
const intersects = raycaster.intersectObject(model, true)
if (intersects.length > 0 && intersects[0]) {
const object = intersects[0].object as THREE.Mesh
// 如果是新的悬停物体
if (hoveredObject.value !== object) {
hoveredObject.value = object
// 添加轮廓描边
outlinePass.selectedObjects = [object]
// 添加辅助坐标线
removeAxesHelper()
const box = new THREE.Box3().setFromObject(object)
const center = box.getCenter(new THREE.Vector3())
axesHelper = new THREE.AxesHelper(2)
axesHelper.position.copy(center)
scene.add(axesHelper)
}
}
else {
// 鼠标没有悬停在物体上
if (hoveredObject.value && hoveredObject.value !== selectedObject.value?.mesh) {
clearHoverEffects()
}
}
}
}
// 画布点击事件
function onCanvasClick(event: MouseEvent) {
const rect = renderer.domElement.getBoundingClientRect()
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
raycaster.setFromCamera(mouse, camera)
if (model) {
const intersects = raycaster.intersectObject(model, true)
if (intersects.length > 0 && intersects[0]) {
const object = intersects[0].object as THREE.Mesh
const point = intersects[0].point
// 获取材质信息
let materialName = '未知材质'
if (object.material) {
if (Array.isArray(object.material)) {
materialName = object.material.map((m: THREE.Material) => m.type).join(', ')
}
else {
materialName = object.material.type
}
}
// 设置选中物体
selectedObject.value = {
name: object.userData.objectName || object.name || '未命名物体',
material: materialName,
position: point.clone(),
mesh: object,
}
// 保持轮廓描边和坐标线
hoveredObject.value = object
outlinePass.selectedObjects = [object]
updateLabelPosition()
}
else {
// 点击空白处,清除所有效果
clearAllEffects()
}
}
}
// 清除悬停效果
function clearHoverEffects() {
hoveredObject.value = null
if (!selectedObject.value) {
outlinePass.selectedObjects = []
removeAxesHelper()
}
}
// 清除所有效果
function clearAllEffects() {
hoveredObject.value = null
selectedObject.value = null
outlinePass.selectedObjects = []
removeAxesHelper()
}
// 移除辅助坐标线
function removeAxesHelper() {
if (axesHelper) {
scene.remove(axesHelper)
axesHelper = null
}
}
function onResize() {
if (!threeRef.value)
return
const width = threeRef.value.clientWidth
const height = threeRef.value.clientHeight
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height)
composer.setSize(width, height)
}
onMounted(init)
onUnmounted(() => {
cancelAnimationFrame(animationId)
window.removeEventListener('resize', onResize)
if (renderer.domElement) {
renderer.domElement.removeEventListener('mousemove', onMouseMove)
renderer.domElement.removeEventListener('click', onCanvasClick)
}
renderer.dispose()
composer.dispose()
})
</script>
<template>
<div ref="threeRef" class="w-full h-full" />
<!-- 物体信息标签 -->
<div
v-if="selectedObject"
class="object-label"
:style="{
left: `${labelPosition.x}px`,
top: `${labelPosition.y}px`,
}"
>
<div class="label-content">
<div class="label-title">
{{ selectedObject.name }}
</div>
<div class="label-item">
<span class="label-key">材质</span>
<span class="label-value">{{ selectedObject.material }}</span>
</div>
<div class="label-item">
<span class="label-key">坐标</span>
<span class="label-value">
({{ selectedObject.position.x.toFixed(2) }},
{{ selectedObject.position.y.toFixed(2) }},
{{ selectedObject.position.z.toFixed(2) }})
</span>
</div>
</div>
</div>
<router-link to="/three/11" class="position-fixed left-20px top-20px link-btn">
后期处理效果
</router-link>
<div class="position-fixed right-20px top-20px info-panel">
<div class="info-text">
鼠标移入物体显示轮廓和坐标轴
</div>
<div class="info-text">
点击物体显示详细信息
</div>
<div class="info-text">
点击空白处取消选择
</div>
</div>
</template>
<style scoped>
.object-label {
position: absolute;
transform: translate(-50%, -100%);
pointer-events: none;
z-index: 100;
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translate(-50%, -100%) scale(0.8);
}
to {
opacity: 1;
transform: translate(-50%, -100%) scale(1);
}
}
.label-content {
background: rgba(0, 0, 0, 0.85);
color: white;
padding: 12px 16px;
border-radius: 6px;
border: 2px solid #00ff00;
box-shadow: 0 4px 12px rgba(0, 255, 0, 0.3);
min-width: 200px;
}
.label-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
color: #00ff00;
border-bottom: 1px solid rgba(0, 255, 0, 0.3);
padding-bottom: 6px;
}
.label-item {
font-size: 13px;
margin: 6px 0;
line-height: 1.5;
}
.label-key {
color: #aaa;
font-weight: 500;
}
.label-value {
color: #fff;
}
.info-panel {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-text {
background: rgba(0, 0, 0, 0.75);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
border-left: 3px solid #00ff00;
}
</style>