CyberEngine

August, 2023

CyberEngine is a game engine coded in C#. It includes a custom Visual Studio project type for game content with NuGet support, game content pipeline, user interface framework, and physics engine. Graphics support for Direct3D 11, Direct3D 12, Vulkan, OpenGL, rasterisation, and ray tracing. The engine supports Windows with most functions also supported on Linux.

The engine abstracts all rendering APIs behind interfaces so only one application needs to be created. Not all APIs share the same features, so a lowest common feature set was used where possible. The engine adheres closest to Direct3D terminology and features.

Graphics

CyberEngine supports Direct3D 11, Direct3D 12, Vulkan, and OpenGL. The engine currently uses Vortice for Direct3D 11, Direct3D 12, and Vulkan.

Vortice

Vortice is an open source project that uses SharpGen to generate C# wrappers for native assemblies. I have contributed some bug fixes and features to this project.

OpenGL

For OpenGL, a code generator examines the specification XML and creates the bindings automatically. Below is an example of the method glDrawArrays.

<Function Name="glDrawArrays">
  <Parameter Name="mode">
    <Type Name="PrimitiveType" />
  </Parameter>
  <Parameter Name="first">
    <Type Name="GLint" />
  </Parameter>
  <Parameter Name="count">
    <Type Name="GLsizei" />
  </Parameter>
</Function>
Snippet from the OpenGL specification for the method glDrawArrays.
[Feature("GL_VERSION_1_1")]
public delegate void DrawArraysDelegate(Enumerators.PrimitiveType modeint firstuint count);
Snippet of the auto generated C# delegate for glDrawArrays.
CyberEngine test application for 2D, 3D, and sprites using Direct3D 11.
Test application using Direct3D 12.
Test application using OpenGL.
Test application using Vulkan.

Physically Based Rendering (PBR)

CyberEngine supports a Physically Based Rendering (PBR) pipeline. Auto exposure is calculated by creating a luminance histogram in a compute shader and processing the results on the CPU. Some images from a test program are shown below.

PBR rendering example in a WPF application. Top right illustrating a luminance histogram computed on the GPU. Additional controls allow for tuning the exposure and intensity of the lights and environment map. The spheres vary in metalness/dielectric across the x-axis and roughness along the y-axis.
Another view of the PBR spheres with low roughness near the bottom.
Close up image of a low roughness metallic sphere.
Close up image of a low roughness dielectric (plastic) sphere.

Atmospheric Scattering

A current project requires an atmospheric scattering shader to render planets. A few images from an editor and different settings are shown below.

Atmospheric scattering example with a no Rayleigh or Mie constant.
Atmospheric scattering example with low Rayleigh constant. Note the blue light being scattering within the atmosphere.
Atmospheric scattering example with medium Rayleigh constant.
Atmospheric scattering example with high Rayleigh constant. Note the red shift as the blue light is scattered away.
Atmospheric scattering example with low Mie constant. Note the cloudy appearance as Mie models the scattering of aerosols which scatterings all wavelengths equally.
Atmospheric scattering example with medium Mie constant.
Atmospheric scattering example with medium Rayleigh and Mie constant.
Atmospheric scattering example with low Rayleigh and Mie constant and the sun from behind the planet.
Atmospheric scattering example with medium Rayleigh and Mie constant and the sun from behind the planet.

Shadow Mapping

An example of deferred rendering and shadow mapping which uses multiple render targets is illustrated below.

Deferred rendering illustrating a shadow mapping example with the albedo, normal, depth, and light render targets. In the background is the result of the final pass combining all render targets.
Another image of the shadow mapping example.

Sprites

CyberEngine uses a sprite batch to render images and text from fonts. The fonts can be created in the content pipeline by supplying the font properties and a texture is automatically generated, or a custom font can be created externally in image editing software.

Sprite batch is used to render multiple sprites and text.
Illustration of rendering text as sprites which are created in the content pipeline or custom designed in image editor software.

Ray Tracing

CyberEngine supports ray tracing with Direct3D 12 and a simple example is illustrated below.

Ray tracing example in Direct3D 12.

CPU Ray Tracing

In order to debug shader concepts, a CPU based ray tracer was architected in C#. A scene can be rendered by supplying geometry and hit and miss functions. A multithreading option can be enabled to speed up the output of an image by dividing rays among threads.

Image of a planet and atmosphere generated from a CPU based ray tracer allowing shader concepts to be written in C#, which has a more robust debugger.

Shaders

Cross Compiler

CyberEngine compiles HLSL shaders for Direct3D and cross compiles into SPIR-V for Vulkan and GLSL for OpenGL. This allows one shader to be written and maintained while supporting all graphics APIs. Shaders use compiler directives to state how buffers should be bound to shader programs. One possible improvement is to automate the assignment of binding indices.

Descriptor Table Code Generator

CyberEngine auto generates C# code for constant buffers by analyzing the shader code and creating an analogue constant block in C#. The shader is also analyzed to auto generate a descriptor table eliminating the need to manually keep shader code and C# code in sync.

Below is a simple example of a vertex and pixel shader with its auto generated C# code that renders a cube with a texture.

/* Includes ***************************************************/
#include "CyberEngine\VertexStructs\PositionTextureVertex.hlsl"
#include "CyberEngine\PixelStructs\PositionTexturePixel.hlsl"

/* Constant buffers ***************************************************/
#if OPENGL
[[vk::binding(0)]]
#elif VULKAN
[[vk::binding(0, 0)]]
#endif
cbuffer WorldViewProjectionConstantBlock : register(b0)
{
    // World, view and projection matrix
    row_major float4x4 WorldViewProjection;
};

/* Vertex shader ***************************************************/
PositionTexturePixel VS(PositionTextureVertex vertex)
{
    PositionTexturePixel pixel = (PositionTexturePixel) 0;

    // Transform the position to screen space
    pixel.Position = mul(vertex.Position, WorldViewProjection);

    // Set the properties
    pixel.TextureCoordinate = vertex.TextureCoordinate;

    return pixel;
}
Vertex shader used to render a cube.
// Auto Generated File. Do not modify.

using System;
using CyberneticGames.CyberEngine.Data;
using CyberneticGames.CyberEngine.Graphics;
using CyberneticGames.CyberEngine.Mathematics;

namespace CoreTester.Graphics
{
    public partial class MeshGraphicsProgram
    {

        public static readonly DescriptorTable VertexDescriptorTable = new DescriptorTable(ShaderStage.Vertex,
            new[]
            {
                new DescriptorElement(DescriptorType.ConstantBuffer),
            });

        public static void SetWorldViewProjectionConstantBlock(IDescriptorBinding descriptorBindingint tableIndex, IConstantBuffer buffer) => descriptorBinding.SetConstantBuffer(tableIndex, 0, buffer);


        public class WorldViewProjectionConstantBlock : ConstantBlock
        {
            private static readonly IConstantValueLayout[] ValueLayouts = new IConstantValueLayout[]
            {
                new ConstantValueLayout<CyberneticGames.CyberEngine.Mathematics.MatrixF>(nameof(WorldViewProjection)),
            };

            public WorldViewProjectionConstantBlock(GraphicsEngine graphicsEngine)
                : base(ValueLayouts, graphicsEngine)
            {
            }

            public CyberneticGames.CyberEngine.Mathematics.MatrixF WorldViewProjection { set => Data.SetValue(nameof(WorldViewProjection), value); }
        }

    }
}
Auto generated descriptor table and constant block for the previous vertex shader. Any changes made to the shader will automatically regenerate the C# code.
/* Includes ***************************************************/
#include "CyberEngine\PixelStructs\PositionTexturePixel.hlsl"

/* Constants ***************************************************/

// Alpha channel clip threshold
static const float ClipThreshold = 0.001;

/* Textures and samplers ***************************************************/
#if OPENGL
[[vk::binding(0)]]
#elif VULKAN
[[vk::binding(0, 1)]]
#endif
Texture2D<float4> Texture : register(t0);

#if VULKAN
[[vk::binding(1, 1)]]
#endif
SamplerState TextureSampler : register(s0);

/* Pixel shader ***************************************************/
float4 PS(PositionTexturePixel pixel) : SV_Target0
{
    // Get the colour
    float4 colour = Texture.Sample(TextureSampler, pixel.TextureCoordinate);

    // Clip pixels that are transparent
    clip(colour.a - ClipThreshold);

    return float4(colour.rgb, 1);
}
Pixel shader used to render a cube.
// Auto Generated File. Do not modify.

using System;
using CyberneticGames.CyberEngine.Data;
using CyberneticGames.CyberEngine.Graphics;
using CyberneticGames.CyberEngine.Mathematics;

namespace CoreTester.Graphics
{
    public partial class MeshGraphicsProgram
    {

        public static readonly DescriptorTable PixelDescriptorTable = new DescriptorTable(ShaderStage.Pixel,
            new[]
            {
                new DescriptorElement(DescriptorType.SampledTexture),
            });

        public static void SetTexture(IDescriptorBinding descriptorBindingint tableIndex, ITextureView texture, ITextureSampler textureSampler) => descriptorBinding.SetSampledTexture(tableIndex, 0, texture, textureSampler);


    }
}
Auto generated descriptor table and method to the set the texture and texture sampler. Any changes made to the shader will automatically regenerate the C# code.

Geometry Shader

CyberEngine supports vertex, geometry, pixel, and compute shaders.

Example of a geometry shader rendering the normals of the points and face of triangles.

Compute Shader

An example of a compute shader that generates the visible geometry from a 3D voxel texture.

Compute shader generating visible geometry from a 3D voxel texture.

An example of a compute shader executed in a console application to create a noise texture and save to an image.

Voronoi noise texture generated on the GPU.

User Interface

A user interface framework inspired by WPF created from scratch, supports controls, control styles, and data binding. The user interface is rendered through a drawing interface allowing it to be decoupled from the graphics APIs including Direct3D 11, Direct3D 12, OpenGL, and Vulkan. This also enabled the ability to render to an image, allowing automated tests to be debugged by viewing the results of the UI.

Styling

An example of a Button style is shown below. It illustrates setting default properties and changing properties on triggers.

<c:ResourceDictionary xmlns="assembly:CyberneticGames.CyberEngine.Xpf.Controls;namespace:CyberneticGames.CyberEngine.Xpf.Controls"
                      xmlns:c="assembly:CyberneticGames.CyberEngine.Xpf.Core;namespace:CyberneticGames.CyberEngine.Xpf">

  <c:Style TargetType="{c:Type Button}">
    <c:Setter Property="Button.Margin" Value="3" />
    <c:Setter Property="Button.Padding" Value="2" />

    <c:Setter Property="TextBlock.Foreground" Value="#ffffff" />
    <c:Setter Property="Button.Background" Value="#262626" />
    <c:Setter Property="Button.BorderBrush" Value="#666666" />
    <c:Setter Property="Button.BorderThickness" Value="1" />

    <c:Setter Property="Button.Template">
      <c:Setter.Value>
        <c:Template>
          <Border Background="{c:TemplateBinding Background}" BorderBrush="{c:TemplateBinding BorderBrush}" BorderThickness="{c:TemplateBinding BorderThickness}">
            <ContentPresenter Margin="{c:TemplateBinding Padding}" />
          </Border>
        </c:Template>
      </c:Setter.Value>
    </c:Setter>

    <c:Style.Triggers>
      <c:PropertyTrigger Property="Button.IsMouseWithin" Value="True">
        <c:Setter Property="TextBlock.Foreground" Value="#ffffff" />
        <c:Setter Property="Button.Background" Value="#1eb300" />
        <c:Setter Property="Button.BorderBrush" Value="#158000" />
      </c:PropertyTrigger>

      <c:PropertyTrigger Property="Button.IsPressed" Value="True">
        <c:Setter Property="TextBlock.Foreground" Value="#ffffff" />
        <c:Setter Property="Button.Background" Value="#158000" />
        <c:Setter Property="Button.BorderBrush" Value="#116600" />
      </c:PropertyTrigger>

      <c:PropertyTrigger Property="Button.IsEnabled" Value="False">
        <c:Setter Property="TextBlock.Foreground" Value="#999999" />
        <c:Setter Property="Button.Background" Value="#1a1a1a" />
        <c:Setter Property="Button.BorderBrush" Value="#333333" />
      </c:PropertyTrigger>
    </c:Style.Triggers>
  </c:Style>
</c:ResourceDictionary>
Button style.

Data Binding

An example of how data binding is configured is shown below. Data binding relies on reflection to resolve properties. Events can also be hooked up to the code file by name, or can be bound to a view model command adhering to the MVVM design pattern.

<TabItem Header="Button">
  <StackPanel>
    <TextBlock Name="ButtonTextBlock" />

    <Button Click="Button_Click">
      <TextBlock Text="Button" />
    </Button>

    <RepeatButton Click="RepeatButton_Click">
      <TextBlock Text="RepeatButton" />
    </RepeatButton>

    <ToggleButton Name="ToggleButton">
      <TextBlock Text="ToggleButton" />
    </ToggleButton>

    <TextBlock Text="{c:Binding Source={c:NamedElementBindingSource ToggleButton}, IsChecked}" />

    <TextBlock Text="Text to show and hide with toggle button" Visibility="{c:Binding Source={c:NamedElementBindingSource ToggleButton}, IsChecked, Converter={c:Resource BooleanToVisibilityConverter}}" />
Portion of UI for the buttons illustrating the link to actionable events and data binding.

Examples

Below are a few screens from a test application showing some of the controls.

Part of a test user interface showing a tab control with the buttons currently selected.
Illustration of the sliders tab with a binding for the slider value to the text block.
List control allowing the selection of items and command buttons that perform actions on the list items. The lower portion is also bound to the selected item and can be edited with a text box.
3D viewport within the user interface.

3D UI

The user interface can be drawn to a render target and later rendered on to a 3D object. User input is transformed into the 3D scene so it can be interacted with.

User interface that is rendered onto a 3D quad and also is intractable.
User interface rendered onto a half cylinder.

WPF

A game control for WPF allows scenes to be hosted in WPF applications. Support exists for all graphics APIs. Since WPF runs on Direct3D 9, render targets must be copied to a Direct3D 9 texture for display in WPF applications.

WPF component to render a scene within a WPF application. Direct3D 11 illustrated in this example.
Same WPF example rendered with Direct3D 12.
Same example rendered in Vulkan.
Same WPF example rendered with OpenGL. This example has incorrect colours due to differing render target formats with OpenGL and WPF. RGBA and BGRA are swapping the red and blue channels. This can be seen as the grey and green appear correct, but the blue and yellow are not.

Physics

CyberEngine contains a constraint based physics engine. Narrow phase collision detection uses optimized algorithms between common shapes and GJK, which can be extended to support custom convex hulls. Collision response uses an iterative solver that supports contact manifolds and static and dynamic friction.

Cube within a physics engine debugger illustrating the AABB and contact normals.

Models

CyberEngine supports 3D models and animation. The content pipeline supports Collada and GLTF files. A test application to view models and animations is illustrated below.

3D model renderer hosted in a WPF application.

Instanced Animation

Multiple skinned model instances with bone vertex weights can be rendered in a single draw call using instanced geometry and an array of bone transforms for each instance.

Instanced skeleton model rendering where each model has its own skeletal animation.

Linux

CyberEngine supports Linux using OpenGL and Vulkan.

Sprite example captured running on Linux Mint within a virtual machine using OpenGL.

Visual Studio Project Type

CyberEngine uses a Visual Studio extension to provide a custom project type for game content. It allows content files to be added to a project and configure which importer to handle processing and writing the shipped content file.

During game execution, the file is read which contains the reader type name to properly parse and load the content. The importers are decoupled from the readers so the writers do not need to be shipped with the game.

The importers are contained in assemblies and packaged into NuGet packages which can be added to any game content project. The project type in Visual Studio supports using the NuGet package manager to manage NuGet packages for the project.

The project system is built on MSBuild, so it can be supported in Visual Studio and integrate with C# projects that also use MSBuild.

Custom game content project type within the Visual Studio Solution Explorer.
<ItemGroup>
  <PackageReference Include="CyberneticGames.CyberEngine.Content.Pipeline" Version="1.*" />
  <PackageReference Include="CyberneticGames.CyberEngine.Content.Pipeline.Direct3D11" Version="1.*" />
  <PackageReference Include="CyberneticGames.CyberEngine.Graphics" Version="1.*" />
  <PackageReference Include="Mapromar.Build.ProjectDependency" Version="1.*">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
  </PackageReference>
</ItemGroup>
<ItemGroup>
  <Content Include="..\Shared.TesterContent\Shaders\MeshVS.hlsl">
    <Link>Shaders\MeshVS.hlsl</Link>
    <Importer_Type>CyberneticGames.CyberEngine.Content.Pipeline.CrossShader.VertexShaderImporter</Importer_Type>
    <Importer_EntryPoint>VS</Importer_EntryPoint>
    <Importer_Profile>vs_4_0</Importer_Profile>
    <Importer_EnableDebugging Condition="'$(Configuration)' == 'Debug'">True</Importer_EnableDebugging>
    <Importer_Direct3D11Version>4.0</Importer_Direct3D11Version>
    <Importer_Direct3D12Version>4.0</Importer_Direct3D12Version>
    <Importer_OpenGLVersion>330</Importer_OpenGLVersion>
    <Importer_VulkanVersion>450</Importer_VulkanVersion>
  </Content>
  <Content Include="..\Shared.TesterContent\Shaders\MeshPS.hlsl">
    <Link>Shaders\MeshPS.hlsl</Link>
    <Importer_Type>CyberneticGames.CyberEngine.Content.Pipeline.CrossShader.PixelShaderImporter</Importer_Type>
    <Importer_EntryPoint>PS</Importer_EntryPoint>
    <Importer_Profile>ps_4_0</Importer_Profile>
    <Importer_EnableDebugging Condition="'$(Configuration)' == 'Debug'">True</Importer_EnableDebugging>
    <Importer_Direct3D11Version>4.0</Importer_Direct3D11Version>
    <Importer_Direct3D12Version>4.0</Importer_Direct3D12Version>
    <Importer_OpenGLVersion>330</Importer_OpenGLVersion>
    <Importer_VulkanVersion>450</Importer_VulkanVersion>
  </Content>
</ItemGroup>
Excerpt from a custom game content project illustrating how assets are configured. NuGet packages are added to the project that contain the content pipeline assemblies.
Captured within Visual Studio illustrating full NuGet support for the custom game content project type within the IDE.

Vegabot

Vegabot is a game built on CyberEngine that includes an orbital mechanics simulator. A few images captured from tools and test applications are shown below.

Planet editor built within WPF.
View closer to a planet surface within the editor.
Planet with increased Rayleigh and Mie scattering coefficients.
View of planet atmospheric scattering with the sun behind a planet.
Voxel surface editing and rendering example with differing LODs.
Example of planets and orbital lines within the orbital mechanics simulator.