Closed160

Vue+Babylon.jsのreact-three⁻fiber的な奴ほしい

にー兄さんにー兄さん

React Three Fiberという
jsx記法的にシーンのオーサリングができるツールがある

これ自体単発のプロジェクトというわけではなく、しっかりメンテナンスされてるし
エコシステムもしっかりしてる
さすが ReactとThreeという感じ

にー兄さんにー兄さん

正直なところ、react three fiberのコードリーディングしたけど全然わからなかった
なんであのコードでここまでのことが実現できているんだろう

にー兄さんにー兄さん

reactやjsxの仕様とかもまだわかっていないので、追い切れていないんだと思う
あと、そうなるとVueでの実装方針とは異なりそう

にー兄さんにー兄さん

pnpm iしたらなんか色々deprecatedなパッケージがあるらしいけど
pnpm run devしたら動いてはいるな

本家はyarnで作られているらしい

にー兄さんにー兄さん

troisjsのBox、Meshコンポーネントのコード
参考になる

TroisJSのコード一部
box.ts
import { meshComponent } from './Mesh'
import { props, createGeometry } from '../geometries/BoxGeometry'

export default meshComponent('Box', props, createGeometry)
Mesh.ts
import { ComponentPropsOptions, ComponentPublicInstance, defineComponent, InjectionKey, watch } from 'vue'
import { BufferGeometry, Material, Mesh as TMesh } from 'three'
import Object3D, { Object3DSetupInterface } from '../core/Object3D'
import { bindProp } from '../tools'

export interface MeshSetupInterface extends Object3DSetupInterface {
  mesh?: TMesh
  geometry?: BufferGeometry
  material?: Material
  loading?: boolean
}

export interface MeshInterface extends MeshSetupInterface {
  setGeometry(g: BufferGeometry): void
  setMaterial(m: Material): void
}

export interface MeshPublicInterface extends ComponentPublicInstance, MeshInterface {}

export const MeshInjectionKey: InjectionKey<MeshPublicInterface> = Symbol('Mesh')

const Mesh = defineComponent({
  name: 'Mesh',
  extends: Object3D,
  props: {
    castShadow: Boolean,
    receiveShadow: Boolean,
  },
  setup(): MeshSetupInterface {
    return {}
  },
  provide() {
    return {
      [MeshInjectionKey as symbol]: this,
    }
  },
  mounted() {
    // TODO : proper ?
    if (!this.mesh && !this.loading) this.initMesh()
  },
  methods: {
    initMesh() {
      const mesh = new TMesh(this.geometry, this.material)

      bindProp(this, 'castShadow', mesh)
      bindProp(this, 'receiveShadow', mesh)

      this.mesh = mesh
      this.initObject3D(mesh)
    },
    createGeometry() {},
    addGeometryWatchers(props: Readonly<ComponentPropsOptions>) {
      Object.keys(props).forEach(prop => {
        // @ts-ignore
        watch(() => this[prop], () => {
          this.refreshGeometry()
        })
      })
    },
    setGeometry(geometry: BufferGeometry) {
      this.geometry = geometry
      if (this.mesh) this.mesh.geometry = geometry
    },
    setMaterial(material: Material) {
      this.material = material
      if (this.mesh) this.mesh.material = material
    },
    refreshGeometry() {
      const oldGeo = this.geometry
      this.createGeometry()
      if (this.mesh && this.geometry) this.mesh.geometry = this.geometry
      oldGeo?.dispose()
    },
  },
  unmounted() {
    // for predefined mesh (geometry/material are not unmounted)
    if (this.geometry) this.geometry.dispose()
    if (this.material) this.material.dispose()
  },
  __hmrId: 'Mesh',
})

export default Mesh

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function meshComponent<P extends Readonly<ComponentPropsOptions>>(
  name: string,
  props: P,
  createGeometry: {(c: any): BufferGeometry}
) {
  return defineComponent({
    name,
    extends: Mesh,
    props,
    created() {
      this.createGeometry()
      this.addGeometryWatchers(props)
    },
    methods: {
      createGeometry() {
        this.geometry = createGeometry(this)
      },
    },
  })
}
にー兄さんにー兄さん

defineComponentでコンポーネントを定義し、
setupでretuernすることで外部からrefした時にメンバにアクセスできるようにする
(useExposeでも達成できそう)

また、injectでコンポーネント自分自身をinjectしている。injectに使うkeyも公開

コンポーネントの継承関係や

Mesh
├─ Box
└─ Torus

といった感じ
これらではMeshをInjectしている

にー兄さんにー兄さん

コンポーネントの種類について考える
Babylonの場合、EngineSceneCamera、……的な構造だけど
使い勝手を考えるならEngineとSceneは一つのコンポーネントにしてしまってよさそうに思える

にー兄さんにー兄さん

BabylonScene的な名前のコンポネントで
canvas生成engine生成scene生成までやりましょう

にー兄さんにー兄さん

現状の実装で心配というか不満な点

  • isInitをRef<bool>で渡して子オブジェクトで判定させてる
    • EngineやSceneはmounted後じゃないと初期化できないので、Sceneの子になってるコンポーネントでsetupに処理書いてもだめ
    • 非同期コンポーネントとか検討したほうが良いのかしら(解決するのか?)
  • propsを変えても反映されない
    • 実際のDOMには変化がないからかもしれないし、うまいやり方があるかもしれない
    • watchEffectするしかない?
にー兄さんにー兄さん

あとboxコンポーネントとかは<script>ではなくdefineComponentとかで全部書きたいな
ここらへんtroisjsを参考にしたい

にー兄さんにー兄さん

昨日はeslintとprettierの設定が終わり、なせかデスパのVSCodeでフォーマットされなくて困ったけど

にー兄さんにー兄さん

あと、boxコンポーネントをdefineComponentで書き直すなど
一応今のところ動作はして要るっぽいけど、slotsが動くか気になるのでMaterialコンポーネントとか作って試す

にー兄さんにー兄さん

あと、イベントに関しては自作する方向で検討
sceneか、もしくは別途OnInitializeなどのイベントをprovideして各種子コンポーネントで使えるようにする

にー兄さんにー兄さん

現在defineComponentを使っているのは

  • tsファイルで書ける
  • provideが使える

という理由だけど、provideさえ<script setup>でできれば移行してもいいのかなぁと思ったので調べたら
setup内でthisを取得する方法があった

https://zenn.dev/dozo/articles/f0741550a579a6

getCurrentInstanceを使えばthisにアクセスでき、exposeにアクセスすればdefineExposeで定義したインターフェースにアクセスできる

にー兄さんにー兄さん

↓みたいにすればSetup内でthisをprovideできる
欠点は、<script setup>内ではexportできないのでInjectionKeyは別のtsファイルに分けておかないといけない点

App.vue
<script setup lang="ts">
import { getCurrentInstance, provide } from "vue";
import { HelloExposedInterface, mainInjectionKey } from "./mainInjection";

const hello = 1;

defineExpose<HelloExposedInterface>({
  hello,
});

const instance = getCurrentInstance();
provide(mainInjectionKey, instance?.exposed as HelloExposedInterface);
</script>
にー兄さんにー兄さん

Vue2での共通化というエバmixinだったけど、Vue3ではComposables使った方法がプラクティスになってるっぽいので、それを踏襲したい

にー兄さんにー兄さん

それこそuseBabyuewとか作って、いい感じに共通化できそうな処理を入れられればいいんだよな
provideの処理とかonInitとか

にー兄さんにー兄さん

あと、thisをprovideする必要があるのかも疑問になってきた
{onInit}なオブジェクトを作ってそれをprovideしてdefineExposeすればいいのでは?

にー兄さんにー兄さん

まぁそういう共通化の勘所を鍛えるために
まずは共通化せずにいくつか動くコンポーネントを作ってみて
その中から共通化できそうな部分をコンポーザブルにしていくのが良さそう

リアクティビティの実装とかもやってしまって、最終的な形を決めたい

にー兄さんにー兄さん

あと、provide/injectは同じキーを指定した場合には直近の親で上書きされる仕様っぽい

にー兄さんにー兄さん

コンポーネントになるかどうかの方針決めとして以下を置いてみた

propsにしたくないかどうか

にー兄さんにー兄さん

cubeのpositionはVector3だしまぁいいかなって思うけど
StandardMaterialのテクスチャはpropsにしたくないので、Materialコンポーネントの子要素にTextureコンポーネントを置きたいよねって感じ

にー兄さんにー兄さん

共通化について
どのコンポーネントにもonInitを実装するのはどうなんだろうという気はしてきた
けど現状はonInit経由でMounted時に親オブジェクトを取得する方式になっている

にー兄さんにー兄さん

例えば

  • onInitはsceneコンポーネントだけに実装
  • 親のオブジェクトを参照したいとき(i.e. Materialコンポーネント内でMeshを参照してmaterialフィールドに設定したいとき)

でもそうすると初期化順がわからない(多分親からたどっていくんだけど幅優先なのか深さ優先なのかわからない)
まぁなのでコンポーネントはみんなEntityという単位にして、全部OnInit実装するのが良いか
それで親となっているコンポーネントから必要なオブジェクトを引数で受け取る)

にー兄さんにー兄さん

どうやって共通化するかのアイデア
例としてMeshとしてBoxやSphereを共通化する

  • MeshPublicIntefaceを定義する
    • たぶんmesh?:MeshとかonInit:EventSystem<Mesh>とか持ってる感じ
  • useMeshコンポーザブルを作成。おそらく引数にはMeshを生成する関数を受け取る
  • useMeshの中で今までやってきた処理を共通化
  • useMeshをBoxコンポーネントやSphereコンポーネントで使用する。(box.vueuseMesh.tsを参照する)

第1段階の共通化としては悪くないような気がするが、
MeshやMaterialなどもEntityとして共通化できる。

useEntity<T>()とかにするのがいいのかな

にー兄さんにー兄さん

コンポーネントとかインターフェースとかコンポーザブルのスケッチをしたい

にー兄さんにー兄さん

injectionKeyやコンポ―ネントインターフェースを外だしして、コンポーネントの共通化をコンポーザブルで行い、個々の実装を<script setup>のコンポーネントで行うという方法にいったん落ち着くなど

にー兄さんにー兄さん

直近だとカメラとかでライトのコンポーネントも作ってみようかなというところ

にー兄さんにー兄さん

プロップスのリアクティビティは実装したさがあるのと
OnInitはエミットしたい

にー兄さんにー兄さん

useCameraを使ってカメラコンポーネントの共通化を行った
こちらも他のコンポーネント同様のやり方でできました

にー兄さんにー兄さん

シーンのインジェクションキーを外だししよう

あと、インジェクションキーとコンポーネントインターフェースが別ファイルになってるのは、同一ファイルにして良さそうと思った

にー兄さんにー兄さん

だんだんと共通化が進んできて、共通化によって生み出されたものもさらに共通化できそうな気がしてきた

にー兄さんにー兄さん

で、新たに依存性注入したいと思ったのが
親のコンポーネントデータ

にー兄さんにー兄さん

これは今後の方針にも酔ってくるのだけれど
シーンを注入してシーンの初期化を検知するか
それとも親コンポーネントを注入して親の初期化イベントを監視するか

にー兄さんにー兄さん

これを書きながら思ったのが、結局親を監視しないと行けなさそう
マテリアルとかは親のインスタンスほしいし、親が作られていないと生成しても意味ないし

にー兄さんにー兄さん

なので、シーンとかマテリアルとかとは別に
「親」というインスタンスを別で注入することにする

にー兄さんにー兄さん

ちょっと気になるのが、provide やり過ぎによる弊害って起きないのかな?ってところ

にー兄さんにー兄さん

あと、アイデアとして
シェーダーコンポーネントを作って

  • シェーダ文字列を指定するプロップスを持つもの
  • slotsの文字列を参照するもの

この2つがあっていい気がしてきた

にー兄さんにー兄さん

シェーダってどんなときに使うんだっけな
シェーダマテリアル作るときが一番多いか?

シェーダマテリアルに使うのであれば、シェーダマテリアルコンポーネントを作って、そいつの中身がシェーダになればよいか
シェーダコンポーネントがわざわざシェーダマテリアルコンポーネントのインスタンス参照するのめんどいので

にー兄さんにー兄さん

今割と興味があるのがメッシュコンポーネントたちのpropsで

Babylonの場合はBoxクラスがないので、Create Boxするとメッシュクラスのインスタンスが返ってくる
なので、create boxの引数で指定するbox特有のパラメータ値は、メッシュでは扱えない

何が言いたいかというと、その引数をpropsで受け取ったとき、変更されてもboxの状態を変えられないのでリアクティブにならない

にー兄さんにー兄さん

同様のことがSphereにも起こっていて
パラメータで指定できるdiameterやsegmentは、あとから変更しようと思っても出来ないわけだ
これはBabyuewに限らず起こるのでしょうがないけども
propsで指定するとリアクティブに変更されると思われるんじゃないか?

にー兄さんにー兄さん

あと、現状の方法と比較できていないけど
TresJSで知ったshallow copyを使った方法にも興味がある

イベントシステムいらなくなるかな?

にー兄さんにー兄さん

やっとLightコンポーネントを追加して、いったん最低限のデモできるコンポーネント群はできたような気がする

にー兄さんにー兄さん

Propsのリアクティブ周りの整備を始めていきたい
ここができるとだいぶVueっぽさ増すからな

にー兄さんにー兄さん

リアクティブ関連で考えたいのは、やはりBabylon系列オブジェクトのShallowRef化
いまuseMeshなどで返しているオブジェクトはShallowRefでいいのかも

にー兄さんにー兄さん

改めてTresJSのコードを見直して気づいたこと

  • use系のコンポーザブルの使い方が間違っているかもしれない
  • pnpmのワークスペースうまく使いたい
  • Viteを使ったライブラリのビルド
にー兄さんにー兄さん

コンポーザブルはユーザが使いやすいような設計にされていなければ
単なるコンポーネントのロジックを切り出すものではないということ

にー兄さんにー兄さん

ロジックを切り出すなら別にコンポーネントディレクトリの中に普通のtsコード書けばいいのか

にー兄さんにー兄さん

TresJSのuseRenderLoopあたりは参考にしたいね
あとイベントもなんとかしないとな

にー兄さんにー兄さん

お久ぶりに作業する
いま出ている課題は

  • use~系のコンポーザブルもどきをなんとかしたい
    • 現状はコンポーネントの共通ロジックになってるだけ
    • かつprovide/injectパターンの共通化だけなので、そもそもそれすらも共通化したい
  • propsのリアクティビティ
  • TransformNodeの親子関係をマークアップで再現したい
  • babylonオブジェクトのShallowRef化
にー兄さんにー兄さん

Boxコンポーネントの中でも監視してみることに
ちゃんと反映されている模様

watch(props.position, (position) => {
  const mesh = getMesh();
  console.log(mesh);
  if (mesh) {
    mesh.position = xyzToVector3(position);
  }
});
にー兄さんにー兄さん

ちなみにこの中で使われてるgetMeshは、useMeshから取得できるgetter。
普通にmeshを渡すだけだと中身が変わらないっぽいので、
最初nullであとからmeshいれても取得先で取得できるものが変わらず、getterにしないといけなかった

にー兄さんにー兄さん

ちな、defineXXX系メソッドはsetupの中じゃないと動作しなさそうだった
つまりコンポーザブルでやっても意味がない

にー兄さんにー兄さん

Babyuewの場合はコンポーネントのpropsからpositionなどのVec3型を取得したいと思っている
その場合、propsが変更されたときのリアクティビティを実現するために、
propsをwatchしている

ただ、required=falseなprops、つまり指定必須ではないpropsに関しては
watchがundefinedを引数で受け付けないため、下記のようにエラーになってしまう

にー兄さんにー兄さん

たんにgetterにするだけではうまく動かないのだけど、
watchのオプションにdeep:trueを設定すると動くようになる

watch(
  () => props.position,
  (position) => {
    const sphere = getMesh();
    if (!sphere) {
      return;
    }

    if (!position) {
      return;
    }

    sphere.position = xyzToVector3(position);
  },
  {
    deep: true, // <- これ
  },
);
にー兄さんにー兄さん

今日も配信しました
https://www.youtube.com/watch?v=cpyo1jVuNQU

にー兄さんにー兄さん

この配信の中では以下を進めました

  • lightコンポーネントのpropsのリアクティビティ実装
    • これにより存在しているコンポーネントのpropsは全部対応できた
  • useXXXとなっていたなんちゃってコンポーザブルをコンポーネント・コアという存在に変更
にー兄さんにー兄さん

ちょっと言葉で残しづらいんだけど、

「複数のpropsをひとつのwatchでトラックしようとしたときに、propsの一つがプリミティブではない場合に、コールバックの引数がpropsすべての方がユニオンで繋がれた形になってしまう現象」に会いました
これVueのバグな気がするな~と

にー兄さんにー兄さん

misskeyのcompositionAPIの記事を読んでいて、
composableでステート管理すればいいのかぁってなった
https://gihyo.jp/article/2023/09/misskey-06

にー兄さんにー兄さん

今は親のコンポーネント取得の意図でprovide/injectを使ってるけど
例えばSceneやEngineを取得するにはcomposableでいいな?
composableにグローバルでアクセスできるステートを持っておいて
getterを提供すればよい

その際にShallowRefでシーンを持っておけばよいか

にー兄さんにー兄さん

「Babylon.jsのAPIがVueで使えるようになる」ライブラリよりも
「Vueで楽しく3Dシーンを作れるライブラリ」を目指したさがある

なんとなく前者のほうが自分の好みっぽさはありつつ、
でもBabylonの方をパズルしてコンポーネントにしていく(つまりアダプタ的な)よりも
VueでBabylonが使える面白さというか、そこをうまく調和させることの難しさからくる楽しさ
みたいなものを開発中に味わいたい

にー兄さんにー兄さん

いや、やっぱりどっちもやりたいな
自分はBabylonもVueも好きだし

そういう人が楽しめるライブラリにしたい

にー兄さんにー兄さん

久しぶりにプロジェクトをvscodeで開いた……最近別のことが忙しくて取り組めていなかったので
キャッチアップだけしたところ

色々やりたいことが浮かんだのでissue化するなど

内部整理みたいなことをしておきたい
本当はリリースするのが先なんだけど、後々響いてきそうなのでなるべく内部整理もやる
しかしやりすぎは禁物
本も出さなきゃいけないのでかなりシビア

にー兄さんにー兄さん

これまで、タスクとして挙がっていた内部のリファクタをやっていた
主にsceneの提供の仕方をprovide/injectからコンポ―サブルに変更したことにより
コンポーネントなどの全体的な改修につながっていたので、そこらへんをよしなにした

にー兄さんにー兄さん

だいたいリファクタ周りはうまく終わったので、
次はnpmパッケージ化の動きをしていこうと思う

にー兄さんにー兄さん

モノレポの環境を試したい
そのためのリポジトリを作りたいな

Babyuewの場合、なぜmonorepoにしたいか(というか複数のリポジトリがあるのか)というと
coreなパッケージとそれに依存するオプションパッケージ、それからPlaygroundが欲しいから

にー兄さんにー兄さん

リポジトリの要件、やりたいこと

  • packages/coreとかにコアライブラリ本体を格納
    • パッケージ名は@babyuewjs/coreがいいかな、Babylonjsにならって
  • packages/coreを@babyuewjs/coreとしてnpmにpublishする
  • playground/にテスト用のプロジェクトを格納。多くあってもしょうがないので一つだけで良さそう
  • playgroundはCIによってGitHub Pagesにデプロイされる
  • babyuewjs/coreはVueのコンポーネントライブラリ的な立ち位置
にー兄さんにー兄さん

こちらの記事をなぞりながらmonorepo構成を作って遊んでいた
普通にこの通りにできた市、turbo便利だなぁとなりましたね
https://zenn.dev/uttk/articles/create-pnpm-monorepo

にー兄さんにー兄さん

さらにこちらにPlaygdorundという別のワークスペースを追加して、lib-aを参照して
Vite+Vueでフロント作ってた

vscode上ではうまく参照で来ていたが、run dev とbuildで

"message" is not exported by "../packages/lib-a/dist/index.js", imported by "src/App.vue?vue&type=script&setup=true&lang.ts".

というエラーが発生。なんかcommonjsくさいと思った

にー兄さんにー兄さん

今の所感

これ、別にPlaygroundは必要ない説が出てきたな?
(もともとViteのlib modeでフロント開発できないと思ってた←未調査)

viteのlib modeはbuild時に有効になるオプションなので、devサーバで開発しているときは関係ないか
その場合、lib modeとdevサーバでエントリポイントを変えなくちゃだな

にー兄さんにー兄さん

そういえば、docsフォルダにドキュメント類を整備してPagesに載せたいな……

にー兄さんにー兄さん

packages以下に、ui-components用のワークスペースを作成し、
コンポーネントをlib modeでbuildしつつ、いつもどおりpnpm run devで開発できるか試した

にー兄さんにー兄さん

現状はこんな感じ

にー兄さんにー兄さん

もしかしたら、index.jsで出力してるけど
型定義がlib.d.tsになってるからマッチしないかもしれない
ここはlib.tsの名前をindex.tsに変更すればうまく動くかな

にー兄さんにー兄さん

なんと2週間弱の時間が経ってしまっていた
書典15ももうすぐでてんやわんやなので、頑張ります

にー兄さんにー兄さん

直近はGitHub Projectsやリポジトリに色々書いていた関係で、
あまりこちらにメモできていなかったのもありそう

にー兄さんにー兄さん

基本的にはbabyuew-monorepo-testbedで作った構成になりそうな予感がしている
基本はpackages/coreの開発になると予想

もしかしたらPlaygroundとかDocsとかが入ってくるかもしれない

にー兄さんにー兄さん

こんな感じになりそう

/
├─ playgrounds/
├─ packages/
│    └─ core/
│        ├─ src/
│        │    ├─ components/
│        │    ├─ composables/
│        │    ├─ data/
│        │    ├─ utils/
│        │    ├─ App.vue
│        │    ├─ index.ts
│        │    └─ main.ts
│        ├─ index.html
│        ├─ package.json
│        ├─ tsconfig.json
│        └─ vite.config.ts
├─ package.json
├─ pnpm-workspace.yaml
├─ turbo.json
└─ README.md
にー兄さんにー兄さん

なぜか、BabyuewSceneコンポーネントのStyleが
参照先のPlaygroundで当たらないという問題が起きた

BabyuewSceneコンポーネントのstyleは最終的にはdist/style.cssに出力されるんだけど、
その内容が読み込まれないらしい

しょうがないけどcssをjs上に書くことで暫定対処
あんまりよくない……
でもリリースを優先したいのと、このプロジェクトは幸いcssをほぼ書かないので

にー兄さんにー兄さん
pnpm build

pnpm -F playground preview

でもうまくplaygroundが動いていることが分かったので良かった
BabyuewJSのGitHub Pagesではもしかしたらdocsをデプロイするかもしれないので
playgroundのデプロイはしないでおく

にー兄さんにー兄さん

リリースフローなどいろいろ検討したが、まずはリリースしてからにしようということで
手元でnpm publishを行うことに

にー兄さんにー兄さん

ドキュメントの整備やGitふbのリリースなどの対応がまだです
やって宣伝します

にー兄さんにー兄さん

いま、依存(非dev)にvueとbabylonが入ってるので、インストール時に自動でそれらもインストールされるけどいいんだっけ

にー兄さんにー兄さん

READMEを書いた。たぶんまだあんまりイケてないんだろうなぁって思うけどないよりは大分マシだな

あと、モノレポっぽく
packages/coreのREADMEをsymlinkでルートディレクトリのREADMEにしてます
これいいわね

にー兄さんにー兄さん

symlinkのコピー先から相対パスで指定されたリンクが解決しっぱする問題が発生中
つまり日本語docsに遷移できない

これは日本語docsもsymlinkで繋げば解決しそう

にー兄さんにー兄さん

lerna-lite使って見てる

  • @lerna-lite/cli
  • @lerna-lite/version
  • @lerna-lite/publish

この3つをインストしてる
次期バージョンの決定やtagのpushをしてくれるので、versionはめっちゃいい、かなりいい
publishは、てっきりnpmのpublishしてくれるのかと思ったけど
できなかった もしかしたらできるかもだけど
モノレポなのもあるかもしれない

にー兄さんにー兄さん

いったんリリースしたのでクローズします
今後はGitHub Projectsなどでタスク管理

いやぁ長かった

にー兄さんにー兄さん

クローズしようと思ったけど
scrapを探すのがめんどくさいので
書典終わるまではオープンにします

このスクラップは2023/11/20にクローズされました