175 lines
5.0 KiB
Vue
175 lines
5.0 KiB
Vue
<template>
|
|
<div ref="container" class="w-full h-full min-h-[300px] bg-gray-900 rounded-lg overflow-hidden"></div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
|
import * as THREE from 'three'
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
|
|
|
const props = defineProps<{
|
|
landmarks: number[][] // 468 x [x, y, z]
|
|
}>()
|
|
|
|
const container = ref<HTMLElement>()
|
|
let renderer: THREE.WebGLRenderer | null = null
|
|
let scene: THREE.Scene | null = null
|
|
let camera: THREE.PerspectiveCamera | null = null
|
|
let controls: OrbitControls | null = null
|
|
let animId: number
|
|
let objects: THREE.Object3D[] = []
|
|
|
|
function disposeScene() {
|
|
cancelAnimationFrame(animId)
|
|
for (const obj of objects) {
|
|
scene?.remove(obj)
|
|
if (obj instanceof THREE.Mesh) {
|
|
obj.geometry?.dispose()
|
|
if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose())
|
|
else obj.material?.dispose()
|
|
}
|
|
if (obj instanceof THREE.Points) {
|
|
obj.geometry?.dispose()
|
|
if (obj.material) obj.material.dispose()
|
|
}
|
|
}
|
|
objects = []
|
|
controls?.dispose()
|
|
controls = null
|
|
if (renderer) {
|
|
renderer.dispose()
|
|
renderer = null
|
|
}
|
|
scene = null
|
|
camera = null
|
|
}
|
|
|
|
const FACES_TRI = [
|
|
// Eyes
|
|
[33, 133, 7], [33, 7, 163], [160, 159, 158], [159, 158, 157],
|
|
// Nose
|
|
[168, 6, 197], [6, 197, 195], [197, 195, 5],
|
|
// Mouth outer
|
|
[61, 146, 91], [91, 181, 84], [84, 17, 314], [314, 405, 321],
|
|
// Mouth inner
|
|
[78, 95, 88], [95, 88, 178], [87, 14, 317], [14, 317, 402],
|
|
// Jaw
|
|
[10, 338, 297], [297, 332, 284], [284, 251, 389],
|
|
// Left eye brow
|
|
[46, 53, 52], [53, 52, 65],
|
|
// Right eye brow
|
|
[276, 283, 282], [283, 282, 295],
|
|
// Face oval
|
|
[10, 338, 297], [297, 332, 284], [284, 251, 389], [389, 356, 454],
|
|
[454, 323, 361], [361, 288, 397], [397, 365, 379], [379, 378, 400],
|
|
[400, 377, 152], [152, 148, 176], [176, 149, 150], [150, 136, 172],
|
|
[172, 58, 132], [132, 93, 234], [234, 127, 162], [162, 21, 54],
|
|
[54, 103, 67], [67, 109, 10]
|
|
]
|
|
|
|
function buildMesh(pts: number[][]): THREE.BufferGeometry {
|
|
const verts = new Float32Array(pts.length * 3)
|
|
for (let i = 0; i < pts.length; i++) {
|
|
verts[i * 3] = (pts[i][0] - 0.5) * 2
|
|
verts[i * 3 + 1] = -(pts[i][1] - 0.5) * 2
|
|
verts[i * 3 + 2] = pts[i][2] * 2
|
|
}
|
|
const indices: number[] = []
|
|
for (const tri of FACES_TRI) {
|
|
if (tri.every(i => i < pts.length)) indices.push(...tri)
|
|
}
|
|
const geo = new THREE.BufferGeometry()
|
|
geo.setAttribute('position', new THREE.BufferAttribute(verts, 3))
|
|
geo.setIndex(indices)
|
|
geo.computeVertexNormals()
|
|
return geo
|
|
}
|
|
|
|
function init() {
|
|
if (!container.value) return
|
|
|
|
// Dispose previous scene if re-initializing
|
|
disposeScene()
|
|
|
|
const rect = container.value.getBoundingClientRect()
|
|
const w = rect.width || 400, h = rect.height || 300
|
|
|
|
scene = new THREE.Scene()
|
|
camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 10)
|
|
camera.position.set(0, 0, 2.5)
|
|
|
|
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
|
|
renderer.setSize(w, h)
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
|
container.value.appendChild(renderer.domElement)
|
|
|
|
controls = new OrbitControls(camera, renderer.domElement)
|
|
controls.enableDamping = true
|
|
controls.dampingFactor = 0.05
|
|
controls.autoRotate = true
|
|
controls.autoRotateSpeed = 2
|
|
|
|
// Mesh
|
|
if (props.landmarks?.length) {
|
|
const geo = buildMesh(props.landmarks)
|
|
const mat = new THREE.MeshPhongMaterial({
|
|
color: 0x4488ff,
|
|
flatShading: false,
|
|
transparent: true,
|
|
opacity: 0.85,
|
|
side: THREE.DoubleSide
|
|
})
|
|
const mesh = new THREE.Mesh(geo, mat)
|
|
scene.add(mesh)
|
|
objects.push(mesh)
|
|
|
|
// Points
|
|
const ptGeo = new THREE.BufferGeometry()
|
|
ptGeo.setAttribute('position', geo.getAttribute('position')!)
|
|
const ptMat = new THREE.PointsMaterial({ color: 0x88bbff, size: 0.008 })
|
|
const points = new THREE.Points(ptGeo, ptMat)
|
|
scene.add(points)
|
|
objects.push(points)
|
|
}
|
|
|
|
// Lights
|
|
const ambient = new THREE.AmbientLight(0x404060)
|
|
scene.add(ambient)
|
|
const dir = new THREE.DirectionalLight(0xffffff, 1)
|
|
dir.position.set(1, 1, 1)
|
|
scene.add(dir)
|
|
const dir2 = new THREE.DirectionalLight(0x8888ff, 0.5)
|
|
dir2.position.set(-1, -1, 0.5)
|
|
scene.add(dir2)
|
|
|
|
// Resize observer
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
if (!container.value || !renderer || !camera) return
|
|
const r = container.value.getBoundingClientRect()
|
|
const w = r.width || 400, h = r.height || 300
|
|
renderer.setSize(w, h)
|
|
camera.aspect = w / h
|
|
camera.updateProjectionMatrix()
|
|
})
|
|
resizeObserver.observe(container.value)
|
|
;(container.value as any).__resizeObserver = resizeObserver
|
|
|
|
animate()
|
|
}
|
|
|
|
function animate() {
|
|
animId = requestAnimationFrame(animate)
|
|
controls?.update()
|
|
if (renderer && scene && camera) renderer.render(scene, camera)
|
|
}
|
|
|
|
onMounted(() => init())
|
|
onBeforeUnmount(() => {
|
|
cancelAnimationFrame(animId)
|
|
if ((container.value as any)?.__resizeObserver) {
|
|
(container.value as any).__resizeObserver.disconnect()
|
|
}
|
|
disposeScene()
|
|
})
|
|
</script>
|