teleportxr 1.0.91 → 1.0.93

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.
package/client/client.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const fs= require("fs");
4
+ const path= require("path");
3
5
  const core= require("../core/core.js");
4
6
  const command= require("../protocol/command.js");
5
7
  const message= require("../protocol/message.js");
@@ -54,6 +56,9 @@ class Client {
54
56
  this.clientID=cid;
55
57
  this.origin_uid=0;
56
58
  this.handshakeMessage=new message.HandshakeMessage();
59
+ // The client's axes standard, learned from its Handshake. Used to pick the matching
60
+ // cubemap variant to stream (e.g. GlStyle -> *_ogl.ktx2). NotInitialized until handshake.
61
+ this.clientAxesStandard=core.AxesStandard.NotInitialized;
57
62
  this.geometryService=new gs.GeometryService(cid);
58
63
  // Per-client avatar negotiation state. The host application drives
59
64
  // when (and whether) policy is sent via this service.
@@ -223,6 +228,9 @@ class Client {
223
228
  this.setupCommand.int64_startTimestamp_utc_unix_us = BigInt(core.getStartTimeUnixUs());
224
229
  if(this.scene)
225
230
  {
231
+ // Tell the client which axes standard the server authored the scene in. Node transforms
232
+ // are converted to the client's own standard at encode time (see SendNode).
233
+ this.setupCommand.AxesStandard_axesStandard=this.scene.serverAxesStandard;
226
234
  if(this.scene.backgroundTexturePath&&this.scene.backgroundTexturePath!="")
227
235
  {
228
236
  this.setupCommand.BackgroundMode_backgroundMode=BackgroundMode.TEXTURE;
@@ -545,7 +553,9 @@ class Client {
545
553
  var node=this.scene.GetNode(uid);
546
554
  const MAX_NODE_SIZE=500;
547
555
  const buffer = new ArrayBuffer(MAX_NODE_SIZE);
548
- const nodeSize=node_encoder.encodeNode(node,buffer);
556
+ // Convert the node's transform from the server's axes standard to this client's, exactly as
557
+ // the C++ server does in GeometryEncoder::encodeNodes (ConvertTransform server->client).
558
+ const nodeSize=node_encoder.encodeNode(node,buffer,this.scene.serverAxesStandard,this.clientAxesStandard);
549
559
  const view2 = new DataView(buffer, 0, nodeSize);
550
560
  if(!this.webRtcConnection)
551
561
  {
@@ -564,6 +574,34 @@ class Client {
564
574
  if(sendSuccess)
565
575
  this.geometryService.EncodedResource(uid);
566
576
  }
577
+ //! For a cubemap resource, return the URL of the variant matching this client's axes
578
+ //! standard (e.g. /envCloudyCubemap.ktx2 -> /envCloudyCubemap_ogl.ktx2). Returns undefined
579
+ //! to mean "use the resource's base url" — for non-cubemaps, unknown axes standards, or
580
+ //! when the variant file is missing under the public path (so streaming never breaks).
581
+ CubemapUrlForClient(resource)
582
+ {
583
+ if(!resource || !resource.isCubemap)
584
+ return undefined;
585
+ const suffix=core.AxesStandardToCubemapSuffix(this.clientAxesStandard);
586
+ if(!suffix)
587
+ return undefined;
588
+ const candidate=resources.InsertCubemapAxesSuffix(resource.url, suffix);
589
+ if(candidate===resource.url)
590
+ return undefined;
591
+ // Only serve the variant if it actually exists; otherwise fall back to the base file.
592
+ if(this.scene&&this.scene.publicPath)
593
+ {
594
+ // URLs are server-relative (e.g. "/envCloudyCubemap.ktx2"); strip any scheme/host.
595
+ const rel=candidate.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/]*/, "");
596
+ const full=path.join(this.scene.publicPath, rel);
597
+ if(!fs.existsSync(full))
598
+ {
599
+ console.warn("Cubemap variant "+full+" not found; serving base "+resource.url+" to client "+this.clientID);
600
+ return undefined;
601
+ }
602
+ }
603
+ return candidate;
604
+ }
567
605
  SendGenericResource(uid)
568
606
  {
569
607
  var resource=resources.GetResourceFromUid(uid);
@@ -572,9 +610,10 @@ class Client {
572
610
  console.warn("No resource of uid ",uid," was found.")
573
611
  return;
574
612
  }
613
+ const urlOverride=this.CubemapUrlForClient(resource);
575
614
  const MAX_BUFFER_SIZE=resource.encodedSize();;
576
615
  const buffer = new ArrayBuffer(MAX_BUFFER_SIZE);
577
- const resourceSize=resource_encoder.EncodeResource(resource,buffer);
616
+ const resourceSize=resource_encoder.EncodeResource(resource,buffer,urlOverride);
578
617
  const view2 = new DataView(buffer, 0, resourceSize);
579
618
  if(!this.webRtcConnection)
580
619
  {
@@ -588,7 +627,7 @@ class Client {
588
627
  return;
589
628
  }
590
629
  const sendSuccess = this.webRtcConnection.sendGeometry(view2);
591
- console.log("[T+"+this.elapsedMsSinceStart()+"ms, conn+"+this.elapsedMsSinceConnected()+"ms] Sending resource "+uid+" "+resource.url+" to Client "+this.clientID+", size: "+resourceSize+" bytes — "+
630
+ console.log("[T+"+this.elapsedMsSinceStart()+"ms, conn+"+this.elapsedMsSinceConnected()+"ms] Sending resource "+uid+" "+(urlOverride||resource.url)+" to Client "+this.clientID+", size: "+resourceSize+" bytes — "+
592
631
  (sendSuccess ? "OK" : "FAILED"));
593
632
  if(sendSuccess)
594
633
  this.geometryService.EncodedResource(uid);
@@ -619,6 +658,10 @@ class Client {
619
658
  }
620
659
  var handshakeMessage=new message.HandshakeMessage();
621
660
  core.decodeFromUint8Array(handshakeMessage,data);
661
+ // Remember the client's axes standard so cubemaps are streamed in the matching variant.
662
+ this.clientAxesStandard=handshakeMessage.AxesStandard_axesStandard;
663
+ console.log("Client "+this.clientID+" handshake axesStandard="+this.clientAxesStandard
664
+ +" (cubemap suffix '"+core.AxesStandardToCubemapSuffix(this.clientAxesStandard)+"')");
622
665
  const excess =data.length-message.HandshakeMessage.sizeof();
623
666
  const numReceived =excess/core.UID_SIZE;
624
667
  if(numReceived!=handshakeMessage.uint64_resourceCount) {
package/core/core.js CHANGED
@@ -175,12 +175,136 @@ const AxesStandard =
175
175
  LeftHanded: 2,
176
176
  YVertical: 4,
177
177
  ZVertical: 8,
178
- EngineeringStyle: this.ZVertical | this.RightHanded,
179
- GlStyle: 16 | this.YVertical | this.RightHanded,
180
- UnrealStyle: 32 | this.ZVertical | this.LeftHanded,
181
- UnityStyle: 64 | this.YVertical | this.LeftHanded,
178
+ // NB: these must be explicit literals, not `this.ZVertical | ...`. Inside an object
179
+ // literal `this` is the module scope, not the object, so the bitwise expressions would
180
+ // silently evaluate to the wrong values (e.g. EngineeringStyle=0, GlStyle=16). The wire
181
+ // protocol and clients use 9 and 21, so hard-code the resolved values to match.
182
+ EngineeringStyle: 8 | 1, // ZVertical | RightHanded = 9
183
+ GlStyle: 16 | 4 | 1, // 16 | YVertical | RightHanded = 21
184
+ UnrealStyle: 32 | 8 | 2, // 32 | ZVertical | LeftHanded = 42
185
+ UnityStyle: 64 | 4 | 2, // 64 | YVertical | LeftHanded = 70
182
186
  };
183
187
 
188
+ //! Map a client's axes standard to the filename suffix used for the matching cubemap
189
+ //! variant. e.g. GlStyle -> "ogl", so /envCloudyCubemap.ktx2 -> /envCloudyCubemap_ogl.ktx2.
190
+ //! Returns "" for an unknown/uninitialised standard, meaning "serve the original file".
191
+ function AxesStandardToCubemapSuffix(axesStandard)
192
+ {
193
+ switch (axesStandard)
194
+ {
195
+ case AxesStandard.GlStyle: return "ogl";
196
+ case AxesStandard.EngineeringStyle: return "eng";
197
+ case AxesStandard.UnrealStyle: return "unreal";
198
+ case AxesStandard.UnityStyle: return "unity";
199
+ default: return "";
200
+ }
201
+ }
202
+
203
+ // ---- Axes-standard conversions for object transforms ----
204
+ // Ported from the C++ server (libavstream common_maths.h ConvertPosition/Rotation/Scale), which
205
+ // converts each node's transform from the server's axes standard to the client's during encoding.
206
+ // The C++ table only covers Unreal/Unity <-> Gl/Engineering (its server is engine-driven), so the
207
+ // Engineering <-> GlStyle cases below are added here (both right-handed: position/quat vector part
208
+ // maps as (x, z, -y); its inverse as (x, -z, y); scale permutes y/z).
209
+ function ConvertPosition(from, to, p)
210
+ {
211
+ const A = AxesStandard;
212
+ if (from === to) return { x: p.x, y: p.y, z: p.z };
213
+ if (from === A.UnrealStyle)
214
+ {
215
+ if (to === A.GlStyle) return { x: +p.y, y: +p.z, z: -p.x };
216
+ if (to === A.EngineeringStyle) return { x: p.y, y: p.x, z: p.z };
217
+ }
218
+ else if (from === A.UnityStyle)
219
+ {
220
+ if (to === A.GlStyle) return { x: p.x, y: p.y, z: -p.z };
221
+ if (to === A.EngineeringStyle) return { x: p.x, y: p.z, z: p.y };
222
+ }
223
+ else if (from === A.EngineeringStyle)
224
+ {
225
+ if (to === A.UnrealStyle) return { x: p.y, y: p.x, z: p.z };
226
+ if (to === A.UnityStyle) return { x: p.x, y: p.z, z: p.y };
227
+ if (to === A.GlStyle) return { x: p.x, y: p.z, z: -p.y }; // added
228
+ }
229
+ else if (from === A.GlStyle)
230
+ {
231
+ if (to === A.UnrealStyle) return { x: -p.z, y: +p.x, z: +p.y };
232
+ if (to === A.UnityStyle) return { x: p.x, y: p.y, z: -p.z };
233
+ if (to === A.EngineeringStyle) return { x: p.x, y: -p.z, z: p.y }; // added
234
+ }
235
+ console.warn("ConvertPosition: unsupported axes "+from+"->"+to+"; leaving unchanged");
236
+ return { x: p.x, y: p.y, z: p.z };
237
+ }
238
+
239
+ function ConvertRotation(from, to, q)
240
+ {
241
+ const A = AxesStandard;
242
+ if (from === to) return { x: q.x, y: q.y, z: q.z, w: q.w };
243
+ if (from === A.UnrealStyle)
244
+ {
245
+ if (to === A.GlStyle) return { x: -q.y, y: -q.z, z: +q.x, w: q.w };
246
+ if (to === A.EngineeringStyle) return { x: -q.y, y: -q.x, z: -q.z, w: q.w };
247
+ }
248
+ else if (from === A.EngineeringStyle)
249
+ {
250
+ if (to === A.UnrealStyle) return { x: -q.y, y: -q.x, z: -q.z, w: q.w };
251
+ if (to === A.UnityStyle) return { x: -q.x, y: -q.z, z: -q.y, w: q.w };
252
+ if (to === A.GlStyle) return { x: q.x, y: q.z, z: -q.y, w: q.w }; // added
253
+ }
254
+ else if (from === A.GlStyle)
255
+ {
256
+ if (to === A.UnrealStyle) return { x: +q.z, y: -q.x, z: -q.y, w: q.w };
257
+ if (to === A.UnityStyle) return { x: -q.x, y: -q.y, z: q.z, w: q.w };
258
+ if (to === A.EngineeringStyle) return { x: q.x, y: -q.z, z: q.y, w: q.w }; // added
259
+ }
260
+ else if (from === A.UnityStyle)
261
+ {
262
+ if (to === A.GlStyle) return { x: -q.x, y: -q.y, z: q.z, w: q.w };
263
+ if (to === A.EngineeringStyle) return { x: -q.x, y: -q.z, z: -q.y, w: q.w };
264
+ }
265
+ console.warn("ConvertRotation: unsupported axes "+from+"->"+to+"; leaving unchanged");
266
+ return { x: q.x, y: q.y, z: q.z, w: q.w };
267
+ }
268
+
269
+ function ConvertScale(from, to, s)
270
+ {
271
+ const A = AxesStandard;
272
+ if (from === to) return { x: s.x, y: s.y, z: s.z };
273
+ if (from === A.UnrealStyle)
274
+ {
275
+ if (to === A.GlStyle) return { x: +s.y, y: +s.z, z: s.x };
276
+ if (to === A.EngineeringStyle) return { x: s.y, y: s.x, z: s.z };
277
+ }
278
+ else if (from === A.UnityStyle)
279
+ {
280
+ if (to === A.GlStyle) return { x: s.x, y: s.y, z: s.z };
281
+ if (to === A.EngineeringStyle) return { x: s.x, y: s.z, z: s.y };
282
+ }
283
+ else if (from === A.EngineeringStyle)
284
+ {
285
+ if (to === A.UnrealStyle) return { x: s.y, y: s.x, z: s.z };
286
+ if (to === A.UnityStyle) return { x: s.x, y: s.z, z: s.y };
287
+ if (to === A.GlStyle) return { x: s.x, y: s.z, z: s.y }; // added (abs of position map)
288
+ }
289
+ else if (from === A.GlStyle)
290
+ {
291
+ if (to === A.UnrealStyle) return { x: s.z, y: +s.x, z: +s.y };
292
+ if (to === A.UnityStyle) return { x: s.x, y: s.y, z: s.z };
293
+ if (to === A.EngineeringStyle) return { x: s.x, y: s.z, z: s.y }; // added
294
+ }
295
+ return { x: s.x, y: s.y, z: s.z };
296
+ }
297
+
298
+ //! Convert a {position, orientation, scale} pose from one axes standard to another.
299
+ function ConvertPose(from, to, pose)
300
+ {
301
+ return {
302
+ position: ConvertPosition(from, to, pose.position),
303
+ orientation: ConvertRotation(from, to, pose.orientation),
304
+ scale: ConvertScale(from, to, pose.scale || { x: 1, y: 1, z: 1 }),
305
+ };
306
+ }
307
+
184
308
  const GeometryPayloadType =
185
309
  {
186
310
  Invalid: 0,
@@ -441,7 +565,9 @@ function put_string(dataView, byteOffset, name) {
441
565
 
442
566
  module.exports = {
443
567
  UID_SIZE, endian, SizeOfType, encodeIntoDataView, decodeFromDataView
444
- , vec4, BackgroundMode, AxesStandard, GeometryPayloadType, DisplayInfo, RenderingFeatures, LightingMode, VideoCodec
568
+ , vec4, BackgroundMode, AxesStandard, AxesStandardToCubemapSuffix
569
+ , ConvertPosition, ConvertRotation, ConvertScale, ConvertPose
570
+ , GeometryPayloadType, DisplayInfo, RenderingFeatures, LightingMode, VideoCodec
445
571
  , VideoConfig, ClientDynamicLighting, encodeToUint8Array, decodeFromUint8Array
446
572
  , generateUid, getStartTimeUnixUs, getTimestampUs,
447
573
  unixTimeToUTCString, put_float32, put_uint16, put_int32, put_uint32
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "name": "teleportxr",
18
18
  "description": "Teleport Spatial Server on node.js",
19
- "version": "1.0.91",
19
+ "version": "1.0.93",
20
20
  "repository": {
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/simul/teleport-nodejs.git"
@@ -13,13 +13,15 @@ function putPlaceholderSize(dataView)
13
13
  }
14
14
 
15
15
  // return the size of the encoded node.
16
- function encodeNode(node,buffer)
16
+ // fromAxes/toAxes, when supplied, convert the node's transform from the server's axes standard to
17
+ // the client's during encoding (matches the C++ server). Omit them to encode the pose unchanged.
18
+ function encodeNode(node,buffer,fromAxes,toAxes)
17
19
  {
18
20
  var byteOffset=0;
19
- const dataView = new DataView(buffer);
21
+ const dataView = new DataView(buffer);
20
22
  byteOffset=putPlaceholderSize(dataView);
21
23
 
22
- var t=node.encodeIntoDataView(dataView,byteOffset);
24
+ var t=node.encodeIntoDataView(dataView,byteOffset,fromAxes,toAxes);
23
25
  byteOffset=t;
24
26
  // Actual size is now known: write the count of bytes that follow the
25
27
  // size field, in little-endian (the protocol convention). Without the
@@ -13,12 +13,14 @@ function putPlaceholderSize(dataView)
13
13
  }
14
14
 
15
15
  // return the size of the encoded resource.
16
- function EncodeResource(res,buffer)
16
+ // urlOverride, if supplied, is passed to the resource so it can encode a client-specific
17
+ // url (e.g. an axes-standard cubemap variant) without mutating the shared resource.
18
+ function EncodeResource(res,buffer,urlOverride)
17
19
  {
18
20
  var byteOffset=0;
19
- const dataView = new DataView(buffer);
21
+ const dataView = new DataView(buffer);
20
22
  byteOffset=putPlaceholderSize(dataView);
21
- var t=res.encodeIntoDataView(dataView,byteOffset);
23
+ var t=res.encodeIntoDataView(dataView,byteOffset,urlOverride);
22
24
  byteOffset=t;
23
25
  t-=8;
24
26
  // Actual size is now known: write the count of bytes that follow the
package/scene/node.js CHANGED
@@ -308,7 +308,7 @@ class Node {
308
308
  );
309
309
  this.components.push(tc);
310
310
  }
311
- encodeIntoDataView(dataView, byteOffset) {
311
+ encodeIntoDataView(dataView, byteOffset, fromAxes, toAxes) {
312
312
  byteOffset = core.put_uint8(
313
313
  dataView,
314
314
  byteOffset,
@@ -317,8 +317,19 @@ class Node {
317
317
 
318
318
  byteOffset = core.put_uint64(dataView, byteOffset, this.uid);
319
319
  byteOffset = core.put_string(dataView, byteOffset, this.name);
320
+ // Convert the node's transform into the client's axes standard (server -> client). When no
321
+ // conversion is requested, or the standards match, the pose is encoded unchanged.
320
322
  var clientsidePose = this.pose;
321
-
323
+ if (fromAxes !== undefined && toAxes !== undefined && fromAxes !== toAxes &&
324
+ toAxes !== core.AxesStandard.NotInitialized)
325
+ {
326
+ const c = core.ConvertPose(fromAxes, toAxes, this.pose);
327
+ clientsidePose = new Pose();
328
+ clientsidePose.position = c.position;
329
+ clientsidePose.orientation = c.orientation;
330
+ clientsidePose.scale = c.scale;
331
+ }
332
+
322
333
  byteOffset = clientsidePose.encodeIntoDataView(dataView, byteOffset);
323
334
 
324
335
  byteOffset = core.put_uint8(dataView, byteOffset, this.stationary);
@@ -16,16 +16,21 @@ class Resource {
16
16
  this.uid = uid;
17
17
  this.url = url;
18
18
  this.type = type;
19
+ // True for environment cubemaps (background/diffuse/specular). Such resources are
20
+ // served per-client in the variant matching the client's axes standard.
21
+ this.isCubemap = false;
19
22
  }
20
23
  encodedSize(){
21
24
  return 500;
22
25
  }
23
- encodeIntoDataView(dataView, byteOffset) {
26
+ //! urlOverride, if supplied, replaces this.url for this encoding only (e.g. to send a
27
+ //! client-specific cubemap variant). The stored this.url is left untouched.
28
+ encodeIntoDataView(dataView, byteOffset, urlOverride) {
24
29
  byteOffset = core.put_uint8(dataView, byteOffset, this.type);
25
30
  byteOffset = core.put_uint64(dataView, byteOffset, this.uid);
26
- var url = this.url;
27
- if (this.url.search("://") == -1)
28
- url = Resource.defaultPathRoot + this.url;
31
+ var url = urlOverride || this.url;
32
+ if (url.search("://") == -1)
33
+ url = Resource.defaultPathRoot + url;
29
34
  byteOffset = core.put_string(dataView, byteOffset, url);
30
35
  return byteOffset;
31
36
  }
@@ -211,6 +216,28 @@ function GetOrAddTexture(url) {
211
216
  return GetOrAddResourceFromUrl(core.GeometryPayloadType.TexturePointer, url);
212
217
  }
213
218
 
219
+ //! Get or add a cubemap texture, flagging the resource so it is served per-client in the
220
+ //! variant matching the client's axes standard. Returns the resource uid.
221
+ function GetOrAddCubemap(url) {
222
+ const uid = GetOrAddTexture(url);
223
+ const res = GetResourceFromUid(uid);
224
+ if (res)
225
+ res.isCubemap = true;
226
+ return uid;
227
+ }
228
+
229
+ //! Insert an axes suffix before a cubemap URL's extension:
230
+ //! InsertCubemapAxesSuffix("/envCloudyCubemap.ktx2", "ogl") -> "/envCloudyCubemap_ogl.ktx2"
231
+ //! Returns the url unchanged when the suffix is empty.
232
+ function InsertCubemapAxesSuffix(url, suffix) {
233
+ if (!suffix || !url)
234
+ return url;
235
+ const dot = url.lastIndexOf(".");
236
+ if (dot < 0)
237
+ return url + "_" + suffix;
238
+ return url.substring(0, dot) + "_" + suffix + url.substring(dot);
239
+ }
240
+
214
241
  //! Get or add the mesh url as a resource.
215
242
  function GetOrAddMesh(url) {
216
243
  return GetOrAddResourceFromUrl(core.GeometryPayloadType.MeshPointer, url);
@@ -238,6 +265,8 @@ module.exports = {
238
265
  GetOrAddResourceUidFromUrl,
239
266
  GetResourceFromUid,
240
267
  GetOrAddTexture,
268
+ GetOrAddCubemap,
269
+ InsertCubemapAxesSuffix,
241
270
  GetOrAddMesh,
242
271
  AddFontAtlas,
243
272
  AddTextCanvas,
package/scene/scene.js CHANGED
@@ -15,6 +15,10 @@ class Scene {
15
15
  this.specularCubemapPath="";
16
16
  this.assetsPath="assets";
17
17
  this.publicPath="http_resources";
18
+ // The axes standard the scene (node poses, cubemaps) is authored in. Node transforms are
19
+ // converted from this to each client's axes standard when streamed. Z-up Engineering by
20
+ // default, matching the C++ client's native frame and the *_eng cubemap source.
21
+ this.serverAxesStandard=core.AxesStandard.EngineeringStyle;
18
22
  }
19
23
  GetOrCreateNode(uid) {
20
24
  if (!this.nodes.has(uid)) {
@@ -191,17 +195,19 @@ class Scene {
191
195
  if(j.environment.background_texture)
192
196
  {
193
197
  this.backgroundTexturePath=j.environment.background_texture;
194
- resources.GetOrAddTexture(this.backgroundTexturePath);
198
+ // The environment background is a cubemap; flag it so each client is served the
199
+ // variant matching its axes standard (falls back to the base file if absent).
200
+ resources.GetOrAddCubemap(this.backgroundTexturePath);
195
201
  }
196
202
  if(j.environment.diffuse_cubemap)
197
203
  {
198
204
  this.diffuseCubemapPath=j.environment.diffuse_cubemap;
199
- resources.GetOrAddTexture(this.diffuseCubemapPath);
205
+ resources.GetOrAddCubemap(this.diffuseCubemapPath);
200
206
  }
201
207
  if(j.environment.specular_cubemap)
202
208
  {
203
209
  this.specularCubemapPath=j.environment.specular_cubemap;
204
- resources.GetOrAddTexture(this.specularCubemapPath);
210
+ resources.GetOrAddCubemap(this.specularCubemapPath);
205
211
  }
206
212
  }
207
213
  if(j.font_atlases)