UnityGLTF icon indicating copy to clipboard operation
UnityGLTF copied to clipboard

How to load glb as byte[] or MemoryStream

Open cpetry opened this issue 1 year ago • 5 comments

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.

cpetry avatar Apr 15 '24 07:04 cpetry

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

pfcDorn avatar Jul 12 '24 07:07 pfcDorn

SOLUTION

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

  • parsing the stream for gltf root
  • disabled multithreading
  • loading scene synced
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?

Phlegmati avatar Jul 15 '24 08:07 Phlegmati

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?

cpetry avatar Sep 13 '24 13:09 cpetry

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();

pfcDorn avatar Sep 16 '24 06:09 pfcDorn

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

cpetry avatar Sep 16 '24 11:09 cpetry