# feature: add interactive Earth 3d model

Opensource 3d model + Three.js
This commit is contained in:
austnv
2026-05-24 03:35:37 +03:00
parent 88b4250f4c
commit e57ff98f4a
12 changed files with 5489 additions and 5 deletions
+189
View File
@@ -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>
+6 -1
View File
@@ -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>