The RPG Maker Resource Kit

RMRK RPG Maker Creation => Resources => Graphics => Topic started by: Eclipse0468 on May 03, 2012, 07:56:53 AM

Title: [JS] Charset compilation script for Photoshop CS users
Post by: Eclipse0468 on May 03, 2012, 07:56:53 AM
So I just discovered this board...
This is a script I wrote a few months back.

This is a script for Photoshop CS.  I know it will work with CS3 and above, not sure about below.  Its written in Javascript.

Code: [Select]
// Written by Eclipse

/*

// BEGIN__HARVEST_EXCEPTION_ZSTRING

<javascriptresource>
<name>Create Spritesheet from Charset animation</name>
<category>Game Development</category>
</javascriptresource>

// END__HARVEST_EXCEPTION_ZSTRING

*/

// enable double clicking from the Macintosh Finder or the Windows Explorer
#target photoshop

// debugging code
// debug level: 0-2 (0:disable, 1:break on error, 2:break at beginning)
/*
debugger;
$.level = 1;
*/



////////////////////////////////////////////////////
// script configuration
////////////////////////////////////////////////////
function configObj()
{
    // document configuration
    this.document = {};
    // link of valid document suffixes
    this.document.suffix = {
        "D" : "D",
        "L" : "L",
        "R" : "R",
        "U" : "U",
        "LR": "LR",
        "_" : "-",
    };
   
    // spritesheet configuration
    this.spritesheet = {};
    this.spritesheet.cols = 4;
    this.spritesheet.rows = 4;
    this.spritesheet.colWidth = 32;
    this.spritesheet.rowHeight = 48;
    this.spritesheet.xOffsetFormula = "Math.ceil( (config.spritesheet.colWidth - {WIDTH})/2 )";
    this.spritesheet.yOffsetFormula = "(config.spritesheet.rowHeight - {HEIGHT})";
    this.spritesheet.animOrder = [this.document.suffix["D"], this.document.suffix["L"], this.document.suffix["R"], this.document.suffix["U"]];
    this.spritesheet.solidBackground = false;
   
    // current animation configuration
    this.current = {};
    // detect the name of the current document
    this.current.docName = app.activeDocument.name.replace(".psd", "");
    for (var i in this.document.suffix) {
        this.current.docName = this.current.docName.replace(this.document.suffix[i], "");
    }
   
    // create a list of the relevant animations
    this.current.rootPath = app.activeDocument.path;
    this.current.tempPath = "Z:/tmp";
    this.current.savePath = this.current.rootPath;
    this.current.docPaths = {};
    for (var sufx in this.document.suffix) {
        this.current.docPaths[this.document.suffix[sufx]] = this.current.rootPath+"/"+this.current.docName+this.document.suffix["_"]+this.document.suffix[sufx]+".psd";
    }
    delete this.current.docPaths[this.document.suffix["_"]];
   
    // create a list of the animations that exist
    this.current.validDocs = {};
    for (var doc in this.current.docPaths) {
        var df = File(this.current.docPaths[doc]);
        if (df.exists) {
            this.current.validDocs[doc] = this.current.docPaths[doc];
        }
    }
}



////////////////////////////////////////////////////
// starting dialog window
//      config  :   the config object (passed by reference)
////////////////////////////////////////////////////
function dialogStart(cfg)
{
    // create new window
    var dlg = new Window("dialog", "Generate RMXP Charset from Photoshop animation files", [100, 100, 700, 500]);

    // add Animation Info panel
    dlg.msgPnl = dlg.add("panel", [15, 15, 580, 220], "Animation File Info:");
    // solid bg?
    dlg.msgPnl.aiSolidBgText = dlg.msgPnl.add("StaticText", [20, 20, 300, 60], "Do the animation files have a solid background?");
    dlg.msgPnl.aiSolidBg = dlg.msgPnl.add("CheckBox", [300, 17, 320, 45], cfg.spritesheet.solidBackground);
    dlg.msgPnl.aiSolidBgWarningText1 = dlg.msgPnl.add("StaticText", [340, 20, 550, 40], "If the animation files have a");
    dlg.msgPnl.aiSolidBgWarningText2 = dlg.msgPnl.add("StaticText", [340, 40, 550, 60], "transparent background, the script");
    dlg.msgPnl.aiSolidBgWarningText3 = dlg.msgPnl.add("StaticText", [340, 60, 550, 80], "will take much longer to execute.");
    // detected files
    dlg.msgPnl.detectedFilesTextPnl = dlg.add("panel", [35, 85, 300, 200], "Files Detected:");
    // list detected files
    var y = 0;
    for (var doc in cfg.current.validDocs) {
        dlg.msgPnl.detectedFilesTextPnl.add( "StaticText", [20, y+20, 100, y+70], cfg.current.validDocs[doc].substr(cfg.current.validDocs[doc].lastIndexOf("/")+1) );
        y += 20;
    }
   
    // add New Spritesheet Info panel
    dlg.ssPnl = dlg.add("panel", [15, 240, 580, 340], "New Spritesheet Info:");
    // num spritesheet columns
    dlg.ssPnl.ssColsText1 = dlg.ssPnl.add("StaticText", [20, 22, 120, 40], "Column Width:");
    dlg.ssPnl.ssCols      = dlg.ssPnl.add("EditText", [125, 20, 160, 40], cfg.spritesheet.colWidth);
    dlg.ssPnl.ssColsText2 = dlg.ssPnl.add("StaticText", [165, 22, 180, 40], "px");
    // num spritesheet rows
    dlg.ssPnl.ssRowsText1 = dlg.ssPnl.add("StaticText", [20, 52, 120, 70], "Row Height:");
    dlg.ssPnl.ssRows      = dlg.ssPnl.add("EditText", [125, 50, 160, 70], cfg.spritesheet.rowHeight);
    dlg.ssPnl.ssRowsText2 = dlg.ssPnl.add("StaticText", [165, 52, 180, 40], "px");
   
   
   
    // add accept button...
    dlg.beginBtn = dlg.add("button", [195, 360, 295, 380], "Begin", {name:"ok"});                     
    // add canel button.
    dlg.cancelBtn = dlg.add("button", [315, 360, 415, 380], "Cancel", {name:"ok"});
   
   
   
    // when BEGIN is clicked
    dlg.beginBtn.onClick = function() {
        config.spritesheet.colWidth = parseInt(dlg.ssPnl.ssCols.text);
        config.spritesheet.rowHeight = parseInt(dlg.ssPnl.ssRows.text);
        alert(dlg.msgPnl.aiSolidBg.value);
        config.spritesheet.solidBackground = (dlg.msgPnl.aiSolidBg.value == "true") ? true : false;
       
        /*// insert author into active documentinfo...
        activeDocument.info.title = dlg.msgPnl.msgAuthor.text;                                                                     
        // insert copyright notice into active documentinfo...
        activeDocument.info.copyrightNotice = dlg.msgPnl.msgCpyVermerk.text;*/
       
        this.ena = true;
        this.parent.close(0);
    }
   
    // when CANCEL is clicked
    dlg.cancelBtn.onClick = function() {
        this.ena = false;
        this.parent.close(0);
        //ObjectCraft();
    }
   
    // show dialog window
    dlg.show();
}



////////////////////////////////////////////////////
// clean up a path name
//      pathStr            :   the path
////////////////////////////////////////////////////
function cleanPathStr(pathStr)
{
    var pth = pathStr;
    while (pth != pth.replace("//", "/")) {
        pth = pth.replace("//", "/");
    }
    return pth;
}



////////////////////////////////////////////////////
// close all open documents
//      save_options    :   file save options
////////////////////////////////////////////////////
function closeAllDocuments(save_options)
{
    if (save_options == null) {
        save_options = SaveOptions.PROMPTTOSAVECHANGES;
    }
    while (app.documents.length) {
        app.activeDocument.close(save_options);
    }
}



////////////////////////////////////////////////////
// export image in bitmap format
//      path            :   the path to the new file
//      filename    :   the new files name
////////////////////////////////////////////////////
function exportImageToBMP(path, filename)
{
    try
    {
        var pth = cleanPathStr(path+"/"+filename);
        imgFile = new File(pth);
        var options = new BMPSaveOptions();
        //options.quality = 100;
        options.depth = BMPDepthType.SIXTEEN;
        options.rleCompression = false;
        //options.alphaChannels = true;
        app.activeDocument.saveAs(imgFile, options, true);
    }
    catch (e)
    {   // display error
        alert("Error encountered when saving the image! \r\r" + e);
        return;     // quit
    }
}



////////////////////////////////////////////////////
// export image in PNG format (24-bit)
//      path            :   the path to the new file
//      filename    :   the new files name
////////////////////////////////////////////////////
function saveForWebPNG(path, filename)
{
    var opts, file, p;
    opts = new ExportOptionsSaveForWeb();
    opts.format = SaveDocumentType.PNG;
    opts.PNG8 = false;
    opts.quality = 100;
    if (filename.length > 27) {
        p = cleanPathStr(path+"/temp.png");
        file = new File(p);
        app.activeDocument.exportDocument(file, ExportType.SAVEFORWEB, opts);
        file.rename(filename + ".png");
    }
    else {
        p = cleanPathStr(path+"/"+filename+".png");
        file = new File(p);
        app.activeDocument.exportDocument(file, ExportType.SAVEFORWEB, opts);
    }
}



////////////////////////////////////////////////////
// transform - flip layer
//      type    :   0 = Horizontal, 1 = Vertical
////////////////////////////////////////////////////
function flipLayer(layer, type)
{
    var x_scl, y_scl;
    switch (type) {
        case 0:
            x_scl = -100;
            y_scl = 100;
            break;
        case 1:
            x_scl = 100;
            y_scl = -100;
            break;
    }
    layer.resize(new UnitValue(x_scl,'%'), new UnitValue(y_scl,'%'));
}



////////////////////////////////////////////////////
// transform - flip layer
//      lyr     :   a layer object
//      x       :   x position (pixels)
//      y       :   y position (pixels)
////////////////////////////////////////////////////
function positionLayer(lyr, x, y)
{
     // if can not move layer return
     if(lyr.iisBackgroundLayer || lyr.positionLocked) return
     // get the layer bounds
     var layerBounds = lyr.bounds;
     // get top left position
     var layerX = layerBounds[0].value;
     var layerY = layerBounds[1].value;
     // the difference between where layer needs to be and is now
     var deltaX = x-layerX;
     var deltaY = y-layerY;
     // move the layer into position
     lyr.translate(deltaX, deltaY);
}



////////////////////////////////////////////////////
// calculate spritesheet frame offsets
//      fh    :     file handle
//      cfg   :     the config object
//      o     :     the offset object (p: x,y)
////////////////////////////////////////////////////
function calculateFrameOffsets(fh, cfg, o)
{
    if (!(o instanceof Object)) {
        var o = {};
        o.x = 0;
        o.y = 0;
    }
   
    // if the animations have a solid background,
    // no offset is needed
    if (cfg.spritesheet.solidBackground === true) {
        o.x = 0;
        o.y = 0;
    } else {
    // if the animations have a transparent background,
    // we must scan every pixel of each frame to determine the pixel offset
        var sampler = fh.colorSamplers.add([0, 0]);
       
        framePixelCheck_X:
        for (var px = 0; px < fh.width; px ++) {
            for (var py = 0; py < fh.height; py++) {
               
                sampler.move([UnitValue(px, "px"), UnitValue(py, "px")]);
                var pColor = undefined;
               
                // try to grab pixel data for   [px,py]
                try {
                    pColor = sampler.color;
                // if there is no pixel at  [px,py] , exception will be thrown
                } catch(err) { }
               
                // if there is a pixel at [px,py]
                if (pColor != undefined) {
                    // a pixel exists
                    o.x = px;
                    break framePixelCheck_X;
                } else { // no pixel at [px,py]
                    continue;
                }
               
            }
        }

        framePixelCheck_Y:
        for (var py = 0; py < fh.height; py++) {
            for (var px = 0; px < fh.width; px ++) {
               
                sampler.move([UnitValue(px, "px"), UnitValue(py, "px")]);
                var pColor = undefined;
               
                // try to grab pixel data for   [px,py]
                try {
                    pColor = sampler.color;
                // if there is no pixel at  [px,py] , exception will be thrown
                } catch(err) { }
               
                // if there is a pixel at [px,py]
                if (pColor != undefined) {
                    // a pixel exists
                    o.y = py;
                    break framePixelCheck_Y;
                } else { // no pixel at [px,py]
                    continue;
                }
               
            }
        }
   
    }

}



////////////////////////////////////////////////////
// main method
////////////////////////////////////////////////////
function main()
{
    // initialize config
    var config = new configObj();
   
    // bring up starting dialog
    dialogStart(config);
   
   
   
    app.displayDialogs = DialogModes.NO;
    closeAllDocuments();
   
    // open only relevant animation files
    var doc;
    for (var idx in config.current.validDocs) {
        doc = File(config.current.validDocs[idx]);
        if (doc.exists) {
            var f = open(doc);
        } else {
            delete config.document.suffix[idx];
        }
    }
   
    // save all files as animated gif's
    var animFiles = [];
    for (var i = 0; i < app.documents.length; i++) {
        // keep track of file properties
        app.activeDocument = app.documents[i];
        var fobj = {};
        fobj.filename = app.documents[i].name.replace(".psd", "");
        fobj.file_ext = ".gif";
        fobj.path = config.current.tempPath+"/"+fobj.filename+fobj.file_ext;
        animFiles.push(fobj);
       
        // delete the file if it already exists
        var aFile = File(fobj.path);
        aFile.remove();
       
        // save for web options
        var options = new ExportOptionsSaveForWeb();
        options.ditherAmount = 0;
        options.dither = Dither.NOISE;
        options.palette = Palette.LOCALADAPTIVE;
        options.format = SaveDocumentType.COMPUSERVEGIF;
        /*options.format = SaveDocumentType.COMPUSERVEGIF;
        options.colors = 256;
        options.dither = Dither.NONE;
        options.ditherAmount = 100;
        options.palette = Palette.LOCALSELECTIVE;
        options.interlaced = true;
        options.lossy = 0;*/

        // export the image
        app.activeDocument.exportDocument(aFile, ExportType.SAVEFORWEB, options);
    }
   
    closeAllDocuments(SaveOptions.DONOTSAVECHANGES);
   
    // open only relevant animation files (open saved animated gifs)
    var animFrameFiles = [];
    for (var i = 0; i < animFiles.length; i++) {
        // for each animation file
        var cur_file = open(File(animFiles[i].path));
        app.activeDocument = cur_file;
       
        // hide all layers first
        var layers = app.activeDocument.layers;
        for (var j = 0; j < layers.length; j++) {
            try {
                layers[j].visible = false;
            } catch(err) { }
        }
        // export each layer as a seperate image
        for (var j = layers.length-1; j >= 0; j--) {
            var flipAndRepeat = false;
            app.activeDocument.activeLayer = layers[j];
            try {
                app.activeDocument.activeLayer.visible = true;
               
                // face direction
                var basefn = animFiles[i].filename.substring(0, animFiles[i].filename.indexOf(config.document.suffix["_"]));
                var face_dir = animFiles[i].filename.substring(animFiles[i].filename.indexOf(config.document.suffix["_"])+1);
                var aframe = (layers.length-1) - j;
               
                // if the animation file is Left/Right, save Left facing frame first
                if (face_dir == config.document.suffix["LR"]) {
                    flipAndRepeat = true;
                    face_dir = config.document.suffix["L"];
                }
               
                // save frame as PNG
                var export_fname = basefn + config.document.suffix["_"] + face_dir + aframe;
                saveForWebPNG(config.current.tempPath, export_fname);
               
                // keep track of saved images
                if (!(animFrameFiles[face_dir] instanceof Array)) {
                    animFrameFiles[face_dir] = [];
                }
                animFrameFiles[face_dir][aframe] = config.current.tempPath+"/"+export_fname+".png";
               
                // if the animation file is Left/Right, save Right facing frame
                if (flipAndRepeat === true) {
                    flipLayer(app.activeDocument.activeLayer, 0);
                    var r_fname = basefn + config.document.suffix["_"] + config.document.suffix["R"] + aframe;
                    saveForWebPNG(config.current.tempPath, r_fname);
                    if (!(animFrameFiles[config.document.suffix["R"]] instanceof Array)) {
                        animFrameFiles[config.document.suffix["R"]] = [];
                    }
                    animFrameFiles[config.document.suffix["R"]][aframe] = config.current.tempPath+"/"+r_fname+".png";
                }
               
                app.activeDocument.activeLayer.visible = false;
            } catch(err) { }
        }
       
        cur_file.close(SaveOptions.DONOTSAVECHANGES);
       
        // export visible layers to seperate files
        //$.evalFile("C:/Program Files/Adobe/Adobe Photoshop CS5 (64 Bit)/Presets/Scripts/Export Layers To Files.jsx");
    }
   
    // create a new document and insert the animation frames
    // create document
    var charsetWidth = config.spritesheet.cols * config.spritesheet.colWidth;
    var charsetHeight = config.spritesheet.rows * config.spritesheet.rowHeight;
    var charsetRef = app.documents.add(charsetWidth, charsetHeight, 300, "newCharset", NewDocumentMode.RGB, DocumentFill.TRANSPARENT);
   
    // loop through animation frames
    for (var i in config.spritesheet.animOrder) {
        var wDirFrames = animFrameFiles[config.spritesheet.animOrder[i]];
        for (var j in wDirFrames) {
            // open frame
            var aFile = open(File(wDirFrames[j]));
            // duplicate layer into the new document
            aFile.layers[0].duplicate(charsetRef);
           
            // calculate frame offsets
            var offset = {};
            calculateFrameOffsets(aFile, config, offset);
           
            // position the new layer properly
            var new_x = j * config.spritesheet.colWidth;
            var new_y = i * config.spritesheet.rowHeight;
            //offset.x = 0;//eval(config.spritesheet.xOffsetFormula.replace("{WIDTH}", aFile.width));
            //offset.y = 0;//eval(config.spritesheet.yOffsetFormula.replace("{HEIGHT}", aFile.height));
            app.activeDocument = charsetRef;
            positionLayer(charsetRef.layers[0], new_x+offset.x, new_y+offset.y);
           
            // close frame
            aFile.close(SaveOptions.DONOTSAVECHANGES);
        }
    }
   
    // save new charset as PNG
    saveForWebPNG(config.current.savePath, config.current.docName);
   
    // cleanup - remove temp files
    for (var i = 0; i < animFiles.length; i++) {
        var f = File(animFiles[i].path);
        f.remove();
    }
    for (var i in config.spritesheet.animOrder) {
        for (var j in animFrameFiles[config.spritesheet.animOrder[i]]) {
            var f = File(animFrameFiles[config.spritesheet.animOrder[i]][j]);
            f.remove();
        }
    }
   
   
   
    // end main method
}










// execute script
main();

To install, place the code in a file with the extension .jsx in your Photoshop scripts directory.
The default should be something like: C:\Program Files\Adobe\Adobe Photoshop CS5\Presets\Scripts



When I do graphic design, I like working at the pixel level, especially when I'm doing custom charsets.  I've always wanted a way to easily make changes at the pixel level to my charsets if something doesn't look right, while keeping each frame separate with layers (for different elements of the character, like jacket, hair, eyes, etc), AND being able to "play" the animation and watch it.  So I started using Photoshop animations.

I have separate files for each walking direction, and I can zoom in as close as I want make sure the animation looks perfect.  I also have each frame broken down into separate components.  Excuse the obnoxious watermarks on the pics, I don't want anybody heisting this character just yet.

(http://img638.imageshack.us/img638/6632/ssm1j.png)

Here are the files I have for this character.  They correspond to the Down, Left/Right and Up walking animations:

(http://img811.imageshack.us/img811/5928/ssm3.png)

Basically the naming convention is as follows: CHARNAME-DIRECTION_LETTER.psd
CHARNAME can be anything
DIRECTION_LETTER can be U, D, L, R, or LR.  If the L and R animations are supposed to be very different, have them in separate files.  Otherwise, use LR as a suffix and the R animation will be assumed to be a mirror image of L.

Keep in mind this is just my naming convention, and the default for the script.  You can change the highlighted part of the script if you want:

(http://img804.imageshack.us/img804/2305/ssm2.png)



So what you do, is have all your properly named PSD files in a directory, and then open one of them and run the script.  It will show up in a separate category as "Create Spritesheet from Charset animation" under File -> Scripts in PS.

(http://img140.imageshack.us/img140/9554/ssm4.png)

This window will appear!  It will show you the other animation files it detected for the current one you have open.  You can change the width/height of a cell if your charset uses a non-standard width/height.

Pay attention to the checkbox at the top.  If your animation has a transparent background, the script will take a couple minutes to run.  This is because photoshop has no way of copying empty space from one document to another, so the script must scan each frame of the image to determine how many pixels the sprite must be offset in its cell.  Unfortunately I don't think there's a way around this.

However if your animation has a solid color background, the script will finish in seconds because every frame is the same width and height.

After your done setting the options click Begin!  After some seconds/minutes, the compiled spritesheet will appear!
Ignore the "X" frames, those are not completed, my animation file contains those exact frames.

(http://img820.imageshack.us/img820/1441/ssm5.png)

It will also be saved as a 24-bit PNG in the same directory as the other animation files by default.

(http://img825.imageshack.us/img825/18/ssm6.png)

You can change the save directory in the configuration by changing the value of this.current.savePath.  You can put your projects charset directory there and the compiled spritesheet will be automatically exported and ready to use in RMXP.  Just make sure Photoshop can write to that directory.



Anyway I've put a decent amount of time into this script, so hopefully other Photoshop CS users will find this useful.  Enjoy!
Title: Re: [JS] Charset compilation script for Photoshop CS users
Post by: Lethrface on May 03, 2012, 02:29:29 PM
I'm not even sure exactly what this does.  I read all of that and I think I have an idea but I'm still slightly confused.  Does it just compile from files or does it add something to the pixel level that photoshop doesn't do because you spent a lot of time talking about how you like to edit your sprites but not much about what the script itself does so now I'm kind of confused slightly lol.

Oh and just thought i'd let ya know, nobody who is going to take your character.  It's not an insult, it's just a fact because if anyone wanted to take your character, they would have to actually edit the image and whatnot, finish the extra portions that aren't finished and whatnot...and people are just too lazy to do that when they go out to hijack other people's materials lol.  They want it 100% complete or have minimal effort so I think your crazy watermarks are kind of unnecessary XD
Title: Re: [JS] Charset compilation script for Photoshop CS users
Post by: Eclipse0468 on May 03, 2012, 11:54:56 PM
I started creating charsets by splitting them up into 4 files, one for each facing direction (Up, Right, Down, Left), and animating them directly in Photoshop.  This is so I can work on each animation individually at the pixel level, and so I don't have to import the whole charset into RMXP to see what the walking animation looks like.  Since every aspect of the sprite can be in separate layers in PS you can easily apply effects/changes to the whole animation, instead of having to (example) change the hair style in each frame individually.

Anyway, this script takes all 4 animation files like that and compiles them into a full charset.

And about the watermarks: yeah I know :P but whatever, you can call me paranoid 8)
Title: Re: [JS] Charset compilation script for Photoshop CS users
Post by: Lethrface on May 04, 2012, 05:57:36 AM
I started creating charsets by splitting them up into 4 files, one for each facing direction (Up, Right, Down, Left), and animating them directly in Photoshop.  This is so I can work on each animation individually at the pixel level, and so I don't have to import the whole charset into RMXP to see what the walking animation looks like.  Since every aspect of the sprite can be in separate layers in PS you can easily apply effects/changes to the whole animation, instead of having to (example) change the hair style in each frame individually.

Anyway, this script takes all 4 animation files like that and compiles them into a full charset.

And about the watermarks: yeah I know :P but whatever, you can call me paranoid 8)

lol Fair enough.  It sounds fairly useful, especially since it can be a pain in the ass to save the file, load up rpg maker, set the player graphic to the character and load the game up every time you make a change to the graphic.  I might take a look sometime when I load up photoshop since I've just been using gimp lately.  Sadly, there are things I used a lot in photoshop that I miss a lot (like organizing layers into folders) so I'll probably be switching back soon.