@mirage-engine/core 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,38 +6,62 @@ import {
6
6
  CoreConfig,
7
7
  MirageMode,
8
8
  USER_LAYER,
9
- SYSTEM_LAYER,
9
+ SELECT_LAYER,
10
10
  travelerClipArea,
11
+ THREE_LAYERS,
12
+ ATTR_TRAVEL,
13
+ LayerTarget,
11
14
  } from "../types";
12
- import { Painter } from "@mirage-engine/painter";
15
+ import { Painter, TextStyles, BoxStyles } from "@mirage-engine/painter";
16
+ import { MeshRegistry } from "../store/MeshRegistry";
17
+ import { TextureLifecycleManager } from "../store/TextureLifecycleManager";
13
18
 
14
19
  export class Renderer {
15
20
  public readonly canvas: HTMLCanvasElement;
16
21
  private readonly scene: THREE.Scene;
17
22
  private readonly camera: THREE.OrthographicCamera;
18
23
  private readonly renderer: THREE.WebGLRenderer;
19
- private renderTarget: THREE.WebGLRenderTarget | null = null;
24
+ private renderTargets: THREE.WebGLRenderTarget[] = [];
20
25
  private renderOrder: number = 0;
21
- private qualityFactor: number = 2;
26
+ public qualityFactor: number = 2;
22
27
  private mode: MirageMode = "overlay";
23
28
  private clipArea: travelerClipArea = 1;
29
+ public targetLayer: number | LayerTarget = "base";
24
30
 
25
31
  private target: HTMLElement;
26
32
  private mountContainer: HTMLElement;
33
+ private registry: MeshRegistry;
27
34
  private targetRect: DOMRect;
28
35
 
29
- private meshMap: Map<HTMLElement, THREE.Mesh> = new Map();
36
+ private travelersByLayer: Set<THREE.Mesh>[] = Array.from(
37
+ { length: ATTR_TRAVEL.MAX_LAYERS },
38
+ () => new Set(),
39
+ );
40
+ private textureManager: TextureLifecycleManager;
41
+ // private meshMap: Map<HTMLElement, THREE.Mesh> = new Map();
42
+
43
+ public readonly fixedMeshes: Set<THREE.Mesh> = new Set();
30
44
 
31
45
  constructor(
32
46
  target: HTMLElement,
33
47
  config: CoreConfig,
34
48
  mountContainer: HTMLElement,
49
+ registry: MeshRegistry,
35
50
  ) {
36
51
  this.target = target;
37
52
  this.mountContainer = mountContainer;
53
+ this.registry = registry;
54
+
55
+ this.textureManager = new TextureLifecycleManager((el, texture) => {
56
+ const mesh = this.registry.get(el);
57
+ if (mesh && mesh.material instanceof THREE.ShaderMaterial) {
58
+ Painter.forceUpdateUniforms(mesh.material, { texture: texture });
59
+ }
60
+ });
38
61
 
39
62
  this.mode = config.mode ?? "overlay";
40
63
  this.clipArea = config.travelerClipArea ?? 1;
64
+ this.targetLayer = config.layer ?? "base";
41
65
  this.canvas = document.createElement("canvas");
42
66
  this.scene = new THREE.Scene();
43
67
 
@@ -58,10 +82,7 @@ export class Renderer {
58
82
  1000,
59
83
  );
60
84
  this.camera.position.z = 100;
61
- this.camera.layers.set(0);
62
-
63
- // [new]
64
- // THREE.ColorManagement.enabled = false;
85
+ this.camera.layers.set(this.getSceneLayer());
65
86
 
66
87
  this.renderer = new THREE.WebGLRenderer({
67
88
  canvas: this.canvas,
@@ -71,24 +92,41 @@ export class Renderer {
71
92
  // premultipliedAlpha: true
72
93
  });
73
94
 
95
+ THREE.ColorManagement.enabled = false;
96
+ this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
97
+
74
98
  this.renderer.setPixelRatio(window.devicePixelRatio);
75
99
  this.renderer.setSize(width, height);
76
100
 
77
101
  this.applyTextQuality(config.quality ?? "medium");
78
102
  }
103
+
104
+ private getSceneLayer(): number {
105
+ if (typeof this.targetLayer === "number") {
106
+ return this.targetLayer;
107
+ } else if (this.targetLayer === "selected") {
108
+ return THREE_LAYERS.SELECTED;
109
+ } else {
110
+ return THREE_LAYERS.BASE;
111
+ }
112
+ }
113
+
79
114
  public createRenderTarget() {
80
- this.renderTarget = new THREE.WebGLRenderTarget(
81
- this.targetRect.width * this.qualityFactor,
82
- this.targetRect.height * this.qualityFactor,
83
- {
84
- minFilter: THREE.LinearFilter,
85
- magFilter: THREE.LinearFilter,
86
- format: THREE.RGBAFormat,
87
- stencilBuffer: false,
88
- depthBuffer: true,
89
- samples: 4,
90
- },
91
- );
115
+ for (let i = 0; i < ATTR_TRAVEL.MAX_LAYERS; i++) {
116
+ this.renderTargets.push(
117
+ new THREE.WebGLRenderTarget(
118
+ this.targetRect.width * this.qualityFactor,
119
+ this.targetRect.height * this.qualityFactor,
120
+ {
121
+ minFilter: THREE.LinearFilter,
122
+ magFilter: THREE.LinearFilter,
123
+ format: THREE.RGBAFormat,
124
+ stencilBuffer: false,
125
+ depthBuffer: true,
126
+ },
127
+ ),
128
+ );
129
+ }
92
130
  }
93
131
 
94
132
  private applyTextQuality(quality: Quality) {
@@ -104,6 +142,9 @@ export class Renderer {
104
142
  this.qualityFactor = 4;
105
143
  break;
106
144
  case "medium":
145
+ // this.qualityFactor = 2;
146
+ this.qualityFactor = 2;
147
+ break;
107
148
  default:
108
149
  this.qualityFactor = 2;
109
150
  break;
@@ -135,19 +176,42 @@ export class Renderer {
135
176
  }
136
177
  }
137
178
 
179
+ public updateUniforms(element: HTMLElement, uniforms: Record<string, any>) {
180
+ const mesh = this.registry.get(element);
181
+ if (!mesh) return;
182
+
183
+ mesh.traverse((child) => {
184
+ if ((child as THREE.Mesh).isMesh && (child as THREE.Mesh).material) {
185
+ Painter.forceUpdateUniforms(
186
+ (child as THREE.Mesh).material as THREE.ShaderMaterial,
187
+ uniforms,
188
+ );
189
+ }
190
+ });
191
+
192
+ if (mesh.userData.nativeMesh) {
193
+ mesh.userData.nativeMesh.traverse((child: THREE.Object3D) => {
194
+ if ((child as THREE.Mesh).isMesh && (child as THREE.Mesh).material) {
195
+ Painter.forceUpdateUniforms(
196
+ (child as THREE.Mesh).material as THREE.ShaderMaterial,
197
+ uniforms,
198
+ );
199
+ }
200
+ });
201
+ }
202
+ }
203
+
138
204
  public dispose() {
139
205
  this.renderer.dispose();
140
206
  this.canvas.remove();
207
+ this.textureManager.disposeAll();
141
208
  // TODO: Scene 내부 Mesh들도 순회하며 dispose
142
209
  }
143
210
 
144
211
  public setSize(width: number, height: number) {
145
212
  this.renderer.setSize(width, height);
146
- if (this.renderTarget) {
147
- this.renderTarget.setSize(
148
- width * this.qualityFactor,
149
- height * this.qualityFactor,
150
- );
213
+ for (const target of this.renderTargets) {
214
+ target.setSize(width * this.qualityFactor, height * this.qualityFactor);
151
215
  }
152
216
  this.camera.left = width / -2;
153
217
  this.camera.right = width / 2;
@@ -156,7 +220,7 @@ export class Renderer {
156
220
  this.camera.updateProjectionMatrix();
157
221
  }
158
222
 
159
- public syncScene(graphNode: SceneNode) {
223
+ public syncScene(graphNode: SceneNode, pendingDeletions: Set<HTMLElement>) {
160
224
  const newRect = this.target.getBoundingClientRect();
161
225
 
162
226
  const isResized =
@@ -182,110 +246,230 @@ export class Renderer {
182
246
 
183
247
  this.renderOrder = 0;
184
248
 
185
- const activeElements = new Set<HTMLElement>();
186
-
187
- this.reconcileNode(graphNode, activeElements);
188
-
189
- for (const [el, mesh] of this.meshMap.entries()) {
190
- if (!activeElements.has(el)) {
191
- this.scene.remove(mesh);
192
-
193
- mesh.geometry.dispose();
194
- if (mesh.material instanceof THREE.Material) mesh.material.dispose();
195
- this.meshMap.delete(el);
196
- }
249
+ this.reconcileNode(graphNode);
250
+
251
+ if (pendingDeletions.size > 0) {
252
+ pendingDeletions.forEach((el) => {
253
+ const meshToDestroy = this.registry.get(el) as THREE.Mesh | undefined;
254
+ if (meshToDestroy) {
255
+ this.scene.remove(meshToDestroy);
256
+ for (const set of this.travelersByLayer) {
257
+ set.delete(meshToDestroy);
258
+ }
259
+ this.fixedMeshes.delete(meshToDestroy);
260
+ meshToDestroy.geometry.dispose();
261
+ if (meshToDestroy.userData.nativeMesh) {
262
+ this.scene.remove(meshToDestroy.userData.nativeMesh);
263
+ if (Array.isArray(meshToDestroy.userData.nativeMesh.material)) {
264
+ meshToDestroy.userData.nativeMesh.material.forEach((mat: THREE.Material) => mat.dispose());
265
+ } else {
266
+ meshToDestroy.userData.nativeMesh.material.dispose();
267
+ }
268
+ meshToDestroy.userData.nativeMesh.geometry.dispose();
269
+ }
270
+ meshToDestroy.traverse((child) => {
271
+ if (child instanceof THREE.Mesh) {
272
+ if (child.geometry) child.geometry.dispose();
273
+ if (child.material) {
274
+ if (Array.isArray(child.material)) {
275
+ child.material.forEach((mat) => mat.dispose());
276
+ } else {
277
+ child.material.dispose();
278
+ }
279
+ }
280
+ }
281
+ });
282
+ this.registry.remove(el);
283
+ this.textureManager.unregister(el);
284
+ }
285
+ });
197
286
  }
287
+
288
+ // mesh.geometry.dispose();
289
+ // if (mesh.material instanceof THREE.Material) mesh.material.dispose();
290
+ // this.meshMap.delete(el);
291
+ // }
292
+ // }
198
293
  }
199
294
 
200
- private reconcileNode(node: SceneNode, activeElements: Set<HTMLElement>) {
201
- activeElements.add(node.element);
295
+ // 탐색 후 완성된 Scene node 이용하여 mesh를 만들거나 조정
296
+ // => 이 과정에서 scene node에 있는 ele를 넣은 hash => activeElements
297
+ // => 이후 activeElements를 이용하여 mesh를 정리!!!+ map에서도 삭제
202
298
 
203
- let mesh = this.meshMap.get(node.element);
299
+ // private reconcileNode(node: SceneNode, activeElements: Set<HTMLElement>) {
300
+ private reconcileNode(node: SceneNode) {
301
+ let mesh = this.registry.get(node.element) as THREE.Mesh | undefined;
302
+
204
303
  if (!mesh) {
205
304
  const geometry = new THREE.PlaneGeometry(1, 1);
206
- let material: THREE.MeshBasicMaterial | THREE.Material;
207
- material = Painter.create(
305
+ const initialTexture = node.isTraveler
306
+ ? this.renderTargets[node.captureLayer - 2]?.texture
307
+ : this.textureManager.get(node.element);
308
+
309
+ const material = Painter.create(
208
310
  "BOX",
209
311
  node.styles,
210
312
  "",
211
313
  node.rect.width,
212
314
  node.rect.height,
213
315
  this.qualityFactor,
214
- node.isTraveler ? this.renderTarget?.texture : undefined,
215
- node.shaderHooks
316
+ initialTexture,
317
+ node.shaderHooks,
216
318
  );
319
+
217
320
  mesh = new THREE.Mesh(geometry, material);
321
+
218
322
  if (node.type === "TEXT") mesh.name = "BG_MESH";
219
323
  this.scene.add(mesh);
220
- this.meshMap.set(node.element, mesh);
324
+
325
+ this.registry.register(node.element, mesh);
326
+ mesh.userData.baseMaterial = material;
221
327
  }
222
328
 
223
- mesh.userData.domRect = node.rect;
329
+
330
+
331
+ // [Important] use whene mesh animating with js
224
332
 
225
333
  this.updateMeshProperties(mesh, node);
226
334
  this.updateMeshLayers(mesh, node);
227
- if (node.isTraveler) mesh.layers.enable(28);
335
+ if (node.isTraveler) {
336
+ for (let i = 0; i < ATTR_TRAVEL.MAX_LAYERS; i++) {
337
+ if (i === node.captureLayer - 2) {
338
+ this.travelersByLayer[i].add(mesh);
339
+ } else {
340
+ this.travelersByLayer[i].delete(mesh);
341
+ }
342
+ }
343
+ } else {
344
+ for (const set of this.travelersByLayer) {
345
+ set.delete(mesh);
346
+ }
347
+ }
348
+
349
+ if (node.isFixed) {
350
+ this.fixedMeshes.add(mesh);
351
+ } else {
352
+ this.fixedMeshes.delete(mesh);
353
+ }
354
+
355
+ if (node.styles.imageSrc) {
356
+ this.textureManager.register(node.element, node.styles.imageSrc);
357
+ } else {
358
+ this.textureManager.unregister(node.element);
359
+ }
228
360
 
229
361
  if (node.type === "BOX") {
230
362
  for (const child of node.children) {
231
- this.reconcileNode(child, activeElements);
363
+ this.reconcileNode(child);
232
364
  }
233
365
  } else if (node.type === "TEXT") {
234
- this.reconcileTextChild(mesh, node);
366
+ this.reconcileTextChild(mesh, node, false);
367
+ if (mesh.userData.nativeMesh && node.nativeStyles) {
368
+ this.reconcileTextChild(mesh.userData.nativeMesh as THREE.Mesh, node, true);
369
+ }
235
370
  }
236
371
  }
237
372
 
238
- private reconcileTextChild(parentMesh: THREE.Mesh, node: SceneNode) {
239
- let textMesh = parentMesh.children.find(
240
- (c) => c.name === "TEXT_CHILD",
241
- ) as THREE.Mesh;
242
-
243
- const currentStyleHash = JSON.stringify(node.textStyles);
244
- const cachedStyleHash = textMesh?.userData?.styleHash;
373
+ private reconcileTextChild(parentMesh: THREE.Mesh, node: SceneNode, isNative: boolean) {
374
+ const lines = node.textLines || [
375
+ { text: node.textContent || "", rect: node.rect },
376
+ ];
377
+
378
+ const stylesToUse = (isNative ? node.nativeStyles : node.textStyles) as TextStyles;
379
+ const currentStyleHash =
380
+ JSON.stringify(stylesToUse) +
381
+ node.textContent +
382
+ lines.map((l) => l.text).join("|");
383
+ const cachedStyleHash = parentMesh.userData?.textChildStyleHash;
245
384
  const isDirty =
246
- !textMesh ||
247
- node.dirtyMask & DIRTY_CONTENT ||
248
- currentStyleHash !== cachedStyleHash;
385
+ node.dirtyMask & DIRTY_CONTENT || currentStyleHash !== cachedStyleHash;
249
386
 
250
387
  if (isDirty) {
251
- if (textMesh) {
388
+ // Remove all existing TEXT_CHILD meshes
389
+ const existingChildren = parentMesh.children.filter((c) =>
390
+ c.name.startsWith("TEXT_CHILD"),
391
+ );
392
+ existingChildren.forEach((child) => {
393
+ const textMesh = child as THREE.Mesh;
252
394
  (textMesh.material as THREE.MeshBasicMaterial).map?.dispose();
253
395
  textMesh.geometry.dispose();
254
396
  parentMesh.remove(textMesh);
255
- }
397
+ });
256
398
 
257
- const material = Painter.create(
258
- "TEXT",
259
- node.textStyles!,
260
- node.textContent || "",
261
- node.rect.width,
262
- node.rect.height,
263
- this.qualityFactor,
264
- );
265
-
266
- const geometry = new THREE.PlaneGeometry(1, 1);
267
- textMesh = new THREE.Mesh(geometry, material);
268
-
269
- textMesh.name = "TEXT_CHILD";
270
- textMesh.userData = { styleHash: currentStyleHash };
271
- this.updateMeshLayers(textMesh, node);
272
- textMesh.position.z = 0.005;
273
- parentMesh.add(textMesh);
274
- }
275
-
276
- if (textMesh) {
277
- const parentRect = parentMesh.userData.domRect;
399
+ const parentRect = node.rect;
278
400
  const parentCenterX = parentRect.x + parentRect.width / 2;
279
401
  const parentCenterY = parentRect.y + parentRect.height / 2;
280
402
 
281
- const textCenterX = node.rect.x + node.rect.width / 2;
282
- const textCenterY = node.rect.y + node.rect.height / 2;
403
+ lines.forEach((line, index) => {
404
+ const material = Painter.create(
405
+ "TEXT",
406
+ stylesToUse,
407
+ line.text,
408
+ line.rect.width,
409
+ line.rect.height,
410
+ this.qualityFactor,
411
+ );
412
+
413
+ const geometry = new THREE.PlaneGeometry(1, 1);
414
+ const textMesh = new THREE.Mesh(geometry, material);
415
+
416
+ textMesh.name = `TEXT_CHILD_${index}`;
417
+
418
+ // Parent is already scaled to node.rect.width/height.
419
+ // We counter-scale the child so its absolute size is exactly line.rect.width/height.
420
+ const scaleX =
421
+ node.rect.width === 0 ? 1 : line.rect.width / node.rect.width;
422
+ const scaleY =
423
+ node.rect.height === 0 ? 1 : line.rect.height / node.rect.height;
424
+ textMesh.scale.set(scaleX, scaleY, 1);
425
+
426
+ const textCenterX = line.rect.x + line.rect.width / 2;
427
+ const textCenterY = line.rect.y + line.rect.height / 2;
428
+
429
+ const offsetX = textCenterX - parentCenterX;
430
+ const offsetY = -(textCenterY - parentCenterY);
431
+
432
+ // Position must also be counter-scaled relative to parent's scale
433
+ textMesh.position.set(
434
+ node.rect.width === 0 ? 0 : offsetX / node.rect.width,
435
+ node.rect.height === 0 ? 0 : offsetY / node.rect.height,
436
+ 0.005,
437
+ );
438
+
439
+ parentMesh.add(textMesh);
440
+ });
441
+
442
+ parentMesh.userData.textChildStyleHash = currentStyleHash;
443
+ }
283
444
 
284
- const offsetX = textCenterX - parentCenterX;
285
- const offsetY = -(textCenterY - parentCenterY);
445
+ // Always update layers for all text children, as visibility/nativeLayer can change without dirtifying the style
446
+ parentMesh.children.forEach((child) => {
447
+ if (!child.name.startsWith("TEXT_CHILD")) return;
448
+ const textMesh = child as THREE.Mesh;
449
+
450
+ const layerNum = node.visibility & USER_LAYER ? THREE_LAYERS.BASE : THREE_LAYERS.HIDDEN;
451
+ textMesh.layers.set(layerNum);
286
452
 
287
- textMesh.position.set(offsetX, offsetY, 0.005);
288
- }
453
+ if (node.visibility & SELECT_LAYER) {
454
+ textMesh.layers.enable(THREE_LAYERS.SELECTED);
455
+ }
456
+
457
+ if (node.visibility & USER_LAYER) {
458
+ if (!isNative && node.nativeLayer !== undefined && node.nativeStyles !== undefined) {
459
+ for (let i = node.captureLayer; i < node.nativeLayer; i++) {
460
+ textMesh.layers.enable(THREE_LAYERS.getCaptureLayer(i));
461
+ }
462
+ } else if (isNative && node.nativeLayer !== undefined) {
463
+ for (let i = Math.max(node.captureLayer, node.nativeLayer); i <= ATTR_TRAVEL.MAX_LAYERS + 1; i++) {
464
+ textMesh.layers.enable(THREE_LAYERS.getCaptureLayer(i));
465
+ }
466
+ } else {
467
+ for (let i = node.captureLayer; i <= ATTR_TRAVEL.MAX_LAYERS + 1; i++) {
468
+ textMesh.layers.enable(THREE_LAYERS.getCaptureLayer(i));
469
+ }
470
+ }
471
+ }
472
+ });
289
473
  }
290
474
 
291
475
  private updateMeshProperties(mesh: THREE.Mesh, node: SceneNode) {
@@ -294,9 +478,16 @@ export class Renderer {
294
478
  const pixelRatio = this.renderer.getPixelRatio();
295
479
  const canvasWidth = this.renderer.domElement.width / pixelRatio;
296
480
  const canvasHeight = this.renderer.domElement.height / pixelRatio;
297
-
481
+
482
+ mesh.material = mesh.userData.baseMaterial as THREE.Material;
298
483
  mesh.scale.set(rect.width, rect.height, 1);
299
484
 
485
+ mesh.userData.domRect = {
486
+ ...rect,
487
+ width: rect.width,
488
+ height: rect.height,
489
+ };
490
+
300
491
  const Z_MICRO_OFFSET = 0.001;
301
492
  this.renderOrder++;
302
493
 
@@ -306,50 +497,168 @@ export class Renderer {
306
497
  const localX = rect.x - targetPageX;
307
498
  const localY = rect.y - targetPageY;
308
499
 
500
+ const baseX = localX - canvasWidth / 2 + rect.width / 2;
501
+ const baseY = -localY + canvasHeight / 2 - rect.height / 2;
502
+
309
503
  mesh.position.set(
310
- localX - canvasWidth / 2 + rect.width / 2,
311
- -localY + canvasHeight / 2 - rect.height / 2,
504
+ baseX,
505
+ baseY,
312
506
  styles.zIndex + this.renderOrder * Z_MICRO_OFFSET,
313
507
  );
508
+ const pureDOM_X = rect.x; // 트랜스폼 오염 전 순수 좌표 (가정)
509
+ const pureDOM_Y = rect.y;
510
+
511
+ mesh.userData.basePosition = { x: baseX, y: baseY };
512
+ mesh.userData.originalBasePosition = { x: baseX, y: baseY };
513
+ mesh.userData.baseSize = { width: rect.width, height: rect.height };
514
+
515
+ mesh.userData.baseDOM = { x: pureDOM_X, y: pureDOM_Y };
516
+ mesh.userData.isFixed = node.isFixed;
517
+ mesh.userData.initialScroll = { x: window.scrollX, y: window.scrollY };
518
+
519
+ const targetEl =
520
+ node.element.nodeType === Node.TEXT_NODE
521
+ ? node.element.parentElement!
522
+ : node.element;
523
+ const computed = window.getComputedStyle(targetEl);
524
+ let baseTx = 0,
525
+ baseTy = 0;
526
+ if (computed.transform && computed.transform !== "none") {
527
+ const matrix = new DOMMatrix(computed.transform);
528
+ baseTx = matrix.m41;
529
+ baseTy = matrix.m42;
530
+ }
531
+ mesh.userData.baseTransform = { x: baseTx, y: baseTy };
532
+
533
+ delete mesh.userData.originRatioX;
534
+ delete mesh.userData.originRatioY;
535
+ // mesh.position.set(
536
+ // localX - canvasWidth / 2 + rect.width / 2,
537
+ // -localY + canvasHeight / 2 - rect.height / 2,
538
+ // styles.zIndex + this.renderOrder * Z_MICRO_OFFSET,
539
+ // );
314
540
  Painter.update(
315
- mesh.material as THREE.Material,
541
+ mesh.userData.baseMaterial,
316
542
  "BOX",
317
- node.styles,
543
+ styles,
318
544
  "",
319
- node.rect.width,
320
- node.rect.height,
545
+ rect.width,
546
+ rect.height,
321
547
  this.qualityFactor,
322
- node.isTraveler ? this.renderTarget?.texture : undefined,
548
+ node.isTraveler
549
+ ? this.renderTargets[node.captureLayer - 2]?.texture
550
+ : this.textureManager.get(node.element),
323
551
  );
552
+
553
+ if (node.nativeStyles && node.nativeRect) {
554
+ if (!mesh.userData.nativeMesh) {
555
+ const nativeMaterial = Painter.create(
556
+ "BOX",
557
+ node.nativeStyles,
558
+ "",
559
+ node.nativeRect.width,
560
+ node.nativeRect.height,
561
+ this.qualityFactor,
562
+ node.isTraveler
563
+ ? this.renderTargets[node.captureLayer - 2]?.texture
564
+ : this.textureManager.get(node.element),
565
+ node.shaderHooks,
566
+ );
567
+ const nativeMesh = new THREE.Mesh(mesh.geometry, nativeMaterial);
568
+ if (node.type === "TEXT") nativeMesh.name = "BG_MESH";
569
+ this.scene.add(nativeMesh);
570
+ mesh.userData.nativeMesh = nativeMesh;
571
+ }
572
+
573
+ const nativeMesh = mesh.userData.nativeMesh as THREE.Mesh;
574
+ const nativeLocalX = node.nativeRect.x - targetPageX;
575
+ const nativeLocalY = node.nativeRect.y - targetPageY;
576
+ const nativeBaseX = nativeLocalX - canvasWidth / 2 + node.nativeRect.width / 2;
577
+ const nativeBaseY = -nativeLocalY + canvasHeight / 2 - node.nativeRect.height / 2;
578
+
579
+ nativeMesh.scale.set(node.nativeRect.width, node.nativeRect.height, 1);
580
+ nativeMesh.position.set(
581
+ nativeBaseX,
582
+ nativeBaseY,
583
+ (node.nativeStyles as BoxStyles).zIndex + this.renderOrder * Z_MICRO_OFFSET,
584
+ );
585
+
586
+ Painter.update(
587
+ nativeMesh.material as THREE.Material,
588
+ "BOX",
589
+ node.nativeStyles,
590
+ "",
591
+ node.nativeRect.width,
592
+ node.nativeRect.height,
593
+ this.qualityFactor,
594
+ node.isTraveler
595
+ ? this.renderTargets[node.captureLayer - 2]?.texture
596
+ : this.textureManager.get(node.element),
597
+ );
598
+ } else {
599
+ if (mesh.userData.nativeMesh) {
600
+ this.scene.remove(mesh.userData.nativeMesh);
601
+ if (mesh.userData.nativeMesh.material instanceof THREE.Material) {
602
+ mesh.userData.nativeMesh.material.dispose();
603
+ }
604
+ delete mesh.userData.nativeMesh;
605
+ }
606
+ }
324
607
  }
325
608
 
326
609
  private updateMeshLayers(mesh: THREE.Mesh, node: SceneNode) {
327
- const layerNum = (1 - (node.visibility & USER_LAYER)) * 30;
610
+ const layerNum =
611
+ node.visibility & USER_LAYER ? THREE_LAYERS.BASE : THREE_LAYERS.HIDDEN;
328
612
  mesh.layers.set(layerNum);
329
- if (node.visibility === (USER_LAYER | SYSTEM_LAYER)) mesh.layers.enable(29);
330
- }
331
613
 
332
- private captureRenderTarget() {
333
- const travelers: THREE.Mesh[] = [];
334
- for (const mesh of this.meshMap.values()) {
335
- if ((mesh.layers.mask & (1 << 28)) !== 0) {
336
- travelers.push(mesh);
614
+ if (node.visibility & SELECT_LAYER) {
615
+ mesh.layers.enable(THREE_LAYERS.SELECTED);
616
+ }
617
+
618
+ if (mesh.userData.nativeMesh && node.nativeLayer !== undefined) {
619
+ const nativeMesh = mesh.userData.nativeMesh as THREE.Mesh;
620
+ nativeMesh.layers.set(THREE_LAYERS.HIDDEN);
621
+
622
+ if (node.visibility & USER_LAYER) {
623
+ for (let i = node.captureLayer; i < node.nativeLayer; i++) {
624
+ mesh.layers.enable(THREE_LAYERS.getCaptureLayer(i));
625
+ }
626
+ for (let i = Math.max(node.captureLayer, node.nativeLayer); i <= ATTR_TRAVEL.MAX_LAYERS + 1; i++) {
627
+ nativeMesh.layers.enable(THREE_LAYERS.getCaptureLayer(i));
628
+ }
629
+ }
630
+ } else {
631
+ if (node.visibility & USER_LAYER) {
632
+ for (let i = node.captureLayer; i <= ATTR_TRAVEL.MAX_LAYERS + 1; i++) {
633
+ mesh.layers.enable(THREE_LAYERS.getCaptureLayer(i));
634
+ }
337
635
  }
338
636
  }
339
- if (travelers.length === 0) return;
637
+ }
638
+
639
+ private captureRenderTarget(
640
+ travelers: Set<THREE.Mesh>,
641
+ targetLayer: number,
642
+ renderTarget: THREE.WebGLRenderTarget | null,
643
+ ) {
644
+ // [Problem] this method called on requestAnimationFrame
645
+ // => this logic travers all meshes every frame, need optimization
646
+
647
+ // if (travelers.length === 0) return;
648
+ if (travelers.size === 0 || !renderTarget) return;
340
649
 
341
650
  const oldClearColor = new THREE.Color();
342
651
  const oldClearAlpha = this.renderer.getClearAlpha();
343
652
  this.renderer.getClearColor(oldClearColor);
344
653
 
345
654
  this.renderer.setClearColor(0x000000, 0);
346
- this.renderer.setRenderTarget(this.renderTarget);
655
+ this.renderer.setRenderTarget(renderTarget);
347
656
 
348
657
  this.renderer.clear();
349
658
 
350
659
  this.renderer.autoClear = false;
351
660
  this.renderer.setScissorTest(true);
352
- this.camera.layers.set(29);
661
+ this.camera.layers.set(targetLayer);
353
662
 
354
663
  const vector = new THREE.Vector3();
355
664
  const canvasWidth = this.targetRect.width;
@@ -392,35 +701,23 @@ export class Renderer {
392
701
  this.renderer.setScissorTest(false);
393
702
  this.renderer.autoClear = true;
394
703
  this.renderer.setRenderTarget(null);
395
- this.camera.layers.set(28);
704
+ this.camera.layers.set(this.getSceneLayer());
396
705
  this.renderer.setClearColor(oldClearColor, oldClearAlpha);
397
706
  }
398
707
 
399
708
  public render() {
400
- if (this.renderTarget) this.captureRenderTarget();
401
- this.renderer.render(this.scene, this.camera);
402
- }
403
-
404
- // for debugging
405
- public showScissoredRenderTarget() {
406
- if (!this.renderTarget) return;
407
-
408
- const w = this.targetRect.width;
409
- const h = this.targetRect.height;
410
-
411
- const geometry = new THREE.PlaneGeometry(w, h);
412
- const material = new THREE.MeshBasicMaterial({
413
- map: this.renderTarget.texture,
414
- side: THREE.DoubleSide,
415
- transparent: true,
416
- opacity: 0.8,
417
- });
418
-
419
- const debugMesh = new THREE.Mesh(geometry, material);
709
+ for (let i = 0; i < ATTR_TRAVEL.MAX_LAYERS; i++) {
710
+ const currentLayer = i + 1;
420
711
 
421
- debugMesh.position.set(0, 0, 90);
422
- debugMesh.layers.set(28);
712
+ this.captureRenderTarget(
713
+ this.travelersByLayer[i],
714
+ THREE_LAYERS.getCaptureLayer(currentLayer),
715
+ this.renderTargets[i],
716
+ );
717
+ }
423
718
 
424
- this.scene.add(debugMesh);
719
+ this.renderer.render(this.scene, this.camera);
425
720
  }
721
+
722
+
426
723
  }