Needle Engine

Changes between version 3.8.0-alpha and 3.9.0-alpha
Files changed (16) hide show
  1. src/engine/codegen/register_types.js +2 -0
  2. src/engine-components/ui/Button.ts +3 -1
  3. src/engine-components/codegen/components.ts +1 -0
  4. src/engine/engine_context.ts +1 -1
  5. src/engine/engine_element_overlay.ts +41 -52
  6. src/engine/engine_element.ts +15 -8
  7. src/engine-components/ui/EventSystem.ts +3 -1
  8. src/engine/extensions/extensions.ts +1 -1
  9. src/engine-components/ui/Graphic.ts +3 -3
  10. src/engine-components/Renderer.ts +19 -14
  11. src/engine-components/RendererLightmap.ts +36 -31
  12. src/engine-components/export/usdz/ThreeUSDZExporter.ts +43 -37
  13. src/engine-components/export/usdz/USDZExporter.ts +2 -0
  14. src/engine-components/export/usdz/extensions/USDZText.ts +41 -1
  15. src/engine-components/webxr/WebXR.ts +1 -1
  16. src/engine-components/export/usdz/extensions/USDZUI.ts +120 -0
src/engine/codegen/register_types.js CHANGED
@@ -189,6 +189,7 @@
189
189
  import { UsageMarker } from "../../engine-components/Interactable";
190
190
  import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter";
191
191
  import { USDZText } from "../../engine-components/export/usdz/extensions/USDZText";
192
+ import { USDZUIExtension } from "../../engine-components/export/usdz/extensions/USDZUI";
192
193
  import { VariantAction } from "../../engine-components/export/usdz/extensions/behavior/Actions";
193
194
  import { VelocityOverLifetimeModule } from "../../engine-components/ParticleSystemModules";
194
195
  import { VerticalLayoutGroup } from "../../engine-components/ui/Layout";
@@ -406,6 +407,7 @@
406
407
  TypeStore.add("UsageMarker", UsageMarker);
407
408
  TypeStore.add("USDZExporter", USDZExporter);
408
409
  TypeStore.add("USDZText", USDZText);
410
+ TypeStore.add("USDZUIExtension", USDZUIExtension);
409
411
  TypeStore.add("VariantAction", VariantAction);
410
412
  TypeStore.add("VelocityOverLifetimeModule", VelocityOverLifetimeModule);
411
413
  TypeStore.add("VerticalLayoutGroup", VerticalLayoutGroup);
src/engine-components/ui/Button.ts CHANGED
@@ -116,8 +116,10 @@
116
116
  }
117
117
  }
118
118
 
119
- onPointerClick(_args: PointerEventData) {
119
+ onPointerClick(args: PointerEventData) {
120
120
  if (!this.interactable) return;
121
+ // Button clicks should only run with left mouse button
122
+ if(args.pointerId !== 0) return;
121
123
  if (debug) {
122
124
  console.warn("Button Click", this.onClick);
123
125
  showBalloonMessage("CLICKED button " + this.name + " at " + this.context.time.frameCount);
src/engine-components/codegen/components.ts CHANGED
@@ -184,6 +184,7 @@
184
184
  export { UsageMarker } from "../Interactable";
185
185
  export { USDZExporter } from "../export/usdz/USDZExporter";
186
186
  export { USDZText } from "../export/usdz/extensions/USDZText";
187
+ export { USDZUIExtension } from "../export/usdz/extensions/USDZUI";
187
188
  export { VariantAction } from "../export/usdz/extensions/behavior/Actions";
188
189
  export { VelocityOverLifetimeModule } from "../ParticleSystemModules";
189
190
  export { VerticalLayoutGroup } from "../ui/Layout";
src/engine/engine_context.ts CHANGED
@@ -538,7 +538,7 @@
538
538
  this._onBeforeRenderListeners.set(target.uuid, []);
539
539
  target.onBeforeRender = this._createRenderCallbackWrapper(target, this._onBeforeRenderListeners);
540
540
  }
541
- this._onBeforeRenderListeners.get(target.uuid)?.push(callback);
541
+ this._onBeforeRenderListeners.get(target.uuid)!.push(callback);
542
542
  }
543
543
  removeBeforeRenderListener(target: Object3D, callback: OnRenderCallback) {
544
544
  if (this._onBeforeRenderListeners.has(target.uuid)) {
src/engine/engine_element_overlay.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Context } from "./engine_setup";
2
- import { getParam, isMozillaXR } from "./engine_utils";
2
+ import { getParam, isMobileDevice, isMozillaXR } from "./engine_utils";
3
3
 
4
4
  const debug = getParam("debugaroverlay");
5
5
  export const arContainerClassName = "ar";
@@ -21,6 +21,7 @@
21
21
 
22
22
  private _createdAROnlyElements: Array<any> = [];
23
23
  private _reparentedObjects: Array<{ el: Element, previousParent: HTMLElement | null }> = [];
24
+ private contentElement: HTMLElement | null = null;
24
25
 
25
26
  requestEndAR() {
26
27
  this.onRequestedEndAR();
@@ -30,13 +31,16 @@
30
31
  this.currentSession = session;
31
32
  this.arContainer = overlayContainer;
32
33
 
33
- const arElements = context.domElement.shadowRoot!.querySelectorAll(`.${arContainerClassName}`);
34
- arElements.forEach(el => {
35
- if (!el) return;
36
- if (el === this.arContainer) return;
37
- this._reparentedObjects.push({ el: el, previousParent: el.parentElement });
38
- this.arContainer?.appendChild(el);
39
- });
34
+ if (isMozillaXR()) {
35
+ const arElements = context.domElement!.children;
36
+ for (let i = 0; i < arElements?.length; i++) {
37
+ const el = arElements[i];
38
+ if (!el) return;
39
+ if (el === this.arContainer) return;
40
+ this._reparentedObjects.push({ el: el, previousParent: el.parentElement });
41
+ this.arContainer?.appendChild(el);
42
+ }
43
+ }
40
44
 
41
45
  const quit_Elements = overlayContainer.getElementsByClassName(quitARClassName);
42
46
  if (!quit_Elements || quit_Elements.length <= 0) {
@@ -91,46 +95,20 @@
91
95
  }
92
96
  }
93
97
 
94
- findOrCreateARContainer(element: HTMLElement): HTMLElement {
95
- if(debug) console.log("findOrCreateARContainer");
96
- // search in the needle-engine element
97
- if (element.classList.contains(arContainerClassName)) {
98
- if(debug) console.log("Found overlay container in needle-engine element");
99
- return element;
100
- }
101
- if (element.shadowRoot) {
102
- const el = element.shadowRoot!.querySelector(`.${arContainerClassName}`);
103
- if (el) {
104
- if(debug) console.log("Found overlay container in needle-engine element");
105
- return el as HTMLElement;
106
- };
107
- }
108
98
 
109
- // search in document as well; "ar" element could live outside needle-engine element
110
- const arElements = document.getElementsByClassName(arContainerClassName);
111
- if (arElements && arElements.length > 0){
112
- if(debug) console.log("Found overlay container in document");
113
- return arElements[0] as HTMLElement;
114
- }
99
+ createOverlayContainer(needleEngineElement: HTMLElement): HTMLElement {
100
+ if (this.contentElement) return this.contentElement;
115
101
 
116
102
  if (debug)
117
- console.log("No overlay container found in document - generating new ony");
118
- const el = document.createElement("div");
119
- el.classList.add(arContainerClassName);
120
- el.style.cssText = `
121
- position: fixed;
122
- top: 0;
123
- left: 0;
124
- width: 100%;
125
- height: 100%;
126
- display: flex;
127
- visibility: visible;
128
- z-index: 9999;
129
- pointer-events: none;
130
- // background: rgba(0,0,0,1);
131
- `;
132
- if(debug) this.createFallbackCloseARButton(element);
133
- return this.appendElement(el, element) as HTMLElement;
103
+ console.log("Setup overlay container");
104
+
105
+ const contentElement = needleEngineElement.shadowRoot!.querySelector(".content") as HTMLElement;
106
+ this.contentElement = contentElement;
107
+
108
+ const overlaySlot = needleEngineElement.shadowRoot!.querySelector(".overlay-content");
109
+ if (overlaySlot) contentElement.appendChild(overlaySlot);
110
+ if (debug && !isMobileDevice()) this.createFallbackCloseARButton(contentElement);
111
+ return contentElement;
134
112
  }
135
113
 
136
114
  private onRequestedEndAR() {
@@ -147,30 +125,41 @@
147
125
  private createFallbackCloseARButton(element: HTMLElement) {
148
126
  const quitARSlot = document.createElement("slot");
149
127
  quitARSlot.setAttribute("name", "quit-ar");
150
- this.appendElement(quitARSlot, element);
151
- if(debug) quitARSlot.addEventListener('click', () => console.log("Clicked fallback close button"));
152
- quitARSlot.addEventListener('click', this.closeARCallback);
128
+ this.appendElement(quitARSlot, element);
129
+ if (debug) quitARSlot.addEventListener('click', () => console.log("Clicked fallback close button"));
130
+ quitARSlot.addEventListener('click', this.closeARCallback);
153
131
  this._createdAROnlyElements.push(quitARSlot);
154
132
  // for mozilla XR reparenting we have to make sure the close button is clickable so we set it on the element directly
155
133
  // it's in general perhaps more safe to set it on the element to ensure it's clickable
156
134
  quitARSlot.style.pointerEvents = "auto";
157
135
 
136
+ // we need another container to make sure the button is always on top
137
+ const fixedButtonContainer = document.createElement("div");
138
+ fixedButtonContainer.style.cssText = `
139
+ position: fixed;
140
+ top: 0;
141
+ right: 0;
142
+ z-index: 600;
143
+ pointer-events: all;
144
+ `;
145
+ this.appendElement(fixedButtonContainer, quitARSlot);
146
+
158
147
  var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
159
148
  svg.classList.add("quit-ar-button");
160
149
  svg.setAttribute('width', "38px");
161
150
  svg.setAttribute('height', "38px");
162
- quitARSlot.appendChild(svg);
151
+ fixedButtonContainer.appendChild(svg);
163
152
 
164
153
  var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
165
154
  path.setAttribute('d', 'M 12,12 L 28,28 M 28,12 12,28');
166
- path.setAttribute('stroke', '#ddd');
155
+ path.setAttribute('stroke', '#aaa');
167
156
  path.setAttribute('stroke-width', "3px");
168
157
  svg.appendChild(path);
169
- if(debug) console.log("Created fallback close button", svg, element);
158
+ if (debug) console.log("Created fallback close button", svg, element);
170
159
  }
171
160
 
172
161
  private appendElement(element: Element, parent: HTMLElement) {
173
- if(parent.shadowRoot) return parent.shadowRoot.appendChild(element);
162
+ if (parent.shadowRoot) return parent.shadowRoot.appendChild(element);
174
163
  return parent.appendChild(element);
175
164
  }
176
165
 
src/engine/engine_element.ts CHANGED
@@ -109,6 +109,17 @@
109
109
  user-select: none;
110
110
  touch-action: none;
111
111
  }
112
+ :host .content {
113
+ position: fixed;
114
+ visibility: visible;
115
+ z-index: 500; /* < must be less than the webxr buttons element */
116
+ pointer-events: none;
117
+ }
118
+ :host .overlay-content {
119
+ position: absolute;
120
+ user-select: auto;
121
+ pointer-events: all;
122
+ }
112
123
  :host slot[name="quit-ar"]:hover {
113
124
  cursor: pointer;
114
125
  }
@@ -120,6 +131,9 @@
120
131
  }
121
132
  </style>
122
133
  <canvas></canvas>
134
+ <div class="content">
135
+ <slot class="overlay-content"></slot>
136
+ </div>
123
137
  `;
124
138
 
125
139
  if (this.shadowRoot)
@@ -407,7 +421,7 @@
407
421
  }
408
422
 
409
423
  getAROverlayContainer(): HTMLElement {
410
- return this._overlay_ar.findOrCreateARContainer(this);
424
+ return this._overlay_ar.createOverlayContainer(this);
411
425
  }
412
426
 
413
427
  getVROverlayContainer(): HTMLElement | null {
@@ -473,13 +487,6 @@
473
487
  private setupElementsForMode(el: HTMLElement, currentSessionType: string, _session: XRSession | null = null) {
474
488
  if (el === this._context?.renderer.domElement) return;
475
489
  if (el.id === "VRButton" || el.id === "ARButton") return;
476
- el.style.position = "absolute";
477
- // el.style.zIndex = "100";
478
- // ch.style.width = "100hv";
479
- // ch.style.height = "100hv";
480
- // set pointer events to none by default (if not explicitly declared)
481
- // if (!el.style.pointerEvents)
482
- // el.style.pointerEvents = "none";
483
490
 
484
491
  const classList = el.classList;
485
492
  if (classList.contains(currentSessionType)) {
src/engine-components/ui/EventSystem.ts CHANGED
@@ -127,6 +127,7 @@
127
127
  MeshUIHelper.resetLastSelected();
128
128
  const opts = new PointerEventData(this.context.input);
129
129
  opts.inputSource = ctrl;
130
+ opts.pointerId = 0;
130
131
  opts.isDown = ctrl.selectionDown;
131
132
  opts.isUp = ctrl.selectionUp;
132
133
  opts.isPressed = ctrl.selectionPressed;
@@ -136,10 +137,11 @@
136
137
  args.grab = null;
137
138
  };
138
139
  }
139
- this._selectEndFn ??= (ctrl, args: { grab: THREE.Object3D }) => {
140
+ this._selectEndFn ??= (ctrl: WebXRController, args: { grab: THREE.Object3D }) => {
140
141
  if (!args.grab) return;
141
142
  const opts = new PointerEventData(this.context.input);
142
143
  opts.inputSource = ctrl;
144
+ opts.pointerId = 0;
143
145
  opts.isDown = ctrl.selectionDown;
144
146
  opts.isUp = ctrl.selectionUp;
145
147
  opts.isPressed = ctrl.selectionPressed;
src/engine/extensions/extensions.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  import { NEEDLE_progressive } from "./NEEDLE_progressive";
16
16
  import { InternalUsageTrackerPlugin } from "./usage_tracker";
17
17
  import { isUsageTrackingEnabled } from "../engine_assetdatabase";
18
- import { GLTFLoaderPlugin } from "three/examples/jsm/loaders/GLTFLoader";
18
+ import { GLTFLoaderPlugin } from "three/examples/jsm/loaders/GLTFLoader.js";
19
19
  // import { GLTFAnimationPointerExtension } from "three/examples/jsm/loaders/GLTFLoaderAnimationPointer";
20
20
 
21
21
  // lazily import the GLTFAnimationPointerExtension in case it doesnt exist (e.g. using vanilla three)
src/engine-components/ui/Graphic.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { RGBAColor } from "../js-extensions/RGBAColor"
4
4
  import { BaseUIComponent } from "./BaseUIComponent";
5
5
  import { serializable } from '../../engine/engine_serialization_decorator';
6
- import { Color, LinearEncoding, sRGBEncoding, Texture } from 'three';
6
+ import { Color, LinearSRGBColorSpace, SRGBColorSpace, Texture } from 'three';
7
7
  import { RectTransform } from './RectTransform';
8
8
  import { onChange, scheduleAction } from "./Utils"
9
9
  import { GameObject } from '../Component';
@@ -191,12 +191,12 @@
191
191
  this.setOptions({ backgroundOpacity: 0 });
192
192
  if (tex) {
193
193
  // workaround for https://github.com/needle-tools/needle-engine-support/issues/109
194
- if (tex.encoding === sRGBEncoding) {
194
+ if (tex.colorSpace === SRGBColorSpace) {
195
195
  if (Graphic.textureCache.has(tex)) {
196
196
  tex = Graphic.textureCache.get(tex)!;
197
197
  } else {
198
198
  const clone = tex.clone();
199
- clone.encoding = LinearEncoding;
199
+ clone.colorSpace = LinearSRGBColorSpace;
200
200
  Graphic.textureCache.set(tex, clone);
201
201
  tex = clone;
202
202
  }
src/engine-components/Renderer.ts CHANGED
@@ -352,7 +352,7 @@
352
352
 
353
353
  if (this.isMultiMaterialObject(this.gameObject)) {
354
354
  for (const child of this.gameObject.children) {
355
- this.context.addBeforeRenderListener(child, this.onBeforeRenderThree.bind(this));
355
+ this.context.addBeforeRenderListener(child, this.onBeforeRenderThree);
356
356
  child.layers.mask = this.gameObject.layers.mask;
357
357
  }
358
358
 
@@ -376,14 +376,13 @@
376
376
  }
377
377
  // TODO: custom shader with sub materials
378
378
  else if (this.isMeshOrSkinnedMesh(this.gameObject)) {
379
+ this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThree);
379
380
 
380
- this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThree.bind(this));
381
-
382
381
  if (this.renderOrder !== undefined && this.renderOrder.length > 0)
383
382
  this.gameObject.renderOrder = this.renderOrder[0];
384
383
  }
385
384
  else {
386
- this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThree.bind(this));
385
+ this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThree);
387
386
  }
388
387
 
389
388
  this.applyLightmapping();
@@ -415,7 +414,7 @@
415
414
  const mat = this.gameObject["material"];
416
415
  if (!mat?.isMeshBasicMaterial) {
417
416
  if (this._lightmaps.length <= 0) {
418
- const rm = new RendererLightmap(this.gameObject, this.context);
417
+ const rm = new RendererLightmap(this.gameObject as any as Mesh, this.context);
419
418
  this._lightmaps.push(rm);
420
419
  }
421
420
  const rm = this._lightmaps[0];
@@ -434,16 +433,12 @@
434
433
  if (!child["material"]?.isMeshBasicMaterial) {
435
434
  let rm: RendererLightmap | undefined = undefined;
436
435
  if (i >= this._lightmaps.length) {
437
- rm = new RendererLightmap(child as GameObject, this.context);
436
+ rm = new RendererLightmap(child as Mesh, this.context);
438
437
  this._lightmaps.push(rm);
439
438
  }
440
439
  else
441
440
  rm = this._lightmaps[i];
442
441
  rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex);
443
- // onBeforeRender is not called when the renderer is on a group
444
- // this is an issue we probably also need to handle for custom shaders
445
- // and need a better solution, but for now this fixes lightmaps for multimaterial objects
446
- rm.bindOnBeforeRender();
447
442
  }
448
443
  }
449
444
  }
@@ -529,7 +524,6 @@
529
524
  }
530
525
 
531
526
  this.updateReflectionProbe();
532
-
533
527
  }
534
528
 
535
529
  onDisable() {
@@ -542,13 +536,22 @@
542
536
 
543
537
  onDestroy(): void {
544
538
  this.handles = null;
539
+
540
+ if (this.isMultiMaterialObject(this.gameObject)) {
541
+ for (const child of this.gameObject.children) {
542
+ this.context.removeBeforeRenderListener(child, this.onBeforeRenderThree);
543
+ }
544
+ }
545
+ else {
546
+ this.context.removeBeforeRenderListener(this.gameObject, this.onBeforeRenderThree);
547
+ }
548
+
545
549
  }
546
550
 
547
551
  applyStencil() {
548
552
  NEEDLE_render_objects.applyStencil(this);
549
553
  }
550
554
 
551
-
552
555
  onBeforeRender() {
553
556
  if (!this.gameObject) {
554
557
  return;
@@ -611,8 +614,10 @@
611
614
  }
612
615
 
613
616
  }
614
- onBeforeRenderThree(_renderer, _scene, _camera, _geometry, material, _group) {
615
617
 
618
+
619
+ private onBeforeRenderThree = (_renderer, _scene, _camera, _geometry, material, _group) => {
620
+
616
621
  this.loadProgressiveTextures(material);
617
622
 
618
623
  if (material.envMapIntensity !== undefined) {
@@ -656,7 +661,7 @@
656
661
 
657
662
  if (this._lightmaps) {
658
663
  for (const lm of this._lightmaps) {
659
- lm.onBeforeRenderThree(material);
664
+ lm.updateLightmapUniforms(material);
660
665
  }
661
666
  }
662
667
  }
src/engine-components/RendererLightmap.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Behaviour, GameObject } from "./Component";
2
- import { Material, Mesh, ShaderMaterial, Texture, Vector4 } from "three";
2
+ import { Material, Mesh, Shader, ShaderMaterial, Texture, Vector4 } from "three";
3
3
  import { Context, OnRenderCallback } from "../engine/engine_setup";
4
4
  import { getParam } from "../engine/engine_utils";
5
5
 
@@ -24,12 +24,12 @@
24
24
  lightmapScaleOffset: THREE.Vector4 = new Vector4(1, 1, 0, 0);
25
25
 
26
26
  private context: Context;
27
- private gameObject: GameObject;
27
+ private gameObject: Mesh;
28
28
  private lightmapTexture: Texture | null = null;
29
29
  private lightmapScaleOffsetUniform = { value: new Vector4(1, 1, 0, 0) };
30
30
  private lightmapUniform: { value: Texture | null } = { value: null };
31
31
 
32
- constructor(gameObject: GameObject, context: Context) {
32
+ constructor(gameObject: Mesh, context: Context) {
33
33
  this.gameObject = gameObject;
34
34
  this.context = context;
35
35
  }
@@ -46,19 +46,21 @@
46
46
  this.applyLightmap();
47
47
  }
48
48
 
49
- bindOnBeforeRender() {
50
- this.context.removeBeforeRenderListener(this.gameObject, this.onBeforeRenderThreeComplete);
51
- this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThreeComplete);
49
+ updateLightmapUniforms(material: Material) {
50
+ const uniforms = material["uniforms"];
51
+ if (uniforms && uniforms.lightmap) {
52
+ this.lightmapScaleOffsetUniform.value = this.lightmapScaleOffset;
53
+ this.lightmapUniform.value = this.lightmapTexture;
54
+ uniforms.lightmap = this.lightmapUniform;
55
+ uniforms.lightmapScaleOffset = this.lightmapScaleOffsetUniform;
56
+ }
52
57
  }
53
58
 
54
- private onBeforeRenderThreeComplete = (_renderer, _scene, _camera, _geometry, material, _group) => {
55
- this.onBeforeRenderThree(material);
56
- }
57
59
 
58
60
  private applyLightmap() {
59
-
60
61
  if (this.gameObject.type === "Object3D") {
61
- // console.warn("Can not add lightmap. Is this object missing a renderer?");
62
+ if (debug)
63
+ console.warn("Can not add lightmap. Is this object missing a renderer?", this.gameObject.name);
62
64
  return;
63
65
  }
64
66
 
@@ -70,23 +72,27 @@
70
72
  console.assert(this.gameObject.type === "Mesh", "Lightmap only works on meshes", this);
71
73
 
72
74
  const mesh = this.gameObject as unknown as Mesh;
73
- // TODO: ensure uv2 exists
74
75
  if (!mesh.geometry.getAttribute("uv1"))
75
76
  mesh.geometry.setAttribute("uv1", mesh.geometry.getAttribute("uv"));
76
77
 
77
- const mat: Material = this.gameObject["material"].clone();
78
- this.gameObject["material"] = mat;
78
+ if (Array.isArray(this.gameObject.material)) {
79
+ const mats: Material[] = this.gameObject.material;
80
+ for (let i = 0; i < mats.length; i++) {
81
+ const mat = mats[i];
82
+ const cloned = mat.clone();
83
+ mats[i] = cloned;
84
+ cloned.onBeforeCompile = this.onBeforeCompile;
85
+ }
86
+ }
87
+ else {
88
+ const mat: Material = this.gameObject.material.clone();
89
+ this.gameObject.material = mat;
90
+ this.gameObject.material.onBeforeCompile = this.onBeforeCompile;
91
+ }
79
92
 
80
- this.gameObject["material"].onBeforeCompile = (shader, _) => {
81
- shader.lightMapUv = "uv1";
82
- shader.uniforms.lightmap = this.lightmapUniform;
83
- shader.uniforms.lightmapScaleOffset = this.lightmapScaleOffsetUniform;
84
- };
85
-
86
-
87
93
  if (this.lightmapIndex >= 0) {
88
94
  const lightmap = this.lightmapTexture;
89
- const mat = this.gameObject["material"];
95
+ const mat = this.gameObject.material as any;
90
96
  if (mat && lightmap) {
91
97
  if (!mat.uniforms) mat.uniforms = {};
92
98
  mat.lightMap = lightmap;
@@ -95,15 +101,14 @@
95
101
  }
96
102
  }
97
103
 
98
- onBeforeRenderThree(material: THREE.Material) {
99
-
100
- const uniforms = material["uniforms"];
101
- if (uniforms && uniforms.lightmap) {
102
- this.lightmapScaleOffsetUniform.value = this.lightmapScaleOffset;
103
- this.lightmapUniform.value = this.lightmapTexture;
104
- uniforms.lightmap = this.lightmapUniform;
105
- uniforms.lightmapScaleOffset = this.lightmapScaleOffsetUniform;
106
- }
104
+ private onBeforeCompile = (shader: Shader, _) => {
105
+ if(debug) console.log("Lightmaps, before compile")
106
+ //@ts-ignore
107
+ shader.lightMapUv = "uv1";
108
+ this.lightmapScaleOffsetUniform.value = this.lightmapScaleOffset;
109
+ this.lightmapUniform.value = this.lightmapTexture;
110
+ shader.uniforms.lightmap = this.lightmapUniform;
111
+ shader.uniforms.lightmapScaleOffset = this.lightmapScaleOffsetUniform;
107
112
  }
108
113
 
109
114
  private setLightmapDebugMaterial() {
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -657,7 +657,7 @@
657
657
 
658
658
  if ( geometry ) {
659
659
 
660
- if ( material.isMeshStandardMaterial ) { // || material.isMeshBasicMaterial // TODO convert unlit to lit+emissive
660
+ if ( material.isMeshStandardMaterial || material.isMeshBasicMaterial ) { // TODO convert unlit to lit+emissive
661
661
 
662
662
  const geometryFileName = 'geometries/Geometry_' + geometry.id + '.usd';
663
663
 
@@ -1124,7 +1124,7 @@
1124
1124
 
1125
1125
  }
1126
1126
 
1127
- function buildMaterial( material: MeshStandardMaterial, textures, quickLookCompatible = false ) {
1127
+ function buildMaterial( material: MeshBasicMaterial, textures, quickLookCompatible = false ) {
1128
1128
 
1129
1129
  // https://graphics.pixar.com/usd/docs/UsdPreviewSurface-Proposal.html
1130
1130
 
@@ -1182,7 +1182,9 @@
1182
1182
 
1183
1183
  const needsTextureScale = mapType !== 'normal' && (color && (color.r !== 1 || color.g !== 1 || color.b !== 1 || opacity !== 1)) || false;
1184
1184
  const needsNormalScaleAndBias = mapType === 'normal';
1185
- const normalScaleValueString = (material.normalScale ? material.normalScale.x * 2 : 2).toFixed( PRECISION );
1185
+ const normalScaleValueString = material instanceof MeshStandardMaterial
1186
+ ? (material.normalScale ? material.normalScale.x * 2 : 2).toFixed( PRECISION )
1187
+ : "1";
1186
1188
 
1187
1189
  return `
1188
1190
  ${needsTextureTransform ? `def Shader "Transform2d_${mapType}" (
@@ -1254,73 +1256,77 @@
1254
1256
 
1255
1257
  }
1256
1258
 
1257
- if ( material.emissiveMap ) {
1259
+ if ( material.aoMap ) {
1258
1260
 
1259
- inputs.push( `${pad}color3f inputs:emissiveColor.connect = </Materials/Material_${material.id}/Texture_${material.emissiveMap.id}_emissive.outputs:rgb>` );
1261
+ inputs.push( `${pad}float inputs:occlusion.connect = </Materials/Material_${material.id}/Texture_${material.aoMap.id}_occlusion.outputs:r>` );
1260
1262
 
1261
- samplers.push( buildTexture( material.emissiveMap, 'emissive' ) );
1263
+ samplers.push( buildTexture( material.aoMap, 'occlusion' ) );
1262
1264
 
1263
- } else if ( material.emissive?.getHex() > 0 ) {
1265
+ }
1264
1266
 
1265
- inputs.push( `${pad}color3f inputs:emissiveColor = ${buildColor( material.emissive )}` );
1267
+ if ( material.alphaMap ) {
1266
1268
 
1269
+ inputs.push( `${pad}float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.alphaMap.id}_opacity.outputs:r>` );
1270
+ inputs.push( `${pad}float inputs:opacityThreshold = 0.0001` );
1271
+
1272
+ samplers.push( buildTexture( material.alphaMap, 'opacity' ) );
1273
+
1267
1274
  } else {
1268
-
1269
- inputs.push( `${pad}color3f inputs:emissiveColor = (0, 0, 0)` );
1270
1275
 
1276
+ inputs.push( `${pad}float inputs:opacity = ${effectiveOpacity}` );
1277
+
1271
1278
  }
1272
1279
 
1273
- if ( material.normalMap ) {
1280
+ if ( material instanceof MeshStandardMaterial ) {
1274
1281
 
1275
- inputs.push( `${pad}normal3f inputs:normal.connect = </Materials/Material_${material.id}/Texture_${material.normalMap.id}_normal.outputs:rgb>` );
1282
+ if ( material.emissiveMap ) {
1276
1283
 
1277
- samplers.push( buildTexture( material.normalMap, 'normal' ) );
1284
+ inputs.push( `${pad}color3f inputs:emissiveColor.connect = </Materials/Material_${material.id}/Texture_${material.emissiveMap.id}_emissive.outputs:rgb>` );
1278
1285
 
1279
- }
1286
+ samplers.push( buildTexture( material.emissiveMap, 'emissive' ) );
1280
1287
 
1281
- if ( material.aoMap ) {
1288
+ } else if ( material.emissive?.getHex() > 0 ) {
1282
1289
 
1283
- inputs.push( `${pad}float inputs:occlusion.connect = </Materials/Material_${material.id}/Texture_${material.aoMap.id}_occlusion.outputs:r>` );
1290
+ inputs.push( `${pad}color3f inputs:emissiveColor = ${buildColor( material.emissive )}` );
1284
1291
 
1285
- samplers.push( buildTexture( material.aoMap, 'occlusion' ) );
1292
+ } else {
1293
+
1294
+ inputs.push( `${pad}color3f inputs:emissiveColor = (0, 0, 0)` );
1286
1295
 
1287
- }
1296
+ }
1288
1297
 
1289
- if ( material.roughnessMap && material.roughness === 1 ) {
1298
+ if ( material.normalMap ) {
1290
1299
 
1291
- inputs.push( `${pad}float inputs:roughness.connect = </Materials/Material_${material.id}/Texture_${material.roughnessMap.id}_roughness.outputs:g>` );
1300
+ inputs.push( `${pad}normal3f inputs:normal.connect = </Materials/Material_${material.id}/Texture_${material.normalMap.id}_normal.outputs:rgb>` );
1292
1301
 
1293
- samplers.push( buildTexture( material.roughnessMap, 'roughness' ) );
1302
+ samplers.push( buildTexture( material.normalMap, 'normal' ) );
1294
1303
 
1295
- } else {
1304
+ }
1296
1305
 
1297
- inputs.push( `${pad}float inputs:roughness = ${material.roughness !== undefined ? material.roughness : 1 }` );
1306
+ if ( material.roughnessMap && material.roughness === 1 ) {
1298
1307
 
1299
- }
1308
+ inputs.push( `${pad}float inputs:roughness.connect = </Materials/Material_${material.id}/Texture_${material.roughnessMap.id}_roughness.outputs:g>` );
1300
1309
 
1301
- if ( material.metalnessMap && material.metalness === 1 ) {
1310
+ samplers.push( buildTexture( material.roughnessMap, 'roughness' ) );
1302
1311
 
1303
- inputs.push( `${pad}float inputs:metallic.connect = </Materials/Material_${material.id}/Texture_${material.metalnessMap.id}_metallic.outputs:b>` );
1312
+ } else {
1304
1313
 
1305
- samplers.push( buildTexture( material.metalnessMap, 'metallic' ) );
1314
+ inputs.push( `${pad}float inputs:roughness = ${material.roughness !== undefined ? material.roughness : 1 }` );
1306
1315
 
1307
- } else {
1316
+ }
1308
1317
 
1309
- inputs.push( `${pad}float inputs:metallic = ${material.metalness !== undefined ? material.metalness : 0 }` );
1318
+ if ( material.metalnessMap && material.metalness === 1 ) {
1310
1319
 
1311
- }
1320
+ inputs.push( `${pad}float inputs:metallic.connect = </Materials/Material_${material.id}/Texture_${material.metalnessMap.id}_metallic.outputs:b>` );
1312
1321
 
1313
- if ( material.alphaMap ) {
1322
+ samplers.push( buildTexture( material.metalnessMap, 'metallic' ) );
1314
1323
 
1315
- inputs.push( `${pad}float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.alphaMap.id}_opacity.outputs:r>` );
1316
- inputs.push( `${pad}float inputs:opacityThreshold = 0.0001` );
1324
+ } else {
1317
1325
 
1318
- samplers.push( buildTexture( material.alphaMap, 'opacity' ) );
1326
+ inputs.push( `${pad}float inputs:metallic = ${material.metalness !== undefined ? material.metalness : 0 }` );
1319
1327
 
1320
- } else {
1328
+ }
1321
1329
 
1322
- inputs.push( `${pad}float inputs:opacity = ${effectiveOpacity}` );
1323
-
1324
1330
  }
1325
1331
 
1326
1332
  if ( material instanceof MeshPhysicalMaterial ) {
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  import { BehaviorExtension } from "./extensions/behavior/Behaviour";
17
17
  import { AudioExtension } from "./extensions/behavior/AudioExtension";
18
18
  import { TextExtension } from "./extensions/USDZText";
19
+ import { USDZUIExtension } from "./extensions/USDZUI";
19
20
  import { Renderer } from "../../Renderer"
20
21
 
21
22
  const debug = getParam("debugusdz");
@@ -97,6 +98,7 @@
97
98
  this.extensions.push(new BehaviorExtension());
98
99
  this.extensions.push(new AudioExtension());
99
100
  this.extensions.push(new TextExtension());
101
+ this.extensions.push(new USDZUIExtension());
100
102
  }
101
103
  }
102
104
 
src/engine-components/export/usdz/extensions/USDZText.ts CHANGED
@@ -146,8 +146,13 @@
146
146
  return "text";
147
147
  }
148
148
 
149
- onExportObject(object: Object3D, model: USDObject, _context: USDZExporterContext) {
149
+ // HACK we should clean this up, text export has moved to USDZUI.ts and is
150
+ // integrated into the hierarchy now
151
+ onExportObject(_object: Object3D, _model: USDObject, _context: USDZExporterContext) {
150
152
 
153
+ return;
154
+
155
+ /*
151
156
  const text = GameObject.getComponent(object, Text);
152
157
  if (text) {
153
158
  const rt = GameObject.getComponent(object, RectTransform);
@@ -186,8 +191,43 @@
186
191
  textObj.writeTo(undefined, writer);
187
192
  });
188
193
  }
194
+ */
189
195
  }
190
196
 
197
+ exportText(object: Object3D, newModel: USDObject, _context: USDZExporterContext) {
198
+
199
+ const text = GameObject.getComponent(object, Text);
200
+ if (!text) return;
201
+
202
+ const rt = GameObject.getComponent(object, RectTransform);
203
+ let width = 100;
204
+ let height = 100;
205
+ if (rt) {
206
+ width = rt.width;
207
+ height = rt.height;
208
+ }
209
+
210
+ newModel.matrix = rotateYAxisMatrix.clone();
211
+ if (rt) // Not ideal but works for now:
212
+ newModel.matrix.premultiply(invertX);
213
+
214
+ const color = new Color().copySRGBToLinear(text.color);
215
+ newModel.material = new MeshStandardMaterial({ color: color, emissive: color });
216
+
217
+ newModel.addEventListener("serialize", (writer: USDWriter, _context: USDZExporterContext) => {
218
+ let txt = text.text;
219
+ txt = txt.replace(/\n/g, "\\n");
220
+ const textObj = TextBuilder.multiLine(txt, width, height, HorizontalAlignment.center, VerticalAlignment.bottom, TextWrapMode.flowing);
221
+ this.setTextAlignment(textObj, text.alignment);
222
+ this.setOverflow(textObj, text);
223
+ if (newModel.material)
224
+ textObj.material = newModel.material;
225
+ textObj.pointSize = this.convertToTextSize(text.fontSize);
226
+ textObj.depth = .001;
227
+ textObj.writeTo(undefined, writer);
228
+ });
229
+ }
230
+
191
231
  private convertToTextSize(pixel: number) {
192
232
  return 1 / 0.0502 * 144 * pixel;
193
233
  }
src/engine-components/webxr/WebXR.ts CHANGED
@@ -370,7 +370,7 @@
370
370
  if (xrRig) {
371
371
  // make it match unity forward
372
372
  this.rig = xrRig.gameObject;
373
- // this.rig.rotateY(Math.PI);
373
+ this.rig.rotateY(Math.PI);
374
374
  // this.rig.position.copy(existing.worldPosition);
375
375
  // this.rig.quaternion.premultiply(existing.worldQuaternion);
376
376
  }
src/engine-components/export/usdz/extensions/USDZUI.ts ADDED
@@ -0,0 +1,120 @@
1
+ import { IUSDExporterExtension } from "../Extension";
2
+ import { USDObject, USDZExporterContext } from "../ThreeUSDZExporter";
3
+ import { GameObject } from "../../../Component";
4
+ import { Canvas } from "../../../ui/Canvas";
5
+ import { CanvasGroup } from "../../../ui/CanvasGroup";
6
+ import { $shadowDomOwner } from "../../../ui/BaseUIComponent";
7
+ import { RectTransform } from "../../../ui/RectTransform";
8
+ import { Color, Mesh, MeshBasicMaterial, Object3D } from "three";
9
+ import { TextExtension } from "./USDZText";
10
+ import { RenderMode } from "../../../ui/Canvas";
11
+
12
+ export class USDZUIExtension implements IUSDExporterExtension {
13
+ get extensionName(): string {
14
+ return "tmui";
15
+ }
16
+
17
+ // TODO would probably be better to export each object instead of the entire Canvas
18
+ // so that we don't export them twice (once as regular hierarchy, once as part of Canvas export)
19
+ onExportObject(object: Object3D, model: USDObject, _context: USDZExporterContext) {
20
+ const canvas = GameObject.getComponent(object, Canvas);
21
+
22
+ if (canvas && canvas.activeAndEnabled && canvas.renderMode === RenderMode.WorldSpace) {
23
+ const textExt = new TextExtension();
24
+ const rt = GameObject.getComponent(object, RectTransform);
25
+ const canvasGroup = GameObject.getComponent(object, CanvasGroup);
26
+
27
+ let width = 100;
28
+ let height = 100;
29
+ if (rt) {
30
+ width = rt.width;
31
+ height = rt.height;
32
+
33
+ const shadowRootModel = USDObject.createEmpty();
34
+ const shadowComponent = rt.shadowComponent;
35
+ model.add(shadowRootModel);
36
+
37
+ if (shadowComponent) {
38
+ const mat = shadowComponent.matrix;
39
+ shadowRootModel.matrix.copy(mat);
40
+ // shadowRootModel.matrix.premultiply(invertX);
41
+
42
+ // TODO build map of parent GOs to USDObjects so we can reparent while traversing
43
+ const usdObjectMap = new Map<Object3D, USDObject>();
44
+ const opacityMap = new Map<Object3D, number>();
45
+ usdObjectMap.set(shadowComponent, shadowRootModel);
46
+ opacityMap.set(shadowComponent, canvasGroup ? canvasGroup.alpha : 1);
47
+
48
+ shadowComponent.traverse((child) => {
49
+ if (child === shadowComponent) return;
50
+
51
+ const childModel = USDObject.createEmpty();
52
+ childModel.matrix.copy(child.matrix);
53
+
54
+ const childParent = child.parent;
55
+ const isText = childParent && typeof childParent["textContent"] === "string" && childParent["textContent"].length;
56
+ let hierarchyOpacity = opacityMap.get(childParent!) || 1;
57
+
58
+ // TODO CanvasGroup doesn't render something but modifies opacity
59
+ // get CanvasGroup and modify alpha here
60
+
61
+ const canvasGroup = GameObject.getComponent(child, CanvasGroup);
62
+ if (canvasGroup)
63
+ hierarchyOpacity *= canvasGroup.alpha;
64
+
65
+ if (child instanceof Mesh && isText) {
66
+ // get shadoDomOwner so we can export Text from the text extension directly
67
+ const shadowDomOwner = child[$shadowDomOwner].gameObject;
68
+ textExt.exportText(shadowDomOwner, childModel, _context);
69
+ }
70
+
71
+ if (child instanceof Mesh && !isText)
72
+ {
73
+ // UI behaves weird: it seems it's always flipped right now,
74
+ // and three magically fixes it when rendering
75
+ // see https://github.com/mrdoob/three.js/pull/12720#issue-275923930
76
+ // https://github.com/mrdoob/three.js/issues/17361
77
+ // So we need to fix the winding order after applying the negative scale.
78
+ const clonedGeo = child.geometry.clone();
79
+ clonedGeo.scale(1, 1, -1);
80
+ this.flipWindingOrder(clonedGeo);
81
+ childModel.geometry = clonedGeo;
82
+
83
+ const color = new Color();
84
+ const ownOpacity = child.material.opacity;
85
+ color.copy(child.material.color);
86
+
87
+ // Calculate opacity from parent chain and own alpha
88
+ childModel.material = new MeshBasicMaterial({
89
+ color: color,
90
+ opacity: ownOpacity * hierarchyOpacity,
91
+ map: child.material.map,
92
+ transparent: true,
93
+ });
94
+ }
95
+
96
+ usdObjectMap.set(child, childModel);
97
+ opacityMap.set(child, hierarchyOpacity);
98
+
99
+ const parentUsdzObject = usdObjectMap.get(childParent!);
100
+ if (!parentUsdzObject) {
101
+ console.error("Error when exporting UI: shadow component parent not found!", child, child.parent);
102
+ return;
103
+ }
104
+ parentUsdzObject.add(childModel);
105
+ });
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ private flipWindingOrder(geometry) {
112
+ const index = geometry.index.array
113
+ for (let i = 0, il = index.length / 3; i < il; i++) {
114
+ let x = index[i * 3]
115
+ index[i * 3] = index[i * 3 + 2]
116
+ index[i * 3 + 2] = x
117
+ }
118
+ geometry.index.needsUpdate = true
119
+ }
120
+ }