ScreenPocket - 画面の隙間

Unityエンジニアの日々の雑記。たまにpython3、DirectX、PhotoshopScript(JavaScript)も触ります

macにJenkinsをパッケージインストーラーからインストールし、初回起動時に「Offlineに見えます」と言われたときの解決方法

具体的なタイトル。Jenkinsのインストール後の流れの一部で躓いたのでブログに残しておきます。
Jenkinsのインストール

パスワードの入力を求められたときは、ターミナルで

sudo cat /Users/Shared/Jenkins/Home/secrets/initialAdminPassword


で、その後

This Jenkins instance appears to be offline

と言われてしまった時の対応。

stackoverflow.com
この記事にある通り、

/Users/Shared/Jenkins/Home/hudson.model.UpdateCenter.xml

のファイル内の https を http に書き換えて、Jenkinsを停止→起動とすることで解決できます。

停止、起動については下記の記事
qiita.com
に倣って

停止

sudo launchctl unload /Library/LaunchDaemons/org.jenkins-ci.plist

起動

sudo launchctl load /Library/LaunchDaemons/org.jenkins-ci.plist

とすることで停止→起動が可能になります。

ようやく家の環境に自動でPluginをインストールできました;

SmartObjectレイヤーの名前と、配置されたSmartObjectの4頂点の座標を一括出力するスクリプト

一晩寝て、ちょっと調べたら解決策があったので、もう一つ記事投稿しておきます

//SmartObjectの4頂点の座標を返す
function GetSmartObjectCorner()
{  
	try
	{  
		var r = new ActionReference();  
		r.putEnumerated( charIDToTypeID( "Lyr " ), charIDToTypeID( "Ordn" ), charIDToTypeID( "Trgt" ) );  
		var d;  
		try
		{
			d = executeActionGet(r);
		}
		catch (e)
		{
			alert(e);
			return;
		}  
	
		try
		{
			d = d.getObjectValue(stringIDToTypeID("smartObjectMore"));
		}
		catch (e)
		{
			alert(e);
			return;
		}
	
		try
		{
			d = d.getList(stringIDToTypeID("nonAffineTransform"));
		}
		catch (e)
		{
			alert(e);
			return;
		}
	
		var ret = [[d.getDouble(0),d.getDouble(1)],  
			[d.getDouble(2),d.getDouble(3)],  
			[d.getDouble(4),d.getDouble(5)],  
			[d.getDouble(6),d.getDouble(7)]];  
		return ret;
	}  
	catch (e)
	{
		alert(e);
	}  
}


//レイヤーを探す
//LayerSetだったら再帰で探す
function FindLayer( rootLayer, condition )
{
	var artLayers = [];
	var layers = rootLayer.layers;
	var length = layers.length;
	for ( var i = 0 ; i < length ; ++i )
	{
		var layer = layers[i];
		if ( layer.typename == "ArtLayer" )
		{
			if ( condition( layer ) )
			{
				artLayers.push(layer);
			}
		}
		else if ( layer.typename == "LayerSet" )
		{
			artLayers = artLayers.concat( FindLayer(layer, condition) );
		}
	}
	return artLayers;
}

//全てのArtLayerを取得したいときのCondition
function FindAlways( artLayer )
{
	return true;
}

//全てのArtLayerを取得したいときのCondition
function FindSmartObject( artLayer )
{
	return artLayer.kind == LayerKind.SMARTOBJECT;
}

//文字列(** px)となっているboundsをparseしてIntで返す
function GetIntBounds( artLayer )
{
	var intBounds = [];
	intBounds.push( parseInt(artLayer.bounds[0]) );
	intBounds.push( parseInt(artLayer.bounds[1]) );
	intBounds.push( parseInt(artLayer.bounds[2]) );
	intBounds.push( parseInt(artLayer.bounds[3]) );
	return intBounds;
}

function WriteLineSmartObjectCorner( f, artLayer )
{
	app.activeDocument.activeLayer = artLayer;
	f.write(artLayer.name+",");
	
	var corner = GetSmartObjectCorner();
	f.write(corner[0]+","+corner[1]+","+corner[2]+","+corner[3]);
	f.write("\n");
}

//処理メイン
function Main()
{	
	var activeDocument = app.activeDocument;
	if ( activeDocument == null )
	{
		return;
	}

	//初期設定
	var keepActiveLayer = activeDocument.activeLayer;
	var keepUnit = preferences.rulerUnits;
	preferences.rulerUnits = Units.PIXELS;//pixelユニットとする

	//ドキュメント名
	var documentName = activeDocument.name;

	var findLayers = FindLayer(activeDocument, FindSmartObject);
	var length = findLayers.length;
	if ( length == 0 )
	{
		preferences.rulerUnits = keepUnit;
		activeDocument.activeLayer = keepActiveLayer;
		alert("目的のレイヤーを発見できませんでした。");
		return;
	}

	var txtFile = File.openDialog("矩形情報Textファイルを出力します", "*.txt");
	if ( txtFile == null )
	{
		preferences.rulerUnits = keepUnit;
		activeDocument.activeLayer = keepActiveLayer;
		return;
	}

	var f = new File(txtFile);
	f.open('w');
	for ( var i = 0 ; i < length ; ++i )
	{
		WriteLineSmartObjectCorner(f, findLayers[i]);
	}
	f.close();
	
	//戻す
	preferences.rulerUnits = keepUnit;
	activeDocument.activeLayer = keepActiveLayer;
	
	//完了ダイアログ表示
	alert("出力が完了しました");
}

Main();

SmartObjectのScaleにも対応されているようなので、とりあえずこれで目的は達成されました。
これでアーティストに座標を聞きに行く手間が解消されるかな。

↓参考サイト
forums.adobe.com

SmartObjectレイヤーの名前を出力するPhotoshopスクリプト

ちょっと必要に迫られたので、スマートオブジェクトレイヤーの一覧をtxtファイルに一括出力するスクリプトを用意しました。

//レイヤーを探す
//LayerSetだったら再帰で探す
function FindLayer( rootLayer, condition )
{
	var artLayers = [];
	var layers = rootLayer.layers;
	var length = layers.length;
	for ( var i = 0 ; i < length ; ++i )
	{
		var layer = layers[i];
		if ( layer.typename == "ArtLayer" )
		{
			if ( condition( layer ) )
			{
				artLayers.push(layer);
			}
		}
		else if ( layer.typename == "LayerSet" )
		{
			artLayers = artLayers.concat( FindLayer(layer, condition) );
		}
	}
	return artLayers;
}

//SmartObjectのArtLayerを取得したいときのCondition
function FindSmartObject( artLayer )
{
	return artLayer.kind == LayerKind.SMARTOBJECT;
}

function WriteLine( f, artLayer )
{
	f.write(artLayer.name);
//情報を足したいときはここに
	f.write("\n");
}

//処理メイン
function Main()
{	
	var activeDocument = app.activeDocument;
	if ( activeDocument == null )
	{
		return;
	}

	//初期設定
	keepUnit = preferences.rulerUnits;
	preferences.rulerUnits = Units.PIXELS;//pixelユニットとする

	//ドキュメント名
	var documentName = activeDocument.name;

	var findLayers = FindLayer(activeDocument, FindSmartObject);
	var length = findLayers.length;
	if ( length == 0 )
	{
		preferences.rulerUnits = keepUnit;
		alert("目的のレイヤーを発見できませんでした。");
		return;
	}

	var txtFile = File.openDialog("矩形情報Textファイルを出力します", "*.txt");
	var f = new File(txtFile);
	f.open('w');
	for ( var i = 0 ; i < length ; ++i )
	{
		WriteLine(f, findLayers[i]);
	}
	f.close();
	
	//戻す
	preferences.rulerUnits = keepUnit;
	
	//完了ダイアログ表示
	alert("出力が完了しました");
}

Main();

本当はスマートオブジェクトのTransformも出力したかったけれどもAMCodeを呼ばなければいけないようなので、後ほどの宿題ということで。

SceneViewのカメラ位置を、特定のカメラ位置と合わせるコンポーネント

SceneViewのOverDrawやMipMapを確認する時に、結局現在のGameViewカメラで見た時の具合を確認したい時があります。
なので、特定のカメラの位置情報をSceneViewカメラに渡すためのコンポーネントを書いてみました。

#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;

namespace ScreenPocket
{
	/// <summary>
	/// カメラの位置をSceneViewのカメラに流し込む
	/// </summary>
	[DisallowMultipleComponent]
	[RequireComponent(typeof(Camera))]
	public class SyncSceneViewCamera : MonoBehaviour
	{
#if UNITY_EDITOR
		public bool isSync = false;

		void OnGUI()
		{
			if (!isSync)
			{
				return;
			}

			var view = SceneView.lastActiveSceneView;
			if ( view == null )
			{
				return;
			}

			var myTransform = transform;
			var forward = myTransform.forward;
			var myCamera = GetComponent<Camera>();

			view.pivot = myTransform.position + forward * myCamera.farClipPlane;
			view.rotation = Quaternion.LookRotation(forward);
			view.size = myCamera.farClipPlane;
			view.orthographic = myCamera.orthographic;
		}
#endif
	}
}

これで大体の見た目は合わせられます。
GameViewの位置サイズを取ってきてSceneViewの位置サイズも合わせようかと思いましたが、
エディタのレイアウトが崩れてしまったので、コレくらいの処理で止めておきます。

単色テクスチャを作るエディタスクリプト

不意に適当なサイズのテクスチャが欲しくなる時がありませんか?
私はあります。
一々Photoshopやペイントを立ち上げるのが面倒だったりするので。。

なので、エディタスクリプトで手軽にテクスチャを追加できる仕組みを作りました

using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEditor;

namespace ScreenPocket
{
	public class TextureUtility
	{

		[MenuItem("Assets/ScreenPocket/Texture/Create")]
		static void Create()
		{
			OpenCreateTextureWindow((w,h,col) =>
			{
				var icon = EditorGUIUtility.FindTexture("Texture2D Icon");
				ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, new DoCreateTexture() { width = w, height = h, color = col  }, "New Texture.png", icon, null);
			});
		}

		static void OpenCreateTextureWindow(System.Action<int,int,Color> callback)
		{
			var w = EditorWindow.GetWindow<CreateTextureInformationWindow>(true, "テクスチャ作成");
			w.callback = callback;
			w.position = new Rect(150f, 150f, 280f, 100f);
		}

		public static void Fill(Texture2D texture, Color color )
		{
			for (int y = 0; y < texture.height; ++y)
			{
				for (int x = 0; x < texture.width; ++x)
				{
					texture.SetPixel(x, y, color);
				}
			}
			texture.Apply();
		}
	}

	class DoCreateTexture : UnityEditor.ProjectWindowCallback.EndNameEditAction
	{
		public int width;
		public int height;
		public Color color;
		public override void Action(int instanceId, string pathName, string resourceFile)
		{
			var texture = new Texture2D( width, height );
			texture.name = Path.GetFileNameWithoutExtension( pathName );
			TextureUtility.Fill( texture, color );
			byte[] bytes = texture.EncodeToPNG();
			DestroyImmediate( texture );
			File.WriteAllBytes(pathName, bytes);
			AssetDatabase.Refresh();
		}
	}


	public class CreateTextureInformationWindow : EditorWindow
	{
		int w = 0;
		int h = 0;

		TextureImporterNPOTScale npotW = TextureImporterNPOTScale.None;
		TextureImporterNPOTScale npotH = TextureImporterNPOTScale.None;

		Color color = Color.white;

		public System.Action<int, int, Color> callback = null;
		void OnGUI()
		{
			EditorGUILayout.BeginHorizontal();
			w = EditorGUILayout.IntField(w);
			EditorGUILayout.LabelField("x", GUILayout.Width(12f));
			h = EditorGUILayout.IntField(h);
			EditorGUILayout.EndHorizontal();

			EditorGUILayout.BeginHorizontal();
			GUI.enabled = !( Mathf.IsPowerOfTwo(w) );
			npotW = (TextureImporterNPOTScale)EditorGUILayout.EnumPopup(npotW);
			GUI.enabled = !(Mathf.IsPowerOfTwo(h));
			npotH = (TextureImporterNPOTScale)EditorGUILayout.EnumPopup(npotH);
			EditorGUILayout.EndHorizontal();

			int finalW = w;
			int finalH = h;

			switch (npotW)
			{
			case TextureImporterNPOTScale.ToNearest:
				finalW = Mathf.ClosestPowerOfTwo(finalW);
				break;
			case TextureImporterNPOTScale.ToLarger:
				finalW = Mathf.NextPowerOfTwo(finalW);
				break;
			case TextureImporterNPOTScale.ToSmaller:
				finalW = Mathf.NextPowerOfTwo(finalW)/2;
				break;
			}

			switch (npotH)
			{
			case TextureImporterNPOTScale.ToNearest:
				finalH = Mathf.ClosestPowerOfTwo(finalH);
				break;
			case TextureImporterNPOTScale.ToLarger:
				finalH = Mathf.NextPowerOfTwo(finalH);
				break;
			case TextureImporterNPOTScale.ToSmaller:
				finalH = Mathf.NextPowerOfTwo(finalH)/2;
				break;
			}

			color = EditorGUILayout.ColorField(color);
			var isValidSize = !(finalW == 0 || finalH == 0);
			string msg = "サイズが不正なため、テクスチャを作成できません!";
			if (isValidSize)
			{
				msg = finalW.ToString() + "x" + finalH.ToString() + " のテクスチャを作成します";
			}
			EditorGUILayout.LabelField(msg);
			GUI.enabled = isValidSize;
			if (GUILayout.Button("Create"))
			{
				if (callback != null)
				{
					callback(w,h,color);
				}
				Close();
			}
			GUI.enabled = true;
		}
	}
}

使うときは Projectフォルダを右クリック > ScreenPocket > Texture > Create で、パネルを表示して入力して下さい。

OnGUI()内でEditorGUILayout.TextField()を呼ぶと例外

タイトルで完結してしまいましたが、この間やった凡ミスのメモ書き。

using UnityEngine;
using UnityEditor;

#if DEVELOPMENT_BUILD || UNITY_EDITOR

namespace ScreenPocket
{
	public class Hoge : MonoBehaviour
	{
		string inputText;
		void OnGUI()
		{
			inputText = EditorGUILayout.TextField(inputText);
		}
	}
}
#endif

と書いてしまって、NullReferenceExceptionを起こしてしまったので同じミスをしないようにメモ。
すぐには例外が出なくて、クリックした時に内部で例外を発するので気づくのが遅れた;

EditorGUILayout → GUILayout
にしましょう。

大きなサイズのpngテクスチャをMultipleSpriteで分割する

お久しぶりです。
マイルストーンやら転職やら、色々ありまして、半年ほど時期が開いてしまいました。

現在転職前の有給消化期間なので、趣味のプログラムを組んでいたのですが、
こちらのアセット
https://assetstore.unity.com/packages/2d/gui/cartoon-ui-pack-200-sprites-70518
を買った所、めちゃくちゃデカい画像だったので、ちょっと分割したいなぁと思い、Unity上で分割できないかどうかを試してみることにしました。

問題点は下記
・サイズが 3981x16104px と膨大
・UnityのTexture2Dアセットとして、Assetdatabase.LoadAsset()で読み込むと、2048(とか4096とかの設定した最大サイズ)で読み込まれてしまう。。
・どうせ分割するなら、解像度も担保したいし元々のサイズを基準に分割したい!

という事で、下記のスクリプトを作成しました。

using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEditor;

namespace ScreenPocket
{
	public class TextureUtility
	{
		[MenuItem("Assets/ScreenPocket/Texture/DivideBySprite")]
		static void DivideTextureBySprite()
		{
			Object[] selections = Selection.GetFiltered(typeof(Object), SelectionMode.Assets | SelectionMode.DeepAssets);
			List<Texture2D> targets = new List<Texture2D>(selections.Length);
			foreach (var @object in selections)
			{
				var texture = @object as Texture2D;
				if (texture == null)
				{
					continue;
				}
				targets.Add(texture);
			}

			if (targets.Count == 0)
			{
				EditorUtility.DisplayDialog("Error", "NotFound Texture", "OK");
				return;
			}

			foreach ( var targetTexture in targets)
			{
				var path = AssetDatabase.GetAssetPath(targetTexture);
				var importer = (TextureImporter)TextureImporter.GetAtPath( path );
				if (importer.spriteImportMode != SpriteImportMode.Multiple)
				{
					EditorUtility.DisplayDialog("Error", "Not Multiple Speite" + importer.name, "OK");
					continue;
				}

				//テクスチャ名と同一名のディレクトリを追加する
				var createDirectryPath = Path.GetDirectoryName(path) + "/" + Path.GetFileNameWithoutExtension(path);
				if (!Directory.Exists(createDirectryPath))
				{
					Directory.CreateDirectory(createDirectryPath);
				}

				var isReadable = importer.isReadable;
				importer.isReadable = true;
				if (!isReadable)
				{
					importer.SaveAndReimport();
				}

				var originalPngTexture = ReadPng(path);

				var spriteMetaDatas = importer.spritesheet;
				foreach (var spriteMetaData in spriteMetaDatas)
				{
					var spriteRect = spriteMetaData.rect;
					int x = Mathf.RoundToInt(spriteRect.x);
					int y = Mathf.RoundToInt(spriteRect.y);
					int w = Mathf.RoundToInt(spriteRect.width);
					int h = Mathf.RoundToInt(spriteRect.height);

					Texture2D newSpriteTexture = new Texture2D( w, h );
					newSpriteTexture.SetPixels(originalPngTexture.GetPixels(x,y,w,h));

					var pngData = newSpriteTexture.EncodeToPNG();
					string filePath = GetSavePath(createDirectryPath, spriteMetaData.name);
					File.WriteAllBytes(filePath, pngData);
				}

				//Readableを戻す
				importer.isReadable = isReadable;
				if (!isReadable)
				{
					importer.SaveAndReimport();
				}

				//TextureImporterを作るためにリフレッシュ
				AssetDatabase.Refresh();

				//BorderとPivotを元のSpriteと合わせる
				foreach (var spriteMetaData in spriteMetaDatas)
				{
					string filePath = GetSavePath( createDirectryPath, spriteMetaData.name);
					var newImporter = (TextureImporter)TextureImporter.GetAtPath(filePath);

					bool isNeedSave = false;
					if (newImporter.spriteBorder != spriteMetaData.border)
					{
						newImporter.spriteBorder = spriteMetaData.border;
						isNeedSave = true;
					}
					TextureImporterSettings settings = new TextureImporterSettings();
					newImporter.ReadTextureSettings(settings);
					if (settings.spritePivot != spriteMetaData.pivot)
					{
						settings.spritePivot = spriteMetaData.pivot;
						settings.spriteAlignment = spriteMetaData.alignment;
						newImporter.SetTextureSettings(settings);
						isNeedSave = true;
					}

					//大本のテクスチャのImporterと設定を合わせたいなら、ここで値を代入する処理を追加する

					if (isNeedSave)
					{
						newImporter.SaveAndReimport();
					}
				}
			}

			AssetDatabase.SaveAssets();
		}

		private static string GetSavePath(string parentDirectryName, string spriteMetaDataName)
		{
			return parentDirectryName + "/" + spriteMetaDataName + ".png";
		}

		/// <summary>
		/// pngファイルをそのまま読み込む
		/// ↓参考サイト
		/// http://macomu.sakura.ne.jp/blog/?p=55
		/// </summary>
		/// <param name="path"></param>
		/// <returns></returns>
		static byte[] ReadPngFile(string path)
		{
			FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
			BinaryReader bin = new BinaryReader(fileStream);
			byte[] values = bin.ReadBytes((int)bin.BaseStream.Length);
			bin.Close();
			return values;
		}

		/// <summary>
		/// 対象パスのpngをそのまま読み込んでTexture2Dにする
		/// ※Texture2Dアセットを読み込むわけではないのでテクスチャサイズの制限を越えて取得できる
		/// ↓参考サイト
		/// https://qiita.com/r-ngtm/items/6cff25643a1a6ba82a6c
		/// </summary>
		/// <param name="path"></param>
		/// <returns></returns>
		static Texture2D ReadPng(string path)
		{
			byte[] readBinary = ReadPngFile(path);

			int pos = 16; // 16バイトから開始

			int width = 0;
			for (int i = 0; i < 4; i++)
			{
				width = width * 256 + readBinary[pos++];
			}

			int height = 0;
			for (int i = 0; i < 4; i++)
			{
				height = height * 256 + readBinary[pos++];
			}

			Texture2D texture = new Texture2D(width, height);
			texture.LoadImage(readBinary);

			return texture;
		}
	}
}

使用する際はProjectツリーで該当のテクスチャを右クリックして ScreenPocket > Texture > DivideBySprite を実行して下さい。
MultipleSpriteのテクスチャと同名のディレクトリを作成し、その中にスプライト名をファイル名としたpngを列挙します。

参考サイトは下記
UnityでPNGファイルを動的に読み込む方法 | ma_comu雑記帳
【Unity】pngファイルを読み込み同サイズのTexture2Dを生成する - Qiita

pngをそもそもTexture2Dアセットではなく、直接読み込んだ上でTexture2D化したい、という事で、上記のサイトのコードを使わせていただきました。
とりあえず上記でファイルを分割しつつ、MultipleSprite時のBorderとか、Pivotを引き継いだテクスチャが作成されるかと思います。

上記のコードで懸念としては、
Mathf.RoundToInt()で、ピクセルが少しずれてしまわないか?という点と、
Importer設定を完全に引き継いでいないので、もうちょっとしっかりコピーしたい点でしょうか。

使用する際は良い感じに書き換えてあげて下さいmm