303 lines
9.4 KiB
JavaScript
303 lines
9.4 KiB
JavaScript
import * as THREE from 'three';
|
||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
||
import Stats from 'three/examples/jsm/libs/stats.module';
|
||
import TWEEN from '@tweenjs/tween.js';
|
||
import { deepMerge, isType } from '@/utils';
|
||
|
||
export default class Earth3d {
|
||
constructor(options = {}) {
|
||
let defaultOptions = {
|
||
isFull: true,
|
||
container: null,
|
||
width: window.innerWidth,
|
||
height: window.innerHeight,
|
||
bgColor: 0x0e2438, // 深蓝偏亮背景,替代纯黑以提升亮度
|
||
materialColor: 0xff0000,
|
||
controls: {
|
||
visibel: true, // 是否开启
|
||
enableDamping: true, // 阻尼
|
||
autoRotate: false, // 自动旋转
|
||
maxPolarAngle: Math.PI, // 相机垂直旋转角度的上限
|
||
},
|
||
statsVisibel: true,
|
||
axesVisibel: true,
|
||
axesHelperSize: 250, // 左边尺寸
|
||
};
|
||
this.options = deepMerge(defaultOptions, options);
|
||
this.container = document.querySelector(this.options.container);
|
||
|
||
// 确保容器尺寸有效,避免Canvas尺寸为0的错误
|
||
this.options.width = Math.max(this.container.offsetWidth, 800);
|
||
this.options.height = Math.max(this.container.offsetHeight, 600);
|
||
|
||
// 如果容器尺寸仍然为0,使用默认值
|
||
if (this.options.width === 0 || this.options.height === 0) {
|
||
this.options.width = 800;
|
||
this.options.height = 600;
|
||
}
|
||
this.scene = new THREE.Scene(); // 场景
|
||
this.camera = null; // 相机
|
||
this.renderer = null; // 渲染器
|
||
this.mesh = null; // 网格
|
||
this.animationStop = null; // 用于停止动画
|
||
this.controls = null; // 轨道控制器
|
||
this.stats = null; // 统计
|
||
|
||
this.init();
|
||
}
|
||
init() {
|
||
this.initStats();
|
||
this.initCamera();
|
||
this.initModel();
|
||
this.initRenderer(); // 异步初始化,其他组件在continueInit中初始化
|
||
}
|
||
async initModel() {}
|
||
|
||
/**
|
||
* 运行
|
||
*/
|
||
run() {
|
||
// 如果渲染器已经准备好,直接开始循环
|
||
if (this.renderer) {
|
||
this.loop();
|
||
} else {
|
||
// 否则标记需要启动,等待渲染器创建完成
|
||
this.shouldStart = true;
|
||
}
|
||
}
|
||
// 循环
|
||
loop() {
|
||
// 检查渲染器是否存在
|
||
if (!this.renderer) {
|
||
return;
|
||
}
|
||
|
||
this.animationStop = window.requestAnimationFrame(() => {
|
||
this.loop();
|
||
});
|
||
// 这里是你自己业务上需要的code
|
||
this.renderer.render(this.scene, this.camera);
|
||
// 控制相机旋转缩放的更新
|
||
if (this.options.controls.visibel) this.controls.update();
|
||
// 统计更新
|
||
if (this.options.statsVisibel) this.stats.update();
|
||
|
||
TWEEN.update();
|
||
}
|
||
initCamera() {
|
||
let { width, height } = this.options;
|
||
let rate = width / height;
|
||
// 设置45°的透视相机,更符合人眼观察
|
||
this.camera = new THREE.PerspectiveCamera(45, rate, 0.1, 1500);
|
||
// this.camera.position.set(-428.88, 861.97, -1438.0)
|
||
this.camera.position.set(270.27, 173.24, 257.54);
|
||
// this.camera.position.set(-102, 205, -342)
|
||
|
||
this.camera.lookAt(0, 0, 0);
|
||
}
|
||
/**
|
||
* 初始化渲染器
|
||
*/
|
||
initRenderer() {
|
||
let { width, height, bgColor } = this.options;
|
||
|
||
// 强制清理所有WebGL上下文
|
||
if (this.renderer) {
|
||
this.renderer.dispose();
|
||
this.renderer.forceContextLoss();
|
||
this.renderer = null;
|
||
}
|
||
|
||
// 清理容器中现有的所有子元素
|
||
while (this.container.firstChild) {
|
||
this.container.removeChild(this.container.firstChild);
|
||
}
|
||
|
||
// 强制垃圾回收
|
||
if (window.gc) {
|
||
window.gc();
|
||
}
|
||
|
||
// 延迟创建新的渲染器,确保上下文完全释放
|
||
setTimeout(() => {
|
||
try {
|
||
// 重新获取容器尺寸,确保有效
|
||
const containerWidth = this.container.offsetWidth || window.innerWidth;
|
||
const containerHeight = this.container.offsetHeight || window.innerHeight;
|
||
|
||
// 确保尺寸有效
|
||
const validWidth = Math.max(containerWidth, 800);
|
||
const validHeight = Math.max(containerHeight, 600);
|
||
|
||
// 更新options中的尺寸
|
||
this.options.width = validWidth;
|
||
this.options.height = validHeight;
|
||
|
||
// 创建一个新的canvas元素
|
||
const canvas = document.createElement('canvas');
|
||
let renderer = new THREE.WebGLRenderer({
|
||
canvas: canvas,
|
||
antialias: true,
|
||
preserveDrawingBuffer: false,
|
||
powerPreference: "high-performance"
|
||
});
|
||
|
||
// 设置canvas的分辨率
|
||
renderer.setPixelRatio(window.devicePixelRatio);
|
||
// 设置canvas 的尺寸大小
|
||
renderer.setSize(validWidth, validHeight);
|
||
// 设置背景色
|
||
renderer.setClearColor(bgColor, 1);
|
||
// 设置canvas的z-index,确保CSS2D渲染器在其之上
|
||
renderer.domElement.style.zIndex = '1';
|
||
renderer.domElement.style.position = 'absolute';
|
||
// 插入到dom中
|
||
this.container.appendChild(renderer.domElement);
|
||
this.renderer = renderer;
|
||
|
||
// 继续初始化其他组件
|
||
this.continueInit();
|
||
} catch (error) {
|
||
console.error('WebGL渲染器创建失败:', error);
|
||
// 创建一个错误提示
|
||
const errorDiv = document.createElement('div');
|
||
errorDiv.innerHTML = '3D渲染器初始化失败,请刷新页面重试';
|
||
errorDiv.style.cssText = 'color: #ff6b6b; text-align: center; padding: 50px; font-size: 16px;';
|
||
this.container.appendChild(errorDiv);
|
||
}
|
||
}, 100);
|
||
}
|
||
|
||
continueInit() {
|
||
// 原来在init方法中renderer初始化后的逻辑
|
||
this.initLight();
|
||
this.initStats();
|
||
this.initControls();
|
||
this.initAxes();
|
||
|
||
// 如果之前调用了run方法,现在启动循环
|
||
if (this.shouldStart) {
|
||
this.shouldStart = false;
|
||
this.loop();
|
||
}
|
||
}
|
||
initLight() {
|
||
// 平行光1
|
||
let directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.6);
|
||
directionalLight1.position.set(400, 200, 200);
|
||
// 平行光2
|
||
let directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.6);
|
||
directionalLight2.position.set(-400, -200, -300);
|
||
// 环境光
|
||
let ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
||
// 将光源添加到场景中
|
||
this.addObject(directionalLight1);
|
||
this.addObject(directionalLight2);
|
||
this.addObject(ambientLight);
|
||
}
|
||
|
||
initStats() {
|
||
if (!this.options.statsVisibel) return false;
|
||
|
||
// 确保容器有有效的尺寸
|
||
if (!this.container || this.container.offsetWidth === 0 || this.container.offsetHeight === 0) {
|
||
console.warn('Container not ready for stats initialization, skipping...');
|
||
return false;
|
||
}
|
||
|
||
this.stats = new Stats();
|
||
|
||
// 确保stats的DOM元素有正确的尺寸
|
||
if (this.stats.dom) {
|
||
this.stats.dom.style.position = 'absolute';
|
||
this.stats.dom.style.top = '0px';
|
||
this.stats.dom.style.left = '0px';
|
||
this.stats.dom.style.zIndex = '100';
|
||
}
|
||
|
||
this.container.appendChild(this.stats.dom);
|
||
}
|
||
initControls() {
|
||
try {
|
||
let {
|
||
controls: { enableDamping, autoRotate, visibel, maxPolarAngle },
|
||
} = this.options;
|
||
if (!visibel) return false;
|
||
// 轨道控制器,使相机围绕目标进行轨道运动(旋转|缩放|平移)
|
||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||
this.controls.maxPolarAngle = maxPolarAngle;
|
||
this.controls.autoRotate = autoRotate;
|
||
this.controls.enableDamping = enableDamping;
|
||
} catch (error) {
|
||
console.log(error);
|
||
}
|
||
}
|
||
initAxes() {
|
||
if (!this.options.axesVisibel) return false;
|
||
var axes = new THREE.AxesHelper(this.options.axesHelperSize);
|
||
this.addObject(axes);
|
||
}
|
||
|
||
// 清空dom
|
||
empty(elem) {
|
||
while (elem && elem.lastChild) elem.removeChild(elem.lastChild);
|
||
}
|
||
/**
|
||
* 添加对象到场景
|
||
* @param {*} object {} []
|
||
*/
|
||
addObject(object) {
|
||
if (isType('Array', object)) {
|
||
this.scene.add(...object);
|
||
} else {
|
||
this.scene.add(object);
|
||
}
|
||
}
|
||
/**
|
||
* 移除对象
|
||
* @param {*} object {} []
|
||
*/
|
||
removeObject(object) {
|
||
if (isType('Array', object)) {
|
||
object.map((item) => {
|
||
item.geometry.dispose();
|
||
});
|
||
this.scene.remove(...object);
|
||
} else {
|
||
object.geometry.dispose();
|
||
this.scene.remove(object);
|
||
}
|
||
}
|
||
/**
|
||
* 重置
|
||
*/
|
||
resize() {
|
||
// 重新设置宽高
|
||
let newWidth = this.container.offsetWidth || window.innerWidth;
|
||
let newHeight = this.container.offsetHeight || window.innerHeight;
|
||
|
||
// 确保尺寸有效
|
||
this.options.width = Math.max(newWidth, 800);
|
||
this.options.height = Math.max(newHeight, 600);
|
||
|
||
if (this.renderer) {
|
||
this.renderer.setSize(this.options.width, this.options.height);
|
||
}
|
||
// 重新设置相机的位置
|
||
let rate = this.options.width / this.options.height;
|
||
|
||
// 必須設置相機的比例,重置的時候才不会变形
|
||
this.camera.aspect = rate;
|
||
|
||
// 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
|
||
// 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
|
||
// 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
|
||
this.camera.updateProjectionMatrix();
|
||
|
||
// 如果stats还没有初始化(可能之前容器尺寸为0),现在重新尝试初始化
|
||
if (this.options.statsVisibel && !this.stats) {
|
||
this.initStats();
|
||
}
|
||
}
|
||
}
|