Search code examples
unity-game-engineunity3d-editorunity-webgl

EditorUtility.OpenFilePanel for Unity WebGL (Runtime)


I want the user to select a image from computer in Unity WebGL game but I am not able to get any library or code for this thing. I want exact functionality of EditorUtility.OpenFilePanel for UnityWebGL (Runtime).

string path = EditorUtility.OpenFilePanel("Overwrite with png","","png");

Is there any way to get this open dialog in Unity WebGL build? Or Is there any way I can do it in java script? Like I get image in java-script from user and pass it to my C# code.


Solution

  • This may sound like something simple, but actually it is quite complicated to do, and the reason is that WebGL build runs in browser, and is subject to many security restrictions, among others limiting its access to local file system. It is possible to do though, in a hacky way.

    The idea is to use HTML file input to open the file browse dialog. We can call it from Unity code using ExternalEval, see more here: http://docs.unity3d.com/Manual/UnityWebPlayerandbrowsercommunication.html http://docs.unity3d.com/ScriptReference/Application.ExternalEval.html

    Yet, it is not that easy. The problem is that all modern browsers allow to show files dialog only as a result to user click event, as a security restriction, and you can't do anything about it.

    Ok, so we can create a button, and open file dialog on click, this will work, right? WRONG. If we simply create unity button and handle click - this will not work, since Unity has its own event management, it is synchronized with frame rate, so the event will occur only when the actual javascript event is over. It is almost same problem like described here, http://docs.unity3d.com/Manual/webgl-cursorfullscreen.html except Unity has no good built in solution.

    So here is the hack: click is mouse down + mouse up, right? We add click listener to HTML document, then in unity we listen to mouse down on our button. When it is down, we know that next UP will be click, so we mark some flag in HTML document to remember it. Then, when we get click in document, we can look at this flag and conclude our button was clicked. Then we call javascript function that opens file dialog, and we send results back to Unity using SendMessage http://docs.unity3d.com/Manual/webgl-interactingwithbrowserscripting.html. Finally.

    But wait, there is more. The problem is that we can't simply get file path when running in browser. Our application is not allowed to get any info about user's computer, again, security restriction. The best we can do is get a blob url using URL.CreateObjectURL which will work on most browsers, http://caniuse.com/#search=createobjecturl

    We can use WWW class to retrieve data from it, just remember that this URL is accessible only from within your application scope.

    So with all these, the solution is very hacky, but possible. Here is an example code that allows user to select an image, and set it as material texture.

    using UnityEngine;
    using UnityEngine.UI;
    using UnityEngine.EventSystems;
    using System.Collections;
    
    public class OpenFileDialog : MonoBehaviour, IPointerDownHandler {
    
        public Renderer preview;
        public Text text;
    
        void Start() {
            Application.ExternalEval(
                @"
    document.addEventListener('click', function() {
    
        var fileuploader = document.getElementById('fileuploader');
        if (!fileuploader) {
            fileuploader = document.createElement('input');
            fileuploader.setAttribute('style','display:none;');
            fileuploader.setAttribute('type', 'file');
            fileuploader.setAttribute('id', 'fileuploader');
            fileuploader.setAttribute('class', 'focused');
            document.getElementsByTagName('body')[0].appendChild(fileuploader);
    
            fileuploader.onchange = function(e) {
            var files = e.target.files;
                for (var i = 0, f; f = files[i]; i++) {
                    window.alert(URL.createObjectURL(f));
                    SendMessage('" + gameObject.name +@"', 'FileDialogResult', URL.createObjectURL(f));
                }
            };
        }
        if (fileuploader.getAttribute('class') == 'focused') {
            fileuploader.setAttribute('class', '');
            fileuploader.click();
        }
    });
                ");
        }
    
        public void OnPointerDown (PointerEventData eventData)  {
            Application.ExternalEval(
                @"
    var fileuploader = document.getElementById('fileuploader');
    if (fileuploader) {
        fileuploader.setAttribute('class', 'focused');
    }
                ");
        }
    
        public void FileDialogResult(string fileUrl) {
            Debug.Log(fileUrl);
            text.text = fileUrl;
            StartCoroutine(PreviewCoroutine(fileUrl));
        }
    
        IEnumerator PreviewCoroutine(string url) {
            var www = new WWW(url);
            yield return www;
            preview.material.mainTexture = www.texture;
        }
    }
    

    If someone manages to finds a simpler way please share, but I really doubt it is possible. Hope this helps.