新增场景释放机制和Dispose方法

This commit is contained in:
陈梓阳 2025-05-09 11:23:57 +08:00
parent 4a648c2f97
commit efb220612e
9 changed files with 294 additions and 93 deletions

View File

@ -1,7 +1,7 @@
{
"name": "material-editor",
"private": true,
"version": "1.0.2",
"version": "1.0.3",
"type": "module",
"scripts": {
"dev": "vite",

View File

@ -29,8 +29,6 @@ async function textureRenderUpdate(textureRecord:TextureTableRecord){
*/
export class MaterialEditor extends Singleton
{
private _pointerLocked = false;
Geometrys: Map<string, Geometry | BufferGeometry>;
CurGeometryName = "球";
@ -38,6 +36,7 @@ export class MaterialEditor extends Singleton
ShowObject: Object3D;
ShowMesh: Mesh;
Viewer: Viewer;
CameraControl?: MaterialEditorCameraControl;
private _MeshMaterial: MeshPhysicalMaterial = new MeshPhysicalMaterial({});
//构造
@ -72,7 +71,7 @@ export class MaterialEditor extends Singleton
// this.Viewer.PreViewer.Cursor.CursorObject.visible = false;
// this.Viewer.UsePass = false;
this.initScene();
new MaterialEditorCameraControl(this.Viewer, this.ShowObject.position);
this.CameraControl = new MaterialEditorCameraControl(this.Viewer, this.ShowObject.position);
this.Viewer.ZoomAll();
// 初始化相机位置到观察物体的正后方
@ -82,6 +81,9 @@ export class MaterialEditor extends Singleton
this.Viewer.UpdateRender();
this.Viewer.Fov = 90;
}
else {
console.warn("Viewer has already been initialized");
}
}
SetViewer(canvas: HTMLElement)
{
@ -186,8 +188,24 @@ export class MaterialEditor extends Singleton
this.Update();
}
dispose()
Dispose()
{
this.Geometrys.clear();
this.Geometrys = undefined;
this.Canvas = undefined;
// object在Viewer中进行释放
this.ShowObject = undefined;
this.ShowMesh = undefined;
this.CameraControl?.dispose();
this.Viewer?.Dispose();
this.CameraControl = undefined;
this.Viewer = undefined;
this.exrPromise = undefined;
this.exrTexture?.dispose();
this.exrTexture = undefined;
this.metaTexture?.dispose();
this.metaTexture = undefined;
}
async Update()

View File

@ -24,12 +24,24 @@ export class MaterialEditorCameraControl {
initMouseControl() {
let el = this.Viewer.Renderer.domElement;
el.addEventListener("pointerdown", (e) => { this.pointId = e.pointerId; }, false);
el.addEventListener("pointerdown", this.onPointerDown, false);
el.addEventListener("mousedown", this.onMouseDown, false);
el.addEventListener("mousemove", this.onMouseMove, false);
el.addEventListener("mouseup", this.onMouseUp, false);
el.addEventListener('wheel', this.onMouseWheel, false);
}
dispose() {
const el = this.Viewer.Renderer.domElement;
el.removeEventListener("pointerdown", this.onPointerDown);
el.removeEventListener("mousedown", this.onMouseDown);
el.removeEventListener("mousemove", this.onMouseMove);
el.removeEventListener("mouseup", this.onMouseUp);
el.removeEventListener('wheel', this.onMouseWheel, false);
}
onPointerDown = (event: PointerEvent) => { this.pointId = event.pointerId; };
onMouseDown = (event: MouseEvent) => {
this.requestPointerLock();
this._MouseIsDown = true;
@ -49,14 +61,11 @@ export class MaterialEditorCameraControl {
else this.Viewer.Rotate(this._movement);
}
};
onMouseWheel = (event: WheelEvent) =>
{
if (event.deltaY < 0)
{
onMouseWheel = (event: WheelEvent) => {
if (event.deltaY < 0) {
this.Viewer.Zoom(0.6);
}
else if (event.deltaY > 0)
{
else if (event.deltaY > 0) {
this.Viewer.Zoom(1.4);
}
};

View File

@ -2,13 +2,11 @@
let instanceMap = new Map();
export interface PrototypeType<T> extends Function
{
export interface PrototypeType<T> extends Function {
prototype: T;
}
export interface ConstructorFunctionType<T = any> extends PrototypeType<T>
{
export interface ConstructorFunctionType<T = any> extends PrototypeType<T> {
new(...args: any[]): T;
}
@ -23,13 +21,11 @@ export type ConstructorType<T = unknown, Static extends Record<string, any> = Pr
* //获得单例
* let a = A.GetInstance();
*/
export class Singleton
{
export class Singleton {
protected constructor() { }
//ref:https://github.com/Microsoft/TypeScript/issues/5863
static GetInstance<T extends Singleton>(this: ConstructorType<T, typeof Singleton>): T
{
static GetInstance<T extends Singleton>(this: ConstructorType<T, typeof Singleton>): T {
if (instanceMap.has(this))
return instanceMap.get(this);
//@ts-ignore
@ -37,4 +33,23 @@ export class Singleton
instanceMap.set(this, __instance__);
return __instance__;
}
/**
*
* @returns {boolean} true false
*/
static ReleaseInstance<T extends Singleton>(this: ConstructorType<T, typeof Singleton>): boolean {
if (instanceMap.has(this)) {
const instance = instanceMap.get(this);
// 如果实例有清理方法,可以在这里调用
if (typeof (instance as any).dispose === 'function') {
(instance as any).dispose();
}
instanceMap.delete(this);
console.log(`Singleton instance of ${this.name} has been released.`);
return true;
}
console.log(`No singleton instance of ${this.name} found to release.`);
return false;
}
}

View File

@ -1,20 +1,25 @@
import { Raycaster, Scene, Vector3, WebGLRenderer, type Vector, type WebGLRendererParameters } from "three";
import { Raycaster, Scene, Vector3, WebGLRenderer, type WebGLRendererParameters, Object3D, Mesh, Line, Points, Material, Texture } from "three";
import { cZeroVec, GetBox, GetBoxArr } from "./GeUtils";
import { PlaneExt } from "./PlaneExt";
import { CameraType, CameraUpdate } from "webcad_ue4_api";
import { disposeNode } from "../helpers/helper.three";
export class Viewer {
m_LookTarget: any;
m_Camera: CameraUpdate = new CameraUpdate();
protected NeedUpdate: boolean = true;
Renderer: WebGLRenderer;//渲染器 //暂时只用这个类型
m_DomEl: HTMLElement; //画布容器
Renderer!: WebGLRenderer; // 在 initRender 中初始化,使用 ! 断言
m_DomEl: HTMLElement;
_Height: number = 0;
_Width: number = 0;
_Scene: Scene = new Scene();
private m_IsDisposed: boolean = false; // 添加一个标志来停止渲染循环和防止重复释放
private m_AnimationFrameId: number | null = null; // 用于取消动画帧
get Scene() {
return this._Scene;
}
@ -28,90 +33,95 @@ export class Viewer {
this.NeedUpdate = true;
}
UpdateRender()
{
UpdateRender() {
this.NeedUpdate = true;
}
/**
*
* @param {HTMLElement} canvasContainer div或者一个画布
* @memberof Viewer
*/
constructor(canvasContainer: HTMLElement) {
this.m_DomEl = canvasContainer;
this.initRender(canvasContainer);
this.OnSize();
this.StartRender();
window.addEventListener("resize", () => {
this.OnSize();
});
this.SetSize(); // 初始尺寸设置
this.StartRender(); // 开始渲染循环
// 注意: OnSize 是一个箭头函数属性,它的 this 上下文是固定的
// 因此可以直接用于添加和移除事件监听器
window.addEventListener("resize", this.OnResize);
this.InitCamera();
}
//初始化render
initRender(canvasContainer: HTMLElement) {
let params: WebGLRendererParameters = {
antialias: true,//antialias:true/false是否开启反锯齿
precision: "highp",//precision:highp/mediump/lowp着色精度选择
alpha: true//alpha:true/false是否可以设置背景色透明
antialias: true,
precision: "highp",
alpha: true
};
this.Renderer = new WebGLRenderer(params);
//加到画布
canvasContainer.appendChild(this.Renderer.domElement);
this.Renderer.autoClear = true;
//如果设置那么它希望所有的纹理和颜色都是预乘的伽玛。默认值为false。
// this.Renderer.gammaInput = true;
// this.Renderer.gammaOutput = true;
// this.Renderer.shadowMap.enabled = true;
// this.Renderer.toneMapping = ReinhardToneMapping;
//设置设备像素比。 这通常用于HiDPI设备以防止模糊输出画布。
this.Renderer.setPixelRatio(window.devicePixelRatio);
this.Renderer.physicallyCorrectLights = true;
//this.Renderer.toneMappingExposure = Math.pow(1, 5.0); // to allow for very bright scenes.
//设置它的背景色为黑色
this.Renderer.physicallyCorrectLights = true; // r150 后默认 false, 根据需要设置
this.Renderer.setClearColor(0xffffff, 1);
this.OnSize();
// this.OnSize(); // 在构造函数中调用,这里可以不用重复
}
InitCamera() {
this.m_Camera.CameraType = CameraType.PerspectiveCamera;
this.m_Camera.Near = 0.001;
if (this.m_Camera.Camera) { // 确保 CameraUpdate 内部的 Camera 已初始化
this.m_Camera.Camera.far = 1000000000;
}
OnSize = (width?: number, height?: number) => {
}
OnResize = (evt: UIEvent) => this.SetSize();
SetSize = (width?: number, height?: number) => {
if (this.m_IsDisposed || !this.m_DomEl) return; // 如果已销毁或容器不存在,则不执行
this._Width = width ? width : this.m_DomEl.clientWidth;
this._Height = height ? height : this.m_DomEl.clientHeight;
//校验.成为2的倍数 避免外轮廓错误.
if (this._Width % 2 == 1)
this._Width -= 1;
if (this._Height % 2 == 1)
this._Height -= 1;
if (this._Width % 2 === 1) this._Width -= 1;
if (this._Height % 2 === 1) this._Height -= 1;
if (this.Renderer) {
this.Renderer.setSize(this._Width, this._Height);
}
if (this.m_Camera) {
this.m_Camera.SetSize(this._Width, this._Height);
}
this.NeedUpdate = true; // 尺寸变化后需要重新渲染
};
StartRender = () => {
requestAnimationFrame(this.StartRender);
if (this._Scene != null && this.NeedUpdate) {
if (this.m_IsDisposed) {
if (this.m_AnimationFrameId !== null) {
cancelAnimationFrame(this.m_AnimationFrameId);
this.m_AnimationFrameId = null;
}
return;
}
this.m_AnimationFrameId = requestAnimationFrame(this.StartRender);
if (this._Scene && this.NeedUpdate) { // 确保 _Scene 存在
this.Render();
if (this.m_Camera) { // 确保 m_Camera 存在
this.m_Camera.Update();
}
this.NeedUpdate = false;
}
};
Render() {
if (this.m_IsDisposed || !this.Renderer || !this._Scene || !this.m_Camera || !this.m_Camera.Camera) {
return;
}
this.Renderer.render(this._Scene, this.m_Camera.Camera);
}
// ScreenToWorld, WorldToScreen, UpdateLockTarget, Rotate, Pan, Zoom, etc. methods remain the same...
// ... (省略了您提供的其他方法,它们与 dispose 无直接关系,保持不变) ...
ScreenToWorld(pt: Vector3, planVec?: Vector3) {
if (this.m_IsDisposed || !this._Scene || !this.m_Camera || !this.m_Camera.Camera) return;
//变换和求交点
let plan = new PlaneExt(planVec || new Vector3(0, 0, 1));
let raycaster = new Raycaster();
@ -123,9 +133,10 @@ export class Viewer {
}
, this.m_Camera.Camera
);
plan.intersectRay(raycaster.ray, pt, true);
plan.intersectRay(raycaster.ray, pt, true); // 假设 intersectRay 修改 pt
}
WorldToScreen(pt: Vector3) {
if (this.m_IsDisposed || !this.m_Camera || !this.m_Camera.Camera) return;
let widthHalf = this._Width * 0.5;
let heightHalf = this._Height * 0.5;
@ -135,51 +146,133 @@ export class Viewer {
pt.y = - (pt.y * heightHalf) + heightHalf;
}
/**
* ()
*
* @memberof Viewer
*/
UpdateLockTarget() {
let renderList = this.Renderer.renderLists.get(this._Scene, this.m_Camera.Camera);
let box = GetBoxArr(renderList.opaque.map(o => o.object));
if (this.m_IsDisposed || !this.Renderer || !this._Scene || !this.m_Camera || !this.m_Camera.Camera) return;
// @ts-ignore access to private renderLists
const renderList = this.Renderer.renderLists.get(this._Scene, this.m_Camera.Camera);
// @ts-ignore
const box = GetBoxArr(renderList.opaque.map(o => o.object)); // 确保 GetBoxArr 定义正确
if (box)
// @ts-ignore
this.m_LookTarget = box.getCenter(new Vector3());
else
this.m_LookTarget = cZeroVec;
// @ts-ignore
this.m_LookTarget = cZeroVec; // 确保 cZeroVec 定义正确
}
Rotate(mouseMove: Vector3) {
if (this.m_IsDisposed || !this.m_Camera) return;
this.m_Camera.Rotate(mouseMove, this.m_LookTarget);
this.NeedUpdate = true;
}
RotateAround(movement: Vector3, center: Vector3) {
if (this.m_IsDisposed || !this.m_Camera) return;
this.m_Camera.Rotate(movement, center);
this.NeedUpdate = true;
}
Pan(mouseMove: Vector3) {
if (this.m_IsDisposed || !this.m_Camera) return;
this.m_Camera.Pan(mouseMove);
this.NeedUpdate = true;
}
Zoom(scale: number, center?: Vector3) {
if (this.m_IsDisposed || !this.m_Camera) return;
this.m_Camera.Zoom(scale, center);
this.NeedUpdate = true;
}
ZoomAll()
{
this.m_Camera.ZoomExtentsBox3(GetBox(this._Scene, true));
ZoomAll() {
if (this.m_IsDisposed || !this.m_Camera || !this._Scene) return;
// @ts-ignore
this.m_Camera.ZoomExtentsBox3(GetBox(this._Scene, true)); // 确保 GetBox 定义正确
this.NeedUpdate = true;
}
ViewToTop() {
if (this.m_IsDisposed || !this.m_Camera) return;
this.m_Camera.LookAt(new Vector3(0, 0, -1));
this.NeedUpdate = true;
}
ViewToFront() {
if (this.m_IsDisposed || !this.m_Camera) return;
this.m_Camera.LookAt(new Vector3(0, 1, 0));
this.NeedUpdate = true;
}
ViewToSwiso() {
if (this.m_IsDisposed || !this.m_Camera) return;
this.m_Camera.LookAt(new Vector3(1, 1, -1));
this.NeedUpdate = true;
}
/**
* Viewer
*/
public Dispose(): void {
if (this.m_IsDisposed) {
return; // 防止重复释放
}
console.log("Disposing Viewer...");
// 1. 停止渲染循环
this.m_IsDisposed = true; // 设置标志,渲染循环将在下一帧停止
if (this.m_AnimationFrameId !== null) {
cancelAnimationFrame(this.m_AnimationFrameId);
this.m_AnimationFrameId = null;
}
// 2. 移除事件监听器
window.removeEventListener("resize", this.OnResize);
// 如果 m_DomEl 上有其他直接添加的事件监听器,也应在此移除
// 3. 销毁场景内容 (几何体、材质、纹理)
if (this._Scene) {
// 遍历场景中的所有对象并释放它们的资源
this._Scene.traverse((object) => {
disposeNode(object);
});
// 从场景中移除所有子对象
// scene.clear() 也可以,但在此之前我们已经遍历并释放了资源
while (this._Scene.children.length > 0) {
const child = this._Scene.children[0];
// 确保子对象的资源已通过上面的 traverse 处理
this._Scene.remove(child);
}
// console.log("Scene cleared and resources disposed.");
}
// 4. 销毁渲染器
if (this.Renderer) {
// @ts-ignore access to private renderLists
if (this.Renderer.renderLists) { // 检查 renderLists 是否存在
// @ts-ignore
this.Renderer.renderLists.dispose();
// console.log("Renderer renderLists disposed.");
}
this.Renderer.dispose();
// console.log("Renderer disposed.");
// 从 DOM 中移除渲染器的 canvas 元素
if (this.Renderer.domElement && this.Renderer.domElement.parentElement) {
this.Renderer.domElement.parentElement.removeChild(this.Renderer.domElement);
// console.log("Renderer DOM element removed.");
}
}
// 5. 清除引用 (帮助垃圾回收)
// @ts-ignore
this._Scene = null;
// @ts-ignore
this.Renderer = null;
// @ts-ignore
this.m_DomEl = null;
// @ts-ignore
this.m_Camera = null;
// @ts-ignore
this.m_LookTarget = null;
console.log("Viewer disposed successfully.");
}
}

View File

@ -5,7 +5,7 @@
</CfFlex>
</template>
<script setup lang="ts">
import { onMounted, ref, useTemplateRef } from 'vue';
import { onBeforeUnmount, onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
import MaterialAdjuster from './MaterialAdjuster.vue';
import { useScene } from '../stores/sceneStore';
import CfFlex from './CfFlex.vue';
@ -21,15 +21,26 @@ const textureSrc = ref(config.textureSrc);
//
document.addEventListener('contextmenu', (e) => e.preventDefault());
onMounted(() => {
scene.Initial(container.value);
eventbus.Subscribe('submit', () => adjusterRef.value.Upload());
eventbus.Subscribe('update-texture', () => {
textureSrc.value = config.textureSrc
eventbus.Subscribe('submit', HandleUpload);
eventbus.Subscribe('update-texture', HandleChangeTexture);
});
onBeforeUnmount(() => {
eventbus.Unsubscribe('submit', HandleUpload);
eventbus.Unsubscribe('update-texture', HandleChangeTexture);
scene.Dispose();
});
function HandleUpload() {
adjusterRef.value.Upload();
}
function HandleChangeTexture() {
textureSrc.value = config.textureSrc;
}
</script>
<style scoped lang="scss">

View File

@ -0,0 +1,39 @@
import type { Material, Texture, Object3D, Mesh, Line, Points } from "three";
/** 深度释放材质和纹理 */
export function disposeMaterial(material: Material | Material[]): void {
if (Array.isArray(material)) {
material.forEach(mat => disposeSingleMaterial(mat));
} else if (material) { // 确保 material 不是 undefined 或 null
disposeSingleMaterial(material);
}
}
export function disposeSingleMaterial(material: Material): void {
material.dispose();
// 遍历材质的属性,查找并释放纹理
for (const key in material) {
const value = (material as any)[key] as unknown; // 使用 unknown 来进行更安全的类型检查
if (value && typeof value === 'object' && 'isTexture' in value && (value as Texture).isTexture) {
(value as Texture).dispose();
}
}
}
/** 释放节点的几何体、材质和纹理 */
export function disposeNode(node: Object3D): void {
// 类型守卫,检查节点是否为 Mesh, Line, 或 Points
if ((node as Mesh).isMesh || (node as Line).isLine || (node as Points).isPoints) {
const obj = node as Mesh | Line | Points;
if (obj.geometry) {
obj.geometry.dispose();
// console.log("Disposed geometry:", obj.geometry.uuid);
}
if (obj.material) {
disposeMaterial(obj.material);
// console.log("Disposed material(s) for node:", obj.uuid);
}
}
}

View File

@ -12,7 +12,11 @@ export const useEvent = defineStore('event', () => {
events[name]?.forEach(e => e(...args));
}
return { Subscribe, Publish };
function Unsubscribe(name: EventNames, callback: Function) {
events[name] = events[name]?.filter(e => e !== callback) || [];
}
return { Subscribe, Publish, Unsubscribe };
});
export type EventNames = 'submit' | 'update-texture'

View File

@ -8,7 +8,7 @@ import { materialRenderer } from "../common/MaterialRenderer";
import { MaterialOut } from "../common/MaterialSerializer";
export const useScene = defineStore('scene', () => {
let _editor: MaterialEditor;
let _editor: MaterialEditor | undefined;
const _currGeometry = ref<string>('球');
const _currTexture = ref<Texture>();
const CurrGeometry = computed({
@ -37,11 +37,22 @@ export const useScene = defineStore('scene', () => {
const view = _editor.Viewer;
window.onresize = () => {
view.OnSize(canvas.clientWidth, canvas.clientHeight);
view.SetSize(canvas.clientWidth, canvas.clientHeight);
view.UpdateRender();
}
}
function Dispose() {
console.log("Disposing...");
Material.value.GoodBye();
_editor?.Dispose();
_editor = undefined;
window.onresize = undefined;
// 释放Singleton
MaterialEditor.ReleaseInstance();
}
function ChangeGeometry(geoName: string) {
_currGeometry.value = geoName;
let geo = _editor.Geometrys.get(_currGeometry.value);
@ -121,7 +132,8 @@ export const useScene = defineStore('scene', () => {
ChangeTextureAsync,
UpdateTexture,
SerializeMaterialAsync,
GenerateMaterialLogoAsync
GenerateMaterialLogoAsync,
Dispose
};
});