KhronosGroup / UnityGLTF

Runtime glTF 2.0 Loader for Unity3D
MIT License
1.83k stars 489 forks source link

How to load glb as byte[] or MemoryStream #732

Open cpetry opened 7 months ago

cpetry commented 7 months ago

Hello,

I'd like to load a byte[] or MemoryStream as a glb file without a file path from script (not using Inspector at all). Is this possible? Didn't find anything in examples or tests.

pfcDorn commented 4 months ago

hey, should be possible with public GLTFSceneImporter(GLTFRoot rootNode, Stream gltfStream, ImportOptions options), there you can set a stream

Phlegmati commented 3 months ago

SOLUTION

If anyone is still interested, this is how I did it for my webGL Application:

protected override void LoadFromBytes(byte[] data)
{
    // Dataloader with empty constructor to skip check for directory name
    var importOptions = new ImportOptions
    {
        DataLoader = new UnityWebRequestLoader("")
    };

    // Parse the stream
    GLTFParser.ParseJson(stream, out var gltfRoot);

    stream.Position = 0;
    var loader = new GLTFSceneImporter(gltfRoot, stream, importOptions)
    {
            // For webGL Builds: disable multithreading
        IsMultithreaded = false
    };

    // Load the scene by memorystream 
    loader.LoadScene(-1, true);
}

If anyone has a better approach, please let me know!

Old Comment History . . . **Original comment:** I do have the same "problem". I want to load a glb file in webGL. After I used a filebrowser, I do get a byte[] array, which I fed into a memorystream and then created a new SceneImporter with the stream and importOptions: ``` IEnumerator LoadGLB(byte[] data) { MemoryStream stream = new MemoryStream(data); var importOptions = new ImportOptions { AnimationMethod = AnimationMethod.MecanimHumanoid }; var sceneImporter = new GLTFSceneImporter( null, stream, importOptions ); yield return sceneImporter.LoadScene(-1, true, AfterImportFinished); } ``` But at initialization, in the constructor of GltfSceneImporter the "VerifyDataLoader" Method gets called, which checks for a directoryname - which I dont have if I am using a stream as input: ``` public GLTFSceneImporter(GLTFRoot rootNode, Stream gltfStream, ImportOptions options) : this(options) { _gltfRoot = rootNode; if (gltfStream != null) { _gltfStream = new GLBStream { Stream = gltfStream, StartPosition = gltfStream.Position }; } VerifyDataLoader(); } private void VerifyDataLoader() { if (_options.DataLoader == null) { if (_options.ExternalDataLoader == null) { _options.DataLoader = new UnityWebRequestLoader(URIHelper.GetDirectoryName(_gltfFileName)); _gltfFileName = URIHelper.GetFileFromUri(new Uri(_gltfFileName)); } else { _options.DataLoader = LegacyLoaderWrapper.Wrap(_options.ExternalDataLoader); } } } ``` If I am trying to use it, unity throws a NullReferenceException, because the "_gltfFileName" is empty. For now I dont know how to use it - any recommendations? . . . **EDIT:** _Okay I tried with the UnityWebRequestLoader but it only works in editor playmode, built to webGL it doesnt. This is my code so far, only working in Editor:_ ``` IEnumerator LoadGLB(string uri) { // uri contains host and blob of my browser chosen file // something like: http://127.0.0.1:5500/39550693-4ad4-4e87-8856-83ac8b84ed85 var importOptions = new ImportOptions { AnimationMethod = AnimationMethod.MecanimHumanoid }; var sceneImporter = new GLTFSceneImporter( uri, importOptions ); yield return sceneImporter.LoadScene(-1, true, CreatePuppetBehaviour); } ``` And this was my working code for uploading a file to my WebGL APP, using a different gltf plugin, which worked by loading from byte[]: IEnumerator LoadGLB(string uri) { using UnityWebRequest uwr = UnityWebRequest.Get(uri); yield return uwr.SendWebRequest(); if (uwr.error != null) Debug.Log(uwr.error); else { byte[] result = new byte[uwr.downloadHandler.data.Length]; System.Array.Copy(uwr.downloadHandler.data, 0, result, 0, uwr.downloadHandler.data.Length); importer.LoadGLBFromBytes(result); } } I feel like something in between is missing. Maybe it needs to copy the buffer? . . . **EDIT 2:** Okay I am pretty sure it is the "System.Threading.Tasks" include, that WebGL can't handle. So only Option B) is qualified, as I can handle the WebRequest download single-threaded. ~A) Use the built-in UnityWebRequestLoader, but need to understand what am I missing for now~ (not working in WebGL) **B) Use the Stream approach, but need to work around the filename check** @pfcDorn What would you suggest?

cpetry commented 1 month ago

I am trying again now like this with glb files and the "fake" UnityWebRequestLoader:

private async UniTask<GameObject> LoadWithUnityGLTF(byte[] bytes)
{
    using (var stream = new MemoryStream(bytes))
    {
        var glb = GLBBuilder.ConstructFromStream(stream);
        _unityGltf.DataLoader = new UnityGLTF.Loader.UnityWebRequestLoader("");
        var importer = new UnityGLTF.GLTFSceneImporter(glb.Root, stream, _unityGltf);
        await importer.LoadScene(-1, showSceneObj: true);
        return importer.LastLoadedScene;
    }
}

But I get lots of mesh errors like these and of course no valid model:

Failed setting triangles. Some indices are referencing out of bounds vertices. IndexCount: 204, VertexCount: 80 UnityEngine.Mesh:SetIndices (int[],UnityEngine.MeshTopology,int,bool,int)

I also tried creating my own MemoryLoader with the IDataLoader interface but this didn't work as well.

private class MemoryDataLoader : UnityGLTF.Loader.IDataLoader
{
    private readonly MemoryStream _memoryStream;

    public MemoryDataLoader(MemoryStream memoryStream)
    {
         _memoryStream = memoryStream;
    }

    public async Task<Stream> LoadStreamAsync(string relativeFilePath)
    {
        return _memoryStream;
    }
}

Any ideas?

pfcDorn commented 1 month ago

Hey, can you take a look on this branch: https://github.com/KhronosGroup/UnityGLTF/tree/fix/load-from-stream I made some changes, so you should only need these lines to load from a stream:

var stream = new FileStream(filePath, FileMode.Open);
var importOptions = new ImportOptions();
var importer = new GLTFSceneImporter(stream, importOptions);
await importer.LoadSceneAsync();
stream.Dispose();

Generally, you should only load GLBs which contains all textures and data. References to files would not work.
Let me know if it's working for you :)

Btw: You already has found a working solution with a custom DataLoader:

private class StreamDataLoader : UnityGLTF.Loader.IDataLoader
{
    private readonly Stream _stream;

    public StreamDataLoader(Stream stream)
    {
         _stream = stream;
    }

    public async Task<Stream> LoadStreamAsync(string relativeFilePath)
    {
        return _stream;
    }
}

  var stream = new FileStream(filePath, FileMode.Open);
  var importOptions = new ImportOptions();
  importOptions.DataLoader = new StreamDataLoader(stream);
  GLTFParser.ParseJson(stream, out var gltfRoot);
  var importer = new GLTFSceneImporter(gltfRoot, stream, importOptions);
  await importer.LoadSceneAsync();
  stream.Dispose();
cpetry commented 1 month ago

Works like a charm! Thanks a lot for this.

I didn't manage to get it to work with the custom DataLoader but with your first example on the separate branch. Please add documentation somewhere. Can be closed once merged