** Spine을 사용할 수 있는 상황이라면 이럴 필요 없음 **
받은 스파인 파일의 Texture 파일이 여러개로 나뉘어있는 상황이었는데, 1개의 Texture 파일로 합치고 싶었음
이유 : AddSkin할 때마다 material이 너무 많이 생겨서 드로우콜이 난리남
그렇다고 런타임에 Repack을 하니 느리고 뭔가 불안불안해서 그냥 한 번에 그릴 수 있는 통짜 이미지가 갖고 싶었음
(+멀티 게임이라, 런타임중에 내 스킨만 갖고 있는 Texture로 만들면 다른 사람 접속할 때마다 새로 텍스처를 만드는데 이게 맘에 안들어서 전체 스킨이 다 포함된 이미지가 갖고 싶었음.
솔플 게임이라면 그냥 본인 스킨만 그 때 그 때 repack해도 괜찮을 거 같음)
,
여튼
처음에는 이걸로 해보려고 했지만 잘 안됐다,,

그래서 스크립트로 합치게 됐다,,
먼저 인스펙터에서 넣어줘야할 것들은 이 두 개다
public SkeletonAnimation skMecanim;
public TextAsset atlasTextAsset;
씬에 원본 Spine 파일을 끌어넣어서 SkeletonAnimation/SkeletonMecanim 중 한개로 만들어주고
인스펙터로 끌어넣기
atlasTextAsset에는 원본 Spine 폴더에 들어있는 (파일명).atlas.txt 파일이 있을텐데
이거를 넣어주면 된다
[Serializable]
public class SaveAtlasInfo
{
public string skinName;
public int[] bounds;
public int[] offsets;
public int[] rotate = new int[1] { 0 };
}
[Serializable]
public class SaveAtlasInfoSave
{
public List<SaveAtlasInfo> skinInfos = new List<SaveAtlasInfo>();
}
그리고 텍스트 파일을 만들기 위한 클래스를 만들어주고
private SaveAtlasInfoSave info;
private Dictionary<string, SaveAtlasInfo> infoDic = new();
private Dictionary<string, float> rotateDic = new();
private List<string> nameList = new();
저장할 정보 관련된 변수들을 선언해주기
1. 원본파일에서 정보 갖고오기
private void GetOriginalRotationInfo()
{
string atlasAsset = atlasTextAsset.ToString();
string[] lines = atlasAsset.Split("\n");
string currentKey = null;
for (int i = 4; i < lines.Length; i++)
{
string line = lines[i];
bool isName = !line.Contains(':');
if (i + 3 > lines.Length)
{
break;
}
if (lines[i].StartsWith("rotate:"))
{
// 키 라인 처리
currentKey = lines[i - 3];
currentKey = currentKey.Split("\r")[0];
string rotateValue = line.Split(':')[1].Trim();
int rotate = int.Parse(rotateValue);
rotateDic[currentKey] = rotate;
}
}
}
먼저 rotate 값을 갖고와야한다
왜인진 모르겠지만, Skin에서 rotate값을 가져오려고하면 원래값과 다른 값이 들어온다
그래서 이리저리 해보다가 그냥 atlas 텍스트 파일에서 rotate값을 직접 딕셔너리에 넣었음
2. 하나로 합쳐진 이미지 저장하기
private string CreateNewAtlas()
{
Skin combinedSkin = new Skin("combined skin");
Skeleton sk = skMecanim.Skeleton;
SkeletonDataAsset skDataAsset = skMecanim.skeletonDataAsset;
SkeletonData skData = sk.Data;
// 원본 파일의 Skin 정보 전부 가져오기
foreach (var s in skData.Skins)
{
combinedSkin.AddSkin(s);
}
sk.SetSkin(combinedSkin);
sk.SetSlotsToSetupPose();
Skin newSkin = sk.Skin;
// 전부 합쳐진 스킨으로 하나의 Texture 파일 뽑기
Material mat;
Material inputMat = skDataAsset.atlasAssets[0].PrimaryMaterial;
Texture2D texture;
int maxSize = 4096;
Skin repackedSkin = newSkin.GetRepackedSkin("repacked Skin", inputMat, out mat, out texture, maxSize, 8);
info = new SaveAtlasInfoSave();
AddRegionInfo(repackedSkin.Attachments);
sk.SetSkin(repackedSkin);
sk.SetSlotsToSetupPose();
SaveTextureFile(texture);
string json = JsonUtility.ToJson(info, true);
json = JsonToAtlasFormat(json);
return json;
}
private void SaveTextureFile(Texture2D texture)
{
byte[] bytes = texture.EncodeToPNG();
string filePath = Path.Combine(Application.dataPath, "SavedTexture.png");
File.WriteAllBytes(filePath, bytes);
}
GetRepackedSkin을 하면 하나로 합쳐진 텍스쳐가 나온다
요걸 저장하고 repackedSkin에서 skin들 좌표랑 오프셋을 뽑아서 txt파일로 저장해야함
3-1. 합쳐진 스킨에서 정보 뽑기
private void AddRegionInfo(ICollection<Skin.SkinEntry> repackedSkin)
{
foreach (var atch in repackedSkin)
{
Spine.Attachment attachment = atch.Attachment;
if (attachment is RegionAttachment regionAttachment)
{
TextureRegion textreRegion = regionAttachment.Region;
AtlasRegion atlasRegion = textreRegion as AtlasRegion;
string skinName = atlasRegion.name;
int rotate = 0;
if (rotateDic.ContainsKey(skinName))
{
rotate = Mathf.RoundToInt(rotateDic[skinName]);
atlasRegion.rotate = true;
atlasRegion.degrees = rotate;
regionAttachment.SetRotation(rotate);
regionAttachment.UpdateRegion();
textreRegion = regionAttachment.Region;
}
SaveAtlasInfo slotInfo = GetRegionInfo(textreRegion, rotate);
if (rotate != 0)
{
slotInfo.rotate[0] = rotate;
}
slotInfo.skinName = skinName;
if (!nameList.Contains(skinName))
{
nameList.Add(skinName);
info.skinInfos.Add(slotInfo);
}
}
else if (attachment is MeshAttachment meshAttachment)
{
TextureRegion textreRegion = meshAttachment.Region;
AtlasRegion atlasRegion = textreRegion as AtlasRegion;
string skinName = atlasRegion.name;
int rotate = 0;
if (rotateDic.ContainsKey(skinName))
{
rotate = Mathf.RoundToInt(rotateDic[skinName]);
atlasRegion.rotate = true;
}
SaveAtlasInfo slotInfo = GetRegionInfo(textreRegion, rotate);
if (rotate != 0)
{
slotInfo.rotate[0] = rotate;
}
slotInfo.skinName = skinName;
if (!nameList.Contains(skinName))
{
nameList.Add(skinName);
info.skinInfos.Add(slotInfo);
}
// Debug.Log($"MeshAttachment found: {meshAttachment.Name}");
}
else if (attachment is BoundingBoxAttachment boundingBoxAttachment)
{
Debug.Log($"BoundingBoxAttachment found: {boundingBoxAttachment.Name}");
}
else
{
Debug.Log($"Attachment of different type: {attachment.GetType().Name}");
}
}
}
RegionAttachment 가 제일 많았고, MeshAttachment도 몇 개 있었음
nameList에 추가했던건 초반에 attatchment명이 중복되는 경우가 있어서(밑에 따로 씀) 확인하려고 넣었다
나머지 종류는 내 파일에는 없어서 따로 코드를 안넣었지만 만약에 필요하다면 똑같이 하면 될듯하다
3-2. 스킨 하나하나 Attatchment 정보 넣기
private SaveAtlasInfo GetRegionInfo(TextureRegion textureRegion, int rotation = 0)
{
SaveAtlasInfo slotInfo = new SaveAtlasInfo();
AtlasRegion atlasRegion = textureRegion as AtlasRegion;
int X = Mathf.RoundToInt(atlasRegion.x);
int Y = Mathf.RoundToInt(atlasRegion.y);
int Width = Mathf.RoundToInt(textureRegion.width);
int Height = Mathf.RoundToInt(textureRegion.height);
int offsetX = Mathf.RoundToInt(atlasRegion.offsetX);
int offsetY = Mathf.RoundToInt(atlasRegion.offsetY);
int OriginalWidth = Mathf.RoundToInt(textureRegion.OriginalWidth);
int OriginalHeight = Mathf.RoundToInt(textureRegion.OriginalHeight);
Vector2Int offset = Vector2Int.zero;
Vector2Int OriginalSize = Vector2Int.zero;
Vector2Int RepackedSize = Vector2Int.zero;
OriginalSize = new Vector2Int(OriginalWidth, OriginalHeight);
if (rotation != 0)
{
RepackedSize = new Vector2Int(Height, Width);
offset = new Vector2Int(offsetY, offsetX);
}
else
{
RepackedSize = new Vector2Int(Width, Height);
offset = new Vector2Int(offsetX, offsetY);
}
slotInfo.bounds = new int[4] { X, Y, RepackedSize.x, RepackedSize.y };
slotInfo.offsets = new int[4] { offset.x, offset.y, OriginalSize.x, OriginalSize.y };
return slotInfo;
}
bounds는 { 아틀라스 내의 위치 X, 아틀라스 내의 위치 Y, 변경된 사이즈의 가로,세로}
offsets는 { 오프셋 X, 오프셋 Y, 원래 사이즈 X, Y }
중요한건 회전해있는 이미지 일 경우 bounds에 있는 변경된 사이즈의 가로 세로를 거꾸로 바꿔줘야함
원래 사이즈 X, Y는 원래부터 제대로 나오기 때문에 바꿔줄 필요없음
4. 텍스트 파일로 atlas 저장하기
public string JsonToAtlasFormat(string strJson)
{
string str = strJson;
string[] strArrays = { "bounds", "offsets", "rotate" };
char[] trim1 = { ' ', '\n' };
int startIdx = 0;
int idx1 = -1;
for (int i = 0; i < strArrays.Length; ++i)
{
startIdx = 0;
idx1 = -1;
while ((idx1 = str.IndexOf(strArrays[i], startIdx)) != -1)
{
int idx2 = str.IndexOf("[", idx1);
int idx3 = str.IndexOf("]", idx1);
int len = idx3 - idx2 + 1;
string subStr = str.Substring(idx2, len);
string subStr2 = subStr.Replace(" ", "");
subStr2 = subStr2.Replace("\n", "");
if (i < 2)
{
subStr2 = subStr2.Replace("\n", "");
if (subStr.Length != subStr2.Length)
{
str = str.Remove(idx2, len).Insert(idx2, subStr2);
}
}
else
{
subStr2 = subStr2.Replace("\n", "");
if (subStr.Length != subStr2.Length)
{
str = str.Remove(idx2, len).Insert(idx2, subStr2);
}
if (subStr2 == "[0]")
{
int idxb1 = str.LastIndexOf(",", idx1);
int len2 = idx3 - idxb1 + 1;
str = str.Remove(idxb1, len2).Insert(idx1, "\n");
}
}
startIdx = idx2;
}
}
str = str.Replace("\"skinName\":", string.Empty);
str = CleanJsonString(str);
return str;
}
private string CleanJsonString(string jsonString)
{
// 제거할 문자 목록
char[] charsToRemove = new char[] { '{', '}', '[', ']', '"', ' ' };
foreach (char c in charsToRemove)
{
jsonString = jsonString.Replace(c.ToString(), string.Empty);
}
return jsonString;
}
private void SaveAtlasTextFile(string jsonFile)
{
string str = jsonFile.ToString();
string[] lines = str.Split("\n");
string newStr = "";
for (int i = 0; i < lines.Length; i++)
{
bool isCommaLine = false;
string line = lines[i];
if (line.EndsWith(','))
{
isCommaLine = true;
int index = lines[i].LastIndexOf(',');
lines[i] = lines[i].Remove(index, 1).Insert(index, "\n");
}
if (isCommaLine)
{
newStr += lines[i];
}
else
{
if (!string.IsNullOrWhiteSpace(line))
{
if (!lines[i].Equals("\n"))
{
lines[i] = lines[i] += "\n";
newStr += lines[i];
}
}
}
if (lines[i] == "\n\n")
{
lines[i] = "\n";
}
}
newStr = newStr.Replace("\n\n", "\n");
File.WriteAllText(Application.dataPath + "/TESTTEST.txt", newStr);
}
private string ReplaceLastCommaWithNewLine(string input)
{
int lastCommaIndex = input.LastIndexOf(',');
if (lastCommaIndex != -1)
{
input = input.Remove(lastCommaIndex, 1).Insert(lastCommaIndex, "\n");
}
return input;
}
json 형태에서 atlas파일 형태로 바꿔주는 과정
{ , } , [ , ] 이런걸 다 떼주고 스킨이름은 그냥 덩그러니 있기 때문에 앞에도 짤라준다
rotate도 0일경우 제거해야함
base/skinName
bounds: 1234,1242,15,30
offsets:6,6,41,50
rotate:90
뭐이런식의 형태로 쭉 나오게 변환해주기
그러면
맨 위에 헤더파일만 짤린 txt파일이 나온다
맨 위에
(이미지 파일명).png
size:2048,2048
filter:Linear,Linear
pma:true
이런 헤더를 추가해줘야하는데 size는 내가 뽑은 텍스처 크기, 맨 위에는 내가 뽑은 텍스처 파일 이름 넣어주면됨
그리고 스파인 임포트할 때 쓰이는 json, png, atlas.txt 파일 세개는 모두 앞에 이름이 같아야한다
난 유니티 내에서 안하고 폴더 열어서 내가 만든 png, atlas.txt, 원본 json파일 을 넣어줬음
이렇게 하면 감쪽같이 하나의 텍스처 파일로 바뀐 Spine 파일이 나온다!
물론 Spine이 있다면,, 저장 버튼 한 번 누르면 됨,,
++
attatchment명이 중복되는 경우가 있다
이름은 항상 base/top, base/hand 뭐 이런식으로 앞 뒤로 /기준으로 나뉘는데 내가 쓴 스파인 파일은
pink_pants/left_1
blue_pants/left_1
이런식으로.. 뒤에 이름이 중복되는 경우가 있었다
이러면 처음에 AddSkin을 할 때 중복되는 스킨은 다 씹혀서 날아가기때문에 미싱 리전이 왕창 뜬다
그래서 나는 Spine 에 있는 Skin.cs 파일을 좀 수정했다
public void SetAttachment (int slotIndex, string name, Attachment attachment) {
if (attachment == null) throw new ArgumentNullException("attachment", "attachment cannot be null.");
SkinKey NewKey = new SkinKey(slotIndex, name);
if(attachments.ContainsKey(NewKey))
{
NewKey = new SkinKey(slotIndex, name, attachment.Name, true);
}
attachments[NewKey] = new SkinEntry(slotIndex, name, attachment);
}
먼저 중복 검사를 해주고 SkinKey 생성자를 수정했다
private struct SkinKey
{
internal readonly int slotIndex;
internal readonly string name;
internal readonly int hashCode;
public SkinKey (int slotIndex, string name, string fullName = "", bool duplicated = false) {
if (slotIndex < 0) throw new ArgumentException("slotIndex must be >= 0.");
if (name == null) throw new ArgumentNullException("name", "name cannot be null");
this.slotIndex = slotIndex;
if(duplicated)
{
string[] splitName = null;
if(fullName.Contains('/'))
{
splitName = fullName.Split('/');
string _a = "_1_";
string _b = "_2_";
if (name.Contains(_1))
{
string[] a = name.Split(_a);
name = splitName[0] + '/' + splitName[0] + _a + a[1];
}
else if(name.Contains(_2))
{
string[] b = name.Split(_b);
name = splitName[0] + '/' + splitName[0] + _b + b[1];
}
}
this.name = name;
}
else
{
this.name = name;
}
this.hashCode = name.GetHashCode() + slotIndex * 37;
}
}
(그대로 복붙했는데 코드 블럭 왜 이렇게 생기지ㅠ)
생성자에서 중복될 경우 앞의 이름을 참고해서 변경되게 바꿨다
1, 2는 내 파일의 경우가 그렇고 다른 파일은 이름 규칙이 다를테니 상황에 맞춰서 바꿔쓰면 될듯??
이렇게하면 전체 다 예쁘게 잘 나온다
'Unity' 카테고리의 다른 글
| [Unity] JoyStick 움직임이 버벅거리는 현상 (0) | 2024.07.15 |
|---|---|
| [Unity] Sprite Atlas를 Addressable로 불러오기 (0) | 2024.07.08 |
| [Unity] Android Script Debugging (0) | 2024.07.05 |
| [Unity] Android GoogleSignin 빌드 실패 (0) | 2024.07.05 |
| [Unity] TMP InputField에서 마지막 글자가 짤리는 현상 (0) | 2024.02.17 |