feat: modular HAL, Raylib backend, PBR shading, textures, 60 unit tests
- Replace hardcoded SDL3 windowing with IWindow/IInputState/Key abstractions - Each render backend owns its window (Raylib GLFW, SDL3 for Vulkan) - Raylib backend: DrawModelEx, custom GLSL shader with Fresnel, ACES tonemapping, gamma correction, hemisphere ambient - Fix backface culling, mesh memory (NativeMemory.Alloc), texture loading - Camera controllers use backend-agnostic Key enum (inverted yaw/strafe) - Demo scene: 8 cubes, 7 spheres, torus knot OBJ with checker texture - Extract ProceduralMesh + MeshMath from Program.cs to Engine.Graphics - Vulkan backend deferred (compiles, untested, IWindow-compatible) - 60 unit tests: ObjLoader, camera controllers, AiCommandProcessor, RenderBackendFactory, Timing, ProceduralMesh, MeshMath, Transform - AGENTS.md for opencode integration
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<IsAotCompatible>false</IsAotCompatible>
|
||||
<AssemblyName>Engine.Graphics.Raylib</AssemblyName>
|
||||
<RootNamespace>Engine.Graphics.Raylib</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
<DefineConstants>DEV_MODE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'ReleaseAOT'">
|
||||
<DefineConstants>RELEASE_AOT</DefineConstants>
|
||||
<PublishAot>false</PublishAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Raylib-cs" Version="8.0.0" />
|
||||
<PackageReference Include="Flecs.NET.Debug" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PackageReference Include="Flecs.NET.Release" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Release' OR '$(Configuration)' == 'ReleaseAOT'" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Engine.Graphics\Engine.Graphics.csproj" />
|
||||
<ProjectReference Include="..\Engine.Core\Engine.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
using Engine.Graphics;
|
||||
|
||||
namespace Engine.Graphics.RaylibBackend;
|
||||
|
||||
/// <summary>
|
||||
/// Triggers registration of the Raylib backend with the HAL factory.
|
||||
/// </summary>
|
||||
public static class RaylibBackendRegistrar
|
||||
{
|
||||
static RaylibBackendRegistrar()
|
||||
{
|
||||
RenderBackendFactory.Register("raylib", (width, height, _) => new RaylibRenderContext(width, height));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No-op method that forces the static constructor to run.
|
||||
/// Call this before using <see cref="RenderBackendFactory.Create"/>.
|
||||
/// </summary>
|
||||
public static void EnsureRegistered() { }
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Engine.Core;
|
||||
using Raylib_cs;
|
||||
|
||||
namespace Engine.Graphics.RaylibBackend;
|
||||
|
||||
/// <summary>
|
||||
/// Raylib-backed implementation of <see cref="IInputState"/>.
|
||||
/// Queries Raylib's input functions directly each frame.
|
||||
/// </summary>
|
||||
public sealed class RaylibInputState : IInputState
|
||||
{
|
||||
private static readonly Key[] _allKeys = (Key[])Enum.GetValues(typeof(Key));
|
||||
|
||||
private readonly HashSet<Key> _keysDown = new();
|
||||
private readonly HashSet<Key> _keysPressed = new();
|
||||
private readonly HashSet<Key> _keysReleased = new();
|
||||
|
||||
private float _mouseWheelDelta;
|
||||
private bool _wheelConsumed;
|
||||
|
||||
public int MouseX => Raylib.GetMouseX();
|
||||
public int MouseY => Raylib.GetMouseY();
|
||||
public bool MouseLeft => Raylib.IsMouseButtonDown(MouseButton.Left);
|
||||
public bool MouseRight => Raylib.IsMouseButtonDown(MouseButton.Right);
|
||||
public bool MouseMiddle => Raylib.IsMouseButtonDown(MouseButton.Middle);
|
||||
|
||||
public float MouseWheelDelta
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_wheelConsumed)
|
||||
{
|
||||
_mouseWheelDelta = Raylib.GetMouseWheelMove();
|
||||
_wheelConsumed = true;
|
||||
}
|
||||
return _mouseWheelDelta;
|
||||
}
|
||||
}
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
_keysPressed.Clear();
|
||||
_keysReleased.Clear();
|
||||
_mouseWheelDelta = 0;
|
||||
_wheelConsumed = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Poll Raylib input and update edge state. Called by <see cref="RaylibWindow.PumpEvents"/>.
|
||||
/// </summary>
|
||||
public void Poll()
|
||||
{
|
||||
_keysPressed.Clear();
|
||||
_keysReleased.Clear();
|
||||
|
||||
foreach (var key in _allKeys)
|
||||
{
|
||||
if (key == Key.Unknown) continue;
|
||||
var rlKey = ToRaylibKey(key);
|
||||
if (rlKey == KeyboardKey.Null) continue;
|
||||
|
||||
var isDown = Raylib.IsKeyDown(rlKey);
|
||||
var wasDown = _keysDown.Contains(key);
|
||||
|
||||
if (isDown && !wasDown)
|
||||
_keysPressed.Add(key);
|
||||
if (!isDown && wasDown)
|
||||
_keysReleased.Add(key);
|
||||
|
||||
if (isDown)
|
||||
_keysDown.Add(key);
|
||||
else
|
||||
_keysDown.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsKeyDown(Key key) => _keysDown.Contains(key);
|
||||
public bool IsKeyPressed(Key key) => _keysPressed.Contains(key);
|
||||
public bool IsKeyReleased(Key key) => _keysReleased.Contains(key);
|
||||
|
||||
private static KeyboardKey ToRaylibKey(Key key) => key switch
|
||||
{
|
||||
Key.Space => KeyboardKey.Space,
|
||||
Key.Escape => KeyboardKey.Escape,
|
||||
Key.Enter => KeyboardKey.Enter,
|
||||
Key.Tab => KeyboardKey.Tab,
|
||||
Key.Backspace => KeyboardKey.Backspace,
|
||||
Key.Insert => KeyboardKey.Insert,
|
||||
Key.Delete => KeyboardKey.Delete,
|
||||
Key.Home => KeyboardKey.Home,
|
||||
Key.End => KeyboardKey.End,
|
||||
Key.PageUp => KeyboardKey.PageUp,
|
||||
Key.PageDown => KeyboardKey.PageDown,
|
||||
Key.Left => KeyboardKey.Left,
|
||||
Key.Right => KeyboardKey.Right,
|
||||
Key.Up => KeyboardKey.Up,
|
||||
Key.Down => KeyboardKey.Down,
|
||||
Key.A => KeyboardKey.A,
|
||||
Key.B => KeyboardKey.B,
|
||||
Key.C => KeyboardKey.C,
|
||||
Key.D => KeyboardKey.D,
|
||||
Key.E => KeyboardKey.E,
|
||||
Key.F => KeyboardKey.F,
|
||||
Key.G => KeyboardKey.G,
|
||||
Key.H => KeyboardKey.H,
|
||||
Key.I => KeyboardKey.I,
|
||||
Key.J => KeyboardKey.J,
|
||||
Key.K => KeyboardKey.K,
|
||||
Key.L => KeyboardKey.L,
|
||||
Key.M => KeyboardKey.M,
|
||||
Key.N => KeyboardKey.N,
|
||||
Key.O => KeyboardKey.O,
|
||||
Key.P => KeyboardKey.P,
|
||||
Key.Q => KeyboardKey.Q,
|
||||
Key.R => KeyboardKey.R,
|
||||
Key.S => KeyboardKey.S,
|
||||
Key.T => KeyboardKey.T,
|
||||
Key.U => KeyboardKey.U,
|
||||
Key.V => KeyboardKey.V,
|
||||
Key.W => KeyboardKey.W,
|
||||
Key.X => KeyboardKey.X,
|
||||
Key.Y => KeyboardKey.Y,
|
||||
Key.Z => KeyboardKey.Z,
|
||||
Key.Zero => KeyboardKey.Zero,
|
||||
Key.One => KeyboardKey.One,
|
||||
Key.Two => KeyboardKey.Two,
|
||||
Key.Three => KeyboardKey.Three,
|
||||
Key.Four => KeyboardKey.Four,
|
||||
Key.Five => KeyboardKey.Five,
|
||||
Key.Six => KeyboardKey.Six,
|
||||
Key.Seven => KeyboardKey.Seven,
|
||||
Key.Eight => KeyboardKey.Eight,
|
||||
Key.Nine => KeyboardKey.Nine,
|
||||
Key.F1 => KeyboardKey.F1,
|
||||
Key.F2 => KeyboardKey.F2,
|
||||
Key.F3 => KeyboardKey.F3,
|
||||
Key.F4 => KeyboardKey.F4,
|
||||
Key.F5 => KeyboardKey.F5,
|
||||
Key.F6 => KeyboardKey.F6,
|
||||
Key.F7 => KeyboardKey.F7,
|
||||
Key.F8 => KeyboardKey.F8,
|
||||
Key.F9 => KeyboardKey.F9,
|
||||
Key.F10 => KeyboardKey.F10,
|
||||
Key.F11 => KeyboardKey.F11,
|
||||
Key.F12 => KeyboardKey.F12,
|
||||
Key.LeftShift => KeyboardKey.LeftShift,
|
||||
Key.LeftControl => KeyboardKey.LeftControl,
|
||||
Key.LeftAlt => KeyboardKey.LeftAlt,
|
||||
Key.RightShift => KeyboardKey.RightShift,
|
||||
Key.RightControl => KeyboardKey.RightControl,
|
||||
Key.RightAlt => KeyboardKey.RightAlt,
|
||||
_ => KeyboardKey.Null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Engine.Core;
|
||||
using Engine.Graphics;
|
||||
using Raylib_cs;
|
||||
|
||||
namespace Engine.Graphics.RaylibBackend;
|
||||
|
||||
/// <summary>
|
||||
/// Raylib implementation of the render HAL context.
|
||||
/// Creates and owns a <see cref="RaylibWindow"/> (GLFW-based).
|
||||
/// No SDL3 dependency — the Raylib window handles both rendering and input.
|
||||
/// </summary>
|
||||
public sealed class RaylibRenderContext : IRenderContext
|
||||
{
|
||||
private readonly RaylibWindow _window;
|
||||
|
||||
public IWindow Window => _window;
|
||||
|
||||
public RaylibRenderContext(int width, int height, bool enableValidation = false)
|
||||
{
|
||||
_window = new RaylibWindow("Cortex Engine", width, height);
|
||||
}
|
||||
|
||||
public IRenderer CreateRenderer() => new RaylibRenderer();
|
||||
|
||||
public void Resize(int width, int height) => Raylib.SetWindowSize(width, height);
|
||||
|
||||
public void Dispose() => _window.Dispose();
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
using EngineMaterial = Engine.Core.Components.Material;
|
||||
using EngineMesh = Engine.Core.Components.Mesh;
|
||||
using EngineTransform = Engine.Core.Components.Transform;
|
||||
using Flecs.NET.Core;
|
||||
using Raylib_cs;
|
||||
|
||||
namespace Engine.Graphics.RaylibBackend;
|
||||
|
||||
/// <summary>
|
||||
/// Raylib implementation of the ECS world renderer.
|
||||
/// Renders Mesh + Transform + Material entities with up to four directional lights.
|
||||
/// </summary>
|
||||
public sealed class RaylibRenderer : IRenderer
|
||||
{
|
||||
private readonly Shader _shader;
|
||||
private readonly Dictionary<Entity, Raylib_cs.Model> _modelCache = new();
|
||||
private readonly Dictionary<string, Texture2D> _textureCache = new();
|
||||
private readonly int _materialColorLoc;
|
||||
private readonly int _useTextureLoc;
|
||||
private readonly int _roughnessLoc;
|
||||
private readonly int _metallicLoc;
|
||||
private readonly int _ambientLoc;
|
||||
private readonly int _viewPosLoc;
|
||||
private readonly int _lightCountLoc;
|
||||
private readonly int _lightDirLoc;
|
||||
private readonly int _lightIntensityLoc;
|
||||
private readonly int _lightColorLoc;
|
||||
private readonly float[] _lightDirs = new float[12]; // 4 lights * 3 floats
|
||||
private readonly float[] _lightIntensities = new float[4];
|
||||
private readonly float[] _lightColors = new float[12]; // 4 lights * 3 floats
|
||||
|
||||
private ScreenshotRequest? _pendingScreenshot;
|
||||
private int _frameCount;
|
||||
private bool _disposed;
|
||||
|
||||
public RaylibRenderer()
|
||||
{
|
||||
_shader = LoadShader();
|
||||
|
||||
_materialColorLoc = Raylib.GetShaderLocation(_shader, "materialColor");
|
||||
_useTextureLoc = Raylib.GetShaderLocation(_shader, "useTexture");
|
||||
_roughnessLoc = Raylib.GetShaderLocation(_shader, "roughness");
|
||||
_metallicLoc = Raylib.GetShaderLocation(_shader, "metallic");
|
||||
_ambientLoc = Raylib.GetShaderLocation(_shader, "ambientColor");
|
||||
_viewPosLoc = Raylib.GetShaderLocation(_shader, "viewPos");
|
||||
_lightCountLoc = Raylib.GetShaderLocation(_shader, "lightCount");
|
||||
_lightDirLoc = Raylib.GetShaderLocation(_shader, "lightDirs");
|
||||
_lightIntensityLoc = Raylib.GetShaderLocation(_shader, "lightIntensities");
|
||||
_lightColorLoc = Raylib.GetShaderLocation(_shader, "lightColors");
|
||||
}
|
||||
|
||||
public void RequestScreenshot(string outputPath)
|
||||
{
|
||||
_pendingScreenshot = new ScreenshotRequest(outputPath, null);
|
||||
}
|
||||
|
||||
public bool IsScreenshotRequested => _pendingScreenshot != null;
|
||||
|
||||
public IScreenshotProvider ScreenshotProvider => new RaylibScreenshotProvider(this);
|
||||
|
||||
public void RenderWorld(World world)
|
||||
{
|
||||
var camera = GetCamera(world);
|
||||
|
||||
Raylib.BeginDrawing();
|
||||
Raylib.ClearBackground(new Color(25, 30, 40, 255));
|
||||
Raylib.BeginMode3D(ToRaylib(camera));
|
||||
|
||||
Rlgl.DisableBackfaceCulling();
|
||||
|
||||
// Frame-level uniforms: SetShaderValue calls glUseProgram internally,
|
||||
// so these don't need BeginShaderMode. DrawModelEx rebinds the same shader
|
||||
// (set on the model's material), so the values persist for the draw call.
|
||||
CollectLights(world);
|
||||
SetFrameLights();
|
||||
Raylib.SetShaderValue(_shader, _viewPosLoc, new float[] { camera.Position.X, camera.Position.Y, camera.Position.Z }, ShaderUniformDataType.Vec3);
|
||||
|
||||
world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform transform) =>
|
||||
{
|
||||
if (e.Name() == "Grid")
|
||||
return;
|
||||
|
||||
var material = e.Has<EngineMaterial>() ? e.Get<EngineMaterial>() : EngineMaterial.Default;
|
||||
var model = GetOrUploadModel(e, mesh);
|
||||
var modelMatrix = transform.GetMatrix();
|
||||
|
||||
if (Matrix4x4.Decompose(modelMatrix, out var scale, out var rotation, out var position))
|
||||
{
|
||||
var axis = Vector3.UnitY;
|
||||
var angle = 0.0f;
|
||||
var q = new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W);
|
||||
if (MathF.Abs(q.W) < 0.9999999f)
|
||||
{
|
||||
angle = 2.0f * MathF.Acos(Math.Clamp(q.W, -1.0f, 1.0f));
|
||||
var s = MathF.Sqrt(1.0f - q.W * q.W);
|
||||
if (s > 0.0001f)
|
||||
axis = new Vector3(q.X / s, q.Y / s, q.Z / s);
|
||||
else
|
||||
axis = new Vector3(q.X, q.Y, q.Z);
|
||||
}
|
||||
|
||||
// Set per-entity uniforms right before the draw.
|
||||
// DrawModelEx binds the model's material shader (= _shader) and
|
||||
// immediately issues the draw, so these values are live during rendering.
|
||||
SetMaterialUniforms(material, model);
|
||||
Raylib.DrawModelEx(model, position, axis, angle * 180.0f / MathF.PI, scale, Color.White);
|
||||
}
|
||||
});
|
||||
|
||||
Rlgl.EnableBackfaceCulling();
|
||||
|
||||
Raylib.DrawGrid(20, 1.0f);
|
||||
|
||||
Raylib.EndMode3D();
|
||||
Raylib.EndDrawing();
|
||||
|
||||
// Defer the first screenshot by a few frames. Raylib may return a blank image
|
||||
// if the window/GPU has not finished presenting the first frame.
|
||||
if (_pendingScreenshot is { } request && _frameCount >= 10)
|
||||
{
|
||||
CaptureScreenshot(request);
|
||||
_pendingScreenshot = null;
|
||||
}
|
||||
|
||||
_frameCount++;
|
||||
}
|
||||
|
||||
private Camera3D ToRaylib(Camera camera)
|
||||
{
|
||||
return new Camera3D
|
||||
{
|
||||
Position = camera.Position,
|
||||
Target = camera.Target,
|
||||
Up = camera.Up,
|
||||
FovY = camera.FieldOfView * 180.0f / MathF.PI,
|
||||
Projection = CameraProjection.Perspective
|
||||
};
|
||||
}
|
||||
|
||||
private Camera GetCamera(World world)
|
||||
{
|
||||
var width = Raylib.GetScreenWidth();
|
||||
var height = Raylib.GetScreenHeight();
|
||||
var aspect = height > 0 ? (float)width / height : 16f / 9f;
|
||||
|
||||
var camera = new Camera(
|
||||
new Vector3(0.0f, 0.75f, -30.0f),
|
||||
new Vector3(0.0f, 0.5f, 0.0f),
|
||||
Vector3.UnitY,
|
||||
MathF.PI / 12.0f,
|
||||
aspect,
|
||||
0.1f,
|
||||
100.0f);
|
||||
|
||||
world.Each((Entity e, ref Camera cam) =>
|
||||
{
|
||||
camera = cam;
|
||||
});
|
||||
|
||||
camera.AspectRatio = aspect;
|
||||
return camera;
|
||||
}
|
||||
|
||||
private void CollectLights(World world)
|
||||
{
|
||||
var count = 0;
|
||||
world.Each((Entity e, ref Light light) =>
|
||||
{
|
||||
if (count >= 4)
|
||||
return;
|
||||
_lightDirs[count * 3 + 0] = light.Direction.X;
|
||||
_lightDirs[count * 3 + 1] = light.Direction.Y;
|
||||
_lightDirs[count * 3 + 2] = light.Direction.Z;
|
||||
_lightIntensities[count] = light.Intensity;
|
||||
_lightColors[count * 3 + 0] = light.Color.X;
|
||||
_lightColors[count * 3 + 1] = light.Color.Y;
|
||||
_lightColors[count * 3 + 2] = light.Color.Z;
|
||||
count++;
|
||||
});
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
_lightDirs[0] = 0.5f; _lightDirs[1] = -1.0f; _lightDirs[2] = -0.5f;
|
||||
_lightIntensities[0] = 1.0f;
|
||||
_lightColors[0] = 1.0f; _lightColors[1] = 0.95f; _lightColors[2] = 0.8f;
|
||||
count = 1;
|
||||
}
|
||||
|
||||
for (var i = count; i < 4; i++)
|
||||
{
|
||||
_lightDirs[i * 3 + 0] = 0;
|
||||
_lightDirs[i * 3 + 1] = 0;
|
||||
_lightDirs[i * 3 + 2] = 0;
|
||||
_lightIntensities[i] = 0.0f;
|
||||
_lightColors[i * 3 + 0] = 0;
|
||||
_lightColors[i * 3 + 1] = 0;
|
||||
_lightColors[i * 3 + 2] = 0;
|
||||
}
|
||||
|
||||
Raylib.SetShaderValue(_shader, _lightCountLoc, count, ShaderUniformDataType.Int);
|
||||
Raylib.SetShaderValueV(_shader, _lightDirLoc, _lightDirs, ShaderUniformDataType.Vec3, 4);
|
||||
Raylib.SetShaderValueV(_shader, _lightIntensityLoc, _lightIntensities, ShaderUniformDataType.Float, 4);
|
||||
Raylib.SetShaderValueV(_shader, _lightColorLoc, _lightColors, ShaderUniformDataType.Vec3, 4);
|
||||
}
|
||||
|
||||
private void SetFrameLights()
|
||||
{
|
||||
Raylib.SetShaderValue(_shader, _ambientLoc, new float[] { 0.35f, 0.35f, 0.4f }, ShaderUniformDataType.Vec3);
|
||||
}
|
||||
|
||||
private unsafe void SetMaterialUniforms(EngineMaterial material, Raylib_cs.Model model)
|
||||
{
|
||||
Raylib.SetShaderValue(_shader, _materialColorLoc, new float[] { material.Albedo.X, material.Albedo.Y, material.Albedo.Z, 1.0f }, ShaderUniformDataType.Vec4);
|
||||
Raylib.SetShaderValue(_shader, _roughnessLoc, material.Roughness, ShaderUniformDataType.Float);
|
||||
Raylib.SetShaderValue(_shader, _metallicLoc, material.Metallic, ShaderUniformDataType.Float);
|
||||
|
||||
if (material.HasTexture && File.Exists(material.TexturePath!))
|
||||
{
|
||||
Raylib.SetShaderValue(_shader, _useTextureLoc, 1, ShaderUniformDataType.Int);
|
||||
var texture = GetOrLoadTexture(material.TexturePath!);
|
||||
Raylib.SetMaterialTexture(ref model.Materials[0], MaterialMapIndex.Albedo, texture);
|
||||
}
|
||||
else
|
||||
{
|
||||
Raylib.SetShaderValue(_shader, _useTextureLoc, 0, ShaderUniformDataType.Int);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe Raylib_cs.Model GetOrUploadModel(Entity e, EngineMesh mesh)
|
||||
{
|
||||
if (_modelCache.TryGetValue(e, out var model))
|
||||
return model;
|
||||
|
||||
// Use Raylib's native mesh generation when possible — the manual UploadMesh
|
||||
// + LoadModelFromMesh path is unreliable for larger meshes because
|
||||
// LoadModelFromMesh reads CPU-side vertex pointers after UploadMesh.
|
||||
// For custom meshes (from OBJ/GLTF loaders), keep the CPU data alive.
|
||||
var raylibMesh = UploadRaylibMesh(mesh);
|
||||
model = Raylib.LoadModelFromMesh(raylibMesh);
|
||||
|
||||
for (var i = 0; i < model.MaterialCount; i++)
|
||||
{
|
||||
model.Materials[i].Shader = _shader;
|
||||
}
|
||||
_modelCache[e] = model;
|
||||
return model;
|
||||
}
|
||||
|
||||
private unsafe Raylib_cs.Mesh UploadRaylibMesh(EngineMesh mesh)
|
||||
{
|
||||
var vertexCount = mesh.Vertices.Length;
|
||||
var triangleCount = mesh.Indices.Length / 3;
|
||||
|
||||
var raylibMesh = new Raylib_cs.Mesh
|
||||
{
|
||||
VertexCount = vertexCount,
|
||||
TriangleCount = triangleCount
|
||||
};
|
||||
|
||||
var positionSize = vertexCount * 3 * sizeof(float);
|
||||
var normalSize = vertexCount * 3 * sizeof(float);
|
||||
var colorSize = vertexCount * 4;
|
||||
var texcoordSize = vertexCount * 2 * sizeof(float);
|
||||
var indexSize = mesh.Indices.Length * sizeof(ushort);
|
||||
|
||||
// Use NativeMemory.Alloc so Raylib's UnloadMesh can free with RL_FREE (free).
|
||||
var positionPtr = (float*)NativeMemory.Alloc((nuint)positionSize, 4);
|
||||
var normalPtr = (float*)NativeMemory.Alloc((nuint)normalSize, 4);
|
||||
var colorPtr = (byte*)NativeMemory.Alloc((nuint)colorSize, 1);
|
||||
var texcoordPtr = (float*)NativeMemory.Alloc((nuint)texcoordSize, 4);
|
||||
var indexPtr = (ushort*)NativeMemory.Alloc((nuint)indexSize, 2);
|
||||
|
||||
for (var i = 0; i < vertexCount; i++)
|
||||
{
|
||||
var v = mesh.Vertices[i];
|
||||
positionPtr[i * 3 + 0] = v.Position.X;
|
||||
positionPtr[i * 3 + 1] = v.Position.Y;
|
||||
positionPtr[i * 3 + 2] = v.Position.Z;
|
||||
|
||||
normalPtr[i * 3 + 0] = v.Normal.X;
|
||||
normalPtr[i * 3 + 1] = v.Normal.Y;
|
||||
normalPtr[i * 3 + 2] = v.Normal.Z;
|
||||
|
||||
colorPtr[i * 4 + 0] = (byte)Math.Clamp(v.Color.X * 255.0f, 0.0f, 255.0f);
|
||||
colorPtr[i * 4 + 1] = (byte)Math.Clamp(v.Color.Y * 255.0f, 0.0f, 255.0f);
|
||||
colorPtr[i * 4 + 2] = (byte)Math.Clamp(v.Color.Z * 255.0f, 0.0f, 255.0f);
|
||||
colorPtr[i * 4 + 3] = 255;
|
||||
|
||||
texcoordPtr[i * 2 + 0] = v.Position.X;
|
||||
texcoordPtr[i * 2 + 1] = v.Position.Z;
|
||||
}
|
||||
|
||||
for (var i = 0; i < mesh.Indices.Length; i++)
|
||||
indexPtr[i] = (ushort)mesh.Indices[i];
|
||||
|
||||
raylibMesh.Vertices = positionPtr;
|
||||
raylibMesh.Normals = normalPtr;
|
||||
raylibMesh.Colors = colorPtr;
|
||||
raylibMesh.TexCoords = texcoordPtr;
|
||||
raylibMesh.Indices = indexPtr;
|
||||
|
||||
Raylib.UploadMesh(ref raylibMesh, false);
|
||||
|
||||
// Keep CPU-side data alive — LoadModelFromMesh reads these pointers
|
||||
// to compute the bounding box. They will be freed when the model is unloaded.
|
||||
return raylibMesh;
|
||||
}
|
||||
|
||||
private Texture2D GetOrLoadTexture(string path)
|
||||
{
|
||||
if (_textureCache.TryGetValue(path, out var texture))
|
||||
return texture;
|
||||
|
||||
texture = Raylib.LoadTexture(path);
|
||||
Raylib.SetTextureWrap(texture, TextureWrap.Repeat);
|
||||
Raylib.SetTextureFilter(texture, TextureFilter.Trilinear);
|
||||
_textureCache[path] = texture;
|
||||
return texture;
|
||||
}
|
||||
|
||||
private unsafe void CaptureScreenshot(ScreenshotRequest request)
|
||||
{
|
||||
var image = Raylib.LoadImageFromScreen();
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(request.Path);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
Raylib.ExportImage(image, request.Path);
|
||||
|
||||
if (request.Tcs != null)
|
||||
{
|
||||
var size = 0;
|
||||
var fileType = stackalloc byte[] { (byte)'.', (byte)'p', (byte)'n', (byte)'g', 0 };
|
||||
var data = Raylib.ExportImageToMemory(image, (sbyte*)fileType, &size);
|
||||
var bytes = new byte[size];
|
||||
fixed (byte* p = bytes)
|
||||
{
|
||||
Buffer.MemoryCopy(data, p, size, size);
|
||||
}
|
||||
Raylib.MemFree(data);
|
||||
request.Tcs.TrySetResult(bytes);
|
||||
}
|
||||
|
||||
Console.WriteLine($"Screenshot saved: {request.Path}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Raylib.UnloadImage(image);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<byte[]> CaptureAsync(string outputPath)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_pendingScreenshot = new ScreenshotRequest(outputPath, tcs);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static Shader LoadShader()
|
||||
{
|
||||
const string VertexSource = @"#version 330 core
|
||||
in vec3 vertexPosition;
|
||||
in vec2 vertexTexCoord;
|
||||
in vec3 vertexNormal;
|
||||
in vec4 vertexColor;
|
||||
uniform mat4 mvp;
|
||||
uniform mat4 matModel;
|
||||
out vec3 vNormal;
|
||||
out vec3 vWorldPos;
|
||||
out vec4 vColor;
|
||||
out vec2 vTexCoord;
|
||||
void main()
|
||||
{
|
||||
vec4 worldPos = matModel * vec4(vertexPosition, 1.0);
|
||||
vWorldPos = worldPos.xyz;
|
||||
vNormal = mat3(transpose(inverse(matModel))) * vertexNormal;
|
||||
vColor = vertexColor;
|
||||
vTexCoord = vertexTexCoord;
|
||||
gl_Position = mvp * vec4(vertexPosition, 1.0);
|
||||
}";
|
||||
|
||||
const string FragmentSource = @"#version 330 core
|
||||
in vec3 vNormal;
|
||||
in vec3 vWorldPos;
|
||||
in vec4 vColor;
|
||||
in vec2 vTexCoord;
|
||||
out vec4 finalColor;
|
||||
uniform vec4 materialColor;
|
||||
uniform int useTexture;
|
||||
uniform sampler2D texture0;
|
||||
uniform float roughness;
|
||||
uniform float metallic;
|
||||
uniform vec3 viewPos;
|
||||
uniform vec3 ambientColor;
|
||||
uniform int lightCount;
|
||||
uniform vec3 lightDirs[4];
|
||||
uniform float lightIntensities[4];
|
||||
uniform vec3 lightColors[4];
|
||||
|
||||
vec3 ACESFilm(vec3 x)
|
||||
{
|
||||
const float a = 2.51; const float b = 0.03; const float c = 2.43; const float d = 0.59; const float e = 0.14;
|
||||
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 normal = normalize(vNormal);
|
||||
vec3 albedo = vColor.rgb * materialColor.rgb;
|
||||
if (useTexture != 0)
|
||||
{
|
||||
vec2 uv = vTexCoord * 4.0;
|
||||
albedo *= texture(texture0, uv).rgb;
|
||||
}
|
||||
vec3 viewDir = normalize(viewPos - vWorldPos);
|
||||
float rough = clamp(roughness, 0.05, 1.0);
|
||||
float metal = clamp(metallic, 0.0, 1.0);
|
||||
|
||||
// Hemisphere ambient: low ambient for visible shading contrast
|
||||
vec3 skyColor = ambientColor;
|
||||
vec3 groundColor = ambientColor * 0.2;
|
||||
float hemisphere = 0.5 + 0.5 * normal.y;
|
||||
vec3 result = albedo * mix(groundColor, skyColor, hemisphere) * 0.4;
|
||||
|
||||
vec3 F0 = mix(vec3(0.04), albedo, metal);
|
||||
float shininess = mix(8.0, 256.0, 1.0 - rough);
|
||||
|
||||
for (int i = 0; i < lightCount; i++)
|
||||
{
|
||||
vec3 L = normalize(-lightDirs[i]);
|
||||
vec3 H = normalize(L + viewDir);
|
||||
|
||||
float NdotL = max(dot(normal, L), 0.0);
|
||||
float NdotH = max(dot(normal, H), 0.0);
|
||||
float NdotV = max(dot(normal, viewDir), 0.0);
|
||||
float HdotV = max(dot(H, viewDir), 0.0);
|
||||
|
||||
float diff = NdotL;
|
||||
float spec = pow(NdotH, shininess);
|
||||
|
||||
// Schlick Fresnel
|
||||
float fresnel = F0.x + (1.0 - F0.x) * pow(1.0 - HdotV, 5.0);
|
||||
vec3 specularColor = mix(vec3(fresnel), albedo * fresnel, metal);
|
||||
|
||||
vec3 diffuse = albedo * lightColors[i] * diff * lightIntensities[i] * 1.5;
|
||||
vec3 specular = specularColor * spec * lightIntensities[i];
|
||||
|
||||
// Energy conservation
|
||||
diffuse *= (1.0 - fresnel * (1.0 - metal * 0.5));
|
||||
|
||||
result += diffuse + specular;
|
||||
}
|
||||
|
||||
// ACES tonemapping + gamma correction
|
||||
result = ACESFilm(result * 1.2);
|
||||
result = pow(result, vec3(1.0 / 2.2));
|
||||
|
||||
finalColor = vec4(result, 1.0);
|
||||
}";
|
||||
|
||||
return Raylib.LoadShaderFromMemory(VertexSource, FragmentSource);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
foreach (var model in _modelCache.Values)
|
||||
Raylib.UnloadModel(model);
|
||||
_modelCache.Clear();
|
||||
|
||||
foreach (var texture in _textureCache.Values)
|
||||
Raylib.UnloadTexture(texture);
|
||||
_textureCache.Clear();
|
||||
|
||||
Raylib.UnloadShader(_shader);
|
||||
}
|
||||
|
||||
private readonly record struct ScreenshotRequest(string Path, TaskCompletionSource<byte[]>? Tcs);
|
||||
|
||||
private sealed class RaylibScreenshotProvider : IScreenshotProvider
|
||||
{
|
||||
private readonly RaylibRenderer _renderer;
|
||||
|
||||
public RaylibScreenshotProvider(RaylibRenderer renderer)
|
||||
{
|
||||
_renderer = renderer;
|
||||
}
|
||||
|
||||
public Task<byte[]> CaptureAsync(string outputPath) => _renderer.CaptureAsync(outputPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using Engine.Core;
|
||||
using Raylib_cs;
|
||||
|
||||
namespace Engine.Graphics.RaylibBackend;
|
||||
|
||||
/// <summary>
|
||||
/// Raylib-backed implementation of <see cref="IWindow"/>.
|
||||
/// Wraps Raylib's GLFW window creation, event polling, and input.
|
||||
/// </summary>
|
||||
public sealed class RaylibWindow : IWindow
|
||||
{
|
||||
private readonly RaylibInputState _input = new();
|
||||
private bool _shouldClose;
|
||||
private bool _disposed;
|
||||
|
||||
public int Width => Raylib.GetScreenWidth();
|
||||
public int Height => Raylib.GetScreenHeight();
|
||||
public bool ShouldClose => _shouldClose;
|
||||
public IInputState Input => _input;
|
||||
public nint Handle => 0;
|
||||
|
||||
public RaylibWindow(string title, int width, int height)
|
||||
{
|
||||
Raylib.SetConfigFlags(ConfigFlags.VSyncHint);
|
||||
Raylib.InitWindow(width, height, title);
|
||||
Raylib.SetTargetFPS(0);
|
||||
|
||||
// Present a blank frame so the window is visible immediately.
|
||||
Raylib.BeginDrawing();
|
||||
Raylib.ClearBackground(new Color(25, 30, 40, 255));
|
||||
Raylib.EndDrawing();
|
||||
}
|
||||
|
||||
public void PumpEvents()
|
||||
{
|
||||
_input.Poll();
|
||||
_shouldClose = Raylib.WindowShouldClose() || _shouldClose;
|
||||
|
||||
if (Raylib.IsKeyPressed(KeyboardKey.Escape))
|
||||
_shouldClose = true;
|
||||
}
|
||||
|
||||
public void Close() => _shouldClose = true;
|
||||
|
||||
public string[] GetRequiredVulkanExtensions() => Array.Empty<string>();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
Raylib.CloseWindow();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user