Interesting Engine

Category : personal


It’s been a few years since I’ve built the original MonkeyEngine. I’ve learnt a lot since then, and I see quite a few things I’d like to improve or change. That’s why, also as a technical challenge to myself and to keep learning new things, I’ve started on a new version of the engine, called the Interesting Engine.

Research

As research, I’ve read the great book Game Engine Architecture by Jason Gregory, one of the Engine Developers at Naughty Dog. In the book, he breaks down a lot of the different components that make up a game engine, and shows the different approaches game engines can take to tackle certain challenges. This Interesting Engine project is a fun way for myself to try out some of the theory from the book.

As a first step, I’m currently focussing on the rendering pipeline. Where the MonkeyEngine only supported 2D (although it did have some support for 3D meshes at some point), this time I want to be able to create a full 3D world in my engine. I recently got hold of a Nintendo Switch Development Kit, so I’m combining these two.

What I have so far, is a simple renderer that can load meshes, materials with shaders, a skybox, and a simple scene in which you can move around with the camera. This is all built in C++, using the NVN library, Nintendo’s own low-level rendering library that is optimised for the Nintendo Switch hardware, and is quite similar in use to DirectX 12.

For further research, I’ve also been getting into DirectX 12, Raytracing, and looking into how I can combine the two.

In addition, I’ve also been looking at the source code of other game engines for reference and inspiration. Among these are the Godot Engine, the OGRE rendering engnine, and perhaps most interestingly, the HPL2 Engine by Frictional Games, used to create Amnesia: The Dark Descent. What I especially like about this last one, is the apparent simplicity of the code, making it very intuitive to browse through and see how all the moving parts connect. More on this last one below.

Mesh Loading

I needed a way to get mesh data into my engine. One possible solution that I’ve used in the past, is to implement the Assimp library to load an FBX file. But for now, that felt a bit like overkill, though I’ll probably add it in the future. Instead, I wrote a small exporter in Unity, that simply takes an array of all the meshes I want to export, and exports them into a binary file, containing only the data I actually want to use. In this case, the vertices, indices, normals and UV’s. This binary file can then be read directly in the engine.

foreach(var mesh in meshes)
{
    var data = new List<byte>();

    data.AddRange(StructToBytes(mesh.vertexCount));
    data.AddRange(StructToBytes(mesh.GetIndexCount(0)));

    for (int i = 0; i < mesh.vertexCount; i++)
    {
        data.AddRange(StructToBytes(mesh.vertices[i]));
        data.AddRange(StructToBytes(mesh.normals[i]));
        data.AddRange(StructToBytes(mesh.uv[i]));
    }
    
    foreach(var index in mesh.GetIndices(0))
    {
        data.AddRange(StructToBytes(index));
    }

    File.WriteAllBytes(Path.Combine(targetDirectory, $"{mesh.name}.mesh"), data.ToArray());
}

Rainy Hall

Having a textured mesh and a skybox is all well and good, but now it’s time to actually turn it into something usable. Since I was already browsing through the HPL Engine, I thought, perhaps I can recreate a level from Amnesia: The Dark Descent, running on the Nintendo Switch, in my own Engine. (Sadly, I’m not allowed to show code snippets using the Nintendo SDK here).

Unity

I have a copy of the game in my Steam library, so the first step was to go through the files there to see if there’s something I can work with. Luckily for me, and for the Amnesia Modding Community, there was plenty. All the asset files are available there in a well-structured manner. Maps are in open XML format, same goes for Entity information and Materials, and models are common DAE files, which also happen to be XML.

First I used Unity to rapidly test my level loader, before trying it in my own Engine. The level loader parses the map data for the Rainy Hall and instantiates and positions all the models. I learned a couple of interesting things from this:

  • Unity uses a different rotation order than the HPL engine, so models weren’t rotated property. The HPL engine rotates in XYZ order, whereas Unity will rotate in ZXY order. I fixed this by constructing the rotation matrix in a different order.
  • Unity uses a Left-handed coordinate system, whereas HPL is right-handed. As a result, all assets are mirrored compared to the original game. I scaled all the assets by -1 on the X-axis to remedy this. The entire level layout is still mirrored though.
  • Not all model files for Amnesia are exported in the same way. Some of them are Y-Up, while some are Z-Up. This lead to most assets being oriented correctly after loading, but some were facing the wrong way. I eventually figured this out by inspecting the DAE files themselves, in which I found the element Z_UP. A quick search through the HPL engine brought me to this beautiful snippet of code, indicating the engine fixes it at loading time:
if(tString(pAxisText->Value())=="Z_UP"){
    mbZToY = true;
    //Log("!!!!!!!Z IS UP!!!!!!");
}
  • To fix it in Unity, I wrote an asset post-processor that checks whether the model file contains the Z_UP element, and enables ‘Bake Axis Conversion’ on the imported model. This transforms the geometry itself from Z_UP to Y_UP. This made it easier for me to later load the meshes in my own engine.
  • Spot lights in Amnesia are squares instead of cones.
  • The textures of the walls aren’t tileable, but the seams are cleverly hidden behind pillars and in darkness.
  • There’s a bottle of Amnesia in the very first room you start in, but it can’t be seen in the game.

Interesting Engine

The result so far is a level that is loaded from the original Map XML file that you can completely fly through. It’s hard to capture the proper brightness of the scene in a photo or screenshot, because it depends on a lot of factors. For example, the scene looks a lot darker when viewed on the Switch screen than in the video.

  • Level data is loaded from XML using TinyXML-2.
  • To stay consistent with my Unity test, I use a left-handed coordinate system and a ZXY rotation order.
  • Model data is exported from Unity to my own XML format, containing only the relevant data I need, to make parsing easier.
  • Static Objects, Planes and Entites are all places in the level.
  • SolidDfifuse, Decals and Translucent objects have their own shaders.
  • Point Lights and Spot lights work, although for now the Spot lights use round cones instead of squares.
  • You have a lantern in your hand, with a flickering point light. I’ve also made the illumination texture flicker.
  • Materials can have illumination textures and normal maps. Specular maps and height maps aren’t supported yet.
  • The candles that can be lit in the game, can also be lit in this level.

Parsing the level file

void Scene::LoadAmnesiaMap(RenderSystem* pRenderSystem)
{
    InterestingFile xmlFile("00_rainy_hall.map");

    XMLDocument doc;
    doc.Parse(xmlFile.GetData(), xmlFile.GetDataSize());

    XMLElement* mapContents = doc.FirstChildElement("Level")->FirstChildElement("MapData")->FirstChildElement("MapContents");
    
    XMLElement* staticObjectsFileIndexElement = mapContents->FirstChildElement("FileIndex_StaticObjects");
    std::vector<File> staticObjectFiles;

    ParseFileIndex(staticObjectsFileIndexElement, staticObjectFiles);

    XMLElement* staticObjectsElement = mapContents->FirstChildElement("StaticObjects");
    std::vector<StaticObject> staticObjects;

    ParseStaticObjects(staticObjectsElement, staticObjects);

    float minX = std::numeric_limits<float>().max();
    float minZ = minX;
    float maxX = std::numeric_limits<float>().min();
    float maxZ = maxX;

    for (auto& staticObject : staticObjects)
    {
        File& file = staticObjectFiles[staticObject.FileIndex];

        GameObject* pRoot = new GameObject();
        GameObject* pObject = g_pInterestingGame->GetInterestingObjectLoader()->GetInterestingObject(file.Path + ".xml", nullptr, nullptr);
        
        auto positionStrVec = splitstr(staticObject.WorldPos, ' ');
        auto rotationStrVec = splitstr(staticObject.Rotation, ' ');
        auto scaleStrVec = splitstr(staticObject.Scale, ' ');

        glm::vec3 position(std::stof(positionStrVec[0]), std::stof(positionStrVec[1]), std::stof(positionStrVec[2]));
        pRoot->GetTransform()->m_Position = position;

        minX = glm::min(minX, position.x);
        maxX = glm::max(maxX, position.x);
        minZ = glm::min(minZ, position.z);
        maxZ = glm::max(maxZ, position.z);
        
        glm::vec3 euler(std::stof(rotationStrVec[0]), std::stof(rotationStrVec[1]), std::stof(rotationStrVec[2]));
        glm::mat4 rot = glm::eulerAngleZYX(euler.z, euler.y, euler.x);
        glm::extractEulerAngleYXZ(rot, euler.y, euler.x, euler.z);

        pRoot->GetTransform()->m_Rotation = euler;
        
        pRoot->GetTransform()->m_Scale = glm::vec3(-std::stof(scaleStrVec[0]), std::stof(scaleStrVec[1]), std::stof(scaleStrVec[2]));

        pRoot->AddChild(pObject);

        pRoot->UpdateObjectToWorldMatrix(glm::mat4(1.0f));
        
        m_GameObjects.push_back(pRoot);
    }

    Vertex planeVerts[] = {
        { glm::vec3(.0f,  .0f, .0f), glm::vec3(.0f, 1.0f, .0f), glm::vec4(-1.0f, .0f, .0f, -1.0f), glm::vec2(.0f, .0f)},
        { glm::vec3(.0f,  .0f, 1.0f), glm::vec3(.0f, 1.0f, .0f), glm::vec4(-1.0f, .0f, .0f, -1.0f), glm::vec2(.0f, 1.0f) },
        { glm::vec3(1.0f,  .0f, .0f), glm::vec3(.0f, 1.0f, .0f), glm::vec4(-1.0f, .0f, .0f, -1.0f), glm::vec2(1.0f, .0f) },
        { glm::vec3(1.0f,  .0f, 1.0f), glm::vec3(.0f, 1.0f, .0f), glm::vec4(-1.0f, .0f, .0f, -1.0f), glm::vec2(1.0f, 1.0f) }
    };

    uint32_t planeIndices[] = {
        0, 2, 1, 2, 3, 1
    };

    Mesh* planeMesh = new Mesh("");
    planeMesh->InitWithData(pRenderSystem, planeVerts, 4, planeIndices, 6);

    XMLElement* primitivesElement = mapContents->FirstChildElement("Primitives");
    std::vector<Plane> primitives;

    ParsePrimitives(primitivesElement, primitives);

    for (auto& plane : primitives)
    {
        GameObject* pPlaneObject = new GameObject("Plane");
        pPlaneObject->SetMesh(planeMesh);
        pPlaneObject->GetTransform()->m_Position = plane.WorldPos;

        minX = glm::min(minX, plane.WorldPos.x);
        maxX = glm::max(maxX, plane.WorldPos.x);
        minZ = glm::min(minZ, plane.WorldPos.z);
        maxZ = glm::max(maxZ, plane.WorldPos.z);

        glm::mat4 rot = glm::eulerAngleZYX(plane.Rotation.z, plane.Rotation.y, plane.Rotation.x);
        glm::extractEulerAngleYXZ(rot, plane.Rotation.y, plane.Rotation.x, plane.Rotation.z);
        pPlaneObject->GetTransform()->m_Rotation = plane.Rotation;

        glm::vec3 size = plane.EndCorner - plane.StartCorner;
        pPlaneObject->GetTransform()->m_Scale = plane.Scale * glm::vec3(size.x, 1.0f, size.z);

        pPlaneObject->UpdateObjectToWorldMatrix(glm::mat4(1.0f));

        auto material = new SolidLitMaterial();
        material->Init(pRenderSystem);

        auto texture = g_pInterestingGame->GetTextureManager()->GetTexture(plane.Material.substr(0, plane.Material.size() - 3) + "tex");
        material->SetAlbedoTexture(texture);

        material->SetNormalMapping(false);

        pPlaneObject->SetMaterial(material);

        m_GameObjects.push_back(pPlaneObject);
    }

    XMLElement* entitiesFileIndexElement = mapContents->FirstChildElement("FileIndex_Entities");
    std::vector<File> entityFiles;

    ParseFileIndex(entitiesFileIndexElement, entityFiles);

    XMLElement* entitiesElement = mapContents->FirstChildElement("Entities");
    std::vector<Entity> entities;
    std::vector<PointLightData> pointLights;
    std::vector<SpotLightData> spotLights;

    ParseEntities(entitiesElement, entities, pointLights, spotLights);

    for (auto& entity : entities)
    {
        File& file = entityFiles[entity.FileIndex];

        GameObject* pRoot = new GameObject();
        PointLight* pPointLight = nullptr;
        bool canBeLit;
        GameObject* pObject = g_pInterestingGame->GetInterestingObjectLoader()->GetInterestingObject(file.Path.substr(0, file.Path.size() - 3) + "dae.xml", &pPointLight, &canBeLit);

        if (pPointLight)
        {
            pPointLight->m_Transform.m_Position = entity.WorldPos + glm::vec3(-pPointLight->m_Transform.m_Position.x, pPointLight->m_Transform.m_Position.y, pPointLight->m_Transform.m_Position.z);
            m_LightSources.push_back(pPointLight);

            if (entity.UserVariables["Lit"] == "true")
            {
                SetIlluminationFactor(pObject, 1.0f);
            }
            else
            {
                SetIlluminationFactor(pObject, .0f);
            }

            if (canBeLit)
            {
                LightableLightSource* pLightable = new LightableLightSource();
                pLightable->m_pLightSource = pPointLight;
                pLightable->m_pGameObject = pRoot;

                m_LightableSources.push_back(pLightable);
            }
        }

        pRoot->GetTransform()->m_Position = entity.WorldPos;

        minX = glm::min(minX, entity.WorldPos.x);
        maxX = glm::max(maxX, entity.WorldPos.x);
        minZ = glm::min(minZ, entity.WorldPos.z);
        maxZ = glm::max(maxZ, entity.WorldPos.z);

        glm::mat4 rot = glm::eulerAngleZYX(entity.Rotation.z, entity.Rotation.y, entity.Rotation.x);
        glm::extractEulerAngleYXZ(rot, entity.Rotation.y, entity.Rotation.x, entity.Rotation.z);

        pRoot->GetTransform()->m_Rotation = entity.Rotation;

        pRoot->GetTransform()->m_Scale = glm::vec3(-entity.Scale.x, entity.Scale.y, entity.Scale.z);

        pRoot->AddChild(pObject);

        pRoot->UpdateObjectToWorldMatrix(glm::mat4(1.0f));

        m_GameObjects.push_back(pRoot);
    }

    for (auto& pointLight : pointLights)
    {
        PointLight* pPointLight = new PointLight();
        pPointLight->m_Transform.m_Position = pointLight.WorldPos;
        pPointLight->m_DiffuseColour = pointLight.DiffuseColor;
        pPointLight->m_Range = pointLight.Radius;
        pPointLight->m_Intensity = 1.0f;

        m_LightSources.push_back(pPointLight);
    }

    for (auto& spotLight : spotLights)
    {
        SpotLight* pSpotLight = new SpotLight();
        pSpotLight->m_Transform.m_Position = spotLight.WorldPos;
        pSpotLight->m_DiffuseColour = spotLight.DiffuseColor;
        pSpotLight->m_Range = spotLight.Radius;
        pSpotLight->m_Angle = spotLight.FOV;
        pSpotLight->m_Intensity = 1.0f;
        
        glm::mat4 rot = glm::eulerAngleZYX(spotLight.Rotation.z, spotLight.Rotation.y, spotLight.Rotation.x);
        glm::mat4 flip = glm::eulerAngleXYZ(.0f, glm::radians(180.0f), .0f);

        rot = rot * flip;

        glm::vec4 forward(.0f, .0f, 1.0f, .0f);
        forward = rot * forward;

        pSpotLight->m_Transform.m_Rotation = glm::vec3(forward.x, forward.y, forward.z);

        m_LightSources.push_back(pSpotLight);
    }

    LoadHand(pRenderSystem);

    for (GameObject* pGameObject : m_GameObjects)
    {
        UpdateClosestLights(pGameObject, m_LightSources);
    }

    m_RenderChunkOffsetX = -minX;
    m_RenderChunkOffsetZ = -minZ;

    float renderChunkRangeX = maxX - minX;
    float renderChunkRangeZ = maxZ - minZ;

    int renderChunksX = (renderChunkRangeX / g_RenderChunkSize) + 1;
    int renderChunksZ = (renderChunkRangeZ / g_RenderChunkSize) + 1;
    int numRenderChunks = renderChunksX * renderChunksZ;

    m_RenderChunks.resize(numRenderChunks, nullptr);
    for (int i = 0; i < m_RenderChunks.size(); ++i)
    {
        m_RenderChunks[i] = new RenderChunk();

        int xIndex = i % renderChunksX;
        int zIndex = i / renderChunksZ;

        float x = -m_RenderChunkOffsetX + xIndex * g_RenderChunkSize;
        float z = -m_RenderChunkOffsetZ + zIndex * g_RenderChunkSize;

        m_RenderChunks[i]->m_CenterPoint = glm::vec3(x, .0f, z);
    }

    for (auto pGameObject : m_GameObjects)
    {
        float x = pGameObject->GetTransform()->m_Position.x + m_RenderChunkOffsetX;
        float z = pGameObject->GetTransform()->m_Position.z + m_RenderChunkOffsetZ;

        int chunkIndexX = x / g_RenderChunkSize;
        int chunkIndexZ = z / g_RenderChunkSize;

        int chunkIndex = chunkIndexZ * renderChunksX + chunkIndexX;

        AddGameObjectToChunk(m_RenderChunks[chunkIndex], pGameObject);
    }
}

Parsing Mesh objects

GameObject* InterestingObjectLoader::GetInterestingObject(const std::string& filepath, PointLight** ppOutPointLight, bool* pOutCanBeLit)
{
    BaseResource* pResource = GetResource(filepath);
    if (pResource != nullptr)
    {
        return static_cast<GameObject*>(pResource);
    }
    else
    {
        InterestingFile file(filepath);

        XMLDocument doc;
        doc.Parse(reinterpret_cast<const char*>(file.GetData()), file.GetDataSize());

        return ProcessElement(doc.FirstChildElement("InterestingMeshObject"), ppOutPointLight, pOutCanBeLit);
    }
}

GameObject* InterestingObjectLoader::ProcessElement(XMLElement* pElement, PointLight** ppOutPointLight, bool* pOutCanBeLit)
{
    std::string name = pElement->Attribute("Name");
    if (name[0] == '_') return nullptr;

    GameObject* obj = new GameObject(pElement->Attribute("Name"));

    std::string positionStr = pElement->Attribute("Position");
    std::string rotationStr = pElement->Attribute("Rotation");
    std::string scaleStr = pElement->Attribute("Scale");

    obj->GetTransform()->m_Position = SToVec3(positionStr);
    obj->GetTransform()->m_Rotation = SToVec3(rotationStr);
    obj->GetTransform()->m_Scale = SToVec3(scaleStr);

    XMLElement* pMeshDataElement;
    if ((pMeshDataElement = pElement->FirstChildElement("MeshData")))
    {
        size_t numVertices = std::stoi(pMeshDataElement->FirstChildElement("VertexCount")->GetText());

        auto verticesStrVec = split(pMeshDataElement->FirstChildElement("Vertices")->GetText(), ' ');
        auto normalsStrVec = split(pMeshDataElement->FirstChildElement("Normals")->GetText(), ' ');
        auto tangentsStrVec = split(pMeshDataElement->FirstChildElement("Tangents")->GetText(), ' ');
        auto uvsStrVec = split(pMeshDataElement->FirstChildElement("UVs")->GetText(), ' ');
        auto indicesStrVec = split(pMeshDataElement->FirstChildElement("Indices")->GetText(), ' ');

        std::vector<Vertex> vertices;
        for (size_t i = 0; i < numVertices; ++i)
        {
            Vertex v;
            v.position = glm::vec3(std::stof(verticesStrVec[i * 3]), std::stof(verticesStrVec[i * 3 + 1]), std::stof(verticesStrVec[i * 3 + 2]));
            v.normal = glm::vec3(std::stof(normalsStrVec[i * 3]), std::stof(normalsStrVec[i * 3 + 1]), std::stof(normalsStrVec[i * 3 + 2]));
            v.tangent = glm::vec4(std::stof(tangentsStrVec[i * 4]), std::stof(tangentsStrVec[i * 4 + 1]), std::stof(tangentsStrVec[i * 4 + 2]), std::stof(tangentsStrVec[i * 4 + 3]));
            v.uv = glm::vec2(std::stof(uvsStrVec[i * 2]), std::stof(uvsStrVec[i * 2 + 1]));
            vertices.push_back(std::move(v));
        }

        std::vector<uint32_t> indices;
        for (size_t i = 0; i < indicesStrVec.size(); ++i)
        {
            indices.push_back(std::stoi(indicesStrVec[i]));
        }

        Mesh* mesh = new Mesh("");
        mesh->InitWithData(m_pRenderSystem, vertices.data(), numVertices, indices.data(), indices.size());

        obj->SetMesh(mesh);

        std::string materialType = pElement->Attribute("Material");

        Material* material;

        if(materialType == "soliddiffuse" || materialType == "water" || materialType == "")
        { 
            auto solidMaterial = new SolidLitMaterial();
            solidMaterial->Init(m_pRenderSystem);
        
            std::string textureStr(pElement->Attribute("Texture"));
            if (textureStr.size() > 0)
            {
                auto texture = g_pInterestingGame->GetTextureManager()->GetTexture(textureStr + ".tex");
                solidMaterial->SetAlbedoTexture(texture);
            }
            
            std::string normalStr(pElement->Attribute("Normal"));
            if (normalStr.size() > 0)
            {
                auto normal = g_pInterestingGame->GetTextureManager()->GetTexture(normalStr + ".tex");
                solidMaterial->SetNormalTexture(normal);
                solidMaterial->SetNormalMapping(true);
            }
            else
            {
                solidMaterial->SetNormalMapping(false);
            }

            std::string illumStr(pElement->Attribute("Illumination"));
            if (illumStr.size() > 0)
            {
                auto illum = g_pInterestingGame->GetTextureManager()->GetTexture(illumStr + ".tex");
                solidMaterial->SetIlluminationTexture(illum);
                solidMaterial->SetIlluminationFactor(1.0f);
            }

            material = solidMaterial;
        }
        else if (materialType == "translucent")
        {
            auto translucentMaterial = new TransparentUnlitMaterial();
            translucentMaterial->Init(m_pRenderSystem);

            std::string textureStr(pElement->Attribute("Texture"));
            if (textureStr.size() > 0)
            {
                auto texture = g_pInterestingGame->GetTextureManager()->GetTexture(textureStr + ".tex");
                translucentMaterial->SetAlbedoTexture(texture);
            }

            material = translucentMaterial;
        }
        else if (materialType == "decal")
        {
            auto decalMaterial = new DecalUnlitMaterial();
            decalMaterial->Init(m_pRenderSystem);

            std::string textureStr(pElement->Attribute("Texture"));
            if (textureStr.size() > 0)
            {
                auto texture = g_pInterestingGame->GetTextureManager()->GetTexture(textureStr + ".tex");
                decalMaterial->SetAlbedoTexture(texture);
            }

            material = decalMaterial;
        }
        else
        {
            material = nullptr;
        }

        obj->SetMaterial(material);
    }

    if (ppOutPointLight)
    {
        XMLElement* pPointLightElement;
        if ((pPointLightElement = pElement->FirstChildElement("PointLight")))
        {
            PointLight* p = new PointLight();
            p->m_DiffuseColour = SToVec4(pPointLightElement->Attribute("DiffuseColor"));
            p->m_Range = pPointLightElement->FloatAttribute("Radius");
            p->m_Transform.m_Position = SToVec3(pPointLightElement->Attribute("WorldPos"));
            p->m_Intensity = .0f;

            *pOutCanBeLit = pElement->BoolAttribute("CanBeLit");

            *ppOutPointLight = p;
        }
    }

    XMLElement* pChildrenElement;
    if ((pChildrenElement = pElement->FirstChildElement("Children")))
    {
        for (XMLElement* pChildElement = pChildrenElement->FirstChildElement(); pChildElement; pChildElement = pChildElement->NextSiblingElement())
        {
            GameObject* pChild = ProcessElement(pChildElement, ppOutPointLight, pOutCanBeLit);
            if (pChild != nullptr)
            {
                obj->AddChild(pChild);
            }
        }
    }

    return obj;
}

Performance

  • My version runs the level at a stable frame rate of 60 fps.
  • At first, a lot of performance issues came from unnecessary copying of data from the CPU to the GPU every frame. For example, the ModelView matrix was calculated and sent to the GPU before rendering every time. I fixed a lot of these issues by only updating certain information when necessary, pre-calculating child-to-world-matrices, and for example using global uniform buffers in shaders for information that is shared between materials and material instances.
  • For now, the engine uses forward rendering to simplify the rendering pipeline at this stage. I know Amnesia uses deferred rendering, so that would be a logical next step to implement.
  • For rendering, the level is divided into chunks of 10 by 10 meters, and only the closest few are rendered, as a rudimentary form of occlusion culling.

Next Steps

Comparing my test level to the original game, I’m reasonably pleased so far, but it’s obvious there’s still a lot of work to be done to make it as atmospheric as the original.

  • Switch to deferred rendering. This would allow me to better support all the dynamic lights in the level, as these lights are extremely important to the atmosphere. It would also allow me to add shadows to the level.
  • Halo’s around the light sources.
  • Billboards, to for example create the God Rays by the windows.
  • Particles, used for fog and the candles.
  • Swinging lantern, for a more dynamic feel to the level.
  • Support for specular maps.
  • Support for fall-off textures to use with the lights, because the fall-off is now linear.
  • Square point lights with projection textures, to project the window frame on the floor.
  • Flickering lights for the lightning effects.
  • For some reason, as you can also see in the video, the face Normals in the paintings seem to be pointing the wrong way, messing up the lighting… I should figure out why that is.
  • Post-processing effects, such as bloom.
  • Make the renderer multi-threaded.
  • Better memory management to avoid cache misses and dynamic allocations.
  • And plenty more.

Going even further

  • Getting this to work in DirectX using ray-tracing seems like a fun challenge.

WIP: Render the level in DirectX 12

void RenderGameObject(ComPtr<ID3D12GraphicsCommandList2> pCommandList, GameObject* pGameObject, Camera* pCamera)
{
    for (auto& pChild : *pGameObject->GetChildren())
    {
        RenderGameObject(pCommandList, pChild, pCamera);
    }

    if (pGameObject->GetMaterial() && pGameObject->GetMesh())
    {
        pGameObject->GetMaterial()->Bind(pCommandList);

        struct BufferInput
        {
            glm::mat4 model;
            glm::mat4 view;
            glm::mat4 projection;
            glm::vec3 camPos;
        };

        glm::mat4 mvp = pCamera->GetProjectionMatrix() * pCamera->GetViewMatrix() * pGameObject->m_ObjectToWorldMatrix;
        BufferInput input
        {
            pGameObject->m_ObjectToWorldMatrix,
            pCamera->GetViewMatrix(),
            pCamera->GetProjectionMatrix(),
            pCamera->GetTransform().m_Position
        };

        pCommandList->SetGraphicsRoot32BitConstants(0, sizeof(BufferInput) / 4, &input, 0);

        pGameObject->GetMesh()->Bind(pCommandList);
        pGameObject->GetMesh()->Draw(pCommandList);
    }
}

void RenderSystem::Render()
{
    auto commandQueue = m_DirectCommandQueue;
    auto commandList = commandQueue->GetCommandList();

    UINT currentBackBufferIndex = m_CurrentBackBufferIndex;
    auto backBuffer = m_BackBuffers[currentBackBufferIndex];
    CD3DX12_CPU_DESCRIPTOR_HANDLE rtv(m_RTVDescriptorHeap->GetCPUDescriptorHandleForHeapStart(), m_CurrentBackBufferIndex, m_RTVDescriptorSize);
    auto dsv = m_DSVHeap->GetCPUDescriptorHandleForHeapStart();

    {
        CD3DX12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition(backBuffer.Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);

        commandList->ResourceBarrier(1, &barrier);

        FLOAT clearColour[] = { 0.4f, 0.6f, .9f, 1.0f };

        commandList->ClearRenderTargetView(rtv, clearColour, 0, nullptr);
        commandList->ClearDepthStencilView(dsv, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
    }

    commandList->RSSetViewports(1, &m_Viewport);
    commandList->RSSetScissorRects(1, &m_ScissorRect);

    commandList->OMSetRenderTargets(1, &rtv, FALSE, &dsv);

    for (auto& pGameObject : m_CurrentScene.m_GameObjects)
    {
        RenderGameObject(commandList, pGameObject, &m_CurrentScene.m_Camera);
    }

    {
        CD3DX12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition(backBuffer.Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);

        commandList->ResourceBarrier(1, &barrier);

        m_FrameFenceValues[m_CurrentBackBufferIndex] = commandQueue->ExecuteCommandList(commandList);

        UINT syncInterval = m_VSync ? 1 : 0;
        UINT presentFlags = m_TearingSupported && !m_VSync ? DXGI_PRESENT_ALLOW_TEARING : 0;
        ThrowIfFailed(m_SwapChain->Present(syncInterval, presentFlags));

        m_CurrentBackBufferIndex = m_SwapChain->GetCurrentBackBufferIndex();

        commandQueue->WaitForFenceValue(m_FrameFenceValues[m_CurrentBackBufferIndex]);
    }
}
void Mesh::InitWithData(RenderSystem* pRenderSystem, Vertex* vertices, size_t numVertices, uint32_t* indices, size_t numIndices)
{
	m_Vertices.resize(numVertices);
	memcpy(m_Vertices.data(), vertices, numVertices * sizeof(Vertex)); //Copy is probably unnecessary, but it works for now

	m_Indices.resize(numIndices);
	memcpy(m_Indices.data(), indices, numIndices * sizeof(uint32_t)); //Copy is probably unnecessary, but it works for now

	size_t attributeDataSize = sizeof(Vertex) * m_Vertices.size();
	size_t indexDataSize = sizeof(uint32_t) * m_Indices.size();

	auto commandQueue = pRenderSystem->GetCopyCommandQueue();
	auto commandList = commandQueue->GetCommandList();

	ComPtr<ID3D12Resource> intermediateVertexBuffer;
	pRenderSystem->UpdateBufferResource(commandList, &m_VertexBuffer, &intermediateVertexBuffer, m_Vertices.size(), sizeof(Vertex), m_Vertices.data());

	m_VertexBufferView.BufferLocation = m_VertexBuffer->GetGPUVirtualAddress();
	m_VertexBufferView.SizeInBytes = attributeDataSize;
	m_VertexBufferView.StrideInBytes = sizeof(Vertex);

	ComPtr<ID3D12Resource> intermediateIndexBuffer;
	pRenderSystem->UpdateBufferResource(commandList, &m_IndexBuffer, &intermediateIndexBuffer, m_Indices.size(), sizeof(uint32_t), m_Indices.data());

	m_IndexBufferView.BufferLocation = m_IndexBuffer->GetGPUVirtualAddress();
	m_IndexBufferView.Format = DXGI_FORMAT_R32_UINT;
	m_IndexBufferView.SizeInBytes = indexDataSize;

	auto fenceValue = commandQueue->ExecuteCommandList(commandList);
	commandQueue->WaitForFenceValue(fenceValue);
}

void Mesh::Bind(ComPtr<ID3D12GraphicsCommandList2> pCommandList)
{
	pCommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	pCommandList->IASetVertexBuffers(0, 1, &m_VertexBufferView);
	pCommandList->IASetIndexBuffer(&m_IndexBufferView);
}

void Mesh::Draw(ComPtr<ID3D12GraphicsCommandList2> pCommandList)
{
	pCommandList->DrawIndexedInstanced(m_Indices.size(), 1, 0, 0, 0);
}
void Shader::Initialize(RenderSystem* pRenderSystem)
{
	if (!m_IsInitialized)
	{
		m_pRenderSystem = pRenderSystem;

		auto commandQueue = pRenderSystem->GetCopyCommandQueue();
		auto commandList = commandQueue->GetCommandList();

		ComPtr<ID3DBlob> vertexShaderBlob;
		ThrowIfFailed(D3DReadFileToBlob(L"C:\\Users\\vince\\Projects\\TrialsOfADX12\\x64\\Debug\\vertex.cso", &vertexShaderBlob));

		ComPtr<ID3DBlob> pixelShaderBlob;
		ThrowIfFailed(D3DReadFileToBlob(L"C:\\Users\\vince\\Projects\\TrialsOfADX12\\x64\\Debug\\pixel.cso", &pixelShaderBlob));

		D3D12_INPUT_ELEMENT_DESC inputLayout[] =
		{
			{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
			{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
			{ "TANGENT", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
			{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
		};

		D3D12_FEATURE_DATA_ROOT_SIGNATURE featureData = {};
		featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_1;
		if (FAILED(pRenderSystem->GetDevice()->CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE, &featureData, sizeof(featureData))))
		{
			featureData.HighestVersion = D3D_ROOT_SIGNATURE_VERSION_1_0;
		}

		D3D12_ROOT_SIGNATURE_FLAGS rootSignatureFlags =
			D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT |
			D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS |
			D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS |
			D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS;

		CD3DX12_DESCRIPTOR_RANGE1 descriptorRange(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
		CD3DX12_DESCRIPTOR_RANGE1 pixelConstantRange(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 1);

		CD3DX12_DESCRIPTOR_RANGE1 ranges[]
		{
			descriptorRange, pixelConstantRange
		};

		CD3DX12_ROOT_PARAMETER1 rootParameters[2];
		rootParameters[0].InitAsConstants((sizeof(glm::mat4) / 4) * 3 + sizeof(glm::vec3) / 4, 0, 0, D3D12_SHADER_VISIBILITY_VERTEX);
		rootParameters[1].InitAsDescriptorTable(_countof(ranges), ranges, D3D12_SHADER_VISIBILITY_PIXEL);

		CD3DX12_STATIC_SAMPLER_DESC linearRepeatSampler(0, D3D12_FILTER_COMPARISON_MIN_MAG_MIP_LINEAR);

		CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC rootSignatureDescription;
		rootSignatureDescription.Init_1_1(_countof(rootParameters), rootParameters, 1, &linearRepeatSampler, rootSignatureFlags);

		ComPtr<ID3DBlob> rootSignatureBlob;
		ComPtr<ID3DBlob> errorBlob;
		ThrowIfFailed(D3DX12SerializeVersionedRootSignature(&rootSignatureDescription,
			featureData.HighestVersion, &rootSignatureBlob, &errorBlob));

		ThrowIfFailed(pRenderSystem->GetDevice()->CreateRootSignature(0, rootSignatureBlob->GetBufferPointer(),
			rootSignatureBlob->GetBufferSize(), IID_PPV_ARGS(&m_RootSignature)));

		struct PipelineStateStream
		{
			CD3DX12_PIPELINE_STATE_STREAM_ROOT_SIGNATURE pRootSignature;
			CD3DX12_PIPELINE_STATE_STREAM_INPUT_LAYOUT InputLayout;
			CD3DX12_PIPELINE_STATE_STREAM_PRIMITIVE_TOPOLOGY PrimitiveTopologyType;
			CD3DX12_PIPELINE_STATE_STREAM_VS VS;
			CD3DX12_PIPELINE_STATE_STREAM_PS PS;
			CD3DX12_PIPELINE_STATE_STREAM_DEPTH_STENCIL_FORMAT DSVFormat;
			CD3DX12_PIPELINE_STATE_STREAM_RENDER_TARGET_FORMATS RTVFormats;
			CD3DX12_PIPELINE_STATE_STREAM_RASTERIZER Rasterizer;
		} pipelineStateStream;

		D3D12_RT_FORMAT_ARRAY rtvFormats = {};
		rtvFormats.NumRenderTargets = 1;
		rtvFormats.RTFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;

		CD3DX12_RASTERIZER_DESC  rasterizerDesc = {};
		rasterizerDesc.FillMode = D3D12_FILL_MODE_SOLID;
		rasterizerDesc.CullMode = D3D12_CULL_MODE_BACK;
		rasterizerDesc.FrontCounterClockwise = TRUE;
		rasterizerDesc.DepthBias = D3D12_DEFAULT_DEPTH_BIAS;
		rasterizerDesc.DepthBiasClamp = D3D12_DEFAULT_DEPTH_BIAS_CLAMP;
		rasterizerDesc.SlopeScaledDepthBias = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS;
		rasterizerDesc.DepthClipEnable = TRUE;
		rasterizerDesc.MultisampleEnable = FALSE;
		rasterizerDesc.AntialiasedLineEnable = FALSE;
		rasterizerDesc.ForcedSampleCount = 0;
		rasterizerDesc.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF;

		pipelineStateStream.pRootSignature = m_RootSignature.Get();
		pipelineStateStream.InputLayout = { inputLayout, _countof(inputLayout) };
		pipelineStateStream.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
		pipelineStateStream.VS = CD3DX12_SHADER_BYTECODE(vertexShaderBlob.Get());
		pipelineStateStream.PS = CD3DX12_SHADER_BYTECODE(pixelShaderBlob.Get());
		pipelineStateStream.DSVFormat = DXGI_FORMAT_D32_FLOAT;
		pipelineStateStream.RTVFormats = rtvFormats;
		pipelineStateStream.Rasterizer = rasterizerDesc;

		D3D12_PIPELINE_STATE_STREAM_DESC pipelineStateStreamDesc = {
			sizeof(PipelineStateStream), &pipelineStateStream
		};
		ThrowIfFailed(pRenderSystem->GetDevice()->CreatePipelineState(&pipelineStateStreamDesc, IID_PPV_ARGS(&m_PipelineState)));

		auto fenceValue = commandQueue->ExecuteCommandList(commandList);
		commandQueue->WaitForFenceValue(fenceValue);

		m_IsInitialized = true;
	}
}
struct ModelViewProjection
{
    matrix model;
    matrix view;
    matrix projection;
    float3 camPos;
};

ConstantBuffer<ModelViewProjection> ModelViewProjectionCB : register(b0);

struct Vertex
{
    float3 Position : POSITION;
    float3 Normal : NORMAL;
    float4 Tangent : TANGENT;
    float2 TexCoord : TEXCOORD;
};

struct VertexShaderOutput
{
    float2 TexCoord : TEXCOORD;
    float3 Normal : NORMAL;
    float3 Tangent : TANGENT;
    float3 Binormal : BINORMAL;
    float3 Pos : POS;
    float3 CameraPosition : CAMPOS;
    float4 Position : SV_Position;
};

VertexShaderOutput main(Vertex input)
{
    VertexShaderOutput output;
    
    output.Pos = mul(ModelViewProjectionCB.model, float4(input.Position, 1.0f)).xyz;
    
    output.Position = mul(ModelViewProjectionCB.projection, mul(ModelViewProjectionCB.view, mul(ModelViewProjectionCB.model, float4(input.Position, 1.0f))));
    output.TexCoord = input.TexCoord;
    output.CameraPosition = ModelViewProjectionCB.camPos;
    
    output.Normal = mul(ModelViewProjectionCB.model, float4(input.Normal, .0f)).xyz; //normalize(transpose(inverse(ModelViewProjectionCB.model)));
    output.Tangent = mul(ModelViewProjectionCB.model, input.Tangent).xyz;
    output.Binormal = normalize(cross(output.Normal, output.Tangent)) * input.Tangent.w;
    
    return output;
}
struct PixelShaderInput
{
    float2 TexCoord : TEXCOORD;
    float3 Normal : NORMAL;
    float3 Tangent : TANGENT;
    float3 Binormal : BINORMAL;
    float3 Pos : POS;
    float3 CameraPosition : CAMPOS;
};

struct PointLight
{
    float3 Position;
    float radius;
    float4 diffuseColour;
    float intensity;
};

cbuffer SceneData : register(b1)
{
    PointLight PointLights[4];
    float padding[28];
};

Texture2D DiffuseTexture : register(t0);

SamplerState LinearRepeatSampler : register(s0);

float3 Point(int lightIndex, float3 pos, float3 normalDir)
{
    float3 lightDir = PointLights[lightIndex].Position - pos;
    float3 nDir = normalize(lightDir);

    float d = length(lightDir);
    float p = max(1.0f - (d / PointLights[lightIndex].radius), .0f);

    return float3(p, p, p) * PointLights[lightIndex].diffuseColour.xyz * max(dot(nDir, normalDir), .0f) * PointLights[lightIndex].intensity;
}

float4 main(PixelShaderInput input) : SV_Target
{
    float4 albedo = DiffuseTexture.Sample(LinearRepeatSampler, float2(input.TexCoord.x, 1.0 - input.TexCoord.y));
    if (albedo.a < .01)
        discard;

    float3 normalDir = normalize(input.Normal);
    
    float3 totalLight = float3(.1f, .1f, .1f);
    
    for (int i = 0; i < 4; ++i)
    {
        totalLight += Point(i, input.Pos, normalDir);
    }

    return float4(albedo.rgb * totalLight, 1.0f);
}

About Vincent Booman

Hi, my name is Vincent Booman. For me, development is not about a specific language or software, it's about what kind of impact they enable.

Follow @Feathora