Performant ways to render many images on planes in a scene

It sounds like the text on a material approach is going to be better than using images on ADTs. The developer who generates the image was able to create a texture atlas version. There was some work to do to turn the coordinates he sent me into UV data, but it totally worked.

This is very much a work in progress, but it lets us use a single material for all the meshes.

import * as BABYLON from "@babylonjs/core";
import { useDataStore } from "../composables/DataStore";
import { watch } from "vue";

const { clipboardDataFlat, clipboardDataDictionary, setMainSelectionID, mainSelectionID, textureAtlasImage } = useDataStore();

export class LLProcessor {
  private scene: BABYLON.Scene;
  private layoutMat!: BABYLON.StandardMaterial;
  private selectMat!: BABYLON.StandardMaterial;
  private textureAtlas: BABYLON.Texture | null = null;

  constructor(scene: BABYLON.Scene) {
    this.scene = scene;
    this.initializeLayoutMat();
  }

  private initializeLayoutMat(): void {
    this.layoutMat = new BABYLON.StandardMaterial("timeline-material", this.scene);
    this.layoutMat.alpha = 0.75;
    this.layoutMat.backFaceCulling = false;

    this.selectMat = new BABYLON.StandardMaterial("timeline-material", this.scene);
    this.selectMat.diffuseColor = BABYLON.Color3.FromHexString("#818CF8");
    this.selectMat.specularColor = new BABYLON.Color3(0.2, 0.2, 0.2);
  }

  clean() {
    this.scene.meshes.forEach((mesh) => {
      if (mesh.name !== "grid") {
        mesh.dispose();
      }
    });
  }

  generate() {
    if (textureAtlasImage.value) {
      const b64String = `data:image/png;base64,${textureAtlasImage.value}`;

      this.textureAtlas = new BABYLON.Texture(b64String, this.scene);
      this.textureAtlas.hasAlpha = true;

      this.layoutMat.diffuseTexture = this.textureAtlas;
      this.layoutMat.emissiveTexture = this.textureAtlas;
      this.layoutMat.useAlphaFromDiffuseTexture = true;

      this.textureAtlas.onLoadObservable.add(() => {
        clipboardDataFlat.forEach((element: any) => {
          this.createLayerBox(element, this.scene);
        });
      });
    }
  }

  // TODO Conssider a custom type or interface for the clipboard data instead of using any
  private createLayerBox = (item: any, scene: BABYLON.Scene) => {
    const offset = 100;
    const parentID = item.idParent;
    const parent = (clipboardDataDictionary as any)[parentID];
    const width = (item.boundsRight - item.boundsLeft) / offset;
    const height = (item.boundsBottom - item.boundsTop) / offset;

    let posX = item.boundsLeft; // get the item's left position
    if (parent) {
      posX += parent.boundsLeft; // add the parent's left position
    }
    posX /= offset; // divide by the offset
    posX += width / 2; // add half the width

    let posY = item.boundsTop; // get the item's top position
    if (parent) {
      posY += parent.boundsTop; // add the parent's top position
    }
    posY /= offset; // divide by the offset
    posY += height / 2; // add half the height

    let posZ = item.containmentDepth + item.renderDepth / 100;

    if (this.textureAtlas) {
      const atlasSize = this.textureAtlas.getSize(); // Get the size of the texture atlas

      const { upperLeftX, upperLeftY, lowerRightX, lowerRightY } = item.textureCoordinates;
      const u1 = upperLeftX / atlasSize.width;
      const v1 = 1 - upperLeftY / atlasSize.height; // v coordinates are flipped
      const u2 = lowerRightX / atlasSize.width;
      const v2 = 1 - lowerRightY / atlasSize.height; // v coordinates are flipped

      console.log(item.name, `u1: ${u1}, v1: ${v1}, u2: ${u2}, v2: ${v2}`);

      // Create a custom mesh with the calculated UV coordinates
      // The UV coordinates are assigned in the order: bottom-left, bottom-right, top-right, top-left
      const verticesData = [
        -width / 2,
        -height / 2,
        0,
        u1,
        v2, // bottom-left
        width / 2,
        -height / 2,
        0,
        u2,
        v2, // bottom-right
        width / 2,
        height / 2,
        0,
        u2,
        v1, // top-right
        -width / 2,
        height / 2,
        0,
        u1,
        v1 // top-left
      ];
      const indices = [0, 1, 2, 0, 2, 3];
      const mesh = new BABYLON.Mesh(item.id, scene);
      const vertexData = new BABYLON.VertexData();
      vertexData.positions = verticesData.filter((_, i) => i % 5 < 3);
      vertexData.indices = indices;
      vertexData.uvs = verticesData.filter((_, i) => i % 5 >= 3);
      vertexData.applyToMesh(mesh);

      // Set the position, rotation and material of the mesh
      mesh.position.x = -posX;
      mesh.position.y = -posY;
      mesh.position.z = posZ;
      mesh.rotation.y = Math.PI;
      mesh.material = this.layoutMat;

      // Add an action manager to the mesh
      const am = new BABYLON.ActionManager(scene);
      mesh.actionManager = am;
      mesh.actionManager.registerAction(
        new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnPickTrigger, () => {
          setMainSelectionID(item.id);
        })
      );
    }

    // Watch mainSelectionID for changes
    watch(mainSelectionID, (newValue, oldValue) => {
      // get mesh by id: old value
      const oldMesh = scene.getMeshById(oldValue)! as BABYLON.Mesh;
      if (oldMesh) {
        oldMesh.showBoundingBox = false;
      }
      const oldMat = scene.getMaterialByName(oldValue)! as BABYLON.StandardMaterial;
      if (oldMat) {
        oldMat.alpha = 0.75;
      }

      // get mesh by id: new value
      const newMesh = scene.getMeshById(newValue);
      if (newMesh) {
        newMesh.showBoundingBox = true;
        // newMesh.material?.alpha = 1;
        const camera = scene.activeCamera as BABYLON.ArcRotateCamera;
        if (camera) {
          camera.setTarget(newMesh);
        }
      }

      const newMat = scene.getMaterialByName(newValue)! as BABYLON.StandardMaterial;
      if (newMat) {
        newMat.alpha = 1;
      }
    });
  };
}

1 Like