打标签
本章你可以学习到
- 在三维空间中拾取点并映射到屏幕,进行 DOM 标签渲染。
- 通过
project2d
在相机移动时刷新标签位置。
Vue 示例:屏幕标签
- JavaScript
- TypeScript
<template>
<div style="width: 100vw; height: 100vh">
<div ref="container" style="width: 100%; height: 100%; overflow: hidden; position: relative" />
<div v-for="t in validTags" :key="t.id" :style="t.style">
<div style="background: #333; color: #fff; padding: 6px 10px; border-radius: 4px; font-size: 12px">标签</div>
</div>
<div style="position: fixed; top: 16px; left: 16px"><button class="btn btn-primary" @click="enabled = !enabled">{{
enabled ? '关闭拾取' : '开启拾取' }}</button></div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, computed } from 'vue'
import { Five, parseWork } from '@realsee/five'
const container = ref(null)
let five = null
const enabled = ref(false)
const tags = ref([]) // { id, position: THREE.Vector3 }
const tick = ref(0)
const workURL = 'https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json'
// 计算属性:安全地过滤和投影标签
const validTags = computed(() => {
// 依赖 tick 值来触发重新计算
tick.value
return tags.value
.filter(tag => tag && tag.position) // 确保 tag 和 position 都存在
.map(tag => {
const projected = project(tag.position)
return projected ? { ...tag, projected, style: toStyle(projected) } : null
})
.filter(Boolean) // 移除无效的投影结果
})
onMounted(() => {
five = new Five()
if (container.value) five.appendTo(container.value)
fetch(workURL).then(r => r.json()).then(json => five.load(parseWork(json)))
const onResize = () => five.refresh()
window.addEventListener('resize', onResize, false)
const onCameraUpdate = () => { tick.value++ }
five.on('cameraUpdate', onCameraUpdate)
const onWantsTap = (raycaster) => {
if (!enabled.value) return true
const hit = five.model.intersectRaycaster(raycaster)
if (hit && hit[0] && hit[0].point) {
tags.value.push({ id: Date.now(), position: hit[0].point })
}
return false
}
five.on('wantsTapGesture', onWantsTap)
onUnmounted(() => {
window.removeEventListener('resize', onResize, false)
five.off('cameraUpdate', onCameraUpdate)
five.off('wantsTapGesture', onWantsTap)
five.dispose()
})
})
function project(pos) {
if (!five || !pos) return null
return five.project2d(pos, true)
}
function toStyle(v2) {
if (!v2 || typeof v2.x !== 'number' || typeof v2.y !== 'number') {
return { display: 'none' }
}
return { position: 'absolute', transform: 'translate(-50%, 0)', left: `${v2.x}px`, top: `${v2.y}px` }
}
</script>
<template>
<div style="width: 100vw; height: 100vh">
<div ref="container" style="width: 100%; height: 100%; overflow: hidden; position: relative" />
<div v-for="t in validTags" :key="t.id" :style="t.style">
<div style="background: #333; color: #fff; padding: 6px 10px; border-radius: 4px; font-size: 12px">标签</div>
</div>
<div style="position: fixed; top: 16px; left: 16px"><button class="btn btn-primary" @click="enabled = !enabled">{{
enabled ? '关闭拾取' : '开启拾取' }}</button></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, computed } from 'vue'
import { Five, parseWork } from '@realsee/five'
import type { Vector2, Vector3 } from 'three'
const container = ref<HTMLDivElement | null>(null)
let five: Five | null = null
const enabled = ref(false)
type Tag = { id: number; position: Vector3 }
const tags = ref<Tag[]>([])
const tick = ref(0)
const workURL = 'https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json'
// 计算属性:安全地过滤和投影标签
const validTags = computed(() => {
// 依赖 tick 值来触发重新计算
tick.value
return tags.value
.filter(tag => tag && tag.position) // 确保 tag 和 position 都存在
.map(tag => {
const projected = project(tag.position)
return projected ? { ...tag, projected, style: toStyle(projected) } : null
})
.filter(Boolean) // 移除无效的投影结果
})
onMounted(() => {
five = new Five()
if (container.value) five.appendTo(container.value)
fetch(workURL).then(r => r.json()).then(json => five!.load(parseWork(json)))
const onResize = () => five!.refresh()
window.addEventListener('resize', onResize, false)
const onCameraUpdate = () => { tick.value++ }
five.on('cameraUpdate', onCameraUpdate)
const onWantsTap = (raycaster: any) => {
if (!enabled.value) return true
const hit = five!.model.intersectRaycaster(raycaster)
if (hit && hit[0] && hit[0].point) {
tags.value.push({ id: Date.now(), position: hit[0].point })
}
return false
}
five.on('wantsTapGesture', onWantsTap)
onUnmounted(() => {
window.removeEventListener('resize', onResize, false)
five!.off('cameraUpdate', onCameraUpdate)
five!.off('wantsTapGesture', onWantsTap)
five!.dispose()
})
})
function project(pos: Vector3): Vector2 | null {
if (!five || !pos) return null
return five.project2d(pos, true)
}
function toStyle(v2: Vector2) {
if (!v2 || typeof v2.x !== 'number' || typeof v2.y !== 'number') {
return { display: 'none' }
}
return { position: 'absolute', transform: 'translate(-50%, 0)', left: `${v2.x}px`, top: `${v2.y}px` }
}
</script>
要点:
- 使用
wantsTapGesture
阻止默认点位移动; - 通过
five.model.intersectRaycaster
拾取三维点; - 使用
cameraUpdate
触发重渲染,project2d
将三维点映射为屏幕坐标。