How do I load gltf with drag and drop from user input file

Hello,

I am trying to create a viewer with drag and drop feature so the user can view any asset they want on ReactJS.
So far, I got .glb file type work by creating blob with window.URL.createObjectURL(file) then pass that blob link into SceneLoader.ImportMeshAsync.
Now, I want to get gltf file to work as well. But for that I would have to drag and drop the entire folder that contain texture, scene.gltf, scene.bin, etc. I could create blob for each of the object, but I would have to map the texture path etc inside scene.gltf to match those newly created blob. I think Threejs can do this. I am just new to Babylon, and I would appreciate for any help.

1 Like

I don’t have a solution for you, but I know the code on this page does what you’re asking for:

As for myself, once I’ve created a file object for each file entity (.bin, .gltf and accompanying image/texture files), I just insert it into BABYLON.FilesInputStore object like so:

  gltfFiles.forEach((file) => {
    BABYLON.FilesInputStore.FilesToLoad[file.name] = file
  })
3 Likes

May I ask where to find the source code for that sandbox?

As for FilesInputStore, would it work for textures with trees? Because some textures could be inside a directory, which could be inside another directory

1 Like

It should work with nested folders. Here is the reference for FilesInput: FilesInput | Babylon.js Documentation (babylonjs.com).

4 Likes

I found a working solution (not necessarily the best?) eventually for my specific React project. I can drag and drop multiple glb and gltf files/folders/zip on my nettlify web now as you can see.

In the component, I created a custom hook to traverse through all the drag-and-drop files, including contents of zip, then get the files and put them into FilesInputStore.FilesToLoad

import { FilesInputStore } from "babylonjs";
import { loadAsync } from "jszip";
import { useDispatch } from "react-redux";
import { clearAllFiles, loadAllFiles } from "../reducer";
import { CompressedDirectoryEXT } from "../types";
import { getEXT, getPathAndName } from "../utils";

export function useLoadFiles() {
  const dispatch = useDispatch();

  return (itemList: DataTransferItemList) =>
    queueFiles(itemList)
      .then(() => dispatch(loadAllFiles()))
      .catch(() => dispatch(clearAllFiles()));
}

function queueFiles(itemList: DataTransferItemList) {
  const entries: FileSystemEntry[] = [];
  for (let i = 0; i < itemList.length; i++) {
    const entry = itemList[i].webkitGetAsEntry();
    if (entry) entries.push(entry);
  }
  return Promise.all(entries.map((entry) => queueFileTree(entry)));
}

function queueFileTree(entry: FileSystemEntry, path = ""): Promise<void> {
  return new Promise((resolve, reject) => {
    const errorCallback = (error?: DOMException) =>
      reject(`Failed to load ${entry.name}${error ? `: ${error}` : ""}`);
    if (entry.isFile) {
      (entry as FileSystemFileEntry).file((file) => {
        if (getEXT(file.name) === CompressedDirectoryEXT.ZIP)
          queueCompressedDirectory(file, path)
            .then(() => resolve())
            .catch(errorCallback);
        else {
          appendFileToLoad(path + file.name, file);
          resolve();
        }
      }, errorCallback);
    } else if (entry.isDirectory) {
      const reader = (entry as FileSystemDirectoryEntry).createReader();
      reader.readEntries(async (entries) => {
        for (let i = 0; i < entries.length; i++)
          await queueFileTree(entries[i], path + entry.name + "/");
        resolve();
      }, errorCallback);
    } else errorCallback();
  });
}

async function queueCompressedDirectory(zipFile: File, path: string) {
  const unzipped = await loadAsync(zipFile);
  const entries = Object.entries(unzipped.files);
  for (let [entryPath, jsZipObject] of entries) {
    if (jsZipObject.dir) continue;
    const { name, path: pathWithinZip } = getPathAndName(entryPath);
    const blob = await jsZipObject.async("blob");
    const file = new File([blob], name);
    const fullPath = path + zipFile.name + "/" + pathWithinZip;
    if (getEXT(name) === CompressedDirectoryEXT.ZIP)
      await queueCompressedDirectory(file, fullPath);
    else appendFileToLoad(fullPath + name, file);
  }
}

function appendFileToLoad(path: string, file: File) {
  FilesInputStore.FilesToLoad[path.toLowerCase()] = file;
}

Then inside the middleware where I set up my scene, I go through all files inside FilesInputStore, look for anything with extension .glb and .gltf and load them

  public async loadAllFiles() {
    const pathNameCombined = Object.keys(FilesInputStore.FilesToLoad);
    const loadedMeshes: AbstractMesh[] = [];
    for (let i = pathNameCombined.length - 1; i >= 0; i--) {
      const fullPath = pathNameCombined[i];
      const ext = getEXT(fullPath);
      if (!ext || !([Asset3DEXT.GLB, Asset3DEXT.GLTF] as EXT[]).includes(ext))
        continue;
      const { path, name } = getPathAndName(fullPath);
      try {
        const result = await SceneLoader.ImportMeshAsync(
          "",
          `file:${path}`,
          name,
          this.scene,
          null,
          ext
        );
        loadedMeshes.push(...this.processSceneLoaderResult(result, {}));
      } catch {
        throw new Error("Encountered error in loading " + fullPath);
      }
    }
    return loadedMeshes;
  }

My breakthrough was putting file:${path} as url and making sure all keys inside FilesInputStore are lower case paths.

I am still not sure how to use FilesInput properly. It does seem to call reload callback when I drop file, but why reload callback is called but not success callback?

Source code for my project is GitHub - vtranduc/babylon-on-saga-experimental

I am still new to babylon after few months of coding in threejs and there’s so much questions I have about it. Thank you for responses!

3 Likes

My breakthrough was putting file:${path} as url and making sure all keys inside FilesInputStore are lower case paths.

The lower case path thing has gotten me so many times lol, but good job! This is pretty much the same I do when processing groups of files that form a gltf bundle.