This commit is contained in:
2025-11-21 09:41:00 +08:00
parent afcd701edc
commit b4683674b9
3 changed files with 573 additions and 5 deletions

View File

@@ -3,11 +3,435 @@
* @Autor: 相卿
* @Date: 2025-11-17 16:07:33
* @LastEditors: 相卿
* @LastEditTime: 2025-11-20 11:45:40
* @LastEditTime: 2025-11-21 09:26:39
-->
<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>10</div>
<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>

View File

@@ -3,11 +3,155 @@
* @Autor: 相卿
* @Date: 2025-11-17 16:07:33
* @LastEditors: 相卿
* @LastEditTime: 2025-11-20 11:45:27
* @LastEditTime: 2025-11-21 09:04:18
-->
<script lang="ts" setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
const threeRef = shallowRef<HTMLDivElement>()
const isEmitting = ref(false)
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let animationId: number
const particles: THREE.Sprite[] = []
let particleTexture: THREE.Texture
function init() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
scene = new THREE.Scene()
scene.background = new THREE.Color(0x000000)
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.set(0, 5, 10)
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
threeRef.value!.appendChild(renderer.domElement)
controls = new OrbitControls(camera, renderer.domElement)
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.5)
scene.add(ambientLight)
particleTexture = createSmokeTexture()
animate()
window.addEventListener('resize', onResize)
}
function createSmokeTexture() {
const canvas = document.createElement('canvas')
canvas.width = 32
canvas.height = 32
const ctx = canvas.getContext('2d')!
const grad = ctx.createRadialGradient(16, 16, 0, 16, 16, 16)
grad.addColorStop(0, 'rgba(255, 255, 255, 1)')
grad.addColorStop(1, 'rgba(255, 255, 255, 0)')
ctx.fillStyle = grad
ctx.fillRect(0, 0, 32, 32)
const texture = new THREE.Texture(canvas)
texture.needsUpdate = true
return texture
}
function toggleParticle() {
isEmitting.value = !isEmitting.value
if (!isEmitting.value) {
particles.forEach(p => scene.remove(p))
particles.length = 0
}
}
function animate() {
animationId = requestAnimationFrame(animate)
controls.update()
if (isEmitting.value) {
// Create new particle
const material = new THREE.SpriteMaterial({
map: particleTexture,
transparent: true,
opacity: 0.5,
color: 0xAAAAAA,
depthWrite: false, // Important for transparency
})
const particle = new THREE.Sprite(material)
particle.position.set(0, 0, 0)
particle.position.x = (Math.random() - 0.5) * 2
particle.position.z = (Math.random() - 0.5) * 2
;(particle as any).velocity = new THREE.Vector3(
(Math.random() - 0.5) * 0.05,
0.1 + Math.random() * 0.1,
(Math.random() - 0.5) * 0.05,
)
;(particle as any).age = 0
scene.add(particle)
particles.push(particle)
}
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i]
if (!p)
continue
const velocity = (p as any).velocity
p.position.add(velocity)
;(p as any).age++
p.scale.setScalar(1 + (p as any).age * 0.05)
p.material.opacity = 0.5 - ((p as any).age / 100) * 0.5
if ((p as any).age > 100) {
scene.remove(p)
particles.splice(i, 1)
p.material.dispose() // Clean up
}
}
renderer.render(scene, camera)
}
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)
}
onMounted(init)
onUnmounted(() => {
cancelAnimationFrame(animationId)
window.removeEventListener('resize', onResize)
renderer.dispose()
particles.forEach((p) => {
scene.remove(p)
p.material.dispose()
})
})
</script>
<template>
<div>9</div>
<div ref="threeRef" class="w-full h-full" />
<router-link to="/three/10" class="position-fixed left-20px top-20px">
模型与Web的交互
</router-link>
<div class="position-fixed right-20px top-20px flex flex-col gap-12px">
<div class="flex gap-12px">
<button @click="toggleParticle">
{{ isEmitting ? '关闭' : '打开' }}粒子效果蒸汽
</button>
</div>
</div>
</template>