A few months ago I was implementing something similar for one of my toys. The screenshot shows the orange dots of trees:

They are arranged with different densities. The algorithm itself has complexity commensurate with the number of trees generated. I do not know whether this method will suit you or not, since trees are generated for each chunk separately ( per-chunk ).
A small preface. In my algorithm, I generated different structures in different ways, but as for the landscape and trees, they were generated for each chunk separately.
Each chunk had its own seed , according to which this chunk was generated. I took the Sid from the noise of the pearl according to the coordinates of the chunk, for example: seed = getPerlin(chunkX, chunkY) . Next, using this seed, I generated all the structures on the chunk.
And now to the trees. To begin, I chose an algorithm that can pseudo-randomly arrange trees. The choice fell on the halton sequence . Everything is simple:
getHalton(index, base) { var result = 0; var f = 1 / base; var i = index; while(i > 0) { result = result + f * (i % base); i = Math.floor(i / base); f = f / base; } return result; }
index is the number in the sequence, base is the base. In order to arrange the points in two-dimensional space, I used the bases 2 and 3 . Something like this:
var x = getHalton(index, 2) * chunkSize; var y = getHalton(index, 3) * chunkSize;
Here chunkSize is the size of the chunk (because the getHalton values getHalton in the interval [0, 1] ). In order to receive, depending on the sid, different variations of the arrangement of trees, I introduced the variable indexShift . This is just an integer generated by a seed . And we just add it to index 'y.
For clarity, we introduce a certain function setTree(x, y) , which will set the trees at the point of the chunk in which we need. Then the generation of trees will look like this:
for (var i = 0; i < treesCount; ++i) { var x = getHalton(i + indexShift, 2) * chunkSize; var y = getHalton(i + indexShift, 3) * chunkSize; setTree(x, y); }
Good. Now we have pseudo-randomly arranged trees. But this is not enough for us, right?
And then the same pearl noise comes to the rescue. Since I used noise in my project, which returned values ​​in the range [-1, 1] , and I wanted to get values ​​between [0, 1] , I took the value like this:
var value = getPerlin(x * noiseScale, y * noiseScale) * 0.5 + 0.5;
The variable noiseScale used to adjust the noise scale. Well, now we have a value in the range [0; 1] [0; 1] . But how will this help us? And let's modify the code for generating trees like this !:
for (var i = 0; i < treesCount; ++i) { var x = getHalton(i + indexShift, 2) * chunkSize; var y = getHalton(i + indexShift, 3) * chunkSize; var densityInPoint = getPerlin(x * noiseScale, y * noiseScale) * 0.5 + 0.5; if (i < treesCount * densityInPoint) setTree(x, y); }
Thus, we sift out some of the trees in places where the noise of the pearl is low.
So, what we have: some algorithm. The result of his work depends on three variables:
treesCount - the more, the greater the overall density of the trees.noiseScale - the more, the faster the density changes per unit of distance (the less homogeneous the density).densityInPoint - different values ​​give a generally different result.
PS: by the way, it seemed to me that the linear densityInPoint value gives not a very good result, so I additionally squared it.
PPS: different density of trees in different biomes is visible on the map. For this result, I just set different treesCount values ​​for each biome.
PPPS: if your trees are set in a matrix, then the x and y values ​​must be integer. Then just take the whole part of them:
setTree(Math.floor(x), Math.floor(y));
PPPPS: I'm not saying that this solution is the best and most correct. However, it is much faster than the method you found in the article, and for me it gave acceptable results.