Skip to content

Commit

Permalink
Merge pull request #31 from SceneGate/feature/sprites-tiff
Browse files Browse the repository at this point in the history
✨ Implement layered sprite export and import to TIFF format
  • Loading branch information
pleonex authored Sep 19, 2023
2 parents a40888d + c2c5329 commit 9a34d4b
Show file tree
Hide file tree
Showing 14 changed files with 821 additions and 239 deletions.
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<PackageVersion Include="System.Drawing.Common" Version="7.0.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.0.1" />
<PackageVersion Include="BitMiracle.LibTiff.NET" Version="2.4.649" />
<PackageVersion Include="Magick.NET-Q16-HDRI-AnyCPU" Version="13.2.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta1.21216.1" />
<PackageVersion Include="YamlDotNet" Version="13.1.1" />
<PackageVersion Include="BenchmarkDotNet" Version="0.13.7" />
Expand Down
30 changes: 30 additions & 0 deletions src/Texim.Games/Nitro/FullImage2NitroCell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,35 @@ public class FullImage2NitroCell :
{
private FullImage2NitroCellParams parameters;

private bool hasRotationScaling;
private byte rotationScalingGroup;
private bool hasDoubleSize;
private bool isDisabled;
private bool isMosaic;
private ObjectAttributeMemoryMode memoryMode;

public void Initialize(FullImage2NitroCellParams parameters)
{
ArgumentNullException.ThrowIfNull(parameters);

this.parameters = parameters;
base.Initialize(parameters);

// We can only guess the original metadata if every original OAMs have it
// otherwise, as original and new OAMs may differ, it's hard to know.
var originalOams = parameters.ReferenceCell.Segments.Cast<ObjectAttributeMemory>().ToArray();
hasRotationScaling = originalOams.DistinctBy(o => o.HasRotationOrScaling).Count() == 1
&& originalOams[0].HasRotationOrScaling;
rotationScalingGroup = originalOams.DistinctBy(o => o.RotationOrScalingGroup).Count() == 1
? originalOams[0].RotationOrScalingGroup : (byte)0;
hasDoubleSize = originalOams.DistinctBy(obj => obj.HasDoubleSize).Count() == 1
&& originalOams[0].HasDoubleSize;
isDisabled = originalOams.DistinctBy(obj => obj.IsDisabled).Count() == 1
&& originalOams[0].IsDisabled;
isMosaic = originalOams.DistinctBy(obj => obj.IsMosaic).Count() == 1
&& originalOams[0].IsMosaic;
memoryMode = originalOams.DistinctBy(o => o.Mode).Count() == 1
? originalOams[0].Mode : ObjectAttributeMemoryMode.Normal;
}

public override ISprite Convert(FullImage source)
Expand Down Expand Up @@ -66,6 +89,13 @@ protected override IImageSegment AssignImageToSegment(IImageSegment segmentStruc
? NitroPaletteMode.Palette256x1
: NitroPaletteMode.Palette16x16;

nitroCell.HasRotationOrScaling = hasRotationScaling;
nitroCell.RotationOrScalingGroup = rotationScalingGroup;
nitroCell.HasDoubleSize = hasDoubleSize;
nitroCell.IsMosaic = isMosaic;
nitroCell.IsDisabled = isDisabled;
nitroCell.Mode = memoryMode;

return nitroCell;
}
}
102 changes: 71 additions & 31 deletions src/Texim.Games/Nitro/NitroImageSegmentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ namespace Texim.Games.Nitro;
/// </summary>
public class NitroImageSegmentation : IImageSegmentation
{
// We define two modes but only use the second one so far (75% non-transparent)
// For each mode there is a list of tries for width and height.
// First value is the limit and the second is the side.
// From limit to side there must be non-transparent pixels to set it.
// So that it's worthier a bigger cell than two small ones.
private static readonly int[][,] Modes = {
new int[,] { { 32, 64 }, { 16, 32 }, { 8, 16 }, { 0, 8 } }, // 50%
new int[,] { { 48, 64 }, { 24, 32 }, { 8, 16 }, { 0, 8 } }, // 75%
Expand All @@ -44,10 +47,13 @@ public class NitroImageSegmentation : IImageSegmentation

public int CanvasHeight { get; set; } = 256;

public bool SkipTrimming { get; set; }

public SpriteRelativeCoordinatesKind RelativeCoordinates { get; set; } = SpriteRelativeCoordinatesKind.Center;

public (Sprite Sprite, FullImage TrimmedImage) Segment(FullImage frame)
{
(int startX, int startY, FullImage trimmed) = TrimImage(frame);
if (trimmed is null) {
if (SearchNoTransparentPoint(frame, 0) == -1) {
var emptySprite = new Sprite {
Width = 0,
Height = 0,
Expand All @@ -57,27 +63,39 @@ public class NitroImageSegmentation : IImageSegmentation
return (emptySprite, emptyImage);
}

var segments = CreateObjects(trimmed, startX, startY, 0, 0, trimmed.Height);
FullImage objImage;
int startX = 0, startY = 0;
if (SkipTrimming) {
objImage = frame;
} else {
(startX, startY, objImage) = TrimImage(frame);
}

var segments = CreateObjects(objImage, startX, startY, 0, 0, objImage.Height);

// Return new frame
var sprite = new Sprite {
Segments = new Collection<IImageSegment>(segments),
Width = trimmed.Width,
Height = trimmed.Height,
Width = objImage.Width,
Height = objImage.Height,
};
return (sprite, trimmed);
return (sprite, objImage);
}

private List<IImageSegment> CreateObjects(FullImage frame, int startX, int startY, int x, int y, int maxHeight)
{
var segments = new List<IImageSegment>();

// Go to first non-transparent pixel
int newX = SearchNoTransparentPoint(frame, 1, x, y, yEnd: y + maxHeight);
int newY = SearchNoTransparentPoint(frame, 0, x, y, yEnd: y + maxHeight);

if (newY == -1 || newX == -1) {
return segments;
int newX = x, newY = y;
if (!SkipTrimming) {
newX = SearchNoTransparentPoint(frame, 1, x, y, yEnd: y + maxHeight);
newY = SearchNoTransparentPoint(frame, 0, x, y, yEnd: y + maxHeight);

// Only transparent pixels at this point.
if (newY == -1 || newX == -1) {
return segments;
}
}

int diffX = newX - x;
Expand All @@ -88,16 +106,34 @@ private List<IImageSegment> CreateObjects(FullImage frame, int startX, int start
diffY -= diffY % 8;
y = diffY + y;

(int width, int height) = GetObjectSize(frame, x, y, frame.Width, maxHeight - diffY);
// Reach the end of the image
if (startX + x == frame.Width && startY + y == frame.Height) {
return segments;
}

int width, height;

// If our cell is already valid, do not split further.
if (IsValidSize(frame.Width, maxHeight - diffY)) {
width = frame.Width;
height = maxHeight - diffY;
} else {
(width, height) = GetObjectSize(frame, x, y, frame.Width, maxHeight - diffY);
}

if (width != 0 && height != 0) {
var segment = new ImageSegment {
CoordinateX = startX + x - (CanvasWidth / 2),
CoordinateY = startY + y - (CanvasHeight / 2),
CoordinateX = startX + x,
CoordinateY = startY + y,
Width = width,
Height = height,
Layer = 0,
};
if (RelativeCoordinates == SpriteRelativeCoordinatesKind.Center) {
segment.CoordinateX -= CanvasWidth / 2;
segment.CoordinateY -= CanvasHeight / 2;
}

segments.Add(segment);
} else {
// If everything is transparent
Expand Down Expand Up @@ -180,23 +216,27 @@ private List<IImageSegment> CreateObjects(FullImage frame, int startX, int start
Justification = "Readability of the algorithm")]
private static bool IsValidSize(int width, int height)
{
if (width < 0 || width > 64 || width % 8 != 0) {
return false;
}

if (height < 0 || height > 64 || height % 8 != 0) {
return false;
}

if (width == 64 && (height == 8 || height == 16)) {
return false;
}

if ((width == 8 || width == 16) && height == 64) {
return false;
}

return true;
return (width, height) switch {
// Square mode
(8, 8) => true,
(16, 16) => true,
(32, 32) => true,
(64, 64) => true,

// Rectangle horizontal
(16, 8) => true,
(32, 8) => true,
(32, 16) => true,
(64, 32) => true,

// Rectangle vertical
(8, 16) => true,
(8, 32) => true,
(16, 32) => true,
(32, 64) => true,

_ => false,
};
}

private static (int X, int Y, FullImage Trimmed) TrimImage(FullImage image)
Expand Down
Loading

0 comments on commit 9a34d4b

Please sign in to comment.