diff --git a/my-blog/content/posts/2025-11-20-three-cloud.md b/my-blog/content/posts/2025-11-20-three-cloud.md
new file mode 100644
index 0000000..547398e
--- /dev/null
+++ b/my-blog/content/posts/2025-11-20-three-cloud.md
@@ -0,0 +1,367 @@
+---
+title: "three.jsでatmosphereを作る"
+slug: "three-cloud"
+date: "2025-11-20"
+tags: ["vrm", "react", "three.js", "webgl"]
+language: ["ja", "en"]
+draft: false
+---
+
+今回は、atmpsphere+three-vrmでキャラクターを表示する方法を紹介。
+
+非常に良いpackageを見つけたので、それを使います。
+
+[https://github.com/takram-design-engineering/three-geospatial](https://github.com/takram-design-engineering/three-geospatial)
+
+
+
+## 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(
+
+
+ ,
+)
+```
+
+```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 ;
+}
+
+export default function App() {
+ return (
+
+ );
+}
+```
+
+```html:index.html
+
+
+
+
+
+ VRM Animation Preview
+
+
+
+
+
+
+
+```
+
+これで`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 (
+
+ );
+}
+
+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 ;
+}
+
+function AvatarLayer() {
+ return (
+
+ );
+}
+
+export default function App() {
+ const layerStyle = {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100%',
+ };
+
+ return (
+
+ );
+}
+```
+
diff --git a/my-blog/static/img/three_cloud_vrm_0001.png b/my-blog/static/img/three_cloud_vrm_0001.png
new file mode 100644
index 0000000..07b6901
Binary files /dev/null and b/my-blog/static/img/three_cloud_vrm_0001.png differ