GH python GH Tutorial Grasshopper Three.js

【Grasshopper-Three.js】Grasshopperで作成したモデルのブラウザでの表示と更新

今回はGrasshopperで生成したモデルをブラウザで表示し、Grasshopperでの変更をリアルタイムでブラウザにも反映させていく方法について書いていきます。(※リアルタイムといってもブラウザのリロードは必要です。)
今回はローカルで行いますが、クラウドでも実装可能だと思うので、モデルの閲覧だけならブラウザでチーム共有するみたいなこともできるかと思います。

概要

トップにある画像が全体の流れになります。

  • step1: Grasshopperでモデルの作成
  • step2: Ghpythonコンポーネントを使用して、作成したモデルをfbx形式でExport
  • step3: GhpythonコンポーネントからDraco圧縮を行うリクエストを投げます。
  • step4: Node.jsを使用して.fbxで出力したモデルをDraco圧縮してgltf形式で出力します。
  • step5:Three.jsを使用してDraco圧縮をかけたモデルを表示します。
  • step:6: Grasshopperでモデルを変更。=>step2に戻る。

といった流れになります。

step4のDraco圧縮はNode.jsを使って行ってますが、food4Rhinoの以下のアドオンを使用すればstep2でfbxで出力せず、そのままDraco圧縮をかけたモデルを出力できます。

しかし、非推奨になってしまっていたので今回は使用しませんでした。(いちおまだ使うことはできるようです。)

food4Rhino : GLTF-BINEXPORTER (DEPRECATED)

Step1 : Grasshopperでモデル作成 (モデル作成)

上記がGrasshopperのプログラム全体像になってます。

3グループに分かれてますが1グループ目はモデルを生成してます。ここは何でもよくて最終的にBrepで出力されてればOKです。それぞれお好きなモデルで実装してみてください。私は以前記事で書いた以下のモデルを使用します。

Step1 : Grasshopperでモデル作成 (Mesh化して色付け)

  • 作成したモデル(Brep)をFlattenしてMeshコンポーネントに接続してMesh化します
  • Mesh Joinで1つのMeshにまとめます。
  • Meshに色情報を持たせます。Deconstruct Meshで頂点を取得、Deconstructで頂点をxyz成分に分けます。
  • RemapコンポーネントとBoundsコンポーネントを使用して、xyz成分それぞれを0 ~ 1にRemapします。
  • Colour RGB(f)コンポーネントにRemapした値をRGBに接続します。
  • Mesh Colourコンポーネントに接続してMeshの頂点に色情報を持たせて完了です。私の場合は以下のようなモデルが出来上がりました。

step2: ghpythonコンポーネントを使用してモデルをExport

作成したMeshをfbx形式でExportしていきます。プラグインElefrontを使用しています、インストールしてない方はインストールお願いします。

  • ElefrontのBake Objectsコンポーネントを使用して作成したMeshをBakeしていきます。この時、BakeNameにBakeするオブジェクトの名前を設定してください。今回は「baked_mesh」としてます。これを設定することで、モデルを変更して再度Bakeした際に変更前のモデルを削除してからBakeしてくれます。
  • ElefrontAttributesコンポーネントを使用してBake先のLayerを指定します。今回は「default」という名前のレイヤにBakeしていきます。
  • これでElefrontのBake ObjectsコンポーネントのBakeボタンを押すと作成したモデルがRhinocerosに「baked_mesh」という名前で「default」レイヤにBakeされます。
  • Ghpythonコンポーネントを用意し、Bake Objectsコンポーネントの出力端子Gをbakedという変数名で、layer_nameという変数名でレイヤ名「default」を受け取ります。
  • Bake ObjectsコンポーネントはBakeすると、出力端子GからBakeしたモデルのGuidを返します。この値が返ってきらpythonが実行するようにコードを書いていきます。
import rhinoscriptsyntax as rs
import Rhino
import scriptcontext as sc
sc.doc = Rhino.RhinoDoc.ActiveDoc

output_folder = "C:\\Users\\81803\\Desktop\\outpuFolder\\"
obj_name = "voronoiSphere"
object_file_name = "{}.fbx".format(obj_name)
file_path = output_folder + object_file_name

if baked != None:
    ids = rs.ObjectsByLayer(layer_name)
    selected_objects = rs.SelectObjects(ids)
    rs.Command("_-SaveAs {} _Enter".format(file_path))
    rs.UnselectAllObjects()
    finish_export = "finish"
  • 上記がpythonのコードになります。1~3行目で必要なモジュールをインポートし、4行目でRhino上のオブジェクトを扱えるようにします。
  • 6行目でoutput先フォルダのpathを指定してます。
  • 7行目で保存するオブジェクトの名前を決めてます。
  • 8行目で保存するデータ形式を指定してます。今回は.fbxで出力します。 (※上記だと object_file_name = voronoiSphere.fbx となります)
  • 9行目で保存後のファイルのpathを指定してます。(※上記だと file_path ="C:\\Users\\81803\\Desktop\\outpuFolder\\voronoiSphere.fbx となります)
  • 11行目以降がファイルをExportするプログラムになります。Bake ObjectsコンポーネントのBakeボタンを押すと変数bakedに値がわたってきてプログラムが実行されます。
  • 12行目でRhinocerosのdefaultレイヤ内のオブジェクトのidを取得し、12行目で受け取ったidのオブジェクトをRhinoceros上で選択します。
  • 13行目でRhinocerosのSaveAsコマンドを実行し、指定したフォルダーにfbx形式で保存されます。
  • 14行目で選択したオブジェクトを解除し、15行目でexportが終わったことを出力します。

Step3,Step4 :モデルの圧縮 (Node.jsでAPI作成)

Node.jsを使用して先ほど出力したfbx形式のモデルをDraco圧縮していきます。

モデルの容量が大きくない場合は圧縮する必要はないかもしれませんが、ある程度容量が大きくなるとブラウザでのロード時間が長くなってしまいます。

ある程度容量の大きなモデルを想定して圧縮処理を加えていきます。今回はNode.jsを使用して圧縮をかけていきますが、Node.js自体の解説は詳しくしていきません。ご自身のお得意な言語で実装して見てください。

また冒頭でも述べましたが、より簡単な方法はfood4Rhino : GLTF-BINEXPORTER (DEPRECATED)のアドオンを使用するとRhinoceros上でDraco圧縮されたファイルをExportできるので、事前にDraco圧縮する設定をしておき、step2で.fbxで出力するところを.gltfで出力すればこのNode.jsを使用せずともDraco圧縮できます。まだダウンロードできますが、DEPRECATEDになっているのでご注意ください。

以下がNode.jsで作成した圧縮処理になります、フレームワークはExpressを使用してます。すべてのコードを確認したい方はgithubをのぞいてみてください。

import { Router } from "express";
import fsExtra from "fs-extra";
const convert = require("fbx2gltf");
const compress = require("gltf-pipeline");

const router = Router();

router.post("/", async (req, res) => {
  const { path, fileName }: { path: string; fileName: string } = req.body;

  //fbx => glb
  const inputPath = path + fileName + ".fbx";
  const outputPath = `${path + fileName}.glb`;
  await convert(inputPath, outputPath);

  // gltfPipeline glb => gltf
  const inputGlbFilePath = outputPath;
  const outputGlbFilePath = `${path + fileName}_converted.gltf`;
  const glbToGltf = compress.glbToGltf;
  const glb = fsExtra.readFileSync(inputGlbFilePath);
  await glbToGltf(glb).then(function (results: any) {
    fsExtra.writeJsonSync(outputGlbFilePath, results.gltf);
  });

  // gltfPipeline gltf => draco_compress
  const processGltf = compress.processGltf;
  const inputCompressFilePath = outputGlbFilePath;
  const compressFilePath = `C:\\Users\\81803\\dev\\three-projects\\grasshopper-three\\public\\model\\${fileName}_compress.gltf`;

  const gltf = fsExtra.readJsonSync(inputCompressFilePath);
  const options = {
    dracoOptions: {
      compressionLevel: 10,
    },
  };

  await processGltf(gltf, options).then(function (results: any) {
    fsExtra.writeJsonSync(compressFilePath, results.gltf);
  });

  res.status(200).json("compression_finished");
});

export default router;
  • POSTメソッドで作成してます。エンドポイントは「http://localhost:3001/api/compression」になるように設定してます。(※githubリポジトリ 要確認)
  • Ghpythonからリクエストを送る想定をしています。保存した.fbxのfileのpathとfileNameを受け取ります。
  • 最初にfbx2gltfを使用してfbx =>glbに変換します。変換の完了を待ってから次の処理に進んでほしいのでawaitしてます。
  • 次にgltfPipelineを使用してglb=>gltf形式に変換します。こちらの変換の完了を待ってから次の処理に進んでほしいので同様にawaitします。
  • その後、gltfPipelineを使用してDraco圧縮をかけてます、こちらも完了を待ちたいのでawaitします。output先は後程立ち上げるThreejsプロジェクトのフォルダを指定してます。Draco圧縮が完了後"compression_finished"をレスポンスで返します。(※本当はglbから一発でDraco圧縮かけたかったのですが、うまくいかず・・・一手間多くかかっております)

これでDraco圧縮するためのAPI完成です。

ちなみに今回のモデルを圧縮すると、以下のようになります。もともと1.58Mbなので圧縮する必要ないのですが、1.58Mb => 338kbまで圧縮できました。

Step3,Step4 :モデルの圧縮 (Ghpythonからリクエストを送る)

前項で作成したエンドポイントに対して、Ghpythonからリクエストを送ります。githubリポジトリはこちら

先ほど作成したGhpythonコンポーネントに追記していきます。

import urllib2
import json
import rhinoscriptsyntax as rs
import Rhino
import scriptcontext as sc
sc.doc = Rhino.RhinoDoc.ActiveDoc

output_folder = "C:\\Users\\81803\\Desktop\\outputFolder\\"
obj_name = "voronoiSphere"
object_file_name = "{}.fbx".format(obj_name)
file_path = output_folder + object_file_name

def compression():
    url = 'http://localhost:3001/api/compression'
    sendValues = {
      'path':output_folder,
      'fileName':obj_name
    }

    sendValuesJson = json.dumps(sendValues).encode("utf8")
    request = urllib2.Request(url,sendValuesJson)
    request.add_header("Content-Type",'application/json')
    request.get_method = lambda: 'POST'
    response = json.load(urllib2.urlopen(request))
    print response

if baked != None:
    ids = rs.ObjectsByLayer(layer_name)
    selected_objects = rs.SelectObjects(ids)
    rs.Command("_-SaveAs {} _Enter _Enter".format(file_path))
    rs.UnselectAllObjects()
    compression()
    finish_export = "finish"
  • 13行目からcompression関数を定義しています。変数urlが作成したエンドポイントで、sendValuesが送信するデータになります。pathとfileNameにfbxファイルのアウトプットフォルダのpathとfile名を渡してます。
  • その後sendValuesをJSON化して21行目にリクエストのbodyにセットしてます。
  • 22行目でbodyの形式をJSONで指定し、23行目でリクエストを送信する際のメソッドをPOSTで指定してます。
  • 24行目でリクエストを送信し、JSON形式でレスポンスが返ってくるようにAPIを記述したので返ってきたJSON形式の値をパースしてます。
  • 25行目でレスポンスをprintしてます。"compression_finished"が返ってくるはずです。
  • 32行目でcompression関数を実行してます。これで、Bake ObjectsコンポーネントのBakeボタンを押すと => bakedに値が入ってGhpythonコンポーネントの処理が走る=>.fbxで出力=>.gltf(圧縮済み)で出力=>"compression_finished"が出力される。といった処理が完成しました。

step5:Three.jsを使用してDraco圧縮をかけたモデルを表示

プロジェクトを立ち上げてDraco圧縮したモデルを表示していきます。今回はReactプロジェクトを立ち上げて、Three.jsもReact Three Fiberを使用しています。詳しい方法は過去の記事で解説してますので以下を参照してください。

プログラムの一部を解説していきます。プログラム全体はgithub参照願います。

import "./App.css";
import * as THREE from "three";
import { Canvas } from "@react-three/fiber";
import { OrbitControls, Stage, useGLTF } from "@react-three/drei";


const material = new THREE.MeshStandardMaterial({
  vertexColors: true,
});

function App() {
  const modelPath = "/model/voronoiSphere_compress.gltf";
  const gltf2 = useGLTF(modelPath);
  gltf2.scene.traverse((child) => {
    if (child instanceof THREE.Mesh) {
      child.material = material;
    }
  });

  return (
    <>
      <Canvas
        style={{ width: "100vw", height: "100vh", background: "#333" }}
        camera={{
          fov: 75,
          near: 0.1,
          far: 20000,
          position: [0, 20, 30],
        }}
      >
        <OrbitControls makeDefault />
        <directionalLight intensity={1.15} />
        <axesHelper scale={200} />
        <Stage intensity={0.02} environment={"city"} adjustCamera={true}>
          <primitive object={gltf2.scene} />
        </Stage>
      </Canvas>
    </>
  );
}

export default App;
  • 12行目のmodelのpathはreactプロジェクトのpublicフォルダ内を指定してます。public/model/voronoiSphere_compress.gltfになります。Node.jsでAPIを作成した際はここに保存されるように指定をしてました。
  • 13行目でmodelをロードして35行目でSceneに追加してます。
  • 7~9行目でマテリアルを作成してます。今回は頂点の色情報を出力しており、それを使用するのでVertexColorsをtrueにします。14~18行目でロードしたモデルのメッシュに対してマテリアルを指定してます。
  • 32,34行目で光源などの環境設定を行い、33行目でxyz軸を表示してます。

これで完了です。

step:6: Grasshopperでモデルを変更

上のgifが実際に動かした際の様子になります。

  • Grasshopperでモデルを変更 => Bakeボタンを押す => Ghpythonが走りfbxでの出力と圧縮が実行 => Three.jsに反映される

といった流れになります。

保存先をクラウドにしてプロジェクトをデプロイしていれば、チームで共有するなども可能になるかと思います。

以上

【参考】

夢は大きく. 対象コースが¥1,600から。

-GH python, GH Tutorial, Grasshopper, Three.js