@needle-tools/engine 4.10.4 → 4.10.5-next.267a93d

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.
@@ -9,6 +9,9 @@ import { getTempVector } from "../../engine/engine_three_utils.js";
9
9
  import { Behaviour } from "../Component.js";
10
10
  /**
11
11
  * The CursorFollow component makes the object follow the cursor (or touch) position on screen.
12
+ *
13
+ * - Example: [Look At Cursor sample](https://engine.needle.tools/samples/look-at-cursor-interactive-3d-header/). This sample combines the CursorFollow component with a LookAt component to create an interactive 3D header that looks at the cursor.
14
+ *
12
15
  * @category Web
13
16
  * @group Components
14
17
  * @component
@@ -19,31 +22,59 @@ export class CursorFollow extends Behaviour {
19
22
  * @default 0
20
23
  */
21
24
  damping = 0;
25
+ /**
26
+ * When enabled the object will follow the cursor even outside of the needle-engine canvas. This is useful for example for look at effects where you have a small needle-engine element on your page and you want the 3D object to keep looking at the cursor even when it's outside of the canvas.
27
+ * @default true
28
+ */
29
+ useFullPage = true;
22
30
  /**
23
31
  * If true, the initial distance to the camera is maintained when following the cursor.
24
32
  * @default true
25
33
  */
26
34
  keepDistance = true;
27
- awake() {
28
- this._distance = -1;
29
- }
30
35
  _distance = -1;
31
- updateDistance() {
32
- if (this.keepDistance && this._distance !== -1) {
36
+ updateDistance(force = false) {
37
+ if (!force && (this.keepDistance && this._distance !== -1)) {
33
38
  return;
34
39
  }
35
40
  this._distance = this.gameObject.worldPosition.distanceTo(this.context.mainCamera.worldPosition);
36
41
  }
37
42
  /** @internal */
43
+ awake() {
44
+ this._distance = -1;
45
+ }
46
+ onEnable() {
47
+ this._distance = -1;
48
+ window.addEventListener('pointermove', this._onPointerMove);
49
+ }
50
+ onDisable() {
51
+ window.removeEventListener('pointermove', this._onPointerMove);
52
+ }
53
+ _ndc_x = 0;
54
+ _ndc_y = 0;
55
+ _onPointerMove = (e) => {
56
+ if (!this.useFullPage)
57
+ return;
58
+ const x = e.clientX;
59
+ const y = e.clientY;
60
+ const domx = this.context.domX;
61
+ const domy = this.context.domY;
62
+ const domw = this.context.domWidth;
63
+ const domh = this.context.domHeight;
64
+ this._ndc_x = (x - domx) / domw * 2 - 1;
65
+ this._ndc_y = -(y - domy) / domh * 2 + 1;
66
+ };
67
+ /** @internal */
38
68
  update() {
39
69
  // continuously update distance in case camera or object moves
40
70
  this.updateDistance();
71
+ const x = this.useFullPage ? this._ndc_x : this.context.input.mousePositionRC.x;
72
+ const y = this.useFullPage ? this._ndc_y : this.context.input.mousePositionRC.y;
41
73
  // follow cursor in screenspace but maintain initial distance from camera
42
- const cursor = this.context.input.mousePositionRC;
43
74
  const camera = this.context.mainCamera;
44
75
  const cameraPosition = camera.worldPosition;
45
76
  // create ray from camera through cursor position
46
- const rayDirection = getTempVector(cursor.x, cursor.y, 1).unproject(camera);
77
+ const rayDirection = getTempVector(x, y, 1).unproject(camera);
47
78
  rayDirection.sub(cameraPosition).normalize();
48
79
  // position object at initial distance along the ray
49
80
  const newPosition = rayDirection.multiplyScalar(this._distance).add(cameraPosition);
@@ -60,6 +91,9 @@ export class CursorFollow extends Behaviour {
60
91
  __decorate([
61
92
  serializable()
62
93
  ], CursorFollow.prototype, "damping", void 0);
94
+ __decorate([
95
+ serializable()
96
+ ], CursorFollow.prototype, "useFullPage", void 0);
63
97
  __decorate([
64
98
  serializable()
65
99
  ], CursorFollow.prototype, "keepDistance", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"CursorFollow.js","sourceRoot":"","sources":["../../../src/engine-components/web/CursorFollow.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gDAAgD,CAAC;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,OAAO,YAAa,SAAQ,SAAS;IAEvC;;;OAGG;IAEH,OAAO,GAAW,CAAC,CAAC;IAEpB;;;OAGG;IAEH,YAAY,GAAY,IAAI,CAAC;IAE7B,KAAK;QACD,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;IACxB,CAAC;IAEO,SAAS,GAAW,CAAC,CAAC,CAAC;IAE/B,cAAc;QACV,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,SAAS,KAAK,CAAC,CAAC,EAAE;YAC5C,OAAO;SACV;QACD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IACrG,CAAC;IAED,gBAAgB;IAChB,MAAM;QACF,8DAA8D;QAC9D,IAAI,CAAC,cAAc,EAAE,CAAC;QAEtB,yEAAyE;QACzE,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC;QAClD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;QACvC,MAAM,cAAc,GAAG,MAAM,CAAC,aAAa,CAAC;QAE5C,iDAAiD;QACjD,MAAM,YAAY,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC5E,YAAY,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,SAAS,EAAE,CAAC;QAE7C,oDAAoD;QACpD,MAAM,WAAW,GAAG,YAAY,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QACpF,IAAI,IAAI,CAAC,OAAO,GAAG,CAAC,EAAE;YAClB,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC;YAC1C,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;YAClE,IAAI,CAAC,UAAU,CAAC,aAAa,GAAG,GAAG,CAAC;SACvC;aACI;YACD,IAAI,CAAC,UAAU,CAAC,aAAa,GAAG,WAAW,CAAC;SAC/C;IAEL,CAAC;CAEJ;AAjDG;IADC,YAAY,EAAE;6CACK;AAOpB;IADC,YAAY,EAAE;kDACc"}
1
+ {"version":3,"file":"CursorFollow.js","sourceRoot":"","sources":["../../../src/engine-components/web/CursorFollow.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/engine",
3
- "version": "4.10.4",
3
+ "version": "4.10.5-next.267a93d",
4
4
  "description": "Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.",
5
5
  "main": "dist/needle-engine.min.js",
6
6
  "exports": {
@@ -169,4 +169,4 @@
169
169
  "module": "lib/needle-engine.js",
170
170
  "typings": "lib/needle-engine.d.ts",
171
171
  "types": "lib/needle-engine.d.ts"
172
- }
172
+ }
@@ -244,7 +244,7 @@ function getLogsContainer(domElement: HTMLElement): HTMLElement {
244
244
  padding-top: 0px;
245
245
  max-width: 70%;
246
246
  max-height: calc(100% - 5px);
247
- z-index: 9999999999;
247
+ z-index: 100000;
248
248
  pointer-events: scroll;
249
249
  display: flex;
250
250
  align-items: end;
@@ -1627,6 +1627,7 @@ export class Context implements IContext {
1627
1627
  // but as a fallback we still register them (if e.g. there's no camera for compile async)
1628
1628
  looputils.runPrewarm(this);
1629
1629
  this._currentFrameEvent = FrameEvent.Undefined;
1630
+ nodeFrame.camera = this.mainCamera as Camera | null;
1630
1631
  nodeFrame.update();
1631
1632
  this.renderNow();
1632
1633
  this._currentFrameEvent = FrameEvent.OnAfterRender;
@@ -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);
@@ -3,6 +3,7 @@ import { Material, Mesh, MeshPhysicalMaterial, ShaderMaterial, Texture, Vector4,
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
 
@@ -88,32 +89,49 @@ export class RendererLightmap {
88
89
  console.assert(this.gameObject.type === "Mesh", "Lightmap only works on meshes", this);
89
90
 
90
91
  const mesh = this.gameObject as unknown as Mesh;
91
- if (!mesh.geometry.getAttribute("uv1"))
92
+ if (!mesh.geometry.getAttribute("uv1")) {
92
93
  mesh.geometry.setAttribute("uv1", mesh.geometry.getAttribute("uv"));
94
+ }
95
+
96
+ for (let i = 0; i < this.renderer.sharedMaterials.length; i++) {
93
97
 
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]);
98
+ const mat = this.renderer.sharedMaterials[i];
99
+ if (!mat) continue;
100
+
101
+ const newMat = this.ensureLightmapMaterial(mat);
102
+ if (mat !== newMat) {
103
+ this.renderer.sharedMaterials[i] = newMat;
98
104
  }
99
- }
100
- else {
101
- this.gameObject.material = this.ensureLightmapMaterial(this.gameObject.material);
105
+
102
106
  }
103
107
 
108
+ // if (Array.isArray(this.gameObject.material)) {
109
+ // const mats: Material[] = this.gameObject.material;
110
+ // for (let i = 0; i < mats.length; i++) {
111
+ // mats[i] = this.ensureLightmapMaterial(mats[i]);
112
+ // }
113
+ // }
114
+ // else {
115
+ // this.gameObject.material = this.ensureLightmapMaterial(this.gameObject.material);
116
+ // }
117
+
104
118
  if (this.lightmapIndex >= 0 && this.lightmapTexture) {
105
119
  // always on channel 1 for now. We could optimize this by passing the correct lightmap index along
106
120
  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);
111
-
112
- }
113
- }
114
- else if (mat) {
115
- this.assignLightmapTexture(mat);
121
+ for (const mat of this.renderer.sharedMaterials) {
122
+ if (mat) this.assignLightmapTexture(mat);
116
123
  }
124
+
125
+ // const mat = this.gameObject.material;
126
+ // if (Array.isArray(mat)) {
127
+ // for (const entry of mat) {
128
+ // this.assignLightmapTexture(entry as any);
129
+
130
+ // }
131
+ // }
132
+ // else if (mat) {
133
+ // this.assignLightmapTexture(mat);
134
+ // }
117
135
  }
118
136
  }
119
137
 
@@ -127,7 +145,7 @@ export class RendererLightmap {
127
145
  if (material["NEEDLE:lightmap-material-version"] == undefined) {
128
146
  if (debug) console.warn("Cloning material for lightmap " + material.name);
129
147
  const mat: Material = material.clone();
130
- if(!mat.name?.includes("(lightmap)")) mat.name = material.name + " (lightmap)";
148
+ if (!mat.name?.includes("(lightmap)")) mat.name = material.name + " (lightmap)";
131
149
  material = mat;
132
150
  material.onBeforeCompile = this.onBeforeCompile;
133
151
  }
@@ -3,7 +3,10 @@ import { getTempVector } from "../../engine/engine_three_utils.js";
3
3
  import { Behaviour } from "../Component.js";
4
4
 
5
5
  /**
6
- * The CursorFollow component makes the object follow the cursor (or touch) position on screen.
6
+ * The CursorFollow component makes the object follow the cursor (or touch) position on screen.
7
+ *
8
+ * - Example: [Look At Cursor sample](https://engine.needle.tools/samples/look-at-cursor-interactive-3d-header/). This sample combines the CursorFollow component with a LookAt component to create an interactive 3D header that looks at the cursor.
9
+ *
7
10
  * @category Web
8
11
  * @group Components
9
12
  * @component
@@ -17,6 +20,13 @@ export class CursorFollow extends Behaviour {
17
20
  @serializable()
18
21
  damping: number = 0;
19
22
 
23
+ /**
24
+ * When enabled the object will follow the cursor even outside of the needle-engine canvas. This is useful for example for look at effects where you have a small needle-engine element on your page and you want the 3D object to keep looking at the cursor even when it's outside of the canvas.
25
+ * @default true
26
+ */
27
+ @serializable()
28
+ useFullPage: boolean = true;
29
+
20
30
  /**
21
31
  * If true, the initial distance to the camera is maintained when following the cursor.
22
32
  * @default true
@@ -24,31 +34,58 @@ export class CursorFollow extends Behaviour {
24
34
  @serializable()
25
35
  keepDistance: boolean = true;
26
36
 
37
+
38
+ private _distance: number = -1;
39
+ updateDistance(force:boolean = false) {
40
+ if (!force && (this.keepDistance && this._distance !== -1)) {
41
+ return;
42
+ }
43
+ this._distance = this.gameObject.worldPosition.distanceTo(this.context.mainCamera.worldPosition);
44
+ }
45
+
46
+ /** @internal */
27
47
  awake() {
28
48
  this._distance = -1;
29
49
  }
30
50
 
31
- private _distance: number = -1;
51
+ onEnable(): void {
52
+ this._distance = -1;
53
+ window.addEventListener('pointermove', this._onPointerMove);
54
+ }
55
+ onDisable(): void {
56
+ window.removeEventListener('pointermove', this._onPointerMove);
57
+ }
32
58
 
33
- updateDistance() {
34
- if (this.keepDistance && this._distance !== -1) {
35
- return;
36
- }
37
- this._distance = this.gameObject.worldPosition.distanceTo(this.context.mainCamera.worldPosition);
59
+ private _ndc_x = 0;
60
+ private _ndc_y = 0;
61
+
62
+ private _onPointerMove = (e:PointerEvent) => {
63
+ if(!this.useFullPage) return;
64
+ const x = e.clientX;
65
+ const y = e.clientY;
66
+ const domx = this.context.domX;
67
+ const domy = this.context.domY;
68
+ const domw = this.context.domWidth;
69
+ const domh = this.context.domHeight;
70
+ this._ndc_x = (x - domx) / domw * 2 - 1;
71
+ this._ndc_y = - (y - domy) / domh * 2 + 1;
38
72
  }
39
73
 
74
+
40
75
  /** @internal */
41
76
  update() {
42
77
  // continuously update distance in case camera or object moves
43
78
  this.updateDistance();
44
79
 
80
+ const x = this.useFullPage ? this._ndc_x : this.context.input.mousePositionRC.x;
81
+ const y = this.useFullPage ? this._ndc_y : this.context.input.mousePositionRC.y;
82
+
45
83
  // follow cursor in screenspace but maintain initial distance from camera
46
- const cursor = this.context.input.mousePositionRC;
47
84
  const camera = this.context.mainCamera;
48
85
  const cameraPosition = camera.worldPosition;
49
86
 
50
87
  // create ray from camera through cursor position
51
- const rayDirection = getTempVector(cursor.x, cursor.y, 1).unproject(camera);
88
+ const rayDirection = getTempVector(x, y, 1).unproject(camera);
52
89
  rayDirection.sub(cameraPosition).normalize();
53
90
 
54
91
  // position object at initial distance along the ray