Babylon JS Day 15

Today is Project Friday so I took the day to work on the scene that I’ve been building throughout Week Three. Rather than produce a demo scene like in the previous weeks, today I just made incremental progress the interactive 3D GUI cards for VR.

I started by adding some images to the item cards. All the cards use the same image for now, but eventually the cards will load images related to their post data. I added a new GUI Image object to the panel on the item card.

// Load an image from the project assets folder 
var image = new BABYLON.GUI.Image("image", "../assets/images/babylon-month/2021.03.02-A-Month-of-Babylon-JS.png");
  image.height = "400px";
  panel.addControl(image);

Then I went to work on the detail card that I mocked up yesterday. The detail card is a box object that has a plane object as a child. The plane contains an AdvancedDynamicTexture where I draw the data that needs to be displayed.

I added a ScrollViewer to the card instead of using a StackPanel. I’m not sure if I’ll keep this around or not. It’s nice to know that I can make scrollable content in VR, but I’m not convinced this is the best way to handle substantial amounts of content. I added a TextBlock and setup the padding and sizing to work with the ScrollViewer. I referenced this playground when I was building the card.

const detailCard = BABYLON.MeshBuilder.CreateBox("detailCard", { height: 2.2, width: 3, depth: 0.2 });
    detailCard.position = new BABYLON.Vector3(1.8, 1.6, -0.3);
    detailCard.rotation = new BABYLON.Vector3(0, 0.5, 0);
    detailCard.scaling = new BABYLON.Vector3(0.3, 0.3, 0.3);
    makeGrabbable(detailCard);

    const detailPlane = BABYLON.MeshBuilder.CreatePlane("plane", { height: 2, width: 3 });
    detailPlane.parent = detailCard;
    detailPlane.position.z = -0.11;
    detailPlane.position.y = 0.1;
    var advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateForMesh(detailPlane);
    advancedTexture.background = "white";

    var sv = new BABYLON.GUI.ScrollViewer();
    sv.thickness = 8;
    sv.color = "gray";
    sv.width = 1;
    sv.height = 1;
    sv.background = "white";

    advancedTexture.addControl(sv);

    var detailNote = new BABYLON.GUI.TextBlock("detailNote");
    detailNote.fontFamily = "Tahoma, sans-serif";
    detailNote.text = "This is a test";
    detailNote.textWrapping = true;
    detailNote.color = "black";
    detailNote.fontSize = 24;
    detailNote.resizeToFit = true;
    detailNote.paddingTop = "5%";
    detailNote.paddingLeft = "30px";
    detailNote.paddingRight = "20px";
    detailNote.paddingBottom = "5%";
    detailNote.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
    detailNote.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
    detailNote.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
    detailNote.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;

    detailNoteTextBlock = detailNote;

    sv.addControl(detailNote);

Then it was just a matter of updating the button action on the info cards to send data to the detail card. I cheated here. I couldn’t find a way to query the TextBlock. It has something to do with the fact that it’s rendered insider of the AdvancedDynamicTexture. For now, I just made a global variable that holds a reference to the detailNoteTextBlock. While not ideal, there is only one of these cards in the scene so this will work until I can find a better way.

 button1.onPointerUpObservable.add(function () {
    let detailCard = scene.getMeshByName("detailCard");
    if (!detailCard.isEnabled()) {
      detailCard.setEnabled(true);
      detailNoteTextBlock.text = decodeHtml(item.contentEncoded);
    } else {
      detailCard.setEnabled(false);
      detailNoteTextBlock.text = "";
    }
  });

Here is a preview of selecting an item card and sending data to the detail card. The sample data I’m working with contains HTML markup, but the TextBlock doesn’t know what to do with it. I need to create some better sample data…

Select an item card to show data on the detail card.

I rewrote the process that positions the item cards. When I load the sample data, I get back an array of 15-25 items. I decided to chunk this array in an array of sub-arrays, then loop over both arrays to set positions for each card. I also created an anchor object so I can move all the cards together.

function createCards(scene, items) {
  var anchor = new BABYLON.TransformNode("itemCards");
  anchor.position = new BABYLON.Vector3(0, 2.2, 0);

  let chunked = chunk(items, 5);
  let j = 0;
  for (section of chunked) {
    j++;
    let i = 0;
    for (item of section) {
      i++;
      let x = 0.25 * i;
      let y = -0.35 * j;
      let positionObject = {
        x: x,
        y: y,
        z: 0
      };
      createCard(scene, item, positionObject, anchor);
    }
  }
}

Then I moved on to VR interaction. There are a ton of VR features that I want to add over time. I started with a simple “grab” feature. While looking for examples on the Babylon JS forum I found this post and adapted the sample code to suit my needs. The card meshes are grabbable, but the GUI content (and the plane) are not. To get around that I added an extra section to the bottom of the cards. This grab feature relies on the pointer event system. A user points a controller at a card, presses the trigger button to start the grab, then moves the card around the scene. Press the trigger again to drop the card. Not a bad start.

Moving cards

That’s all that I had time for today. This weekend I may spend a bit more time on this. I’d like to start working with some better sample data. I have a personal database of my VR games and apps. I might use that instead of the blog post data. While this demo scene is just part of the learning process for me this month, the features I’m working on are something that I want to use on several projects in the future.

Item cards and a detail card