13
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

EthereumAdvent Calendar 2021

Day 7

full-on-chain pixel art NFT

Last updated at Posted at 2021-12-07

こんにちは。double jump.tokyo CEO 上野です。
仕事が多忙な時期に何も考えずアドベントカレンダーへ参加表明してしまったため、夜中に記事(というかオープンソース)を書くハメになってしまいました。
先ほど公開したオープンソースは「SolidityでBitmapファイルフォーマットの画像を扱う」ライブラリです。
https://github.com/doublejumptokyo/solidity-bitmap

使い方

誰か使ってくれる人がいたらgithubに使い方を書いてくれるだろうというOptimisticな気持ちで最低限書きます。

contracts/Bitmap.sol
Bitmapを扱うライブラリで、struct Bitmapが本体です。initしてsetPixelしていけば、dataにBMPバイナリデータができあがっています。
contracts/BitmapNFT.sol
Bitmap.solの利用例ですが、凝りすぎてしまって複雑かも。
# モチベーション 今年、2021年はまさに(世間の注目を浴びたという意味で)NFT元年と言って良い年でした。アートNFTからゲームで使えるユーティリティNFTまで様々なNFTに価値が認められる中、とりわけfull-on-chain NFTという外部ストレージに依存しないNFTが通の間で嗜好品として重宝されています。 とはいえ、大きな画像データをそのままon-chainに乗せてしまうのは、gas feeも高い上になにより世界共有DBのストレージを食ってしまうのでできれば避けたい。そのため、プログラム生成するGenerativeな画像やベクターデータ(SVG)でコンパクトになった画像がよく使われます。 画像サイズとしてはコンパクトな32*32くらいのドット絵(Pixel Art)もon-chainに乗せるのに適していますが、意外とBitmapフォーマットをそのまま扱うライブラリがないのに気づきました。(SVGでドットをrectで扱うようなものはありますが)Bitmapフォーマットは比較的素直なバイナリフォーマットなので、ちょっと手を加えればSolidityでも扱えるんじゃね?という軽い気持ちで作りはじめました。 # Bitmapファイルフォーマット Bitmapファイルフォーマットについては以下の記事を参考にしました。 https://qiita.com/ledsun/items/9ca815739201ea395ed2 32bit BMP ではなく 24bit BMP なところがお気に入りです。(RGB 8bit*3色=24bitなら32bitは余計) この記事内にあるようにBitmapは54バイトのヘッダー情報を作るところが面倒ですが、ボディ情報はRGBを埋めていくだけなので単純です。リトルエンディアン方式なところだけ注意が必要です。
contracts/Bitmap.sol
pragma solidity ^0.8.0;

struct XY {
    int256 x;
    int256 y;
}

struct RGB {
    bytes1 red;
    bytes1 green;
    bytes1 blue;
}

struct Bitmap {
    bytes data;
    uint256 linePadding;
    uint256 lineSize;
}

library BitmapLib {
    uint256 constant headerSize = 54;
    uint256 constant pixelSize = 3;

    function init(Bitmap memory bitmap, XY memory size) internal pure {
        bitmap.linePadding = (uint256(size.y) * pixelSize) % 4 == 0
            ? 0
            : 4 - ((uint256(size.x) * pixelSize) % 4);
        bitmap.lineSize = uint256(size.x) * pixelSize + bitmap.linePadding;
        uint256 bodySize = bitmap.lineSize * uint256(size.y);
        uint256 fileSize = headerSize + bodySize;
        bitmap.data = new bytes(fileSize);

        // これがBitmapヘッダ領域
        bitmap.data[0] = 0x42;
        bitmap.data[1] = 0x4d;
        setUint32(bitmap, 2, uint32(fileSize));
        setUint32(bitmap, 10, uint32(headerSize));
        setUint32(bitmap, 14, 40);
        setUint32(bitmap, 18, uint32(int32(size.x)));
        setUint32(bitmap, 22, uint32(int32(size.y)));
        setUint16(bitmap, 26, 1);
        setUint16(bitmap, 28, uint16(pixelSize * 8));
        setUint32(bitmap, 34, uint32(bodySize));
    }

    // Bitmapボディ領域は素直に詰めるだけ
    function setPixel(
        Bitmap memory bitmap,
        XY memory position,
        RGB memory pixel
    ) internal pure {
        uint256 index = headerSize +
            uint256(position.y) *
            bitmap.lineSize +
            uint256(position.x) *
            pixelSize;
        bitmap.data[index] = pixel.blue;
        bitmap.data[index + 1] = pixel.green;
        bitmap.data[index + 2] = pixel.red;
    }

    // Bitmapボディ領域をそのまま詰めても良い
    function setBody(Bitmap memory bitmap, bytes memory body) internal pure {
        uint256 bodyLength = body.length;
        require(
            bitmap.data.length == headerSize + bodyLength,
            "invalid body size"
        );
        for (uint256 i = 0; i < bodyLength; i++) {
            bitmap.data[headerSize + i] = body[i];
        }
    }

    // リトルエンディアンでuint32を詰める
    function setUint32(
        Bitmap memory bitmap,
        uint256 offset,
        uint32 value
    ) private pure {
        for (uint256 i = 0; i < 4; i++) {
            bitmap.data[offset + i] = bytes1(uint8(value / (2**(8 * i))));
        }
    }

    // リトルエンディアンでuint16を詰める
    function setUint16(
        Bitmap memory bitmap,
        uint256 offset,
        uint16 value
    ) private pure {
        for (uint256 i = 0; i < 2; i++) {
            bitmap.data[offset + i] = bytes1(uint8(value / (2**(8 * i))));
        }
    }
}

今後

Bitmapファイルフォーマットの保存を考えた場合、もう少し改善の余地がありそう。
いずれにしても、3232や6464くらいのBitmapファイルならある程度コンパクトに保存も可能なので、そのあたりのユースケースが増えてくれば改善していけるかなと思っているが、ひとまずこれくらいコンパクトな仕様で良いだろう。
というか、まずはマイクリくらいの64*64Bitmapなら使えるんじゃないかな、と思う。

13
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?