Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

Lorca:ノードで様々な値とアバターの髪色を連動させたい:前提知識編

🔗目次

  1. まえがき
  2. 作りたいもの
  3. Lorcaって?
  4. 環境構築
  5. Flumeを使ってみる
  6. あとがき

🔗まえがき

皆さんどうもこんにちは、株式会社ECN所属のFuseです。
突然ですが皆さんはノードベースエディタ、お好きですか?
私は大好きです。UnrealEngine5やBlenderなど、いつもお世話になっています。
そんなノードベースエディタをReactで作れるライブラリを最近見つけました。
その名はFlume
どうやらUIを作るだけでなくロジックも仕込めるらしく、これとOSCで何か面白いアプリを作れないか?と考えていたところいい案を思いついたので早速執筆に取り掛かることにしました。

chrisjpatty/flume - GitHub

↑目次に戻る

🔗作りたいもの

突然ですが皆さんは2DサンドボックスゲームTerrariaに登場するアイテム群Hair Dyesをご存じでしょうか?
髪の色がHPやMPといった自身の状態や時間帯や高度といった周辺の環境によって動的に変わるようになるアイテムです。
あれをVRChatでも再現したいと思ったのですが、VRChat内で読み取れる情報には限界があります。
そこでOSCを使うことで様々な条件に応じた髪色変更を可能にしてしまおう。
ついでに扱いやすいようにノードベースエディタもつけてしまおうという算段です。
ですが全部一気にやると記事の長さがものすごいことになりそうなので環境構築やキャッチアップを行う学習編と実装を行う実践編に分割します。
↑目次に戻る

🔗Lorcaって?

LorcaはモダンなHTML5デスクトップアプリを作るためのライブラリで、
Electronとは異なりアプリにChromeをバンドルするのではなく、すでにインストールされているものを再利用するためビルド後のサイズが非常に軽量、
重い動作やブラウザからは触れない部分をGo言語に任せることで高速に様々な操作が行えるなどの強みがあります。

zserge/lorca - GitHub

↑目次に戻る

🔗環境構築

まずは環境構築です。いつものようにリポジトリを作成して…

そしたら中に入って go mod init

go mod init 1_osc_app

その後 lorcaやginなどの必要なライブラリを go getします。

go get github.com/zserge/lorca
go get github.com/gin-gonic/gin
go get github.com/gin-contrib/static

その後、リポジトリ内でviteアプリを作成し、ビルドします。

yarn create vite lorca_osc_frontend --template react-ts
cd ./lorca_osc_frontend
yarn install
yarn build

その後、 main.goにビルドしたファイルをginで配信する処理とlorcaでそれを見る処理を書きます。

main.go

package main

import (
    "log"

    "github.com/gin-contrib/static"
    "github.com/gin-gonic/gin"
    "github.com/zserge/lorca"
)

var staticDir = "./lorca_osc_frontend/dist"

func main() {
    go runGin()

    err := runLorca()
    if err != nil {
        log.Fatal(err)
    }
}

// フロントエンドを配信するサーバー
func runGin() {
    router := gin.Default()
    router.Use(static.Serve("/", static.LocalFile(staticDir, true)))
    err := router.Run("127.0.0.1:9000")
    if err != nil {
        log.Fatal(err)
    }
}

// lorcaを起動する
func runLorca() error {
    ui, err := lorca.New("http://localhost:9000", "", 960, 720, "--remote-allow-origins=*") //--remote-allow-origins=*がないと動かないので注意
    if err != nil {
        return err
    }
    <-ui.Done()//ブラウザの終了を待つ
    return nil
}

いつもの画面が表示されれば環境構築は成功です。

↑目次に戻る

🔗Flumeを使ってみる

それでは試しにFlumeを使って簡単な計算機を作ってみましょう。
早速yarn addし、触ってみましょう。

yarn add flume

今回作るのは足し算だけの簡単な計算機です。
まずはコンフィグを生成しポートで使う型とノードを定義していきます。

lorca_osc_frontend/src/const/config.ts

import { Colors, Controls, FlumeConfig } from "flume"

const config = new FlumeConfig()
config
    .addPortType({
        type: "number",
        name: "number",
        label: "数値",
        color: Colors.red,
        controls: [
            Controls.number({
                name: "number",
                label: "数値"
            })
        ]
    })
    .addNodeType({
        type: "number",
        label: "数値",
        description: "数値を出力する。",
        inputs: ports => [
            ports.number()
        ],
        outputs: ports => [
            ports.number()
        ]
    })
    .addNodeType({
        type: "add",
        label: "加算",
        description: "2つの入力を足した結果を出力する。",
        inputs: ports => [
            ports.number({ name: "a", label: "A" }),
            ports.number({ name: "b", label: "B" }),
        ],
        outputs: ports => [
            ports.number({ name: "result", label: "結果" })
        ]
    })
    .addRootNodeType({
        type: "result",
        label: "結果",
        initialWidth: 170,
        inputs: ports => [
            ports.number({
                name: "result",
                label: "結果"
            }),
        ]
    })
export default config

結構長いですが、順を追って説明していきます。

型の定義
    .addPortType({
        type: "number",
        name: "number",
        label: "数値",
        color: Colors.red,
        controls: [
            Controls.number({
                name: "number",
                label: "数値"
            })
        ]
    })

FlumeConfig.addPortTypeメソッドで型を定義できます。
typeはその型の識別子で、一意である必要があります。
nameはポートのデフォルトの名前で、typeと同じ文字列にすることが推奨されています。
labelはノード上に実際に表示される名前で、日本語が使えます。
また、colorを指定することでポートの丸い部分の色を型ごとに決めることができます。
また、controlsを追加すればユーザーに値を入力してもらうこともできます。

ノードの定義

型の定義が終わったら次はノードの定義です。
FlumeConfig.addNodeTypeメソッドを使います。

    .addNodeType({
        type: "number",
        label: "数値",
        description: "数値を出力する。",
        inputs: ports => [
            ports.number()
        ],
        outputs: ports => [
            ports.number()
        ]
    })
    .addNodeType({
        type: "add",
        label: "加算",
        description: "2つの入力を足した結果を出力する。",
        inputs: ports => [
            ports.number({ name: "a", label: "A" }),
            ports.number({ name: "b", label: "B" }),
        ],
        outputs: ports => [
            ports.number({ name: "result", label: "結果" })
        ]
    })

typeは型のときと同じく識別子。やはり一意である必要があります。
labeldescriptionはノードの名前と説明文で、日本語が使えます。
最後にinputoutputで入力ポートと出力ポートを定義すれば完成です。
ポートのnameはロジックの作成時に使うので覚えておきましょう。

ルートノードの定義

最後に終点となるルートノードを定義します。ルートノードへの入力は最終的な出力結果になります。
FlumeConfig.addRootNodeTypeメソッドを使います。

    .addRootNodeType({
        type: "result",
        label: "結果",
        initialWidth: 170,
        inputs: ports => [
            ports.number({
                name: "result",
                label: "結果"
            }),
        ]
    })

パラメータの解説は通常のノードと同じなので割愛します。

エンジンを作る

次にノードのロジック部分を担うエンジンを作っていきます。

lorca_osc_frontend/src/const/config.ts

//@ts-nocheck
import { RootEngine, InputData, FlumeNode } from 'flume'
import config from './config'

const resolvePorts = (portType: string, data: InputData) => {
    switch (portType) {
        case 'string':
            return data.string
        case 'boolean':
            return data.boolean
        case 'number':
            return data.number
        default:
            return data
    }
}
const resolveNodes = (node: FlumeNode, inputValues: InputData) => {
    switch (node.type) {
        case 'number':
            return { number: inputValues.number }
        case 'add':
            return { result: inputValues.a + inputValues.b }
        default:
            return inputValues
    }
}
const engine = new RootEngine(config, resolvePorts, resolveNodes)

export default engine

入力から出力を計算し返却する単純な構造となっております。

ノードエディタとUIをつなぐ

最後に、計算結果をUIに表示できるようにしていきます。

import { NodeEditor, NodeMap, useRootEngine } from "flume";
import { useCallback, useState } from "react";
import config from "./const/config";
import engine from "./const/engine";

const App = () => {
  const [nodes, setNodes] = useState<NodeMap>({})

  useCallback((nodes: NodeMap) => {
    // 無限ループ対策として必ずメモ化する必要がある
    setNodes(nodes)
  }, [])
  const e = useRootEngine(nodes, engine)//先ほど作ったエンジンを使う
  console.log(e)

  return (
    <div style={{ width: 800, height: 600 }}>
      <span>計算結果:{e?.result ?? <br />}</span>
      <NodeEditor
        {...config}
        onChange={setNodes}
        defaultNodes={[
          {
            type: "result",
            x: 190,
            y: -150
          }
        ]}
      />
    </div>
  )
}
export default App

useRootEngineフックにノードのリストと先ほど作ったエンジン、あとは任意で入力値(今回は省略)を渡すことで計算結果を受け取ることができます。
計算結果の型は[inputName: string]: anyになっており入力ポートの名前(今回はresult)を指定することで中身を取り出すことができます。

ビルドする

あとはビルド用のバッチを作成し、走らせれば…

build.bat

@echo off
cd ./lorca_osc_frontend
call yarn install 
call yarn build
cd ../
go generate
go build -ldflags "-H windowsgui" -o lorca-example.exe

無事、デスクトップアプリ化に成功しました!

↑目次に戻る

🔗あとがき

いかがでしたか?今回はFlumeを使ったノードベースエディタの構築とLorcaを使ったデスクトップアプリ化に挑戦しました。
次回は実践編としてOSCを使ったアバターの髪色制御に挑戦していこうと思います。それでは、また次回お会いしましょう。

ライセンス

記事アイキャッチのReact.jsロゴはCC A 4.0 Internationalに基づいて利用しています。

参考: コモンズ証 - 表示 4.0 国際 - Creative Commons


株式会社ECNはPHP、JavaScriptを中心にお客様のご要望に合わせたwebサービス、システム開発を承っております。
ビジネスの最初から最後までをサポートを行い
お客様のイメージに合わせたWebサービス、システム開発、デザインを行います。


BPSアドベントカレンダー2024


関連記事

該当する記事がありません。

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。