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
@@ -0,0 +1,195 @@
import * as THREE from "three";
import CameraControls from "camera-controls";
import Viewer from "../Viewer"
/**
* @author ErSan
* @email mlt131220@163.com
* @date 2024/10/21 21:16
* @description 场景相机管理器。 TODO: 后续应把viewport.camera也管理进来
*/
export class CameraManage {
private readonly viewer: Viewer;
/**
* 默认交互相机参数
*/
options: {};
private readonly controls: CameraControls;
constructor(viewer:Viewer,cameraControls:CameraControls) {
this.viewer = viewer;
this.controls = cameraControls;
this.options = this.viewer.options.control;
this.init();
}
init(){
this.setInteract(this.options);
}
/**
* 设置交互模式
*/
setInteract(config){
const opt = Object.assign({}, this.options, config);
Object.keys(opt).forEach(key => {
if(this.controls[key] !== undefined){
this.controls[key] = opt[key];
}
})
}
/**
* 前视图
* @description 以z轴方向为正前方
*/
front() {
return new Promise((resolve, reject) => {
try {
this.setInteract({
minAzimuthAngle: 0,
maxAzimuthAngle: 0,
minPolarAngle: Math.PI / 2,
maxPolarAngle: Math.PI / 2,
})
this.controls.rotateTo(THREE.MathUtils.degToRad(0), THREE.MathUtils.degToRad(90), true).then(() => {
this.controls.fitToSphere(this.viewer.scene, true);
})
resolve(this.viewer.camera);
} catch (e) {
reject(e)
}
})
}
/**
* 后视图
* @description -z方向
*/
rear() {
return new Promise((resolve, reject) => {
try {
this.setInteract({
minAzimuthAngle: -Math.PI,
maxAzimuthAngle: -Math.PI,
minPolarAngle: Math.PI / 2,
maxPolarAngle: Math.PI / 2,
});
this.controls.rotateTo(THREE.MathUtils.degToRad(-180), THREE.MathUtils.degToRad(90), true).then(() => {
this.controls.fitToSphere(this.viewer.scene, true);
})
resolve(this.viewer.camera);
} catch (e) {
reject(e)
}
})
}
/**
* 左视图
* @description -x方向
*/
left() {
return new Promise((resolve, reject) => {
try {
this.setInteract({
minAzimuthAngle: -Math.PI / 2,
maxAzimuthAngle: -Math.PI / 2,
minPolarAngle: Math.PI / 2,
maxPolarAngle: Math.PI / 2,
});
this.controls.rotateTo(THREE.MathUtils.degToRad(-180), THREE.MathUtils.degToRad(180), true).then(() => {
this.controls.fitToSphere(this.viewer.scene, true);
})
resolve(this.viewer.camera);
} catch (e) {
reject(e)
}
})
}
/**
* 右视图
* @description x方向
*/
right() {
return new Promise((resolve, reject) => {
try {
this.setInteract({
minAzimuthAngle: Math.PI / 2,
maxAzimuthAngle: Math.PI / 2,
minPolarAngle: Math.PI / 2,
maxPolarAngle: Math.PI / 2
});
this.controls.rotateTo(THREE.MathUtils.degToRad(180), THREE.MathUtils.degToRad(180), true).then(() => {
this.controls.fitToSphere(this.viewer.scene, true);
})
resolve(this.viewer.camera);
} catch (e) {
reject(e)
}
})
}
/**
* 顶视图
* @description y方向
*/
top() {
return new Promise((resolve, reject) => {
try {
this.setInteract({
minAzimuthAngle: -Infinity,
maxAzimuthAngle: Infinity,
minPolarAngle: 0,
maxPolarAngle: 0,
})
this.controls.rotatePolarTo(0,true).then(() => {
this.controls.fitToSphere(this.viewer.scene, true);
});
resolve(this.viewer.camera);
} catch (e) {
reject(e)
}
})
}
/**
* 底视图
* @description -y方向
*/
bottom() {
return new Promise((resolve, reject) => {
try {
this.setInteract({
minAzimuthAngle: -Infinity,
maxAzimuthAngle: Infinity,
minPolarAngle: Math.PI,
maxPolarAngle: Math.PI,
});
this.controls.rotatePolarTo(Math.PI,true).then(() => {
this.controls.fitToSphere(this.viewer.scene, true);
});
resolve(this.viewer.camera);
} catch (e) {
reject(e)
}
})
}
}
@@ -0,0 +1,141 @@
import * as THREE from "three";
import {DragControls} from '@/core/controls/DragControls.js';
import {useDispatchSignal} from "@/hooks";
import Viewer from "../Viewer";
import App from "@/core/app/App";
import {Measure} from "@/core/tools";
// dragControls 绑定函数
let dragStartFn,dragFn, dragEndFn,clickblankFn;
class Drag {
protected _dragObjects: THREE.Object3D[] = []; // 拖拽对象
public dragControls: any;
private onDownPosition: { x: number; y: number } = {x:-1,y:-1};
public viewer:Viewer;
public measureInstance:Measure | null = null;
public isDragging = false;
constructor(viewer:Viewer) {
this.viewer = viewer;
// 物体拖拽控制器
this.dragControls = new DragControls(this._dragObjects, viewer.camera, viewer.renderer.domElement);
this.dragControls.deactivate(); // 默认禁用
dragStartFn = this.dragControlsStart.bind(this);
this.dragControls.addEventListener("dragstart", dragStartFn);
dragFn = this.drag.bind(this);
this.dragControls.addEventListener("drag", dragFn);
dragEndFn = this.dragControlsEnd.bind(this);
this.dragControls.addEventListener("dragend", dragEndFn);
// 点击可拖拽物体之外
clickblankFn = this.clickblank.bind(this);
this.dragControls.addEventListener("clickblank", clickblankFn);
}
set domElement(element: HTMLElement) {
this.dragControls.setDomElement(element);
}
setMeasureInstance(measureInstance: Measure) {
this.measureInstance = measureInstance;
}
setDragObjects(objects: THREE.Object3D[], type: "eq" | "push" | "remove" = "eq") {
// 当前拖拽对象为空时加入对象需激活控制器
if (this._dragObjects.length === 0) {
if (objects.length > 0) {
this.dragControls.activate();
}
this._dragObjects = objects;
} else {
// 当前拖拽对象不为空时
if (type === "eq") {
// 是清空拖拽对象的设置,则禁用控制器
if (objects.length === 0) {
this.dragControls.deactivate();
}
this._dragObjects = objects;
} else if (type === "push") {
this._dragObjects.push(...objects);
} else if (type === "remove") {
this._dragObjects = this._dragObjects.filter((item) => !objects.includes(item));
}
}
this.dragControls.setObjects(this._dragObjects)
}
// 拖拽开始
dragControlsStart(e) {
// 右键拖拽不响应
if(e.e.button === 2 || !e.object.userData.type || !e.object.visible || !App.sceneHelpers.visible) return;
e.e.preventDefault();
// 拖拽时禁用其他控制器
this.viewer.modules.controls.enabled = false;
this.viewer.modules.transformControls && (this.viewer.modules.transformControls.enabled = false);
this.isDragging = true;
// 记录拖拽按下的位置和对象
this.onDownPosition = {x: e.e.clientX, y: e.e.clientY};
switch (e.object.userData.type) {
case "measure-marker":
this.measureInstance?.redraw(e.object);
break;
}
}
// 拖拽中
drag(e) {
useDispatchSignal("objectChanged", e.object)
}
// 拖拽结束
dragControlsEnd(e) {
// 右键拖拽不响应
if(e.e.button === 2 || !e.object.visible || !App.sceneHelpers.visible) return;
// 拖拽结束启用其他控制器
this.viewer.modules.controls.enabled = true;
this.viewer.modules.transformControls && (this.viewer.modules.transformControls.enabled = true);
this.isDragging = false;
if (!e.object.userData.type) return;
// 判断位置是否有变化,没有变化则为点击
if(this.onDownPosition.x === e.e.clientX && this.onDownPosition.y === e.e.clientY) {
if(e.object.userData.onClick){
e.object.userData.onClick(e);
}
}
switch (e.object.userData.type) {
case "measure-marker":
this.measureInstance?.redrawComplete();
break;
}
}
// 点击可拖拽物体之外
clickblank(e){
if (e.e.button === 2) return;
}
dispose() {
this._dragObjects = [];
this.dragControls.removeEventListener("dragstart", dragStartFn);
this.dragControls.removeEventListener("dragend", dragEndFn);
this.dragControls.dispose();
}
}
export {Drag};
@@ -0,0 +1,454 @@
/**
* @author ErSan
* @email mlt131220@163.com
* @date 2024/10/26 18:27
* @description 后处理
*/
import * as THREE from "three";
import {EffectComposer} from "three/examples/jsm/postprocessing/EffectComposer.js";
import {Pass} from "three/examples/jsm/postprocessing/Pass.js";
import {OutlinePass} from "three/examples/jsm/postprocessing/OutlinePass.js";
import {ShaderPass} from "three/examples/jsm/postprocessing/ShaderPass.js";
import {FXAAShader} from 'three/examples/jsm/shaders/FXAAShader.js';
import {RenderPass} from 'three/examples/jsm/postprocessing/RenderPass.js';
import {GammaCorrectionShader} from "three/examples/jsm/shaders/GammaCorrectionShader.js";
import {UnrealBloomPass} from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
import {BokehPass} from 'three/examples/jsm/postprocessing/BokehPass.js';
import {RenderPixelatedPass} from 'three/examples/jsm/postprocessing/RenderPixelatedPass.js';
import {HalftonePass} from 'three/examples/jsm/postprocessing/HalftonePass.js';
import {LUTPass} from 'three/examples/jsm/postprocessing/LUTPass.js';
import {LUTCubeLoader} from 'three/examples/jsm/loaders/LUTCubeLoader.js';
import {LUT3dlLoader} from 'three/examples/jsm/loaders/LUT3dlLoader.js';
import {LUTImageLoader} from 'three/examples/jsm/loaders/LUTImageLoader.js';
import { AfterimagePass } from 'three/examples/jsm/postprocessing/AfterimagePass.js';
// import {OutputPass} from "three/examples/jsm/postprocessing/OutputPass.js";
import {useAddSignal} from "@/hooks";
import Viewer from "../Viewer";
import App from "@/core/app/App";
type supportPass = Pass | OutlinePass | ShaderPass | UnrealBloomPass | BokehPass | RenderPixelatedPass | HalftonePass;
let _passConfigChangeFn: any = null;
export const LUTEffectMap = {
'Bourbon 64.CUBE': null,
'Chemical 168.CUBE': null,
'Clayton 33.CUBE': null,
'Cubicle 99.CUBE': null,
'Remy 24.CUBE': null,
'Presetpro-Cinematic.3dl': null,
'NeutralLUT.png': null,
'B&WLUT.png': null,
'NightLUT.png': null
};
export class Effect {
private viewer: Viewer;
composer: EffectComposer | undefined;
outlinePass: OutlinePass | undefined;
// 其他可动态配置的通道
static PassMap = new Map<string, supportPass>();
constructor(viewer: Viewer) {
this.viewer = viewer;
_passConfigChangeFn = this.handlePassConfigChange.bind(this);
useAddSignal("effectPassConfigChange", _passConfigChangeFn)
}
get enabled() {
return App.project.getKey("effect.enabled");
}
createComposer() {
if (this.composer) {
this.clear();
}
const {composer, outlinePass} = this.initComposer();
this.composer = composer as EffectComposer;
this.outlinePass = outlinePass;
// 加入默认打开的通道
const effectConfig = App.project.getKey("effect");
if (effectConfig) {
Object.keys(effectConfig).forEach(key => {
if (key === "enabled" || key === "Outline") return;
// 判断是否存在 enabled 属性
if (effectConfig[key].hasOwnProperty('enabled') && effectConfig[key].enabled === true) {
if (!this[key] || !(this[key] instanceof Pass)) return;
this.composer?.addPass(this[key]);
}
})
}
}
protected initComposer() {
if (!this.viewer.renderer) return {};
const pixelRatio = this.viewer.renderer.getPixelRatio();
// 创建后处理对象EffectComposerWebGL渲染器作为参数
let composer = new EffectComposer(this.viewer.renderer);
composer.setPixelRatio(pixelRatio);
composer.setSize(this.viewer.container.offsetWidth, this.viewer.container.offsetHeight);
// let ssaaRenderPass = new SSAARenderPass(this.viewer.scene, this.viewer.camera);
// ssaaRenderPass.unbiased = false;
// ssaaRenderPass.sampleLevel = 2;
// ssaaRenderPass.clearColor = new THREE.Color("#272727");
// ssaaRenderPass.clearAlpha = 0;
// composer.addPass(ssaaRenderPass);
const renderPass = new RenderPass(this.viewer.scene, this.viewer.camera);
renderPass.clearColor = new THREE.Color("#272727");
renderPass.clearAlpha = 0;
renderPass.clearDepth = true;
composer.addPass(renderPass);
const outlineConfig = App.project.getKey("effect.Outline");
const outlinePass = new OutlinePass(new THREE.Vector2(this.viewer.container.offsetWidth, this.viewer.container.offsetHeight), this.viewer.scene, this.viewer.camera)
outlinePass.visibleEdgeColor = new THREE.Color(outlineConfig.visibleEdgeColor || "#ffee00");
outlinePass.hiddenEdgeColor = new THREE.Color(outlineConfig.hiddenEdgeColor || "#ff6a00");
outlinePass.edgeStrength = outlineConfig.edgeStrength || Number(3.0);
outlinePass.edgeGlow = outlineConfig.edgeGlow || Number(0.2);
outlinePass.edgeThickness = outlineConfig.edgeThickness || Number(1.0);
outlinePass.pulsePeriod = outlineConfig.pulsePeriod || Number(0.0);
outlinePass.usePatternTexture = outlineConfig.usePatternTexture || false;
outlinePass.selectedObjects = [];
composer.addPass(outlinePass);
// 创建伽马校正通道. 解决gltf模型后处理时,颜色偏差的问题
const gammaPass = new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaPass);
// const outputPass = new OutputPass();
// composer.addPass(outputPass);
return {composer, outlinePass};
}
/**
* FXAA 抗锯齿通道
*/
get FXAA(): ShaderPass | null {
if (Effect.PassMap.has("FXAA")) {
return Effect.PassMap.get("FXAA") as ShaderPass;
}
if (!this.viewer.renderer) return null;
const options = App.project.getKey("effect.FXAA");
const fxaaPass = new ShaderPass(FXAAShader);
fxaaPass.clear = true;
const pixelRatio = this.viewer.renderer.getPixelRatio();
fxaaPass.material.uniforms['resolution'].value.x = 1 / (this.viewer.container.offsetWidth * pixelRatio);
fxaaPass.material.uniforms['resolution'].value.y = 1 / (this.viewer.container.offsetHeight * pixelRatio);
fxaaPass.enabled = options.enabled || false;
Effect.PassMap.set("FXAA", fxaaPass);
return fxaaPass;
}
/**
* UnrealBloom 仿UE辉光
*/
get UnrealBloom(): UnrealBloomPass {
if (Effect.PassMap.has("UnrealBloom")) {
return Effect.PassMap.get("UnrealBloom") as UnrealBloomPass;
}
const options = App.project.getKey("effect.UnrealBloom");
const bloomPass = new UnrealBloomPass(new THREE.Vector2(this.viewer.container.offsetWidth, this.viewer.container.offsetWidth), 1, 0, 0);
bloomPass.threshold = options.threshold || 0;
bloomPass.strength = options.strength === undefined ? 1 : options.strength;
bloomPass.radius = options.radius || 0;
Effect.PassMap.set("UnrealBloom", bloomPass);
return bloomPass;
}
/**
* Bokeh 变焦,背景虚化(焦外成像)
*/
get Bokeh(): BokehPass {
if (Effect.PassMap.has("Bokeh")) {
return Effect.PassMap.get("Bokeh") as BokehPass;
}
const options = App.project.getKey("effect.Bokeh");
const bokehPass = new BokehPass(this.viewer.scene, App.viewportCamera, {
focus: options.focus,
aperture: options.aperture,
maxblur: options.maxblur
});
Effect.PassMap.set("Bokeh", bokehPass);
return bokehPass;
}
/**
* Pixelate 像素风
* @description 渲染时会受场景背景影响,纯色背景才有最佳渲染效果
*/
get Pixelate(): RenderPixelatedPass {
if (Effect.PassMap.has("Pixelate")) {
return Effect.PassMap.get("Pixelate") as RenderPixelatedPass;
}
const options = App.project.getKey("effect.Pixelate");
const pixelatedPass = new RenderPixelatedPass(options.pixelSize || 6, this.viewer.scene, App.viewportCamera, {
normalEdgeStrength: options.normalEdgeStrength,
depthEdgeStrength: options.depthEdgeStrength
});
Effect.PassMap.set("Pixelate", pixelatedPass);
return pixelatedPass;
}
/**
* Halftone 半色调
*/
get Halftone(): HalftonePass {
if (Effect.PassMap.has("Halftone")) {
return Effect.PassMap.get("Halftone") as HalftonePass;
}
const options = App.project.getKey("effect.Halftone");
const halftonePass = new HalftonePass(options);
Effect.PassMap.set("Halftone", halftonePass);
return halftonePass;
}
/**
* LUT 颜色滤镜
*/
get LUT(): LUTPass {
if (Effect.PassMap.has("LUT")) {
return Effect.PassMap.get("LUT") as LUTPass;
}
const options = App.project.getKey("effect.LUT");
const _LUTCubeLoader = new LUTCubeLoader();
const _LUTImageLoader = new LUTImageLoader();
const _LUT3dlLoader = new LUT3dlLoader();
// 优先加载配置项的lut
if (!LUTEffectMap[options.lut]) {
const lutUrl = new URL(`${import.meta.env.BASE_URL}resource/luts/${options.lut}`, import.meta.url).href;
if (/\.CUBE$/i.test(options.lut)) {
_LUTCubeLoader.load(lutUrl, (result) => {
LUTEffectMap[options.lut] = result;
this.viewer.render();
});
} else if (/\.png$/i.test(options.lut)) {
_LUTImageLoader.load(lutUrl, (result) => {
LUTEffectMap[options.lut] = result;
this.viewer.render();
});
} else {
_LUT3dlLoader.load(lutUrl, (result) => {
LUTEffectMap[options.lut] = result;
this.viewer.render();
});
}
}
Object.keys(LUTEffectMap).forEach(name => {
if (LUTEffectMap[name]) return;
const lutUrl = new URL(`${import.meta.env.BASE_URL}resource/luts/${name}`, import.meta.url).href;
if (/\.CUBE$/i.test(name)) {
_LUTCubeLoader.load(lutUrl, function (result) {
LUTEffectMap[name] = result;
});
} else if (/\.png$/i.test(name)) {
_LUTImageLoader.load(lutUrl, function (result) {
LUTEffectMap[name] = result;
});
} else {
_LUT3dlLoader.load(lutUrl, function (result) {
LUTEffectMap[name] = result;
});
}
});
const lutPass = new LUTPass({
intensity: options.intensity
});
Effect.PassMap.set("LUT", lutPass);
return lutPass;
}
/**
* 运动残影
*/
get Afterimage():AfterimagePass{
if (Effect.PassMap.has("Afterimage")) {
return Effect.PassMap.get("Afterimage") as AfterimagePass;
}
const options = App.project.getKey("effect.Afterimage");
const afterimagePass = new AfterimagePass(options.damp);
Effect.PassMap.set("Afterimage", afterimagePass);
return afterimagePass;
}
/**
* 通道配置变更
* @param name 通道名
* @param config 新配置
*/
handlePassConfigChange(name: string, config) {
// App.project.setKey(`effect.${name}`, config);
if (name === "Outline") {
if (this.outlinePass) {
for (const key in config) {
(<OutlinePass>this.outlinePass)[key] = this.getPassConfigValue(key, config[key]);
}
}
} else {
if (!config.enabled) {
if (Effect.PassMap.has(name)) {
this.composer?.removePass(Effect.PassMap.get(name) as Pass)
Effect.PassMap.delete(name);
this.viewer.render();
}
return;
}
if (!Effect.PassMap.has(name)) {
if (!this[name] || !(this[name] instanceof Pass)) return;
// get this[name]时创建的Pass会自动加入到Effect.PassMap
this.composer?.addPass(this[name]);
this.viewer.render();
return;
}
if (!this[name]) return;
switch (name) {
// 参数配置在uniforms的Pass
case "Bokeh":
case 'Halftone':
case 'Afterimage':
for (const key in config) {
if (this[name].uniforms[key] === undefined) {
this[name][key] = this.getPassConfigValue(key, config[key]);
continue;
}
this[name].uniforms[key].value = this.getPassConfigValue(key, config[key]);
}
break;
case "Pixelate":
for (const key in config) {
if (key === "pixelSize") {
this.Pixelate.setPixelSize(config[key]);
} else {
this.Pixelate[key] = this.getPassConfigValue(key, config[key]);
}
}
break;
case "LUT":
for (const key in config) {
if (key === "lut") {
const lut = LUTEffectMap[config[key]];
Boolean(lut) && (this.LUT.lut = lut.texture3D);
} else {
this.LUT[key] = this.getPassConfigValue(key, config[key]);
}
}
break;
default:
for (const key in config) {
this[name][key] = this.getPassConfigValue(key, config[key]);
}
break;
}
}
this.viewer.render();
}
/**
* 处理通道值
*/
getPassConfigValue(key: string, value: any) {
if (["visibleEdgeColor", "hiddenEdgeColor"].includes(key)) {
return new THREE.Color(value);
}
if (["rotateR", "rotateG", "rotateB"].includes(key)) {
return value * (Math.PI / 180);
}
return value;
}
render(deltaTime: number) {
if (!this.enabled || !this.composer) return;
// PassMap中存在 LUT 则说明 LUT 是启用的
// lutMap是异步加载的,所以此处做判断
if (Effect.PassMap.has("LUT")) {
const lut = LUTEffectMap[App.project.getKey("effect.LUT.lut")];
this.LUT.enabled = Boolean(lut);
if (this.LUT.enabled) {
this.LUT.lut = lut.texture3D;
}
}
this.composer.render(deltaTime);
}
clear() {
if (this.composer) {
for (let i = this.composer.passes.length; i > 0; i--) {
this.composer.passes[i - 1].dispose && this.composer.passes[i - 1].dispose();
this.composer.removePass(this.composer.passes[i - 1]);
}
this.composer.dispose();
}
}
dispose() {
this.clear();
_passConfigChangeFn = null;
}
}
@@ -0,0 +1,171 @@
import { ViewportGizmo, GizmoOptions } from "three-viewport-gizmo";
import CameraControls from "camera-controls";
import { useAddSignal, useRemoveSignal } from "@/hooks";
import App from "@/core/app/App";
import Viewer from "../Viewer";
import {getOsTheme} from "@/utils";
let _updateFn;
export class Helper {
private viewer: Viewer;
private gizmo: ViewportGizmo | undefined;
private controls: CameraControls;
constructor(viewer: Viewer, controls: CameraControls) {
this.viewer = viewer;
this.controls = controls;
}
/**
* 如果当前正在动画视图更改,则返回true
*/
get animating() {
if (this.gizmo) {
return this.gizmo.animating;
}
return false;
}
set hidden(value: boolean) {
if (this.gizmo) {
const dom = document.querySelector(`#${this.gizmo.options.id}`) as HTMLElement;
if (!dom) return;
dom.style.display = value ? "block" : "none";
this.update();
}
}
init() {
_updateFn = this.update.bind(this);
useAddSignal('cameraChanged', _updateFn);
useAddSignal('sceneResize', _updateFn);
if (!this.viewer.renderer) return;
this.gizmo = new ViewportGizmo(this.viewer.camera, this.viewer.renderer, this.getGizmoConfig());
this.controls.getTarget(this.gizmo.target);
this.initEvent();
}
/**
* 初始化视角控制器事件
*/
initEvent() {
if (!this.gizmo) return;
this.gizmo.addEventListener("start", () => {
this.controls.enabled = false;
this.viewer.render();
});
this.gizmo.addEventListener("end", () => {
this.controls.enabled = true;
this.viewer.render();
});
this.gizmo.addEventListener("change", () => {
this.controls.setPosition(...this.viewer.camera.position.toArray());
this.viewer.render();
});
this.initDomEvent();
}
/**
* 初始化视角控制器dom事件
*/
initDomEvent() {
if (!this.gizmo?.options?.id) return;
const dom = document.querySelector(`#${this.gizmo.options.id}`);
if (!dom) return;
dom.addEventListener("pointermove", () => {
this.viewer.render();
})
}
/**
* 获取ViewportGizmo配置
* @param type
*/
getGizmoConfig(type = "cube") {
const _opt = {
type,
id: "astral-viewer-helper",
container: this.viewer.container,
placement: "bottom-right",
} as GizmoOptions;
if (type === "sphere") return _opt;
const configTheme = App.config.getKey('theme');
if (configTheme === "os") {
if (getOsTheme() !== "dark") {
return _opt;
}
} else if (configTheme === "light") {
return _opt;
}
let colors = {
color: 0x333333,
labelColor: 0xdddddd,
hover: {
color: App.config.getKey('mainColor') || "#7FE7C4",
labelColor: 0xffffff,
},
};
let background = {
color: 0x444444,
hover: { color: 0x444444 },
};
return {
..._opt,
background: background,
corners: colors,
edges: colors,
right: colors,
top: colors,
front: colors,
} as GizmoOptions;
}
/**
* 设置外观,会在全局配置中的主色调及白天/黑夜模式切换时调用
*/
setConfig() {
if (!this.gizmo) return;
this.gizmo.set(this.getGizmoConfig());
this.initDomEvent();
this.viewer.render();
}
update() {
if (!this.gizmo) return;
this.controls.getTarget(this.gizmo.target);
this.gizmo.update(false);
}
render() {
if (!this.gizmo) return;
this.gizmo.render();
}
dispose() {
if (this.gizmo) this.gizmo.dispose();
if(_updateFn){
useRemoveSignal('cameraChanged', _updateFn);
useRemoveSignal('sceneResize', _updateFn);
_updateFn = null;
}
}
}
@@ -0,0 +1,294 @@
import * as THREE from 'three';
import System, * as Particle from '@/core/libs/three-nebula';
import { useAddSignal, useDispatchSignal, useRemoveSignal } from "@/hooks";
import * as BasicObject3D from "@/core/built-in/assets/BasicObject3D";
import ParticleEmitter from "@/core/objects/ParticleEmitter";
import { isParticleObject } from "@/utils";
import Viewer from "../Viewer";
import App from "@/core/app/App";
import Preview from "@/core/preview/Preview.ts";
let _objectRemovedFn, _objectAddedFn, _bodyChangedFn,_addEmitterFn,_emitterAdd2Fn;
class ParticleSystem {
private viewer: Viewer | Preview;
// 存储所有粒子的组
particlesGroup: THREE.Group;
spriteRenderer: Particle.SpriteRenderer;
meshRenderer: Particle.MeshRenderer;
spriteSystem: System;
meshSystem: System;
/**
* 场景中粒子body类型为Point的对象
*/
static PointBody: THREE.Points = (function () {
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([0, 0, 0]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
const material = new THREE.PointsMaterial({ size: 0.1, color: 0xff0000 });
return new THREE.Points(geometry, material);
})();
/**
* 场景中所有粒子用到的Sprite | Mesh,统一管理
* key: 粒子ParticleEmitter的uuidvalue: 粒子的body对象
*/
static Body3DMap: Map<string, THREE.Sprite | THREE.Mesh> = new Map();
constructor(viewer:Viewer | Preview) {
this.viewer = viewer;
this.particlesGroup = new THREE.Group();
this.particlesGroup.name = 'astral-particles-group';
// 不在场景树中显示此组
this.particlesGroup.ignore = true;
this.viewer.scene.add(this.particlesGroup);
this.spriteSystem = new System();
this.spriteSystem.name = 'spriteSystem';
this.meshSystem = new System();
this.meshSystem.name = 'meshSystem';
this.spriteRenderer = new Particle.SpriteRenderer(this.particlesGroup, THREE);
this.meshRenderer = new Particle.MeshRenderer(this.particlesGroup, THREE);
this.spriteSystem.addRenderer(this.spriteRenderer);
this.meshSystem.addRenderer(this.meshRenderer);
this.initEvent();
}
get needsUpdate() {
return this.spriteSystem.emitters.length > 0 || this.meshSystem.emitters.length > 0;
}
initEvent(){
_objectRemovedFn = this.handleObjectRemoved.bind(this);
useAddSignal("objectRemoved", _objectRemovedFn);
_objectAddedFn = this.handleObjectAdded.bind(this);
useAddSignal("objectAdded", _objectAddedFn);
_bodyChangedFn = this.handleParticleBodyChanged.bind(this);
useAddSignal("particleBodyChanged", _bodyChangedFn);
_emitterAdd2Fn = this.handleEmitterAdd.bind(this);
useAddSignal("emitterAdd2ParticleSystem", _emitterAdd2Fn);
_addEmitterFn = this.handleAddEmitter.bind(this);
this.spriteSystem.eventDispatcher.addEventListener('EMITTER_ADDED', _addEmitterFn)
this.meshSystem.eventDispatcher.addEventListener('EMITTER_ADDED', _addEmitterFn)
}
handleAddEmitter(emitter: Particle.Emitter){
useDispatchSignal('particleSystemAddEmitter', emitter);
}
/**
* 监听对象移除
*/
handleObjectRemoved(object) {
if (!object || object.type !== "Particle") return;
if (!object.emitter) return;
const emitterJSON = object.toJSON().object.emitter;
if (this.spriteSystem.emitters.includes(object.emitter)) {
this.spriteSystem.removeEmitter(object.emitter);
} else {
this.meshSystem.removeEmitter(object.emitter);
}
object.dispose();
object.emitter = emitterJSON;
}
/**
* 监听对象添加
* @description 主要应用于对象添加的redo命令
*/
handleObjectAdded(object) {
if (!isParticleObject(object)) return;
if (object.emitter.parent) return;
object.emitter = ParticleEmitter.fromJSON({
uuid: object.uuid,
type: object.type,
name: object.name,
emitter: object.emitter,
children: []
}).emitter;
// 重新初始化事件,不然粒子无法选中
object.initEvent();
}
/**
* 监听粒子body对象改变
*/
handleParticleBodyChanged(data: IParticle.Config['init']['body'], object3D: THREE.Texture | string | THREE.Mesh, isObjectChange = false, done = (_data: IParticle.Config['init']['body']) => {}) {
if (!object3D || !data?.body) return;
const selected = App.selected;
if (!selected || !(selected instanceof ParticleEmitter)) return;
const setBody3D = (body3D: THREE.Sprite | THREE.Mesh) => {
// if (ParticleSystem.Body3DMap.has(selected.uuid)) {
// const oldBody3D = ParticleSystem.Body3DMap.get(selected.uuid);
// if (oldBody3D && oldBody3D !== body3D) {
// if (oldBody3D.material) {
// if (Array.isArray(oldBody3D.material)) {
// oldBody3D.material.forEach(m => m.dispose());
// } else {
// oldBody3D.material.dispose();
// }
// }
// oldBody3D.removeFromParent();
// }
// ParticleSystem.Body3DMap.delete(selected.uuid);
// }
ParticleSystem.Body3DMap.set(selected.uuid, body3D);
}
const emitter = selected.emitter as Particle.Emitter;
const initializer = emitter.initializers.find((item) => item.type === "Body");
// 如果body内的对象未变更直接修改基础属性并返回
if (initializer && !isObjectChange){
initializer.isEnabled = data.isEnabled;
done(data);
return;
}
if (this.spriteSystem.emitters.includes(emitter)) {
this.spriteSystem.removeEmitter(emitter);
} else {
this.meshSystem.removeEmitter(emitter);
}
// 不重新实例化,直接更新初始化器的body属性
if (!initializer) {
switch (data.body.type) {
case "Sprite":
console.log(111);
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: object3D as THREE.Texture,
color: 0xffffff,
//blending: THREE.AdditiveBlending,
//fog: true,
}));
emitter.addInitializer(new Particle.Body(sprite, null, null, data.isEnabled));
setBody3D(sprite);
this.spriteSystem.addEmitter(emitter);
done(Object.assign(data, { uuid: sprite.uuid }));
break;
case "Mesh":
let mesh: THREE.Mesh;
if (typeof object3D === "string") {
// 首字母需要大写
const _objectMethod = object3D.charAt(0).toUpperCase() + object3D.slice(1);
if (BasicObject3D[_objectMethod]) {
mesh = BasicObject3D[_objectMethod]();
} else {
mesh = BasicObject3D.Box();
}
} else {
mesh = object3D as THREE.Mesh;
}
emitter.addInitializer(new Particle.Body(mesh, null, null, data.isEnabled));
setBody3D(mesh);
this.meshSystem.addEmitter(emitter);
done(Object.assign(data, { uuid: null }));
break;
case "Point":
emitter.addInitializer(new Particle.Body(ParticleSystem.PointBody.clone(), null, null, data.isEnabled));
this.meshSystem.addEmitter(emitter);
done(Object.assign(data, { uuid: null }));
break;
}
} else {
switch (data.body.type) {
case "Sprite":
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: object3D as THREE.Texture,
color: 0xffffff,
//blending: THREE.AdditiveBlending,
//fog: true,
}));
initializer.body.items = [sprite];
setBody3D(sprite);
this.spriteSystem.addEmitter(emitter);
done(Object.assign(data, { uuid: sprite.uuid }));
break;
case "Mesh":
let mesh: THREE.Mesh;
if (typeof object3D === "string") {
// 首字母需要大写
const _objectMethod = object3D.charAt(0).toUpperCase() + object3D.slice(1);
if (BasicObject3D[_objectMethod]) {
mesh = BasicObject3D[_objectMethod]();
} else {
mesh = BasicObject3D.Box();
}
} else {
mesh = object3D as THREE.Mesh;
}
initializer.body.items = [mesh];
setBody3D(mesh);
this.meshSystem.addEmitter(emitter);
done(Object.assign(data, { uuid: null }));
break;
case "Point":
initializer.body.items = [ParticleSystem.PointBody.clone()];
this.meshSystem.addEmitter(emitter);
done(Object.assign(data, { uuid: null }));
break;
}
}
}
/**
* 监听添加Emitter
*/
handleEmitterAdd(emitter:Particle.Emitter,system = "spriteSystem"){
this[system].addEmitter(emitter);
}
update(delta: number) {
if (this.spriteSystem.emitters.length > 0) {
this.spriteSystem.update(delta);
}
if (this.meshSystem.emitters.length > 0) {
this.meshSystem.update(delta);
}
}
dispose() {
useRemoveSignal("objectRemoved", _objectRemovedFn);
_objectRemovedFn = null;
useRemoveSignal("particleBodyChanged", _bodyChangedFn);
_bodyChangedFn = null;
this.spriteSystem.eventDispatcher.removeEventListener('EMITTER_ADDED', _addEmitterFn);
this.meshSystem.eventDispatcher.removeEventListener('EMITTER_ADDED', _addEmitterFn);
_addEmitterFn = null;
this.spriteSystem.destroy();
this.meshSystem.destroy();
}
}
export {ParticleSystem};
@@ -0,0 +1,497 @@
import * as THREE from "three";
import {RoomEnvironment} from "three/examples/jsm/environments/RoomEnvironment.js";
import {ShaderPass} from "three/examples/jsm/postprocessing/ShaderPass.js";
import {Effect} from "./Effect";
import {useAddSignal} from "@/hooks";
import Viewer from "../Viewer";
import App from "@/core/app/App";
import {focusObject} from "@/utils/scene/controls.ts";
export class Signals {
private readonly viewer: Viewer;
private useBackgroundAsEnvironment = false;
constructor(viewer:Viewer) {
this.viewer = viewer;
this.init();
}
init() {
useAddSignal("sceneCleared", this.sceneCleared.bind(this));
useAddSignal("transformModeChanged", this.transformModeChanged.bind(this));
useAddSignal("snapChanged", this.snapChanged.bind(this));
useAddSignal("spaceChanged", this.spaceChanged.bind(this));
useAddSignal("effectEnabledChange", this.effectEnabledChange.bind(this));
useAddSignal("rendererUpdated", this.rendererUpdated.bind(this));
useAddSignal("rendererCreated", this.rendererCreated.bind(this));
useAddSignal("rendererConfigUpdate", this.rendererConfigUpdate.bind(this));
useAddSignal("rendererDetectKTX2Support", this.rendererDetectKTX2Support.bind(this));
useAddSignal("sceneBackgroundChanged", this.sceneBackgroundChanged.bind(this));
useAddSignal("sceneEnvironmentChanged", this.sceneEnvironmentChanged.bind(this));
useAddSignal("sceneGraphChanged", this.sceneGraphChanged.bind(this));
useAddSignal("cameraChanged", this.cameraChanged.bind(this));
useAddSignal("cameraReset", this.viewer.updateAspectRatio.bind(this.viewer));
useAddSignal("viewportCameraChanged", this.viewportCameraChanged.bind(this));
useAddSignal("viewportShadingChanged", this.viewportShadingChanged.bind(this));
useAddSignal("objectSelected", this.objectSelected.bind(this));
useAddSignal("objectFocused", this.objectFocused.bind(this));
useAddSignal("objectAdded", this.objectAdded.bind(this));
useAddSignal("objectChanged", this.objectChanged.bind(this));
useAddSignal("objectRemoved", this.objectRemoved.bind(this));
useAddSignal("geometryChanged", this.geometryChanged.bind(this));
useAddSignal("materialChanged", this.materialChanged.bind(this));
useAddSignal("sceneResize", this.sceneResize.bind(this));
useAddSignal("showGridChanged", this.showGridChanged.bind(this));
useAddSignal("showHelpersChanged", this.showHelpersChanged.bind(this));
useAddSignal("scriptAdded",this.scriptAdded.bind(this));
useAddSignal("scriptRemoved",this.scriptRemoved.bind(this));
useAddSignal("scriptChanged",this.scriptChanged.bind(this));
}
/**
* 判断对象是否是可射线选中的
*/
objectIsCanPick(object:THREE.Object3D | null){
return object !== null && object !== this.viewer.scene && object !== this.viewer.camera;
}
/**
* 清空
*/
sceneCleared() {
this.viewer.modules.controls.setTarget(0, 0, 0,true);
this.viewer.pathtracer?.reset();
this.viewer.css2DRenderer.domElement.innerHTML = "";
this.viewer.css3DRenderer.domElement.innerHTML = "";
const rendererConfig = App.project.getKey("renderer");
App.FPS = rendererConfig.fps;
this.viewer.renderer.shadowMap.enabled = rendererConfig.shadow.enabled;
this.viewer.renderer.shadowMap.type = rendererConfig.shadow.type;
this.viewer.renderer.toneMapping = rendererConfig.toneMapping;
this.viewer.renderer.toneMappingExposure = rendererConfig.toneMappingExposure
if(this.viewer.options.hdr){
this.viewer.loadEnv(true);
}
this.viewer.initPT();
this.viewer.render();
}
/**
* 模型变换控制器模式改变
* @param mode
*/
transformModeChanged(mode) {
this.viewer.modules.transformControls?.setMode(mode);
}
/**
* 模型变换控制器吸附距离改变
* @param dist
*/
snapChanged(dist:number) {
this.viewer.modules.transformControls?.setTranslationSnap(dist);
}
/**
* 模型变换控制器坐标系改变
* @param space
*/
spaceChanged(space:"world" | "local") {
this.viewer.modules.transformControls?.setSpace(space);
}
/**
* 启用/禁用后处理
*/
effectEnabledChange(enabled:boolean){
if(enabled){
this.viewer.selectionBox.visible = false;
if (this.objectIsCanPick(App.selected) && this.viewer.modules.effect.outlinePass) {
this.viewer.modules.effect.outlinePass.selectedObjects = [App.selected as THREE.Object3D];
}
}else{
if(this.viewer.modules.effect.outlinePass){
this.viewer.modules.effect.outlinePass.selectedObjects = [];
}
if (this.objectIsCanPick(App.selected)) {
this.viewer.box.setFromObject(App.selected as THREE.Object3D, true);
if (!this.viewer.box.isEmpty()) {
this.viewer.selectionBox.visible = true;
}
}
}
this.render();
}
/**
* 渲染器更新
*/
rendererUpdated() {
this.viewer.scene.traverse(function (child) {
if (child.material !== undefined) {
(<THREE.Material>child.material).needsUpdate = true;
}
});
this.viewer.render();
}
/**
* 渲染器创建完成后调用
* @param newRenderer
*/
rendererCreated(newRenderer: THREE.WebGLRenderer) {
this.viewer.engineCreated(newRenderer);
}
rendererConfigUpdate(){
this.viewer.createEngine();
}
rendererDetectKTX2Support( ktx2Loader ) {
ktx2Loader.detectSupport(this.viewer.renderer);
}
/**
* 场景背景变更
* @param backgroundType
* @param backgroundColor
* @param backgroundTexture
* @param backgroundEquirectangularTexture
* @param backgroundBlurriness
* @param backgroundIntensity
* @param backgroundRotation
*/
sceneBackgroundChanged(backgroundType:"" | "Color" | "Texture" | "Equirectangular", backgroundColor:string, backgroundTexture, backgroundEquirectangularTexture, backgroundBlurriness:number,backgroundIntensity:number, backgroundRotation:number){
this.viewer.scene.background = null;
switch (backgroundType) {
case 'Color':
this.viewer.scene.background = new THREE.Color(backgroundColor);
break;
case 'Texture':
if (backgroundTexture) {
this.viewer.scene.background = backgroundTexture;
}
break;
case 'Equirectangular':
if (backgroundEquirectangularTexture) {
backgroundEquirectangularTexture.mapping = THREE.EquirectangularReflectionMapping;
this.viewer.scene.background = backgroundEquirectangularTexture;
this.viewer.scene.backgroundBlurriness = backgroundBlurriness;
this.viewer.scene.backgroundIntensity = backgroundIntensity;
this.viewer.scene.backgroundRotation.y = backgroundRotation * THREE.MathUtils.DEG2RAD;
if (this.useBackgroundAsEnvironment) {
this.viewer.scene.environment = this.viewer.scene.background as THREE.Texture;
this.viewer.scene.environmentRotation.y = backgroundRotation * THREE.MathUtils.DEG2RAD;
}
}
break;
}
this.viewer.dispatchEvent({type:"onSceneBackgroundChange",backgroundType:backgroundType,background:this.viewer.scene.background})
this.viewer.updatePTBackground();
this.render();
}
/**
* 场景环境贴图变更
* @param environmentType
* @param environmentEquirectangularTexture
*/
sceneEnvironmentChanged(environmentType: '' | 'Background' | 'Equirectangular' | 'ModelViewer', environmentEquirectangularTexture: THREE.Texture){
this.viewer.scene.environment = null;
this.useBackgroundAsEnvironment = false;
switch (environmentType) {
case 'Background':
this.useBackgroundAsEnvironment = true;
this.viewer.scene.environment = this.viewer.scene.background as THREE.Texture;
this.viewer.scene.environment.mapping = THREE.EquirectangularReflectionMapping;
this.viewer.scene.environmentRotation.y = this.viewer.scene.backgroundRotation.y;
break;
case 'Equirectangular':
if (environmentEquirectangularTexture) {
this.viewer.scene.environment = environmentEquirectangularTexture;
this.viewer.scene.environment.mapping = THREE.EquirectangularReflectionMapping;
}
break;
case 'ModelViewer':
if(!this.viewer.pmremGenerator){
// 创建一个PMREMGenerator,从立方体映射环境纹理生成预过滤的 Mipmap 辐射环境贴图
this.viewer.pmremGenerator = new THREE.PMREMGenerator(this.viewer.renderer);
this.viewer.pmremGenerator.compileEquirectangularShader();
}
this.viewer.scene.environment = this.viewer.pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
break;
}
this.viewer.dispatchEvent({type:"onSceneEnvironmentChange",environmentType:environmentType,environment:this.viewer.scene.environment})
this.viewer.updatePTEnvironment();
this.render();
}
/**
* 手动场景渲染
*/
sceneGraphChanged(){
this.viewer.initPT();
this.render();
}
/**
* 切换主相机
*/
cameraChanged(){
this.viewer.pathtracer?.reset();
this.render();
}
/**
* 场景主相机变更
*/
viewportCameraChanged(){
const viewportCamera = App.viewportCamera;
if ( viewportCamera.isPerspectiveCamera || viewportCamera.isOrthographicCamera ) {
this.viewer.updateAspectRatio();
}
if (viewportCamera.isPerspectiveCamera) {
(<THREE.PerspectiveCamera>viewportCamera).aspect = App.camera.aspect;
viewportCamera.projectionMatrix.copy(App.camera.projectionMatrix);
} else if ( viewportCamera.isOrthographicCamera ) {
// TODO
}
// 设置用户Camera时禁用EditorControls
this.viewer.modules.controls.enabled = (viewportCamera === App.camera);
// 替换控制器相机
// this.viewer.modules.controls.camera = viewportCamera;
this.viewer.initPT();
this.render();
}
/**
* 场景Shading变更
* @description 当开启OutlinePass后处理时,设置scene.overrideMaterial无效。
* @link https://github.com/mrdoob/three.js/issues/30577
*/
viewportShadingChanged(){
const viewportShading = App.viewportShading;
switch (viewportShading) {
case 'realistic':
this.viewer.pathtracer?.init(this.viewer.scene, this.viewer.camera);
break;
case 'solid':
this.viewer.scene.overrideMaterial = null;
break;
case 'normals':
this.viewer.scene.overrideMaterial = new THREE.MeshNormalMaterial();
break;
case 'wireframe':
console.log("wireframe")
this.viewer.scene.overrideMaterial = new THREE.MeshBasicMaterial({color: 0x000000, wireframe: true});
break;
}
this.render();
}
/**
* 选中模型
* @param object
*/
objectSelected(object){
this.viewer.selectionBox.visible = false;
this.viewer.modules.transformControls?.detach();
if (this.objectIsCanPick(object)) {
if(this.viewer.modules.effect.enabled && this.viewer.modules.effect.outlinePass){
this.viewer.modules.effect.outlinePass.selectedObjects = [object];
}else{
this.viewer.box.setFromObject(object, true);
if (!this.viewer.box.isEmpty()) {
this.viewer.selectionBox.visible = true;
}
}
this.viewer.modules.transformControls?.attach(object);
}
this.render();
}
/**
* 聚焦模型
* @param object
*/
objectFocused(object){
focusObject(object,this.viewer.modules.controls);
}
/**
* 场景新增模型
*/
objectAdded(){
this.viewer.computedSceneBox3();
}
/**
* 模型属性变更
* @param object
*/
objectChanged(object){
if (App.selected === object) {
this.viewer.box.setFromObject(object, true);
this.viewer.computedSceneBox3();
}
if (object.isPerspectiveCamera) {
object.updateProjectionMatrix();
}
const helper = App.helpers[object.id];
if (helper !== undefined && !helper.isSkeletonHelper) {
helper.update();
}
this.viewer.initPT();
this.render();
}
/**
* 模型被移除
* @param object
*/
objectRemoved(object){
this.viewer.modules.controls.enabled = true;
if (this.viewer.modules.transformControls && object === this.viewer.modules.transformControls.object) {
this.viewer.modules.transformControls.detach();
}
this.viewer.computedSceneBox3();
}
/**
* geometry 变更
* @param object
*/
geometryChanged(object){
if (object !== undefined) {
this.viewer.box.setFromObject(object, true);
}
this.viewer.initPT();
this.render();
}
/**
* material 变更
*/
materialChanged(){
this.viewer.updatePTMaterials();
this.render();
}
/**
* windowResize
*/
sceneResize(){
this.viewer.updateAspectRatio();
this.viewer.renderer?.setSize(this.viewer.container.offsetWidth,this.viewer.container.offsetHeight);
if(this.viewer.modules.effect.enabled && this.viewer.modules.effect.composer){
this.viewer.modules.effect.composer.setSize(this.viewer.container.offsetWidth,this.viewer.container.offsetHeight);
if(Effect.PassMap.has("FXAA")){
const FXAA = Effect.PassMap.get("FXAA") as ShaderPass;
const pixelRatio = this.viewer.renderer.getPixelRatio();
FXAA.material.uniforms[ 'resolution' ].value.x = 1 / (this.viewer.container.offsetWidth * pixelRatio);
FXAA.material.uniforms[ 'resolution' ].value.y = 1 / (this.viewer.container.offsetHeight * pixelRatio);
}
}
this.viewer.pathtracer?.setSize();
this.viewer.css3DRenderer.setSize(this.viewer.container.offsetWidth,this.viewer.container.offsetHeight);
this.viewer.modules.viewHelper.update();
App.csm.updateFrustums();
this.render();
}
/**
* 是否显示场景网格
* @param showGrid
*/
showGridChanged(showGrid:boolean){
if(this.viewer.grid){
this.viewer.grid.visible = showGrid;
}
this.render();
}
/**
* 显示场景辅助线等
* @param showHelpers
*/
showHelpersChanged(showHelpers:boolean){
this.viewer.showSceneHelpers = showHelpers;
if(this.viewer.modules.transformControls){
this.viewer.modules.transformControls.enabled = showHelpers;
}
this.render();
}
/**
* 添加脚本
*/
scriptAdded(object:THREE.Object3D, _:ISceneScript){
this.viewer.installScripts([object.uuid]);
}
/**
* 移除脚本
*/
scriptRemoved(object:THREE.Object3D, sc:ISceneScript){
this.viewer.uninstallScriptsByUuid(object.uuid,sc.name);
}
/**
* 脚本变化
*/
scriptChanged(attributeName:string,object:THREE.Object3D, sc:ISceneScript){
if(attributeName !== "source") return;
this.viewer.installScripts([object.uuid],sc.name);
}
/**
* 渲染
*/
render(){
this.viewer.render();
}
}
@@ -0,0 +1,279 @@
/**
* @author ErSan
* @email mlt131220@163.com
* @date 2025/9/23 21:31
* @description 3d tiles加载管理
*/
import {TilesRenderer} from "3d-tiles-renderer";
import type {PriorityQueue} from "3d-tiles-renderer";
import {Mesh, Sphere, Vector3, Quaternion, Material, Scene} from 'three';
import { MeshBVH,StaticGeometryGenerator,/*CENTER*/ } from 'three-mesh-bvh';
import Tiles from "@/core/objects/Tile.ts";
export class TilesManage {
private scene:Scene;
protected _tilesMergeMesh:Mesh | null = null;
// Tiles BVH重建的监听函数
rebuildBVHEventsTiles: Tiles[] = [];
// 防抖计时器的 ID
private debounceTimeout: ReturnType<typeof setTimeout> | null = null;
// 防抖延迟(毫秒),可根据需要调整
private debounceDelay: number = 50;
// 防抖函数绑定
private scheduleRebuildFn = this.scheduleRebuild.bind(this);
// url -> Tiles[]
tilesMap = new Map<string, Tiles[]>;
// 共享队列(正确确定下载优先级)
downloadQueue:PriorityQueue | null = null;
parseQueue:PriorityQueue | null = null;
processNodeQueue:PriorityQueue | null = null;
needRender = false;
constructor(scene:Scene) {
this.scene = scene;
}
/**
* 获取所有tiles当前的静态网格
* 一般会在`漫游/物理碰撞支持`场景需要
*/
get mergeMesh():Mesh | null{
if(!this._tilesMergeMesh){
this.monitorComputeMergeMesh();
// 判断是否需要监听和重建bvh
if(this.rebuildBVHEventsTiles.length === 0 && this.tilesMap.size > 0){
for (const tiles of this.tilesMap.values()) {
tiles.forEach(_tile => {
// 监听 'tiles-load-end' 事件,這是触发 BVH 重建的最佳时机
_tile.renderer.addEventListener('tiles-load-end', this.scheduleRebuildFn);
this.rebuildBVHEventsTiles.push(_tile);
});
}
}
}
return this._tilesMergeMesh;
}
addTiles(tiles: Tiles):Promise<Tiles> {
return new Promise(resolve => {
// 设置共享队列(正确确定下载优先级)
if(!this.parseQueue){
this.downloadQueue = tiles.renderer.downloadQueue;
this.parseQueue = tiles.renderer.parseQueue;
this.processNodeQueue = tiles.renderer.processNodeQueue;
}else{
tiles.renderer.downloadQueue = this.downloadQueue as PriorityQueue;
tiles.renderer.parseQueue = this.parseQueue as PriorityQueue;
tiles.renderer.processNodeQueue = this.processNodeQueue as PriorityQueue;
}
if(tiles.options.reset2origin){
tiles.renderer.addEventListener('load-tile-set', () => {
// 调整瓦片的位置和方向,重置回原点
this.adjustTilesPositionAndDirection(tiles.renderer);
});
}
tiles.renderer.addEventListener('tiles-load-end', () => {
this.needRender = true;
resolve(tiles);
});
tiles.renderer.addEventListener('needs-update', () => {
this.needRender = true;
});
let _tiles = this.tilesMap.get(tiles.options.url);
if(!_tiles || _tiles.length === 0) {
_tiles = [];
}
_tiles.push(tiles);
this.tilesMap.set(tiles.options.url, _tiles);
if(this._tilesMergeMesh){
this.monitorComputeMergeMesh();
// 监听 'tiles-load-end' 事件,這是触发 BVH 重建的最佳时机
tiles.renderer.addEventListener('tiles-load-end', this.scheduleRebuildFn);
this.rebuildBVHEventsTiles.push(tiles);
}
})
}
removeTiles(tiles: Tiles) {
if (!tiles) return;
const _tiles = this.tilesMap.get(tiles.options.url);
if(!_tiles || _tiles.length === 0) return;
const filterTiles = _tiles.filter(_tile => _tile.uuid !== tiles.uuid);
if(filterTiles.length > 0) {
this.tilesMap.set(tiles.options.url,filterTiles);
}else{
this.tilesMap.delete(tiles.options.url);
}
if(this._tilesMergeMesh){
tiles.renderer.removeEventListener('tiles-load-end', this.scheduleRebuildFn);
this.rebuildBVHEventsTiles = this.rebuildBVHEventsTiles.filter(_tile => _tile.uuid !== tiles.uuid);
this.monitorComputeMergeMesh();
}
tiles.dispose();
}
update() {
for (const tiles of this.tilesMap.values()) {
tiles.forEach(tile => tile.update());
}
if(!this.needRender) return false;
this.needRender = false;
return true;
}
/**
* 重置瓦片的位置和方向回原点
*/
adjustTilesPositionAndDirection(tiles:TilesRenderer) {
if (!tiles) {
return;
}
const sphere = new Sphere();
tiles.getBoundingSphere(sphere);
const position = sphere.center.clone();
const distanceToEllipsoidCenter = position.length();
const surfaceDirection = position.normalize();
const up = new Vector3(0, 1, 0);
const rotationToNorthPole = this.rotationBetweenDirections(surfaceDirection, up);
tiles.group.quaternion.x = rotationToNorthPole.x;
tiles.group.quaternion.y = rotationToNorthPole.y;
tiles.group.quaternion.z = rotationToNorthPole.z;
tiles.group.quaternion.w = rotationToNorthPole.w;
tiles.group.position.y = -distanceToEllipsoidCenter;
}
rotationBetweenDirections(dir1:Vector3, dir2:Vector3) {
const rotation = new Quaternion();
const a = new Vector3().crossVectors(dir1, dir2);
rotation.x = a.x;
rotation.y = a.y;
rotation.z = a.z;
rotation.w = 1 + dir1.clone().dot(dir2);
rotation.normalize();
return rotation;
}
/**
* 获取场景中实时的所有Tiles的Mesh
*/
getAllTileMesh(){
const meshArray:Mesh[] = []
for (const tiles of this.tilesMap.values()) {
tiles.forEach(value => {
const group = value.group;
group.traverseVisible((child) => {
if (child.isMesh) {
meshArray.push(child as Mesh);
}
});
});
}
return meshArray;
}
/**
* 调度 BVH 重建(this.monitorComputeMergeMesh),核心是防抖
* 防止在摄影机连续移动导致瓦片频繁载入時,过度频繁地执行高耗能的 BVH 计算
*/
public scheduleRebuild(): void {
// 如果已經有一個計時器在等待,则清除它
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout);
}
// 设置一个新的计时器,在延迟时间后执行重建
this.debounceTimeout = setTimeout(() => {
this.monitorComputeMergeMesh();
}, this.debounceDelay);
}
/**
* 监听tiles变化并实时计算合并的静态网格
*/
protected monitorComputeMergeMesh(){
if (this._tilesMergeMesh) {
this.scene.remove(this._tilesMergeMesh);
this._tilesMergeMesh.geometry.dispose();
(this._tilesMergeMesh.material as Material).dispose();
this._tilesMergeMesh = null;
}
const tileMeshes = this.getAllTileMesh();
if (tileMeshes.length === 0) return;
const generator = new StaticGeometryGenerator([...tileMeshes]);
generator.attributes = ['position'];
const geometry = generator.generate();
// geometry.computeBoundsTree({
// // 使用 CENTER 策略在很多情況下性能更好
// strategy: CENTER
// });
geometry.boundsTree = new MeshBVH(geometry);
this._tilesMergeMesh = new Mesh(geometry);
this._tilesMergeMesh.name = "astral-3d-tiles-merge-mesh";
this._tilesMergeMesh.visible = false;
this._tilesMergeMesh.ignore = true;
this.scene.add(this._tilesMergeMesh);
}
dispose(){
if(this._tilesMergeMesh){
this.scene.remove(this._tilesMergeMesh);
this._tilesMergeMesh.geometry.dispose();
(this._tilesMergeMesh.material as Material).dispose();
this._tilesMergeMesh = null;
this.rebuildBVHEventsTiles.forEach(tiles => {
tiles.renderer.removeEventListener('tiles-load-end', this.scheduleRebuildFn);
})
this.rebuildBVHEventsTiles.length = 0;
}
for (const tiles of this.tilesMap.values()) {
tiles.forEach(tile => tile.dispose());
}
this.tilesMap.clear();
this.downloadQueue = null;
this.parseQueue = null;
this.processNodeQueue = null;
}
}
@@ -0,0 +1,294 @@
/*
* @author ErSan
* @email mlt131220@163.com
* @date 2024/12/23 19:27
* @description 天气系统
*/
import * as THREE from "three";
import {useAddSignal, useRemoveSignal} from "@/hooks";
import Rain from "@/core/objects/weather/Rain";
import Snow from "@/core/objects/weather/Snow";
import {SnowingShaderMaterial} from "@/core/shaderMaterial/modules/SnowingShaderMaterial";
import Viewer from "../Viewer";
import App from "@/core/app/App";
let _fogConfigChangeFn: any = null;
let _rainConfigChangeFn: any = null;
let _snowConfigChangeFn: any = null;
let _objectAddedFn: any = null;
export class Weather {
private viewer: Viewer;
rain: Rain | null = null;
snow: Snow | null = null;
snowingMaterialObj: THREE.Mesh[] = [];
constructor(viewer: Viewer) {
this.viewer = viewer;
_fogConfigChangeFn = this.sceneFogSettingsChanged.bind(this)
useAddSignal("sceneFogSettingsChanged", _fogConfigChangeFn);
_rainConfigChangeFn = this.sceneRainSettingsChanged.bind(this);
useAddSignal("sceneRainSettingsChanged", _rainConfigChangeFn);
_snowConfigChangeFn = this.sceneSnowSettingsChanged.bind(this);
useAddSignal("sceneSnowSettingsChanged", _snowConfigChangeFn);
_objectAddedFn = this.objectAdded.bind(this);
useAddSignal("objectAdded", _objectAddedFn);
}
objectAdded(object) {
const {enabled, accumulation} = App.project.getKey("weather.snow");
if (enabled && accumulation) {
object.traverseByCondition((obj) => {
this.replaceSnowMaterial(obj);
}, (child) => !child.ignore && child.visible);
}
}
/**
* 场景雾效设置项变更
*/
sceneFogSettingsChanged() {
const fog = App.project.getKey("weather.fog")
if (!fog.enabled) {
this.viewer.scene.fog = null;
this.viewer.render();
return;
}
switch (fog.type) {
case 'Fog':
if (!(this.viewer.scene.fog instanceof THREE.Fog)) {
this.viewer.scene.fog = new THREE.Fog(fog.color, fog.near, fog.far);
} else {
const _fog = this.viewer.scene.fog as THREE.Fog;
_fog.color = new THREE.Color(fog.color);
_fog.near = fog.near;
_fog.far = fog.far;
}
break;
case 'FogExp2':
if (!(this.viewer.scene.fog instanceof THREE.FogExp2)) {
this.viewer.scene.fog = new THREE.FogExp2(fog.color, fog.density);
} else {
const _fog = this.viewer.scene.fog as THREE.FogExp2
_fog.color = new THREE.Color(fog.color);
_fog.density = fog.density;
}
break;
}
this.viewer.render();
}
/**
* 场景雨效设置项变更
*/
sceneRainSettingsChanged() {
const {enabled, speed, color, size, radian, alpha} = App.project.getKey("weather.rain");
if (enabled) {
if (this.rain) {
this.rain.updateOptions({
speed,
color,
size,
radian,
alpha
})
} else {
this.rain = new Rain({
speed: speed,
color: color,
size: size,
radian: radian,
alpha: alpha
}, this.viewer.modules.controls);
this.rain.mesh.ignore = true;
this.viewer.scene.add(this.rain.mesh as THREE.Object3D);
}
} else {
if (!this.rain) return;
this.rain.dispose();
this.rain = null;
this.viewer.render();
}
}
/**
* 替换材质贴图增加雪堆积
*/
replaceSnowMaterial(obj) {
if (!obj.material) return;
if (obj.material.map && obj.material.map instanceof THREE.Texture) {
// 存储原材质
!obj.metaData && (obj.metaData = {});
obj.metaData.material = obj.material;
obj.material = SnowingShaderMaterial.InstanceShaderMaterial().clone();
obj.material.copyAttr(obj.metaData.material);
obj.material.transparent = obj.metaData.material.transparent;
obj.material.uniforms.uTime.value = 0.01;
obj.material.uniforms.uHasTexture.value = 1.0;
obj.material.uniforms.uTexture.value = obj.metaData.material.map;
obj.material.needsUpdate = true;
this.snowingMaterialObj.push(obj);
} else if (obj.material.color) {
// 存储原材质
!obj.metaData && (obj.metaData = {});
obj.metaData.material = obj.material;
const alpha = obj.material.transparent ? obj.material.opacity : 1;
const color = new THREE.Vector4(obj.material.color.r, obj.material.color.g, obj.material.color.b, alpha);
obj.material = SnowingShaderMaterial.InstanceShaderMaterial().clone();
obj.material.copyAttr(obj.metaData.material);
obj.material.transparent = obj.metaData.material.transparent;
obj.material.uniforms.uTime.value = 0.01;
obj.material.uniforms.uHasTexture.value = 0.0;
obj.material.uniforms.uColor.value = color;
obj.material.needsUpdate = true;
this.snowingMaterialObj.push(obj);
}
}
/**
* 初始化替换材质贴图增加雪堆积
*/
initSnowMap() {
this.snowingMaterialObj = [];
this.viewer.scene.traverseByCondition((obj) => {
this.replaceSnowMaterial(obj);
}, (child) => !child.ignore && child.visible);
}
/**
* 关闭雪,还原贴图
*/
removeSnowMap() {
for (let i = this.snowingMaterialObj.length - 1; i >= 0; i--) {
const obj = this.snowingMaterialObj[i];
// 释放 ShaderMaterial
// TODO: 20250519: R176版本调用报错,暂不销毁
// (<THREE.ShaderMaterial>obj.material).dispose();
// 恢复原始材质
obj.material = obj.metaData.material as THREE.Material;
// 清除 metaData 中的材质引用
// @ts-ignore
obj.metaData.material = null;
delete obj.metaData.material;
// 从数组中删除当前元素
this.snowingMaterialObj.splice(i, 1);
}
this.snowingMaterialObj = [];
}
/**
* 场景雪效设置项变更
*/
sceneSnowSettingsChanged() {
const {enabled, speed, size, density, alpha, accumulation} = App.project.getKey("weather.snow");
if (enabled) {
if (this.snow) {
this.snow.updateOptions({
speed,
size,
density,
alpha
})
if(accumulation && this.snowingMaterialObj.length === 0){
this.initSnowMap()
}else if(!accumulation && this.snowingMaterialObj.length > 0){
this.removeSnowMap();
}
} else {
this.snow = new Snow({
speed: speed,
size: size,
density: density,
alpha: alpha
}, this.viewer.modules.controls);
this.snow.mesh.ignore = true;
this.viewer.scene.add(this.snow.mesh as THREE.Object3D);
accumulation && this.initSnowMap();
}
} else {
if (!this.snow) return;
this.snow.dispose();
this.snow = null;
this.removeSnowMap();
this.viewer.render();
}
}
/**
* 更新天气效果
* @param deltaTime
* @return {boolean} 是否需要调用viewport.render()
*/
update(deltaTime) {
let needRender = false;
if (this.rain) {
this.rain.update(deltaTime);
needRender = true;
}
if (this.snow) {
this.snow.update(deltaTime);
if (App.project.getKey("weather.snow.accumulation")) {
const speed = this.snow.options.speed;
this.snowingMaterialObj.forEach(obj => {
const m = obj.material as THREE.ShaderMaterial;
if (m.uniforms.uTime.value > speed / 2) {
m.uniforms.uTime.value = speed / 2;
return;
}
m.uniforms.uTime.value += 0.001 * speed;
});
}
needRender = true;
}
return needRender;
}
dispose() {
useRemoveSignal("sceneFogSettingsChanged", _fogConfigChangeFn);
_fogConfigChangeFn = null;
useRemoveSignal("sceneRainSettingsChanged", _rainConfigChangeFn);
_rainConfigChangeFn = null;
useRemoveSignal("sceneSnowSettingsChanged", _snowConfigChangeFn);
_snowConfigChangeFn = null;
this.rain && this.rain.dispose();
this.snow && this.snow.dispose();
}
}
@@ -0,0 +1,8 @@
export {Helper} from './Helper';
export {CameraManage} from './CameraManage';
export {Effect,LUTEffectMap} from "./Effect";
export {Weather} from "./Weather";
export {Signals} from "./Signals";
export {ParticleSystem} from "./ParticleSystem";
export {Drag} from "./Drag";
export {TilesManage} from "./TilesManage";