|
|
|
|
import * as THREE from "three";
|
|
|
|
|
import { AmbientLight, AxesHelper, Camera, PerspectiveCamera, Raycaster, Scene, SRGBColorSpace, Vector2, WebGLRenderer } from "three";
|
|
|
|
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
|
|
|
|
import { CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer.js";
|
|
|
|
|
import { Tween } from "three/examples/jsm/libs/tween.module.js";
|
|
|
|
|
import Stats from "three/examples/jsm/libs/stats.module.js";
|
|
|
|
|
import Events from "./Events";
|
|
|
|
|
import { threeStore } from "@/store";
|
|
|
|
|
|
|
|
|
|
export type Animate = {
|
|
|
|
|
fun: (arg: any) => any;
|
|
|
|
|
content: any;
|
|
|
|
|
};
|
|
|
|
|
export default class Viewer {
|
|
|
|
|
public id: string;
|
|
|
|
|
public viewerDom!: HTMLElement;
|
|
|
|
|
public renderer!: WebGLRenderer;
|
|
|
|
|
public scene!: Scene;
|
|
|
|
|
public camera!: PerspectiveCamera;
|
|
|
|
|
public controls!: OrbitControls;
|
|
|
|
|
public css2Renderer: CSS2DRenderer | undefined;
|
|
|
|
|
public raycaster!: Raycaster;
|
|
|
|
|
public mouse!: Vector2;
|
|
|
|
|
public isDestroy = false; // 是否销毁
|
|
|
|
|
public animateEventList = new Map();
|
|
|
|
|
public statsControls!: Stats; // 性能监测
|
|
|
|
|
public raycasterObjects: THREE.Object3D[] = [];
|
|
|
|
|
public tween!: Tween<THREE.Vector3>;
|
|
|
|
|
public mouseEvent: MouseEvent | undefined;
|
|
|
|
|
|
|
|
|
|
constructor(id: string) {
|
|
|
|
|
this.id = id;
|
|
|
|
|
this.initViewer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private initViewer() {
|
|
|
|
|
this.initRenderer(); //创建渲染器
|
|
|
|
|
this.initScene(); //创建场景
|
|
|
|
|
this.initLight(); // 创建光源
|
|
|
|
|
this.initCamera(); // 创建相机
|
|
|
|
|
this.initControl(); // 创建控制器
|
|
|
|
|
// this.initCss2Renderer(); // 创建css2渲染器
|
|
|
|
|
|
|
|
|
|
this.raycaster = new Raycaster(); // 创建射线
|
|
|
|
|
this.mouse = new Vector2(); // 创建鼠标位置
|
|
|
|
|
|
|
|
|
|
const animate = () => {
|
|
|
|
|
if (this.isDestroy) return;
|
|
|
|
|
requestAnimationFrame(animate);
|
|
|
|
|
// TWEEN.update(); // 更新动画
|
|
|
|
|
// this.css2Renderer?.render(this.scene, this.camera);
|
|
|
|
|
this.updateDom();
|
|
|
|
|
this.renderDom();
|
|
|
|
|
|
|
|
|
|
// 全局的公共动画函数,添加函数可同步执行
|
|
|
|
|
this.animateEventList.forEach((event) => {
|
|
|
|
|
// console.log("animateEventList");
|
|
|
|
|
if (event.fun && event.content) event.fun(event.content);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
animate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private initRenderer() {
|
|
|
|
|
// 获取画布dom
|
|
|
|
|
this.viewerDom = document.getElementById(this.id) as HTMLElement;
|
|
|
|
|
// 初始化渲染器
|
|
|
|
|
this.renderer = new WebGLRenderer({
|
|
|
|
|
logarithmicDepthBuffer: true, //true/false用于启用或禁用对数深度缓冲区。
|
|
|
|
|
antialias: true, // true/false表示是否开启反锯齿
|
|
|
|
|
alpha: true, // true/false 表示是否可以设置背景色透明
|
|
|
|
|
precision: "mediump", // highp/mediump/lowp 表示着色精度选择
|
|
|
|
|
premultipliedAlpha: true // true/false 表示是否可以设置像素深度(用来度量图像的分辨率)
|
|
|
|
|
// preserveDrawingBuffer: false, // true/false 表示是否保存绘图缓冲
|
|
|
|
|
// physicallyCorrectLights: true, // true/false 表示是否开启物理光照
|
|
|
|
|
});
|
|
|
|
|
this.renderer.clearDepth(); //清除深度缓冲区。在渲染之前,这通常用于重置深度缓冲区,以确保正确的深度测试
|
|
|
|
|
|
|
|
|
|
// 开启模型对象的局部剪裁平面功能
|
|
|
|
|
// 如果不设置为true,设置剪裁平面的模型不会被剪裁
|
|
|
|
|
this.renderer.localClippingEnabled = true;
|
|
|
|
|
|
|
|
|
|
this.renderer.shadowMap.enabled = true;
|
|
|
|
|
this.renderer.outputColorSpace = SRGBColorSpace; // 可以看到更亮的材质,同时这也影响到环境贴图。
|
|
|
|
|
this.viewerDom.appendChild(this.renderer.domElement);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private initScene() {
|
|
|
|
|
this.scene = new Scene();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private initLight() {
|
|
|
|
|
const ambient = new AmbientLight(0xffffff, 0.6);
|
|
|
|
|
this.scene.add(ambient);
|
|
|
|
|
|
|
|
|
|
const light = new THREE.DirectionalLight(0xffffff);
|
|
|
|
|
light.position.set(0, 200, 100);
|
|
|
|
|
light.castShadow = true;
|
|
|
|
|
|
|
|
|
|
light.shadow.camera.top = 180;
|
|
|
|
|
light.shadow.camera.bottom = -100;
|
|
|
|
|
light.shadow.camera.left = -120;
|
|
|
|
|
light.shadow.camera.right = 400;
|
|
|
|
|
light.shadow.camera.near = 0.1;
|
|
|
|
|
light.shadow.camera.far = 400;
|
|
|
|
|
// 设置mapSize属性可以使阴影更清晰,不那么模糊
|
|
|
|
|
light.shadow.mapSize.set(1024, 1024);
|
|
|
|
|
light.name = "initLight";
|
|
|
|
|
|
|
|
|
|
this.scene.add(light);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private initCamera() {
|
|
|
|
|
// 渲染相机
|
|
|
|
|
this.camera = new PerspectiveCamera(25, this.viewerDom.clientWidth / this.viewerDom.clientHeight, 1, 2000);
|
|
|
|
|
//设置相机位置
|
|
|
|
|
// this.camera.position.set(5, 1, -5);
|
|
|
|
|
this.camera.position.set(4, 2, -3);
|
|
|
|
|
//设置相机方向
|
|
|
|
|
this.camera.lookAt(0, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private initControl() {
|
|
|
|
|
this.controls = new OrbitControls(this.camera as Camera, this.renderer?.domElement);
|
|
|
|
|
this.controls.enabled = true; // 控制器关
|
|
|
|
|
this.controls.enableDamping = false;
|
|
|
|
|
this.controls.screenSpacePanning = false; // 定义平移时如何平移相机的位置 控制不上下移动
|
|
|
|
|
this.controls.minDistance = 2;
|
|
|
|
|
this.controls.maxDistance = 1000;
|
|
|
|
|
this.controls.addEventListener("change", () => {
|
|
|
|
|
this.renderer.render(this.scene, this.camera);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private initCss2Renderer() {
|
|
|
|
|
this.css2Renderer = new CSS2DRenderer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateDom() {
|
|
|
|
|
this.controls.update();
|
|
|
|
|
// // 更新参数
|
|
|
|
|
this.camera.aspect = this.viewerDom.clientWidth / this.viewerDom.clientHeight; // 摄像机视锥体的长宽比,通常是使用画布的宽/画布的高
|
|
|
|
|
this.camera.updateProjectionMatrix(); // 更新摄像机投影矩阵。在任何参数被改变以后必须被调用,来使得这些改变生效
|
|
|
|
|
this.renderer.setSize(this.viewerDom.clientWidth, this.viewerDom.clientHeight);
|
|
|
|
|
this.renderer.setPixelRatio(window.devicePixelRatio); // 设置设备像素比
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 渲染dom
|
|
|
|
|
private renderDom() {
|
|
|
|
|
this.renderer?.render(this.scene as Scene, this.camera as Camera);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private statsUpdate(statsControls: any) {
|
|
|
|
|
statsControls.update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**坐标轴辅助 */
|
|
|
|
|
public addAxis() {
|
|
|
|
|
const axis = new AxesHelper(1000); //创建坐标轴辅助对象
|
|
|
|
|
this.scene?.add(axis);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public addAnimate(id: string, animate: Animate) {
|
|
|
|
|
this.animateEventList.set(id, animate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public addStats() {
|
|
|
|
|
if (!this.statsControls) this.statsControls = new Stats();
|
|
|
|
|
this.statsControls.dom.style.position = "absolute";
|
|
|
|
|
this.viewerDom.appendChild(this.statsControls.dom);
|
|
|
|
|
|
|
|
|
|
// 添加到动画
|
|
|
|
|
this.addAnimate("stats", { fun: this.statsUpdate, content: this.statsControls });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**自定义鼠标事件触发的范围,给定一个模型组,对给定的模型组鼠标事件才生效 */
|
|
|
|
|
public setRaycasterObjects(objList: THREE.Object3D[]): void {
|
|
|
|
|
this.raycasterObjects = objList;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**注册鼠标事件监听 */
|
|
|
|
|
public initRaycaster() {
|
|
|
|
|
this.raycaster = new Raycaster();
|
|
|
|
|
|
|
|
|
|
const initRaycasterEvent: Function = (eventName: keyof HTMLElementEventMap): void => {
|
|
|
|
|
//这里的container就是画布所在的div,也就是说,这个是要拿整个scene所在的容器来界定的
|
|
|
|
|
let getBoundingClientRect = this.viewerDom.getBoundingClientRect();
|
|
|
|
|
let offsetWidth = this.viewerDom.offsetWidth;
|
|
|
|
|
let offsetHeight = this.viewerDom.offsetHeight;
|
|
|
|
|
const funWrap = _.throttle((event: any) => {
|
|
|
|
|
this.mouseEvent = {
|
|
|
|
|
...event, //真正的鼠标相对于画布的位置
|
|
|
|
|
x: event.clientX - getBoundingClientRect.left,
|
|
|
|
|
y: event.clientY - getBoundingClientRect.top
|
|
|
|
|
};
|
|
|
|
|
this.mouse.x = ((event.clientX - getBoundingClientRect.left) / offsetWidth) * 2 - 1;
|
|
|
|
|
this.mouse.y = -((event.clientY - getBoundingClientRect.top) / offsetHeight) * 2 + 1;
|
|
|
|
|
threeStore.setRaycasterIntersectObjects(this.getRaycasterIntersectObjects());
|
|
|
|
|
}, 100);
|
|
|
|
|
this.viewerDom.addEventListener(eventName, funWrap, false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
initRaycasterEvent("click");
|
|
|
|
|
// initRaycasterEvent("dblclick");
|
|
|
|
|
// initRaycasterEvent("mousemove");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取射线与物体相交的物体
|
|
|
|
|
private getRaycasterIntersectObjects(): THREE.Intersection[] {
|
|
|
|
|
// 如果射线与物体相交的数组为空,则返回空数组
|
|
|
|
|
if (!this.raycasterObjects.length) return [];
|
|
|
|
|
// 设置射线从相机发出,经过鼠标位置
|
|
|
|
|
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|
|
|
|
// 返回射线与物体相交的数组
|
|
|
|
|
return this.raycaster.intersectObjects(this.raycasterObjects, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public addCameraTween(targetPosition = new THREE.Vector3(1, 1, 1), duration = 1000, onComplete: () => void) {
|
|
|
|
|
this.initCameraTween();
|
|
|
|
|
this.tween.to(targetPosition, duration).start().onComplete(onComplete);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 初始化补间动画库tween
|
|
|
|
|
*/
|
|
|
|
|
public initCameraTween() {
|
|
|
|
|
if (!this.camera) return;
|
|
|
|
|
this.tween = new Tween(this.camera.position);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public destroy() {
|
|
|
|
|
this.scene.traverse((child: any) => {
|
|
|
|
|
if (child.material) {
|
|
|
|
|
child.material.dispose();
|
|
|
|
|
}
|
|
|
|
|
if (child.geometry) {
|
|
|
|
|
child.geometry.dispose();
|
|
|
|
|
}
|
|
|
|
|
child = null;
|
|
|
|
|
});
|
|
|
|
|
this.renderer.forceContextLoss();
|
|
|
|
|
this.renderer.dispose();
|
|
|
|
|
this.scene.clear();
|
|
|
|
|
|
|
|
|
|
this.isDestroy = true;
|
|
|
|
|
}
|
|
|
|
|
}
|