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: 0x000000,
|
|||
|
|
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();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|