# feature: add interactive Earth 3d model
Opensource 3d model + Three.js
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<div ref="container" class="w-full h-full min-h-[400px]"></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>
|
||||
@@ -12,15 +12,20 @@
|
||||
</button>
|
||||
<p class="text-gray-500 text-sm mt-4">30-дневная гарантия возврата денег</p>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<!-- <div class="flex justify-center">
|
||||
<img
|
||||
src="/images/earth.png"
|
||||
alt="VPN защита"
|
||||
/>
|
||||
</div> -->
|
||||
|
||||
<div class="h-100 md:h-125 w-full">
|
||||
<Hero3D />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Zap } from '@lucide/vue'
|
||||
import Hero3D from './Hero3D.vue'
|
||||
</script>
|
||||
Reference in New Issue
Block a user