22 Commits

Author SHA1 Message Date
cf2708c7f3 fix 2025-12-12 14:02:50 +09:00
db29524307 fix 2025-12-12 13:49:43 +09:00
e5aeffb621 fix 2025-12-11 12:46:42 +09:00
cbf3c424e3 fix 2025-12-09 17:28:48 +09:00
adf2121a8b fix 2025-12-09 17:22:31 +09:00
5ba0b77154 fix 2025-12-08 18:51:00 +09:00
22fda9cb2d fix 2025-12-08 14:13:15 +09:00
b0ca3f9163 fix 2025-12-04 18:48:42 +09:00
08c4a5cd52 fix 2025-12-04 16:45:04 +09:00
c004905d39 fix 2025-12-04 15:19:46 +09:00
a970bc008f fix 2025-12-04 14:35:17 +09:00
678c238ee7 fix 2025-11-29 07:50:48 +09:00
9f9fabd478 fix 2025-11-22 08:05:30 +09:00
acf9cacda0 fix 2025-11-22 07:31:53 +09:00
a88a61f866 fix 2025-11-22 07:22:07 +09:00
afd95636da fix 2025-11-22 07:14:49 +09:00
62ecab5f04 fix 2025-11-21 22:08:48 +09:00
d46502d177 fix 2025-11-21 22:03:38 +09:00
0d742ca1f2 fix 2025-11-21 21:46:34 +09:00
0e820f0746 fix 2025-11-21 05:34:01 +09:00
896734e265 fix 2025-11-21 05:13:23 +09:00
9535e7f08d fix 2025-11-21 05:12:12 +09:00
16 changed files with 673 additions and 11 deletions

View File

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

View File

@@ -13,7 +13,7 @@ $ uname -r
6.12.53-1-lts
```
運用のコツとしては、`linux-lts`を使うこと。`linux-firmware`を入れないこと。`broadcom-wl`を入れること。
運用のコツとしては、`linux-lts`を使うこと。`linux-firmware`を入れないこと。`broadcom-wl-dkms`を入れること。
```sh
$ pacman -S linux-lts
@@ -25,9 +25,11 @@ $ mkinitcpio -P
```
```sh:/etc/pacman.conf
IgnorePkg = linux linux-headers
IgnorePkg = linux linux-headers broadcom-wl-dkms
```
linux-ltsをupdateしたときは、必ず`broadcom-wl-dkms`をreinstallをしないと動きません。
## usbからの実行
```sh

View File

@@ -11,7 +11,7 @@ draft: false
[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が使えなくなった期間は他のサービスを使っていました。"]
[msg type="info" content="claudeは性能劣化、limitの多発、突然動かなくなる現象が多発した時期があり、plan:maxでもほとんど使えない期間がありました。1ヶ月間、ほとんど使わなかったのにplan:maxは無駄だったという経験から、それ以降はplan:proに切り替えています。claudeが使えなくなった期間は他のサービスを使っていました。"]
今回は、特に印象的だったことを紹介。

View File

@@ -14,6 +14,8 @@ draft: false
- [https://antigravity.google](https://antigravity.google)
- [https://gemini.google.com](https://gemini.google.com)
今後は、claudeはproにして、geminiもproで使用するのが良いのではないかと考えています。
`.google`という企業独占のTLDをwebにも使うというのは、非常にかっこいいですね。しかも、`app-bundle-id`とよく合います。
| 目的 | 命名規則の例 |
@@ -21,9 +23,8 @@ draft: false
| ドメイン名 | `antigravity.google` |
| リバースドメインネーム | `google.antigravity` |
| アプリケーションID | `google.antigravity` |
| ローカルパス | `~/Library/google.antigravity/` |
なので、今後は、claudeはproにして、geminiもproで使用するのが良いのではないかと考えています。
| ローカルパス1 | `~/Library/google.antigravity/` |
| ローカルパス2 | `~/Library/google/antigravity/` |
```sh
$ nvm use 25

View File

@@ -0,0 +1,414 @@
---
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

@@ -0,0 +1,17 @@
---
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

@@ -0,0 +1,128 @@
---
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

@@ -0,0 +1,51 @@
---
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

@@ -0,0 +1,48 @@
---
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を少し調整。

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

View File

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

View File

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

View File

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