Graphics
KeenEyes provides a modular graphics architecture with backend-agnostic abstractions and Silk.NET OpenGL implementation.
Architecture Overview
The graphics system is split across multiple packages:
| Package | Purpose |
|---|---|
KeenEyes.Graphics.Abstractions |
Backend-agnostic interfaces and components |
KeenEyes.Graphics |
Backend-agnostic systems (CameraSystem, RenderSystem) |
KeenEyes.Graphics.Silk |
Silk.NET OpenGL implementation |
KeenEyes.Runtime |
Main loop builder (WorldRunnerBuilder) |
This separation allows:
- Swapping rendering backends without changing game code
- Writing backend-agnostic plugins and systems
- Testing graphics logic without GPU dependencies
Requirements and capability detection
The Silk.NET backend requires OpenGL 3.3 or newer. Every built-in shader declares
#version 330 core, so this is a hard floor, not a recommendation.
When the window loads, the backend asks the device for its identity via
IGraphicsDevice.GetDeviceInfo(), which returns a GraphicsDeviceInfo carrying the driver's
vendor, renderer, version, and shading language version strings plus the parsed major/minor
version. It then:
- Rejects a driver below 3.3 by throwing
UnsupportedGraphicsDeviceExceptionbefore any shader or renderer is created. The message states the detected version, the driver's renderer and vendor strings, the required version, and the likely remedy - installing the GPU vendor's driver. The exception also exposes theGraphicsDeviceInfofor callers that want to present it themselves. - Logs one line naming the driver it did get (
Debug.WriteLine), so a session's OpenGL and shading language versions appear in diagnostic output even when everything works.
Shader compilation and linking failures also append the driver's OpenGL and shading language versions to the info log, so a version mismatch is self-evident in the error text.
The classic cause of a below-minimum driver on Windows is that no vendor GPU driver is installed, which leaves the Microsoft Basic Display Adapter software fallback in place; it reports OpenGL 1.1, as do remote-desktop sessions.
Quick Start
using KeenEyes;
using KeenEyes.Graphics;
using KeenEyes.Graphics.Abstractions;
using KeenEyes.Graphics.Silk;
using KeenEyes.Platform.Silk;
using KeenEyes.Runtime;
// Configure the window
var windowConfig = new WindowConfig
{
Title = "My Game",
Width = 1280,
Height = 720,
VSync = true
};
// Configure graphics rendering
var graphicsConfig = new SilkGraphicsConfig
{
ClearColor = new Vector4(0.2f, 0.3f, 0.4f, 1f),
EnableDepthTest = true,
EnableCulling = true
};
// Create world and install plugins
using var world = new World();
world.InstallPlugin(new SilkWindowPlugin(windowConfig)); // Window first
world.InstallPlugin(new SilkGraphicsPlugin(graphicsConfig));
// Add backend-agnostic systems
world.AddSystem<CameraSystem>(SystemPhase.EarlyUpdate);
world.AddSystem<RenderSystem>(SystemPhase.Render);
// Run with the builder pattern (recommended)
world.CreateRunner()
.OnReady(() => CreateScene(world))
.Run(); // Blocks until window closes, auto-calls world.Update()
Main Loop Setup
Recommended: WorldRunnerBuilder
The WorldRunnerBuilder provides a clean, backend-agnostic way to run your game:
world.CreateRunner()
.OnReady(() =>
{
// Called once when graphics are ready
var graphics = world.GetExtension<IGraphicsContext>();
CreateScene(world, graphics);
})
.OnResize((width, height) =>
{
Console.WriteLine($"Window resized to {width}x{height}");
})
.OnClosing(() =>
{
Console.WriteLine("Goodbye!");
})
.Run(); // Blocks until closed
Key features:
- Auto-update:
world.Update(dt)is called automatically each frame - Backend-agnostic: Works with any
ILoopProviderimplementation - Clean syntax: No manual event wiring
macOS: call
Run()on the process main thread.Run()creates the OS window, and AppKit only permits that on the thread the process started on. AnawaitbeforeRun()moves the rest of an asyncMainonto a thread-pool thread and the Mac build dies at startup, even though Windows and Linux are happy. See Runtime: threading.
Explicit Update Control
For custom update logic, provide an OnUpdate callback:
world.CreateRunner()
.OnReady(() => CreateScene(world))
.OnUpdate((dt) =>
{
// Custom logic before update
HandleInput();
// Manually call world update
world.Update(dt);
// Custom logic after update
UpdateUI();
})
.Run();
Alternative: Direct Event Wiring
For advanced scenarios, you can wire events directly on ILoopProvider:
var loopProvider = world.GetExtension<ILoopProvider>();
loopProvider.OnReady += () => CreateScene(world);
loopProvider.OnUpdate += (dt) => world.Update(dt);
loopProvider.OnResize += (w, h) => Console.WriteLine($"Resized: {w}x{h}");
loopProvider.OnClosing += () => Console.WriteLine("Closing...");
loopProvider.Initialize();
loopProvider.Run(); // Blocks until closed
Configuration
Window Configuration
var windowConfig = new WindowConfig
{
Title = "My Game", // Window title
Width = 1920, // Initial window width
Height = 1080, // Initial window height
VSync = true, // Enable vertical sync
Resizable = true, // Allow window resizing
TargetFramerate = 60.0, // Target FPS (0 = uncapped)
TargetUpdateFrequency = 60.0 // Target fixed update rate
};
Graphics Configuration
var graphicsConfig = new SilkGraphicsConfig
{
ClearColor = new Vector4(0.1f, 0.1f, 0.1f, 1f), // Background color
EnableDepthTest = true, // Enable depth testing
EnableCulling = true // Enable backface culling
};
Components
All graphics components are in KeenEyes.Graphics.Abstractions for backend independence.
Camera
Defines a viewpoint for rendering:
// Perspective camera (3D with depth)
world.Spawn()
.With(new Transform3D(new Vector3(0, 5, 10), Quaternion.Identity, Vector3.One))
.With(Camera.CreatePerspective(
fieldOfView: 60f, // Vertical FOV in degrees
aspectRatio: 16f/9f, // Width / Height
nearPlane: 0.1f, // Near clip distance
farPlane: 1000f)) // Far clip distance
.WithTag<MainCameraTag>()
.Build();
// Orthographic camera (2D-like, no perspective)
world.Spawn()
.With(Transform3D.Identity)
.With(Camera.CreateOrthographic(
size: 5f, // Half-height of view
aspectRatio: 16f/9f,
nearPlane: 0.1f,
farPlane: 100f))
.Build();
Camera properties:
Priority- Higher values render later (on top)Viewport- Normalized screen region (x, y, width, height)ClearColor- Background color for this cameraClearColorBuffer/ClearDepthBuffer- What to clear before rendering
Renderable
Marks an entity for rendering:
var graphics = world.GetExtension<IGraphicsContext>();
var meshId = graphics.CreateCube();
world.Spawn()
.With(Transform3D.Identity)
.With(new Renderable(meshId.Id, layer: 0))
.With(new Material
{
ShaderId = graphics.LitShader.Id,
TextureId = graphics.WhiteTexture.Id,
Color = new Vector4(1, 0, 0, 1) // Red
})
.Build();
Material
Defines surface appearance:
var material = new Material
{
ShaderId = graphics.LitShader.Id,
TextureId = graphics.WhiteTexture.Id,
Color = new Vector4(1, 1, 1, 1), // White
Metallic = 0.8f, // Metallic look
Roughness = 0.2f // Smooth surface
};
Light
Illuminates the scene:
// Directional light (sun)
world.Spawn()
.With(new Transform3D(Vector3.Zero,
Quaternion.CreateFromYawPitchRoll(0.5f, -0.8f, 0), Vector3.One))
.With(Light.Directional(
color: new Vector3(1, 0.95f, 0.8f),
intensity: 1.0f))
.Build();
// Point light (bulb)
world.Spawn()
.With(new Transform3D(new Vector3(5, 3, 0), Quaternion.Identity, Vector3.One))
.With(Light.Point(
color: new Vector3(1, 0.8f, 0.6f),
intensity: 0.5f,
range: 15f))
.Build();
// Spot light (flashlight)
world.Spawn()
.With(new Transform3D(position, rotation, Vector3.One))
.With(Light.Spot(
color: new Vector3(1, 1, 1),
intensity: 2.0f,
range: 15f,
innerAngle: 15f,
outerAngle: 30f))
.Build();
Resource Management
Access resources through IGraphicsContext:
var graphics = world.GetExtension<IGraphicsContext>();
Meshes
// Built-in primitives
var quad = graphics.CreateQuad(width: 1f, height: 1f);
var cube = graphics.CreateCube(size: 1f);
// Delete when done
graphics.DeleteMesh(quad);
Textures
// From raw RGBA data
var texture = graphics.CreateTexture(256, 256, pixelData);
// Built-in white texture
var white = graphics.WhiteTexture;
// Delete when done
graphics.DeleteTexture(texture);
Shaders
// Built-in shaders
var lit = graphics.LitShader; // PBR lighting
var unlit = graphics.UnlitShader; // No lighting
var solid = graphics.SolidShader; // Solid color
// Custom GLSL shader
var custom = graphics.CreateShader(vertexSource, fragmentSource);
graphics.DeleteShader(custom);
Custom Shaders
KeenEyes uses GLSL shaders with the Silk.NET backend.
Shader Structure
Vertex Shader:
#version 330 core
layout (location = 0) in vec3 aPosition;
layout (location = 1) in vec2 aTexCoord;
layout (location = 2) in vec3 aNormal;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
out vec2 vTexCoord;
out vec3 vNormal;
out vec3 vFragPos;
void main()
{
vec4 worldPos = uModel * vec4(aPosition, 1.0);
gl_Position = uProjection * uView * worldPos;
vTexCoord = aTexCoord;
vNormal = mat3(transpose(inverse(uModel))) * aNormal;
vFragPos = worldPos.xyz;
}
Fragment Shader:
#version 330 core
in vec2 vTexCoord;
in vec3 vNormal;
in vec3 vFragPos;
uniform sampler2D uTexture;
uniform vec4 uColor;
out vec4 FragColor;
void main()
{
vec4 texColor = texture(uTexture, vTexCoord);
FragColor = texColor * uColor;
}
Creating Custom Shaders
var graphics = world.GetExtension<IGraphicsContext>();
// Load from strings
string vertexSource = File.ReadAllText("shaders/custom.vert");
string fragmentSource = File.ReadAllText("shaders/custom.frag");
var shader = graphics.CreateShader(vertexSource, fragmentSource);
// Use in material
var material = new Material
{
ShaderId = shader.Id,
TextureId = myTexture.Id,
Color = Vector4.One
};
// Clean up when done
graphics.DeleteShader(shader);
Built-in Shaders
| Shader | Purpose | Key Uniforms |
|---|---|---|
LitShader |
PBR lighting | uModel, uView, uProjection, light uniforms |
UnlitShader |
No lighting (2D, UI) | uModel, uView, uProjection, uTexture, uColor |
SolidShader |
Solid color fill | uModel, uView, uProjection, uColor |
Common Shader Patterns
Tinting:
// Apply color tint to texture
FragColor = texture(uTexture, vTexCoord) * uColor;
Alpha Cutout (for sprites):
vec4 texColor = texture(uTexture, vTexCoord);
if (texColor.a < 0.5)
discard;
FragColor = texColor * uColor;
Simple Gradient:
// Vertical gradient based on UV
float gradient = vTexCoord.y;
FragColor = mix(uColorTop, uColorBottom, gradient);
Systems
The following systems should be added to your world:
// Camera system - updates camera matrices based on window size
world.AddSystem<CameraSystem>(SystemPhase.EarlyUpdate, order: 0);
// Render system - draws all renderables
world.AddSystem<RenderSystem>(SystemPhase.Render, order: 0);
These systems are in KeenEyes.Graphics and work with any backend implementing IGraphicsContext.
Shadows
The KeenEyes.Graphics.Shadows namespace provides shadow mapping for directional, point, and spot lights. Directional lights use Cascaded Shadow Maps (CSM, up to 4 cascades); point lights use cubemap shadow maps; spot lights use a single 2D shadow map.
Enabling Shadows
Shadow casting is controlled per-light and per-renderable:
// Light.Directional() and Light.Spot() enable CastShadows by default;
// Light.Point() does not.
world.Spawn()
.With(new Transform3D(Vector3.Zero,
Quaternion.CreateFromYawPitchRoll(0.5f, -0.8f, 0), Vector3.One))
.With(Light.Directional(new Vector3(1, 0.95f, 0.8f), 1.0f)) // CastShadows = true
.Build();
// Renderable.CastShadows / ReceiveShadows default to true
var graphics = world.GetExtension<IGraphicsContext>();
var cube = graphics.CreateCube();
world.Spawn()
.With(Transform3D.Identity)
.With(new Renderable(cube.Id, 0) { CastShadows = true, ReceiveShadows = true })
.With(new Material { ShaderId = graphics.LitShader.Id })
.Build();
ShadowRenderingSystem
ShadowRenderingSystem renders the depth-only shadow passes for every shadow-casting light before the main color pass. It owns a ShadowMapManager, which allocates and updates the render targets and light-space matrices for each shadow-casting light. To have the main render pass actually sample the resulting shadow maps, assign the ShadowRenderingSystem instance to RenderSystem.ShadowSystem:
using KeenEyes.Graphics.Shadows;
var shadowSystem = new ShadowRenderingSystem
{
Settings = new ShadowSettings
{
Resolution = ShadowResolution.High,
FilterMode = ShadowFilterMode.Pcf3x3,
CascadeCount = 4,
CascadeSplitLambda = 0.75f,
MaxShadowDistance = 100f
}
};
var renderSystem = new RenderSystem { ShadowSystem = shadowSystem };
// Shadow pass must run before the color pass that samples it
world.AddSystem(shadowSystem, SystemPhase.Render, order: -1);
world.AddSystem(renderSystem, SystemPhase.Render, order: 0);
ShadowSettings (in KeenEyes.Graphics.Abstractions) controls quality:
| Property | Purpose |
|---|---|
Resolution |
ShadowResolution.Low/Medium/High/VeryHigh (512–4096px) |
FilterMode |
ShadowFilterMode.Hard/Pcf3x3/Pcf5x5/Pcf7x7 |
CascadeCount |
Number of directional light cascades (clamped 1-4) |
CascadeSplitLambda |
Blends uniform (0) vs. logarithmic (1) cascade splits |
MaxShadowDistance |
Distance from the camera beyond which shadows aren't rendered |
NormalBias / DepthBias |
Reduce shadow acne |
The cascade math itself (split distances, frustum corners, light-space matrices, cubemap face matrices, and texel-snapping stabilization) lives in the static CascadeUtils class, which ShadowMapManager calls internally each frame.
Debug Visualization
ShadowMapVisualizationSystem draws shadow maps as on-screen thumbnails for debugging. Point it at an existing ShadowRenderingSystem and add it after the render pass:
var shadowViz = new ShadowMapVisualizationSystem
{
ShadowSystem = shadowSystem,
Mode = ShadowVisualizationMode.Cascades, // Cascades, SpotLights, PointLights, or All
ShowCascadeColors = true
};
world.AddSystem(shadowViz, SystemPhase.Render, order: 1);
Call shadowViz.SetViewportSize(width, height) from your OnResize callback so thumbnails stay correctly positioned.
Instanced Rendering
InstanceBatchingSystem (in KeenEyes.Graphics) groups entities that share the same mesh, material, and a user-defined batch ID into a single GPU instanced draw call. This is intended for rendering many copies of the same object (trees, rocks, particles) with minimal CPU overhead.
Mark entities with the InstanceBatch component alongside Transform3D and Renderable:
using KeenEyes.Graphics.Abstractions;
// 1000 trees rendered with a single draw call per material/mesh combination
for (int i = 0; i < 1000; i++)
{
world.Spawn()
.With(new Transform3D(positions[i], Quaternion.Identity, Vector3.One))
.With(new Renderable(treeMeshId, treeMaterialId))
.With(new InstanceBatch(batchId: 1))
.Build();
}
world.AddSystem(new InstanceBatchingSystem(), SystemPhase.Render, order: -1);
world.AddSystem<RenderSystem>(SystemPhase.Render, order: 0);
InstanceBatch.ColorTint (a Vector4) is multiplied with the material's base color, giving per-instance color variation without separate materials. RenderSystem automatically skips any entity that has an InstanceBatch component — those entities are drawn by InstanceBatchingSystem itself, which issues the instanced draw calls directly during its Update.
Level of Detail (LOD)
LodSystem (in KeenEyes.Graphics) automatically swaps an entity's mesh based on distance from the camera or projected screen coverage, reducing triangle counts for distant objects. It should run before RenderSystem so the mesh swap happens before the object is drawn.
Attach a LodGroup component (in KeenEyes.Graphics.Abstractions) alongside Transform3D and Renderable:
using KeenEyes.Graphics.Abstractions;
var highDetail = graphics.CreateMesh(highVertices, highIndices);
var mediumDetail = graphics.CreateMesh(mediumVertices, mediumIndices);
var lowDetail = graphics.CreateMesh(lowVertices, lowIndices);
world.Spawn()
.With(Transform3D.Identity)
.With(new Renderable(highDetail.Id, materialId))
.With(LodGroup.Create(
new LodLevel(highDetail.Id, 0f), // Level 0: used up to 20 units
new LodLevel(mediumDetail.Id, 20f), // Level 1: used 20-50 units
new LodLevel(lowDetail.Id, 50f))) // Level 2: used beyond 50 units
.Build();
world.AddSystem(new LodSystem { HysteresisFactor = 0.1f });
world.AddSystem<RenderSystem>(SystemPhase.Render);
LodGroup.Create(...) overloads support 1-4 levels; each LodLevel(MeshId, Threshold) pairs a mesh handle with a distance (or screen-coverage) threshold. LodGroup.SelectionMode chooses between LodSelectionMode.Distance (threshold is world-space distance) and LodSelectionMode.ScreenSize (threshold is projected screen coverage, 0-1); LodGroup.Bias offsets the calculation per-entity. LodSystem.HysteresisFactor (default 0.05) widens the switch threshold in the direction the entity is currently trending, to prevent flickering when an entity sits near a boundary; LodSystem.GlobalBias applies the same kind of offset to every entity.
2D Rendering Patterns
While KeenEyes graphics primarily targets 3D, 2D games can use orthographic cameras and specialized patterns.
Orthographic Camera Setup
// Create an orthographic camera for 2D
world.Spawn()
.With(new Transform3D(new Vector3(0, 0, 10), Quaternion.Identity, Vector3.One))
.With(Camera.CreateOrthographic(
size: 5f, // Half-height (10 units total vertically)
aspectRatio: 16f/9f,
nearPlane: 0.1f,
farPlane: 100f))
.WithTag<MainCameraTag>()
.Build();
Sprite Rendering with Quads
var graphics = world.GetExtension<IGraphicsContext>();
var quad = graphics.CreateQuad(1f, 1f); // Unit quad
// Create a sprite entity
world.Spawn()
.With(new Transform3D(new Vector3(0, 0, 0), Quaternion.Identity, Vector3.One))
.With(new Renderable(quad.Id, layer: 0))
.With(new Material
{
ShaderId = graphics.UnlitShader.Id, // No lighting for 2D
TextureId = myTexture.Id,
Color = Vector4.One
})
.Build();
Layer-Based Rendering Order
Use Renderable.Layer for draw order. Lower layers render first (background), higher layers render on top (foreground):
// Background (layer 0)
CreateSprite(backgroundTexture, layer: 0, z: 0);
// Gameplay objects (layer 1)
CreateSprite(playerTexture, layer: 1, z: 0);
// Foreground decorations (layer 2)
CreateSprite(foregroundTexture, layer: 2, z: 0);
// UI (handled by UI system, renders on top)
Pixel-Perfect Rendering
For pixel-art games, calculate orthographic size from your virtual resolution:
int gameHeight = 180; // Virtual resolution height in pixels
float orthoSize = gameHeight / 2f; // Half-height for ortho camera
world.Spawn()
.With(Transform3D.Identity)
.With(Camera.CreateOrthographic(
size: orthoSize,
aspectRatio: (float)windowWidth / windowHeight,
nearPlane: 0.1f,
farPlane: 100f))
.WithTag<MainCameraTag>()
.Build();
Blend Modes
The 2D batch renderer (I2DRenderer in KeenEyes.Graphics.Abstractions) exposes a blend mode that applies to subsequent draw calls:
renderer.Begin();
renderer.FillRect(0, 0, 100, 100, backgroundColor); // Alpha (default)
renderer.SetBlendMode(BlendMode.Additive); // Flushes the queued alpha draws
renderer.FillCircle(50, 50, 20, glowColor); // Drawn additively
renderer.End();
The contract:
SetBlendMode(mode)- setting a mode different fromCurrentBlendModeflushes the current batch so already-queued geometry is drawn with the previous mode; draws issued afterwards use the new mode. Setting the same mode is a no-op.CurrentBlendMode- the mode applied to subsequent draw calls.Begin()resets the blend mode toBlendMode.Alpha.
BlendMode |
Source Factor | Destination Factor | Typical Use |
|---|---|---|---|
Alpha |
SrcAlpha |
OneMinusSrcAlpha |
Standard alpha blending (default) |
Additive |
SrcAlpha |
One |
Glow, fire, light effects |
Multiply |
DstColor |
OneMinusSrcAlpha |
Darkening, shadows, tinting |
Premultiplied |
One |
OneMinusSrcAlpha |
Textures with premultiplied alpha |
BlendModeExtensions.ToBlendFactors() converts a BlendMode to its (BlendFactor Src, BlendFactor Dst) pair for custom renderer implementations. The particle system's ParticleRenderSystem is the canonical consumer: it groups emitters by blend mode and issues one batch per mode (see Particles).
Sprite Animation
Animate sprites by updating texture source rectangles or swapping textures:
public class SpriteAnimationSystem : ISystem
{
public void Update(float dt)
{
foreach (var entity in world.Query<SpriteAnimation, Material>())
{
ref var anim = ref world.Get<SpriteAnimation>(entity);
ref var material = ref world.Get<Material>(entity);
anim.Timer += dt;
if (anim.Timer >= anim.FrameDuration)
{
anim.Timer = 0;
anim.CurrentFrame = (anim.CurrentFrame + 1) % anim.FrameCount;
material.TextureId = anim.Frames[anim.CurrentFrame];
}
}
}
}
Complete Example
using System.Numerics;
using KeenEyes;
using KeenEyes.Common;
using KeenEyes.Graphics;
using KeenEyes.Graphics.Abstractions;
using KeenEyes.Graphics.Silk;
using KeenEyes.Platform.Silk;
using KeenEyes.Runtime;
var windowConfig = new WindowConfig
{
Title = "Spinning Cube",
Width = 1280,
Height = 720,
VSync = true
};
var graphicsConfig = new SilkGraphicsConfig
{
ClearColor = new Vector4(0.2f, 0.3f, 0.4f, 1f),
EnableDepthTest = true,
EnableCulling = true
};
using var world = new World();
world.InstallPlugin(new SilkWindowPlugin(windowConfig));
world.InstallPlugin(new SilkGraphicsPlugin(graphicsConfig));
// Add systems
world.AddSystem<CameraSystem>(SystemPhase.EarlyUpdate);
world.AddSystem<RenderSystem>(SystemPhase.Render);
world.AddSystem<SpinSystem>(SystemPhase.Update); // Custom system
// Run with builder pattern
world.CreateRunner()
.OnReady(() =>
{
var graphics = world.GetExtension<IGraphicsContext>();
// Create camera
world.Spawn()
.With(new Transform3D(new Vector3(0, 2, 5), Quaternion.Identity, Vector3.One))
.With(Camera.CreatePerspective(60f, 16f/9f, 0.1f, 100f))
.WithTag<MainCameraTag>()
.Build();
// Create light
world.Spawn()
.With(new Transform3D(Vector3.Zero,
Quaternion.CreateFromYawPitchRoll(0.5f, -0.5f, 0), Vector3.One))
.With(Light.Directional(new Vector3(1, 1, 1), 1f))
.Build();
// Create spinning cube
var cube = graphics.CreateCube();
world.Spawn()
.With(Transform3D.Identity)
.With(new Renderable(cube.Id, 0))
.With(new Material
{
ShaderId = graphics.LitShader.Id,
TextureId = graphics.WhiteTexture.Id,
Color = new Vector4(1, 0.3f, 0.3f, 1f),
Metallic = 0.2f,
Roughness = 0.5f
})
.With(new SpinSpeed { Value = 1f })
.Build();
})
.OnResize((w, h) => Console.WriteLine($"Resized: {w}x{h}"))
.Run();
// Custom component and system
[Component]
public partial struct SpinSpeed { public float Value; }
public class SpinSystem : ISystem
{
private IWorld? world;
public bool Enabled { get; set; } = true;
public void Initialize(IWorld world) => this.world = world;
public void Update(float dt)
{
foreach (var entity in world!.Query<Transform3D, SpinSpeed>())
{
ref var transform = ref world.Get<Transform3D>(entity);
var speed = world.Get<SpinSpeed>(entity);
transform.Rotation *= Quaternion.CreateFromAxisAngle(Vector3.UnitY, speed.Value * dt);
}
}
public void Dispose() { }
}
Lifecycle Events
The ILoopProvider interface (provided by SilkWindowPlugin) provides these events:
| Event | Timing | Use Case |
|---|---|---|
OnReady |
Once, when ready | Create resources, set up scene |
OnUpdate |
Every frame | Game logic (if not using auto-update) |
OnRender |
Every frame | Additional rendering |
OnResize |
When resized | Update viewports, aspect ratios |
OnClosing |
When closing | Final cleanup |
Plugin Architecture
The graphics system requires the window plugin to be installed first:
SilkWindowPlugin (must be installed first)
├── Creates native window
├── Provides ILoopProvider (main loop)
└── Provides ISilkWindowProvider (shared window access)
SilkGraphicsPlugin (uses shared window)
├── Subscribes to window lifecycle events
├── Creates OpenGL context
└── Provides IGraphicsContext
Required installation order:
// Window plugin first (provides ILoopProvider)
world.InstallPlugin(new SilkWindowPlugin(windowConfig));
// Then graphics and/or input plugins (any order after window)
world.InstallPlugin(new SilkGraphicsPlugin(graphicsConfig));
world.InstallPlugin(new SilkInputPlugin(inputConfig));
See the Input documentation for more details.
Debugging & Troubleshooting
Common Issues
Nothing Renders
- Check camera exists: Ensure an entity with
CameraandMainCameraTagexists - Check clear color: Use a bright clear color to verify camera is working
- Check systems added: Verify
CameraSystemandRenderSystemare registered - Check loop provider: Ensure
world.Update(dt)is being called
// Verify camera exists
var cameras = world.Query<Camera>().ToList();
Console.WriteLine($"Camera count: {cameras.Count}");
Objects Not Visible
- Position: Check
Transform3D.Positionis in front of camera - Scale: Ensure scale is not zero
- Material: Verify
ShaderIdandTextureIdare valid handles - Layer: Check
Renderable.Layerisn't being culled
// Debug renderable entities
foreach (var entity in world.Query<Renderable, Transform3D>())
{
var transform = world.Get<Transform3D>(entity);
var renderable = world.Get<Renderable>(entity);
Console.WriteLine($"Entity at {transform.Position}, mesh: {renderable.MeshId}");
}
Black Objects
- Lighting: Add a
Lightentity or useUnlitShader - Texture: Verify texture loaded successfully (valid handle)
- Color alpha: Check
Material.Color.Wis not zero - Shader: Use
UnlitShaderfor 2D or debug
// Quick fix: Add a directional light
world.Spawn()
.With(new Transform3D(Vector3.Zero,
Quaternion.CreateFromYawPitchRoll(0.5f, -0.5f, 0), Vector3.One))
.With(Light.Directional(new Vector3(1, 1, 1), 1f))
.Build();
Performance Issues
- Batching: Group objects with the same material
- Draw calls: Minimize unique material combinations
- Culling: Enable backface culling for closed meshes
- VSync: Disable for uncapped framerate testing
Debug Visualization
// Print rendering statistics
var cameras = world.Query<Camera>().Count();
var renderables = world.Query<Renderable>().Count();
var lights = world.Query<Light>().Count();
Console.WriteLine($"Cameras: {cameras}");
Console.WriteLine($"Renderables: {renderables}");
Console.WriteLine($"Lights: {lights}");
Window Resize Handling
Update camera aspect ratio when the window resizes:
world.CreateRunner()
.OnResize((width, height) =>
{
float aspectRatio = (float)width / height;
foreach (var entity in world.Query<Camera>())
{
ref var camera = ref world.Get<Camera>(entity);
camera.AspectRatio = aspectRatio;
}
})
.Run();
Checking Resource Validity
var graphics = world.GetExtension<IGraphicsContext>();
// Check if handles are valid (non-zero)
if (meshHandle.Id == 0)
Console.WriteLine("Invalid mesh handle!");
if (textureHandle.Id == 0)
Console.WriteLine("Invalid texture handle!");
// Built-in resources are always valid
var whiteTexture = graphics.WhiteTexture; // Always valid
var litShader = graphics.LitShader; // Always valid
Dependencies
- KeenEyes.Graphics.Abstractions - Backend-agnostic interfaces
- KeenEyes.Graphics - Backend-agnostic systems
- KeenEyes.Graphics.Silk - Silk.NET OpenGL backend
- KeenEyes.Platform.Silk - Shared window provider
- KeenEyes.Runtime - WorldRunnerBuilder
- KeenEyes.Common - Transform3D component
- Silk.NET - OpenGL/Vulkan bindings