@needle-tools/engine 4.10.5-next.267a93d → 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 (52) hide show
  1. package/CHANGELOG.md +6 -1
  2. package/components.needle.json +1 -1
  3. package/dist/{needle-engine.bundle-Bj33kRGE.min.js → needle-engine.bundle-BJ2hrLWW.min.js} +130 -130
  4. package/dist/{needle-engine.bundle-DXZewFGi.js → needle-engine.bundle-DZS0TuER.js} +5952 -5792
  5. package/dist/{needle-engine.bundle-BozXof79.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/RendererLightmap.d.ts +1 -0
  20. package/lib/engine-components/RendererLightmap.js +15 -30
  21. package/lib/engine-components/RendererLightmap.js.map +1 -1
  22. package/lib/engine-components/SeeThrough.d.ts +70 -0
  23. package/lib/engine-components/SeeThrough.js +223 -0
  24. package/lib/engine-components/SeeThrough.js.map +1 -0
  25. package/lib/engine-components/codegen/components.d.ts +1 -0
  26. package/lib/engine-components/codegen/components.js +1 -0
  27. package/lib/engine-components/codegen/components.js.map +1 -1
  28. package/lib/engine-components/ui/Graphic.js +13 -1
  29. package/lib/engine-components/ui/Graphic.js.map +1 -1
  30. package/lib/engine-components/ui/RaycastUtils.js +5 -3
  31. package/lib/engine-components/ui/RaycastUtils.js.map +1 -1
  32. package/lib/engine-components/utils/LookAt.js +4 -2
  33. package/lib/engine-components/utils/LookAt.js.map +1 -1
  34. package/lib/engine-components/web/Clickthrough.d.ts +2 -1
  35. package/lib/engine-components/web/Clickthrough.js +2 -1
  36. package/lib/engine-components/web/Clickthrough.js.map +1 -1
  37. package/lib/engine-components/web/CursorFollow.d.ts +7 -1
  38. package/lib/engine-components/web/CursorFollow.js +35 -2
  39. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  40. package/package.json +2 -2
  41. package/src/engine/codegen/register_types.ts +2 -0
  42. package/src/engine/engine_gizmos.ts +3 -3
  43. package/src/engine/engine_physics.ts +24 -12
  44. package/src/engine/js-extensions/Object3D.ts +32 -0
  45. package/src/engine-components/RendererLightmap.ts +16 -36
  46. package/src/engine-components/SeeThrough.ts +256 -0
  47. package/src/engine-components/codegen/components.ts +1 -0
  48. package/src/engine-components/ui/Graphic.ts +13 -1
  49. package/src/engine-components/ui/RaycastUtils.ts +9 -8
  50. package/src/engine-components/utils/LookAt.ts +4 -1
  51. package/src/engine-components/web/Clickthrough.ts +2 -1
  52. package/src/engine-components/web/CursorFollow.ts +48 -7
@@ -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();
@@ -11,9 +11,8 @@ export class UIRaycastUtils {
11
11
  /** returns the real object when dealing with shadow UI */
12
12
  static getObject(obj: Object3D): Object3D {
13
13
  const shadowOwner = obj[$shadowDomOwner];
14
- if(shadowOwner)
15
- {
16
- if((shadowOwner as IComponent).isComponent === true) obj = (shadowOwner as IComponent).gameObject;
14
+ if (shadowOwner) {
15
+ if ((shadowOwner as IComponent).isComponent === true) obj = (shadowOwner as IComponent).gameObject;
17
16
  else obj = shadowOwner;
18
17
  }
19
18
  return obj;
@@ -29,8 +28,8 @@ export class UIRaycastUtils {
29
28
 
30
29
  obj = this.getObject(obj);
31
30
 
32
- if(!obj.visible) return false;
33
-
31
+ if (!obj.visible) return false;
32
+
34
33
  const canvasGroup = this.tryFindCanvasGroup(obj);
35
34
  if (canvasGroup?.isCanvasGroup === true) {
36
35
  if (out) out.canvasGroup = canvasGroup as ICanvasGroup;
@@ -38,7 +37,7 @@ export class UIRaycastUtils {
38
37
  if (canvasGroup.interactable === false) return false;
39
38
  }
40
39
  // handle Graphic Raycast target
41
- const graphic : IGraphic | undefined = foreachComponent(obj, c => {
40
+ const graphic: IGraphic | undefined = foreachComponent(obj, c => {
42
41
  if ((c as unknown as IGraphic).isGraphic === true) return c;
43
42
  return undefined;
44
43
  }, false);
@@ -58,8 +57,10 @@ export class UIRaycastUtils {
58
57
  if (!obj) return null;
59
58
  // test for canvas groups
60
59
  const res = foreachComponent(obj, c => {
61
- const gr = c as unknown as ICanvasGroup;
62
- if (gr.blocksRaycasts !== undefined && gr.interactable !== undefined) return gr;
60
+ if (c.activeAndEnabled) {
61
+ const gr = c as unknown as ICanvasGroup;
62
+ if (gr.blocksRaycasts !== undefined && gr.interactable !== undefined) return gr;
63
+ }
63
64
  return undefined;
64
65
  }, false);
65
66
  if (res !== undefined) return res;
@@ -45,7 +45,10 @@ export class LookAt extends Behaviour implements UsdzBehaviour {
45
45
  let target: Object3D | null | undefined = this.target;
46
46
  if (!target) {
47
47
  target = this.context.mainCamera;
48
- if (isDevEnvironment()) console.warn(`[LookAt] No target set on ${this.name}, using main camera as target.`);
48
+ if (isDevEnvironment() && !this["__did_warn"]) {
49
+ this["__did_warn"] = true;
50
+ console.debug(`[LookAt] No target set on ${this.name}, using main camera as target.`);
51
+ }
49
52
  }
50
53
  if (!target) return;
51
54
 
@@ -29,7 +29,8 @@ onStart(ctx => {
29
29
  * - Add the ClickThrough component to any GameObject in your scene.
30
30
  * - Alternatively, add the `clickthrough` attribute to the `<needle-engine>` HTML element (e.g. `<needle-engine clickthrough></needle-engine>`).
31
31
  *
32
- * @link Example https://stackblitz.com/~/github.com/needle-engine/sample-3d-over-html
32
+ * - Example https://stackblitz.com/~/github.com/needle-engine/sample-3d-over-html
33
+ *
33
34
  * @category Web
34
35
  * @group Components
35
36
  * @component
@@ -1,7 +1,14 @@
1
+ import { Ray } from "three";
2
+
3
+ import { Gizmos } from "../../engine/engine_gizmos.js";
1
4
  import { serializable } from "../../engine/engine_serialization_decorator.js";
2
5
  import { getTempVector } from "../../engine/engine_three_utils.js";
6
+ import { getParam } from "../../engine/engine_utils.js";
3
7
  import { Behaviour } from "../Component.js";
4
8
 
9
+
10
+ const debug = getParam("debugcursor");
11
+
5
12
  /**
6
13
  * The CursorFollow component makes the object follow the cursor (or touch) position on screen.
7
14
  *
@@ -34,9 +41,15 @@ export class CursorFollow extends Behaviour {
34
41
  @serializable()
35
42
  keepDistance: boolean = true;
36
43
 
37
-
44
+ /**
45
+ * If true, the object will attempt to snap to the surface of other objects in the scene using a raycast.
46
+ */
47
+ @serializable()
48
+ snapToSurface: boolean = false;
49
+
50
+
38
51
  private _distance: number = -1;
39
- updateDistance(force:boolean = false) {
52
+ updateDistance(force: boolean = false) {
40
53
  if (!force && (this.keepDistance && this._distance !== -1)) {
41
54
  return;
42
55
  }
@@ -48,10 +61,12 @@ export class CursorFollow extends Behaviour {
48
61
  this._distance = -1;
49
62
  }
50
63
 
64
+ /** @internal */
51
65
  onEnable(): void {
52
66
  this._distance = -1;
53
67
  window.addEventListener('pointermove', this._onPointerMove);
54
68
  }
69
+ /** @internal */
55
70
  onDisable(): void {
56
71
  window.removeEventListener('pointermove', this._onPointerMove);
57
72
  }
@@ -59,8 +74,8 @@ export class CursorFollow extends Behaviour {
59
74
  private _ndc_x = 0;
60
75
  private _ndc_y = 0;
61
76
 
62
- private _onPointerMove = (e:PointerEvent) => {
63
- if(!this.useFullPage) return;
77
+ private _onPointerMove = (e: PointerEvent) => {
78
+ if (!this.useFullPage) return;
64
79
  const x = e.clientX;
65
80
  const y = e.clientY;
66
81
  const domx = this.context.domX;
@@ -73,7 +88,7 @@ export class CursorFollow extends Behaviour {
73
88
 
74
89
 
75
90
  /** @internal */
76
- update() {
91
+ lateUpdate() {
77
92
  // continuously update distance in case camera or object moves
78
93
  this.updateDistance();
79
94
 
@@ -89,16 +104,42 @@ export class CursorFollow extends Behaviour {
89
104
  rayDirection.sub(cameraPosition).normalize();
90
105
 
91
106
  // position object at initial distance along the ray
92
- const newPosition = rayDirection.multiplyScalar(this._distance).add(cameraPosition);
107
+ const newPosition = getTempVector(rayDirection).multiplyScalar(this._distance).add(cameraPosition);
108
+ let _position = newPosition;
109
+
110
+
93
111
  if (this.damping > 0) {
94
112
  const pos = this.gameObject.worldPosition;
95
113
  pos.lerp(newPosition, this.context.time.deltaTime / this.damping);
96
114
  this.gameObject.worldPosition = pos;
115
+ _position = pos;
97
116
  }
98
117
  else {
99
118
  this.gameObject.worldPosition = newPosition;
100
119
  }
101
120
 
121
+
122
+ if (this.snapToSurface) {
123
+ ray.origin = _position;
124
+ ray.direction = rayDirection.multiplyScalar(-1);
125
+ const hits = this.context.physics.raycastFromRay(ray);
126
+ if (hits?.length) {
127
+ const hit = hits[0];
128
+ if (this.damping > 0) {
129
+ this.gameObject.worldPosition = _position.lerp(hit.point, this.context.time.deltaTime / this.damping);
130
+ }
131
+ else {
132
+ this.gameObject.worldPosition = hit.point;
133
+ }
134
+
135
+ if(debug) {
136
+ Gizmos.DrawLine(hit.point, hit.normal!.add(hit.point), 0x00FF00);
137
+ }
138
+ }
139
+ }
140
+
102
141
  }
103
142
 
104
- }
143
+ }
144
+
145
+ const ray = new Ray();