Needle Engine

Changes between version 3.32.20-alpha and 3.32.21-alpha
Files changed (4) hide show
  1. src/engine/engine_input.ts +35 -17
  2. src/engine-components/ui/EventSystem.ts +10 -2
  3. src/engine/xr/NeedleXRController.ts +1 -1
  4. src/engine-components/export/usdz/ThreeUSDZExporter.ts +133 -61
src/engine/engine_input.ts CHANGED
@@ -717,6 +717,7 @@
717
717
  if (!this.isNewEvent(evt.timeStamp, id, this._pointerUpTimestamp)) continue;
718
718
  const ne = this.createPointerEventFromTouch("pointerup", touch.identifier, touch.clientX, touch.clientY, touch.force, evt);
719
719
  this.onUp(ne);
720
+ this._pointerIds.splice(this._pointerIds.indexOf(id), 1); // remove pointer id from used pointers
720
721
  }
721
722
  }
722
723
  private createPointerEventFromTouch(type: InputEventNames, touchIdentifier: number, x: number, y: number, force: number, evt: Event): NEPointerEvent {
@@ -830,24 +831,8 @@
830
831
  }
831
832
  if (debug) console.log(evt.pointerType, "DOWN", index);
832
833
  if (!this.isInRect(evt)) return;
834
+ if (this.isMouseEventEmmitedFromTouch(evt)) return;
833
835
 
834
- // TODO: this whole pointer handling doesnt consider that in VR we have multiple pointers. It's not enough to store the button as the pointer ID anymore
835
-
836
- // check if we received an mouse UP event for a touch (for some reason we get a mouse.down for touch.up)
837
- if (evt.pointerType === PointerType.Mouse) {
838
- const upTime = this._pointerUpTimestamp[index];
839
- if (upTime > 0 && evt.source?.timeStamp !== undefined) {
840
- const diff = (evt.source.timeStamp - upTime);
841
- // on android touch up and mouse up have the exact same value
842
- // but on iOS they are not the same but a few milliseconds apart
843
- if (diff < 60 && diff >= 0) {
844
- // we received an UP event for a touch, ignore this DOWN event
845
- if (debug) console.log("Ignoring mouse.down for touch.up");
846
- return;
847
- }
848
- }
849
- }
850
-
851
836
  this.setPointerState(index, this._pointerPressed, true);
852
837
  this.setPointerState(index, this._pointerDown, true);
853
838
  this.setPointerStateT(index, this._pointerEvent, evt.source);
@@ -874,6 +859,7 @@
874
859
  const isDown = this.getPointerPressed(index);
875
860
  if (isDown === false && !this.isInRect(evt)) return;
876
861
  if (evt.pointerType === PointerType.Touch && !isDown) return;
862
+ if (this.isMouseEventEmmitedFromTouch(evt)) return;
877
863
  if (debug) console.log(evt.pointerType, "MOVE", index, "hasSpace=" + evt.space != null);
878
864
 
879
865
  this.updatePointerPosition(evt);
@@ -887,7 +873,9 @@
887
873
  if (debug) console.log(evt.pointerType, "UP", index, "was not down");
888
874
  return;
889
875
  }
876
+ if (this.isMouseEventEmmitedFromTouch(evt)) return;
890
877
  if (debug) console.log(evt.pointerType, "UP", index);
878
+
891
879
  this.setPointerState(index, this._pointerPressed, false);
892
880
  this.setPointerStateT(index, this._pointerEvent, evt.source);
893
881
  this.setPointerState(index, this._pointerUp, true);
@@ -927,6 +915,36 @@
927
915
  this.onDispatchEvent(evt);
928
916
  }
929
917
 
918
+ private isMouseEventEmmitedFromTouch(evt: NEPointerEvent) {
919
+ if (evt.pointerType === PointerType.Mouse) {
920
+ const lastPointer = this._pointerUpTimestamp.map((v, i) => ({ timestamp: v, index: i})).sort((a, b) => a.timestamp - b.timestamp).at(-1);
921
+ if(lastPointer && this._pointerTypes[lastPointer.index] === PointerType.Touch) {
922
+ if (lastPointer.timestamp > 0 && evt.source?.timeStamp !== undefined) {
923
+ const diff = (evt.source.timeStamp - lastPointer.timestamp);
924
+ if (diff < 320 && diff >= 0) {
925
+ // we received an UP event for a touch, ignore this DOWN event
926
+ if (debug) console.log("Ignoring mouse.down for touch.up");
927
+
928
+ return true;
929
+ }
930
+ }
931
+ }
932
+
933
+ /* const upTime = this._pointerUpTimestamp[index];
934
+ if (upTime > 0 && evt.source?.timeStamp !== undefined) {
935
+ const diff = (evt.source.timeStamp - upTime);
936
+ // on android touch up and mouse up have the exact same value
937
+ // but on iOS they are not the same but a few milliseconds apart
938
+ if (diff < 60 && diff >= 0) {
939
+ // we received an UP event for a touch, ignore this DOWN event
940
+ if (debug) console.log("Ignoring mouse.down for touch.up");
941
+ return;
942
+ }
943
+ } */
944
+ }
945
+ return false;
946
+ }
947
+
930
948
  private updatePointerPosition(evt: NEPointerEvent) {
931
949
  const index = evt.pointerId;
932
950
 
src/engine-components/ui/EventSystem.ts CHANGED
@@ -346,7 +346,7 @@
346
346
  }
347
347
  this.hoveredByID.delete(args.pointerId);
348
348
 
349
- // if it was up, it means it doesn't should notify things that it down on before
349
+ // if it was up, it means it should notify things that it down on before
350
350
  if (args.isUp) {
351
351
  this.pressedByID.get(args.pointerId)?.handlers.forEach(h => this.invokeOnPointerUp(args, h));
352
352
  this.pressedByID.delete(args.pointerId);
@@ -515,7 +515,7 @@
515
515
  case "hand":
516
516
  // for hands and controller we assume they are never totally still (except for simulated environments)
517
517
  // we might want to add a threshold here (e.g. if a user holds their hand very still or controller)
518
- // so maybe check the angle everxy frame?
518
+ // so maybe check the angle every frame?
519
519
  break;
520
520
  }
521
521
 
@@ -558,6 +558,14 @@
558
558
  // The original component that received the down event SHOULD also receive the up event
559
559
  pressedEvent?.handlers.delete(comp);
560
560
  }
561
+
562
+ // handle touch onExit (touchUp) since the pointer stops existing
563
+ // mouse onExit (mouseUp) is handled when we hover over something else / on nothing
564
+ // Mouse 0 is always persistent
565
+ if (comp.onPointerExit && args.event?.pointerType === PointerType.Touch) {
566
+ this.handlePointerExit(comp, args);
567
+ this.hoveredByID.delete(args.pointerId!);
568
+ }
561
569
  }
562
570
 
563
571
  if (args.isClick) {
src/engine/xr/NeedleXRController.ts CHANGED
@@ -312,7 +312,7 @@
312
312
  const t = gripPose.transform;
313
313
  this._gripPosition.set(t.position.x, t.position.y, t.position.z);
314
314
  this._gripQuaternion.set(t.orientation.x, t.orientation.y, t.orientation.z, t.orientation.w);
315
- if (gripPose.linearVelocity)
315
+ if ("linearVelocity" in gripPose && gripPose.linearVelocity)
316
316
  this._linearVelocity.set(gripPose.linearVelocity.x, gripPose.linearVelocity.y, gripPose.linearVelocity.z);
317
317
  }
318
318
  }
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -33,9 +33,10 @@
33
33
  import { Renderer } from '../../Renderer.js';
34
34
 
35
35
  function makeNameSafe( str ) {
36
+ // Remove characters that are not allowed in USD ASCII identifiers
36
37
  str = str.replace( /[^a-zA-Z0-9_]/g, '' );
37
38
 
38
- // if str does not start with a-zA-Z_ add _ to the beginning
39
+ // If name doesn't start with a-zA-Z_, add _ to the beginning – required by USD
39
40
  if ( !str.match( /^[a-zA-Z_]/ ) )
40
41
  str = '_' + str;
41
42
 
@@ -87,6 +88,7 @@
87
88
 
88
89
  uuid: string;
89
90
  name: string;
91
+ displayName: string;
90
92
  matrix: Matrix4;
91
93
  private _isDynamic: boolean;
92
94
  get isDynamic() { return this._isDynamic; }
@@ -123,6 +125,7 @@
123
125
 
124
126
  this.uuid = id;
125
127
  this.name = makeNameSafe( name );
128
+ this.displayName = name;
126
129
  this.matrix = matrix.clone();
127
130
  this.geometry = mesh;
128
131
  this.material = material;
@@ -616,7 +619,7 @@
616
619
  if ( canvas ) {
617
620
 
618
621
  const blob = await canvas.convertToBlob( {type: isRGBA ? 'image/png' : 'image/jpeg', quality: 0.95 } );
619
- files[ `textures/Texture_${id}.${isRGBA ? 'png' : 'jpg'}` ] = new Uint8Array( await blob.arrayBuffer() );
622
+ files[ `textures/${id}.${isRGBA ? 'png' : 'jpg'}` ] = new Uint8Array( await blob.arrayBuffer() );
620
623
 
621
624
  } else {
622
625
 
@@ -702,6 +705,8 @@
702
705
 
703
706
  if ( model ) {
704
707
 
708
+ model.displayName = object.name;
709
+
705
710
  if ( parentModel ) {
706
711
 
707
712
  parentModel.add( model );
@@ -804,7 +809,7 @@
804
809
 
805
810
  if ( material && ( 'isMeshStandardMaterial' in material && material.isMeshStandardMaterial || 'isMeshBasicMaterial' in material && material.isMeshBasicMaterial ) ) { // TODO convert unlit to lit+emissive
806
811
 
807
- const geometryFileName = 'geometries/Geometry_' + geometry.id + '.usd';
812
+ const geometryFileName = 'geometries/' + getGeometryName(geometry, object.name) + '.usda';
808
813
 
809
814
  if ( ! ( geometryFileName in context.files ) ) {
810
815
 
@@ -1034,10 +1039,14 @@
1034
1039
 
1035
1040
  }
1036
1041
 
1037
- function getBoneName(bone) {
1042
+ function getBoneName(bone: Object3D) {
1038
1043
  return makeNameSafe(bone.name || 'bone_' + bone.uuid);
1039
1044
  }
1040
1045
 
1046
+ function getGeometryName(geometry: BufferGeometry, fallbackName: string) {
1047
+ return makeNameSafe(geometry.name || fallbackName || 'Geometry_') + geometry.id;
1048
+ }
1049
+
1041
1050
  function getPathToSkeleton(bone: Object3D, assumedRoot: Object3D) {
1042
1051
  let path = getBoneName(bone);
1043
1052
  let current = bone.parent;
@@ -1084,20 +1093,24 @@
1084
1093
  // NE-4084: To use the doubleSided workaround with skeletal meshes we'd have to
1085
1094
  // also emit extra data for jointIndices etc., so we're skipping skinned meshes here.
1086
1095
  if (context.quickLookCompatible && material && material.side === DoubleSide && !isSkinnedMesh)
1087
- writer.appendLine(`prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry_doubleSided>`);
1096
+ writer.appendLine(`prepend references = @./geometries/${getGeometryName(geometry, name)}.usda@</Geometry_doubleSided>`);
1088
1097
  else
1089
- writer.appendLine(`prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry>`);
1098
+ writer.appendLine(`prepend references = @./geometries/${getGeometryName(geometry, name)}.usda@</Geometry>`);
1090
1099
  writer.appendLine(`prepend apiSchemas = [${apiSchemas}]`);
1091
- writer.closeBlock( ")" );
1092
- writer.beginBlock();
1093
1100
  }
1094
1101
  else if ( camera )
1095
- writer.beginBlock( `def Camera "${name}"` );
1102
+ writer.beginBlock( `def Camera "${name}"`, "(", false );
1096
1103
  else
1097
- writer.beginBlock( `def Xform "${name}"` );
1104
+ writer.beginBlock( `def Xform "${name}"`, "(", false);
1098
1105
 
1106
+ if (model.displayName)
1107
+ writer.appendLine(`displayName = "${model.displayName}"`);
1108
+ writer.closeBlock( ")" );
1109
+ writer.beginBlock();
1110
+
1099
1111
  if ( geometry && material ) {
1100
- writer.appendLine( `rel material:binding = </StageRoot/Materials/Material_${material.id}>` );
1112
+ const materialName = makeNameSafe(material.name) + `_${material.id}`;
1113
+ writer.appendLine( `rel material:binding = </StageRoot/Materials/${materialName}>` );
1101
1114
 
1102
1115
  // Turns out QuickLook / RealityKit doesn't support the doubleSided attribute, so we
1103
1116
  // work around that by emitting additional indices above, and then we shouldn't emit the attribute either as geometry is
@@ -1183,7 +1196,7 @@
1183
1196
 
1184
1197
  // Mesh
1185
1198
 
1186
- function buildMeshObject( geometry, bonesArray: Bone[] = [] ) {
1199
+ function buildMeshObject( geometry: BufferGeometry, bonesArray: Bone[] = [] ) {
1187
1200
 
1188
1201
  const mesh = buildMesh( geometry, bonesArray );
1189
1202
  return `
@@ -1446,22 +1459,47 @@
1446
1459
 
1447
1460
  }
1448
1461
 
1462
+ /** Slot of the exported texture. Some slots (e.g. normal, opacity) require additional processing. */
1463
+ declare type MapType = 'diffuse' | 'normal' | 'occlusion' | 'opacity' | 'roughness' | 'emissive' | 'metallic' | 'transmission';
1464
+
1449
1465
  function buildMaterial( material: MeshBasicMaterial, textures: TextureMap, quickLookCompatible = false ) {
1450
1466
 
1451
1467
  // https://graphics.pixar.com/usd/docs/UsdPreviewSurface-Proposal.html
1452
1468
 
1453
- const pad = ' ';
1469
+ const pad = ' ';
1454
1470
  const inputs: Array<string> = [];
1455
1471
  const samplers: Array<string> = [];
1472
+ const materialName = makeNameSafe(material.name) + `_${material.id}`;
1456
1473
 
1457
- function buildTexture( texture, mapType, color: Color | undefined = undefined, opacity: number | undefined = undefined ) {
1474
+ function texName(tex: Texture) {
1475
+ return makeNameSafe(tex.name) + '_' + tex.id;
1476
+ }
1458
1477
 
1459
- const id = texture.id + ( color ? '_' + color.getHexString() : '' ) + ( opacity !== undefined ? '_' + opacity : '' );
1478
+ function buildTexture( texture: Texture, mapType: MapType, color: Color | undefined = undefined, opacity: number | undefined = undefined ) {
1460
1479
 
1480
+ const name = texName(texture);
1481
+ // File names need to be unique; so when we're baking color and/or opacity into a texture we need to keep track of that.
1482
+ // TODO This currently results in textures potentially being exported multiple times that are actually identical!
1483
+ // const colorHex = color ? color.getHexString() : undefined;
1484
+ // const colorId = ( color && colorHex != 'ffffff' ? '_' + color.getHexString() : '' );
1485
+ const id = name + ( opacity !== undefined && opacity !== 1.0 ? '_' + opacity : '' );
1486
+
1461
1487
  // Seems neither QuickLook nor usdview support scale/bias on .a values, so we need to bake opacity multipliers into
1462
1488
  // the texture. This is not ideal.
1463
- const opacityIsAppliedToTextureAndNotAsScale = quickLookCompatible && opacity !== undefined;
1489
+ const opacityIsAppliedToTextureAndNotAsScale = quickLookCompatible && opacity !== undefined && opacity !== 1.0;
1464
1490
  const scaleToApply = opacityIsAppliedToTextureAndNotAsScale ? new Vector4(1, 1, 1, opacity) : undefined;
1491
+
1492
+ // Treat undefined opacity as 1
1493
+ if (opacity === undefined)
1494
+ opacity = 1.0;
1495
+
1496
+ // When we're baking opacity into the texture, we shouldn't apply it as scale as well – so we set it to 1.
1497
+ if (opacityIsAppliedToTextureAndNotAsScale)
1498
+ opacity = 1.0;
1499
+
1500
+ // sanitize scaleToApply.w === 0 - this isn't working right now, seems the PNG can't be read back properly.
1501
+ if (scaleToApply && scaleToApply.w <= 0.05) scaleToApply.w = 0.05;
1502
+
1465
1503
  textures[ id ] = { texture, scale: scaleToApply };
1466
1504
 
1467
1505
  const uv = texture.channel > 0 ? 'st' + texture.channel : 'st';
@@ -1508,7 +1546,7 @@
1508
1546
  }
1509
1547
 
1510
1548
  const needsTextureTransform = ( repeat.x != 1 || repeat.y != 1 || offset.x != 0 || offset.y != 0 || rotation != 0 );
1511
- const textureTransformInput = `${materialRoot}/Material_${material.id}/${'uvReader_' + uv}.outputs:result>`; const textureTransformOutput = `${materialRoot}/Material_${material.id}/Transform2d_${mapType}.outputs:result>`;
1549
+ const textureTransformInput = `${materialRoot}/${materialName}/${'uvReader_' + uv}.outputs:result>`; const textureTransformOutput = `${materialRoot}/${materialName}/Transform2d_${mapType}.outputs:result>`;
1512
1550
  const needsTextureScale = mapType !== 'normal' && (color && (color.r !== 1 || color.g !== 1 || color.b !== 1 || opacity !== 1)) || false;
1513
1551
 
1514
1552
  const needsNormalScaleAndBias = mapType === 'normal';
@@ -1532,14 +1570,14 @@
1532
1570
  float2 outputs:result
1533
1571
  }
1534
1572
  ` : '' }
1535
- def Shader "Texture_${texture.id}_${mapType}"
1573
+ def Shader "${name}_${mapType}"
1536
1574
  {
1537
1575
  uniform token info:id = "UsdUVTexture"
1538
- asset inputs:file = @textures/Texture_${id}.${isRGBA ? 'png' : 'jpg'}@
1576
+ asset inputs:file = @textures/${id}.${isRGBA ? 'png' : 'jpg'}@
1539
1577
  token inputs:sourceColorSpace = "${ texture.colorSpace === 'srgb' ? 'sRGB' : 'raw' }"
1540
1578
  float2 inputs:st.connect = ${needsTextureTransform ? textureTransformOutput : textureTransformInput}
1541
1579
  ${needsTextureScale ? `
1542
- float4 inputs:scale = (${color ? color.r + ', ' + color.g + ', ' + color.b : '1, 1, 1'}, ${(opacity !== undefined && !opacityIsAppliedToTextureAndNotAsScale) ? opacity : '1'})
1580
+ float4 inputs:scale = (${color ? color.r + ', ' + color.g + ', ' + color.b : '1, 1, 1'}, ${opacity})
1543
1581
  ` : `` }
1544
1582
  ${needsNormalScaleAndBias ? `
1545
1583
  float4 inputs:scale = (${normalScaleValueString}, ${normalScaleValueString}, ${normalScaleValueString}, 1)
@@ -1557,6 +1595,8 @@
1557
1595
  }
1558
1596
 
1559
1597
  let effectiveOpacity = ( material.transparent || material.alphaTest ) ? material.opacity : 1;
1598
+ let haveConnectedOpacity = false;
1599
+ let haveConnectedOpacityThreshold = false;
1560
1600
 
1561
1601
  if ( material instanceof MeshPhysicalMaterial && material.transmission !== undefined) {
1562
1602
 
@@ -1565,18 +1605,21 @@
1565
1605
 
1566
1606
  }
1567
1607
 
1568
- if ( material.map !== null ) {
1608
+ if ( material.map ) {
1569
1609
 
1570
- inputs.push( `${pad}color3f inputs:diffuseColor.connect = ${materialRoot}/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:rgb>` );
1610
+ inputs.push( `${pad}color3f inputs:diffuseColor.connect = ${materialRoot}/${materialName}/${texName(material.map)}_diffuse.outputs:rgb>` );
1571
1611
 
1572
1612
  if ( material.transparent ) {
1573
1613
 
1574
- inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:a>` );
1614
+ inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/${materialName}/${texName(material.map)}_diffuse.outputs:a>` );
1615
+ haveConnectedOpacity = true;
1575
1616
 
1576
1617
  } else if ( material.alphaTest > 0.0 ) {
1577
1618
 
1578
- inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:a>` );
1619
+ inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/${materialName}/${texName(material.map)}_diffuse.outputs:a>` );
1620
+ haveConnectedOpacity = true;
1579
1621
  inputs.push( `${pad}float inputs:opacityThreshold = ${material.alphaTest}` );
1622
+ haveConnectedOpacityThreshold = true;
1580
1623
 
1581
1624
  }
1582
1625
 
@@ -1588,9 +1631,22 @@
1588
1631
 
1589
1632
  }
1590
1633
 
1634
+ if ( material.alphaHash ) {
1635
+
1636
+ // Seems we can do this to basically enforce alpha hashing / dithered transparency in QuickLook –
1637
+ // works completely different in usdview though...
1638
+ if (haveConnectedOpacityThreshold) {
1639
+ console.warn('Opacity threshold for ' + material.name + ' was already connected. Skipping alphaHash opacity threshold.');
1640
+ }
1641
+ else {
1642
+ inputs.push( `${pad}float inputs:opacityThreshold = 0.0000000001` );
1643
+ haveConnectedOpacityThreshold = true;
1644
+ }
1645
+ }
1646
+
1591
1647
  if ( material.aoMap ) {
1592
1648
 
1593
- inputs.push( `${pad}float inputs:occlusion.connect = ${materialRoot}/Material_${material.id}/Texture_${material.aoMap.id}_occlusion.outputs:r>` );
1649
+ inputs.push( `${pad}float inputs:occlusion.connect = ${materialRoot}/${materialName}/${texName(material.aoMap)}_occlusion.outputs:r>` );
1594
1650
 
1595
1651
  samplers.push( buildTexture( material.aoMap, 'occlusion' ) );
1596
1652
 
@@ -1598,19 +1654,32 @@
1598
1654
 
1599
1655
  if ( material.alphaMap ) {
1600
1656
 
1601
- inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/Material_${material.id}/Texture_${material.alphaMap.id}_opacity.outputs:r>` );
1602
- inputs.push( `${pad}float inputs:opacityThreshold = 0.0001` );
1657
+ inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/${materialName}/${texName(material.alphaMap)}_opacity.outputs:r>` );
1658
+ // TODO this is likely not correct and will prevent blending from working correctly – masking will be used instead
1659
+ inputs.push( `${pad}float inputs:opacityThreshold = 0.0000000001` );
1660
+ haveConnectedOpacity = true;
1661
+ haveConnectedOpacityThreshold = true;
1603
1662
 
1604
1663
  samplers.push( buildTexture( material.alphaMap, 'opacity', new Color( 1, 1, 1 ), effectiveOpacity ) );
1605
1664
 
1606
- } else {
1665
+ } else { // opacity will always be connected if we have a map
1607
1666
 
1608
- inputs.push( `${pad}float inputs:opacity = ${effectiveOpacity}` );
1667
+ if (haveConnectedOpacity) {
1668
+ console.warn('Opacity for ' + material.name + ' was already connected. Skipping default opacity.');
1669
+ } else {
1670
+ inputs.push( `${pad}float inputs:opacity = ${effectiveOpacity}` );
1671
+ haveConnectedOpacity = true;
1672
+ }
1609
1673
 
1610
1674
  if ( material.alphaTest > 0.0 ) {
1611
1675
 
1612
- inputs.push( `${pad}float inputs:opacityThreshold = ${material.alphaTest}` );
1613
-
1676
+ if (haveConnectedOpacityThreshold) {
1677
+ console.warn('Opacity threshold for ' + material.name + ' was already connected. Skipping default opacity threshold.');
1678
+ }
1679
+ else {
1680
+ inputs.push( `${pad}float inputs:opacityThreshold = ${material.alphaTest}` );
1681
+ haveConnectedOpacityThreshold = true;
1682
+ }
1614
1683
  }
1615
1684
 
1616
1685
  }
@@ -1619,7 +1688,7 @@
1619
1688
 
1620
1689
  if ( material.emissiveMap ) {
1621
1690
 
1622
- inputs.push( `${pad}color3f inputs:emissiveColor.connect = ${materialRoot}/Material_${material.id}/Texture_${material.emissiveMap.id}_emissive.outputs:rgb>` );
1691
+ inputs.push( `${pad}color3f inputs:emissiveColor.connect = ${materialRoot}/${materialName}/${texName(material.emissiveMap)}_emissive.outputs:rgb>` );
1623
1692
  const color = material.emissive.clone();
1624
1693
  color.multiplyScalar( material.emissiveIntensity );
1625
1694
  samplers.push( buildTexture( material.emissiveMap, 'emissive', color ) );
@@ -1632,13 +1701,14 @@
1632
1701
 
1633
1702
  } else {
1634
1703
 
1635
- inputs.push( `${pad}color3f inputs:emissiveColor = (0, 0, 0)` );
1704
+ // We don't need to export (0,0,0) as emissive color
1705
+ // inputs.push( `${pad}color3f inputs:emissiveColor = (0, 0, 0)` );
1636
1706
 
1637
1707
  }
1638
1708
 
1639
1709
  if ( material.normalMap ) {
1640
1710
 
1641
- inputs.push( `${pad}normal3f inputs:normal.connect = ${materialRoot}/Material_${material.id}/Texture_${material.normalMap.id}_normal.outputs:rgb>` );
1711
+ inputs.push( `${pad}normal3f inputs:normal.connect = ${materialRoot}/${materialName}/${texName(material.normalMap)}_normal.outputs:rgb>` );
1642
1712
 
1643
1713
  samplers.push( buildTexture( material.normalMap, 'normal' ) );
1644
1714
 
@@ -1646,7 +1716,7 @@
1646
1716
 
1647
1717
  if ( material.roughnessMap && material.roughness === 1 ) {
1648
1718
 
1649
- inputs.push( `${pad}float inputs:roughness.connect = ${materialRoot}/Material_${material.id}/Texture_${material.roughnessMap.id}_roughness.outputs:g>` );
1719
+ inputs.push( `${pad}float inputs:roughness.connect = ${materialRoot}/${materialName}/${texName(material.roughnessMap)}_roughness.outputs:g>` );
1650
1720
 
1651
1721
  samplers.push( buildTexture( material.roughnessMap, 'roughness' ) );
1652
1722
 
@@ -1658,7 +1728,7 @@
1658
1728
 
1659
1729
  if ( material.metalnessMap && material.metalness === 1 ) {
1660
1730
 
1661
- inputs.push( `${pad}float inputs:metallic.connect = ${materialRoot}/Material_${material.id}/Texture_${material.metalnessMap.id}_metallic.outputs:b>` );
1731
+ inputs.push( `${pad}float inputs:metallic.connect = ${materialRoot}/${materialName}/${texName(material.metalnessMap)}_metallic.outputs:b>` );
1662
1732
 
1663
1733
  samplers.push( buildTexture( material.metalnessMap, 'metallic' ) );
1664
1734
 
@@ -1678,7 +1748,7 @@
1678
1748
 
1679
1749
  if ( !material.transparent && ! (material.alphaTest > 0.0) && material.transmissionMap) {
1680
1750
 
1681
- inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/Material_${material.id}/Texture_${material.transmissionMap.id}_transmission.outputs:r>` );
1751
+ inputs.push( `${pad}float inputs:opacity.connect = ${materialRoot}/${materialName}/${texName(material.transmissionMap)}_transmission.outputs:r>` );
1682
1752
 
1683
1753
  samplers.push( buildTexture( material.transmissionMap, 'transmission' ) );
1684
1754
  }
@@ -1686,37 +1756,39 @@
1686
1756
  }
1687
1757
 
1688
1758
  return `
1689
- def Material "Material_${material.id}"
1690
- {
1691
- def Shader "PreviewSurface"
1692
- {
1693
- uniform token info:id = "UsdPreviewSurface"
1759
+ def Material "${materialName}" (
1760
+ ${material.name ? `displayName = "${material.name}"` : ''}
1761
+ )
1762
+ {
1763
+ def Shader "PreviewSurface"
1764
+ {
1765
+ uniform token info:id = "UsdPreviewSurface"
1694
1766
  ${inputs.join( '\n' )}
1695
- int inputs:useSpecularWorkflow = 0
1696
- token outputs:surface
1697
- }
1767
+ int inputs:useSpecularWorkflow = 0
1768
+ token outputs:surface
1769
+ }
1698
1770
 
1699
- token outputs:surface.connect = ${materialRoot}/Material_${material.id}/PreviewSurface.outputs:surface>
1771
+ token outputs:surface.connect = ${materialRoot}/${materialName}/PreviewSurface.outputs:surface>
1700
1772
 
1701
- def Shader "uvReader_st"
1702
- {
1703
- uniform token info:id = "UsdPrimvarReader_float2"
1704
- token inputs:varname = "st"
1705
- float2 inputs:fallback = (0.0, 0.0)
1706
- float2 outputs:result
1707
- }
1773
+ def Shader "uvReader_st"
1774
+ {
1775
+ uniform token info:id = "UsdPrimvarReader_float2"
1776
+ token inputs:varname = "st"
1777
+ float2 inputs:fallback = (0.0, 0.0)
1778
+ float2 outputs:result
1779
+ }
1708
1780
 
1709
- def Shader "uvReader_st2"
1710
- {
1711
- uniform token info:id = "UsdPrimvarReader_float2"
1712
- token inputs:varname = "st2"
1713
- float2 inputs:fallback = (0.0, 0.0)
1714
- float2 outputs:result
1715
- }
1781
+ def Shader "uvReader_st2"
1782
+ {
1783
+ uniform token info:id = "UsdPrimvarReader_float2"
1784
+ token inputs:varname = "st2"
1785
+ float2 inputs:fallback = (0.0, 0.0)
1786
+ float2 outputs:result
1787
+ }
1716
1788
 
1717
1789
  ${samplers.join( '\n' )}
1718
1790
 
1719
- }
1791
+ }
1720
1792
  `;
1721
1793
 
1722
1794
  }