Browse Source

Emscripten shell (#2608)

* emscripten shell added, wip

* include dirs

* namespace

* more namespace fixes

* cmake tweak

* tweak

* emscripten shell added in different cmake file

* shell setup fixed

* application.html renamed to shell.html, engine tweaks

* shell.html location updated

* reworked canvas size detection and applying, highdpi disabled for web builds

* typo

* background tab loading fix

* web handlers moved to graphics subsystem

* removed a bit too much

* fix

* ready event resize

* svg fullscreen icon

* id used for fullscreen icon

* emscripten request pointer used instead

* pointer lock tweaks

* yet another tweak

* ui cursor fixed to take into account ui scale, emscripten mouse state reset when changing resolution

* code style updates

* ui scale calculated differently

* fix

* pointer position calculation properly fixed

* cursor position retrieval updated

* Fix code style. Simplify UI scale handling.

* cmake updates

Co-authored-by: Eugene Kozlov <[email protected]>
Arnis Lielturks 5 years ago
parent
commit
72e2342343

+ 7 - 4
CMake/Modules/UrhoCommon.cmake

@@ -668,8 +668,9 @@ else ()
         if (WEB)
             if (EMSCRIPTEN)
                 # Emscripten-specific setup
-                set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-warn-absolute-paths -Wno-unknown-warning-option")
-                set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-warn-absolute-paths -Wno-unknown-warning-option")
+                set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-warn-absolute-paths -Wno-unknown-warning-option --bind")
+                set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-warn-absolute-paths -Wno-unknown-warning-option --bind")
+
                 if (URHO3D_THREADING)
                     set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -s USE_PTHREADS=1")
                     set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -s USE_PTHREADS=1")
@@ -1106,13 +1107,15 @@ macro (add_html_shell)
             set (HTML_SHELL ${ARGN})
         else ()
             # Create Urho3D custom HTML shell that also embeds our own project logo
-            if (NOT EXISTS ${CMAKE_BINARY_DIR}/Source/shell.html)
+            if (NOT EXISTS ${CMAKE_SOURCE_DIR}/bin/shell.html)
                 file (READ ${EMSCRIPTEN_ROOT_PATH}/src/shell.html HTML_SHELL)
                 string (REPLACE "<!doctype html>" "<!-- This is a generated file. DO NOT EDIT!-->\n\n<!doctype html>" HTML_SHELL "${HTML_SHELL}")     # Stringify to preserve semicolons
                 string (REPLACE "<body>" "<body>\n<script>document.body.innerHTML=document.body.innerHTML.replace(/^#!.*\\n/, '');</script>\n<a href=\"https://urho3d.github.io\" title=\"Urho3D Homepage\"><img src=\"https://urho3d.github.io/assets/images/logo.png\" alt=\"link to https://urho3d.github.io\" height=\"80\" width=\"160\" /></a>\n" HTML_SHELL "${HTML_SHELL}")
                 file (WRITE ${CMAKE_BINARY_DIR}/Source/shell.html "${HTML_SHELL}")
+                set (HTML_SHELL ${CMAKE_BINARY_DIR}/Source/shell.html)
+            else ()
+                set (HTML_SHELL ${CMAKE_SOURCE_DIR}/bin/shell.html)
             endif ()
-            set (HTML_SHELL ${CMAKE_BINARY_DIR}/Source/shell.html)
         endif ()
         list (APPEND SOURCE_FILES ${HTML_SHELL})
         set_source_files_properties (${HTML_SHELL} PROPERTIES EMCC_OPTION shell-file)

+ 84 - 0
Source/Urho3D/Graphics/OpenGL/OGLGraphics.cpp

@@ -53,6 +53,12 @@
 #endif
 
 #ifdef __EMSCRIPTEN__
+#include "../Input/Input.h"
+#include "../UI/Cursor.h"
+#include "../UI/UI.h"
+#include <emscripten/emscripten.h>
+#include <emscripten/bind.h>
+
 // Emscripten provides even all GL extension functions via static linking. However there is
 // no GLES2-specific extension header at the moment to include instanced rendering declarations,
 // so declare them manually from GLES3 gl2ext.h. Emscripten will provide these when linking final output.
@@ -62,6 +68,71 @@ extern "C"
     GL_APICALL void GL_APIENTRY glDrawElementsInstancedANGLE (GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei primcount);
     GL_APICALL void GL_APIENTRY glVertexAttribDivisorANGLE (GLuint index, GLuint divisor);
 }
+
+// Helper functions to support emscripten canvas resolution change
+static const Urho3D::Context *appContext;
+
+static void JSCanvasSize(int width, int height, bool fullscreen, float scale)
+{
+    URHO3D_LOGINFOF("JSCanvasSize: width=%d height=%d fullscreen=%d ui scale=%f", width, height, fullscreen, scale);
+
+    using namespace Urho3D;
+
+    if (appContext)
+    {
+        bool uiCursorVisible = false;
+        bool systemCursorVisible = false;
+        MouseMode mouseMode{};
+
+        // Detect current system pointer state
+        Input* input = appContext->GetSubsystem<Input>();
+        if (input)
+        {
+            systemCursorVisible = input->IsMouseVisible();
+            mouseMode = input->GetMouseMode();
+        }
+
+        UI* ui = appContext->GetSubsystem<UI>();
+        if (ui)
+        {
+            ui->SetScale(scale);
+
+            // Detect current UI pointer state
+            Cursor* cursor = ui->GetCursor();
+            if (cursor)
+                uiCursorVisible = cursor->IsVisible();
+        }
+
+        // Apply new resolution
+        appContext->GetSubsystem<Graphics>()->SetMode(width, height);
+
+        // Reset the pointer state as it was before resolution change
+        if (input)
+        {
+            if (uiCursorVisible)
+                input->SetMouseVisible(false);
+            else
+                input->SetMouseVisible(systemCursorVisible);
+
+            input->SetMouseMode(mouseMode);
+        }
+
+        if (ui)
+        {
+            Cursor* cursor = ui->GetCursor();
+            if (cursor)
+            {
+                cursor->SetVisible(uiCursorVisible);
+                cursor->SetPosition(input->GetMousePosition());
+            }
+        }
+    }
+}
+
+using namespace emscripten;
+EMSCRIPTEN_BINDINGS(Module) {
+    function("JSCanvasSize", &JSCanvasSize);
+}
 #endif
 
 #ifdef _WIN32
@@ -242,6 +313,10 @@ Graphics::Graphics(Context* context) :
 
     // Register Graphics library object factories
     RegisterGraphicsLibrary(context_);
+
+#ifdef __EMSCRIPTEN__
+    appContext = context_;
+#endif
 }
 
 Graphics::~Graphics()
@@ -340,8 +415,11 @@ bool Graphics::SetScreenMode(int width, int height, const ScreenModeParams& para
             flags |= SDL_WINDOW_BORDERLESS;
         if (newParams.resizable_)
             flags |= SDL_WINDOW_RESIZABLE;
+
+#ifndef __EMSCRIPTEN__
         if (newParams.highDPI_)
             flags |= SDL_WINDOW_ALLOW_HIGHDPI;
+#endif
 
         SDL_SetHint(SDL_HINT_ORIENTATIONS, orientations_.CString());
 
@@ -2152,6 +2230,12 @@ void Graphics::OnWindowResized()
 
     URHO3D_LOGDEBUGF("Window was resized to %dx%d", width_, height_);
 
+#ifdef __EMSCRIPTEN__
+    EM_ASM({
+        Module.SetRendererSize($0, $1);
+    }, width_, height_);
+#endif
+
     using namespace ScreenMode;
 
     VariantMap& eventData = GetEventDataMap();

+ 20 - 7
Source/Urho3D/UI/UI.cpp

@@ -768,7 +768,10 @@ void UI::SetCustomSize(int width, int height)
 
 IntVector2 UI::GetCursorPosition() const
 {
-    return cursor_ ? cursor_->GetPosition() : GetSubsystem<Input>()->GetMousePosition();
+    if (cursor_)
+        return ConvertUIToSystem(cursor_->GetPosition());
+
+    return GetSubsystem<Input>()->GetMousePosition();
 }
 
 UIElement* UI::GetElementAt(const IntVector2& position, bool enabledOnly, IntVector2* elementScreenPosition)
@@ -846,6 +849,16 @@ UIElement* UI::GetElementAt(int x, int y, bool enabledOnly)
     return GetElementAt(IntVector2(x, y), enabledOnly);
 }
 
+IntVector2 UI::ConvertSystemToUI(const IntVector2& systemPos) const
+{
+    return VectorFloorToInt(static_cast<Vector2>(systemPos) / GetScale());
+}
+
+IntVector2 UI::ConvertUIToSystem(const IntVector2& uiPos) const
+{
+    return VectorFloorToInt(static_cast<Vector2>(uiPos) * GetScale());
+}
+
 UIElement* UI::GetFrontElement() const
 {
     const Vector<SharedPtr<UIElement> >& rootChildren = rootElement_->GetChildren();
@@ -1790,20 +1803,20 @@ void UI::HandleMouseMove(StringHash eventType, VariantMap& eventData)
     const IntVector2& rootSize = rootElement_->GetSize();
     const IntVector2& rootPos = rootElement_->GetPosition();
 
-    IntVector2 DeltaP = IntVector2(eventData[P_DX].GetInt(), eventData[P_DY].GetInt());
+    const IntVector2 mouseDeltaPos{ eventData[P_DX].GetInt(), eventData[P_DY].GetInt() };
+    const IntVector2 mousePos{ eventData[P_X].GetInt(), eventData[P_Y].GetInt() };
 
     if (cursor_)
     {
         if (!input->IsMouseVisible())
         {
             if (!input->IsMouseLocked())
-                cursor_->SetPosition(IntVector2(eventData[P_X].GetInt(), eventData[P_Y].GetInt()));
+                cursor_->SetPosition(ConvertSystemToUI(mousePos));
             else if (cursor_->IsVisible())
             {
                 // Relative mouse motion: move cursor only when visible
                 IntVector2 pos = cursor_->GetPosition();
-                pos.x_ += eventData[P_DX].GetInt();
-                pos.y_ += eventData[P_DY].GetInt();
+                pos += ConvertSystemToUI(mouseDeltaPos);
                 pos.x_ = Clamp(pos.x_, rootPos.x_, rootPos.x_ + rootSize.x_ - 1);
                 pos.y_ = Clamp(pos.y_, rootPos.y_, rootPos.y_ + rootSize.y_ - 1);
                 cursor_->SetPosition(pos);
@@ -1812,7 +1825,7 @@ void UI::HandleMouseMove(StringHash eventType, VariantMap& eventData)
         else
         {
             // Absolute mouse motion: move always
-            cursor_->SetPosition(IntVector2(eventData[P_X].GetInt(), eventData[P_Y].GetInt()));
+            cursor_->SetPosition(ConvertSystemToUI(mousePos));
         }
     }
 
@@ -1820,7 +1833,7 @@ void UI::HandleMouseMove(StringHash eventType, VariantMap& eventData)
     bool cursorVisible;
     GetCursorPositionAndVisible(cursorPos, cursorVisible);
 
-    ProcessMove(cursorPos, DeltaP, mouseButtons_, qualifiers_, cursor_, cursorVisible);
+    ProcessMove(cursorPos, mouseDeltaPos, mouseButtons_, qualifiers_, cursor_, cursorVisible);
 }
 
 void UI::HandleMouseWheel(StringHash eventType, VariantMap& eventData)

+ 9 - 5
Source/Urho3D/UI/UI.h

@@ -147,7 +147,11 @@ public:
     /// Return UI element at global screen coordinates. By default returns only input-enabled elements.
     UIElement* GetElementAt(int x, int y, bool enabledOnly = true);
     /// Get a child element at element's screen position relative to specified root element.
-    UIElement* GetElementAt(UIElement* root, const IntVector2& position, bool enabledOnly=true);
+    UIElement* GetElementAt(UIElement* root, const IntVector2& position, bool enabledOnly = true);
+    /// Convert system mouse position (or offset) to scaled UI position (or offset).
+    IntVector2 ConvertSystemToUI(const IntVector2& systemPos) const;
+    /// Convert scaled UI position (or offset) to system mouse position (or offset).
+    IntVector2 ConvertUIToSystem(const IntVector2& uiPos) const;
 
     /// Return focused element.
     UIElement* GetFocusElement() const { return focusElement_; }
@@ -167,10 +171,10 @@ public:
 
     /// Return UI element double click interval in seconds.
     float GetDoubleClickInterval() const { return doubleClickInterval_; }
-    
-    /// Get max screen distance in pixels for double clicks to register.
+
+    /// Return max screen distance in pixels for double clicks to register.
     float GetMaxDoubleClickDistance() const { return maxDoubleClickDist_;}
-    
+
     /// Return UI drag start event interval in seconds.
     float GetDragBeginInterval() const { return dragBeginInterval_; }
 
@@ -303,7 +307,7 @@ private:
 
     /// Send a UI double click event.
     void SendDoubleClickEvent(UIElement* beginElement, UIElement* endElement, const IntVector2& firstPos, const IntVector2& secondPos, MouseButton button, MouseButtonFlags buttons, QualifierFlags qualifiers);
-    
+
     /// Handle screen mode event.
     void HandleScreenMode(StringHash eventType, VariantMap& eventData);
     /// Handle mouse button down event.

+ 256 - 0
bin/shell.html

@@ -0,0 +1,256 @@
+<!doctype html>
+<html lang="en-us">
+<head>
+    <meta charset=utf-8>
+    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
+    <title>Urho3D</title>
+    <style>
+        body, html {
+            padding: 0;
+            margin: 0;
+            margin: 0;
+            overflow: hidden;
+        }
+
+        .emscripten {
+            padding-right: 0;
+            margin-left: auto;
+            margin-right: auto;
+            display: block;
+        }
+
+        div.emscripten {
+            text-align: center;
+        }
+
+        #fullscreen-button {
+            position: absolute;
+            width: 2em;
+            height: 2em;
+            left: 50%;
+            top: 4px;
+            -moz-transform: translateX(-50%);
+            -webkit-transform: translateX(-50%);
+            transform: translateX(-50%);
+            stroke: #999999;
+            stroke-width: 10px;
+        }
+
+        #fullscreen-button:hover {
+            fill: #999999;
+            cursor: pointer;
+        }
+
+        #canvas {
+            position: absolute;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: 100%;
+            background-color: #000;
+            border: 1px solid #000000;
+            border: none;
+            cursor: default !important;
+        }
+
+        .centered {
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            -moz-transform: translateX(-50%) translateY(-50%);
+            -webkit-transform: translateX(-50%) translateY(-50%);
+            transform: translateX(-50%) translateY(-50%);
+        }
+    </style>
+</head>
+<body>
+    <div class="centered">
+        <div class="emscripten" id="status">Downloading...</div>
+        <progress hidden id="progress" max=100 value=10></progress>
+    </div>
+    <canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1 width=100 height=100 style="display: none;"></canvas>
+    <div id="fullscreen-button" onclick="enterFullscreen()" style="display: none;">
+        <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+             viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+            <path d="M93.1,139.6l46.5-46.5L93.1,46.5L139.6,0H0v139.6l46.5-46.5L93.1,139.6z M93.1,372.4l-46.5,46.5L0,372.4V512h139.6
+                l-46.5-46.5l46.5-46.5L93.1,372.4z M372.4,139.6H139.6v232.7h232.7V139.6z M325.8,325.8H186.2V186.2h139.6V325.8z M372.4,0
+                l46.5,46.5l-46.5,46.5l46.5,46.5l46.5-46.5l46.5,46.5V0H372.4z M418.9,372.4l-46.5,46.5l46.5,46.5L372.4,512H512V372.4l-46.5,46.5
+                L418.9,372.4z"/>
+        </svg>
+    </div>
+    <script>
+        var canvasElement = document.getElementById('canvas');
+        var devicePixelRatio = window.devicePixelRatio || 1;
+        var canvasWidth = 0;
+        var canvasHeight = 0;
+
+        // Detect the visible width and height of the window
+        function calculateCanvasSize() {
+            canvasWidth = parseInt(window.getComputedStyle(canvasElement).getPropertyValue('width')) * devicePixelRatio;
+            canvasHeight = parseInt(window.getComputedStyle(canvasElement).getPropertyValue('height')) * devicePixelRatio;
+        }
+        calculateCanvasSize();
+
+        // Detect fullscreen change and resize canvas resolution accordingly
+        function viewportResizeHandler() {
+            if (document.hidden) {
+                return;
+            }
+
+            calculateCanvasSize();
+            if (Module['JSCanvasSize']) {
+                if (isFullScreen()) {
+                    Module.JSCanvasSize(screen.width * devicePixelRatio, screen.height * devicePixelRatio, true, devicePixelRatio);
+                } else {
+                    Module.JSCanvasSize(canvasWidth, canvasHeight, false, devicePixelRatio);
+                }
+            }
+        }
+
+        function visibilityChanged() {
+            if (document.hidden) {
+                return;
+            }
+
+            // Overwrite some emscripten functions that break the input
+            __registerFocusEventCallback = function() {
+                if (!JSEvents.focusEvent) JSEvents.focusEvent = _malloc(256);
+            };
+            __registerFullscreenChangeEventCallback = function() {
+                if (!JSEvents.fullscreenChangeEvent) JSEvents.fullscreenChangeEvent = _malloc(280);
+            };
+
+            setTimeout(() => {
+                viewportResizeHandler();
+            }, 100);
+        }
+
+        document.addEventListener('fullscreenchange', viewportResizeHandler, false);
+        document.addEventListener('mozfullscreenchange', viewportResizeHandler, false);
+        document.addEventListener('webkitfullscreenchange', viewportResizeHandler, false);
+        document.addEventListener('MSFullscreenChange', viewportResizeHandler, false);
+
+        document.addEventListener('visibilitychange', visibilityChanged, false);
+        document.addEventListener('msvisibilitychange', visibilityChanged, false);
+        document.addEventListener('webkitvisibilitychange', visibilityChanged, false);
+
+        // When window size has changed, resize the canvas accordingly
+        window.addEventListener('resize', function(evt) {
+            // resize event is called before the resizing has finished, we must wait a bit so the new calculations use the new viewport size
+            setTimeout(() => {
+                viewportResizeHandler(evt);
+            }, 1000);
+        });
+
+        // Enter the fullscreen mode
+        function enterFullscreen(show) {
+            if (show === undefined) show = !isFullScreen();
+            if (show) {
+                if (canvasElement.requestFullscreen) canvasElement.requestFullscreen();
+                else if (canvasElement.webkitRequestFullScreen) canvasElement.webkitRequestFullScreen();
+                else if (canvasElement.mozRequestFullScreen) canvasElement.mozRequestFullScreen();
+                else if (canvasElement.msRequestFullscreen) canvasElement.msRequestFullscreen();
+            } else {
+                if (document.exitFullscreen) document.exitFullscreen();
+                else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
+                else if (document.mozCancelFullScreen) document.mozCancelFullScreen();
+                else if (document.msExitFullscreen) document.msExitFullscreen();
+            }
+        }
+
+        function isFullScreen() {
+            return !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
+        }
+
+        // App is ready to launch, make canvas and fullscreen button visible
+        function ready() {
+            document.getElementById('canvas').style.display = 'block';
+            document.getElementById('fullscreen-button').style.display = 'block';
+
+            if (document.hidden) {
+                return;
+            }
+
+            // Overwrite some emscripten functions that break the input
+            __registerFocusEventCallback = function() {
+                if (!JSEvents.focusEvent) JSEvents.focusEvent = _malloc(256);
+            };
+            __registerFullscreenChangeEventCallback = function() {
+                if (!JSEvents.fullscreenChangeEvent) JSEvents.fullscreenChangeEvent = _malloc(280);
+            };
+
+            setTimeout(() => {
+                viewportResizeHandler();
+            }, 100);
+        }
+
+        var Module = {
+            preRun: [],
+            postRun: [],
+            canvas: canvasElement,
+            forcedAspectRatio: false,
+
+            print: function (text) {
+                console.log(text);
+            },
+
+            printErr: function(text) {
+                console.error(text);
+            },
+
+            // Urho3D called method which tells the canvas the current renderer resolution, based on E_SCREENMODE event values
+            SetRendererSize: function(width, height) {
+                console.log('Engine renderer size changed to', width, height);
+                calculateCanvasSize();
+
+                if (document.hidden) {
+                    return;
+                }
+
+                var aspectRatio = width / height;
+                canvasElement.width = width;
+                canvasElement.height = height;
+
+                // Compare calculated canvas resolution with the actual renderer resolution
+                if (canvasWidth === width && canvasHeight === height) {
+                    return;
+                }
+
+                // Renderer resolution is wrong, update it with the calculated values
+                console.log('Renderer and canvas resolution mismatch, updating renderer resolution width', this.canvas.width, 'to', width, 'and height', this.canvas.height, 'to', height);
+                Module.JSCanvasSize(canvasWidth, canvasHeight, false, devicePixelRatio);
+            },
+
+            // Retrieve the current status of the application
+            setStatus: function(text) {
+                if (text === 'Running...') {
+                    ready();
+                }
+                if (Module.setStatus.interval) clearInterval(Module.setStatus.interval);
+                var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
+                var statusElement = document.getElementById('status');
+                var progressElement = document.getElementById('progress');
+                if (m) {
+                    text = m[1];
+                    progressElement.value = parseInt(m[2])*100;
+                    progressElement.max = parseInt(m[4])*100;
+                    progressElement.hidden = false;
+                } else {
+                    progressElement.value = null;
+                    progressElement.max = null;
+                    progressElement.hidden = true;
+                }
+                statusElement.innerHTML = text;
+            },
+
+            totalDependencies: 0,
+            monitorRunDependencies: function(left) {
+                this.totalDependencies = Math.max(this.totalDependencies, left);
+                Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
+            },
+        };
+        Module.setStatus('Downloading...');
+    </script>
+    {{{ SCRIPT }}}
+</body>
+</html>