This commit is contained in:
2025-01-02 05:15:16 +03:00
parent 3c66a65b40
commit ac00eb18a9
96 changed files with 384134 additions and 230 deletions

View File

@@ -10,4 +10,8 @@
<ProjectReference Include="..\Engine\Engine.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="src\Scene\Play\" />
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
# Blender 4.2.3 LTS
# www.blender.org
o Cube
v -1.000000 -1.000000 1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
vn -1.0000 -0.0000 -0.0000
vn -0.0000 -0.0000 -1.0000
vn 1.0000 -0.0000 -0.0000
vn -0.0000 -0.0000 1.0000
vn -0.0000 -1.0000 -0.0000
vn -0.0000 1.0000 -0.0000
vt 0.625000 0.000000
vt 0.375000 0.250000
vt 0.375000 0.000000
vt 0.625000 0.250000
vt 0.375000 0.500000
vt 0.625000 0.500000
vt 0.375000 0.750000
vt 0.625000 0.750000
vt 0.375000 1.000000
vt 0.125000 0.750000
vt 0.125000 0.500000
vt 0.875000 0.500000
vt 0.625000 1.000000
vt 0.875000 0.750000
s 0
f 2/1/1 3/2/1 1/3/1
f 4/4/2 7/5/2 3/2/2
f 8/6/3 5/7/3 7/5/3
f 6/8/4 1/9/4 5/7/4
f 7/5/5 1/10/5 3/11/5
f 4/12/6 6/8/6 8/6/6
f 2/1/1 4/4/1 3/2/1
f 4/4/2 8/6/2 7/5/2
f 8/6/3 6/8/3 5/7/3
f 6/8/4 2/13/4 1/9/4
f 7/5/5 5/7/5 1/10/5
f 4/12/6 2/14/6 6/8/6

View File

@@ -0,0 +1,38 @@
# Blender 4.2.3 LTS
# www.blender.org
o Cube
v -1.000000 -1.000000 1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
vt 0.625000 0.000000
vt 0.375000 0.250000
vt 0.375000 0.000000
vt 0.625000 0.250000
vt 0.375000 0.500000
vt 0.625000 0.500000
vt 0.375000 0.750000
vt 0.625000 0.750000
vt 0.375000 1.000000
vt 0.125000 0.750000
vt 0.125000 0.500000
vt 0.875000 0.500000
vt 0.625000 1.000000
vt 0.875000 0.750000
s 0
f 2/1 3/2 1/3
f 4/4 7/5 3/2
f 8/6 5/7 7/5
f 6/8 1/9 5/7
f 7/5 1/10 3/11
f 4/12 6/8 8/6
f 2/1 4/4 3/2
f 4/4 8/6 7/5
f 8/6 6/8 5/7
f 6/8 2/13 1/9
f 7/5 5/7 1/10
f 4/12 2/14 6/8

View File

@@ -0,0 +1,24 @@
# Blender 4.2.3 LTS
# www.blender.org
o Cube
v -1.000000 -1.000000 1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
s 0
f 2 3 1
f 4 7 3
f 8 5 7
f 6 1 5
f 7 1 3
f 4 6 8
f 2 4 3
f 4 8 7
f 8 6 5
f 6 2 1
f 7 5 1
f 4 2 6

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -0,0 +1,41 @@
using Engine.Scene.Component.BuiltIn;
using OpenTK.Mathematics;
namespace DoomDeathmatch.Component;
public class BillboardComponent : Engine.Scene.Component.Component
{
public Transform? Target { get; set; }
public Vector3 Up { get; set; } = Vector3.UnitZ;
public override void Update(double parDeltaTime)
{
if (Target == null)
{
return;
}
var targetPosition = Target.TransformMatrix.ExtractTranslation();
var currentPosition = GameObject.Transform.Translation;
var forward = targetPosition - currentPosition;
if (forward.LengthSquared > 0)
forward.Normalize();
var right = Vector3.Cross(Up, forward);
if (right.LengthSquared > 0)
right.Normalize();
var recalculatedUp = Vector3.Cross(forward, right);
var rotationMatrix = new Matrix3(
right.X, recalculatedUp.X, forward.X,
right.Y, recalculatedUp.Y, forward.Y,
right.Z, recalculatedUp.Z, forward.Z
);
var rotation = Quaternion.FromMatrix(rotationMatrix);
GameObject.Transform.Rotation = rotation;
}
}

View File

@@ -6,28 +6,51 @@ namespace DoomDeathmatch.Component;
public class ControllerComponent : Engine.Scene.Component.Component
{
public float Speed { get; set; } = 10.0f;
public float RotationSpeed { get; set; } = 70.0f;
private readonly IInputHandler _inputHandler = Engine.Engine.Instance.InputHandler!;
private RigidbodyComponent _rigidbody;
public override void Awake()
{
_rigidbody = GameObject.GetComponent<RigidbodyComponent>()!;
}
public override void Update(double parDeltaTime)
{
var movement = Vector3.Zero;
var rotation = 0.0f;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.W))
movement.Z += 1;
movement.Y += 1;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.S))
movement.Z -= 1;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.A))
movement.X -= 1;
movement.Y -= 1;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.D))
movement.X += 1;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.Space))
movement.Y += 1;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.Shift))
movement.Y -= 1;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.A))
movement.X -= 1;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.Q))
rotation += RotationSpeed;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.E))
rotation -= RotationSpeed;
if (movement.LengthSquared > 0)
{
movement.Normalize();
movement = GameObject.Transform.Rotation * movement;
GameObject.Transform.Translation += movement * Speed * (float)parDeltaTime;
_rigidbody.AddVelocity(Speed * movement);
}
var velocityXy = _rigidbody.Velocity.Xy;
if (velocityXy.LengthSquared > Speed * Speed)
{
var length = velocityXy.Length;
_rigidbody.AddVelocity(new Vector3(-(length - Speed) / length * velocityXy));
}
GameObject.Transform.Rotation *= Quaternion.FromAxisAngle(Vector3.UnitZ, MathHelper.DegreesToRadians(rotation) *
(float)parDeltaTime);
}
}

View File

@@ -0,0 +1,21 @@
using OpenTK.Mathematics;
namespace DoomDeathmatch.Component;
public class DragComponent : Engine.Scene.Component.Component
{
public float Drag { get; set; } = 1f;
public Vector3 Coefficient { get; set; } = Vector3.One;
private RigidbodyComponent _rigidbody;
public override void Awake()
{
_rigidbody = GameObject.GetComponent<RigidbodyComponent>()!;
}
public override void Update(double parDeltaTime)
{
_rigidbody.AddForce(-Drag * (_rigidbody.Velocity * Coefficient));
}
}

View File

@@ -0,0 +1,44 @@
using OpenTK.Mathematics;
namespace DoomDeathmatch.Component;
public class GravityComponent : Engine.Scene.Component.Component
{
public float Strength { get; set; } = 10.0f;
public Vector3 Direction
{
get => _direction;
set => _direction = value.Normalized();
}
public float Floor { get; set; } = 5.0f;
public bool IsInAir { get; private set; } = false;
private RigidbodyComponent _rigidbody;
private Vector3 _direction = -Vector3.UnitZ;
public override void Awake()
{
_rigidbody = GameObject.GetComponent<RigidbodyComponent>()!;
}
public override void Update(double parDeltaTime)
{
var heightAlongDirection = Vector3.Dot(GameObject.Transform.Translation, Direction);
if (heightAlongDirection > Floor)
{
IsInAir = false;
var velocityAlongDirection = Vector3.Dot(_rigidbody.Velocity, Direction);
if (velocityAlongDirection > 0)
_rigidbody.AddVelocity(-velocityAlongDirection * Direction);
return;
}
IsInAir = true;
var gravity = Strength * Direction;
_rigidbody.AddForce(gravity);
}
}

View File

@@ -0,0 +1,33 @@
namespace DoomDeathmatch.Component;
public class HealthComponent : Engine.Scene.Component.Component
{
public float MaxHealth { get; set; } = 100;
public float Health { get; private set; } = 100;
public event Action<HealthComponent>? HealthChanged;
public event Action<HealthComponent>? Died;
public void TakeDamage(float parDamage)
{
Health -= parDamage;
if (Health <= 0)
{
Died?.Invoke(this);
}
else
{
HealthChanged?.Invoke(this);
}
}
public void Heal(float parHeal)
{
Health += parHeal;
if (Health > MaxHealth)
{
Health = MaxHealth;
}
HealthChanged?.Invoke(this);
}
}

View File

@@ -0,0 +1,39 @@
using OpenTK.Mathematics;
namespace DoomDeathmatch.Component;
public class RigidbodyComponent : Engine.Scene.Component.Component
{
public Vector3 Velocity { get; private set; } = Vector3.Zero;
public Vector3 Acceleration { get; private set; } = Vector3.Zero;
public Vector3 Force { get; private set; } = Vector3.Zero;
public float Mass { get; set; } = 1.0f;
public bool IsStatic { get; set; } = false;
public void AddForce(Vector3 parForce)
{
if (IsStatic)
return;
Force += parForce;
}
public void AddVelocity(Vector3 parVelocity)
{
if (IsStatic)
return;
Velocity += parVelocity;
}
public override void Update(double parDeltaTime)
{
if (IsStatic)
return;
Acceleration = Force / Mass;
Velocity += Acceleration * (float)parDeltaTime;
GameObject.Transform.Translation += Velocity * (float)parDeltaTime;
Force = Vector3.Zero;
}
}

View File

@@ -0,0 +1,14 @@
namespace DoomDeathmatch.Component.UI;
public enum Anchor
{
TopLeft,
TopCenter,
TopRight,
CenterLeft,
Center,
CenterRight,
BottomLeft,
BottomCenter,
BottomRight
}

View File

@@ -0,0 +1,7 @@
namespace DoomDeathmatch.Component.UI;
public enum Orientation
{
Horizontal,
Vertical
}

View File

@@ -0,0 +1,88 @@
using Engine.Input;
namespace DoomDeathmatch.Component.UI;
public class SelectorComponent : Engine.Scene.Component.Component
{
public List<UiComponent> Children => _children;
public KeyboardButtonCode SelectKey { get; set; } = KeyboardButtonCode.Space;
public KeyboardButtonCode NextKey { get; set; } = KeyboardButtonCode.Down;
public KeyboardButtonCode PrevKey { get; set; } = KeyboardButtonCode.Up;
public event Action<UiComponent>? OnSelect;
private readonly IInputHandler _inputHandler = Engine.Engine.Instance.InputHandler!;
private readonly List<UiComponent> _children = [];
private int _selectedIndex = 0;
public override void Start()
{
foreach (var child in Children)
{
child.OnMouseOver += Select;
}
SelectionChanged();
}
public override void Update(double parDeltaTime)
{
if (_inputHandler.IsKeyJustPressed(SelectKey))
_children[_selectedIndex].InvokeClick();
if (_inputHandler.IsKeyJustPressed(NextKey))
{
_selectedIndex++;
if (_selectedIndex >= Children.Count)
_selectedIndex = 0;
SelectionChanged();
}
if (_inputHandler.IsKeyJustPressed(PrevKey))
{
_selectedIndex--;
if (_selectedIndex < 0)
_selectedIndex = Children.Count - 1;
SelectionChanged();
}
UpdatePosition();
}
private void SelectionChanged()
{
OnSelect?.Invoke(Children[_selectedIndex]);
}
private void UpdatePosition()
{
var child = Children[_selectedIndex];
var transformMatrix = child.GameObject.Transform.FullTransformMatrix;
var translation = transformMatrix.ExtractTranslation();
var scale = transformMatrix.ExtractScale();
GameObject.Transform.Translation = translation;
GameObject.Transform.Translation.X -= scale.X / 2 + 0.05f;
GameObject.Transform.Size.Y = scale.Y;
}
private void Select(UiComponent parComponent)
{
var index = Children.IndexOf(parComponent);
if (index < 0)
index = 0;
if (index == _selectedIndex)
return;
_selectedIndex = index;
SelectionChanged();
}
}

View File

@@ -0,0 +1,32 @@
using Engine.Scene;
using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.UI;
public class StackComponent : UiContainerComponent
{
public List<UiComponent> Children => _children;
public Orientation Orientation { get; set; } = Orientation.Vertical;
private readonly List<UiComponent> _children = [];
public override void Update(double parDeltaTime)
{
base.Update(parDeltaTime);
var size = GameObject.Transform.Size.Xy;
var count = _children.Count;
size *= Orientation == Orientation.Horizontal ? new Vector2(1, 0) : new Vector2(0, 1);
var offset = new Vector2(-size.X / 2 + size.X / count / 2, -size.Y / 2 + size.Y / count / 2);
for (var i = count - 1; i >= 0; i--)
{
var child = _children[i];
child.Offset = offset;
offset += size / count;
}
}
}

View File

@@ -0,0 +1,20 @@
using Engine.Input;
using Engine.Scene.Component.BuiltIn;
namespace DoomDeathmatch.Component.UI;
public class TestComponent : Engine.Scene.Component.Component
{
private readonly IInputHandler _inputHandler = Engine.Engine.Instance.InputHandler!;
public Camera? Camera { get; set; }
public override void Update(double parDeltaTime)
{
if (Camera == null)
{
return;
}
GameObject.Transform.Size.Xy = 2 * Camera.ScreenToWorld(_inputHandler.MousePosition).Xy;
}
}

View File

@@ -0,0 +1,54 @@
using Engine.Scene.Component.BuiltIn.Renderer;
using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.UI;
public class TextAlignComponent : Engine.Scene.Component.Component
{
public Align Alignment { get; set; } = Align.Left;
private TextRenderer _textRenderer = null!;
private string? _cachedText;
public override void Awake()
{
_textRenderer = GameObject.GetComponent<TextRenderer>()!;
}
public override void Update(double parDeltaTime)
{
if (_textRenderer.Text == null)
return;
if (_cachedText == _textRenderer.Text)
return;
_cachedText = _textRenderer.Text;
var font = _textRenderer.Font;
var size = font.Measure(_textRenderer.Text);
var scale = GameObject.Transform.FullTransformMatrix.ExtractScale();
var offset = GetOffset(size) + new Vector2(0, font.Metadata.Metrics.LineHeight - font.Metadata.Metrics.Ascender) / 2;
offset *= scale.Xy;
GameObject.Transform.Translation.Xy = offset;
}
public Vector2 GetOffset(Vector2 parSize)
{
return Alignment switch
{
Align.Left => new Vector2(0, -parSize.Y / 2),
Align.Center => new Vector2(-parSize.X / 2, -parSize.Y / 2),
Align.Right => new Vector2(-parSize.X, -parSize.Y / 2),
_ => throw new ArgumentOutOfRangeException(nameof(Alignment), Alignment, null)
};
}
public enum Align
{
Left,
Center,
Right
}
}

View File

@@ -0,0 +1,72 @@
using Engine.Input;
using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.UI;
public class UiComponent : Engine.Scene.Component.Component
{
public UiContainerComponent? Container { get; set; }
public Anchor Anchor { get; set; } = Anchor.Center;
public Vector2 Offset { get; set; } = Vector2.Zero;
public Vector2 Center { get; set; } = Vector2.Zero;
public event Action<UiComponent>? OnClick;
public event Action<UiComponent>? OnMouseOver;
private readonly IInputHandler _inputHandler = Engine.Engine.Instance.InputHandler!;
public override void Update(double parDeltaTime)
{
if (Container == null)
{
return;
}
GameObject.Transform.Translation.Xy = GetAnchorPosition(Container.GameObject.Transform.Size.Xy) + Offset;
var transformMatrix = GameObject.Transform.FullTransformMatrix;
var translation = transformMatrix.ExtractTranslation();
var scale = transformMatrix.ExtractScale();
var relativeMousePosition = Container.MousePosition.Xy -
(translation.Xy);
var objectSize = scale.Xy;
if (Math.Abs(relativeMousePosition.X) <= objectSize.X / 2 && Math.Abs(relativeMousePosition.Y) <= objectSize.Y / 2)
{
OnMouseOver?.Invoke(this);
if (_inputHandler.IsMouseButtonJustPressed(MouseButtonCode.Left))
{
OnClick?.Invoke(this);
}
}
}
public void InvokeClick()
{
OnClick?.Invoke(this);
}
private Vector2 GetAnchorPosition(Vector2 parSize)
{
return parSize * GetAnchorRatio();
}
private Vector2 GetAnchorRatio()
{
return Anchor switch
{
Anchor.TopLeft => new Vector2(-0.5f, 0.5f),
Anchor.TopCenter => new Vector2(0, 0.5f),
Anchor.TopRight => new Vector2(0.5f, 0.5f),
Anchor.CenterLeft => new Vector2(-0.5f, 0),
Anchor.Center => new Vector2(0, 0),
Anchor.CenterRight => new Vector2(0.5f, 0),
Anchor.BottomLeft => new Vector2(-0.5f, -0.5f),
Anchor.BottomCenter => new Vector2(0, -0.5f),
Anchor.BottomRight => new Vector2(0.5f, -0.5f),
_ => throw new ArgumentOutOfRangeException(nameof(Anchor), Anchor, null)
};
}
}

View File

@@ -0,0 +1,28 @@
using Engine.Input;
using Engine.Scene.Component.BuiltIn;
using OpenTK.Mathematics;
namespace DoomDeathmatch.Component.UI;
public class UiContainerComponent : UiComponent
{
private readonly IInputHandler _inputHandler = Engine.Engine.Instance.InputHandler!;
public Camera? Camera { get; set; }
public Vector3 MousePosition { get; private set; }
public override void Update(double parDeltaTime)
{
base.Update(parDeltaTime);
if (Camera != null)
{
GameObject.Transform.Size.Xy = Camera.GameObject.Transform.Size.Xy;
MousePosition = Camera.ScreenToWorld(_inputHandler.MousePosition);
}
else if (Container != null)
{
MousePosition = Container.MousePosition;
}
}
}

View File

@@ -1,5 +1,15 @@
using DoomDeathmatch.Component;
using System.Text.Json;
using DoomDeathmatch.Component;
using DoomDeathmatch.Component.UI;
using DoomDeathmatch.Scene.Main;
using DoomDeathmatch.Scene.Rules;
using Engine.Asset.Font;
using Engine.Asset.Font.Metadata;
using Engine.Asset.Mesh;
using Engine.Asset.Mesh.Loader;
using Engine.Graphics.Camera;
using Engine.Graphics.Pipeline;
using Engine.Graphics.Texture;
using Engine.Scene;
using Engine.Scene.Component.BuiltIn;
using Engine.Scene.Component.BuiltIn.Renderer;
@@ -11,32 +21,70 @@ public static class DoomDeathmatch
{
public static void Initialize(Engine.Engine parEngine)
{
parEngine.SceneManager.TransitionTo(MainScene());
parEngine.SceneManager.TransitionTo(MainScene.Create(parEngine));
}
private static Scene MainScene()
{
var cameraObject = new GameObject();
cameraObject.Transform.Translation.Z = -6;
cameraObject.AddComponent<PerspectiveCamera>();
cameraObject.AddComponent<ControllerComponent>();
using var reader = new StreamReader("../DoomDeathmatch/asset/model/test2.obj");
var mesh = ObjMeshLoader.Load(reader);
var box2dRenderer = new GameObject();
box2dRenderer.AddComponent(new MeshRenderer { Mesh = mesh });
box2dRenderer.AddComponent<RotateComponent>();
var testChild = new GameObject();
testChild.Transform.Scale /= 4;
testChild.AddComponent(new MeshRenderer { Mesh = mesh });
var scene = new Scene();
scene.Add(cameraObject);
scene.Add(box2dRenderer);
scene.AddChild(box2dRenderer, testChild);
return scene;
}
// private static Scene MainScene(Engine.Engine parEngine)
// {
// var playerObject = new GameObject();
// playerObject.AddComponent<RigidbodyComponent>();
// playerObject.AddComponent(new ControllerComponent { Speed = 5f });
// playerObject.AddComponent(new DragComponent { Drag = 5f, Coefficient = new Vector3(1, 1, 0) });
// playerObject.AddComponent(new TestComponent());
//
// var cameraObject = new GameObject();
// cameraObject.Transform.Translation.Z = 2;
// cameraObject.AddComponent<PerspectiveCamera>();
//
// var testObject = new GameObject { Transform = { Translation = new Vector3(0, 6, 0), Rotation = Quaternion.FromAxisAngle(Vector3.UnitX, (float)Math.PI / 2) } };
// testObject.AddComponent(new Box2DRenderer { Color = new Vector4(1, 0, 0, 1) });
//
// var mesh = parEngine.AssetResourceManager.Load<Mesh>("model/untitled.obj");
// var texture = parEngine.AssetResourceManager.Load<Texture>("TestImage.png");
// var font = parEngine.AssetResourceManager.Load<Font>("font/test");
//
// var box2dRenderer = new GameObject
// {
// Transform = { Scale = new Vector3(1), Rotation = Quaternion.FromAxisAngle(Vector3.UnitX, (float)Math.PI / 2) }
// };
// // box2dRenderer.AddComponent(new MeshRenderer { Mesh = mesh, Albedo = texture });
// box2dRenderer.AddComponent(new TextRenderer { Font = font, Text = "A", RenderLayer = RenderLayer.HUD });
// // box2dRenderer.AddComponent(new BillboardComponent { Target = cameraObject.Transform });
//
// var xAxis = new GameObject();
// xAxis.Transform.Translation.X = 5;
// xAxis.Transform.Scale.X = 10;
// xAxis.AddComponent(new Box2DRenderer { Color = new Vector4(1, 0, 0, 1) });
//
// var yAxis = new GameObject();
// yAxis.Transform.Translation.Y = 5;
// yAxis.Transform.Scale.X = 10;
// yAxis.Transform.Rotation *= Quaternion.FromAxisAngle(Vector3.UnitZ, (float)Math.PI / 2);
// yAxis.AddComponent(new Box2DRenderer { Color = new Vector4(0, 1, 0, 1) });
//
// var zAxis = new GameObject();
// zAxis.Transform.Translation.Z = 5;
// zAxis.Transform.Scale.Y = 10;
// zAxis.Transform.Scale.X = 10;
// zAxis.Transform.Rotation *= Quaternion.FromAxisAngle(Vector3.UnitX, (float)Math.PI / 2);
// zAxis.AddComponent(new Box2DRenderer
// {
// Color = new Vector4(0, 0, 1, 1), Texture = parEngine.AssetResourceManager.Load<Texture>("test.jpeg")
// });
//
// var scene = new Scene();
// scene.Add(cameraObject);
// scene.AddChild(cameraObject, testObject);
// scene.Add(playerObject);
// scene.SetChild(playerObject, cameraObject);
//
// scene.Add(box2dRenderer);
// // scene.AddChild(box2dRenderer, testChild);
//
// scene.Add(xAxis);
// // scene.Add(yAxis);
// // scene.Add(zAxis);
//
// return scene;
// }
}

View File

@@ -0,0 +1,52 @@
using DoomDeathmatch.Component.UI;
using DoomDeathmatch.Scene.Main;
using Engine.Asset.Font;
using Engine.Input;
using Engine.Scene;
using Engine.Scene.Component.BuiltIn.Renderer;
using OpenTK.Mathematics;
namespace DoomDeathmatch.Scene.Leaders;
public static class LeadersScene
{
public static Engine.Scene.Scene Create(Engine.Engine parEngine)
{
var scene = new Engine.Scene.Scene();
var (cameraObject, camera) = UiUtil.CreateOrthographicCamera(scene);
var (uiContainerObject, uiContainer) = UiUtil.CreateBackgroundUi(scene, camera);
var (logoObject, logoUi) = UiUtil.CreateLogoUi(parEngine, scene, uiContainer);
var (backUiObject, backUi) = UiUtil.CreateTextUi(scene, uiContainer,
UiUtil.GetDoomFont(parEngine), "Назад");
backUi.OnClick += _ => parEngine.SceneManager.TransitionTo(MainScene.Create(parEngine));
var (stackObject, stack) = UiUtil.CreateStackUi(scene, uiContainer,
[backUi]);
stack.Offset = new Vector2(0, -1f);
stackObject.Transform.Size = new Vector3(1f, 6f, 1f);
var selectorObject = new GameObject
{
Transform = { Translation = new Vector3(0, 0, -1), Size = new Vector3(0.5f, 1f, 1f) }
};
selectorObject.AddComponent(new SelectorComponent
{
Children = { backUi },
SelectKey = KeyboardButtonCode.Space,
NextKey = KeyboardButtonCode.Down,
PrevKey = KeyboardButtonCode.Up,
});
selectorObject.AddComponent(new Box2DRenderer { Color = new Vector4(1, 0, 0, 1) });
scene.AddChild(uiContainerObject, selectorObject);
scene.SetChild(uiContainerObject, logoObject);
scene.SetChild(uiContainerObject, stackObject);
scene.SetChild(stackObject, backUiObject);
return scene;
}
}

View File

@@ -0,0 +1,77 @@
using DoomDeathmatch.Component.UI;
using DoomDeathmatch.Scene.Leaders;
using DoomDeathmatch.Scene.Rules;
using Engine.Asset.Font;
using Engine.Input;
using Engine.Scene;
using Engine.Scene.Component.BuiltIn;
using Engine.Scene.Component.BuiltIn.Renderer;
using OpenTK.Mathematics;
namespace DoomDeathmatch.Scene.Main;
public static class MainScene
{
public static Engine.Scene.Scene Create(Engine.Engine parEngine)
{
var scene = new Engine.Scene.Scene();
var (cameraObject, camera) = UiUtil.CreateOrthographicCamera(scene);
AddMainUi(parEngine, scene, camera);
return scene;
}
private static void AddMainUi(Engine.Engine parEngine, Engine.Scene.Scene parScene, Camera parCamera)
{
var (uiContainerObject, uiContainer) = UiUtil.CreateBackgroundUi(parScene, parCamera);
var (playUiObject, playUi) =
UiUtil.CreateTextUi(parScene, uiContainer, UiUtil.GetDoomFont(parEngine), "Играть");
var (leadersUiObject, leadersUi) =
UiUtil.CreateTextUi(parScene, uiContainer, UiUtil.GetDoomFont(parEngine), "Лидеры");
var (rulesUiObject, rulesUi) =
UiUtil.CreateTextUi(parScene, uiContainer, UiUtil.GetDoomFont(parEngine), "Правила");
var (exitUiObject, exitUi) =
UiUtil.CreateTextUi(parScene, uiContainer, UiUtil.GetDoomFont(parEngine), "Выход");
var (stackObject, stack) = UiUtil.CreateStackUi(parScene, uiContainer,
[playUi, leadersUi, rulesUi, exitUi]);
stack.Offset = new Vector2(0, -1f);
stackObject.Transform.Size = new Vector3(2f, 5f, 1f);
playUi.OnClick += _ => Console.WriteLine("Play");
leadersUi.OnClick += _ => parEngine.SceneManager.TransitionTo(LeadersScene.Create(parEngine));
rulesUi.OnClick += _ => parEngine.SceneManager.TransitionTo(RulesScene.Create(parEngine));
exitUi.OnClick += _ => parEngine.Close();
var (logoObject, logoUi) = UiUtil.CreateLogoUi(parEngine, parScene, uiContainer);
var selectorObject = new GameObject
{
Transform = { Translation = new Vector3(0, 0, -1), Size = new Vector3(0.5f, 1f, 1f) }
};
selectorObject.AddComponent(new SelectorComponent
{
Children = { playUi, leadersUi, rulesUi, exitUi },
SelectKey = KeyboardButtonCode.Space,
NextKey = KeyboardButtonCode.Down,
PrevKey = KeyboardButtonCode.Up,
});
selectorObject.AddComponent(new Box2DRenderer { Color = new Vector4(1, 0, 0, 1) });
parScene.AddChild(uiContainerObject, selectorObject);
parScene.SetChild(uiContainerObject, stackObject);
parScene.SetChild(uiContainerObject, logoObject);
parScene.SetChild(stackObject, playUiObject);
parScene.SetChild(stackObject, leadersUiObject);
parScene.SetChild(stackObject, rulesUiObject);
parScene.SetChild(stackObject, exitUiObject);
}
}

View File

@@ -0,0 +1,58 @@
using DoomDeathmatch.Component.UI;
using DoomDeathmatch.Scene.Main;
using Engine.Asset.Font;
using Engine.Input;
using Engine.Scene;
using Engine.Scene.Component.BuiltIn.Renderer;
using OpenTK.Mathematics;
namespace DoomDeathmatch.Scene.Rules;
public static class RulesScene
{
public static Engine.Scene.Scene Create(Engine.Engine parEngine)
{
var scene = new Engine.Scene.Scene();
var (cameraObject, camera) = UiUtil.CreateOrthographicCamera(scene);
var (uiContainerObject, uiContainer) = UiUtil.CreateBackgroundUi(scene, camera);
var (logoObject, logoUi) = UiUtil.CreateLogoUi(parEngine, scene, uiContainer);
var (backUiObject, backUi) = UiUtil.CreateTextUi(scene, uiContainer,
parEngine.AssetResourceManager.Load<Font>("font/test"), "Назад");
backUi.OnClick += _ => parEngine.SceneManager.TransitionTo(MainScene.Create(parEngine));
var (rulesObject, rulesUi) = UiUtil.CreateTextUi(scene, uiContainer,
parEngine.AssetResourceManager.Load<Font>("font/test"), "Правила");
var (stackObject, stack) = UiUtil.CreateStackUi(scene, uiContainer,
[rulesUi, backUi]);
stack.Offset = new Vector2(0, -1f);
stackObject.Transform.Size = new Vector3(1f, 6f, 1f);
var selectorObject = new GameObject
{
Transform = { Translation = new Vector3(0, 0, -1), Size = new Vector3(0.5f, 1f, 1f) }
};
selectorObject.AddComponent(new SelectorComponent
{
Children = { backUi },
SelectKey = KeyboardButtonCode.Space,
NextKey = KeyboardButtonCode.Down,
PrevKey = KeyboardButtonCode.Up,
});
selectorObject.AddComponent(new Box2DRenderer { Color = new Vector4(1, 0, 0, 1) });
scene.AddChild(uiContainerObject, selectorObject);
scene.SetChild(uiContainerObject, logoObject);
scene.SetChild(uiContainerObject, stackObject);
scene.SetChild(stackObject, rulesObject);
scene.SetChild(stackObject, backUiObject);
return scene;
}
}

View File

@@ -0,0 +1,116 @@
using DoomDeathmatch.Component.UI;
using Engine.Asset.Font;
using Engine.Graphics.Texture;
using Engine.Scene;
using Engine.Scene.Component.BuiltIn;
using Engine.Scene.Component.BuiltIn.Renderer;
using OpenTK.Mathematics;
namespace DoomDeathmatch;
public static class UiUtil
{
public static Font GetDoomFont(Engine.Engine parEngine)
{
return parEngine.AssetResourceManager.Load<Font>("font/doom");
}
public static (GameObject, OrthographicCamera) CreateOrthographicCamera(Engine.Scene.Scene parScene)
{
var cameraObject = new GameObject();
var camera = new OrthographicCamera();
cameraObject.AddComponent(camera);
parScene.Add(cameraObject);
return (cameraObject, camera);
}
public static (GameObject, UiComponent) CreateTextUi(Engine.Scene.Scene parScene, UiContainerComponent parContainer,
Font parFont, string parText)
{
var size = parFont.Measure(parText);
var outerObject = new GameObject
{
Transform = { Size = new Vector3(size.X, size.Y, 1f), Translation = new Vector3(0, 0, -1) }
};
var uiComponent = new UiComponent { Container = parContainer, Anchor = Anchor.Center };
outerObject.AddComponent(uiComponent);
outerObject.AddComponent(new Box2DRenderer { Color = new Vector4(0, 0, 1, 1) });
var innerObject = new GameObject { Transform = { Translation = new Vector3(0, 0, -1) } };
innerObject.AddComponent(new TextRenderer { Font = parFont, Text = parText });
innerObject.AddComponent(new TextAlignComponent { Alignment = TextAlignComponent.Align.Center });
parScene.Add(outerObject);
parScene.AddChild(outerObject, innerObject);
return (outerObject, uiComponent);
}
public static (GameObject, UiContainerComponent) CreateBackgroundUi(Engine.Scene.Scene parScene, Camera parCamera)
{
var uiContainerObject = new GameObject();
var uiContainer = new UiContainerComponent { Camera = parCamera };
uiContainerObject.AddComponent(uiContainer);
uiContainerObject.AddComponent(new Box2DRenderer { Color = new Vector4(0.1f, 0.1f, 0.1f, 1) });
parScene.Add(uiContainerObject);
return (uiContainerObject, uiContainer);
}
public static (GameObject, UiComponent) CreateLogoUi(Engine.Engine parEngine, Engine.Scene.Scene parScene,
UiContainerComponent parContainer)
{
var logoObject = new GameObject
{
Transform =
{
Translation = new Vector3(0, 0, -10), Scale = new Vector3(3), Size = new Vector3(1.6385869565f, 1f, 1f)
}
};
logoObject.AddComponent(new Box2DRenderer
{
Texture = parEngine.AssetResourceManager.Load<Texture>("texture/doom_logo.png")
});
var uiComponent = new UiComponent { Container = null, Anchor = Anchor.Center };
logoObject.AddComponent(new UiComponent
{
Container = parContainer, Anchor = Anchor.Center, Offset = new Vector2(0, 3f)
});
parScene.Add(logoObject);
return (logoObject, uiComponent);
}
public static (GameObject, StackComponent) CreateStackUi(Engine.Scene.Scene parScene,
UiContainerComponent parContainer, List<UiComponent> parChildren, Orientation parOrientation = Orientation.Vertical)
{
var stack = new StackComponent
{
Container = parContainer,
Anchor = Anchor.Center
};
stack.Children.AddRange(parChildren);
foreach (var child in parChildren)
{
child.Container = stack;
}
var stackObject = new GameObject
{
Transform = { Translation = new Vector3(0, 0, -1) }
};
stackObject.AddComponent(stack);
stackObject.AddComponent(new Box2DRenderer { Color = new Vector4(1, 0, 0, 1) });
parScene.Add(stackObject);
return (stackObject, stack);
}
}

View File

@@ -26,6 +26,7 @@
<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"/>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
</ItemGroup>
<ItemGroup>

View File

@@ -11,7 +11,7 @@ layout (location = 3) in int aTextureId;
layout (location = 4) in mat4 aModelMatrix;
layout (location = 0) out vec2 oUV;
layout (location = 1) out int oTextureId;
layout (location = 1) flat out int oTextureId;
layout (location = 2) out vec3 oNormal;
void main()
@@ -35,7 +35,7 @@ layout (location = 0) out vec4 FragColor;
void main()
{
vec3 lightColor = vec3(iUV, 0);
vec3 lightColor = vec3(1);
FragColor = vec4(lightColor, 1.0);
if (iTextureId >= 0)
FragColor *= texture(uTexture[iTextureId], iUV);

View File

@@ -12,7 +12,7 @@ layout (location = 4) in mat4 aModel;
layout (location = 0) out vec4 oColor;
layout (location = 1) out vec2 oUV;
layout (location = 2) out int oTextureId;
layout (location = 2) flat out int oTextureId;
void main()
{

View File

@@ -0,0 +1,61 @@
// #type vertex
#version 460 core
uniform mat4 u_Projection;
uniform mat4 u_View;
layout (location = 0) in vec4 a_Position;
layout (location = 1) in vec2 a_UV;
layout (location = 2) in vec4 a_Color;
layout (location = 3) in int a_AtlasId;
layout (location = 4) in vec2 a_UnitRange;
layout (location = 5) in mat4 a_Model;
layout (location = 0) out vec4 o_Color;
layout (location = 1) out vec2 o_UV;
layout (location = 2) flat out int o_AtlasId;
layout (location = 3) flat out vec2 o_UnitRange;
void main()
{
o_Color = a_Color;
o_UV = a_UV;
o_AtlasId = a_AtlasId;
o_UnitRange = a_UnitRange;
gl_Position = u_Projection * u_View * a_Model * a_Position;
}
// #type fragment
#version 460 core
uniform sampler2D u_Atlas[16];
layout (location = 0) in vec4 i_Color;
layout (location = 1) in vec2 i_UV;
layout (location = 2) flat in int i_AtlasId;
layout (location = 3) flat in vec2 i_UnitRange;
layout (location = 0) out vec4 FragColor;
float median(float r, float g, float b) {
return max(min(r, g), min(max(r, g), b));
}
float screenPxRange() {
vec2 screenTexSize = vec2(1.0) / fwidth(i_UV);
return max(0.5 * dot(i_UnitRange, screenTexSize), 1.0);
}
void main()
{
vec3 msd = texture(u_Atlas[i_AtlasId], i_UV).rgb;
float sd = median(msd.r, msd.g, msd.b);
float screenPxDistance = screenPxRange() * (sd - 0.5);
float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0);
// this is a temporary solution for TODO: fix depth test self discard on overlap
if (opacity == 0.0) {
discard;
}
FragColor = mix(vec4(0), i_Color, opacity);
}

View File

@@ -0,0 +1,109 @@
using Engine.Asset.Font.Metadata;
using Engine.Graphics.Texture;
using OpenTK.Mathematics;
namespace Engine.Asset.Font;
public class Font
{
public StaticTexture AtlasTexture => _atlasTexture;
public Metadata.Metadata Metadata => _metadata;
public Vector2 UnitRange => _unitRange;
private readonly StaticTexture _atlasTexture;
private readonly Metadata.Metadata _metadata;
private readonly Dictionary<int, Glyph> _glyphs = new();
private readonly Dictionary<int, GlyphData> _glyphData = new();
private readonly Dictionary<(int, int), Kerning> _kernings = new();
private readonly Vector2 _unitRange;
public Font(StaticTexture parAtlasTexture, Metadata.Metadata parMetadata)
{
_atlasTexture = parAtlasTexture;
_metadata = parMetadata;
_unitRange = new Vector2(_metadata.Atlas.DistanceRange / _metadata.Atlas.Width,
_metadata.Atlas.DistanceRange / _metadata.Atlas.Height);
LoadGlyphs();
LoadKernings();
}
public Glyph? GetGlyph(int parUnicode)
{
return _glyphs.GetValueOrDefault(parUnicode);
}
public GlyphData? GetGlyphData(int parUnicode)
{
return _glyphData.GetValueOrDefault(parUnicode);
}
public Kerning? GetKerning(int parUnicode1, int parUnicode2)
{
return _kernings.GetValueOrDefault((parUnicode1, parUnicode2));
}
public FontIterator Iterator(string parText)
{
return new FontIterator(this, parText);
}
public Vector2 Measure(string parText)
{
var fontIterator = Iterator(parText);
_ = fontIterator.ToList();
return new Vector2(fontIterator.MaxWidth, fontIterator.MaxHeight);
}
private void LoadGlyphs()
{
foreach (var glyph in _metadata.Glyphs)
{
_glyphs.Add(glyph.Unicode, glyph);
if (glyph.PlaneBounds == null || glyph.AtlasBounds == null)
{
continue;
}
_glyphData.Add(glyph.Unicode, new GlyphData(_metadata, glyph));
}
}
private void LoadKernings()
{
foreach (var kerning in _metadata.Kerning)
{
_kernings.Add((kerning.Unicode1, kerning.Unicode2), kerning);
}
}
public record GlyphData
{
public Vector2[] Positions { get; }
public Vector2[] UVs { get; }
public GlyphData(in Metadata.Metadata parMetadata, in Glyph parGlyph)
{
var size = new Vector2(parMetadata.Atlas.Width, parMetadata.Atlas.Height);
Positions =
[
new Vector2(parGlyph.PlaneBounds!.Left, parGlyph.PlaneBounds.Bottom),
new Vector2(parGlyph.PlaneBounds.Left, parGlyph.PlaneBounds.Top),
new Vector2(parGlyph.PlaneBounds.Right, parGlyph.PlaneBounds.Bottom),
new Vector2(parGlyph.PlaneBounds.Right, parGlyph.PlaneBounds.Top),
];
UVs =
[
new Vector2(parGlyph.AtlasBounds!.Left, parGlyph.AtlasBounds.Bottom) / size,
new Vector2(parGlyph.AtlasBounds.Left, parGlyph.AtlasBounds.Top) / size,
new Vector2(parGlyph.AtlasBounds.Right, parGlyph.AtlasBounds.Bottom) / size,
new Vector2(parGlyph.AtlasBounds.Right, parGlyph.AtlasBounds.Top) / size,
];
}
}
}

View File

@@ -0,0 +1,119 @@
using System.Collections;
using OpenTK.Mathematics;
namespace Engine.Asset.Font;
public class FontIterator : IEnumerable<FontIterator.NextGlyphData>
{
public float MaxWidth => _maxWidth;
public float MaxHeight => _maxHeight;
private readonly Font _font;
private readonly string _text;
private int _currentIndex;
private int _previousCodepoint = -1;
private int _lineCharCount;
private Vector2 _cursor = Vector2.Zero;
private Vector2 _kerning = Vector2.Zero;
private float _maxWidth;
private float _maxHeight;
public FontIterator(Font parFont, string parText)
{
_font = parFont;
_text = parText;
}
private static bool IsLineBreak(int parCodepoint)
{
return parCodepoint == '\n';
}
private static bool IsTab(int parCodepoint)
{
return parCodepoint == '\t';
}
private void Tab()
{
var spaceGlyph = _font.GetGlyph(' ');
if (spaceGlyph == null)
return;
var missingSpaces = 4 - _lineCharCount % 4;
_cursor.X += missingSpaces * spaceGlyph.Advance;
}
private void LineBreak()
{
_kerning = Vector2.Zero;
_cursor.X = 0;
_cursor.Y += _font.Metadata.Metrics.LineHeight;
_lineCharCount = 0;
_previousCodepoint = -1;
}
public IEnumerator<NextGlyphData> GetEnumerator()
{
while (_currentIndex < _text.Length)
{
var codepoint = char.ConvertToUtf32(_text, _currentIndex);
if (IsLineBreak(codepoint))
{
LineBreak();
_currentIndex += 1;
continue;
}
if (IsTab(codepoint))
{
Tab();
_currentIndex += 1;
continue;
}
var glyph = _font.GetGlyph(codepoint);
if (glyph == null)
{
glyph = _font.GetGlyph('?');
if (glyph == null)
{
_currentIndex += 1;
continue;
}
}
if (_previousCodepoint != -1)
{
var kerning = _font.GetKerning(_previousCodepoint, glyph.Unicode);
if (kerning != null)
{
_kerning.X += kerning.Advance;
}
}
var glyphData = _font.GetGlyphData(glyph.Unicode);
_maxWidth = Math.Max(_maxWidth, _cursor.X + glyph.Advance);
_maxHeight = Math.Max(_maxHeight, _cursor.Y + _font.Metadata.Metrics.Ascender);
if (glyphData != null)
yield return new NextGlyphData(glyphData.Positions, glyphData.UVs, (_cursor + _kerning) * new Vector2(1, -1));
_cursor.X += glyph.Advance;
_lineCharCount++;
_previousCodepoint = glyph.Unicode;
_currentIndex += 1;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public record NextGlyphData(Vector2[] Positions, Vector2[] UVs, Vector2 Offset);
}

View File

@@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
namespace Engine.Asset.Font.Metadata;
[JsonSerializable(typeof(Atlas))]
[Serializable]
public record Atlas
{
[JsonPropertyName("distanceRange")]
[JsonInclude]
public float DistanceRange { get; private set; }
[JsonPropertyName("size")]
[JsonInclude]
public float Size { get; private set; }
[JsonPropertyName("width")]
[JsonInclude]
public int Width { get; private set; }
[JsonPropertyName("height")]
[JsonInclude]
public int Height { get; private set; }
[JsonPropertyName("yOrigin")]
[JsonInclude]
public string YOrigin { get; private set; }
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace Engine.Asset.Font.Metadata;
[JsonSerializable(typeof(Bounds))]
[Serializable]
public record Bounds
{
[JsonPropertyName("left")]
[JsonInclude]
public float Left { get; private set; }
[JsonPropertyName("bottom")]
[JsonInclude]
public float Bottom { get; private set; }
[JsonPropertyName("right")]
[JsonInclude]
public float Right { get; private set; }
[JsonPropertyName("top")]
[JsonInclude]
public float Top { get; private set; }
}

View File

@@ -0,0 +1,25 @@
using System.Text.Json.Serialization;
using OpenTK.Mathematics;
namespace Engine.Asset.Font.Metadata;
[JsonSerializable(typeof(Glyph))]
[Serializable]
public record Glyph
{
[JsonPropertyName("unicode")]
[JsonInclude]
public int Unicode { get; private set; }
[JsonPropertyName("advance")]
[JsonInclude]
public float Advance { get; private set; }
[JsonPropertyName("planeBounds")]
[JsonInclude]
public Bounds? PlaneBounds { get; private set; }
[JsonPropertyName("atlasBounds")]
[JsonInclude]
public Bounds? AtlasBounds { get; private set; }
}

View File

@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
namespace Engine.Asset.Font.Metadata;
[JsonSerializable(typeof(Kerning))]
[Serializable]
public record Kerning
{
[JsonPropertyName("unicode1")]
[JsonInclude]
public int Unicode1 { get; private set; }
[JsonPropertyName("unicode2")]
[JsonInclude]
public int Unicode2 { get; private set; }
[JsonPropertyName("advance")]
[JsonInclude]
public float Advance { get; private set; }
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace Engine.Asset.Font.Metadata;
[JsonSerializable(typeof(Metadata))]
[Serializable]
public record Metadata
{
[JsonPropertyName("atlas")]
[JsonInclude]
public Atlas Atlas { get; private set; }
[JsonPropertyName("metrics")]
[JsonInclude]
public Metrics Metrics { get; private set; }
[JsonPropertyName("glyphs")]
[JsonInclude]
public Glyph[] Glyphs { get; private set; }
[JsonPropertyName("kerning")]
[JsonInclude]
public Kerning[] Kerning { get; private set; }
}

View File

@@ -0,0 +1,31 @@
using System.Text.Json.Serialization;
namespace Engine.Asset.Font.Metadata;
[JsonSerializable(typeof(Metrics))]
[Serializable]
public record Metrics
{
[JsonPropertyName("emSize")]
[JsonInclude]
public float EmSize { get; private set; }
[JsonPropertyName("lineHeight")]
[JsonInclude]
public float LineHeight { get; private set; }
[JsonPropertyName("ascender")]
[JsonInclude]
public float Ascender { get; private set; }
[JsonPropertyName("descender")]
[JsonInclude]
public float Descender { get; private set; }
[JsonPropertyName("underlineY")]
[JsonInclude]
public float UnderlineY { get; private set; }
[JsonPropertyName("underlineThickness")]
public float UnderlineThickness { get; private set; }
}

View File

@@ -3,19 +3,19 @@ using Engine.Graphics.Texture;
namespace Engine.Asset;
public class Image<T>(T[,] parPixels)
where T : struct, IPixel
public class Image<T> where T : struct, IPixel
{
public int Width { get; }
public int Height { get; }
public T[,] Pixels { get; } = parPixels;
public T[,] Pixels { get; }
public T this[int parY, int parX] => Pixels[parY, parX];
public Image(int parWidth, int parHeight) : this(new T[parHeight, parWidth])
public Image(int parWidth, int parHeight)
{
Width = parWidth;
Height = parHeight;
Pixels = new T[parHeight, parWidth];
}
public DynamicTexture ToDynamicTexture()

View File

@@ -1,8 +1,16 @@
using System.Diagnostics;
using System.Text;
using Engine.Asset;
using Engine.Asset.Font;
using Engine.Asset.Mesh;
using Engine.Graphics;
using Engine.Graphics.Pixel;
using Engine.Graphics.Shader;
using Engine.Graphics.Texture;
using Engine.Input;
using Engine.Resource;
using Engine.Resource.Loader;
using Engine.Scene;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
@@ -50,9 +58,15 @@ public sealed class Engine
private IInputHandler? _inputHandler;
private IPresenter? _presenter;
internal ResourceManager EngineResourceManager => _engineResourceManager;
public IResourceManager AssetResourceManager => _assetResourceManager;
private readonly ResourceManager _engineResourceManager;
private readonly ResourceManager _assetResourceManager;
private Thread? _updateThread;
public Engine(int parWidth, int parHeight, bool parHeadless, string parTitle, ILogger parLogger)
public Engine(int parWidth, int parHeight, bool parHeadless, string parTitle, string parAssetFolder,
ILogger parLogger)
{
if (Instance != null)
{
@@ -61,20 +75,59 @@ public sealed class Engine
Instance = this;
Log.Logger = parLogger;
_logger = Log.ForContext<Engine>();
var settings = new NativeWindowSettings
{
ClientSize = parHeadless ? new Vector2i(1, 1) : new Vector2i(parWidth, parHeight),
Title = parTitle,
StartVisible = !parHeadless,
APIVersion = new Version(4, 6),
Profile = ContextProfile.Core
Profile = ContextProfile.Core,
DepthBits = 0,
StencilBits = 0
};
Renderer = new Renderer(parWidth, parHeight, settings);
Window = new Window(this, Renderer.NativeWindow, parHeadless);
_engineResourceManager = CreateEngineResourceManager();
_assetResourceManager = CreateAssetResourceManager(parAssetFolder);
Log.Logger = parLogger;
_logger = Log.ForContext<Engine>();
_logger.Information("Created asset resource manager in {AssetFolder}", parAssetFolder);
Renderer = new Renderer(this, parWidth, parHeight, settings);
Window = new Window(this, Renderer.NativeWindow, parHeadless);
}
private static ResourceManager CreateEngineResourceManager()
{
var memoryStreamProvider = new MemoryResourceStreamProvider();
memoryStreamProvider.AddResource("shader/mesh", Encoding.UTF8.GetBytes(ShaderResource.Mesh));
memoryStreamProvider.AddResource("shader/quad", Encoding.UTF8.GetBytes(ShaderResource.Quad));
memoryStreamProvider.AddResource("shader/text", Encoding.UTF8.GetBytes(ShaderResource.Text));
var resourceManager = new ResourceManager(memoryStreamProvider);
RegisterDefaultLoaders(resourceManager);
return resourceManager;
}
private static ResourceManager CreateAssetResourceManager(string parAssetFolder)
{
var filesystemStreamProvider = new FilesystemResourceStreamProvider(parAssetFolder);
var resourceManager = new ResourceManager(filesystemStreamProvider);
RegisterDefaultLoaders(resourceManager);
return resourceManager;
}
private static void RegisterDefaultLoaders(ResourceManager parResourceManager)
{
parResourceManager.RegisterLoader<Program>(new ProgramLoader());
parResourceManager.RegisterLoader<Mesh>(new MeshLoader());
parResourceManager.RegisterLoader<Image<Rgba8>>(new ImageLoader());
parResourceManager.RegisterLoader<Texture>(new TextureLoader());
parResourceManager.RegisterLoader<Font>(new FontLoader());
}
private readonly object _sceneLock = new();
@@ -89,6 +142,11 @@ public sealed class Engine
_updateThread.Join();
}
public void Close()
{
Presenter?.Exit();
}
private void RunRender()
{
while (!Presenter?.IsExiting ?? false)
@@ -114,18 +172,11 @@ public sealed class Engine
SceneManager.Render();
}
Renderer.QuadRenderer.Render(projection, view);
Renderer.QuadRenderer.Reset();
Renderer.GlobalMeshRenderer.Render(projection, view);
Renderer.GlobalMeshRenderer.Reset();
Renderer.EndFrame(projection, view);
Presenter!.Present(Renderer.RenderTexture);
Presenter!.Render();
#if DEBUG
Debug.RenderDocEndFrame();
#endif
@@ -143,9 +194,16 @@ public sealed class Engine
InputHandler?.Update(deltaTime);
lock (_sceneLock)
{
try
{
SceneManager.Update(deltaTime);
}
catch (Exception ex)
{
_logger.Error(ex, "Exception in scene update");
}
}
deltaTime = timer.Elapsed.TotalSeconds;
timer.Restart();

View File

@@ -13,6 +13,7 @@ public sealed class EngineBuilder
private bool _headless;
private int _width = 1;
private int _height = 1;
private string _assetFolder = "./";
private Func<Engine, IPresenter>? _presenterFunc;
private Func<Engine, IInputHandler>? _inputHandlerFunc;
@@ -51,6 +52,12 @@ public sealed class EngineBuilder
return this;
}
public EngineBuilder AssetFolder(string parAssetFolder)
{
_assetFolder = parAssetFolder;
return this;
}
public EngineBuilder Presenter(Func<Engine, IPresenter> parPresenterFunc)
{
_presenterFunc = parPresenterFunc;
@@ -94,7 +101,7 @@ public sealed class EngineBuilder
public Engine Build()
{
var logger = BuildLogger();
var engine = new Engine(_width, _height, _headless, _title, logger);
var engine = new Engine(_width, _height, _headless, _title, _assetFolder, logger);
var presenter = _presenterFunc?.Invoke(engine);
if (presenter != null)

View File

@@ -1,6 +1,7 @@
using Engine.Graphics.Pixel;
using Engine.Graphics.Render.Mesh;
using Engine.Graphics.Render.Quad;
using Engine.Graphics.Render.Text;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
@@ -8,18 +9,23 @@ namespace Engine.Graphics;
public class GenericRenderer : IRenderer
{
public QuadRenderer QuadRenderer => _quadRenderer ??= new QuadRenderer(1024 * 8);
public GlobalMeshRenderer GlobalMeshRenderer => _globalMeshRenderer ??= new GlobalMeshRenderer(1024);
public QuadRenderer QuadRenderer => _quadRenderer ??= new QuadRenderer(_engine, 1024 * 8);
public AnyMeshRenderer AnyMeshRenderer => _globalMeshRenderer ??= new AnyMeshRenderer(_engine, 1024);
public TextRenderer TextRenderer => _textRenderer ??= new TextRenderer(_engine, 1024 * 8);
private QuadRenderer? _quadRenderer;
private GlobalMeshRenderer? _globalMeshRenderer;
private AnyMeshRenderer? _globalMeshRenderer;
private TextRenderer? _textRenderer;
private readonly Engine _engine;
internal readonly Framebuffer.Framebuffer _framebuffer;
private bool _frameStarted;
public GenericRenderer(int parWidth, int parHeight)
public GenericRenderer(Engine parEngine, int parWidth, int parHeight)
{
_engine = parEngine;
_framebuffer = Framebuffer.Framebuffer.Builder(parWidth, parHeight)
.AddColorAttachment<Rgba8>()
.AddDepthAttachment()
@@ -38,14 +44,17 @@ public class GenericRenderer : IRenderer
_framebuffer.Bind();
GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL.ClearColor(0.0f, 0.0f, 0.0f, 0.0f);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
QuadRenderer.Render(parProjectionMatrix, parViewMatrix);
QuadRenderer.Reset();
GlobalMeshRenderer.Render(parProjectionMatrix, parViewMatrix);
GlobalMeshRenderer.Reset();
AnyMeshRenderer.Render(parProjectionMatrix, parViewMatrix);
AnyMeshRenderer.Reset();
TextRenderer.Render(parProjectionMatrix, parViewMatrix);
TextRenderer.Reset();
_framebuffer.Unbind();

View File

@@ -13,4 +13,5 @@ public interface IPresenter : IUpdate, IRender
event Action<ResizeEventArgs> Resize;
void Present(IConstTexture parTexture);
void Exit();
}

View File

@@ -1,18 +1,20 @@
namespace Engine.Graphics.Pipeline;
public class RenderLayer
public class RenderLayer : IComparable<RenderLayer>
{
public static readonly RenderLayer DEFAULT = new("default");
public static readonly RenderLayer HUD = new("hud");
public static readonly RenderLayer OVERLAY = new("overlay");
public static readonly RenderLayer DEFAULT = new("default", 0);
public static readonly RenderLayer OVERLAY = new("overlay", 1);
public static readonly RenderLayer HUD = new("hud", 2);
public static readonly IReadOnlyList<RenderLayer> ALL = new List<RenderLayer> { DEFAULT, HUD, OVERLAY }.AsReadOnly();
public static readonly IReadOnlyList<RenderLayer> ALL = new List<RenderLayer> { DEFAULT, OVERLAY, HUD }.AsReadOnly();
public string Name { get; }
private readonly int _order;
private RenderLayer(string parName)
private RenderLayer(string parName, int parOrder)
{
Name = parName;
_order = parOrder;
}
public override string ToString()
@@ -20,6 +22,11 @@ public class RenderLayer
return Name;
}
public int CompareTo(RenderLayer? parOther)
{
return _order.CompareTo(parOther?._order);
}
public override int GetHashCode()
{
return Name.GetHashCode();

View File

@@ -50,6 +50,7 @@ public abstract class InstancedRenderer<C, I>
return;
}
if (DataChanged())
_instanceVertexBuffer.UploadData(_instanceVertices, _queuedInstanceCount);
_vertexArray.Bind();
@@ -71,4 +72,9 @@ public abstract class InstancedRenderer<C, I>
protected virtual void SetAdditionalUniforms(Program parProgram)
{
}
protected virtual bool DataChanged()
{
return true;
}
}

View File

@@ -3,23 +3,23 @@ using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Mesh;
public class GlobalMeshRenderer(int parMaxInstanceCount)
public class AnyMeshRenderer(Engine parEngine, int parMaxInstanceCount)
{
private readonly Dictionary<Asset.Mesh.Mesh, MeshRenderer> _meshRenderers = new();
private readonly HashSet<Asset.Mesh.Mesh> _frameMeshes = [];
private readonly Program _program = ProgramLoader.LoadFromSource(ShaderResource.Mesh);
private readonly Program _program = parEngine.EngineResourceManager.Load<Program>("shader/mesh");
public void Commit(Asset.Mesh.Mesh parMesh, Matrix4 parModelMatrix)
public void Commit(Asset.Mesh.Mesh parMesh, Matrix4 parModelMatrix, Texture.Texture? parAlbedo = null)
{
if (_meshRenderers.TryGetValue(parMesh, out var meshRenderer))
{
meshRenderer.Commit(parModelMatrix);
meshRenderer.Commit(parModelMatrix, parAlbedo);
}
else
{
var newMeshRenderer = new MeshRenderer(parMesh, parMaxInstanceCount, _program);
newMeshRenderer.Commit(parModelMatrix);
newMeshRenderer.Commit(parModelMatrix, parAlbedo);
_meshRenderers.Add(parMesh, newMeshRenderer);
}
@@ -42,11 +42,11 @@ public class GlobalMeshRenderer(int parMaxInstanceCount)
meshRenderer.Reset();
}
var meshes = _meshRenderers.Keys;
var unusedMeshes = meshes.Where(parMesh => !_frameMeshes.Contains(parMesh));
foreach (var unusedMesh in unusedMeshes)
var meshes = _meshRenderers.Keys.ToList();
foreach (var mesh in meshes)
{
_meshRenderers.Remove(unusedMesh);
if (!_frameMeshes.Contains(mesh))
_meshRenderers.Remove(mesh);
}
_frameMeshes.Clear();

View File

@@ -8,4 +8,9 @@ public struct MeshInstanceVertex : IVertex
{
[Vertex(VertexAttribType.Int)] public int _textureId;
[Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix;
public override int GetHashCode()
{
return HashCode.Combine(_textureId, _modelMatrix);
}
}

View File

@@ -17,6 +17,9 @@ public class MeshRenderer(Asset.Mesh.Mesh parMesh, int parInstanceCount, Program
private readonly TextureUnitMap _textureUnitMap = new(16);
private readonly int[] _textureUnitIndices = new int[16];
private int _frameHash;
private int _previousHash;
public void Commit(Matrix4 parModelMatrix, Texture.Texture? parTexture = null)
{
if (_queuedInstanceCount >= _instanceCount)
@@ -32,6 +35,9 @@ public class MeshRenderer(Asset.Mesh.Mesh parMesh, int parInstanceCount, Program
_instanceVertices[_queuedInstanceCount]._textureId = textureId;
_instanceVertices[_queuedInstanceCount]._modelMatrix = parModelMatrix;
_frameHash = HashCode.Combine(_frameHash, _instanceVertices[_queuedInstanceCount]);
_queuedInstanceCount++;
}
@@ -45,4 +51,17 @@ public class MeshRenderer(Asset.Mesh.Mesh parMesh, int parInstanceCount, Program
parProgram.SetUniform("uTexture", _textureUnitIndices);
}
protected override bool DataChanged()
{
return _frameHash != _previousHash;
}
public override void Reset()
{
base.Reset();
_textureUnitMap.Reset();
_previousHash = _frameHash;
_frameHash = 0;
}
}

View File

@@ -9,4 +9,9 @@ public struct QuadInstanceVertex : IVertex
[Vertex(VertexAttribType.Float, 4)] public Vector4 _color;
[Vertex(VertexAttribType.Int)] public int _textureId;
[Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix;
public override int GetHashCode()
{
return HashCode.Combine(_color, _textureId, _modelMatrix);
}
}

View File

@@ -8,16 +8,20 @@ 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(int parInstanceCount)
: base(PrimitiveType.Triangles, parInstanceCount, [0, 1, 2, 2, 3, 0], [
new QuadCommonVertex { _position = new Vector3(-0.5f, -0.5f, 0), _uv = new Vector2(0, 1) },
new QuadCommonVertex { _position = new Vector3(0.5f, -0.5f, 0), _uv = new Vector2(1, 1) },
new QuadCommonVertex { _position = new Vector3(0.5f, 0.5f, 0), _uv = new Vector2(1, 0) },
new QuadCommonVertex { _position = new Vector3(-0.5f, 0.5f, 0), _uv = new Vector2(0, 0) }
private int _frameHash;
private int _previousHash;
public QuadRenderer(Engine parEngine, int parInstanceCount)
: base(PrimitiveType.Triangles, parInstanceCount, [0, 2, 1, 2, 3, 1], [
new QuadCommonVertex { _position = new Vector3(-0.5f, -0.5f, 0), _uv = new Vector2(0, 0) },
new QuadCommonVertex { _position = new Vector3(-0.5f, 0.5f, 0), _uv = new Vector2(0, 1) },
new QuadCommonVertex { _position = new Vector3(0.5f, -0.5f, 0), _uv = new Vector2(1, 0) },
new QuadCommonVertex { _position = new Vector3(0.5f, 0.5f, 0), _uv = new Vector2(1, 1) },
],
ProgramLoader.LoadFromSource(ShaderResource.Quad))
parEngine.EngineResourceManager.Load<Program>("shader/quad"))
{
}
@@ -37,6 +41,9 @@ public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVert
_instanceVertices[_queuedInstanceCount]._modelMatrix = parModelMatrix;
_instanceVertices[_queuedInstanceCount]._color = parColor;
_instanceVertices[_queuedInstanceCount]._textureId = textureId;
_frameHash = HashCode.Combine(_frameHash, _instanceVertices[_queuedInstanceCount]);
_queuedInstanceCount++;
}
@@ -51,9 +58,16 @@ public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVert
parProgram.SetUniform("uTexture", _textureUnitIndices);
}
protected override bool DataChanged()
{
return _frameHash != _previousHash;
}
public override void Reset()
{
base.Reset();
_textureUnitMap.Reset();
_previousHash = _frameHash;
_frameHash = 0;
}
}

View File

@@ -0,0 +1,13 @@
using Engine.Graphics.Buffer.Vertex;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Text;
public struct GlyphCommonVertex : IVertex
{
[Vertex(VertexAttribType.Float, 4)] public Vector4 _color;
[Vertex(VertexAttribType.Int)] public int _atlasId;
[Vertex(VertexAttribType.Float, 2)] public Vector2 _unitRange;
[Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix;
}

View File

@@ -0,0 +1,15 @@
using Engine.Graphics.Buffer.Vertex;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Text;
public struct GlyphVertex : IVertex
{
[Vertex(VertexAttribType.Float, 2)] public Vector2 _position;
[Vertex(VertexAttribType.Float, 2)] public Vector2 _uv;
[Vertex(VertexAttribType.Float, 4)] public Vector4 _color;
[Vertex(VertexAttribType.Int)] public int _atlasId;
[Vertex(VertexAttribType.Float, 2)] public Vector2 _unitRange;
[Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix;
}

View File

@@ -0,0 +1,125 @@
using Engine.Asset.Font;
using Engine.Graphics.Buffer;
using Engine.Graphics.Shader;
using Engine.Graphics.Texture;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Text;
public class TextRenderer
{
private readonly IndexBuffer _indexBuffer;
private readonly VertexArray _vertexArray;
private readonly VertexBuffer<GlyphVertex> _glyphVertexBuffer;
// private readonly VertexBuffer<GlyphCommonVertex> _glyphCommonVertexBuffer;
private readonly Program _program;
private readonly int _characterCount;
private int _queuedCharacterCount;
private readonly TextureUnitMap _textureUnitMap = new(16);
private readonly int[] _textureUnitIndices = new int[16];
private readonly GlyphVertex[] _glyphVertices;
// private readonly GlyphCommonVertex[] _glyphCommonVertices;
public TextRenderer(Engine parEngine, int parCharacterCount)
{
_characterCount = parCharacterCount;
_glyphVertices = new GlyphVertex[parCharacterCount * 4];
// _glyphCommonVertices = new GlyphCommonVertex[parCharacterCount];
_program = parEngine.EngineResourceManager.Load<Program>("shader/text");
_indexBuffer = new IndexBuffer(CreateIndices(_characterCount));
_glyphVertexBuffer = new VertexBuffer<GlyphVertex>(_characterCount * 4,
BufferStorageFlags.DynamicStorageBit);
// _glyphCommonVertexBuffer = new VertexBuffer<GlyphCommonVertex>(_characterCount,
// BufferStorageFlags.DynamicStorageBit);
_vertexArray = new VertexArray();
_vertexArray.BindIndexBuffer(_indexBuffer);
_vertexArray.BindVertexBuffer(_glyphVertexBuffer, 0, 0);
// _vertexArray.BindVertexBuffer(_glyphCommonVertexBuffer, 1, 1);
}
public void Commit(Font parFont, string parText, Vector4 parColor, in Matrix4 parModelMatrix)
{
if (_queuedCharacterCount >= _characterCount)
{
throw new InvalidOperationException("Character count exceeded");
}
var textureId = _textureUnitMap.GetUnit(parFont.AtlasTexture);
var fontIterator = parFont.Iterator(parText);
foreach (var glyphData in fontIterator)
{
for (var i = 0; i < 4; i++)
{
_glyphVertices[_queuedCharacterCount * 4 + i]._position = glyphData.Positions[i] + glyphData.Offset;
_glyphVertices[_queuedCharacterCount * 4 + i]._uv = glyphData.UVs[i];
_glyphVertices[_queuedCharacterCount * 4 + i]._color = parColor;
_glyphVertices[_queuedCharacterCount * 4 + i]._atlasId = textureId;
_glyphVertices[_queuedCharacterCount * 4 + i]._unitRange = parFont.UnitRange;
_glyphVertices[_queuedCharacterCount * 4 + i]._modelMatrix = parModelMatrix;
}
_queuedCharacterCount++;
}
}
public void Render(in Matrix4 parProjectionMatrix, in Matrix4 parViewMatrix)
{
if (_queuedCharacterCount <= 0)
{
return;
}
_glyphVertexBuffer.UploadData(_glyphVertices, _queuedCharacterCount * 4);
// _glyphCommonVertexBuffer.UploadData(_glyphCommonVertices, _queuedCharacterCount);
_vertexArray.Bind();
_program.Bind();
_program.SetUniform("u_Projection", in parProjectionMatrix);
_program.SetUniform("u_View", in parViewMatrix);
foreach (var (texture, unit) in _textureUnitMap.Textures)
{
texture.BindUnit(unit);
_textureUnitIndices[unit] = unit;
}
_program.SetUniform("u_Atlas", _textureUnitIndices);
GL.DrawElements(PrimitiveType.Triangles, _queuedCharacterCount * 6, DrawElementsType.UnsignedInt, 0);
}
public void Reset()
{
_textureUnitMap.Reset();
_queuedCharacterCount = 0;
}
private static uint[] CreateIndices(int parCharacterCount)
{
var indices = new uint[parCharacterCount * 6];
for (var i = 0; i < parCharacterCount; i++)
{
var index = i * 6;
var offset = i * 4;
indices[index + 0] = (uint)offset + 0;
indices[index + 1] = (uint)offset + 2;
indices[index + 2] = (uint)offset + 1;
indices[index + 3] = (uint)offset + 2;
indices[index + 4] = (uint)offset + 3;
indices[index + 5] = (uint)offset + 1;
}
return indices;
}
}

View File

@@ -1,10 +1,11 @@
using Engine.Graphics.Pipeline;
using System.Collections.Generic;
using Engine.Graphics.Pipeline;
using Engine.Graphics.Pixel;
using Engine.Graphics.Render.Mesh;
using Engine.Graphics.Render.Quad;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using OpenTK.Windowing.Desktop;
using Serilog;
namespace Engine.Graphics;
@@ -13,12 +14,22 @@ public class Renderer
internal Framebuffer.Framebuffer RenderFramebuffer => _framebuffer;
internal Texture.Texture RenderTexture => _framebuffer.TextureInternal!;
public QuadRenderer QuadRenderer { get; }
public GlobalMeshRenderer GlobalMeshRenderer { get; }
private QuadRenderer QuadRenderer { get; }
public int ViewportWidth => _framebuffer.Width;
public int ViewportHeight => _framebuffer.Height;
private readonly Dictionary<RenderLayer, GenericRenderer> _renderers = new();
private readonly SortedDictionary<RenderLayer, GenericRenderer> _renderers = new();
public GenericRenderer this[RenderLayer parRenderLayer]
{
get
{
if (_renderers.TryGetValue(parRenderLayer, out var renderer))
return renderer;
throw new InvalidOperationException($"Renderer for layer {parRenderLayer} not found");
}
}
internal NativeWindow NativeWindow { get; }
@@ -27,9 +38,10 @@ public class Renderer
private readonly Queue<Action> _scheduleActions = new();
public Renderer(int parWidth, int parHeight, NativeWindowSettings parSettings)
public Renderer(Engine parEngine, int parWidth, int parHeight, NativeWindowSettings parSettings)
{
Thread.CurrentThread.Name = "RendererThread";
_renderThread = Thread.CurrentThread;
_renderThread.Name = "RendererThread";
#if DEBUG
Debug.InitializeRenderDoc();
@@ -39,15 +51,16 @@ public class Renderer
InitializeOpenGl(parWidth, parHeight);
_renderThread = Thread.CurrentThread;
_framebuffer = Framebuffer.Framebuffer.Builder(parWidth, parHeight)
.AddColorAttachment<Rgb8>()
.AddDepthAttachment()
.AddColorAttachment<Rgba8>()
.Build();
QuadRenderer = new QuadRenderer(1024 * 8);
GlobalMeshRenderer = new GlobalMeshRenderer(1024);
QuadRenderer = new QuadRenderer(parEngine, 1);
foreach (var layer in RenderLayer.ALL)
{
_renderers.Add(layer, new GenericRenderer(parEngine, parWidth, parHeight));
}
}
private void InitializeOpenGl(int parWidth, int parHeight)
@@ -57,6 +70,7 @@ public class Renderer
#endif
GL.Enable(EnableCap.DepthTest);
// GL.Enable(EnableCap.CullFace);
// GL.Enable(EnableCap.FramebufferSrgb);
@@ -81,39 +95,63 @@ public class Renderer
_scheduleActions.Enqueue(parAction);
}
internal Task<T> Schedule<T>(Func<T> parAction)
{
var completionSource = new TaskCompletionSource<T>();
Schedule(() =>
{
try
{
completionSource.SetResult(parAction());
}
catch (Exception ex)
{
completionSource.SetException(ex);
}
});
return completionSource.Task;
}
internal void StartFrame()
{
EnsureRenderThread();
RunScheduledActions();
_framebuffer.Bind();
GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL.Clear(ClearBufferMask.ColorBufferBit);
// foreach (var renderer in _renderers.Values)
// {
// renderer.StartFrame();
// }
foreach (var renderer in _renderers.Values)
{
renderer.StartFrame();
}
}
internal void EndFrame(in Matrix4 parViewMatrix, in Matrix4 parProjectionMatrix)
{
EnsureRenderThread();
// foreach (var renderer in _renderers.Values)
// {
// renderer.EndFrame(in parViewMatrix, in parProjectionMatrix);
// }
//
// foreach (var renderer in _renderers.Values)
// {
// QuadRenderer.Commit(Matrix4.CreateScale(2f), Vector4.One, renderer._framebuffer.TextureInternal);
// }
//
// QuadRenderer.Render(in parProjectionMatrix, in parViewMatrix);
// QuadRenderer.Reset();
foreach (var renderer in _renderers.Values)
{
renderer.EndFrame(in parViewMatrix, in parProjectionMatrix);
}
_framebuffer.Bind();
// GL.Disable(EnableCap.DepthTest);
// GL.Disable(EnableCap.CullFace);
GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL.Clear(ClearBufferMask.ColorBufferBit);
foreach (var renderer in _renderers.Values)
{
QuadRenderer.Commit(Matrix4.CreateScale(2f, -2f, 1f), Vector4.One, renderer._framebuffer.TextureInternal);
QuadRenderer.Render(Matrix4.Identity, Matrix4.Identity);
QuadRenderer.Reset();
}
// GL.Enable(EnableCap.DepthTest);
// GL.Enable(EnableCap.CullFace);
_framebuffer.Unbind();
}
@@ -122,6 +160,11 @@ public class Renderer
{
_framebuffer.Resize(parWidth, parHeight);
GL.Viewport(0, 0, parWidth, parHeight);
foreach (var renderer in _renderers.Values)
{
renderer.Resize(parWidth, parHeight);
}
}
private void RunScheduledActions()

View File

@@ -125,6 +125,8 @@ public class Program : OpenGlObject
if (linkStatus == 0)
{
var log = GL.GetProgramInfoLog(programId);
GL.DeleteProgram(programId);
throw new ShaderLinkException(log);
}
@@ -133,11 +135,15 @@ public class Program : OpenGlObject
if (validateStatus == 0)
{
var log = GL.GetProgramInfoLog(programId);
GL.DeleteProgram(programId);
throw new ShaderValidationException(log);
}
}
finally
{
GL.DetachShader(programId, parVertexShader);
GL.DetachShader(programId, parFragmentShader);
GL.DeleteShader(parVertexShader);
GL.DeleteShader(parFragmentShader);
}

View File

@@ -123,7 +123,6 @@ public abstract class Texture : OpenGlObject, ITexture
public void BindUnit(int parUnit = 0)
{
GL.ActiveTexture(TextureUnit.Texture0 + parUnit);
GL.BindTextureUnit(parUnit, Handle);
}

View File

@@ -1,9 +1,12 @@
using Engine.Scene;
using OpenTK.Mathematics;
namespace Engine.Input;
public interface IInputHandler : IUpdate
{
Vector2 MousePosition { get; }
bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode);
bool IsKeyJustPressed(KeyboardButtonCode parKeyboardButtonCode);

View File

@@ -1,16 +1,25 @@
using OpenTK.Windowing.GraphicsLibraryFramework;
using OpenTK.Mathematics;
using OpenTK.Windowing.GraphicsLibraryFramework;
namespace Engine.Input;
public class WindowInputHandler(Window parWindow) : IInputHandler
{
public Vector2 MousePosition => parWindow.NativeWindow.MouseState.Position;
private KeyboardState _previousKeyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
private KeyboardState _keyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
private MouseState _previousMouseState = parWindow.NativeWindow.MouseState.GetSnapshot();
private MouseState _mouseState = parWindow.NativeWindow.MouseState.GetSnapshot();
public void Update(double parDeltaTime)
{
_previousKeyboardState = _keyboardState;
_keyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
_previousMouseState = _mouseState;
_mouseState = parWindow.NativeWindow.MouseState.GetSnapshot();
}
public bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode)
@@ -26,12 +35,13 @@ public class WindowInputHandler(Window parWindow) : IInputHandler
public bool IsMouseButtonPressed(MouseButtonCode parButtonCode)
{
return parWindow.NativeWindow.MouseState.IsButtonDown(MapMouseButtonCode(parButtonCode));
return _mouseState.IsButtonDown(MapMouseButtonCode(parButtonCode));
}
public bool IsMouseButtonJustPressed(MouseButtonCode parButtonCode)
{
return parWindow.NativeWindow.MouseState.IsButtonPressed(MapMouseButtonCode(parButtonCode));
return _mouseState.IsButtonDown(MapMouseButtonCode(parButtonCode)) &&
!_previousMouseState.IsButtonDown(MapMouseButtonCode(parButtonCode));
}
private static MouseButton MapMouseButtonCode(MouseButtonCode parButton)

View File

@@ -0,0 +1,9 @@
namespace Engine.Resource;
public class FilesystemResourceStreamProvider(string parBasePath) : IResourceStreamProvider
{
public Stream GetStream(string parPath)
{
return File.OpenRead(Path.Combine(parBasePath, parPath));
}
}

View File

@@ -0,0 +1,6 @@
namespace Engine.Resource;
public interface IResourceLoader
{
object Load(string parPath, IResourceStreamProvider parStreamProvider);
}

View File

@@ -0,0 +1,6 @@
namespace Engine.Resource;
public interface IResourceManager
{
T Load<T>(string parPath) where T : class;
}

View File

@@ -0,0 +1,6 @@
namespace Engine.Resource;
public interface IResourceStreamProvider
{
Stream GetStream(string parPath);
}

View File

@@ -0,0 +1,26 @@
using System.Text.Json;
using Engine.Asset.Font;
using Engine.Asset.Font.Metadata;
namespace Engine.Resource.Loader;
public class FontLoader : IResourceLoader
{
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
var metadataPath = Path.Combine(parPath, "metadata.json");
using var metadataStream = parStreamProvider.GetStream(metadataPath);
var metadata = JsonSerializer.Deserialize<Metadata>(metadataStream);
if (metadata == null)
{
throw new InvalidOperationException($"Failed to load metadata from {metadataPath}");
}
var atlasPath = Path.Combine(parPath, "atlas.png");
using var atlasStream = parStreamProvider.GetStream(atlasPath);
var atlasTexture = ImageLoader.Load(atlasStream).ToStaticTexture();
return new Font(atlasTexture, metadata);
}
}

View File

@@ -0,0 +1,35 @@
using System.Runtime.InteropServices;
using Engine.Graphics.Pixel;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace Engine.Resource.Loader;
public class ImageLoader : IResourceLoader
{
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
using var stream = parStreamProvider.GetStream(parPath);
return Load(stream);
}
internal static Asset.Image<Rgba8> Load(Stream parStream)
{
var sharpImage = SixLabors.ImageSharp.Image.Load<Rgba32>(parStream);
if (sharpImage == null)
{
throw new InvalidOperationException($"Failed to load image from stream");
}
sharpImage.Mutate(parImageContext => parImageContext.Flip(FlipMode.Vertical));
var image = new Asset.Image<Rgba8>(sharpImage.Width, sharpImage.Height);
var span = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetArrayDataReference(image.Pixels),
image.Pixels.Length * Marshal.SizeOf<Rgba8>());
sharpImage.CopyPixelDataTo(span);
return image;
}
}

View File

@@ -0,0 +1,19 @@
using Engine.Asset.Mesh.Loader;
namespace Engine.Resource.Loader;
public class MeshLoader : IResourceLoader
{
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
var extension = Path.GetExtension(parPath);
using var reader = new StreamReader(parStreamProvider.GetStream(parPath));
return extension switch
{
".obj" => ObjMeshLoader.Load(reader),
".stl" => StlMeshLoader.Load(reader),
_ => throw new InvalidOperationException($"Unsupported mesh format: {extension}")
};
}
}

View File

@@ -1,21 +1,24 @@
using System.Text;
using System.Text.RegularExpressions;
using Engine.Graphics.Shader;
namespace Engine.Graphics.Shader;
namespace Engine.Resource.Loader;
public static partial class ProgramLoader
public partial class ProgramLoader : IResourceLoader
{
[GeneratedRegex(@"^//\s+#type\s+(?<type>[a-z]+)$", RegexOptions.Compiled)]
private static partial Regex TypeRegex();
public static Program LoadFromSource(string parSource)
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
var textReader = new StreamReader(parStreamProvider.GetStream(parPath));
var vertexSource = new StringBuilder();
var fragmentSource = new StringBuilder();
var inFragment = false;
var inVertex = false;
foreach (var line in parSource.Split('\n').Select(parLine => parLine.TrimEnd()))
while (textReader.ReadLine() is { } line)
{
var match = TypeRegex().Match(line);
if (match.Success)

View File

@@ -0,0 +1,12 @@
namespace Engine.Resource.Loader;
public class TextureLoader : IResourceLoader
{
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
using var stream = parStreamProvider.GetStream(parPath);
var image = ImageLoader.Load(stream);
return image.ToStaticTexture();
}
}

View File

@@ -0,0 +1,16 @@
namespace Engine.Resource;
public class MemoryResourceStreamProvider : IResourceStreamProvider
{
private readonly Dictionary<string, byte[]> _resources = new();
public Stream GetStream(string parPath)
{
return new MemoryStream(_resources[parPath]);
}
internal void AddResource(string parPath, byte[] parData)
{
_resources.Add(parPath, parData);
}
}

View File

@@ -0,0 +1,36 @@
namespace Engine.Resource;
public class ResourceHandle<T>
{
public T? Value
{
get
{
if (_isLoaded)
{
return _value!;
}
if (!_task.IsCompleted)
{
return _defaultValue;
}
_value = _task.Result;
_isLoaded = true;
return _value!;
}
}
private readonly T? _defaultValue;
private readonly Task<T> _task;
private T? _value;
private bool _isLoaded;
public ResourceHandle(Task<T> parTask, T? parDefaultValue)
{
_defaultValue = parDefaultValue;
_task = parTask;
}
}

View File

@@ -0,0 +1,71 @@
namespace Engine.Resource;
public class ResourceManager : IResourceManager
{
internal IResourceStreamProvider StreamProvider => _streamProvider;
private readonly IResourceStreamProvider _streamProvider;
private readonly Dictionary<Type, IResourceLoader> _loaders = new();
private readonly Dictionary<Type, ResourceStorage> _storages = new();
public ResourceManager(IResourceStreamProvider parStreamProvider)
{
_streamProvider = parStreamProvider;
}
internal void RegisterLoader<T>(IResourceLoader parLoader) where T : class
{
_loaders.Add(typeof(T), parLoader);
}
public T Load<T>(string parPath) where T : class
{
if (!_storages.TryGetValue(typeof(T), out var storage))
{
storage = new ResourceStorage();
_storages.Add(typeof(T), storage);
}
if (storage.Get<T>(parPath) is { } cachedResource)
{
return cachedResource;
}
var loader = _loaders.GetValueOrDefault(typeof(T));
if (loader == null)
{
throw new InvalidOperationException($"No loader found for type {typeof(T)}");
}
var resource = loader.Load(parPath, _streamProvider);
storage.Add(parPath, resource);
return (T)resource;
}
internal void Reset()
{
_storages.Clear();
}
private class ResourceStorage
{
private readonly Dictionary<string, object> _resources = new();
public void Add<T>(string parPath, T parResource) where T : class
{
_resources.Add(parPath, parResource);
}
public T? Get<T>(string parPath) where T : class
{
return _resources.TryGetValue(parPath, out var resource) ? (T)resource : null;
}
public void Remove(string parPath)
{
_resources.Remove(parPath);
}
}
}

View File

@@ -59,33 +59,6 @@ namespace Engine {
}
}
/// <summary>
/// Looks up a localized string similar to #shader vertex
///#version 330 core
///
///uniform mat4 uViewMatrix;
///uniform mat4 uProjectionMatrix;
///
///layout(location = 0) in vec3 aPos;
///layout(location = 1) in vec3 aNormal;
///layout(location = 2) in vec2 aTexCoords;
///layout(location = 3) in mat4 aModelMatrix;
///
///out vec2 outTexCoords;
///
///void main()
///{
/// gl_Position = uProjectionMatrix * uViewMatrix * aModelMatrix * vec4(aPos, 1.0);
/// outTexCoords = aTexCoords;
///}
///
///#shader fragment
///#version 330 core
///
///uniform sampler2D uTexture;
///
///layout(location [rest of string was truncated]&quot;;.
/// </summary>
internal static string Mesh {
get {
return ResourceManager.GetString("Mesh", resourceCulture);
@@ -97,5 +70,11 @@ namespace Engine {
return ResourceManager.GetString("Quad", resourceCulture);
}
}
internal static string Text {
get {
return ResourceManager.GetString("Text", resourceCulture);
}
}
}
}

View File

@@ -31,4 +31,8 @@
<data name="Quad" type="System.Resources.ResXFileRef" xml:space="preserve">
<value>../../assets/shader/quad.shader;System.String, mscorlib, Version=4.0.0.0, Culture=neutral;utf-8</value>
</data>
<data name="Text" type="System.Resources.ResXFileRef" xml:space="preserve">
<value>../../assets/shader/text.shader;System.String, mscorlib, Version=4.0.0.0, Culture=neutral;utf-8</value>
</data>
</root>

View File

@@ -13,28 +13,62 @@ public class OrthographicCamera(
{
public Axis FixedAxis { get; set; } = parAxis;
public float Size { get; set; } = parSize;
public bool UseScreenSize { get; set; } = false;
public override Matrix4 View => GameObject.Transform.TransformMatrix.Inverted();
public override Matrix4 Projection => FixedAxis == Axis.X
? Matrix4.CreateOrthographic(Size, -Size / AspectRatio, NearPlane, FarPlane)
: Matrix4.CreateOrthographic(Size * AspectRatio, -Size, NearPlane, FarPlane);
public override Matrix4 Projection => GetProjectionMatrix();
private Matrix4 GetProjectionMatrix()
{
var size = GameObject.Transform.Size.Xy;
return Matrix4.CreateOrthographic(size.X, size.Y, -NearPlane, -FarPlane);
}
public override Vector3 ScreenToWorld(Vector2 parScreenPosition)
{
var offset = FixedAxis == Axis.X
? new Vector2(Size, Size / AspectRatio)
: new Vector2(Size * AspectRatio, Size);
offset /= 2;
var normalized = parScreenPosition / ScreenSize - new Vector2(0.5f);
normalized.X *= 2;
normalized.Y *= -2;
return new Vector4(parScreenPosition.X - offset.X, parScreenPosition.Y - offset.Y, 0, 1)
return new Vector4(normalized.X, normalized.Y, 0, 1)
.MulProject(Projection.Inverted())
.MulProject(GameObject.Transform.TransformMatrix)
.Xyz;
}
public override Vector2 WorldToScreen(Vector3 parWorldPosition)
{
throw new NotImplementedException();
var normalized = new Vector4(parWorldPosition, 1)
.MulProject(GameObject.Transform.TransformMatrix.Inverted())
.MulProject(Projection)
.Xy;
normalized.X /= 2;
normalized.Y /= -2;
return (normalized + new Vector2(0.5f)) * ScreenSize;
}
public override void Update(double parDeltaTime)
{
if (UseScreenSize)
{
GameObject.Transform.Size.Xy = ScreenSize.ToVector2();
}
else
{
if (FixedAxis == Axis.X)
{
GameObject.Transform.Size.X = Size;
GameObject.Transform.Size.Y = Size / AspectRatio;
}
else
{
GameObject.Transform.Size.X = Size * AspectRatio;
GameObject.Transform.Size.Y = Size;
}
}
}
public enum Axis

View File

@@ -1,11 +1,10 @@
using Engine.Util;
using OpenTK.Mathematics;
using Math = System.Math;
namespace Engine.Scene.Component.BuiltIn;
public class PerspectiveCamera(
float parFieldOfView = 90.0f,
float parFieldOfView = 60.0f,
float parNearPlane = 0.01f,
float parFarPlane = 1000f
)
@@ -18,9 +17,9 @@ public class PerspectiveCamera(
get
{
var transformMatrix = GameObject.Transform.TransformMatrix;
var forward = new Vector4(0, 0, 1, 1).MulProject(transformMatrix);
var eye = new Vector4(0, 0, 0, 1).MulProject(transformMatrix);
var up = (new Vector4(0, -1, 0, 1).MulProject(transformMatrix) - eye).Normalized();
var forward = new Vector4(0, 1, 0, 1).MulProject(in transformMatrix);
var eye = new Vector4(0, 0, 0, 1).MulProject(in transformMatrix);
var up = (new Vector4(0, 0, 1, 1).MulProject(in transformMatrix) - eye).Normalized();
return Matrix4.LookAt(eye.Xyz, forward.Xyz, up.Xyz);
}
@@ -32,11 +31,26 @@ public class PerspectiveCamera(
public override Vector3 ScreenToWorld(Vector2 parScreenPosition)
{
throw new NotImplementedException();
var normalized = parScreenPosition / ScreenSize - new Vector2(0.5f);
normalized.X *= 2;
normalized.Y *= -2;
return new Vector4(normalized.X, normalized.Y, 0, 1)
.MulProject(Projection.Inverted())
.MulProject(GameObject.Transform.TransformMatrix)
.Xyz;
}
public override Vector2 WorldToScreen(Vector3 parWorldPosition)
{
throw new NotImplementedException();
var normalized = new Vector4(parWorldPosition, 1)
.MulProject(GameObject.Transform.TransformMatrix.Inverted())
.MulProject(Projection)
.Xy;
normalized.X /= 2;
normalized.Y /= -2;
return (normalized + new Vector2(0.5f)) * ScreenSize;
}
}

View File

@@ -1,4 +1,5 @@
using Engine.Graphics.Texture;
using Engine.Graphics.Pipeline;
using Engine.Graphics.Texture;
using OpenTK.Mathematics;
namespace Engine.Scene.Component.BuiltIn.Renderer;
@@ -7,11 +8,13 @@ public class Box2DRenderer : Component
{
public ref Vector4 Color => ref _color;
public Texture? Texture { get; set; } = null;
public RenderLayer RenderLayer { get; set; } = RenderLayer.DEFAULT;
private Vector4 _color = Vector4.One;
public override void Render()
{
Engine.Instance.Renderer.QuadRenderer.Commit(GameObject.Transform.TransformMatrix, Color, Texture);
Engine.Instance.Renderer[RenderLayer].QuadRenderer
.Commit(GameObject.Transform.FullTransformMatrix, Color, Texture);
}
}

View File

@@ -1,13 +1,18 @@
using Engine.Asset.Mesh;
using Engine.Graphics.Pipeline;
using Engine.Graphics.Texture;
namespace Engine.Scene.Component.BuiltIn.Renderer;
public class MeshRenderer : Component
{
public Mesh Mesh { get; set; } = null!;
public Texture? Albedo { get; set; } = null;
public RenderLayer RenderLayer { get; set; } = RenderLayer.DEFAULT;
public override void Render()
{
Engine.Instance.Renderer.GlobalMeshRenderer.Commit(Mesh, GameObject.Transform.TransformMatrix);
Engine.Instance.Renderer[RenderLayer].AnyMeshRenderer
.Commit(Mesh, GameObject.Transform.FullTransformMatrix, Albedo);
}
}

View File

@@ -0,0 +1,25 @@
using Engine.Asset.Font;
using Engine.Graphics.Pipeline;
using OpenTK.Mathematics;
namespace Engine.Scene.Component.BuiltIn.Renderer;
public class TextRenderer : Component
{
public Font Font { get; set; } = null!;
public string? Text { get; set; }
public ref Vector4 Color => ref _color;
public RenderLayer RenderLayer { get; set; } = RenderLayer.DEFAULT;
private Vector4 _color = Vector4.One;
public override void Render()
{
if (Text == null)
return;
Engine.Instance.Renderer[RenderLayer].TextRenderer
.Commit(Font, Text, Color, GameObject.Transform.FullTransformMatrix);
}
}

View File

@@ -7,12 +7,12 @@ public class Transform : Component
private Vector3 _translation = Vector3.Zero;
private Quaternion _rotation = Quaternion.Identity;
private Vector3 _scale = Vector3.One;
private Vector3 _localScale = Vector3.One;
private Vector3 _size = Vector3.One;
public ref Vector3 Translation => ref _translation;
public ref Quaternion Rotation => ref _rotation;
public ref Vector3 Scale => ref _scale;
public ref Vector3 LocalScale => ref _localScale;
public ref Vector3 Size => ref _size;
public Matrix4 LocalTransformMatrix => Matrix4.CreateScale(Scale) *
Matrix4.CreateFromQuaternion(Rotation) *
@@ -20,6 +20,8 @@ public class Transform : Component
public Matrix4 TransformMatrix => LocalTransformMatrix * ParentTransformMatrix;
public Matrix4 FullTransformMatrix => Matrix4.CreateScale(Size) * TransformMatrix;
private Matrix4 ParentTransformMatrix
{
get
@@ -32,7 +34,7 @@ public class Transform : Component
public Transform Clone()
{
var clone =
new Transform { Translation = Translation, Rotation = Rotation, Scale = Scale, LocalScale = LocalScale };
new Transform { Translation = Translation, Rotation = Rotation, Scale = Scale, Size = Size };
return clone;
}

View File

@@ -28,7 +28,7 @@ public class Scene : IUpdate, IRender
public T? FindFirstComponent<T>() where T : Component.Component
{
return Hierarchy.Objects.Select(parGameObject => parGameObject.GetComponent<T>()).FirstOrDefault();
return Hierarchy.Objects.Select(parGameObject => parGameObject.GetComponent<T>()).FirstOrDefault(parComponent => parComponent != null);
}
public void Update(double parDeltaTime)
@@ -87,10 +87,15 @@ public class Scene : IUpdate, IRender
});
}
public void AddChild(GameObject parParent, GameObject parGameObject)
public void SetChild(GameObject parParent, GameObject parChild)
{
Add(parGameObject);
Hierarchy.AddChild(parParent, parGameObject);
Hierarchy.AddChild(parParent, parChild);
}
public void AddChild(GameObject parParent, GameObject parChild)
{
Add(parChild);
SetChild(parParent, parChild);
}
public void Remove(GameObject parGameObject)
@@ -115,14 +120,14 @@ public class Scene : IUpdate, IRender
{
Hierarchy.ProcessChanges();
while (_sceneActions.TryDequeue(out var action))
{
action();
}
foreach (var gameObject in Hierarchy.Objects)
{
gameObject.ProcessChanges();
}
while (_sceneActions.TryDequeue(out var action))
{
action();
}
}
}

View File

@@ -22,6 +22,7 @@ public class SceneManager : IUpdate, IRender
_currentScene.Enter();
}
if (parDeltaTime != 0)
_currentScene?.Update(parDeltaTime);
}

View File

@@ -66,10 +66,15 @@ public class Window : IPresenter
GL.BlitNamedFramebuffer(_engine.Renderer.RenderFramebuffer.Handle, 0,
0, parTexture.Height, parTexture.Width, 0,
0, 0, Width, Height,
ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit,
ClearBufferMask.ColorBufferBit,
BlitFramebufferFilter.Nearest
);
}
public void Exit()
{
_window.Close();
}
}
public static class NativeWindowExtensions

View File

@@ -237,7 +237,6 @@ public class HierarchyTests
_hierarchy.AddChild(parent1, child);
_hierarchy.ProcessChanges();
// Reparent the child to parent2
_hierarchy.AddChild(parent2, child);
_hierarchy.ProcessChanges();
@@ -333,13 +332,11 @@ public class HierarchyTests
_hierarchy.Add(child1);
_hierarchy.Add(child2);
// Initial hierarchy setup
_hierarchy.AddChild(grandParent, parent);
_hierarchy.AddChild(parent, child1);
_hierarchy.AddChild(parent, child2);
_hierarchy.ProcessChanges();
// Reparent child1 to child2
_hierarchy.AddChild(child2, child1);
_hierarchy.ProcessChanges();
@@ -357,14 +354,12 @@ public class HierarchyTests
const int hierarchyDepth = 100;
var objects = new object[hierarchyDepth];
// Create objects
for (var i = 0; i < hierarchyDepth; i++)
{
objects[i] = new object();
_hierarchy.Add(objects[i]);
}
// Create a deep hierarchy
for (var i = 1; i < hierarchyDepth; i++)
{
_hierarchy.AddChild(objects[i - 1], objects[i]);
@@ -374,14 +369,12 @@ public class HierarchyTests
Assert.Multiple(() =>
{
// Verify parent-child relationships
for (var i = 1; i < hierarchyDepth; i++)
{
Assert.That(_hierarchy.GetParent(objects[i]), Is.EqualTo(objects[i - 1]));
Assert.That(_hierarchy.IsInHierarchy(objects[0], objects[i]), Is.True);
}
// Verify children
for (var i = 0; i < hierarchyDepth - 1; i++)
{
var children = _hierarchy.GetChildren(objects[i]).ToList();
@@ -409,7 +402,6 @@ public class HierarchyTests
_hierarchy.AddChild(parent, child2);
_hierarchy.ProcessChanges();
// Remove parent, which will also remove its children
_hierarchy.Remove(parent);
_hierarchy.ProcessChanges();
@@ -431,10 +423,9 @@ public class HierarchyTests
.ToList();
_hierarchy.Add(parent);
children.ForEach(child => _hierarchy.Add(child));
children.ForEach(parChild => _hierarchy.Add(parChild));
// Simulate concurrent child additions
children.AsParallel().ForAll(child => { _hierarchy.AddChild(parent, child); });
children.AsParallel().ForAll(parChild => { _hierarchy.AddChild(parent, parChild); });
_hierarchy.ProcessChanges();
@@ -446,13 +437,8 @@ public class HierarchyTests
{
Assert.Multiple(() =>
{
// Null child
Assert.That(_hierarchy.IsInHierarchy(new object(), null), Is.False);
// Null ancestor
Assert.That(_hierarchy.IsInHierarchy(null, new object()), Is.False);
// Both null
Assert.That(_hierarchy.IsInHierarchy(null, null), Is.False);
});
}
@@ -505,12 +491,10 @@ public class HierarchyTests
Assert.Multiple(() =>
{
// Verify deep hierarchy relationships
Assert.That(_hierarchy.IsInHierarchy(grandParent, grandChild), Is.True);
Assert.That(_hierarchy.IsInHierarchy(parent, grandChild), Is.True);
Assert.That(_hierarchy.IsInHierarchy(child, grandChild), Is.True);
// Verify inverse relationships are false
Assert.That(_hierarchy.IsInHierarchy(grandChild, grandParent), Is.False);
Assert.That(_hierarchy.IsInHierarchy(grandChild, parent), Is.False);
Assert.That(_hierarchy.IsInHierarchy(grandChild, child), Is.False);
@@ -540,12 +524,10 @@ public class HierarchyTests
Assert.Multiple(() =>
{
// Verify within same branch
Assert.That(_hierarchy.IsInHierarchy(root, branch1Parent), Is.True);
Assert.That(_hierarchy.IsInHierarchy(root, branch1Child), Is.True);
Assert.That(_hierarchy.IsInHierarchy(branch1Parent, branch1Child), Is.True);
// Verify between different branches
Assert.That(_hierarchy.IsInHierarchy(branch1Parent, branch2Child), Is.False);
Assert.That(_hierarchy.IsInHierarchy(branch2Parent, branch1Child), Is.False);
});

View File

@@ -96,6 +96,21 @@ int findMostPerceptuallyAccurateColor(vec3 color) {
return bestMatchIndex;
}
//int findMostPerceptuallyAccurateColor(vec3 color) {
// int closestIndex = 0;
// float minDistance = distance(color, ConsoleColorVec3[0]);
//
// for (int i = 1; i < 16; i++) {
// float dist = distance(color, ConsoleColorVec3[i]);
// if (dist < minDistance) {
// minDistance = dist;
// closestIndex = i;
// }
// }
//
// return closestIndex;
//}
// Enhanced luminosity calculation considering human perception
float calculatePerceptualLuminance(vec3 color) {
// BT.709 luminance coefficients with slight adjustment
@@ -118,7 +133,7 @@ layout (location = 0) in vec2 iUV;
layout (location = 0) out vec4 FragColor;
void main() {
vec3 pixelColor = texture(uInputTexture, iUV).rgb;
vec3 pixelColor = texture(uInputTexture, iUV * 4).rgb;
// Find most perceptually accurate console color
int colorIndex = findMostPerceptuallyAccurateColor(pixelColor);

View File

@@ -1,9 +1,12 @@
using Engine.Input;
using OpenTK.Mathematics;
namespace PresenterConsole;
public class ConsoleInputHandler : IInputHandler
{
public Vector2 MousePosition => Vector2.Zero;
private readonly bool[] _currentKeys = new bool[256];
private readonly bool[] _previousKeys = new bool[256];

View File

@@ -1,22 +1,26 @@
using Engine.Asset;
using System.Text;
using Engine.Asset;
using Engine.Graphics;
using Engine.Graphics.Buffer;
using Engine.Graphics.Framebuffer;
using Engine.Graphics.Shader;
using Engine.Graphics.Texture;
using Engine.Resource;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using OpenTK.Windowing.Common;
using PresenterConsole.Resource;
namespace PresenterConsole;
public class ConsolePresenter : IPresenter
{
public bool IsExiting => false;
public bool IsExiting { get; private set; }
public int Width { get; private set; } = 2;
public int Height { get; private set; } = 1;
public event Action<ResizeEventArgs>? Resize;
private readonly Engine.Engine _engine;
private readonly Framebuffer _framebuffer;
private readonly Engine.Graphics.Shader.Program _asciiProgram;
private Image<AsciiPixel>? _asciiImage;
@@ -28,9 +32,13 @@ public class ConsolePresenter : IPresenter
private readonly ConsoleFastOutput _consoleOutput;
private static readonly char[] LIGHTMAP = " .,:;=*#%@".Reverse().ToArray();
public ConsolePresenter()
public ConsolePresenter(Engine.Engine parEngine)
{
_asciiProgram = ProgramLoader.LoadFromSource(Resource.ShaderResource.Ascii);
_engine = parEngine;
((MemoryResourceStreamProvider)_engine.EngineResourceManager.StreamProvider).AddResource("shader/ascii",
Encoding.UTF8.GetBytes(ShaderResource.Ascii));
_asciiProgram = _engine.EngineResourceManager.Load<Engine.Graphics.Shader.Program>("shader/ascii");
_framebuffer = Framebuffer.Builder(Width / 2, Height)
.AddColorAttachment<AsciiPixel>()
@@ -55,6 +63,8 @@ public class ConsolePresenter : IPresenter
{
var openglTexture = (Texture)parTexture;
// GL.Viewport(0, 0, Width / 2, Height);
_framebuffer.Bind();
openglTexture.BindUnit();
@@ -67,6 +77,8 @@ public class ConsolePresenter : IPresenter
_framebuffer.Unbind();
// GL.Viewport(0, 0, Width / 2 * 4, Height * 4);
var asciiTexture = _framebuffer.TextureInternal;
if (asciiTexture == null)
throw new InvalidOperationException("Framebuffer texture is null");
@@ -75,10 +87,10 @@ public class ConsolePresenter : IPresenter
_asciiImage = new Image<AsciiPixel>(asciiTexture.Width, asciiTexture.Height);
asciiTexture.ReadPixels(_asciiImage);
Output(_asciiImage);
DrawImage(_asciiImage);
}
private void Output(Image<AsciiPixel> parImage)
private void DrawImage(Image<AsciiPixel> parImage)
{
for (var y = 0; y < parImage.Height; y++)
{
@@ -108,10 +120,15 @@ public class ConsolePresenter : IPresenter
Width = consoleWidth;
Height = consoleHeight;
Resize?.Invoke(new ResizeEventArgs(Width / 2, Height));
Resize?.Invoke(new ResizeEventArgs(Width / 2 * 4, Height * 4));
_framebuffer.Resize(Width / 2, Height);
_consoleOutput.Resize(Width, Height);
}
}
public void Exit()
{
IsExiting = true;
}
}

View File

@@ -10,8 +10,9 @@ internal static class Program
.Headless()
.LogToFile(true, "log.txt")
.LogLevel(LogEventLevel.Debug)
.Presenter(_ => new ConsolePresenter())
.Presenter(parEngine => new ConsolePresenter(parEngine))
.InputHandler(_ => new ConsoleInputHandler())
.AssetFolder(Path.GetFullPath("../DoomDeathmatch/asset"))
.Build();
DoomDeathmatch.DoomDeathmatch.Initialize(engine);

View File

@@ -18,6 +18,7 @@ internal static class Program
.LogLevel(LogEventLevel.Debug)
.Presenter(parEngine => parEngine.Window)
.InputHandler(parEngine => new WindowInputHandler(parEngine.Window))
.AssetFolder(Path.GetFullPath("../DoomDeathmatch/asset"))
.Build();
DoomDeathmatch.DoomDeathmatch.Initialize(engine);

View File

@@ -1,10 +1,12 @@
using System.Configuration;
using System.Data;
using System.IO;
using System.Windows;
using Engine;
using Engine.Graphics;
using Engine.Graphics.Texture;
using Engine.Input;
using OpenTK.Mathematics;
using OpenTK.Windowing.Common;
using Serilog.Events;
@@ -27,6 +29,7 @@ public partial class App : Application
.LogToConsole()
.LogToFile(true, "log.txt")
.LogLevel(LogEventLevel.Debug)
.AssetFolder(Path.GetFullPath("../DoomDeathmatch/asset"))
.Build();
// Since engine claims current thread for rendering, we need to create a new thread to run WPF
@@ -53,6 +56,8 @@ public partial class App : Application
private class InputHandlerWrapper : IInputHandler
{
public Vector2 MousePosition => _inputHandler?.MousePosition ?? Vector2.Zero;
private IInputHandler? _inputHandler;
public IInputHandler? InputHandler
@@ -137,5 +142,10 @@ public partial class App : Application
{
Resize?.Invoke(e);
}
public void Exit()
{
Presenter?.Exit();
}
}
}

View File

@@ -74,6 +74,11 @@ public partial class MainWindow : Window, IPresenter
});
}
public void Exit()
{
Dispatcher.Invoke(Close);
}
private void DrawImage(Image<Rgb8> parImage)
{
try

View File

@@ -1,18 +1,27 @@
using System.Windows;
using System.Windows.Input;
using Engine.Input;
using OpenTK.Mathematics;
namespace PresenterWpf;
public class WpfInputHandler : IInputHandler
{
public Vector2 MousePosition => _mousePosition;
private readonly Window _window;
private readonly bool[] _actualKeys = new bool[(int)KeyboardButtonCode.TotalCount];
private readonly bool[] _currentKeys = new bool[(int)KeyboardButtonCode.TotalCount];
private readonly bool[] _previousKeys = new bool[(int)KeyboardButtonCode.TotalCount];
private readonly bool[] _actualMouseButtons = new bool[(int)MouseButtonCode.TotalCount];
private readonly bool[] _currentMouseButtons = new bool[(int)MouseButtonCode.TotalCount];
private readonly bool[] _previousMouseButtons = new bool[(int)MouseButtonCode.TotalCount];
private Vector2 _mousePosition = Vector2.Zero;
public WpfInputHandler(Window parWindow)
{
_window = parWindow;
@@ -21,6 +30,7 @@ public class WpfInputHandler : IInputHandler
_window.PreviewKeyUp += Window_PreviewKeyUp;
_window.PreviewMouseDown += Window_PreviewMouseDown;
_window.PreviewMouseUp += Window_PreviewMouseUp;
_window.MouseMove += Window_MouseMove;
}
public void Update(double parDeltaTime)
@@ -28,50 +38,58 @@ public class WpfInputHandler : IInputHandler
for (var i = 0; i < _currentKeys.Length; i++)
{
_previousKeys[i] = _currentKeys[i];
_currentKeys[i] = _actualKeys[i];
}
for (var i = 0; i < _currentMouseButtons.Length; i++)
{
_previousMouseButtons[i] = _currentMouseButtons[i];
_currentMouseButtons[i] = _actualMouseButtons[i];
}
}
private void Window_PreviewKeyDown(object parSender, KeyEventArgs parEventArgs)
{
var keyCode = ConvertToKeyboardButtonCode(parEventArgs.Key);
if (keyCode >= 0 && keyCode < _currentKeys.Length)
if (keyCode >= 0 && keyCode < _actualKeys.Length)
{
_currentKeys[keyCode] = true;
_actualKeys[keyCode] = true;
}
}
private void Window_PreviewKeyUp(object parSender, KeyEventArgs parEventArgs)
{
var keyCode = ConvertToKeyboardButtonCode(parEventArgs.Key);
if (keyCode >= 0 && keyCode < _currentKeys.Length)
if (keyCode >= 0 && keyCode < _actualKeys.Length)
{
_currentKeys[keyCode] = false;
_actualKeys[keyCode] = false;
}
}
private void Window_PreviewMouseDown(object parSender, MouseButtonEventArgs parEventArgs)
{
var buttonCode = ConvertToMouseButtonCode(parEventArgs.ChangedButton);
if (buttonCode >= 0 && buttonCode < _currentMouseButtons.Length)
if (buttonCode >= 0 && buttonCode < _actualMouseButtons.Length)
{
_currentMouseButtons[buttonCode] = true;
_actualMouseButtons[buttonCode] = true;
}
}
private void Window_PreviewMouseUp(object parSender, MouseButtonEventArgs parEventArgs)
{
var buttonCode = ConvertToMouseButtonCode(parEventArgs.ChangedButton);
if (buttonCode >= 0 && buttonCode < _currentMouseButtons.Length)
if (buttonCode >= 0 && buttonCode < _actualMouseButtons.Length)
{
_currentMouseButtons[buttonCode] = false;
_actualMouseButtons[buttonCode] = false;
}
}
private void Window_MouseMove(object parSender, MouseEventArgs parEventArgs)
{
var position = parEventArgs.GetPosition(null);
_mousePosition = new Vector2((float)position.X, (float)position.Y);
}
public bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode)
{
var keyCode = (int)parKeyboardButtonCode;