こんにちは、フルオンチェーンNFTクリエイターのnawooです。
本記事では、以前取り上げたdom氏の制作した3DフルオンチェーンNFT「ROSES」のような、Three.jsを使ったフルオンチェーンNFTを作成する方法について解説します。
一般的には「フルオンチェーンNFT」といえばドット絵やSVGが多い印象ですが、Three.jsを使うと『3Dでの豪華なオンチェーン表現』が可能になります。
Three.jsはとても機能が多いので、今回は簡単なサンプルコードを例に「どうやってJavaScriptのコードをフルオンチェーンNFTにするのか」という点を中心に、解説していきます。
でははじめに、この記事の構成について説明します。
本記事が、「Three.jsを使ったフルオンチェーンNFTの制作方法」などについて理解したいと思われている方にとって、少しでもお役に立てれば幸いです。
※本記事は一般的な情報提供を目的としたものであり、法的または投資上のアドバイスとして解釈されることを意図したものではなく、また解釈されるべきではありません。ゆえに、特定のFT/NFTの購入を推奨するものではございませんので、あくまで勉強の一環としてご活用ください。
イーサリアムnaviの活動をサポートしたい方は、「定期購読プラン」をご利用ください。
ROSESの仕組みをおさらい
ROSESについてはこちらの記事で詳しく解説していますが、要点だけ簡単に説明すると以下の仕組みで作られています。
- メタデータの
animation_url
でHTMLを返す - Three.jsはgzipで圧縮しておき、JavaScriptのfflateライブラリを使って展開する
- データコントラクトを使ってガス代を安くする
ROSESで使われているThree.jsやfflateのデータコントラクト、データコントラクトを扱うためのDataChunkCompiler
コントラクトは、誰でも利用することができます。
今回はこれらのコントラクトを使うことで、dom氏のいう「on-chain npm」を体験してみました。
flowchart explaining the rough process
— dom (@dhof) September 13, 2022
data contracts/sstore2 are well understood at this point so will skip that
main unlock is using compressed data, then decompressing and loading at runtime via an injected helper script in the data uri
“on-chain npm” etc should be possible pic.twitter.com/h8GQnczvLN
コントラクト | アドレス (Goerli) |
---|---|
FFlateDataChunk1 | 0x5707fFa3fE83303342786265a4fddBEAf61B2af4 |
FFlateDataChunk2 | 0xc2217600DeE61b6160cB07e32714CB67e4d3eEe0 |
ThreeDataChunk1 (※1) | 0x6a29761F7913Ae3848BF74df28fb1E9975480FC3 |
ThreeDataChunk2 | 0x42c5521eABdC14C50ED1967edbD424672187CBD4 |
ThreeDataChunk3 | 0xcfbE18E29269B649722174ebD7BDA871383009C3 |
ThreeDataChunk4 | 0xb4550ab1E9507ba5c91a52563c624Ae6463f80B2 |
ThreeDataChunk5 | 0x7e734f6daE766bb5A4dc7F1Ce957e39dC0Cf8f95 |
ThreeDataChunk6 | 0xCb835b9087c57223345D51A960e795f3b251cc0a |
ThreeDataChunk7 | 0xBe8d24fdf071A5586f35bB120c1b8Bf2baD25e81 |
ThreeDataChunk8 | 0xA213524a7a155a422D09797F3F28E120cEC7E11e |
ThreeDataChunk9 | 0x60e82790A1EC642EfDFBbD44ed9441C8cD938095 |
DataChunkCompiler | 0x139A815344FE764921468F2B4B8DA40b3b4b0618 |
DataChunkCompilerV2 (※2) | 0xe4c4400e11Bd8Ab2e4507B5aE4504E60E4A32433 |
- 1) Three.jsのバージョンはr121です。OrbitControlsなどの外部モジュールを使う場合はバージョンに注意してください。
- 2)
DataChunkCompiler.sol
を一部修正してDataChunkCompilerV2.sol
としてデプロイしました。 変更点は次の2点です。- SCRIPT_VAR関数で
omitQuotes=false
の場合にvar "name=value";
となってしまうバグを修正 (正しくはvar name="value";
) - 遅延読み込み用の
<script defer src="...">
の追加 (今回の記事では未使用)
- SCRIPT_VAR関数で
シンプルなNFTを作ってみよう (Sample1)
まずは本章では、物体が回転するだけのシンプルなNFTを作ってみます。
コントラクトの作成
今回は、ROSESのコントラクトを参考にしていますが、ROSESの場合はtokenURI
関数のanimation_url
にHTMLを設定しています。
そしてこのHTMLには、以下5つのscriptとスタイルシートが含まれています。
コントラクトのコードは以下です。
// Sample1.sol
contract Sample1 is ERC721("Sample1", "SAMPLE1"), Ownable {
using Strings for uint256;
IDataChunkCompilerV2 private compiler;
address[9] private threeAddresses;
string private constant STYLE_CODE = "%253Cstyle%253E%252A%257Bmargin (中略) %252Fstyle%253E";
string private constant JS_CODE = "〜"; // ここに script5 (JavaScriptのメインコード)を埋め込む(後述)
function setCompilerAddress(address newAddress) public onlyOwner {
compiler = IDataChunkCompilerV2(newAddress);
}
function setThreeAddress(
address chunk1, address chunk2, address chunk3,
address chunk4, address chunk5, address chunk6,
address chunk7, address chunk8, address chunk9
) public onlyOwner {
threeAddresses[0] = chunk1;
threeAddresses[1] = chunk2;
threeAddresses[2] = chunk3;
threeAddresses[3] = chunk4;
threeAddresses[4] = chunk5;
threeAddresses[5] = chunk6;
threeAddresses[6] = chunk7;
threeAddresses[7] = chunk8;
threeAddresses[8] = chunk9;
}
function mint(uint256 tokenId) public onlyOwner {
_mint(msg.sender, tokenId);
}
function tokenURI(uint256 tokenId) public view override returns (string memory) {
string memory threejs = compiler.compile9(
threeAddresses[0], threeAddresses[1], threeAddresses[2],
threeAddresses[3], threeAddresses[4], threeAddresses[5],
threeAddresses[6], threeAddresses[7], threeAddresses[8]
);
string memory tokenIdStr = tokenId.toString();
return
string.concat(
compiler.BEGIN_JSON(),
string.concat(
compiler.BEGIN_METADATA_VAR("animation_url", false),
compiler.HTML_HEAD(),
STYLE_CODE,
string.concat(
compiler.BEGIN_SCRIPT_DATA_COMPRESSED(),
threejs,
compiler.END_SCRIPT_DATA_COMPRESSED()
),
string.concat(
compiler.BEGIN_SCRIPT(),
compiler.SCRIPT_VAR("tokenId", tokenIdStr, true),
compiler.END_SCRIPT(),
compiler.BEGIN_SCRIPT(),
JS_CODE,
compiler.END_SCRIPT()
),
compiler.END_METADATA_VAR(false)
),
string.concat(compiler.BEGIN_METADATA_VAR("name", false), name(), "%20%23", tokenIdStr, "%22"),
compiler.END_JSON()
);
}
}
DataChunkCompilerV2
コントラクトには、HTMLやメタデータを簡単に作成する関数が用意されています。
例えば、
compiler.BEGIN_SCRIPT(),
compiler.SCRIPT_VAR("tokenId", tokenIdStr, true),
compiler.END_SCRIPT(),
と書くと、
<script>
var tokenId=1;
</script>
のようなHTMLが出力されます。
後で説明しますが、2回URLエンコードされた状態で出力されます。
続いてtokenURI
関数を見てみましょう。ここでscript1〜5とstyleを結合し、animation_url
を作成しています。
compiler.BEGIN_METADATA_VAR("animation_url", false),
compiler.HTML_HEAD(), // script1, 2 を作成
STYLE_CODE, // style を作成
string.concat(
// script3 を作成 (Three.js本体)
compiler.BEGIN_SCRIPT_DATA_COMPRESSED(),
threejs, // データコントラクトから読み込んだ圧縮されたThree.js
compiler.END_SCRIPT_DATA_COMPRESSED()
),
string.concat(
// script4 を作成 (tokenId変数を定義)
compiler.BEGIN_SCRIPT(),
compiler.SCRIPT_VAR("tokenId", tokenIdStr, true),
compiler.END_SCRIPT(),
// script5 を作成 (メインコード)
compiler.BEGIN_SCRIPT(),
JS_CODE,
compiler.END_SCRIPT()
),
compiler.END_METADATA_VAR(false)
Three.jsは、データコントラクトから読み出したものをcompiler.compile9
関数で結合しています。
スタイルシートはSTYLE_CODE
定数(constant)、JavaScriptのメインコード(script5)はJS_CODE
定数で定義しました。
JavaScriptのメインコードを作成したら、後ほどJS_CODE
定数に設定します。
JavaScriptメインコードの作成
続いては、JavaScriptのメインコードです。
ここではThree.jsのチュートリアルを参考にして、物体(TorusKnot)を回転させてみました。
window.onload = () => {
// シーン、カメラ、レンダラーの設定
const size = { width: window.innerWidth, height: window.innerHeight };
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
const camera = new THREE.PerspectiveCamera(75, size.width / size.height, 0.1, 1000);
camera.position.z = 4;
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(size.width, size.height);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// ライトとメッシュの作成と追加
const light = new THREE.HemisphereLight(0xffffff, 0x202020, 1);
const geometry = new THREE.TorusKnotGeometry(1, 0.3, 100, 16, 2, 3);
const material = new THREE.MeshPhongMaterial({ color: 0x00ffff });
const mesh = new THREE.Mesh(geometry, material);
scene.add(light, mesh);
// resizeイベント
window.addEventListener('resize', () => {
size.width = window.innerWidth;
size.height = window.innerHeight;
camera.aspect = size.width / size.height;
camera.updateProjectionMatrix();
renderer.setSize(size.width, size.height);
renderer.setPixelRatio(window.devicePixelRatio);
});
// アニメーション
const clock = new THREE.Clock();
const animate = function () {
requestAnimationFrame(animate);
const delta = clock.getDelta();
mesh.rotation.x += delta;
mesh.rotation.y += delta;
renderer.render(scene, camera);
};
animate();
};
通常のThree.jsプロジェクトであればwindow.onload
に入れる必要はないのですが、 今回は圧縮したThree.jsを展開して<script>
タグを動的に生成しています。
この動的に生成した<script>
が読み込まれてからでないと、Three.jsを使うことができません。
そのためにwindow.onload
を使って、Three.jsが読み込まれた後にメインコードが実行されるようにしています。
また、それ以外はThree.jsのチュートリアルと同じ基本的なコードです。
ライトはHemisphereLight、 メッシュはTorusKnotGeometryを使ったり、resize
イベントでカメラとレンダラーを再調整し、amimate
関数でメッシュを回転させています。
HTMLファイルを開くと、物体が回転しているのが確認できます。 (JSFiddleで見る)
埋め込み用JavaScriptコードの作成
作成したJavaScriptのメインコードは、コントラクトのJS_CODE
にそのまま埋め込むのではなく、まずデータサイズを減らすためにコードを軽量化(Minify)してから、URLエンコードを2回行います。
なぜ2回かというと、メタデータ(JSON)を作成するときにURLエンコードして、さらにtokenURIを作成するときに再度URLエンコードするからです。
A. <script>JavaScriptのコード</script>...
↓ メタデータを作成
B. {animation_url: "data:text/html,(ここにAをURLエンコードしたもの)", name: "..."}
↓ tokenURIを作成
C. data:application/json,(ここにBをURLエンコードしたもの)
では、まずJavaScriptコードを軽量化(Minify)します。
Online JavaScript minifierなどのオンラインツールもありますし、スクリプトやコマンドを使う場合はUglifyJSが便利です。
Minifyしたものが以下です。コメントや改行が削除され、変数名も短くなっています。
window.onload=()=>{const e={width:window.innerWidth,height:window.innerHeight},i=new THREE.Scene,t=...(略)
次に、URLエンコードします。
URIエンコード変換ツールなどのオンラインツールでもいいですし、JavaScriptのencodeURIComponent
関数で変換しても良いです。
window.onload%3D()%3D%3E%7Bconst%20e%3D%7Bwidth%3Awindow.innerWidth%2Cheight%3Awindow.innerHeight%7D...(略)
さらに、もう一度URLエンコードします。
window.onload%253D()%253D%253E%257Bconst%2520e%253D%257Bwidth%253Awindow.innerWidth%252Cheight%253A...(略)
今回は手作業で行いましたが、何度も行う場合は一括変換するスクリプトを作っておくと便利ですよ。
では、作成したコードをコントラクトのJS_CODE
定数に設定します。
// コントラクト (Sample1.sol)
string private constant JS_CODE = "window.onload%253D()%253D%253E%257Bconst%2520e%253D%257Bwidth%253A...(略)";
以上で、コントラクトは完成です。
デプロイ&ミント
デプロイしたら忘れずに、以下をおこなっておきましょう。
setThreeAddress
関数で、Three.jsのデータコントラクトのアドレス設定setDataCompiler
関数で、DataChunkCompilerV2のアドレス設定
そして、実際に筆者がミントしたものがこちらです。
ランダム要素を追加してみよう (Sample2)
さて、先ほどのNFTでは、何枚ミントしても全て同じ内容でした。
でも、どうせなら1枚ごとに別々の内容にしたいですよね。
ということで本章では、先ほどのNFTを修正して、ミントする度にランダムに色や形が変わるジェネラティブNFTを作ってみましょう。
乱数生成について
JavaScriptには、ランダムな値(乱数)を生成するMath.random
関数があるのですが、今回はこの関数は使えません。
なぜなら、JavaScriptはブラウザをリロードするたびに実行されますが、Math.random
関数は実行される度に異なる値を返すため、 同じtokenIDなのにリロードするたびに内容が変化してしまうからです。
ジェネラティブNFTに必要なのは、tokenIDが同じであれば何度実行しても同じ乱数が得られる、「再現性のある乱数を生成する関数」です。
しかし、残念ながらJavaScriptには「再現性のある乱数を生成する関数」が用意されていないので、自分で作る必要があります。
ここで、ROSESではどうやっているのか、実際のコードを見てみましょう。
const o = (o) => (void 0 !== o && (l = o % 2147483647) <= 0 && (l += 2147483646), ((l = (16807 * l) % 2147483647) - 1) / 2147483646);
軽量化(Minify)されているので読みにくいですが、この部分が乱数を生成する関数です。
ここでは、Park&Millerという疑似乱数生成法を使っています。
最初にo(seed)
でseedを設定し、以後はo()
で0.0〜1.0の乱数を生成します。
これにより、同じseedを与えると同じ乱数が得られるので、seedにtokenIDを設定すればブラウザをリロードしても内容が変わりません。
Randomクラスを作成
今回はROSESのコードを参考にして、Randomクラスを作ってみました。
seedを指定して、さまざまなタイプの乱数を取得できます。
const MAX_INT32 = 2147483647;
class Random {
constructor(seed) {
if (seed <= 0) seed += MAX_INT32 - 1;
this._value = seed;
this.int(); // ※1
}
// 0 <= x < 2147483647 の整数を取得
int() {
this._value = (this._value * 48271) % MAX_INT32; // ※2
return this._value;
}
// 0.0 <= x < 1.0 の浮動小数点を取得
float() {
return (this.int() - 1) / (MAX_INT32 - 1);
}
// min <= x < max の整数を取得
intRange(min, max) {
return Math.floor(this.floatRange(min, max));
}
// min <= x < max の浮動小数点を取得
floatRange(min, max) {
return min + (max - min) * this.float();
}
// true or false のブール値を取得
boolean() {
return this.int() % 2 === 0;
}
// 0x000000 <= x <= 0xffffff の色コードを取得
color() {
return this.intRange(0, 0x1000000);
}
}
コンストラクタの引数でseedを指定し、乱数を生成するには int
,float
,intRange
,floatRange
,color
メソッドを使います。color
メソッドでは0x000000〜0xffffffの値が得られるので、Three.jsの色指定に使えます。
// 使用例
const rand = new Random(seed); // seedを指定
const a = rand.int(); // 0 <= a < MAX_INT32
const b = rand.float(); // 0.0 <= b < 1.0
const c = rand.intRange(1, 10); // 1 <= c < 10
const d = rand.intFloat(2.5, 3.5); // 2.5 <= d < 3.5
const e = rand.boolean(); // true or false
const f = rand.color(); // 0x000000 <= f <= 0xffffff
- 1) seedに1,2,3…というtokenIDを渡す場合、Park&Millerの計算式だと最初に生成する乱数は偏りが生じてしまいます。そこで、最初に生成した乱数は使わないように、コンストラクタ内で一度intメソッドを呼び出しておきます。
- 2) ROSESでは乗数に16807を使っていますが、Wikipediaによると48271を使うほうが良いらしいので48271を使いました。
JavaScriptコード
Sample1のコードに、先ほどのRandomクラスを追加しました。
// ランダムな値を取得
const rand = new Random(tokenId);
const bgColor = rand.color(); // 背景色
const geomColor = rand.color(); // 物体の色
const p = rand.intRange(1, 9); // p: 1〜8の整数
const q = rand.intRange(1, 9); // q: 1〜8の整数
// (略)
scene.background = new THREE.Color(bgColor); // 背景色を設定
// (略)
const geometry = new THREE.TorusKnotGeometry(1, 0.2, 200, 32, p, q); // 形を設定
const material = new THREE.MeshPhongMaterial({ color: geomColor }); // 物体の色を設定
Randomクラスを使って、「背景色」「物体の色」「物体(TorusKnotGeometry)の形を決めるp,qの値」をランダムな値に設定します。
これらの値はtokenIDが同じであれば、何度実行しても同じになります。
コントラクトの作成
コントラクトは、Sample1と同じです。
デプロイ&ミント
デプロイして、setThreeAddress
関数とsetDataCompiler
関数でアドレス設定しておきます。
今回は5枚ミントしてみました
ちゃんと1枚ごとに色や形が変化していますね。同じtokenIDであればリロードしても、色や形が変わったりしません。
Etherscanからフリーミントできるので、欲しい方はご自由にどうぞ。
テクスチャを貼ってみよう (Sample3)
この続き: 1,922文字 / 画像6枚
まとめ
今回は、Three.jsを使ったフルオンチェーンNFTを4つ作ってみながら、ジェネラティブ・テクスチャ・外部データコントラクトの使用例などについて紹介しました。
本記事が、「Three.jsを使ったフルオンチェーンNFTの制作方法」などについて理解したいと思われている方にとって少しでもお役に立ったのであれば幸いです。
また励みになりますので、参考になったという方はぜひTwitterでのシェア・コメントなどしていただけると嬉しいです。
🆕記事をアップしました🆕
— イーサリアムnavi🧭 (@ethereumnavi) December 28, 2022
今回は、Three.jsを使った『3DフルオンチェーンNFT』の作り方について解説📝
dom氏の制作した「ROSES」のようなオンチェーン表現が可能になります🌹
年末年始の時間に、新たなweb3開発に挑戦してみたい方などは、この機会にぜひご参考ください!https://t.co/ZlYHTWsUQE
Three.jsには多くの機能があるので、アイデア次第でもっと複雑なこともできると思います。ぜひ驚くようなフルオンチェーンNFTを作ってみてください!
イーサリアムnaviを運営するSTILL合同会社では、web3/crypto関連のリサーチ代行、アドバイザー業務、その他(ご依頼・ご提案・ご相談など)に関するお問い合わせを受け付けております。
まずはお気軽に、こちらからご連絡ください。
- 法人プランLP:https://ethereumnavi.com/lp/corporate/
- Twitter:@STILL_Corp
- メールアドレス:info@still-llc.com