Last active
February 8, 2022 08:46
-
-
Save GetUpKidAK/f3b4c36d7761264255d2 to your computer and use it in GitHub Desktop.
Photoshop script to export textures from layered PSD
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// Enables exporting of several PBR texture maps from a single PSD with a few clicks: | |
// | |
// 1. Select an export folder | |
// 2. Choose which PSD layer group corresponds to which map (split into separate RGB/Alpha channels) | |
// 3. Change the file export options (if required) | |
// 4. Hit export. | |
// | |
// Created by Ash Kendall. ash.kendall(at)gmail.com | |
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
#target photoshop | |
app.bringToFront(); | |
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// DEFAULT OPTIONS | |
// | |
// autoSave (true/false) : Auto-save exported files when created (this will overwrite any existing file without a prompt) | |
// lowercaseFilename (true/false) : Make export filenames all lowercase (Base filename will match the document you are exporting from) | |
// removeFilenameSpaces (true/false) : Replace spaces in exported filename with underscores | |
// closeDocsOnSave (true/false) : Close auto-saved documents after being exported | |
// useCompression (true/false) : Use Targa's RLE compression when saving files | |
// exportPathDefault (string) : Use this as the default export path. If blank the current document path will be used (Save before you export!) | |
// Usage example: "C:/Users/~YourUserName~/Documents/" - Windows (NOTE THE FORWARD SLASHES) | |
// "/Users/~YourUserName~/Documents/" - OSX | |
// exportedFileExtension : Not supported, and will likely break something. DO NOT CHANGE. SERIOUSLY | |
// | |
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
var options = { | |
autoSave: true, | |
lowercaseFilename: true, | |
removeFilenameSpaces: true, | |
useCompression: true, | |
closeDocsOnSave: true, | |
exportedFileExtension: ".tga" // Not supported, don't change! | |
}; | |
// Map constructor. These are set up below... | |
var Map = function(mapName, defaultLayerName, defaultAlphaLayerName, filePostfix, alphaState) | |
{ | |
this.mapName = mapName; | |
this.defaultLayerName = defaultLayerName; | |
this.defaultAlphaLayerName = defaultAlphaLayerName; | |
this.filePostfix = filePostfix; | |
this.exportMap = false; | |
this.exportRGB = false; | |
this.exportAlpha = false; | |
this.rgbLayerIndex = 0; | |
this.alphaLayerIndex = 0; | |
this.alphaState = alphaState; | |
this.UpdateMapInfo = function(selected, rgb, a) | |
{ | |
this.exportMap = selected; | |
this.exportRGB = rgb > 0 ? true : false; | |
this.exportAlpha = a > 0 ? true : false; | |
this.rgbLayerIndex = rgb; | |
this.alphaLayerIndex = a; | |
}; | |
this.ReadyToExport = function() | |
{ | |
if (!this.exportRGB && !this.exportAlpha) // Selected for export but no layer groups selected | |
{ | |
errorsLog += "- The " + this.mapName + " map is marked for export but no channels have been selected.\n\n"; | |
return false; | |
} | |
if (!this.exportRGB && this.exportAlpha) | |
{ | |
if (this.alphaState == alpha.EXCLUSIVE) return true; | |
errorsLog += "- You can't export an Alpha channel without an RGB channel on the " + this.mapName + " map.\n\n"; | |
return false; | |
} | |
else if (this.exportRGB && (!this.exportAlpha && this.alphaState == alpha.REQUIRED)) | |
{ | |
errorsLog += "- The " + this.mapName + " map can't be exported without the required Alpha channel.\n\n"; | |
return false; | |
} | |
return true; | |
} | |
} | |
var alpha = { AVAILABLE: 0, REQUIRED: 1, DISABLED: 2, EXCLUSIVE: 3 } | |
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// AVAILABLE MAPS CAN BE ADJUSTED BELOW | |
// Parameters are as follows: | |
// | |
// Map name: Display name for the map | |
// Default RGB Layer Name: Name to be checked against the Layer Groups when pre-filling the dropdown (for RGB channel) | |
// Default Alpha Layer Name: Name to be checked against the Layer Groups when pre-filling the dropdown (for Alpha channel) | |
// File postfix: Postfix to be added after the base filename when exported | |
// Alpha Channel status: Status of Alpha channel for map - AVAILABLE for use, REQUIRED for the map, DISABLED (Unused) for map, | |
// and EXCLUSIVE (Alpha-only) | |
// | |
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// As detailed above: |Map Name |RGB Layer Name |Alpha Layer Name |File post-fix |Alpha channel status | |
var maps = {}; | |
maps.albedo = new Map ("Albedo", "albedo", "transparency", "_albedo", alpha.AVAILABLE); | |
maps.metallic = new Map ("Metallic", "metallic", "smoothness", "_metallic", alpha.REQUIRED); | |
maps.normal = new Map ("Normal", "normal", "", "_normal", alpha.DISABLED); | |
maps.height = new Map ("Height", "height", "", "_height", alpha.DISABLED); | |
maps.ao = new Map ("Occlusion", "ao", "", "_ao", alpha.DISABLED); | |
maps.emission = new Map ("Emission", "emission", "", "_emission", alpha.DISABLED); | |
maps.detailMask = new Map ("Detail Mask", "", "detail mask", "_detailMask", alpha.EXCLUSIVE); | |
// Document caches | |
var doc; // Active document | |
var docFilename; // Document filename | |
var docLayerSets; // Layer groups cache | |
// Global variables | |
var exportedFilename; // Filename for exported file | |
var exportedFilePath; // Path for exported file | |
// Settings file info | |
var settingsPath = new Folder(Folder.myDocuments + "/TextureExporter/"); // Settings path | |
var settingsFilename = "Settings.cfg"; // Settings filename | |
var settingsFile = new File(settingsPath.fsName + "/" + settingsFilename); // Full settings filepath | |
var errorsLog; // Error log | |
var mapsToExport = new Array(0); // Maps to export | |
function Main () | |
{ | |
// Check there is an open document, otherwise quit | |
if (app.documents.length == 0) { alert ("No active document."); return 1; } | |
// Document cache | |
doc = app.activeDocument; | |
// Quit if there are no layer groups in the document | |
if (doc.layerSets.length == 0) { alert ("There are no Layer Groups in the active document."); return 1; } | |
// Early setup (set up export path and ready error log) | |
exportedFilePath = GetExportPath(); | |
errorsLog = "The following errors occurred: \n\n"; | |
// Show dialog | |
var result = ShowDialog(); | |
// Form is all ok | |
if (result == 0) | |
{ | |
// Get the updated filename | |
exportedFilename = GetBaseFilename(); | |
// Export all eligible maps | |
for (var i = 0; i < mapsToExport.length; i++) | |
{ | |
ExportMaps(mapsToExport[i]); | |
} | |
alert("All textures exported."); | |
// Save settings | |
SaveSettingsFile(); | |
} | |
else | |
{ | |
// Quit | |
return 1; | |
} | |
// Quit | |
return 0; | |
} | |
// GET EXPORT PATH | |
function GetExportPath () | |
{ | |
// If settings file doesn't exist | |
if (!settingsFile.exists) | |
{ | |
// Return current document path | |
return doc.path.toString(); | |
} | |
else | |
{ | |
// Open settings file and get saved path | |
settingsFile.open("r"); | |
var path = settingsFile.readln(); | |
settingsFile.close(); | |
return path; // Return path | |
} | |
} | |
// CREATE BASE FILENAME FOR EXPORTED FILE(S) | |
function GetBaseFilename () | |
{ | |
var baseName = doc.name.substr(0, doc.name.lastIndexOf('.')); | |
if (options.lowercaseFilename) baseName = baseName.toLowerCase(); | |
if (options.removeFilenameSpaces) baseName = baseName.replace (" ", "_"); | |
return baseName; | |
} | |
function SaveSettingsFile () | |
{ | |
if (!settingsPath.exists) | |
{ | |
if (!settingsPath.create()) | |
{ | |
alert("Couldn't create the settings file."); | |
return; | |
} | |
} | |
settingsFile.open ("w"); | |
settingsFile.writeln(exportedFilePath); | |
settingsFile.close(); | |
} | |
// MAIN DIALOG WINDOW | |
function ShowDialog () | |
{ | |
// Create the window | |
var win = new Window ("dialog", "Texture Map Exporter"); | |
var mainPanel = win.add("panel", undefined, "Export Textures"); | |
// Add a group for the exported file details | |
var exportInfoGroup = mainPanel.add("panel", undefined, "Export path"); | |
var folderGroup = exportInfoGroup.add("group", undefined); | |
var exportPathField = folderGroup.add("edittext", undefined, exportedFilePath); | |
exportPathField.size = [420, ""]; | |
var selectFolderBtn = folderGroup.add ("button", undefined, "..."); | |
// Caches for checkboxes and exported channels | |
var exportCheckboxes = new Array(0); | |
var rgbChannels = new Array(0); | |
var alphaChannels = new Array(0); | |
// Generate layer groups list for drop-downs | |
var docLayerSets = [ " -- " ]; | |
for (var i = 0; i < doc.layerSets.length; i++) | |
{ | |
var layerName = doc.layerSets[i].name; | |
docLayerSets.push(layerName); | |
} | |
// Add panels | |
var i = 0; | |
// Create panels and dropdowns for each exportable map | |
for (var map in maps) | |
{ | |
var mapPanel = mainPanel.add("panel", undefined, maps[map].mapName + " map:"); | |
mapPanel.orientation = "row"; | |
mapPanel.add("statictext", undefined, "Export map:"); | |
exportCheckboxes[i] = mapPanel.add("checkbox", undefined); | |
mapPanel.add("statictext", undefined, "RGB channel: "); | |
rgbChannels[i] = mapPanel.add("dropdownList", undefined, docLayerSets); | |
FindDefaultLayer(rgbChannels[i], maps[map].defaultLayerName); | |
mapPanel.add("statictext", undefined, "Alpha channel: "); | |
alphaChannels[i] = mapPanel.add("dropdownList", undefined, docLayerSets); | |
FindDefaultLayer(alphaChannels[i], maps[map].defaultAlphaLayerName); | |
if (maps[map].alphaState == alpha.EXCLUSIVE) rgbChannels[i].enabled = false; | |
if (maps[map].alphaState == alpha.DISABLED) alphaChannels[i].enabled = false; | |
if (rgbChannels[i].selection != 0 || alphaChannels[i].selection != 0) | |
{ | |
exportCheckboxes[i].value = true; | |
} | |
// CALLBACKS | |
rgbChannels[i].onChange = function() | |
{ | |
if (this.selection.index != 0) | |
{ | |
this.parent.children[1].value = true; | |
} | |
else | |
{ | |
if (this.parent.children[5].selection == 0) | |
{ | |
this.parent.children[1].value = false; | |
} | |
} | |
} | |
alphaChannels[i].onChange = function() | |
{ | |
if (this.selection.index != 0) | |
{ | |
this.parent.children[1].value = true; | |
} | |
else | |
{ | |
if (this.parent.children[3].selection == 0) | |
{ | |
this.parent.children[1].value = false; | |
} | |
} | |
} | |
i++; | |
} | |
// Add export options group | |
var optionsGroup = mainPanel.add("panel", undefined, "Export options"); | |
optionsGroup.size = [510, 160] | |
var autoSaveCheckbox = optionsGroup.add("checkbox", undefined, "Auto-save exported files (Overwrites any existing files)"); | |
var lowercaseCheckbox = optionsGroup.add("checkbox", undefined, "Convert filename to lowercase ('ExportMap' to 'exportmap')"); | |
var removeSpacesCheckbox = optionsGroup.add("checkbox", undefined, "Convert filename spaces to underscores ('export map' to 'export_map')"); | |
var saveOptionsGroup = optionsGroup.add("panel", undefined, "Save options"); | |
saveOptionsGroup.orientation = "row"; | |
var compressionCheckbox = saveOptionsGroup.add("checkbox", undefined, "Use RLE compression"); | |
var closeOnSaveCheckbox = saveOptionsGroup.add("checkbox", undefined, "Auto-close exported documents"); | |
// Enable/disable options according to defaults | |
autoSaveCheckbox.value = options.autoSave; | |
lowercaseCheckbox.value = options.lowercaseFilename; | |
removeSpacesCheckbox.value = options.removeFilenameSpaces; | |
compressionCheckbox.value = options.useCompression; | |
closeOnSaveCheckbox.value = options.closeDocsOnSave; | |
// Enabled/disable save options based on auto-save checkbox | |
compressionCheckbox.enabled = autoSaveCheckbox.value; | |
closeOnSaveCheckbox.enabled = autoSaveCheckbox.value; | |
// Add buttons group | |
var buttonGroup = mainPanel.add("group"); | |
var exportBtn = buttonGroup.add("button", undefined, "Export maps", {name: "ok"}); | |
var cancelBtn = buttonGroup.add("button", undefined, "Cancel"); | |
// Main callbacks | |
autoSaveCheckbox.onClick = function () | |
{ | |
// Update save options based on auto-save checkbox | |
compressionCheckbox.enabled = this.value; | |
closeOnSaveCheckbox.enabled = this.value; | |
} | |
selectFolderBtn.onClick = function () | |
{ | |
// Show folder select dialog and update based on selection | |
var newPath = new Folder(exportedFilePath); | |
newPath = newPath.selectDlg("Select a folder to export to: "); | |
if (newPath != null) | |
{ | |
exportPathField.text = newPath.fsName; | |
} | |
} | |
exportBtn.onClick = function () | |
{ | |
// Update options based on selections | |
options.autoSave = autoSaveCheckbox.value; | |
options.lowercaseFilename = lowercaseCheckbox.value; | |
options.removeFilenameSpaces = removeSpacesCheckbox.value; | |
options.useCompression = compressionCheckbox.value; | |
options.closeDocsOnSave = closeOnSaveCheckbox.value; | |
exportedFilePath = exportPathField.text; | |
// Update map info based on form info | |
var i = 0; | |
for (var map in maps) | |
{ | |
maps[map].UpdateMapInfo(exportCheckboxes[i].value, rgbChannels[i].selection, alphaChannels[i].selection); | |
i++; | |
} | |
// Check if entries are all valid | |
if (FormEntryValid()) | |
win.close(1); // If validation passes | |
else | |
alert(errorsLog); // Show errors if not | |
errorsLog = "The following errors occurred: \n\n"; | |
} | |
if (win.show() == 1) | |
{ | |
errorsLog = "The following errors occurred: \n\n"; | |
return 0; | |
} | |
else | |
{ | |
return 1; | |
} | |
} | |
// CHECK IF DEFAULT LAYER NAME EXISTS FOR THIS MAP | |
function FindDefaultLayer (list, defaultLayerName) | |
{ | |
// Set active selection to first entry | |
list.selection = 0; | |
// Loop through dropdown | |
for (var i = 0; i < list.items.length; i++) | |
{ | |
// Get item name | |
var itemName = list.items[i].text.toLowerCase(); | |
// Check if current entry matches the default layer name for this map | |
if (itemName == defaultLayerName.toLowerCase()) | |
{ | |
list.selection = i; // Set as active selection if matching | |
return; // Stop looping | |
} | |
} | |
} | |
// CHECK FORM IS VALID BEFORE SUBMITTING | |
function FormEntryValid () | |
{ | |
if (ExportMapSelectionsValid() && FolderExists ()) | |
return true; | |
} | |
// CHECK IF EXPORT MAPS ARE VALID | |
function ExportMapSelectionsValid () | |
{ | |
mapsToExport.length = 0; | |
var errorsCount = 0; | |
for (var map in maps) | |
{ | |
if (maps[map].exportMap) | |
{ | |
if (!maps[map].ReadyToExport()) | |
{ | |
errorsCount++; | |
} | |
else | |
{ | |
mapsToExport.push(maps[map]); | |
} | |
} | |
} | |
if (mapsToExport.length == 0) { errorsLog += "- There are no maps selected for export.\n\n"; errorsCount++; } | |
if (errorsCount > 0) return false; | |
return true; | |
} | |
// CHECK IF EXPORT FOLDER EXISTS | |
function FolderExists () | |
{ | |
if (exportedFilePath.exists) | |
return true; // All good | |
else | |
{ | |
// Setup folder variable | |
var newPath = new Folder (exportedFilePath); | |
// Try to create path | |
if (newPath.create()) | |
return true; // Created successfully | |
else | |
{ | |
errorsLog += "- The export folder doesn't exist and couldn't be created. Please check you've entered a valid path.\n\n"; | |
return false; // Failed, and error added to log | |
} | |
} | |
} | |
// DO THE EXPORT | |
function ExportMaps (mapToExport) | |
{ | |
// New document variable | |
var newDoc; | |
var docCreated = false; // Check if new doc has been made yet. | |
// Set original doc as active document | |
app.activeDocument = doc; | |
// Set active channels to component channels (RGB) | |
doc.activeChannels = doc.componentChannels; | |
HideAllLayers(); | |
// If RGB channel is being exported for this map... | |
if (mapToExport.exportRGB) | |
{ | |
// Get the layer group that is being exported, make it visible and the active layer | |
var layerToExport = doc.layerSets[mapToExport.rgbLayerIndex]; | |
layerToExport.visible = true; | |
doc.activeLayer = layerToExport; | |
// Select all and copy merged | |
var selection = doc.selection.selectAll(); | |
doc.selection.copy (true); | |
// Create new document | |
newDoc = app.documents.add(doc.width, doc.height, doc.resolution, exportedFilename + mapToExport.filePostfix); | |
docCreated = true; | |
// Make the new document active, select RGB channels and paste | |
app.activeDocument = newDoc; | |
newDoc.activeChannels = newDoc.componentChannels; | |
newDoc.paste(); | |
} | |
// If Alpha channel is being exported for this map... | |
if (mapToExport.exportAlpha) | |
{ | |
// Set original doc as active document | |
app.activeDocument = doc; | |
HideAllLayers (); | |
// Get the layer group that is being exported, make it visible and the active layer | |
var layerToExport = doc.layerSets[mapToExport.alphaLayerIndex]; | |
layerToExport.visible = true; | |
doc.activeLayer = layerToExport; | |
// Select all and copy merged | |
var selection = doc.selection.selectAll(); | |
doc.selection.copy (true); | |
// If document has been created, select it... | |
if (docCreated) | |
app.activeDocument = newDoc; | |
else | |
{ | |
// Otherwise, create the document | |
newDoc = app.documents.add(doc.width, doc.height, doc.resolution, exportedFilename + mapToExport.filePostfix); | |
} | |
// Create the alpha channel, select the alpha channel, and paste | |
var alphaChannel = newDoc.channels.add(); | |
newDoc.activeChannels = [alphaChannel]; | |
newDoc.paste(); | |
} | |
// If autosave is enabled, save away | |
if (options.autoSave) | |
{ | |
SaveFile(newDoc); | |
} | |
// Set original doc as active document, select RGB channels | |
app.activeDocument = doc; | |
doc.activeChannels = doc.componentChannels; | |
// Deselect all | |
doc.selection.deselect(); | |
} | |
// HIDE ALL LAYERS | |
function HideAllLayers () | |
{ | |
// Loop through layers | |
for (var i = 0; i < doc.layerSets.length; i++) | |
{ | |
// Turn off visibility | |
doc.layerSets[i].visible = false; | |
} | |
} | |
// SAVE FILE | |
function SaveFile () | |
{ | |
// Cache active document | |
var currentDoc = app.activeDocument; | |
// Create SaveOptions variable | |
tgaSaveOptions = new TargaSaveOptions(); | |
// Check if document has an alpha channel | |
var alphaExists = currentDoc.channels.length > 3; | |
// Alpha Channel is saved if alpha channel exists in document | |
tgaSaveOptions.alphaChannels = alphaExists ? true : false; | |
// 24-bit resolution (if no alpha) or 32-bit resolution (if alpha exists) | |
tgaSaveOptions.resolution = alphaExists ? TargaBitsPerPixels.THIRTYTWO : TargaBitsPerPixels.TWENTYFOUR; | |
// Compression set based on selection options | |
tgaSaveOptions.rleCompression = options.useCompression; | |
// Generate final file path | |
fullSavePath = new File(exportedFilePath + "/" + currentDoc.name + options.exportedFileExtension); | |
// Save the file | |
currentDoc.saveAs(fullSavePath, tgaSaveOptions, true, Extension.LOWERCASE); | |
// Close doc if option is set | |
if (options.closeDocsOnSave) currentDoc.close(SaveOptions.DONOTSAVECHANGES); | |
} | |
// GO! | |
Main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment