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.
 
 
 

701 lines
18 KiB

<template>
<div class="model3d-view">
<el-space :gutter="10" class="toolbar">
<el-button type="primary" @click="test1">测试1</el-button>
<el-button type="primary" @click="test2">测试2</el-button>
<el-button type="primary" @click="test3">测试3</el-button>
<div class="demo-color-block">
<span class="demonstration">物体数:<el-text type="danger">{{ restate.objects }}</el-text></span>
<span class="demonstration"> 顶点数:<el-text type="danger">{{ restate.vertices }}</el-text></span>
<span class="demonstration"> 三角形:<el-text type="danger">{{ restate.faces }}</el-text></span>
<span class="demonstration"> 标签:<el-text type="danger">{{ restate.viewLabelCount }}</el-text></span>
</div>
</el-space>
<div class="main-content">
<div class="canvas-container" ref="canvasContainer" />
</div>
</div>
</template>
<script setup lang="ts">
import * as THREE from 'three'
import { getCurrentInstance, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import Stats from 'three/examples/jsm/libs/stats.module'
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry'
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial'
import { Line2 } from 'three/examples/jsm/lines/Line2'
import MeasureRenderer from '@/modules/measure/MeasureRenderer.ts'
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry'
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2'
import { Text } from 'troika-three-text'
import SimSunTTF from '@/assets/fonts/simsunb.ttf'
const canvasContainer = ref(null)
let resizeObserver: ResizeObserver | null = null
let scene: THREE.Scene | null = null
let renderer: THREE.WebGLRenderer | null = null
let viewerDom: HTMLElement | null = null
let camera: THREE.PerspectiveCamera | THREE.OrthographicCamera | null = null
let controls: OrbitControls | null = null
let axesHelper: THREE.AxesHelper | null = null
let gridHelper: THREE.GridHelper | null = null
let statsControls: Stats | null = null
let animationFrameId: number | null = null
let modelGroup = new THREE.Group()
const pointMaterial = new THREE.SpriteMaterial({
color: 0xFFFF99, // 0x303133,
transparent: true,
side: THREE.DoubleSide,
opacity: 1,
sizeAttenuation: true
})
// const lineMaterial = new LineMaterial({
// color: 0xFF8C00,
// linewidth: 5,
// vertexColors: false,
// dashed: false
// })
const lineMaterial = new LineMaterial({
color: 0xFF8C00,
linewidth: 5
})
/**
* drawLine2
*/
function test1() {
cleanupThree()
const xcount = 100
const zcount = 100
const dist = 1.25
const spacing = dist
const y = 0.1
const points = []
// 创建所有点
for (let z = 0; z < zcount; z++) {
for (let x = 0; x < xcount; x++) {
const px = x * dist
const pz = z * dist
const point = new THREE.Sprite(pointMaterial)
point.position.set(px, y, pz)
point.scale.set(0.25, 0.25, 1) // 设置大小
scene.add(point)
points.push(point.position.clone())
}
}
function drawLine(p1: THREE.Vector3, p2: THREE.Vector3) {
const geometry = new LineGeometry()
geometry.setPositions([p1.x, p1.y, p1.z, p2.x, p2.y, p2.z])
const line = new Line2(geometry, lineMaterial)
line.computeLineDistances()
scene.add(line)
}
// 绘制线段(每两个点一组)
for (let z = 0; z < zcount; z++) {
for (let x = 0; x < xcount - 1; x++) {
const i = z * xcount + x
drawLine(points[i], points[i + 1])
}
}
for (let z = 0; z < zcount - 1; z++) {
for (let x = 0; x < xcount; x++) {
const i = z * xcount + x
const belowIndex = (z + 1) * xcount + x
drawLine(points[i], points[belowIndex])
}
}
refreshCount()
}
/**
* LineSegments2
*/
function test2() {
cleanupThree()
const xcount = 100
const zcount = 100
const dist = 1.25
const spacing = dist
const y = 0.1
const points = []
// 创建所有点
for (let z = 0; z < zcount; z++) {
for (let x = 0; x < xcount; x++) {
const px = x * dist
const pz = z * dist
const point = new THREE.Sprite(pointMaterial)
point.position.set(px, y, pz)
point.scale.set(0.25, 0.25, 1) // 设置大小
scene.add(point)
points.push(point.position.clone())
}
}
const positions = []
// 横向连接(右)
for (let z = 0; z < zcount; z++) {
for (let x = 0; x < xcount - 1; x++) {
const x1 = x * spacing
const z1 = z * spacing
const x2 = (x + 1) * spacing
const z2 = z * spacing
positions.push(x1, y, z1, x2, y, z2)
}
}
// 纵向连接(下)
for (let z = 0; z < zcount - 1; z++) {
for (let x = 0; x < xcount; x++) {
const x1 = x * spacing
const z1 = z * spacing
const x2 = x * spacing
const z2 = (z + 1) * spacing
positions.push(x1, y, z1, x2, y, z2)
}
}
const geometry = new LineSegmentsGeometry()
geometry.setPositions(positions)
const material = new LineMaterial({
color: 0xFF8C00,
linewidth: 5,
vertexColors: false
})
const lineSegments = new LineSegments2(geometry, material)
lineSegments.computeLineDistances() // 必须调用一次
lineSegments.name = 'grid-lines'
scene.add(lineSegments)
refreshCount()
}
function isLabelInView(label, frustum) {
const pos = new THREE.Vector3()
label.getWorldPosition(pos)
// 正交相机的视锥体范围
if (frustum.containsPoint(pos)) {
// 检查标签是否在相机位置的最大距离内
if (shouldShowLabel(label)) {
return true
}
}
return false
}
const labels: Text[] = []
function getLabelPixelSize(fontSize, cameraZoom) {
const pixelRatio = renderer.getPixelRatio()
const referenceZoom = 1
const referenceFontSize = 0.2
const referencePixelSize = 16 // fontSize=0.2, zoom=1 时显示为 16px
const scale = (fontSize / referenceFontSize) * (cameraZoom / referenceZoom)
return referencePixelSize * scale * pixelRatio
}
function shouldShowLabel(label, minPixelSize = 700) {
const pixelSize = getLabelPixelSize(label.fontSize, camera.zoom)
return pixelSize >= minPixelSize
}
/**
* InstanceMesh(Point) + BufferGeometry + Label
*/
function test3() {
cleanupThree() // 清空画布
const xcount = 300
const zcount = 100
const dist = 1.25
const spacing = dist
const y = 0.1
const noShaderMaterial = new THREE.MeshBasicMaterial({
color: 0xFFFF99,
transparent: true,
depthWrite: false,
side: THREE.DoubleSide
})
const planeGeometry = new THREE.PlaneGeometry(0.25, 0.25)
// 使用 InstancedMesh 网格优化
const instancedMesh = new THREE.InstancedMesh(planeGeometry, noShaderMaterial, zcount * xcount)
instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage)
const dummy = new THREE.Object3D()
const points = []
// 创建所有点
for (let z = 0; z < zcount; z++) {
for (let x = 0; x < xcount; x++) {
const px = x * dist
const pz = z * dist
dummy.position.set(px, y, pz)
dummy.rotation.set(-Math.PI / 2, 0, 0)
dummy.updateMatrix()
instancedMesh.setMatrixAt(z * xcount + x, dummy.matrix)
points.push(new THREE.Vector3(px, y, pz))
}
}
scene.add(instancedMesh)
const positions = []
labels.length = 0
function createTextLabel(text, position): Text {
const label = new Text()
label.text = text
label.font = SimSunTTF
label.fontSize = 0.2
label.color = '#333333'
label.opacity = 0.8
label.padding = 0.2
label.anchorX = 'center'
label.anchorY = 'middle'
label.depthOffset = 1
label.backgroundColor = '#000000'
label.backgroundOpacity = 0.6
label.material.depthTest = false
label.position.copy(position)
label.name = MeasureRenderer.LABEL_NAME
label.position.set(position.x, position.y + 0.3, position.z - 0.2)
label.quaternion.copy(camera.quaternion)
label.visible = false
// label.sync()
return label
}
// 横向连接(右)
for (let z = 0; z < zcount; z++) {
for (let x = 0; x < xcount - 1; x++) {
const x1 = x * spacing
const z1 = z * spacing
const x2 = (x + 1) * spacing
const z2 = z * spacing
positions.push(x1, y, z1, x2, y, z2)
// 计算中点和长度
const midPoint = new THREE.Vector3((x1 + x2) / 2, y + 0.5, (z1 + z2) / 2)
const length = Math.hypot(x2 - x1, y - y, z2 - z1)
const label = createTextLabel(length.toFixed(2) + 'm', midPoint)
labels.push(label)
}
}
// 纵向连接(下)
for (let z = 0; z < zcount - 1; z++) {
for (let x = 0; x < xcount; x++) {
const x1 = x * spacing
const z1 = z * spacing
const x2 = x * spacing
const z2 = (z + 1) * spacing
positions.push(x1, y, z1, x2, y, z2)
// 计算中点和长度
const midPoint = new THREE.Vector3((x1 + x2) / 2, y + 0.5, (z1 + z2) / 2)
const length = Math.hypot(x2 - x1, y - y, z2 - z1)
const label = createTextLabel(length.toFixed(2) + 'm', midPoint)
labels.push(label)
}
}
const positionNums = new Float32Array(positions)
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.BufferAttribute(positionNums, 3))
const material = new THREE.LineBasicMaterial({ color: 0xFF8C00 })
const lineSegments = new THREE.LineSegments(geometry, material)
lineSegments.name = 'grid-lines'
scene.add(lineSegments)
labels.forEach(label => scene.add(label))
// 统计总数量
refreshCount()
}
const restate = reactive({
targetColor: '#ff0000',
loadScale: 1,
viewLabelCount: 0,
mode: 'translate',
objects: 0,
vertices: 0,
faces: 0
})
// 状态变量
const state = {
showAxesHelper: true,
showGridHelper: true,
camera: {
position: { x: 0, y: 5, z: 10 },
rotation: { x: 0, y: 0, z: 0 }
}
}
onMounted(() => {
nextTick(() => {
initThree()
const viewerDom = canvasContainer.value
if (resizeObserver) {
resizeObserver.unobserve(viewerDom)
}
resizeObserver = new ResizeObserver(handleResize)
resizeObserver.observe(viewerDom)
window['cp'] = getCurrentInstance()
})
})
onBeforeUnmount(() => {
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId)
animationFrameId = null
}
cleanupThree()
const viewerDom = canvasContainer.value
if (resizeObserver) {
resizeObserver.unobserve(viewerDom)
}
window['cp'] = null
})
function initThree() {
viewerDom = canvasContainer.value
if (!viewerDom) {
console.error('Viewer DOM element not found')
return
}
// 场景
scene = new THREE.Scene()
scene.background = new THREE.Color(0xeeeeee)
// 渲染器
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
powerPreference: 'high-performance'
})
renderer.clearDepth()
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(viewerDom.clientWidth, viewerDom.clientHeight)
viewerDom.appendChild(renderer.domElement)
// 摄像机
// initMode3DCamera()
initMode2DCamera()
// 辅助线
axesHelper = new THREE.AxesHelper(5)
scene.add(axesHelper)
gridHelper = new THREE.GridHelper(1000, 1000)
scene.add(gridHelper)
gridHelper.visible = false
// 光照
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.right = '0'
statsControls.dom.style.left = 'auto'
viewerDom.appendChild(statsControls.dom)
renderer.setAnimationLoop(animate)
renderer.setClearColor(0x000000, 0)
// animate()
}
// 动画循环
let frameCount = 0
function animate() {
// animationFrameId = requestAnimationFrame(animate)
renderView()
if (frameCount++ % 60 === 0) { // 每 60 帧更新一次文本
const frustum = new THREE.Frustum()
const cameraCopy = camera.clone()
// 必须更新相机的世界矩阵
cameraCopy.updateMatrixWorld()
// 构造投影矩阵
const projScreenMatrix = new THREE.Matrix4().multiplyMatrices(
cameraCopy.projectionMatrix,
cameraCopy.matrixWorldInverse
)
// 设置视锥体
frustum.setFromProjectionMatrix(projScreenMatrix)
let viewLabelCount = 0
labels.forEach((label: Text) => {
// label.quaternion.copy(camera.quaternion) // billboard 效果保持朝向相机
const isvis = isLabelInView(label, frustum, camera.position)
if (isvis) {
viewLabelCount++
}
if (isvis && label.visible === false) {
label.visible = true
label.sync()
} else if (!isvis && label.visible === true) {
label.visible = false
label.sync()
}
})
restate.viewLabelCount = viewLabelCount
}
}
function handleResize(entries) {
for (let entry of entries) {
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.setPixelRatio(window.devicePixelRatio)
renderer.setSize(width, height)
break
}
}
/**
* 初始化2D相机
*/
function initMode2DCamera() {
if (camera) {
scene.remove(camera)
}
// ============================ 创建正交相机
const cameraNew = new THREE.OrthographicCamera(
viewerDom.clientWidth / -2,
viewerDom.clientWidth / 2,
viewerDom.clientHeight / 2,
viewerDom.clientHeight / -2,
1,
500
)
cameraNew.position.set(0, 60, 0)
cameraNew.lookAt(0, 0, 0)
cameraNew.zoom = 60
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, RIGHT: 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
cameraNew.updateProjectionMatrix()
}
function initMode3DCamera() {
if (camera) {
scene.remove(camera)
}
const viewerDom = canvasContainer.value
// ============================ 创建透视相机
const cameraNew = new THREE.PerspectiveCamera(25, viewerDom.clientWidth / viewerDom.clientHeight, 0.1, 2000)
cameraNew.position.set(5, 5, 5)
cameraNew.lookAt(0, 0, 0)
camera = cameraNew
scene.add(camera)
const controlsNew = new OrbitControls(camera, viewerDom)
controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.ROTATE } // 鼠标中键平移
controlsNew.enableDamping = false
controlsNew.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动
controlsNew.minDistance = 2
controlsNew.addEventListener('change', syncCameraState)
controls = controlsNew
controls.update()
camera.updateProjectionMatrix()
syncCameraState()
}
/**
* 重新加载相机状态到全局状态
*/
function syncCameraState() {
if (camera) {
state.camera.position.x = camera.position.x
state.camera.position.y = camera.position.y
state.camera.position.z = camera.position.z
state.camera.rotation.x = camera.rotation.x
state.camera.rotation.y = camera.rotation.y
state.camera.rotation.z = camera.rotation.z
}
}
function renderView() {
statsControls?.update()
renderer?.render(scene, camera)
}
function refreshCount() {
// 遍历场景中的所有对象
let totalObjects = 0
let totalVertices = 0
let totalFaces = 0
scene.traverse(function(child) {
if (child.isMesh) {
totalObjects++
// 获取几何体
const geometry = child.geometry
// 如果几何体是 BufferGeometry 类型
if (geometry.isBufferGeometry) {
// 计算顶点数
if (geometry.attributes.position) {
totalVertices += geometry.attributes.position.count
}
// 计算面数(假设每个面都是由三个顶点组成的三角形)
if (geometry.index) {
totalFaces += geometry.index.count / 3
} else if (geometry.attributes.position) {
// 如果没有索引,计算非索引几何体的面数
totalFaces += geometry.attributes.position.count / 3
}
}
// 如果几何体是 Geometry 类型(较旧的版本使用)
else if (geometry.isGeometry) {
// 计算顶点数
totalVertices += geometry.vertices.length
// 计算面数
totalFaces += geometry.faces.length
}
}
})
restate.objects = totalObjects
restate.vertices = totalVertices
restate.faces = totalFaces
}
function cleanupThree() {
// 移除旧模型
if (scene) {
scene.traverse((obj: THREE.Mesh) => {
// 释放几何体
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()
}
})
// 清空场景
scene.children = []
modelGroup = null
}
}
</script>
<style scoped lang="less">
.model3d-view {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
.main-content {
display: flex;
flex: 1;
overflow: hidden;
.model3d-content {
height: 100%;
display: flex;
flex-direction: row;
}
}
.canvas-container {
width: 100%;
height: 100%;
position: relative;
}
}
</style>