.
This commit is contained in:
@@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresenterWpf", "PresenterWp
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresenterConsole", "PresenterConsole\PresenterConsole.csproj", "{85AA55C5-F8AF-4C38-8874-702684BCAFBB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresenterNative", "PresenterNative\PresenterNative.csproj", "{3B8B7867-5B38-4013-A366-159083F5C095}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -36,5 +38,9 @@ Global
|
||||
{85AA55C5-F8AF-4C38-8874-702684BCAFBB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{85AA55C5-F8AF-4C38-8874-702684BCAFBB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{85AA55C5-F8AF-4C38-8874-702684BCAFBB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3B8B7867-5B38-4013-A366-159083F5C095}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3B8B7867-5B38-4013-A366-159083F5C095}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3B8B7867-5B38-4013-A366-159083F5C095}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3B8B7867-5B38-4013-A366-159083F5C095}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#shader vertex
|
||||
#version 330 core
|
||||
|
||||
layout(location = 0) in vec2 aPos;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(aPos, 0.0, 1.0);
|
||||
}
|
||||
|
||||
#shader fragment
|
||||
#version 330 core
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
FragColor = vec4(1.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
@@ -8,14 +8,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Engine\Engine.csproj" />
|
||||
<ProjectReference Include="..\Engine\Engine.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
|
||||
<PackageReference Include="Serilog" Version="4.1.0"/>
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0"/>
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.10"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -6,15 +6,7 @@ namespace DoomDeathmatch;
|
||||
|
||||
internal abstract class Program
|
||||
{
|
||||
[DllImport("kernel32.dll")]
|
||||
static extern IntPtr GetConsoleWindow();
|
||||
|
||||
[DllImport("User32.dll", SetLastError = true)]
|
||||
public static extern IntPtr GetDC(IntPtr hwnd);
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var engine = new Engine.Engine(480, 270, false, "Doom Deathmatch");
|
||||
engine.Run();
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,11 @@ namespace DoomDeathmatch;
|
||||
|
||||
public struct QuadVertex : IVertex
|
||||
{
|
||||
[Vertex(VertexAttribType.Float, 2)]
|
||||
public Vector2 Position2;
|
||||
[Vertex(VertexAttribType.Float, 2)] public Vector2 Position2;
|
||||
|
||||
[Vertex(VertexAttribType.Float, 2)]
|
||||
public Vector2 Position;
|
||||
[Vertex(VertexAttribType.Float, 2)] public Vector2 Position;
|
||||
|
||||
[Vertex(VertexAttribType.Float, 2)]
|
||||
public Vector2 Position4;
|
||||
[Vertex(VertexAttribType.Float, 2)] public Vector2 Position4;
|
||||
|
||||
[Vertex(VertexAttribType.Float, 2)]
|
||||
public Vector2 Position3;
|
||||
[Vertex(VertexAttribType.Float, 2)] public Vector2 Position3;
|
||||
}
|
||||
@@ -10,15 +10,21 @@
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="EngineTests"/>
|
||||
<InternalsVisibleTo Include="PresenterConsole"/>
|
||||
<InternalsVisibleTo Include="PresenterWpf"/>
|
||||
<InternalsVisibleTo Include="PresenterNative"/>
|
||||
|
||||
<PackageReference Include="OpenTK" Version="4.8.2"/>
|
||||
<PackageReference Include="Serilog" Version="4.1.0"/>
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0"/>
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -6,14 +6,16 @@ namespace Engine.Asset;
|
||||
public class Image<T>(T[,] parPixels)
|
||||
where T : struct, IPixel
|
||||
{
|
||||
public int Width { get; } = parPixels.GetLength(1);
|
||||
public int Height { get; } = parPixels.GetLength(0);
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
public T[,] Pixels { get; } = parPixels;
|
||||
|
||||
public T this[int parY, int parX] => Pixels[parY, parX];
|
||||
|
||||
public Image(int parWidth, int parHeight) : this(new T[parHeight, parWidth])
|
||||
{
|
||||
Width = parWidth;
|
||||
Height = parHeight;
|
||||
}
|
||||
|
||||
public DynamicTexture ToDynamicTexture()
|
||||
|
||||
@@ -7,25 +7,49 @@ using OpenTK.Mathematics;
|
||||
using OpenTK.Windowing.Common;
|
||||
using OpenTK.Windowing.Desktop;
|
||||
using Serilog;
|
||||
using Serilog.Sinks.SystemConsole.Themes;
|
||||
|
||||
namespace Engine;
|
||||
|
||||
public sealed class Engine
|
||||
{
|
||||
public Renderer Renderer => _renderer;
|
||||
public SceneManager SceneManager => _sceneManager;
|
||||
public Renderer Renderer { get; }
|
||||
public SceneManager SceneManager { get; } = new();
|
||||
|
||||
internal Window Window { get; }
|
||||
|
||||
internal IPresenter? Presenter
|
||||
{
|
||||
get => _presenter;
|
||||
set
|
||||
{
|
||||
if (_presenter != null)
|
||||
{
|
||||
_presenter.Resize -= PresenterResize;
|
||||
}
|
||||
|
||||
_presenter = value;
|
||||
|
||||
if (_presenter != null)
|
||||
{
|
||||
_presenter.Resize += PresenterResize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal IInputHandler? InputHandler
|
||||
{
|
||||
get => _inputHandler;
|
||||
set => _inputHandler = value;
|
||||
}
|
||||
|
||||
private readonly Window _window;
|
||||
private readonly Renderer _renderer;
|
||||
private readonly SceneManager _sceneManager = new();
|
||||
private readonly ILogger _logger;
|
||||
private readonly IInputHandler _inputHandler;
|
||||
private readonly IPresenter _presenter;
|
||||
|
||||
private IInputHandler? _inputHandler;
|
||||
private IPresenter? _presenter;
|
||||
|
||||
private Thread? _updateThread;
|
||||
|
||||
public Engine(int parWidth, int parHeight, bool parHeadless = false, string parTitle = "")
|
||||
public Engine(int parWidth, int parHeight, bool parHeadless, string parTitle, ILogger parLogger)
|
||||
{
|
||||
var settings = new NativeWindowSettings
|
||||
{
|
||||
@@ -33,41 +57,16 @@ public sealed class Engine
|
||||
Title = parTitle,
|
||||
StartVisible = !parHeadless,
|
||||
APIVersion = new Version(4, 6),
|
||||
Profile = ContextProfile.Compatability
|
||||
Profile = ContextProfile.Core
|
||||
};
|
||||
|
||||
_window = new Window(this, new NativeWindow(settings), parHeadless);
|
||||
_renderer = new Renderer(parWidth, parHeight);
|
||||
if (!parHeadless)
|
||||
{
|
||||
_window.Resize += parArgs =>
|
||||
{
|
||||
_renderer.Resize(parArgs.Width, parArgs.Height);
|
||||
};
|
||||
_presenter = _window;
|
||||
}
|
||||
|
||||
Thread.CurrentThread.Name = "RendererThread";
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Verbose()
|
||||
.Enrich.WithThreadName()
|
||||
.Enrich.WithThreadId()
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console(
|
||||
outputTemplate:
|
||||
"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u3}] [{ThreadName,-15:l}:{ThreadId,-4:d4}] [{SourceContext:l}] {Message:lj}{NewLine}{Exception}",
|
||||
theme: AnsiConsoleTheme.Literate)
|
||||
.CreateLogger();
|
||||
Window = new Window(this, new NativeWindow(settings), parHeadless);
|
||||
Renderer = new Renderer(parWidth, parHeight);
|
||||
|
||||
Log.Logger = parLogger;
|
||||
_logger = Log.ForContext<Engine>();
|
||||
}
|
||||
|
||||
public Engine(int parWidth, int parHeight, Func<Engine, IPresenter> parPresenter) : this(parWidth, parHeight, true)
|
||||
{
|
||||
_presenter = parPresenter(this);
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
_updateThread = new Thread(RunUpdate);
|
||||
@@ -75,23 +74,24 @@ public sealed class Engine
|
||||
|
||||
var timer = Stopwatch.StartNew();
|
||||
var deltaTime = 0.0;
|
||||
while (!_presenter.IsExiting)
|
||||
while (!Presenter?.IsExiting ?? false)
|
||||
{
|
||||
var time = deltaTime;
|
||||
_renderer.Commit(_ => GL.ClearColor(0.6f, 0.0f, 0.6f, 1.0f));
|
||||
_renderer.Commit(_ => GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit));
|
||||
_renderer.Commit(parRenderer =>
|
||||
{
|
||||
parRenderer.QuadRenderer.Commit(
|
||||
Matrix4.CreateScale(MathF.Sin((float)time) * 2) * Matrix4.CreateRotationZ(MathF.Sin((float)time)),
|
||||
new Vector4((MathF.Sin((float)time * 3) + 1.0f) / 2, (MathF.Sin((float)time) + 1.0f) / 2, (MathF.Sin((float)time * 5) + 1.0f) / 2, 1.0f));
|
||||
parRenderer.QuadRenderer.Render(Matrix4.Identity, Matrix4.Identity);
|
||||
parRenderer.QuadRenderer.Reset();
|
||||
});
|
||||
_renderer.Render();
|
||||
_presenter.Present(_renderer.TextureInternal);
|
||||
Renderer.StartFrame();
|
||||
|
||||
_presenter.Update(deltaTime);
|
||||
GL.ClearColor(0.6f, 0.0f, 0.6f, 1.0f);
|
||||
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
||||
|
||||
Renderer.QuadRenderer.Commit(
|
||||
Matrix4.CreateRotationZ((float)deltaTime),
|
||||
new Vector4((MathF.Sin((float)deltaTime) + 1.0f) / 2, (MathF.Sin((float)deltaTime) + 1.0f) / 2,
|
||||
(MathF.Sin((float)deltaTime) + 1.0f) / 2, 1.0f));
|
||||
Renderer.QuadRenderer.Render(Matrix4.Identity, Matrix4.Identity);
|
||||
Renderer.QuadRenderer.Reset();
|
||||
|
||||
Renderer.EndFrame();
|
||||
|
||||
Presenter!.Present(Renderer.TextureInternal);
|
||||
Presenter!.Update(deltaTime);
|
||||
deltaTime = timer.Elapsed.TotalSeconds;
|
||||
}
|
||||
|
||||
@@ -102,11 +102,19 @@ public sealed class Engine
|
||||
{
|
||||
var timer = Stopwatch.StartNew();
|
||||
var deltaTime = 0.0;
|
||||
while (!_presenter.IsExiting)
|
||||
while (!Presenter?.IsExiting ?? false)
|
||||
{
|
||||
_sceneManager.Update(deltaTime);
|
||||
SceneManager.Update(deltaTime);
|
||||
timer.Restart();
|
||||
deltaTime = timer.Elapsed.TotalSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
private void PresenterResize(ResizeEventArgs parEventArgs)
|
||||
{
|
||||
if (parEventArgs.Width == 0 || parEventArgs.Height == 0)
|
||||
return;
|
||||
|
||||
Renderer.Resize(parEventArgs.Width, parEventArgs.Height);
|
||||
}
|
||||
}
|
||||
151
Engine/src/EngineBuilder.cs
Normal file
151
Engine/src/EngineBuilder.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using Engine.Graphics;
|
||||
using Engine.Input;
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Sinks.SystemConsole.Themes;
|
||||
|
||||
namespace Engine;
|
||||
|
||||
public sealed class EngineBuilder
|
||||
{
|
||||
private string _title = "";
|
||||
private bool _headless;
|
||||
private int _width = 1;
|
||||
private int _height = 1;
|
||||
|
||||
private Func<Engine, IPresenter>? _presenterFunc;
|
||||
private Func<Engine, IInputHandler>? _inputHandlerFunc;
|
||||
|
||||
// Logging
|
||||
private bool _logToConsole;
|
||||
private bool _logToFile;
|
||||
private string? _logFilePath;
|
||||
private LogEventLevel _logLevel = LogEventLevel.Information;
|
||||
|
||||
public EngineBuilder() { }
|
||||
|
||||
public EngineBuilder Title(string parTitle)
|
||||
{
|
||||
_title = parTitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EngineBuilder Headless(bool parHeadless = true)
|
||||
{
|
||||
_headless = parHeadless;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EngineBuilder Width(int parWidth)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parWidth);
|
||||
|
||||
_width = parWidth;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EngineBuilder Height(int parHeight)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parHeight);
|
||||
|
||||
_height = parHeight;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EngineBuilder Presenter(IPresenter parPresenter)
|
||||
{
|
||||
_presenterFunc = _ => parPresenter;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EngineBuilder Presenter(Func<Engine, IPresenter> parPresenterFunc)
|
||||
{
|
||||
_presenterFunc = parPresenterFunc;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EngineBuilder InputHandler(IInputHandler parInputHandler)
|
||||
{
|
||||
_inputHandlerFunc = _ => parInputHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EngineBuilder InputHandler(Func<Engine, IInputHandler> parInputHandlerFunc)
|
||||
{
|
||||
_inputHandlerFunc = parInputHandlerFunc;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EngineBuilder LogToConsole(bool parLogToConsole = true)
|
||||
{
|
||||
_logToConsole = parLogToConsole;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EngineBuilder LogToFile(bool parLogToFile = true, string? parLogFilePath = null)
|
||||
{
|
||||
if (parLogToFile && parLogFilePath == null)
|
||||
throw new ArgumentNullException(nameof(parLogFilePath));
|
||||
|
||||
_logToFile = parLogToFile;
|
||||
_logFilePath = parLogFilePath;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EngineBuilder LogLevel(LogEventLevel parLogLevel)
|
||||
{
|
||||
_logLevel = parLogLevel;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Engine Build()
|
||||
{
|
||||
var logger = BuildLogger();
|
||||
var engine = new Engine(_width, _height, _headless, _title, logger);
|
||||
|
||||
var presenter = _presenterFunc?.Invoke(engine);
|
||||
if (presenter != null)
|
||||
{
|
||||
engine.Presenter = presenter;
|
||||
}
|
||||
|
||||
var inputHandler = _inputHandlerFunc?.Invoke(engine);
|
||||
if (inputHandler != null)
|
||||
{
|
||||
engine.InputHandler = inputHandler;
|
||||
}
|
||||
|
||||
return engine;
|
||||
}
|
||||
|
||||
private Logger BuildLogger()
|
||||
{
|
||||
const string template =
|
||||
"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u3}] [{ThreadName,-15:l}:{ThreadId,-4:d4}] [{SourceContext:l}] {Message:lj}{NewLine}{Exception}";
|
||||
|
||||
var loggerConfiguration = new LoggerConfiguration()
|
||||
.MinimumLevel.Is(_logLevel)
|
||||
.Enrich.WithThreadName()
|
||||
.Enrich.WithThreadId()
|
||||
.Enrich.FromLogContext();
|
||||
|
||||
if (_logToConsole)
|
||||
{
|
||||
loggerConfiguration.WriteTo.Console(
|
||||
outputTemplate: template,
|
||||
theme: AnsiConsoleTheme.Literate
|
||||
);
|
||||
}
|
||||
|
||||
if (_logToFile)
|
||||
{
|
||||
loggerConfiguration.WriteTo.File(
|
||||
_logFilePath!,
|
||||
outputTemplate: template
|
||||
);
|
||||
}
|
||||
|
||||
return loggerConfiguration.CreateLogger();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ namespace Engine.Graphics.Render.Quad;
|
||||
public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVertex>
|
||||
{
|
||||
private readonly TextureUnitMap _textureUnitMap = new(16);
|
||||
private readonly int[] _textureUnitIndices = new int[16];
|
||||
|
||||
public QuadRenderer(Renderer parRenderer, int parInstanceCount)
|
||||
: base(parRenderer, PrimitiveType.Triangles, parInstanceCount, [0, 1, 2, 2, 3, 0], [
|
||||
@@ -41,14 +42,13 @@ public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVert
|
||||
|
||||
protected override void SetAdditionalUniforms(Program parProgram)
|
||||
{
|
||||
var samplers = new int[_textureUnitMap.Size];
|
||||
foreach (var (texture, unit) in _textureUnitMap)
|
||||
foreach (var (texture, unit) in _textureUnitMap.Textures)
|
||||
{
|
||||
texture.BindUnit(unit);
|
||||
samplers[unit] = unit;
|
||||
_textureUnitIndices[unit] = unit;
|
||||
}
|
||||
|
||||
parProgram.SetUniform("uTexture", samplers);
|
||||
parProgram.SetUniform("uTexture", _textureUnitIndices);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
|
||||
@@ -8,15 +8,16 @@ public class Renderer
|
||||
{
|
||||
internal Texture.Texture TextureInternal => _framebuffer.TextureInternal!;
|
||||
|
||||
public readonly QuadRenderer QuadRenderer;
|
||||
public QuadRenderer QuadRenderer { get; }
|
||||
|
||||
private readonly Framebuffer.Framebuffer _framebuffer;
|
||||
private readonly Queue<Action<Renderer>> _renderActions = new();
|
||||
|
||||
private readonly Thread _renderThread;
|
||||
|
||||
public Renderer(int parWidth, int parHeight)
|
||||
{
|
||||
Thread.CurrentThread.Name = "RendererThread";
|
||||
|
||||
InitializeOpenGl(parWidth, parHeight);
|
||||
|
||||
_renderThread = Thread.CurrentThread;
|
||||
@@ -37,7 +38,7 @@ public class Renderer
|
||||
|
||||
GL.Enable(EnableCap.DepthTest);
|
||||
|
||||
GL.Enable(EnableCap.FramebufferSrgb);
|
||||
// GL.Enable(EnableCap.FramebufferSrgb);
|
||||
|
||||
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||||
GL.Enable(EnableCap.Blend);
|
||||
@@ -47,29 +48,27 @@ public class Renderer
|
||||
|
||||
public void EnsureRenderThread()
|
||||
{
|
||||
#if DEBUG
|
||||
if (Thread.CurrentThread == _renderThread)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Renderer is not on render thread");
|
||||
#endif
|
||||
}
|
||||
|
||||
internal void Commit(Action<Renderer> parRenderAction)
|
||||
{
|
||||
_renderActions.Enqueue(parRenderAction);
|
||||
}
|
||||
|
||||
internal void Render()
|
||||
internal void StartFrame()
|
||||
{
|
||||
EnsureRenderThread();
|
||||
|
||||
_framebuffer.Bind();
|
||||
while (_renderActions.TryDequeue(out var renderAction))
|
||||
{
|
||||
renderAction(this);
|
||||
}
|
||||
|
||||
internal void EndFrame()
|
||||
{
|
||||
EnsureRenderThread();
|
||||
|
||||
_framebuffer.Unbind();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,13 @@
|
||||
|
||||
namespace Engine.Graphics.Texture;
|
||||
|
||||
public class TextureUnitMap : IEnumerable<KeyValuePair<Texture, int>>
|
||||
public class TextureUnitMap(int parCapacity)
|
||||
{
|
||||
public int Size => _textures.Count;
|
||||
public int Capacity => _capacity;
|
||||
public int Capacity => parCapacity;
|
||||
public IReadOnlyDictionary<Texture, int> Textures => _textures;
|
||||
|
||||
private readonly Dictionary<Texture, int> _textures = new();
|
||||
private readonly int _capacity;
|
||||
|
||||
public TextureUnitMap(int parCapacity)
|
||||
{
|
||||
_capacity = parCapacity;
|
||||
}
|
||||
|
||||
public int GetUnit(Texture parTexture)
|
||||
{
|
||||
@@ -22,7 +17,7 @@ public class TextureUnitMap : IEnumerable<KeyValuePair<Texture, int>>
|
||||
return unit;
|
||||
}
|
||||
|
||||
if (_textures.Count >= _capacity)
|
||||
if (_textures.Count >= parCapacity)
|
||||
{
|
||||
throw new InvalidOperationException("Texture unit map is full");
|
||||
}
|
||||
@@ -37,14 +32,4 @@ public class TextureUnitMap : IEnumerable<KeyValuePair<Texture, int>>
|
||||
{
|
||||
_textures.Clear();
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<Texture, int>> GetEnumerator()
|
||||
{
|
||||
return _textures.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
|
||||
</xsd:element>
|
||||
@@ -13,10 +14,14 @@
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
<data name="Mesh" type="System.Resources.ResXFileRef" xml:space="preserve">
|
||||
|
||||
@@ -51,8 +51,6 @@ public class Window : IPresenter
|
||||
|
||||
public void Present(IConstTexture parTexture)
|
||||
{
|
||||
GL.Viewport(0, 0, Width, Height);
|
||||
|
||||
GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Engine\Engine.csproj" />
|
||||
<ProjectReference Include="..\Engine\Engine.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="src\" />
|
||||
<Folder Include="src\"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -9,7 +9,7 @@ public class HierarchyTests
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_hierarchy = new Hierarchy<object>();
|
||||
_hierarchy = [];
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -17,68 +17,106 @@ void main()
|
||||
|
||||
precision highp float;
|
||||
|
||||
// All components are in the range [0…1], including hue.
|
||||
vec3 rgb2hsv(vec3 c)
|
||||
{
|
||||
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
||||
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
|
||||
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
|
||||
|
||||
float d = q.x - min(q.w, q.y);
|
||||
float e = 1.0e-10;
|
||||
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
|
||||
}
|
||||
|
||||
// 16 Windows color palette (approximated)
|
||||
const vec3 windowsPalette[7] = vec3[](
|
||||
vec3(rgb2hsv(vec3(0.0, 0.0, 1.0)).rg, 0.0), // Blue (FOREGROUND_BLUE)
|
||||
vec3(rgb2hsv(vec3(0.0, 1.0, 0.0)).rg, 0.0), // Green (FOREGROUND_GREEN)
|
||||
vec3(rgb2hsv(vec3(0.0, 1.0, 1.0)).rg, 0.0), // Cyan (FOREGROUND_GREEN | FOREGROUND_BLUE)
|
||||
vec3(rgb2hsv(vec3(1.0, 0.0, 0.0)).rg, 0.0), // Red (FOREGROUND_RED)
|
||||
vec3(rgb2hsv(vec3(1.0, 0.0, 1.0)).rg, 0.0), // Magenta (FOREGROUND_RED | FOREGROUND_BLUE)
|
||||
vec3(rgb2hsv(vec3(1.0, 1.0, 0.0)).rg, 0.0), // Yellow (FOREGROUND_RED | FOREGROUND_GREEN)
|
||||
vec3(rgb2hsv(vec3(1.0, 1.0, 1.0)).rg, 0.0) // White (all colors set)
|
||||
// ConsoleColor vec3 palette (normalized)
|
||||
const vec3 ConsoleColorVec3[16] = vec3[](
|
||||
vec3(0.0, 0.0, 0.0), // Black
|
||||
vec3(0.0, 0.0, 0.5), // DarkBlue
|
||||
vec3(0.0, 0.5, 0.0), // DarkGreen
|
||||
vec3(0.0, 0.5, 0.5), // DarkCyan
|
||||
vec3(0.5, 0.0, 0.0), // DarkRed
|
||||
vec3(0.5, 0.0, 0.5), // DarkMagenta
|
||||
vec3(0.5, 0.5, 0.0), // DarkYellow
|
||||
vec3(0.5, 0.5, 0.5), // Gray
|
||||
vec3(0.25, 0.25, 0.25), // DarkGray
|
||||
vec3(0.0, 0.0, 1.0), // Blue
|
||||
vec3(0.0, 1.0, 0.0), // Green
|
||||
vec3(0.0, 1.0, 1.0), // Cyan
|
||||
vec3(1.0, 0.0, 0.0), // Red
|
||||
vec3(1.0, 0.0, 1.0), // Magenta
|
||||
vec3(1.0, 1.0, 0.0), // Yellow
|
||||
vec3(1.0, 1.0, 1.0) // White
|
||||
);
|
||||
|
||||
// Find the closest color in the Windows palette
|
||||
int findClosestColor(vec3 color) {
|
||||
int closestIndex = 0;
|
||||
float minDistance = distance(color, windowsPalette[0]);
|
||||
|
||||
for (int i = 1; i < 7; i++) {
|
||||
float dist = distance(color, windowsPalette[i]);
|
||||
if (dist < minDistance) {
|
||||
minDistance = dist;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return closestIndex + 1;
|
||||
vec3 f(vec3 x) {
|
||||
const float epsilon = 0.008856;
|
||||
const float k = 903.3;
|
||||
vec3 fx;
|
||||
fx.x = x.x > epsilon ? pow(x.x, 1.0 / 3.0) : (k * x.x + 16.0) / 116.0;
|
||||
fx.y = x.y > epsilon ? pow(x.y, 1.0 / 3.0) : (k * x.y + 16.0) / 116.0;
|
||||
fx.z = x.z > epsilon ? pow(x.z, 1.0 / 3.0) : (k * x.z + 16.0) / 116.0;
|
||||
return fx;
|
||||
}
|
||||
|
||||
float calculateLuminosity(vec3 color) {
|
||||
// Standard luminosity calculation
|
||||
float luminosity = dot(color, vec3(0.299, 0.587, 0.114));
|
||||
return clamp(luminosity, 0.0, 1.0);
|
||||
// Perceptually weighted color difference (CIEDE2000-inspired)
|
||||
float perceptualColorDistance(vec3 color1, vec3 color2) {
|
||||
// RGB to LAB conversion (simplified approximation)
|
||||
|
||||
vec3 lab1 = f(color1);
|
||||
vec3 lab2 = f(color2);
|
||||
|
||||
// Compute LAB-like color difference with perceptual weighting
|
||||
float deltaL = lab1.x - lab2.x;
|
||||
float deltaA = lab1.y - lab2.y;
|
||||
float deltaB = lab1.z - lab2.z;
|
||||
|
||||
// Perceptual weighting
|
||||
return deltaL * deltaL * 1.0 + // Lightness difference
|
||||
deltaA * deltaA * 1.5 + // Green-Red difference
|
||||
deltaB * deltaB * 1.5; // Blue-Yellow difference
|
||||
}
|
||||
|
||||
// Advanced color matching considering multiple color attributes
|
||||
int findMostPerceptuallyAccurateColor(vec3 color) {
|
||||
int bestMatchIndex = 0;
|
||||
float minDistance = perceptualColorDistance(color, ConsoleColorVec3[0]);
|
||||
|
||||
for (int i = 1; i < 16; i++) {
|
||||
float currentDistance = perceptualColorDistance(color, ConsoleColorVec3[i]);
|
||||
if (currentDistance < minDistance) {
|
||||
minDistance = currentDistance;
|
||||
bestMatchIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatchIndex;
|
||||
}
|
||||
|
||||
// Enhanced luminosity calculation considering human perception
|
||||
float calculatePerceptualLuminance(vec3 color) {
|
||||
// BT.709 luminance coefficients with slight adjustment
|
||||
return pow(
|
||||
0.2126 * pow(color.r, 2.2) +
|
||||
0.7152 * pow(color.g, 2.2) +
|
||||
0.0722 * pow(color.b, 2.2),
|
||||
1.0 / 2.2
|
||||
);
|
||||
}
|
||||
|
||||
// Dithering function to reduce color banding
|
||||
float interleavedGradientNoise(vec2 pixel) {
|
||||
return fract(52.982919 * fract(0.06711056 * pixel.x + 0.00583715 * pixel.y));
|
||||
}
|
||||
|
||||
uniform sampler2D uInputTexture;
|
||||
uniform vec2 uResolution;
|
||||
|
||||
layout (location = 0) in vec2 iUV;
|
||||
layout (location = 0) out vec4 FragColor;
|
||||
|
||||
void main() {
|
||||
vec3 pixelColor = texture(uInputTexture, iUV).rgb;
|
||||
vec3 pixelColorHsv = rgb2hsv(pixelColor);
|
||||
|
||||
// Find closest color index (4 bits)
|
||||
int colorIndex = findClosestColor(vec3(pixelColorHsv.rg, 0));
|
||||
// Find most perceptually accurate console color
|
||||
int colorIndex = findMostPerceptuallyAccurateColor(pixelColor);
|
||||
|
||||
// Calculate luminosity (4 bits)
|
||||
float luminosity = calculateLuminosity(pixelColor);
|
||||
// Calculate perceptual luminance with gamma correction
|
||||
float luminance = calculatePerceptualLuminance(pixelColor);
|
||||
|
||||
// Combine into a single byte-like value
|
||||
// High 4 bits: color index
|
||||
// Low 4 bits: luminosity
|
||||
FragColor = vec4(luminosity, colorIndex / 255.0, 0, 1);
|
||||
// Output with high precision color mapping
|
||||
FragColor = vec4(
|
||||
luminance, // Red: Perceptual luminance
|
||||
float(colorIndex) / 15.0, // Green: Normalized color index
|
||||
0.0, // Blue: Unused
|
||||
1.0 // Alpha
|
||||
);
|
||||
}
|
||||
@@ -11,8 +11,8 @@ public struct AsciiPixel : IPixel
|
||||
public PixelInternalFormat InternalFormat => PixelInternalFormat.Rg8;
|
||||
public SizedInternalFormat SizedInternalFormat => SizedInternalFormat.Rg8;
|
||||
|
||||
public byte LightnessIndex => R;
|
||||
public byte ColorIndex => G;
|
||||
public byte Luminance => R;
|
||||
public byte Color => G;
|
||||
|
||||
public byte R;
|
||||
public byte G;
|
||||
|
||||
@@ -1,158 +1,71 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace PresenterConsole;
|
||||
|
||||
public class ConsoleFastOutput
|
||||
public sealed class ConsoleFastOutput : IDisposable
|
||||
{
|
||||
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
static extern SafeFileHandle CreateFile(
|
||||
string fileName,
|
||||
[MarshalAs(UnmanagedType.U4)] uint fileAccess,
|
||||
[MarshalAs(UnmanagedType.U4)] uint fileShare,
|
||||
IntPtr securityAttributes,
|
||||
[MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
|
||||
[MarshalAs(UnmanagedType.U4)] int flags,
|
||||
IntPtr template);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern bool WriteConsoleOutput(
|
||||
SafeFileHandle hConsoleOutput,
|
||||
CharInfo[] lpBuffer,
|
||||
Coord dwBufferSize,
|
||||
Coord dwBufferCoord,
|
||||
ref SmallRect lpWriteRegion);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Coord
|
||||
{
|
||||
public short X;
|
||||
public short Y;
|
||||
|
||||
public Coord(short X, short Y)
|
||||
{
|
||||
this.X = X;
|
||||
this.Y = Y;
|
||||
}
|
||||
};
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public struct CharUnion
|
||||
{
|
||||
[FieldOffset(0)] public char UnicodeChar;
|
||||
[FieldOffset(0)] public byte AsciiChar;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public struct CharInfo
|
||||
{
|
||||
[FieldOffset(0)] public CharUnion Char;
|
||||
[FieldOffset(2)] public short Attributes;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct SmallRect
|
||||
{
|
||||
public short Left;
|
||||
public short Top;
|
||||
public short Right;
|
||||
public short Bottom;
|
||||
}
|
||||
private const uint GENERIC_WRITE = 0x40000000;
|
||||
private const int FILE_SHARE_WRITE = 0x2;
|
||||
|
||||
private readonly SafeFileHandle _handle;
|
||||
private CharInfo[] _buffer;
|
||||
private SmallRect _region;
|
||||
private readonly WindowsFFI.Coord _bufferCoord = new(0, 0);
|
||||
|
||||
private int _width;
|
||||
private int _height;
|
||||
private WindowsFFI.CharInfo[] _buffer;
|
||||
private WindowsFFI.Coord _bufferSize;
|
||||
|
||||
|
||||
public ConsoleFastOutput(int parWidth, int parHeight)
|
||||
{
|
||||
_width = parWidth;
|
||||
_height = parHeight;
|
||||
_handle = CreateFile("CONOUT$", 0x40000000, 2, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parWidth);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parHeight);
|
||||
|
||||
_handle = WindowsFFI.CreateFile("CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE,
|
||||
IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);
|
||||
|
||||
if (_handle.IsInvalid)
|
||||
{
|
||||
throw new Exception("Failed to open console");
|
||||
throw new InvalidOperationException("Failed to open console handle");
|
||||
|
||||
Resize(parWidth, parHeight);
|
||||
}
|
||||
|
||||
_buffer = new CharInfo[parWidth * parHeight];
|
||||
_region = new SmallRect { Left = 0, Top = 0, Right = (short)_width, Bottom = (short)_height };
|
||||
}
|
||||
|
||||
public void WriteChar(char parChar, short parX, short parY, ushort parForeground, ushort parBackground)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteChar(char parCharacter, int parX, int parY, ConsoleColor parForeground, ConsoleColor parBackground)
|
||||
{
|
||||
var index = parX + parY * _width;
|
||||
if (index >= _buffer.Length)
|
||||
if (parX < 0 || parX >= _width || parY < 0 || parY >= _height)
|
||||
return;
|
||||
|
||||
_buffer[parX + parY * _width].Char.UnicodeChar = parChar;
|
||||
_buffer[parX + parY * _width].Attributes = (short)(parForeground | (parBackground << 4));
|
||||
var index = parX + parY * _width;
|
||||
ref var charInfo = ref _buffer[index];
|
||||
charInfo.Char.UnicodeChar = parCharacter;
|
||||
charInfo.Attributes = (short)((int)parForeground | ((int)parBackground << 4));
|
||||
}
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
var regions = SplitRegion(_region, 1, 1);
|
||||
for (var i = 0; i < regions.Length; i++)
|
||||
{
|
||||
var region = regions[i];
|
||||
var bufferSize = new Coord(region.Right, region.Bottom);
|
||||
var bufferCoord = new Coord(region.Left, region.Top);
|
||||
WriteConsoleOutput(_handle, _buffer, bufferSize, bufferCoord, ref region);
|
||||
}
|
||||
var writeRegion = new WindowsFFI.SmallRect { Left = 0, Top = 0, Right = (short)_width, Bottom = (short)_height };
|
||||
|
||||
// var bufferSize = new Coord(_region.Right, _region.Bottom);
|
||||
// var bufferCoord = new Coord(0, 0);
|
||||
// WriteConsoleOutput(_handle, _buffer, bufferSize, bufferCoord, ref _region);
|
||||
//
|
||||
// _region.Left = 0;
|
||||
// _region.Top = 0;
|
||||
// _region.Right = (short)_width;
|
||||
// _region.Bottom = (short)_height;
|
||||
|
||||
// run in parallel
|
||||
// Parallel.ForEach(regions, region =>
|
||||
// {
|
||||
// var bufferSize = new Coord(_region.Right, _region.Bottom);
|
||||
// var bufferCoord = new Coord(region.Left, region.Top);
|
||||
// WriteConsoleOutput(_handle, _buffer, bufferSize, bufferCoord, ref region);
|
||||
// });
|
||||
}
|
||||
|
||||
private SmallRect[] SplitRegion(SmallRect parRegion, int countX, int countY)
|
||||
{
|
||||
var regions = new SmallRect[countX * countY];
|
||||
for (var y = 0; y < countY; y++)
|
||||
{
|
||||
for (var x = 0; x < countX; x++)
|
||||
{
|
||||
var region = parRegion;
|
||||
region.Left = (short)(parRegion.Left + x * parRegion.Right / countX);
|
||||
region.Right = (short)(parRegion.Left + (x + 1) * parRegion.Right / countX);
|
||||
region.Top = (short)(parRegion.Top + y * parRegion.Bottom / countY);
|
||||
region.Bottom = (short)(parRegion.Top + (y + 1) * parRegion.Bottom / countY);
|
||||
regions[y * countX + x] = region;
|
||||
}
|
||||
}
|
||||
|
||||
return regions;
|
||||
WindowsFFI.WriteConsoleOutput(_handle, _buffer, _bufferSize, _bufferCoord, ref writeRegion);
|
||||
}
|
||||
|
||||
public void Resize(int parWidth, int parHeight)
|
||||
{
|
||||
if (parWidth <= 0 || parHeight <= 0)
|
||||
{
|
||||
throw new ArgumentException("Width and height must be greater than 0");
|
||||
}
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parWidth);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parHeight);
|
||||
|
||||
if (parWidth == _width && parHeight == _height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_width = parWidth;
|
||||
_height = parHeight;
|
||||
_buffer = new WindowsFFI.CharInfo[parWidth * parHeight];
|
||||
_bufferSize = new WindowsFFI.Coord((short)_width, (short)_height);
|
||||
}
|
||||
|
||||
_buffer = new CharInfo[parWidth * parHeight];
|
||||
_region = new SmallRect { Left = 0, Top = 0, Right = (short)parWidth, Bottom = (short)parHeight };
|
||||
public void Dispose()
|
||||
{
|
||||
_handle.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ public class ConsolePresenter : IPresenter
|
||||
private readonly VertexArray _vertexArray;
|
||||
|
||||
private readonly ConsoleFastOutput _consoleOutput;
|
||||
private static readonly char[] LIGHTMAP = " .:-+=#%@".Reverse().ToArray();
|
||||
|
||||
public ConsolePresenter(Engine.Engine parEngine)
|
||||
{
|
||||
@@ -41,17 +42,18 @@ public class ConsolePresenter : IPresenter
|
||||
.AddColorAttachment<AsciiPixel>()
|
||||
.Build();
|
||||
|
||||
_indexBuffer = new IndexBuffer([0, 1, 2, 2, 3, 0], BufferStorageFlags.MapReadBit);
|
||||
_indexBuffer = new IndexBuffer([0, 1, 2, 2, 3, 0]);
|
||||
_vertexBuffer = new VertexBuffer<AsciiVertex>([
|
||||
new AsciiVertex { _position = new Vector2(-1.0f, -1.0f), _uv = new Vector2(0.0f, 0.0f) },
|
||||
new AsciiVertex { _position = new Vector2(1.0f, -1.0f), _uv = new Vector2(1.0f, 0.0f) },
|
||||
new AsciiVertex { _position = new Vector2(1.0f, 1.0f), _uv = new Vector2(1.0f, 1.0f) },
|
||||
new AsciiVertex { _position = new Vector2(-1.0f, 1.0f), _uv = new Vector2(0.0f, 1.0f) }
|
||||
], BufferStorageFlags.MapReadBit);
|
||||
]);
|
||||
_vertexArray = new VertexArray();
|
||||
_vertexArray.BindIndexBuffer(_indexBuffer);
|
||||
_vertexArray.BindVertexBuffer(_vertexBuffer);
|
||||
|
||||
Console.CursorVisible = false;
|
||||
_consoleOutput = new ConsoleFastOutput(Width, Height);
|
||||
}
|
||||
|
||||
@@ -87,15 +89,15 @@ public class ConsolePresenter : IPresenter
|
||||
|
||||
private void Output(Image<AsciiPixel> parImage)
|
||||
{
|
||||
var lightmap = " .-:=+*#%@".Reverse().ToArray();
|
||||
for (var y = 0; y < parImage.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < parImage.Width; x++)
|
||||
{
|
||||
var pixel = parImage[y, x];
|
||||
var lightnessIndex = (int)(pixel.LightnessIndex / 255.0f * (lightmap.Length - 1));
|
||||
_consoleOutput.WriteChar(lightmap[lightnessIndex], (short)x, (short)y, 0,
|
||||
pixel.ColorIndex);
|
||||
var lightnessIndex = (int)(pixel.Luminance / 255.0f * (LIGHTMAP.Length - 1));
|
||||
var colorIndex = (int)(pixel.Color / 255.0f * 15.0f);
|
||||
_consoleOutput.WriteChar(LIGHTMAP[lightnessIndex], (short)x, (short)y, 0,
|
||||
(ConsoleColor)colorIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,8 +111,9 @@ public class ConsolePresenter : IPresenter
|
||||
_cachedWidth = Width;
|
||||
_cachedHeight = Height;
|
||||
|
||||
Resize?.Invoke(new ResizeEventArgs(Width, Height));
|
||||
|
||||
_framebuffer.Resize(Width, Height);
|
||||
_engine.Renderer.Resize(Width, Height);
|
||||
_consoleOutput.Resize(Width, Height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
// See https://aka.ms/new-console-template for more information
|
||||
|
||||
using PresenterConsole;
|
||||
using Serilog.Events;
|
||||
|
||||
internal class Program
|
||||
namespace PresenterConsole;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
public static void Main(string[] parArgs)
|
||||
{
|
||||
var engine = new Engine.Engine(240, 135, (parEngine) => new ConsolePresenter(parEngine));
|
||||
var engine = new Engine.EngineBuilder()
|
||||
.Headless()
|
||||
.LogToFile(true, "log.txt")
|
||||
.LogLevel(LogEventLevel.Debug)
|
||||
.Presenter(parEngine => new ConsolePresenter(parEngine))
|
||||
.Build();
|
||||
|
||||
engine.Run();
|
||||
}
|
||||
}
|
||||
63
PresenterConsole/src/WindowsFFI.cs
Normal file
63
PresenterConsole/src/WindowsFFI.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.Marshalling;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
[assembly: DisableRuntimeMarshalling]
|
||||
|
||||
namespace PresenterConsole;
|
||||
|
||||
public static partial class WindowsFFI
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Coord(short parX, short parY)
|
||||
{
|
||||
public short X = parX;
|
||||
public short Y = parY;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public struct CharUnion
|
||||
{
|
||||
[FieldOffset(0)] public char UnicodeChar;
|
||||
[FieldOffset(0)] public byte AsciiChar;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public struct CharInfo
|
||||
{
|
||||
[FieldOffset(0)] public CharUnion Char;
|
||||
[FieldOffset(2)] public short Attributes;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct SmallRect
|
||||
{
|
||||
public short Left;
|
||||
public short Top;
|
||||
public short Right;
|
||||
public short Bottom;
|
||||
}
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "CreateFileW")]
|
||||
public static partial SafeFileHandle CreateFile(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string parFileName,
|
||||
uint parFileAccess,
|
||||
uint parFileShare,
|
||||
IntPtr parSecurityAttributes,
|
||||
FileMode parCreationDisposition,
|
||||
int parFlags,
|
||||
IntPtr parTemplate
|
||||
);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "WriteConsoleOutputW")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static partial bool WriteConsoleOutput(
|
||||
SafeFileHandle parHConsoleOutput,
|
||||
[MarshalUsing(CountElementName = nameof(parDwBufferSize))]
|
||||
CharInfo[] parLpBuffer,
|
||||
Coord parDwBufferSize,
|
||||
Coord parDwBufferCoord,
|
||||
ref SmallRect parLpWriteRegion
|
||||
);
|
||||
}
|
||||
14
PresenterNative/PresenterNative.csproj
Normal file
14
PresenterNative/PresenterNative.csproj
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Engine\Engine.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
23
PresenterNative/src/Program.cs
Normal file
23
PresenterNative/src/Program.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Engine;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace PresenterNative;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
public static void Main(string[] parArgs)
|
||||
{
|
||||
var engine = new EngineBuilder()
|
||||
.Headless(false)
|
||||
.Width(1280)
|
||||
.Height(720)
|
||||
.Title("Doom Deathmatch")
|
||||
.LogToConsole()
|
||||
.LogToFile(true, "log.txt")
|
||||
.LogLevel(LogEventLevel.Debug)
|
||||
.Presenter(parEngine => parEngine.Window)
|
||||
.Build();
|
||||
|
||||
engine.Run();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
using Engine;
|
||||
using Engine.Graphics;
|
||||
using Engine.Graphics.Texture;
|
||||
using OpenTK.Windowing.Common;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace PresenterWpf;
|
||||
|
||||
@@ -13,10 +15,16 @@ namespace PresenterWpf;
|
||||
public partial class App : Application
|
||||
{
|
||||
// Hijack the default startup event to start the engine
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
protected override void OnStartup(StartupEventArgs parEventArgs)
|
||||
{
|
||||
var presenter = new PresenterWrapper();
|
||||
var engine = new Engine.Engine(1, 1, _ => presenter);
|
||||
var engine = new EngineBuilder()
|
||||
.Headless()
|
||||
.Presenter(_ => presenter)
|
||||
.LogToConsole()
|
||||
.LogToFile(true, "log.txt")
|
||||
.LogLevel(LogEventLevel.Debug)
|
||||
.Build();
|
||||
|
||||
// Since engine claims current thread for rendering, we need to create a new thread to run WPF
|
||||
var thread = new Thread(() =>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Height="450" Width="800" Closing="MainWindow_OnClosing">
|
||||
<Grid>
|
||||
<Image x:Name="Image" Width="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=ActualWidth}"
|
||||
Height="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=ActualHeight}" />
|
||||
<Image x:Name="Image" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stretch="Fill" />
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,42 +1,29 @@
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Threading;
|
||||
using Engine.Asset;
|
||||
using Engine.Graphics;
|
||||
using Engine.Graphics.Pixel;
|
||||
using Engine.Graphics.Texture;
|
||||
using OpenTK.Windowing.Common;
|
||||
using CancelEventArgs = System.ComponentModel.CancelEventArgs;
|
||||
|
||||
namespace PresenterWpf;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window, IPresenter
|
||||
{
|
||||
public bool IsExiting { get; set; }
|
||||
public int Width => (int)_cachedWidth;
|
||||
public int Height => (int)_cachedHeight;
|
||||
public bool IsExiting { get; private set; }
|
||||
public new int Width { get; private set; }
|
||||
public new int Height { get; private set; }
|
||||
|
||||
private bool _scheduledResize;
|
||||
|
||||
public event Action<ResizeEventArgs>? Resize;
|
||||
|
||||
|
||||
private Engine.Engine _engine;
|
||||
private readonly Engine.Engine _engine;
|
||||
private Image<Rgb8>? _image;
|
||||
private WriteableBitmap? _bitmap;
|
||||
|
||||
private int _cachedWidth;
|
||||
private int _cachedHeight;
|
||||
|
||||
public MainWindow(Engine.Engine parEngine)
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -48,16 +35,21 @@ public partial class MainWindow : Window, IPresenter
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
if ((int)Image.Width != _cachedWidth || (int)Image.Height != _cachedHeight)
|
||||
if ((int)Image.ActualWidth != Width || (int)Image.ActualHeight != Height)
|
||||
{
|
||||
_cachedWidth = (int)Image.Width;
|
||||
_cachedHeight = (int)Image.Height;
|
||||
Width = (int)Image.ActualWidth;
|
||||
Height = (int)Image.ActualHeight;
|
||||
|
||||
_scheduledResize = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Resizes are lazy so resizing only happens when the window's size actually changes
|
||||
if (Width != 0 && Height != 0)
|
||||
_engine.Renderer.Resize(Width, Height);
|
||||
if (_scheduledResize)
|
||||
{
|
||||
_scheduledResize = false;
|
||||
Resize?.Invoke(new ResizeEventArgs(Width, Height));
|
||||
}
|
||||
}
|
||||
|
||||
public void Present(IConstTexture parTexture)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace PresenterWpf;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user