@mirage-engine/core 0.2.2 → 0.3.1

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,10 +6,13 @@ 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";
13
16
  import { MeshRegistry } from "../store/MeshRegistry";
14
17
  import { TextureLifecycleManager } from "../store/TextureLifecycleManager";
15
18
 
@@ -18,18 +21,22 @@ export class Renderer {
18
21
  private readonly scene: THREE.Scene;
19
22
  private readonly camera: THREE.OrthographicCamera;
20
23
  private readonly renderer: THREE.WebGLRenderer;
21
- private renderTarget: THREE.WebGLRenderTarget | null = null;
24
+ private renderTargets: THREE.WebGLRenderTarget[] = [];
22
25
  private renderOrder: number = 0;
23
- private qualityFactor: number = 2;
26
+ public qualityFactor: number = 2;
24
27
  private mode: MirageMode = "overlay";
25
28
  private clipArea: travelerClipArea = 1;
29
+ public targetLayer: number | LayerTarget = "base";
26
30
 
27
31
  private target: HTMLElement;
28
32
  private mountContainer: HTMLElement;
29
33
  private registry: MeshRegistry;
30
34
  private targetRect: DOMRect;
31
35
 
32
- private travelers: Set<THREE.Mesh> = new Set();
36
+ private travelersByLayer: Set<THREE.Mesh>[] = Array.from(
37
+ { length: ATTR_TRAVEL.MAX_LAYERS },
38
+ () => new Set(),
39
+ );
33
40
  private textureManager: TextureLifecycleManager;
34
41
  // private meshMap: Map<HTMLElement, THREE.Mesh> = new Map();
35
42
 
@@ -44,7 +51,7 @@ export class Renderer {
44
51
  this.target = target;
45
52
  this.mountContainer = mountContainer;
46
53
  this.registry = registry;
47
-
54
+
48
55
  this.textureManager = new TextureLifecycleManager((el, texture) => {
49
56
  const mesh = this.registry.get(el);
50
57
  if (mesh && mesh.material instanceof THREE.ShaderMaterial) {
@@ -54,6 +61,7 @@ export class Renderer {
54
61
 
55
62
  this.mode = config.mode ?? "overlay";
56
63
  this.clipArea = config.travelerClipArea ?? 1;
64
+ this.targetLayer = config.layer ?? "base";
57
65
  this.canvas = document.createElement("canvas");
58
66
  this.scene = new THREE.Scene();
59
67
 
@@ -74,8 +82,7 @@ export class Renderer {
74
82
  1000,
75
83
  );
76
84
  this.camera.position.z = 100;
77
- this.camera.layers.set(0);
78
-
85
+ this.camera.layers.set(this.getSceneLayer());
79
86
 
80
87
  this.renderer = new THREE.WebGLRenderer({
81
88
  canvas: this.canvas,
@@ -85,29 +92,41 @@ export class Renderer {
85
92
  // premultipliedAlpha: true
86
93
  });
87
94
 
88
-
89
95
  THREE.ColorManagement.enabled = false;
90
96
  this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
91
97
 
92
-
93
98
  this.renderer.setPixelRatio(window.devicePixelRatio);
94
99
  this.renderer.setSize(width, height);
95
100
 
96
101
  this.applyTextQuality(config.quality ?? "medium");
97
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
+
98
114
  public createRenderTarget() {
99
- this.renderTarget = new THREE.WebGLRenderTarget(
100
- this.targetRect.width * this.qualityFactor,
101
- this.targetRect.height * this.qualityFactor,
102
- {
103
- minFilter: THREE.LinearFilter,
104
- magFilter: THREE.LinearFilter,
105
- format: THREE.RGBAFormat,
106
- stencilBuffer: false,
107
- depthBuffer: true,
108
- // samples: 0.7,
109
- },
110
- );
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
+ }
111
130
  }
112
131
 
113
132
  private applyTextQuality(quality: Quality) {
@@ -123,6 +142,9 @@ export class Renderer {
123
142
  this.qualityFactor = 4;
124
143
  break;
125
144
  case "medium":
145
+ // this.qualityFactor = 2;
146
+ this.qualityFactor = 2;
147
+ break;
126
148
  default:
127
149
  this.qualityFactor = 2;
128
150
  break;
@@ -154,6 +176,31 @@ export class Renderer {
154
176
  }
155
177
  }
156
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
+
157
204
  public dispose() {
158
205
  this.renderer.dispose();
159
206
  this.canvas.remove();
@@ -163,11 +210,8 @@ export class Renderer {
163
210
 
164
211
  public setSize(width: number, height: number) {
165
212
  this.renderer.setSize(width, height);
166
- if (this.renderTarget) {
167
- this.renderTarget.setSize(
168
- width * this.qualityFactor,
169
- height * this.qualityFactor,
170
- );
213
+ for (const target of this.renderTargets) {
214
+ target.setSize(width * this.qualityFactor, height * this.qualityFactor);
171
215
  }
172
216
  this.camera.left = width / -2;
173
217
  this.camera.right = width / 2;
@@ -202,9 +246,6 @@ export class Renderer {
202
246
 
203
247
  this.renderOrder = 0;
204
248
 
205
- // const activeElements = new Set<HTMLElement>();
206
-
207
- // this.reconcileNode(graphNode, activeElements);
208
249
  this.reconcileNode(graphNode);
209
250
 
210
251
  if (pendingDeletions.size > 0) {
@@ -212,9 +253,20 @@ export class Renderer {
212
253
  const meshToDestroy = this.registry.get(el) as THREE.Mesh | undefined;
213
254
  if (meshToDestroy) {
214
255
  this.scene.remove(meshToDestroy);
215
- this.travelers.delete(meshToDestroy);
256
+ for (const set of this.travelersByLayer) {
257
+ set.delete(meshToDestroy);
258
+ }
216
259
  this.fixedMeshes.delete(meshToDestroy);
217
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
+ }
218
270
  meshToDestroy.traverse((child) => {
219
271
  if (child instanceof THREE.Mesh) {
220
272
  if (child.geometry) child.geometry.dispose();
@@ -246,16 +298,15 @@ export class Renderer {
246
298
 
247
299
  // private reconcileNode(node: SceneNode, activeElements: Set<HTMLElement>) {
248
300
  private reconcileNode(node: SceneNode) {
249
- // activeElements.add(node.element);
250
-
251
- // let mesh = this.meshMap.get(node.element);
252
301
  let mesh = this.registry.get(node.element) as THREE.Mesh | undefined;
302
+
253
303
  if (!mesh) {
254
304
  const geometry = new THREE.PlaneGeometry(1, 1);
255
- let material: THREE.MeshBasicMaterial | THREE.Material;
256
- const initialTexture = node.isTraveler ? this.renderTarget?.texture : this.textureManager.get(node.element);
305
+ const initialTexture = node.isTraveler
306
+ ? this.renderTargets[node.captureLayer - 2]?.texture
307
+ : this.textureManager.get(node.element);
257
308
 
258
- material = Painter.create(
309
+ const material = Painter.create(
259
310
  "BOX",
260
311
  node.styles,
261
312
  "",
@@ -265,23 +316,34 @@ export class Renderer {
265
316
  initialTexture,
266
317
  node.shaderHooks,
267
318
  );
319
+
268
320
  mesh = new THREE.Mesh(geometry, material);
321
+
269
322
  if (node.type === "TEXT") mesh.name = "BG_MESH";
270
323
  this.scene.add(mesh);
324
+
271
325
  this.registry.register(node.element, mesh);
326
+ mesh.userData.baseMaterial = material;
272
327
  }
273
328
 
329
+
330
+
274
331
  // [Important] use whene mesh animating with js
275
- mesh.userData.domRect = node.rect;
276
332
 
277
333
  this.updateMeshProperties(mesh, node);
278
334
  this.updateMeshLayers(mesh, node);
279
335
  if (node.isTraveler) {
280
- mesh.layers.enable(28);
281
- this.travelers.add(mesh);
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
+ }
282
343
  } else {
283
- mesh.layers.disable(28);
284
- this.travelers.delete(mesh);
344
+ for (const set of this.travelersByLayer) {
345
+ set.delete(mesh);
346
+ }
285
347
  }
286
348
 
287
349
  if (node.isFixed) {
@@ -301,22 +363,33 @@ export class Renderer {
301
363
  this.reconcileNode(child);
302
364
  }
303
365
  } else if (node.type === "TEXT") {
304
- 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
+ }
305
370
  }
306
371
  }
307
372
 
308
- private reconcileTextChild(parentMesh: THREE.Mesh, node: SceneNode) {
309
- const lines = node.textLines || [{ text: node.textContent || "", rect: node.rect }];
310
- const currentStyleHash = JSON.stringify(node.textStyles) + node.textContent + lines.map(l => l.text).join("|");
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("|");
311
383
  const cachedStyleHash = parentMesh.userData?.textChildStyleHash;
312
384
  const isDirty =
313
- node.dirtyMask & DIRTY_CONTENT ||
314
- currentStyleHash !== cachedStyleHash;
385
+ node.dirtyMask & DIRTY_CONTENT || currentStyleHash !== cachedStyleHash;
315
386
 
316
387
  if (isDirty) {
317
388
  // Remove all existing TEXT_CHILD meshes
318
- const existingChildren = parentMesh.children.filter(c => c.name.startsWith("TEXT_CHILD"));
319
- existingChildren.forEach(child => {
389
+ const existingChildren = parentMesh.children.filter((c) =>
390
+ c.name.startsWith("TEXT_CHILD"),
391
+ );
392
+ existingChildren.forEach((child) => {
320
393
  const textMesh = child as THREE.Mesh;
321
394
  (textMesh.material as THREE.MeshBasicMaterial).map?.dispose();
322
395
  textMesh.geometry.dispose();
@@ -330,7 +403,7 @@ export class Renderer {
330
403
  lines.forEach((line, index) => {
331
404
  const material = Painter.create(
332
405
  "TEXT",
333
- node.textStyles!,
406
+ stylesToUse,
334
407
  line.text,
335
408
  line.rect.width,
336
409
  line.rect.height,
@@ -341,12 +414,13 @@ export class Renderer {
341
414
  const textMesh = new THREE.Mesh(geometry, material);
342
415
 
343
416
  textMesh.name = `TEXT_CHILD_${index}`;
344
- this.updateMeshLayers(textMesh, node);
345
417
 
346
- // Parent is already scaled to node.rect.width/height.
418
+ // Parent is already scaled to node.rect.width/height.
347
419
  // We counter-scale the child so its absolute size is exactly line.rect.width/height.
348
- const scaleX = node.rect.width === 0 ? 1 : line.rect.width / node.rect.width;
349
- const scaleY = node.rect.height === 0 ? 1 : line.rect.height / node.rect.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;
350
424
  textMesh.scale.set(scaleX, scaleY, 1);
351
425
 
352
426
  const textCenterX = line.rect.x + line.rect.width / 2;
@@ -359,7 +433,7 @@ export class Renderer {
359
433
  textMesh.position.set(
360
434
  node.rect.width === 0 ? 0 : offsetX / node.rect.width,
361
435
  node.rect.height === 0 ? 0 : offsetY / node.rect.height,
362
- 0.005
436
+ 0.005,
363
437
  );
364
438
 
365
439
  parentMesh.add(textMesh);
@@ -367,6 +441,35 @@ export class Renderer {
367
441
 
368
442
  parentMesh.userData.textChildStyleHash = currentStyleHash;
369
443
  }
444
+
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);
452
+
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
+ });
370
473
  }
371
474
 
372
475
  private updateMeshProperties(mesh: THREE.Mesh, node: SceneNode) {
@@ -375,9 +478,16 @@ export class Renderer {
375
478
  const pixelRatio = this.renderer.getPixelRatio();
376
479
  const canvasWidth = this.renderer.domElement.width / pixelRatio;
377
480
  const canvasHeight = this.renderer.domElement.height / pixelRatio;
378
-
481
+
482
+ mesh.material = mesh.userData.baseMaterial as THREE.Material;
379
483
  mesh.scale.set(rect.width, rect.height, 1);
380
484
 
485
+ mesh.userData.domRect = {
486
+ ...rect,
487
+ width: rect.width,
488
+ height: rect.height,
489
+ };
490
+
381
491
  const Z_MICRO_OFFSET = 0.001;
382
492
  this.renderOrder++;
383
493
 
@@ -401,15 +511,18 @@ export class Renderer {
401
511
  mesh.userData.basePosition = { x: baseX, y: baseY };
402
512
  mesh.userData.originalBasePosition = { x: baseX, y: baseY };
403
513
  mesh.userData.baseSize = { width: rect.width, height: rect.height };
404
- // ✨ 추가: 역산을 위한 순수 DOM 초기 좌표 저장
514
+
405
515
  mesh.userData.baseDOM = { x: pureDOM_X, y: pureDOM_Y };
406
516
  mesh.userData.isFixed = node.isFixed;
407
517
  mesh.userData.initialScroll = { x: window.scrollX, y: window.scrollY };
408
518
 
409
- // 추가: 초기 transform 상태를 저장하여 애니메이션 시 delta 값만 적용하도록 함 (이중 적용 방지)
410
- const targetEl = node.element.nodeType === Node.TEXT_NODE ? node.element.parentElement! : node.element;
519
+ const targetEl =
520
+ node.element.nodeType === Node.TEXT_NODE
521
+ ? node.element.parentElement!
522
+ : node.element;
411
523
  const computed = window.getComputedStyle(targetEl);
412
- let baseTx = 0, baseTy = 0;
524
+ let baseTx = 0,
525
+ baseTy = 0;
413
526
  if (computed.transform && computed.transform !== "none") {
414
527
  const matrix = new DOMMatrix(computed.transform);
415
528
  baseTx = matrix.m41;
@@ -417,7 +530,6 @@ export class Renderer {
417
530
  }
418
531
  mesh.userData.baseTransform = { x: baseTx, y: baseTy };
419
532
 
420
- // ✨ 추가: 애니메이션이 끝나고 씬이 갱신되면 비율 캐시 초기화!
421
533
  delete mesh.userData.originRatioX;
422
534
  delete mesh.userData.originRatioY;
423
535
  // mesh.position.set(
@@ -426,42 +538,127 @@ export class Renderer {
426
538
  // styles.zIndex + this.renderOrder * Z_MICRO_OFFSET,
427
539
  // );
428
540
  Painter.update(
429
- mesh.material as THREE.Material,
541
+ mesh.userData.baseMaterial,
430
542
  "BOX",
431
- node.styles,
543
+ styles,
432
544
  "",
433
- node.rect.width,
434
- node.rect.height,
545
+ rect.width,
546
+ rect.height,
435
547
  this.qualityFactor,
436
- node.isTraveler ? this.renderTarget?.texture : this.textureManager.get(node.element),
548
+ node.isTraveler
549
+ ? this.renderTargets[node.captureLayer - 2]?.texture
550
+ : this.textureManager.get(node.element),
437
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
+ }
438
607
  }
439
608
 
440
609
  private updateMeshLayers(mesh: THREE.Mesh, node: SceneNode) {
441
- const layerNum = (1 - (node.visibility & USER_LAYER)) * 30;
610
+ const layerNum =
611
+ node.visibility & USER_LAYER ? THREE_LAYERS.BASE : THREE_LAYERS.HIDDEN;
442
612
  mesh.layers.set(layerNum);
443
- if (node.visibility === (USER_LAYER | SYSTEM_LAYER)) mesh.layers.enable(29);
613
+
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
+ }
635
+ }
636
+ }
444
637
  }
445
638
 
446
- private captureRenderTarget() {
639
+ private captureRenderTarget(
640
+ travelers: Set<THREE.Mesh>,
641
+ targetLayer: number,
642
+ renderTarget: THREE.WebGLRenderTarget | null,
643
+ ) {
447
644
  // [Problem] this method called on requestAnimationFrame
448
645
  // => this logic travers all meshes every frame, need optimization
449
646
 
450
647
  // if (travelers.length === 0) return;
451
- if (this.travelers.size === 0) return;
648
+ if (travelers.size === 0 || !renderTarget) return;
452
649
 
453
650
  const oldClearColor = new THREE.Color();
454
651
  const oldClearAlpha = this.renderer.getClearAlpha();
455
652
  this.renderer.getClearColor(oldClearColor);
456
653
 
457
654
  this.renderer.setClearColor(0x000000, 0);
458
- this.renderer.setRenderTarget(this.renderTarget);
655
+ this.renderer.setRenderTarget(renderTarget);
459
656
 
460
657
  this.renderer.clear();
461
658
 
462
659
  this.renderer.autoClear = false;
463
660
  this.renderer.setScissorTest(true);
464
- this.camera.layers.set(29);
661
+ this.camera.layers.set(targetLayer);
465
662
 
466
663
  const vector = new THREE.Vector3();
467
664
  const canvasWidth = this.targetRect.width;
@@ -469,7 +666,7 @@ export class Renderer {
469
666
 
470
667
  const pixelRatio = this.renderer.getPixelRatio();
471
668
 
472
- for (const traveler of this.travelers) {
669
+ for (const traveler of travelers) {
473
670
  vector.setFromMatrixPosition(traveler.matrixWorld);
474
671
  vector.project(this.camera);
475
672
 
@@ -504,35 +701,23 @@ export class Renderer {
504
701
  this.renderer.setScissorTest(false);
505
702
  this.renderer.autoClear = true;
506
703
  this.renderer.setRenderTarget(null);
507
- this.camera.layers.set(0);
704
+ this.camera.layers.set(this.getSceneLayer());
508
705
  this.renderer.setClearColor(oldClearColor, oldClearAlpha);
509
706
  }
510
707
 
511
708
  public render() {
512
- if (this.renderTarget) this.captureRenderTarget();
513
- this.renderer.render(this.scene, this.camera);
514
- }
515
-
516
- // for debugging
517
- public showScissoredRenderTarget() {
518
- if (!this.renderTarget) return;
519
-
520
- const w = this.targetRect.width;
521
- const h = this.targetRect.height;
522
-
523
- const geometry = new THREE.PlaneGeometry(w, h);
524
- const material = new THREE.MeshBasicMaterial({
525
- map: this.renderTarget.texture,
526
- side: THREE.DoubleSide,
527
- transparent: true,
528
- opacity: 0.8,
529
- });
709
+ for (let i = 0; i < ATTR_TRAVEL.MAX_LAYERS; i++) {
710
+ const currentLayer = i + 1;
530
711
 
531
- const debugMesh = new THREE.Mesh(geometry, material);
532
-
533
- debugMesh.position.set(0, 0, 90);
534
- debugMesh.layers.set(28);
712
+ this.captureRenderTarget(
713
+ this.travelersByLayer[i],
714
+ THREE_LAYERS.getCaptureLayer(currentLayer),
715
+ this.renderTargets[i],
716
+ );
717
+ }
535
718
 
536
- this.scene.add(debugMesh);
719
+ this.renderer.render(this.scene, this.camera);
537
720
  }
721
+
722
+
538
723
  }
@@ -58,22 +58,37 @@ export class TextureLifecycleManager {
58
58
  this.loadStatus.set(element, true);
59
59
 
60
60
  try {
61
- const response = await fetch(url);
62
- const blob = await response.blob();
63
- const bitmap = await createImageBitmap(blob, { imageOrientation: "flipY" });
61
+ let textureImage: ImageBitmap | HTMLImageElement;
62
+
63
+ if (url.startsWith("data:image/svg+xml")) {
64
+ textureImage = await new Promise<HTMLImageElement>((resolve, reject) => {
65
+ const img = new Image();
66
+ img.onload = () => resolve(img);
67
+ img.onerror = reject;
68
+ img.src = url;
69
+ });
70
+ } else {
71
+ const response = await fetch(url);
72
+ const blob = await response.blob();
73
+ textureImage = await createImageBitmap(blob, { imageOrientation: "flipY" });
74
+ }
64
75
 
65
76
  // Check if URL changed or element was unregistered while decoding
66
77
  if (this.elementUrls.get(element) !== url) {
67
- bitmap.close();
78
+ if ('close' in textureImage) textureImage.close();
68
79
  return;
69
80
  }
70
81
 
71
- // Check if element is no longer intersecting (debounced unobserve might have fired)
72
- // Actually we just check if it's still observed and has the URL.
73
- // A more robust check for intersection state might be needed, but for now we rely on disposeTexture.
74
-
75
- const texture = new THREE.CanvasTexture(bitmap);
82
+ const texture = new THREE.Texture(textureImage);
83
+ // createImageBitmap already flips Y, but HTMLImageElement relies on THREE.js flipY
84
+ if (!(textureImage instanceof HTMLImageElement)) {
85
+ texture.flipY = false;
86
+ }
87
+
88
+ // Inherit CanvasTexture defaults or set them explicitly if needed
89
+ texture.colorSpace = THREE.LinearSRGBColorSpace; // or adjust according to your pipeline
76
90
  texture.needsUpdate = true;
91
+
77
92
  this.textures.set(element, texture);
78
93
  this.onUpdate(element, texture);
79
94
  } catch (e) {