diff --git a/content/blog/2024-10-01-holo.md b/content/blog/2024-10-01-holo.md index 3c2adef..638575f 100644 --- a/content/blog/2024-10-01-holo.md +++ b/content/blog/2024-10-01-holo.md @@ -13,3 +13,211 @@ title = "3Dホログラムを作った" ここまではっきり映るとは思ってなくて驚いた。余ってるスマホを何に使おうと思ってたので、こういうのを表示させておくといいかも。ただし、表記は反転させないといけないみたい。 + +## react + tsx + three-vrm + +一応、最小限のコードを載せておきます。反転は対応しています。 + +```json:package.json +{ + "name": "holoai", + "version": "0.1.0", + "private": true, + "dependencies": { + "@pixiv/three-vrm": "^3.1.1", + "@pixiv/three-vrm-animation": "^3.1.1", + "@react-three/drei": "^9.114.0", + "@react-three/fiber": "^8.17.9", + "@react-three/postprocessing": "^2.16.3", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^27.5.2", + "@types/node": "^16.18.112", + "@types/react": "^18.3.10", + "@types/react-dom": "^18.3.0", + "@types/three": "^0.167.2", + "axios": "^1.7.7", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-scripts": "5.0.1", + "three": "^0.167.1", + "three-stdlib": "^2.30.5", + "typescript": "^4.9.5", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} +``` + +```css:src/index.css +.time { + transform:scale(-1,1); + position: absolute; + top: 10px; + padding: 10px; + z-index: 100; + color: #e9ff00; + font-size: 30px; + text-align: center; +} +``` + +```tsx:src/pages/time.tsx +import React, { useState, useEffect } from 'react'; + +//function reverseString(str: string): string { +// return str.split('').reverse().join(''); +//} + +const ScreenTimeCanvas: React.FC = () => { + const [currentDateTime, setCurrentDateTime] = useState(''); + //const [reversedDateTime, setReversedDateTime] = useState(''); + + useEffect(() => { + const updateDateTime = () => { + const now = new Date(); + const formatted = now.toLocaleString("ja-JP", { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + setCurrentDateTime(formatted); + //setReversedDateTime(reverseString(formatted)); + }; + updateDateTime(); + const timer = setInterval(updateDateTime, 1000); + return () => clearInterval(timer); + }, []); + + return ( +
+

{currentDateTime}

+
+ ); +}; + +export default ScreenTimeCanvas; +``` + +```tsx:src/pages/vrm.tsx +import * as THREE from 'three' +import React, { useState, useEffect, useRef } from 'react'; +import { OrbitControls } from '@react-three/drei' +import { useFrame, Canvas } from '@react-three/fiber'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { VRM, VRMUtils, VRMLoaderPlugin } from '@pixiv/three-vrm'; +import { VRMAnimationLoaderPlugin, VRMAnimation, createVRMAnimationClip } from "@pixiv/three-vrm-animation"; + +interface ModelProps { + url: string + url_anim: string + scale: [number, number, number] + position: [number, number, number] + rotation: [number, number, number] +} + +const VRMModel: React.FC = ({ url, url_anim, position, rotation, scale }) => { + + const [vrm, setVrm] = useState(null); + const mixerRef = useRef(null); + + useEffect(() => { + const loader = new GLTFLoader(); + loader.register((parser) => new VRMLoaderPlugin(parser)); + loader.register((parser) => new VRMAnimationLoaderPlugin(parser)); + loader.load(url, (gltf) => { + const vrmModel = gltf.userData.vrm as VRM; + VRMUtils.removeUnnecessaryJoints(vrmModel.scene); + setVrm(vrmModel); + const mixer = new THREE.AnimationMixer(vrmModel.scene); + mixerRef.current = mixer; + loader.load(url_anim, (animGltf) => { + const vrmAnimations = animGltf.userData.vrmAnimations as VRMAnimation[]; + if (vrmAnimations && vrmAnimations.length > 0) { + const clip = createVRMAnimationClip(vrmAnimations[0], vrmModel); + mixer.clipAction(clip).play(); + } + }); + }); + }, [url, url_anim]); + + useFrame((state, delta) => { + if (mixerRef.current) mixerRef.current.update(delta); + if (vrm) vrm.update(delta); + }); + + return vrm ? : null; +}; + +export const VRMModelCanvas = () => { + return ( +
+ + {/* Light gray background */} + + + + + +
+ ) +} +export default VRMModelCanvas; +``` + +```tsx:src/App.tsx +import React from 'react' +import VRMModelCanvas from './pages/vrm' +import ScreenTimeCanvas from './pages/time' + +const App = () => { + return ( + <> + + + + ) +} + +export default App; +```