You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
330 lines
8.2 KiB
330 lines
8.2 KiB
<template>
|
|
<div class="canvas-container" ref="canvasContainer" tabindex="1" />
|
|
</template>
|
|
<script setup lang="ts">
|
|
import * as THREE from 'three'
|
|
import $ from 'jquery'
|
|
import Stats from 'three/examples/jsm/libs/stats.module'
|
|
import {
|
|
ref,
|
|
onMounted,
|
|
nextTick,
|
|
reactive,
|
|
watch,
|
|
getCurrentInstance,
|
|
onUnmounted,
|
|
onBeforeUnmount,
|
|
defineExpose
|
|
} from 'vue'
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
|
|
|
// DOM refs
|
|
const canvasContainer = ref(null)
|
|
|
|
// 窗口大小关联
|
|
let resizeObserver
|
|
|
|
// Three.js 场景相关
|
|
let scene, camera, renderer, controls
|
|
let statsControls, axesHelper, gridHelper
|
|
|
|
const state = designer.editorState
|
|
|
|
function initMode2DCamera() {
|
|
if (camera) {
|
|
scene.remove(camera)
|
|
}
|
|
|
|
// ============================ 创建正交相机
|
|
const viewerDom = canvasContainer.value
|
|
const cameraNew = new THREE.OrthographicCamera(
|
|
viewerDom.clientWidth / -2,
|
|
viewerDom.clientWidth / 2,
|
|
viewerDom.clientHeight / 2,
|
|
viewerDom.clientHeight / -2,
|
|
1,
|
|
500
|
|
)
|
|
cameraNew.position.set(0, 100, 0)
|
|
cameraNew.lookAt(0, 0, 0)
|
|
cameraNew.zoom = 30
|
|
camera = cameraNew
|
|
scene.add(camera)
|
|
|
|
// ============================ 创建控制器
|
|
const controlsNew = new OrbitControls(
|
|
camera,
|
|
renderer.domElement
|
|
)
|
|
controlsNew.enableDamping = false
|
|
controlsNew.enableZoom = true
|
|
controlsNew.enableRotate = false
|
|
controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN } // 鼠标中键平移
|
|
controlsNew.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动
|
|
controlsNew.listenToKeyEvents(viewerDom) // 监听键盘事件
|
|
controlsNew.keys = { LEFT: 'KeyA', UP: 'KeyW', RIGHT: 'KeyD', BOTTOM: 'KeyS' }
|
|
controlsNew.panSpeed = 1
|
|
controlsNew.keyPanSpeed = 20 // normal 7
|
|
controlsNew.minDistance = 0.1
|
|
controlsNew.maxDistance = 1000
|
|
controls = controlsNew
|
|
controlsNew.addEventListener('change', syncCameraState)
|
|
|
|
camera.updateProjectionMatrix()
|
|
|
|
syncCameraState()
|
|
}
|
|
|
|
function initThree() {
|
|
const viewerDom = canvasContainer.value
|
|
|
|
// 场景
|
|
scene = new THREE.Scene()
|
|
scene.background = new THREE.Color(0xeeeeee)
|
|
|
|
// 渲染器
|
|
renderer = new THREE.WebGLRenderer({
|
|
logarithmicDepthBuffer: true,
|
|
antialias: true,
|
|
alpha: true,
|
|
precision: 'mediump',
|
|
premultipliedAlpha: true,
|
|
preserveDrawingBuffer: false,
|
|
powerPreference: 'high-performance'
|
|
})
|
|
renderer.outputEncoding = THREE.SRGBColorSpace
|
|
renderer.clearDepth()
|
|
renderer.shadowMap.enabled = true
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
|
renderer.setPixelRatio(window.devicePixelRatio)
|
|
renderer.setSize(viewerDom.getBoundingClientRect().width, viewerDom.getBoundingClientRect().height)
|
|
viewerDom.appendChild(renderer.domElement)
|
|
|
|
// 创建正交摄像机
|
|
initMode2DCamera()
|
|
|
|
// 辅助线
|
|
axesHelper = new THREE.AxesHelper(5)
|
|
scene.add(axesHelper)
|
|
|
|
gridHelper = new THREE.GridHelper(500, 500)
|
|
gridHelper.material = new THREE.LineBasicMaterial({
|
|
color: 0x888888,
|
|
opacity: 1,
|
|
transparent: true
|
|
})
|
|
|
|
scene.add(gridHelper)
|
|
|
|
// 光照
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 1.5)
|
|
scene.add(ambientLight)
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5)
|
|
directionalLight.position.set(5, 5, 5).multiplyScalar(3)
|
|
directionalLight.castShadow = true
|
|
scene.add(directionalLight)
|
|
|
|
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1)
|
|
scene.add(hemisphereLight)
|
|
|
|
// 性能监控
|
|
statsControls = new Stats()
|
|
statsControls.showPanel(0)
|
|
statsControls.dom.style.position = 'absolute'
|
|
statsControls.dom.style.top = '2px'
|
|
statsControls.dom.style.left = '0'
|
|
viewerDom.appendChild(statsControls.dom)
|
|
$(statsControls.dom).children().css('height', '28px')
|
|
|
|
// 创建几何体和材质
|
|
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
|
const material = new THREE.MeshStandardMaterial({
|
|
color: 0xcccccc,
|
|
metalness: 0.9,
|
|
roughness: 0.1
|
|
})
|
|
|
|
const cube = new THREE.Mesh(geometry, material)
|
|
scene.add(cube)
|
|
camera.position.z = 5
|
|
|
|
animate()
|
|
}
|
|
|
|
// 动画循环
|
|
function animate() {
|
|
requestAnimationFrame(animate)
|
|
renderView()
|
|
}
|
|
|
|
function renderView() {
|
|
statsControls?.update()
|
|
renderer?.render(scene, camera)
|
|
}
|
|
|
|
onMounted(() => {
|
|
const editor = getCurrentInstance()
|
|
nextTick(() => {
|
|
initThree()
|
|
|
|
const viewerDom = canvasContainer.value
|
|
if (resizeObserver) {
|
|
resizeObserver.unobserve(viewerDom)
|
|
}
|
|
resizeObserver = new ResizeObserver(handleResize)
|
|
resizeObserver.observe(viewerDom)
|
|
|
|
state.ready = true
|
|
|
|
window['editor'] = editor
|
|
window['renderer'] = renderer
|
|
window['scene'] = scene
|
|
window['camera'] = camera
|
|
window['renderer'] = renderer
|
|
window['controls'] = controls
|
|
|
|
viewerDom?.focus()
|
|
})
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
state.ready = false
|
|
|
|
cleanupThree()
|
|
|
|
const viewerDom = canvasContainer.value
|
|
if (resizeObserver) {
|
|
resizeObserver.unobserve(viewerDom)
|
|
}
|
|
|
|
delete window['editor']
|
|
delete window['renderer']
|
|
delete window['scene']
|
|
delete window['camera']
|
|
delete window['renderer']
|
|
delete window['controls']
|
|
})
|
|
|
|
|
|
function handleResize(entries) {
|
|
for (let entry of entries) {
|
|
// entry.contentRect包含了元素的尺寸信息
|
|
console.log('Element size changed:', entry.contentRect)
|
|
|
|
const width = entry.contentRect.width
|
|
const height = entry.contentRect.height
|
|
|
|
if (camera instanceof THREE.PerspectiveCamera) {
|
|
camera.aspect = width / height
|
|
camera.updateProjectionMatrix()
|
|
|
|
} else if (camera instanceof THREE.OrthographicCamera) {
|
|
camera.left = width / -2
|
|
camera.right = width / 2
|
|
camera.top = height / 2
|
|
camera.bottom = height / -2
|
|
camera.updateProjectionMatrix()
|
|
}
|
|
renderer.setSize(width, height)
|
|
break
|
|
}
|
|
}
|
|
|
|
watch(() => state.camera.position.y, (newVal) => {
|
|
if (state.ready) {
|
|
updateGridVisibility()
|
|
}
|
|
}, { deep: true })
|
|
|
|
/**
|
|
* 根据可视化范围更新网格的透明度
|
|
*/
|
|
function updateGridVisibility() {
|
|
const cameraDistance = state.camera.position.y
|
|
const maxVisibleDistance = 60 // 网格完全可见的最大距离
|
|
const fadeStartDistance = 10 // 开始淡出的距离
|
|
|
|
// 计算透明度(0~1)
|
|
let opacity = 1
|
|
if (cameraDistance > fadeStartDistance) {
|
|
opacity = 1 - Math.min((cameraDistance - fadeStartDistance) / (maxVisibleDistance - fadeStartDistance), 1)
|
|
}
|
|
|
|
// 修改网格材质透明度
|
|
gridHelper.material.opacity = opacity
|
|
gridHelper.material.transparent = opacity < 1
|
|
console.log('opacity', opacity)
|
|
}
|
|
|
|
function cleanupThree() {
|
|
// 移除旧模型
|
|
if (scene) {
|
|
scene.traverse((obj) => {
|
|
// 释放几何体
|
|
if (obj.geometry) {
|
|
obj.geometry.dispose()
|
|
}
|
|
|
|
// 释放材质
|
|
if (obj.material) {
|
|
if (Array.isArray(obj.material)) {
|
|
obj.material.forEach(m => m.dispose())
|
|
} else {
|
|
obj.material.dispose()
|
|
}
|
|
}
|
|
|
|
// 释放纹理
|
|
if (obj.texture) {
|
|
obj.texture.dispose()
|
|
}
|
|
|
|
// 释放渲染目标
|
|
if (obj.renderTarget) {
|
|
obj.renderTarget.dispose()
|
|
}
|
|
|
|
// 移除事件监听(如 OrbitControls)
|
|
if (obj.dispose) {
|
|
obj.dispose()
|
|
}
|
|
})
|
|
|
|
// 清空场景
|
|
scene.children = []
|
|
}
|
|
|
|
if (statsControls) {
|
|
statsControls.dom.remove()
|
|
}
|
|
|
|
if (renderer) {
|
|
renderer.dispose()
|
|
renderer.forceContextLoss()
|
|
console.log('WebGL disposed, memory:', renderer.info.memory)
|
|
renderer.domElement = null
|
|
}
|
|
}
|
|
|
|
function getEffectiveViewDistance() {
|
|
// 1. 获取相机到目标的距离
|
|
const targetDistance = controls.target.distanceTo(camera.position)
|
|
|
|
// 2. 计算当前视口高度(世界单位)
|
|
const viewHeight = (camera.top - camera.bottom) / camera.zoom
|
|
|
|
// 3. 计算等效的透视相机距离
|
|
// 假设我们希望匹配一个虚拟的透视相机(通常使用45度fov作为参考)
|
|
const referenceFOV = 45 // 参考视场角
|
|
return viewHeight / (2 * Math.tan(THREE.MathUtils.degToRad(referenceFOV) / 2))
|
|
}
|
|
|
|
function syncCameraState() {
|
|
if (camera) {
|
|
state.camera.position.x = camera.position.x
|
|
state.camera.position.y = getEffectiveViewDistance()
|
|
state.camera.position.z = camera.position.z
|
|
}
|
|
}
|
|
</script>
|