TkAstral3D/packages/sdk/lib/core/app/App.ts

1023 lines
27 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @author ErSan
* @email mlt131220@163.com
* @date 2025/4/22 10:16
* @description 静态类,全局场景管理
*/
import * as THREE from 'three';
// 原生three的扩展
import '../expansion';
import Logger from "@/utils/log/Logger";
import { Config, Storage, Project, Selector, History as _History, Resource, CSM } from "./modules";
import { AnimationManager } from "../animation/AnimationManager";
import { useAddSignal, useDispatchSignal, useSetSignalActive } from '@/hooks';
import Loader from "@/core/loader/Loader.ts";
import { AddScriptCommand, RemoveScriptCommand } from "@/core/commands/Commands.ts";
import Viewer from "@/core/viewer/Viewer.ts";
const _DEFAULT_CAMERA = new THREE.PerspectiveCamera(45, 1, 0.01, 100 * 1000);
_DEFAULT_CAMERA.name = "默认相机";
_DEFAULT_CAMERA.position.set(0, 5, 10);
_DEFAULT_CAMERA.lookAt(new THREE.Vector3());
export class App {
/**
* 默认场景
*/
public scene: THREE.Scene = new THREE.Scene();
/**
* 辅助场景
*/
public sceneHelpers: THREE.Scene = new THREE.Scene();
/**
* 场景默认相机
*/
public camera: THREE.PerspectiveCamera = _DEFAULT_CAMERA.clone();
/**
* 当前视口正在使用的相机
*/
public viewportCamera: THREE.Camera = this.camera;
/**
* 当前视口渲染模式
*/
public viewportShading: string = 'default';
/**
* 场景中的几何数据集合
*/
public geometries: { [uuid: string]: THREE.BufferGeometry } = {};
/**
* 场景中的材质集合
*/
public materials: { [uuid: string]: THREE.Material } = {};
/**
* 场景中的贴图集合
*/
public textures: { [uuid: string]: THREE.Texture } = {};
/**
* 场景中的脚本集合
*/
public scripts: ISceneJson['scripts'] = {};
/**
* 场景中的辅助集合
*/
public helpers: Record<number, THREE.Object3D> = {};
/**
* 场景中的相机集合
*/
public cameras: { [uuid: string]: THREE.Camera } = {};
/**
* 场景元数据(即记录更改前的数据以等待还原)
*/
public metadata: Record<string, any> = {};
/**
* 跟踪材质使用的频率
*/
protected materialsRefCounter: Map<object, number> = new Map();
/**
* 场景选中的模型
*/
public selected: THREE.Object3D | null = null;
/**
* 场景锁定的模型
*/
public locked: THREE.Object3D | null = null;
/**
* 日志记录
*/
public log: typeof Logger = Logger;
/**
* 本地indexDB
*/
public storage: Storage = new Storage();
/**
* 配置项
*/
public config: Config = new Config(this.storage);
/**
* 当前工程相关,包括当前工程配置
*/
public project: Project = new Project(this);
/**
* 模型选择器
*/
public selector: Selector = new Selector();
/**
* 历史记录
*/
public history: _History = new _History();
/**
* 资源管理
*/
public resource: Resource = new Resource();
/**
* 全局动画管理
*/
public animationManager: AnimationManager = new AnimationManager();
/**
* 级联阴影映射
*/
public csm: CSM = new CSM(this.project.getKey("csm") as IAppProject.CSM);
/**
* 间隔多长时间渲染渲染一次,用于固定fps上限单位秒
*/
public singleFrameTime: number = 1 / this.FPS;
/**
* 当前视口示例,实例化视口时赋值
*/
public viewer: Viewer | null = null;
constructor() {
this.scene.name = "默认场景";
this.addCamera(this.camera);
useAddSignal("objectFocusByUuid", this.focusByUuid.bind(this))
}
/**
* 获取渲染帧率上限
*/
get FPS(): number {
return this.project.getKey("renderer.fps");
}
/**
* 设置渲染帧率上限
* @param fps
*/
set FPS(fps: number) {
this.project.setKey("renderer.fps", fps, false);
this.singleFrameTime = fps ? (1 / fps) : 0;
}
/**
* 设置初始配置
*/
setConfig(_config: Record<string, any>) {
this.config.setConfig(_config);
}
/**
* 生成场景
* @param scene
*/
setScene(scene: THREE.Scene) {
this.scene.copy(scene, false)
// copy方法不会复制uuid需要手动赋值
this.scene.uuid = scene.uuid;
if (this.scene.animations && this.scene.animations.length > 0) this.clipAction(this.scene);
// 避免对象渲染
useSetSignalActive('sceneGraphChanged', false);
while (scene.children.length > 0) {
this.addObject(scene.children[0]);
}
useSetSignalActive('sceneGraphChanged', true);
useDispatchSignal('sceneGraphChanged');
return this.scene;
}
/**
* 剪辑动画
* @param object
*/
clipAction(object: THREE.Object3D) {
if (!object.animations || !object.animations.length) return;
// 每个包含动画的模型都会有自己的混合器因为如果采用共用scene混合器方案会造成全场景动画播放进度统一的情况
let mixer = this.animationManager.mixerMap.get(object.uuid);
if (!mixer) {
mixer = new THREE.AnimationMixer(object);
this.animationManager.mixerMap.set(object.uuid, mixer);
}
object.animations.forEach((animation, index) => {
if ((animation instanceof THREE.AnimationAction) && animation.getClip()) {
this.animationManager.actionMap.set(animation.getClip().uuid, animation)
return;
}
if (!(animation instanceof THREE.AnimationClip)) return;
const action = (<THREE.AnimationMixer>mixer).clipAction(animation, object);
// @ts-ignore
object.animations[index] = action;
this.animationManager.actionMap.set(animation.uuid, action);
})
}
/**
* 添加模型
* @param object
* @param parent
* @param index
*/
addObject(object: THREE.Object3D, parent?: THREE.Object3D, index?: number) {
// 使用自己版本threejs(比如插件)创建的物体调用此方法时需要递归修复原型链
const fixPrototypeChain = (obj: THREE.Object3D) => {
if (!obj.traverseByCondition) {
Object.setPrototypeOf(obj, THREE.Object3D.prototype);
}
obj.children.forEach(child => child.traverse(c => fixPrototypeChain(c)));
};
fixPrototypeChain(object);
object.traverseByCondition((child) => {
if (child.animations && child.animations.length > 0) this.clipAction(child);
if (child.geometry !== undefined) this.addGeometry(child.geometry);
if (child.material !== undefined) this.addMaterial(child.material);
this.addCamera(child);
this.addHelper(child);
// 20250325: 除灯光外默认全打开接收与投射阴影
if (child.isLight) return;
child.castShadow = true;
child.receiveShadow = true;
}, (child) => !child.ignore);
if (parent === undefined) {
this.scene.add(object);
} else {
parent.children.splice(index || 0, 0, object);
object.parent = parent;
}
useDispatchSignal('objectAdded', object);
useDispatchSignal('sceneGraphChanged');
}
/**
* 移动模型
* @param object
* @param parent
* @param before
*/
moveObject(object: THREE.Object3D, parent: THREE.Object3D, before: THREE.Object3D) {
if (parent === undefined) {
parent = this.scene;
}
parent.add(object);
// 对子数组进行排序
if (before !== undefined) {
const index = parent.children.indexOf(before);
parent.children.splice(index, 0, object);
parent.children.pop();
}
useDispatchSignal('sceneGraphChanged');
}
/**
* 重命名模型
* @param object
* @param name
*/
nameObject(object: THREE.Object3D, name: string) {
object.name = name;
useDispatchSignal('sceneGraphChanged');
}
/**
* 移除模型
* @param object
*/
removeObject(object: THREE.Object3D) {
// 由于含有ignore属性的对象与业务关联不受scene管控
// object.parent === null避免删除相机或场景
if (object.parent === null || object.ignore) return;
object.traverseByCondition((child: THREE.Object3D) => {
this.removeCamera(child);
this.removeHelper(child);
if (child.material !== undefined) this.removeMaterial(child.material);
}, (child: THREE.Object3D) => !child.ignore);
object.parent.remove(object);
useDispatchSignal('objectRemoved', object);
useDispatchSignal('sceneGraphChanged');
}
/**
* 添加几何数据
* @param geometry
*/
addGeometry(geometry: THREE.BufferGeometry) {
this.geometries[geometry.uuid] = geometry;
}
/**
* 设置几何名称
* @param geometry
* @param name
*/
setGeometryName(geometry: THREE.BufferGeometry, name: string) {
geometry.name = name;
useDispatchSignal('sceneGraphChanged');
}
/**
* 场景中新增材质
* @param material
*/
addMaterial(material: THREE.Material | THREE.Material[]) {
if (Array.isArray(material)) {
for (let i = 0, l = material.length; i < l; i++) {
this.addMaterialToRefCounter(material[i]);
}
} else {
this.addMaterialToRefCounter(material);
}
useDispatchSignal('materialAdded');
}
/**
* 新增材质的使用计数
* @param material
*/
addMaterialToRefCounter(material: THREE.Material) {
let materialsRefCounter = this.materialsRefCounter;
let count = materialsRefCounter.get(material);
if (count === undefined) {
materialsRefCounter.set(material, 1);
this.materials[material.uuid] = material;
// 材质添加到csm
this.csm.setupMaterial(material);
material.needsUpdate = true;
} else {
count++;
materialsRefCounter.set(material, count);
}
}
/**
* 场景中移除材质
* @param material
*/
removeMaterial(material: THREE.Material | THREE.Material[]) {
if (Array.isArray(material)) {
for (let i = 0, l = material.length; i < l; i++) {
this.removeMaterialFromRefCounter(material[i]);
}
} else {
this.removeMaterialFromRefCounter(material);
}
useDispatchSignal('materialRemoved');
}
/**
* 移除材质时减少对应材质使用计数
* @param material
*/
removeMaterialFromRefCounter(material: THREE.Material) {
let materialsRefCounter = this.materialsRefCounter;
let count = materialsRefCounter.get(material) as number;
count--;
if (count === 0) {
materialsRefCounter.delete(material);
delete this.materials[material.uuid];
} else {
materialsRefCounter.set(material, count);
}
}
/**
* 通过材质uuid获取材质
* @param uuid
*/
getMaterialByUuid(uuid: string) {
return this.materials[uuid];
}
/**
* 设置材质名称
* @param material
* @param name
*/
setMaterialName(material: THREE.Material, name: string) {
material.name = name;
useDispatchSignal('sceneGraphChanged');
}
/**
* 场景中新增贴图
* @param texture
*/
addTexture(texture: THREE.Texture) {
this.textures[texture.uuid] = texture;
}
/**
* 场景中新增相机
* @param camera
*/
addCamera(camera: THREE.Camera) {
if (camera.isCamera) {
this.cameras[camera.uuid] = camera;
useDispatchSignal('cameraAdded', camera);
}
}
/**
* 场景中移除相机
* @param camera
*/
removeCamera(camera: THREE.Camera | THREE.Object3D) {
if (this.cameras[camera.uuid] !== undefined) {
delete this.cameras[camera.uuid];
useDispatchSignal('cameraRemoved', camera);
}
}
/**
* 场景中新增三维辅助工具
* @param object
* @param helper
*/
addHelper(object: any, helper?: THREE.Object3D) {
if (helper === undefined) {
if (object.isCamera) {
helper = new THREE.CameraHelper(object);
} else if (object.isPointLight) {
helper = new THREE.PointLightHelper(object, 1);
} else if (object.isDirectionalLight) {
helper = new THREE.DirectionalLightHelper(object, 1);
} else if (object.isSpotLight) {
helper = new THREE.SpotLightHelper(object);
} else if (object.isHemisphereLight) {
helper = new THREE.HemisphereLightHelper(object, 1);
} else if (object.isSkinnedMesh && object.skeleton?.bones) {
helper = new THREE.SkeletonHelper(object.skeleton.bones[0]);
} else if (object.isBone && object.parent?.isBone !== true) {
helper = new THREE.SkeletonHelper(object);
} else {
// no helper for this object type
return;
}
let geometry = new THREE.SphereGeometry(2, 4, 2);
let material = new THREE.MeshBasicMaterial({ color: 0xff0000, visible: false });
const picker = new THREE.Mesh(geometry, material);
picker.name = 'picker';
picker.proxy = object;
helper.add(picker);
}
this.sceneHelpers.add(helper);
this.helpers[object.id] = helper;
}
/**
* 移除某个模型上的三维辅助工具
* @param object
*/
removeHelper(object: THREE.Object3D) {
if (this.helpers[object.id] !== undefined) {
const helper = this.helpers[object.id];
helper.parent?.remove(helper);
delete this.helpers[object.id];
}
}
/**
* 新增脚本
* @param object
* @param script
*/
addScript(object: THREE.Object3D, script: ISceneScript) {
this.execute(new AddScriptCommand(object, script));
}
/**
* 移除脚本
* @param object
* @param script
*/
removeScript(object: THREE.Object3D, script: ISceneScript) {
this.execute(new RemoveScriptCommand(object, script));
}
/**
* 获取模型材质
* @param object
* @param slot
*/
getObjectMaterial(object: THREE.Object3D, slot: number) {
let material = object.material;
if (Array.isArray(material) && slot !== undefined) {
material = material[slot];
}
return material;
}
/**
* 设置模型材质
* @param object
* @param slot
* @param newMaterial
*/
setObjectMaterial(object: THREE.Object3D, slot: number | undefined, newMaterial: THREE.Material) {
if (Array.isArray(object.material) && slot !== undefined) {
object.material[slot] = newMaterial;
} else {
object.material = newMaterial;
}
}
/**
* 设置当前视图相机
* @param uuid
*/
setViewportCamera(uuid: string) {
this.viewportCamera = this.cameras[uuid];
useDispatchSignal('viewportCameraChanged');
}
/**
* 设置当前视图渲染方式
* @param value
*/
setViewportShading(value: string) {
this.viewportShading = value;
useDispatchSignal("viewportShadingChanged");
}
/**
* 选中模型
* @param object
*/
select(object: THREE.Object3D) {
this.selector.select(object);
}
/**
* 通过模型id选中模型
* @param id
*/
selectById(id: number) {
if (id === this.camera.id) {
this.select(this.camera);
return;
}
const obj = this.scene.getObjectById(id);
obj && this.select(obj);
}
/**
* 通过模型uuid选中模型
* @param uuid
*/
selectByUuid(uuid: string) {
const scope = this;
this.scene.traverse(function (child: THREE.Object3D) {
if (child.uuid === uuid) {
scope.select(child);
}
});
}
/**
* 取消模型选中状态
*/
deselect() {
this.selector.deselect();
}
/**
* 锁定模型
* @param object
*/
lock(object?: THREE.Object3D | null) {
if (!object) {
object = this.selected;
}
if (object) {
this.locked = object;
useDispatchSignal('objectLocked', object);
}
}
/**
* 取消模型锁定状态
*/
unlock() {
this.locked = null;
useDispatchSignal('objectUnlocked');
}
/**
* 相机聚焦模型
* @param object
*/
focus(object: THREE.Object3D) {
if (object !== undefined) {
useDispatchSignal('objectFocused', object);
}
}
/**
* 通过id聚焦模型
* @param id
*/
focusById(id: number) {
const obj = this.scene.getObjectById(id);
obj && this.focus(obj);
}
/**
* 通过uuid聚焦模型
* @param uuid
*/
focusByUuid(uuid: string) {
if (uuid === undefined) {
this.deselect();
return;
}
const obj = this.getObjectByUuid(uuid);
obj && this.focus(obj);
}
/**
* 通过uuid获取模型
* @param uuid
*/
getObjectByUuid(uuid: string) {
return this.scene.getObjectByProperty('uuid', uuid);
}
/**
* 遍历平铺所有子级mesh
* @param object
*/
traverseMeshToArr(object: THREE.Object3D) {
if (object.isMesh) return [object];
const arr: THREE.Mesh[] = [];
object.traverse((item: THREE.Object3D) => {
if (item.isMesh) {
arr.push(item as THREE.Mesh);
}
})
return arr;
}
/**
* 获取不包含ignore属性模型的scene
*/
getSceneWithoutIgnore() {
const newScene = this.scene.clone(false);
newScene.uuid = this.scene.uuid;
this.scene.children.forEach((item) => {
if (!item.ignore) {
const model = item.clone();
model.uuid = item.uuid;
newScene.add(model);
}
})
return newScene;
}
/**
* 创建PBR材质
* @param textures
* @param properties
*/
createPBRMaterial(textures: { [type: string]: string | THREE.Texture } = {}, properties: any = {}): Promise<THREE.MeshStandardMaterial> {
return new Promise((resolve, reject) => {
const material = new THREE.MeshStandardMaterial({
// 位移贴图对网格的影响程度默认设置为0
displacementScale: 0
});
properties && Object.keys(properties).forEach(key => {
material[key] = properties[key];
});
const num = new Proxy({ value: 10 }, {
set(target: { value: number }, p: string | symbol, newValue: any): boolean {
target[p] = newValue;
if (p === 'value' && newValue === 0) {
resolve(material);
}
return true;
}
})
// 基础颜色贴图(高光反射/光泽度工作流:diffuse, 金属/粗糙度工作流:baseColor)
if (textures.baseColor) {
this.resource.loadURLTexture(textures.baseColor, (texture => {
material.map = texture;
num.value--;
}), err => {
reject(err);
});
} else {
num.value--;
}
// 法线贴图
if (textures.normal) {
this.resource.loadURLTexture(textures.normal, (texture => {
material.normalMap = texture;
num.value--;
}), err => {
reject(err);
});
} else if (textures.bump) {
// 凹凸贴图(如果定义了法线贴图,则将忽略该贴图)
this.resource.loadURLTexture(textures.bump, (texture => {
material.bumpMap = texture;
num.value--;
}), err => {
reject(err);
});
} else {
num.value--;
}
// 置换贴图(位移贴图)
if (textures.displacement) {
this.resource.loadURLTexture(textures.displacement, (texture => {
material.displacementMap = texture;
num.value--;
}), err => {
reject(err);
});
} else {
num.value--;
}
// 粗糙度贴图
if (textures.roughness) {
this.resource.loadURLTexture(textures.roughness, (texture => {
material.roughnessMap = texture;
num.value--;
}), err => {
reject(err);
});
} else {
num.value--;
}
// 金属度贴图
if (textures.metalness) {
this.resource.loadURLTexture(textures.metalness, (texture => {
material.metalnessMap = texture;
num.value--;
}), err => {
reject(err);
});
} else {
num.value--;
}
// 环境遮挡贴图
if (textures.ao) {
this.resource.loadURLTexture(textures.ao, (texture => {
material.aoMap = texture;
num.value--;
}), err => {
reject(err);
});
} else {
num.value--;
}
// 自发光贴图
if (textures.emissive) {
this.resource.loadURLTexture(textures.emissive, (texture => {
material.emissiveMap = texture;
num.value--;
}), err => {
reject(err);
});
} else {
num.value--;
}
// 透明贴图
if (textures.alpha) {
this.resource.loadURLTexture(textures.alpha, (texture => {
material.alphaMap = texture;
num.value--;
}), err => {
reject(err);
});
} else {
num.value--;
}
// 环境贴图一般不会设置因为会使用scene.environment
if (textures.env) {
this.resource.loadURLTexture(textures.env, (texture => {
material.envMap = texture;
num.value--;
}), err => {
reject(err);
});
} else {
num.value--;
}
// 光照贴图
if (textures.light) {
this.resource.loadURLTexture(textures.light, (texture => {
material.lightMap = texture;
num.value--;
}), err => {
reject(err);
});
} else {
num.value--;
}
})
}
/**
* 清空场景
*/
clear() {
this.history.clear();
this.camera.copy(_DEFAULT_CAMERA);
useDispatchSignal('cameraReset');
this.scene.name = '默认场景';
this.scene.position.set(0, 0, 0);
this.scene.rotation.set(0, 0, 0);
this.scene.userData = {};
this.scene.background = null;
this.scene.environment = null;
this.scene.fog = null;
for (let i = this.scene.children.length - 1; i >= 0; i--) {
this.removeObject(this.scene.children[i]);
}
this.geometries = {};
this.materials = {};
this.textures = {};
this.scripts = {};
this.materialsRefCounter.clear();
this.animationManager.mixerMap.forEach(mixer => mixer.stopAllAction());
this.deselect();
useDispatchSignal('sceneCleared');
}
/**
* 从json数据生成场景
* @param sceneJson
*/
async fromJSON(sceneJson: ISceneJson) {
//先清空场景
this.clear();
// 清除图纸状态
this.project.resetDrawing();
this.metadata = sceneJson.metadata || {};
sceneJson.metadata = {};
let loader = Loader.objectLoader;
let camera = await loader.parseAsync(sceneJson.camera) as THREE.Camera;
this.camera.copy(camera as THREE.PerspectiveCamera);
useDispatchSignal('cameraReset');
if (sceneJson.scripts) {
this.scripts = sceneJson.scripts;
}
const scene = this.setScene(await loader.parseAsync(sceneJson.scene) as THREE.Scene);
// 20250718: 环境类型是ModelViewer时需要手动设置因为scene.toJSON()不会处理renderTargetTexture
switch (sceneJson.scene.object.environmentType) {
case "ModelViewer":
useDispatchSignal("sceneEnvironmentChanged", 'ModelViewer');
useDispatchSignal("sceneGraphChanged");
break
}
return scene;
}
/**
* 场景信息转JSON
*/
toJSON() {
// 脚本清理
let scene = this.scene;
let scripts = this.scripts;
for (let key in scripts) {
let script = scripts[key];
if (script.length === 0 || scene.getObjectByProperty('uuid', key) === undefined) {
delete scripts[key];
}
}
const projectRender = this.project.getKey("renderer");
return {
metadata: {},
project: {
xr: this.project.getKey("xr"),
antialias: projectRender.antialias,
shadows: projectRender.shadows.enabled,
shadowType: projectRender.shadows.type,
toneMapping: projectRender.shadows.toneMapping,
toneMappingExposure: projectRender.shadows.toneMappingExposure,
},
camera: this.camera.toJSON(),
scene: this.scene.toJSON(),
scripts: this.scripts,
//history: this.history.toJSON(),
};
}
/**
* 执行历史记录中的命令
* @param cmd
* @param optionalName
*/
execute(cmd, optionalName?: string) {
this.history.execute(cmd, optionalName);
}
/**
* 撤销
*/
undo() {
this.history.undo();
}
/**
* 重做
*/
redo() {
this.history.redo();
}
}
const app = new App();
export default app;