@needle-tools/engine 4.10.5-next.a5d5bf4 → 4.11.0-beta.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{gltf-progressive-B63NpN_i.js → gltf-progressive-CXVECA3a.js} +1 -1
  4. package/dist/{needle-engine.bundle-D56E0HeK.min.js → needle-engine.bundle-6j5gE-aQ.min.js} +137 -137
  5. package/dist/{needle-engine.bundle-B2qX4saI.js → needle-engine.bundle-BDZ09xyt.js} +6589 -6438
  6. package/dist/{needle-engine.bundle-DPHrCUDs.umd.cjs → needle-engine.bundle-CFc4UIqz.umd.cjs} +139 -139
  7. package/dist/needle-engine.d.ts +14 -0
  8. package/dist/needle-engine.js +321 -320
  9. package/dist/needle-engine.min.js +1 -1
  10. package/dist/needle-engine.umd.cjs +1 -1
  11. package/lib/engine/codegen/register_types.js +2 -0
  12. package/lib/engine/codegen/register_types.js.map +1 -1
  13. package/lib/engine/engine_gizmos.js +2 -2
  14. package/lib/engine/engine_gizmos.js.map +1 -1
  15. package/lib/engine/engine_physics.js +44 -14
  16. package/lib/engine/engine_physics.js.map +1 -1
  17. package/lib/engine/js-extensions/Object3D.d.ts +14 -0
  18. package/lib/engine/js-extensions/Object3D.js +13 -0
  19. package/lib/engine/js-extensions/Object3D.js.map +1 -1
  20. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +2 -1
  21. package/lib/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js.map +1 -1
  22. package/lib/engine-components/Renderer.js +33 -32
  23. package/lib/engine-components/Renderer.js.map +1 -1
  24. package/lib/engine-components/RendererLightmap.d.ts +7 -5
  25. package/lib/engine-components/RendererLightmap.js +29 -30
  26. package/lib/engine-components/RendererLightmap.js.map +1 -1
  27. package/lib/engine-components/SeeThrough.d.ts +70 -0
  28. package/lib/engine-components/SeeThrough.js +223 -0
  29. package/lib/engine-components/SeeThrough.js.map +1 -0
  30. package/lib/engine-components/codegen/components.d.ts +1 -0
  31. package/lib/engine-components/codegen/components.js +1 -0
  32. package/lib/engine-components/codegen/components.js.map +1 -1
  33. package/lib/engine-components/ui/Graphic.js +13 -1
  34. package/lib/engine-components/ui/Graphic.js.map +1 -1
  35. package/lib/engine-components/ui/RaycastUtils.js +5 -3
  36. package/lib/engine-components/ui/RaycastUtils.js.map +1 -1
  37. package/lib/engine-components/utils/LookAt.js +4 -2
  38. package/lib/engine-components/utils/LookAt.js.map +1 -1
  39. package/lib/engine-components/web/Clickthrough.d.ts +2 -1
  40. package/lib/engine-components/web/Clickthrough.js +2 -1
  41. package/lib/engine-components/web/Clickthrough.js.map +1 -1
  42. package/lib/engine-components/web/CursorFollow.d.ts +7 -1
  43. package/lib/engine-components/web/CursorFollow.js +35 -2
  44. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  45. package/lib/engine-components/web/ScrollFollow.js +1 -2
  46. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  47. package/package.json +2 -2
  48. package/plugins/common/needle-engine.js +41 -0
  49. package/plugins/common/worker.js +129 -0
  50. package/plugins/vite/asap.js +5 -23
  51. package/plugins/vite/dependencies.js +21 -11
  52. package/plugins/vite/index.js +7 -0
  53. package/plugins/vite/needle-app.js +194 -0
  54. package/src/engine/codegen/register_types.ts +2 -0
  55. package/src/engine/engine_gizmos.ts +3 -3
  56. package/src/engine/engine_physics.ts +51 -14
  57. package/src/engine/js-extensions/Object3D.ts +32 -0
  58. package/src/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +3 -1
  59. package/src/engine-components/Renderer.ts +38 -37
  60. package/src/engine-components/RendererLightmap.ts +31 -33
  61. package/src/engine-components/SeeThrough.ts +256 -0
  62. package/src/engine-components/codegen/components.ts +1 -0
  63. package/src/engine-components/ui/Graphic.ts +13 -1
  64. package/src/engine-components/ui/RaycastUtils.ts +9 -8
  65. package/src/engine-components/utils/LookAt.ts +4 -1
  66. package/src/engine-components/web/Clickthrough.ts +2 -1
  67. package/src/engine-components/web/CursorFollow.ts +48 -7
  68. package/src/engine-components/web/ScrollFollow.ts +1 -1
@@ -0,0 +1,194 @@
1
+ import { writeFile } from 'fs';
2
+ import { tryParseNeedleEngineSrcAttributeFromHtml } from '../common/needle-engine.js';
3
+
4
+
5
+
6
+ /**
7
+ * @param {'serve' | 'build'} command
8
+ * @param {{} | undefined | null} config
9
+ * @param {import('../types').userSettings} userSettings
10
+ * @returns {import('vite').Plugin[] | null}
11
+ */
12
+ export const needleApp = (command, config, userSettings) => {
13
+
14
+ if (command !== "build") {
15
+ return null;
16
+ }
17
+
18
+ /** @type {Array<import("rollup").OutputChunk>} */
19
+ const entryFiles = new Array();
20
+
21
+ let outputDir = "dist";
22
+
23
+ /**
24
+ * @type {import('vite').Plugin}
25
+ */
26
+ return [
27
+ {
28
+ name: 'needle:app',
29
+ enforce: "post",
30
+ configResolved(config) {
31
+ outputDir = config.build.outDir || "dist";
32
+ },
33
+ transformIndexHtml: {
34
+ handler: async function (html, context) {
35
+ const name = context.filename;
36
+ if (name.includes("index.html")) {
37
+ if (context.chunk?.isEntry) {
38
+ try {
39
+ entryFiles.push(context.chunk);
40
+ const path = context.chunk.fileName;
41
+ // console.log("[needle-dependencies] entry chunk imports", {
42
+ // name: context.chunk.fileName,
43
+ // imports: context.chunk.imports,
44
+ // dynamicImports: context.chunk.dynamicImports,
45
+ // refs: context.chunk.referencedFiles,
46
+ // });
47
+
48
+ // TODO: here we try to find the main asset (entrypoint) for this web app. It's a bit hacky right now but we have to assign this url directly to make it work with `gen.js` where multiple needle-apps are on different (or the same) pages.
49
+ const attribute_src = tryParseNeedleEngineSrcAttributeFromHtml(html);
50
+ const imported_glbs = Array.from(context.chunk.viteMetadata?.importedAssets?.values() || [])?.filter(f => f.endsWith(".glb") || f.endsWith(".gltf"));
51
+
52
+ const main_asset = attribute_src?.[0] || imported_glbs?.[0];
53
+
54
+ const webComponent = generateNeedleEmbedWebComponent(path, main_asset);
55
+ await writeFile(`${outputDir}/needle-app.js`, webComponent, (err) => {
56
+ if (err) {
57
+ console.error("[needle-app] could not create needle-app.js", err);
58
+ }
59
+ else {
60
+ console.log("[needle-app] created needle-app.js");
61
+ }
62
+ });
63
+ }
64
+ catch (e) {
65
+ console.warn("WARN: could not create needle-app.js\n", e);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ },
71
+
72
+ }
73
+ ]
74
+ }
75
+
76
+
77
+ /**
78
+ * @param {string} filepath
79
+ * @param {string | null} src
80
+ * @returns {string}
81
+ */
82
+ function generateNeedleEmbedWebComponent(filepath, src) {
83
+
84
+
85
+ // filepath is e.g. `assets/index-XXXXXXXX.js`
86
+ // we want to make sure the path is correct relative to where the component will be used
87
+ // this script will be emitted in the output directory root (e.g. needle-embed.js)
88
+
89
+ const componentDefaultName = 'needle-app';
90
+
91
+ return `
92
+
93
+ // Needle Engine attributes we want to allow to be overriden
94
+ const knownAttributes = [
95
+ "src",
96
+ "background-color",
97
+ "background-image",
98
+ "environment-image",
99
+ "focus-rect",
100
+ ];
101
+
102
+ const scriptUrl = new URL(import.meta.url);
103
+ const componentName = scriptUrl.searchParams.get("component-name") || '${componentDefaultName}';
104
+
105
+
106
+ if (!customElements.get(componentName)) {
107
+ console.debug(\`Defining needle-app as <\${componentName}>\`);
108
+ customElements.define(componentName, class extends HTMLElement {
109
+
110
+ static get observedAttributes() {
111
+ return knownAttributes;
112
+ }
113
+
114
+ constructor() {
115
+ super();
116
+ this.attachShadow({ mode: 'open' });
117
+ const template = document.createElement('template');
118
+ template.innerHTML = \`
119
+ <style>
120
+ :host {
121
+ position: relative;
122
+ display: block;
123
+ width: max(360px 100%);
124
+ height: max(240px, 100%);
125
+ margin: 0;
126
+ padding: 0;
127
+ }
128
+ needle-engine {
129
+ position: absolute;
130
+ top: 0;
131
+ left: 0;
132
+ width: 100%;
133
+ height: 100%;
134
+ }
135
+ </style>
136
+ \`;
137
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
138
+
139
+ const script = document.createElement('script');
140
+ script.type = 'module';
141
+ const url = new URL('.', import.meta.url);
142
+ this.basePath = this.getAttribute('base-path') || \`\${url.protocol}//\${url.host}\${url.pathname}\`;
143
+ while(this.basePath.endsWith('/')) {
144
+ this.basePath = this.basePath.slice(0, -1);
145
+ }
146
+ script.src = this.getAttribute('script-src') || \`\${this.basePath}/${filepath}\`;
147
+ this.shadowRoot.appendChild(script);
148
+
149
+ this.needleEngine = document.createElement('needle-engine');
150
+ this.updateAttributes();
151
+ this.shadowRoot.appendChild(this.needleEngine);
152
+
153
+ console.debug(this.basePath, script.src, this.needleEngine.getAttribute("src"));
154
+ }
155
+
156
+ onConnectedCallback() {
157
+ console.debug('NeedleEmbed connected to the DOM');
158
+ }
159
+
160
+ disconnectedCallback() {
161
+ console.debug('NeedleEmbed disconnected from the DOM');
162
+ }
163
+
164
+ attributeChangedCallback(name, oldValue, newValue) {
165
+ console.debug(\`NeedleApp attribute changed: \${name} from \${oldValue} to \${newValue}\`);
166
+ this.updateAttributes();
167
+ }
168
+
169
+ updateAttributes() {
170
+ console.debug("NeedleApp updating attributes");
171
+
172
+ const src = this.getAttribute('src') || ${src?.length ? `\`\${this.basePath}/${src}\`` : null};
173
+ if(src) this.needleEngine.setAttribute("src", src);
174
+ else this.needleEngine.removeAttribute("src");
175
+
176
+ for(const attr of knownAttributes) {
177
+
178
+ if(attr === "src") continue; // already handled above
179
+
180
+ if(this.hasAttribute(attr)) {
181
+ this.needleEngine.setAttribute(attr, this.getAttribute(attr));
182
+ }
183
+ else {
184
+ this.needleEngine.removeAttribute(attr);
185
+ }
186
+ }
187
+ }
188
+ });
189
+ }
190
+ else {
191
+ console.warn(\`needle-app <\${componentName}> already defined.\`);
192
+ }
193
+ `
194
+ }
@@ -90,6 +90,7 @@ import { SkinnedMeshRenderer } from "../../engine-components/Renderer.js";
90
90
  import { Rigidbody } from "../../engine-components/RigidBody.js";
91
91
  import { SceneSwitcher } from "../../engine-components/SceneSwitcher.js";
92
92
  import { ScreenCapture } from "../../engine-components/ScreenCapture.js";
93
+ import { SeeThrough } from "../../engine-components/SeeThrough.js";
93
94
  import { ShadowCatcher } from "../../engine-components/ShadowCatcher.js";
94
95
  import { RemoteSkybox } from "../../engine-components/Skybox.js";
95
96
  import { SmoothFollow } from "../../engine-components/SmoothFollow.js";
@@ -249,6 +250,7 @@ TypeStore.add("SkinnedMeshRenderer", SkinnedMeshRenderer);
249
250
  TypeStore.add("Rigidbody", Rigidbody);
250
251
  TypeStore.add("SceneSwitcher", SceneSwitcher);
251
252
  TypeStore.add("ScreenCapture", ScreenCapture);
253
+ TypeStore.add("SeeThrough", SeeThrough);
252
254
  TypeStore.add("ShadowCatcher", ShadowCatcher);
253
255
  TypeStore.add("RemoteSkybox", RemoteSkybox);
254
256
  TypeStore.add("SmoothFollow", SmoothFollow);
@@ -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;
@@ -560,6 +572,9 @@ declare module 'three' {
560
572
 
561
573
 
562
574
  namespace NEMeshBVH {
575
+
576
+ let failedToCreateMeshBVHWorker = 0;
577
+
563
578
  export function runMeshBVHRaycast(method: Raycaster | Sphere, mesh: Mesh, results: Intersection[], context: Pick<Context, "xr">, options: { allowSlowRaycastFallback?: boolean }): boolean {
564
579
  if (!mesh.geometry) {
565
580
  return false;
@@ -618,6 +633,10 @@ namespace NEMeshBVH {
618
633
  || geom.index && geom.index?.["isInterleavedBufferAttribute"]) {
619
634
  canUseWorker = false;
620
635
  }
636
+ else if (failedToCreateMeshBVHWorker > 10) {
637
+ // if we failed to create a worker multiple times, don't try again
638
+ canUseWorker = false;
639
+ }
621
640
 
622
641
  // if we have a worker use that
623
642
  if (canUseWorker && _GenerateMeshBVHWorker) {
@@ -634,8 +653,23 @@ namespace NEMeshBVH {
634
653
  }
635
654
  // if there are no workers available, create a new one
636
655
  if (!workerInstance && workerInstances.length < 3) {
637
- workerInstance = new _GenerateMeshBVHWorker();
638
- workerInstances.push(workerInstance);
656
+ try {
657
+ workerInstance = new _GenerateMeshBVHWorker();
658
+ workerInstances.push(workerInstance);
659
+ }
660
+ catch (err) {
661
+ const isSecurityError = err instanceof DOMException && err.name === "SecurityError";
662
+ if (isSecurityError) {
663
+ console.warn("Failed to create MeshBVH worker, falling back to main thread generation. This can happen when running from file://, if the browser does not support workers or if the browser is blocking workers for other reasons.");
664
+ console.debug(err);
665
+ failedToCreateMeshBVHWorker += 10;
666
+ }
667
+ else {
668
+ console.error("Failed to create MeshBVH worker");
669
+ console.debug(err);
670
+ }
671
+ failedToCreateMeshBVHWorker++;
672
+ }
639
673
  }
640
674
 
641
675
  if (workerInstance != null && !workerInstance.running) {
@@ -794,6 +828,9 @@ namespace NEMeshBVH {
794
828
  if (debugPhysics || isDevEnvironment()) {
795
829
  console.warn("Failed to setup mesh bvh worker");
796
830
  }
831
+ else {
832
+ console.debug("Failed to setup mesh bvh worker", _err);
833
+ }
797
834
  })
798
835
  .finally(() => {
799
836
  isRequestingWorker = 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 () {
@@ -4,14 +4,16 @@ import { MeshBVH } from 'three-mesh-bvh';
4
4
  // Modified according to https://github.com/gkjohnson/three-mesh-bvh/issues/636#issuecomment-2209571751
5
5
  import { WorkerBase } from "three-mesh-bvh/src/workers/utils/WorkerBase.js";
6
6
 
7
+
7
8
  export class GenerateMeshBVHWorker extends WorkerBase {
8
9
 
9
10
  constructor() {
10
11
  // TODO: make mesh bvh worker "work" for prebundled CDN loading
11
12
  // https://linear.app/needle/issue/NE-6572
12
13
  // Also we don't use toplevel imports to not completely fail to load needle-engine where loading the worker fails
13
- // const url = new URL('three-mesh-bvh/src/workers/generateMeshBVH.worker.js', import.meta.url)
14
+ // const meta_url = import.meta.url;
14
15
  // const getWorker = () => new Worker(url, { type: 'module' })
16
+ // console.log(meta_url, url, getWorker());
15
17
  super(new Worker(new URL('three-mesh-bvh/src/workers/generateMeshBVH.worker.js', import.meta.url), { type: 'module' }));
16
18
  this.name = 'GenerateMeshBVHWorker';
17
19
 
@@ -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
  }