Three.jsを使った3DフルオンチェーンNFTを作ってみよう【開発】

こんにちは、フルオンチェーンNFTクリエイターのnawooです。

本記事では、以前取り上げたdom氏の制作した3DフルオンチェーンNFT「ROSES」のような、Three.jsを使ったフルオンチェーンNFTを作成する方法について解説します。

nawoo

一般的には「フルオンチェーンNFT」といえばドット絵やSVGが多い印象ですが、Three.jsを使うと『3Dでの豪華なオンチェーン表現』が可能になります。

Three.jsはとても機能が多いので、今回は簡単なサンプルコードを例に「どうやってJavaScriptのコードをフルオンチェーンNFTにするのか」という点を中心に、解説していきます。

でははじめに、この記事の構成について説明します。

STEP
ROSESの仕組みをおさらい
STEP
シンプルなNFTを作ってみよう (Sample1)
STEP
ランダム要素を追加してみよう (Sample2)
STEP
テクスチャを貼ってみよう (Sample3)
STEP
大きなテクスチャを使ってみよう (Sample4)

本記事が、「Three.jsを使ったフルオンチェーンNFTの制作方法」などについて理解したいと思われている方にとって、少しでもお役に立てれば幸いです。

※本記事は一般的な情報提供を目的としたものであり、法的または投資上のアドバイスとして解釈されることを意図したものではなく、また解釈されるべきではありません。ゆえに、特定のFT/NFTの購入を推奨するものではございませんので、あくまで勉強の一環としてご活用ください。

【AD】Nouns DAO JAPAN

Nouns DAO JAPANは世界で一番Nounsを広げるコミュニティを目指します。Discord参加はこちら

目次

ROSESの仕組みをおさらい

出典:OpenSea

ROSESについてはこちらの記事で詳しく解説していますが、要点だけ簡単に説明すると以下の仕組みで作られています。

  • メタデータのanimation_urlでHTMLを返す
  • Three.jsはgzipで圧縮しておき、JavaScriptのfflateライブラリを使って展開する
  • データコントラクトを使ってガス代を安くする

ROSESで使われているThree.jsやfflateのデータコントラクト、データコントラクトを扱うためのDataChunkCompilerコントラクトは、誰でも利用することができます。

nawoo

今回はこれらのコントラクトを使うことで、dom氏のいう「on-chain npm」を体験してみました。

とはいえ、dom氏がデプロイしたEthereumメインネットではガス代等の問題で気軽に試すことが難しいため、今回は同じデータコントラクトをGoerliテストネットにもデプロイしておきました。

コントラクトアドレス (Goerli)
FFlateDataChunk10x5707fFa3fE83303342786265a4fddBEAf61B2af4
FFlateDataChunk20xc2217600DeE61b6160cB07e32714CB67e4d3eEe0
ThreeDataChunk1 (※1)0x6a29761F7913Ae3848BF74df28fb1E9975480FC3
ThreeDataChunk20x42c5521eABdC14C50ED1967edbD424672187CBD4
ThreeDataChunk30xcfbE18E29269B649722174ebD7BDA871383009C3
ThreeDataChunk40xb4550ab1E9507ba5c91a52563c624Ae6463f80B2
ThreeDataChunk50x7e734f6daE766bb5A4dc7F1Ce957e39dC0Cf8f95
ThreeDataChunk60xCb835b9087c57223345D51A960e795f3b251cc0a
ThreeDataChunk70xBe8d24fdf071A5586f35bB120c1b8Bf2baD25e81
ThreeDataChunk80xA213524a7a155a422D09797F3F28E120cEC7E11e
ThreeDataChunk90x60e82790A1EC642EfDFBbD44ed9441C8cD938095
DataChunkCompiler0x139A815344FE764921468F2B4B8DA40b3b4b0618
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="...">の追加 (今回の記事では未使用)

シンプルなNFTを作ってみよう (Sample1)

まずは本章では、物体が回転するだけのシンプルなNFTを作ってみます。

コントラクトの作成

今回は、ROSESのコントラクトを参考にしていますが、ROSESの場合はtokenURI関数のanimation_urlにHTMLを設定しています。

そしてこのHTMLには、以下5つのscriptとスタイルシートが含まれています。

出典:ethereumnavi.com/2022/10/01/what-are-roses/#index_id3

コントラクトのコードは以下です。

// 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が出力されます。

nawoo

後で説明しますが、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のメインコードです。

nawoo

ここでは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>タグを動的に生成しています。

nawoo

この動的に生成した<script>が読み込まれてからでないと、Three.jsを使うことができません。

そのためにwindow.onloadを使って、Three.jsが読み込まれた後にメインコードが実行されるようにしています。

また、それ以外はThree.jsのチュートリアルと同じ基本的なコードです。

ライトはHemisphereLight、 メッシュはTorusKnotGeometryを使ったり、resizeイベントでカメラとレンダラーを再調整し、amimate関数でメッシュを回転させています。

nawoo

HTMLファイルを開くと、物体が回転しているのが確認できます。 (JSFiddleで見る)

埋め込み用JavaScriptコードの作成

作成したJavaScriptのメインコードは、コントラクトのJS_CODEにそのまま埋め込むのではなく、まずデータサイズを減らすためにコードを軽量化(Minify)してから、URLエンコードを2回行います。

nawoo

なぜ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が便利です。

nawoo

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...(略)
nawoo

今回は手作業で行いましたが、何度も行う場合は一括変換するスクリプトを作っておくと便利ですよ。

では、作成したコードをコントラクトのJS_CODE定数に設定します。

// コントラクト (Sample1.sol)
string private constant JS_CODE = "window.onload%253D()%253D%253E%257Bconst%2520e%253D%257Bwidth%253A...(略)";

以上で、コントラクトは完成です。

デプロイ&ミント

デプロイしたら忘れずに、以下をおこなっておきましょう。

  • setThreeAddress関数で、Three.jsのデータコントラクトのアドレス設定
  • setDataCompiler関数で、DataChunkCompilerV2のアドレス設定
nawoo

そして、実際に筆者がミントしたものがこちらです。

出典:testnets.opensea.io/ja/assets/goerli/0x4645cc106437f4c745ac823053bc3986a7bba03c/1

実際にOpenSea画面で見てみると、3Dの物体が回転していることが確認できます。

ランダム要素を追加してみよう (Sample2)

さて、先ほどのNFTでは、何枚ミントしても全て同じ内容でした。

nawoo

でも、どうせなら1枚ごとに別々の内容にしたいですよね。

ということで本章では、先ほどのNFTを修正して、ミントする度にランダムに色や形が変わるジェネラティブNFTを作ってみましょう。

乱数生成について

JavaScriptには、ランダムな値(乱数)を生成するMath.random関数があるのですが、今回はこの関数は使えません。

なぜなら、JavaScriptはブラウザをリロードするたびに実行されますが、Math.random関数は実行される度に異なる値を返すため、 同じtokenIDなのにリロードするたびに内容が変化してしまうからです。

ジェネラティブNFTに必要なのは、tokenIDが同じであれば何度実行しても同じ乱数が得られる、「再現性のある乱数を生成する関数」です。

nawoo

しかし、残念ながらJavaScriptには「再現性のある乱数を生成する関数」が用意されていないので、自分で作る必要があります。

ここで、ROSESではどうやっているのか、実際のコードを見てみましょう。

const o = (o) => (void 0 !== o && (l = o % 2147483647) <= 0 && (l += 2147483646), ((l = (16807 * l) % 2147483647) - 1) / 2147483646);

軽量化(Minify)されているので読みにくいですが、この部分が乱数を生成する関数です。

ここでは、Park&Millerという疑似乱数生成法を使っています。

参考: Wikipedia – 線形合同法#Park&Miller

最初にo(seed)でseedを設定し、以後はo()で0.0〜1.0の乱数を生成します。

これにより、同じseedを与えると同じ乱数が得られるので、seedにtokenIDを設定すればブラウザをリロードしても内容が変わりません。

Randomクラスを作成

今回はROSESのコードを参考にして、Randomクラスを作ってみました。

nawoo

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と同じです。

nawoo

先ほどと同じように、JavaScriptコードをMinifyして2回URLエンコードしてから、コントラクトのJS_CODE定数に埋め込みます。

デプロイ&ミント

デプロイして、setThreeAddress関数とsetDataCompiler関数でアドレス設定しておきます。

今回は5枚ミントしてみました

  1. Sample2 #1
  2. Sample2 #2
  3. Sample2 #3
  4. Sample2 #4
  5. Sample2 #5
nawoo

ちゃんと1枚ごとに色や形が変化していますね。同じtokenIDであればリロードしても、色や形が変わったりしません。
Etherscanからフリーミントできるので、欲しい方はご自由にどうぞ。

テクスチャを貼ってみよう (Sample3)

せっかく3Dを扱うので、次は、テクスチャを使うサンプルを作ってみましょう。

Three.jsでテクスチャを扱うには、TextureLoaderを使います。

通常は外部の画像ファイルを指定しますが、dataURLを使うことで、JavaScriptコード内に画像を埋め込むことができます。

画像をdataURLに変換する

nawoo

使用した画像は以下です。某ゲームを参考に自分で描きました。

また、画像ファイルからdataURLへの変換は、こちらのオンラインツールを使用しました。

JavaScriptコード

立方体(BoxGeometry)を作成して、6面にそれぞれテクスチャを指定します。 コードは以下です。

// 画像をdataURLで定義する
const sidePath = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/...(略)';
const topPath = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/...(略)';
const bottomPath = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/...(略)';

window.onload = () => {
  (略)
  const geometry = new THREE.BoxGeometry();
  const loader = new THREE.TextureLoader(); // TextureLoaderを生成
  const sideTexture = loader.load(sidePath); // dataURLから画像を読み込む
  sideTexture.magFilter = THREE.NearestFilter; // ドットがぼやけないように設定
  const topTexture = loader.load(topPath);
  topTexture.magFilter = THREE.NearestFilter;
  const bottomTexture = loader.load(bottomPath);
  bottomTexture.magFilter = THREE.NearestFilter;
  const materials = [
    new THREE.MeshBasicMaterial({ map: sideTexture }), // マテリアルにテクスチャを設定
    new THREE.MeshBasicMaterial({ map: sideTexture }),
    new THREE.MeshBasicMaterial({ map: topTexture }),
    new THREE.MeshBasicMaterial({ map: bottomTexture }),
    new THREE.MeshBasicMaterial({ map: sideTexture }),
    new THREE.MeshBasicMaterial({ map: sideTexture }),
  ];
  const cube = new THREE.Mesh(geometry, materials); // メッシュを作成
  scene.add(cube);
  (略)
  // mousemoveイベント
  window.addEventListener('mousemove', (event) => {
    cube.rotation.y = (event.clientX / size.width - 0.5) * Math.PI; // -90deg ~ +90deg
    cube.rotation.x = (event.clientY / size.height - 0.5) * Math.PI;
  });
  (略)
};
  • TextureLoaderで画像をテクスチャとして読み込み、マテリアルのmapに指定します。
  • texture.magFilter = THREE.NearestFilterとすると、ドットがぼやけないようになります。
  • mousemoveイベントで立方体がマウスカーソルの位置に応じて回転するようにしてみました。

コントラクトの作成

コントラクトはSample1、Sample2と同じです。

JavaScriptコードをMinifyして2回URLエンコードしてから、コントラクトのJS_CODE定数に埋め込みます。

nawoo

そして、実際に筆者がミントしたものがこちらです。OpenSea画面でマウスを動かすと、立方体が回転します。

出典:testnets.opensea.io/ja/assets/goerli/0xd84a0963459ab2f1ad1c6ff580b8cba923f9da71/1

大きなテクスチャを使ってみよう (Sample4)

nawoo

次は、もっと大きな画像を使ってみます。

以下の画像(パブリックドメイン)を球体に貼り付けて、回転する地球を作ってみましょう。

出典:www.shadedrelief.com/natural3/pages/textures.html

画像データコントラクトの作成

先ほどのSample3で使った画像は400〜600バイトとファイルサイズが小さかったので、dataURL化してJavaScriptコードに埋め込むことができました。

しかし、今回はファイルサイズが大きいので、データコントラクトを使います。

nawoo

画像をデータコントラクトに変換してデプロイしておいて、コントラクトのtokenURI関数で、画像のデータコントラクトを読み込むという方法を使います。

画像ファイルサイズは20,355バイト、dataURL化すると27,162バイトになりましたが、これを分割して2つのデータコントラクトを作成し、デプロイします。

// EarthDataChunk1.sol
pragma solidity ^0.8.1;

contract EarthDataChunk1 {
    string public constant data =
        "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlgAAAEsBAMAAADtE9ClAAAA...(略)";
}
// EarthDataChunk2.sol
pragma solidity ^0.8.1;

contract EarthDataChunk2 {
    string public constant data =
        "2r81cvj+U/B4exZjrna30CwatybB5ryJVPlm0SZlKX5S/vftGtfjiRE/bYgX5sPBRnL9phw...(略)";
}

コントラクトの作成

続いて、コントラクトにsetImageAddress関数を追加します。この関数で画像データコントラクトのアドレスを指定します。

さらに、tokenURI関数に以下の処理を追加します。

  • 2つの画像データコントラクトを読み込んで結合する
  • imageUrl変数としてJavaScriptに画像データを渡す
// Sample4.sol
contract Sample4 is ERC721("Sample4", "SAMPLE4"), Ownable {
    (略)
    IDataChunkCompilerV2 private compiler;
    address[2] private imageAddresses;

    function setImageAddress(address chunk1, address chunk2) public onlyOwner {
        imageAddresses[0] = chunk1;
        imageAddresses[1] = chunk2;
    }

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        (略)
        // データコントラクトから画像を読み込む
        string memory image = compiler.compile2(imageAddresses[0], imageAddresses[1]);

        return
            string.concat(
                compiler.BEGIN_JSON(),
                string.concat(
                    compiler.BEGIN_METADATA_VAR("animation_url", false),
                    compiler.HTML_HEAD(),
                    STYLE_CODE,
                    string.concat(
                        // Three.js
                        compiler.BEGIN_SCRIPT_DATA_COMPRESSED(),
                        threejs,
                        compiler.END_SCRIPT_DATA_COMPRESSED()
                    ),
                    string.concat(
                        // variables
                        compiler.BEGIN_SCRIPT(),
                        compiler.SCRIPT_VAR("tokenId", tokenIdStr, true),
                        compiler.SCRIPT_VAR("imageUrl", image, false), // ← ここでimageUrl変数を追加
                        compiler.END_SCRIPT(),
                        // main 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()
            );
    }
}
nawoo

2つのデータコントラクトを結合するには、DataChunkCompilerV2のcompile2関数を使います。

JavaScriptの変数定義はSCRIPT_VAR関数が便利です。

compiler.SCRIPT_VAR("imageUrl", image, false);

とすると、

var imageUrl = "data:image/png;base64,...";

というJavaScriptコードが、2回URLエンコードされた状態で出力されます。

JavaScriptコード

imageUrl変数に画像データ(dataURL)が入っているので、こちらをテクスチャとして読み込みます。

window.onload = () => {
  (略)
  // メッシュを作成
  const geometry = new THREE.SphereGeometry(1, 80, 40);
  const texture = new THREE.TextureLoader().load(imageUrl); // テクスチャを読み込み
  const material = new THREE.MeshStandardMaterial({ map: texture }); // マテリアルにテクスチャを設定
  material.roughness = 0.4;
  const sphere = new THREE.Mesh(geometry, material); // メッシュ(球体)を作成
  // ライトを設定
  const light1 = new THREE.AmbientLight(0x202020);
  const light2 = new THREE.PointLight();
  light2.position.set(2, 2, 2);
  scene.add(sphere, light1, light2);
  (略)
}

球体(SphereGeometry)を作成して、 TextureLoaderimageUrlからテクスチャを読み込み、マテリアルのmapに指定すれば、地球の完成です。

ライトは、AmbientLightPointLightの2つにしました。

デプロイ&ミント

デプロイして、setThreeAddress関数とsetDataCompiler関数でアドレス設定しておきます。 さらに今回はsetImageAddress関数で画像データコントラクトのアドレスも設定する必要があります。

nawoo

そして、実際に筆者がミントしたものがこちらです。

出典:testnets.opensea.io/ja/assets/goerli/0xd71dfa7e758385483c410b7a9bd374a334dca226/1

まとめ

【AD】Nouns DAO JAPAN

Nouns DAO JAPANは世界で一番Nounsを広げるコミュニティを目指します。Discord参加はこちら


今回は、Three.jsを使ったフルオンチェーンNFTを4つ作ってみながら、ジェネラティブ・テクスチャ・外部データコントラクトの使用例などについて紹介しました。

本記事が、「Three.jsを使ったフルオンチェーンNFTの制作方法」などについて理解したいと思われている方にとって少しでもお役に立ったのであれば幸いです。

また励みになりますので、参考になったという方はぜひTwitterでのシェア・コメントなどしていただけると嬉しいです。

nawoo

Three.jsには多くの機能があるので、アイデア次第でもっと複雑なこともできると思います。ぜひ驚くようなフルオンチェーンNFTを作ってみてください!

なお、本記事のコントラクトやスクリプトはGitHubで公開しています。よければご参考ください。

イーサリアムnaviを運営するSTILL合同会社では、以下などに関するお問い合わせを受け付けております。

  • 広告掲載
  • PR記事の執筆業務
  • リサーチ代行業務
  • アドバイザー業務
  • その他(ご依頼・ご提案・ご相談など)

まずはお気軽に、ご連絡ください。

励みになるので、よかったらSNSなどでシェアしてください!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

フルオンチェーンNFTクリエイター。Lootを見たことがきっかけでNFTやSolidityにハマりました。 今までに作ったフルオンチェーンNFTは以下をご覧ください。

目次