Now and Nawoo

NFTの制作記録、技術メモ → C#, Solidity, Blockchain, Bitcoin, Ethereum, NFT

ShieldBitmapNFTのコード解説

遅くなってしまいましたが、ちょっと前に作ったShieldBitmapNFTの紹介とコード解説です。

ShieldBitmapNFTの紹介

NFT第5弾になります。Bitmapライブラリ(@h2uenoさん作)を使ってジェネラティブNFT作ってみました。

  • フルオンチェーン
  • Polygonメインネット
  • ガス代のみ
  • ATフィー○ド風の模様をランダム生成
  • 256枚限定
  • 直コンでmintしてね

紹介ツイート
Polygonscan ←直コンはこちら
OpenSeaで見る

f:id:nawoo5:20211224162931p:plain

Bitmapライブラリについて

SolidityでBitmapを扱うライブラリです。24bitカラー専用になっています。

使い方は簡単で、init関数でサイズを指定してBitmapを初期化し、setPixelで指定位置のピクセルに色を指定していくだけです。

GitHub - doublejumptokyo/solidity-bitmap
full-on-chain pixel art NFT - Qiita (紹介記事)

コード解説

MathInt

今回はintを多く使うのでint関連の関数をlibraryにまとめました。

library MathInt {
    function sqrt(int256 x) internal pure returns (int256 y) {...}
    function max(int256 a, int256 b) internal pure returns (int256) {...}
    function min(int256 a, int256 b) internal pure returns (int256) {...}
    function diff(int256 a, int256 b) internal pure returns (int256) {...}
}

int256でのsqrt,max,min,diffを計算しています。 diffは引数aとbの差の絶対値です。

sqrtはStackExchangeの回答からのコピペです。どうしてこれでsqrtの計算になるのか、いまいち理解していません(^^;

Can I square root in Solidity? - Ethereum Stack Exchange

function sqrt(int256 x) internal pure returns (int256 y) {
    int256 z = (x + 1) / 2;
    y = x;
    while (z < y) {
        y = z;
        z = (x / z + z) / 2;
    }
}

tokenURI

tokenURIでは

  1. tokenIDからパラメータを作成 (getParams)
  2. bmpを作成 (getBmp)
  3. bmpからsvgを作成 (getSvg)
  4. attributesを作成
  5. svgとattributesをJSON(メタデータ)に指定
  6. JSONBase64エンコードして返す

という処理を行っています。

今回のNFTではメタデータのattributesに対応しました。trait_typeColorShapeを設定しています。

function tokenURI(uint256 tokenId) public view override returns (string memory) {
    require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

    (XY memory center, uint256 color, uint256 shape, int256 rDelta) = getParams(tokenId);
    bytes memory bmp = getBmp(center, color, shape, rDelta);
    bytes memory svg = getSvg(bmp);

    bytes memory attributes = abi.encodePacked(
        '[{"trait_type":"Color","value":"',
        getColorName(color),
        '"},{"trait_type":"Shape","value":"',
        getShapeName(shape),
        '"}]'
    );

    bytes memory json = abi.encodePacked(
        '{"name": "ShieldBitmapNFT #',
        tokenId.toString(),
        '", "description": "Full on-chain generative bitmap NFT. created by Nawoo (@NowAndNawoo)", ',
        '"image": "data:image/svg+xml;base64,',
        Base64.encode(svg),
        '", "attributes": ',
        attributes,
        "}"
    );
    return string(abi.encodePacked("data:application/json;base64,", Base64.encode(json)));
}

getParams

getParamsではtokenIDから以下の4つのパラメータを作成します。 keccak256で求めたハッシュ値は256ビットあるので、ビットシフトしながら1つのハッシュ値を使いまわしています。

  • center: シールド中心点
  • color: 色のパターン(10種類)
  • shape: シールドの形(4種類)
  • rDelta: 色の繰り返し幅の変化値 (getPixelで出てくるrMin+rDeltaが色の繰り返し幅になる)
function getParams(uint256 tokenId)
    private
    pure
    returns (
        XY memory center,
        uint256 color,
        uint256 shape,
        int256 rDelta
    )
{
    uint256 rand = uint256(keccak256(abi.encodePacked("bmp", tokenId)));
    center.x = int256(rand % (WIDTH - 8)) + 4; // padding:4
    center.y = int256((rand >> 8) % (HEIGHT - 8)) + 4; // padding:4
    color = (rand >> 16) % 10; // 0-9
    shape = (rand >> 24) % 4; // 0-3
    rDelta = int256((rand >> 32) % 10);
}

getSvg

bmpbase64エンコードして、svgのimageタグに設定します。

   function getSvg(bytes memory bmp) private pure returns (bytes memory) {
        return
            abi.encodePacked(
                '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 400 400">',
                '<image width="100%" height="100%" style="image-rendering:pixelated" href="data:image/bmp;base64,',
                Base64.encode(bmp),
                '"/></svg>'
            );
    }

メタデータのimageにbmpを直接指定(※)すると、OpenSeaでは画像が表示されなかったので、このようにsvgのimageタグでbmpを含む方法を使っています。

※下のようなメタデータ(JSON)だと、OpenSeaでは画像が表示されませんでした。

{
  "name": ~,
  "description": ~,
  "image": "data:image/bmp;base64,~"
}

getBmp

いよいよ、メインのBitmapの作成です。 bitmap.initでサイズを指定して初期化し、 y,xのループで全ピクセルについて bitmap.setPixelを呼び出します。

function getBmp(
    XY memory center,
    uint256 color,
    uint256 shape,
    int256 rDelta
) public pure returns (bytes memory) {
    Bitmap memory bitmap;
    bitmap.init(XY(int256(WIDTH), int256(HEIGHT)));
    for (int256 y = 0; y < int256(HEIGHT); y++) {
        for (int256 x = 0; x < int256(WIDTH); x++) {
            XY memory p = XY(x, y);
            RGB memory pixel = getPixel(p, center, color, shape, rDelta);
            bitmap.setPixel(XY(x, y), pixel);
        }
    }
    return bitmap.data;
}

pixelに設定する色(RGB)は次のgetPixelで求めます。

getPixel

function getPixel(
    XY memory p,
    XY memory center,
    uint256 color,
    uint256 shape,
    int256 rDelta
) private pure returns (RGB memory) {
    int256 d;
    int256 dx = MathInt.diff(center.x, p.x);
    int256 dy = MathInt.diff(center.y, p.y);
    int256 rMin;

    if (shape == 0) {
       // circle
        rMin = 5;
        d = MathInt.sqrt(dx * dx + dy * dy);
    } else if (shape == 1) {
       // diamond
        rMin = 7;
        d = dx + dy;
    } else if (shape == 2) {
       // square
        rMin = 4;
        d = MathInt.max(dx, dy);
    } else {
       // octagon
        rMin = 7;
        int256 dmax = MathInt.max(dx, dy);
        int256 dmin = MathInt.min(dx, dy);
        d = dmax + MathInt.max((dmax * 424) / 1024, dmin); // 0.414
    }
    return getColor(rMin + rDelta, d, color);
}

まず、中心点(center)から点p(x,y)までの距離(d)を求めますが、shapeごとに距離(d)の求め方が異なります。 centerとpとのx座標の差分がdx, y座標の差分がdyです。

  • circle: 直線距離
  • diamond: dx+dy (いわゆるマンハッタン距離)
  • square: max(dx,dy)
  • octagon: diamondとsquareの合成

距離(d)が決まれば、getColorで色を取得します。つまり同じ距離(d)であれば同じ色になります。

octagonで出てくる0.414というのはtan(360°/16)です。この角度でsquareとdiamondを切り替えています。

f:id:nawoo5:20211224171606p:plain:w300

ちなみに414/1000ではなく、424/1024としているのは、単なる趣味で意味はありません。 昔のプログラムでは、割り算を高速化するために、分母を2の累乗にするテクがあったなぁと思い出したので書いてみました。 Bitmapを触っていると、昔を思い出したのでつい。Solidityでやる意味はないと思います。

getColor

ここでグラデーションを作成します。

半径(r)と中心からの距離(d)と色パターン(color)から、色(RGB)を求めます。

例えば color==1 (Black-Green) の場合は、 dが0のときは黒、dがrのときは緑になり、その間は徐々に色が変わっていきます。 dがrを超えるとまた黒に戻って、、、を繰り返します。これによってグラデーションがリピートしています。

function getColor(
    int256 r,
    int256 d,
    uint256 color
) private pure returns (RGB memory) {
    uint8 c = uint8(int8(((255 * d) / r) % 256));
    bytes1 c0 = bytes1(c);
    bytes1 c1 = bytes1(255 - c);
    if (color == 1) return RGB(0, c0, 0); // Black-Green
    if (color == 2) return RGB(0, 0, c0); // Black-Blue
    if (color == 3) return RGB(c0, c0, 0); // Black-Yellow(R+G)
    if (color == 4) return RGB(c0, 0, c0); // Black-Fuchsia(R+B)
    if (color == 5) return RGB(0, c0, c0); // Black-Aqua(G+B)
    if (color == 6) return RGB(0xff, c1, c1); // White-Red
    if (color == 7) return RGB(c1, 0xff, c1); // White-Green
    if (color == 8) return RGB(c1, c1, 0xff); // White-Blue
    if (color == 9) return RGB(0xff, c1, 0xff); // White-Fuchsia
   //if (color == 0)
    return RGB(c0, 0, 0); // Black-Red
}

getShapeName, getColorName

attributesで使うためのshapeとcolorの名前を取得します。

function getShapeName(uint256 shape) private pure returns (string memory) {
    return ["Circle", "Diamond", "Square", "Octagon"][shape];
}
function getColorName(uint256 color) private pure returns (string memory) {
   // 略
}

おわりに

Bitmapライブラリを見て、すごい!と思って、勢いで作ったNFTです。 256枚限定ですが、まだまだ余ってるので、どうぞmintしてみてください。