Skip to content

Instantly share code, notes, and snippets.

@garzaa
Created July 4, 2022 21:22
Show Gist options
  • Save garzaa/013ee075c67ad1da6a93f39904aa1045 to your computer and use it in GitHub Desktop.
Save garzaa/013ee075c67ad1da6a93f39904aa1045 to your computer and use it in GitHub Desktop.
Unity Sprite Autotiler
#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using System.IO;
using UnityEngine;
using System.Linq;
using UnityEngine.Tilemaps;
public class RuleTileCreator : MonoBehaviour {
static RuleTile.TilingRule currentRule;
static Dictionary<Vector3Int, int> neighborDict = new Dictionary<Vector3Int, int>();
static List<Sprite> sprites;
static RuleTile tile;
[MenuItem("Assets/Create Rule Tile From Texture", true)]
static bool CanMakeRuleTile() {
if (Selection.activeObject is Texture2D) {
Texture2D t = Selection.activeObject as Texture2D;
return t.width==TiledBlockCreator.destWidth && t.height==TiledBlockCreator.destHeight;
}
return false;
}
[MenuItem("Assets/Create Rule Tile From Texture")]
static void CreateRuleTileFromTexture() {
Initialize();
string assetPath = AssetDatabase.GetAssetPath(Selection.activeObject);
string ruleTilePath = BaseToOutputName(assetPath);
// check if it's present first
// true is not working, it's never loaded
bool saveNew = false;
tile = AssetDatabase.LoadAssetAtPath(ruleTilePath, typeof(RuleTile)) as RuleTile;
if(tile == null) {
saveNew = true;
tile = ScriptableObject.CreateInstance("RuleTile") as RuleTile;
} else {
tile.m_TilingRules.Clear();
}
sprites = AssetDatabase.LoadAllAssetsAtPath(assetPath).Where(x => x is Sprite).Cast<Sprite>().ToList();
// sprite 12 is the no-neighbor one, they're 0-indexed
tile.m_DefaultColliderType = UnityEngine.Tilemaps.Tile.ColliderType.Grid;
tile.m_DefaultSprite = sprites[12];
// two or more corners
AddSprite(15);
Top(true);
Left(true);
Right(true);
Bottom(true);
TopLeft(false);
TopRight(false);
AddSprite(16);
Top(true);
Left(true);
Right(true);
Bottom(true);
BottomLeft(false);
BottomRight(false);
AddSprite(17);
Top(true);
Left(true);
Right(true);
Bottom(true);
TopLeft(false);
BottomLeft(false);
// one corner
AddSprite(7);
Top(true);
Left(true);
Right(true);
Bottom(true);
BottomLeft(false);
AddSprite(14);
Top(true);
Left(true);
Right(false);
Bottom(true);
TopLeft(false);
AddSprite(18);
Top(false);
Left(true);
Bottom(true);
Right(true);
BottomLeft(false);
AddSprite(19);
Top(true);
Left(true);
Bottom(false);
Right(true);
TopLeft(false);
AddSprite(20);
Top(false);
Left(false);
Bottom(true);
Right(true);
BottomRight(false);
// basics
AddSprite(0);
Top(false);
Left(false);
Right(true);
Bottom(true);
AddSprite(1);
Top(true);
Left(false);
Right(true);
Bottom(true);
AddSprite(2);
Top(true);
Left(false);
Right(true);
Bottom(false);
AddSprite(3);
Top(false);
Left(true);
Right(true);
Bottom(true);
AddSprite(5);
Top(true);
Left(true);
Right(true);
Bottom(false);
AddSprite(6);
Top(true);
Left(true);
Right(true);
Bottom(true);
TopLeft(false);
AddSprite(8);
Top(false);
Left(false);
Right(true);
Bottom(false);
AddSprite(9);
Top(false);
Left(false);
Right(false);
Bottom(true);
AddSprite(10);
Top(true);
Left(false);
Right(false);
Bottom(false);
AddSprite(11);
Top(false);
Left(true);
Right(true);
Bottom(false);
AddSprite(12);
Top(false);
Left(false);
Right(false);
Bottom(false);
AddSprite(13);
Top(true);
Left(false);
Right(false);
Bottom(true);
AddSprite(4);
Top(true);
Left(true);
Right(true);
Bottom(true);
ApplyLastRule();
if (saveNew) {
AssetDatabase.CreateAsset(tile,ruleTilePath);
}
AssetDatabase.Refresh();
}
static void AddSprite(int spriteIndex) {
ApplyLastRule();
neighborDict.Clear();
currentRule = new RuleTile.TilingRule();
currentRule.m_Sprites[0] = sprites[spriteIndex];
}
static void TopLeft(bool neighborRule) {
AddRule(0, neighborRule);
}
static void Top(bool neighborRule) {
AddRule(1, neighborRule);
}
static void TopRight(bool neighborRule) {
AddRule(2, neighborRule);
}
static void Left(bool neighborRule) {
AddRule(3, neighborRule);
}
static void Right(bool neighborRule) {
AddRule(4, neighborRule);
}
static void BottomLeft(bool neighborRule) {
AddRule(5, neighborRule);
}
static void Bottom(bool neighborRule) {
AddRule(6, neighborRule);
}
static void BottomRight(bool neighborRule) {
AddRule(7, neighborRule);
}
static void AddRule(int neighborIndex, bool neighborRule) {
AddRule(neighborIndex, neighborRule ? RuleTile.TilingRuleOutput.Neighbor.This : RuleTile.TilingRuleOutput.Neighbor.NotThis);
}
static void AddRule(int neighborIndex, int neighborRule) {
neighborDict.Add(currentRule.m_NeighborPositions[neighborIndex], neighborRule);
}
static void MirrorX() {
currentRule.m_RuleTransform = RuleTile.TilingRuleOutput.Transform.MirrorX;
}
static void ApplyLastRule() {
// rule can be non-null between runs
if (currentRule == null) return;
MirrorX();
currentRule.ApplyNeighbors(neighborDict);
tile.m_TilingRules.Add(currentRule);
}
static void Initialize() {
currentRule = null;
neighborDict.Clear();
tile = null;
}
static string BaseToOutputName(string baseAssetPath) {
string[] splitPath = baseAssetPath.Split('/');
string originalFileName = splitPath[splitPath.Length-1];
splitPath[splitPath.Length-1] = originalFileName.Split('.')[0]+"Tile.asset";
return string.Join('/', splitPath);
}
}
#endif
#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
public class TiledBlockCreator : MonoBehaviour {
const int tileSize = 64;
const int blocksPerTile = 2;
const int blockSize = tileSize / blocksPerTile;
const int xTiles = 7;
const int yTiles = 3;
public const int templateSize = 2 * tileSize;
public const int destWidth = xTiles * tileSize;
public const int destHeight = yTiles * tileSize;
static string assetPath;
static Texture2D src;
static Texture2D dest;
static Vector2Int workingTile;
[MenuItem("Assets/Create Tiled Block")]
static void CreateTiledBlockFromTexture() {
assetPath = AssetDatabase.GetAssetPath(Selection.activeObject);
PrepareBaseTexture();
LoadBaseTexture();
CreateOutputTexture();
CreateBlocks();
CleanOldOutput();
SaveOutputTexture();
SliceOutputTexture();
}
static void CleanOldOutput() {
AssetDatabase.DeleteAsset(BaseToOutputName(assetPath));
}
static void LoadBaseTexture() {
src = Selection.activeObject as Texture2D;
}
static void CreateOutputTexture() {
/*
(0, 2) (1, 2) (2, 2), (3, 2)
(0, 1) (1, 1) (2, 1), (3, 1)
(0, 0) (1, 0) (2, 0), (3, 0) ... etc
*/
dest = MakeDefaultTexture(tileSize * xTiles, tileSize * yTiles);
}
static void CreateBlocks() {
// 1 - edge, 0 - enclosed
// 11
// 10
SetWorkingTile(v(0, 2));
FillMiddle();
TopEdge();
LeftEdge();
TopLeftCorner();
// 11
// 00
SetWorkingTile(v(1, 2));
FillMiddle();
TopEdge();
// 10
// 10
SetWorkingTile(v(0, 1));
FillMiddle();
LeftEdge();
// 10
// 11
SetWorkingTile(v(0, 0));
FillMiddle();
LeftEdge();
BottomEdge();
BottomLeftCorner();
// 00
// 11
SetWorkingTile(v(1, 0));
FillMiddle();
BottomEdge();
// 00
// 00
SetWorkingTile(v(1, 1));
FillMiddle();
// inner corner - top edge to side edge
SetWorkingTile(v(2, 2));
FillMiddle();
InnerTopLeftCorner();
// inner corner - bottom edge to side edge
SetWorkingTile(v(2, 1));
FillMiddle();
InnerBottomLeftCorner();
// end cap for a 1-high row
SetWorkingTile(v(2, 0));
TopEdge();
BottomEdge();
TopLeftCorner();
BottomLeftCorner();
// top cap for a 1-wide column
SetWorkingTile(v(3, 2));
LeftEdge();
RightEdge();
TopLeftCorner();
TopRightCorner();
// bottom cap for a 1-wide column
SetWorkingTile(v(3, 1));
LeftEdge();
RightEdge();
BottomLeftCorner();
BottomRightCorner();
// center for a 1-high row
SetWorkingTile(v(3, 0));
TopEdge();
BottomEdge();
// zero-neighbor
SetWorkingTile(v(4, 2));
TopLeftCorner();
TopRightCorner();
BottomLeftCorner();
BottomRightCorner();
// middle of a 1-wide column
SetWorkingTile(v(4, 1));
LeftEdge();
RightEdge();
SetWorkingTile(v(4, 0));
FillMiddle();
InnerTopLeftCorner();
RightEdge();
SetWorkingTile(v(5, 2));
FillMiddle();
InnerTopLeftCorner();
InnerTopRightCorner();
SetWorkingTile(v(5, 1));
FillMiddle();
InnerBottomLeftCorner();
InnerBottomRightCorner();
SetWorkingTile(v(5, 0));
FillMiddle();
InnerTopLeftCorner();
InnerBottomLeftCorner();
SetWorkingTile(v(6, 2));
FillMiddle();
TopEdge();
InnerBottomLeftCorner();
SetWorkingTile(v(6, 1));
FillMiddle();
BottomEdge();
InnerTopLeftCorner();
SetWorkingTile(v(6, 0));
LeftEdge();
TopEdge();
TopLeftCorner();
InnerBottomRightCorner();
}
static Vector2Int v(int x, int y) {
return new Vector2Int(x, y);
}
static void SetWorkingTile(Vector2Int v) {
workingTile = v;
}
static void CopyBlock(Vector2Int sourceBlock, Vector2Int destBlock) {
/*
assumes a block with this structure
(0, 1) (1, 1)
(0, 0) (1, 0)
*/
CopyBlock(sourceBlock, v(1, 1), destBlock);
}
static void CopyBlock(Vector2Int sourceBlock, Vector2Int sourceBlockSize, Vector2Int destBlock) {
CopyPixelBlock(
sourceBlock.x * blockSize,
sourceBlock.y * blockSize,
sourceBlockSize.x * blockSize,
sourceBlockSize.y * blockSize,
(destBlock.x * blockSize) + (tileSize * workingTile.x),
(destBlock.y * blockSize) + (tileSize * workingTile.y)
);
}
static void CopyPixelBlock(int sourceX, int sourceY, int blockWidth, int blockHeight, int destX, int destY) {
Graphics.CopyTexture(src, 0, 0, sourceX, sourceY, blockWidth, blockHeight, dest, 0, 0, destX, destY);
}
static Texture2D MakeDefaultTexture(int xSize, int ySize) {
Texture2D output = new Texture2D(xSize, ySize);
output.filterMode = FilterMode.Point;
return output;
}
[MenuItem("Assets/Create Tiled Block", true)]
static bool CanMakeTiledBlock() {
if (Selection.activeObject is Texture2D) {
Texture2D t = Selection.activeObject as Texture2D;
return t.width == templateSize && t.height == templateSize;
}
return false;
}
static void FillMiddle() {
/*
given a texture with four target tiles, it's mapped like this:
(1, 0) (1, 1)
(0, 0) (0, 1)
and each tile will have 4 blocks in it
*/
// fill the middle with the middle texture, straight copy
CopyBlock(v(1, 1), v(2, 2), v(0, 0));
}
static void TopEdge() {
CopyBlock(v(1, 3), v(2, 1), v(0, 1));
}
static void RightEdge() {
CopyBlock(v(0, 1), v(1, 2), v(1, 0));
MirrorBlock(v(1, 0));
MirrorBlock(v(1, 1));
}
static void BottomEdge() {
CopyBlock(v(1, 0), v(2, 1), v(0, 0));
}
static void LeftEdge() {
CopyBlock(v(0, 1), v(1, 2), v(0, 0));
}
static void TopLeftCorner() {
CopyBlock(v(0, 3), v(0, 1));
}
static void TopRightCorner() {
CopyBlock(v(0, 3), v(1, 1));
MirrorBlock(v(1, 1));
}
static void BottomRightCorner() {
CopyBlock(v(0, 0), v(1, 0));
MirrorBlock(v(1, 0));
}
static void BottomLeftCorner() {
CopyBlock(v(0, 0), v(0, 0));
}
static void InnerTopLeftCorner() {
CopyBlock(v(3, 1), v(0, 1));
}
static void InnerTopRightCorner() {
CopyBlock(v(3, 1), v(1, 1));
MirrorBlock(v(1, 1));
}
static void InnerBottomLeftCorner() {
CopyBlock(v(3, 0), v(0, 0));
}
static void InnerBottomRightCorner() {
CopyBlock(v(3, 0), v(1, 0));
MirrorBlock(v(1, 0));
}
static void MirrorBlock(Vector2Int targetBlock) {
// translate the block's origin into texture space
// first offset by tile, then look ino the specific block
Vector2Int origin = (tileSize * workingTile) + (targetBlock * blockSize);
Color[] pixels = dest.GetPixels(origin.x, origin.y, blockSize, blockSize);
// then go and flip each row, swap the first element with the nth, second with n-1, etc
int diff;
Color other;
// swap pixels in the left half with the right half
for (int i=0; i<pixels.Length; i++) {
diff = (blockSize - (i%blockSize)*2 - 1);
if (i%blockSize < blockSize/2) {
other = pixels[i + diff];
pixels[i + diff] = pixels[i];
pixels[i] = other;
}
}
dest.SetPixels(origin.x, origin.y, blockSize, blockSize, pixels);
dest.Apply();
}
static void PrepareBaseTexture() {
// the texture's asset importer setting has to be RGBA32, because that's the format of the default PNG dest tex we create
TextureImporter t = AssetImporter.GetAtPath(assetPath) as TextureImporter;
TextureImporterPlatformSettings texset = t.GetDefaultPlatformTextureSettings();
texset.format = TextureImporterFormat.RGBA32;
t.SetPlatformTextureSettings(texset);
// also make sure it's readable
t.isReadable = true;
AssetDatabase.ImportAsset(assetPath);
AssetDatabase.Refresh();
}
static void SaveOutputTexture() {
File.WriteAllBytes(BaseToOutputName(assetPath), dest.EncodeToPNG());
// then refresh so the new texture shows up
AssetDatabase.Refresh();
}
static string BaseToOutputName(string baseAssetPath) {
string[] splitPath = baseAssetPath.Split('/');
string originalFileName = splitPath[splitPath.Length-1];
splitPath[splitPath.Length-1] = originalFileName.Split('.')[0]+"_generated.png";
return string.Join('/', splitPath);
}
static void SliceOutputTexture() {
string outputPath = BaseToOutputName(assetPath);
TextureImporter ti = AssetImporter.GetAtPath(outputPath) as TextureImporter;
ti.spriteImportMode = SpriteImportMode.Multiple;
List<SpriteMetaData> metaData = new List<SpriteMetaData>();
int spriteNum = 0;
for (int x=0; x<dest.width; x+=tileSize) {
for (int y=dest.height; y>0; y-=tileSize) {
SpriteMetaData meta = new SpriteMetaData();
meta.pivot = new Vector2(0.5f, 0.5f);
meta.alignment = 9;
meta.name = spriteNum.ToString();
meta.rect = new Rect(x, y-tileSize, tileSize, tileSize);
metaData.Add(meta);
spriteNum++;
}
}
ti.spritesheet = metaData.ToArray();
AssetDatabase.ImportAsset(outputPath, ImportAssetOptions.ForceUpdate);
AssetDatabase.Refresh();
}
}
#endif
@garzaa
Copy link
Author

garzaa commented Jul 4, 2022

blocktemplate
tile template, contains labeled sections for corners, edges, middle sections, and inner corners

example webm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment