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

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

Open
cpetry opened this issue Apr 15, 2024 · 5 comments
Open

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

cpetry opened this issue Apr 15, 2024 · 5 comments
Assignees

Comments

@cpetry
Copy link

cpetry commented Apr 15, 2024

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
Copy link
Contributor

pfcDorn commented Jul 12, 2024

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

@Phlegmati
Copy link

Phlegmati commented Jul 15, 2024

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?

@cpetry
Copy link
Author

cpetry commented Sep 13, 2024

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
Copy link
Contributor

pfcDorn commented Sep 16, 2024

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
Copy link
Author

cpetry commented Sep 16, 2024

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

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

No branches or pull requests

3 participants