@needle-tools/engine 4.10.5-next.a5d5bf4 → 4.11.0-beta

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{needle-engine.bundle-D56E0HeK.min.js → needle-engine.bundle-BJ2hrLWW.min.js} +130 -130
  4. package/dist/{needle-engine.bundle-B2qX4saI.js → needle-engine.bundle-DZS0TuER.js} +5974 -5826
  5. package/dist/{needle-engine.bundle-DPHrCUDs.umd.cjs → needle-engine.bundle-JCl0_y_J.umd.cjs} +133 -133
  6. package/dist/needle-engine.d.ts +14 -0
  7. package/dist/needle-engine.js +321 -320
  8. package/dist/needle-engine.min.js +1 -1
  9. package/dist/needle-engine.umd.cjs +1 -1
  10. package/lib/engine/codegen/register_types.js +2 -0
  11. package/lib/engine/codegen/register_types.js.map +1 -1
  12. package/lib/engine/engine_gizmos.js +2 -2
  13. package/lib/engine/engine_gizmos.js.map +1 -1
  14. package/lib/engine/engine_physics.js +19 -12
  15. package/lib/engine/engine_physics.js.map +1 -1
  16. package/lib/engine/js-extensions/Object3D.d.ts +14 -0
  17. package/lib/engine/js-extensions/Object3D.js +13 -0
  18. package/lib/engine/js-extensions/Object3D.js.map +1 -1
  19. package/lib/engine-components/Renderer.js +33 -32
  20. package/lib/engine-components/Renderer.js.map +1 -1
  21. package/lib/engine-components/RendererLightmap.d.ts +7 -5
  22. package/lib/engine-components/RendererLightmap.js +29 -30
  23. package/lib/engine-components/RendererLightmap.js.map +1 -1
  24. package/lib/engine-components/SeeThrough.d.ts +70 -0
  25. package/lib/engine-components/SeeThrough.js +223 -0
  26. package/lib/engine-components/SeeThrough.js.map +1 -0
  27. package/lib/engine-components/codegen/components.d.ts +1 -0
  28. package/lib/engine-components/codegen/components.js +1 -0
  29. package/lib/engine-components/codegen/components.js.map +1 -1
  30. package/lib/engine-components/ui/Graphic.js +13 -1
  31. package/lib/engine-components/ui/Graphic.js.map +1 -1
  32. package/lib/engine-components/ui/RaycastUtils.js +5 -3
  33. package/lib/engine-components/ui/RaycastUtils.js.map +1 -1
  34. package/lib/engine-components/utils/LookAt.js +4 -2
  35. package/lib/engine-components/utils/LookAt.js.map +1 -1
  36. package/lib/engine-components/web/Clickthrough.d.ts +2 -1
  37. package/lib/engine-components/web/Clickthrough.js +2 -1
  38. package/lib/engine-components/web/Clickthrough.js.map +1 -1
  39. package/lib/engine-components/web/CursorFollow.d.ts +7 -1
  40. package/lib/engine-components/web/CursorFollow.js +35 -2
  41. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  42. package/package.json +2 -2
  43. package/src/engine/codegen/register_types.ts +2 -0
  44. package/src/engine/engine_gizmos.ts +3 -3
  45. package/src/engine/engine_physics.ts +24 -12
  46. package/src/engine/js-extensions/Object3D.ts +32 -0
  47. package/src/engine-components/Renderer.ts +38 -37
  48. package/src/engine-components/RendererLightmap.ts +31 -33
  49. package/src/engine-components/SeeThrough.ts +256 -0
  50. package/src/engine-components/codegen/components.ts +1 -0
  51. package/src/engine-components/ui/Graphic.ts +13 -1
  52. package/src/engine-components/ui/RaycastUtils.ts +9 -8
  53. package/src/engine-components/utils/LookAt.ts +4 -1
  54. package/src/engine-components/web/Clickthrough.ts +2 -1
  55. package/src/engine-components/web/CursorFollow.ts +48 -7
@@ -1,4 +1,4 @@
1
- import { AxesHelper, Box3, BoxGeometry, BufferAttribute, BufferGeometry, Color, type ColorRepresentation, CylinderGeometry, EdgesGeometry, Line, LineBasicMaterial, LineSegments, Material,Matrix4, Mesh, MeshBasicMaterial, Object3D, Quaternion, SphereGeometry, Vector3 } from 'three';
1
+ import { AxesHelper, Box3, BoxGeometry, BufferAttribute, BufferGeometry, Color, type ColorRepresentation, CylinderGeometry, EdgesGeometry, Line, LineBasicMaterial, LineSegments, Material, Matrix4, Mesh, MeshBasicMaterial, Object3D, Quaternion, SphereGeometry, Vector3 } from 'three';
2
2
  import ThreeMeshUI, { Inline, Text } from "three-mesh-ui"
3
3
  import { type Options } from 'three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js';
4
4
 
@@ -289,11 +289,11 @@ export class Gizmos {
289
289
  const mesh = Internal.getMesh(options.duration ?? 0);
290
290
  if ("mesh" in options) {
291
291
  mesh.geometry = options.mesh.geometry;
292
- mesh.matrix.copy(options.mesh.matrixWorld);
292
+ mesh.matrixWorld.copy(options.mesh.matrixWorld);
293
293
  }
294
294
  else {
295
295
  mesh.geometry = options.geometry;
296
- mesh.matrix.copy(options.matrix);
296
+ mesh.matrixWorld.copy(options.matrix);
297
297
  }
298
298
  mesh.matrixAutoUpdate = false;
299
299
  mesh.matrixWorldAutoUpdate = false;
@@ -325,8 +325,12 @@ export class Physics {
325
325
  const mesh = obj as Mesh | SkinnedMesh;
326
326
  const geo = mesh.geometry;
327
327
 
328
+ if (obj.raycastAllowed === false) {
329
+ shouldIntersectObject = false;
330
+ }
331
+
328
332
  // We need to run this first because of "EventSystem.testObject" implementation
329
- if (options.testObject) {
333
+ if (shouldIntersectObject && options.testObject) {
330
334
  const testResult = options.testObject?.(obj);
331
335
  if (testResult === false) {
332
336
  continue;
@@ -335,8 +339,7 @@ export class Physics {
335
339
  shouldIntersectObject = false;
336
340
  }
337
341
  }
338
-
339
- if (shouldIntersectObject) {
342
+ else if (shouldIntersectObject) {
340
343
  if (!geo) {
341
344
  shouldIntersectObject = false;
342
345
  }
@@ -348,18 +351,24 @@ export class Physics {
348
351
 
349
352
 
350
353
  if (shouldIntersectObject) {
351
- const raycastMesh = getRaycastMesh(obj);
352
- if (raycastMesh) mesh.geometry = raycastMesh as any;
353
354
  const lastResultsCount = results.length;
355
+ const preference = obj.raycastPreference || "lod";
354
356
 
355
- let usePrecise = true;
357
+ let usePrecise = preference !== "bounds";
356
358
  if (options.precise === false) usePrecise = false;
357
359
  usePrecise ||= geo.getAttribute("position")?.array?.length < 64;
358
360
  if (mesh instanceof GroundedSkybox) {
359
361
  usePrecise = false;
360
362
  }
361
363
 
362
- if (!usePrecise && customRaycast(mesh, raycaster, results)) {
364
+
365
+ if (preference === "lod") {
366
+ const raycastMesh = getRaycastMesh(obj);
367
+ if (raycastMesh) mesh.geometry = raycastMesh as any;
368
+ }
369
+
370
+
371
+ if (!usePrecise && boundsRaycast(mesh, raycaster, results)) {
363
372
  // did handle raycast
364
373
  }
365
374
  else if (options.useAcceleratedRaycast !== false) {
@@ -369,13 +378,16 @@ export class Physics {
369
378
  raycaster.intersectObject(mesh, false, results);
370
379
  }
371
380
 
381
+ // Restore
382
+ mesh.geometry = geo;
383
+
384
+ // Debug
372
385
  if (debugPhysics && results.length != lastResultsCount) {
373
386
  const latestResult = results[results.length - 1];
374
- const col = raycastMesh ? 0x88dd55 : 0x770000;
375
- Gizmos.DrawWireSphere(latestResult.point, .1, col, 1, false);
376
- Gizmos.DrawWireMesh({ mesh: obj as Mesh, depthTest: false, duration: .2, color: col });
387
+ Gizmos.DrawWireSphere(latestResult.point, .1, 0x770000, 1, false);
388
+ Gizmos.DrawWireMesh({ mesh: obj as Mesh, depthTest: false, duration: .2, color: 0x770000 });
377
389
  }
378
- mesh.geometry = geo;
390
+
379
391
  }
380
392
 
381
393
  if (options.recursive !== false) {
@@ -481,7 +493,7 @@ const normalUpMatrix = new Matrix3();
481
493
  /**
482
494
  * @returns false if custom raycasting can not run, otherwise true
483
495
  */
484
- function customRaycast(mesh: Mesh, raycaster: Raycaster, results: Intersection[]): boolean {
496
+ function boundsRaycast(mesh: Mesh, raycaster: Raycaster, results: Intersection[]): boolean {
485
497
  const originalComputeIntersectionsFn = mesh["_computeIntersections"];
486
498
  if (!originalComputeIntersectionsFn) {
487
499
  return false;
@@ -34,6 +34,23 @@ declare module 'three' {
34
34
  */
35
35
  hideFlags: HideFlags;
36
36
 
37
+ /**
38
+ * If false the object will be ignored for raycasting (e.g. pointer events). Default is true.
39
+ * @default true
40
+ */
41
+ raycastAllowed: boolean;
42
+
43
+ /**
44
+ * Set a raycast preference for the object:
45
+ * - `lod` will use the raycast mesh lod if available (default). This is usually a simplified mesh for raycasting.
46
+ * - `bounds` will use the bounding box of the object for raycasting. This is very fast but not very accurate.
47
+ * - `full` will use the full mesh for raycasting. This is the most accurate but also the slowest option.
48
+ *
49
+ * **NOTE:** Needle Engine's Raycast system will use Mesh BVH by default - so event 'full' is usually faster than default three.js raycasting.
50
+ */
51
+ raycastPreference?: 'lod' | 'bounds' | 'full';
52
+
53
+
37
54
  /**
38
55
  * Add a Needle Engine component to the {@link Object3D}.
39
56
  * @param comp The component instance or constructor to add.
@@ -242,6 +259,21 @@ if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "activeSelf")) {
242
259
  });
243
260
  }
244
261
 
262
+
263
+ if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "raycastAllowed")) {
264
+ Object.defineProperty(Object3D.prototype, "raycastAllowed", {
265
+ get: function () {
266
+ return this.userData && this.userData.raycastAllowed !== false;
267
+ },
268
+ set: function (val: boolean) {
269
+ const self = this as Object3D;
270
+ if (!self.userData) self.userData = {};
271
+ self.userData.raycastAllowed = val;
272
+ }
273
+ });
274
+ }
275
+
276
+
245
277
  if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "worldPosition")) {
246
278
  Object.defineProperty(Object3D.prototype, "worldPosition", {
247
279
  get: function () {
@@ -359,7 +359,7 @@ export class Renderer extends Behaviour implements IRenderer {
359
359
  get sharedMaterials(): SharedMaterialArray {
360
360
 
361
361
  // @ts-ignore (original materials will be set during deserialization)
362
- if(this._originalMaterials === undefined) return null;
362
+ if (this._originalMaterials === undefined) return null;
363
363
 
364
364
  // @ts-ignore during deserialization code might access this property *before* the setter and then create an empty array
365
365
  if (this.__isDeserializing === true) return null;
@@ -476,41 +476,43 @@ export class Renderer extends Behaviour implements IRenderer {
476
476
  ? this._lightmapTextureOverride
477
477
  : this.context.lightmaps.tryGetLightmap(this.sourceId, this.lightmapIndex);
478
478
  if (tex) {
479
- if (!this._lightmaps)
480
- this._lightmaps = [];
481
-
482
- if (type === "Mesh") {
483
- const mat = this.gameObject["material"];
484
- if (!mat?.isMeshBasicMaterial) {
485
- if (this._lightmaps.length <= 0) {
486
- const rm = new RendererLightmap(this.gameObject as any as Mesh, this.context);
487
- this._lightmaps.push(rm);
488
- }
489
- const rm = this._lightmaps[0];
490
- rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex);
491
- }
492
- else {
493
- if (mat)
494
- console.warn("Lightmapping is not supported on MeshBasicMaterial", mat.name)
495
- }
496
- }
497
- // for multi materials we need to loop through children
498
- // and then we add a lightmap renderer component to each of them
499
- else if (this.isMultiMaterialObject(this.gameObject) && this.sharedMaterials.length > 0) {
500
- for (let i = 0; i < this.gameObject.children.length; i++) {
501
- const child = this.gameObject.children[i];
502
- if (!child["material"]?.isMeshBasicMaterial) {
503
- let rm: RendererLightmap | undefined = undefined;
504
- if (i >= this._lightmaps.length) {
505
- rm = new RendererLightmap(child as Mesh, this.context);
506
- this._lightmaps.push(rm);
507
- }
508
- else
509
- rm = this._lightmaps[i];
510
- rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex);
511
- }
512
- }
513
- }
479
+ if (!this._lightmaps) this._lightmaps = [];
480
+
481
+
482
+ const rm = new RendererLightmap(this);
483
+ rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex);
484
+ this._lightmaps.push(rm);
485
+
486
+ // if (type === "Mesh") {
487
+ // const mat = this.gameObject["material"];
488
+ // if (!mat?.isMeshBasicMaterial) {
489
+ // if (this._lightmaps.length <= 0) {
490
+ // }
491
+ // const rm = this._lightmaps[0];
492
+ // rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex);
493
+ // }
494
+ // else {
495
+ // if (mat)
496
+ // console.warn("Lightmapping is not supported on MeshBasicMaterial", mat.name)
497
+ // }
498
+ // }
499
+ // // for multi materials we need to loop through children
500
+ // // and then we add a lightmap renderer component to each of them
501
+ // else if (this.isMultiMaterialObject(this.gameObject) && this.sharedMaterials.length > 0) {
502
+ // for (let i = 0; i < this.gameObject.children.length; i++) {
503
+ // const child = this.gameObject.children[i];
504
+ // if (!child["material"]?.isMeshBasicMaterial) {
505
+ // let rm: RendererLightmap | undefined = undefined;
506
+ // if (i >= this._lightmaps.length) {
507
+ // rm = new RendererLightmap(child as Mesh, this.context);
508
+ // this._lightmaps.push(rm);
509
+ // }
510
+ // else
511
+ // rm = this._lightmaps[i];
512
+ // rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex);
513
+ // }
514
+ // }
515
+ // }
514
516
  }
515
517
  else {
516
518
  if (debugRenderer) console.warn("Lightmap not found", this.sourceId, this.lightmapIndex);
@@ -742,7 +744,6 @@ export class Renderer extends Behaviour implements IRenderer {
742
744
  const environmentIntensity = this.context.mainCameraComponent?.environmentIntensity ?? 1;
743
745
  material.envMapIntensity = Math.max(0, environmentIntensity * this.context.sceneLighting.environmentIntensity / factor);
744
746
  }
745
-
746
747
  if (this._lightmaps) {
747
748
  for (const lm of this._lightmaps) {
748
749
  lm.updateLightmapUniforms(material);
@@ -1,8 +1,9 @@
1
1
  import { NEEDLE_progressive } from "@needle-tools/gltf-progressive";
2
- import { Material, Mesh, MeshPhysicalMaterial, ShaderMaterial, Texture, Vector4, WebGLProgramParametersWithUniforms } from "three";
2
+ import { Group, Material, Mesh, MeshPhysicalMaterial, Object3D, ShaderMaterial, Texture, Vector4, WebGLProgramParametersWithUniforms } from "three";
3
3
 
4
4
  import type { Context } from "../engine/engine_setup.js";
5
5
  import { getParam } from "../engine/engine_utils.js";
6
+ import { type Renderer } from "./Renderer.js";
6
7
 
7
8
  const debug = getParam("debuglightmaps");
8
9
 
@@ -31,15 +32,15 @@ export class RendererLightmap {
31
32
  private lightmapIndex: number = -1;
32
33
  private lightmapScaleOffset: Vector4 = new Vector4(1, 1, 0, 0);
33
34
 
34
- private context: Context;
35
- private gameObject: Mesh;
35
+ private readonly renderer: Renderer;
36
+ private get context(): Context { return this.renderer.context; }
37
+ private get gameObject() { return this.renderer.gameObject; }
36
38
  private lightmapTexture: Texture | null = null;
37
39
  private lightmapScaleOffsetUniform = { value: new Vector4(1, 1, 0, 0) };
38
40
  private lightmapUniform: { value: Texture | null } = { value: null };
39
41
 
40
- constructor(gameObject: Mesh, context: Context) {
41
- this.gameObject = gameObject;
42
- this.context = context;
42
+ constructor(renderer: Renderer) {
43
+ this.renderer = renderer;
43
44
  }
44
45
 
45
46
  init(lightmapIndex: number, lightmapScaleOffset: Vector4, lightmapTexture: Texture) {
@@ -55,7 +56,7 @@ export class RendererLightmap {
55
56
  console.log("Lightmap:", this.gameObject.name, lightmapIndex, "\nScaleOffset:", lightmapScaleOffset, "\nTexture:", lightmapTexture)
56
57
  this.setLightmapDebugMaterial();
57
58
  }
58
- else if(debug) console.log("Use debuglightmaps=show to render lightmaps only in the scene.")
59
+ else if (debug) console.log("Use debuglightmaps=show to render lightmaps only in the scene.")
59
60
  this.applyLightmap();
60
61
  }
61
62
 
@@ -77,42 +78,38 @@ export class RendererLightmap {
77
78
  return;
78
79
  }
79
80
 
80
- if (this.gameObject.type === "Group") {
81
- if (this.gameObject["Needle:Multimaterial-LightmapWarning"] === undefined) {
82
- this.gameObject["Needle:Multimaterial-LightmapWarning"] = true;
83
- console.warn("Lightmap on multimaterial object is not supported yet... please open a feature request on https://github.com/needle-tools/needle-engine-support if your project requires it");
84
- }
85
- return;
86
- }
81
+ const mesh = this.gameObject as unknown as (Mesh | Group);
82
+ this.ensureLightmapUvs(mesh);
87
83
 
88
- console.assert(this.gameObject.type === "Mesh", "Lightmap only works on meshes", this);
84
+ for (let i = 0; i < this.renderer.sharedMaterials.length; i++) {
89
85
 
90
- const mesh = this.gameObject as unknown as Mesh;
91
- if (!mesh.geometry.getAttribute("uv1"))
92
- mesh.geometry.setAttribute("uv1", mesh.geometry.getAttribute("uv"));
86
+ const mat = this.renderer.sharedMaterials[i];
87
+ if (!mat) continue;
93
88
 
94
- if (Array.isArray(this.gameObject.material)) {
95
- const mats: Material[] = this.gameObject.material;
96
- for (let i = 0; i < mats.length; i++) {
97
- mats[i] = this.ensureLightmapMaterial(mats[i]);
89
+ const newMat = this.ensureLightmapMaterial(mat);
90
+ if (mat !== newMat) {
91
+ this.renderer.sharedMaterials[i] = newMat;
98
92
  }
99
93
  }
100
- else {
101
- this.gameObject.material = this.ensureLightmapMaterial(this.gameObject.material);
102
- }
103
94
 
104
95
  if (this.lightmapIndex >= 0 && this.lightmapTexture) {
105
96
  // always on channel 1 for now. We could optimize this by passing the correct lightmap index along
106
97
  this.lightmapTexture.channel = 1;
107
- const mat = this.gameObject.material;
108
- if (Array.isArray(mat)) {
109
- for (const entry of mat) {
110
- this.assignLightmapTexture(entry as any);
98
+ for (const mat of this.renderer.sharedMaterials) {
99
+ if (mat) this.assignLightmapTexture(mat);
100
+ }
101
+ }
102
+ }
111
103
 
112
- }
104
+ private ensureLightmapUvs(object: Object3D | Group | Mesh) {
105
+ if (object instanceof Mesh) {
106
+ if (!object.geometry.getAttribute("uv1")) {
107
+ object.geometry.setAttribute("uv1", object.geometry.getAttribute("uv"));
113
108
  }
114
- else if (mat) {
115
- this.assignLightmapTexture(mat);
109
+ }
110
+ else if (object instanceof Group) {
111
+ for (const child of object.children) {
112
+ this.ensureLightmapUvs(child);
116
113
  }
117
114
  }
118
115
  }
@@ -127,7 +124,7 @@ export class RendererLightmap {
127
124
  if (material["NEEDLE:lightmap-material-version"] == undefined) {
128
125
  if (debug) console.warn("Cloning material for lightmap " + material.name);
129
126
  const mat: Material = material.clone();
130
- if(!mat.name?.includes("(lightmap)")) mat.name = material.name + " (lightmap)";
127
+ if (!mat.name?.includes("(lightmap)")) mat.name = material.name + " (lightmap)";
131
128
  material = mat;
132
129
  material.onBeforeCompile = this.onBeforeCompile;
133
130
  }
@@ -152,6 +149,7 @@ export class RendererLightmap {
152
149
 
153
150
  // assign the lightmap
154
151
  material.lightMap = this.lightmapTexture;
152
+ material.needsUpdate = true;
155
153
  // store the version of the material
156
154
  material["NEEDLE:lightmap-material-version"] = material.version;
157
155
  }
@@ -0,0 +1,256 @@
1
+ import { Material, Object3D, Vector3 } from "three";
2
+
3
+ import { Gizmos } from "../engine/engine_gizmos.js";
4
+ import { Mathf } from "../engine/engine_math.js";
5
+ import { serializable } from "../engine/engine_serialization_decorator.js";
6
+ import { getTempVector } from "../engine/engine_three_utils.js";
7
+ import { getParam } from "../engine/engine_utils.js";
8
+ import { Behaviour } from "./Component.js";
9
+ import { Renderer } from "./Renderer.js";
10
+
11
+ const debugSeeThrough = getParam("debugseethrough");
12
+
13
+ type MaterialState = {
14
+ opacity: number,
15
+ transparent: boolean,
16
+ alphaHash: boolean
17
+ }
18
+
19
+ type MaterialWithState = Material & {
20
+ /** Original values */
21
+ userData: {
22
+ seeThrough: {
23
+ initial: MaterialState,
24
+ }
25
+ }
26
+ };
27
+
28
+ let i = 0;
29
+
30
+
31
+ /**
32
+ * Makes the object fade out when it is obscuring the reference point from the camera. This component can be put on any object in the scene. It will affect all Renderer components on the same object and child objects.
33
+ *
34
+ * Useful for e.g. making walls transparent when the camera is outside or hiding object's that would otherwise block the view.
35
+ *
36
+ * Requires a Renderer component on the same object or a child object.
37
+ *
38
+ * - Example https://see-through-walls-z23hmxbz1kjfjn.needle.run/
39
+ */
40
+ export class SeeThrough extends Behaviour {
41
+
42
+ /**
43
+ * Assign a reference point - if this point will be obscured from the camera by this object then this object will fade out.
44
+ * If no reference point is assigned the scene's root object will be used as reference point.
45
+ */
46
+ @serializable(Object3D)
47
+ referencePoint: Object3D | null = null;
48
+
49
+ /**
50
+ * Fade Duration in seconds
51
+ * @default 0.05
52
+ */
53
+ @serializable()
54
+ fadeDuration: number = .05;
55
+
56
+ /**
57
+ * Minimum alpha value when fading out (0-1)
58
+ * @default 0
59
+ */
60
+ @serializable()
61
+ minAlpha: number = 0;
62
+
63
+ /**
64
+ * When useAlphaHash is enabled the object will fade out using alpha hashing, this means the object can stay opaque. If disabled the object will set to be transparent when fading out.
65
+ * @default true
66
+ */
67
+ @serializable()
68
+ useAlphaHash: boolean = true;
69
+
70
+ /**
71
+ * Set this to force updating the reference point position and direction
72
+ */
73
+ set needsUpdate(val: boolean) {
74
+ this._needsUpdate = val;
75
+ }
76
+ get needsUpdate() {
77
+ return this._needsUpdate;
78
+ }
79
+
80
+ /**
81
+ * Override the alpha value, -1 means no override
82
+ * @default -1
83
+ */
84
+ @serializable()
85
+ overrideAlpha: number = -1;
86
+
87
+ /**
88
+ *
89
+ */
90
+ @serializable()
91
+ autoUpdate: boolean = true;
92
+
93
+
94
+ private readonly _referencePointVector: Vector3 = new Vector3();
95
+ private readonly _referencePointDir: Vector3 = new Vector3();
96
+ private _distance: number = 0;
97
+ private _renderer: Renderer[] | null = null;
98
+ private _needsUpdate = true;
99
+ private _id = i++;
100
+
101
+ /** * @internal */
102
+ onEnable() {
103
+ this._needsUpdate = true;
104
+ this._renderer = null;
105
+ }
106
+
107
+ /** @internal */
108
+ onDisable() {
109
+ this._renderer?.forEach(r => {
110
+ const original = this.rendererMaterialsOriginal.get(r);
111
+ for (let i = 0; i < r.sharedMaterials.length; i++) {
112
+ const mat = r.sharedMaterials[i];
113
+ if (!mat) continue;
114
+ if (original && original[i]) {
115
+ r.sharedMaterials[i] = original[i];
116
+ }
117
+ }
118
+ this.rendererMaterials.delete(r);
119
+ this.rendererMaterialsOriginal.delete(r);
120
+ });
121
+ }
122
+
123
+ /**
124
+ * @internal
125
+ */
126
+ update(): void {
127
+
128
+
129
+ if (this._needsUpdate) {
130
+ this._needsUpdate = false;
131
+ this._renderer = this.gameObject.getComponentsInChildren(Renderer);
132
+
133
+ // NOTE: instead of using the object's anchor (gameObject.worldPosition) we could also get the object's bounding box center:
134
+ // getBoundingBox(this.gameObject); // < import { getBoundingBox } from "@needle-tools/engine";
135
+ this.updateDirection();
136
+ }
137
+ else if (this.autoUpdate && (this.context.time.frame + this._id) % 20 === 0) {
138
+ this.updateDirection();
139
+ }
140
+
141
+
142
+
143
+ if (!this.autoUpdate) return;
144
+ if (!this.referencePoint) return;
145
+
146
+
147
+ const dot = this._referencePointDir.dot(this.context.mainCamera.worldForward);
148
+ const shouldHide = dot > .2;
149
+
150
+ if (debugSeeThrough && this.referencePoint) {
151
+ const wp = this.gameObject.worldPosition;
152
+ Gizmos.DrawArrow(getTempVector(wp), wp.sub(this._referencePointDir), shouldHide ? 0xFF0000 : 0x00FF00);
153
+ Gizmos.DrawWireSphere(this.referencePoint.worldPosition, .05, 0x0000FF);
154
+ }
155
+
156
+ if (shouldHide) {
157
+ this.updateAlpha(this.minAlpha, this.fadeDuration);
158
+ }
159
+ else {
160
+ this.updateAlpha(1, this.fadeDuration);
161
+ }
162
+ }
163
+
164
+ private readonly rendererMaterials = new WeakMap<Renderer, Array<MaterialWithState>>();
165
+ private readonly rendererMaterialsOriginal = new WeakMap<Renderer, Array<Material>>();
166
+
167
+ private updateDirection() {
168
+ this.referencePoint ??= this.context.scene;
169
+ this._referencePointVector.copy(this.gameObject.worldPosition.sub(this.referencePoint.worldPosition));
170
+ this._distance = this._referencePointVector.length();
171
+ this._referencePointDir.copy(this._referencePointVector)
172
+ .multiply(getTempVector(1, .5, 1)) // Reduce vertical influence
173
+ .normalize();
174
+ }
175
+
176
+
177
+ /**
178
+ * Update the alpha of the object's materials towards the target alpha over the given duration.
179
+ * @param targetAlpha Target alpha value (0-1)
180
+ * @param duration Duration in seconds to reach the target alpha. 0 means immediate. Default is the component's fadeDuration.
181
+ */
182
+ updateAlpha(targetAlpha: number, duration: number = this.fadeDuration) {
183
+
184
+ if (this.overrideAlpha !== undefined && this.overrideAlpha !== -1) {
185
+ targetAlpha = this.overrideAlpha;
186
+ }
187
+
188
+ this._renderer?.forEach(renderer => {
189
+
190
+ if (targetAlpha < .9) {
191
+ renderer.gameObject.raycastAllowed = false;
192
+ }
193
+ else {
194
+ renderer.gameObject.raycastAllowed = true;
195
+ }
196
+
197
+ if (!this.rendererMaterials.has(renderer)) {
198
+ const originalMaterials = new Array<Material>();
199
+ const clonedMaterials = new Array<MaterialWithState>();
200
+
201
+ // We clone the materials once and store them, so we can modify the opacity without affecting other objects using the same material. This could potentially be optimized further to re-use materials between renderers if multiple renderers use the same material.
202
+ for (let i = 0; i < renderer.sharedMaterials.length; i++) {
203
+ const mat = renderer.sharedMaterials[i];
204
+ if (!mat) continue;
205
+ originalMaterials.push(mat);
206
+ const matClone = mat.clone() as MaterialWithState;
207
+ // @ts-ignore
208
+ matClone.userData = mat.userData || {};
209
+ matClone.userData.seeThrough = {
210
+ initial: {
211
+ opacity: matClone.opacity,
212
+ transparent: matClone.transparent,
213
+ alphaHash: matClone.alphaHash
214
+ }
215
+ }
216
+ clonedMaterials.push(matClone);
217
+ renderer.sharedMaterials[i] = matClone;
218
+ }
219
+
220
+ this.rendererMaterials.set(renderer, clonedMaterials);
221
+ this.rendererMaterialsOriginal.set(renderer, originalMaterials);
222
+ }
223
+
224
+ const materials = renderer.hasLightmap ? renderer.sharedMaterials : this.rendererMaterials.get(renderer);
225
+ if (!materials) return;
226
+
227
+ for (const mat of materials) {
228
+ if (!mat) continue;
229
+
230
+ let newAlpha = Mathf.lerp(mat.opacity, targetAlpha, duration <= 0 ? 1 : this.context.time.deltaTime / duration);;
231
+ if (newAlpha >= 0.99) newAlpha = 1;
232
+ else if (newAlpha <= 0.01) newAlpha = 0;
233
+
234
+
235
+ const wasTransparent = mat.transparent;
236
+ const wasAlphaHash = mat.alphaHash;
237
+
238
+ mat.alphaHash = this.useAlphaHash;
239
+
240
+ if (mat.userData && "seeThrough" in mat.userData) {
241
+ const initial = mat.userData.seeThrough.initial as MaterialState;
242
+ mat.opacity = initial.opacity * newAlpha;
243
+ mat.transparent = mat.opacity >= 1 ? initial.transparent : !this.useAlphaHash;
244
+ }
245
+ else {
246
+ mat.transparent = mat.opacity >= 1 ? false : !this.useAlphaHash;
247
+ }
248
+
249
+ if (wasTransparent != mat.transparent || wasAlphaHash != mat.alphaHash) {
250
+ mat.needsUpdate = true;
251
+ }
252
+ }
253
+ });
254
+ }
255
+
256
+ }
@@ -145,6 +145,7 @@ export { RendererLightmap } from "../RendererLightmap.js";
145
145
  export { Rigidbody } from "../RigidBody.js";
146
146
  export { SceneSwitcher } from "../SceneSwitcher.js";
147
147
  export { ScreenCapture } from "../ScreenCapture.js";
148
+ export { SeeThrough } from "../SeeThrough.js";
148
149
  export { ShadowCatcher } from "../ShadowCatcher.js";
149
150
  export { RemoteSkybox } from "../Skybox.js";
150
151
  export { SmoothFollow } from "../SmoothFollow.js";
@@ -59,7 +59,19 @@ export class Graphic extends BaseUIComponent implements IGraphic, IRectTransform
59
59
  this.sRGBColor.copy(this._color);
60
60
  this.sRGBColor.convertLinearToSRGB();
61
61
  _colorStateObject.backgroundColor = this.sRGBColor;
62
- _colorStateObject.backgroundOpacity = this._color.alpha * this._alphaFactor;
62
+ _colorStateObject.backgroundOpacity = this._color.alpha;
63
+
64
+ // E.g. when a button is setting states, we need to merge the state color with the base color
65
+ const activeStateName = this.uiObject["_simpleState__activeStates"]?.[0];
66
+ if (activeStateName) {
67
+ const active = this.uiObject["_simpleState__states"]?.[activeStateName];
68
+ if (active) {
69
+ if ("backgroundColor" in active) _colorStateObject.backgroundColor = active["backgroundColor"];
70
+ if ("backgroundOpacity" in active) _colorStateObject.backgroundOpacity = active["backgroundOpacity"];
71
+ }
72
+ }
73
+
74
+ _colorStateObject.backgroundOpacity *= this._alphaFactor;
63
75
  this.applyEffects(_colorStateObject, this._alphaFactor);
64
76
  this.uiObject.set(_colorStateObject);
65
77
  this.markDirty();