189 lines
6.2 KiB
Vue
189 lines
6.2 KiB
Vue
<template>
|
|
<div ref="container" class="w-full h-full min-h-100"></div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
import * as THREE from 'three'
|
|
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'
|
|
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js'
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
|
|
|
const container = ref(null)
|
|
let renderer, scene, camera, controls, animationId
|
|
|
|
onMounted(() => {
|
|
initScene()
|
|
loadModel()
|
|
animate()
|
|
window.addEventListener('resize', onResize)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
cancelAnimationFrame(animationId)
|
|
renderer?.dispose()
|
|
window.removeEventListener('resize', onResize)
|
|
})
|
|
|
|
function initScene() {
|
|
scene = new THREE.Scene()
|
|
scene.background = null
|
|
|
|
camera = new THREE.PerspectiveCamera(
|
|
45,
|
|
container.value.clientWidth / container.value.clientHeight,
|
|
0.01,
|
|
1000
|
|
)
|
|
camera.position.set(0, 0, 5)
|
|
|
|
renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
|
|
renderer.setSize(container.value.clientWidth, container.value.clientHeight)
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
|
container.value.appendChild(renderer.domElement)
|
|
|
|
// Освещение
|
|
scene.add(new THREE.AmbientLight(0x404060, 1.8))
|
|
const sun = new THREE.DirectionalLight(0xffffff, 2.2)
|
|
sun.position.set(5, 3, 5)
|
|
scene.add(sun)
|
|
const back = new THREE.DirectionalLight(0x3366aa, 0.5)
|
|
back.position.set(-3, -1, -3)
|
|
scene.add(back)
|
|
|
|
// OrbitControls (только вращение)
|
|
controls = new OrbitControls(camera, renderer.domElement)
|
|
controls.enableDamping = true
|
|
controls.dampingFactor = 0.08
|
|
controls.autoRotate = true
|
|
controls.autoRotateSpeed = 0.4
|
|
controls.enableZoom = false
|
|
controls.enablePan = false
|
|
controls.target.set(0, 0, 0)
|
|
}
|
|
|
|
function loadModel() {
|
|
const basePath = '/models/earth/'
|
|
|
|
new MTLLoader()
|
|
.setPath(basePath)
|
|
.load('Earth 2K.mtl', (materials) => {
|
|
materials.preload()
|
|
|
|
new OBJLoader()
|
|
.setMaterials(materials)
|
|
.setPath(basePath)
|
|
.load('Earth 2K.obj', (object) => {
|
|
// Применяем текстуры (с DoubleSide и needsUpdate)
|
|
applyTextures(object)
|
|
|
|
// Группа только для непрозрачной Земли (без облаков и атмосферы)
|
|
const earthGroup = new THREE.Group()
|
|
object.traverse((child) => {
|
|
if (!child.isMesh) return
|
|
const matName = child.material?.name?.toLowerCase() || ''
|
|
if (!matName.includes('atmosphere') && !matName.includes('cloud')) {
|
|
earthGroup.add(child.clone())
|
|
}
|
|
})
|
|
|
|
// Вычисляем bounding sphere только для Земли
|
|
const earthSphere = new THREE.Sphere()
|
|
if (earthGroup.children.length > 0) {
|
|
new THREE.Box3().setFromObject(earthGroup).getBoundingSphere(earthSphere)
|
|
} else {
|
|
// fallback
|
|
new THREE.Box3().setFromObject(object).getBoundingSphere(earthSphere)
|
|
}
|
|
|
|
// Центрируем ВСЮ модель (с облаками) по центру Земли
|
|
object.position.sub(earthSphere.center)
|
|
object.position.y += 0.1
|
|
|
|
// Расстояние камеры, чтобы земная сфера заняла экран с отступом 2%
|
|
const radius = earthSphere.radius * 0.6
|
|
const aspect = container.value.clientWidth / container.value.clientHeight
|
|
const fovRad = camera.fov * Math.PI / 180
|
|
const distanceH = radius / Math.sin(fovRad / 2) / aspect
|
|
const distanceV = radius / Math.sin(fovRad / 2)
|
|
const finalDistance = Math.max(distanceH, distanceV)
|
|
|
|
camera.position.set(0, radius * 0.1, finalDistance)
|
|
camera.lookAt(0, 0, 0)
|
|
controls.target.set(0, 0, 0)
|
|
controls.update()
|
|
|
|
scene.add(object)
|
|
console.log('🌍 Земля вписана, радиус Земли:', earthSphere.radius.toFixed(2))
|
|
})
|
|
})
|
|
}
|
|
|
|
function applyTextures(object) {
|
|
const loader = new THREE.TextureLoader()
|
|
const texPath = '/models/earth/Textures/'
|
|
|
|
const textures = {
|
|
diffuse: loader.load(texPath + 'Diffuse_2K.png'),
|
|
bump: loader.load(texPath + 'Bump_2K.png'),
|
|
clouds: loader.load(texPath + 'Clouds_2K.png'),
|
|
night: loader.load(texPath + 'Night_lights_2K.png'),
|
|
oceanMask: loader.load(texPath + 'Ocean_Mask_2K.png')
|
|
}
|
|
|
|
object.traverse((child) => {
|
|
if (!child.isMesh) return
|
|
const matName = child.material?.name?.toLowerCase() || ''
|
|
|
|
if (matName.includes('atmosphere')) {
|
|
child.material = new THREE.MeshStandardMaterial({
|
|
color: 0x4488ff,
|
|
transparent: true,
|
|
opacity: 0.1, // уменьшена плотность
|
|
roughness: 1,
|
|
metalness: 0,
|
|
side: THREE.DoubleSide,
|
|
depthWrite: false // не перекрывает то, что сзади
|
|
})
|
|
} else if (matName.includes('cloud')) {
|
|
child.material = new THREE.MeshStandardMaterial({
|
|
map: textures.clouds,
|
|
transparent: true,
|
|
opacity: 0.5, // облака стали прозрачнее
|
|
roughness: 0.8,
|
|
metalness: 0,
|
|
side: THREE.DoubleSide,
|
|
depthWrite: false // ключевая строка
|
|
})
|
|
} else {
|
|
child.material = new THREE.MeshStandardMaterial({
|
|
map: textures.diffuse,
|
|
bumpMap: textures.bump,
|
|
bumpScale: 0.04,
|
|
roughnessMap: textures.oceanMask,
|
|
roughness: 0.6,
|
|
metalnessMap: textures.oceanMask,
|
|
metalness: 0.2,
|
|
emissiveMap: textures.night,
|
|
emissive: new THREE.Color(0xffaa33),
|
|
emissiveIntensity: 0.5,
|
|
side: THREE.DoubleSide
|
|
})
|
|
}
|
|
child.material.needsUpdate = true
|
|
})
|
|
}
|
|
|
|
function onResize() {
|
|
if (!container.value) return
|
|
camera.aspect = container.value.clientWidth / container.value.clientHeight
|
|
camera.updateProjectionMatrix()
|
|
renderer.setSize(container.value.clientWidth, container.value.clientHeight)
|
|
}
|
|
|
|
function animate() {
|
|
animationId = requestAnimationFrame(animate)
|
|
controls.update()
|
|
renderer.render(scene, camera)
|
|
}
|
|
</script> |