427 lines
No EOL
18 KiB
C#
427 lines
No EOL
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.ComponentModel;
|
|
using System.Drawing;
|
|
using JD.Asset.Types.Map;
|
|
using JD.Serialization;
|
|
using Ubisoft;
|
|
|
|
using Packager.Parameters;
|
|
|
|
using LogCategory = JD.LogService.LogCategory;
|
|
using Path = System.IO.Path;
|
|
using File = System.IO.File;
|
|
|
|
|
|
namespace Packager.Tasks
|
|
{
|
|
[DisplayName("Atlas Texture Generation")]
|
|
|
|
public class GenAtlasTask : Task
|
|
{
|
|
[Input("Dependency File List", IOCardinality.Single, IOType.File)]
|
|
public ReferenceList DependencyFileList { get; set; }
|
|
|
|
[Output("New Map Dependencies Files", IOType.File)]
|
|
public ReferenceList UpdatedMapDependenciesList = new ReferenceList();
|
|
|
|
[Output("Map List to Replace Dtape", IOType.String)]
|
|
public ReferenceList MontageMapList = new ReferenceList();
|
|
|
|
public GenAtlasTask()
|
|
{
|
|
DependencyFileList = new ReferenceList();
|
|
}
|
|
|
|
private class PictosMontage
|
|
{
|
|
public string Path;
|
|
public string AtlasPath;
|
|
public string PictosPath;
|
|
public string PictosFolder;
|
|
|
|
public int TotalPictos;
|
|
public int Height;
|
|
public int Width;
|
|
public int PictoHeight;
|
|
public int PictoWidth;
|
|
public int PictosRow;
|
|
public int PictosCol;
|
|
public int PictosLastRow;
|
|
|
|
public List<string> PictosList;
|
|
|
|
public const int MaxImagesToCheck = 4;
|
|
public const int PictosMaxLimit = 64;
|
|
public const int MontageMaxLength = 4096;
|
|
public const int AlternatePictosRow = 4;
|
|
|
|
public PictosMontage()
|
|
{
|
|
Path = "";
|
|
AtlasPath = "";
|
|
PictosPath = "";
|
|
PictosFolder = "";
|
|
|
|
TotalPictos = 0;
|
|
Height = 0;
|
|
Width = 0;
|
|
PictoHeight = 0;
|
|
PictoWidth = 0;
|
|
PictosRow = 0;
|
|
PictosCol = 0;
|
|
PictosLastRow = 0;
|
|
}
|
|
}
|
|
|
|
#region Dependencies Processing
|
|
protected override void StartInternal(BuildConfiguration profile, cookerWrapper cooker)
|
|
{
|
|
foreach (string file in DependencyFileList)
|
|
{
|
|
var montage = new PictosMontage();
|
|
|
|
var mapDependenciesList = GetDependenciesFromFile(file);
|
|
string mapName = GetMapName(mapDependenciesList);
|
|
var map = Map.FromName(mapName);
|
|
|
|
string taskInputFile = Path.GetFileNameWithoutExtension(file);
|
|
string taskOutputFile = Path.Combine(TaskOutputFolder, taskInputFile + ".txt");
|
|
|
|
GetPictogramDependencies(montage, map);
|
|
|
|
if (DoWork(montage, mapName))
|
|
UpdateDependencies(montage, mapDependenciesList);
|
|
|
|
File.WriteAllLines(taskOutputFile, mapDependenciesList);
|
|
UpdatedMapDependenciesList.Add(taskOutputFile);
|
|
}
|
|
}
|
|
|
|
|
|
private void UpdateDependencies(PictosMontage montage, List<string> mapDependencies)
|
|
{
|
|
string montageFolder = montage.PictosFolder + "/montage.png";
|
|
string atlFolder = montage.PictosFolder + "/montage.atl";
|
|
|
|
mapDependencies.RemoveAll((x => x.StartsWith(montage.PictosFolder)));
|
|
mapDependencies.Add(atlFolder);
|
|
mapDependencies.Add(montageFolder);
|
|
}
|
|
|
|
private List<string> GetDependenciesFromFile(string file)
|
|
{
|
|
var dependencies = File.ReadAllLines(file);
|
|
return dependencies.ToList();
|
|
}
|
|
|
|
private string GetMapName(List<string> mapDependenciesList)
|
|
{
|
|
var dependencyFile = mapDependenciesList.FirstOrDefault();
|
|
var fileName = Path.GetFileNameWithoutExtension(dependencyFile).ToLower();
|
|
|
|
return fileName.Substring(0, fileName.IndexOf("_main_scene", StringComparison.CurrentCultureIgnoreCase));
|
|
}
|
|
|
|
private void GetPictogramDependencies(PictosMontage montage, Map map)
|
|
{
|
|
montage.PictosPath = map.PictosFolder;
|
|
montage.PictosFolder = map.PictosFolder.RelativeUri.ToLower();
|
|
|
|
// The pictograms that are only referenced from disabled pictogram clips should not be included in the montage.
|
|
// These clips will not be included in the bundled data.
|
|
|
|
var danceTape = Tape.CreateTape(map.DanceTapeFile);
|
|
var activePictogramClips = danceTape.Clips.OfType<PictogramClip>().Where(clip => clip.IsActive);
|
|
var activeDistinctPictos = activePictogramClips.DistinctBy(x => x.Model.Name).ToList();
|
|
var activeDistinctPictoFiles = activeDistinctPictos.Select(p => p.Model.PictoPath.ToString()).ToList();
|
|
|
|
montage.PictosList = activeDistinctPictoFiles;
|
|
|
|
montage.PictosList.Sort(StringComparer.Ordinal);
|
|
// ordinal sorting is needed here to get the same order as when sorting engine side, with default c++ string comparison
|
|
}
|
|
|
|
private bool DoWork(PictosMontage montage, string mapName)
|
|
{
|
|
if (montage.PictosList.Count() > 1)
|
|
{
|
|
LogMessage($"Beginning the Montage Generation for map: {mapName}", LogCategory.Message);
|
|
|
|
if (CreateMontage(montage))
|
|
{
|
|
CreateAtl(montage);
|
|
CreateTfi(montage);
|
|
MontageMapList.Add(mapName);
|
|
|
|
LogMessage($"[ SUCCESS ] Atlas texture and .atl have been generated for map: {mapName}", LogCategory.Message);
|
|
}
|
|
else
|
|
{
|
|
LogMessage($"[ END TASK ] Atlas texture won't be created for the map: {mapName}", LogCategory.Message);
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogMessage($"[ END TASK ] No Pictos found for the map: {mapName}", LogCategory.Warning);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Montage Creation
|
|
private bool CreateMontage(PictosMontage montage)
|
|
{
|
|
string basePath = montage.PictosPath.Substring(0, montage.PictosPath.IndexOf("World", StringComparison.CurrentCultureIgnoreCase));
|
|
montage.Path = Path.Combine(montage.PictosPath, "montage.png");
|
|
List<string> pictosFileList = new List<string>();
|
|
|
|
foreach (var element in montage.PictosList)
|
|
{
|
|
var path = Path.Combine(basePath, element);
|
|
|
|
if (Path.GetExtension(path) == ".tga")
|
|
{
|
|
LogMessage("Unable to build the montage with .tga files!", LogCategory.Error);
|
|
return false;
|
|
}
|
|
|
|
if (File.Exists(path))
|
|
pictosFileList.Add(path);
|
|
else
|
|
{
|
|
LogMessage($"The file {path} is missing!", LogCategory.Error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
montage.TotalPictos = pictosFileList.Count();
|
|
|
|
if (montage.TotalPictos == 0)
|
|
{
|
|
LogMessage("No pictos was found for this Map", LogCategory.Warning);
|
|
return false;
|
|
}
|
|
if (montage.TotalPictos > PictosMontage.PictosMaxLimit)
|
|
{
|
|
LogMessage($"Too many pictos for the Texture, amount of pictos: {montage.TotalPictos} Pics", LogCategory.Message);
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
CheckPictosDimensions(montage, pictosFileList);
|
|
|
|
ComputeMontageDimensions(montage);
|
|
|
|
if (IsMontageTooBig(montage))
|
|
return false;
|
|
|
|
BuildTheAtlasImage(montage, pictosFileList);
|
|
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogMessage($"Exception: {ex.Message}", LogCategory.Error);
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
private void CheckPictosDimensions(PictosMontage montage, List<string> pictoFiles)
|
|
{
|
|
// We check the dimensions of at most 4 pictures in the picto's folder
|
|
// and keep the biggest dimensions as a model for the montage's tiles
|
|
|
|
var tempHeight = 0;
|
|
var tempWidth = 0;
|
|
var maxImagesToCheck = Math.Min(PictosMontage.MaxImagesToCheck, pictoFiles.Count());
|
|
|
|
for (var i = 0; i < maxImagesToCheck; i++)
|
|
{
|
|
using (var image = Image.FromFile(pictoFiles[i]))
|
|
{
|
|
montage.PictoHeight = Math.Max(image.Height, tempHeight);
|
|
montage.PictoWidth = Math.Max(image.Width, tempWidth);
|
|
tempHeight = image.Height;
|
|
tempWidth = image.Width;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private void ComputeMontageDimensions(PictosMontage montage)
|
|
{
|
|
// We want to make an atlas texture with Power of 2 dimensions.
|
|
// That's why we are taking the smallest power of 2 superior or equal to the amount of pictures,
|
|
// and we set the number of pictures in the row to the half of this value (or the smallest value superior to the half)
|
|
// to make the montage as square as possible. The number of pics in a column is deduced from the row value.
|
|
|
|
double nextPow2 = (int)Math.Ceiling(Math.Log(montage.TotalPictos, 2));
|
|
double halfPow2 = (int)Math.Ceiling(nextPow2 / 2);
|
|
montage.PictosRow = (int)Math.Pow(2, halfPow2);
|
|
montage.PictosCol = (int)Math.Ceiling((double)montage.TotalPictos / montage.PictosRow);
|
|
}
|
|
|
|
private bool IsMontageTooBig(PictosMontage montage)
|
|
{
|
|
montage.Width = montage.PictosRow * montage.PictoWidth;
|
|
montage.Height = montage.PictosCol * montage.PictoHeight;
|
|
|
|
if (montage.Width > PictosMontage.MontageMaxLength)
|
|
{
|
|
LogMessage($"The textures' width are too wide montageWidth = {montage.Width}px > 4096px, trying to adjust the montage", LogCategory.Message);
|
|
|
|
AdjustingMontageDimensions(montage);
|
|
|
|
if (montage.Height > PictosMontage.MontageMaxLength)
|
|
{
|
|
LogMessage($"The montage's Height is {montage.Height}px, still above 4096px", LogCategory.Message);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void AdjustingMontageDimensions(PictosMontage montage)
|
|
{
|
|
// We Re-Adjust the pictos to ensure that the montage fit the Engine's limitations,
|
|
// especially for maps where we got 2 or more Coaches.
|
|
// In those cases, images width are 740px and we can put only 6 pictures maximum,
|
|
// on each row before exceed the limit of 4096px.
|
|
// And because 6 isn't a Power of 2, we choose to put 4 pictos in a row.
|
|
|
|
montage.PictosRow = PictosMontage.AlternatePictosRow;
|
|
montage.PictosCol = (int)Math.Ceiling((double)montage.TotalPictos / montage.PictosRow);
|
|
montage.Height = montage.PictosCol * montage.PictoHeight;
|
|
montage.Width = montage.PictosRow * montage.PictoWidth;
|
|
}
|
|
|
|
private void BuildTheAtlasImage(PictosMontage montage, List<string> pictosFiles)
|
|
{
|
|
// Here we use the images's tile index to define its position on the row and the column.
|
|
// The row index correspond to the division of the tile index by the number of pictures in a row.
|
|
// The column index is the remainder of this division. For these two computations, the total is an integer.
|
|
//
|
|
// ___ ___ ___ ___ ___
|
|
// | 0 | 1 | 2 | 3 | 4 | -> 0/5 = 1/5 = ... = 4/5 = 0
|
|
// | 5 | 6 | 7 | 8 | 9 | -> 5/5 = 6/5 = ... = 9/5 = 1
|
|
// | | |
|
|
// V V V
|
|
// 0%5 1%5 ... 4%5
|
|
// 5%5 6%5 ... 9%5
|
|
// 10%5 11%5 ... 14%5
|
|
// = = ... =
|
|
// 0 1 ... 4
|
|
|
|
using (var montageImg = new Bitmap(montage.Width, montage.Height))
|
|
using (var graphics = Graphics.FromImage(montageImg))
|
|
{
|
|
int tile = 0;
|
|
|
|
foreach (var img in pictosFiles)
|
|
{
|
|
using (var image = Image.FromFile(img))
|
|
{
|
|
var columnIndex = tile % montage.PictosRow;
|
|
var rowIndex = tile / montage.PictosRow;
|
|
var columnPos = columnIndex * montage.PictoWidth;
|
|
var rowPos = rowIndex * montage.PictoHeight;
|
|
var rec = new Rectangle(columnPos, rowPos, montage.PictoWidth, montage.PictoHeight);
|
|
|
|
graphics.DrawImage(image, rec);
|
|
tile++;
|
|
}
|
|
}
|
|
montageImg.Save(montage.Path);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Atl Creation
|
|
|
|
private void CreateAtl(PictosMontage montage)
|
|
{
|
|
//The .atl file is a list of the UV coordinates of each pictures on the montage
|
|
|
|
var indexPicto = 0;
|
|
const string FileBegin = "ITF_GFX_UV_ATLAS =\n{\n";
|
|
const string FileEnd = "}";
|
|
|
|
//If the last row isn't filled
|
|
montage.PictosLastRow = montage.PictosRow - ((montage.PictosRow * montage.PictosCol) - montage.TotalPictos);
|
|
|
|
var textureAspectRatio = (float)montage.Width / montage.Height;
|
|
|
|
montage.AtlasPath = Path.Combine(montage.PictosPath, "montage.atl");
|
|
|
|
if (File.Exists(montage.AtlasPath))
|
|
{
|
|
File.Delete(montage.AtlasPath);
|
|
}
|
|
|
|
|
|
File.AppendAllText(montage.AtlasPath, FileBegin);
|
|
File.AppendAllText(montage.AtlasPath, $"\tTextureHeight={montage.Height},\n\tTextureAspectRatio={textureAspectRatio},\n");
|
|
|
|
for (var i = 0; i < montage.PictosCol; i++)
|
|
{
|
|
var linePictos = montage.PictosRow;
|
|
if (i == (montage.PictosCol - 1) && montage.PictosLastRow != 0)
|
|
linePictos = montage.PictosLastRow;
|
|
|
|
for (var j = 0; j < linePictos; j++)
|
|
{
|
|
// u0 v0 are the coordinates of the higher left corner of the image
|
|
// u1 v1 are the coordinates of the lower right corner
|
|
var u0 = ((1.0 * j) / montage.PictosRow);
|
|
var v0 = ((1.0 * i) / montage.PictosCol);
|
|
var u1 = ((1.0 * (j + 1)) / montage.PictosRow);
|
|
var v1 = ((1.0 * (i + 1)) / montage.PictosCol);
|
|
|
|
File.AppendAllText(montage.AtlasPath, $"\t{{index={indexPicto},uv0=vector2dNew({u0},{v0}),uv1=vector2dNew({u1},{v1})}},\n");
|
|
indexPicto++;
|
|
}
|
|
}
|
|
|
|
File.AppendAllText(montage.AtlasPath, FileEnd);
|
|
|
|
}
|
|
#endregion
|
|
|
|
#region Tfi Creation
|
|
|
|
private void CreateTfi(PictosMontage montage)
|
|
{
|
|
var tfiPath = Path.Combine(montage.PictosPath, "montage.png.tfi");
|
|
|
|
if (File.Exists(tfiPath))
|
|
{
|
|
File.Delete(tfiPath);
|
|
}
|
|
|
|
File.AppendAllText(tfiPath,
|
|
"<root>\n" +
|
|
" <TextureConfiguration TargetName=\"ORBIS\" CompressionMode=\"DXT5\" MipmapLevelStart=\"0\" UseMipmap=\"False\" WrapModeX=\"WrapMode_Clamp\" WrapModeY=\"WrapMode_Clamp\" Filter=\"MipmapFilter_Box\" AllowDegradation=\"False\" LodBias=\"0\" AllowDegradationAlpha=\"False\" OneColMode=\"no\" Anisotropy=\"none\" />\n" +
|
|
" <TextureConfiguration TargetName=\"DURANGO\" CompressionMode=\"DXT5\" MipmapLevelStart=\"0\" UseMipmap=\"False\" WrapModeX=\"WrapMode_Clamp\" WrapModeY=\"WrapMode_Clamp\" Filter=\"MipmapFilter_Box\" AllowDegradation=\"False\" LodBias=\"0\" AllowDegradationAlpha=\"False\" OneColMode=\"no\" Anisotropy=\"none\" />\n" +
|
|
" <TextureConfiguration TargetName=\"X360\" CompressionMode=\"DXT5\" MipmapLevelStart=\"1\" UseMipmap=\"False\" WrapModeX=\"WrapMode_Clamp\" WrapModeY=\"WrapMode_Clamp\" Filter=\"MipmapFilter_Box\" AllowDegradation=\"False\" LodBias=\"0\" AllowDegradationAlpha=\"False\" OneColMode=\"no\" Anisotropy=\"none\" />\n" +
|
|
" <TextureConfiguration TargetName=\"PS3\" CompressionMode=\"DXT5\" MipmapLevelStart=\"1\" UseMipmap=\"False\" WrapModeX=\"WrapMode_Clamp\" WrapModeY=\"WrapMode_Clamp\" Filter=\"MipmapFilter_Box\" AllowDegradation=\"False\" LodBias=\"0\" AllowDegradationAlpha=\"False\" OneColMode=\"no\" Anisotropy=\"none\" />\n" +
|
|
" <TextureConfiguration TargetName=\"CAFE\" CompressionMode=\"DXT5\" MipmapLevelStart=\"1\" UseMipmap=\"False\" WrapModeX=\"WrapMode_Clamp\" WrapModeY=\"WrapMode_Clamp\" Filter=\"MipmapFilter_Box\" AllowDegradation=\"False\" LodBias=\"0\" AllowDegradationAlpha=\"False\" OneColMode=\"no\" Anisotropy=\"none\" />\n" +
|
|
" <TextureConfiguration TargetName=\"WII\" CompressionMode=\"DXT5\" MipmapLevelStart=\"2\" UseMipmap=\"False\" WrapModeX=\"WrapMode_Clamp\" WrapModeY=\"WrapMode_Clamp\" Filter=\"MipmapFilter_Box\" AllowDegradation=\"False\" LodBias=\"0\" AllowDegradationAlpha=\"False\" OneColMode=\"no\" Anisotropy=\"none\" />\n" +
|
|
" <TextureConfiguration TargetName=\"NX\" CompressionMode=\"DXT5\" MipmapLevelStart=\"0\" UseMipmap=\"False\" WrapModeX=\"WrapMode_Clamp\" WrapModeY=\"WrapMode_Clamp\" Filter=\"MipmapFilter_Box\" AllowDegradation=\"False\" LodBias=\"0\" AllowDegradationAlpha=\"False\" OneColMode=\"no\" Anisotropy=\"none\" />\n" +
|
|
"</root>\n");
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
} |