Why?
For plan renderings, I currently use tree/forest scatter brushes with layer styles applied, but this method doesn't look as great due to not having the shadow in the tree canopy. I thought about having a pattern overlay that incorporates trees canopy shadows, but then you have to get precise with tree brush placements on the edge of forests to match up with the texture and if needing to change the shadow direction, the pattern overlay would need to be rotated too and the forest border would need to be refined.
General Solution
Create a tool that can scatter trees on individual layers so bevel and emboss can be applied to each canopy vs an entire forest canopy mass. Downside is it's processing intensive and if there are significant design updates, the quickest thing would be to delete all trees and run the script again so the script needs to be efficient to run on your average laptop. There would be a "forest area" layer that is just a solid fill for where trees should be scattered/spawned so that border can be easily selected to run the script.
What I've done so far
Used AI to create scripts and experimented with different methods to try and get desired results with optimization. I first tried to randomly place trees as smart objects, but couldn't get the script to respect a pre-selected forest area very well and it was very slow. Then tried a grid sample approach if a point was in the selected area to spawn trees, this was slow and I let my pc process overnight, but never saw results. Lastly, switched to a stamp method (see attached image and script) which works alright but it didn't achieve the desired density and was relatively slow since it has to open and close a tree.png for every tree placement which could be hundreds of times, which is not ideal and some trees are too overlapped. I tried improving it to increase density, reduce overlap, and even open all the tree .pngs first and/or add them to the same .psd and reference them for better performance but couldn't get those options to really work.
Another solution would be to have an array of trees with realistic shadows vs dropshadows that are connected to a dynamic border, but I haven't seen anything like that in Photoshop to know if that's feasible.
Disclaimer: the trees and the effects below are just a quick draft to see if it's possible to work with a script (tree .png's are attached). If someone here can improve this script or Adobe creates a better workflow for this, then I'll create much better looking trees and effects.


#target photoshop
// Define the resolution parameter (pixels/inch)
var resolution = 200; // Default to 200 DPI, user can modify
// Define the server path
var serverPath = "C:\\Vegetation\\Trees\\Plan";
// Define the tree file names
var treeFiles = ["Tree01.png", "Tree02.png", "Tree03.png", "Tree04.png", "Tree05.png", "Tree06.png"];
// Load .png files from server
function loadPNGsFromServer(serverPath, treeFiles) {
var pngPaths = [];
for (var i = 0; i < treeFiles.length; i++) {
var fullPath = new File(serverPath + "\\" + treeFiles[i]);
if (fullPath.exists) {
pngPaths.push(fullPath);
} else {
alert("File not found: " + fullPath.fsName);
}
}
return pngPaths;
}
// Scatter .png files within a preselected area
function scatterPNGsInArea(pngPaths, scale, treeDensity, resolution, minSizeFeet, maxSizeFeet) {
var docRef = app.activeDocument;
try {
if (docRef.selection && docRef.selection.bounds && docRef.selection.bounds.length > 0) {
var selectionBounds = docRef.selection.bounds;
var xMin = selectionBounds[0].value;
var yMin = selectionBounds[1].value;
var xMax = selectionBounds[2].value;
var yMax = selectionBounds[3].value;
var width = xMax - xMin;
var height = yMax - yMin;
var area = width * height;
// Convert min/max sizes from feet to pixels
var minSizePx = (minSizeFeet / scale) * resolution; // Convert to pixels
var maxSizePx = (maxSizeFeet / scale) * resolution; // Convert to pixels
// Calculate tree count for specified density
var avgSizePx = (minSizePx + maxSizePx) / 2;
var effectiveArea = area * (treeDensity / 100); // Density as percentage
var treesPerRow = Math.max(1, Math.floor(Math.sqrt(effectiveArea) / avgSizePx));
var treeCount = Math.min(1000, Math.max(100, Math.floor(treesPerRow * treesPerRow))); // Increased density by 10x
var placedTrees = 0;
for (var i = 0; i < treeCount; i++) {
try {
var randomIndex = Math.floor(Math.random() * pngPaths.length);
var fileRef = pngPaths[randomIndex];
var treeDoc = app.open(fileRef);
// Ensure the tree document is at the correct resolution
treeDoc.resizeImage(undefined, undefined, resolution, ResampleMethod.NONE);
treeDoc.selection.selectAll();
treeDoc.selection.copy();
treeDoc.close(SaveOptions.DONOTSAVECHANGES);
docRef.paste();
var layerRef = docRef.activeLayer;
// Random size within min/max range
var sizePx = minSizePx + (Math.random() * (maxSizePx - minSizePx));
// Resize the layer using percentage scaling (base size is 100px)
var scaleFactor = (sizePx / 100) * 100; // Percentage relative to 100px
layerRef.resize(scaleFactor, scaleFactor, AnchorPosition.MIDDLECENTER);
// Get dimensions for positioning
var newWidth = layerRef.bounds[2].value - layerRef.bounds[0].value;
var newHeight = layerRef.bounds[3].value - layerRef.bounds[1].value;
// Random position ensuring the entire tree stays within bounds
var xPos = xMin + (newWidth / 2) + Math.random() * (width - newWidth);
var yPos = yMin + (newHeight / 2) + Math.random() * (height - newHeight);
layerRef.translate(xPos - layerRef.bounds[0].value, yPos - layerRef.bounds[1].value);
// Random rotation
var rotation = Math.random() * 360;
var idTrnf = charIDToTypeID("Trnf");
var desc3 = new ActionDescriptor();
desc3.putEnumerated(charIDToTypeID("FTcs"), charIDToTypeID("QCSt"), charIDToTypeID("Qcsa"));
var idOfst = charIDToTypeID("Ofst");
var desc4 = new ActionDescriptor();
desc4.putUnitDouble(charIDToTypeID("Hrzn"), charIDToTypeID("#Pxl"), 0);
desc4.putUnitDouble(charIDToTypeID("Vrtc"), charIDToTypeID("#Pxl"), 0);
desc3.putObject(idOfst, charIDToTypeID("Ofst"), desc4);
desc3.putUnitDouble(charIDToTypeID("Angl"), charIDToTypeID("#Ang"), rotation);
executeAction(idTrnf, desc3, DialogModes.NO);
placedTrees++;
} catch (e) {
alert("Failed to place tree " + (i + 1) + ". Error: " + e + "\nContinuing with next tree.");
continue;
}
}
docRef.selection.deselect();
alert("Successfully placed " + placedTrees + " trees in the selected area.");
} else {
alert("No valid selection found. Please draw a selection with the Marquee Tool (M) before running the script.");
}
} catch (e) {
alert("Error processing selection.\nError: " + e + "\nPlease ensure a selection exists and try again.");
}
}
// Main function
function main() {
var pngPaths = loadPNGsFromServer(serverPath, treeFiles);
if (pngPaths.length === 0) {
alert("No valid tree files found. Check the server path and file names.");
return;
}
// Create a dialog for setting parameters
var dlg = new Window("dialog", "Tree Scatter");
dlg.orientation = "column";
dlg.alignChildren = "left";
dlg.add("statictext", undefined, "Resolution (DPI):");
var resolutionInput = dlg.add("edittext", undefined, "200");
resolutionInput.characters = 5;
dlg.add("statictext", undefined, "Scale:");
var scaleInput = dlg.add("edittext", undefined, "100");
scaleInput.characters = 5;
dlg.add("statictext", undefined, "Minimum Size (feet diameter):");
var minSizeInput = dlg.add("edittext", undefined, "15");
minSizeInput.characters = 5;
dlg.add("statictext", undefined, "Maximum Size (feet diameter):");
var maxSizeInput = dlg.add("edittext", undefined, "50");
maxSizeInput.characters = 5;
dlg.add("statictext", undefined, "Density Percent:");
var densityInput = dlg.add("edittext", undefined, "95");
densityInput.characters = 5;
var scatterButton = dlg.add("button", undefined, "Scatter Trees");
scatterButton.onClick = function() {
var resolution = parseFloat(resolutionInput.text);
var scale = parseFloat(scaleInput.text);
var minSizeFeet = parseFloat(minSizeInput.text);
var maxSizeFeet = parseFloat(maxSizeInput.text);
var treeDensity = parseFloat(densityInput.text);
if (isNaN(resolution) || isNaN(scale) || isNaN(minSizeFeet) || isNaN(maxSizeFeet) || isNaN(treeDensity)) {
alert("Please enter valid numerical values for all fields.");
return;
}
if (resolution <= 0 || scale <= 0 || minSizeFeet <= 0 || maxSizeFeet <= 0 || treeDensity <= 0 || treeDensity > 100) {
alert("Resolution, scale, sizes, and density must be positive numbers; density must be 0-100.");
return;
}
if (minSizeFeet > maxSizeFeet) {
alert("Minimum size cannot be greater than maximum size.");
return;
}
scatterPNGsInArea(pngPaths, scale, treeDensity, resolution, minSizeFeet, maxSizeFeet);
dlg.close();
};
dlg.show();
}
main();