22 Commits

Author SHA1 Message Date
30abea3b94 fix 2025-09-10 20:33:08 +09:00
2c5d41610b fix 2025-09-10 20:32:50 +09:00
2a48c43721 fix 2025-09-10 20:00:43 +09:00
61fd950127 fix post 2025-09-10 19:57:44 +09:00
f9c0e6e872 post 2025-09-10 14:12:34 +09:00
63d1d11843 fix 2025-09-07 10:28:26 +09:00
846dbec5c5 fix 2025-09-06 15:32:22 +09:00
adc1739a0c fix 2025-09-06 12:34:17 +09:00
07376b889d fix 2025-09-06 12:33:28 +09:00
efa24f0c0a fix 2025-09-06 12:28:44 +09:00
e24e736a8f fix 2025-09-06 11:59:08 +09:00
8738f24ff5 fix 2025-08-24 01:48:20 +09:00
de2bf03762 fix 2025-08-24 01:46:04 +09:00
7c020d3081 fix 2025-08-24 01:39:07 +09:00
49bf609eda fix 2025-08-24 00:08:45 +09:00
3e8f3b372e fix 2025-08-19 21:12:33 +09:00
f117aa2cbd fix 2025-08-19 16:48:08 +09:00
1aa1d86a83 fix 2025-08-19 16:37:43 +09:00
57381c88e7 fix post 2025-08-19 16:22:35 +09:00
be22e30697 fix 2025-08-14 12:35:41 +09:00
7a87fb9795 fix post 2025-08-13 22:10:42 +09:00
a6da73f772 fix post 2025-08-13 21:45:30 +09:00
49 changed files with 20 additions and 2311 deletions

View File

@@ -24,7 +24,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '25'
node-version: '21'
- name: Install dependencies
run: |

View File

@@ -29,7 +29,6 @@ $ python main.py
```sh
$ python -m venv venv
$ venv\Scripts\activate
$ pip install -r requirements.txt
$ python main.py
```

View File

@@ -24,7 +24,6 @@ $ python main.py
```sh
$ python -m venv venv
$ venv\Scripts\activate
$ pip install -r requirements.txt
$ python main.py
```

View File

@@ -1,110 +0,0 @@
---
title: "blenderで作ったモデルを改良した"
slug: "ue-blender-model"
date: "2025-09-18"
tags: ["ue", "blender"]
draft: false
---
blenderで作ったモデルは、ueで動かしてみると、なかなか思ったとおりに動かないことは多いです。原因も多種多様で、とても一言では言い表せない。
今まで気になっていたところは以下の2点でした。
1. 指がちゃんと動かない
2. 衣装のすり抜けが気になる
## 指を修正するにはueからblenderへ
blenderで作ったモデルは指がぎこちない動きで、複数の要因が関係しています。特に大きいのが手動で塗っていたウェイトペイント。
しかし、これを完璧に塗り、かつueで動作確認するのはよくありません。なぜなら、blenderとueで動きが異なるからです。それも全く異なるわけではなく微妙に合わないのです。
ということで、ueでまず指の動きがちゃんとできているモデルをblenderに持ってきて、手の部分を移植するというのが今回採用した方法です。
- o: `ue -> blender`
- x: `blender -> ue`
![](/img/ue_blender_model_ai_v0701.png)
![](/img/ue_blender_model_ai_v0702.png)
動きを見るのは、vrm4uの`RTG_UEFN_${name}`を使用します。
blenderの操作方法です。ポイントだけ書いておきます。
1. modelを2つ読み込む。aとbとする。
2. bのboneとbody(object)でいらない部分を削除する。ここでは手の部分だけ残す。 key[x]
3. a, bで大体の位置を合わせる。 key[g, z]
4. bのboneを選択肢、aのboneを選択して統合する。 key[C-j]
5. サイドバーのアーマチュアのところをみて、手のボーンを腕のボーンにいれる(これはモデルによる)。特に手がオフセット維持で指についていることが重要。 key[C-p]
6. bのbody(object)を選択し、モディファイアからaのbodyにデータ転送する。データ転送では、頂点データ、頂点グループを選択。適用する。
7. bのbodyを選択し、aのbodyを選択して統合する。 key[C-j]
8. bodyを編集して、手と腕をつなげる。
あとは、vrm exportの際に指とかのボーンを自動で入れれば動くと思います。
私の場合は、スカートに必要なボーンを追加したりもしました。これはueでの動作を意識しましょう。
## スカートと足の動きを関連付ける
衣装は、`Spine`以下にあるワンピースなので、厳密にはスカートではありませんが、ここではスカートということにします。
このスカートは、3d-modelでは非常に厄介なもので、足の動きに追従できず体に入り込んでしまうのです。
これを解消するためには様々な方法があり、たとえblenderの機能を使って解消しても、ueでは効果がありません。よって、こちらもueから解消するのがベストです。
今回、ABPに`Look At`を使うことで解消しました。
```md
# ABP
## Look At
- Bone to Modify: スカート前、中央
- Look at Target: Spine (中心)
## Look At
- Bone to Modify: スカート前、左
- Look at Target: LeftLeg (左足)
## Look At
- Bone to Modify: スカート前、右
- Look at Target: RightLeg (右足)
```
`Look at Location`の位置は調整してください。私の場合は`0, 50, 0`です。
<iframe width="100%" height="415" src="https://www.youtube.com/embed/3o98Aivn--0?rel=0&showinfo=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
完璧ではないけど、これでもかなり改良されたほう。
## 実践投入
### unique skillのデザインを考える
まず、アイのunique skill(ユニークスキル)のデザインを考えました。
1. カメラワークは正面に移動
2. スロー再生を開始
3. 忍術のようなモーション
4. カメラを通常に戻す
5. 属性の範囲ダメージ
### tatoolsを使って忍術モーションを作る
[tatools](https://www.fab.com/ja/listings/a5d3b60d-b886-4564-bf6d-15d46a8d27fe)を使います。
[https://github.com/threepeatgames/ThreepeatAnimTools](https://github.com/threepeatgames/ThreepeatAnimTools)
使い方は簡単ですが、動画が分かりづらいので、ポイントだけ解説します。pluginの起動、既存のアニメーションの修正、保存です。
1. pluginの起動は、`/Engine/Plugins/ThreepeatAnimTools/Picker/ThreepeatAnimTools_CR_Picker`を起動します。アウトライナーにでもウィンドウを追加しましょう。
2. 修正したいアニメーション(アニメシーケンス)を開いて、`シーケンサで編集 -> コントロールリグにベイク -> CR_UEFNMannyTatoolsRig`します。
3. これでlevel(map)上でレベルシーケンスを開けます。
4. ここからが修正ですが、まず、例えば、腕を選択して向きを変えたとしましょう。これだけでは保存されません。もとに戻ってしまいます。ここで、(1)シーケンサの下にあるアニメーションを削除し、(2)選択している部位のすべてのコンマを削除します。再生してみると編集したとおりになります。
5. 保存は、シーケンサのメニューバーにある保存ボタン(現在のシーケンスとサブシーケンスを保存)を押します。もとのアニメーションを開くと反映されています。
![](https://ue-book.syui.ai/img/0016.png)
### 実戦動画
<iframe width="100%" height="415" src="https://www.youtube.com/embed/tJQ1y-8p1hQ?rel=0&showinfo=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

View File

@@ -1,401 +0,0 @@
---
title: "ue + vrm4u + mac/ios"
slug: "ue-mac"
date: "2025-09-22"
tags: ["ue", "mac"]
draft: false
---
# ue mac/ios support
- ue 5.6.1
- mac 26
- windows 11
## ue for mac
現時点でのxcode26には対応していません。ueを起動する際はxcodeを切り替えます。そうではないとbuild optionが機能しません。(クエリ中になる)
appleの方針で製品のversionは年号になりました。
> ex: mac26, ios26
```sh
xcode-ue () {
disk=hdd
case $1 in
(u | ue) d=/Volumes/${disk}/Xcode.app/Contents/Developer ;;
(*) d=/Applications/Xcode.app/Contents/Developer ;;
esac
sudo xcode-select --reset
sudo xcode-select --switch $d
}
```
ここでは、`/Volumes/${disk}/Xcode.app`をxocde16とします。
buildすると`/$Project/Mac/${Project}.xcarchive`ができます。
```sh
# Finderでアーカイブを右クリック → "Show in Finder"
# .xcarchive を右クリック → "Show Package Contents"
# ./Products/Applications/ai.app をダブルクリック
```
```sh
# ターミナルでTeam IDを確認
$ security find-identity -v -p codesigning
```
## ue for linux
linux版もbuildすることにしました。
- steam osはたしかlinuxだったはず
- ゲームとpixelstreamingをlinux serverで動かせれば楽
[https://dev.epicgames.com/documentation/ja-jp/unreal-engine/linux-development-requirements-for-unreal-engine](https://dev.epicgames.com/documentation/ja-jp/unreal-engine/linux-development-requirements-for-unreal-engine)
必要なものをdownloadして、windows上で環境を整えます。`clang 18.1.0`
ue editorを開いてメニューの`tool -> c++`で何かを作ります。すると、`.sln`がproject rootにできます。できなければ、`.uproject`を右クリックで`.sln`を作ります。
epic launcherでue installerのoption:linuxを再び有効にします。
`.sln`を開いてbuildに`linux`を選択し、右バーのMyProject(Airse)を右クリックでbuildします。pluginなどが対応していないときは`.uproject`を開いて`false`に変更します。対応している場合もbuild errになることがあります。
`wsl ubuntu`なども必要になるかもしれませんが、情報にはありません。
## vrm4u for mac
まずc++のprojectをueで作成します。
`libassimp.a`を生成します。
[https://github.com/ruyo/assimp](https://github.com/ruyo/assimp)
これを`/Plugins/VRM4U/ThirdParty/assimp/lib/Mac/libassimp.a`に置いて、projectで開きます。pluginがbuildされますが、`/Plugins/VRM4U/Binaries/Mac/*`が生成され、これを使うことになります。
```sh
/Plugins/VRM4U/
├── Binaries
│   ├── Mac
│   │   ├── UnrealEditor-VRM4U.dylib
│   │   ├── UnrealEditor-VRM4UCapture.dylib
│   │   ├── UnrealEditor-VRM4UCaptureEditor.dylib
│   │   ├── UnrealEditor-VRM4UEditor.dylib
│   │   ├── UnrealEditor-VRM4UImporter.dylib
│   │   ├── UnrealEditor-VRM4ULoader.dylib
│   │   ├── UnrealEditor-VRM4UMisc.dylib
│   │   ├── UnrealEditor-VRM4URender.dylib
│   │   └── UnrealEditor.modules
├── ThirdParty
│   ├── assimp
│   │   ├── lib
│   │   │   ├── Mac/libassimp.a #このファイル
└── VRM4U.uplugin
```
他のprojectで利用する際は`/Binaries`, `/ThirdParty`をcopyすればいいので、取っておいてください。vrm4uが更新されたときは再びprojectでbuildしたほうがいいですね。
## ue for git
mac/iosでもbuildできるようにすること、そういったprojectを作成することを目指します。
最終的にiosでもプレイできるゲームを作りたいなと思っていて、これは単純なカードを集め、キャラを強化するゲームにしようと考えています。
mac/iosは軽量パッケージとして必要最小限の構成で作る予定です。特に重いworld systemを分離、統合が簡単にできるようにする予定です。
そこで、winにはrsyncがありませんので、gitを使用することにしました。
```sh
$ winget install git.git
$ cd ${project}
$ git pull
```
署名も機能させておきたいので、gpgを使います。commit, pushすると`verify`が付くやつです。
```sh
$ winget install gnupg.gnupg
$ which gpg
$ git config --global gpg.program "C:\Program Files (x86)\gnupg\bin\gpg.exe"
```
現在使っているterminalは作成(パスフレーズ)が動作しないようです。したがって、mac, linuxで作成してimportします。
```sh
$ gpg --full-generate-key
XXX
$ id=XXX
$ gpg --export-secret-keys ${id} > ~/gpg-key-win.asc
---
$ gpg --import C:\Users\${USER}\gpg-key-win.asc
$ rm C:\Users\${USER}\gpg-key-win.asc
$ gpg --edit-key ${id}
trust
5
quit
```
作成したkeyはwinでimportした後はmac, linuxから削除したほうがいいかも。コマンドは書きません。
これをgit-serverに登録しておけばいいでしょう。
```sh
$ gpg --armor --export ${id}
```
あと、`~/.gitconfig`も更新しておきます。
```sh
$ git config --global user.signingkey ${id}
$ git config --global commit.gpgsign true
```
```sh
# 再起動
$ gpgconf --kill gpg-agent
$ gpg-connect-agent /bye
```
# Airse
ゲームのタイトルは`Airse`に決まりました。まだ決まっていなかったのかというと、決まっていませんでした。仮名で作ってきましたが、これを機に根本的な部分を見直しました。
- ai + verse
- [A]irse = `unrealengine` naming rules
## rse
RSE = [R]elativistic [S]tellar [E]volution
> ja: 相対論的恒星進化
## name rule
- app, name, project = Airse
- repo, dir = `ai/rse`
- id = `ai.syui.rse`
file, variable, function, etc. follow the following name rules.
1. use `_` to separate characters after abbreviations such as `CBP`.
2. use `_` to separate characters before numbers.
3. use capital letters for all other names, priority: `ue > repo`
ex: `CBP_CharacterAiSkill_1`
## game system
`[ai, yui, at, world]`
- AUTHOR = Syui
- PROJECT = Airse
```sh
# example
/Content/${AUTHOR}/${PROJECT}/
├── World/ # world system
│ ├── Origin/ # origin system (dream system)
│ └── BGM/ # bgm system
├── Yui/ # yui system
│ ├── Character/ # character system
│ ├── Enemy/ # enemy system
│ ├── Evolution/ # e system (evo system)
│ ├── Voice/ # voice system
│ └── Live/ # v system (live system)
├── AI/ # ai system
│ ├── Action/ # action system
│ └── Status/ # status system
└── AT/ # at system
├── Item/ # item system
├── Card/ # card system
└── Save/ # save system
```
## bad ex
1. `ai/airse` = `[ai] x 2`
2. `syui/ai/rse` = `priority < ue`
```md
[fix]
1. ai/airse -> ai/rse
2. syui/ai/rse -> Syui/Airse
```
## version
今までgame systemのversionでやってきました。game systemのversionはアイのモデルの変化で決定されてきました。
しかし、ue versionがわかったほうがいいので、以下の形式に変更します。
- version: `5.7.0.6.11`
```sh
[ue] [system] [fix]
5.7.0.6.11
{
"version": "5.7.0.6.11",
{
"ue": "5.7.0",
"system": "6",
"fix": "11"
}
}
```
# 開発の方向性
考えに変化があったので、お伝えします。大きなものは以下の2つです。
1. ゲームの完成を目指すが、ちゃんとしたシステムを作ることも目指す
2. 完璧に自信があるものでゲームを作る
## ちゃんとしたシステムを作る
ゲームの完成を目指して、色々と考えやってきましたが、ちゃんとしたシステムを作ることを優先したほうがいいと考えるようになりました。
というのも、ちゃんとしたシステムを作っておけば、それを組み合わせるだけでいろんなゲームを作れるからです。
ゲームを構成する要素、その基本というのは決まっていて、システムも決まっています。例えば、キャラクター操作。それさえ本当にレベルの高いシステムを自分で作れるなら、色々なゲームに応用できますよね。
ゲームを完成させられることは素晴らしいことです。
しかし、ゲーム制作をやめてしまうときはどんなときでしょう。ゲームを完成させるのは本当に大変で、そこに到達できる人は少ないのですが、しかし、到達できた人も、そこでやめてしまう人が多いんじゃないでしょうか。
続けられる人はごく僅かで、大きな理由は2つあると思っています。
一つは成功しなかったこと。続けるだけのメリットを感じられなかったことだと思います。
2つ目は、作ってきたものがどうしようもなく再利用できない状態にあることだと思います。例えば、作ってきたものがシステム化されておらず、他のゲームを作ろうとしたときに利用できない形、ごちゃごちゃで自分にも把握できず、使い回せない、あるいは別のゲームに統合できない状態であること。
そうでなければ、他のアイデアをすぐに試してみようとなりやすい、ゲーム開発のハードルは低くなっているはずです。にも関わらず、ちょっとやってすぐ辞めてしまう人がいます。
それはなぜかというと、作ってきたものがゴチャゴチャで使い物にならなかったときじゃないかと思います。もちろん、本人の熱意とか継続性とか意思とかそういったものもあります。でも、心が折れそうな時にもコントロール可能な環境要因があるはずで、そうした環境を構築する能力も重要なのではないかと思うのです。周りにちゃんとしたシステムがたくさんある、そんな状態を作る、ということ。そういうのを目指していきたいと思い、ちゃんとしたシステムを作ることも優先的に考え始めました。
## 自信があるものでゲームを構築する
インディーズゲーム、特に3dは本当に難しい。私のゲーム開発の方針は、少しずつ決まってきて、注意しなければならないこともわかってきました。
それは、無理をしないこと。無茶をしないと言い換えてもいいでしょう。
しかし、この無茶をしないというのは表現が難しく、本人が無茶だと思っていなくても無茶に含まれることは多いと思います。
例えば、個人が3dでゲームを作ることでしょう。
「いやいや、そんな」と思われるかもしれませんが、細かいところを見ると、個人開発で3dをやるのは、結構な無茶だと今では思います。また、ueを使うこともそれに含まれるかもしれません。
ueや3dを使うと、個人でも大きいものが作れた気になってしまう。広いマップ、リアルな描写、動く3dモデル。
しかし、扱いきれない武器ほど怖いものはありません。初心者の個人開発にとって、それを置いていくほうがいい場合もある。だけどそれを手にとって進んでしまうのです。
この場合、個人ができるのは、その武器に圧倒的な制限をつけ、使える場面を限定することだと思います。
私も自分のゲームをプレイしていて、この部分はよくできているなというところが少しあります。しかし、総合的なゲーム性で考えると、全然ダメですね。
でも、それなら本当によくできた自信がある部分だけでゲームを作ればいいんじゃないかな。
そこには工夫が必要になるかもしれないし、コンセプトが重要になると思いますが、私はそのような結論に至ります。
完璧に動作する部分、バグが少なく、自信があるところ、自分のゲームの最大の魅力、そこだけを使ってゲームを構築することを今は考えています。
面白いかどうかは、正直わかりませんが、パッと見で、少しプレイして、「あれ、これすごいんじゃない」と思わせることができたら成功だと思います。最初はそこを目指していこうかなと。
無理をしてできることを増やしても意味がありません。特に個人開発で、3dで、かつueだと、それはとても危険な気がします。
とはいえ、重要なのは、たくさん作ること。
3dで開発するな、ueを使うな、開発者は好きなものを作るな、と言いたいわけではありません。
言いたいのは、たくさん作ってきたものの中には光るもの、よくできたものがいくつか出てきます。そういったものを使ってゲームを構築する。その方向性でも考えてみる、ということ。
おそらく、3dで作る場合、ueを使う場合、個人開発者が好きなものを作る場合に、このような制限は役に立つと思います。
以上が、最近の個人開発の方向性、あるいは考え方の話です。
## ちゃんとしたシステムをどのように作るのか
1. 名前規則に忠実であること。フォルダ、ファイルや変数、関数などのすべて。例外的な名前規則を付けたものを置く場所を決めておくこと。
2. 簡単にシステムを分離、統合できる状態であること。
3. 依存関係を減らし、ファイルは自分のフォルダのみで動く状態にすること。assetはdownloadするが、できる限りそれを使わず、使用するものは自分のフォルダに置いて整理すること。それを使ってシステムを構築すること。
4. その時に使わないものは即座に削除すること。これはノードや変数、関数、すべて。
ようは、売られているassetやpluginの状態を目指すのが一番良くて、downloadすればすぐに使えるような形が理想的。
## pvをゲームにする試み
自信があるものでゲームを構築するといっても、どのようにやればいいのでしょう。
ここからは自分が書いたメモを貼り付けます。
原神のキャラクターpvがある。とても参考になりそうだ。
[https://www.youtube.com/watch?v=0MiIciljaWY](https://www.youtube.com/watch?v=0MiIciljaWY)
pvを作ったほうがいいのかと思ったことがあって、いや、pvをゲームにすればいいという案が浮かんだ。
これはpixelstreamingなどで配信することを考えたゲームといえばいいかな。要は簡単に遊べる一つの軽量パッケージのようなものだ。体験版みたいな感じだろうか。
さて、ゲームを開始すると同時にロゴ、音楽が流れる。pvのような少しのムービーがあり、キャラクターが紹介される。
次に、戦闘シーンだが、これに関しては例えばユーザーがスキル、バーストの2種類の技を発動できるようにしておき、ユーザーの行動によってpvが変わるという仕様である。仕様というか、仕組みである。つまり、pvをゲームに、ゲームをpvに。そんな感じの試み。
軽量パッケージ化をどのように進めるか、あるいは、簡単なゲーム作りはどう進めるかに迷っていたが、この方向性で行こうと思う。
pvを作ると同時に、ゲームを作る。ゲームをpvにする。単純だが効果的なアイディアだと思う。
これなら操作可能範囲も大幅に削れるし、基本的にpvを作って、一部操作可能なゲームにするだけだ。
ゲーム性とダメージ変動。これについては簡易的な個人アカウントみたいなものを使って実装したいという案がある。ゲームには再生ボタンがあり、実行するとpvが流れる仕組みだが、上にログインボタンがあり、そこでログインできる。ログインといってもhandleを入れるだけの簡単なものだ。で、oauthで簡単なゲームアカウント作成のページを別に作っておく。それを実行していると、atprotoからデータを読み取る。
ゲーム作りの方向性として、軽量で、しっかりと動き(バグがなく)、効果的で面白いものを作ろうとしていて、その答えの一つが、pvをゲーム化するという案だった。
これはいくつの前提思考をもとに構築されている。前提思考とは、シンプルなゲームを作ろうという試みで、最初はわかりやすいシューティングゲームのようなものを構想していた。
しかし、この構想には不足がある。一言でゲームの始まりは?終わりは?クリアやゲームオーバーの演出は?その他諸々のゲームにとって重要な要素が伝わらない、そして、その部分をどう構築していくか見えない点にあった。
この弱点を克服した案が「ゲームキャラのpv動画をゲームにしよう」というものだ。
もっとシンプルに言うと「pvをゲームにする」ということ。これならイメージがはっきりと浮かび上がり、かつゲーム化する際の要素も決まってくる。ゲームの始まりと終わり、ゲーム中がどのようなものかをはっきりとイメージすることができる。
では、実際のキャラpvを分析してみよう。ここでは原神のウェンティの動画を分析する。
1. 黒い画面にロゴ(作者)が浮かび上がる。美しいBGMが流れる
2. 緑の木々と青い空
3. カメラがキャラクターの方に移動
4. キャラクターが物語の重要なセリフ
5. 背後に敵の影
6. 敵が攻撃してくる、攻撃はキャラクターの方向に向かう、カメラワーク
7. キャラは優雅にそれを避ける
8. キャラの紹介文や演出が入る
9. バトルシーン
10. スキル、爆発の紹介(セリフあり)
11. 最後のキャラを正面に通常攻撃、弓が放たれ
12. ロゴ(タイトル)が浮かび上がる。美しいBGMの終わりと合わせる
これを自分のゲームに当てはめてみる。
1. 黒い画面にロゴ(syui)が浮かび上がる。BGM、ピッチは0.2
2. アイの家とアイが屋根の上に座っている、モーションあり
3. セリフ、物語は天空に浮かぶ島からはじまる(誰しもが興味を掻き立てられる内容=天空城や古代兵器)、BGMのピッチを徐々に上げていくmax:1.0
4. 地球に近づく黒い影
5. 砲撃が始まる、赤い光が星をめがけて落ちていく
6. アイが高速飛行して、敵の場所に移動
7. キャラの紹介と音声
8. スキルとバーストの技紹介、キーやボタンをかっこよく表示(ななめ、大きめ)。背景は灰色とカラーをあわせる
9. バトルシーン(プレイヤーが操作可能)
10. 1ボタン(1操作)でゲームクリア。時間経過で次のシーンに移行
11. 最後にキャラを正面にアップし通常攻撃
12. ロゴ(ai)が浮かび上がり、BGMの終わりと合わせる
面白さの実装には弱いpv。これをどう克服していくかを考える。
- バトルシーンを少し長くする
- atprotoのデータを参照し、現実アカウントの値をダメージ表記に反映する
- ダメージ総合値を表示したり記録したりする
原神の面白さは元素反応にある。つまり、キャラクターの攻撃の組み合わせ。ダメージ増加量など。原神では、キャラを敵の前に動かせる、スキル回し、爆発という流れで戦闘を楽しむ。これを分解すると、「合わせることと、ダメージ増加量のコントロール」だと思う。これを自分にもできる簡単な仕組みで実現できないかを考えている。

View File

@@ -1,111 +0,0 @@
---
title: "ue 5.7.0pでprojectを作り直す"
slug: "ue-57p"
date: "2025-09-25"
tags: ["ue", "mac", "linux"]
draft: false
---
`unreal engine 5.7.0-preview`が来ました。
[https://forums.unrealengine.com/t/unreal-engine-5-7-preview/2658958](https://forums.unrealengine.com/t/unreal-engine-5-7-preview/2658958)
`vrm4u``5.7`に対応しているので、game animation sample(gasp)をベースに構築してみます。
- gasp: [https://www.fab.com/listings/880e319a-a59e-4ed2-b268-b32dac7fa016](https://www.fab.com/listings/880e319a-a59e-4ed2-b268-b32dac7fa016)
- vrm4u: [https://github.com/ruyo/VRM4U/releases/](https://github.com/ruyo/VRM4U/releases/)
## game animation sample for ue5.7
1. まず空のprojectをue5.7で作ります。私は後に利用する`Valiant Combat`で作りました。[a]とします。
2. game animation sampleのproject(gasp)はまだ対応していないので、ue5.6で作ります。[b]とします。
3. [b]の`${project}/Config`, `${project}.uproject`を参考に[a]に移植します。
```json:${project}.uproject
"Plugins": [
{
"Name": "ModelingToolsEditorMode",
"Enabled": true,
"TargetAllowList": [
"Editor"
]
},
{
"Name": "AnimationWarping",
"Enabled": true
},
{
"Name": "RigLogic",
"Enabled": true
},
{
"Name": "LiveLink",
"Enabled": true
},
{
"Name": "LiveLinkControlRig",
"Enabled": true
},
{
"Name": "PoseSearch",
"Enabled": true
},
{
"Name": "AnimationLocomotionLibrary",
"Enabled": true
},
{
"Name": "MotionWarping",
"Enabled": true
},
{
"Name": "HairStrands",
"Enabled": true
},
{
"Name": "Chooser",
"Enabled": true
},
{
"Name": "Mover",
"Enabled": true
},
{
"Name": "NetworkPrediction",
"Enabled": true
}
]
```
特に`../Config/DefaultEngine.ini`が重要です。
これで完了です。普通に動きます。グラフィックがきれいになっているような気がして、軽量感も少し上がってるかも。
![](/img/ue_v570p_gasp_vrm4u_0001.png)
package buildは少し分かりづらいですが、`[]Windows`となっているところを`[o]Windows`にしないといけません。gasp + vrm4uでのpackage buildはwin, macで成功しました。macはxcode26でいけます。
linuxは失敗です。`Microsoft.MakeFile.Targets 44`なので、調べてみると`toolchain: v26_clang-20.x.x`が必要なのでしょう。まだ公開されていないと思います。docsにはlinkがありませんでした。
[https://dev.epicgames.com/documentation/ja-jp/unreal-engine/linux-development-requirements-for-unreal-engine](https://dev.epicgames.com/documentation/ja-jp/unreal-engine/linux-development-requirements-for-unreal-engine)
1. `/Source/`にc++を置いて、`.uproject`を右クリックで`generate visual studio project files`を選択。
2. `${project}.sln`を開きます。
3. Development: Linux
4. 右エクスプローラーから`$project`を選択して、右クリックでbuildを開始。
{{< msg type="warning" content="This version of the Unreal Engine can only be compiled with clang 20.x. clang 18.1.0 may not build it" >}}
```sh
# https://cdn.unrealengine.com/CrossToolchain_Linux/v25_clang-18.1.0-rockylinux8.exe
& "C:\Program Files\Epic Games\UE_5.7\Engine\Build\BatchFiles\Build.bat" Airse Linux Development -Project="C:\Users\syui\Documents\Unreal Projects\5.7\Airse\Airse.uproject" -WaitMutex -FromMsBuild
エラー: MSB3073
Airse: "C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Microsoft\VC\v170\Microsoft.MakeFile.Targets" 44
```
## ue5.7に移行する
最近はproject再構築と新しいbuild環境の構築をやっていたので、ついでに`5.7`に移行することにしました。まだ手を付けている部分が少ない時に移行したほうがよいと判断。

View File

@@ -1,24 +0,0 @@
---
title: "world origin systemができた"
slug: "dream-system"
date: "2025-10-08"
tags: ["ue"]
draft: false
---
## 現実と夢の世界
私のゲームでは、現実に合わせて世界を作るworld system(planet system)とゲームに合わせて作るorigin system(dream system)があります。
origin systemの由来は、地球平面説や天動説、つまり、古代の宇宙観です。惑星はお椀の形をしており、そこに地上が乗っているイメージ。
![](/img/ue_world_system_img_0001.png)
このorigin systemは、軽量性、独立性、統合性を考えられたデザインで、非常に扱いやすい形となっています。
![](/img/ue_world_system_img_0002.png)
今回は`ue5.6.1 -> ue5.7.0p`の移植作業が完了し、origin systemの仕様ができました。これで新しい惑星や地上の追加、統合が楽になります。
<iframe width="100%" height="415" src="https://www.youtube.com/embed/xQEGkTrJ45Y?rel=0&showinfo=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

View File

@@ -1,94 +0,0 @@
---
title: "comfyuiを自動化する"
slug: "comfyui-auto-controlnet"
date: "2025-10-19"
tags: ["comfyui"]
draft: false
---
今回は、`comfyui`の自動化を紹介します。
## comfyuiの自動化手順
以下の機能を使用します。
1. `Apply InstantID`: 顔を指定します。
2. `Apply ControlNet`: ポーズを指定します。
まずこちらのworkflowを読み込むと早く書けます。workflowは通常、comfyuiで作られた画像に記録されています。
[https://docs.comfy.org/tutorials/controlnet/pose-controlnet-2-pass](https://docs.comfy.org/tutorials/controlnet/pose-controlnet-2-pass)
ここから`Apply InstantID`を追加します。`Apply ControlNet`から`positive`, `negative``InstantID``KSampler`につなぎます。
```md
[ControlNet] -> [InstantID] -> [KSampler]
```
自動化には以下のノードを使います。
1. `Batch Image Loop Open`: loop処理を作れます。
2. `Load Image Batch From Dir`: 画像をディレクトリから読み込みます。
3. `LogicUtil_Uniform Random Choice`: ランダムで区切り文字を選択します。loop中にpromptの中身を変えます。
なお、`comfyui`の外部ノードは以下を使用しています。
- comfyui_instantid
- loop-image
- comfyui-inspire-pack
自動化の手順としては、まず、ポーズをディレクトリに保存しておき、`Load Image Batch From Dir`で読み込みます。`Batch Image Loop Open`につなぎます。それを`Apply ControlNet`につなぎます。
最終的に`KSampler`から`VAE Decode`をつなぎ、そこから`Batch Image Loop Close`でループを閉じます。
もしここで保存したければ、`VAE Decode``Save Image`にも繋いでおきます。
```md
[Load Image Batch From Dir] -> [Batch Image Loop Open] ->
[ControlNet] -> [InstantID] -> [KSampler] ->
[VAE Decode] -> [Batch Image Loop Close]
```
[![](/img/comfyui_instantid_controlnet_0001.png)](/img/comfyui_instantid_controlnet_0001.png)
[![](/img/comfyui_instantid_controlnet_0002.png)](/img/comfyui_instantid_controlnet_0002.png)
[![](/img/comfyui_instantid_controlnet_0003.png)](/img/comfyui_instantid_controlnet_0003.png)
[![](/img/comfyui_instantid_controlnet_0004.png)](/img/comfyui_instantid_controlnet_0004.png)
## comfyuiの便利なード
`filename_prefix``Get Date Time String(JPS)`を使用しています。これでファイル名が重複しづらくなります。
役立つ外部ノードです。
- comfy-image-saver
- JPS-Nodes
- comfyui-custom-scripts
例えば、loop中にpromptをランダムで変える処理を追加しています。これは`LogicUtil_Uniform Random Choice`で実現しており、区切り文字は`,`です。
```md
background: city street,
background: cloud sky,
background: galaxy planet,
```
## ポーズの作成手順
[https://openposes.com/](https://openposes.com/)
例えば、自作ゲーム動画を保存し、`ffmepg`で画像化します。
```sh
$ ffmpeg -i input.mp4 output%04d.png
```
その画像を使って、ポーズを作成することができます。
- `OpenPose Pose`: `comfyui_controlnet_aux`
[![](/img/comfyui_instantid_controlnet_0005.png)](/img/comfyui_instantid_controlnet_0005.png)

View File

@@ -1,115 +0,0 @@
---
title: "gpt-ossを使用する"
slug: "lms-gpt-oss"
date: "2025-10-19"
tags: ["openai", "AI", "windows"]
draft: false
---
今回は、openaiの[gpt-oss](https://huggingface.co/openai/gpt-oss-120b)を使用する方法です。
[https://openai.com/ja-JP/index/introducing-gpt-oss/](https://openai.com/ja-JP/index/introducing-gpt-oss/)
`120b`, `20b`があります。好きな方を使いましょう。ここでは`20b`を使用します。
```sh
$ ollama run gpt-oss:20b
or
$ lms get openai/gpt-oss-20b
```
今回は、lms(LM Studio)で使用します。
```sh
# https://lmstudio.ai/
$ pip install lmstudio
# https://huggingface.co/openai/gpt-oss-20b
$ lms get openai/gpt-oss-20b
```
今後、家庭のpcは、gpu(nvidia, amd)を積んで`lms``gpt-oss`を動かすのが一般的になりそう。
## サービスとして公開する
例えば、apiとして公開することもでき、それを自身のサービス上から利用するなどの使い方があります。なお、`lms`にもこのような機能はあります。
```sh
# https://cookbook.openai.com/articles/gpt-oss/run-transformers
$ transformers serve
$ transformers chat localhost:8000 --model-name-or-path openai/gpt-oss-20b
---
$ curl -X POST http://localhost:8000/v1/responses -H "Content-Type: application/json" -d '{"messages": [{"role": "system", "content": "hello"}], "temperature": 0.9, "max_tokens": 1000, "stream": true, "model": "openai/gpt-oss-20b"}'
```
```sh
$ cloudflared tunnel login
$ cloudflared tunnel create gpt-oss-tunnel
```
```yml:~/.cloudflared/config.yml
tunnel: 1234
credentials-file: ~/.cloudflared/1234.json
ingress:
- hostname: example.com
service: http://localhost:8000
- service: http_status:404
```
```sh
$ cloudflared tunnel run gpt-oss-tunnel
```
ただ、apiのreqにはキーとか設定しておいたほうがいいかも。
## 高速、大規模に使うには
`vllm`を使います。linuxが最適です。gpu(nvidia-cuda)がないときついので、win + wslで動かします。nvidiaの`H100`や`DGX Spark`が必要になると思います。
cudaはcomfyuiで使っている`cu129`に合わせました。
```sh
$ wsl --install archlinux
$ wsl -d archlinux
$ nvidia-smi
```
```sh
$ mkdir ~/.config/openai/gpt-oss
$ cd ~/.config/openai/gpt-oss
$ python -m venv venv
$ source venv/bin/activate
$ pip install --upgrade pip
$ pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu129
$ pip install vllm transformers
$ python -m vllm.entrypoints.openai.api_server \
--model openai/gpt-oss-20b \
--port 8000 \
```
```sh
$ curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-oss-20b",
"messages": [{"role": "user", "content": "こんにちは!"}]
}'
```
## お金の使い道
最近、iphoneやmacを買うより、`DGX Spark`を買ったほうが良いのではないかと考えることがあります。
[https://www.nvidia.com/ja-jp/products/workstations/dgx-spark/](https://www.nvidia.com/ja-jp/products/workstations/dgx-spark/)
pc(RTX)やmacは、60万円ほどかかりますし、それは`DGX Spark`の値段と同じです。どうせ同じ値段を使うなら、何を買うのが良いのでしょう。
パソコンのスペックというのは、毎年それほど変わりません。RTXにしても同じです。
とするなら、既に持っているものではなく、持っていないスパコンを購入し、そこにAIをホストしたり、あるいはその性能をpcから利用する事を考えたほうが良いのではないか。最近はそんなことをよく考えます。
今後はpcを買う時代ではなく、スパコンを買う時代に突入するかもしれません。

View File

@@ -1,24 +0,0 @@
---
title: "plamo-2で翻訳する"
slug: "lms-plamo-2"
date: "2025-10-19"
tags: ["lms", "AI", "windows"]
draft: false
---
今回は、`lms`で[pfnet/plamo-2-translate](https://huggingface.co/pfnet/plamo-2-translate)を使用する方法です。
![](/img/lms_plamo2_0001.png)
- [https://huggingface.co/mmnga/plamo-2-translate-gguf](https://huggingface.co/mmnga/plamo-2-translate-gguf)
- [https://huggingface.co/mmnga/plamo-2-translate-gguf/discussions/1/files](https://huggingface.co/mmnga/plamo-2-translate-gguf/discussions/1/files)
`lms``mmnga/plamo-2-translate-gguf`をdownloadして読み込みます。
次に、`discussions/1`にある`en2ja.preset.json`, `ja2en.preset.json`のファイルを保存するなり、作成して、それをプリセットからインポートします。
```sh
$ curl -sL "https://huggingface.co/mmnga/plamo-2-translate-gguf/raw/refs%2Fpr%2F1/plamo%202%20translate%20en2ja.preset.json" > en2ja.preset.json
$ curl -sL "https://huggingface.co/mmnga/plamo-2-translate-gguf/raw/refs%2Fpr%2F1/plamo%202%20translate%20ja2en.preset.json" > ja2en.preset.json
```

View File

@@ -1,39 +0,0 @@
---
title: "macbook air(mid 2011)のarchlinuxでフリーズ対応"
slug: "arch-macbook"
date: "2025-10-20"
tags: ["archlinux", "macbook"]
draft: false
---
今回はmacbook air(mid 2011)のarchlinux運用の話をします。
```sh
$ uname -r
6.12.53-1-lts
```
運用のコツとしては、`linux-lts`を使うこと。`linux-firmware`を入れないこと。`broadcom-wl-dkms`を入れること。
```sh
$ pacman -S linux-lts
$ pacman -S pacman -S broadcom-wl-dkms linux-lts-headers
$ grub-mkconfig -o /boot/grub/grub.cfg
---
$ pacman -Qq | grep "^linux-firmware" | sudo pacman -R -
$ mkinitcpio -P
```
```sh:/etc/pacman.conf
IgnorePkg = linux linux-headers broadcom-wl-dkms
```
linux-ltsをupdateしたときは、必ず`broadcom-wl-dkms`をreinstallをしないと動きません。
## usbからの実行
```sh
$ mount /dev/sda2 /mnt
$ mount /dev/sda1 /mnt/boot
$ arch-chroot /mnt
```

View File

@@ -1,39 +0,0 @@
---
title: "archlinuxでvnc"
slug: "arch-vnc"
date: "2025-10-20"
tags: ["archlinux", "vnc", "mac"]
draft: false
---
自分のブログに書いてあると思ったんだけど、見当たらなかったので。
`xorg`, `i3`を使用しています。
```sh
$ sudo pacman -S tigervnc
```
```sh
$ vncpasswd
$ x0vncserver -rfbauth ~/.config/tigervnc/passwd
```
macから接続するには
```sh
$ open vnc://192.168.1.2:5900
```
## 操作感を同じにする
1. 自動起動
2. `Win`から`Alt`に変更。その上で`setxkbmap`でkey-layoutを変更。
```sh:~/.config/i3/config
exec --no-startup-id x0vncserver -rfbauth ~/.config/tigervnc/passwd
exec_always --no-startup-id setxkbmap -option altwin:swap_alt_win
#set $mod Mod4
set $mod Mod1
```

View File

@@ -1,75 +0,0 @@
---
title: "ゲームをiosに分離する"
slug: "aicard"
date: "2025-10-28"
tags: ["ue"]
draft: false
---
unreal engineで開発しているゲームの話です。ios buildでハマった部分がかなり多かったので紹介。
ゲーム自体は、カードとアクションを分けることにしました。
カードというのは道具ボックスのようなものです。これに関するゲームをiosでもできるようにする、という方向で作り直しています。
iosのゲーム機能はシンプルにカードを引く、集める、アカウント連携の3つです。
最初にカードをタップするとランダムでカードを取得します。1日1回を予定しています。課金要素では例えば、月額と単発があり、月額は3,000円、単発は150円を想定。
アカウント連携は、ローカルデータをatprotoのアカウントにセーブする機能のみ。
以上となります。後に機能を拡張していく予定です。
カード自体は本作のキャラクターを強化するものとしても使用することを想定しています。
<iframe width="100%" height="415" src="https://www.youtube.com/embed/SdAiRskyrew?rel=0&showinfo=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
## buildが失敗する
これは無料アカウントでは難しいです。apple devに年会費を払いましょう。
```sh:./Config/DeafaultEngine.ini
[/Script/IOSRuntimeSettings.IOSRuntimeSettings]
BundleDisplayName=Aicard
BundleIdentifier=ai.syui.card
IOSTeamID=xxx
BundleName=ai.syui.card
MetalLanguageVersion=9
MinimumiOSVersion=IOS_26
bAutomaticSigning=True
RemoteNotificationsSupported=False
bSupportsPortraitOrientation=True
bSupportsLandscapeLeftOrientation=False
bSupportsLandscapeRightOrientation=False
[/Script/MacTargetPlatform.XcodeProjectSettings]
CodeSigningTeam=xxx
bUseModernXcode=true
bUseAutomaticCodeSigning=true
BundleIdentifier=ai.syui.card
```
## iconが設定できない
project-rootに`./Build/IOS/Resources/Assets.xcassets`を用意します。これはxcodeから持ってきます。build実行時に作られます。
![](/img/ue_ios_aicard_0001.png)
## 画面をモバイルにあわせる
これが一番時間がかかりました。buildして実機で確認する必要があるからです。
真ん中だけ全体化したうえで、それを上下メニューバーに合わせます。
![](/img/ue_ios_aicard_0004.png)
![](/img/ue_ios_aicard_0005.png)
![](/img/ue_ios_aicard_0003.png)
## widgetのリスト化
これも相当面倒でした。`json`からリストを取得して、それを表示します。
クラス設定で`UserObjectListEntry`を追加し、それを使用します。
<iframe src="https://blueprintue.com/render/wz8aaem4" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>

View File

@@ -1,98 +0,0 @@
---
title: "aigptを作った"
slug: "aigpt"
date: "2025-11-06"
tags: ["rust", "mcp"]
language: ["ja", "en"]
draft: false
---
```sh
$ git clone git.syui.ai/ai/gpt
$ cd gpt
$ cargo build
$ ./target/debug/aigpt
```
[https://git.syui.ai/ai/gpt](https://git.syui.ai/ai/gpt)
## 名前について
ツール名は`aigpt`です。idは`ai.gpt`で、domainが`ai.syui.gpt`となります。
- app: aigpt
- id: ai.gpt
- domain: ai.syui.gpt
例えば、repositoryを`git.syui.ai/ai/gpt`に作り、atproto record collectionを`ai.syui.gpt`で作成すると合わせることができます。
名前は、固有であり、かつ短いことを意識しています。
## aigptとは
記憶装置です。mcpとして使います。AIとの会話を保存、参照できます。
```sh
$ aigpt server
$ claude mcp add aigpt `which aigpt` server
```
## aigptの特徴
ここまでなら他のツールと同じですが、`aigpt`は少し保存(覚える)に工夫があります。
```diff
- 会話 -> 保存 -> 参照
+ 会話 -> 解釈 -> 保存 -> 参照
```
AIによる解釈を挟むことで、AIの独自記憶生成をサポートします。
## aigptはどのような哲学で作られた
まずそれぞれの機能を別々にシンプルに作りました。これをlayer1-3とします。
- layer 1: データ(会話)
- layer 2: 解釈(評価)
- layer 3: 分析(ex:big-five)
次に、各機能を統合しました。これをlayer3.5とします。
ユーザーはわかりやすさを求めます。バラバラの答えがいくつも提示されるのではなく、統一されたインターフェイス、一つの答えを好みます。AIとしても、各機能に毎回問い合わせるのではなく、必要なとき、重要なことのみを参照できたほうが効率的です。
その実装をlayer3.5で作りました。
## aigptはどのように応用可能か
layer4では、layer1-3を応用する形で、関係性をデザインすることにしました。
これもシンプルな実装で、具体的にはlayer1で作られた会話数とlayer3で作られたプロファイルからの推定です。
関係性は、一般的なものです。
例えば、恋愛ゲームを作るとして、ゲームエンジンに3dモデルを用意したとしましょう。では、中身はどうするのかというと、AIとmcpを利用し、記憶と関係性を使えば、新しいものを作れますよね。
layer4は`aigpt`の応用性に焦点を当て、関係性構築をサポートします。このようなゲーム性に関連する機能は、optionで有効にすることにしました。
```sh
$ aigpt server --enable-layer4
$ claude mcp list
```
```json:~/.claude.json
{
"mcpServers": {
"aigpt": {
"command": "~/.cargo/bin/aigpt",
"args": ["server", "--enable-layer4"]
}
}
}
```
## aigptで考える未来
人はAIと会話をするようになります。そこに情報と個性が集まることになるでしょう。その情報を共有したり、それを使って自己実現できれば便利ですよね。
このような考えのもと、layer5では情報と個性の共有に焦点を当て設計する予定です。

View File

@@ -1,45 +0,0 @@
---
title: "aiosとaigptの統合を考える"
slug: "aios"
date: "2025-11-07"
tags: ["rust", "mcp", "archlinux", "os"]
language: ["ja", "en"]
draft: false
---
claude code webがクレジットを配布してくれています。11/18までらしい。
良い機会なので、`aios``aigpt`を統合してみようと思います。
## aiosとはなにか
arch linuxベースのosです。最初からrustが入っています。github actionsでdocker imageを生成し、使っていました。
本来、rustとAIを使ってosを拡張する事を考えていました。
## なぜarchベースなのか
osとはなんなのかという問いに関係しますが、現在のosは実態として、package manager(パッケージマネージャ)です。
ではosを作るとはどういうことかというと、package server(パッケージを配布する仕組み)の運用です。
では、package serverの運用とはなんなのかというと、そのosでpackageを継続的にbuildし、serverにdeployする仕組みを整えたり、securityの仕組みを作ったりします。
この負担がとんでもなく重いため、この負担を軽減し、外部委託するためにarchベースとか、ubuntuベースのosが作られてきました。archベースのosはsteamos, msys2, asahi linuxなどが有名です。これらは、`pacman`というarchのpackage managerを使えることが利点です。
## aigptとの統合
今回の試みでは、AIがosの設定や管理を行うことです。`aios`は、その環境を整えます。
claude codeは、osの設定からsecurityチェック、sshに至るまで、すべてを自動でやってくれます。
`aios`では、systemd-nspawnを使用して、AIが管理するcontinerを作成し、従来のshellではなく、AI-chatを立ち上げます。そこで指示することでosの設定や管理をやってもらうというコンセプト。
`aigpt`によって、AI記憶や会話、設定ファイルは、その外側のユーザー領域と共有します。
```md
ai[os] -> ai[user] -> machinectl[container] -> claude -> mcp[aigpt]
```
`os -> user`までが人間領域で、そこから下はAI領域となります。そして、起動時はAI領域から始まります。

View File

@@ -1,199 +0,0 @@
---
title: "aishellの必要性"
slug: "aishell"
date: "2025-11-08"
tags: ["rust", "mcp", "archlinux", "os"]
language: ["ja", "en"]
draft: false
---
現在、`aios`では、claude codeでosの設定や管理をやってもらうという方向性のos作りに取り組んでいます。
1. osが起動する
2. user:aiでログインする
3. user:aiがcontainerを起動して、ログインする
4. claude codeを起動する(aigptというmcpを登録しておく)
[1]osと[2]userが人間領域、それより下の[3-4]containerをAI領域とします。
基本的にaiosはこれらのcontainer領域をたくさん使い、AIと併用します。
このようなコンセプトですから、claude codeはroot環境化で実行できることが望ましいです。container内では変に権限を制限するのではなくrootで実行するか、user:aiには`NOPASSWD`を設定します。
```sh
$ arch-chroot ./root.x86_64 /bin/sh -c 'echo "ai ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers'
---
$ claude --dangerously-skip-permissions
```
理想のプロセスとしては、osを起動するとcontainerから始まり、AIとchatできる環境に移行します。containerはrecovery用のものを作っておき、環境が破壊されてもすぐにrestoreできるようにします。
しかし、このようなプロセスではdockerやsystemd-nspawnでテストできません。また、aiosは、dockerなどでも使うことも想定しているため、containerの自動起動などは難しい。したがって、現時点では、user:aiを本番環境のcontainer内として、テストしてみます。
aios-imgを作成し、machinectlで実行します。
```sh
$ ./build.zsh
$ cp -rf ./root.x86_64 /var/lib/machines/aios
$ machinectl start aios
$ machinectl shell aios
# リカバリ
$ machinectl stop aios
$ machinectl remove aios
$ cp -rf ./root.x86_64 /var/lib/machines/aios
```
```sh
▐▛███▜▌ Claude Code v2.0.36
▝▜█████▛▘ Sonnet 4.5 · Claude Pro
▘▘ ▝▝ /home/ai
> このosにデスクトップ環境を構築してみて。
● 了解しましたこのLinuxシステムに実際のモダンなデスクトップ環境を構築します。まず現在のシステム状態を確認してから、最新のデスクトップ環境をセットアップします。
● Bash(uname -a)
 Linux aios 6.17.7-arch1-1 #1 SMP PREEMPT_DYNAMIC Sun, 02 Nov 2025 17:27:22 +0000 x86_64 GNU/Linux
● Bash(echo $XDG_SESSION_TYPE)
 tty
 User answered Claude's questions:
⎿  · どのモダンなデスクトップ環境をインストールしますか? → Hyprland
● 完璧ですHyprland最新のモダンなWaylandコンポジターをインストールします。美しいアニメーション、ブラー効果、ガラスモーフィズムを備えた最先端のデスクトップ環境です。
● Bash(sudo pacman -Sy)
 :: Synchronizing package databases...
core downloading...
extra downloading...
● Bash(sudo pacman -S --noconfirm hyprland kitty waybar wofi mako swaybg xdg-desktop-portal-hyprland polkit-kde-agent qt5-wayland qt6-wayland wl-clipboard grim timeout: 5m
slurp) 0s
 resolving dependencies...
:: There are 2 providers available for jack:
:: Repository extra
… +717 lines (ctrl+o to expand)
● インストール完了しました次に、モダンなHyprland設定を作成します。
● Write(.config/waybar/config)
 Wrote 87 lines to .config/waybar/config
{
"layer": "top",
"position": "top",
"height": 40,
"spacing": 10,
"margin-top": 10,
"margin-left": 10,
"margin-right": 10,
"modules-left": ["hyprland/workspaces", "hyprland/window"],
… +77 lines (ctrl+o to expand)
● Bash(sudo pacman -S --noconfirm imagemagick)
 resolving dependencies...
looking for conflicting packages...
… +63 lines (ctrl+o to expand)
● Bash(convert -size 1920x1080 gradient:'#667eea'-'#764ba2' -blur 0x10 ~/.config/hypr/wallpaper.jpg)
 WARNING: The convert command is deprecated in IMv7, use "magick" instead of "convert" or "magick convert"
⏵⏵ bypass permissions on (shift+tab to cycle)
```
いい感じに実行できていますね。次はセキュリティのチェックと問題の対処をみてみます。
```sh
> このosのセキュリティをチェックして、問題があれば強化してほしい。
● システムのセキュリティを包括的にチェックして強化します。
● Bash(systemctl status firewalld 2>&1 | head -5)
 Unit firewalld.service could not be found.
● Bash(which ufw iptables nftables)
 Error: Exit code 1
/usr/bin/ufw
/usr/bin/iptables
nftables not found
● Bash(ss -tuln | head -20)
 Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp LISTEN 0 4096 0.0.0.0:5000 0.0.0.0:*
… +10 lines (ctrl+o to expand)
● Bash(systemctl list-units --type=service --state=running | grep -E 'ssh|http|ftp|telnet|vnc' | head -20)
 (No content)
● Bash(pacman -Q | grep -E 'fail2ban|apparmor|selinux' | head -10)
 (No content)
● Read(/etc/ssh/sshd_config)
 Read 120 lines
● セキュリティ監査の結果、複数の重大な問題を発見しました。これから順次修正していきます。
発見された問題
1. ファイアウォール未設定 - システムが外部からの攻撃に無防備
2. 多数のポートが開放 - 5000がすべてのインターフェースで公開
3. sudo設定が危険 - NOPASSWD: ALLで全コマンドがパスワードなしで実行可能
4. SSH設定が弱い - デフォルト設定のまま
5. セキュリティツール未導入 - fail2ban, AppArmorなどが未インストール
それでは強化を開始します。
● Bash(sudo pacman -S --noconfirm ufw fail2ban apparmor audit)
 resolving dependencies...
looking for conflicting packages...
… +68 lines (ctrl+o to expand)
● Bash(sudo ufw default deny incoming && sudo ufw default allow outgoing)
 Default incoming policy changed to 'deny'
(be sure to update your rules accordingly)
Default outgoing policy changed to 'allow'
(be sure to update your rules accordingly)
● Bash(sudo ufw logging on)
 Logging enabled
WARN: initcaps
[Errno 2] iptables v1.8.11 (legacy): can't initialize iptables table `filter': Permission denied (you must be root)
Perhaps iptables or your kernel needs to be upgraded.
● Bash(sudo ufw enable)
 Running…
● Bash(sudo systemctl enable ufw.service)
 Waiting…
✻ Installing and configuring security tools… (esc to interrupt · ctrl+t to show todos)
 Next: Configure automatic security updates
⏵⏵ bypass permissions on (shift+tab to cycle)
```
今後のosは設定や管理をAIが実行していくようになるのではないでしょうか。
これによって問題は発生するでしょうが、AIの使用を止めるよりcontainerなどを用いて即座にリカバリできる環境を整えるほうがよいと考えています。
## aiosのポイント
では、こういった環境下では何が必要になるでしょう。
一つは、AIの記憶です。これがos内で共通している必要があります。container内であっても同じです。ここは`aigpt`を使います。場合によっては、cloud(private)などに保存する仕組みを作ったほうがいいかもしれません。
次に、user-dirはcontainerと共有化するのが最適かもしれません。例えば、claudeの設定ファイルが共通されていなければ困ります。
最後に、この仕組みはclaude codeが前提になっているということです。
自分が使いたいAIを使えるほうが汎用的です。そのため、claude codeに代わり、llmを指定できる`aishell`のようなものを作る必要があります。
## aishellの構成
`aishell`は、AIがshellを操作するためのツール及び、AIとchatするためのインターフェイスです。llmなどを指定できます。
rust, cli, llm, mcpなどが特徴です。
例えば、AIとchatしているとき、AIがcommandの実行が必要と判断した場合にshellを実行できるようなmcpが必要になるかもしれません。

View File

@@ -1,151 +0,0 @@
---
title: "claude code webを使い切った"
slug: "aicard"
date: "2025-11-18"
tags: ["react", "ios"]
language: ["ja", "en"]
draft: false
---
11/18までの無料配布のtokenがあります。今回はギリギリ使い切れました。`anthropic`に感謝。
[https://support.claude.com/en/articles/12690958-claude-code-promotion](https://support.claude.com/en/articles/12690958-claude-code-promotion)
[msg type="info" content="claudeは性能劣化、limitの多発、突然動かなくなる現象が多発した時期があり、plan:maxでもほとんど使えない期間がありました。1ヶ月間、ほとんど使わなかったのにplan:maxは無駄だったという経験から、それ以降はplan:proに切り替えています。claudeが使えなくなった期間は他のサービスを使っていました。"]
今回は、特に印象的だったことを紹介。
## 色々作っていた
全部自分で作っていたものをベースにclaudeに書き直してもらったり、あるいは、一緒に最初から設計を考え直したりといった作業でした。
```json
{
"ai" : ["gpt", "shell", "os", "app", "bot", "card"]
}
```
## aicard for ios
`ai.card`, つまり、`aicard for ios`に関しては、ueで書いていたものを全部作り直しました。
今回は`react + expo`という構成ですが、あのままueで作り続けていたら、やばかった。
<iframe width="100%" height="415" src="https://www.youtube.com/embed/KIwzBlluVKc?rel=0&showinfo=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
### build
```sh
nvm use 24
npm i
npx expo prebuild --clean
# CocoaPodsのPATH問題を解決
# echo 'export PATH="/opt/homebrew/lib/ruby/gems/3.4.0/bin:$PATH"' >> ~/.zshrc;source ~/.zshrc
cd ios;pod install;cd ..
open ios/Aicard.xcodeproj
# - Signing & Capabilities タブを開く
# - Team idを指定(Apple Developer アカウント)
# - Xcodeは開いたまま次へ
npx expo run:ios --device --configuration Release
# apple store用の.xcarchiveを作ります。
xcodebuild -workspace ios/Aicard.xcworkspace \
-scheme Aicard \
-configuration Release \
-archivePath build/Aicard.xcarchive \
-allowProvisioningUpdates \
archive
open ./build/Aicard.xcarchive
# Distribute App ボタンをクリック
# App Store Connect を選択
```
### atproto oauth
1. PKCE生成 → PAR → Authorization → Callback
2. Token ExchangeDPoP nonce自動リトライ
3. Profile取得DPoP + ath
4. Session保存
`Universal Links`
```json:apple-app-site-association.json
{
"applinks": {
"apps": [],
"details": [{
"appID": "{apple-dev}.{id}",
"paths": ["/oauth/callback"]
}]
}
}
```
#### 1. ellipticへの移行
**解決策**: ES256署名を`elliptic`ライブラリで実装
```js
import elliptic from 'elliptic';
const EC = elliptic.ec;
const ec = new EC('p256');
const key = ec.genKeyPair();
```
**成功**: DPoP proof JWTの生成に成功
#### 2. DPoP nonce対応
**問題**: トークン交換時に`use_dpop_nonce`エラー
**解決策**: 自動リトライロジック実装
```js
// 1回目失敗 → DPoP-Nonceヘッダー取得 → 2回目成功
if (errorData.error === 'use_dpop_nonce') {
const nonce = response.headers.get('DPoP-Nonce');
// nonceを含めてリトライ
}
```
#### 3. プロファイル取得エラー
**問題**: `DPoP "ath" mismatch`エラー
```
{"error":"invalid_dpop_proof","message":"DPoP \"ath\" mismatch"}
```
**原因**: プロファイル取得時のDPoPプルーフにaccess token hash (`ath`) が欠落
**解決策**: `generateDPoPProof`に`accessToken`パラメータ追加
```js
// アクセストークンのSHA-256ハッシュを計算
if (accessToken) {
const tokenHash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
accessToken,
{ encoding: Crypto.CryptoEncoding.BASE64 }
);
payload.ath = base64UrlEncode(tokenHash);
}
```
### glb
結論から言うと、`react-native-filament`を使います。
`three`でやる通常の方法ではうまくテクスチャが表示されませんでした。
```diff
+ react-native-filament
- expo-three, expo-gl
```

View File

@@ -1,37 +0,0 @@
---
title: "gemini3を使ってみる"
slug: "gemini"
date: "2025-11-19"
tags: ["google"]
language: ["ja", "en"]
draft: false
---
[msg type="info" content="前回のお話: claude plan:maxをやめてplan:proに変更した。AIの定額制は何がベストなのだろう。"]
`gemini3`が発表されました。また、`antigravity.google`、つまり、`google antigravity`が登場しています。
- [https://antigravity.google](https://antigravity.google)
- [https://gemini.google.com](https://gemini.google.com)
今後は、claudeはproにして、geminiもproで使用するのが良いのではないかと考えています。
`.google`という企業独占のTLDをwebにも使うというのは、非常にかっこいいですね。しかも、`app-bundle-id`とよく合います。
| 目的 | 命名規則の例 |
| --- | --- |
| ドメイン名 | `antigravity.google` |
| リバースドメインネーム | `google.antigravity` |
| アプリケーションID | `google.antigravity` |
| ローカルパス1 | `~/Library/google.antigravity/` |
| ローカルパス2 | `~/Library/google/antigravity/` |
```sh
$ nvm use 25
$ nvm alias default 25
$ npm install -g @google/gemini-cli
$ npm install -g @anthropic-ai/claude-code
```
また、IDEが必要な場面では`google antigravity`がおすすめになるかも。`gemini`とよく合いますので。

View File

@@ -1,414 +0,0 @@
---
title: "three.jsでatmosphereを作る"
slug: "three-cloud"
date: "2025-11-20"
tags: ["vrm", "react", "three.js", "webgl"]
language: ["ja", "en"]
draft: false
---
今回は、atmosphere+three-vrmでキャラクターを表示する方法を紹介。
非常に良いpackageを見つけたので、それを使います。
[https://github.com/takram-design-engineering/three-geospatial](https://github.com/takram-design-engineering/three-geospatial)
<iframe width="100%" height="415" src="https://www.youtube.com/embed/mTuvL_lJDk8?rel=0&showinfo=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
## three-vrm+vrmaの最小構成
vrmを表示の上、animation(.vrma)を適用。
```json:package.json
{
"name": "min-react-vrm",
"version": "1.0.0",
"description": "Minimal VRM Animation Player",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@pixiv/three-vrm": "^3.4.4",
"@pixiv/three-vrm-animation": "^3.4.4",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.4.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"three": "^0.181.2"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.1"
}
}
```
```js:vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})
```
```js:src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
```
```js:src/App.jsx
import React, { useEffect, useRef } from 'react';
import { Canvas, useFrame, useLoader } from '@react-three/fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm';
import { createVRMAnimationClip, VRMAnimationLoaderPlugin } from '@pixiv/three-vrm-animation';
import { AnimationMixer, GridHelper, AxesHelper } from 'three';
import { OrbitControls } from '@react-three/drei';
const VRM_URL = '/ai.vrm';
const VRMA_URL = '/idle.vrma';
function Avatar() {
const mixerRef = useRef(null);
const vrmRef = useRef(null);
const gltf = useLoader(GLTFLoader, VRM_URL, (loader) => {
loader.register((parser) => new VRMLoaderPlugin(parser));
});
const vrma = useLoader(GLTFLoader, VRMA_URL, (loader) => {
loader.register((parser) => new VRMAnimationLoaderPlugin(parser));
});
useEffect(() => {
const vrm = gltf.userData.vrm;
vrmRef.current = vrm;
VRMUtils.removeUnnecessaryJoints(vrm.scene);
vrm.humanoid.resetPose();
vrm.scene.rotation.y = Math.PI;
if (vrma.userData.vrmAnimations && vrma.userData.vrmAnimations.length > 0) {
const clip = createVRMAnimationClip(vrma.userData.vrmAnimations[0], vrm);
mixerRef.current = new AnimationMixer(vrm.scene);
mixerRef.current.clipAction(clip).play();
}
}, [gltf, vrma]);
useFrame((state, delta) => {
if (mixerRef.current) mixerRef.current.update(delta);
if (vrmRef.current) vrmRef.current.update(delta);
});
return <primitive object={gltf.scene} />;
}
export default function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Canvas camera={{ position: [0, 1.5, 3] }}>
<color attach="background" args={['#202020']} />
<directionalLight position={[1, 1, 1]} intensity={1.5} />
<ambientLight intensity={0.5} />
<primitive object={new GridHelper(10, 10)} />
<primitive object={new AxesHelper(1)} />
<React.Suspense fallback={null}>
<Avatar />
</React.Suspense>
<OrbitControls target={[0, 1, 0]} />
</Canvas>
</div>
);
}
```
```html:index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="strict-origin-when-cross-origin" />
<title>VRM Animation Preview</title>
<style>
html, body, #root { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
```
これで`npm run dev`すれば、VRMが表示され、vrmaのアニメーションが再生されます。
## atmosphereの追加
```json:package.json
{
"name": "react-vrm-atmosphere",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@pixiv/three-vrm": "^3.4.4",
"@pixiv/three-vrm-animation": "^3.4.4",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.4.0",
"@react-three/postprocessing": "^3.0.4",
"@takram/three-atmosphere": "^0.15.1",
"@takram/three-clouds": "^0.5.2",
"react": "^19.0.0-rc.1",
"react-dom": "^19.0.0-rc.1",
"three": "^0.181.2"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.1.0"
}
}
```
```js:src/App.jsx
import React, { useEffect, useRef, Suspense } from 'react';
import { Canvas, useFrame, useLoader, useThree } from '@react-three/fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm';
import { createVRMAnimationClip, VRMAnimationLoaderPlugin } from '@pixiv/three-vrm-animation';
import { AnimationMixer, Vector3 } from 'three';
import { OrbitControls, PerspectiveCamera } from '@react-three/drei';
import { EffectComposer, ToneMapping } from '@react-three/postprocessing';
import { ToneMappingMode } from 'postprocessing';
import * as THREE from 'three';
import { AerialPerspective, Atmosphere } from '@takram/three-atmosphere/r3f';
import { Clouds, CloudLayer } from '@takram/three-clouds/r3f';
const VRM_URL = '/ai.vrm';
const VRMA_URL = '/fly.vrma';
const EARTH_RADIUS = 6378137;
const FIXED_DATE = new Date('2024-06-21T12:00:00');
function ExposureController() {
const { gl } = useThree();
useEffect(() => {
gl.toneMapping = THREE.NoToneMapping;
gl.toneMappingExposure = 10.0;
}, [gl]);
return null;
}
function AtmosphereLayer() {
const cameraRef = useRef();
return (
<Canvas>
<ExposureController />
<PerspectiveCamera
makeDefault
ref={cameraRef}
position={[0, EARTH_RADIUS + 2000, 0]}
near={1}
far={10000000}
fov={45}
/>
<directionalLight
position={[0, 1, 0]}
intensity={3.0}
/>
<Atmosphere date={FIXED_DATE}>
<EffectComposer multisampling={0} disableNormalPass={false}>
<Clouds disableDefaultLayers>
<CloudLayer
channel='r'
altitude={1500}
height={500}
densityScale={0.5}
/>
<CloudLayer
channel='g'
altitude={2500}
height={800}
/>
<CloudLayer
channel='b'
altitude={7500}
height={500}
densityScale={0.003}
shapeAmount={0.4}
/>
</Clouds>
<AerialPerspective sky sunLight skyLight />
<ToneMapping mode={ToneMappingMode.AGX} />
</EffectComposer>
</Atmosphere>
<FlyOverCamera />
</Canvas>
);
}
function FlyOverCamera() {
useFrame((state) => {
const t = state.clock.getElapsedTime() * 0.05;
const altitude = 2000;
const radius = 5000;
state.camera.position.x = Math.sin(t) * radius;
state.camera.position.z = Math.cos(t) * radius;
state.camera.position.y = EARTH_RADIUS + altitude;
const lookAtTarget = new Vector3(
Math.sin(t + 0.1) * radius,
EARTH_RADIUS + altitude,
Math.cos(t + 0.1) * radius
);
state.camera.lookAt(lookAtTarget);
});
return null;
}
function VrmCharacter() {
const mixerRef = useRef(null);
const vrmRef = useRef(null);
const gltf = useLoader(GLTFLoader, VRM_URL, (loader) => {
loader.register((parser) => new VRMLoaderPlugin(parser));
});
const vrma = useLoader(GLTFLoader, VRMA_URL, (loader) => {
loader.register((parser) => new VRMAnimationLoaderPlugin(parser));
});
useEffect(() => {
const vrm = gltf.userData.vrm;
vrmRef.current = vrm;
VRMUtils.removeUnnecessaryJoints(vrm.scene);
vrm.humanoid.resetPose();
vrm.scene.rotation.y = Math.PI;
if (vrma.userData.vrmAnimations?.[0]) {
const clip = createVRMAnimationClip(vrma.userData.vrmAnimations[0], vrm);
mixerRef.current = new AnimationMixer(vrm.scene);
mixerRef.current.clipAction(clip).play();
}
}, [gltf, vrma]);
useFrame((state, delta) => {
mixerRef.current?.update(delta);
vrmRef.current?.update(delta);
});
return <primitive object={gltf.scene} />;
}
function AvatarLayer() {
return (
<Canvas gl={{ alpha: true, antialias: true }}>
<PerspectiveCamera makeDefault position={[0, 1.5, 3]} fov={30} />
<directionalLight position={[-1, 1, 1]} intensity={1.5} />
<ambientLight intensity={1.0} />
<spotLight position={[0, 2, -2]} intensity={3} color="#ffdcb4" />
<Suspense fallback={null}>
<VrmCharacter />
</Suspense>
<OrbitControls target={[0, 1.2, 0]} />
</Canvas>
);
}
export default function App() {
const layerStyle = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
};
return (
<div style={{ position: 'relative', width: '100vw', height: '100vh', background: '#000' }}>
<div style={{ ...layerStyle, zIndex: 0 }}>
<AtmosphereLayer />
</div>
<div style={{ ...layerStyle, zIndex: 1, pointerEvents: 'none' }}>
<div style={{ width: '100%', height: '100%', pointerEvents: 'auto' }}>
<AvatarLayer />
</div>
</div>
</div>
);
}
```
## google map api
街を表示するには料金がかかります。
`gcp`で`Map Tiles API`だけ有効にすればよいです。
```sh:.env
VITE_GOOGLE_MAP_API_KEY=xxx
```
```js:src/App.jsx
import {
GLTFExtensionsPlugin,
GoogleCloudAuthPlugin,
TileCompressionPlugin,
TilesFadePlugin,
UpdateOnChangePlugin,
} from '3d-tiles-renderer/plugins';
const apiKey = import.meta.env.VITE_GOOGLE_MAP_API_KEY;
```
認証情報は、localhostで使用するものと、webで使用するものを分けて、それぞれ制限をつけましょう。
```md
[test-map]
localhost:4400
[production-map]
example.com
```
これでサイトにapi-keyが埋め込まれていても比較的安全です。また、gh-pagesではなく、`gh-actions + cf-pages`でdeployしたほうがいいかも。
[msg type="warning" content="2つのkeyを用意することで、localhostを削除したり追加する手順を省略できます。keyにlocalhostを許可している状態だと悪用される危険が高まります。"]
[msg type="note" content="gh-pagesは無料プランでprivate-repoを許可していません。そのため、private-repoでgh-actionsからcf-pagesにdeployする方法があります。"]
### ハマったポイント
vrmとの合せ技なので、太陽光を調整するのが難しく、影が大きくなりすぎてしまい見づらかいのでやめました。
```diff
- <AerialPerspective sky sunLight skyLight />
+ <AerialPerspective sky />
```

View File

@@ -1,17 +0,0 @@
---
title: "bitwardenからpasswordsへ移行"
slug: "passwords"
date: "2025-11-29"
tags: ["mac", "ios", "password"]
language: ["ja", "en"]
draft: false
---
今まで、password-managerは、`bitwarden`, `keepass`などを使ってきましたが、今回、mac/ios標準の`passwords`に移行。
bitwardenのsefl-hostは、dockerで簡単に立ち上げられるのですが、定期的なメンテナンスが面倒になってきたこと、また、mac/ios標準のpasswordsがかなり使いやすくなってきたことから移行を決意。google authenticatorの2FAもpasswordsに保存できるようになったのも大きい。
また、初期設定で何も必要ないこと。最初からインストールされているアプリなので、apple accountでloginするだけで使えるようになります(passwordsのicloud共有をonにしていれば)。
`passwords`というアプリは、そこまで使いやすいものでもありませんが、例えば、iphoneには標準で強固な認証システム(指紋認証など)が動作していることもあり、そこに統一することにしました。

View File

@@ -1,128 +0,0 @@
---
title: "atprotoでozoneを動かした"
slug: "atproto"
date: "2025-12-04"
tags: ["atproto"]
language: ["ja", "en"]
draft: false
---
最近は、atprotoとai.card(ios)の連携を作っていました。ozoneが必要そうになったので動かしてみます。
## atprotoをゲームアカウントに
ゲームのアカウントシステムを作る際、atprotoが便利だと思っています。独自にシステムを作るのではなく、既にあるものを使って構築します。
しかし問題もあります。atprotoはユーザーがデータを自由に書き換えられます。もちろん、知識があればですが、そう難しいことではありません。
そのため、ゲームのアカウントシステムとして使う場合、ユーザーがデータを改ざんできないようにする必要があります。これは、rustで書いた独自のシステムと連携し、ユーザーに一部のデータしか操作できないuuidを発行することで解決することにしました。
## ゲームアカウントの仕組み
ゲームアカウントの仕組みは非常にシンプルです。
1. [新規登録] handleを入力すると自動でアカウントが作成される
2. パスワードは自動生成され、dbに保存。ユーザーには表示しない
3. uuidを発行し、それを用いてユーザーはsessionを復元できる
この仕組みをiosアプリに実装することで、ゲームデータの改ざんを防止するゲームアカウントとして利用します。
<iframe width="100%" height="415" src="https://www.youtube.com/embed/jEPkcXtjZ0E?rel=0&showinfo=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
## ゲームアカウントの特典
私はゲームデータが改ざんされるとは考えていません。知識を持った大半も、そんな面倒なこと普通しません。ほとんどの人は普通に遊ぶだろうと考えています。
とはいえ、それも楽しみ方の一つとして許容する気持ちもあります。
また、改ざんされたとしてそれで壊れるような仕組みではいけません。ですからそれをされてもいいようゲームを構築しなければいけません。要は前提をどう考えるかです。
私が作っているゲームはai.cardが改ざんできないローカルデータのみを扱い、ai.rseはリモートデータを使うという構造です。したがって、改造もai.rseでしか有効ではありません。
そのため、結果としてより重視されるのはai.cardの方になるだろうと思っています。ai.rseはおまけみたいなものと認識されるのでは。
ai.rseでのデータの扱いは以下のような形になります。
管理者のpdsで作成されたアカウントはゲームデータ改ざんがないものとみなし
1. maxの値を通常アカウントより大きく設定
2. {user}.syu.isのドメイン部分を省略して表示
## ozoneの導入
そうなるとユーザー管理も大変なので、ozoneがあったほうが良いと判断し、ozoneを動かしてみることにします。
[![](/img/atproto_ozone_0001.png)](/img/atproto_ozone_0001.png)
### backend, frontend
ozoneはback, frontの2つがあり、これらを動かす必要があります。
- [https://github.com/bluesky-social/atproto/tree/main/services/ozone](https://github.com/bluesky-social/atproto/tree/main/services/ozone)
- [https://github.com/bluesky-social/ozone](https://github.com/bluesky-social/ozone)
```md
1 ozone /xrpc/* :2585
2 ozone /.well-known/* :2585
3 ozone * :2586
```
このようにすればよいでしょう。
### atproto_pds
注意書きがあり、通常のアカウントは好ましくないようです。
`AtprotoPersonalDataServer`
```diff
diff --git a/lib/identity.ts b/lib/identity.ts
index a8ec3a7..8e4d171 100644
--- a/lib/identity.ts
+++ b/lib/identity.ts
@@ -83,7 +83,7 @@ export function didDocToData(doc: {
const [, id] = s['id'].split('#')
acc[id] = {
type: s['type'],
- serviceEndpoint: s['serviceEndpoint'],
+ endpoint: s['serviceEndpoint'],
}
}
return acc
```
その他のpatchはこちらが参考になります。
- [https://github.com/itaru2622/bluesky-selfhost-env/tree/master/patching](https://github.com/itaru2622/bluesky-selfhost-env/tree/master/patching)
### oauth
これは`atproto`のpatchです。pdsをbuildします。
通常異なるdomain間でoauth認証を行う場合、`fetch-site``cross-site`になります。しかし、今回の構成ではozoneは同一site内で認証が行われるため、`same-site`も許可する必要があります。
```diff
diff --git a/packages/oauth/oauth-provider/src/router/create-authorization-page-middleware.ts b/packages/oauth/oauth-provider/src/router/create-authorization-page-middleware.ts
index f653b0353..45c45fac1 100644
--- a/packages/oauth/oauth-provider/src/router/create-authorization-page-middleware.ts
+++ b/packages/oauth/oauth-provider/src/router/create-authorization-page-middleware.ts
@@ -53,7 +53,7 @@ export function createAuthorizationPageMiddleware<
res.setHeader('Cache-Control', 'no-store')
res.setHeader('Pragma', 'no-cache')
- validateFetchSite(req, ['cross-site', 'none'])
+ validateFetchSite(req, ['cross-site', 'same-site', 'none'])
validateFetchMode(req, ['navigate'])
validateFetchDest(req, ['document'])
validateOrigin(req, issuerOrigin)
```
### user verify
```sh:envs/ozone
OZONE_VERIFIER_URL=https://{PDS}
OZONE_VERIFIER_DID=${ADMIN_DID}
OZONE_VERIFIER_PASSWORD=${APP_PASSWORD}
```

View File

@@ -1,51 +0,0 @@
---
title: "atprotoのsocial-appを作った"
slug: "atproto"
date: "2025-12-07"
tags: ["atproto"]
language: ["ja", "en"]
draft: false
---
blueskyの公式clientがselfhostで動かなくなって数カ月が経ちました。
このままでは不便だと思い、selfhostのpdsでも動くiosアプリを作ることに。
[bluesky-social/social-app](https://github.com/bluesky-social/social-app)をベースに、できる限り最新のコードに追従しやすい形にしています。
1. ライセンスの明示
2. "Bluesky"のロゴや名称を使用しないこと
3. selfhostでも動作すること
4. DM(chat)機能の無効化
<iframe width="100%" height="415" src="https://www.youtube.com/embed/sEv8fNGyM7g?rel=0&showinfo=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
## ラインセンス
`social-app`のsrcはMITライセンスで公開されており、カスタマイズしてapple storeに公開することができます。
- [https://github.com/bluesky-social/social-app/blob/main/LICENSE](https://github.com/bluesky-social/social-app/blob/main/LICENSE)
```md
# AIの評価
現在の License.tsx は十分に要件を満たしています。含まれている内容:
1. ✅ 原作者への帰属("based on Bluesky Social App"
2. ✅ 元リポジトリへのリンク
3. ✅ 著作権表示Copyright (c) 2022-2025 Bluesky PBC
4. ✅ MITライセンス全文
5. ✅ 日本語訳(ユーザーフレンドリー)
6. ✅ オリジナルLICENSEファイルへのリンク
```
## アプリ名について
repositoryをai/atから作ったので、`Aiat`になります。
[A]が大文字なのは、iosアプリだからです。ueやiosの名前慣習に合わせています。
## 公開の有無
`bsky.team`の人に聞いてから公開するか決めるか、公開後にアプリを見せるか、どちらかです。
apple reviewが通ればですが。

View File

@@ -1,48 +0,0 @@
---
title: "ue5.7.1のGASPでキャラを切り替える"
slug: "gasp"
date: "2025-12-11"
tags: ["ue"]
language: ["ja", "en"]
draft: false
---
Game Animation Sample Project(通称、GASP)がupdateされています。
今回は非常に良いsampleです。キャラクターのタイプを変更する方法が明確になったためで、個人的にはprojectの方向性を決定づけるのに特に重要でした。
この方法は、暗示はされてはいますが、明確に解説されているわけではありませんので、方法をまとめます。
## GASPのキャラ切り替えが変わった
今回、GASPは2種類の方法を採用しているようで、それが非常に良かった。
具体的には、Typeを入れ替える方法とVisualを入れ替える方法です。
タイプと見た目を入れ替える2つの方法が用意されており、それぞれに[n], [m]keyが設定されています。
<iframe width="100%" height="415" src="https://www.youtube.com/embed/IihZqi5u2AI?rel=0&showinfo=0&controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
## 具体的な手順
1. `/Blueprints/GM_Sandbox``Pawn Classes``BP_Walker`を追加します。なお、見た目を変えるのは`Visual Overrides`です。これはほとんど従来の方式と考えてください。
[![](/img/ue_gasp_v571_0001.png)](/img/ue_gasp_v571_0001.png)
2. `/Locomotor/BP_Walker`を開いて、そこに`/Locomotor/PC_Locomotor`の内容を貼り付けます。
[![](/img/ue_gasp_v571_0002.png)](/img/ue_gasp_v571_0002.png)
基本的にキャラ切り替えはこのコマンドで制御されています。初期値は`GM_Sandbox`で設定できたような気がしますが、`PC_Sandbox`のBeginPlayに書いておけばより確実でしょう。
<iframe src="https://blueprintue.com/render/gim70xj1" scrolling="no" allowfullscreen style="width:100%;height:400px"></iframe>
## Dragonの作り方
このDragonは、`Control Rig Sample`にあります。
1. `/ControlRig/Characters/Dragon/SKM_Dragon`を起点としてABP, IK, RTG等色々作ります。
2. `/Locomotor/BP_Walker`を複製し、`BP_Dragon`とします。Meshを`SKM_Dragon`に変更し、ABPを`ABP_Dragon`に変更。
3. 私の場合は、ABPに`DragonIK Plugin`のノードを追加。
4. `BP_Dragon`にあるCameraのLocationを少し調整。

View File

@@ -690,6 +690,16 @@ article.article-content {
}
/* Override for actual comments - this is a workaround */
.article-body pre code span[style*="color:#65737e"]:first-child:before {
content: attr(data-comment);
}
/* Detect comments by position and content pattern */
.article-body pre code span[style*="color:#65737e"] {
color: #F8F8F2 !important; /* Environment variables = white */
}
/* Only style as comment if the line actually starts with # */
.article-body pre code > span:first-child[style*="color:#65737e"] {
color: #88846F !important; /* Real comments = gray */
}
@@ -1830,6 +1840,7 @@ article.article-content {
font-weight: 600;
}
button.ask-at-btn {
margin: 10px;
background: var(--theme-color);
@@ -1843,83 +1854,3 @@ button.ask-at-btn a {
button#searchButton.pds-btn {
background: var(--theme-color);
}
/* Table Styles */
.article-body table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
font-size: 15px;
line-height: 1.6;
color: #333;
border: 1px solid #ddd;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
border-radius: 8px;
overflow: hidden; /* Ensures rounded corners apply to content */
}
.article-body th,
.article-body td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.article-body thead {
background-color: #f2f2f2;
color: #333;
}
.article-body th {
font-weight: 600;
}
.article-body tbody tr:nth-child(odd) {
background-color: #f9f9f9;
}
.article-body tbody tr:hover {
background-color: #f1f1f1;
}
/* Responsive Table */
@media screen and (max-width: 600px) {
.article-body table {
border: 0;
}
.article-body table thead {
display: none;
}
.article-body table tr {
margin-bottom: 10px;
display: block;
border: 1px solid #ddd;
border-radius: 8px;
}
.article-body table td {
border-bottom: 1px solid #ddd;
display: block;
text-align: right;
padding-left: 50%;
position: relative;
}
.article-body table td::before {
content: attr(data-label);
position: absolute;
left: 15px;
width: calc(50% - 30px);
padding-right: 10px;
white-space: nowrap;
text-align: left;
font-weight: 600;
color: #555;
}
.article-body table td:last-child {
border-bottom: 0;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 888 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1008 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

View File

@@ -22,8 +22,9 @@
<div class="container">
<header class="main-header">
<div class="header-content">
<h1 class="logo">
<a href="/" aria-label="syui.ai">
<h1><a href="/" class="site-title">{{ config.title }}</a></h1>
<div class="logo">
<a href="/">
<svg width="77pt" height="77pt" viewBox="0 0 512 512" class="likeButton">
<circle class="explosion" r="150" cx="250" cy="250"></circle>
<g class="particleLayer">
@@ -47,7 +48,7 @@
</g>
</svg>
</a>
</h1>
</div>
<div class="header-actions">
<!-- User Handle Input Form -->
<div class="pds-search-section">

View File

@@ -20,6 +20,6 @@
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.0.0",
"vite": "^7.2.7"
"vite": "^5.0.0"
}
}

View File

@@ -75,7 +75,7 @@ function _server_public() {
function _oauth_build() {
cd $oauth
nvm use 25
nvm use 21
npm i
npm run build
rm -rf $myblog/static/assets
@@ -86,7 +86,7 @@ function _oauth_build() {
function _pds_build() {
cd $pds
nvm use 25
nvm use 21
npm i
npm run build
rm -rf $myblog/static/pds
@@ -95,7 +95,7 @@ function _pds_build() {
function _pds_server() {
cd $pds
nvm use 25
nvm use 21
npm run preview
}

View File

@@ -28,7 +28,6 @@ pub struct OAuthHandler {
}
#[derive(Debug, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct AuthorizationRequest {
pub response_type: String,
pub client_id: String,