267 lines
8.7 KiB
Vue
267 lines
8.7 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
|
import {
|
|
Box3, CircleGeometry, Color, DoubleSide, Group, Mesh, MeshBasicMaterial,
|
|
MeshStandardMaterial,
|
|
MeshStandardMaterialParameters,
|
|
PlaneGeometry, SpriteMaterial, TextureLoader, Vector2, Vector3,
|
|
} from 'three';
|
|
|
|
import { useTresContext, useSeek, useRenderLoop, useTexture } from '@tresjs/core';
|
|
import { useGLTF } from '@tresjs/cientos'
|
|
|
|
import Env from './env.vue'
|
|
|
|
import { IMAGE_URL, PROMOBG, SERVER_URL, } from '../../constants'
|
|
import { usePromoSidebar } from '../../stores/promo_sidebar';
|
|
import { usePromoScene } from '../../stores/promo_scene';
|
|
|
|
const props = defineProps(['source', 'loaded', 'loaded_pan'])
|
|
|
|
const models = ref<model3DType[]>([])
|
|
const clickable = ref<clickableAreaType[]>([])
|
|
const clickable_items = ref<any[]>([])
|
|
const clickable_refs = ref<any[]>([])
|
|
const sidebar = usePromoSidebar();
|
|
const sidebar_scene = usePromoScene()
|
|
const { controls, camera, scene, raycaster, renderer } = useTresContext()
|
|
const { seekByName, seekAllByName } = useSeek()
|
|
const envVars = reactive({}) as {
|
|
focus: number,
|
|
hdr_gainmap?: string,
|
|
hdr_json?: string,
|
|
hdr_webp?: string,
|
|
clear_color?: string,
|
|
env_displacementmap?: string,
|
|
env_normalmap?: string
|
|
}
|
|
|
|
|
|
const groundTexture = await useTexture({
|
|
displacementMap: '/ground_displacement.jpg',
|
|
})
|
|
|
|
// renderer.value.capabilities.maxTextures = 4
|
|
// renderer.value.capabilities.maxTextureSize = 512
|
|
// renderer.value.capabilities.precision = 'lowp'
|
|
|
|
const loadModels = async () => {
|
|
const res = await fetch(`${SERVER_URL}/api/obj/scene/${props.source}`)
|
|
const raw_data = await res.json() as scene3D
|
|
|
|
envVars.focus = raw_data.min_distance * 1
|
|
if (raw_data.env) {
|
|
Object.assign(envVars, raw_data.env)
|
|
} else {
|
|
delete envVars.hdr_gainmap
|
|
delete envVars.hdr_json
|
|
delete envVars.hdr_webp
|
|
envVars.clear_color = PROMOBG
|
|
}
|
|
|
|
|
|
const data = raw_data.elements
|
|
if (!controls.value) return;
|
|
|
|
camera.value?.position.set(1, 1, 1);
|
|
controls.value.enabled = false;
|
|
(controls.value as any).minDistance = raw_data.min_distance;
|
|
(controls.value as any).maxDistance = raw_data.max_distance;
|
|
(controls.value as any)._needsUpdate = true;
|
|
(controls.value as any).update()
|
|
|
|
const sidebar_items = []
|
|
clickable_items.value = []
|
|
for (let index = 0; index < data.length; index++) {
|
|
const element = data[index];
|
|
sidebar_items.push({ ...element })
|
|
const item = {} as model3DType
|
|
|
|
item.modelUrl = `${IMAGE_URL}/${element.model_file}`
|
|
let { scene: loaded_scene } = await useGLTF(item.modelUrl)
|
|
item.modelFile = loaded_scene
|
|
item.name = element.name
|
|
|
|
if (!element.is_enabled) {
|
|
item.modelFile.visible = false
|
|
}
|
|
|
|
models.value.push(item)
|
|
|
|
const res = await fetch(`${SERVER_URL}/api/obj/clickable/?source=${element.id}`)
|
|
const clickable_areas = await res.json()
|
|
clickable.value.push(...clickable_areas)
|
|
}
|
|
|
|
|
|
let c = new Color()
|
|
if (envVars.clear_color) {
|
|
c.set(envVars.clear_color)
|
|
} else {
|
|
renderer.value.getClearColor(c)
|
|
}
|
|
const tex = {} as any
|
|
if (envVars.env_displacementmap) { tex.displacementMap = `${IMAGE_URL}/${envVars.env_displacementmap}` }
|
|
if (envVars.env_normalmap) { tex.normalMap = `${IMAGE_URL}/${envVars.env_normalmap}` }
|
|
let addTexture: any
|
|
if (Object.keys(tex).length > 0) {
|
|
addTexture = await useTexture(tex)
|
|
}
|
|
|
|
const mesh = {
|
|
color: c.offsetHSL(0, 0, -0.33),
|
|
displacementScale: envVars.focus,
|
|
roughness: 100,
|
|
side: DoubleSide
|
|
} as MeshStandardMaterialParameters
|
|
if (envVars.env_displacementmap) {
|
|
mesh.displacementMap = addTexture.displacementMap
|
|
} else {
|
|
mesh.displacementMap = groundTexture.displacementMap
|
|
}
|
|
if (envVars.env_normalmap) {
|
|
mesh.normalMap = addTexture.normalMap
|
|
}
|
|
const ground = new Mesh(
|
|
new CircleGeometry(envVars.focus * 10, envVars.focus * 10),
|
|
new MeshStandardMaterial(mesh)
|
|
)
|
|
ground.position.y = -0.33 * envVars.focus
|
|
ground.rotateX(-Math.PI / 2)
|
|
ground.name = "ground"
|
|
models.value.push({ name: 'ground', modelFile: ground })
|
|
|
|
sidebar_scene.setData(sidebar_items)
|
|
|
|
for (let index = 0; index < clickable.value.length; index++) {
|
|
const element = clickable.value[index];
|
|
const find_element = seekByName(scene.value, element.object_name)
|
|
if (!find_element) continue
|
|
if (find_element && !(find_element as Group).isGroup) {
|
|
const world_position = new Vector3();
|
|
((find_element as Mesh).geometry.boundingBox as any).getCenter(world_position);
|
|
(find_element as Mesh).localToWorld(world_position);
|
|
|
|
const p = raw_data.min_distance * 0.05
|
|
const plane = new PlaneGeometry(p, p, 32)
|
|
|
|
const mesh_material = new MeshBasicMaterial({ side: DoubleSide })
|
|
const sprite_material = new SpriteMaterial()
|
|
if (element.image) {
|
|
const map = new TextureLoader().load(`${IMAGE_URL}/${element.image}`);
|
|
mesh_material.map = map
|
|
sprite_material.map = map
|
|
} else {
|
|
mesh_material.color = new Color('red')
|
|
sprite_material.color = new Color('red')
|
|
}
|
|
|
|
const point = new Mesh(plane, mesh_material);
|
|
point.position.set(world_position.x, p * 3, world_position.z)
|
|
point.name = `${element.id}_clickable`
|
|
point.renderOrder = 10
|
|
|
|
if (clickable_items.value.find(el => el.name == point.name)) continue
|
|
clickable_items.value.push(point)
|
|
clickable_refs.value.push(ref(`${element.id}_clickable`))
|
|
}
|
|
}
|
|
|
|
const loaded = seekByName(scene.value, 'loaded')
|
|
if (loaded) {
|
|
const box = new Box3();
|
|
box.expandByObject(loaded);
|
|
const box_size = new Vector3();
|
|
box.getSize(box_size)
|
|
props.loaded_pan(
|
|
new Vector3(box_size.x * 0.5, box_size.y * 0.5, box_size.z * 0.5),
|
|
new Vector3(box_size.x * -0.5, box_size.y * -0.25, box_size.z * -0.5),
|
|
)
|
|
}
|
|
|
|
controls.value.enabled = true;
|
|
props.loaded(false)
|
|
}
|
|
|
|
const { onLoop } = useRenderLoop()
|
|
onLoop(() => {
|
|
clickable_refs.value.map(el => {
|
|
if (el.value[0] && typeof el.value[0].lookAt == 'function') {
|
|
el.value[0].lookAt(camera.value?.position)
|
|
}
|
|
})
|
|
})
|
|
|
|
const openSidebar = (id: number) => {
|
|
const target = clickable.value.find(el => el.id == id)
|
|
if (!target) return
|
|
const sidebar_data = {
|
|
title: target.name,
|
|
description: target.description
|
|
} as PromoSidebarData
|
|
if (target?.target) {
|
|
sidebar_data.target = target.target.toString()
|
|
sidebar_data.target_name = target.target_name
|
|
}
|
|
sidebar.setData(sidebar_data)
|
|
sidebar.open()
|
|
}
|
|
|
|
loadModels()
|
|
watch(() => props.source, () => {
|
|
const loaded = seekByName(scene.value, 'loaded')
|
|
if (loaded) {
|
|
loaded.children = []
|
|
}
|
|
sidebar.close()
|
|
loadModels()
|
|
})
|
|
|
|
onMounted(() => {
|
|
document.addEventListener('click', clickEvent)
|
|
if (sidebar.is_open) {
|
|
sidebar.close()
|
|
}
|
|
})
|
|
onUnmounted(() => { document.removeEventListener('click', clickEvent) })
|
|
const pointer = reactive({ x: 0, y: 0 })
|
|
const clickEvent = (event: MouseEvent) => {
|
|
const x = (event.clientX / window.innerWidth) * 2 - 1
|
|
const y = - (event.clientY / window.innerHeight) * 2 + 1
|
|
if (x == pointer.x && y == pointer.y) return
|
|
if (!camera.value) return
|
|
|
|
pointer.x = x
|
|
pointer.y = y
|
|
raycaster.value.setFromCamera(new Vector2(pointer.x, pointer.y), camera.value);
|
|
|
|
const intersects = raycaster.value.intersectObjects(seekAllByName(scene.value, '_clickable'));
|
|
const names = intersects.map(el => el.object.name ?? false).filter(Boolean)
|
|
if (names.length) {
|
|
openSidebar(parseInt(names[0].replace('_clickable', '')))
|
|
}
|
|
}
|
|
|
|
watch(() => sidebar_scene.list, () => {
|
|
sidebar_scene.list.forEach(element => {
|
|
const el = seekByName(scene.value, element.name)
|
|
if (!el) return
|
|
if (el.visible !== element.is_enabled) {
|
|
el.visible = element.is_enabled
|
|
}
|
|
});
|
|
}, { deep: true })
|
|
</script>
|
|
<template>
|
|
<TresGroup name="loaded">
|
|
<Env v-bind="envVars" />
|
|
<template v-for="item in models">
|
|
<TresGroup :name="item.name">
|
|
<TresObject3D v-bind="item.modelFile.clone()" />
|
|
</TresGroup>
|
|
</template>
|
|
<template v-for="(item, i) in clickable_items">
|
|
<TresMesh v-bind="item" :ref="clickable_refs[i]" />
|
|
</template>
|
|
</TresGroup>
|
|
</template> |