Compare commits
22 Commits
2d8a565a34
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
cf2708c7f3
|
|||
|
db29524307
|
|||
|
e5aeffb621
|
|||
|
cbf3c424e3
|
|||
|
adf2121a8b
|
|||
|
5ba0b77154
|
|||
|
22fda9cb2d
|
|||
|
b0ca3f9163
|
|||
|
08c4a5cd52
|
|||
|
c004905d39
|
|||
|
a970bc008f
|
|||
|
678c238ee7
|
|||
|
9f9fabd478
|
|||
|
acf9cacda0
|
|||
|
a88a61f866
|
|||
|
afd95636da
|
|||
|
62ecab5f04
|
|||
|
d46502d177
|
|||
|
0d742ca1f2
|
|||
|
0e820f0746
|
|||
|
896734e265
|
|||
|
9535e7f08d
|
2
.github/workflows/cloudflare-pages.yml
vendored
2
.github/workflows/cloudflare-pages.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '21'
|
node-version: '25'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ $ uname -r
|
|||||||
6.12.53-1-lts
|
6.12.53-1-lts
|
||||||
```
|
```
|
||||||
|
|
||||||
運用のコツとしては、`linux-lts`を使うこと。`linux-firmware`を入れないこと。`broadcom-wl`を入れること。
|
運用のコツとしては、`linux-lts`を使うこと。`linux-firmware`を入れないこと。`broadcom-wl-dkms`を入れること。
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ pacman -S linux-lts
|
$ pacman -S linux-lts
|
||||||
@@ -25,9 +25,11 @@ $ mkinitcpio -P
|
|||||||
```
|
```
|
||||||
|
|
||||||
```sh:/etc/pacman.conf
|
```sh:/etc/pacman.conf
|
||||||
IgnorePkg = linux linux-headers
|
IgnorePkg = linux linux-headers broadcom-wl-dkms
|
||||||
```
|
```
|
||||||
|
|
||||||
|
linux-ltsをupdateしたときは、必ず`broadcom-wl-dkms`をreinstallをしないと動きません。
|
||||||
|
|
||||||
## usbからの実行
|
## usbからの実行
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -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)
|
[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が使えなくなった期間は他のサービスを使っていました。"]
|
||||||
|
|
||||||
今回は、特に印象的だったことを紹介。
|
今回は、特に印象的だったことを紹介。
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ draft: false
|
|||||||
- [https://antigravity.google](https://antigravity.google)
|
- [https://antigravity.google](https://antigravity.google)
|
||||||
- [https://gemini.google.com](https://gemini.google.com)
|
- [https://gemini.google.com](https://gemini.google.com)
|
||||||
|
|
||||||
|
今後は、claudeはproにして、geminiもproで使用するのが良いのではないかと考えています。
|
||||||
|
|
||||||
`.google`という企業独占のTLDをwebにも使うというのは、非常にかっこいいですね。しかも、`app-bundle-id`とよく合います。
|
`.google`という企業独占のTLDをwebにも使うというのは、非常にかっこいいですね。しかも、`app-bundle-id`とよく合います。
|
||||||
|
|
||||||
| 目的 | 命名規則の例 |
|
| 目的 | 命名規則の例 |
|
||||||
@@ -21,9 +23,8 @@ draft: false
|
|||||||
| ドメイン名 | `antigravity.google` |
|
| ドメイン名 | `antigravity.google` |
|
||||||
| リバースドメインネーム | `google.antigravity` |
|
| リバースドメインネーム | `google.antigravity` |
|
||||||
| アプリケーションID | `google.antigravity` |
|
| アプリケーションID | `google.antigravity` |
|
||||||
| ローカルパス | `~/Library/google.antigravity/` |
|
| ローカルパス1 | `~/Library/google.antigravity/` |
|
||||||
|
| ローカルパス2 | `~/Library/google/antigravity/` |
|
||||||
なので、今後は、claudeはproにして、geminiもproで使用するのが良いのではないかと考えています。
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ nvm use 25
|
$ nvm use 25
|
||||||
|
|||||||
414
my-blog/content/posts/2025-11-20-three-cloud.md
Normal file
414
my-blog/content/posts/2025-11-20-three-cloud.md
Normal 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 />
|
||||||
|
```
|
||||||
17
my-blog/content/posts/2025-11-28-passwords.md
Normal file
17
my-blog/content/posts/2025-11-28-passwords.md
Normal 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には標準で強固な認証システム(指紋認証など)が動作していることもあり、そこに統一することにしました。
|
||||||
|
|
||||||
128
my-blog/content/posts/2025-12-04-atproto.md
Normal file
128
my-blog/content/posts/2025-12-04-atproto.md
Normal 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)
|
||||||
|
|
||||||
|
### 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}
|
||||||
|
```
|
||||||
|
|
||||||
51
my-blog/content/posts/2025-12-07-social-app.md
Normal file
51
my-blog/content/posts/2025-12-07-social-app.md
Normal 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が通ればですが。
|
||||||
|
|
||||||
48
my-blog/content/posts/2025-12-11-gasp.md
Normal file
48
my-blog/content/posts/2025-12-11-gasp.md
Normal 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)
|
||||||
|
|
||||||
|
2. `/Locomotor/BP_Walker`を開いて、そこに`/Locomotor/PC_Locomotor`の内容を貼り付けます。
|
||||||
|
|
||||||
|
[](/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を少し調整。
|
||||||
|
|
||||||
BIN
my-blog/static/img/atproto_ozone_0001.png
Normal file
BIN
my-blog/static/img/atproto_ozone_0001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 331 KiB |
BIN
my-blog/static/img/three_cloud_vrm_0001.png
Normal file
BIN
my-blog/static/img/three_cloud_vrm_0001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 MiB |
BIN
my-blog/static/img/ue_gasp_v571_0001.png
Normal file
BIN
my-blog/static/img/ue_gasp_v571_0001.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 397 KiB |
BIN
my-blog/static/img/ue_gasp_v571_0002.png
Normal file
BIN
my-blog/static/img/ue_gasp_v571_0002.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 396 KiB |
@@ -20,6 +20,6 @@
|
|||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
"@types/react-dom": "^18.2.0",
|
"@types/react-dom": "^18.2.0",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^7.2.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function _server_public() {
|
|||||||
|
|
||||||
function _oauth_build() {
|
function _oauth_build() {
|
||||||
cd $oauth
|
cd $oauth
|
||||||
nvm use 21
|
nvm use 25
|
||||||
npm i
|
npm i
|
||||||
npm run build
|
npm run build
|
||||||
rm -rf $myblog/static/assets
|
rm -rf $myblog/static/assets
|
||||||
@@ -86,7 +86,7 @@ function _oauth_build() {
|
|||||||
|
|
||||||
function _pds_build() {
|
function _pds_build() {
|
||||||
cd $pds
|
cd $pds
|
||||||
nvm use 21
|
nvm use 25
|
||||||
npm i
|
npm i
|
||||||
npm run build
|
npm run build
|
||||||
rm -rf $myblog/static/pds
|
rm -rf $myblog/static/pds
|
||||||
@@ -95,7 +95,7 @@ function _pds_build() {
|
|||||||
|
|
||||||
function _pds_server() {
|
function _pds_server() {
|
||||||
cd $pds
|
cd $pds
|
||||||
nvm use 21
|
nvm use 25
|
||||||
npm run preview
|
npm run preview
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub struct OAuthHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct AuthorizationRequest {
|
pub struct AuthorizationRequest {
|
||||||
pub response_type: String,
|
pub response_type: String,
|
||||||
pub client_id: String,
|
pub client_id: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user