feat(All):Initial
This commit is contained in:
@@ -0,0 +1,537 @@
|
||||
/**
|
||||
* @author ErSan
|
||||
* @email mlt131220@163.com
|
||||
* @date 2025/7/30 18:49
|
||||
* @description 资源预览类
|
||||
*/
|
||||
import * as THREE from 'three';
|
||||
import CameraControls from 'camera-controls';
|
||||
import { Timer } from 'three/examples/jsm/misc/Timer.js';
|
||||
import {CSS3DRenderer} from "three/examples/jsm/renderers/CSS3DRenderer.js";
|
||||
import {PreviewOptions} from "@/core/preview/PreviewOptions.ts";
|
||||
import Loader from "@/core/loader/Loader.ts";
|
||||
import {createDivContainer, deepAssign, parseMaterialZip} from "@/utils";
|
||||
import {getDefaultBillboardOptions} from "@/core/objects";
|
||||
import {POSITION} from "@/constant";
|
||||
import {Emitter} from '@/core/libs/three-nebula';
|
||||
import ParticleEmitter from "@/core/objects/ParticleEmitter.ts"
|
||||
import Billboard from "@/core/objects/Billboard.ts";
|
||||
import Tiles from "../objects/Tile.ts";
|
||||
import {focusObject} from "@/utils/scene/controls.ts";
|
||||
import {ParticleSystem,TilesManage} from "@/core/viewer/modules";
|
||||
|
||||
export interface PreviewerEventMap {
|
||||
// 场景当前动画帧循环完成之后渲染之前触发,每一次渲染执行一次
|
||||
beforeRender: { };
|
||||
|
||||
// 场景当前帧渲染完成之后触发,每一次渲染执行一次
|
||||
afterRender: { };
|
||||
}
|
||||
|
||||
export interface PreviewerModules {
|
||||
controls: CameraControls,
|
||||
particleSystem: ParticleSystem,
|
||||
tilesManage:TilesManage,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
});
|
||||
|
||||
export default class Preview extends THREE.EventDispatcher<PreviewerEventMap> {
|
||||
public _container: HTMLElement;
|
||||
public options: IPreviewSetting;
|
||||
public renderer: THREE.WebGLRenderer;
|
||||
public camera: THREE.PerspectiveCamera;
|
||||
public scene: THREE.Scene;
|
||||
public modules: PreviewerModules;
|
||||
public css3DRenderer: CSS3DRenderer;
|
||||
|
||||
public timer = new Timer();
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
private resize: () => void;
|
||||
|
||||
constructor(options: IPreviewSetting) {
|
||||
super();
|
||||
|
||||
this._container = options.container || createDivContainer();
|
||||
|
||||
this.options = PreviewOptions();
|
||||
deepAssign(this.options, options);
|
||||
|
||||
const {camera, scene, renderer,css3DRenderer} = this.basicCreation();
|
||||
this.camera = camera;
|
||||
this.scene = scene;
|
||||
this.renderer = renderer;
|
||||
this.css3DRenderer = css3DRenderer;
|
||||
this._container.appendChild(renderer.domElement);
|
||||
this._container.appendChild(css3DRenderer.domElement);
|
||||
|
||||
this.modules = this.initModules();
|
||||
|
||||
this.loadEnv({setBg:true});
|
||||
|
||||
this.renderer.setAnimationLoop(this.animate.bind(this));
|
||||
|
||||
this.resize = this.onResize();
|
||||
this.resize();
|
||||
}
|
||||
|
||||
get container(): HTMLElement {
|
||||
return this._container;
|
||||
}
|
||||
|
||||
set container(container: HTMLElement) {
|
||||
this._container.removeChild(this.renderer.domElement);
|
||||
|
||||
if(this.resizeObserver){
|
||||
this.resizeObserver.unobserve(this._container);
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
this._container = container;
|
||||
|
||||
this._container.appendChild(this.renderer.domElement);
|
||||
|
||||
this.resize = this.onResize();
|
||||
}
|
||||
|
||||
basicCreation(){
|
||||
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100 * 1000);
|
||||
camera.name = "Camera";
|
||||
camera.position.set(0, 5, 10);
|
||||
camera.lookAt(new THREE.Vector3());
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.name = "Scene";
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
preserveDrawingBuffer: false,
|
||||
powerPreference: "high-performance",
|
||||
});
|
||||
renderer.autoClear = false;
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1;
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.autoUpdate = false;
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
renderer.xr.enabled = false;
|
||||
renderer.setClearColor(0x272727, 1);
|
||||
renderer.setPixelRatio(Math.max(Math.ceil(window.devicePixelRatio), 1));
|
||||
renderer.setSize(this._container.offsetWidth, this._container.offsetHeight);
|
||||
|
||||
const css3DRenderer = new CSS3DRenderer();
|
||||
css3DRenderer.setSize(this._container.offsetWidth, this._container.offsetHeight);
|
||||
css3DRenderer.domElement.setAttribute("id", "astral-3d-preview-css3DRenderer");
|
||||
css3DRenderer.domElement.style.position = 'absolute';
|
||||
css3DRenderer.domElement.style.top = '0px';
|
||||
css3DRenderer.domElement.style.pointerEvents = 'none';
|
||||
|
||||
return { camera,scene,renderer,css3DRenderer };
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化功能模块
|
||||
*/
|
||||
initModules():PreviewerModules {
|
||||
const controls = new CameraControls(this.camera,this.renderer.domElement);
|
||||
controls.addEventListener("update", () => {});
|
||||
|
||||
return {
|
||||
controls,
|
||||
// 粒子系统
|
||||
particleSystem: new ParticleSystem(this),
|
||||
// 3d tiles管理器
|
||||
tilesManage: new TilesManage(this.scene),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载默认环境和背景
|
||||
*/
|
||||
loadEnv(
|
||||
options?:{
|
||||
setBg?: boolean,
|
||||
extension?: string,
|
||||
onLoad?: (texture: THREE.Texture) => void,
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
) {
|
||||
if (!this.options.hdr) return;
|
||||
|
||||
const params = Object.assign({
|
||||
setBg: true,
|
||||
extension: this.options.hdr.split(".").pop()?.toLowerCase() || 'hdr'
|
||||
},options)
|
||||
|
||||
Loader.loadUrlTexture(params.extension, this.options.hdr, (texture: THREE.Texture) => {
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
this.scene.environment = texture;
|
||||
params.setBg && (this.scene.background = texture);
|
||||
|
||||
this.render();
|
||||
|
||||
params.onLoad && params.onLoad(texture)
|
||||
},(err) => params.onError && params.onError(err));
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载预览项
|
||||
*/
|
||||
load(fileOrUrl:string | File,type:string = "Model") {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
this.clear();
|
||||
|
||||
let file = fileOrUrl;
|
||||
if(!(fileOrUrl instanceof File) && !["Texture","Billboard","HDR","Tiles"].includes(type)) {
|
||||
const response = await fetch(fileOrUrl);
|
||||
if (!response.ok) {
|
||||
reject('The network is responding abnormally');
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = fileOrUrl.substring(fileOrUrl.lastIndexOf("/") + 1);
|
||||
|
||||
const blob = await response.blob();
|
||||
file = new File([blob], filename,{ type: blob.type });
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "Model":
|
||||
Loader.loadFile(file, new THREE.LoadingManager(),null,false).then((model) => {
|
||||
this.scene.add(model);
|
||||
focusObject(model,this.modules.controls);
|
||||
|
||||
resolve(model);
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
})
|
||||
break;
|
||||
case "Material":
|
||||
parseMaterialZip(file as File)
|
||||
.then((material) => {
|
||||
const geometry = new THREE.SphereGeometry(1, 32, 32, 0, Math.PI * 2, 0, Math.PI);
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
this.scene.add(mesh);
|
||||
|
||||
focusObject(mesh,this.modules.controls);
|
||||
|
||||
resolve(mesh);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
})
|
||||
break;
|
||||
case "Texture":
|
||||
let mapPath = file;
|
||||
if(file instanceof File){
|
||||
mapPath = URL.createObjectURL(file);
|
||||
}
|
||||
|
||||
const geometry = new THREE.PlaneGeometry(1, 1);
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
side: THREE.DoubleSide,
|
||||
map: new THREE.TextureLoader().load(mapPath as string,(texture) => {
|
||||
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
||||
texture.repeat.set(1, 1);
|
||||
if(file instanceof File){
|
||||
URL.revokeObjectURL(mapPath as string)
|
||||
}
|
||||
|
||||
const panel = new THREE.Mesh(geometry, material);
|
||||
|
||||
this.scene.add(panel);
|
||||
focusObject(panel, this.modules.controls);
|
||||
|
||||
resolve(panel);
|
||||
},undefined,(err) => reject(err))
|
||||
});
|
||||
|
||||
break;
|
||||
case "Billboard":
|
||||
const _ops = getDefaultBillboardOptions();
|
||||
_ops.image.visible = true;
|
||||
_ops.image.position = POSITION.LEFT;
|
||||
if(file instanceof File){
|
||||
_ops.name = file.name;
|
||||
_ops.image.url = URL.createObjectURL(file);
|
||||
} else {
|
||||
_ops.name = file.substring(file.lastIndexOf("/") + 1);
|
||||
_ops.image.url = file;
|
||||
}
|
||||
|
||||
const billboard = new Billboard(_ops);
|
||||
|
||||
const handleBillboardImgLoaded = () => {
|
||||
if(file instanceof File){
|
||||
URL.revokeObjectURL(_ops.image.url);
|
||||
}
|
||||
|
||||
billboard.removeEventListener("imgLoaded", handleBillboardImgLoaded);
|
||||
}
|
||||
billboard.addEventListener("imgLoaded", handleBillboardImgLoaded);
|
||||
|
||||
this.scene.add(billboard);
|
||||
focusObject(billboard, this.modules.controls);
|
||||
|
||||
resolve(billboard);
|
||||
break;
|
||||
case "HDR":
|
||||
let hdrPath = file, extension = "hdr";
|
||||
if(file instanceof File){
|
||||
hdrPath = URL.createObjectURL(file);
|
||||
extension = file.name.split(".").pop()?.toLowerCase() || "hdr"
|
||||
}else {
|
||||
extension = file.split(".").pop()?.toLowerCase() || "hdr";
|
||||
}
|
||||
|
||||
this.options.hdr = hdrPath as string;
|
||||
this.loadEnv({
|
||||
setBg:true,
|
||||
extension:extension,
|
||||
onLoad:(texture) => {
|
||||
if(file instanceof File){
|
||||
URL.revokeObjectURL(hdrPath as string);
|
||||
}
|
||||
|
||||
resolve(texture);
|
||||
},
|
||||
onError: (error) => {
|
||||
if(file instanceof File){
|
||||
URL.revokeObjectURL(hdrPath as string);
|
||||
}
|
||||
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
break;
|
||||
case "Tiles":
|
||||
if(fileOrUrl instanceof File){
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
if(!fileOrUrl.includes("tileset.json")) {
|
||||
fileOrUrl = `${fileOrUrl}/tileset.json`;
|
||||
}
|
||||
const tiles = new Tiles({
|
||||
url: fileOrUrl,
|
||||
name: "AstralPreviewTiles",
|
||||
reset2origin:true,
|
||||
debug:false,
|
||||
})
|
||||
this.addTiles(tiles).then(() => {
|
||||
this.modules.controls.fitToBox(tiles,true);
|
||||
})
|
||||
|
||||
resolve(tiles);
|
||||
break;
|
||||
default:
|
||||
reject("A type for which previews are not yet supported");
|
||||
break;
|
||||
}
|
||||
|
||||
this.render();
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听视窗变化(节流)
|
||||
*/
|
||||
onResize(){
|
||||
const resize = () => {
|
||||
this.camera.aspect = this._container.offsetWidth / this._container.offsetHeight;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(this._container.offsetWidth, this._container.offsetHeight);
|
||||
}
|
||||
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
if (timer) return;
|
||||
timer = setTimeout(() => {
|
||||
resize();
|
||||
|
||||
timer = null;
|
||||
}, 16)
|
||||
});
|
||||
this.resizeObserver.observe(this._container);
|
||||
|
||||
return resize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空场景
|
||||
*/
|
||||
clear(){
|
||||
for(let i = this.scene.children.length - 1; i >= 0; i--){
|
||||
const child = this.scene.children[i];
|
||||
if(!child.parent || child.ignore) continue;
|
||||
|
||||
child.parent.remove(child);
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
animate(){
|
||||
this.timer.update();
|
||||
|
||||
const delta = this.timer.getDelta();
|
||||
|
||||
// let needRender = this.modules.controls.update(delta);
|
||||
this.modules.controls.update(delta);
|
||||
|
||||
this.modules.particleSystem.update(delta);
|
||||
// if (this.modules.particleSystem.needsUpdate) {
|
||||
// needRender = true;
|
||||
// }
|
||||
|
||||
// 3dTiles渲染
|
||||
this.modules.tilesManage.update()
|
||||
// if(this.modules.tilesManage.update()){
|
||||
// needRender = true;
|
||||
// }
|
||||
|
||||
// if (needRender) this.render();
|
||||
this.render();
|
||||
}
|
||||
|
||||
render(){
|
||||
this.dispatchEvent({type: 'beforeRender'});
|
||||
|
||||
this.renderer.autoClear = false;
|
||||
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
|
||||
this.css3DRenderer.render(this.scene, this.camera);
|
||||
|
||||
this.renderer.autoClear = true;
|
||||
|
||||
this.dispatchEvent({type: 'afterRender'});
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
dispose() {
|
||||
this.resizeObserver?.unobserve(this._container);
|
||||
this.resizeObserver?.disconnect();
|
||||
|
||||
Object.keys(this.modules).forEach(key => {
|
||||
if (this.modules[key].dispose) {
|
||||
this.modules[key].dispose();
|
||||
}
|
||||
})
|
||||
|
||||
this.modules.controls.disconnect();
|
||||
|
||||
this.clear();
|
||||
this.scene.background = null;
|
||||
this.scene.environment = null;
|
||||
|
||||
this._container.removeChild(this.renderer.domElement);
|
||||
|
||||
this.renderer.setAnimationLoop(null);
|
||||
this.renderer.dispose();
|
||||
// @ts-ignore
|
||||
this.renderer = null;
|
||||
|
||||
// @ts-ignore 清空EventDispatcher监听
|
||||
if(this._listeners){
|
||||
// @ts-ignore
|
||||
Object.keys(this._listeners).forEach(type => {
|
||||
// @ts-ignore
|
||||
this._listeners[type].forEach(listener => {
|
||||
// @ts-ignore
|
||||
this.removeEventListener(type, listener);
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------暂时放在Preview下的工具方法-------------------- */
|
||||
|
||||
/**
|
||||
* 添加粒子
|
||||
* @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);
|
||||
|
||||
this.scene.add(particleEmitter);
|
||||
|
||||
return particleEmitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加瓦片
|
||||
*/
|
||||
addTiles(tiles:Tiles){
|
||||
return new Promise(resolve => {
|
||||
tiles.setCameraAndRenderer(this.camera, this.renderer);
|
||||
|
||||
this.modules.tilesManage.addTiles(tiles).then(tiles => {
|
||||
resolve(tiles);
|
||||
})
|
||||
|
||||
this.scene.add(tiles);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除瓦片
|
||||
*/
|
||||
removeTiles(tiles:Tiles){
|
||||
this.scene.remove(tiles);
|
||||
|
||||
this.modules.tilesManage.removeTiles(tiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取画布的截屏图片
|
||||
* @returns Promise<HTMLImageElement> 截屏的图片对象
|
||||
*/
|
||||
getViewportImage() {
|
||||
return new Promise<HTMLImageElement>((resolve, rejcet) => {
|
||||
// @ts-ignore
|
||||
const _preserveDrawingBuffer = this.renderer.getContext().preserveDrawingBuffer;
|
||||
// @ts-ignore
|
||||
this.renderer.getContext().preserveDrawingBuffer = true;
|
||||
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.render();
|
||||
|
||||
resolve(image);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export const PreviewOptions = (): IPreviewSetting => ({
|
||||
// 场景canvas容器
|
||||
container: undefined,
|
||||
// 场景背景及环境贴图
|
||||
hdr: undefined,
|
||||
// 请求相关
|
||||
request:{
|
||||
baseUrl:""
|
||||
},
|
||||
// 相机控制器
|
||||
control: {
|
||||
minAzimuthAngle: -Infinity,
|
||||
maxAzimuthAngle: Infinity,
|
||||
minDistance: 0.0,
|
||||
maxDistance: Infinity,
|
||||
maxPolarAngle: Math.PI,
|
||||
minPolarAngle: 0,
|
||||
maxZoom:Infinity,
|
||||
minZoom:0.01,
|
||||
dollySpeed:1,
|
||||
// 缩放是否以鼠标位置为中心
|
||||
dollyToCursor: false,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user