【问题标题】:Trying to implement a custom image format尝试实现自定义图像格式
【发布时间】:2019-11-15 04:13:45
【问题描述】:

我正在尝试扩展每个像素可以存储在位图中的数据量。

我在 Unity 中创建了一个 tilemap 系统,它为地形类型引用 .PNG 的 r 通道,为 actor 类型引用 g 通道,然后结合 b 和 a 通道​​来创建一个引用单独数据文件的地址,其中我可以为该图块存储不确定数量的数据。

我当前的系统读取 PNG,将其转换为 Color32[],然后从 Color32[] 加载播放器周围的块。这一切都很好,但最终对于目标来说太有限了。

我想将每个像素的数据量扩展到 Color32 的 4 个参数之外。理想情况下,我希望在已知长度的标头中有一个具有宽度、高度、名称、其他数据等的二进制文件,然后能够遍历其余字节并拉出:字节地形、字节演员、字节x2 dataAddress、字节提升、字节光照等...

我这几天一直在做心理操,试图完成这项工作,但在直接使用 Byte [] 时,我似乎遗漏了一些重要信息。我所有的转换尝试都导致了 IndexOutOfRangeException: Array index is out of range.

任何帮助将不胜感激:)

提前致谢

【问题讨论】:

  • 您根本不应该为此使用位图。您可能想编写自己的数据类和序列化程序。
  • 很有趣,但很奇怪。为什么这不是一个漂亮、简单的二进制流?还是压缩的 JSON 之类的?
  • 同意之前的两个cmets .. 制作自己的数据类/格式,并将所需的所有信息存储在一个文件中。这听起来像是一个很大的XY-Problem
  • 你似乎也在问多个问题,也许关于IndexOutOfRangeException 的部分是重要的部分。您想要实现什么目标?我们如何帮助您实现目标?
  • @Corey,我问了多个问题,对此感到抱歉。我决定尝试一个序列化的自定义类。我创建了一个脚本,它读取一个 png 并创建一个 color32[],将 color32[] 中的数据转换为 Class MapData{ Tile[]},然后将 MapData 类写入文件。我的 PNG 是 512x512@100kb。我的自定义文件是 4,865kb。在 MapData (512x512) 中创建了 262144 个图块。知道为什么文件这么大吗?以及如何发布脚本?干杯:)

标签: c# arrays unity3d bitmap game-engine


【解决方案1】:

从 cmets 看来,这是一个相当直接的序列化问题。我们可以通过几种方式做到这一点,而不必担心数组操作,除非你真的想这样做。

我将使用以下类:

class Map
{
    public int Height { get; set; }
    public int Width { get; set; }
    public string Name { get; set; }
    public Tile[] Tiles { get; set; }
}

class Tile
{
    public byte Terrain { get; set; }
    public byte Elevation { get; set; }
    public byte Illumination { get; set; }
    public int ActorID { get; set; }
}

现在,使用快速制造方法和一些(可重复的)随机数,我创建了一个地图,在所有图块中都有一些可怕的随机数据。将 Width 和 Height 都设置为 512,我们有 262,144 个充满随机垃圾的图块。

现在我们可以进行一些序列化...

内置二进制序列化

在为MapTitle 类添加一些[Serializable] 属性后,我们可以使用内置的.NET 方法对其进行序列化。 Map 类现在看起来像这样:

[Serializable]
class Map
{
    // ...all the same members here...
}

序列化的代码如下:

// Usings:
//  using System.Runtime.Serialization;
//  using System.Runtime.Serialization.Formatters.Binary;

var map = GenerateMap();
IFormatter formatter = new BinaryFormatter();
byte[] serbytes;
using (var ms = new MemoryStream())
{
    fmt.Serialize(ms, map);
    serbytes = ms.ToArray();
}

运行它会得到一个 5.5MB 大小的字节数组,这并不是非常好。我们可以压缩它,但由于我有随机数据,它不能很好地压缩——Brotli 压缩为 1.5MB,GZip 压缩为 2.1MB。实际数据可能会更好。这是带有压缩的序列化部分:

// Usings:
//  using System.IO.Compression;
//  using System.Runtime.Serialization;
//  using System.Runtime.Serialization.Formatters.Binary;

var map = GenerateMap();
IFormatter formatter = new BinaryFormatter();
byte[] serbytes;
using (var ms = new MemoryStream)
{
    using (var cmp = new GZipStream(ms, CompressionLevel.Optimal, true))
    {
        fmt.Serialize(ms, map);
    }
    serbytes = ms.ToArray();
}

从字节数组解压反序列化:

Map deser;
using (var ms = new MemoryStream(serbytes))
using (var dec = new GZipStream(ms, CompressionMode.Decompress, true))
{
    deser = (Map)formatter.Deserialize(dec);
}

仅此而已。

使用 ProtoBuf-net

内置序列化程序不一定是一般情况下使用的最佳选择。另一种选择是使用protobuf-net,它的属性更重一些。您需要为要序列化的类和所有成员添加一个属性,如下所示:

[ProtoContract(SkipConstructor = true)]
public class Tile
{
    [ProtoMember(1)]
    public byte Terrain { get; set; }

    [ProtoMember(2)]
    public byte Elevation { get; set; }

    [ProtoMember(3)]
    public byte Illumination { get; set; }

    [ProtoMember(4)]
    public byte Foo { get; set; }

    [ProtoMember(5)]
    public int ActorID { get; set; }
}

[ProtoContract(SkipConstructor = true)]
public class Map
{
    [ProtoMember(1)]
    public int Height { get; set; }
    [ProtoMember(2)]
    public int Width { get; set; }
    [ProtoMember(3)]
    public string Name { get; set; }
    [ProtoMember(4)]
    public Tile[] Tiles { get; set; }
}

现在我们可以像使用内部序列化器一样做同样的事情,在序列化和反序列化方面只有一行不同:

// Usings:
//  using ProtoBuf;

var map = GenerateMap();
byte[] serbytes;
using (var ms = new MemoryStream())
{
    using (var cmp = new GZipStream(ms, CompressionLevel.Optimal, true))
    {
        Serializer.Serialize(ms, map);
    }
    serbytes = ms.ToArray();
}

Map deser;
using (var ms = new MemoryStream(serbyte))
using (var dec = new GZipStream(ms, CompressionMode.Decompress, true))
{
    deser = Serializer.Deserialize<Map>(ms);
}

非常简单,根据我的测试数据,它使用 GZip 压缩得更好 - 1.4MB 与 2.1MB 对于本机序列化程序。

或者自己动手...

如果你真的,真的希望你可以使用BinaryWriterBinaryReader 在每个类上手动创建SerializeDeserialize 方法。优点是文件格式完全在您的控制之下。缺点是你必须自己做所有事情。

例如:

class Map
{
    public int Height { get; set; }
    public int Width { get; set; }
    public string Name { get; set; }
    public Tile[] Tiles { get; set; }

    public void Serialize(Stream stream)
    {
        using (var writer = new BinaryWriter(stream, Encoding.UTF8, true))
        {
            writer.Write(Height);
            writer.Write(Width);
            writer.Write(Name);
            foreach (var tile in Tiles)
            {
                tile.Serialize(stream);
            }
        }
    }

    public static Map Deserialize(Stream stream)
    {
        var res = new Map();
        using (var reader = new BinaryReader(stream, Encoding.UTF8, true))
        {
            res.Height = reader.ReadInt32();
            res.Width = reader.ReadInt32();
            res.Name = reader.ReadString();
            int tileCount = res.Height * res.Width;
            res.Tiles = new Tile[tileCount];
            for (int i = 0; i < tileCount; i++)
            {
                res.Tiles[i] = Tile.Deserialize(stream);
            }
        }
    }
}

使用我一直在使用的测试数据,生成的 GZip 压缩二进制数据略低于 1.1MB,但无论如何这是高熵输入。

结论

二进制序列化可以是简单的也可以是困难的,这取决于您尝试序列化的是什么。压缩只是在开始将序列化数据填充到其中之前在流组合中添加另一个阶段的问题。如果您不想这样做,则不需要您弄乱数组……除了您自己数据中的数组。

注意:在上述所有示例中,您当然可以将 MemoryStream 替换为 FileStream 并直接处理磁盘上的文件。

【讨论】:

  • 感谢科里的出色回复。我最终选择了第三个选项,但是让你像这样把它全部列出来是非常有用的。再次感谢:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-16
  • 1970-01-01
相关资源
最近更新 更多