demos
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user