import * as THREE from 'three'; import CameraControls from 'camera-controls'; import {Timer} from 'three/examples/jsm/misc/Timer.js'; import {CSS2DRenderer} from "three/examples/jsm/renderers/CSS2DRenderer"; import {CSS3DRenderer} from "three/examples/jsm/renderers/CSS3DRenderer.js"; import {TransformControls} from "three/examples/jsm/controls/TransformControls.js"; import TWEEN from "three/examples/jsm/libs/tween.module.js"; import App from "../app/App"; import {ViewerOptions} from "./ViewerOptions"; import {PluginManager} from "@/core/plugin/plugin"; import { Helper, CameraManage, Effect, Weather, Signals, ParticleSystem, Drag, TilesManage, } from "./modules"; import {ShaderMaterialManager} from "@/core/shaderMaterial/ShaderMaterialManager"; import {deepAssign, deepEqual, getMousePosition, isEmptyObject, isNil, createDivContainer} from "@/utils"; import {useDispatchSignal} from "@/hooks"; import { AddObjectCommand, RemoveObjectCommand, SetPositionCommand, SetRotationCommand, SetScaleCommand } from "@/core/commands/Commands"; import {Emitter} from '@/core/libs/three-nebula'; import ParticleEmitter from "@/core/objects/ParticleEmitter.ts"; import {ViewerPathTracer} from "@/core/viewer/ViewerPathTracer.ts"; import {Helper as ScriptHelper} from "../script"; import Tiles from "../objects/Tile.ts"; import {Package} from "@/core/loader/Package"; export interface ViewerEventMap { // 场景加载完成时执行,仅执行一次 loaded: {}; // 场景当前动画帧循环开始之前触发,每一帧执行一次 beforeAnimation: { delta: number }; // 场景当前动画帧循环完成之后立即触发,每一帧执行一次 afterAnimation: { delta: number, toBeRender: (_need: boolean) => void }; // 场景当前动画帧循环完成之后渲染之前触发,每一次渲染执行一次 beforeRender: { delta: number }; // 场景当前帧渲染完成之后触发,每一次渲染执行一次 afterRender: { delta: number }; // 场景销毁前调用,仅执行一次 beforeDestroy: {}; // 场景销毁后调用,仅执行一次 afterDestroy: {}; // 模型单击事件 onPick: { intersect: THREE.Intersection, object: THREE.Object3D }; // 模型双击事件 onDoubleClick: { intersect: THREE.Intersection, object: THREE.Object3D }; // 数据组件绑定数据更新 bindDataChange: { object: THREE.Object3D; data: unknown; config: unknown; index: number; }; // 键盘按下事件(全局) onKeyDown: { event: KeyboardEvent }; // 键盘抬起事件(全局) onKeyUp: { event: KeyboardEvent }; //指针按下事件(全局) onPointerDown: { event: PointerEvent }; //指针抬起事件(全局) onPointerUp: { event: PointerEvent }; //指针移动事件(全局) onPointerMove: { event: PointerEvent }; //触屏按下事件(全局) onTouchStart: { event: TouchEvent }; //触屏释放事件(全局) onTouchEnd: { event: TouchEvent }; // 场景背景变更 onSceneBackgroundChange: { backgroundType: '' | 'Color' | 'Texture' | 'Equirectangular', background: null | THREE.Color | THREE.Texture } // 场景环境变更 onSceneEnvironmentChange: { environmentType: '' | 'Background' | 'Equirectangular' | 'ModelViewer', environment: null | THREE.Texture } } export interface ViewerModules { plugin: PluginManager, viewHelper: Helper, cameraManage: CameraManage, controls: CameraControls, transformControls?: TransformControls, effect: Effect, weather: Weather, registerSignal: Signals, shaderMaterialManager: ShaderMaterialManager, particleSystem: ParticleSystem, dragControl: Drag, tilesManage:TilesManage, } export interface InstallScriptsOptions { invokeLoaded?: boolean; loadedScriptNames?: string[]; } CameraControls.install({ THREE: { Vector2: THREE.Vector2, Vector3: THREE.Vector3, Vector4: THREE.Vector4, Quaternion: THREE.Quaternion, Matrix4: THREE.Matrix4, Spherical: THREE.Spherical, Box3: THREE.Box3, Sphere: THREE.Sphere, Raycaster: THREE.Raycaster, } }); const onDownPosition = new THREE.Vector2(); const onUpPosition = new THREE.Vector2(); const onDoubleClickPosition = new THREE.Vector2(); // 表示animate()函数被多次调用累积时间,用于限制FPS let timeStamp = 0; // 脚本管理数据结构 type EventHandlers = { [eventName: string]: { [uuid: string]: Function[]; }; }; const createScriptEvents = (): EventHandlers => ({ loaded: {}, beforeAnimation: {}, afterAnimation: {}, beforeRender: {}, afterRender: {}, beforeDestroy: {}, afterDestroy: {}, onPick: {}, onDoubleClick: {}, bindDataChange: {}, onKeyDown: {}, onKeyUp: {}, onPointerDown: {}, onPointerUp: {}, onPointerMove: {}, onTouchStart: {}, onTouchEnd: {}, }); export default class Viewer extends THREE.EventDispatcher { public container: HTMLElement; public options: IViewerSetting; public renderer: THREE.WebGLRenderer; public camera: THREE.PerspectiveCamera; public scene: THREE.Scene; public sceneHelpers: THREE.Scene; public grid: THREE.Group | undefined; public box: THREE.Box3 = new THREE.Box3(); public selectionBox: THREE.Box3Helper; public raycaster: THREE.Raycaster; public pmremGenerator: THREE.PMREMGenerator | null = null; public pathtracer: ViewerPathTracer | undefined; public modules: ViewerModules; public css2DRenderer: CSS2DRenderer = new CSS2DRenderer(); public css3DRenderer: CSS3DRenderer = new CSS3DRenderer(); public timer = new Timer(); public events: EventHandlers = createScriptEvents(); public uuidEventMap: Map> = new Map(); public fns: any = { mousedown: null, pointerdown: null, pointerup: null, pointermove: null, keydown: null, keyup: null, touchstart: null, dblclick: null, }; //整个主场景的box3 public sceneBox3 = new THREE.Box3(); public package: Package; constructor(options: IViewerSetting) { super(); App.viewer = this; this.container = options.container || createDivContainer(); this.options = ViewerOptions(); deepAssign(this.options, options); this.camera = App.camera; this.scene = App.scene; this.sceneHelpers = App.sceneHelpers; this.renderer = this.createEngine(); this.modules = this.initModules(); this.package = new Package(this); /** helpers **/ if (this.options.grid.enabled) { this.grid = new THREE.Group(); this.grid.ignore = true; this.initGrid(); this.scene.add(this.grid); } //选中时的包围框 this.selectionBox = new THREE.Box3Helper(this.box); (this.selectionBox.material as THREE.Material).depthTest = false; (this.selectionBox.material as THREE.Material).transparent = true; this.selectionBox.visible = false; this.sceneHelpers.add(this.selectionBox as THREE.Object3D); // 拾取对象 this.raycaster = new THREE.Raycaster(); //Raycaster 将只从它遇到的第一个对象中获取信息 this.raycaster.firstHitOnly = true; this.engineCreated(this.renderer) this.loadEnv(true); this.initEvent(); //监听视窗变化(节流) let timer: NodeJS.Timeout | null = null; const resizeObserver = new ResizeObserver(() => { if (timer) return; timer = setTimeout(() => { useDispatchSignal("sceneResize", this.container.offsetWidth, this.container.offsetHeight); timer = null; }, 16) }); resizeObserver.observe(this.container); useDispatchSignal("viewerInitCompleted", this); } /** * 获取编辑态配置 */ get edit(): IViewerEdit { return this.options.edit as IViewerEdit; } /** * 设置编辑态配置 * @param config */ set edit(config: IViewerEdit) { if(!config) return; if (deepEqual(config,this.options.edit)) return; deepAssign(this.options.edit,config); if (this.options.edit?.gizmo) { if(this.modules.transformControls) return; let objectPositionOnDown = new THREE.Vector3(); let objectRotationOnDown = new THREE.Euler(); let objectScaleOnDown = new THREE.Vector3(); const transformControls = new TransformControls(this.camera, this.container); transformControls.addEventListener("change", () => { const object = transformControls.object; if (object !== undefined) { this.box.setFromObject(object, true); useDispatchSignal("objectChanged", object); } this.render(); }) transformControls.addEventListener("mouseDown", () => { const object = transformControls.object as THREE.Object3D; objectPositionOnDown = object.position.clone(); objectRotationOnDown = object.rotation.clone(); objectScaleOnDown = object.scale.clone(); this.modules.controls.enabled = false; }) transformControls.addEventListener("mouseUp", () => { const object = transformControls.object as THREE.Object3D; if (object !== undefined) { switch (transformControls.getMode()) { case 'translate': if (!objectPositionOnDown.equals(object.position)) { App.execute(new SetPositionCommand(object, object.position, objectPositionOnDown)); } break; case 'rotate': if (!objectRotationOnDown.equals(object.rotation)) { App.execute(new SetRotationCommand(object, object.rotation, objectRotationOnDown)); } break; case 'scale': if (!objectScaleOnDown.equals(object.scale)) { App.execute(new SetScaleCommand(object, object.scale, objectScaleOnDown)); } break; } } this.modules.controls.enabled = true; }) const gizmo = transformControls.getHelper(); this.sceneHelpers.add(gizmo); this.modules.transformControls = transformControls; } else { if (!this.modules.transformControls) return; const gizmo = this.modules.transformControls.getHelper(); this.sceneHelpers.remove(gizmo); this.modules.transformControls.dispose(); this.modules.transformControls = undefined; } // TODO 处理 this.options.edit?.helpers,以兼容只显示gizmo不显示helpers的情况 this.render(); } /** * 获取是否启用脚本运行 */ get enableScript() { return this.options.enableScript || false; } /** * 设置是否启用脚本运行 */ set enableScript(enable: boolean) { if (enable === this.enableScript) return; this.options.enableScript = enable; if (enable) { this.installScripts(); } else { this.unInstallScripts(); } } /** * 创建默认渲染引擎 */ createEngine() { const renderConfig = App.project.getKey("renderer"); const renderer = new THREE.WebGLRenderer({ antialias: renderConfig.antialias, alpha: true, preserveDrawingBuffer: false, powerPreference: "high-performance", }); renderer.autoClear = false; renderer.setClearColor(0x272727, 1); renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = renderConfig.toneMapping; renderer.toneMappingExposure = renderConfig.toneMappingExposure; renderer.shadowMap.enabled = renderConfig.shadow.enabled; renderer.shadowMap.type = renderConfig.shadow.type; renderer.xr.enabled = App.project.getKey("xr"); this.modules && this.engineCreated(renderer); return renderer; } /** * 创建渲染引擎后 * @param newRenderer */ engineCreated(newRenderer: THREE.WebGLRenderer) { if (this.renderer && this.renderer !== newRenderer) { this.renderer.setAnimationLoop(null); this.renderer.dispose(); this.pmremGenerator?.dispose(); this.pmremGenerator = null; this.modules.controls.disconnect(); this.container.removeChild(this.renderer.domElement); this.modules.viewHelper.dispose(); } this.renderer = newRenderer; this.renderer.setAnimationLoop(this.animate.bind(this)); this.renderer.setPixelRatio(Math.max(Math.ceil(window.devicePixelRatio), 1)); this.renderer.setSize(this.container.offsetWidth, this.container.offsetHeight); if (this.scene.environment && this.scene.environment.isRenderTargetTexture) { useDispatchSignal("sceneEnvironmentChanged", 'ModelViewer'); useDispatchSignal("sceneGraphChanged"); } this.pathtracer = new ViewerPathTracer(newRenderer); // 在container中最前面插入渲染器的dom元素 this.container.insertBefore(newRenderer.domElement, this.container.firstChild); // 控制器绑定 this.modules.controls.connect(newRenderer.domElement); // 初始化后处理 this.modules.effect.createComposer(); this.modules.viewHelper.init(); // 防止重复添加 if (this.css2DRenderer.domElement.parentNode !== this.container) { this.css2DRenderer.setSize(this.container.offsetWidth, this.container.offsetHeight); this.css2DRenderer.domElement.setAttribute("id", "astral-3d-css2DRenderer"); this.css2DRenderer.domElement.style.position = 'absolute'; this.css2DRenderer.domElement.style.top = '0px'; this.css2DRenderer.domElement.style.pointerEvents = 'none'; this.container.appendChild(this.css2DRenderer.domElement); } if (this.css3DRenderer.domElement.parentNode !== this.container) { this.css3DRenderer.setSize(this.container.offsetWidth, this.container.offsetHeight); this.css3DRenderer.domElement.setAttribute("id", "astral-3d-css3DRenderer"); this.css3DRenderer.domElement.style.position = 'absolute'; this.css3DRenderer.domElement.style.top = '0px'; this.css3DRenderer.domElement.style.pointerEvents = 'none'; this.container.appendChild(this.css3DRenderer.domElement); } useDispatchSignal("rendererUpdated"); } /** * 初始化网格 * @protected */ initGrid() { if (!this.grid) { this.grid = new THREE.Group(); this.grid.ignore = true; this.scene.add(this.grid); } if (this.grid.children.length > 0) { this.grid.children.forEach((child: THREE.Object3D) => { child.dispose(); }); this.grid.children = []; } const grid = new THREE.GridHelper(this.options.grid.row, this.options.grid.column, parseInt(App.config.getKey('mainColor').slice(1), 16), this.options.grid.color); this.grid.add(grid); this.render(); } /** * 初始化功能模块 */ initModules() { const controls = new CameraControls(this.camera); controls.addEventListener("update", () => { useDispatchSignal("cameraChanged", this.camera, controls); }); const modules: ViewerModules = { // 插件系统 plugin: new PluginManager(), viewHelper: new Helper(this, controls), cameraManage: new CameraManage(this, controls), controls, effect: new Effect(this), weather: new Weather(this), // 注册signal registerSignal: new Signals(this), shaderMaterialManager: new ShaderMaterialManager(), // 粒子系统 particleSystem: new ParticleSystem(this), // 拖拽 dragControl: new Drag(this), // 3d tiles管理器 tilesManage: new TilesManage(this.scene,this.camera,this.renderer), } if (this.edit?.enabled) { let objectPositionOnDown = new THREE.Vector3(); let objectRotationOnDown = new THREE.Euler(); let objectScaleOnDown = new THREE.Vector3(); const transformControls = new TransformControls(this.camera, this.container); transformControls.addEventListener("change", () => { const object = transformControls.object; if (object !== undefined) { this.box.setFromObject(object, true); useDispatchSignal("objectChanged", object); } this.render(); }) transformControls.addEventListener("mouseDown", () => { const object = transformControls.object as THREE.Object3D; objectPositionOnDown = object.position.clone(); objectRotationOnDown = object.rotation.clone(); objectScaleOnDown = object.scale.clone(); this.modules.controls.enabled = false; }) transformControls.addEventListener("mouseUp", () => { const object = transformControls.object as THREE.Object3D; if (object !== undefined) { switch (transformControls.getMode()) { case 'translate': if (!objectPositionOnDown.equals(object.position)) { App.execute(new SetPositionCommand(object, object.position, objectPositionOnDown)); } break; case 'rotate': if (!objectRotationOnDown.equals(object.rotation)) { App.execute(new SetRotationCommand(object, object.rotation, objectRotationOnDown)); } break; case 'scale': if (!objectScaleOnDown.equals(object.scale)) { App.execute(new SetScaleCommand(object, object.scale, objectScaleOnDown)); } break; } } this.modules.controls.enabled = true; }) const gizmo = transformControls.getHelper(); this.sceneHelpers.add(gizmo); modules.transformControls = transformControls; } return modules; } /** * 加载默认环境和背景 */ loadEnv(setBg: boolean = true, onLoad?:(texture: THREE.Texture) => void,onError?:(error: Error) => void) { if (!this.options.hdr) return; App.resource.loadURLTexture(this.options.hdr, (texture: THREE.Texture) => { texture.mapping = THREE.EquirectangularReflectionMapping; this.scene.environment = texture; setBg && (this.scene.background = texture); useDispatchSignal("sceneGraphChanged"); onLoad && onLoad(texture) }, (err) => onError && onError(err)) } /** * 初始化事件监听 */ initEvent() { this.fns.pointerdown = this.onPointerDown.bind(this); this.container.addEventListener('pointerdown', this.fns.pointerdown); this.fns.pointermove = this.onPointerMove.bind(this); this.container.addEventListener('pointermove', this.fns.pointermove); this.fns.touchstart = this.onTouchStart.bind(this); this.container.addEventListener('touchstart', this.fns.touchstart); this.fns.dblclick = this.onDoubleClick.bind(this) this.container.addEventListener('dblclick', this.fns.dblclick); } /** * 组装脚本(支持热更新) * @param uuids 传入此参数则仅组装此数组下Object.uuid的脚本 * @param filterName 传入此参数则仅组装此数组下Object.uuid的脚本中name匹配的脚本 */ installScripts(uuids?: string | string[], filterName: string = "", options: InstallScriptsOptions = {}) { if (!this.enableScript) return; const { invokeLoaded = false, loadedScriptNames = [] } = options; const loadedScriptNameSet = new Set(loadedScriptNames.filter(Boolean)); const shouldInvokeLoadedAfterInstall = invokeLoaded || loadedScriptNameSet.size > 0; // 注册 Helper const helper = new ScriptHelper(this.scene); // 定义注入的参数名 let scriptWrapParams = 'THREE,helper,renderer,scene,camera,controls,timer,render'; // 定义返回的结构 const scriptWrapResultObj = {}; // 注入的函数绑定this const fns = { render: this.render.bind(this) } // 拼接下方闭包函数返回的结构,即返回脚本中写的支持的事件函数 const validEvents = Object.keys(this.events); // 准备返回结构 validEvents.forEach(eventName => { scriptWrapParams += ',' + eventName; scriptWrapResultObj[eventName] = eventName; }); // scriptWrapResultObj是json,会包含双引号,但是后面拼接闭包函数的返回结果里面不能有双引号,因为这些值都会解析为变量 const scriptWrapResult = JSON.stringify(scriptWrapResultObj).replace(/"/g, ''); // 处理特定 UUID(热更新) const processUuid = (uuid: string) => { // 先卸载旧脚本 this.uninstallScriptsByUuid(uuid, filterName); const object = this.scene.getObjectByProperty('uuid', uuid); if (!object) { App.log.warn(`[Script] 不存在uuid为${uuid}的对象`); return; } // 一个模型允许存在多个脚本 const scripts = App.scripts[uuid] || []; const uuidEvents = this.uuidEventMap.get(uuid) || new Map(); const scriptsNeedLoaded = new Set(); scripts.forEach(script => { // 如果存在需要按照name过滤 if (filterName && filterName !== script.name) return; const functions = new Function( scriptWrapParams, `${script.source}\nreturn ${scriptWrapResult};` ).bind(object)( THREE, helper, this.renderer, this.scene, this.camera, this.modules.controls, this.timer, fns.render ); const shouldInvokeLoaded = loadedScriptNameSet.size > 0 ? loadedScriptNameSet.has(script.name) : invokeLoaded; if (shouldInvokeLoaded && typeof functions.loaded === "function") { scriptsNeedLoaded.add(script.name); } Object.entries(functions).forEach(([name, fn]) => { if (!fn || !validEvents.includes(name)) { if (fn && !validEvents.includes(name)) { App.log.warn(`[Script] 不支持的事件类型:${name}`); } return; } const boundFn = (e: any) => { const {type, target, ...params} = e; // 点击事件只分发给对应模型 if (["onPick", "onDoubleClick", "bindDataChange"].includes(name)) { const {intersect, object: _object, data, config, index} = params; if (_object.uuid !== object.uuid) return; if (name === "bindDataChange") { (fn as Function).bind(object)(data, config, index); } else { (fn as Function).bind(object)(intersect as THREE.Intersection); } } else { if (isEmptyObject(params)) { (fn as Function).bind(object)(); } else { (fn as Function).bind(object)(...Object.values(params)); } } } // 添加到全局事件集合 if (!this.events[name][uuid]) this.events[name][uuid] = []; this.events[name][uuid].push(boundFn); // 添加到 UUID 事件映射 if (!uuidEvents.has(name)) uuidEvents.set(name, []); uuidEvents.get(name)!.push({ name: script.name, fn: boundFn }); // 添加事件监听 this.addEventListener(name as keyof ViewerEventMap, boundFn); }); }); // 更新 UUID 映射 this.uuidEventMap.set(uuid, uuidEvents); if (shouldInvokeLoadedAfterInstall) { scriptsNeedLoaded.forEach(scriptName => { this.invokeInstalledScriptEvent(uuid, scriptName, "loaded"); }); } }; // 处理指定 UUID 或全部 if (uuids) { (Array.isArray(uuids) ? uuids : [uuids]).forEach(processUuid); } else { Object.keys(App.scripts).forEach(processUuid); } if (!this.fns.keydown) { this.fns.keydown = (event: KeyboardEvent) => { this.dispatchEvent({type: "onKeyDown", event}) } window.addEventListener('keydown', this.fns.keydown); this.fns.keyup = (event: KeyboardEvent) => { this.dispatchEvent({type: "onKeyUp", event}) } window.addEventListener('keyup', this.fns.keyup); } } /** * 按 object UUID 卸载脚本(热更新用) * @param uuid 传入此参数则仅卸载此Object.uuid的脚本 * @param filterName 传入此参数则仅卸载此Object.uuid的脚本中name匹配的脚本 */ uninstallScriptsByUuid(uuid: string, filterName: string = "") { if (!this.uuidEventMap.has(uuid)) return; const uuidEvents = this.uuidEventMap.get(uuid)!; const uuidEventsArray = Array.from(uuidEvents); for (let i = uuidEventsArray.length - 1; i >= 0; i--) { const [eventName, scripts] = uuidEventsArray[i]; // 移除事件监听 for (let i = scripts.length - 1; i >= 0; i--) { const sc = scripts[i]; if (filterName && filterName !== sc.name) continue; // @ts-ignore this.removeEventListener(eventName as keyof ViewerEventMap, sc.fn); // 从uuidEventMap 移除 scripts.splice(i, 1); if (scripts.length === 0) { uuidEvents.delete(eventName); } // 全局事件集合 const es = this.events[eventName][uuid]; // 移除相应函数 const ei = es.findIndex(f => f === sc.fn); if (ei !== -1) { es.splice(ei, 1); } if (es.length === 0) { delete this.events[eventName][uuid]; } } } // 清理 UUID 映射 if (Array.from(uuidEvents.keys()).length === 0) { this.uuidEventMap.delete(uuid); } } invokeInstalledScriptEvent( uuid: string, scriptName: string, eventName: T, payload: Partial = {} ): boolean { const uuidEvents = this.uuidEventMap.get(uuid); if (!uuidEvents) return false; const handlers = uuidEvents.get(String(eventName)); if (!handlers || handlers.length === 0) return false; let executed = false; handlers.forEach(handler => { if (handler.name !== scriptName) return; try { handler.fn({ ...payload, type: eventName, target: this, }); executed = true; } catch (error) { const message = error instanceof Error ? error.message : String(error); App.log.error(`[Script] 执行 ${scriptName}.${String(eventName)} 失败: ${message}`); } }); return executed; } reinstallObjectScripts(uuid: string, loadedScriptNames: string[] = []): void { if (!uuid) return; const uniqueLoadedScriptNames = Array.from(new Set(loadedScriptNames.filter(Boolean))); this.uninstallScriptsByUuid(uuid); this.installScripts([uuid], "", uniqueLoadedScriptNames.length > 0 ? { loadedScriptNames: uniqueLoadedScriptNames } : undefined); } /** * 卸载所有脚本 */ unInstallScripts() { // 直接遍历 UUID 映射,复杂度 O(n) this.uuidEventMap.forEach((_, uuid) => { this.uninstallScriptsByUuid(uuid); }); // 重置数据结构 this.uuidEventMap.clear(); Object.keys(this.events).forEach(event => { this.events[event] = {}; }); if (this.fns.keydown) { window.removeEventListener('keydown', this.fns.keydown); this.fns.keydown = null; window.removeEventListener('keyup', this.fns.keyup); this.fns.keyup = null; } } /** * 加载离线场景包 */ loadOfflineScene() { // const requestConfig = App.config.getKey('request'); // const packageUrl = `${requestConfig.baseUrl}/${requestConfig.sceneId}`; } /** * 计算整个场景的Box3 */ computedSceneBox3() { this.sceneBox3.setFromObject(this.scene); } /** * 更新相机宽高比 */ updateAspectRatio() { for (const uuid in App.cameras) { const camera = App.cameras[uuid]; const aspect = this.container.offsetWidth / this.container.offsetHeight; if (camera.isPerspectiveCamera) { (camera).aspect = aspect; } camera.updateProjectionMatrix(); const cameraHelper = App.helpers[camera.id]; if (cameraHelper) cameraHelper.update(); } } /** * 获取射线选中 * @param point */ getIntersects(point: THREE.Vector2) { const mouse = new THREE.Vector2(); mouse.set((point.x * 2) - 1, -(point.y * 2) + 1); this.raycaster.setFromCamera(mouse, this.camera); const objects: THREE.Object3D[] = []; (App.locked ? App.locked : this.scene).traverseByCondition((child) => { this.camera.layers.test(child.layers) && objects.push(child); }, (child) => !child.ignore && child.visible); this.sceneHelpers.traverseVisible((child) => { if (child.name === 'picker') { this.camera.layers.test(child.layers) && objects.push(child); } }); // 粒子 this.modules.particleSystem.particlesGroup.traverse((child) => { if (!this.camera.layers.test(child.layers)) return; if (child.children.length === 0) { if (App.locked) { child.proxy?.isAncestor(App.locked) && objects.push(child); } else { objects.push(child); } } }); return this.raycaster.intersectObjects(objects, false); } /** * 处理点击 */ handleClick() { if (onDownPosition.distanceTo(onUpPosition) === 0) { const intersects = this.getIntersects(onUpPosition); useDispatchSignal("intersectionsDetected", intersects); if (intersects.length > 0) { let object = intersects[0].object; if (object.proxy) { object = object.proxy; } this.dispatchEvent({type: "onPick", intersect: intersects[0], object}); } this.render(); } } /** * 处理鼠标按下 * @param event */ onPointerDown(event: PointerEvent) { this.dispatchEvent({type: "onPointerDown", event}); event.preventDefault(); const array = getMousePosition(this.container, event.clientX, event.clientY); onDownPosition.fromArray(array); this.fns.pointerup = this.onPointerUp.bind(this); document.addEventListener('pointerup', this.fns.pointerup); } /** * 处理鼠标抬起 * @param event */ onPointerUp(event: PointerEvent) { this.dispatchEvent({type: "onPointerUp", event}); const array = getMousePosition(this.container, event.clientX, event.clientY); onUpPosition.fromArray(array); this.handleClick(); document.removeEventListener('pointerup', this.fns.pointerup); this.fns.pointerup = null; } /** * 处理鼠标移动 * @param event */ onPointerMove(event: PointerEvent) { this.dispatchEvent({type: "onPointerMove", event}); } /** * 处理触屏按下 * @param event */ onTouchStart(event: TouchEvent) { this.dispatchEvent({type: "onTouchStart", event}); const touch = event.changedTouches[0]; const array = getMousePosition(this.container, touch.clientX, touch.clientY); onDownPosition.fromArray(array); this.fns.pointerup = this.onTouchEnd.bind(this); document.addEventListener('touchend', this.fns.pointerup); } /** * 处理触屏释放 * @param event */ onTouchEnd(event: TouchEvent) { this.dispatchEvent({type: "onTouchEnd", event}); const touch = event.changedTouches[0]; const array = getMousePosition(this.container, touch.clientX, touch.clientY); onUpPosition.fromArray(array); this.handleClick(); document.removeEventListener('touchend', this.fns.pointerup); this.fns.pointerup = null; } /** * 处理双击 * @param event */ onDoubleClick(event: PointerEvent) { const array = getMousePosition(this.container, event.clientX, event.clientY); onDoubleClickPosition.fromArray(array); const intersects = this.getIntersects(onDoubleClickPosition); if (intersects.length > 0) { let object = intersects[0].object; useDispatchSignal("objectFocused", object); if (object.proxy) { object = object.proxy; } this.dispatchEvent({type: "onDoubleClick", intersect: intersects[0], object}); } } /** * 循环动画 */ animate() { this.timer.update(); const delta = this.timer.getDelta(); timeStamp += delta; if (timeStamp < App.singleFrameTime) return; this.dispatchEvent({type: 'beforeAnimation', delta: timeStamp}); let needRender = App.animationManager.update(timeStamp); if (TWEEN.update()) { needRender = true; } if (needRender) { if (App.selected !== null && App.selected.animations.length > 0) { // 避免某些蒙皮网格的帧延迟效应(e.g. Michelle.glb) App.selected.updateWorldMatrix(false, true); if (!this.modules.effect.enabled) { // 选择框应反映当前动画状态 this.selectionBox.box.setFromObject(App.selected, true); } } } if (this.modules.controls.enabled && !this.modules.viewHelper.animating) { needRender = this.modules.controls.update(timeStamp) || needRender; } if (this.modules.weather.update(timeStamp)) { needRender = true; } this.modules.shaderMaterialManager.update(); if (this.modules.shaderMaterialManager.needRender) { needRender = true; } this.modules.particleSystem.update(timeStamp); if (this.modules.particleSystem.needsUpdate) { needRender = true; } if (this.modules.dragControl.isDragging) { needRender = true; } if (this.renderer?.xr.isPresenting) { needRender = true; } // 3dTiles渲染 if(this.modules.tilesManage.update()){ needRender = true; } this.dispatchEvent({type: 'afterAnimation', delta: timeStamp, toBeRender: (_needRender:boolean = false) => { needRender = _needRender; } }); if (needRender) this.render(timeStamp); this.updatePT(); // console.log(`调用.animate时间间隔${timeStamp*1000}毫秒,delta:${delta}`); // 剩余的时间合并进入下次的判断计算 timeStamp = App.singleFrameTime ? (timeStamp % App.singleFrameTime) : 0; } /** * 初始化光线路径追踪模拟 */ initPT() { if (App.viewportShading === 'realistic') { this.pathtracer?.init(this.scene, this.camera); } } updatePTBackground() { if (App.viewportShading === 'realistic') { this.pathtracer?.setBackground(); } } updatePTEnvironment() { if (App.viewportShading === 'realistic') { this.pathtracer?.setEnvironment(); } } updatePTMaterials() { if (App.viewportShading === 'realistic') { this.pathtracer?.updateMaterials(); } } updatePT() { if (App.viewportShading === 'realistic') { this.pathtracer?.update(); useDispatchSignal("pathTracerUpdated", this.pathtracer?.getSamples()) } } /** * 渲染场景帧 */ render(delta?: number) { if (!this.renderer) return; if (isNil(delta)) { delta = this.timer.getDelta(); } const startTime = performance.now(); this.dispatchEvent({type: 'beforeRender', delta: delta}); this.renderer.clearDepth(); App.csm.update(); if (this.modules.effect.enabled) { this.modules.effect.render(delta); } else { this.renderer.render(this.scene, App.viewportCamera); } // 非默认相机不渲染辅助 if (this.camera === App.viewportCamera) { if (this.options.edit?.enabled) this.renderer.render(this.sceneHelpers, this.camera); } this.css2DRenderer.render(this.scene, App.viewportCamera); this.css3DRenderer.render(this.scene, App.viewportCamera); this.modules.viewHelper.render(); this.dispatchEvent({type: 'afterRender', delta: delta}); const endTime = performance.now(); // 计算帧时 const fs = endTime - startTime; useDispatchSignal("sceneRendered", fs); } /** * 销毁 */ dispose() { this.dispatchEvent({type: "beforeDestroy"}); this.container.removeEventListener('mousedown', this.fns.mousedown); this.fns.mousedown = null; this.container.removeEventListener('pointermove', this.fns.pointermove); this.fns.pointermove = null; this.container.removeEventListener('touchstart', this.fns.touchstart); this.fns.touchstart = null; this.container.removeEventListener('dblclick', this.fns.dblclick); this.fns.dblclick = null; Object.keys(this.modules).forEach(key => { if (this.modules[key].dispose) { this.modules[key].dispose(); } }) this.dispatchEvent({type: "afterDestroy"}); this.unInstallScripts(); this.package?.dispose(); this.package = null as any; } /* -----------------暂时放在Viewer下的工具方法-------------------- */ /** * 添加粒子 * @emitter 粒子发射器 * @body 粒子主体 */ addParticle(emitter: Emitter, body: THREE.Sprite | THREE.Mesh, name: string = "Particles") { const particleEmitter = new ParticleEmitter(emitter); particleEmitter.name = name; ParticleSystem.Body3DMap.set(particleEmitter.uuid, body); this.modules.particleSystem.spriteSystem.addEmitter(emitter); App.execute(new AddObjectCommand(particleEmitter)); } /** * 添加瓦片 * @param tiles 瓦片对象 * @param addMode 添加进场景的方式:none-不添加;command-使用记录命令添加;normal-直接添加,不会记录历史 */ addTiles(tiles:Tiles,addMode: "none" | "command" | "normal" = "command"){ tiles.setCameraAndRenderer(this.camera, this.renderer); this.modules.tilesManage.addTiles(tiles); switch (addMode) { case "command": App.execute(new AddObjectCommand(tiles), `Add 3DTiles: ${tiles.group.name}`); break; case "normal": App.addObject(tiles); App.select(tiles); break; } } /** * 移除瓦片 */ removeTiles(tiles:Tiles){ this.modules.tilesManage.removeTiles(tiles); App.execute(new RemoveObjectCommand(tiles)); } /** * 获取画布的截屏图片 * @returns Promise 截屏的图片对象 */ getViewportImage() { return new Promise((resolve, rejcet) => { // @ts-ignore const _preserveDrawingBuffer = this.renderer.getContext().preserveDrawingBuffer; // @ts-ignore this.renderer.getContext().preserveDrawingBuffer = true; this.modules.viewHelper.hidden = false; this.render(); this.renderer.domElement.toBlob((blob) => { if (blob === null) { rejcet('Screenshots fail'); return; } const image = new Image(); image.src = URL.createObjectURL(blob); // @ts-ignore this.renderer.getContext().preserveDrawingBuffer = _preserveDrawingBuffer; this.modules.viewHelper.hidden = true; this.render(); resolve(image); }); }); } }