+++ date = "2024-10-01" tags = ["vrm"] title = "3Dホログラムを作った" +++ この前、ホログラムを作ってみようと思い10分くらいで作ってみた。必要なものは透明板だけ。それを斜めに設置してスマホで黒背景の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", "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; } ``` ```ts: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; ``` ```ts: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; ``` ```ts:src/App.tsx import React from 'react' import VRMModelCanvas from './pages/vrm' import ScreenTimeCanvas from './pages/time' const App = () => { return ( <> ) } export default App; ```