- 方式A:纯 DOM 投影(推荐,完全不影响 Three 的鼠标交互)
- 计算模型上方世界点
object.updateWorldMatrix(true, true) const box = new THREE.Box3().setFromObject(object) if (box.isEmpty()) { const p = new THREE.Vector3(); object.getWorldPosition(p); p.y += 0.5 // p 即标签世界点 } const center = new THREE.Vector3(), size = new THREE.Vector3() box.getCenter(center); box.getSize(size) const worldPos = new THREE.Vector3(center.x, box.max.y + THREE.MathUtils.clamp(size.y*0.1, 0.3, 2), center.z) ``` 2) 投影为屏幕像素坐标并定位 DOM ```ts function worldToScreen(p: THREE.Vector3, camera: THREE.Camera, renderer: THREE.WebGLRenderer) { const ndc = p.clone().project(camera) const rect = renderer.domElement.getBoundingClientRect() const x = rect.left + (ndc.x + 1) * 0.5 * rect.width const y = rect.top + (1 - (ndc.y + 1) * 0.5) * rect.height return { x, y } } // 创建 DOM 标签 const el = document.createElement('div') el.className = 'task-marker' el.style.cssText = 'position:fixed;left:0;top:0;transform:translate(-9999px,-9999px);z-index:16000;pointer-events:auto;' el.innerHTML = '标题' document.body.appendChild(el) // 每帧更新位置 function loop() { const { x, y } = worldToScreen(worldPos, camera, renderer) el.style.transform = `translate(${x}px, ${y}px)` requestAnimationFrame(loop) } loop() ``` 3) 点击事件(不影响其他交互) - 直接绑定在标签 DOM(不会拦截画布外的鼠标事件) ```ts el.addEventListener('click', (e) => { e.stopPropagation() openTaskDetail(task) // 打开你自己的弹层 }) ``` 4) 清理 ```ts cancelAnimationFrame(rafId) el.remove() ``` - 方式B:CSS2DObject + CSS2DRenderer(Three 自带) 1) 准备 DOM 并创建 `CSS2DObject` ```ts const div = document.createElement('div') div.className = 'task-marker' div.style.pointerEvents = 'auto' div.innerHTML = ' 标题' const label = new CSS2DObject(div) label.position.copy(worldPos) viewer.sceneHelpers.add(label) ``` 2) 点击事件 ```ts div.addEventListener('click', (e) => { e.stopPropagation() openTaskDetail(task) }) ``` 3) 关键注意 - 确保容器 `viewer.css2DRenderer.domElement` 设置合适的 `z-index`,且不要把 `pointer-events:none` 应用到容器(否则点击不到)。 - 若与 OrbitControls/其它捕获冲突,可在标签元素上加捕获监听: ```ts div.addEventListener('pointerdown', (e) => e.stopPropagation(), { capture: true }) ``` ### 最佳实践与细节 - 偏移高度自适应:用 `size.y * 0.1` 并做 clamp,避免大小模型出现过高/过低。 - 可见性优化:投影后若在屏幕外或被相机背面(ndc.z > 1 或 ndc.z < -1),可以隐藏标签。 - 防遮挡/重叠:标签多时可做简单碰撞/避让(例如同列纵向错开、聚合)。 - 清理与重建:场景重新加载或对象销毁时,记得移除 DOM、取消 RAF、清空引用,避免内存泄漏。 - 弹层:挂载到 body,较高 z-index;打开时不要改变画布的 pointer-events,这样完全不影响 3D 交互。 如果你告诉我标签的样式和需要展示的字段,我可以把以上封装成两个小工具:createDomLabel 与 bindTaskClick,直接复用。2) 投影为屏幕像素坐标并定位 DOM
function worldToScreen(p: THREE.Vector3, camera: THREE.Camera, renderer: THREE.WebGLRenderer) { const ndc = p.clone().project(camera) const rect = renderer.domElement.getBoundingClientRect() const x = rect.left + (ndc.x + 1) * 0.5 * rect.width const y = rect.top + (1 - (ndc.y + 1) * 0.5) * rect.height return { x, y } } // 创建 DOM 标签 const el = document.createElement('div') el.className = 'task-marker' el.style.cssText = 'position:fixed;left:0;top:0;transform:translate(-9999px,-9999px);z-index:16000;pointer-events:auto;' el.innerHTML = '标题' document.body.appendChild(el) // 每帧更新位置 function loop() { const { x, y } = worldToScreen(worldPos, camera, renderer) el.style.transform = `translate(${x}px, ${y}px)` requestAnimationFrame(loop) } loop() - 方式B:CSS2DObject + CSS2DRenderer(Three 自带)
const div = document.createElement('div') div.className = 'task-marker' div.style.pointerEvents = 'auto' div.innerHTML = ' 标题' const label = new CSS2DObject(div) label.position.copy(worldPos) viewer.sceneHelpers.add(label)
浙公网安备 33010602011771号