diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae65cbcc36..28620803e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -234,6 +234,7 @@
     Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking
     Feature #3537: Shader-based water ripples
     Feature #5173: Support for NiFogProperty
+    Feature #5197: Editor: terrain vertex paint editmode
     Feature #5492: Let rain and snow collide with statics
     Feature #5926: Refraction based on water depth
     Feature #5944: Option to use camera as sound listener
diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt
index a131c56358..6cd7722280 100644
--- a/apps/opencs/CMakeLists.txt
+++ b/apps/opencs/CMakeLists.txt
@@ -80,7 +80,7 @@ opencs_units (view/world
     )
 
 opencs_units (view/widget
-    scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton
+    scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton scenetoolvertexpaintbrush
     scenetooltoggle2 scenetooltexturebrush scenetoolshapebrush completerpopup coloreditor colorpickerpopup droplineedit
     )
 
@@ -88,7 +88,7 @@ opencs_units (view/render
     scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget
     previewwidget editmode instancemode instanceselectionmode instancemovemode
     orbitcameramode pathgridmode selectionmode pathgridselectionmode cameracontroller
-    cellwater terraintexturemode actor terrainselection terrainshapemode brushdraw commands
+    cellwater terraintexturemode terrainvertexpaintmode actor terrainselection terrainshapemode brushdraw commands
     )
 
 opencs_units (view/render
diff --git a/apps/opencs/view/render/pagedworldspacewidget.cpp b/apps/opencs/view/render/pagedworldspacewidget.cpp
index 3fd35b7740..360ee98761 100644
--- a/apps/opencs/view/render/pagedworldspacewidget.cpp
+++ b/apps/opencs/view/render/pagedworldspacewidget.cpp
@@ -42,6 +42,7 @@
 #include "mask.hpp"
 #include "terrainshapemode.hpp"
 #include "terraintexturemode.hpp"
+#include "terrainvertexpaintmode.hpp"
 
 class QWidget;
 
@@ -170,9 +171,8 @@ void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons(CSVWidget::Sce
     /// \todo replace EditMode with suitable subclasses
     tool->addButton(new TerrainShapeMode(this, mRootNode, tool), "terrain-shape");
     tool->addButton(new TerrainTextureMode(this, mRootNode, tool), "terrain-texture");
-    const QIcon vertexIcon = Misc::ScalableIcon::load(":scenetoolbar/editing-terrain-vertex-paint");
+    tool->addButton(new TerrainVertexPaintMode(this, mRootNode, tool), "terrain-vertex");
     const QIcon movementIcon = Misc::ScalableIcon::load(":scenetoolbar/editing-terrain-movement");
-    tool->addButton(new EditMode(this, vertexIcon, Mask_Reference, "Terrain vertex paint editing"), "terrain-vertex");
     tool->addButton(new EditMode(this, movementIcon, Mask_Reference, "Terrain movement"), "terrain-move");
 }
 
diff --git a/apps/opencs/view/render/terrainvertexpaintmode.cpp b/apps/opencs/view/render/terrainvertexpaintmode.cpp
new file mode 100644
index 0000000000..93cdf1d37b
--- /dev/null
+++ b/apps/opencs/view/render/terrainvertexpaintmode.cpp
@@ -0,0 +1,867 @@
+#include "terrainvertexpaintmode.hpp"
+
+#include <algorithm>
+#include <cmath>
+#include <memory>
+#include <string>
+
+#include <QComboBox>
+#include <QDropEvent>
+#include <QEvent>
+#include <QIcon>
+#include <QWidget>
+
+#include <osg/Camera>
+#include <osg/Vec3f>
+
+#include <apps/opencs/model/doc/document.hpp>
+#include <apps/opencs/model/prefs/category.hpp>
+#include <apps/opencs/model/prefs/setting.hpp>
+#include <apps/opencs/model/world/cellselection.hpp>
+#include <apps/opencs/model/world/columnimp.hpp>
+#include <apps/opencs/model/world/columns.hpp>
+#include <apps/opencs/model/world/data.hpp>
+#include <apps/opencs/model/world/idcollection.hpp>
+#include <apps/opencs/model/world/idtable.hpp>
+#include <apps/opencs/model/world/land.hpp>
+#include <apps/opencs/model/world/record.hpp>
+#include <apps/opencs/model/world/universalid.hpp>
+#include <apps/opencs/view/widget/brushshapes.hpp>
+#include <apps/opencs/view/widget/scenetool.hpp>
+
+#include <components/debug/debuglog.hpp>
+#include <components/esm3/loadland.hpp>
+
+#include "../widget/scenetoolbar.hpp"
+#include "../widget/scenetoolvertexpaintbrush.hpp"
+
+#include "../../model/prefs/state.hpp"
+#include "../../model/world/commands.hpp"
+#include "../../model/world/idtree.hpp"
+
+#include "brushdraw.hpp"
+#include "commands.hpp"
+#include "editmode.hpp"
+#include "mask.hpp"
+#include "pagedworldspacewidget.hpp"
+#include "terrainselection.hpp"
+#include "worldspacewidget.hpp"
+
+class QPoint;
+class QWidget;
+
+namespace CSMWorld
+{
+    struct Cell;
+}
+
+namespace osg
+{
+    class Group;
+}
+
+CSVRender::TerrainVertexPaintMode::TerrainVertexPaintMode(
+    WorldspaceWidget* worldspaceWidget, osg::Group* parentNode, QWidget* parent)
+    : EditMode(worldspaceWidget, QIcon{ ":scenetoolbar/editing-terrain-vertex-paint" }, Mask_Terrain,
+        "Terrain vertex paint editing", parent)
+    , mParentNode(parentNode)
+    , mVertexPaintEditToolColor(Qt::white)
+{
+}
+
+void CSVRender::TerrainVertexPaintMode::activate(CSVWidget::SceneToolbar* toolbar)
+{
+    if (!mTerrainSelection)
+    {
+        mTerrainSelection
+            = std::make_shared<TerrainSelection>(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Shape);
+    }
+
+    if (!mVertexPaintBrushScenetool)
+    {
+        mVertexPaintBrushScenetool = new CSVWidget::SceneToolVertexPaintBrush(
+            toolbar, "scenetoolvertexpaintbrush", getWorldspaceWidget().getDocument());
+        connect(mVertexPaintBrushScenetool->mVertexPaintBrushWindow, &CSVWidget::VertexPaintBrushWindow::passBrushSize,
+            this, &TerrainVertexPaintMode::setBrushSize);
+        connect(mVertexPaintBrushScenetool->mVertexPaintBrushWindow, &CSVWidget::VertexPaintBrushWindow::passBrushShape,
+            this, &TerrainVertexPaintMode::setBrushShape);
+        connect(mVertexPaintBrushScenetool->mVertexPaintBrushWindow->mSizeSliders->mBrushSizeSlider,
+            &QSlider::valueChanged, this, &TerrainVertexPaintMode::setBrushSize);
+        connect(mVertexPaintBrushScenetool->mVertexPaintBrushWindow->mToolSelector,
+            qOverload<int>(&QComboBox::currentIndexChanged), this, &TerrainVertexPaintMode::setVertexPaintEditTool);
+        connect(mVertexPaintBrushScenetool->mVertexPaintBrushWindow->mColorButtonWidget,
+            &CSVWidget::ColorButtonWidget::colorChanged, this, &TerrainVertexPaintMode::setVertexPaintColor);
+    }
+
+    if (!mBrushDraw)
+        mBrushDraw = std::make_unique<BrushDraw>(mParentNode);
+
+    EditMode::activate(toolbar);
+    toolbar->addTool(mVertexPaintBrushScenetool);
+}
+
+void CSVRender::TerrainVertexPaintMode::deactivate(CSVWidget::SceneToolbar* toolbar)
+{
+    if (mVertexPaintBrushScenetool)
+    {
+        toolbar->removeTool(mVertexPaintBrushScenetool);
+    }
+
+    if (mTerrainSelection)
+    {
+        mTerrainSelection.reset();
+    }
+
+    if (mBrushDraw)
+        mBrushDraw.reset();
+
+    EditMode::deactivate(toolbar);
+}
+
+void CSVRender::TerrainVertexPaintMode::primaryOpenPressed(const WorldspaceHitResult& hit) // Apply changes here
+{
+}
+
+void CSVRender::TerrainVertexPaintMode::primaryEditPressed(const WorldspaceHitResult& hit)
+{
+    if (hit.hit && hit.tag == nullptr)
+    {
+        if (mDragMode == InteractionType_PrimaryEdit)
+        {
+            editVertexColourGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true);
+        }
+    }
+    endVertexPaintEditing();
+}
+
+void CSVRender::TerrainVertexPaintMode::primarySelectPressed(const WorldspaceHitResult& hit)
+{
+    if (hit.hit && hit.tag == nullptr)
+    {
+        selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0);
+        mTerrainSelection->clearTemporarySelection();
+    }
+}
+
+void CSVRender::TerrainVertexPaintMode::secondarySelectPressed(const WorldspaceHitResult& hit)
+{
+    if (hit.hit && hit.tag == nullptr)
+    {
+        selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1);
+        mTerrainSelection->clearTemporarySelection();
+    }
+}
+
+bool CSVRender::TerrainVertexPaintMode::primaryEditStartDrag(const QPoint& pos)
+{
+    WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask());
+
+    mDragMode = InteractionType_PrimaryEdit;
+
+    if (hit.hit && hit.tag == nullptr)
+    {
+        mEditingPos = hit.worldPos;
+        mIsEditing = true;
+        CSMDoc::Document& document = getWorldspaceWidget().getDocument();
+        QUndoStack& undoStack = document.getUndoStack();
+        undoStack.beginMacro("Set land vertex colours");
+    }
+
+    return true;
+}
+
+bool CSVRender::TerrainVertexPaintMode::secondaryEditStartDrag(const QPoint& pos)
+{
+    return false;
+}
+
+bool CSVRender::TerrainVertexPaintMode::primarySelectStartDrag(const QPoint& pos)
+{
+    WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask());
+    mDragMode = InteractionType_PrimarySelect;
+    if (!hit.hit || hit.tag != nullptr)
+    {
+        mDragMode = InteractionType_None;
+        return false;
+    }
+    selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0);
+    return true;
+}
+
+bool CSVRender::TerrainVertexPaintMode::secondarySelectStartDrag(const QPoint& pos)
+{
+    WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask());
+    mDragMode = InteractionType_SecondarySelect;
+    if (!hit.hit || hit.tag != nullptr)
+    {
+        mDragMode = InteractionType_None;
+        return false;
+    }
+    selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1);
+    return true;
+}
+
+void CSVRender::TerrainVertexPaintMode::drag(const QPoint& pos, int diffX, int diffY, double speedFactor)
+{
+    if (mDragMode == InteractionType_PrimaryEdit)
+    {
+        WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask());
+        mTotalDiffY += diffY;
+        if (mIsEditing)
+            editVertexColourGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true);
+    }
+
+    if (mDragMode == InteractionType_PrimarySelect)
+    {
+        WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask());
+        if (hit.hit && hit.tag == nullptr)
+            selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0);
+    }
+
+    if (mDragMode == InteractionType_SecondarySelect)
+    {
+        WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask());
+        if (hit.hit && hit.tag == nullptr)
+            selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1);
+    }
+}
+
+void CSVRender::TerrainVertexPaintMode::dragCompleted(const QPoint& pos)
+{
+    if (mDragMode == InteractionType_PrimaryEdit)
+    {
+        if (mIsEditing)
+        {
+            CSMDoc::Document& document = getWorldspaceWidget().getDocument();
+            QUndoStack& undoStack = document.getUndoStack();
+            undoStack.endMacro();
+        }
+        endVertexPaintEditing();
+    }
+    if (mDragMode == InteractionType_PrimarySelect || mDragMode == InteractionType_SecondarySelect)
+    {
+        mTerrainSelection->clearTemporarySelection();
+    }
+}
+
+void CSVRender::TerrainVertexPaintMode::dragAborted()
+{
+    if (mIsEditing)
+    {
+        CSMDoc::Document& document = getWorldspaceWidget().getDocument();
+        QUndoStack& undoStack = document.getUndoStack();
+        undoStack.endMacro();
+    }
+    endVertexPaintEditing();
+    mDragMode = InteractionType_None;
+}
+
+void CSVRender::TerrainVertexPaintMode::dragWheel(int diff, double speedFactor) {}
+
+void CSVRender::TerrainVertexPaintMode::endVertexPaintEditing()
+{
+    mIsEditing = false;
+    mTerrainSelection->update();
+}
+
+void CSVRender::TerrainVertexPaintMode::editVertexColourGrid(
+    const std::pair<int, int>& vertexCoords, bool dragOperation)
+{
+    CSMDoc::Document& document = getWorldspaceWidget().getDocument();
+    CSMWorld::IdTable& landTable
+        = dynamic_cast<CSMWorld::IdTable&>(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land));
+
+    std::string mCellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(vertexCoords);
+    if (allowLandColourEditing(mCellId))
+    {
+    }
+
+    std::pair<CSMWorld::CellCoordinates, bool> cellCoordinates_pair = CSMWorld::CellCoordinates::fromId(mCellId);
+
+    int cellX = cellCoordinates_pair.first.getX();
+    int cellY = cellCoordinates_pair.first.getY();
+
+    int xHitInCell = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.first);
+    int yHitInCell = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.second);
+    if (xHitInCell < 0)
+    {
+        xHitInCell = xHitInCell + ESM::Land::LAND_SIZE;
+        cellX = cellX - 1;
+    }
+    if (yHitInCell > 64)
+    {
+        yHitInCell = yHitInCell - ESM::Land::LAND_SIZE;
+        cellY = cellY + 1;
+    }
+
+    mCellId = CSMWorld::CellCoordinates::generateId(cellX, cellY);
+    if (allowLandColourEditing(mCellId))
+    {
+    }
+
+    std::string iteratedCellId;
+
+    int colourColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandColoursIndex);
+
+    int r = std::max(static_cast<float>(mBrushSize) / 2.0f, 1.0f);
+
+    if (mBrushShape == CSVWidget::BrushShape_Point)
+    {
+        CSMWorld::LandColoursColumn::DataType newTerrain
+            = landTable.data(landTable.getModelIndex(mCellId, colourColumn))
+                  .value<CSMWorld::LandColoursColumn::DataType>();
+
+        if (allowLandColourEditing(mCellId))
+        {
+            alterColour(newTerrain, xHitInCell, yHitInCell, 0.0f);
+            pushEditToCommand(newTerrain, document, landTable, mCellId);
+        }
+    }
+
+    if (mBrushShape == CSVWidget::BrushShape_Square)
+    {
+        int upperLeftCellX = cellX - std::floor(r / ESM::Land::LAND_SIZE);
+        int upperLeftCellY = cellY - std::floor(r / ESM::Land::LAND_SIZE);
+        if (xHitInCell - (r % ESM::Land::LAND_SIZE) < 0)
+            upperLeftCellX--;
+        if (yHitInCell - (r % ESM::Land::LAND_SIZE) < 0)
+            upperLeftCellY--;
+
+        int lowerrightCellX = cellX + std::floor(r / ESM::Land::LAND_SIZE);
+        int lowerrightCellY = cellY + std::floor(r / ESM::Land::LAND_SIZE);
+        if (xHitInCell + (r % ESM::Land::LAND_SIZE) > ESM::Land::LAND_SIZE - 1)
+            lowerrightCellX++;
+        if (yHitInCell + (r % ESM::Land::LAND_SIZE) > ESM::Land::LAND_SIZE - 1)
+            lowerrightCellY++;
+
+        for (int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++)
+        {
+            for (int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++)
+            {
+                iteratedCellId = CSMWorld::CellCoordinates::generateId(i_cell, j_cell);
+                if (allowLandColourEditing(iteratedCellId))
+                {
+                    CSMWorld::LandColoursColumn::DataType newTerrain
+                        = landTable.data(landTable.getModelIndex(iteratedCellId, colourColumn))
+                              .value<CSMWorld::LandColoursColumn::DataType>();
+                    for (int i = 0; i < ESM::Land::LAND_SIZE; i++)
+                    {
+                        for (int j = 0; j < ESM::Land::LAND_SIZE; j++)
+                        {
+
+                            if (i_cell == cellX && j_cell == cellY && abs(i - xHitInCell) < r
+                                && abs(j - yHitInCell) < r)
+                            {
+                                alterColour(newTerrain, i, j, 0.0f);
+                            }
+                            else
+                            {
+                                int distanceX(0);
+                                int distanceY(0);
+                                if (i_cell < cellX)
+                                    distanceX = xHitInCell + ESM::Land::LAND_SIZE * abs(i_cell - cellX) - i;
+                                if (j_cell < cellY)
+                                    distanceY = yHitInCell + ESM::Land::LAND_SIZE * abs(j_cell - cellY) - j;
+                                if (i_cell > cellX)
+                                    distanceX = -xHitInCell + ESM::Land::LAND_SIZE * abs(i_cell - cellX) + i;
+                                if (j_cell > cellY)
+                                    distanceY = -yHitInCell + ESM::Land::LAND_SIZE * abs(j_cell - cellY) + j;
+                                if (i_cell == cellX)
+                                    distanceX = abs(i - xHitInCell);
+                                if (j_cell == cellY)
+                                    distanceY = abs(j - yHitInCell);
+                                if (distanceX < r && distanceY < r)
+                                    alterColour(newTerrain, i, j, 0.0f);
+                            }
+                        }
+                    }
+                    pushEditToCommand(newTerrain, document, landTable, iteratedCellId);
+                }
+            }
+        }
+    }
+
+    if (mBrushShape == CSVWidget::BrushShape_Circle)
+    {
+        int upperLeftCellX = cellX - std::floor(r / ESM::Land::LAND_SIZE);
+        int upperLeftCellY = cellY - std::floor(r / ESM::Land::LAND_SIZE);
+        if (xHitInCell - (r % ESM::Land::LAND_SIZE) < 0)
+            upperLeftCellX--;
+        if (yHitInCell - (r % ESM::Land::LAND_SIZE) < 0)
+            upperLeftCellY--;
+
+        int lowerrightCellX = cellX + std::floor(r / ESM::Land::LAND_SIZE);
+        int lowerrightCellY = cellY + std::floor(r / ESM::Land::LAND_SIZE);
+        if (xHitInCell + (r % ESM::Land::LAND_SIZE) > ESM::Land::LAND_SIZE - 1)
+            lowerrightCellX++;
+        if (yHitInCell + (r % ESM::Land::LAND_SIZE) > ESM::Land::LAND_SIZE - 1)
+            lowerrightCellY++;
+
+        for (int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++)
+        {
+            for (int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++)
+            {
+                iteratedCellId = CSMWorld::CellCoordinates::generateId(i_cell, j_cell);
+                if (allowLandColourEditing(iteratedCellId))
+                {
+                    CSMWorld::LandColoursColumn::DataType newTerrain
+                        = landTable.data(landTable.getModelIndex(iteratedCellId, colourColumn))
+                              .value<CSMWorld::LandColoursColumn::DataType>();
+                    for (int i = 0; i < ESM::Land::LAND_SIZE; i++)
+                    {
+                        for (int j = 0; j < ESM::Land::LAND_SIZE; j++)
+                        {
+                            if (i_cell == cellX && j_cell == cellY && abs(i - xHitInCell) < r
+                                && abs(j - yHitInCell) < r)
+                            {
+                                int distanceX = abs(i - xHitInCell);
+                                int distanceY = abs(j - yHitInCell);
+                                float distance = std::round(sqrt(pow(distanceX, 2) + pow(distanceY, 2)));
+                                if (distance < r)
+                                    alterColour(newTerrain, i, j, 0.0f);
+                            }
+                            else
+                            {
+                                int distanceX(0);
+                                int distanceY(0);
+                                if (i_cell < cellX)
+                                    distanceX = xHitInCell + ESM::Land::LAND_SIZE * abs(i_cell - cellX) - i;
+                                if (j_cell < cellY)
+                                    distanceY = yHitInCell + ESM::Land::LAND_SIZE * abs(j_cell - cellY) - j;
+                                if (i_cell > cellX)
+                                    distanceX = -xHitInCell + ESM::Land::LAND_SIZE * abs(i_cell - cellX) + i;
+                                if (j_cell > cellY)
+                                    distanceY = -yHitInCell + ESM::Land::LAND_SIZE * abs(j_cell - cellY) + j;
+                                if (i_cell == cellX)
+                                    distanceX = abs(i - xHitInCell);
+                                if (j_cell == cellY)
+                                    distanceY = abs(j - yHitInCell);
+                                float distance = std::round(sqrt(pow(distanceX, 2) + pow(distanceY, 2)));
+                                if (distance < r)
+                                    alterColour(newTerrain, i, j, 0.0f);
+                            }
+                        }
+                    }
+                    pushEditToCommand(newTerrain, document, landTable, iteratedCellId);
+                }
+            }
+        }
+    }
+}
+
+void CSVRender::TerrainVertexPaintMode::alterColour(
+    CSMWorld::LandColoursColumn::DataType& landColorsNew, int inCellX, int inCellY, float alteredHeight, bool useTool)
+{
+    const int red = mVertexPaintEditToolColor.red();
+    const int green = mVertexPaintEditToolColor.green();
+    const int blue = mVertexPaintEditToolColor.blue();
+
+    // TODO: handle different smoothing/blend types with different tools, right now this expects Replace
+    landColorsNew[(inCellY * ESM::Land::LAND_SIZE + inCellX) * 3 + 0] = red;
+    landColorsNew[(inCellY * ESM::Land::LAND_SIZE + inCellX) * 3 + 1] = green;
+    landColorsNew[(inCellY * ESM::Land::LAND_SIZE + inCellX) * 3 + 2] = blue;
+}
+
+void CSVRender::TerrainVertexPaintMode::pushEditToCommand(const CSMWorld::LandColoursColumn::DataType& newLandColours,
+    CSMDoc::Document& document, CSMWorld::IdTable& landTable, const std::string& cellId)
+{
+    QVariant changedLand;
+    changedLand.setValue(newLandColours);
+
+    QModelIndex index(
+        landTable.getModelIndex(cellId, landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandColoursIndex)));
+
+    QUndoStack& undoStack = document.getUndoStack();
+    undoStack.push(new CSMWorld::ModifyCommand(landTable, index, changedLand));
+}
+
+bool CSVRender::TerrainVertexPaintMode::isInCellSelection(int globalSelectionX, int globalSelectionY)
+{
+    if (CSVRender::PagedWorldspaceWidget* paged
+        = dynamic_cast<CSVRender::PagedWorldspaceWidget*>(&getWorldspaceWidget()))
+    {
+        std::pair<int, int> vertexCoords = std::make_pair(globalSelectionX, globalSelectionY);
+        std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(vertexCoords);
+        return paged->getCellSelection().has(CSMWorld::CellCoordinates::fromId(cellId).first) && isLandLoaded(cellId);
+    }
+    return false;
+}
+
+void CSVRender::TerrainVertexPaintMode::handleSelection(
+    int globalSelectionX, int globalSelectionY, std::vector<std::pair<int, int>>* selections)
+{
+    if (isInCellSelection(globalSelectionX, globalSelectionY))
+        selections->emplace_back(globalSelectionX, globalSelectionY);
+    else
+    {
+        int moduloX = globalSelectionX % (ESM::Land::LAND_SIZE - 1);
+        int moduloY = globalSelectionY % (ESM::Land::LAND_SIZE - 1);
+        bool xIsAtCellBorder = moduloX == 0;
+        bool yIsAtCellBorder = moduloY == 0;
+        if (!xIsAtCellBorder && !yIsAtCellBorder)
+            return;
+        int selectionX = globalSelectionX;
+        int selectionY = globalSelectionY;
+
+        /*
+            The northern and eastern edges don't belong to the current cell.
+            If the corresponding adjacent cell is not loaded, some special handling is necessary to select border
+           vertices.
+        */
+        if (xIsAtCellBorder && yIsAtCellBorder)
+        {
+            /*
+                Handle the NW, NE, and SE corner vertices.
+                NW corner: (+1, -1) offset to reach current cell.
+                NE corner: (-1, -1) offset to reach current cell.
+                SE corner: (-1, +1) offset to reach current cell.
+            */
+            if (isInCellSelection(globalSelectionX - 1, globalSelectionY - 1)
+                || isInCellSelection(globalSelectionX + 1, globalSelectionY - 1)
+                || isInCellSelection(globalSelectionX - 1, globalSelectionY + 1))
+            {
+                selections->emplace_back(globalSelectionX, globalSelectionY);
+            }
+        }
+        else if (xIsAtCellBorder)
+        {
+            selectionX--;
+        }
+        else if (yIsAtCellBorder)
+        {
+            selectionY--;
+        }
+
+        if (isInCellSelection(selectionX, selectionY))
+            selections->emplace_back(globalSelectionX, globalSelectionY);
+    }
+}
+
+void CSVRender::TerrainVertexPaintMode::selectTerrainShapes(
+    const std::pair<int, int>& vertexCoords, unsigned char selectMode)
+{
+    int r = mBrushSize / 2;
+    std::vector<std::pair<int, int>> selections;
+
+    if (mBrushShape == CSVWidget::BrushShape_Point)
+    {
+        handleSelection(vertexCoords.first, vertexCoords.second, &selections);
+    }
+
+    if (mBrushShape == CSVWidget::BrushShape_Square)
+    {
+        for (int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i)
+        {
+            for (int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j)
+            {
+                handleSelection(i, j, &selections);
+            }
+        }
+    }
+
+    if (mBrushShape == CSVWidget::BrushShape_Circle)
+    {
+        for (int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i)
+        {
+            for (int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j)
+            {
+                int distanceX = abs(i - vertexCoords.first);
+                int distanceY = abs(j - vertexCoords.second);
+                float distance = sqrt(pow(distanceX, 2) + pow(distanceY, 2));
+
+                // Using floating-point radius here to prevent selecting too few vertices.
+                if (distance <= mBrushSize / 2.0f)
+                    handleSelection(i, j, &selections);
+            }
+        }
+    }
+
+    std::string selectAction;
+
+    if (selectMode == 0)
+        selectAction = CSMPrefs::get()["3D Scene Editing"]["primary-select-action"].toString();
+    else
+        selectAction = CSMPrefs::get()["3D Scene Editing"]["secondary-select-action"].toString();
+
+    if (selectAction == "Select only")
+        mTerrainSelection->onlySelect(selections);
+    else if (selectAction == "Add to selection")
+        mTerrainSelection->addSelect(selections);
+    else if (selectAction == "Remove from selection")
+        mTerrainSelection->removeSelect(selections);
+    else if (selectAction == "Invert selection")
+        mTerrainSelection->toggleSelect(selections);
+}
+
+bool CSVRender::TerrainVertexPaintMode::noCell(const std::string& cellId)
+{
+    CSMDoc::Document& document = getWorldspaceWidget().getDocument();
+    const CSMWorld::IdCollection<CSMWorld::Cell>& cellCollection = document.getData().getCells();
+    return cellCollection.searchId(ESM::RefId::stringRefId(cellId)) == -1;
+}
+
+bool CSVRender::TerrainVertexPaintMode::noLand(const std::string& cellId)
+{
+    CSMDoc::Document& document = getWorldspaceWidget().getDocument();
+    const CSMWorld::IdCollection<CSMWorld::Land>& landCollection = document.getData().getLand();
+    return landCollection.searchId(ESM::RefId::stringRefId(cellId)) == -1;
+}
+
+bool CSVRender::TerrainVertexPaintMode::noLandLoaded(const std::string& cellId)
+{
+    CSMDoc::Document& document = getWorldspaceWidget().getDocument();
+    const CSMWorld::IdCollection<CSMWorld::Land>& landCollection = document.getData().getLand();
+    return !landCollection.getRecord(ESM::RefId::stringRefId(cellId)).get().isDataLoaded(ESM::Land::DATA_VNML);
+}
+
+bool CSVRender::TerrainVertexPaintMode::isLandLoaded(const std::string& cellId)
+{
+    if (!noCell(cellId) && !noLand(cellId) && !noLandLoaded(cellId))
+        return true;
+    return false;
+}
+
+void CSVRender::TerrainVertexPaintMode::createNewLandData(const CSMWorld::CellCoordinates& cellCoords)
+{
+    CSMDoc::Document& document = getWorldspaceWidget().getDocument();
+    CSMWorld::IdTable& landTable
+        = dynamic_cast<CSMWorld::IdTable&>(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land));
+    CSMWorld::IdTable& ltexTable
+        = dynamic_cast<CSMWorld::IdTable&>(*document.getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures));
+    int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex);
+    int landnormalsColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex);
+
+    float defaultHeight = 0.f;
+    int averageDivider = 0;
+    CSMWorld::CellCoordinates cellLeftCoords = cellCoords.move(-1, 0);
+    CSMWorld::CellCoordinates cellRightCoords = cellCoords.move(1, 0);
+    CSMWorld::CellCoordinates cellUpCoords = cellCoords.move(0, -1);
+    CSMWorld::CellCoordinates cellDownCoords = cellCoords.move(0, 1);
+
+    std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY());
+    std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellLeftCoords.getX(), cellLeftCoords.getY());
+    std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellRightCoords.getX(), cellRightCoords.getY());
+    std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellUpCoords.getX(), cellUpCoords.getY());
+    std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellDownCoords.getX(), cellDownCoords.getY());
+
+    float leftCellSampleHeight = 0.0f;
+    float rightCellSampleHeight = 0.0f;
+    float upCellSampleHeight = 0.0f;
+    float downCellSampleHeight = 0.0f;
+
+    const CSMWorld::LandHeightsColumn::DataType landShapePointer
+        = landTable.data(landTable.getModelIndex(cellId, landshapeColumn))
+              .value<CSMWorld::LandHeightsColumn::DataType>();
+    const CSMWorld::LandNormalsColumn::DataType landNormalsPointer
+        = landTable.data(landTable.getModelIndex(cellId, landnormalsColumn))
+              .value<CSMWorld::LandNormalsColumn::DataType>();
+    CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer);
+    CSMWorld::LandNormalsColumn::DataType landNormalsNew(landNormalsPointer);
+
+    if (CSVRender::PagedWorldspaceWidget* paged
+        = dynamic_cast<CSVRender::PagedWorldspaceWidget*>(&getWorldspaceWidget()))
+    {
+        if (isLandLoaded(cellLeftId))
+        {
+            const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer
+                = landTable.data(landTable.getModelIndex(cellLeftId, landshapeColumn))
+                      .value<CSMWorld::LandHeightsColumn::DataType>();
+
+            ++averageDivider;
+            leftCellSampleHeight
+                = landLeftShapePointer[(ESM::Land::LAND_SIZE / 2) * ESM::Land::LAND_SIZE + ESM::Land::LAND_SIZE - 1];
+            if (paged->getCellAlteredHeight(cellLeftCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE / 2))
+                leftCellSampleHeight
+                    += *paged->getCellAlteredHeight(cellLeftCoords, ESM::Land::LAND_SIZE - 1, ESM::Land::LAND_SIZE / 2);
+        }
+        if (isLandLoaded(cellRightId))
+        {
+            const CSMWorld::LandHeightsColumn::DataType landRightShapePointer
+                = landTable.data(landTable.getModelIndex(cellRightId, landshapeColumn))
+                      .value<CSMWorld::LandHeightsColumn::DataType>();
+
+            ++averageDivider;
+            rightCellSampleHeight = landRightShapePointer[(ESM::Land::LAND_SIZE / 2) * ESM::Land::LAND_SIZE];
+            if (paged->getCellAlteredHeight(cellRightCoords, 0, ESM::Land::LAND_SIZE / 2))
+                rightCellSampleHeight += *paged->getCellAlteredHeight(cellRightCoords, 0, ESM::Land::LAND_SIZE / 2);
+        }
+        if (isLandLoaded(cellUpId))
+        {
+            const CSMWorld::LandHeightsColumn::DataType landUpShapePointer
+                = landTable.data(landTable.getModelIndex(cellUpId, landshapeColumn))
+                      .value<CSMWorld::LandHeightsColumn::DataType>();
+
+            ++averageDivider;
+            upCellSampleHeight
+                = landUpShapePointer[(ESM::Land::LAND_SIZE - 1) * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE / 2)];
+            if (paged->getCellAlteredHeight(cellUpCoords, ESM::Land::LAND_SIZE / 2, ESM::Land::LAND_SIZE - 1))
+                upCellSampleHeight
+                    += *paged->getCellAlteredHeight(cellUpCoords, ESM::Land::LAND_SIZE / 2, ESM::Land::LAND_SIZE - 1);
+        }
+        if (isLandLoaded(cellDownId))
+        {
+            const CSMWorld::LandHeightsColumn::DataType landDownShapePointer
+                = landTable.data(landTable.getModelIndex(cellDownId, landshapeColumn))
+                      .value<CSMWorld::LandHeightsColumn::DataType>();
+
+            ++averageDivider;
+            downCellSampleHeight = landDownShapePointer[ESM::Land::LAND_SIZE / 2];
+            if (paged->getCellAlteredHeight(cellDownCoords, ESM::Land::LAND_SIZE / 2, 0))
+                downCellSampleHeight += *paged->getCellAlteredHeight(cellDownCoords, ESM::Land::LAND_SIZE / 2, 0);
+        }
+    }
+    if (averageDivider > 0)
+        defaultHeight = (leftCellSampleHeight + rightCellSampleHeight + upCellSampleHeight + downCellSampleHeight)
+            / averageDivider;
+
+    for (int i = 0; i < ESM::Land::LAND_SIZE; ++i)
+    {
+        for (int j = 0; j < ESM::Land::LAND_SIZE; ++j)
+        {
+            landShapeNew[j * ESM::Land::LAND_SIZE + i] = defaultHeight;
+            landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 0] = 0;
+            landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 1] = 0;
+            landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 2] = 127;
+        }
+    }
+    QVariant changedShape;
+    changedShape.setValue(landShapeNew);
+    QVariant changedNormals;
+    changedNormals.setValue(landNormalsNew);
+    QModelIndex indexShape(
+        landTable.getModelIndex(cellId, landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex)));
+    QModelIndex indexNormal(
+        landTable.getModelIndex(cellId, landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex)));
+    document.getUndoStack().push(new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId));
+    document.getUndoStack().push(new CSMWorld::ModifyCommand(landTable, indexShape, changedShape));
+    document.getUndoStack().push(new CSMWorld::ModifyCommand(landTable, indexNormal, changedNormals));
+}
+
+bool CSVRender::TerrainVertexPaintMode::allowLandColourEditing(const std::string& cellId, bool useTool)
+{
+    CSMDoc::Document& document = getWorldspaceWidget().getDocument();
+    CSMWorld::IdTable& landTable
+        = dynamic_cast<CSMWorld::IdTable&>(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Land));
+    CSMWorld::IdTree& cellTable
+        = dynamic_cast<CSMWorld::IdTree&>(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Cells));
+
+    if (noCell(cellId))
+    {
+        std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString();
+
+        // target cell does not exist
+        if (mode == "Discard")
+            return false;
+
+        if (mode == "Create cell and land, then edit" && useTool)
+        {
+            auto createCommand = std::make_unique<CSMWorld::CreateCommand>(cellTable, cellId);
+            int parentIndex = cellTable.findColumnIndex(CSMWorld::Columns::ColumnId_Cell);
+            int index = cellTable.findNestedColumnIndex(parentIndex, CSMWorld::Columns::ColumnId_Interior);
+            createCommand->addNestedValue(parentIndex, index, false);
+            document.getUndoStack().push(createCommand.release());
+
+            if (CSVRender::PagedWorldspaceWidget* paged
+                = dynamic_cast<CSVRender::PagedWorldspaceWidget*>(&getWorldspaceWidget()))
+            {
+                CSMWorld::CellSelection selection = paged->getCellSelection();
+                selection.add(CSMWorld::CellCoordinates::fromId(cellId).first);
+                paged->setCellSelection(selection);
+            }
+        }
+    }
+    else if (CSVRender::PagedWorldspaceWidget* paged
+        = dynamic_cast<CSVRender::PagedWorldspaceWidget*>(&getWorldspaceWidget()))
+    {
+        CSMWorld::CellSelection selection = paged->getCellSelection();
+        if (!selection.has(CSMWorld::CellCoordinates::fromId(cellId).first))
+        {
+            // target cell exists, but is not shown
+            std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-visible-landedit"].toString();
+
+            if (mode == "Discard")
+                return false;
+
+            if (mode == "Show cell and edit" && useTool)
+            {
+                selection.add(CSMWorld::CellCoordinates::fromId(cellId).first);
+                paged->setCellSelection(selection);
+            }
+        }
+    }
+
+    if (noLand(cellId))
+    {
+        std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString();
+
+        // target cell does not exist
+        if (mode == "Discard")
+            return false;
+
+        if (mode == "Create cell and land, then edit" && useTool)
+        {
+            document.getUndoStack().push(new CSMWorld::CreateCommand(landTable, cellId));
+            createNewLandData(CSMWorld::CellCoordinates::fromId(cellId).first);
+        }
+    }
+    else if (noLandLoaded(cellId))
+    {
+        std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString();
+
+        if (mode == "Discard")
+            return false;
+
+        if (mode == "Create cell and land, then edit" && useTool)
+        {
+            createNewLandData(CSMWorld::CellCoordinates::fromId(cellId).first);
+        }
+    }
+
+    if (useTool && (noCell(cellId) || noLand(cellId) || noLandLoaded(cellId)))
+    {
+        Log(Debug::Warning) << "Land creation failed at cell id: " << cellId;
+        return false;
+    }
+    return true;
+}
+
+void CSVRender::TerrainVertexPaintMode::dragMoveEvent(QDragMoveEvent* event) {}
+
+void CSVRender::TerrainVertexPaintMode::mouseMoveEvent(QMouseEvent* event)
+{
+    WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask());
+    if (hit.hit && mBrushDraw)
+        mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape);
+    if (!hit.hit && mBrushDraw)
+        mBrushDraw->hide();
+}
+
+std::shared_ptr<CSVRender::TerrainSelection> CSVRender::TerrainVertexPaintMode::getTerrainSelection()
+{
+    return mTerrainSelection;
+}
+
+void CSVRender::TerrainVertexPaintMode::setBrushSize(int brushSize)
+{
+    mBrushSize = brushSize;
+}
+
+void CSVRender::TerrainVertexPaintMode::setBrushShape(CSVWidget::BrushShape brushShape)
+{
+    mBrushShape = brushShape;
+}
+
+void CSVRender::TerrainVertexPaintMode::setVertexPaintEditTool(int shapeEditTool)
+{
+    mVertexPaintEditTool = shapeEditTool;
+}
+
+void CSVRender::TerrainVertexPaintMode::setVertexPaintColor(const QColor& color)
+{
+    mVertexPaintEditToolColor = color;
+}
+
+CSVRender::PagedWorldspaceWidget& CSVRender::TerrainVertexPaintMode::getPagedWorldspaceWidget()
+{
+    return dynamic_cast<PagedWorldspaceWidget&>(getWorldspaceWidget());
+}
diff --git a/apps/opencs/view/render/terrainvertexpaintmode.hpp b/apps/opencs/view/render/terrainvertexpaintmode.hpp
new file mode 100644
index 0000000000..a713175fe4
--- /dev/null
+++ b/apps/opencs/view/render/terrainvertexpaintmode.hpp
@@ -0,0 +1,180 @@
+#ifndef CSV_RENDER_TERRAINVERTEXPAINTMODE_H
+#define CSV_RENDER_TERRAINVERTEXPAINTMODE_H
+
+#include "editmode.hpp"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <apps/opencs/model/world/cellcoordinates.hpp>
+
+#include <osg/Vec3d>
+
+#ifndef Q_MOC_RUN
+#include "../../model/world/columnimp.hpp"
+#include "../widget/brushshapes.hpp"
+#endif
+
+#include "brushdraw.hpp"
+
+class QDragMoveEvent;
+class QMouseEvent;
+class QObject;
+class QPoint;
+class QWidget;
+
+namespace osg
+{
+    class Group;
+}
+
+namespace CSMDoc
+{
+    class Document;
+}
+
+namespace CSVWidget
+{
+    class SceneToolVertexPaintBrush;
+}
+
+namespace CSMWorld
+{
+    class IdTable;
+}
+
+namespace CSVRender
+{
+    class PagedWorldspaceWidget;
+    class TerrainSelection;
+    class WorldspaceWidget;
+    struct WorldspaceHitResult;
+    class SceneToolbar;
+
+    /// \brief EditMode for handling the terrain shape editing
+    class TerrainVertexPaintMode : public EditMode
+    {
+        Q_OBJECT
+
+    public:
+        enum InteractionType
+        {
+            InteractionType_PrimaryEdit,
+            InteractionType_PrimarySelect,
+            InteractionType_SecondaryEdit,
+            InteractionType_SecondarySelect,
+            InteractionType_None
+        };
+
+        enum VertexPaintEditTool
+        {
+            VertexPaintEditTool_Replace = 0
+        };
+
+        /// Editmode for terrain vertex colour grid
+        TerrainVertexPaintMode(WorldspaceWidget*, osg::Group* parentNode, QWidget* parent = nullptr);
+
+        void primaryOpenPressed(const WorldspaceHitResult& hit) override;
+
+        /// Create single command for one-click vertex paint editing
+        void primaryEditPressed(const WorldspaceHitResult& hit) override;
+
+        /// Open brush settings window
+        void primarySelectPressed(const WorldspaceHitResult&) override;
+
+        void secondarySelectPressed(const WorldspaceHitResult&) override;
+
+        void activate(CSVWidget::SceneToolbar*) override;
+        void deactivate(CSVWidget::SceneToolbar*) override;
+
+        /// Start vertex paint editing command macro
+        bool primaryEditStartDrag(const QPoint& pos) override;
+
+        bool secondaryEditStartDrag(const QPoint& pos) override;
+        bool primarySelectStartDrag(const QPoint& pos) override;
+        bool secondarySelectStartDrag(const QPoint& pos) override;
+
+        /// Handle vertex paint edit behavior during dragging
+        void drag(const QPoint& pos, int diffX, int diffY, double speedFactor) override;
+
+        /// End vertex paint editing command macro
+        void dragCompleted(const QPoint& pos) override;
+
+        /// Cancel vertex paint editing, and reset all pending changes
+        void dragAborted() override;
+
+        void dragWheel(int diff, double speedFactor) override;
+        void dragMoveEvent(QDragMoveEvent* event) override;
+        void mouseMoveEvent(QMouseEvent* event) override;
+
+        std::shared_ptr<TerrainSelection> getTerrainSelection();
+
+    private:
+        /// Reset everything in the current edit
+        void endVertexPaintEditing();
+
+        /// Handle brush mechanics for colour editing
+        void editVertexColourGrid(const std::pair<int, int>& vertexCoords, bool dragOperation);
+
+        /// Alter one pixel's colour
+        void alterColour(CSMWorld::LandColoursColumn::DataType& landColorsNew, int inCellX, int inCellY,
+            float alteredHeight, bool useTool = true);
+
+        /// Check if global selection coordinate belongs to cell in view
+        bool isInCellSelection(int globalSelectionX, int globalSelectionY);
+
+        /// Select vertex at global selection coordinate
+        void handleSelection(int globalSelectionX, int globalSelectionY, std::vector<std::pair<int, int>>* selections);
+
+        /// Handle brush mechanics for terrain selection
+        void selectTerrainShapes(const std::pair<int, int>& vertexCoords, unsigned char selectMode);
+
+        bool noCell(const std::string& cellId);
+
+        bool noLand(const std::string& cellId);
+
+        bool noLandLoaded(const std::string& cellId);
+
+        bool isLandLoaded(const std::string& cellId);
+
+        /// Push terrain vertex coloir edits to command macro
+        void pushEditToCommand(const CSMWorld::LandColoursColumn::DataType& newLandColours, CSMDoc::Document& document,
+            CSMWorld::IdTable& landTable, const std::string& cellId);
+
+        /// Create new blank height record and new normals, if there are valid adjancent cell, take sample points and
+        /// set the average height based on that
+        void createNewLandData(const CSMWorld::CellCoordinates& cellCoords);
+
+        /// Create new cell and land if needed, only user tools may ask for opening new cells (useTool == false is for
+        /// automated land changes)
+        bool allowLandColourEditing(const std::string& textureFileName, bool useTool = true);
+
+        std::string mBrushTexture;
+        int mBrushSize = 1;
+        CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point;
+        std::unique_ptr<BrushDraw> mBrushDraw;
+        CSVWidget::SceneToolVertexPaintBrush* mVertexPaintBrushScenetool = nullptr;
+        int mDragMode = InteractionType_None;
+        osg::Group* mParentNode;
+        bool mIsEditing = false;
+        std::shared_ptr<TerrainSelection> mTerrainSelection;
+        int mTotalDiffY = 0;
+        std::vector<CSMWorld::CellCoordinates> mAlteredCells;
+        osg::Vec3d mEditingPos;
+        int mVertexPaintEditTool = VertexPaintEditTool_Replace;
+        QColor mVertexPaintEditToolColor;
+        int mTargetHeight = 0;
+
+        PagedWorldspaceWidget& getPagedWorldspaceWidget();
+
+    public slots:
+        void setBrushSize(int brushSize);
+        void setBrushShape(CSVWidget::BrushShape brushShape);
+        void setVertexPaintEditTool(int shapeEditTool);
+        void setVertexPaintColor(const QColor& color);
+    };
+}
+
+#endif
diff --git a/apps/opencs/view/widget/scenetoolvertexpaintbrush.cpp b/apps/opencs/view/widget/scenetoolvertexpaintbrush.cpp
new file mode 100644
index 0000000000..d5da14e991
--- /dev/null
+++ b/apps/opencs/view/widget/scenetoolvertexpaintbrush.cpp
@@ -0,0 +1,240 @@
+#include "scenetoolvertexpaintbrush.hpp"
+
+#include <QButtonGroup>
+#include <QComboBox>
+#include <QDragEnterEvent>
+#include <QDropEvent>
+#include <QFrame>
+#include <QGroupBox>
+#include <QHBoxLayout>
+#include <QHeaderView>
+#include <QIcon>
+#include <QLabel>
+#include <QSizePolicy>
+#include <QSlider>
+#include <QTableWidget>
+#include <QVBoxLayout>
+#include <QWidget>
+
+#include <apps/opencs/model/prefs/category.hpp>
+#include <apps/opencs/model/prefs/setting.hpp>
+#include <apps/opencs/view/widget/pushbutton.hpp>
+
+#include "brushshapes.hpp"
+#include "scenetool.hpp"
+
+#include "../../model/prefs/state.hpp"
+
+namespace CSVWidget
+{
+    class SceneToolbar;
+}
+
+namespace CSMDoc
+{
+    class Document;
+}
+
+CSVWidget::VertexPaintBrushSizeControls::VertexPaintBrushSizeControls(const QString& title, QWidget* parent)
+    : QGroupBox(title, parent)
+{
+    mBrushSizeSlider->setTickPosition(QSlider::TicksBothSides);
+    mBrushSizeSlider->setTickInterval(10);
+    mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt());
+    mBrushSizeSlider->setSingleStep(1);
+
+    mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt());
+    mBrushSizeSpinBox->setSingleStep(1);
+
+    QHBoxLayout* layoutSliderSize = new QHBoxLayout;
+    layoutSliderSize->addWidget(mBrushSizeSlider);
+    layoutSliderSize->addWidget(mBrushSizeSpinBox);
+
+    connect(mBrushSizeSlider, &QSlider::valueChanged, mBrushSizeSpinBox, &QSpinBox::setValue);
+    connect(mBrushSizeSpinBox, qOverload<int>(&QSpinBox::valueChanged), mBrushSizeSlider, &QSlider::setValue);
+
+    setLayout(layoutSliderSize);
+}
+
+CSVWidget::VertexPaintBrushWindow::VertexPaintBrushWindow(CSMDoc::Document& document, QWidget* parent)
+    : QFrame(parent, Qt::Popup)
+    , mDocument(document)
+{
+    mButtonPoint = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-point")), "", this);
+    mButtonSquare = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-square")), "", this);
+    mButtonCircle = new QPushButton(QIcon(QPixmap(":scenetoolbar/brush-circle")), "", this);
+
+    mSizeSliders = new VertexPaintBrushSizeControls("Brush size", this);
+
+    QVBoxLayout* layoutMain = new QVBoxLayout;
+    layoutMain->setSpacing(0);
+    layoutMain->setContentsMargins(4, 0, 4, 4);
+
+    QHBoxLayout* layoutHorizontal = new QHBoxLayout;
+    layoutHorizontal->setSpacing(0);
+    layoutHorizontal->setContentsMargins(QMargins(0, 0, 0, 0));
+
+    configureButtonInitialSettings(mButtonPoint);
+    configureButtonInitialSettings(mButtonSquare);
+    configureButtonInitialSettings(mButtonCircle);
+
+    mButtonPoint->setToolTip(toolTipPoint);
+    mButtonSquare->setToolTip(toolTipSquare);
+    mButtonCircle->setToolTip(toolTipCircle);
+
+    QButtonGroup* brushButtonGroup = new QButtonGroup(this);
+    brushButtonGroup->addButton(mButtonPoint);
+    brushButtonGroup->addButton(mButtonSquare);
+    brushButtonGroup->addButton(mButtonCircle);
+
+    brushButtonGroup->setExclusive(true);
+
+    layoutHorizontal->addWidget(mButtonPoint, 0, Qt::AlignTop);
+    layoutHorizontal->addWidget(mButtonSquare, 0, Qt::AlignTop);
+    layoutHorizontal->addWidget(mButtonCircle, 0, Qt::AlignTop);
+
+    mHorizontalGroupBox = new QGroupBox(tr(""));
+    mHorizontalGroupBox->setLayout(layoutHorizontal);
+
+    mToolSelector = new QComboBox(this);
+    mToolSelector->addItem(tr("Replace"));
+    // TOOD: in the future could add types like smooth blend, multiply etc
+
+    QLabel* colorLabel = new QLabel(this);
+    colorLabel->setText("Color:");
+    mColorButtonWidget = new ColorButtonWidget();
+
+    layoutMain->addWidget(mHorizontalGroupBox);
+    layoutMain->addWidget(mSizeSliders);
+    layoutMain->addWidget(mToolSelector);
+    layoutMain->addWidget(colorLabel);
+    layoutMain->addWidget(mColorButtonWidget);
+
+    setLayout(layoutMain);
+
+    connect(mButtonPoint, &QPushButton::clicked, this, &VertexPaintBrushWindow::setBrushShape);
+    connect(mButtonSquare, &QPushButton::clicked, this, &VertexPaintBrushWindow::setBrushShape);
+    connect(mButtonCircle, &QPushButton::clicked, this, &VertexPaintBrushWindow::setBrushShape);
+}
+
+void CSVWidget::VertexPaintBrushWindow::configureButtonInitialSettings(QPushButton* button)
+{
+    button->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
+    button->setContentsMargins(QMargins(0, 0, 0, 0));
+    button->setIconSize(QSize(48 - 6, 48 - 6));
+    button->setFixedSize(48, 48);
+    button->setCheckable(true);
+}
+
+void CSVWidget::VertexPaintBrushWindow::setBrushSize(int brushSize)
+{
+    mBrushSize = brushSize;
+    emit passBrushSize(mBrushSize);
+}
+
+void CSVWidget::VertexPaintBrushWindow::setBrushShape()
+{
+    if (mButtonPoint->isChecked())
+        mBrushShape = BrushShape_Point;
+    if (mButtonSquare->isChecked())
+        mBrushShape = BrushShape_Square;
+    if (mButtonCircle->isChecked())
+        mBrushShape = BrushShape_Circle;
+    emit passBrushShape(mBrushShape);
+}
+
+void CSVWidget::SceneToolVertexPaintBrush::adjustToolTips() {}
+
+CSVWidget::SceneToolVertexPaintBrush::SceneToolVertexPaintBrush(
+    SceneToolbar* parent, const QString& toolTip, CSMDoc::Document& document)
+    : SceneTool(parent, Type_TopAction)
+    , mToolTip(toolTip)
+    , mDocument(document)
+    , mVertexPaintBrushWindow(new VertexPaintBrushWindow(document, this))
+{
+    setAcceptDrops(true);
+    connect(mVertexPaintBrushWindow, &VertexPaintBrushWindow::passBrushShape, this,
+        &SceneToolVertexPaintBrush::setButtonIcon);
+    setButtonIcon(mVertexPaintBrushWindow->mBrushShape);
+
+    mPanel = new QFrame(this, Qt::Popup);
+
+    QHBoxLayout* layout = new QHBoxLayout(mPanel);
+
+    layout->setContentsMargins(QMargins(0, 0, 0, 0));
+
+    mTable = new QTableWidget(0, 2, this);
+
+    mTable->setShowGrid(true);
+    mTable->verticalHeader()->hide();
+    mTable->horizontalHeader()->hide();
+    mTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
+    mTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
+    mTable->setSelectionMode(QAbstractItemView::NoSelection);
+
+    layout->addWidget(mTable);
+
+    connect(mTable, &QTableWidget::clicked, this, &SceneToolVertexPaintBrush::clicked);
+}
+
+void CSVWidget::SceneToolVertexPaintBrush::setButtonIcon(CSVWidget::BrushShape brushShape)
+{
+    QString tooltip = "Change brush settings <p>Currently selected: ";
+
+    switch (brushShape)
+    {
+        case BrushShape_Point:
+
+            setIcon(QIcon(QPixmap(":scenetoolbar/brush-point")));
+            tooltip += mVertexPaintBrushWindow->toolTipPoint;
+            break;
+
+        case BrushShape_Square:
+
+            setIcon(QIcon(QPixmap(":scenetoolbar/brush-square")));
+            tooltip += mVertexPaintBrushWindow->toolTipSquare;
+            break;
+
+        case BrushShape_Circle:
+
+            setIcon(QIcon(QPixmap(":scenetoolbar/brush-circle")));
+            tooltip += mVertexPaintBrushWindow->toolTipCircle;
+            break;
+
+        case BrushShape_Custom:
+
+            setIcon(QIcon(QPixmap(":scenetoolbar/brush-custom")));
+            tooltip += mVertexPaintBrushWindow->toolTipCustom;
+            break;
+    }
+
+    setToolTip(tooltip);
+}
+
+void CSVWidget::SceneToolVertexPaintBrush::showPanel(const QPoint& position) {}
+
+void CSVWidget::SceneToolVertexPaintBrush::updatePanel() {}
+
+void CSVWidget::SceneToolVertexPaintBrush::clicked(const QModelIndex& index) {}
+
+void CSVWidget::SceneToolVertexPaintBrush::activate()
+{
+    QPoint position = QCursor::pos();
+    mVertexPaintBrushWindow->mSizeSliders->mBrushSizeSlider->setRange(
+        1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt());
+    mVertexPaintBrushWindow->mSizeSliders->mBrushSizeSpinBox->setRange(
+        1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt());
+    mVertexPaintBrushWindow->move(position);
+    mVertexPaintBrushWindow->show();
+}
+
+void CSVWidget::SceneToolVertexPaintBrush::dragEnterEvent(QDragEnterEvent* event)
+{
+    emit passEvent(event);
+    event->accept();
+}
+void CSVWidget::SceneToolVertexPaintBrush::dropEvent(QDropEvent* event)
+{
+    emit passEvent(event);
+    event->accept();
+}
diff --git a/apps/opencs/view/widget/scenetoolvertexpaintbrush.hpp b/apps/opencs/view/widget/scenetoolvertexpaintbrush.hpp
new file mode 100644
index 0000000000..a9b845d231
--- /dev/null
+++ b/apps/opencs/view/widget/scenetoolvertexpaintbrush.hpp
@@ -0,0 +1,165 @@
+#ifndef CSV_WIDGET_SCENETOOLVERTEXPAINTBRUSH_H
+#define CSV_WIDGET_SCENETOOLVERTEXPAINTBRUSH_H
+
+#include <QColorDialog>
+#include <QFrame>
+#include <QGroupBox>
+#include <QSlider>
+#include <QSpinBox>
+
+#ifndef Q_MOC_RUN
+#include "brushshapes.hpp"
+#include "scenetool.hpp"
+#endif
+
+class QComboBox;
+class QDragEnterEvent;
+class QDropEvent;
+class QModelIndex;
+class QObject;
+class QPoint;
+class QPushButton;
+class QWidget;
+
+namespace CSMDoc
+{
+    class Document;
+}
+
+class QTableWidget;
+
+namespace CSVRender
+{
+    class TerrainVertexPaintMode;
+}
+
+namespace CSVWidget
+{
+    class SceneToolbar;
+
+    class ColorButtonWidget : public QPushButton
+    {
+        Q_OBJECT
+
+    public:
+        ColorButtonWidget(QWidget* parent = nullptr)
+            : QPushButton(parent)
+        {
+            this->setFixedSize(50, 25);
+            this->setObjectName("colorSwatchButton");
+            this->setStyleSheet("QPushButton#colorSwatchButton { border: 1px solid #ccc; }");
+
+            connect(this, &QPushButton::clicked, this, &ColorButtonWidget::openColorDialog);
+        }
+
+    private:
+        QColor mColor = Qt::white;
+
+    signals:
+        void colorChanged(const QColor& newColor);
+
+    private slots:
+        void openColorDialog()
+        {
+            QColor color = QColorDialog::getColor(mColor, this, "Select Color");
+            if (color.isValid())
+            {
+                mColor = color;
+                QString css = QString("QPushButton#colorSwatchButton { background-color: %1; border: 1px solid #ccc; }")
+                                  .arg(color.name());
+                this->setStyleSheet(css);
+                emit colorChanged(color);
+            }
+        }
+    };
+
+    /// \brief Layout-box for some brush button settings
+    class VertexPaintBrushSizeControls : public QGroupBox
+    {
+        Q_OBJECT
+
+    public:
+        VertexPaintBrushSizeControls(const QString& title, QWidget* parent);
+
+    private:
+        QSlider* mBrushSizeSlider = new QSlider(Qt::Horizontal);
+        QSpinBox* mBrushSizeSpinBox = new QSpinBox;
+
+        friend class SceneToolVertexPaintBrush;
+        friend class CSVRender::TerrainVertexPaintMode;
+    };
+
+    /// \brief Brush settings window
+    class VertexPaintBrushWindow : public QFrame
+    {
+        Q_OBJECT
+
+    public:
+        VertexPaintBrushWindow(CSMDoc::Document& document, QWidget* parent = nullptr);
+        void configureButtonInitialSettings(QPushButton* button);
+
+        const QString toolTipPoint = "Paint single point";
+        const QString toolTipSquare = "Paint with square brush";
+        const QString toolTipCircle = "Paint with circle brush";
+        const QString toolTipCustom = "Paint with custom brush";
+
+    private:
+        CSVWidget::BrushShape mBrushShape = CSVWidget::BrushShape_Point;
+        int mBrushSize = 1;
+        CSMDoc::Document& mDocument;
+        QGroupBox* mHorizontalGroupBox;
+        QComboBox* mToolSelector;
+        QPushButton* mButtonPoint;
+        QPushButton* mButtonSquare;
+        QPushButton* mButtonCircle;
+        VertexPaintBrushSizeControls* mSizeSliders;
+        ColorButtonWidget* mColorButtonWidget;
+
+        friend class SceneToolVertexPaintBrush;
+        friend class CSVRender::TerrainVertexPaintMode;
+
+    public slots:
+        void setBrushShape();
+        void setBrushSize(int brushSize);
+
+    signals:
+        void passBrushSize(int brushSize);
+        void passBrushShape(CSVWidget::BrushShape brushShape);
+    };
+
+    class SceneToolVertexPaintBrush : public SceneTool
+    {
+        Q_OBJECT
+
+        QString mToolTip;
+        CSMDoc::Document& mDocument;
+        QFrame* mPanel;
+        QTableWidget* mTable;
+        VertexPaintBrushWindow* mVertexPaintBrushWindow;
+
+    private:
+        void adjustToolTips();
+
+    public:
+        SceneToolVertexPaintBrush(SceneToolbar* parent, const QString& toolTip, CSMDoc::Document& document);
+
+        void showPanel(const QPoint& position) override;
+        void updatePanel();
+
+        void dropEvent(QDropEvent* event) override;
+        void dragEnterEvent(QDragEnterEvent* event) override;
+
+        friend class CSVRender::TerrainVertexPaintMode;
+
+    public slots:
+        void setButtonIcon(CSVWidget::BrushShape brushShape);
+        void clicked(const QModelIndex& index);
+        void activate() override;
+
+    signals:
+        void passEvent(QDropEvent* event);
+        void passEvent(QDragEnterEvent* event);
+    };
+}
+
+#endif