Overview

ChrisslyEngine is a 3D-Rendering/Audio Engine for the Playstation Portable (PSP), Android devices and Windows written in C++.

Screenshots

Windows

leveleditor

Playstation Portable

animationtest materialtest

Android

animationtest_android

Features

Platform Support

  • Playstation Portable (PSP)
  • Android
  • Windows

Audio

  • channel management
  • buffered and streamed playback
  • 3d sound (built-in software implementation wich works with all devices/drivers that support at least stereo output, linear clamped roll-off)
  • File Format Support
    • RIFF WAVE (*.wav)
    • Ogg Vorbis (*.ogg)
  • real-time dsp system

Graphics

  • Materials
    • C-like syntax material description language that exposes common fixed function operations with support for multi-pass rendering
    • Shader parameters can be modified in the application using a simple highlevel interface
  • Animation
    • Skeletal animation with support for weighted/additive blending of multiple animations and partial animation
    • Morph animation (linear blends between shape snapshots)
  • Texture-based shadows (WIP)
  • Cubemaps
  • MSAA and alpha to coverage

Tools

  • Blender Add-on
    • Export to ChrisslyEngine-Mesh (.mesh), ChrisslyEngine-Texture (.tex), ChrisslyEngine-Materialdescription (.material) and ChrisslyEngine-Skeleton (.skeleton) directly from Blender
  • Command line tools
    • meshtool: Converts Wavefront-ASCII-OBJ (.obj) to ChrisslyEngine-Mesh (.mesh), creating a morphanimation from a series of (*.obj) files is also supported
    • texturetool: Adds ChrisslyEngine texture header to raw texture data

Quickstart

Download

Download repository as zip here or clone it with the following command:

git clone https://github.com/the-real-evolver/chrisslyengine.git

Building, and running the windows sample

Building the sample

  1. install Visual Studio 2019 or higher
  2. open 'windows/ChrisslyEngine_Windows.sln', right click 'ChrisslyEngine_Windows' in the project explorer and click 'Build'
  3. run 'windows/exportassets.cmd'

Running the sample

  1. press 'F5' in Visual Studio to start/debug the sample application

Building, and running the psp sample

Building the sample

  1. install devkitPSP
  2. open a commandline, goto 'chrisslyengine\psp' and run 'make' or open 'ChrisslyEngine_PSP.pnproj' and press 'Alt + 1'
  3. run 'psp\exportassets.cmd'

Running the sample on a real PSP

  1. create a new folder 'ms0:/PSP/GAME/ChrisslyEngine_PSP'
  2. copy the 'export' folder and the 'EBOOT.PBP' to the new folder
  3. use Half-Byte Loader to execute, since the PBP is not signed

Running the sample on the JPCSP emulator

  1. copy the 'export' folder to 'jpcsp\ms0\PSP\GAME\ChrisslyEngine_PSP'
  2. open JPCSP, then load the EBOOT.PBP with 'Load File' and press 'Run'

Running the sample on the PPSSPP emulator

  1. copy the 'export' folder to 'ppsspp_win\memstick\PSP\GAME\ChrisslyEngine_PSP'
  2. open PPSSPP, then load the EBOOT.PBP with 'File' -> 'Open'

Building, and running the android sample

Building the sample

  1. install Android Studio Giraffe | 2022.3.1 or higher
  2. open project 'chrisslyengine/android' in Android Studio
  3. press 'Ctrl + F9' to build the application
  4. run 'exportassets.cmd'

Running the sample with Android Studio

  1. press 'Shift + F10' in Android Studio to start the sample application

Toolchain

Internal and external tools used for working with the engine.

Blender

Install Addon

  1. copy 'tools/blender/io_export_chrisslymesh.py' to 'C:\Program Files\Blender Foundation\Blender 3.1\3.1\scripts\addons'
  2. in Blender go to 'Edit -> Preferences -> Add-ons' and check 'Import-Export ChrisslyEngine-Mesh format(.mesh)'

Export directly to (.mesh), (.material), (.skeleton) and (.tex) using the addon

  • textures are exported in R8G8B8A8 format
  • axis_conversion(from_forward='-Y', to_forward='Z', to_up='Y') on static meshes and morph animations, but not on skeletal animations
  • modifiers are not applied (to apply: 'Object Mode -> Object -> Apply -> All Transforms')
  • set a custom gpu program in material:
    1. select material
    2. 'Custom Properties -> New'
    3. name the new property 'gpu_program' and select 'String' as type
    4. set value (like "skybox.fx")
    5. after export your (.material) file will contain a line 'gpu_program "skybox.fx"' in the pass section
  • add a custom/external cubemap to material:
    1. select material
    2. 'Custom Properties -> New'
    3. name the new property 'cubic_texture' and select 'String' as type
    4. set value (like "irradiance.tex")
    5. after export your (.material) file will contain another texture_unit with the given cubemap in the pass section

Export to "obj" for later conversion with the mesh cl tool

Options blender "obj" exporter for animations:

  • 'Export Animation'
  • 'Triangulated Mesh'

Skeletal Animation Setup

In order to work correctly in the engine there are a few necessities when setting up the rig and animations:

  • limit weights per vertex to 4 (in 'Weight Paint' mode: 'Weights -> Limit Total')
  • normalise weights so all weights of a vertex sum up to 1 (in 'Weight Paint' mode: 'Weights -> Normalize All')
  • in 'Pose Mode' set the bones transform mode to: 'Quaternion (WXYZ)'
  • apply transform if the mesh or skeleton was moved, rotated or scaled
  • always insert 'Location & Rotation' to all channels (scale not supported)
  • to add an animation just create a new Action

About Coordinate Systems

Blender (forward is -Y and Up is +Z)

+Z
^    +Y
|   /
|  /
| /
|/
+--------> +X

OpenGL Right-handed (forward is +Z and Up is +Y)

+Y
^    -Z
|   /
|  /
| /
|/
+--------> +X

That's why the exporter transforms the vertices with axis_conversion(from_forward='-Y', to_forward='Z', to_up='Y').

Separate mesh into equal chunks by a grid

This comes in handy to split large meshes like terrains in to smaller pieces for culling and coarse collision detection.

  1. 'Object Mode -> Add -> Mesh -> Grid'
  2. delete faces of the grid 'Edit Mode -> Mesh -> Delete -> Only Faces'
  3. in 'Edge Select' mode, extrude grid with 'E' in -Z direction to create a shape we can cookie-cut our mesh with
  4. 'Object Mode' select all (the mesh and the grid) and press 'Ctrl + J' to join
  5. 'Edit Mode' select corner of the grid and press 'L'
  6. 'Face -> Intersect(Knife)'
  7. 'Mesh -> Split -> Faces by Edges'
  8. 'Mesh -> Separate -> By Loose Parts' will create separate objects
  9. remove leftovers from the grid
  10. in the exporter check the 'Objects as separate files' option

Gimp

Add alpha channel

  • 'Colours -> Colour to Alpha...'

Extract alpha to single png (useful when using a separate compressed texture for the alpha channel on android)

  1. 'Colours -> Components -> Decompose...' ('RGBA', uncheck 'Decompose to Layers...')
  2. right click on image and 'Export As...'

Add greyscale image as an alpha channel to image

  1. Add an alpha channel to the image you want to mask 'Layer -> Transparency -> Add Alpha Channel'
  2. Right click the image layer in the layers panel, and click 'Add Layer Mask...'. The fill you choose for the mask doesn't matter at this stage - white or black will do.
  3. Open the other image you want to use as a mask, and select all 'Ctrl + A', and copy it, 'Ctrl + C'
  4. Back in the other image, select the layer mask thumbnail by clicking on it in the layers panel
  5. Paste the mask you copied earlier 'Ctrl + V'
  6. Hit the anchor icon or 'Ctrl + H', to anchor the floating selection to the layer mask.

RAWTex Gimp Plugin

Install

  • get plugin here
  • copy 'RAWTex.exe' to 'C:\Users\username\AppData\Roaming\GIMP\2.10\plug-ins'

Usage

  1. 'File -> Export As...'
  2. 'Select File Type (By Extension) -> RAW Texture'
  3. 'Save'
  4. choose options and 'OK'

Windows

  • don't mirror image vertical (except for DXT1-5, the plugin mirrors the texture when exporting to Standard OpenGL)
  • DXT Format: Standard OpenGL (otherwise colors are scrambled)

PSP

  • mirror image vertical (Paint)
  • DXT Format: PSP

cmftStudio

With this tool you can create cubemaps from HDRIs and export the faces as separate textures. It also allows creating of irradiance maps.

Install

Usage

To store the the faces as separate files:

  • File type: '.tga'
  • Output type: 'Facelist'

Then load the faces in to Gimp, export them with the RAWTex plugin and convert them to a .tex with the command line texturetool. Also keep in mind naming convention for the cubemap faces.

Command line tools

Texturetool

Usage

Android

How to use etc1tool to export compressed textures

Export with pkm header and pass '-pkm' to hint texturetool to parse properties from pkm header:

etc1tool.exe mage.png
texturetool.exe -pkm -data mage.pkm -out mage_etc1.tex

Or only export payload using '--encodeNoHeader' and explicitly pass properties:

etc1tool.exe mage.png --encodeNoHeader
texturetool.exe -format PF_ETC1_RGB8 -width 256 -height 256 -data mage_etc1.raw -out mage_etc1.tex

Meshtool

Usage

Export morph animations example

meshtool.exe -morphanim -length 3.0 -numKeys 31 -src %SrcFolder%\cerberus\cerberus_walk -dst %DstFolder%\cerberus_walk.mesh

For the call above the following filestructure is expected (this is how Blender's obj exporter outputs animations):

+-- cerberus
|   +-- cerberus_walk1.obj    <- starts with index 1 !
|   +-- cerberus_walk2.obj
|   +-- ...
|   +-- cerberus_walk31.obj

Leveleditor (WIP)

leveleditor Comming soon!

Prerequisites

The first thing to do is to create a folder named 'export' in your project directory. This is the root directory for your assets. If you export a mesh from Blender the addon exports all filepaths (for example a .tex file referenced in a .material file) relative to the 'export' folder.

Exporting assets from Blender

In Blender go to 'File -> Export -> ChrisslyEngine-Mesh (.mesh)'. Select the 'export' folder (or any subfolder of it), check any options you wish, and click 'Export ChrisslyEngine-Mesh'.

Creating a level

Go to 'File -> New Level'.

Adding entities

Right click a SceneNode in the World Outliner and choose 'Add Entity'. This opens the asset root directory ('export'). Double click the .mesh file you want load or select it and click 'OK'. Press 'F' to focus the entity.

Adding lights

Right click 'Lights' in the World Outliner and choose 'Create Light'.

Adding collision geometry

Right click 'Static Collision' in the World Outliner and choose 'Add Collision Mesh'.

Keyboard Shortcuts

ShortcutAction
Deldelete selected Entity, Scenenode, Light or Collision Mesh
LCtrl + Ssave current level
Ffocus camera on selected object in the World Outliner
LCtrl + Zundo the last Scenenode transformation
RAlt + Pplay

Gamepad Shortcuts

ButtonAction
Startplay

Level-Format

world_settings
{
    shadow_technique none|texture
    shadow_colour "0xe9e0cd"
}

materials
{
    "dir/name.material"
    "dir/another.material"
    ...
}

node // this is the root node "current_level"
{
    position <x y z>
    orientation <x y z w>
    scale <x y z>
    entity
    {
        mesh "dir/name.mesh"
        cast_shadows true|false
        receives_shadows true|false
        cull_frustum true|false
        animation "AnimationName"
        {
            enable true|false
            loop true|false
            weight <f>
        }
        animation "AnotherAnimation"
        {
            ...
        }
    }
    entity
    {
        ...
    }
    node
    {
        ...
    }
}

light
{
    type point|directional|spot
    position <x y z>
    direction <x y z>
    diffuse "0xffffffff"
    specular "0x0"
    attenuation <range constant linear quadradic>
}

collision
{
    "dir/name.mesh"
}

game
{
    spawn_point <x y z>
    spawn_velocity <x y z>
}

Engine

The engine consists of the following subsystems:

  • Core
  • Graphics
  • Audio
  • Input
  • Application

Graphics

The subsystem responsible for rendering and animation.

Mesh-Format

Tag                         Bytes       Type
M_MESH_BOUNDS               1           unsigned char
  bounding-center           12          float
  bounding-radius           4           float

M_SUBMESH                   1           unsigned char
  materialname-length       1           unsigned char
  materialname-data         1 - 255     char
  vertex-count              4           unsigned int
  bytes-per-vertex          4           unsigned int
  bone-weights              16 - 32     float           // only skeletal animation (psp can have up to eight weights)
  bone-indices              16          unsigned int    // only skeletal animation (d3d11 and gles2 rendersystem)
  texture-coordinates       8           float
  color                     4           int
  normal                    12          float
  postition                 12          float

M_ANIMATION                 1           unsigned char
  anim-length               4           float

M_ANIMATION_TRACK           1           unsigned char
  handle(submesh-index)     1           unsigned char

M_ANIMATION_MORPH_KEYFRAME  1           unsigned char
  key-timestamp             4           float
  vertex-count              4           unsigned int
  bytes-per-vertex          4           unsigned int
  texture-coordinates       8           float
  color                     4           int
  normal                    12          float
  postition                 12          float

M_MESH_SKELETON_FILE        1           unsigned char
  skeletonfilepath-length   1           unsigned char
  skeletonfilepath-data     1 - 255     char

Material-Format

material "name"
{
    pass
    {
        fog_override false
        fog_override true <red green blue start end>

        lighting on|off
        emissive vertexcolour
        emissive <red green blue alpha>
        ambient vertexcolour
        ambient <red green blue alpha>
        diffuse vertexcolour
        diffuse <red green blue alpha>
        specular vertexcolour
        specular <red green blue alpha shininess>

        scene_blend one|zero|dest_colour|src_colour|one_minus_dest_colour|one_minus_src_colour|dest_alpha|src_alpha|one_minus_dest_alpha|one_minus_src_alpha
                    one|zero|dest_colour|src_colour|one_minus_dest_colour|one_minus_src_colour|dest_alpha|src_alpha|one_minus_dest_alpha|one_minus_src_alpha
        scene_blend fix fix <red green blue> <red green blue>

        alpha_test never|less|equal|less_equal|greater|not_equal|greater_equal|always <alpha>
        alpha_to_coverage on|off

        cull_hardware none|clockwise|anticlockwise

        depth_check on|off
        depth_write on|off

        texture_unit
        {
            texture "filename.tex"
            cubic_texture "basefilename.tex" // you'll have to provide the following 6 files: "basefilename_px.tex" "basefilename_nx.tex" "basefilename_py.tex" "basefilename_ny.tex" "basefilename_pz.tex" "basefilename_nz.tex"

            colour_op replace|modulate|add|alpha_blend|decal alpha|colour

            filtering none|point|linear none|point|linear none|point|linear

            scroll <u> <v>
            scale <u> <v>

            env_map spherical
        }

        gpu_program "gpuprogramname"
    }
}

Texture-Format

A texture file contains a header (see below) followed by the payload.

Tag             Bytes       Type
format          1           unsigned char
width           2           unsigned short
height          2           unsigned short
num-mipmaps     1           unsigned char
swizzled        1           unsigned char

Skeleton-Animation-Format

skeleton
{
    num_bones <n>
    bone "Bonename"
    {
        index <i>
        parent <p>
        local_matrix <m00 , m01, m02, m03, m10 , m11, m12, m13, m20 , m21, m22, m23, m30 , m31, m32, m33>
        inv_model_matrix <m00 , m01, m02, m03, m10 , m11, m12, m13, m20 , m21, m22, m23, m30 , m31, m32, m33>
    }
    bone "AnotherBone"
    {
        ...
    }
}

animation "Animationname"
{
    length <l>
    num_keyframes <n>
    track "Trackname"
    {
        index <i>
        key
        {
            time <t>
            local_matrix <m00 , m01, m02, m03, m10 , m11, m12, m13, m20 , m21, m22, m23, m30 , m31, m32, m33>
        }
        key
        {
            ...
        }
    }
    track "AnotherTrack"
    {
        ...
    }
}
animation "AnotherAnimation"
{
    ...
}

Animation System Gotchas

  • general
    • a mesh can have either morph or skeletal animation, but not both
    • only one morphanimation (with hardcoded name "default", get with: entity->GetAnimationState("default")) per mesh, blending multiple morphanimations is not supported
    • bone rotations are blended as follows:
      Quaternion q1 = Quaternion::Nlerp(Quaternion::IDENTITY, rotation1, weight1);
      Quaternion q2 = Quaternion::Nlerp(Quaternion::IDENTITY, rotation2, weight2);
      Quaternion blendedQ = q1 * q2;
      
  • d3d11 and gles2 rendersystem
  • psp rendersystem
    • maximum number of bones per skeleton: 8 (limited by the hardware)
    • no indexed vertex blending (not supported by the hardware)

Shadows

Texture-based shadows current limitations

  • general
    • only a single spotlight can cast shadows
  • d3d11 rendersystem
    • skeletal or morph animated entities can cast but not receive shadows
    • shadowmap resolution: 1024x1024
  • psp rendersystem
    • shadowmap resolution: 256x256
  • gles2 rendersystem
    • skeletal animated entities can cast but not receive shadows
    • morph animated entities can neither cast nor receive shadows
    • shadowmap resolution: 256x256

Built-in Default Shaders

Light attenuation:

Formula: attenuation = 1.0f / (constant + linear * distance + quadratic * distance²)

Auto updated shader parameters:

"worldMatrix"
"viewMatrix"
"projectionMatrix"
"worldViewProjMatrix"
"textureMatrix"
"boneMatrices"
"morphWeight"

Default light params matrix:

[ posx    posy    posz        -           ] position
[ diff.r  diff.g  diff.b      -           ] diffuse
[ spec.r  spec.g  spec.b      shininess   ] spec
[ const   linear  quadratic   range       ] attenuation

Cubemaps

Naming convention for the files:

cubemap_px.tex
cubemap_nx.tex
cubemap_py.tex
cubemap_ny.tex
cubemap_pz.tex
cubemap_nz.tex

Load cubemap:

Texture* cubeMap = TextureManager::Instance()->Load("cubemap.tex", TEX_TYPE_CUBE_MAP);

Partial animation with blend masks

It may be necessary to have more refined control over animation blends, instead of just blending all bones at once. This can be achieved with a 'blend mask' wich allows you to set the blend weight for each bone individually. A typical use-case for using 'blend masks' is to exclude lower-body bones so that animation plays only on the upper-body, regardless of the full-body state.

Example

We have two animations: "running" and "shooting". If we want to shoot while running we have to disable (set weight to zero) the lower-body bones for the "shooting" animation and the upper-body bones for the "running" animtion. Assuming the skeleton has 40 bones and the bones 0-31 represent the upper-body and the bones 32-39 the lower-body the blend masks are setup as follows:

AnimationState* running = entity->GetAnimationState("running");
running->CreateBlendMask(40U);
for (int i = 0; i < 32; ++i)
{
    running->SetBlendMaskEntry(i, 0.0f);
}

AnimationState* shooting = entity->GetAnimationState("shooting");
shooting->CreateBlendMask(40U);
for (int i = 32; i < 40; ++i)
{
    shooting->SetBlendMaskEntry(i, 0.0f);
}

Note that the weights in the blend mask are absolute and they are not modulated with the averaged weight of the animation state. You have to prevent 'overshooting' (weights > 1.0f) yourself, when blending multiple animations. For the example above this would require that for every body-part (upper-body, lower-body etc.) you have to loop through all the animation states affecting that part, sum up their weights, calculate the weight factor '1.0f / summed_weights' and then apply this factor to all weights in the blendmask.

Create mesh programmatically

chrissly::graphics::Vertex vtxData[3U] =
{
    {0.0f, 0.0f, 0xffffffff, 0.0f, 0.0f, 1.0f, -0.5f , -0.5f, 0.0f},
    {0.0f, 0.0f, 0xffffffff, 0.0f, 0.0f, 1.0f,  0.5f , -0.5f, 0.0f},
    {0.0f, 0.0f, 0xffffffff, 0.0f, 0.0f, 1.0f,  0.0f ,  0.5f, 0.0f}
};
HardwareVertexBuffer* vertexBuffer = CE_NEW HardwareVertexBuffer(3U, 36U, HBU_STATIC, false);
void* buffer = vertexBuffer->Map();
memcpy(buffer, vtxData, sizeof(vtxData));
vertexBuffer->Unmap();
Mesh* mesh = MeshManager::Instance()->CreateManual("mytrianglemesh");
SubMesh* subMesh = mesh->CreateSubMesh();
subMesh->vertexData = CE_NEW VertexData(vertexBuffer);
subMesh->SetMaterialName("green_material");
subMesh->topology = PT_TRIANGLELIST;
Entity* entity = SceneManager::Instance()->CreateEntity("mytrianglemesh");
sceneNode->AttachObject(entity);

Rendering a full-screen triangle for post-processing

In this example we change the colours of the rendered scene to grayscale. First we need a shader that samples a texture and convert the pixel's colour to a grayscale value:

const char* const PostFxGpuProgram =
"Texture2D texture0 : register(t0);\n"
"SamplerState samplerLinear : register(s0);\n"
"struct VertexIn\n"
"{\n"
"    float2 uv : TEXCOORD0;\n"
"    float3 normal : NORMAL0;\n"
"    float3 position : POSITION0;\n"
"    float4 colour : COLOR0;\n"
"};\n"
"struct VertexOut\n"
"{\n"
"    float2 uv : TEXCOORD;\n"
"    float4 position : SV_Position;\n"
"    float4 colour : COLOR;\n"
"};\n"
"void DefaultVertexShader(VertexIn input, out VertexOut output)\n"
"{\n"
"    output.uv = input.uv;\n"
"    output.position = float4(input.position, 1.0f);\n"
"    output.colour = input.colour;\n"
"}\n"
"void DefaultFragmentShader(VertexOut input, out float4 output : SV_Target)\n"
"{\n"
"    float4 colour = texture0.Sample(samplerLinear, float2(input.uv.x, 1.0f - input.uv.y));\n"
"    float grey = 0.299f * colour.r + 0.587f * colour.g + 0.114f * colour.b;\n"
"    output = float4(grey, grey, grey, 1.0f);\n"
"};\n";

Next we setup the texture we going to render our scene to:

RenderTexture* scene_rt = new RenderTexture();
scene_rt->Create(window->GetWidth(), window->GetHeight(), PF_FLOAT32_RGBA, true);
Viewport* vp = scene_rt->AddViewport(camera, 0, 0, window->GetWidth(), window->GetHeight());
vp->SetBackgroundColour(0xff242424);

Then we'll have to create the pass (material) the full-screen triangle is rendered with:

Pass* postfx_pass = new Pass(0U);
TextureUnitState* tus = postfx_pass->CreateTextureUnitState();
Texture* rt_texture = new Texture(scene_rt);
tus->SetTexture(rt_texture);
GpuProgram* postfx_gpu_program = GpuProgramManager::Instance()->LoadFromSource("postfx.fx", PostFxGpuProgram, "DefaultVertexShader", "DefaultFragmentShader");
postfx_pass->SetGpuProgram(postfx_gpu_program);

The last step of the initialisation is to create the triangle mesh:

chrissly::graphics::Vertex vtx_data[3U] =
{
    {0.0f, 0.0f, 0xffffffff, 0.0f, 0.0f, 1.0f, -1.0f , -1.0f, 0.0f},
    {2.0f, 0.0f, 0xffffffff, 0.0f, 0.0f, 1.0f,  3.0f , -1.0f, 0.0f},
    {0.0f, 2.0f, 0xffffffff, 0.0f, 0.0f, 1.0f,  -1.0f,  3.0f, 0.0f}
};
HardwareVertexBuffer* vertex_buffer = CE_NEW HardwareVertexBuffer(3U, 36U, HBU_STATIC, false);
void* buffer = vertex_buffer->Map();
memcpy(buffer, vtx_data, sizeof(vtx_data));
vertex_buffer->Unmap();
Mesh* mesh = MeshManager::Instance()->CreateManual("fs_triangle_mesh");
SubMesh* sub_mesh = mesh->CreateSubMesh();
sub_mesh->vertexData = CE_NEW VertexData(vertex_buffer);
sub_mesh->topology = PT_TRIANGLELIST;
Entity* fs_triangle = SceneManager::Instance()->CreateEntity("fs_triangle_mesh");

In our renderloop GraphicsSystem::RenderOneFrame() will be replaced with the following code:

// render scene to texture
scene_rt->Update();
// switch to main render window
RenderSystem::Instance()->SetRenderTarget(window);
RenderSystem::Instance()->SetViewport(window->GetViewport(0U));
// set pass that applies posteffect and render full-screen triangle
RenderSystem::Instance()->SetPass(postfx_pass);
RenderSystem::Instance()->Render(fs_triangle->GetSubEntity(0U));
window->UpdateFrameTime();
window->SwapBuffers();
// manually raise frame number to update animations
GraphicsSystem::Instance()->RaiseFrameNumber();