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

<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>