feat(All):Initial

This commit is contained in:
2025-10-04 23:36:07 +08:00
commit 2b4e5d2668
1321 changed files with 415958 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
/**
* 性能状态监视器,基于stats.js
* 默认展示全部面板
*/
import ThreeStats from 'three/examples/jsm/libs/stats.module.js';
import Viewer from "@/core/viewer/Viewer.ts";
export class Stats {
private viewer: Viewer;
private threeStats: ThreeStats;
private panel = 0;
private _visible = true;
private fns:{
beforeRender:null | (() => void);
afterRender:null | (() => void);
} = {
beforeRender: null,
afterRender: null,
}
constructor(viewer: Viewer) {
this.viewer = viewer;
this.threeStats = new ThreeStats();
this.initEvent();
this.init();
}
get domElement():HTMLElement {
return this.threeStats.dom;
}
get visible(){
return this._visible;
}
set visible(visible:boolean) {
this._visible = visible;
this.domElement.style.display = visible ? "block" : 'none';
}
initEvent(){
this.fns.beforeRender = () => {
if(!this.visible) return;
this.threeStats.begin();
};
this.viewer.addEventListener("beforeRender",this.fns.beforeRender);
this.fns.afterRender = () => {
if(!this.visible) return;
this.threeStats.end();
};
this.viewer.addEventListener("afterRender",this.fns.afterRender);
}
init(){
const canvases = this.domElement.querySelectorAll("canvas");
canvases.forEach(canvas => {
canvas.style.width = "5rem";
canvas.style.height = "3rem";
canvas.style.display = "block";
});
}
showPanel(type: number | 'fps' | 'ms' | 'mb') {
if (typeof type === 'number') {
this.threeStats.showPanel(type);
this.panel = type;
return;
}
switch (type.toLowerCase()) {
case 'fps':
this.threeStats.showPanel(0);
this.panel = 0;
break;
case 'ms':
this.threeStats.showPanel(1);
this.panel = 1;
break;
case 'mb':
this.threeStats.showPanel(2);
this.panel = 2;
break;
}
}
showAllPanels(show:boolean) {
const canvases = this.domElement.querySelectorAll("canvas");
canvases.forEach(canvas => {
canvas.style.display = show ? "block" : "none";
});
if(!show){
this.showPanel(this.panel);
}
}
dispose(){
if(this.fns.beforeRender){
this.viewer.removeEventListener("beforeRender",this.fns.beforeRender);
this.fns.beforeRender = null;
}
if(this.fns.afterRender){
this.viewer.removeEventListener("afterRender",this.fns.afterRender);
this.fns.afterRender = null;
}
// @ts-ignore
this.threeStats = null;
}
}
+12
View File
@@ -0,0 +1,12 @@
import {Object3D,Box3,Vector3} from "three";
import CameraControls from "camera-controls";
export function focusObject(object:Object3D,controls:CameraControls,enableTransition: boolean = true){
const box3 = new Box3();
box3.setFromObject(object);
if(box3.isEmpty()){
box3.set(new Vector3(object.position.x-1, object.position.y-1, object.position.z-1), new Vector3(object.position.x+1, object.position.y+1, object.position.z+1));
}
return controls.fitToBox(box3,enableTransition);
}
+139
View File
@@ -0,0 +1,139 @@
import { Object3D,InstancedMesh,Mesh,Matrix4,AnimationAction,AnimationClip } from "three";
import App from "@/core/app/App";
export * from "./material";
export * from "./Stats";
export * from "./controls";
/**
* 获取对象到父对象的路径(结果不包含parentObject)
* @param parentObject
* @param object
* @param attr 对象属性名
* @param splitter 路径分隔符
*/
export function getParentPath(parentObject:Object3D,object:Object3D,attr = 'name',splitter = '/'){
if(!parentObject || !object) return '';
if(parentObject === object) return object[attr];
let path = [object[attr]];
const getPath = (obj) => {
if(!obj.parent) return;
if(obj.parent === parentObject) return;
path.unshift(obj.parent[attr]);
getPath(obj.parent);
}
getPath(object);
return path.join(splitter);
}
/**
* 获取鼠标按下的位置
* @param dom
* @param x
* @param y
*/
export function getMousePosition(dom: HTMLElement, x: number, y: number) {
const rect = dom.getBoundingClientRect();
return [(x - rect.left) / rect.width, (y - rect.top) / rect.height];
}
/**
* InstancedMesh 解出所有 mesh
*/
export function getMeshByInstancedMesh(instancedMesh:InstancedMesh){
const meshes:Mesh[] = [];
// if (instancedMesh.material === undefined) return meshes;
const matrixWorld = instancedMesh.matrixWorld;
const count = instancedMesh.count;
for (let instanceId = 0; instanceId < count; instanceId++) {
const _mesh = new Mesh();
const _instanceLocalMatrix = new Matrix4();
const _instanceWorldMatrix = new Matrix4();
_mesh.geometry = instancedMesh.geometry;
_mesh.material = instancedMesh.material;
// 计算每个实例的世界矩阵
instancedMesh.getMatrixAt(instanceId, _instanceLocalMatrix);
_instanceWorldMatrix.multiplyMatrices(matrixWorld, _instanceLocalMatrix);
// 网格表示这个单一实例
_mesh.matrixWorld = _instanceWorldMatrix;
meshes.push(_mesh);
}
return meshes;
}
/**
* 判断是否是group,因为导入有可能存在被定义为Object3D类型的group
*/
export function isGroup(object3D:Object3D){
return (object3D.isGroup || object3D.children.length > 0)
}
/**
* 判断是否是代理粒子发射器的3D对象
*/
export function isParticleObject(object:Object3D | null){
return object && object.type === "Particle" && object.emitter;
}
/**
* 判断是否是Billboard 3D对象
*/
export function isBillboardObject(object:Object3D | null){
return object && object.type === "Billboard" && object.options;
}
/**
* 判断是否是HtmlPanel 3D对象
*/
export function isHtmlPanelObject(object:Object3D | null){
return object && (object.isHtmlPanel || object.isHtmlSprite) && object.element;
}
/**
* 获取场景/物体中的所有动画
*/
export function getAnimations(object = App.scene) {
const animations: any = [];
object.traverse(function (object) {
animations.push(...object.animations);
});
return animations;
}
/**
* 获取场景/物体中的所有动画剪辑
*/
export function getAnimationClips(object:Object3D = App.scene) {
const animations: any = [];
object.traverse(function (object) {
object.animations.forEach(animation => {
if(animation instanceof AnimationAction){
animations.push(animation.getClip());
}
if(animation instanceof AnimationClip){
animations.push(animation);
}
})
});
return animations;
}
+134
View File
@@ -0,0 +1,134 @@
import JSZip from "jszip";
import * as THREE from "three";
import Loader from "@/core/loader/Loader.ts";
import App from "@/core/app/App.ts";
/**
* 解析材质zip包
*/
export function parseMaterialZip(zipFile: File): Promise<THREE.MeshStandardMaterial> {
return new Promise(async (resolve, reject) => {
const zip = new JSZip();
const zipContent = await zip.loadAsync(zipFile);
// 强制检查根目录下是否存在material.json
let materialJson: any = zipContent.file('material.json');
if (!materialJson) {
materialJson = {
textures: {},
properties: {}
}
// 获取所有文件路径列表(JSZip存储结构为 {路径: 文件对象})
const filePaths = Object.keys(zipContent.files);
for (let i = 0; i < filePaths.length; i++) {
const relativePath: string = filePaths[i];
const file = zipContent.file(relativePath);
if (!file) continue;
// 判断是否为文件(JSZip中目录路径以 '/' 结尾)
if (file.dir || relativePath.endsWith('/')) continue;
if (relativePath.includes("baseColor")) {
materialJson.textures.baseColor = relativePath;
} else if (relativePath.includes("normal")) {
materialJson.textures.normal = relativePath;
} else if (relativePath.includes("bump")) {
materialJson.textures.bump = relativePath;
} else if (relativePath.includes("displacement")) {
materialJson.textures.displacement = relativePath;
} else if (relativePath.includes("emissive")) {
materialJson.textures.emissive = relativePath;
} else if (relativePath.includes("alpha")) {
materialJson.textures.alpha = relativePath;
} else if (relativePath.includes("env")) {
materialJson.textures.env = relativePath;
} else if (relativePath.includes("light")) {
materialJson.textures.light = relativePath;
} else {
// arm可能在一张图上各占一个通道
if (relativePath.includes("roughness")) {
materialJson.textures.roughness = relativePath;
}
if (relativePath.includes("metalness")) {
materialJson.textures.metalness = relativePath;
}
if (relativePath.includes("ao")) {
materialJson.textures.ao = relativePath;
}
}
}
} else {
try {
materialJson = JSON.parse(await materialJson.async('text'));
} catch (error) {
reject(error);
}
}
// 并行加载所有纹理
const texturePromises = Object.entries(materialJson.textures).map(
async ([type, path]:any) => {
const textureFile = zipContent.file(path);
if (!textureFile) {
console.warn(`Texture file not found: ${path}`);
return { type, texture: null };
}
const extension = path.toString().split(".").pop()?.toLowerCase() || "jpg";
let textureBlob: Blob;
try {
// 特殊处理 EXR 格式
if (extension === "exr") {
const buffer = await textureFile.async("arraybuffer");
textureBlob = new Blob([buffer], { type: "image/x-exr" });
} else {
textureBlob = await textureFile.async("blob");
}
} catch (err) {
console.error(`Failed to load texture (${type}):`, err);
return { type, texture: null };
}
const textureUrl = URL.createObjectURL(textureBlob);
return new Promise<{ type: string; texture: THREE.Texture | null }>(
(resolve) => {
Loader.loadUrlTexture(
extension,
textureUrl,
(texture: THREE.Texture) => {
URL.revokeObjectURL(textureUrl); // 清理资源
resolve({ type, texture });
},
(error: Error) => {
URL.revokeObjectURL(textureUrl);
console.error(`Texture load error (${type}):`, error);
resolve({ type, texture: null });
}
);
}
);
}
);
// 等待所有纹理加载完成
const textureResults = await Promise.all(texturePromises);
const textures = textureResults.reduce((acc, { type, texture }) => {
if (texture) acc[type] = texture;
return acc;
}, {} as Record<string, THREE.Texture>);
// 处理无有效纹理的情况
if (Object.keys(textures).length === 0) {
throw new Error("No valid textures found in the zip file");
}
const material = await App.createPBRMaterial(textures, materialJson.properties || {});
resolve(material);
})
}