.
This commit is contained in:
@@ -10,4 +10,8 @@
|
||||
<ProjectReference Include="..\Engine\Engine.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="src\Scene\Play\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
BIN
DoomDeathmatch/asset/TestImage.png
Normal file
BIN
DoomDeathmatch/asset/TestImage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
DoomDeathmatch/asset/font/doom/atlas.png
Normal file
BIN
DoomDeathmatch/asset/font/doom/atlas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
1418
DoomDeathmatch/asset/font/doom/metadata.json
Normal file
1418
DoomDeathmatch/asset/font/doom/metadata.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
DoomDeathmatch/asset/font/test/atlas.png
Normal file
BIN
DoomDeathmatch/asset/font/test/atlas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
255697
DoomDeathmatch/asset/font/test/metadata.json
Normal file
255697
DoomDeathmatch/asset/font/test/metadata.json
Normal file
File diff suppressed because it is too large
Load Diff
121676
DoomDeathmatch/asset/model/teapot.stl
Normal file
121676
DoomDeathmatch/asset/model/teapot.stl
Normal file
File diff suppressed because it is too large
Load Diff
44
DoomDeathmatch/asset/model/test2.obj
Normal file
44
DoomDeathmatch/asset/model/test2.obj
Normal 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
|
||||
38
DoomDeathmatch/asset/model/test3.obj
Normal file
38
DoomDeathmatch/asset/model/test3.obj
Normal 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
|
||||
24
DoomDeathmatch/asset/model/test4.obj
Normal file
24
DoomDeathmatch/asset/model/test4.obj
Normal 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
|
||||
2975
DoomDeathmatch/asset/model/untitled.obj
Normal file
2975
DoomDeathmatch/asset/model/untitled.obj
Normal file
File diff suppressed because it is too large
Load Diff
BIN
DoomDeathmatch/asset/test.jpeg
Normal file
BIN
DoomDeathmatch/asset/test.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
BIN
DoomDeathmatch/asset/texture/doom_logo.png
Normal file
BIN
DoomDeathmatch/asset/texture/doom_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
41
DoomDeathmatch/src/Component/BillboardComponent.cs
Normal file
41
DoomDeathmatch/src/Component/BillboardComponent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
21
DoomDeathmatch/src/Component/DragComponent.cs
Normal file
21
DoomDeathmatch/src/Component/DragComponent.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
44
DoomDeathmatch/src/Component/GravityComponent.cs
Normal file
44
DoomDeathmatch/src/Component/GravityComponent.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
33
DoomDeathmatch/src/Component/HealthComponent.cs
Normal file
33
DoomDeathmatch/src/Component/HealthComponent.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
39
DoomDeathmatch/src/Component/RigidbodyComponent.cs
Normal file
39
DoomDeathmatch/src/Component/RigidbodyComponent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
14
DoomDeathmatch/src/Component/UI/Anchor.cs
Normal file
14
DoomDeathmatch/src/Component/UI/Anchor.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace DoomDeathmatch.Component.UI;
|
||||
|
||||
public enum Anchor
|
||||
{
|
||||
TopLeft,
|
||||
TopCenter,
|
||||
TopRight,
|
||||
CenterLeft,
|
||||
Center,
|
||||
CenterRight,
|
||||
BottomLeft,
|
||||
BottomCenter,
|
||||
BottomRight
|
||||
}
|
||||
7
DoomDeathmatch/src/Component/UI/Orientation.cs
Normal file
7
DoomDeathmatch/src/Component/UI/Orientation.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace DoomDeathmatch.Component.UI;
|
||||
|
||||
public enum Orientation
|
||||
{
|
||||
Horizontal,
|
||||
Vertical
|
||||
}
|
||||
88
DoomDeathmatch/src/Component/UI/SelectorComponent.cs
Normal file
88
DoomDeathmatch/src/Component/UI/SelectorComponent.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
32
DoomDeathmatch/src/Component/UI/StackComponent.cs
Normal file
32
DoomDeathmatch/src/Component/UI/StackComponent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
DoomDeathmatch/src/Component/UI/TestComponent.cs
Normal file
20
DoomDeathmatch/src/Component/UI/TestComponent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
54
DoomDeathmatch/src/Component/UI/TextAlignComponent.cs
Normal file
54
DoomDeathmatch/src/Component/UI/TextAlignComponent.cs
Normal 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
|
||||
}
|
||||
}
|
||||
72
DoomDeathmatch/src/Component/UI/UiComponent.cs
Normal file
72
DoomDeathmatch/src/Component/UI/UiComponent.cs
Normal 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
28
DoomDeathmatch/src/Component/UI/UiContainerComponent.cs
Normal file
28
DoomDeathmatch/src/Component/UI/UiContainerComponent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
// }
|
||||
}
|
||||
52
DoomDeathmatch/src/Scene/Leaders/LeadersScene.cs
Normal file
52
DoomDeathmatch/src/Scene/Leaders/LeadersScene.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
77
DoomDeathmatch/src/Scene/Main/MainScene.cs
Normal file
77
DoomDeathmatch/src/Scene/Main/MainScene.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
58
DoomDeathmatch/src/Scene/Rules/RulesScene.cs
Normal file
58
DoomDeathmatch/src/Scene/Rules/RulesScene.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
116
DoomDeathmatch/src/UiUtil.cs
Normal file
116
DoomDeathmatch/src/UiUtil.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
61
Engine/assets/shader/text.shader
Normal file
61
Engine/assets/shader/text.shader
Normal 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);
|
||||
}
|
||||
109
Engine/src/Asset/Font/Font.cs
Normal file
109
Engine/src/Asset/Font/Font.cs
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
119
Engine/src/Asset/Font/FontIterator.cs
Normal file
119
Engine/src/Asset/Font/FontIterator.cs
Normal 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);
|
||||
}
|
||||
28
Engine/src/Asset/Font/Metadata/Atlas.cs
Normal file
28
Engine/src/Asset/Font/Metadata/Atlas.cs
Normal 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; }
|
||||
}
|
||||
24
Engine/src/Asset/Font/Metadata/Bounds.cs
Normal file
24
Engine/src/Asset/Font/Metadata/Bounds.cs
Normal 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; }
|
||||
}
|
||||
25
Engine/src/Asset/Font/Metadata/Glyph.cs
Normal file
25
Engine/src/Asset/Font/Metadata/Glyph.cs
Normal 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; }
|
||||
}
|
||||
20
Engine/src/Asset/Font/Metadata/Kerning.cs
Normal file
20
Engine/src/Asset/Font/Metadata/Kerning.cs
Normal 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; }
|
||||
}
|
||||
24
Engine/src/Asset/Font/Metadata/Metadata.cs
Normal file
24
Engine/src/Asset/Font/Metadata/Metadata.cs
Normal 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; }
|
||||
}
|
||||
31
Engine/src/Asset/Font/Metadata/Metrics.cs
Normal file
31
Engine/src/Asset/Font/Metadata/Metrics.cs
Normal 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; }
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -144,7 +195,14 @@ public sealed class Engine
|
||||
|
||||
lock (_sceneLock)
|
||||
{
|
||||
SceneManager.Update(deltaTime);
|
||||
try
|
||||
{
|
||||
SceneManager.Update(deltaTime);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Exception in scene update");
|
||||
}
|
||||
}
|
||||
|
||||
deltaTime = timer.Elapsed.TotalSeconds;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -13,4 +13,5 @@ public interface IPresenter : IUpdate, IRender
|
||||
event Action<ResizeEventArgs> Resize;
|
||||
|
||||
void Present(IConstTexture parTexture);
|
||||
void Exit();
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -50,7 +50,8 @@ public abstract class InstancedRenderer<C, I>
|
||||
return;
|
||||
}
|
||||
|
||||
_instanceVertexBuffer.UploadData(_instanceVertices, _queuedInstanceCount);
|
||||
if (DataChanged())
|
||||
_instanceVertexBuffer.UploadData(_instanceVertices, _queuedInstanceCount);
|
||||
_vertexArray.Bind();
|
||||
|
||||
_program.Bind();
|
||||
@@ -71,4 +72,9 @@ public abstract class InstancedRenderer<C, I>
|
||||
protected virtual void SetAdditionalUniforms(Program parProgram)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual bool DataChanged()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
13
Engine/src/Graphics/Render/Text/GlyphCommonVertex.cs
Normal file
13
Engine/src/Graphics/Render/Text/GlyphCommonVertex.cs
Normal 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;
|
||||
}
|
||||
15
Engine/src/Graphics/Render/Text/GlyphVertex.cs
Normal file
15
Engine/src/Graphics/Render/Text/GlyphVertex.cs
Normal 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;
|
||||
}
|
||||
125
Engine/src/Graphics/Render/Text/TextRenderer.cs
Normal file
125
Engine/src/Graphics/Render/Text/TextRenderer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
9
Engine/src/Resource/FilesystemResourceStreamProvider.cs
Normal file
9
Engine/src/Resource/FilesystemResourceStreamProvider.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
6
Engine/src/Resource/IResourceLoader.cs
Normal file
6
Engine/src/Resource/IResourceLoader.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Engine.Resource;
|
||||
|
||||
public interface IResourceLoader
|
||||
{
|
||||
object Load(string parPath, IResourceStreamProvider parStreamProvider);
|
||||
}
|
||||
6
Engine/src/Resource/IResourceManager.cs
Normal file
6
Engine/src/Resource/IResourceManager.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Engine.Resource;
|
||||
|
||||
public interface IResourceManager
|
||||
{
|
||||
T Load<T>(string parPath) where T : class;
|
||||
}
|
||||
6
Engine/src/Resource/IResourceStreamProvider.cs
Normal file
6
Engine/src/Resource/IResourceStreamProvider.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Engine.Resource;
|
||||
|
||||
public interface IResourceStreamProvider
|
||||
{
|
||||
Stream GetStream(string parPath);
|
||||
}
|
||||
26
Engine/src/Resource/Loader/FontLoader.cs
Normal file
26
Engine/src/Resource/Loader/FontLoader.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
35
Engine/src/Resource/Loader/ImageLoader.cs
Normal file
35
Engine/src/Resource/Loader/ImageLoader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
19
Engine/src/Resource/Loader/MeshLoader.cs
Normal file
19
Engine/src/Resource/Loader/MeshLoader.cs
Normal 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}")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
12
Engine/src/Resource/Loader/TextureLoader.cs
Normal file
12
Engine/src/Resource/Loader/TextureLoader.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
16
Engine/src/Resource/MemoryResourceStreamProvider.cs
Normal file
16
Engine/src/Resource/MemoryResourceStreamProvider.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
36
Engine/src/Resource/ResourceHandle.cs
Normal file
36
Engine/src/Resource/ResourceHandle.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
71
Engine/src/Resource/ResourceManager.cs
Normal file
71
Engine/src/Resource/ResourceManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Engine/src/Resource/ShaderResource.Designer.cs
generated
33
Engine/src/Resource/ShaderResource.Designer.cs
generated
@@ -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]";.
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
25
Engine/src/Scene/Component/BuiltIn/Renderer/TextRenderer.cs
Normal file
25
Engine/src/Scene/Component/BuiltIn/Renderer/TextRenderer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,8 @@ public class SceneManager : IUpdate, IRender
|
||||
_currentScene.Enter();
|
||||
}
|
||||
|
||||
_currentScene?.Update(parDeltaTime);
|
||||
if (parDeltaTime != 0)
|
||||
_currentScene?.Update(parDeltaTime);
|
||||
}
|
||||
|
||||
public void Render()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,11 @@ public partial class MainWindow : Window, IPresenter
|
||||
});
|
||||
}
|
||||
|
||||
public void Exit()
|
||||
{
|
||||
Dispatcher.Invoke(Close);
|
||||
}
|
||||
|
||||
private void DrawImage(Image<Rgb8> parImage)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user