From c1eb1fca1a9bd45f0714860e58b60b330ae3ec5c Mon Sep 17 00:00:00 2001 From: Moritz Roetner Date: Fri, 27 May 2022 17:36:50 +0200 Subject: [PATCH] POC --- .../Integrations/Wpf/View/MainWindow.xaml.cs | 8 +- .../Silk/Fusee.Examples.Simple.Silk.csproj | 16 + Examples/Complete/Simple/Silk/FuseeLogo.ico | Bin 0 -> 113246 bytes Examples/Complete/Simple/Silk/Main.cs | 79 + Fusee.sln | 57 +- src/Engine/Core/RenderCanvas.cs | 9 +- .../Imp/Graphics/Shared/BufferHandle.cs | 2 + src/Engine/Imp/Graphics/Shared/Font.cs | 2 + src/Engine/Imp/Graphics/Shared/MeshImp.cs | 3 +- .../Imp/Graphics/Shared/ShaderHandleImp.cs | 2 + src/Engine/Imp/Graphics/Shared/ShaderParam.cs | 2 + .../Imp/Graphics/Shared/TextureHandle.cs | 2 + .../Fusee.Engine.Imp.Graphics.Silk.csproj | 43 + src/Engine/Imp/Graphics/Silk/InputImp.cs | 1012 +++++++ src/Engine/Imp/Graphics/Silk/Keymapper.cs | 106 + .../Imp/Graphics/Silk/RenderCanvasImp.cs | 734 +++++ .../Silk/RenderCanvasImpSyncContext.cs | 42 + .../Imp/Graphics/Silk/RenderContextImp.cs | 2658 +++++++++++++++++ .../Imp/Graphics/Silk/TexturePixelInfo.cs | 14 + src/Engine/Imp/Graphics/Silk/WindowHandle.cs | 16 + .../Graphics/Silk/WindowsTouchDeviceImp.cs | 629 ++++ 21 files changed, 5422 insertions(+), 14 deletions(-) create mode 100644 Examples/Complete/Simple/Silk/Fusee.Examples.Simple.Silk.csproj create mode 100644 Examples/Complete/Simple/Silk/FuseeLogo.ico create mode 100644 Examples/Complete/Simple/Silk/Main.cs create mode 100644 src/Engine/Imp/Graphics/Silk/Fusee.Engine.Imp.Graphics.Silk.csproj create mode 100644 src/Engine/Imp/Graphics/Silk/InputImp.cs create mode 100644 src/Engine/Imp/Graphics/Silk/Keymapper.cs create mode 100644 src/Engine/Imp/Graphics/Silk/RenderCanvasImp.cs create mode 100644 src/Engine/Imp/Graphics/Silk/RenderCanvasImpSyncContext.cs create mode 100644 src/Engine/Imp/Graphics/Silk/RenderContextImp.cs create mode 100644 src/Engine/Imp/Graphics/Silk/TexturePixelInfo.cs create mode 100644 src/Engine/Imp/Graphics/Silk/WindowHandle.cs create mode 100644 src/Engine/Imp/Graphics/Silk/WindowsTouchDeviceImp.cs diff --git a/Examples/Complete/Integrations/Wpf/View/MainWindow.xaml.cs b/Examples/Complete/Integrations/Wpf/View/MainWindow.xaml.cs index ae3e94500..5df66952d 100644 --- a/Examples/Complete/Integrations/Wpf/View/MainWindow.xaml.cs +++ b/Examples/Complete/Integrations/Wpf/View/MainWindow.xaml.cs @@ -53,7 +53,7 @@ private void Position_PropertyChanged(object sender, System.ComponentModel.Prope private void OpenFusee() { - Task.Run(() => + Task.Run((System.Action)(() => { IO.IOImp = new Fusee.Base.Imp.Desktop.IOImp(); @@ -100,8 +100,8 @@ private void OpenFusee() // Inject Fusee.Engine InjectMe dependencies (hard coded) fuseeApp.CanvasImplementor = new Fusee.Engine.Imp.Graphics.Desktop.RenderCanvasImp(); fuseeApp.ContextImplementor = new Fusee.Engine.Imp.Graphics.Desktop.RenderContextImp(fuseeApp.CanvasImplementor); - Input.AddDriverImp(new Fusee.Engine.Imp.Graphics.Desktop.RenderCanvasInputDriverImp(fuseeApp.CanvasImplementor)); - Input.AddDriverImp(new Fusee.Engine.Imp.Graphics.Desktop.WindowsTouchInputDriverImp(fuseeApp.CanvasImplementor)); + Input.AddDriverImp((Engine.Common.IInputDriverImp)new Fusee.Engine.Imp.Graphics.Desktop.RenderCanvasInputDriverImp(fuseeApp.CanvasImplementor)); + Input.AddDriverImp((Engine.Common.IInputDriverImp)new Fusee.Engine.Imp.Graphics.Desktop.WindowsTouchInputDriverImp(fuseeApp.CanvasImplementor)); // app.InputImplementor = new Fusee.Engine.Imp.Graphics.Desktop.InputImp(app.CanvasImplementor); // app.InputDriverImplementor = new Fusee.Engine.Imp.Input.Desktop.InputDriverImp(); // app.VideoManagerImplementor = ImpFactory.CreateIVideoManagerImp(); @@ -110,7 +110,7 @@ private void OpenFusee() // Start the app fuseeApp.Run(); - }); + })); } private void FusToWpfEvents(object sender, Core.FusEvent e) diff --git a/Examples/Complete/Simple/Silk/Fusee.Examples.Simple.Silk.csproj b/Examples/Complete/Simple/Silk/Fusee.Examples.Simple.Silk.csproj new file mode 100644 index 000000000..a9fa613e0 --- /dev/null +++ b/Examples/Complete/Simple/Silk/Fusee.Examples.Simple.Silk.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + $(BaseOutputPath)\Examples\Simple\Desktop\ + + Exe + + + + + + + + + \ No newline at end of file diff --git a/Examples/Complete/Simple/Silk/FuseeLogo.ico b/Examples/Complete/Simple/Silk/FuseeLogo.ico new file mode 100644 index 0000000000000000000000000000000000000000..dbdc5bc33048f406734a624df530aec169067ee4 GIT binary patch literal 113246 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 + { + if (!Path.GetExtension(id).Contains("ttf", System.StringComparison.OrdinalIgnoreCase)) return null; + return await Task.FromResult(new Font { _fontImp = new FontImp((Stream)storage) }); + }, + Decoder = (string id, object storage) => + { + if (!Path.GetExtension(id).Contains("ttf", System.StringComparison.OrdinalIgnoreCase)) return null; + return new Font { _fontImp = new FontImp((Stream)storage) }; + }, + Checker = id => Path.GetExtension(id).Contains("ttf", System.StringComparison.OrdinalIgnoreCase) + }); + fap.RegisterTypeHandler( + new AssetHandler + { + ReturnedType = typeof(SceneContainer), + DecoderAsync = async (string id, object storage) => + { + if (!Path.GetExtension(id).Contains("fus", System.StringComparison.OrdinalIgnoreCase)) return null; + return await FusSceneConverter.ConvertFromAsync(ProtoBuf.Serializer.Deserialize((Stream)storage), id); + }, + Decoder = (string id, object storage) => + { + if (!Path.GetExtension(id).Contains("fus", System.StringComparison.OrdinalIgnoreCase)) return null; + return FusSceneConverter.ConvertFrom(ProtoBuf.Serializer.Deserialize((Stream)storage), id); + }, + Checker = id => Path.GetExtension(id).Contains("fus", System.StringComparison.OrdinalIgnoreCase) + }); + + AssetStorage.RegisterProvider(fap); + + var app = new Core.Simple(); + + // Inject Fusee.Engine InjectMe dependencies (hard coded) + var icon = AssetStorage.Get("FuseeIconTop32.png"); + app.CanvasImplementor = new Fusee.Engine.Imp.Graphics.SilkDesktop.RenderCanvasImp(icon); + app.ContextImplementor = new Fusee.Engine.Imp.Graphics.SilkDesktop.RenderContextImp(app.CanvasImplementor); + //Input.AddDriverImp(new Fusee.Engine.Imp.Graphics.SilkDesktop.RenderCanvasInputDriverImp(app.CanvasImplementor)); + //Input.AddDriverImp(new Fusee.Engine.Imp.Graphics.SilkDesktop.WindowsTouchInputDriverImp(app.CanvasImplementor)); + // app.InputImplementor = new Fusee.Engine.Imp.Graphics.Desktop.InputImp(app.CanvasImplementor); + // app.InputDriverImplementor = new Fusee.Engine.Imp.Input.Desktop.InputDriverImp(); + // app.VideoManagerImplementor = ImpFactory.CreateIVideoManagerImp(); + + app.CanvasImplementor.Init += (s, e) => + { + + app.InitApp(); + }; + + + + // Start the app + app.Run(); + } + } +} \ No newline at end of file diff --git a/Fusee.sln b/Fusee.sln index e68dba608..04b90b3c9 100644 --- a/Fusee.sln +++ b/Fusee.sln @@ -296,14 +296,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fusee.Examples.RenderContex EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fusee.Examples.PickingRayCast.Blazor", "Examples\Complete\PickingRayCast\Blazor\Fusee.Examples.PickingRayCast.Blazor.csproj", "{3AFEF2ED-6325-4C11-9B87-A32514FD5AE1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fusee.Engine.Imp.Graphics.Silk", "src\Engine\Imp\Graphics\Silk\Fusee.Engine.Imp.Graphics.Silk.csproj", "{29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fusee.Examples.Simple.Silk", "Examples\Complete\Simple\Silk\Fusee.Examples.Simple.Silk.csproj", "{DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}" +EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - src\Engine\Imp\Graphics\Shared\Fusee.Engine.Imp.Graphics.Shared.projitems*{253263c9-9c67-44a5-94d3-51c586bbdaec}*SharedItemsImports = 13 - src\Engine\Imp\Graphics\Shared\Fusee.Engine.Imp.Graphics.Shared.projitems*{b3ce4f39-fcc4-4388-8130-9d0b9d65d034}*SharedItemsImports = 5 - src\PointCloud\Las\Shared\Fusee.PointCloud.Las.Shared.projitems*{dcc7da71-3e2e-476c-8391-1f9651637503}*SharedItemsImports = 13 - src\PointCloud\Las\Shared\Fusee.PointCloud.Las.Shared.projitems*{f28634e7-52b8-4935-b19e-cb8a6844e6f1}*SharedItemsImports = 5 - src\Engine\Imp\Graphics\Shared\Fusee.Engine.Imp.Graphics.Shared.projitems*{f7ad2bb5-d2b0-4697-bddb-4cc26152a6b6}*SharedItemsImports = 5 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug-Android|Any CPU = Debug-Android|Any CPU @@ -1714,6 +1711,42 @@ Global {3AFEF2ED-6325-4C11-9B87-A32514FD5AE1}.Release-Blazor|Any CPU.Build.0 = Release|Any CPU {3AFEF2ED-6325-4C11-9B87-A32514FD5AE1}.Release-Desktop|Any CPU.ActiveCfg = Release|Any CPU {3AFEF2ED-6325-4C11-9B87-A32514FD5AE1}.Release-NuGet|Any CPU.ActiveCfg = Release|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Debug-Android|Any CPU.ActiveCfg = Debug|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Debug-Android|Any CPU.Build.0 = Debug|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Debug-Blazor|Any CPU.ActiveCfg = Debug|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Debug-Blazor|Any CPU.Build.0 = Debug|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Debug-Desktop|Any CPU.ActiveCfg = Debug|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Debug-Desktop|Any CPU.Build.0 = Debug|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Release|Any CPU.Build.0 = Release|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Release-Android|Any CPU.ActiveCfg = Release|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Release-Android|Any CPU.Build.0 = Release|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Release-Blazor|Any CPU.ActiveCfg = Release|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Release-Blazor|Any CPU.Build.0 = Release|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Release-Desktop|Any CPU.ActiveCfg = Release|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Release-Desktop|Any CPU.Build.0 = Release|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Release-NuGet|Any CPU.ActiveCfg = Release|Any CPU + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1}.Release-NuGet|Any CPU.Build.0 = Release|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Debug-Android|Any CPU.ActiveCfg = Debug|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Debug-Android|Any CPU.Build.0 = Debug|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Debug-Blazor|Any CPU.ActiveCfg = Debug|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Debug-Blazor|Any CPU.Build.0 = Debug|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Debug-Desktop|Any CPU.ActiveCfg = Debug|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Debug-Desktop|Any CPU.Build.0 = Debug|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Release|Any CPU.Build.0 = Release|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Release-Android|Any CPU.ActiveCfg = Release|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Release-Android|Any CPU.Build.0 = Release|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Release-Blazor|Any CPU.ActiveCfg = Release|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Release-Blazor|Any CPU.Build.0 = Release|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Release-Desktop|Any CPU.ActiveCfg = Release|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Release-Desktop|Any CPU.Build.0 = Release|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Release-NuGet|Any CPU.ActiveCfg = Release|Any CPU + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3}.Release-NuGet|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1845,8 +1878,18 @@ Global {FF0DEE54-C91E-480F-B103-593FB00E92FF} = {E6E73351-7DED-4B97-B254-A0E903D769C1} {ED0AC25F-615E-4AE6-A491-ADD41ABEB2D2} = {E6E73351-7DED-4B97-B254-A0E903D769C1} {3AFEF2ED-6325-4C11-9B87-A32514FD5AE1} = {425DDCA8-659F-44FB-832E-117C7DED152E} + {29B228C2-2168-46A0-BAB4-BDA0D7ABDDB1} = {6295AB06-EBE4-4887-A811-20737F6F8645} + {DEE03C44-A421-43FE-8267-0A9A5BC4E7C3} = {F799DB2B-D7BA-4B88-A16F-F8E2317137D3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CC1775C2-579F-4897-8770-592966D00E3D} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + src\Engine\Imp\Graphics\Shared\Fusee.Engine.Imp.Graphics.Shared.projitems*{253263c9-9c67-44a5-94d3-51c586bbdaec}*SharedItemsImports = 13 + src\Engine\Imp\Graphics\Shared\Fusee.Engine.Imp.Graphics.Shared.projitems*{29b228c2-2168-46a0-bab4-bda0d7abddb1}*SharedItemsImports = 5 + src\Engine\Imp\Graphics\Shared\Fusee.Engine.Imp.Graphics.Shared.projitems*{b3ce4f39-fcc4-4388-8130-9d0b9d65d034}*SharedItemsImports = 5 + src\PointCloud\Las\Shared\Fusee.PointCloud.Las.Shared.projitems*{dcc7da71-3e2e-476c-8391-1f9651637503}*SharedItemsImports = 13 + src\PointCloud\Las\Shared\Fusee.PointCloud.Las.Shared.projitems*{f28634e7-52b8-4935-b19e-cb8a6844e6f1}*SharedItemsImports = 5 + src\Engine\Imp\Graphics\Shared\Fusee.Engine.Imp.Graphics.Shared.projitems*{f7ad2bb5-d2b0-4697-bddb-4cc26152a6b6}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/src/Engine/Core/RenderCanvas.cs b/src/Engine/Core/RenderCanvas.cs index 2c971e4c8..e7f25d066 100644 --- a/src/Engine/Core/RenderCanvas.cs +++ b/src/Engine/Core/RenderCanvas.cs @@ -148,6 +148,11 @@ protected int GetWindowHeight() /// public EventHandler LoadingCompleted; + ///

+ /// This event is usually triggered when loading is completed (after init() method) + /// + public EventHandler InitComplete; + /// /// Called after can be used to await async tasks (e.g. loading methods) /// @@ -309,8 +314,8 @@ public void OpenLink(string link) /// necessary initialization, call the Init method and enter the application main loop. /// public void Run() - { - CanvasImplementor.Run(); + { + CanvasImplementor.Run(); } ///
diff --git a/src/Engine/Imp/Graphics/Shared/BufferHandle.cs b/src/Engine/Imp/Graphics/Shared/BufferHandle.cs index 9e54e63ed..501324329 100644 --- a/src/Engine/Imp/Graphics/Shared/BufferHandle.cs +++ b/src/Engine/Imp/Graphics/Shared/BufferHandle.cs @@ -4,6 +4,8 @@ namespace Fusee.Engine.Imp.Graphics.Desktop #elif PLATFORM_ANDROID namespace Fusee.Engine.Imp.Graphics.Android +#elif PLATFORM_SILK +namespace Fusee.Engine.Imp.Graphics.SilkDesktop #endif { /// diff --git a/src/Engine/Imp/Graphics/Shared/Font.cs b/src/Engine/Imp/Graphics/Shared/Font.cs index bd211bc7f..1bdd8c2a5 100644 --- a/src/Engine/Imp/Graphics/Shared/Font.cs +++ b/src/Engine/Imp/Graphics/Shared/Font.cs @@ -5,6 +5,8 @@ namespace Fusee.Engine.Imp.Graphics.Desktop #elif PLATFORM_ANDROID namespace Fusee.Engine.Imp.Graphics.Android +#elif PLATFORM_SILK +namespace Fusee.Engine.Imp.Graphics.SilkDesktop #endif { internal class Font : IFont diff --git a/src/Engine/Imp/Graphics/Shared/MeshImp.cs b/src/Engine/Imp/Graphics/Shared/MeshImp.cs index e92ad0f56..fedcaae16 100644 --- a/src/Engine/Imp/Graphics/Shared/MeshImp.cs +++ b/src/Engine/Imp/Graphics/Shared/MeshImp.cs @@ -4,8 +4,9 @@ namespace Fusee.Engine.Imp.Graphics.Desktop #elif PLATFORM_ANDROID - namespace Fusee.Engine.Imp.Graphics.Android +#elif PLATFORM_SILK +namespace Fusee.Engine.Imp.Graphics.SilkDesktop #endif { /// diff --git a/src/Engine/Imp/Graphics/Shared/ShaderHandleImp.cs b/src/Engine/Imp/Graphics/Shared/ShaderHandleImp.cs index da2a64344..789e17215 100644 --- a/src/Engine/Imp/Graphics/Shared/ShaderHandleImp.cs +++ b/src/Engine/Imp/Graphics/Shared/ShaderHandleImp.cs @@ -4,6 +4,8 @@ namespace Fusee.Engine.Imp.Graphics.Desktop #elif PLATFORM_ANDROID namespace Fusee.Engine.Imp.Graphics.Android +#elif PLATFORM_SILK +namespace Fusee.Engine.Imp.Graphics.SilkDesktop #endif { /// diff --git a/src/Engine/Imp/Graphics/Shared/ShaderParam.cs b/src/Engine/Imp/Graphics/Shared/ShaderParam.cs index a51ceb731..1b6815bf5 100644 --- a/src/Engine/Imp/Graphics/Shared/ShaderParam.cs +++ b/src/Engine/Imp/Graphics/Shared/ShaderParam.cs @@ -4,6 +4,8 @@ namespace Fusee.Engine.Imp.Graphics.Desktop #elif PLATFORM_ANDROID namespace Fusee.Engine.Imp.Graphics.Android +#elif PLATFORM_SILK +namespace Fusee.Engine.Imp.Graphics.SilkDesktop #endif { /// diff --git a/src/Engine/Imp/Graphics/Shared/TextureHandle.cs b/src/Engine/Imp/Graphics/Shared/TextureHandle.cs index 0c29976f0..5b4005948 100644 --- a/src/Engine/Imp/Graphics/Shared/TextureHandle.cs +++ b/src/Engine/Imp/Graphics/Shared/TextureHandle.cs @@ -4,6 +4,8 @@ namespace Fusee.Engine.Imp.Graphics.Desktop #elif PLATFORM_ANDROID namespace Fusee.Engine.Imp.Graphics.Android +#elif PLATFORM_SILK +namespace Fusee.Engine.Imp.Graphics.SilkDesktop #endif { /// diff --git a/src/Engine/Imp/Graphics/Silk/Fusee.Engine.Imp.Graphics.Silk.csproj b/src/Engine/Imp/Graphics/Silk/Fusee.Engine.Imp.Graphics.Silk.csproj new file mode 100644 index 000000000..43dba3e7d --- /dev/null +++ b/src/Engine/Imp/Graphics/Silk/Fusee.Engine.Imp.Graphics.Silk.csproj @@ -0,0 +1,43 @@ + + + + net6.0 + $(DefineConstants);PLATFORM_SILK + $(OutputPath)\$(RootNamespace).xml + + true + Fusee Engine Imp Graphics Silk + + true + + + + + analyzers + + + analyzers + + + analyzers + + + analyzers + + + analyzers + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Engine/Imp/Graphics/Silk/InputImp.cs b/src/Engine/Imp/Graphics/Silk/InputImp.cs new file mode 100644 index 000000000..56c3df68f --- /dev/null +++ b/src/Engine/Imp/Graphics/Silk/InputImp.cs @@ -0,0 +1,1012 @@ +using Fusee.Engine.Common; +using Silk.NET.Input; +using Silk.NET.Maths; +using Silk.NET.Windowing; +using System; +using System.Collections.Generic; +using System.Linq; +using MouseButton = Silk.NET.Input.MouseButton; + +namespace Fusee.Engine.Imp.Graphics.SilkDesktop +{ + /// + /// Input driver implementation for keyboard and mouse input on Desktop and Android. + /// + public class RenderCanvasInputDriverImp : IInputDriverImp + { + /// + /// Constructor. Use this in platform specific application projects. + /// + /// The render canvas to provide mouse and keyboard input for. + public RenderCanvasInputDriverImp(IRenderCanvasImp renderCanvas) + { + if (renderCanvas == null) + throw new ArgumentNullException(nameof(renderCanvas)); + + if (!(renderCanvas is RenderCanvasImp)) + throw new ArgumentException("renderCanvas must be of type RenderCanvasImp", nameof(renderCanvas)); + + _gameWindow = ((RenderCanvasImp)renderCanvas)._gameWindow.window; + if (_gameWindow == null) + throw new ArgumentNullException(nameof(_gameWindow)); + + _keyboard = new KeyboardDeviceImp(_gameWindow); + _mouse = new MouseDeviceImp(_gameWindow); + //_gamePad0 = new GamePadDeviceImp(_gameWindow, 0); + //_gamePad1 = new GamePadDeviceImp(_gameWindow, 1); + //_gamePad2 = new GamePadDeviceImp(_gameWindow, 2); + //_gamePad3 = new GamePadDeviceImp(_gameWindow, 3); + } + + private readonly IWindow _gameWindow; + private readonly KeyboardDeviceImp _keyboard; + private readonly MouseDeviceImp _mouse; + //private readonly GamePadDeviceImp _gamePad0; + //private readonly GamePadDeviceImp _gamePad1; + //private readonly GamePadDeviceImp _gamePad2; + //private readonly GamePadDeviceImp _gamePad3; + + + /// + /// Devices supported by this driver: One mouse, one keyboard and up to four gamepads. + /// + public IEnumerable Devices + { + get + { + yield return _mouse; + yield return _keyboard; + //yield return _gamePad0; + //yield return _gamePad1; + //yield return _gamePad2; + //yield return _gamePad3; + + } + } + + /// + /// Returns a human readable description of this driver. + /// + public string DriverDesc + { + get + { + const string pf = "Desktop"; + return "OpenTK GameWindow Mouse and Keyboard input driver for " + pf; + } + } + + /// + /// Returns a (hopefully) unique ID for this driver. Uniqueness is granted by using the + /// full class name (including namespace). + /// + public string DriverId + { + get { return GetType().FullName; } + } + +#pragma warning disable 0067 + /// + /// Not supported on this driver. Mouse and keyboard are considered to be connected all the time. + /// You can register handlers but they will never get called. + /// + public event EventHandler DeviceDisconnected; + + /// + /// Not supported on this driver. Mouse and keyboard are considered to be connected all the time. + /// You can register handlers but they will never get called. + /// + public event EventHandler NewDeviceConnected; +#pragma warning restore 0067 + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + /// + /// Part of the Dispose pattern. + /// + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + disposedValue = true; + } + } + + // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + // ~RenderCanvasInputDriverImp() { + // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + // Dispose(false); + // } + + // This code added to correctly implement the disposable pattern. + /// + /// Part of the dispose pattern. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // TODO: uncomment the following line if the finalizer is overridden above. + // GC.SuppressFinalize(this); + } + #endregion + } + + + /// + /// Implements a gamepad control option only works for XBox Gamepads + /// + /// /// + /// The current implementation does not fire the and + /// events. This driver will always report one connected GamePad no matter how many physical devices are connected + /// to the machine. If no physical GamePad is present all of its axes and buttons will return 0 or false. + /// + // public class GamePadDeviceImp : IInputDeviceImp + // { + // private readonly IWindow _gameWindow; + // private readonly int DeviceID; + // private ButtonImpDescription _btnADesc, _btnXDesc, _btnYDesc, _btnBDesc, _btnStartDesc, _btnSelectDesc, _dpadUpDesc, _dpadDownDesc, _dpadLeftDesc, _dpadRightDesc, _btnLeftDesc, _btnRightDesc, _btnL3Desc, _btnR3Desc; + + // internal GamePadDeviceImp(IWindow window, int deviceID = 0) + // { + // _gameWindow = window; + // DeviceID = deviceID; + + // _btnADesc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP A", + // Id = 0 + // }, + // PollButton = true + // }; + // _btnXDesc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP X", + // Id = 1 + // }, + // PollButton = true + // }; + // _btnYDesc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP Y", + // Id = 2 + // }, + // PollButton = true + // }; + // _btnBDesc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP B", + // Id = 3 + // }, + // PollButton = true + // }; + // _btnStartDesc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP Start", + // Id = 4 + // }, + // PollButton = true + // }; + // _btnSelectDesc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP Back", + // Id = 5 + // }, + // PollButton = true + // }; + // _btnLeftDesc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP left button", + // Id = 6 + // }, + // PollButton = true + // }; + // _btnRightDesc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP right button", + // Id = 7 + // }, + // PollButton = true + // }; + // _btnL3Desc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP L3 button", + // Id = 8 + // }, + // PollButton = true + // }; + // _btnR3Desc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP R3 button", + // Id = 9 + // }, + // PollButton = true + // }; + // _dpadUpDesc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP Dpad up", + // Id = 10 + // }, + // PollButton = true + // }; + // _dpadDownDesc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP Dpad Down", + // Id = 11 + // }, + // PollButton = true + // }; + // _dpadLeftDesc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP Dpad Left", + // Id = 12 + // }, + // PollButton = true + // }; + // _dpadRightDesc = new ButtonImpDescription + // { + // ButtonDesc = new ButtonDescription + // { + // Name = "GP Dpad Right", + // Id = 13 + // }, + // PollButton = true + // }; + // } + + // /// + // /// Returns Name of Device. + // /// + // public string Id + // { + + // get + // { + // try + // { + // return GLFW.GetGamepadName(DeviceID); + // } + // catch + // { + // return "No gamepad connected"; + // } + // } + // } + // /// + // /// Description. + // /// + // public string Desc + // { + // get + // { + // return "Standard XBox-Gamepad implementation."; + // } + // } + + // /// + // /// Returns Type of input device. + // /// + // public DeviceCategory Category + // { + // get + // { + // return DeviceCategory.GameController; + // } + // } + // /// + // /// Returns Number of Axes. + // /// + // public int AxesCount => 6; + + // /// + // /// Returns description information for all axes. + // /// + // public IEnumerable AxisImpDesc + // { + // get + // { + // yield return new AxisImpDescription + // { + // AxisDesc = new AxisDescription + // { + // Name = "Left Stick X", + // Id = 0, + // Direction = AxisDirection.X, + // Nature = AxisNature.Position, + // Bounded = AxisBoundedType.Constant, + // MinValueOrAxis = -1, + // MaxValueOrAxis = 1 + // }, + // PollAxis = true + // }; + // yield return new AxisImpDescription + // { + // AxisDesc = new AxisDescription + // { + // Name = "Left Stick Y", + // Id = 1, + // Direction = AxisDirection.Y, + // Nature = AxisNature.Position, + // Bounded = AxisBoundedType.Constant, + // MinValueOrAxis = -1, + // MaxValueOrAxis = 1 + // }, + // PollAxis = true + // }; + // yield return new AxisImpDescription + // { + // AxisDesc = new AxisDescription + // { + // Name = "Right Stick X", + // Id = 2, + // Direction = AxisDirection.X, + // Nature = AxisNature.Position, + // Bounded = AxisBoundedType.Constant, + // MinValueOrAxis = -1, + // MaxValueOrAxis = 1 + // }, + // PollAxis = true + // }; + // yield return new AxisImpDescription + // { + // AxisDesc = new AxisDescription + // { + // Name = "Right Stick Y", + // Id = 3, + // Direction = AxisDirection.Y, + // Nature = AxisNature.Position, + // Bounded = AxisBoundedType.Constant, + // MinValueOrAxis = -1, + // MaxValueOrAxis = 1 + // }, + // PollAxis = true + // }; + // yield return new AxisImpDescription + // { + // AxisDesc = new AxisDescription + // { + // Name = "Left Trigger", + // Id = 4, + // Direction = AxisDirection.Y, + // Nature = AxisNature.Position, + // Bounded = AxisBoundedType.Constant, + // MinValueOrAxis = 0, + // MaxValueOrAxis = 1 + // }, + // PollAxis = true + // }; + // yield return new AxisImpDescription + // { + // AxisDesc = new AxisDescription + // { + // Name = "Right Trigger", + // Id = 5, + // Direction = AxisDirection.Y, + // Nature = AxisNature.Position, + // Bounded = AxisBoundedType.Constant, + // MinValueOrAxis = 0, + // MaxValueOrAxis = 1 + // }, + // PollAxis = true + // }; + // } + // } + // /// + // /// Returns Number of Buttons. + // /// + // public int ButtonCount + // { + // get + // { + // return 14; + // } + + // } + + // /// + // /// A gamepad exposes 14 buttons. + // /// + // public IEnumerable ButtonImpDesc + // { + // get + // { + // yield return _btnADesc; + // yield return _btnXDesc; + // yield return _btnYDesc; + // yield return _btnBDesc; + // yield return _btnStartDesc; + // yield return _btnSelectDesc; + // yield return _btnRightDesc; + // yield return _btnLeftDesc; + // yield return _btnR3Desc; + // yield return _btnL3Desc; + // yield return _dpadUpDesc; + // yield return _dpadDownDesc; + // yield return _dpadLeftDesc; + // yield return _dpadRightDesc; + + // } + // } + // /// + // /// All axis are Poll based see GetAxis + // /// + //#pragma warning disable 0067 + // public event EventHandler AxisValueChanged; + + // /// + // /// no Buttons implemented + // /// + // public event EventHandler ButtonValueChanged; + //#pragma warning restore 0067 + + // /// + // /// Retrieves values for the X, Y and Trigger axes. No other axes are supported by this device. + // /// + // /// The axis to retrieve information for. + // /// The value at the given axis. + // public float GetAxis(int iAxisId) + // { + // JoystickState state = _gameWindow.JoystickStates[DeviceID]; + // if (state != null) + // { + // try + // { + // return state.GetAxis(iAxisId); + // } + // catch + // { + // return 0; + // } + // } + // return 0; + // } + + // /// + // /// Returns a Boolean Value for Controller Input. + // /// + // public bool GetButton(int iButtonId) + // { + // JoystickState state = _gameWindow.JoystickStates[DeviceID]; + // if (state != null) + // { + // try + // { + // return state.IsButtonDown(iButtonId); + // } + // catch + // { + // return false; + // } + // } + // return false; + // } + // } + + /// + /// Keyboard input device implementation for Desktop an Android platforms. + /// + public class KeyboardDeviceImp : IInputDeviceImp + { + private readonly IWindow _gameWindow; + private readonly Keymapper _keymapper; + + /// + /// Should be called by the driver only. + /// + /// + internal KeyboardDeviceImp(IWindow gameWindow) + { + _gameWindow = gameWindow; + _keymapper = new Keymapper(); + var input = _gameWindow.CreateInput(); + for (int i = 0; i < input.Keyboards.Count; i++) + { + input.Keyboards[i].KeyDown += OnGameWinKeyDown; + input.Keyboards[i].KeyUp += OnGameWinKeyUp; + } + } + + /// + /// Returns the number of Axes (==0, keyboard does not support any axes). + /// + public int AxesCount + { + get + { + return 0; + } + } + + /// + /// Empty enumeration for keyboard, since is 0. + /// + public IEnumerable AxisImpDesc + { + get + { + yield break; + } + } + + /// + /// Returns the number of enum values of + /// + public int ButtonCount + { + get + { + return Enum.GetNames(typeof(KeyCodes)).Length; + } + } + + /// + /// Returns a description for each keyboard button. + /// + public IEnumerable ButtonImpDesc + { + get + { + return from k in _keymapper orderby k.Value.Id select new ButtonImpDescription { ButtonDesc = k.Value, PollButton = false }; + } + } + + /// + /// This is a keyboard device, so this property returns . + /// + public DeviceCategory Category + { + get + { + return DeviceCategory.Keyboard; + } + } + + /// + /// Human readable description of this device (to be used in dialogs). + /// + public string Desc + { + get + { + return "Standard Keyboard implementation."; + } + } + + /// + /// Returns a (hopefully) unique ID for this driver. Uniqueness is granted by using the + /// full class name (including namespace). + /// + public string Id + { + get + { + return GetType().FullName; + } + } + + +#pragma warning disable 0067 + /// + /// No axes exist on this device, so listeners registered to this event will never get called. + /// + public event EventHandler AxisValueChanged; + + /// + /// All buttons exhibited by this device are event-driven buttons, so this is the point to hook to in order + /// to get information from this device. + /// + public event EventHandler ButtonValueChanged; +#pragma warning restore 0067 + + /// + /// Called when keyboard button is pressed down. + /// + /// The instance containing the event data. + protected void OnGameWinKeyDown(IKeyboard keyboard, Key key, int value) + { + if (ButtonValueChanged != null && _keymapper.TryGetValue(key, out ButtonDescription btnDesc)) + { + ButtonValueChanged(this, new ButtonValueChangedArgs + { + Pressed = true, + Button = btnDesc + }); + } + } + + /// + /// Called when keyboard button is released. + /// + /// The instance containing the event data. + protected void OnGameWinKeyUp(IKeyboard keyboard, Key key, int arg3) + { + if (ButtonValueChanged != null && _keymapper.TryGetValue(key, out ButtonDescription btnDesc)) + { + ButtonValueChanged(this, new ButtonValueChangedArgs + { + Pressed = false, + Button = btnDesc + }); + } + } + + /// + /// This device does not support any axes at all. Always throws. + /// + /// No matter what you specify here, you'll evoke an exception. + /// No return, always throws. + public float GetAxis(int iAxisId) + { + throw new InvalidOperationException($"Unsupported axis {iAxisId}. This device does not support any axis at all."); + } + + /// + /// This device does not support to-be-polled-buttons. All keyboard buttons are event-driven. Listen to the + /// event to receive keyboard notifications from this device. + /// + /// No matter what you specify here, you'll evoke an exception. + /// No return, always throws. + public bool GetButton(int iButtonId) + { + throw new InvalidOperationException($"Button {iButtonId} does not exist or is no pollable. Listen to the ButtonValueChanged event to receive keyboard notifications from this device."); + } + } + + /// + /// Mouse input device implementation for Desktop an Android platforms. + /// + public class MouseDeviceImp : IInputDeviceImp + { + private readonly IWindow _gameWindow; + private ButtonImpDescription _btnLeftDesc, _btnRightDesc, _btnMiddleDesc; + + /// + /// Creates a new mouse input device instance using an existing . + /// + /// The game window providing mouse input. + public MouseDeviceImp(IWindow gameWindow) + { + _gameWindow = gameWindow; + var input = _gameWindow.CreateInput(); + + for (var i = 0; i < input.Mice.Count; i++) + { + input.Mice[i].MouseMove += OnMouseMove; + input.Mice[i].MouseDown += OnGameWinMouseDown; + input.Mice[i].MouseUp += OnGameWinMouseUp; + } + + + _btnLeftDesc = new ButtonImpDescription + { + ButtonDesc = new ButtonDescription + { + Name = "Left", + Id = (int)MouseButtons.Left + }, + PollButton = false + }; + _btnMiddleDesc = new ButtonImpDescription + { + ButtonDesc = new ButtonDescription + { + Name = "Middle", + Id = (int)MouseButtons.Middle + }, + PollButton = false + }; + _btnRightDesc = new ButtonImpDescription + { + ButtonDesc = new ButtonDescription + { + Name = "Right", + Id = (int)MouseButtons.Right + }, + PollButton = false + }; + } + + /// + /// Number of axes. Here seven: "X", "Y" and "Wheel" as well as MinX, MaxX, MinY and MaxY + /// + public int AxesCount => 7; + + /// + /// Returns description information for all axes. + /// + public IEnumerable AxisImpDesc + { + get + { + yield return new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = "X", + Id = (int)MouseAxes.X, + Direction = AxisDirection.X, + Nature = AxisNature.Position, + Bounded = AxisBoundedType.OtherAxis, + MinValueOrAxis = (int)MouseAxes.MinX, + MaxValueOrAxis = (int)MouseAxes.MaxX + }, + PollAxis = false + }; + yield return new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = "Y", + Id = (int)MouseAxes.Y, + Direction = AxisDirection.Y, + Nature = AxisNature.Position, + Bounded = AxisBoundedType.OtherAxis, + MinValueOrAxis = (int)MouseAxes.MinY, + MaxValueOrAxis = (int)MouseAxes.MaxY + }, + PollAxis = false + }; + yield return new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = "Wheel", + Id = (int)MouseAxes.Wheel, + Direction = AxisDirection.Z, + Nature = AxisNature.Position, + Bounded = AxisBoundedType.Unbound, + MinValueOrAxis = float.NaN, + MaxValueOrAxis = float.NaN + }, + PollAxis = true + }; + yield return new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = "MinX", + Id = (int)MouseAxes.MinX, + Direction = AxisDirection.X, + Nature = AxisNature.Position, + Bounded = AxisBoundedType.Unbound, + MinValueOrAxis = float.NaN, + MaxValueOrAxis = float.NaN + }, + PollAxis = true + }; + yield return new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = "MaxX", + Id = (int)MouseAxes.MaxX, + Direction = AxisDirection.X, + Nature = AxisNature.Position, + Bounded = AxisBoundedType.Unbound, + MinValueOrAxis = float.NaN, + MaxValueOrAxis = float.NaN + }, + PollAxis = true + }; + yield return new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = "MinY", + Id = (int)MouseAxes.MinY, + Direction = AxisDirection.Y, + Nature = AxisNature.Position, + Bounded = AxisBoundedType.Unbound, + MinValueOrAxis = float.NaN, + MaxValueOrAxis = float.NaN + }, + PollAxis = true + }; + yield return new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = "MaxY", + Id = (int)MouseAxes.MaxY, + Direction = AxisDirection.Y, + Nature = AxisNature.Position, + Bounded = AxisBoundedType.Unbound, + MinValueOrAxis = float.NaN, + MaxValueOrAxis = float.NaN + }, + PollAxis = true + }; + } + } + + /// + /// Number of buttons exposed by this device. Here three: Left, Middle and Right mouse buttons. + /// + public int ButtonCount => 3; + + /// + /// A mouse exposes three buttons: left, middle and right. + /// + public IEnumerable ButtonImpDesc + { + get + { + yield return _btnLeftDesc; + yield return _btnMiddleDesc; + yield return _btnRightDesc; + } + } + + /// + /// Returns , just because it's a mouse. + /// + public DeviceCategory Category => DeviceCategory.Mouse; + + /// + /// Short description string for this device to be used in dialogs. + /// + public string Desc => "Standard Mouse implementation."; + + /// + /// Returns a (hopefully) unique ID for this driver. Uniqueness is granted by using the + /// full class name (including namespace). + /// + public string Id => GetType().FullName; + + /// + /// Mouse movement is event-based. Listen to this event to get information about mouse movement. + /// + public event EventHandler AxisValueChanged; + + /// + /// All three mouse buttons are event-based. Listen to this event to get information about mouse button state changes. + /// + public event EventHandler ButtonValueChanged; + + /// + /// Retrieves values for the X, Y and Wheel axes. No other axes are supported by this device. + /// + /// The axis to retrieve information for. + /// The value at the given axis. + public float GetAxis(int iAxisId) + { + var input = _gameWindow.CreateInput(); + + + return iAxisId switch + { + (int)MouseAxes.Wheel => input.Mice[0].ScrollWheels[0].Y, // TODO: refactor + (int)MouseAxes.MinX => 0, + (int)MouseAxes.MaxX => _gameWindow.Size.X, + (int)MouseAxes.MinY => 0, + (int)MouseAxes.MaxY => _gameWindow.Size.Y, + _ => throw new InvalidOperationException($"Unknown axis {iAxisId}. Cannot get value for unknown axis."), + }; + } + + /// + /// Called when the game window's mouse is moved. + /// + /// The instance containing the event data. + protected void OnMouseMove(IMouse mouse, System.Numerics.Vector2 pos) + { + if (AxisValueChanged != null) + { + AxisValueChanged(this, new AxisValueChangedArgs { Axis = AxisImpDesc.First(x => x.AxisDesc.Id == (int)MouseAxes.X).AxisDesc, Value = pos.X }); + AxisValueChanged(this, new AxisValueChangedArgs { Axis = AxisImpDesc.First(y => y.AxisDesc.Id == (int)MouseAxes.Y).AxisDesc, Value = pos.Y }); + } + } + + /// + /// This device does not support to-be-polled-buttons. All mouse buttons are event-driven. Listen to the + /// event to revive keyboard notifications from this device. + /// + /// No matter what you specify here, you'll evoke an exception. + /// No return, always throws. + public bool GetButton(int iButtonId) + { + throw new InvalidOperationException( + $"Unsupported axis {iButtonId}. This device does not support any to-be polled axes at all."); + } + + /// + /// Called when the game window's mouse is pressed down. + /// + /// The instance containing the event data. + protected void OnGameWinMouseDown(IMouse mouse, MouseButton btn) + { + if (ButtonValueChanged != null) + { + ButtonDescription btnDesc; + switch (btn) + { + case MouseButton.Left: + btnDesc = _btnLeftDesc.ButtonDesc; + break; + case MouseButton.Middle: + btnDesc = _btnMiddleDesc.ButtonDesc; + break; + case MouseButton.Right: + btnDesc = _btnRightDesc.ButtonDesc; + break; + default: + return; + } + + ButtonValueChanged(this, new ButtonValueChangedArgs + { + Pressed = true, + Button = btnDesc + }); + } + } + + /// + /// Called when the game window's mouse is released. + /// + /// The instance containing the event data. + protected void OnGameWinMouseUp(IMouse mouse, MouseButton btn) + { + if (ButtonValueChanged != null) + { + ButtonDescription btnDesc; + switch (btn) + { + case MouseButton.Left: + btnDesc = _btnLeftDesc.ButtonDesc; + break; + case MouseButton.Middle: + btnDesc = _btnMiddleDesc.ButtonDesc; + break; + case MouseButton.Right: + btnDesc = _btnRightDesc.ButtonDesc; + break; + default: + return; + } + + ButtonValueChanged(this, new ButtonValueChangedArgs + { + Pressed = false, + Button = btnDesc + }); + } + } + } +} \ No newline at end of file diff --git a/src/Engine/Imp/Graphics/Silk/Keymapper.cs b/src/Engine/Imp/Graphics/Silk/Keymapper.cs new file mode 100644 index 000000000..18bfe2d9c --- /dev/null +++ b/src/Engine/Imp/Graphics/Silk/Keymapper.cs @@ -0,0 +1,106 @@ +using Fusee.Engine.Common; +using Silk.NET.Input; +using System.Collections.Generic; + +namespace Fusee.Engine.Imp.Graphics.SilkDesktop +{ + internal class Keymapper : Dictionary + { + #region Constructors + /// + /// Initializes the map between KeyCodes and OpenTK.Key + /// + internal Keymapper() + { + this.Add(Key.Escape, new ButtonDescription { Name = KeyCodes.Escape.ToString(), Id = (int)KeyCodes.Escape }); + + // Function keys + for (int i = 0; i < 24; i++) + { + this.Add(Key.F1 + i, new ButtonDescription { Name = $"F{i}", Id = (int)KeyCodes.F1 + i }); + } + + // Number keys (0-9) + for (int i = 0; i <= 9; i++) + { + this.Add(Key.D0 + i, new ButtonDescription { Name = $"D{i}", Id = (int)0x30 + i }); + } + + // Letters (A-Z) + for (int i = 0; i < 26; i++) + { + this.Add(Key.A + i, new ButtonDescription { Name = ((KeyCodes)(0x41 + i)).ToString(), Id = (int)(0x41 + i) }); + } + + this.Add(Key.Tab, new ButtonDescription { Name = KeyCodes.Tab.ToString(), Id = (int)KeyCodes.Tab }); + this.Add(Key.CapsLock, new ButtonDescription { Name = KeyCodes.Capital.ToString(), Id = (int)KeyCodes.Capital }); + this.Add(Key.ControlLeft, new ButtonDescription { Name = KeyCodes.LControl.ToString(), Id = (int)KeyCodes.LControl }); + this.Add(Key.ShiftLeft, new ButtonDescription { Name = KeyCodes.LShift.ToString(), Id = (int)KeyCodes.LShift }); + this.Add(Key.SuperLeft, new ButtonDescription { Name = KeyCodes.LWin.ToString(), Id = (int)KeyCodes.LWin }); + this.Add(Key.AltLeft, new ButtonDescription { Name = KeyCodes.LMenu.ToString(), Id = (int)KeyCodes.LMenu }); + this.Add(Key.Space, new ButtonDescription { Name = KeyCodes.Space.ToString(), Id = (int)KeyCodes.Space }); + this.Add(Key.AltRight, new ButtonDescription { Name = KeyCodes.RMenu.ToString(), Id = (int)KeyCodes.RMenu }); + this.Add(Key.SuperRight, new ButtonDescription { Name = KeyCodes.RWin.ToString(), Id = (int)KeyCodes.RWin }); + this.Add(Key.Menu, new ButtonDescription { Name = KeyCodes.Apps.ToString(), Id = (int)KeyCodes.Apps }); + this.Add(Key.ControlRight, new ButtonDescription { Name = KeyCodes.RControl.ToString(), Id = (int)KeyCodes.RControl }); + this.Add(Key.ShiftRight, new ButtonDescription { Name = KeyCodes.RShift.ToString(), Id = (int)KeyCodes.RShift }); + this.Add(Key.Enter, new ButtonDescription { Name = KeyCodes.Return.ToString(), Id = (int)KeyCodes.Return }); + this.Add(Key.Backspace, new ButtonDescription { Name = KeyCodes.Back.ToString(), Id = (int)KeyCodes.Back }); + + this.Add(Key.Semicolon, new ButtonDescription { Name = KeyCodes.Oem1.ToString(), Id = (int)KeyCodes.Oem1 }); + this.Add(Key.Slash, new ButtonDescription { Name = KeyCodes.Oem2.ToString(), Id = (int)KeyCodes.Oem2 }); + //this.Add(Key.Tilde, new ButtonDescription { Name = KeyCodes.Oem3.ToString(), Id = (int)KeyCodes.Oem3 }); + this.Add(Key.LeftBracket, new ButtonDescription { Name = KeyCodes.Oem4.ToString(), Id = (int)KeyCodes.Oem4 }); + this.Add(Key.BackSlash, new ButtonDescription { Name = KeyCodes.Oem5.ToString(), Id = (int)KeyCodes.Oem5 }); + this.Add(Key.RightBracket, new ButtonDescription { Name = KeyCodes.Oem6.ToString(), Id = (int)KeyCodes.Oem6 }); + //this.Add(Key.Quote, new ButtonDescription { Name = KeyCodes.Oem7.ToString(), Id = (int)KeyCodes.Oem7 }); + //this.Add(Key.Plus, new ButtonDescription { Name = KeyCodes.OemPlus.ToString(), Id = (int)KeyCodes.OemPlus }); + this.Add(Key.Comma, new ButtonDescription { Name = KeyCodes.OemComma.ToString(), Id = (int)KeyCodes.OemComma }); + this.Add(Key.Minus, new ButtonDescription { Name = KeyCodes.OemMinus.ToString(), Id = (int)KeyCodes.OemMinus }); + this.Add(Key.Period, new ButtonDescription { Name = KeyCodes.OemPeriod.ToString(), Id = (int)KeyCodes.OemPeriod }); + + this.Add(Key.Home, new ButtonDescription { Name = KeyCodes.Home.ToString(), Id = (int)KeyCodes.Home }); + this.Add(Key.End, new ButtonDescription { Name = KeyCodes.End.ToString(), Id = (int)KeyCodes.End }); + this.Add(Key.Delete, new ButtonDescription { Name = KeyCodes.Delete.ToString(), Id = (int)KeyCodes.Delete }); + this.Add(Key.PageUp, new ButtonDescription { Name = KeyCodes.Prior.ToString(), Id = (int)KeyCodes.Prior }); + this.Add(Key.PageDown, new ButtonDescription { Name = KeyCodes.Next.ToString(), Id = (int)KeyCodes.Next }); + this.Add(Key.PrintScreen, new ButtonDescription { Name = KeyCodes.Print.ToString(), Id = (int)KeyCodes.Print }); + this.Add(Key.Pause, new ButtonDescription { Name = KeyCodes.Pause.ToString(), Id = (int)KeyCodes.Pause }); + this.Add(Key.NumLock, new ButtonDescription { Name = KeyCodes.NumLock.ToString(), Id = (int)KeyCodes.NumLock }); + + this.Add(Key.ScrollLock, new ButtonDescription { Name = KeyCodes.Scroll.ToString(), Id = (int)KeyCodes.Scroll }); + // Do we need to do something here?? this.Add(Key.PrintScreen, new ButtonDescription {Name = KeyCodes.Snapshot.ToString(), Id = (int)KeyCodes.Snapshot}); + //this.Add(Key.Clear, new ButtonDescription { Name = KeyCodes.Clear.ToString(), Id = (int)KeyCodes.Clear }); + this.Add(Key.Insert, new ButtonDescription { Name = KeyCodes.Insert.ToString(), Id = (int)KeyCodes.Insert }); + + //this.Add(Key.Sleep, new ButtonDescription { Name = KeyCodes.Sleep.ToString(), Id = (int)KeyCodes.Sleep }); + + // KeyPad + for (int i = 0; i <= 9; i++) + { + this.Add(Key.Keypad0 + i, new ButtonDescription { Name = $"Numpad{i}", Id = (int)KeyCodes.NumPad0 + i }); + } + + this.Add(Key.KeypadDecimal, new ButtonDescription { Name = KeyCodes.Decimal.ToString(), Id = (int)KeyCodes.Decimal }); + this.Add(Key.KeypadAdd, new ButtonDescription { Name = KeyCodes.Add.ToString(), Id = (int)KeyCodes.Add }); + this.Add(Key.KeypadSubtract, new ButtonDescription { Name = KeyCodes.Subtract.ToString(), Id = (int)KeyCodes.Subtract }); + this.Add(Key.KeypadDivide, new ButtonDescription { Name = KeyCodes.Divide.ToString(), Id = (int)KeyCodes.Divide }); + this.Add(Key.KeypadMultiply, new ButtonDescription { Name = KeyCodes.Multiply.ToString(), Id = (int)KeyCodes.Multiply }); + + // Navigation + this.Add(Key.Up, new ButtonDescription { Name = KeyCodes.Up.ToString(), Id = (int)KeyCodes.Up }); + this.Add(Key.Down, new ButtonDescription { Name = KeyCodes.Down.ToString(), Id = (int)KeyCodes.Down }); + this.Add(Key.Left, new ButtonDescription { Name = KeyCodes.Left.ToString(), Id = (int)KeyCodes.Left }); + this.Add(Key.Right, new ButtonDescription { Name = KeyCodes.Right.ToString(), Id = (int)KeyCodes.Right }); + /* + catch (ArgumentException e) + { + //Debug.Print("Exception while creating keymap: '{0}'.", e.ToString()); + System.Windows.Forms.MessageBox.Show( + String.Format("Exception while creating keymap: '{0}'.", e.ToString())); + } + */ + } + #endregion + } +} \ No newline at end of file diff --git a/src/Engine/Imp/Graphics/Silk/RenderCanvasImp.cs b/src/Engine/Imp/Graphics/Silk/RenderCanvasImp.cs new file mode 100644 index 000000000..5e6c5dcff --- /dev/null +++ b/src/Engine/Imp/Graphics/Silk/RenderCanvasImp.cs @@ -0,0 +1,734 @@ +using Fusee.Base.Core; +using Fusee.Engine.Common; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Silk.NET.Windowing; +using Silk.NET.Input; +using Silk.NET.OpenGL; +using Silk.NET.Maths; + +namespace Fusee.Engine.Imp.Graphics.SilkDesktop +{ + /// + /// This is a default render canvas implementation creating its own rendering window. + /// + public class RenderCanvasImp : RenderCanvasImpBase, IRenderCanvasImp, IDisposable + { + #region Fields + + //Some tryptichon related variables. + private bool _videoWallMode = false; + private int _videoWallMonitorsHor; + private int _videoWallMonitorsVert; + private bool _windowBorderHidden = false; + + /// + /// Window handle for the window the engine renders to. + /// + public IWindowHandle WindowHandle { get; } + + /// + /// Implementation Tasks: Gets and sets the width(pixel units) of the Canvas. + /// + /// + /// The width. + /// + public int Width + { + get => BaseWidth; + set + { + _gameWindow.window.Size = new Vector2D(value, _gameWindow.window.Size.Y); + BaseWidth = value; + ResizeWindow(); + } + } + + /// + /// Gets and sets the height in pixel units. + /// + /// + /// The height. + /// + public int Height + { + get => BaseHeight; + set + { + _gameWindow.window.Size = new Vector2D(_gameWindow.window.Size.X, value); + BaseHeight = value; + ResizeWindow(); + } + } + + /// + /// Gets and sets the caption(title of the window). + /// + /// + /// The caption. + /// + public string Caption + { + get => (_gameWindow == null) ? "" : _gameWindow.window.Title; + set { if (_gameWindow != null) _gameWindow.window.Title = value; } + } + + /// + /// Gets the delta time. + /// The delta time is the time that was required to render the last frame in milliseconds. + /// This value can be used to determine the frames per second of the application. + /// + /// + /// The delta time in milliseconds. + /// + public float DeltaTime + { + get + { + if (_gameWindow != null) + return _gameWindow.DeltaTime; + return 0.01f; + } + } + + /// + /// Gets the delta time. + /// The delta time is the time that was required to update the last frame in milliseconds. + /// + /// + /// The delta time in milliseconds. + /// + public float DeltaTimeUpdate + { + get + { + if (_gameWindow != null) + return _gameWindow.DeltaTimeUpdate; + return 0.01f; + } + } + + /// + /// Gets and sets a value indicating whether [vertical synchronize]. + /// This option is used to reduce "Glitches" during rendering. + /// + /// + /// true if [vertical synchronize]; otherwise, false. + /// + public bool VerticalSync + { + get => (_gameWindow != null) && _gameWindow.window.VSync == true; + set { if (_gameWindow != null) _gameWindow.window.VSync = value; } + } + + /// + /// Gets and sets a value indicating whether [enable blending]. + /// Blending is used to render transparent objects. + /// + /// + /// true if [enable blending]; otherwise, false. + /// + public bool EnableBlending + { + get => _gameWindow.Blending; + set => _gameWindow.Blending = value; + } + + /// + /// Gets and sets a value indicating whether [fullscreen] is enabled. + /// + /// + /// true if [fullscreen]; otherwise, false. + /// + public bool Fullscreen + { + get => (_gameWindow.window.WindowState == WindowState.Fullscreen); + set => _gameWindow.window.WindowState = (value) ? WindowState.Fullscreen : WindowState.Normal; + } + + /// + /// Gets a value indicating whether [focused]. + /// This property is used to identify if this application is the active window of the user. + /// + /// + /// true if [focused]; otherwise, false. + /// + public bool Focused => _gameWindow.window.IsVisible; + + // Some tryptichon related Fields. + + /// + /// Activates (true) or deactivates (false) the video wall feature. + /// + public bool VideoWallMode + { + get => _videoWallMode; + set => _videoWallMode = value; + } + + /// + /// This represents the number of the monitors in a vertical column. + /// + public int TryptMonitorSetupVertical + { + get => _videoWallMonitorsVert; + set => _videoWallMonitorsVert = value; + } + + /// + /// This represents the number of the monitors in a horizontal row. + /// + public int TryptMonitorSetupHorizontal + { + get => _videoWallMonitorsHor; + set => _videoWallMonitorsHor = value; + } + + internal RenderCanvasGameWindow _gameWindow; + + #endregion + + #region Constructors + /// + /// Initializes a new instance of the class. + /// + public RenderCanvasImp() : this(null, false) + { + + } + + /// + /// Initializes a new instance of the class. + /// + /// If true OpenTk will call run() in a new Thread. The default value is false. + /// The window icon to use + public RenderCanvasImp(ImageData icon = null, bool isMultithreaded = false) + { + //TODO: Select correct monitor + //MonitorInfo mon = Monitors.GetMonitors()[0]; + + int width = 1280; + int height = 720; + + //if (mon != null) + //{ + // width = System.Math.Min(mon.HorizontalResolution - 100, width); + // height = System.Math.Min(mon.VerticalResolution - 100, height); + //} + + try + { + _gameWindow = new RenderCanvasGameWindow(this, width, height, false, isMultithreaded); + } + catch + { + _gameWindow = new RenderCanvasGameWindow(this, width, height, false, isMultithreaded); + } + + WindowHandle = new WindowHandle() + { + Handle = _gameWindow.window.Handle + }; + + + //if (_gameWindow.IsMultiThreaded) + // _gameWindow.Context.MakeNoneCurrent(); + + // convert icon to OpenTKImage + if (icon != null) + { + // convert Bgra to Rgba for OpenTK.WindowIcon + + var res = new Span(new Rgba32[width * height]); + var pxData = SixLabors.ImageSharp.Image.LoadPixelData(icon.PixelData, icon.Width, icon.Height); + pxData.Mutate(x => x.AutoOrient()); + pxData.Mutate(x => x.RotateFlip(RotateMode.None, FlipMode.Vertical)); + + pxData.CopyPixelDataTo(res); + + var resBytes = MemoryMarshal.AsBytes(res.ToArray()); + //_gameWindow.window.Icon = new WindowIcon(new Image[] { new Image(icon.Width, icon.Height, resBytes.ToArray()) }); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The width of the render window. + /// The height of the render window. + /// If true OpenTk will call run() in a new Thread. The default value is false. + /// The window created by this constructor is not visible. Should only be used for internal testing. + public RenderCanvasImp(int width, int height, bool isMultithreaded = false) + { + try + { + _gameWindow = new RenderCanvasGameWindow(this, width, height, true, isMultithreaded); + } + catch + { + _gameWindow = new RenderCanvasGameWindow(this, width, height, false, isMultithreaded); + } + + WindowHandle = new WindowHandle() + { + Handle = _gameWindow.window.Handle + }; + + //_gameWindow.IsVisible = false; + //if (_gameWindow.IsMultiThreaded) + // _gameWindow.Context.MakeNoneCurrent(); + } + + /// + /// Implementation of the Dispose pattern. Disposes of the OpenTK game window. + /// + public void Dispose() + { + _gameWindow.Dispose(); + } + + #endregion + + #region Members + + private void ResizeWindow() + { + if (!_videoWallMode) + { + //_gameWindow.window.WindowBorder = _windowBorderHidden ? WindowBorder.Hidden : OpenTK.Windowing.Common.WindowBorder.Resizable; + //_gameWindow.window.Bounds = new OpenTK.Mathematics.Box2i(BaseLeft, BaseTop, BaseWidth, BaseHeight); + } + else + { + //TODO: Select correct monitor + //MonitorInfo mon = Monitors.GetMonitors()[0]; + + //var oneScreenWidth = mon.HorizontalResolution; + //var oneScreenHeight = mon.VerticalResolution; + + //var width = oneScreenWidth * _videoWallMonitorsHor; + //var height = oneScreenHeight * _videoWallMonitorsVert; + + //_gameWindow.Bounds = new OpenTK.Mathematics.Box2i(0, 0, width, height); + + //if (_windowBorderHidden) + // _gameWindow.WindowBorder = OpenTK.Windowing.Common.WindowBorder.Hidden; + } + } + + /// + /// Changes the window of the application to video wall mode. + /// + /// Number of monitors on horizontal axis. + /// Number of monitors on vertical axis. + /// Start the window in activated state- + /// Start the window with a hidden windows border. + public void VideoWall(int monitorsHor = 1, int monitorsVert = 1, bool activate = true, bool borderHidden = false) + { + VideoWallMode = activate; + _videoWallMonitorsHor = monitorsHor > 0 ? monitorsHor : 1; + _videoWallMonitorsVert = monitorsVert > 0 ? monitorsVert : 1; + _windowBorderHidden = borderHidden; + + ResizeWindow(); + } + + /// + /// Sets the size of the output window for desktop development. + /// + /// The width of the window. + /// The height of the window. + /// The x position of the window. + /// The y position of the window. + /// Show the window border or not. + public void SetWindowSize(int width, int height, int posx = -1, int posy = -1, bool borderHidden = false) + { + //MonitorInfo mon = Monitors.GetMonitors()[0]; + + BaseWidth = width; + BaseHeight = height; + + //BaseLeft = (posx == -1) ? mon.HorizontalResolution / 2 - width / 2 : posx; + //BaseTop = (posy == -1) ? mon.VerticalResolution / 2 - height / 2 : posy; + + _windowBorderHidden = borderHidden; + + // Disable video wall mode for this because it would not make sense. + _videoWallMode = false; + + ResizeWindow(); + } + + /// + /// Closes the GameWindow with a call to OpenTk. + /// + public void CloseGameWindow() + { + if (_gameWindow != null) + { + //_gameWindow.Dispose(); + //_gameWindow.ProcessEvents(); + _gameWindow.Dispose(); + } + } + + /// + /// Presents this application instance. Call this function after rendering to show the final image. + /// After Present is called the render buffers get flushed. + /// + public void Present() + { + if (!_gameWindow.window.IsClosing) + _gameWindow.window.SwapBuffers(); + } + + /// + /// Set the cursor (the mouse pointer image) to one of the predefined types + /// + /// The type of the cursor to set. + public void SetCursor(Common.CursorType cursorType) + { + // Currently not supported by OpenTK... Too bad. + } + + /// + /// Opens the given URL in the user's standard web browser. The link MUST start with "http://". + /// + /// The URL to open + public void OpenLink(string link) + { + if (link.StartsWith("http://")) + { + //UseShellExecute needs to be set to true in .net 3.0. See:https://github.com/dotnet/corefx/issues/33714 + ProcessStartInfo psi = new() + { + FileName = link, + UseShellExecute = true + }; + Process.Start(psi); + } + } + + /// + /// Implementation Tasks: Runs this application instance. This function should not be called more than once as its only for initialization purposes. + /// + public void Run() + { + _gameWindow.window.Run(); + + if (_gameWindow.window != null) + { + _gameWindow.window.FramesPerSecond = 0; + _gameWindow.window.UpdatesPerSecond = 60; + _gameWindow.window.Center(); + } + } + + /// + /// Creates a bitmap image from the current frame of the application. + /// + /// The width of the window, and therefore image to render. + /// The height of the window, and therefore image to render. + /// + public SixLabors.ImageSharp.Image ShootCurrentFrame(int width, int height) + { + DoInit(); + DoRender(); + DoResize(width, height); + + var mem = new byte[width * height * 4]; + _gameWindow.GL.PixelStore(PixelStoreParameter.PackRowLength, 1); + _gameWindow.GL.ReadPixels(0, 0, (uint)Width, (uint)Height, GLEnum.Bgra, GLEnum.UnsignedByte, new Span(mem)); + + var img = SixLabors.ImageSharp.Image.LoadPixelData(mem, Width, Height); + + img.Mutate(x => x.AutoOrient()); + img.Mutate(x => x.RotateFlip(RotateMode.None, FlipMode.Vertical)); + + return img; + } + + #endregion + } + + /// + /// OpenTK implementation of RenderCanvas for the window output. + /// + public class RenderCanvasImpBase + { + #region Fields + + /// + /// The Width + /// + protected internal int BaseWidth; + + /// + /// The Height + /// + protected internal int BaseHeight; + + /// + /// The Top Position + /// + protected internal int BaseTop; + + /// + /// The Left Position + /// + protected internal int BaseLeft; + + #endregion + + #region Events + /// + /// Occurs when [initialize]. + /// + public event EventHandler Init; + /// + /// Occurs when [unload]. + /// + public event EventHandler UnLoad; + /// + /// Occurs when [update]. + /// + public event EventHandler Update; + /// + /// Occurs when [render]. + /// + public event EventHandler Render; + /// + /// Occurs when [resize]. + /// + public event EventHandler Resize; + + #endregion + + #region Internal Members + + /// + /// Does the initialize of this instance. + /// + protected internal void DoInit() + { + Init?.Invoke(this, new InitEventArgs()); + } + + /// + /// Does the unload of this instance. + /// + protected internal void DoUnLoad() + { + UnLoad?.Invoke(this, new InitEventArgs()); + } + + /// + /// Does the update of this instance. + /// + protected internal void DoUpdate() + { + Update?.Invoke(this, new RenderEventArgs()); + } + + /// + /// Does the render of this instance. + /// + protected internal void DoRender() + { + Render?.Invoke(this, new RenderEventArgs()); + } + + /// + /// Does the resize on this instance. + /// + protected internal void DoResize(int width, int height) + { + Resize?.Invoke(this, new ResizeEventArgs(width, height)); + } + + #endregion + } + + public class RenderCanvasGameWindow : IDisposable + { + #region Fields + + private readonly RenderCanvasImp _renderCanvasImp; + internal GL GL; + internal IWindow window; + private bool disposedValue; + + public Action Ready; + + /// + /// Gets the delta time. + /// The delta time is the time that was required to render the last frame in milliseconds. + /// This value can be used to determine the frames per second of the application. + /// + /// + /// The delta time in milliseconds. + /// + public float DeltaTime { get; private set; } + + /// + /// Gets the delta time. + /// The delta time is the time that was required to update the last frame in milliseconds. + /// + /// + /// The delta time in milliseconds. + /// + public float DeltaTimeUpdate { get; private set; } + + /// + /// Gets and sets a value indicating whether [blending]. + /// Blending is used to render transparent objects. + /// + /// + /// true if [blending]; otherwise, false. + /// + public bool Blending + { + get => GL.IsEnabled(EnableCap.Blend); + set + { + if (value) + { + GL.Enable(EnableCap.Blend); + GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + } + else + { + GL.Disable(EnableCap.Blend); + } + } + } + + #endregion + + #region Constructors + /// + /// Initializes a new instance of the class. + /// + /// The render canvas implementation. + /// The width. + /// The height. + /// if set to true [anti aliasing] is on. + /// If true OpenTk will call run() in a new Thread. The default value is false. + internal RenderCanvasGameWindow(RenderCanvasImp renderCanvasImp, int width, int height, bool antiAliasing, bool isMultithreaded = false) + { + _renderCanvasImp = renderCanvasImp; + _renderCanvasImp.BaseWidth = width; + _renderCanvasImp.BaseHeight = height; + + window = Window.Create(WindowOptions.Default); + + window.Load += OnLoad; + + window.FramebufferResize += OnResize; + window.Update += OnUpdateFrame; + window.Render += OnRenderFrame; + window.Closing += OnUnload; + } + + #endregion + + #region Overrides + + protected void OnLoad() + { + GL = window.CreateOpenGL(); + + // Check for necessary capabilities + //string version = GL.GetString(StringName.Version); + // + //int major = version[0]; + //// int minor = (int)version[2]; + // + //if (major < 2) + //{ + // throw new InvalidOperationException("You need at least OpenGL 2.0 to run this example. GLSL not supported."); + //} + + GL.ClearColor(25, 25, 112, byte.MaxValue); + + GL.Enable(EnableCap.DepthTest); + GL.Enable(EnableCap.CullFace); + + // Use VSync! + //VSync = OpenTK.Windowing.Common.VSyncMode.On; + + _renderCanvasImp.DoInit(); + } + + protected void OnUnload() + { + _renderCanvasImp.DoUnLoad(); + _renderCanvasImp.Dispose(); + } + + protected void OnResize(Vector2D v) + { + + if (_renderCanvasImp != null) + { + _renderCanvasImp.BaseWidth = v.X; + _renderCanvasImp.BaseHeight = v.Y; + _renderCanvasImp.DoResize(v.X, v.Y); + } + } + + protected void OnUpdateFrame(double updateTime) + { + + DeltaTimeUpdate = (float)updateTime; + + //if (KeyboardState.IsKeyPressed(OpenTK.Windowing.GraphicsLibraryFramework.Keys.F11)) + // WindowState = (WindowState != OpenTK.Windowing.Common.WindowState.Fullscreen) ? OpenTK.Windowing.Common.WindowState.Fullscreen : OpenTK.Windowing.Common.WindowState.Normal; + + _renderCanvasImp?.DoUpdate(); + } + + protected void OnRenderFrame(double delta) + { + + DeltaTime = (float)delta; + + _renderCanvasImp?.DoRender(); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + window.Dispose(); + GL.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~RenderCanvasGameWindow() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Engine/Imp/Graphics/Silk/RenderCanvasImpSyncContext.cs b/src/Engine/Imp/Graphics/Silk/RenderCanvasImpSyncContext.cs new file mode 100644 index 000000000..ff237a3a6 --- /dev/null +++ b/src/Engine/Imp/Graphics/Silk/RenderCanvasImpSyncContext.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Concurrent; +using System.Runtime.ExceptionServices; +using System.Threading; + +namespace Fusee.Engine.Imp.Graphics.SilkDesktop +{ + internal class RenderCanvasImpSyncContext : SynchronizationContext + { + private readonly ConcurrentQueue> _allCallbacks = new(); + + public override void Post(SendOrPostCallback d, object? state) + { + _allCallbacks.Enqueue(Tuple.Create(d, state)); + } + + internal void ExecutePendingPostAwaits() + { + while (!_allCallbacks.IsEmpty) + { + _ = _allCallbacks.TryDequeue(out var callback); + + if (callback != null) + { + try + { + var d = callback.Item1; + var state = callback.Item2; + d(state); + } + catch (Exception exception) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + } + } + } + + // should always be empty at this point, but nevertheless + _allCallbacks.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Engine/Imp/Graphics/Silk/RenderContextImp.cs b/src/Engine/Imp/Graphics/Silk/RenderContextImp.cs new file mode 100644 index 000000000..3c3def721 --- /dev/null +++ b/src/Engine/Imp/Graphics/Silk/RenderContextImp.cs @@ -0,0 +1,2658 @@ +using Fusee.Base.Common; +using Fusee.Base.Core; +using Fusee.Engine.Common; +using Fusee.Engine.Core.ShaderShards; +using Fusee.Engine.Imp.Shared; +using Fusee.Math.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using Silk.NET.OpenGL; +using Silk.NET.Core; +using Silk.NET.OpenGL.Extensions.ARB; + +namespace Fusee.Engine.Imp.Graphics.SilkDesktop +{ + /// + /// Implementation of the interface for usage with OpenTK framework. + /// + public class RenderContextImp : IRenderContextImp + { + /// + /// Constant id that describes the renderer. This can be used in shaders to do platform dependent things. + /// + public FuseePlatformId FuseePlatformId { get; } = FuseePlatformId.Desktop; + + private int _textureCountPerShader; + private readonly Dictionary _shaderParam2TexUnit; + + private GLEnum _blendEquationAlpha; + private GLEnum _blendEquationRgb; + private GLEnum _blendSrcRgb; + private GLEnum _blendDstRgb; + private GLEnum _blendSrcAlpha; + private GLEnum _blendDstAlpha; + + private bool _isCullEnabled; + private bool _isPtRenderingEnabled; + private bool _isLineSmoothEnabled; + +#if DEBUG + private static DebugProc _openGlDebugDelegate; +#endif + + private static GL GL; + + private IRenderCanvasImp canvas; + + /// + /// Initializes a new instance of the class. + /// + /// The render canvas interface. + public RenderContextImp(IRenderCanvasImp renderCanvas) + { + _textureCountPerShader = 0; + _shaderParam2TexUnit = new Dictionary(); + canvas = renderCanvas; + + ((RenderCanvasImp)canvas).Init += (s, e) => + { + GL = ((RenderCanvasImp)canvas)._gameWindow.GL; + Init(); + }; + } + + public void Init() + { + + +#if DEBUG + GL.Enable(EnableCap.DebugOutput); + GL.Enable(EnableCap.DebugOutputSynchronous); + + _openGlDebugDelegate = new DebugProc(OpenGLDebugCallback); + + GL.DebugMessageCallback(_openGlDebugDelegate, IntPtr.Zero); + GL.DebugMessageControl(GLEnum.DontCare, GLEnum.DontCare, GLEnum.DebugSeverityNotification, 0, 0, false); +#endif + + // Due to the right-handed nature of OpenGL and the left-handed design of FUSEE + // the meaning of what's Front and Back of a face simply flips. + // TODO - implement this in render states!!! + GL.CullFace(CullFaceMode.Back); + + //Needed for rendering more than one viewport. + GL.Enable(EnableCap.ScissorTest); + + GL.GetInteger(GetPName.BlendSrcAlpha, out int blendSrcAlpha); + GL.GetInteger(GetPName.BlendDstAlpha, out int blendDstAlpha); + GL.GetInteger(GetPName.BlendDstRgb, out int blendDstRgb); + GL.GetInteger(GetPName.BlendSrcRgb, out int blendSrcRgb); + GL.GetInteger(GetPName.BlendEquationAlpha, out int blendEqA); + GL.GetInteger(GetPName.BlendEquationRgb, out int blendEqRgb); + + _blendDstRgb = (GLEnum)blendDstRgb; + _blendSrcRgb = (GLEnum)blendSrcRgb; + _blendSrcAlpha = (GLEnum)blendSrcAlpha; + _blendDstAlpha = (GLEnum)blendDstAlpha; + _blendEquationAlpha = (GLEnum)blendEqA; + _blendEquationRgb = (GLEnum)blendEqRgb; + + //Diagnostics.Debug(GL.GetString(StringName.Vendor) + " - " + GL.GetString(StringName.Renderer) + " - " + GL.GetString(StringName.Version)); +#if DEBUG + var numExtensions = GL.GetInteger(GLEnum.NumExtensions); + var extensions = new string[numExtensions]; + + for (int i = 0; i < numExtensions; i++) + { + //extensions[i] = GL.GetString(StringNameIndexed.Extensions, i); + } + + Diagnostics.Verbose(string.Join(';', extensions)); +#endif + } + +#if DEBUG + private static void OpenGLDebugCallback(GLEnum source, GLEnum type, int id, GLEnum severity, int length, nint message, nint userParam) + { + Diagnostics.Debug($"{System.Runtime.InteropServices.Marshal.PtrToStringAnsi(message, length)}\n\tid:{id} severity:{severity} type:{type} source:{source}\n"); + } +#endif + + #region Image data related Members + + private Silk.NET.OpenGL.TextureCompareMode GetTexComapreMode(Common.TextureCompareMode compareMode) + { + return compareMode switch + { + Common.TextureCompareMode.None => Silk.NET.OpenGL.TextureCompareMode.None, + Common.TextureCompareMode.CompareRefToTexture => Silk.NET.OpenGL.TextureCompareMode.CompareRefToTexture, + _ => throw new ArgumentException("Invalid compare mode."), + }; + } + + private Tuple GetMinMagFilter(TextureFilterMode filterMode) + { + TextureMinFilter minFilter; + TextureMagFilter magFilter; + + switch (filterMode) + { + case TextureFilterMode.Nearest: + minFilter = TextureMinFilter.Nearest; + magFilter = TextureMagFilter.Nearest; + break; + default: + case TextureFilterMode.Linear: + minFilter = TextureMinFilter.Linear; + magFilter = TextureMagFilter.Linear; + break; + case TextureFilterMode.NearestMipmapNearest: + minFilter = TextureMinFilter.NearestMipmapNearest; + magFilter = TextureMagFilter.Nearest; + break; + case TextureFilterMode.LinearMipmapNearest: + minFilter = TextureMinFilter.LinearMipmapNearest; + magFilter = TextureMagFilter.Linear; + break; + case TextureFilterMode.NearestMipmapLinear: + minFilter = TextureMinFilter.NearestMipmapLinear; + magFilter = TextureMagFilter.Nearest; + break; + case TextureFilterMode.LinearMipmapLinear: + minFilter = TextureMinFilter.LinearMipmapLinear; + magFilter = TextureMagFilter.Linear; + break; + } + + return new Tuple(minFilter, magFilter); + } + + private DepthFunction GetDepthCompareFunc(Compare compareFunc) + { + return compareFunc switch + { + Compare.Never => DepthFunction.Never, + Compare.Less => DepthFunction.Less, + Compare.Equal => DepthFunction.Equal, + Compare.LessEqual => DepthFunction.Lequal, + Compare.Greater => DepthFunction.Greater, + Compare.NotEqual => DepthFunction.Notequal, + Compare.GreaterEqual => DepthFunction.Gequal, + Compare.Always => DepthFunction.Always, + _ => throw new ArgumentOutOfRangeException("value"), + }; + } + + private Silk.NET.OpenGL.TextureWrapMode GetWrapMode(Common.TextureWrapMode wrapMode) + { + return wrapMode switch + { + Common.TextureWrapMode.MirroredRepeat => Silk.NET.OpenGL.TextureWrapMode.MirroredRepeat, + Common.TextureWrapMode.ClampToEdge => Silk.NET.OpenGL.TextureWrapMode.ClampToEdge, + Common.TextureWrapMode.ClampToBorder => Silk.NET.OpenGL.TextureWrapMode.ClampToBorder, + _ => Silk.NET.OpenGL.TextureWrapMode.Repeat, + }; + } + + private SizedInternalFormat GetSizedInteralFormat(ImagePixelFormat format) + { + return format.ColorFormat switch + { + ColorFormat.RGBA => SizedInternalFormat.Rgba8, + ColorFormat.fRGBA16 => SizedInternalFormat.Rgba16f, + ColorFormat.fRGBA32 => SizedInternalFormat.Rgba32f, + ColorFormat.iRGBA32 => SizedInternalFormat.Rgba32i, + _ => throw new ArgumentOutOfRangeException("SizedInternalFormat not supported. Try to use a format with r,g,b and a components."), + }; + } + + private TexturePixelInfo GetTexturePixelInfo(ImagePixelFormat pixelFormat) + { + GLEnum internalFormat; + PixelFormat format; + PixelType pxType; + + //The wrong row alignment will lead to malformed textures. + //See https://www.khronos.org/opengl/wiki/Common_Mistakes#Texture_upload_and_pixel_reads + //and https://www.khronos.org/opengl/wiki/Pixel_Transfer#Pixel_layout + int rowAlignment = 4; + + switch (pixelFormat.ColorFormat) + { + case ColorFormat.RGBA: + internalFormat = GLEnum.Rgba; + format = PixelFormat.Rgba; + pxType = PixelType.UnsignedByte; + break; + + case ColorFormat.RGB: + internalFormat = GLEnum.Rgb; + format = PixelFormat.Rgb; + pxType = PixelType.UnsignedByte; + rowAlignment = 1; + break; + + // TODO: Handle Alpha-only / Intensity-only and AlphaIntensity correctly. + case ColorFormat.Intensity: + internalFormat = GLEnum.R8; + format = PixelFormat.Red; + pxType = PixelType.UnsignedByte; + rowAlignment = 1; + break; + + case ColorFormat.Depth24: + internalFormat = GLEnum.DepthComponent24; + format = PixelFormat.DepthComponent; + pxType = PixelType.Float; + break; + + case ColorFormat.Depth16: + internalFormat = GLEnum.DepthComponent16; + format = PixelFormat.DepthComponent; + pxType = PixelType.Float; + break; + + case ColorFormat.uiRgb8: + internalFormat = GLEnum.Rgba8ui; + format = PixelFormat.RgbaInteger; + pxType = PixelType.UnsignedByte; + rowAlignment = 1; + break; + + case ColorFormat.fRGB32: + internalFormat = GLEnum.Rgb32f; + format = PixelFormat.Rgb; + pxType = PixelType.Float; + break; + + case ColorFormat.fRGB16: + internalFormat = GLEnum.Rgb16f; + format = PixelFormat.Rgb; + pxType = PixelType.Float; + break; + + case ColorFormat.fRGBA16: + internalFormat = GLEnum.Rgba16f; + format = PixelFormat.Rgba; + pxType = PixelType.Float; + break; + + case ColorFormat.fRGBA32: + internalFormat = GLEnum.Rgba32f; + format = PixelFormat.Rgba; + pxType = PixelType.Float; + break; + + case ColorFormat.iRGBA32: + internalFormat = GLEnum.Rgba32i; + format = PixelFormat.RgbaInteger; + pxType = PixelType.Int; + break; + + default: + throw new ArgumentOutOfRangeException("CreateTexture: Image pixel format not supported"); + } + + return new TexturePixelInfo() + { + Format = format, + InternalFormat = internalFormat, + PxType = pxType, + RowAlignment = rowAlignment + }; + } + + /// + /// Creates a new Texture and binds it to the shader. + /// + /// A given ImageData object, containing all necessary information for the upload to the graphics card. + /// An ITextureHandle that can be used for texturing in the shader. In this implementation, the handle is an integer-value which is necessary for OpenTK. + public ITextureHandle CreateTexture(IWritableArrayTexture img) + { + uint id = GL.GenTexture(); + GL.BindTexture(TextureTarget.Texture2DArray, id); + + var glMinMagFilter = GetMinMagFilter(img.FilterMode); + var minFilter = glMinMagFilter.Item1; + var magFilter = glMinMagFilter.Item2; + var glWrapMode = GetWrapMode(img.WrapMode); + var pxInfo = GetTexturePixelInfo(img.PixelFormat); + + GL.TexImage3D((GLEnum)TextureTarget.Texture2DArray, 0, (int)pxInfo.InternalFormat, (uint)img.Width, (uint)img.Height, (uint)img.Layers, 0, (GLEnum)pxInfo.Format, (GLEnum)pxInfo.PxType, 0); + + + if (img.DoGenerateMipMaps) + GL.GenerateMipmap(GLEnum.Texture2D); + + GL.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureCompareMode, (int)GetTexComapreMode(img.CompareMode)); + GL.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureCompareFunc, (int)GetDepthCompareFunc(img.CompareFunc)); + GL.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)minFilter); + GL.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)magFilter); + GL.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)glWrapMode); + GL.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)glWrapMode); + + ITextureHandle texID = new TextureHandle { TexHandle = (int)id }; + + return texID; + } + + /// + /// Creates a new CubeMap and binds it to the shader. + /// + /// A given IWritableCubeMap object, containing all necessary information for the upload to the graphics card. + /// An ITextureHandle that can be used for texturing in the shader. In this implementation, the handle is an integer-value which is necessary for OpenTK. + public ITextureHandle CreateTexture(IWritableCubeMap img) + { + var id = GL.GenTexture(); + GL.BindTexture(TextureTarget.TextureCubeMap, id); + + var glMinMagFilter = GetMinMagFilter(img.FilterMode); + var minFilter = glMinMagFilter.Item1; + var magFilter = glMinMagFilter.Item2; + + var glWrapMode = GetWrapMode(img.WrapMode); + var pxInfo = GetTexturePixelInfo(img.PixelFormat); + + for (int i = 0; i < 6; i++) + GL.TexImage2D((GLEnum)TextureTarget.TextureCubeMapPositiveX + i, 0, (int)pxInfo.InternalFormat, (uint)img.Width, (uint)img.Height, 0, (GLEnum)pxInfo.Format, (GLEnum)pxInfo.PxType, 0); + + GL.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureCompareMode, (int)GetTexComapreMode(img.CompareMode)); + GL.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureCompareFunc, (int)GetDepthCompareFunc(img.CompareFunc)); + GL.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureMagFilter, (int)magFilter); + GL.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureMinFilter, (int)minFilter); + GL.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureWrapS, (int)glWrapMode); + GL.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureWrapT, (int)glWrapMode); + GL.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureWrapR, (int)glWrapMode); + + ITextureHandle texID = new TextureHandle { TexHandle = (int)id }; + + return texID; + } + + /// + /// Creates a new Texture and binds it to the shader. + /// + /// A given ITexture object, containing all necessary information for the upload to the graphics card. + /// An ITextureHandle that can be used for texturing in the shader. In this implementation, the handle is an integer-value which is necessary for OpenTK. + public ITextureHandle CreateTexture(ITexture img) + { + var id = GL.GenTexture(); + GL.BindTexture(TextureTarget.Texture2D, id); + + var glMinMagFilter = GetMinMagFilter(img.FilterMode); + var minFilter = glMinMagFilter.Item1; + var magFilter = glMinMagFilter.Item2; + + var glWrapMode = GetWrapMode(img.WrapMode); + + var pxInfo = GetTexturePixelInfo(img.ImageData.PixelFormat); + + GL.PixelStore(PixelStoreParameter.UnpackAlignment, pxInfo.RowAlignment); + GL.TexImage2D((GLEnum)TextureTarget.Texture2D, 0, (int)pxInfo.InternalFormat, (uint)img.ImageData.Width, (uint)img.ImageData.Height, 0, (GLEnum)pxInfo.Format, (GLEnum)pxInfo.PxType, img.ImageData.PixelData); + + if (img.DoGenerateMipMaps) + GL.GenerateMipmap(GLEnum.Texture2D); + + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)minFilter); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)magFilter); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)glWrapMode); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)glWrapMode); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapR, (int)glWrapMode); + + ITextureHandle texID = new TextureHandle { TexHandle = (int)id }; + + return texID; + } + + /// + /// Creates a new Texture and binds it to the shader. + /// + /// A given IWritableTexture object, containing all necessary information for the upload to the graphics card. + /// An ITextureHandle that can be used for texturing in the shader. In this implementation, the handle is an integer-value which is necessary for OpenTK. + public ITextureHandle CreateTexture(IWritableTexture tex) + { + var id = GL.GenTexture(); + GL.BindTexture(TextureTarget.Texture2D, id); + + var glMinMagFilter = GetMinMagFilter(tex.FilterMode); + var minFilter = glMinMagFilter.Item1; + var magFilter = glMinMagFilter.Item2; + var glWrapMode = GetWrapMode(tex.WrapMode); + var pxInfo = GetTexturePixelInfo(tex.PixelFormat); + + GL.TexImage2D((GLEnum)TextureTarget.Texture2D, 0, (int)pxInfo.InternalFormat, (uint)tex.Width, (uint)tex.Height, 0, (GLEnum)pxInfo.Format, (GLEnum)pxInfo.PxType, 0); + + if (tex.DoGenerateMipMaps) + GL.GenerateMipmap(GLEnum.Texture2D); + + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureCompareMode, (int)GetTexComapreMode(tex.CompareMode)); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureCompareFunc, (int)GetDepthCompareFunc(tex.CompareFunc)); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)minFilter); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)magFilter); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)glWrapMode); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)glWrapMode); + + ITextureHandle texID = new TextureHandle { TexHandle = (int)id }; + + return texID; + } + + /// + /// Updates a specific rectangle of a texture. + /// + /// The texture to which the ImageData is bound to. + /// The ImageData struct containing information about the image. + /// The x-value of the upper left corner of th rectangle. + /// The y-value of the upper left corner of th rectangle. + /// The width of the rectangle. + /// The height of the rectangle. + /// /// Look at the VideoTextureExample for further information. + public void UpdateTextureRegion(ITextureHandle tex, ITexture img, int startX, int startY, int width, int height) + { + var pxInfo = GetTexturePixelInfo(img.ImageData.PixelFormat); + PixelFormat format = pxInfo.Format; + + // copy the bytes from img to GPU texture + int bytesTotal = width * height * img.ImageData.PixelFormat.BytesPerPixel; + var scanlines = img.ImageData.ScanLines(startX, startY, width, height); + byte[] bytes = new byte[bytesTotal]; + int offset = 0; + do + { + if (scanlines.Current != null) + { + var lineBytes = scanlines.Current.GetScanLineBytes(); + System.Buffer.BlockCopy(lineBytes, 0, bytes, offset, lineBytes.Length); + offset += lineBytes.Length; + } + + } while (scanlines.MoveNext()); + + GL.PixelStore(PixelStoreParameter.PackAlignment, pxInfo.RowAlignment); + GL.BindTexture((GLEnum)TextureTarget.Texture2D, (uint)((TextureHandle)tex).TexHandle); + GL.TexSubImage2D((GLEnum)TextureTarget.Texture2D, 0, startX, startY, (uint)width, (uint)height, (GLEnum)format, GLEnum.UnsignedByte, bytes); + + //GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); + + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest); + } + + /// + /// Sets the textures filter mode ( at runtime. + /// + /// The handle of the texture. + /// The new filter mode. + public void SetTextureFilterMode(ITextureHandle tex, TextureFilterMode filterMode) + { + GL.BindTexture(TextureTarget.Texture2D, (uint)((TextureHandle)tex).TexHandle); + var glMinMagFilter = GetMinMagFilter(filterMode); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)glMinMagFilter.Item1); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)glMinMagFilter.Item2); + } + + /// + /// Sets the textures filter mode ( at runtime. + /// + /// The handle of the texture. + ///The new wrap mode. + public void SetTextureWrapMode(ITextureHandle tex, Common.TextureWrapMode wrapMode) + { + GL.BindTexture(TextureTarget.Texture2D, (uint)((TextureHandle)tex).TexHandle); + var glWrapMode = GetWrapMode(wrapMode); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)glWrapMode); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)glWrapMode); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapR, (int)glWrapMode); + } + + /// + /// Free all allocated gpu memory that belong to a frame-buffer object. + /// + /// The platform dependent abstraction of the gpu buffer handle. + public void DeleteFrameBuffer(IBufferHandle bh) + { + GL.DeleteFramebuffer((uint)((FrameBufferHandle)bh).Handle); + } + + /// + /// Free all allocated gpu memory that belong to a render-buffer object. + /// + /// The platform dependent abstraction of the gpu buffer handle. + public void DeleteRenderBuffer(IBufferHandle bh) + { + GL.DeleteFramebuffer((uint)((RenderBufferHandle)bh).Handle); + } + + /// + /// Free all allocated gpu memory that belong to the given . + /// + /// The which gpu allocated memory will be freed. + public void RemoveTextureHandle(ITextureHandle textureHandle) + { + TextureHandle texHandle = (TextureHandle)textureHandle; + + if (texHandle.FrameBufferHandle != -1) + { + GL.DeleteFramebuffer((uint)texHandle.FrameBufferHandle); + } + + if (texHandle.DepthRenderBufferHandle != -1) + { + GL.DeleteRenderbuffer((uint)texHandle.DepthRenderBufferHandle); + } + + if (texHandle.TexHandle != -1) + { + GL.DeleteTexture((uint)texHandle.TexHandle); + _textureCountPerShader--; + } + } + #endregion + + #region Shader related Members + + /// + /// Creates a shader object from compute shader source code. + /// + /// A string containing the compute shader source. + /// + public IShaderHandle CreateShaderProgramCompute(string cs) + { + string info = string.Empty; + int statusCode = -1; + + // Compile compute shader + uint computeObject = 0; + if (!string.IsNullOrEmpty(cs)) + { + computeObject = GL.CreateShader(ShaderType.ComputeShader); + + GL.ShaderSource(computeObject, cs); + GL.CompileShader(computeObject); + GL.GetShaderInfoLog(computeObject, out info); + GL.GetShader(computeObject, GLEnum.CompileStatus, out statusCode); + } + + if (statusCode != 1) + throw new ApplicationException(info); + + uint program = GL.CreateProgram(); + + GL.AttachShader(program, computeObject); + GL.LinkProgram(program); //Must be called AFTER BindAttribLocation + GL.DetachShader(program, computeObject); + GL.DeleteShader(computeObject); + + return new ShaderHandleImp { Handle = (int)program }; + } + + /// + /// Creates the shader program by using a valid GLSL vertex and fragment shader code. This code is compiled at runtime. + /// Do not use this function in frequent updates. + /// + /// The vertex shader code. + /// The geometry shader code. + /// The pixel(=fragment) shader code. + /// An instance of . + /// + /// + public IShaderHandle CreateShaderProgram(string vs, string ps, string gs = null) + { + uint vertexObject = GL.CreateShader(ShaderType.VertexShader); + uint fragmentObject = GL.CreateShader(ShaderType.FragmentShader); + + // Compile vertex shader + GL.ShaderSource(vertexObject, vs); + GL.CompileShader(vertexObject); + GL.GetShaderInfoLog(vertexObject, out string info); + GL.GetShader(vertexObject, GLEnum.CompileStatus, out int statusCode); + + if (statusCode != 1) + throw new ApplicationException(info); + + // Compile geometry shader + uint geometryObject = 0; + if (!string.IsNullOrEmpty(gs)) + { + geometryObject = GL.CreateShader(ShaderType.GeometryShader); + + GL.ShaderSource(geometryObject, gs); + GL.CompileShader(geometryObject); + GL.GetShaderInfoLog(geometryObject, out info); + GL.GetShader(geometryObject, GLEnum.CompileStatus, out statusCode); + } + + if (statusCode != 1) + throw new ApplicationException(info); + + // Compile pixel shader + GL.ShaderSource(fragmentObject, ps); + GL.CompileShader(fragmentObject); + GL.GetShaderInfoLog(fragmentObject, out info); + GL.GetShader(fragmentObject, GLEnum.CompileStatus, out statusCode); + + if (statusCode != 1) + throw new ApplicationException(info); + + uint program = GL.CreateProgram(); + GL.AttachShader(program, fragmentObject); + + if (!string.IsNullOrEmpty(gs)) + GL.AttachShader(program, geometryObject); + + GL.AttachShader(program, vertexObject); + + // enable GLSL (ES) shaders to use fuVertex, fuColor and fuNormal attributes + GL.BindAttribLocation(program, (uint)AttributeLocations.VertexAttribLocation, UniformNameDeclarations.Vertex); + GL.BindAttribLocation(program, (uint)AttributeLocations.ColorAttribLocation, UniformNameDeclarations.VertexColor); + GL.BindAttribLocation(program, (uint)AttributeLocations.Color1AttribLocation, UniformNameDeclarations.VertexColor1); + GL.BindAttribLocation(program, (uint)AttributeLocations.Color2AttribLocation, UniformNameDeclarations.VertexColor2); + GL.BindAttribLocation(program, (uint)AttributeLocations.UvAttribLocation, UniformNameDeclarations.TextureCoordinates); + GL.BindAttribLocation(program, (uint)AttributeLocations.NormalAttribLocation, UniformNameDeclarations.Normal); + GL.BindAttribLocation(program, (uint)AttributeLocations.TangentAttribLocation, UniformNameDeclarations.Tangent); + GL.BindAttribLocation(program, (uint)AttributeLocations.BoneIndexAttribLocation, UniformNameDeclarations.BoneIndex); + GL.BindAttribLocation(program, (uint)AttributeLocations.BoneWeightAttribLocation, UniformNameDeclarations.BoneWeight); + GL.BindAttribLocation(program, (uint)AttributeLocations.BitangentAttribLocation, UniformNameDeclarations.Bitangent); + GL.BindAttribLocation(program, (uint)AttributeLocations.FuseePlatformIdLocation, UniformNameDeclarations.FuseePlatformId); + + GL.LinkProgram(program); //Must be called AFTER BindAttribLocation + + GL.DetachShader(program, fragmentObject); + GL.DetachShader(program, vertexObject); + GL.DeleteShader(fragmentObject); + GL.DeleteShader(vertexObject); + + return new ShaderHandleImp { Handle = (int)program }; + } + + /// + /// + /// Removes shader from the GPU + /// + /// + public void RemoveShader(IShaderHandle sp) + { + var program = ((ShaderHandleImp)sp).Handle; + + // wait for all threads to be finished + GL.Finish(); + GL.Flush(); + + GL.DeleteProgram((uint)program); + } + + + /// + /// Sets the shader program onto the GL Render context. + /// + /// The shader program. + public void SetShader(IShaderHandle program) + { + _textureCountPerShader = 0; + _shaderParam2TexUnit.Clear(); + + GL.UseProgram((uint)((ShaderHandleImp)program).Handle); + } + + /// + /// Gets the shader parameter. + /// The Shader parameter is used to bind values inside of shader programs that run on the graphics card. + /// Do not use this function in frequent updates as it transfers information from graphics card to the cpu which takes time. + /// + /// The shader program. + /// Name of the parameter. + /// The Shader parameter is returned if the name is found, otherwise null. + public IShaderParam GetShaderUniformParam(IShaderHandle shaderProgram, string paramName) + { + int h = GL.GetUniformLocation((uint)((ShaderHandleImp)shaderProgram).Handle, paramName); + return (h == -1) ? null : new ShaderParam { handle = h }; + } + + /// + /// Gets the float parameter value inside a shader program by using a as search reference. + /// Do not use this function in frequent updates as it transfers information from graphics card to the cpu which takes time. + /// + /// The program. + /// The parameter. + /// A float number (default is 0). + public float GetParamValue(IShaderHandle program, IShaderParam param) + { + GL.GetUniform((uint)((ShaderHandleImp)program).Handle, ((ShaderParam)param).handle, out float f); + return f; + } + + /// + /// Returns a List of type for all ShaderStorageBlocks + /// + /// The shader program to query. + public IList GetShaderStorageBufferList(IShaderHandle shaderProgram) + { + var paramList = new List(); + var sProg = (ShaderHandleImp)shaderProgram; + GL.GetProgramInterface((uint)sProg.Handle, GLEnum.ShaderStorageBlock, GLEnum.MaxNameLength, out int ssboMaxLen); + GL.GetProgramInterface((uint)sProg.Handle, GLEnum.ShaderStorageBlock, GLEnum.ActiveResources, out int nParams); + + for (var i = 0; i < nParams; i++) + { + var paramInfo = new ShaderParamInfo(); + GL.GetProgramResourceName((uint)sProg.Handle, GLEnum.ShaderStorageBlock, (uint)i, (uint)ssboMaxLen, out _, out string name); + paramInfo.Name = name; + + uint h = GL.GetProgramResourceIndex((uint)sProg.Handle, GLEnum.ShaderStorageBlock, name); + paramInfo.Handle = (h == -1) ? null : new ShaderParam { handle = (int)h }; + paramList.Add(paramInfo); + } + + return paramList; + } + + /// + /// Gets the shader parameter list of a specific . + /// + /// The shader program. + /// All Shader parameters of a shader program are returned. + /// + public IList GetActiveUniformsList(IShaderHandle shaderProgram) + { + var sProg = (ShaderHandleImp)shaderProgram; + var paramList = new List(); + + GL.GetProgram((uint)sProg.Handle, GLEnum.ActiveUniforms, out int nParams); + + for (var i = 0; i < nParams; i++) + { + var paramInfo = new ShaderParamInfo(); + paramInfo.Name = GL.GetActiveUniform((uint)sProg.Handle, (uint)i, out paramInfo.Size, out UniformType uType); + paramInfo.Handle = GetShaderUniformParam(sProg, paramInfo.Name); + + //Diagnostics.Log($"Active Uniforms: {paramInfo.Name}"); + + paramInfo.Type = uType switch + { + UniformType.Int => typeof(int), + UniformType.Bool => typeof(bool), + UniformType.Float => typeof(float), + UniformType.Double => typeof(double), + UniformType.IntVec2 => typeof(float2), + UniformType.FloatVec2 => typeof(float2), + UniformType.FloatVec3 => typeof(float3), + UniformType.FloatVec4 => typeof(float4), + UniformType.FloatMat4 => typeof(float4x4), + UniformType.Sampler2D or UniformType.UnsignedIntSampler2D or UniformType.IntSampler2D or UniformType.Sampler2DShadow /*or UniformType.Image2D*/ => typeof(ITextureBase), + UniformType.SamplerCube or UniformType.SamplerCubeShadow => typeof(IWritableCubeMap), + UniformType.Sampler2DArray or UniformType.Sampler2DArrayShadow => typeof(IWritableArrayTexture), + _ => throw new ArgumentOutOfRangeException($"ActiveUniformType {uType} unknown."), + }; + paramList.Add(paramInfo); + } + return paramList; + } + + /// + /// Specifies the rasterized width of both aliased and antialiased lines. + /// + /// The width in px. + public void SetLineWidth(float width) + { + GL.LineWidth(width); + } + + /// + /// Sets a float shader parameter. + /// + /// The parameter. + /// The value. + public void SetShaderParam(IShaderParam param, float val) + { + GL.Uniform1(((ShaderParam)param).handle, val); + } + + /// + /// Sets a double shader parameter. + /// + /// The parameter. + /// The value. + public void SetShaderParam(IShaderParam param, double val) + { + GL.Uniform1(((ShaderParam)param).handle, val); + } + + /// + /// Sets a shader parameter. + /// + /// The parameter. + /// The value. + public void SetShaderParam(IShaderParam param, float2 val) + { + GL.Uniform2(((ShaderParam)param).handle, val.x, val.y); + } + + /// + /// Sets a array shader parameter. + /// + /// The parameter. + /// The value. + public unsafe void SetShaderParam(IShaderParam param, float2[] val) + { + fixed (float2* pFlt = &val[0]) + GL.Uniform2(((ShaderParam)param).handle, (uint)val.Length, (float*)pFlt); + } + + /// + /// Sets a shader parameter. + /// + /// The parameter. + /// The value. + public void SetShaderParam(IShaderParam param, float3 val) + { + GL.Uniform3(((ShaderParam)param).handle, val.x, val.y, val.z); + } + + /// + /// Sets a array shader parameter. + /// + /// The parameter. + /// The value. + public unsafe void SetShaderParam(IShaderParam param, float3[] val) + { + fixed (float3* pFlt = &val[0]) + GL.Uniform3(((ShaderParam)param).handle, (uint)val.Length, (float*)pFlt); + } + + /// + /// Sets a shader parameter. + /// + /// The parameter. + /// The value. + public void SetShaderParam(IShaderParam param, float4 val) + { + GL.Uniform4(((ShaderParam)param).handle, val.x, val.y, val.z, val.w); + } + + /// + /// Sets a shader parameter. + /// + /// The parameter. + /// The value. + public void SetShaderParam(IShaderParam param, float4x4 val) + { + unsafe + { + var mF = (float*)(&val); + // Row order notation + // GL.UniformMatrix4(((ShaderParam) param).handle, 1, false, mF); + + // Column order notation + GL.UniformMatrix4(((ShaderParam)param).handle, 1, true, mF); + } + } + + /// + /// Sets a array shader parameter. + /// + /// The parameter. + /// The value. + public unsafe void SetShaderParam(IShaderParam param, float4[] val) + { + fixed (float4* pFlt = &val[0]) + GL.Uniform4(((ShaderParam)param).handle, (uint)val.Length, (float*)pFlt); + } + + /// + /// Sets a array shader parameter. + /// + /// The parameter. + /// The value. + public unsafe void SetShaderParam(IShaderParam param, float4x4[] val) + { + var tmpArray = new float4[val.Length * 4]; + + for (var i = 0; i < val.Length; i++) + { + tmpArray[i * 4] = val[i].Column1; + tmpArray[i * 4 + 1] = val[i].Column2; + tmpArray[i * 4 + 2] = val[i].Column3; + tmpArray[i * 4 + 3] = val[i].Column4; + } + + fixed (float4* pMtx = &tmpArray[0]) + GL.UniformMatrix4(((ShaderParam)param).handle, (uint)val.Length, false, (float*)pMtx); + } + + /// + /// Sets a int shader parameter. + /// + /// The parameter. + /// The value. + public void SetShaderParam(IShaderParam param, int val) + { + GL.Uniform1(((ShaderParam)param).handle, val); + } + + private void BindImage(TextureType texTarget, ITextureHandle texId, int texUint, GLEnum access, SizedInternalFormat format) + { + switch (texTarget) + { + case TextureType.Image2D: + GL.BindImageTexture((uint)texUint, (uint)((TextureHandle)texId).TexHandle, 0, false, 0, access, (GLEnum)format); + break; + default: + throw new ArgumentException($"Unknown texture target: {texTarget}."); + } + } + + private void BindTextureByTarget(ITextureHandle texId, TextureType texTarget) + { + switch (texTarget) + { + case TextureType.Texture1D: + GL.BindTexture(TextureTarget.Texture1D, (uint)((TextureHandle)texId).TexHandle); + break; + case TextureType.Texture2D: + GL.BindTexture(TextureTarget.Texture2D, (uint)((TextureHandle)texId).TexHandle); + break; + case TextureType.Texture3D: + GL.BindTexture(TextureTarget.Texture3D, (uint)((TextureHandle)texId).TexHandle); + break; + case TextureType.TextureCubeMap: + GL.BindTexture(TextureTarget.TextureCubeMap, (uint)((TextureHandle)texId).TexHandle); + break; + case TextureType.ArrayTexture: + GL.BindTexture(TextureTarget.Texture2DArray, (uint)((TextureHandle)texId).TexHandle); + break; + case TextureType.Image2D: + default: + throw new ArgumentException($"Unknown texture target: {texTarget}."); + } + } + + + /// + /// Sets a texture active and binds it. + /// + /// The shader parameter, associated with this texture. + /// The texture handle. + /// The texture type, describing to which texture target the texture gets bound to. + public void SetActiveAndBindTexture(IShaderParam param, ITextureHandle texId, TextureType texTarget) + { + int iParam = ((ShaderParam)param).handle; + if (!_shaderParam2TexUnit.TryGetValue(iParam, out int texUnit)) + { + _textureCountPerShader++; + texUnit = _textureCountPerShader; + _shaderParam2TexUnit[iParam] = texUnit; + } + + GL.ActiveTexture(TextureUnit.Texture0 + texUnit); + BindTextureByTarget(texId, texTarget); + } + + private void SetActiveAndBindImage(IShaderParam param, ITextureHandle texId, TextureType texTarget, ImagePixelFormat format, GLEnum access, out int texUnit) + { + int iParam = ((ShaderParam)param).handle; + if (!_shaderParam2TexUnit.TryGetValue(iParam, out texUnit)) + { + _textureCountPerShader++; + texUnit = _textureCountPerShader; + _shaderParam2TexUnit[iParam] = texUnit; + } + + var sizedIntFormat = GetSizedInteralFormat(format); + + GL.ActiveTexture(TextureUnit.Texture0 + texUnit); + BindImage(texTarget, texId, texUnit, access, sizedIntFormat); + } + + /// + /// Sets a texture active and binds it. + /// + /// The shader parameter, associated with this texture. + /// The texture handle. + /// The texture type, describing to which texture target the texture gets bound to. + /// The texture unit. + public void SetActiveAndBindTexture(IShaderParam param, ITextureHandle texId, TextureType texTarget, out int texUnit) + { + int iParam = ((ShaderParam)param).handle; + if (!_shaderParam2TexUnit.TryGetValue(iParam, out texUnit)) + { + _textureCountPerShader++; + texUnit = _textureCountPerShader; + _shaderParam2TexUnit[iParam] = texUnit; + } + + GL.ActiveTexture(TextureUnit.Texture0 + texUnit); + BindTextureByTarget(texId, texTarget); + } + + /// + /// Sets a given Shader Parameter to a created texture + /// + /// Shader Parameter used for texture binding + /// An array of ITextureHandles returned from CreateTexture method or the ShaderEffectManager. + /// /// The texture type, describing to which texture target the texture gets bound to. + public void SetActiveAndBindTextureArray(IShaderParam param, ITextureHandle[] texIds, TextureType texTarget) + { + int iParam = ((ShaderParam)param).handle; + int[] texUnitArray = new int[texIds.Length]; + + if (!_shaderParam2TexUnit.TryGetValue(iParam, out int firstTexUnit)) + { + _textureCountPerShader++; + firstTexUnit = _textureCountPerShader; + _textureCountPerShader += texIds.Length; + _shaderParam2TexUnit[iParam] = firstTexUnit; + } + + for (int i = 0; i < texIds.Length; i++) + { + texUnitArray[i] = firstTexUnit + i; + + GL.ActiveTexture(TextureUnit.Texture0 + firstTexUnit + i); + BindTextureByTarget(texIds[i], texTarget); + } + } + + /// + /// Sets a texture active and binds it. + /// + /// The shader parameter, associated with this texture. + /// An array of ITextureHandles returned from CreateTexture method or the ShaderEffectManager. + /// The texture type, describing to which texture target the texture gets bound to. + /// The texture units. + public void SetActiveAndBindTextureArray(IShaderParam param, ITextureHandle[] texIds, TextureType texTarget, out int[] texUnitArray) + { + int iParam = ((ShaderParam)param).handle; + texUnitArray = new int[texIds.Length]; + + if (!_shaderParam2TexUnit.TryGetValue(iParam, out int firstTexUnit)) + { + _textureCountPerShader++; + firstTexUnit = _textureCountPerShader; + _textureCountPerShader += texIds.Length; + _shaderParam2TexUnit[iParam] = firstTexUnit; + } + + for (int i = 0; i < texIds.Length; i++) + { + texUnitArray[i] = firstTexUnit + i; + + GL.ActiveTexture(TextureUnit.Texture0 + firstTexUnit + i); + BindTextureByTarget(texIds[i], texTarget); + } + } + + /// + /// Sets a given Shader Parameter to a created texture + /// + /// Shader Parameter used for texture binding + /// An ITextureHandle probably returned from CreateTexture method + /// The texture type, describing to which texture target the texture gets bound to. + /// The internal sized format of the texture. + public void SetShaderParamImage(IShaderParam param, ITextureHandle texId, TextureType texTarget, ImagePixelFormat format) + { + SetActiveAndBindImage(param, texId, texTarget, format, GLEnum.ReadWrite, out int texUnit); + GL.Uniform1(((ShaderParam)param).handle, texUnit); + } + + /// + /// Sets a given Shader Parameter to a created texture + /// + /// Shader Parameter used for texture binding + /// An ITextureHandle probably returned from CreateTexture method + /// The texture type, describing to which texture target the texture gets bound to. + public void SetShaderParamTexture(IShaderParam param, ITextureHandle texId, TextureType texTarget) + { + SetActiveAndBindTexture(param, texId, texTarget, out int texUnit); + GL.Uniform1(((ShaderParam)param).handle, texUnit); + } + + /// + /// Sets a given Shader Parameter to a created texture + /// + /// Shader Parameter used for texture binding + /// An array of ITextureHandles probably returned from CreateTexture method + /// The texture type, describing to which texture target the texture gets bound to. + public unsafe void SetShaderParamTextureArray(IShaderParam param, ITextureHandle[] texIds, TextureType texTarget) + { + SetActiveAndBindTextureArray(param, texIds, texTarget, out int[] texUnitArray); + + fixed (int* pFlt = &texUnitArray[0]) + GL.Uniform1(((ShaderParam)param).handle, (uint)texUnitArray.Length, pFlt); + } + + #endregion + + #region Clear + + /// + /// Clears the specified flags. + /// + /// The flags. + public void Clear(ClearFlags flags) + { + GL.Clear((ClearBufferMask)flags); + } + + /// + /// Gets and sets the color of the background. + /// + /// + /// The color of the clear. + /// + public float4 ClearColor + { + get + { + var ret = new Span(new float[4]); + GL.GetFloat(GetPName.ColorClearValue, ret); + return new float4(ret[0], ret[1], ret[2], ret[3]); + } + set => GL.ClearColor(value.x, value.y, value.z, value.w); + } + + /// + /// Gets and sets the clear depth value which is used to clear the depth buffer. + /// + /// + /// Specifies the depth value used when the depth buffer is cleared. The initial value is 1. This value is clamped to the range [0,1]. + /// + public float ClearDepth + { + get + { + GL.GetFloat(GetPName.DepthClearValue, out float ret); + return ret; + } + set => GL.ClearDepth(value); + } + + #endregion + + #region Rendering related Members + + /// + /// Creates a with the purpose of being used as CPU GBuffer representation. + /// + /// The texture resolution. + public IRenderTarget CreateGBufferTarget(TexRes res) + { + var gBufferRenderTarget = new RenderTarget(res); + gBufferRenderTarget.SetPositionTex(); + gBufferRenderTarget.SetAlbedoTex(); + gBufferRenderTarget.SetNormalTex(); + gBufferRenderTarget.SetDepthTex(); + gBufferRenderTarget.SetSpecularTex(); + gBufferRenderTarget.SetEmissiveTex(); + gBufferRenderTarget.SetSubsurfaceTex(); + + return gBufferRenderTarget; + } + + /// + /// The clipping behavior against the Z position of a vertex can be turned off by activating depth clamping. + /// This is done with glEnable(GL_DEPTH_CLAMP). This will cause the clip-space Z to remain unclipped by the front and rear viewing volume. + /// See: https://www.khronos.org/opengl/wiki/Vertex_Post-Processing#Depth_clamping + /// + public void EnableDepthClamp() + { + GL.Enable(EnableCap.DepthClamp); + } + + /// + /// Disables depths clamping. + /// + public void DisableDepthClamp() + { + GL.Disable(EnableCap.DepthClamp); + } + + /// + /// Create one single multi-purpose attribute buffer + /// + /// + /// + /// + public IAttribImp CreateAttributeBuffer(float3[] attributes, string attributeName) + { + if (attributes == null || attributes.Length == 0) + { + throw new ArgumentException("Vertices must not be null or empty"); + } + + int vertsBytes = attributes.Length * 3 * sizeof(float); + GL.GenBuffers(1, out uint handle); + + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)handle); + + GL.BufferData(GLEnum.ArrayBuffer, (nuint)vertsBytes, new ReadOnlySpan(attributes), GLEnum.StaticDraw); + GL.GetBufferParameter(GLEnum.ArrayBuffer, GLEnum.BufferSize, out int vboBytes); + if (vboBytes != vertsBytes) + throw new ApplicationException(string.Format( + "Problem uploading attribute buffer to VBO ('{2}'). Tried to upload {0} bytes, uploaded {1}.", + vertsBytes, vboBytes, attributeName)); + + return new AttributeImp { AttributeBufferObject = (int)handle }; + } + + /// + /// Remove an attribute buffer previously created with and release all associated resources + /// allocated on the GPU. + /// + /// The attribute handle + public void DeleteAttributeBuffer(IAttribImp attribHandle) + { + if (attribHandle != null) + { + int handle = ((AttributeImp)attribHandle).AttributeBufferObject; + if (handle != 0) + { + GL.DeleteBuffer((uint)handle); + ((AttributeImp)attribHandle).AttributeBufferObject = 0; + } + } + } + + /// + /// Binds the VertexArrayObject onto the GL Render context and assigns its index to the passed instance. + /// + /// The instance. + public void SetVertexArrayObject(IMeshImp mr) + { + if (((MeshImp)mr).VertexArrayObject == 0) + ((MeshImp)mr).VertexArrayObject = (int)GL.GenVertexArray(); + + GL.BindVertexArray((uint)((MeshImp)mr).VertexArrayObject); + } + + /// + /// Binds the vertices onto the GL Render context and assigns an VertexBuffer index to the passed instance. + /// + /// The instance. + /// The vertices. + /// Vertices must not be null or empty + /// + public void SetVertices(IMeshImp mr, float3[] vertices) + { + if (vertices == null || vertices.Length == 0) + { + throw new ArgumentException("Vertices must not be null or empty"); + } + + int vertsBytes = vertices.Length * 3 * sizeof(float); + if (((MeshImp)mr).VertexBufferObject == 0) + { + GL.GenBuffers(1, out uint bufferObj); + ((MeshImp)mr).VertexBufferObject = (int)bufferObj; + } + + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).VertexBufferObject); + GL.BufferData(GLEnum.ArrayBuffer, (nuint)(vertsBytes), new ReadOnlySpan(vertices), GLEnum.StaticDraw); + GL.GetBufferParameter(GLEnum.ArrayBuffer, GLEnum.BufferSize, out int vboBytes); + if (vboBytes != vertsBytes) + throw new ApplicationException(string.Format("Problem uploading vertex buffer to VBO (vertices). Tried to upload {0} bytes, uploaded {1}.", vertsBytes, vboBytes)); + } + + /// + /// Binds the tangents onto the GL Render context and assigns an TangentBuffer index to the passed instance. + /// + /// The instance. + /// The tangents. + /// Tangents must not be null or empty + /// + public void SetTangents(IMeshImp mr, float4[] tangents) + { + if (tangents == null || tangents.Length == 0) + { + throw new ArgumentException("Tangents must not be null or empty"); + } + + int tangentBytes = tangents.Length * 4 * sizeof(float); + if (((MeshImp)mr).TangentBufferObject == 0) + { + GL.GenBuffers(1, out uint bufferObj); + ((MeshImp)mr).TangentBufferObject = (int)bufferObj; + } + + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).TangentBufferObject); + GL.BufferData(GLEnum.ArrayBuffer, (nuint)(tangentBytes), new ReadOnlySpan(tangents), GLEnum.StaticDraw); + GL.GetBufferParameter(GLEnum.ArrayBuffer, GLEnum.BufferSize, out int vboBytes); + if (vboBytes != tangentBytes) + throw new ApplicationException(string.Format("Problem uploading vertex buffer to VBO (tangents). Tried to upload {0} bytes, uploaded {1}.", tangentBytes, vboBytes)); + } + + /// + /// Binds the bitangents onto the GL Render context and assigns an BiTangentBuffer index to the passed instance. + /// + /// The instance. + /// The BiTangents. + /// BiTangents must not be null or empty + /// + public void SetBiTangents(IMeshImp mr, float3[] bitangents) + { + if (bitangents == null || bitangents.Length == 0) + { + throw new ArgumentException("BiTangents must not be null or empty"); + } + + int bitangentBytes = bitangents.Length * 3 * sizeof(float); + if (((MeshImp)mr).BitangentBufferObject == 0) + { + GL.GenBuffers(1, out uint bufferObj); + ((MeshImp)mr).BitangentBufferObject = (int)bufferObj; + } + + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).BitangentBufferObject); + GL.BufferData(GLEnum.ArrayBuffer, (nuint)(bitangentBytes), new ReadOnlySpan(bitangents), GLEnum.StaticDraw); + GL.GetBufferParameter(GLEnum.ArrayBuffer, GLEnum.BufferSize, out int vboBytes); + if (vboBytes != bitangentBytes) + throw new ApplicationException(string.Format("Problem uploading vertex buffer to VBO (bitangents). Tried to upload {0} bytes, uploaded {1}.", bitangentBytes, vboBytes)); + } + + /// + /// Binds the normals onto the GL Render context and assigns an NormalBuffer index to the passed instance. + /// + /// The instance. + /// The normals. + /// Normals must not be null or empty + /// + public void SetNormals(IMeshImp mr, float3[] normals) + { + if (normals == null || normals.Length == 0) + { + throw new ArgumentException("Normals must not be null or empty"); + } + + int normsBytes = normals.Length * 3 * sizeof(float); + if (((MeshImp)mr).NormalBufferObject == 0) + { + GL.GenBuffers(1, out uint bufferObj); + ((MeshImp)mr).NormalBufferObject = (int)bufferObj; + } + + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).NormalBufferObject); + GL.BufferData(GLEnum.ArrayBuffer, (nuint)(normsBytes), new ReadOnlySpan(normals), GLEnum.StaticDraw); + GL.GetBufferParameter(GLEnum.ArrayBuffer, GLEnum.BufferSize, out int vboBytes); + if (vboBytes != normsBytes) + throw new ApplicationException(string.Format("Problem uploading normal buffer to VBO (normals). Tried to upload {0} bytes, uploaded {1}.", normsBytes, vboBytes)); + } + + /// + /// Binds the bone indices onto the GL Render context and assigns an BondeIndexBuffer index to the passed instance. + /// + /// The instance. + /// The bone indices. + /// BoneIndices must not be null or empty + /// + public void SetBoneIndices(IMeshImp mr, float4[] boneIndices) + { + if (boneIndices == null || boneIndices.Length == 0) + { + throw new ArgumentException("BoneIndices must not be null or empty"); + } + + int indicesBytes = boneIndices.Length * 4 * sizeof(float); + if (((MeshImp)mr).BoneIndexBufferObject == 0) + { + GL.GenBuffers(1, out uint bufferObj); + ((MeshImp)mr).BoneIndexBufferObject = (int)bufferObj; + } + + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).BoneIndexBufferObject); + GL.BufferData(GLEnum.ArrayBuffer, (nuint)(indicesBytes), new ReadOnlySpan(boneIndices), GLEnum.StaticDraw); + GL.GetBufferParameter(GLEnum.ArrayBuffer, GLEnum.BufferSize, out int vboBytes); + if (vboBytes != indicesBytes) + throw new ApplicationException(string.Format("Problem uploading bone indices buffer to VBO (bone indices). Tried to upload {0} bytes, uploaded {1}.", indicesBytes, vboBytes)); + } + + /// + /// Binds the bone weights onto the GL Render context and assigns an BondeWeightBuffer index to the passed instance. + /// + /// The instance. + /// The bone weights. + /// BoneWeights must not be null or empty + /// + public void SetBoneWeights(IMeshImp mr, float4[] boneWeights) + { + if (boneWeights == null || boneWeights.Length == 0) + { + throw new ArgumentException("BoneWeights must not be null or empty"); + } + + int weightsBytes = boneWeights.Length * 4 * sizeof(float); + if (((MeshImp)mr).BoneWeightBufferObject == 0) + { + GL.GenBuffers(1, out uint bufferObj); + ((MeshImp)mr).BoneWeightBufferObject = (int)bufferObj; + } + + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).BoneWeightBufferObject); + GL.BufferData(GLEnum.ArrayBuffer, (nuint)(weightsBytes), new ReadOnlySpan(boneWeights), GLEnum.StaticDraw); + GL.GetBufferParameter(GLEnum.ArrayBuffer, GLEnum.BufferSize, out int vboBytes); + if (vboBytes != weightsBytes) + throw new ApplicationException(string.Format("Problem uploading bone weights buffer to VBO (bone weights). Tried to upload {0} bytes, uploaded {1}.", weightsBytes, vboBytes)); + } + + /// + /// Binds the UV coordinates onto the GL Render context and assigns an UVBuffer index to the passed instance. + /// + /// The instance. + /// The UV's. + /// UVs must not be null or empty + /// + public void SetUVs(IMeshImp mr, float2[] uvs) + { + if (uvs == null || uvs.Length == 0) + { + throw new ArgumentException("UVs must not be null or empty"); + } + + int uvsBytes = uvs.Length * 2 * sizeof(float); + if (((MeshImp)mr).UVBufferObject == 0) + { + GL.GenBuffers(1, out uint bufferObj); + ((MeshImp)mr).UVBufferObject = (int)bufferObj; + } + + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).UVBufferObject); + GL.BufferData(GLEnum.ArrayBuffer, (nuint)(uvsBytes), new ReadOnlySpan(uvs), GLEnum.StaticDraw); + GL.GetBufferParameter(GLEnum.ArrayBuffer, GLEnum.BufferSize, out int vboBytes); + if (vboBytes != uvsBytes) + throw new ApplicationException(string.Format("Problem uploading uv buffer to VBO (uvs). Tried to upload {0} bytes, uploaded {1}.", uvsBytes, vboBytes)); + } + + /// + /// Binds the colors onto the GL Render context and assigns an ColorBuffer index to the passed instance. + /// + /// The instance. + /// The colors. + /// colors must not be null or empty + /// + public void SetColors(IMeshImp mr, uint[] colors) + { + if (colors == null || colors.Length == 0) + { + throw new ArgumentException("colors must not be null or empty"); + } + + int colsBytes = colors.Length * sizeof(uint); + if (((MeshImp)mr).ColorBufferObject == 0) + { + GL.GenBuffers(1, out uint bufferObj); + ((MeshImp)mr).ColorBufferObject = (int)bufferObj; + } + + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).ColorBufferObject); + GL.BufferData(GLEnum.ArrayBuffer, (nuint)(colsBytes), new ReadOnlySpan(colors), GLEnum.StaticDraw); + GL.GetBufferParameter(GLEnum.ArrayBuffer, GLEnum.BufferSize, out int vboBytes); + if (vboBytes != colsBytes) + throw new ApplicationException(string.Format("Problem uploading color buffer to VBO (colors). Tried to upload {0} bytes, uploaded {1}.", colsBytes, vboBytes)); + } + + /// + /// Binds the colors onto the GL Render context and assigns an ColorBuffer index to the passed instance. + /// + /// The instance. + /// The colors. + /// colors must not be null or empty + /// + public void SetColors1(IMeshImp mr, uint[] colors) + { + if (colors == null || colors.Length == 0) + { + throw new ArgumentException("colors must not be null or empty"); + } + + int colsBytes = colors.Length * sizeof(uint); + if (((MeshImp)mr).ColorBufferObject1 == 0) + { + GL.GenBuffers(1, out uint bufferObj); + ((MeshImp)mr).ColorBufferObject1 = (int)bufferObj; + } + + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).ColorBufferObject1); + GL.BufferData(GLEnum.ArrayBuffer, (nuint)(colsBytes), new ReadOnlySpan(colors), GLEnum.StaticDraw); + GL.GetBufferParameter(GLEnum.ArrayBuffer, GLEnum.BufferSize, out int vboBytes); + if (vboBytes != colsBytes) + throw new ApplicationException(string.Format("Problem uploading color buffer to VBO (colors). Tried to upload {0} bytes, uploaded {1}.", colsBytes, vboBytes)); + } + + /// + /// Binds the colors onto the GL Render context and assigns an ColorBuffer index to the passed instance. + /// + /// The instance. + /// The colors. + /// colors must not be null or empty + /// + public void SetColors2(IMeshImp mr, uint[] colors) + { + if (colors == null || colors.Length == 0) + { + throw new ArgumentException("colors must not be null or empty"); + } + + int colsBytes = colors.Length * sizeof(uint); + if (((MeshImp)mr).ColorBufferObject2 == 0) + { + GL.GenBuffers(1, out uint bufferObj); + ((MeshImp)mr).ColorBufferObject2 = (int)bufferObj; + } + + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).ColorBufferObject2); + GL.BufferData(GLEnum.ArrayBuffer, (nuint)(colsBytes), new ReadOnlySpan(colors), GLEnum.StaticDraw); + GL.GetBufferParameter(GLEnum.ArrayBuffer, GLEnum.BufferSize, out int vboBytes); + if (vboBytes != colsBytes) + throw new ApplicationException(string.Format("Problem uploading color buffer to VBO (colors). Tried to upload {0} bytes, uploaded {1}.", colsBytes, vboBytes)); + } + + /// + /// Binds the triangles onto the GL Render context and assigns an ElementBuffer index to the passed instance. + /// + /// The instance. + /// The triangle indices. + /// triangleIndices must not be null or empty + /// + public void SetTriangles(IMeshImp mr, ushort[] triangleIndices) + { + if (triangleIndices == null || triangleIndices.Length == 0) + { + throw new ArgumentException("triangleIndices must not be null or empty"); + } + ((MeshImp)mr).NElements = triangleIndices.Length; + int trisBytes = triangleIndices.Length * sizeof(short); + + if (((MeshImp)mr).ElementBufferObject == 0) + { + GL.GenBuffers(1, out uint bufferObj); + ((MeshImp)mr).ElementBufferObject = (int)bufferObj; + } + // Upload the index buffer (elements inside the vertex buffer, not color indices as per the IndexPointer function!) + GL.BindBuffer(GLEnum.ElementArrayBuffer, (uint)((MeshImp)mr).ElementBufferObject); + GL.BufferData(GLEnum.ElementArrayBuffer, (nuint)(trisBytes), new ReadOnlySpan(triangleIndices), GLEnum.StaticDraw); + GL.GetBufferParameter(GLEnum.ElementArrayBuffer, GLEnum.BufferSize, out int vboBytes); + if (vboBytes != trisBytes) + throw new ApplicationException(string.Format("Problem uploading vertex buffer to VBO (offsets). Tried to upload {0} bytes, uploaded {1}.", trisBytes, vboBytes)); + } + + /// + /// Deletes the buffer associated with the mesh implementation. + /// + /// The mesh which buffer respectively GPU memory should be deleted. + public void RemoveVertices(IMeshImp mr) + { + GL.DeleteVertexArray((uint)((MeshImp)mr).VertexArrayObject); + GL.DeleteBuffer((uint)((MeshImp)mr).VertexBufferObject); + ((MeshImp)mr).InvalidateVertices(); + } + + /// + /// Deletes the buffer associated with the mesh implementation. + /// + /// The mesh which buffer respectively GPU memory should be deleted. + public void RemoveNormals(IMeshImp mr) + { + GL.DeleteBuffer((uint)((MeshImp)mr).NormalBufferObject); + ((MeshImp)mr).InvalidateNormals(); + } + + /// + /// Deletes the buffer associated with the mesh implementation. + /// + /// The mesh which buffer respectively GPU memory should be deleted. + public void RemoveColors(IMeshImp mr) + { + GL.DeleteBuffer((uint)((MeshImp)mr).ColorBufferObject); + ((MeshImp)mr).InvalidateColors(); + } + + /// + /// Deletes the buffer associated with the mesh implementation. + /// + /// The mesh which buffer respectively GPU memory should be deleted. + public void RemoveColors1(IMeshImp mr) + { + var bufferObj = (uint)((MeshImp)mr).ColorBufferObject1; + GL.DeleteBuffers(1, bufferObj); + ((MeshImp)mr).InvalidateColors1(); + } + + /// + /// Deletes the buffer associated with the mesh implementation. + /// + /// The mesh which buffer respectively GPU memory should be deleted. + public void RemoveColors2(IMeshImp mr) + { + var bufferObj = (uint)((MeshImp)mr).ColorBufferObject2; + GL.DeleteBuffers(1, bufferObj); + ((MeshImp)mr).InvalidateColors2(); + } + + /// + /// Deletes the buffer associated with the mesh implementation. + /// + /// The mesh which buffer respectively GPU memory should be deleted. + public void RemoveUVs(IMeshImp mr) + { + GL.DeleteBuffer((uint)((MeshImp)mr).UVBufferObject); + ((MeshImp)mr).InvalidateUVs(); + } + + /// + /// Deletes the buffer associated with the mesh implementation. + /// + /// The mesh which buffer respectively GPU memory should be deleted. + public void RemoveTriangles(IMeshImp mr) + { + GL.DeleteBuffer((uint)((MeshImp)mr).ElementBufferObject); + ((MeshImp)mr).InvalidateTriangles(); + } + + /// + /// Deletes the buffer associated with the mesh implementation. + /// + /// The mesh which buffer respectively GPU memory should be deleted. + public void RemoveBoneWeights(IMeshImp mr) + { + GL.DeleteBuffer((uint)((MeshImp)mr).BoneWeightBufferObject); + ((MeshImp)mr).InvalidateBoneWeights(); + } + + /// + /// Deletes the buffer associated with the mesh implementation. + /// + /// The mesh which buffer respectively GPU memory should be deleted. + public void RemoveBoneIndices(IMeshImp mr) + { + GL.DeleteBuffer((uint)((MeshImp)mr).BoneIndexBufferObject); + ((MeshImp)mr).InvalidateBoneIndices(); + } + + /// + /// Deletes the buffer associated with the mesh implementation. + /// + /// The mesh which buffer respectively GPU memory should be deleted. + public void RemoveTangents(IMeshImp mr) + { + GL.DeleteBuffer((uint)((MeshImp)mr).TangentBufferObject); + ((MeshImp)mr).InvalidateTangents(); + } + + /// + /// Deletes the buffer associated with the mesh implementation. + /// + /// The mesh which buffer respectively GPU memory should be deleted. + public void RemoveBiTangents(IMeshImp mr) + { + GL.DeleteBuffer((uint)((MeshImp)mr).BitangentBufferObject); + ((MeshImp)mr).InvalidateBiTangents(); + } + + /// + /// Defines a barrier ordering memory transactions. At the moment it will insert all supported barriers. + /// + public void MemoryBarrier() + { + unchecked // (uint) as -1 + { + GL.MemoryBarrier((uint)GLEnum.AllBarrierBits); + } + } + + /// + /// Launch the bound Compute Shader Program. + /// + /// + /// The number of work groups to be launched in the X dimension. + /// The number of work groups to be launched in the Y dimension. + /// he number of work groups to be launched in the Z dimension. + public void DispatchCompute(int kernelIndex, int threadGroupsX, int threadGroupsY, int threadGroupsZ) + { + GL.DispatchCompute((uint)threadGroupsX, (uint)threadGroupsY, (uint)threadGroupsZ); + } + + /// + /// Renders the specified . + /// + /// The instance. + public unsafe void Render(IMeshImp mr) + { + GL.BindVertexArray((uint)((MeshImp)mr).VertexArrayObject); + + if (((MeshImp)mr).VertexBufferObject != 0) + { + GL.EnableVertexAttribArray((uint)AttributeLocations.VertexAttribLocation); + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).VertexBufferObject); + GL.VertexAttribPointer((uint)AttributeLocations.VertexAttribLocation, 3, GLEnum.Float, false, 0, IntPtr.Zero.ToPointer()); + } + if (((MeshImp)mr).ColorBufferObject != 0) + { + GL.EnableVertexAttribArray((uint)AttributeLocations.ColorAttribLocation); + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).ColorBufferObject); + GL.VertexAttribPointer((uint)AttributeLocations.ColorAttribLocation, 4, GLEnum.UnsignedByte, true, 0, IntPtr.Zero.ToPointer()); + } + if (((MeshImp)mr).ColorBufferObject1 != 0) + { + GL.EnableVertexAttribArray((uint)AttributeLocations.Color1AttribLocation); + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).ColorBufferObject1); + GL.VertexAttribPointer((uint)AttributeLocations.Color1AttribLocation, 4, GLEnum.UnsignedByte, true, 0, IntPtr.Zero.ToPointer()); + } + if (((MeshImp)mr).ColorBufferObject2 != 0) + { + GL.EnableVertexAttribArray((uint)AttributeLocations.Color2AttribLocation); + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).ColorBufferObject2); + GL.VertexAttribPointer((uint)AttributeLocations.Color2AttribLocation, 4, GLEnum.UnsignedByte, true, 0, IntPtr.Zero.ToPointer()); + } + if (((MeshImp)mr).UVBufferObject != 0) + { + GL.EnableVertexAttribArray((uint)AttributeLocations.UvAttribLocation); + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).UVBufferObject); + GL.VertexAttribPointer((uint)AttributeLocations.UvAttribLocation, 2, GLEnum.Float, false, 0, IntPtr.Zero.ToPointer()); + } + if (((MeshImp)mr).NormalBufferObject != 0) + { + GL.EnableVertexAttribArray((uint)AttributeLocations.NormalAttribLocation); + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).NormalBufferObject); + GL.VertexAttribPointer((uint)AttributeLocations.NormalAttribLocation, 3, GLEnum.Float, false, 0, IntPtr.Zero.ToPointer()); + } + if (((MeshImp)mr).TangentBufferObject != 0) + { + GL.EnableVertexAttribArray((uint)AttributeLocations.TangentAttribLocation); + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).TangentBufferObject); + GL.VertexAttribPointer((uint)AttributeLocations.TangentAttribLocation, 3, GLEnum.Float, false, 0, IntPtr.Zero.ToPointer()); + } + if (((MeshImp)mr).BitangentBufferObject != 0) + { + GL.EnableVertexAttribArray((uint)AttributeLocations.BitangentAttribLocation); + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).BitangentBufferObject); + GL.VertexAttribPointer((uint)AttributeLocations.BitangentAttribLocation, 3, GLEnum.Float, false, 0, IntPtr.Zero.ToPointer()); + } + if (((MeshImp)mr).BoneIndexBufferObject != 0) + { + GL.EnableVertexAttribArray((uint)AttributeLocations.BoneIndexAttribLocation); + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).BoneIndexBufferObject); + GL.VertexAttribPointer((uint)AttributeLocations.BoneIndexAttribLocation, 4, GLEnum.Float, false, 0, IntPtr.Zero.ToPointer()); + } + if (((MeshImp)mr).BoneWeightBufferObject != 0) + { + GL.EnableVertexAttribArray((uint)AttributeLocations.BoneWeightAttribLocation); + GL.BindBuffer(GLEnum.ArrayBuffer, (uint)((MeshImp)mr).BoneWeightBufferObject); + GL.VertexAttribPointer((uint)AttributeLocations.BoneWeightAttribLocation, 4, GLEnum.Float, false, 0, IntPtr.Zero.ToPointer()); + } + if (((MeshImp)mr).ElementBufferObject != 0) + { + GL.BindBuffer(GLEnum.ElementArrayBuffer, (uint)((MeshImp)mr).ElementBufferObject); + + switch (((MeshImp)mr).MeshType) + { + case Common.PrimitiveType.Triangles: + default: + GL.DrawElements(GLEnum.Triangles, (uint)((MeshImp)mr).NElements, GLEnum.UnsignedShort, IntPtr.Zero.ToPointer()); + break; + case Common.PrimitiveType.Points: + // enable gl_PointSize to set the point size + if (!_isPtRenderingEnabled) + { + _isPtRenderingEnabled = true; + GL.Enable(EnableCap.ProgramPointSize); + //GL.Enable(EnableCap.PointSprite); + GL.Enable(GLEnum.VertexProgramPointSize); + } + GL.DrawElements(GLEnum.Points, (uint)((MeshImp)mr).NElements, GLEnum.UnsignedShort, IntPtr.Zero.ToPointer()); + break; + case Common.PrimitiveType.Lines: + if (!_isLineSmoothEnabled) + { + GL.Enable(EnableCap.LineSmooth); + _isLineSmoothEnabled = true; + } + GL.DrawElements(GLEnum.Lines, (uint)((MeshImp)mr).NElements, GLEnum.UnsignedShort, IntPtr.Zero.ToPointer()); + break; + case Common.PrimitiveType.LineLoop: + if (!_isLineSmoothEnabled) + { + GL.Enable(EnableCap.LineSmooth); + _isLineSmoothEnabled = true; + } + GL.DrawElements(GLEnum.LineLoop, (uint)((MeshImp)mr).NElements, GLEnum.UnsignedShort, IntPtr.Zero.ToPointer()); + break; + case Common.PrimitiveType.LineStrip: + if (!_isLineSmoothEnabled) + { + GL.Enable(EnableCap.LineSmooth); + _isLineSmoothEnabled = true; + } + GL.DrawElements(GLEnum.LineStrip, (uint)((MeshImp)mr).NElements, GLEnum.UnsignedShort, IntPtr.Zero.ToPointer()); + break; + case Common.PrimitiveType.Patches: + GL.DrawElements(GLEnum.Patches, (uint)((MeshImp)mr).NElements, GLEnum.UnsignedShort, IntPtr.Zero.ToPointer()); + break; + case Common.PrimitiveType.QuadStrip: + //GL.DrawElements(GLEnum.QuadStrip, (uint)((MeshImp)mr).NElements, GLEnum.UnsignedShort, IntPtr.Zero.ToPointer()); + break; + case Common.PrimitiveType.TriangleFan: + GL.DrawElements(GLEnum.TriangleFan, (uint)((MeshImp)mr).NElements, GLEnum.UnsignedShort, IntPtr.Zero.ToPointer()); + break; + case Common.PrimitiveType.TriangleStrip: + GL.DrawElements(GLEnum.TriangleStrip, (uint)((MeshImp)mr).NElements, GLEnum.UnsignedShort, IntPtr.Zero.ToPointer()); + break; + } + } + + if (((MeshImp)mr).VertexBufferObject != 0) + GL.DisableVertexAttribArray((uint)AttributeLocations.VertexAttribLocation); + if (((MeshImp)mr).ColorBufferObject != 0) + GL.DisableVertexAttribArray((uint)AttributeLocations.ColorAttribLocation); + if (((MeshImp)mr).NormalBufferObject != 0) + GL.DisableVertexAttribArray((uint)AttributeLocations.NormalAttribLocation); + if (((MeshImp)mr).UVBufferObject != 0) + GL.DisableVertexAttribArray((uint)AttributeLocations.UvAttribLocation); + if (((MeshImp)mr).TangentBufferObject != 0) + GL.DisableVertexAttribArray((uint)AttributeLocations.TangentAttribLocation); + if (((MeshImp)mr).BitangentBufferObject != 0) + GL.DisableVertexAttribArray((uint)AttributeLocations.TangentAttribLocation); + + GL.BindVertexArray(0); + } + + /// + /// Gets the content of the buffer. + /// + /// The Rectangle where the content is draw into. + /// The texture identifier. + public void GetBufferContent(Common.Rectangle quad, ITextureHandle texId) + { + GL.BindTexture(TextureTarget.Texture2D, (uint)((TextureHandle)texId).TexHandle); + GL.CopyTexImage2D((GLEnum)TextureTarget.Texture2D, 0, GLEnum.Rgba, quad.Left, quad.Top, (uint)quad.Width, (uint)quad.Height, 0); + } + + /// + /// Creates the mesh implementation. + /// + /// The instance. + public IMeshImp CreateMeshImp() + { + return new MeshImp(); + } + + internal static GLEnum BlendOperationToOgl(BlendOperation bo) + { + return bo switch + { + BlendOperation.Add => GLEnum.FuncAdd, + BlendOperation.Subtract => GLEnum.FuncSubtract, + BlendOperation.ReverseSubtract => GLEnum.FuncReverseSubtract, + BlendOperation.Minimum => GLEnum.Min, + BlendOperation.Maximum => GLEnum.Max, + _ => throw new ArgumentOutOfRangeException($"Invalid argument: {bo}"), + }; + } + + internal static BlendOperation BlendOperationFromOgl(GLEnum bom) + { + return bom switch + { + GLEnum.FuncAdd => BlendOperation.Add, + GLEnum.Min => BlendOperation.Minimum, + GLEnum.Max => BlendOperation.Maximum, + GLEnum.FuncSubtract => BlendOperation.Subtract, + GLEnum.FuncReverseSubtract => BlendOperation.ReverseSubtract, + _ => throw new ArgumentOutOfRangeException($"Invalid argument: {bom}"), + }; + } + + internal static GLEnum BlendToOgl(Blend blend, bool isForBlendFactorAlpha = false) + { + return blend switch + { + Blend.Zero => GLEnum.Zero, + Blend.One => GLEnum.One, + Blend.SourceColor => GLEnum.SrcColor, + Blend.InverseSourceColor => GLEnum.OneMinusSrcColor, + Blend.SourceAlpha => GLEnum.SrcAlpha, + Blend.InverseSourceAlpha => GLEnum.OneMinusSrcAlpha, + Blend.DestinationAlpha => GLEnum.DstAlpha, + Blend.InverseDestinationAlpha => GLEnum.OneMinusDstAlpha, + Blend.DestinationColor => GLEnum.DstColor, + Blend.InverseDestinationColor => GLEnum.OneMinusDstColor, + Blend.BlendFactor => ((isForBlendFactorAlpha) ? GLEnum.ConstantAlpha : GLEnum.ConstantColor), + Blend.InverseBlendFactor => ((isForBlendFactorAlpha) ? GLEnum.OneMinusConstantAlpha : GLEnum.OneMinusConstantColor), + // Ignored... + // case Blend.SourceAlphaSaturated: + // break; + //case Blend.Bothsrcalpha: + // break; + //case Blend.BothInverseSourceAlpha: + // break; + //case Blend.SourceColor2: + // break; + //case Blend.InverseSourceColor2: + // break; + _ => throw new ArgumentOutOfRangeException(nameof(blend)), + }; + } + + internal static Blend BlendFromOgl(int bf) + { + return bf switch + { + (int)GLEnum.Zero => Blend.Zero, + (int)GLEnum.One => Blend.One, + (int)GLEnum.SrcColor => Blend.SourceColor, + (int)GLEnum.OneMinusSrcColor => Blend.InverseSourceColor, + (int)GLEnum.SrcAlpha => Blend.SourceAlpha, + (int)GLEnum.OneMinusSrcAlpha => Blend.InverseSourceAlpha, + (int)GLEnum.DstAlpha => Blend.DestinationAlpha, + (int)GLEnum.OneMinusDstAlpha => Blend.InverseDestinationAlpha, + (int)GLEnum.DstColor => Blend.DestinationColor, + (int)GLEnum.OneMinusDstColor => Blend.InverseDestinationColor, + (int)GLEnum.ConstantAlpha or (int)GLEnum.ConstantColor => Blend.BlendFactor, + (int)GLEnum.OneMinusConstantAlpha or (int)GLEnum.OneMinusConstantColor => Blend.InverseBlendFactor, + _ => throw new ArgumentOutOfRangeException("blend"), + }; + } + + /// + /// Sets the RenderState object onto the current OpenGL based RenderContext. + /// + /// State of the render(enum). + /// The value. See for detailed information. + /// + /// value + /// or + /// value + /// or + /// value + /// or + /// renderState + /// + public void SetRenderState(RenderState renderState, uint value) + { + switch (renderState) + { + case RenderState.FillMode: + { + var pm = (FillMode)value switch + { + FillMode.Point => PolygonMode.Point, + FillMode.Wireframe => PolygonMode.Line, + FillMode.Solid => PolygonMode.Fill, + _ => throw new ArgumentOutOfRangeException(nameof(value)), + }; + GL.PolygonMode(MaterialFace.FrontAndBack, pm); + return; + } + case RenderState.CullMode: + { + switch ((Cull)value) + { + case Cull.None: + if (_isCullEnabled) + { + _isCullEnabled = false; + GL.Disable(EnableCap.CullFace); + } + GL.FrontFace(FrontFaceDirection.Ccw); + break; + case Cull.Clockwise: + if (!_isCullEnabled) + { + _isCullEnabled = true; + GL.Enable(EnableCap.CullFace); + } + GL.FrontFace(FrontFaceDirection.CW); + break; + case Cull.Counterclockwise: + if (!_isCullEnabled) + { + _isCullEnabled = true; + GL.Enable(EnableCap.CullFace); + } + GL.FrontFace(FrontFaceDirection.Ccw); + break; + default: + throw new ArgumentOutOfRangeException(nameof(value)); + } + } + break; + case RenderState.Clipping: + // clipping is always on in OpenGL - This state is simply ignored + break; + case RenderState.ZFunc: + { + DepthFunction df = GetDepthCompareFunc((Compare)value); + GL.DepthFunc(df); + } + break; + case RenderState.ZEnable: + if (value == 0) + GL.Disable(EnableCap.DepthTest); + else + GL.Enable(EnableCap.DepthTest); + break; + case RenderState.ZWriteEnable: + GL.DepthMask(value != 0); + break; + case RenderState.AlphaBlendEnable: + if (value == 0) + GL.Disable(EnableCap.Blend); + else + GL.Enable(EnableCap.Blend); + break; + case RenderState.BlendOperation: + { + _blendEquationRgb = BlendOperationToOgl((BlendOperation)value); + GL.BlendEquationSeparate(_blendEquationRgb, _blendEquationAlpha); + } + break; + + case RenderState.BlendOperationAlpha: + { + _blendEquationAlpha = BlendOperationToOgl((BlendOperation)value); + GL.BlendEquationSeparate(_blendEquationRgb, (GLEnum)_blendEquationAlpha); + } + break; + case RenderState.SourceBlend: + { + _blendSrcRgb = BlendToOgl((Blend)value); + GL.BlendFuncSeparate(_blendSrcRgb, _blendDstRgb, _blendSrcAlpha, _blendDstAlpha); + } + break; + case RenderState.DestinationBlend: + { + _blendDstRgb = BlendToOgl((Blend)value); + GL.BlendFuncSeparate(_blendSrcRgb, _blendDstRgb, _blendSrcAlpha, _blendDstAlpha); + } + break; + case RenderState.SourceBlendAlpha: + { + _blendSrcAlpha = BlendToOgl((Blend)value); + GL.BlendFuncSeparate(_blendSrcRgb, _blendDstRgb, _blendSrcAlpha, _blendDstAlpha); + } + break; + case RenderState.DestinationBlendAlpha: + { + _blendDstAlpha = BlendToOgl((Blend)value); + GL.BlendFuncSeparate(_blendSrcRgb, _blendDstRgb, _blendSrcAlpha, _blendDstAlpha); + } + break; + case RenderState.BlendFactor: + GL.BlendColor(System.Drawing.Color.FromArgb((int)value)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(renderState)); + } + } + + /// + /// Gets the current RenderState that is applied to the current OpenGL based RenderContext. + /// + /// State of the render. See for further information. + /// + /// + /// pm;Value + ((PolygonMode)pm) + not handled + /// or + /// depFunc;Value + ((DepthFunction)depFunc) + not handled + /// or + /// renderState + /// + public uint GetRenderState(RenderState renderState) + { + switch (renderState) + { + case RenderState.FillMode: + { + GL.GetInteger(GetPName.PolygonMode, out int pm); + var ret = (PolygonMode)pm switch + { + PolygonMode.Point => FillMode.Point, + PolygonMode.Line => FillMode.Wireframe, + PolygonMode.Fill => FillMode.Solid, + _ => throw new ArgumentOutOfRangeException("pm", "Value " + ((PolygonMode)pm) + " not handled"), + }; + return (uint)ret; + } + case RenderState.CullMode: + { + GL.GetInteger(GetPName.CullFace, out int cullFace); + if (cullFace == 0) + return (uint)Cull.None; + GL.GetInteger(GetPName.FrontFace, out int frontFace); + if (frontFace == (int)FrontFaceDirection.CW) + return (uint)Cull.Clockwise; + return (uint)Cull.Counterclockwise; + } + case RenderState.Clipping: + // clipping is always on in OpenGL - This state is simply ignored + return 1; // == true + case RenderState.ZFunc: + { + GL.GetInteger(GetPName.DepthFunc, out int depFunc); + var ret = (DepthFunction)depFunc switch + { + DepthFunction.Never => Compare.Never, + DepthFunction.Less => Compare.Less, + DepthFunction.Equal => Compare.Equal, + DepthFunction.Lequal => Compare.LessEqual, + DepthFunction.Greater => Compare.Greater, + DepthFunction.Notequal => Compare.NotEqual, + DepthFunction.Gequal => Compare.GreaterEqual, + DepthFunction.Always => Compare.Always, + _ => throw new ArgumentOutOfRangeException("depFunc", "Value " + ((DepthFunction)depFunc) + " not handled"), + }; + return (uint)ret; + } + case RenderState.ZEnable: + { + GL.GetInteger(GetPName.DepthTest, out int depTest); + return (uint)(depTest); + } + case RenderState.ZWriteEnable: + { + GL.GetInteger(GetPName.DepthWritemask, out int depWriteMask); + return (uint)(depWriteMask); + } + case RenderState.AlphaBlendEnable: + { + GL.GetInteger(GetPName.Blend, out int blendEnable); + return (uint)(blendEnable); + } + case RenderState.BlendOperation: + { + GL.GetInteger(GetPName.BlendEquationRgb, out int rgbMode); + return (uint)BlendOperationFromOgl((GLEnum)rgbMode); + } + case RenderState.BlendOperationAlpha: + { + GL.GetInteger(GetPName.BlendEquationAlpha, out int alphaMode); + return (uint)BlendOperationFromOgl((GLEnum)alphaMode); + } + case RenderState.SourceBlend: + { + GL.GetInteger(GetPName.BlendSrcRgb, out int rgbSrc); + return (uint)BlendFromOgl(rgbSrc); + } + case RenderState.DestinationBlend: + { + GL.GetInteger(GetPName.BlendSrcRgb, out int rgbDst); + return (uint)BlendFromOgl(rgbDst); + } + case RenderState.SourceBlendAlpha: + { + GL.GetInteger(GetPName.BlendSrcAlpha, out int alphaSrc); + return (uint)BlendFromOgl(alphaSrc); + } + case RenderState.DestinationBlendAlpha: + { + GL.GetInteger(GetPName.BlendDstAlpha, out int alphaDst); + return (uint)BlendFromOgl(alphaDst); + } + case RenderState.BlendFactor: + int col; + GL.GetInteger(GetPName.BlendColorExt, out col); + return (uint)col; + default: + throw new ArgumentOutOfRangeException(nameof(renderState)); + } + } + + /// + /// Renders into the given texture. + /// + /// The texture. + /// The texture handle, associated with the given texture. Should be created by the TextureManager in the RenderContext. + public void SetRenderTarget(IWritableTexture tex, ITextureHandle texHandle) + { + if (((TextureHandle)texHandle).FrameBufferHandle == -1) + { + var fBuffer = GL.GenFramebuffer(); + ((TextureHandle)texHandle).FrameBufferHandle = (int)fBuffer; + GL.BindFramebuffer(GLEnum.Framebuffer, fBuffer); + + GL.BindTexture(TextureTarget.Texture2D, (uint)((TextureHandle)texHandle).TexHandle); + + if (tex.TextureType != RenderTargetTextureTypes.Depth) + { + CreateDepthRenderBuffer(tex.Width, tex.Height); + GL.FramebufferTexture(GLEnum.Framebuffer, GLEnum.ColorAttachment0, (uint)((TextureHandle)texHandle).TexHandle, 0); + GL.DrawBuffer(DrawBufferMode.ColorAttachment0); + } + else + { + GL.FramebufferTexture(GLEnum.Framebuffer, GLEnum.DepthAttachment, (uint)((TextureHandle)texHandle).TexHandle, 0); + GL.DrawBuffer(DrawBufferMode.None); + GL.ReadBuffer(ReadBufferMode.None); + } + } + else + GL.BindFramebuffer(GLEnum.Framebuffer, (uint)((TextureHandle)texHandle).FrameBufferHandle); + + if (GL.CheckFramebufferStatus(GLEnum.Framebuffer) != GLEnum.FramebufferComplete) + throw new Exception($"Error creating RenderTarget: {GL.GetError()}, {GL.CheckFramebufferStatus(GLEnum.Framebuffer)}"); + + GL.Clear(ClearBufferMask.DepthBufferBit | ClearBufferMask.ColorBufferBit); + } + + /// + /// Renders into the given cube map. + /// + /// The texture. + /// The texture handle, associated with the given cube map. Should be created by the TextureManager in the RenderContext. + public void SetRenderTarget(IWritableCubeMap tex, ITextureHandle texHandle) + { + if (((TextureHandle)texHandle).FrameBufferHandle == -1) + { + var fBuffer = GL.GenFramebuffer(); + ((TextureHandle)texHandle).FrameBufferHandle = (int)fBuffer; + GL.BindFramebuffer(GLEnum.Framebuffer, fBuffer); + + GL.BindTexture(TextureTarget.TextureCubeMap, (uint)((TextureHandle)texHandle).TexHandle); + + if (tex.TextureType != RenderTargetTextureTypes.Depth) + { + CreateDepthRenderBuffer(tex.Width, tex.Height); + GL.FramebufferTexture(GLEnum.Framebuffer, GLEnum.ColorAttachment0, (uint)((TextureHandle)texHandle).TexHandle, 0); + GL.DrawBuffer(DrawBufferMode.ColorAttachment0); + } + else + { + GL.FramebufferTexture(GLEnum.Framebuffer, GLEnum.DepthAttachment, (uint)((TextureHandle)texHandle).TexHandle, 0); + GL.DrawBuffer(DrawBufferMode.None); + GL.ReadBuffer(ReadBufferMode.None); + } + } + else + GL.BindFramebuffer(GLEnum.Framebuffer, (uint)((TextureHandle)texHandle).FrameBufferHandle); + + if (GL.CheckFramebufferStatus(GLEnum.Framebuffer) != GLEnum.FramebufferComplete) + throw new Exception($"Error creating RenderTarget: {GL.GetError()}, {GL.CheckFramebufferStatus(GLEnum.Framebuffer)}"); + + + GL.Clear(ClearBufferMask.DepthBufferBit | ClearBufferMask.ColorBufferBit); + } + + /// + /// Renders into the given layer of the array texture. + /// + /// The array texture. + /// The layer to render to. + /// The texture handle, associated with the given texture. Should be created by the TextureManager in the RenderContext. + public void SetRenderTarget(IWritableArrayTexture tex, int layer, ITextureHandle texHandle) + { + if (((TextureHandle)texHandle).FrameBufferHandle == -1) + { + var fBuffer = GL.GenFramebuffer(); + ((TextureHandle)texHandle).FrameBufferHandle = (int)fBuffer; + GL.BindFramebuffer(GLEnum.Framebuffer, fBuffer); + + GL.BindTexture(TextureTarget.Texture2DArray, (uint)((TextureHandle)texHandle).TexHandle); + + if (tex.TextureType != RenderTargetTextureTypes.Depth) + { + CreateDepthRenderBuffer(tex.Width, tex.Height); + GL.FramebufferTextureLayer(GLEnum.Framebuffer, GLEnum.ColorAttachment0, (uint)((TextureHandle)texHandle).TexHandle, 0, layer); + GL.DrawBuffer(DrawBufferMode.ColorAttachment0); + } + else + { + GL.FramebufferTextureLayer(GLEnum.Framebuffer, GLEnum.DepthAttachment, (uint)((TextureHandle)texHandle).TexHandle, 0, layer); + GL.DrawBuffer(DrawBufferMode.None); + GL.ReadBuffer(ReadBufferMode.None); + } + } + else + { + GL.BindFramebuffer(GLEnum.Framebuffer, (uint)((TextureHandle)texHandle).FrameBufferHandle); + GL.BindTexture(TextureTarget.Texture2DArray, (uint)((TextureHandle)texHandle).TexHandle); + GL.FramebufferTextureLayer(GLEnum.Framebuffer, GLEnum.DepthAttachment, (uint)((TextureHandle)texHandle).TexHandle, 0, layer); + } + + if (GL.CheckFramebufferStatus(GLEnum.Framebuffer) != GLEnum.FramebufferComplete) + throw new Exception($"Error creating RenderTarget: {GL.GetError()}, {GL.CheckFramebufferStatus(GLEnum.Framebuffer)}"); + + GL.Clear(ClearBufferMask.DepthBufferBit | ClearBufferMask.ColorBufferBit); + } + + /// + /// Renders into the given textures of the RenderTarget. + /// + /// The render target. + /// The texture handles, associated with the given textures. Each handle should be created by the TextureManager in the RenderContext. + public void SetRenderTarget(IRenderTarget renderTarget, ITextureHandle[] texHandles) + { + if (renderTarget == null || (renderTarget.RenderTextures.All(x => x == null))) + { + GL.BindFramebuffer(GLEnum.Framebuffer, 0); + return; + } + + int gBuffer; + + if (renderTarget.GBufferHandle == null) + { + renderTarget.GBufferHandle = new FrameBufferHandle(); + gBuffer = CreateFrameBuffer(renderTarget, texHandles); + ((FrameBufferHandle)renderTarget.GBufferHandle).Handle = gBuffer; + } + else + { + gBuffer = ((FrameBufferHandle)renderTarget.GBufferHandle).Handle; + GL.BindFramebuffer(GLEnum.Framebuffer, (uint)gBuffer); + } + + if (renderTarget.RenderTextures[(int)RenderTargetTextureTypes.Depth] == null && !renderTarget.IsDepthOnly) + { + int gDepthRenderbufferHandle; + if (renderTarget.DepthBufferHandle == null) + { + renderTarget.DepthBufferHandle = new RenderBufferHandle(); + // Create and attach depth buffer (renderbuffer) + gDepthRenderbufferHandle = CreateDepthRenderBuffer((int)renderTarget.TextureResolution, (int)renderTarget.TextureResolution); + ((RenderBufferHandle)renderTarget.DepthBufferHandle).Handle = gDepthRenderbufferHandle; + } + else + { + gDepthRenderbufferHandle = ((RenderBufferHandle)renderTarget.DepthBufferHandle).Handle; + GL.BindRenderbuffer(GLEnum.Renderbuffer, (uint)gDepthRenderbufferHandle); + } + } + + if (GL.CheckFramebufferStatus(GLEnum.Framebuffer) != GLEnum.FramebufferComplete) + { + throw new Exception($"Error creating RenderTarget: {GL.GetError()}, {GL.CheckFramebufferStatus(GLEnum.Framebuffer)}"); + } + + GL.Clear(ClearBufferMask.DepthBufferBit | ClearBufferMask.ColorBufferBit); + } + + private int CreateDepthRenderBuffer(int width, int height) + { + GL.Enable(EnableCap.DepthTest); + + GL.GenRenderbuffers(1, out uint gDepthRenderbufferHandle); + //((FrameBufferHandle)renderTarget.DepthBufferHandle).Handle = gDepthRenderbufferHandle; + GL.BindRenderbuffer(GLEnum.Renderbuffer, gDepthRenderbufferHandle); + GL.RenderbufferStorage(GLEnum.Renderbuffer, GLEnum.DepthComponent24, (uint)width, (uint)height); + GL.FramebufferRenderbuffer(GLEnum.Framebuffer, GLEnum.DepthAttachment, GLEnum.Renderbuffer, gDepthRenderbufferHandle); + return (int)gDepthRenderbufferHandle; + } + + private int CreateFrameBuffer(IRenderTarget renderTarget, ITextureHandle[] texHandles) + { + var gBuffer = GL.GenFramebuffer(); + GL.BindFramebuffer(GLEnum.Framebuffer, gBuffer); + + int depthCnt = 0; + + var depthTexPos = (int)RenderTargetTextureTypes.Depth; + + if (!renderTarget.IsDepthOnly) + { + var attachments = new List(); + + //Textures + for (int i = 0; i < texHandles.Length; i++) + { + attachments.Add(GLEnum.ColorAttachment0 + i); + + var texHandle = texHandles[i]; + if (texHandle == null) continue; + + if (i == depthTexPos) + { + GL.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.DepthAttachment + (depthCnt), TextureTarget.Texture2D, (uint)((TextureHandle)texHandle).TexHandle, 0); + depthCnt++; + } + else + GL.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.ColorAttachment0 + (i - depthCnt), TextureTarget.Texture2D, (uint)((TextureHandle)texHandle).TexHandle, 0); + } + GL.DrawBuffers((uint)attachments.Count, attachments.ToArray()); + } + else //If a frame-buffer only has a depth texture we don't need draw buffers + { + var texHandle = texHandles[depthTexPos]; + + if (texHandle != null) + GL.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.DepthAttachment, TextureTarget.Texture2D, (uint)((TextureHandle)texHandle).TexHandle, 0); + else + throw new NullReferenceException("Texture handle is null!"); + + GL.DrawBuffer(DrawBufferMode.None); + GL.ReadBuffer(ReadBufferMode.None); + } + + return (int)gBuffer; + } + + /// + /// Detaches a texture from the frame buffer object, associated with the given render target. + /// + /// The render target. + /// Number of the fbo attachment. For example: attachment = 1 will detach the texture currently associated with . + /// Determines if the texture is a depth texture. In this case the texture currently associated with will be detached. + public void DetachTextureFromFbo(IRenderTarget renderTarget, bool isDepthTex, int attachment = 0) + { + ChangeFramebufferTexture2D(renderTarget, attachment, 0, isDepthTex); + } + + + /// + /// Attaches a texture to the frame buffer object, associated with the given render target. + /// + /// The render target. + /// Number of the fbo attachment. For example: attachment = 1 will attach the texture to . + /// Determines if the texture is a depth texture. In this case the texture is attached to . + /// The gpu handle of the texture. + public void AttacheTextureToFbo(IRenderTarget renderTarget, bool isDepthTex, ITextureHandle texHandle, int attachment = 0) + { + ChangeFramebufferTexture2D(renderTarget, attachment, ((TextureHandle)texHandle).TexHandle, isDepthTex); + } + + private void ChangeFramebufferTexture2D(IRenderTarget renderTarget, int attachment, int handle, bool isDepth) + { + var boundFbo = GL.GetInteger(GLEnum.FramebufferBinding); + var rtFbo = ((FrameBufferHandle)renderTarget.GBufferHandle).Handle; + + var isCurrentFbo = true; + + if (boundFbo != rtFbo) + { + isCurrentFbo = false; + GL.BindFramebuffer(GLEnum.Framebuffer, (uint)rtFbo); + } + + if (!isDepth) + GL.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.ColorAttachment0 + attachment, TextureTarget.Texture2D, (uint)handle, 0); + else + GL.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.DepthAttachment, TextureTarget.Texture2D, (uint)handle, 0); + + if (GL.CheckFramebufferStatus(GLEnum.Framebuffer) != GLEnum.FramebufferComplete) + throw new Exception($"Error creating RenderTarget: {GL.GetError()}, {GL.CheckFramebufferStatus(GLEnum.Framebuffer)}"); + + if (!isCurrentFbo) + GL.BindFramebuffer(GLEnum.Framebuffer, (uint)boundFbo); + } + + + /// + /// Only pixels that lie within the scissor box can be modified by drawing commands. + /// Note that the Scissor test must be enabled for this to work. + /// + /// X Coordinate of the lower left point of the scissor box. + /// Y Coordinate of the lower left point of the scissor box. + /// Width of the scissor box. + /// Height of the scissor box. + public void Scissor(int x, int y, int width, int height) + { + GL.Scissor(x, y, (uint)width, (uint)height); + } + + /// + /// Set the Viewport of the rendering output window by x,y position and width,height parameters. + /// The Viewport is the portion of the final image window. + /// + /// The x. + /// The y. + /// The width. + /// The height. + public void Viewport(int x, int y, int width, int height) + { + GL.Viewport(x, y, (uint)width, (uint)height); + } + + /// + /// Enable or disable Color channels to be written to the frame buffer (final image). + /// Use this function as a color channel filter for the final image. + /// + /// if set to true [red]. + /// if set to true [green]. + /// if set to true [blue]. + /// if set to true [alpha]. + public void ColorMask(bool red, bool green, bool blue, bool alpha) + { + GL.ColorMask(red, green, blue, alpha); + } + + /// + /// Returns the capabilities of the underlying graphics hardware + /// + /// + /// uint + public uint GetHardwareCapabilities(HardwareCapability capability) + { + return capability switch + { + HardwareCapability.CanRenderDeferred => 1U, //!GL.GetString(StringName.Extensions).Contains("EXT_framebuffer_object") ? 0U : 1U, + HardwareCapability.CanUseGeometryShaders => 1U, + _ => throw new ArgumentOutOfRangeException(nameof(capability), capability, null), + }; + } + + /// + /// Returns a human readable description of the underlying graphics hardware. This implementation reports GL_VENDOR, GL_RENDERER, GL_VERSION and GL_EXTENSIONS. + /// + /// + public string GetHardwareDescription() + { + return ""; + //return "Vendor: " + GL.GetString(StringName.Vendor) + "\nRenderer: " + GL.GetString(StringName.Renderer) + "\nVersion: " + GL.GetString(StringName.Version) + "\nExtensions: " + GL.GetString(StringName.Extensions); + } + + /// + /// Draws a Debug Line in 3D Space by using a start and end point (float3). + /// + /// The starting point of the DebugLine. + /// The endpoint of the DebugLine. + /// The color of the DebugLine. + public void DebugLine(float3 start, float3 end, float4 color) + { + //GL.Begin(GLEnum.Lines); + //GL.Color4(color.x, color.y, color.z, color.w); + //GL.Vertex3(start.x, start.y, start.z); + //GL.Color4(color.x, color.y, color.z, color.w); + //GL.Vertex3(end.x, end.y, end.z); + //GL.End(); + } + + #endregion + + #region Shader Storage Buffer + + /// + /// Connects the given SSBO to the currently active shader program. + /// + /// The handle of the current shader program. + /// The Storage Buffer object on the CPU. + /// The SSBO's name. + public void ConnectBufferToShaderStorage(IShaderHandle currentProgram, IStorageBuffer buffer, string ssboName) + { + var shaderProgram = (uint)((ShaderHandleImp)currentProgram).Handle; + var resInx = GL.GetProgramResourceIndex(shaderProgram, ProgramInterface.ShaderStorageBlock, ssboName); + GL.ShaderStorageBlockBinding(shaderProgram, resInx, (uint)buffer.BindingIndex); + GL.BindBufferBase(GLEnum.ShaderStorageBuffer, (uint)buffer.BindingIndex, (uint)((StorageBufferHandle)buffer.BufferHandle).Handle); + } + + /// + /// Uploads the given data to the SSBO. If the buffer is not created on the GPU by no it will be. + /// + /// The data type. + /// The Storage Buffer Object on the CPU. + /// The data that will be uploaded. + public unsafe void StorageBufferSetData(IStorageBuffer storageBuffer, T[] data) where T : struct + { + throw new NotImplementedException(); + + if (storageBuffer.BufferHandle == null) + storageBuffer.BufferHandle = new StorageBufferHandle(); + var bufferHandle = (StorageBufferHandle)storageBuffer.BufferHandle; + int dataBytes = storageBuffer.Count * storageBuffer.Size; + + //1. Generate Buffer and or set the data + if (bufferHandle.Handle == -1) + { + bufferHandle.Handle = (int)GL.GenBuffer(); + } + + if (data == null || data.Length == 0) + { + throw new ArgumentException("Data must not be null or empty"); + } + + + GL.BindBuffer(GLEnum.ShaderStorageBuffer, (uint)bufferHandle.Handle); + if (data != null && data.GetType().IsValueType) + { + // GL.BufferData(GLEnum.ShaderStorageBuffer, (nuint)dataBytes, in data, GLEnum.DynamicCopy); + } + + GL.GetBufferParameter(GLEnum.ShaderStorageBuffer, GLEnum.BufferSize, out int bufferBytes); + if (bufferBytes != dataBytes) + throw new ApplicationException(string.Format("Problem uploading bone indices buffer to SSBO. Tried to upload {0} bytes, uploaded {1}.", bufferBytes, dataBytes)); + + GL.BindBuffer(GLEnum.ShaderStorageBuffer, 0); + } + + /// + /// Deletes the shader storage buffer on the GPU. + /// + /// The buffer object. + public void DeleteStorageBuffer(IBufferHandle storageBufferHandle) + { + GL.DeleteBuffer((uint)((StorageBufferHandle)storageBufferHandle).Handle); + } + + #endregion + + #region Picking related Members + + /// + /// Retrieves a sub-image of the given region. + /// + /// The x value of the start of the region. + /// The y value of the start of the region. + /// The width to copy. + /// The height to copy. + /// The specified sub-image + public IImageData GetPixelColor(int x, int y, int w = 1, int h = 1) + { + ImageData image = ImageData.CreateImage(w, h, ColorUint.Black); + GL.ReadPixels(x, y, (uint)w, (uint)h, GLEnum.Rgb, GLEnum.UnsignedByte, new Span(image.PixelData)); + return image; + } + + /// + /// Retrieves the Z-value at the given pixel position. + /// + /// The x value. + /// The y value. + /// The Z value at (x, y). + public float GetPixelDepth(int x, int y) + { + GL.ReadPixels(x, y, 1, 1, GLEnum.DepthComponent, GLEnum.UnsignedByte, out float depth); + return depth; + } + + + + #endregion + } +} \ No newline at end of file diff --git a/src/Engine/Imp/Graphics/Silk/TexturePixelInfo.cs b/src/Engine/Imp/Graphics/Silk/TexturePixelInfo.cs new file mode 100644 index 000000000..694a52f48 --- /dev/null +++ b/src/Engine/Imp/Graphics/Silk/TexturePixelInfo.cs @@ -0,0 +1,14 @@ +using Fusee.Engine.Common; +using Silk.NET.OpenGL; + +namespace Fusee.Engine.Imp.Graphics.SilkDesktop +{ + /// Type that sums up infos about the pixels OpenGl needs to create textures on the gpu. + internal struct TexturePixelInfo : ITexturePixelInfo + { + public GLEnum InternalFormat; + public PixelFormat Format; + public PixelType PxType; + public int RowAlignment; + } +} \ No newline at end of file diff --git a/src/Engine/Imp/Graphics/Silk/WindowHandle.cs b/src/Engine/Imp/Graphics/Silk/WindowHandle.cs new file mode 100644 index 000000000..2ecb1f2e5 --- /dev/null +++ b/src/Engine/Imp/Graphics/Silk/WindowHandle.cs @@ -0,0 +1,16 @@ +using Fusee.Engine.Common; +using System; + +namespace Fusee.Engine.Imp.Graphics.SilkDesktop +{ + /// + /// Implementation of the cross-platform abstraction of the window handle. + /// + public class WindowHandle : IWindowHandle + { + /// + /// The Window Handle as IntPtr + /// + public IntPtr Handle { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Engine/Imp/Graphics/Silk/WindowsTouchDeviceImp.cs b/src/Engine/Imp/Graphics/Silk/WindowsTouchDeviceImp.cs new file mode 100644 index 000000000..ad1884b48 --- /dev/null +++ b/src/Engine/Imp/Graphics/Silk/WindowsTouchDeviceImp.cs @@ -0,0 +1,629 @@ +using Fusee.Engine.Common; +using Silk.NET.Windowing; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Fusee.Engine.Imp.Graphics.SilkDesktop +{ + + /// + /// Input driver implementation supporting Windows 8 touch input as described in + /// https://msdn.microsoft.com/en-us/library/windows/desktop/hh454904(v=vs.85).aspx + /// + public class WindowsTouchInputDriverImp : IInputDriverImp + { + readonly IWindow _gameWindow; + readonly WindowsTouchInputDeviceImp _touch; + /// + /// Initializes a new instance of the class. + /// + /// The render canvas. Internally this must be a Windows canvas with a valid window handle. + /// + /// + /// RenderCanvas must be of type + public WindowsTouchInputDriverImp(IRenderCanvasImp renderCanvas) + { + if (renderCanvas == null) + throw new ArgumentNullException(nameof(renderCanvas)); + + if (!(renderCanvas is RenderCanvasImp)) + throw new ArgumentException($"renderCanvas must be of type {typeof(RenderCanvasImp).FullName}", nameof(renderCanvas)); + + _gameWindow = ((RenderCanvasImp)renderCanvas)._gameWindow.window; + if (_gameWindow == null) + throw new ArgumentNullException(nameof(_gameWindow)); + + + _touch = new WindowsTouchInputDeviceImp(_gameWindow); + } + + /// + /// Retrieves a list of devices supported by this input driver. + /// + /// + /// The list of devices. + /// + /// + /// The devices yielded represent the current status. At any time other devices can connect or disconnect. + /// Listen to the and events to get + /// informed about new or vanishing devices. Drivers may implement "static" access to devices such that + /// devices are connected at driver instantiation and never disconnected (in this case + /// and are never fired). + /// + public IEnumerable Devices { get { yield return _touch; } } + + /// + /// Gets the unique driver identifier. + /// + /// + /// The driver identifier. + /// + public string DriverId => GetType().FullName; + + /// + /// Gets the driver description string. + /// + /// + /// A human-readable string describing the driver. + /// + public string DriverDesc => "Driver providing a touch device implementation for Windows 8 (and up) touch input."; + +#pragma warning disable 0067 + /// + /// Not supported on this driver. Mouse and keyboard are considered to be connected all the time. + /// You can register handlers but they will never get called. + /// + public event EventHandler DeviceDisconnected; + + /// + /// Not supported on this driver. Mouse and keyboard are considered to be connected all the time. + /// You can register handlers but they will never get called. + /// + public event EventHandler NewDeviceConnected; +#pragma warning restore 0067 + + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + /// + /// Part of the Dispose pattern. + /// + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + disposedValue = true; + } + } + + // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + // ~RenderCanvasInputDriverImp() { + // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + // Dispose(false); + // } + + // This code added to correctly implement the disposable pattern. + /// + /// Part of the dispose pattern. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // TODO: uncomment the following line if the finalizer is overridden above. + // GC.SuppressFinalize(this); + } + #endregion + } + + /// + /// Touch input device implementation for the Windows platform. This implementation directly + /// sniffles at the render window's message pump (identified by the parameter passed + /// to the constructor) to receive + /// WM_POINTER messages. + /// + public class WindowsTouchInputDeviceImp : IInputDeviceImp + { + private readonly Dictionary _tpAxisDescs; + private readonly Dictionary _tpButtonDescs; + private readonly Dictionary _activeTouchpoints; + private readonly int _nTouchPointsSupported = 5; + private readonly HandleRef _handle; + private readonly IWindow _gameWindow; + + + #region Windows handling + // This helper static method is required because the 32-bit version of user32.dll does not contain this API + // (on any versions of Windows), so linking the method will fail at run-time. The bridge dispatches the request + // to the correct function (GetWindowLong in 32-bit mode and GetWindowLongPtr in 64-bit mode) + private static IntPtr SetWindowLongPtr(HandleRef hWnd, int nIndex, IntPtr dwNewLong) + { + if (IntPtr.Size == 8) + return SetWindowLongPtr64(hWnd, nIndex, dwNewLong); + else + return new IntPtr(SetWindowLong32(hWnd, nIndex, dwNewLong.ToInt32())); + } + + [DllImport("user32.dll", EntryPoint = "SetWindowLong")] + private static extern int SetWindowLong32(HandleRef hWnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")] + private static extern IntPtr SetWindowLongPtr64(HandleRef hWnd, int nIndex, IntPtr dwNewLong); + + private readonly int GWLP_WNDPROC = -4; + + private enum WinMessage : int + { + WM_CLOSE = 0x0010, + WM_NCPOINTERUP = 0x0243, + WM_POINTERUPDATE = 0x0245, + WM_POINTERDOWN = 0x0246, + WM_POINTERUP = 0x0247, + } + + // As defined in + [Flags] + private enum PointerMsgFlags : uint + { + // ReSharper disable InconsistentNaming + POINTER_MESSAGE_FLAG_NEW = 0x00000001, // New pointer + POINTER_MESSAGE_FLAG_INRANGE = 0x00000002, // Pointer has not departed + POINTER_MESSAGE_FLAG_INCONTACT = 0x00000004, // Pointer is in contact + POINTER_MESSAGE_FLAG_FIRSTBUTTON = 0x00000010, // Primary action + POINTER_MESSAGE_FLAG_SECONDBUTTON = 0x00000020, // Secondary action + POINTER_MESSAGE_FLAG_THIRDBUTTON = 0x00000040, // Third button + POINTER_MESSAGE_FLAG_FOURTHBUTTON = 0x00000080, // Fourth button + POINTER_MESSAGE_FLAG_FIFTHBUTTON = 0x00000100, // Fifth button + POINTER_MESSAGE_FLAG_PRIMARY = 0x00002000, // Pointer is primary + POINTER_MESSAGE_FLAG_CONFIDENCE = 0x00004000, // Pointer is considered unlikely to be accidental + POINTER_MESSAGE_FLAG_CANCELED = 0x00008000, // Pointer is departing in an abnormal manner + // ReSharper enable InconsistentNaming + } + + private UInt16 LOWORD(UInt32 wParam) => unchecked((UInt16)wParam); + private UInt16 HIWORD(UInt32 wParam) => unchecked((UInt16)((wParam >> 16) & 0xFFFF)); + + private UInt16 GET_X_LPARAM(UInt32 lp) => LOWORD(lp); + private UInt16 GET_Y_LPARAM(UInt32 lp) => HIWORD(lp); + + private int GET_POINTERID_WPARAM(UInt32 wParam) => LOWORD(wParam); + + + [DllImport("user32.dll")] + private static extern IntPtr DefWindowProc(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll")] + // private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, int Msg, int wParam, IntPtr lParam); + static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern IntPtr EnableMouseInPointer(bool fEnable); + /// + /// The touch point. + /// + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + /// + /// The X coordinate of the touch point. + /// + public int X; + + /// + /// The Y coordinate of the touch point. + /// + public int Y; + + /// + /// Initializes a new instance of the struct. + /// + /// The x coordinate. + /// The y coordinate. + public POINT(int x, int y) + { + this.X = x; + this.Y = y; + } + + /// + /// Initializes a new instance of the struct from a given . + /// + /// The to create the instance from. + public POINT(System.Drawing.Point pt) : this(pt.X, pt.Y) { } + + /// + /// Converts a to a . + /// + /// The to convert. + public static implicit operator System.Drawing.Point(POINT p) + { + return new System.Drawing.Point(p.X, p.Y); + } + + /// + /// Converts a to a . + /// + /// The to convert. + public static implicit operator POINT(System.Drawing.Point p) + { + return new POINT(p.X, p.Y); + } + } + + [DllImport("user32.dll")] + static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); + + private delegate IntPtr WinProc(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + + private WinProc _newWinProc; + + private IntPtr _oldWndProc = IntPtr.Zero; + + + private void DisconnectWindowsEvents() + { + if (_handle.Handle != IntPtr.Zero) + { + SetWindowLongPtr(_handle, GWLP_WNDPROC, _oldWndProc); + } + } + + + private IntPtr TouchWindowProc(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam) + { + unchecked + { + POINT winPoint; + switch (Msg) + { + case (int)WinMessage.WM_CLOSE: // Seems to be defect. + DisconnectWindowsEvents(); + break; + case (int)WinMessage.WM_POINTERUPDATE: + winPoint = new POINT(GET_X_LPARAM((uint)lParam), GET_Y_LPARAM((uint)lParam)); + ScreenToClient(hWnd, ref winPoint); + OnWindowsTouchMove(GET_POINTERID_WPARAM((uint)wParam), winPoint.X, winPoint.Y); + return IntPtr.Zero; + case (int)WinMessage.WM_POINTERUP: + winPoint = new POINT(GET_X_LPARAM((uint)lParam), GET_Y_LPARAM((uint)lParam)); + ScreenToClient(hWnd, ref winPoint); + OnWindowsTouchEnd(GET_POINTERID_WPARAM((uint)wParam), winPoint.X, winPoint.Y); + return IntPtr.Zero; + case (int)WinMessage.WM_POINTERDOWN: + winPoint = new POINT(GET_X_LPARAM((uint)lParam), GET_Y_LPARAM((uint)lParam)); + ScreenToClient(hWnd, ref winPoint); + OnWindowsTouchStart(GET_POINTERID_WPARAM((uint)wParam), winPoint.X, winPoint.Y); + return IntPtr.Zero; + case (int)WinMessage.WM_NCPOINTERUP: + winPoint = new POINT(GET_X_LPARAM((uint)lParam), GET_Y_LPARAM((uint)lParam)); + ScreenToClient(hWnd, ref winPoint); + OnWindowsTouchCancel(GET_POINTERID_WPARAM((uint)wParam), winPoint.X, winPoint.Y); + return IntPtr.Zero; + } + } + return CallWindowProc(_oldWndProc, hWnd, Msg, wParam, lParam); + } + + private void ConnectWindowsEvents() + { + OperatingSystem os = Environment.OSVersion; + + // See https://msdn.microsoft.com/library/windows/desktop/ms724832.aspx : Apps, that do NOT target a specific windows version (like 8.1 or 10) + // retrieve Version# 6.2 (resembling Windows 8), which is the version where "Pointer" touch handling is first supported. + if (os.Platform == PlatformID.Win32NT + && (os.Version.Major > 6 + || os.Version.Major == 6 && os.Version.Minor >= 2) + ) + { + EnableMouseInPointer(false); + if (_handle.Handle != IntPtr.Zero) + { + _newWinProc = new WinProc(TouchWindowProc); + _oldWndProc = SetWindowLongPtr(_handle, GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate(_newWinProc)); + } + } + } + + private float GetWindowWidth() + { + return _gameWindow.Size.X; + } + + private float GetWindowHeight() + { + return _gameWindow.Size.Y; + } + #endregion + + private int NextFreeTouchIndex + { + get + { + for (int i = 0; i < _nTouchPointsSupported; i++) + if (!_activeTouchpoints.ContainsValue(i)) + return i; + + return -1; + } + } + + + #region Windows Callbacks + internal void OnWindowsTouchStart(int id, float x, float y) + { + // Diagnostics.Log($"TouchStart {id}"); + if (_activeTouchpoints.ContainsKey(id)) + throw new InvalidOperationException($"Windows Touch id {id} is already tracked. Cannot track another touchpoint using this id."); + + var inx = NextFreeTouchIndex; + if (inx < 0) + return; + + _activeTouchpoints[id] = inx; + ButtonValueChanged?.Invoke(this, new ButtonValueChangedArgs { Button = _tpButtonDescs[(int)TouchPoints.Touchpoint_0 + inx].ButtonDesc, Pressed = true }); + AxisValueChanged?.Invoke(this, new AxisValueChangedArgs { Axis = _tpAxisDescs[(int)TouchAxes.Touchpoint_0_X + 2 * inx].AxisDesc, Value = x }); + AxisValueChanged?.Invoke(this, new AxisValueChangedArgs { Axis = _tpAxisDescs[(int)TouchAxes.Touchpoint_0_Y + 2 * inx].AxisDesc, Value = y }); + } + + internal void OnWindowsTouchMove(int id, float x, float y) + { + // Diagnostics.Log($"TouchMove {id}"); + if (!_activeTouchpoints.TryGetValue(id, out int inx)) + return; + + AxisValueChanged?.Invoke(this, new AxisValueChangedArgs { Axis = _tpAxisDescs[(int)TouchAxes.Touchpoint_0_X + 2 * inx].AxisDesc, Value = x }); + AxisValueChanged?.Invoke(this, new AxisValueChangedArgs { Axis = _tpAxisDescs[(int)TouchAxes.Touchpoint_0_Y + 2 * inx].AxisDesc, Value = y }); + } + internal void OnWindowsTouchEnd(int id, float x, float y) + { + // Diagnostics.Log($"TouchEnd {id}"); + if (!_activeTouchpoints.TryGetValue(id, out int inx)) + return; + + AxisValueChanged?.Invoke(this, new AxisValueChangedArgs { Axis = _tpAxisDescs[(int)TouchAxes.Touchpoint_0_X + 2 * inx].AxisDesc, Value = x }); + AxisValueChanged?.Invoke(this, new AxisValueChangedArgs { Axis = _tpAxisDescs[(int)TouchAxes.Touchpoint_0_Y + 2 * inx].AxisDesc, Value = y }); + ButtonValueChanged?.Invoke(this, new ButtonValueChangedArgs { Button = _tpButtonDescs[(int)TouchPoints.Touchpoint_0 + inx].ButtonDesc, Pressed = false }); + _activeTouchpoints.Remove(id); + } + internal void OnWindowsTouchCancel(int id, float x, float y) + { + // Diagnostics.Log($"TouchCancel {id}"); + if (!_activeTouchpoints.TryGetValue(id, out int inx)) + return; + ButtonValueChanged?.Invoke(this, new ButtonValueChangedArgs { Button = _tpButtonDescs[(int)TouchPoints.Touchpoint_0 + inx].ButtonDesc, Pressed = false }); + _activeTouchpoints.Remove(id); + } + #endregion + + + /// + /// Initializes a new instance of the class. + /// + /// The game window to hook on to receive + /// WM_POINTER messages. + public WindowsTouchInputDeviceImp(IWindow gameWindow) + { + _gameWindow = gameWindow; + _handle = new HandleRef(_gameWindow, _gameWindow.Handle); + ConnectWindowsEvents(); + _tpAxisDescs = new Dictionary(_nTouchPointsSupported * 2 + 5); + _activeTouchpoints = new Dictionary(_nTouchPointsSupported); + + _tpAxisDescs[(int)TouchAxes.ActiveTouchpoints] = new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = $"Active Touchpoints", + Id = (int)TouchAxes.ActiveTouchpoints, + Direction = AxisDirection.Unknown, + Nature = AxisNature.Unknown, + Bounded = AxisBoundedType.Unbound + }, + PollAxis = true + }; + _tpAxisDescs[(int)TouchAxes.MinX] = new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = "MinX", + Id = (int)TouchAxes.MinX, + Direction = AxisDirection.X, + Nature = AxisNature.Position, + Bounded = AxisBoundedType.Unbound, + MinValueOrAxis = float.NaN, + MaxValueOrAxis = float.NaN + }, + PollAxis = true + }; + _tpAxisDescs[(int)TouchAxes.MaxX] = new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = "MaxX", + Id = (int)TouchAxes.MaxX, + Direction = AxisDirection.X, + Nature = AxisNature.Position, + Bounded = AxisBoundedType.Unbound, + MinValueOrAxis = float.NaN, + MaxValueOrAxis = float.NaN + }, + PollAxis = true + }; + _tpAxisDescs[(int)TouchAxes.MinY] = new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = "MinY", + Id = (int)TouchAxes.MinY, + Direction = AxisDirection.Y, + Nature = AxisNature.Position, + Bounded = AxisBoundedType.Unbound, + MinValueOrAxis = float.NaN, + MaxValueOrAxis = float.NaN + }, + PollAxis = true + }; + _tpAxisDescs[(int)TouchAxes.MaxY] = new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = "MaxY", + Id = (int)TouchAxes.MaxY, + Direction = AxisDirection.Y, + Nature = AxisNature.Position, + Bounded = AxisBoundedType.Unbound, + MinValueOrAxis = float.NaN, + MaxValueOrAxis = float.NaN + }, + PollAxis = true + }; + + for (var i = 0; i < _nTouchPointsSupported; i++) + { + int id = 2 * i + (int)TouchAxes.Touchpoint_0_X; + _tpAxisDescs[id] = new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = $"Touchpoint {id} X", + Id = id, + Direction = AxisDirection.X, + Nature = AxisNature.Position, + Bounded = AxisBoundedType.OtherAxis, + MinValueOrAxis = (int)TouchAxes.MinX, + MaxValueOrAxis = (int)TouchAxes.MaxX + }, + PollAxis = false + }; + id++; + _tpAxisDescs[id] = new AxisImpDescription + { + AxisDesc = new AxisDescription + { + Name = $"Touchpoint {id} Y", + Id = id, + Direction = AxisDirection.Y, + Nature = AxisNature.Position, + Bounded = AxisBoundedType.OtherAxis, + MinValueOrAxis = (int)TouchAxes.MinY, + MaxValueOrAxis = (int)TouchAxes.MaxY + }, + PollAxis = false + }; + } + + _tpButtonDescs = new Dictionary(_nTouchPointsSupported); + for (var i = 0; i < _nTouchPointsSupported; i++) + { + int id = i + (int)TouchPoints.Touchpoint_0; + _tpButtonDescs[id] = new ButtonImpDescription + { + ButtonDesc = new ButtonDescription() + { + Name = $"Touchpoint {i} Active", + Id = id, + }, + PollButton = false + }; + } + } + + /// + /// Short description string for this device to be used in dialogs. + /// + public string Desc => "MS Windows standard Touch device."; + + /// + /// Returns a (hopefully) unique ID for this driver. Uniqueness is granted by using the + /// full class name (including namespace). + /// + public string Id => GetType().FullName; + + /// + /// Occurs on value changes of axes exhibited by this device. + /// Only applies for axes where the is set to false. + /// + public event EventHandler AxisValueChanged; + + /// A touchpoints's contact state is communicated by a button. + /// + public event EventHandler ButtonValueChanged; + + + /// + /// Returns , just because it's a touch device :-). + /// + public DeviceCategory Category => DeviceCategory.Touch; + /// + /// Returns the number of axes. Up to five touchpoints (with two axes (X and Y) per Touchpoint plus + /// one axis carrying the number of currently touched touchpoints plus four axes describing the minimum and + /// maximum X and Y values. + /// + /// + /// The axes count. + /// + public int AxesCount => _nTouchPointsSupported * 2 + 5; + + /// + /// Returns description information for all axes. + /// + public IEnumerable AxisImpDesc => _tpAxisDescs.Values; + + /// + /// Retrieves values for the number of currently active touchpoints and the touches min and max values. The touchpoint X and Y axes themselves are event-based axes. + /// Do not query them here. + /// + /// The axis to retrieve information for. + /// The value at the given axis. + public float GetAxis(int iAxisId) + { + return iAxisId switch + { + (int)TouchAxes.ActiveTouchpoints => _activeTouchpoints.Count, + (int)TouchAxes.MinX => 0, + (int)TouchAxes.MaxX => GetWindowWidth(), + (int)TouchAxes.MinY => 0, + (int)TouchAxes.MaxY => GetWindowHeight(), + _ => throw new InvalidOperationException($"Unknown axis {iAxisId}. Probably an event based axis or unsupported by this device."), + }; + } + + /// + /// Retrieves the button count. One button for each of the up to five supported touchpoints signaling that the touchpoint currently has contact. + /// + /// + /// The button count. + /// + public int ButtonCount => _nTouchPointsSupported; + + /// + /// Retrieve a description for each button. + /// + /// + /// The button imp description. + /// + public IEnumerable ButtonImpDesc => _tpButtonDescs.Values; + + /// + /// Gets the button state. This device's buttons signal that a finger (nose, elbow, knee...) currently has contact with the scree surface. + /// + /// The button identifier. + /// true if the button is hit, else false + public bool GetButton(int iButtonId) + { + throw new InvalidOperationException($"Unknown button id {iButtonId}. This device supports no pollable buttons at all."); + } + } +} \ No newline at end of file