Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support loading models from app private storage #2987

Closed
l3utterfly opened this issue Sep 3, 2023 · 5 comments · Fixed by #2982
Closed

Support loading models from app private storage #2987

l3utterfly opened this issue Sep 3, 2023 · 5 comments · Fixed by #2982
Labels
enhancement New feature or request

Comments

@l3utterfly
Copy link
Contributor

Typical use-case:

  1. app downloads model files from an online repository
  2. saves those files in the app private storage, for example using react-native-fs. Those files will be present in a path such as ${RNFS.DocumentDirectoryPath}/path/to/model. They can be accessed through expo assets via: file://{full path}
  3. Developers should be able to load the model using const fbx = useLoader(FBXLoader, 'file://' + path)

The current implementation of does not seem to support this. It tries to call "load" from threejs, which tries to download the file from the internet, which does not work.

This is a modified FBXLoader that will work for loading local FBX files:

class LocalFBXLoader extends Loader {
  constructor(manager) {
    super(manager);
  }
  load(url, onLoad, onProgress, onError) {
    // url must start with "file://"
    if (!url.startsWith('file://')) {
      throw new Error("LocalFBXLoader: url must start with 'file://'");
    }

    const scope = this;
    const path =
      scope.path === '' ? LoaderUtils.extractUrlBase(url) : scope.path;

    // Read file as ArrayBuffer
    RNFS.readFile(url, 'base64')
      .then(base64String => {
        const raw = base64.decode(base64String); // Decode base64
        const rawLength = raw.length;
        const buffer = new ArrayBuffer(rawLength);
        const array = new Uint8Array(buffer);
        for (let i = 0; i < rawLength; i++) {
          array[i] = raw.charCodeAt(i);
        }

        onLoad(scope.parse(buffer, path + 'Textures/'));
      })
      .catch(onError);
  }

  loadAsync(url, onProgress) {
    // return a Promise after calling this.load
    const scope = this;

    return new Promise((resolve, reject) => {
      scope.load(url, resolve, onProgress, reject);
    });
  }

.... Rest of code same as ThreeJS/FBXLoader

I have a working version of this loader being used in my app. If you feel this is a worthwhile inclusion, I can make a pull request.

@CodyJasonBennett CodyJasonBennett added the enhancement New feature or request label Sep 4, 2023
@CodyJasonBennett
Copy link
Member

Is this related to #2986 or still a problem despite our use of expo-asset? We don't assume that loaders implement parse nor is that implemented consistently, so we default to the networking stack except in specific cases (#2982). This could be one of them.

@l3utterfly
Copy link
Contributor Author

l3utterfly commented Sep 4, 2023 via email

@CodyJasonBennett
Copy link
Member

We had a similar case already implemented for Android release mode where we have to copy to a cache directory. Included a possible fix in dd4b300 or https://pkg.csb.dev/pmndrs/react-three-fiber/commit/dd4b3006/@react-three/fiber.

@l3utterfly
Copy link
Contributor Author

l3utterfly commented Sep 4, 2023 via email

@l3utterfly
Copy link
Contributor Author

Hi, I quickly tried that branch with a local file, got the error: Error: Could not load file:///data/user/0/com.laylascenevisualiser/files/lola/lola_unreal.fbx: Cannot load an empty url

Tracing through the code a little, I think the issue is this:

  1. calling threejs's useFBX loader
  2. It calls the FileLoader implementation which attempts to load the file
  3. you are polyfilling the FileLoader.load method with call to getAsset

Here's the polyfill code I found:

THREE__namespace.FileLoader.prototype.load = function load(url, onLoad, onProgress, onError) {
    if (this.path) url = this.path + url;

    const request = new XMLHttpRequest();
    getAsset(url).then(asset => {
      request.open('GET', asset.uri, true);
      request.addEventListener('load', event => {
        if (request.status === 200) {
          onLoad == null ? void 0 : onLoad(request.response);
          this.manager.itemEnd(url);
        } else {
          onError == null ? void 0 : onError(event);
          this.manager.itemError(url);
          this.manager.itemEnd(url);
        }
      }, false);
      request.addEventListener('progress', event => {
        onProgress == null ? void 0 : onProgress(event);
      }, false);
      request.addEventListener('error', event => {
        onError == null ? void 0 : onError(event);
        this.manager.itemError(url);
        this.manager.itemEnd(url);
      }, false);
      request.addEventListener('abort', event => {
        onError == null ? void 0 : onError(event);
        this.manager.itemError(url);
        this.manager.itemEnd(url);
      }, false);
      if (this.responseType) request.responseType = this.responseType;
      if (this.withCredentials) request.withCredentials = this.withCredentials;
      for (const header in this.requestHeader) {
        request.setRequestHeader(header, this.requestHeader[header]);
      }
      request.send(null);
      this.manager.itemStart(url);
    }).catch(onError);
    return request;
  };

I don't think this is sufficient for local storage files. Even after getting the asset local uri, your polyfill still attempts to use XMLRequest to get the file, which results in the error above.

I think a suitable change could be modifying the polyfill function to detect if the uri starts with file://, and if so, plug in the RNFS.readFile function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants