Skip to main content

Object Painter

Introduction
#

During the development of ‘Walter Volt’ I began to create a object painter. The goal of the tool was to allow our Level Designers to easily iterate their levels through quick creation as well as make set dressing less frustrating.

The painter allows the user to:

  • Add any number of objects into its pool
  • Paint random objects from the pool
  • Paint objects in order of the pool
  • Apply a random scale and rotation to placed object
  • Single target painting (On mouse position)
  • Random target painting (Radius around mouse position)

Not Foliage
#

Foliage is something that require specialised tools and pipelines. As we were in need of a tool that could assist in general non foliage setdressing, i decided to create a tool that allowed designers to “paint” pre defined objects into the scene. I plan to expand the tool to have a “Foliage Mode” when the need arises.

Implementation
#

The painter is a static class that exists within our editor. Almost all functions are used to either get or set data for the “main” function ‘TryGetPaintMatrix()’ to use.

ObjectPainting.h

class ObjectPainting
{
public:

    static Matrix4x4f TryGetPaintMatrix(const ImVec2& aMousePos, RenderData* aRenderData);
    static Matrix4x4f GetEraserMatrix(const ImVec2& aMousePos, RenderData* aRenderData);
    static void DrawUI();
    
    static void SetViewportSize(const Vector2f& aViewportSize){ ourViewportSize = aViewportSize; }
    static void SetViewportPos(const Vector2f& aViewportPos) { ourViewportPos = aViewportPos; }
    static void SetActiveCamera(Camera& aCamera) { ourCamera = &aCamera; };
    
    static void ExtractWorldPosition(ID3D11ShaderResourceView* aSRV, bool aRecall = false);
    static void ExtractVertexNormal(ID3D11ShaderResourceView* aSRV);
    
    static void IncrementOrderIndex();
    
    static std::vector<InternalPropertyTypes::AssetPath> GetAssetPaths() { return ourPaths; }
    
    static bool InPaintMode() { return ourPaintMode; }
    static bool InEraseMode() { return ourEraserMode; }
    
    static bool IsActive() { return ourPaintMode || ourEraserMode; }
    
    static bool InRandomPlacement() { return ourRandomMode; }
    static bool InRandomScale() { return ourRandomScaleMode; }
    static bool InRandomRotation() { return ourRandomRotationMode; }

    static int GetCurrentOrderIndex() { return ourCurrentOrderIndex; }
    
    static Vector3f GetMinScaleValue() { return ourMinScaleValue; }
    static Vector3f GetMaxScaleValue() { return ourMaxScaleValue; }
    static Vector3f GetMinRotValue() { return ourMinRotValue; }
    static Vector3f GetMaxRotValue() { return ourMaxRotValue; }
private:
    static Quaternionf FromToHelper(Vector3f aFrom, Vector3f aTo);
    
    static Vector3f GetRandomPoint(Matrix4x4f& aTransform);

    static Matrix4x4f DrawBrush(RenderData* aRenderData, Vector3f aHitWorld, float aWorldRadius, bool aEraseMode = false);
    
    static inline bool ourPaintMode{ false };
    static inline bool ourEraserMode { false };
    static inline bool ourSingleTargetMode{ true };
    static inline bool ourRandomMode{ true };
    static inline bool ourRandomScaleMode{ true };
    static inline bool ourRandomRotationMode{ true };

    static inline int ourCurrentOrderIndex { 0 };
    
    static inline float ourPaintingCircumference{ 500.f };
    
    static inline float ourPaintingDelay{ 100.f };

    static inline Vector2ui ourCurrentRevertedViewPos { 0 };
    
    static inline Vector3f ourLastPosition{ 0.f };
    static inline Vector3f ourCurrentPosition{ 0.f };
    static inline Vector3f ourCurrentRandomPosition{ 0.f };
    static inline Vector3f ourCurrentVertexNormal{ 0.f };
    
    static inline Vector3f ourMinScaleValue{ 1.f };
    static inline Vector3f ourMaxScaleValue{ 1.f };
    
    static inline Vector3f ourMinRotValue{ 0.f };
    static inline Vector3f ourMaxRotValue{ 0.f };
    
    static inline Vector2f ourViewportSize{ 0.f, 0.f };
    static inline Vector2f ourViewportPos{ 0.f, 0.f };

    static inline Matrix4x4f ourBrushMatrix{};
    
    static inline std::vector<InternalPropertyTypes::AssetPath> ourPaths;
    static inline Camera* ourCamera;
};

World/Vertex texture
#

The terrain we use is rendered before all existing objects. Because of this, when active, the object painter reads the world position and vertex normal texture after the terrain has been rendered. This allows it to get a “copy of the world” where only the terrain exists.

// Terrain(RenderData& aRenderData, ResourceManager& aResourceManager)
ObjectPainting::ExtractWorldPosition(aResourceManager.gBuffer.GetTexture(GBufferTexture::WorldPosition).GetSRV(), false);
ObjectPainting::ExtractVertexNormal(aResourceManager.gBuffer.GetTexture(GBufferTexture::VertexNormal).GetSRV());

The object painter uses this buffer to read the world position and vertex normal of the current pixel that the mouse is hovering over, only returning a valid value when hovering over the terrain. The reason I only allow painting on the terrain is to ensure that painted objects dont stack on top of eachother as well as avoiding casting raycasts.

ExtractWorldPosition - Reading the texture
 
{
    // ...
    ID3D11Resource* src;
    const auto& context = DX11::Context;
    aSRV->GetResource(&src);

    D3D11_TEXTURE2D_DESC textureDesc;
    textureDesc.Width = 1;
    textureDesc.Height = 1;
    textureDesc.MipLevels = 1;
    textureDesc.ArraySize = 1;
    textureDesc.Format = (DXGI_FORMAT_R32G32B32A32_FLOAT);
    textureDesc.SampleDesc.Count = 1;
    textureDesc.SampleDesc.Quality = 0;
    textureDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    textureDesc.Usage = D3D11_USAGE_STAGING;
    textureDesc.BindFlags = 0;
    textureDesc.MiscFlags = 0;

    ComPtr<ID3D11Texture2D> tmp;
    HRESULT hr = DX11::Device->CreateTexture2D(&textureDesc, nullptr, tmp.GetAddressOf());
    assert(SUCCEEDED(hr));

    D3D11_BOX srcBox;
    srcBox.left = pos.x;
    srcBox.right = pos.x + 1;
    srcBox.top = pos.y;
    srcBox.bottom = pos.y + 1;
    srcBox.front = 0;
    srcBox.back = 1;

    DX11::Context->CopySubresourceRegion(
        tmp.Get(),
        0, 0, 0, 0,
        src, 0,
        &srcBox
    );
    D3D11_MAPPED_SUBRESOURCE msr = {};
    
    hr = context->Map(tmp.Get(), 0, D3D11_MAP_READ, 0, &msr);
    assert(SUCCEEDED(hr));

    const float* data{ reinterpret_cast<float*>(msr.pData) };
    
    context->Unmap(tmp.Get(), 0);

    if (aRecall)
    {
        ourCurrentRandomPosition.x = data[0];
        ourCurrentRandomPosition.y = data[1];
        ourCurrentRandomPosition.z = data[2];
    }
    else
    {
        ourCurrentPosition.x = data[0];
        ourCurrentPosition.y = data[1];
        ourCurrentPosition.z = data[2];
    }
}
ExtractVertexNormal - Reading the texture

{
    //...
    const Vector2f uv = { (pos.x + 0.5f) / ourViewportSize.x, (pos.y + 0.5f) / ourViewportSize.y };
    auto& resource{ Engine::GetGraphicsEngine().GetRenderer().GetResourceManager() };

    const uint32_t px = static_cast<uint32_t>(uv.x * resource.gBuffer.GetTexture(GBufferTexture::VertexNormal).GetViewport().width);
    const uint32_t py = static_cast<uint32_t>(uv.y * resource.gBuffer.GetTexture(GBufferTexture::VertexNormal).GetViewport().height);

    ID3D11Resource* src;
    const auto& context = DX11::Context;
    aSRV->GetResource(&src);

    D3D11_TEXTURE2D_DESC textureDesc = {};
    textureDesc.Width = 1;
    textureDesc.Height = 1;
    textureDesc.MipLevels = 1;
    textureDesc.ArraySize = 1;
    textureDesc.Format = (DXGI_FORMAT_R10G10B10A2_UNORM);
    textureDesc.SampleDesc.Count = 1;
    textureDesc.SampleDesc.Quality = 0;
    textureDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    textureDesc.Usage = D3D11_USAGE_STAGING;
    textureDesc.BindFlags = 0;
    textureDesc.MiscFlags = 0;

    ComPtr<ID3D11Texture2D> tmp;
    HRESULT hr = DX11::Device->CreateTexture2D(&textureDesc, nullptr, tmp.GetAddressOf());
    assert(SUCCEEDED(hr));

    D3D11_BOX srcBox;
    srcBox.left = px;
    srcBox.right = px + 1;
    srcBox.bottom = py + 1;
    srcBox.top = py;
    srcBox.front = 0;
    srcBox.back = 1;

    DX11::Context->CopySubresourceRegion(
        tmp.Get(),
        0, 0, 0, 0,
        src, 0,
        &srcBox
    );
    D3D11_MAPPED_SUBRESOURCE msr = {};
    hr = context->Map(tmp.Get(), 0, D3D11_MAP_READ, 0, &msr);
    assert(SUCCEEDED(hr));

    using namespace DirectX::PackedVector;

    XMUDECN4 packed = *static_cast<const XMUDECN4*>(msr.pData);

    DirectX::XMFLOAT4 normal;
    DirectX::XMStoreFloat4(&normal, XMLoadUDecN4(&packed));

    context->Unmap(tmp.Get(), 0);

    ourCurrentVertexNormal.x = normal.x * 2 - 1;
    ourCurrentVertexNormal.y = normal.y * 2 - 1;
    ourCurrentVertexNormal.z = normal.z * 2 - 1;
}

ExtractWorldPosition also takes in a ‘Recall’ variable. This bool is used to ensure that the painter always has an updated random pixel position saved. Once the function is called, it recursively calls itself at the very end.


// ...Previous reading logic

if (!ourSingleTargetMode && !ourEraserMode && !aRecall)
{
    auto& resourceManager { Engine::GetGraphicsEngine().GetRenderer().GetResourceManager() };
    ExtractWorldPosition(resourceManager.gBuffer.GetTexture(GBufferTexture::WorldPosition).GetSRV(), true);
}

At the very start of the function, right after calculating the mouse position:

  1. Get a random point from the current brush matrix.
  2. Translate it to clip space and then into view space.
  3. Use that position to read the buffer and get a world position.

if (aRecall)
{
    Vector3f randomPos = GetRandomPoint(ourBrushMatrix);
        
    Matrix4x4f viewMatrix { Matrix4x4f::GetFastInverse(ourCamera->GetTransform()) };
    Matrix4x4f projMatrix { ourCamera->GetProjection() };
        
    Vector4f clipSpace = viewMatrix * projMatrix * Vector4f(randomPos, 1.f);
    Vector3f ndc = Vector3f(clipSpace.x / clipSpace.w, clipSpace.y / clipSpace.w, clipSpace.z / clipSpace.w);
    Vector2f viewSpace;
    viewSpace.x = (ndc.x + 1.f) * 0.5f * ourViewportSize.x;
    viewSpace.y = (1.f - ndc.y) * 0.5f * ourViewportSize.y;

    viewSpace.x = std::clamp(viewSpace.x, 0.f, ourViewportSize.x - 1.f);
    viewSpace.y = std::clamp(viewSpace.y, 0.f, ourViewportSize.y - 1.f);

    Vector2ui revertedMousePos{ static_cast<unsigned int>(viewSpace.x), static_cast<unsigned int>(viewSpace.y) };
    if (revertedMousePos.x < 0 || revertedMousePos.x >= ourViewportSize.x  || revertedMousePos.y < 0 || revertedMousePos.y >= ourViewportSize.y)
    {
        ourCurrentPosition = { 0.f };
        return;
    }
    ourCurrentRevertedViewPos = revertedMousePos;
    pos = revertedMousePos;
}
// ...Previous reading logic

Painting
#

TryGetPaintMatrix calculates on mouse button down if the current position is valid, returning a identity matrix otherwise. This matrix is then used to instantiate an object, increment order index, applying potential random variables if given and appending “(Painted)” to it. This suffix is used when erasing, iterating through all objects within the brush radius and ensuring only painted objects get removed.

TryGetPaintMatrix body
Matrix4x4f ObjectPainting::TryGetPaintMatrix(const ImVec2& aMousePos, RenderData* aRenderData)
{
    Vector2ui mappedMouse{ static_cast<uint32_t>(aMousePos.x), static_cast<uint32_t>(aMousePos.y) };
    
    if (!ImGui::IsMousePosValid(&aMousePos)) { return Matrix4x4f::CreateIdentityMatrix(); }
    
    if (ImGui::IsMouseReleased(ImGuiMouseButton_Left))
    {
        ourLastPosition = { 0.f };
        return Matrix4x4f::CreateIdentityMatrix();
    }
    
    if (!ourSingleTargetMode && !ourEraserMode)
    {
        const float radiusWorld = ourPaintingCircumference * 0.5f;
        ourBrushMatrix = DrawBrush(aRenderData, ourCurrentPosition, radiusWorld);
    }

    Vector2f xzDiff { ourLastPosition.x - ourCurrentPosition.x, ourLastPosition.z - ourCurrentPosition.z };
    if (ImGui::IsMouseDown(ImGuiMouseButton_Left) && abs(xzDiff.Length()) > ourPaintingDelay)
    {
        if (!ourEraserMode)
        {
            if (mappedMouse.x < 0 || mappedMouse.x >= ourViewportSize.x  || mappedMouse.y < 0 || mappedMouse.y >= ourViewportSize.y)
            {
                return Matrix4x4f::CreateIdentityMatrix();
            }
            
            Matrix4x4f rotMatrix;
            Matrix4x4f outputMatrix;
            if (ourSingleTargetMode)
            {
                rotMatrix = Matrix4x4f::CreateFromRotation(FromToHelper(Vector3f(0, 1, 0), ourCurrentVertexNormal));
                outputMatrix = rotMatrix * Matrix4x4f::CreateFromTranslation(ourCurrentPosition);
            }
            else
            {
                rotMatrix = Matrix4x4f::CreateFromRotation(FromToHelper(Vector3f(0, 1, 0), ourCurrentVertexNormal));
                outputMatrix = rotMatrix * Matrix4x4f::CreateFromTranslation(ourCurrentRandomPosition);
            }

            ourLastPosition = ourCurrentPosition;
            return outputMatrix;
        }
    }
    
    return Matrix4x4f::CreateIdentityMatrix();
}

Applying variables
#

Applying variables
const Matrix4x4f worldMatrix = ObjectPainting::TryGetPaintMatrix(mousePos, &myRenderData);

// ...Check if valid, increment order, append (Painted)...

auto& [objTranslation, objRotation, objScale] { object->GetTRS() };
						
if (ObjectPainting::InRandomScale())
{
    const float xScale { Engine::GetRandom().Float(ObjectPainting::GetMinScaleValue().x, ObjectPainting::GetMaxScaleValue().x) };
    const float yScale { Engine::GetRandom().Float(ObjectPainting::GetMinScaleValue().y, ObjectPainting::GetMaxScaleValue().y) };
    const float zScale { Engine::GetRandom().Float(ObjectPainting::GetMinScaleValue().z, ObjectPainting::GetMaxScaleValue().z) };
    objScale = { xScale, yScale, zScale };
}
Vector3f randomRotation { 0.f, 0.f, 0.f };
if (ObjectPainting::InRandomRotation())
{
    const float xRot { Engine::GetRandom().Float(ObjectPainting::GetMinRotValue().x, ObjectPainting::GetMaxRotValue().x) };
    const float yRot { Engine::GetRandom().Float(ObjectPainting::GetMinRotValue().y, ObjectPainting::GetMaxRotValue().y) };
    const float zRot { Engine::GetRandom().Float(ObjectPainting::GetMinRotValue().z, ObjectPainting::GetMaxRotValue().z) };
    randomRotation = { xRot, yRot, zRot };
}
const Quaternionf worldQuat { worldMatrix.GetRotationAsQuaternion() };
const Quaternionf randomQuat { Quaternionf(randomRotation) };
const Quaternionf finalizedQuat { randomQuat * worldQuat };
objRotation = finalizedQuat.GetYawPitchRoll();
objTranslation = worldMatrix.GetPosition();

std::shared_ptr<AddSceneObjectsCommand> command = std::make_shared<AddSceneObjectsCommand>();
command->AddObjects(std::span<std::shared_ptr<SceneObject>>(&object, 1));

Related