ShieldBitmapNFTのコード解説
遅くなってしまいましたが、ちょっと前に作ったShieldBitmapNFTの紹介とコード解説です。
ShieldBitmapNFTの紹介
NFT第5弾になります。Bitmapライブラリ(@h2uenoさん作)を使ってジェネラティブNFT作ってみました。
- フルオンチェーン
- Polygonメインネット
- ガス代のみ
- ATフィー○ド風の模様をランダム生成
- 256枚限定
- 直コンでmintしてね
紹介ツイート
Polygonscan ←直コンはこちら
OpenSeaで見る
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では
- tokenIDからパラメータを作成 (getParams)
- bmpを作成 (getBmp)
- bmpからsvgを作成 (getSvg)
- attributesを作成
- svgとattributesをJSON(メタデータ)に指定
- JSONをBase64エンコードして返す
という処理を行っています。
今回のNFTではメタデータのattributesに対応しました。trait_type
にColor
とShape
を設定しています。
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
bmpをbase64エンコードして、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を切り替えています。
ちなみに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してみてください。