JD2022-TU1/main/tools/apps/Packager/Tasks/GenAtlasTask.cs

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
}
}