Well, first of all, make sure your mesh isn’t all in one piece. That’s one way to do it.
The other way would be to open buffer streams so that you can “live edit” the mesh, although I’m sure there is someone more knowledgeable when it comes to vertex buffers, who can explains the process to you. I’ve never tried it myself, but that’s the more robust, industrial way to do it.
For the first way, though, you manage your mesh through “chunks”, pieces of it that are of reasonable size. And you never actually change the mesh itself, you create it once, then clone it by assigning to mesh property of mesh filters.
During raycasting, you get back which mesh was hit at which triangle and which coordinate. You write this information down into the color array that was already at your disposal, like a ready canvas. You should probably have these ready for all user-accessible meshes. (Don’t forget that your code lives on the CPU side, and you should be the owner of your data, once you send something to the GPU, don’t lose a local reference to it, if your aim is to recreate the thing with an update.) With this method, you wrap up the same preallocated mesh but upload this modified color vertex array along with it. GPU side can access the color information just like any other, it’s just mostly unused with the regular Unity shaders. However it’s a perfectly valid data stream, and many specialized shaders readily use it because it has a full 32-bit range and is natively supported.
With the other method I mentioned before, you can do even more because you get to stream only the updates to GPU, but as I said, let’s stick with what I’m certain how to implement. Because you don’t allocate nothing on the CPU side, there is no time wasted on heap access, and more importantly, there is no garbage. You also do not make a new mesh every time from scratch. You just wrap things up, assign them as they are, literally only references flying around.
Leave enough room in the codebase so that you can adjust the “chunk” size and thus find a perfect trade-off between update frequency and performance, at a later stage.
You should make the process of invalidating the chunk automated, this is called a ‘dirty flag’ or dirtying, where you basically “paint” by instructing some controller/manager that the “brush” has been detected there and there with such and such operation. The “painter” then queries which mesh chunk it was, and which vertices are affected, the information is stored accordingly, and the affected chunks are marked as ‘dirty’.
Another part of the system then sweeps through ‘dirty’ stuff, and repackages them in an optimal way, making sure to do a minimum of work, or even to distribute some work over multiple frames (or threads) as needed, until everything becomes ‘clean’ again. In the meantime, the renderers are always fresh and feel responsive to user input, because the process itself is super-localized and all load is distributed.
In your specific case, you can also mathematically construct a relationship between the “brush” coordinates and the chunk/vertices. Or you can even prebuild a so-called “map”, a one-to-one correspondence where you either analyze (or scan) the setup as it was during initialization, or you actually build everything from ground up, fully in charge of how you’re going to address this space later in the code. So you can very easy completely bypass the need for raycasting, although raycasting would be the easiest way to set this up, if you’re new to system design and would like to have one functional thing that you can rely upon because it simply works out of the box.
One useful way to make a mathematical relationship is by knowing that certain binary tricks can be used to your advantage. For example, if you make your chunk sizes follow the powers of two, you can quickly transform the coordinates by shifting the values to the right. Next, if your level is a finite 2D plane like you’ve shown, there is a very simple numeration scheme to quickly identify the index of a chunk. In a setup where WxH chunks cover the rectangular area, each (X,Y) chunk can be enumerated as N=Y*W+X, letting you to linearize the array and stop thinking in 2D any more (to get X,Y back, you would do X=N%W, Y=N/W, the latter being integer division, but you mostly don’t need the inverse, at least that’s the case in my practice, because the data is already spatially arranged). And you can follow a similar scheme for the quad cells as well.
And so on, this is too much text already. Hopefully some of it makes sense to you.