Reposition and rotate a BIM Model tileset

I’m trying to relocate a iTwin BIM model (iModel) from its source location (0,0,0) to a new location on the other side of the globe, which means it also needs to be rotated. The relocation bit of the code works as designed, but the model tileset is sitting at an angle against the surface of the globe. I understand “quaternion” is part of the answer, but not able to figure it out. Any help very much appreciated. Here is an extract of my code:

   if (auth.isAuthenticated) {
      let viewer: Cesium.Viewer | undefined = undefined;
      let selectedFeature: any | undefined;
      let iModelTiles: Cesium.Cesium3DTileset | undefined = undefined;

      useEffect(() => {
         const initializeCesium = async () => {
            Cesium.ITwinPlatform.defaultAccessToken = auth.user?.access_token!;

            // Set up viewer
            viewer = new Cesium.Viewer('cesiumContainer', {
               animation: false,
               sceneModePicker: false,
               geocoder: Cesium.IonGeocodeProviderType.GOOGLE,
               homeButton: true,
               infoBox: false,
               timeline: false,
               globe: false,
            });
            const scene = viewer.scene;

            // Configure the mouse over event for Cesium Feature and Element Id
            const handler = new Cesium.ScreenSpaceEventHandler(scene?.canvas);
            handler.setInputAction((movement: any) => {
               if (selectedFeature) {
                  selectedFeature = unselectFeature(selectedFeature);
               }

               const feature = scene?.pick(movement.endPosition);
               if (feature instanceof Cesium3DTileFeature) {
                  selectedFeature = selectFeature(feature);
               }
            }, ScreenSpaceEventType.MOUSE_MOVE);

            Cesium.Fullscreen.requestFullscreen(scene.canvas);

            // Use Google Photo realistic tiles
            const tileset = await Cesium.createGooglePhotorealistic3DTileset();
            scene.primitives.add(tileset);

            // Overlay the iTwin / iModel BIM Models
            iModelTiles = await Cesium.ITwinData.createTilesetFromIModelId('d60aef62-0d6b-41f0-8f95-5551591c02c3');
            scene.primitives.add(iModelTiles);

            //Find the position of the Model in the Cesium world
            const cartographic = Cesium.Cartographic.fromCartesian(iModelTiles!.boundingSphere.center);
            console.log('cartographic', cartographic);
            const surface = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0.0);
            console.log('surface', surface);

            //Determine the new position of the model in the Cesium world
            const offset = Cesium.Cartesian3.fromDegrees(-117.412848, 33.273222, 0);
            console.log('offset', offset);
            const updatedPosition = Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3());
            console.log('updatedPosition', updatedPosition);

            //const newModelMatrix = Cesium.Matrix4.fromTranslation(updatedPosition);
            //iModelTiles!.modelMatrix = newModelMatrix;

            //Up to here it works - repositioning the model in the Cesium world

            const heading = Cesium.Math.toRadians(135);
            const pitch = 0;
            const roll = 0;
            const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
            console.log('hpr', hpr);

            const quaternion = Cesium.Transforms.headingPitchRollQuaternion(updatedPosition, hpr);
            console.log('quaternion', quaternion);

            const rotationMatrix = Cesium.Matrix3.fromQuaternion(quaternion);
            console.log('rotationMatrix', rotationMatrix);

            const newModelMatrix = Cesium.Matrix4.fromRotationTranslation(rotationMatrix, updatedPosition);
            console.log('newModelMatrix', newModelMatrix);

            iModelTiles!.modelMatrix = newModelMatrix;
            // this will reposition and rotate the model, but up in the sky somewhere, and when the mouse moves the model disappears

            //Prep the bounding sphere for the iModel tileset so we can zoom to it
            const bs: Cesium.BoundingSphere = iModelTiles!.boundingSphere;
            viewer!.camera.flyToBoundingSphere(bs, { duration: 2 });
         };

         initializeCesium();
      }, [viewer]);

It can be a bit difficult to try this out, based on a (react) code snippet that contains some unrelated elements, and without the proper access rights. A Cesium Sandcastle can be really helpful in these cases.

However, it might be that the answer is simple (and does not even involve the word “quaternion”): You might be able to use the “East-North-Up-To-Fixed-Frame-Transform” for placing the model.

Here is an example where I took that “train station” model from the existing iModel Mesh Export Service Sandcastle, did brutally reset some transform that it already had (to essentially “place it at (0,0,0)”), and then used the eastNorthUpToFixedFrame matrix to create the modelMatrix to place that station somewhere in Philly:

https://sandcastle.cesium.com/index.html#c=nVV/b+I4EP0qFn+BBA6/Cke3ra5HaRuWhJaG0laRTk4yEIMTR7aBhtN+93US6FG2dzpdUKR4PDPvzZuxMQx0BzEIogARJEMiAK0gRXMuEPF9kBIpjkiMqLOlMdpSFfK1QuPrtQrd2DBQqFQizw0jgA0wnoDAHsSKQYp9HhkkodIo8tR8HivBWW3TNDI/oiiPpeEL0Ng1qnT6Wo5vuHEfJF1H2MwwHxhRmk2EA5iTNVNPmc93TfHSjRFyS5AOQ+/Op2M6NKc7s2FTU5rx5Mzvmx1zlbw894c9rJ2S5/sg8VZsZS45tR2r8eqY9VfHUuOZmY6f6vU3x9yNZkNqzx61zWZvd2bDWk6W9u5xZ9ItJbNHDWI2bWewtXbTnUXrTSuatkbOJHq7K2LstEHHN+b7azRIrcg8s5cWHfWHDO6v6Xg5aNk7f2s707blhD382LMWcjZb+R3Pa+zup8MuCd+W7w+KOLPBMrE3ExZ37afWnFhu6Zsbu7GWUCq0obAFgS5RDFu0l+o5t5Xdkp+v+1prQnVf3VLl2yFOqlxzHUi2hCp0rPINUQQXvXAoAwnqVvDItHgAzAzKhdSdTi8IoNOtgdfp1dpQ92qeP2/W5t1mEwB67W6745aqblzJ2erpeHl5QbfjCRq8XFsPo8E5+kOsFWEsRRoBRVl2lHBJc1560Mr1qv5VUDkR3CMeSzHGFTfeM8d/Cs4VVoLEMhsJXcm+BosoQd/b2Gc8hvKJ0bwZ2I7pvH6w6vMoWeuBV6F+P5JlI59bivozOh6ghBEfApTHEZU7LOgGYqSRDD2aRgh0EaqDxl9wcw4miYFIZXOhwmni8Fv6DsGtIBHk8u69+0Qo/UXiFp7rDtzAQgDIcq17hhtnzXb9typq9XCv3el1z6qoWa8cqd3P+5dzLKSNcgUO3HJbIcr/UO5kK9JHkSYsLR9lrf5dfvUYrSD4V1alZjl+cMyxfT06Rzf5zUKCIO8/YUjw/YiGIKCK6BwFGlVAoMcgiy7qOHi1TqsoJJvst9+Oqgmx4hMSaFlluVevZIROs/2TMMcpyx/QRYb/pMlngF+E+bFv3rWUdBF/0bzD8H9u39Hq0P4R56tcqOKCwNLXNztOBI20vhuQWCtd3mfLkPduO84jhx9vlKqlC6lSBldZkdnzO40SPbdoLVgZY0NBpM+FnlPDW/srUNiXslAEoQvjOPQioBtEg8svLibkMyKl3pmvGXuiO3BLVxeG9v8llHHdungx3oBgJM3cwsbVqDDqgi8Mvfw6UnHOPKLBPio5IhSCv/L4+wnsJ2CSgpD/4uDr0yuI/t/S4vhrdep6/P0T

For the case that you additionally want to rotate that model around “its local up-vector”, I added an example of how that rotation matrix could be integrated into the model matrix, but … that’s optional and just for illustration.

Awesome, thank you!