記事の目次

    クリーク・アンド・リバー社の社内CGスタジオであり、ゲームの3DCGグラフィックス制作を中心に手がけているCOYOTE 3DCG STUDIO。本連載では、同社のTAチームによる制作に役立つ技術TIPSを紹介していく。

    TEXT_中林伸和 / Nobukazu Nakabayashi(COYOTE 3DCG STUDIO)
    EDIT_小村仁美 / Hitomi Komura(CGWORLD)

    はじめに

    戦国三英傑の豊臣秀吉の好きなところは天下人に似合わない気さくなイメージ。TAは人と話すことも重要なのでうらやましいスキルです。こんにちは、COYOTE 3DCG STUDIO テクニカルアーティスト(以下、TA)戦国大好き人間の中林です。

    前回から、当社ブログにも掲載したPhotoshop内製ツール紹介『CRPS_指定ファイル保存』の制作を例として、Photoshopのツール制作に関するTipsを3回に分けてお届けしています。今回はその第2回です。

    1:オープンソースエディタBracketsを使ってHTMLでUI作成(第4回
    2:スクリプトで処理の内容を作ろう(今回)
    3:認証の自動化の紹介

    制作事例として紹介するツール「CRPS_指定ファイル保存」は、「複製→レイヤーを統合→画像解像度の変更→フォルダ選択→保存→複製削除」を1クリックで行うというシンプルなものです 。

    • ◀「CRPS_指定ファイル保存」UI

    前回は外側のUIをつくる方法を紹介しましたが、今回は中身の処理について説明していきたいと思います。

    基本的な処理はJavaScriptとJSXに分かれる

    中身の処理に関しては、主にUIを操作するJavaScriptと実際にPhotoshop内を操作するJSXに分かれます。JavaScriptは聞き馴染みのあるスクリプト言語ですが、一方JSXは「(古い)JavaScript+Adobe独自のコマンド」を組み合わせたスクリプトです。

    ちなみにJSXに関しては困ったことに、ネット検索すると主に以下の3つが出てきます。

    1:Reactで用いられるJavaScriptの拡張構文
    2:Adobe製品に搭載されているJavaScriptマクロ
    3:DeNAで開発されたWebアプリケーション向けのプログラミング言語

    ......みんなJavaScriptの進化系にXを付けるのが好きなんだなぁ。おかげで検索する方は大変です。知りたい情報は、2番目の「Adobe製」のものなので、ネット検索する場合は「JSX -React -altJS」などと、マイナス表記で1番目や3番目の情報を省くことをオススメします。この記事では後にも先にもJSXはAdobeのスクリプトのことを指します。

    ところで、JSXでスクリプトを始めて、さらにネット検索すると「ScriptListener」を使おう、という情報が多く出てきますが、個人的にはあまりオススメできません。これを使うとMayaでいうスクリプトエディタのMELのようなコマンドの情報を期待しますが、出てくるコマンドはアクション用にガチガチに変換された情報で、参考にならないのです。ちゃんとJSXのコマンドを調べ、自分で組み合わせて作成するのが一番です。

    JSXのコマンドのリファレンスについては、こちらの日本語サイトがオススメです。独自で調べたコマンドを日本語で解説してくれています。

    ●中綴製作所
    http://nakatoji.lolipop.jp/index.php/extendscript
    ●Photoshop JavaScript Reference
    http://www.openspc2.org/reibun/Photoshop/ref/

    また、英語ではありますがJSXの公式のコマンドリファレンスもあります。多少、翻訳の手間はありますが多くのコマンドを知ることができます。

    ●Adobe Photoshop Scripting
    https://www.adobe.com/devnet/photoshop/scripting.html

    Step 01:JavaScriptでUIの操作

    ちなみにJavaScriptは複数の書き方がありますが、今回は、前回紹介したBracketsエディタの例を参考につくりました。JavaScriptのメインのファイルはBracketsのjs/main.jsになります。

    【Hello World】を参考にすると、「function init()」内に以下の命令があります。

    ●index.html

    html
    	<button id="btn_test" class="topcoat-button--large hostFontSize">Hello World</button>
    

    ●main.js

    javascript
    	$("#btn_test").click(function () {
    		csInterface.evalScript('sayHello()');
    	});
    

    これは、htmlと同じ名前のボタンをクリックしたときに処理をするというながれになっています。今回作成する「CRPS_指定ファイル保存」では主に以下の3つの作業を行なっています。

    1:UIの変更情報の保存
    2:起動時のUIへの情報反映
    3:JSXに変数を渡して実行

    1-1:UIの変更情報の保存

    例えばpngで保存する場合、画像1枚保存して終わりではなく、ある程度の回数はpngで作業をすると思います。しかし、ツールが起ち上がるたびに初期化されると再設定するのにストレスが溜まります。

    そこで、画像形式を変更したタイミングで、localStorageにその情報を保存するようにします。localStorageとは、JavaScriptでデータをブラウザに保存するしくみです。

    下記はチェックボックスを変更したときに保存を行う例です。

    ●index.html

    html
    	<input type="checkbox" id="COYOTE_chkPng_CBox" class="checkbox-input">
     <span class="checkbox-parts" label="png" ></span>
    

    ●main.js

    javascript
    	$("input#COYOTE_chkPng_CBox").change(function() {
    		var tmp = document.getElementById("COYOTE_chkPng_CBox").checked;
    		localStorage.setItem("COYOTE_chkPng_check", tmp);
    	});
    

    これは一例ですが、button以外の全てのメニューが保存対象で、変更するたびに各localStorageに情報を保存しています。

    1-2:起動時のUIへの情報反映

    基本的には1-1で保存した情報をツール起動時に反映するだけです。若干面倒なのは、localStorageは文字列に変換されてしまうので、そのままでは渡せないケースがあることです。その場合は、文字列をif文で比較し、内容に沿って情報を渡すようにします。

    ●index.html

    html
    	<input type="checkbox" id="COYOTE_chkPng_CBox" class="checkbox-input">
     <span class="checkbox-parts" label="png" ></span>
    

    ●main.js

    javascript
     var tmp = localStorage.getItem("COYOTE_chkPng_check");
     if(tmp == "true"){
      document.getElementById("COYOTE_chkPng_CBox").checked = true;
    	});
    

    ツールの設定は、ツール側が記憶できるようにしないと地味にユーザーが面倒です。作る方も面倒だという意見もありますが、作る方の手間は1度だけです。しかし、ユーザーの手間は使い続けるたびに何度も発生します。筆者は1度の手間と複数回の手間を天秤にかけて、ツールをつくるときは絶対にUI情報を記憶させるようにしています。

    1-3:JSXに変数を渡して実行

    ボタンをクリックしたときに、JavaScriptの処理からcsInterface.evalScript(JSXの関数)でJSXの関数を実行します。ただし、JSXから直接HTMLの情報を取得することはできないので、JavaScriptの時点で必要な変数情報を集めておきます。

    ●index.html

    html
    	<button id="COYOTE_duplicate_btn" class="topcoat-button--large hostFontSize">選択画像の複製保存</button>
    

    ●main.js

    javascript
    $("#COYOTE_duplicate_btn").click(function () {
    	//
    	// 保存するイメージの形式をチェックボックスから取得(一部省いてます)
    	var imageList = [];
    	if(document.getElementById("COYOTE_chkTga_CBox").checked){
    		imageList.push('tga');
    	}
    	if(document.getElementById("COYOTE_chkPng_CBox").checked){
    		imageList.push('png');
    	}// テクスチャの変更したいサイズ
    	var sizeX = document.getElementById("COYOTE_sizeX_id").value;
    	var sizeY = document.getElementById("COYOTE_sizeY_id").value;
    	// JSXに変数を渡して実行
    	csInterface.evalScript('COYOTE_saveDuplicateimage("' + imageList + '","' + sizeX + '","' + sizeY + '")');
    });
    

    ここではlist形式で変数を渡していますが、JSX上ではカンマ区切りの数列も文字列になってしまいます。なので、改めてJSX側で分割する必要があります。

    ●〜.jsx

    javascript
    function COYOTE_saveDuplicateimage(imageList, sizeX, sizeY){
    	imageList = imageList.split(","); // 文字列を","で区切ってリスト化
    	// 以下処理が続く......
    }
    

    1-3':JSXの変数を戻す

    JSXからJavaScriptに変数を戻したい場合もあると思います。その場合の例として、JSXから画像の横幅を取得してJavaScriptで利用するケースを紹介します。

    ●〜.jsx

    javascript
    function getDocumentSizeX(){
    	return parseFloat(activeDocument.width);
    }
    

    ●index.html

    html
    	<input type="text" id="COYOTE_sizaX_id" value="2048" onKeyup="this.value=this.value.replace(/[^0-9]+/i,'')" size="3" maxlength="4">
    

    ●main.js

    javascript
    csInterface.evalScript('getDocumentSizeX()',function(cb){
    	// メニューの
    	document.getElementById("COYOTE_sizaX_id").value = cb;
    });
    

    これはfunctionで戻り値を変数で受け取って利用しています。さすがに画像サイズの取得はJSXでないとできないので、戻り値を利用します。

    次ページ:
    Step 02:JSXでの処理の紹介

    [[SplitPage]]

    Step 02:JSXでの処理の紹介

    JSXの処理を全部載せると長くなるので、主な処理のながれと使用したコマンドを紹介します。

    処理のながれは以下の通りです。
    「複製→レイヤを統合→画像解像度の変更→フォルダ選択→保存→複製削除」

    ちなみにJSXのメインファイルはBracketsのjsx/hostscript.jsxになります。と言いたいところですが......manifest.xmlの41行目付近の以下の記述を変えれば好きな名前に変えることができます。

    ●manifest.xml

    html
    <ScriptPath>./jsx/hostscript.jsx</ScriptPath>
    

    全てhostscript.jsxのままだと、複数のエクステンションの作業時にどのデータを見ていたのか混乱するので、筆者は処理に合わせてファイル名を変えています。今回のツールですと「COYOTE_duplicateFile.jsx」という感じです。

    2-1:複製

    ツールの実装中は、基本的に複製したデータで作業するようにしています。そうすることで、元データに影響なく続きの作業ができるからです。実装中にテストで処理を中断したときや失敗したときに、元データが独立して残っていると便利です。少なくとも筆者は実装中に何回も失敗したので、この方法は役に立っています。

    ●〜.jsx

    javascript
    	activeDocument.duplicate();
    

    意外と単純に複製できますね。複製した段階で「activeDocument」が複製したファイルに切り替わるので、後の作業はそちらを利用しています。

    2-2:レイヤーを統合

    表示されているレイヤーを統合します。地味ですがグループレイヤー選択時に実行すると不具合が多かったので、画像レイヤーを追加してからレイヤーを結合することで処理を安定させています。

    ●〜.jsx

    javascript
    	activeDocument.artLayers.add();
    	activeDocument.mergeVisibleLayers();
    

    2-3:画像解像度の変更

    こちらはいわゆる画像解像度の変更です。サイズは仮で512にしてますが、実際の処理ではUIで入手した情報を利用しています。例では再サンプルのアルゴリズムはバイキュービック(自動)ですが、Option次第で変更可能です。

    ●〜.jsx

    javascript
    	var iSizeX = 512
    	var iSizeY = 512
    	activeDocument.resizeImage(parseInt(iSizeX ), parseInt(iSizeY), 72, ResampleMethod.BICUBICAUTOMATIC);
    

    2-4:フォルダ選択

    今回は1つ上のフォルダを取得する方法も書いておきます。1つ上のフォルダ取得はアーティストからの要望でしたが、COYOTEスタジオでは今も重宝している機能です。

    ●〜.jsx

    javascript
    	var folder = Folder(activeDocument.fullName.parent).fsName
    	var lastInt = folder.lastIndexOf("\\");
    	folder = folder .substring(0, lastInt);
    

    2-5:保存

    保存のスクリプトは形式によってちがいます。ここでは、一例としてpng形式の保存方法を挙げます。フォルダ名に関しては上記のfolder変数を利用しています。

    ●〜.jsx

    javascript
    	var tmp = activeDocument.fullName.name;
    	var filename = decodeURI(tmp.substring(0, tmp.indexOf(".")));
    	var saveFile = new File(folder + "/" + filename + ".png");
    	var pngSaveOptions = new PNGSaveOptions();
    	var pngSaveOptions = new PNGSaveOptions();
    	pngSaveOptions.interlaced = false; //インターレース無効
    	activeDocument.saveAs(saveFile, pngSaveOptions, true, Extension.LOWERCASE);
    

    2-6:複製データの削除

    保存が完了すれば複製データは必要なくなるので、削除します。

    ●〜.jsx

    javascript
    	activeDocument.close(SaveOptions.DONOTSAVECHANGES);
    

    実際のソースは画像サイズの取得などもう少しいろいろな処理をしていますが、重要な部分をまとめるとこんな感じです。

    Photoshopのスクリプトの未来について

    筆者個人の意見ですが、今のままではPhotoshopのスクリプトの未来は明るくないように感じています。その理由はJSXの停滞です。

    実際「Photoshop JavaScript Reference」のCC 2014と2020のpdfを比較するとわずか2ページ分の情報だけしか変化しておらず、Photoshop本体の6年分の進化から比べると圧倒的に少ないです。

    では、スクリプトとして充分足りているかというとそんなことはありません。先日も画像上のマウスカーソル位置の座標をスクリプトから取得できないかとあちこち調べましたが(海外フォーラム含む)結局方法は見つからずじまいです。こんな簡単なスクリプトのコマンドすら存在が不確かなのです。

    今、停滞しているPhotoshopのスクリプトの未来を明るくするためには、利用する人が増えて、情報が多くなり、Adobeにスクリプトの強化を感じさせる必要があると思います。もしもこの記事を読んで興味をもっていただけたならば、ぜひPhotoshopでツールを作ってみてほしいなと思います。

    おまけ:筆者が「ScriptListener」を推奨しない理由

    ページに余裕があるので、改めて「ScriptListener」を推奨しない理由を書きます。理由は単純で、コマンドと比較して出てくる「ScriptListener」情報が複雑でわかりづらいからです。

    具体的な導入方法の解説については割愛しますが、スクリプトがあるとこんな感じにメニューに追加されます。

    では、例としてごく単純な「複製」という操作をコマンドと情報で比較したいと思います。

    ●コマンド

    javascript
     activeDocument.duplicate();
    

    コマンドなのでUIを出す必要もなく、1行で済みます。ちなみに、コマンドもネット検索で「JSX 複製 -React -altJS」で調べれば簡単に情報が出てきます。リファレンスを調べれば名前を決めて複製も簡単にできます。

    「ScriptListener」を使うとScriptingListenerJS.logとScriptingListenerVB.logの2ファイルが出力されます。今回はJSXなのでScriptingListenerJS.logの方から、複製の部分だけを抜き出したものを見てみると......。

    ●ScriptingListenerJS.logの情報

    javascript
    // =======================================================
    var idmodalStateChanged = stringIDToTypeID( "modalStateChanged" );
    	var desc507 = new ActionDescriptor();
    	var idLvl = charIDToTypeID( "Lvl " );
    	desc507.putInteger( idLvl, 0 );
    	var idStte = charIDToTypeID( "Stte" );
    	var idStte = charIDToTypeID( "Stte" );
    	var idexit = stringIDToTypeID( "exit" );
    	desc507.putEnumerated( idStte, idStte, idexit );
    	var idkcanDispatchWhileModal = stringIDToTypeID( "kcanDispatchWhileModal" );
    	desc507.putBoolean( idkcanDispatchWhileModal, true );
    	var idTtl = charIDToTypeID( "Ttl " );
    	desc507.putString( idTtl, """Duplicate Image""" );
    executeAction( idmodalStateChanged, desc507, DialogModes.NO );
    

    ......やたらと長い。
    試しに上記の内容だけの.jsxファイルを用意して実行したところ複製ができました。ちなみに上記に加えてさらに前後に似たような記述が沢山あり、その中から複製の部分を探すだけでも大変です。正直、おまけを書くのも挫折しかけました。

    しかし、出力されたログをコピペして実装するだけで、今までのオペレーションを簡単にツール化できてしまう点は、なかなか優秀だと思います。条件分岐や繰り返し処理、または簡単なPhotoshopスクリプトなどと組み合わせて使えば、アクションスクリプトでは難しいオペレーションでも簡単にツール化することが可能です。

    「ツール化しようと思ったけど、どういう処理にすべきかわからない」部分をひとまずScriptListenerで対応しておいて、あとできちんと書き直す、などといった使い方もできるでしょう。

    ただ、その内容については非常に読み解きにくいので、Photoshopスクリプトの習熟を目指す方は、「ScriptListener」の情報だけを鵜呑みにせず、惑わされないようにしてくださいね!

    今回で外側も中身もできるので、デバッグモードであればチーム内などに配信することは可能です。しかし、外部で使用する場合に容易にデバッグモードにしてくださいとは言えません。そんなときのためのデバッグモードなしでの配信の方法について、次回「認証の自動化の紹介」で紹介したいと思います。

    ではまた次回。今宵はここまでに致しとうござりまする。



    Profile.

    • 中林伸和 / Nobukazu Nakabayashi(COYOTE 3DCG STUDIO

      ゲーム開発会社にてマップデザイン、レベルデザイン、エフェクト作成など様々な経験をする中でMELと出会い、DCCツール作成の面白さに目覚めてテクニカルアーティストに転身。現在は株式会社クリーク・アンド・リバー社 COYOTE 3DCG STUDIOにて、Mayaに限らずPhotoshopやMotionBulderのグラフィックソフト、Unityなどのゲームエンジンと言語にとらわれずユーザーフレンドリーのツール開発に従事