Vue+Babylon.jsのreact-three⁻fiber的な奴ほしい
React Three Fiberという
jsx記法的にシーンのオーサリングができるツールがある
これ自体単発のプロジェクトというわけではなく、しっかりメンテナンスされてるし
エコシステムもしっかりしてる
さすが ReactとThreeという感じ
React Three Fiber色々漁っていた
Example見ると機能がちょっとだけわかる
fiberのGitHub repo
正直なところ、react three fiberのコードリーディングしたけど全然わからなかった
なんであのコードでここまでのことが実現できているんだろう
一番参考になりそうというか、先行事例であるVue-Babylonjsについても調査
ProvideとInject
これを使えば親子間でsceneやengineデータを渡せるかもしれん
ん-なんか
Provide/InjectでMeshとGeometryのやり取りをしているので
参考になるかもしれない
troisjs、npmとしてもinstallできるけど、なんでかあまり推奨されていない?
troisjsのリポジトリをダウンロードしてそこから作ることを求められている
pnpm iしたらなんか色々deprecatedなパッケージがあるらしいけど
pnpm run devしたら動いてはいるな
本家はyarnで作られているらしい
troisjsのBox、Meshコンポーネントのコード
参考になる
TroisJSのコード一部
import { meshComponent } from './Mesh'
import { props, createGeometry } from '../geometries/BoxGeometry'
export default meshComponent('Box', props, createGeometry)
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の場合、Engine
、Scene
、Camera
、……的な構造だけど
使い勝手を考えるならEngineとSceneは一つのコンポーネントにしてしまってよさそうに思える
BabylonScene
的な名前のコンポネントで
canvas生成engine生成scene生成までやりましょう
一応それっぽいことができた!
ただ中身があまりよろしくない。モヤッとする実装になってる
現状の実装で心配というか不満な点
- isInitをRef<bool>で渡して子オブジェクトで判定させてる
- EngineやSceneはmounted後じゃないと初期化できないので、Sceneの子になってるコンポーネントでsetupに処理書いてもだめ
- 非同期コンポーネントとか検討したほうが良いのかしら(解決するのか?)
- propsを変えても反映されない
- 実際のDOMには変化がないからかもしれないし、うまいやり方があるかもしれない
- watchEffectするしかない?
あとboxコンポーネントとかは<script>ではなくdefineComponentとかで全部書きたいな
ここらへんtroisjsを参考にしたい
あと、eslintとprettier入れたい
まずそこからやるか
pnpm create @eslint/config
でeslint系の設定をする。よく覚えてたな自分
こちらの記事と比較
@vue/eslint-config-typescript
以外が入ったっぽいな?
vue eslintのmulti-wordに関するルールをオフにする
"rules": {
"vue/multi-word-component-names": "off"
}
昨日はeslintとprettierの設定が終わり、なせかデスパのVSCodeでフォーマットされなくて困ったけど
あと、boxコンポーネントをdefineComponentで書き直すなど
一応今のところ動作はして要るっぽいけど、slotsが動くか気になるのでMaterialコンポーネントとか作って試す
あと、イベントに関しては自作する方向で検討
sceneか、もしくは別途OnInitializeなどのイベントをprovideして各種子コンポーネントで使えるようにする
研究対象が増えた
Tresjs
なんとなく気分でMaterialコンポーネントを仮作成したりした
やりたいこと
- イベントシステムの実装
- tresjsのコードリーディング
actionsの整備
tresjsのメモ
コンポーネントの実態どこかなーと思ったら
@tresjs/cientosパッケージにありそう
GiHub Actionsの整備でPagesを公開し、それに伴いrepoもPublicにしました
イベントシステムのいい感じに実装でき、
だいぶ良くなってきたぞ
イベントシステムの使い勝手としてはこんな感じに
現在defineComponentを使っているのは
- tsファイルで書ける
- provideが使える
という理由だけど、provideさえ<script setup>でできれば移行してもいいのかなぁと思ったので調べたら
setup内でthisを取得する方法があった
getCurrentInstance
を使えばthisにアクセスでき、exposeにアクセスすればdefineExpose
で定義したインターフェースにアクセスできる
↓みたいにすればSetup内でthisをprovideできる
欠点は、<script setup>内ではexportできないのでInjectionKeyは別のtsファイルに分けておかないといけない点
<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使った方法がプラクティスになってるっぽいので、それを踏襲したい
ここのドキュメントがしっかり描いてある。わかりやすい
コンポーザブルがsetupで呼び出されさえすれば、基本的にライフサイクルフックもwatchも動きそうなので
コンポーネント定義の一部を切り分けられる
それこそuseBabyuew
とか作って、いい感じに共通化できそうな処理を入れられればいいんだよな
provideの処理とかonInitとか
あと、thisをprovideする必要があるのかも疑問になってきた
{onInit}なオブジェクトを作ってそれをprovideしてdefineExposeすればいいのでは?
まぁそういう共通化の勘所を鍛えるために
まずは共通化せずにいくつか動くコンポーネントを作ってみて
その中から共通化できそうな部分をコンポーザブルにしていくのが良さそう
リアクティビティの実装とかもやってしまって、最終的な形を決めたい
あと、provide/injectは同じキーを指定した場合には直近の親で上書きされる仕様っぽい
ArcRotateCameraコンポーネントも作成してみた
コンポーネントになるかどうかの方針決めとして以下を置いてみた
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.vue
でuseMesh.ts
を参照する)
第1段階の共通化としては悪くないような気がするが、
MeshやMaterialなどもEntityとして共通化できる。
useEntity<T>()
とかにするのがいいのかな
コンポーネントとかインターフェースとかコンポーザブルのスケッチをしたい
injectionKeyやコンポ―ネントインターフェースを外だしして、コンポーネントの共通化をコンポーザブルで行い、個々の実装を<script setup>のコンポーネントで行うという方法にいったん落ち着くなど
コンポーネントの内部構造を図示して整理
直近だとカメラとかでライトのコンポーネントも作ってみようかなというところ
プロップスのリアクティビティは実装したさがあるのと
OnInitはエミットしたい
Vue.jsのイベントで発表出来るといいよなぁ
知見
お久しぶりに進捗した
別のことやってました。記事書いたり
useCameraを使ってカメラコンポーネントの共通化を行った
こちらも他のコンポーネント同様のやり方でできました
シーンのインジェクションキーを外だししよう
あと、インジェクションキーとコンポーネントインターフェースが別ファイルになってるのは、同一ファイルにして良さそうと思った
だんだんと共通化が進んできて、共通化によって生み出されたものもさらに共通化できそうな気がしてきた
で、新たに依存性注入したいと思ったのが
親のコンポーネントデータ
あと、アイデアとして
シェーダーコンポーネントを作って
- シェーダ文字列を指定するプロップスを持つもの
- slotsの文字列を参照するもの
この2つがあっていい気がしてきた
今割と興味があるのがメッシュコンポーネントたちのpropsで
Babylonの場合はBoxクラスがないので、Create Boxするとメッシュクラスのインスタンスが返ってくる
なので、create boxの引数で指定するbox特有のパラメータ値は、メッシュでは扱えない
何が言いたいかというと、その引数をpropsで受け取ったとき、変更されてもboxの状態を変えられないのでリアクティブにならない
同様のことがSphereにも起こっていて
パラメータで指定できるdiameterやsegmentは、あとから変更しようと思っても出来ないわけだ
これはBabyuewに限らず起こるのでしょうがないけども
propsで指定するとリアクティブに変更されると思われるんじゃないか?
あと、現状の方法と比較できていないけど
TresJSで知ったshallow copyを使った方法にも興味がある
イベントシステムいらなくなるかな?
Propsのリアクティブ周りの整備を始めていきたい
ここができるとだいぶVueっぽさ増すからな
リアクティブ関連で考えたいのは、やはりBabylon系列オブジェクトのShallowRef化
いまuseMeshなどで返しているオブジェクトはShallowRefでいいのかも
改めてTresJSのコードを見直して気づいたこと
- use系のコンポーザブルの使い方が間違っているかもしれない
- pnpmのワークスペースうまく使いたい
- Viteを使ったライブラリのビルド
コンポーザブルはユーザが使いやすいような設計にされていなければ
単なるコンポーネントのロジックを切り出すものではないということ
あとOSSライブラリとして公開するにあたって、TresJSからいろいろ盗みたい
TresJSのVite設定をみると、vite build実行時に色々されているのがわかる
Vue.jsは何かのビルドスクリプトによってビルドされるっぽい
お久ぶりに作業する
いま出ている課題は
- use~系のコンポーザブルもどきをなんとかしたい
- 現状はコンポーネントの共通ロジックになってるだけ
- かつprovide/injectパターンの共通化だけなので、そもそもそれすらも共通化したい
- propsのリアクティビティ
- TransformNodeの親子関係をマークアップで再現したい
- babylonオブジェクトのShallowRef化
babylonの場合は
- position
- absolutePosition
があるらしい
Vueのpropsのwatchに関する記事
ここらへんだな
つまりprops全体を渡すか、propsの特定のプロパティをgetterで渡すか
現Box
コンポーネントの中でも監視してみることに
ちゃんと反映されている模様
watch(props.position, (position) => {
const mesh = getMesh();
console.log(mesh);
if (mesh) {
mesh.position = xyzToVector3(position);
}
});
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, // <- これ
},
);
配信中に進捗を生んでいた
- box, sphere
- standardMaterial
- arcRotateCamera
に関しては現在露出しているpropsはリアクティブになった
lightはまだ
今日も配信しました
misskeyのcompositionAPIの記事を読んでいて、
composableでステート管理すればいいのかぁってなった
今は親のコンポーネント取得の意図でprovide/injectを使ってるけど
例えばSceneやEngineを取得するにはcomposableでいいな?
composableにグローバルでアクセスできるステートを持っておいて
getterを提供すればよい
その際にShallowRefでシーンを持っておけばよいか
「Babylon.jsのAPIがVueで使えるようになる」ライブラリよりも
「Vueで楽しく3Dシーンを作れるライブラリ」を目指したさがある
なんとなく前者のほうが自分の好みっぽさはありつつ、
でもBabylonの方をパズルしてコンポーネントにしていく(つまりアダプタ的な)よりも
VueでBabylonが使える面白さというか、そこをうまく調和させることの難しさからくる楽しさ
みたいなものを開発中に味わいたい
いや、やっぱりどっちもやりたいな
自分はBabylonもVueも好きだし
そういう人が楽しめるライブラリにしたい
久しぶりにプロジェクトをvscodeで開いた……最近別のことが忙しくて取り組めていなかったので
キャッチアップだけしたところ
色々やりたいことが浮かんだのでissue化するなど
内部整理みたいなことをしておきたい
本当はリリースするのが先なんだけど、後々響いてきそうなのでなるべく内部整理もやる
しかしやりすぎは禁物
本も出さなきゃいけないのでかなりシビア
これまで、タスクとして挙がっていた内部のリファクタをやっていた
主にsceneの提供の仕方をprovide/injectからコンポ―サブルに変更したことにより
コンポーネントなどの全体的な改修につながっていたので、そこらへんをよしなにした
ここらへんのissue
だいたいリファクタ周りはうまく終わったので、
次はnpmパッケージ化の動きをしていこうと思う
Vueのコンポーネントをnpmで配布する記事は結構見つかった
Vue3でかつTypeScriptの型定義も必要なので、そこらへんが乗っているもので勉強したい
この記事、Vueということもあり参考にしたいな
Vuw2のときの公式docsがあった
モノレポの環境を試したい
そのためのリポジトリを作りたいな
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便利だなぁとなりましたね
さらにこちらに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くさいと思った
こちらのdocsに書いてある通りに設定したら治った
なおここにたどり着くときに見たissue
ここで心配が一つ
このissueで言及されているが、Viteだとmonorepoでhot-reloadの機能が無いのではないかと言われている
まずったな~~~
viteのlib modeについての記事
今の所感
これ、別にPlaygroundは必要ない説が出てきたな?
(もともとViteのlib modeでフロント開発できないと思ってた←未調査)
viteのlib modeはbuild時に有効になるオプションなので、devサーバで開発しているときは関係ないか
その場合、lib modeとdevサーバでエントリポイントを変えなくちゃだな
そういえば、docsフォルダにドキュメント類を整備してPagesに載せたいな……
packages以下に、ui-components用のワークスペースを作成し、
コンポーネントをlib modeでbuildしつつ、いつもどおりpnpm run devで開発できるか試した
結果的に、うまく動いていそうな雰囲気がある
詳細な実装はこちらを参照
参考にしたのは以下の記事で、
型定義ファイルの出力にはviteのpluginで対応した
現状はこんな感じ
もしかしたら、index.jsで出力してるけど
型定義がlib.d.tsになってるからマッチしないかもしれない
ここはlib.tsの名前をindex.tsに変更すればうまく動くかな
モノレポのリリースフロー構築にlerna使って見たいな
なんと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
とうとう!!!BabyuewJSの本リポジトリが!!!爆誕しました!!!!!(劇遅)
なぜか、BabyuewSceneコンポーネントのStyleが
参照先のPlaygroundで当たらないという問題が起きた
BabyuewSceneコンポーネントのstyleは最終的にはdist/style.css
に出力されるんだけど、
その内容が読み込まれないらしい
しょうがないけどcssをjs上に書くことで暫定対処
あんまりよくない……
でもリリースを優先したいのと、このプロジェクトは幸いcssをほぼ書かないので
ということで、playgroundからcoreを参照して
シーンのレンダリングに成功!
pnpm build
pnpm -F playground preview
でもうまくplaygroundが動いていることが分かったので良かった
BabyuewJSのGitHub Pagesではもしかしたらdocsをデプロイするかもしれないので
playgroundのデプロイはしないでおく
リリースフローを確認したいな
semantic-releaseはすべてを自動化してくれて便利そうだけど、
コミットするのが大変そうなので
もうちょっと手軽でモノレポ考慮されている物を探したい
lerna-liteどうなんだっけ、調べてみよう
semantic-releaseはこんな感じ
lernaの雰囲気
これも色々やってくれるんだな……
liteじゃないほうのdocsがあった
リリースフローなどいろいろ検討したが、まずはリリースしてからにしようということで
手元でnpm publish
を行うことに
ドキュメントの整備やGitふbのリリースなどの対応がまだです
やって宣伝します
あとCIも整備したい。ビルドくらいしかしないけどね
いま、依存(非dev)にvueとbabylonが入ってるので、インストール時に自動でそれらもインストールされるけどいいんだっけ
ルートに.vscodeがいるのが大丈夫か
READMEを書いた。たぶんまだあんまりイケてないんだろうなぁって思うけどないよりは大分マシだな
あと、モノレポっぽく
packages/coreのREADMEをsymlinkでルートディレクトリのREADMEにしてます
これいいわね
lerna-lite使って見てる
- @lerna-lite/cli
- @lerna-lite/version
- @lerna-lite/publish
この3つをインストしてる
次期バージョンの決定やtagのpushをしてくれるので、versionはめっちゃいい、かなりいい
publishは、てっきりnpmのpublishしてくれるのかと思ったけど
できなかった もしかしたらできるかもだけど
モノレポなのもあるかもしれない
とうとう、体裁を整え
1.0.0-alpha.1をリリースしたことにより、胸を張って公開しました!って言える状態に!
いやぁ長かった でもこれからですね