1
0

Compare commits

...

3 Commits

Author SHA1 Message Date
16a52e13de add atmpshere 2025-11-22 06:26:39 +09:00
93ea0e88f7 fig ignore 2025-11-22 06:26:38 +09:00
ea102b308c add holo 2025-11-22 06:26:38 +09:00
45 changed files with 2172 additions and 0 deletions

4
.gitignore vendored
View File

@@ -6,4 +6,8 @@
**lock* **lock*
**_site** **_site**
**.jekyll** **.jekyll**
**package-lock.json
package-lock.json
node_modules
galaxy-react/public/models galaxy-react/public/models
holo/public/models

24
atmosphere/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

1
atmosphere/README.md Normal file
View File

@@ -0,0 +1 @@
# aicloud

1138
atmosphere/docs/README.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
# 開発経緯まとめ
## 目的
`three-geospatial`地球規模の背景と、通常の3Dモデルキャラクター/カード)を同時に表示する。
## 直面した課題と解決策
1. **スケールの不一致**
* **課題**: 地球半径6000kmとキャラクター数mのサイズ差が大きすぎて、表示崩れや操作不能が発生。
* **解決策**: **デュアルシーン構成**を採用。背景(地球)と前景(キャラ)を別々のシーンとして作成し、重ねて描画することで解決。
2. **背景が黒くなる**
* **課題**: 前景を操作(回転)すると、背景の空が消えてしまう。
* **解決策**: レンダリングの自動クリア(`autoClear`)を制御し、背景を描画した後に前景を上書きするように修正。
3. **モデルの裏返り(透過問題)**
* **課題**: GLBモデルの裏面が表面に透けて見えるInside-out
* **解決策**: マテリアル設定を強制的に「不透明Opaque」かつ「深度書き込み有効Depth Write」に変更。
## システム構成
* **Scene 1 (奥)**: 地球、大気、雲 (ECEF座標系)
* **Scene 2 (手前)**: キャラクター、ライト、環境マップ (通常座標系)

13
atmosphere/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Three Clouds</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

35
atmosphere/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "three-clouds-project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@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": {
"@types/react": "npm:types-react@^19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@^19.0.0-rc.1",
"@types/three": "^0.160.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2",
"vite": "^5.1.0"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
atmosphere/src/App.css Normal file
View File

@@ -0,0 +1 @@
/* App specific styles */

22
atmosphere/src/App.tsx Normal file
View File

@@ -0,0 +1,22 @@
import { Canvas } from '@react-three/fiber'
import { Scene } from './components/Scene'
import './App.css'
function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Canvas
gl={{
antialias: false,
depth: false,
logarithmicDepthBuffer: true,
stencil: false
}}
>
<Scene />
</Canvas>
</div>
)
}
export default App

View File

@@ -0,0 +1,187 @@
import { EffectComposer, ToneMapping } from '@react-three/postprocessing'
import { AerialPerspective, Atmosphere, Sky, Stars, type AtmosphereApi } from '@takram/three-atmosphere/r3f'
import { PrecomputedTexturesGenerator, PrecomputedTextures } from '@takram/three-atmosphere'
import { Clouds } from '@takram/three-clouds/r3f'
import { CloudShape, CloudShapeDetail, LocalWeather, Turbulence } from '@takram/three-clouds'
import { useGLTF, OrbitControls, PerspectiveCamera, Html, Environment } from '@react-three/drei'
import { Suspense, useMemo, useState, useEffect, useRef } from 'react'
import { useThree, useFrame, createPortal } from '@react-three/fiber'
import { Geodetic, radians, Ellipsoid } from '@takram/three-geospatial'
import * as THREE from 'three'
import { ToneMappingMode } from 'postprocessing'
function Model({ url, position, scale }: { url: string; position?: THREE.Vector3 | [number, number, number]; scale?: number }) {
const { scene } = useGLTF(url)
useMemo(() => {
scene.traverse((child) => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh
mesh.castShadow = true
mesh.receiveShadow = true
// Attempt to fix missing tangents warning
if (mesh.geometry && !mesh.geometry.attributes.tangent && mesh.geometry.attributes.position && mesh.geometry.attributes.uv) {
try {
mesh.geometry.computeTangents()
} catch (e) {
console.warn('Failed to compute tangents', e)
}
}
if (mesh.material) {
const mat = mesh.material as THREE.MeshStandardMaterial
// Fix "inside-out" rendering by forcing opaque depth writing
mat.transparent = false
mat.depthWrite = true
mat.depthTest = true
mat.side = THREE.FrontSide // Render only front faces
// Use alphaTest for cutouts instead of transparency
if (mat.map || mat.alphaMap) {
mat.alphaTest = 0.5
}
}
}
})
}, [scene])
return <primitive object={scene} position={position} scale={scale} />
}
const ForegroundScene = () => {
const { gl, size } = useThree()
const scene = useMemo(() => new THREE.Scene(), [])
const camera = useMemo(() => new THREE.PerspectiveCamera(50, size.width / size.height, 0.1, 1000), [size])
const cardRef = useRef<THREE.Group>(null)
useEffect(() => {
camera.position.set(0, 0, 5) // Move camera back
camera.lookAt(0, 0, 0)
}, [camera])
useFrame((_state, delta) => {
// Animate card
if (cardRef.current) {
cardRef.current.rotation.y += delta * 0.5 // Slow rotation
}
// Render foreground scene on top
const originalAutoClear = gl.autoClear
gl.autoClear = false
gl.clearDepth()
gl.render(scene, camera)
gl.autoClear = originalAutoClear
}, 2)
return createPortal(
<>
<ambientLight intensity={1} />
<directionalLight position={[5, 5, 5]} intensity={2} />
<Environment preset="city" background={false} />
<group ref={cardRef}>
<Suspense fallback={null}>
<Model url="/pkg/atmosphere/assets/card.glb" position={[0, 0, 0]} scale={0.5} />
</Suspense>
</group>
</>,
scene
)
}
export const Scene = () => {
const { gl, camera } = useThree()
const [atmosphereTextures, setAtmosphereTextures] = useState<PrecomputedTextures | null>(null)
useEffect(() => {
gl.toneMapping = THREE.NoToneMapping
gl.toneMappingExposure = 10
const generator = new PrecomputedTexturesGenerator(gl)
generator.update().then((textures) => {
setAtmosphereTextures(textures)
generator.dispose({ textures: false })
})
return () => {
// Textures should be disposed when no longer needed
}
}, [gl])
const [
localWeatherTexture,
shapeTexture,
shapeDetailTexture,
turbulenceTexture
] = useMemo(() => [
new LocalWeather(),
new CloudShape(),
new CloudShapeDetail(),
new Turbulence()
], [])
// Position setup using Geodetic (Tokyo area roughly)
const { longitude, latitude, height } = { longitude: 139.767, latitude: 35.68, height: 3000 }
const controlsRef = useRef<any>(null)
const atmosphereRef = useRef<AtmosphereApi>(null)
useEffect(() => {
if (controlsRef.current && camera) {
const geodetic = new Geodetic()
const position = new THREE.Vector3()
const up = new THREE.Vector3()
const offset = new THREE.Vector3()
const rotation = new THREE.Quaternion()
geodetic.set(radians(longitude), radians(latitude), height)
geodetic.toECEF(position)
Ellipsoid.WGS84.getSurfaceNormal(position, up)
rotation.setFromUnitVectors(camera.up, up)
offset.copy(camera.position).sub(controlsRef.current.target)
offset.applyQuaternion(rotation)
camera.up.copy(up)
camera.position.copy(position).add(new THREE.Vector3(0, 500, 2000).applyQuaternion(rotation))
controlsRef.current.target.copy(position)
controlsRef.current.update()
}
}, [camera, longitude, latitude, height])
// UTC 03:00 is 12:00 JST (Noon)
const date = useMemo(() => new Date('2000-06-01T03:00:00Z'), [])
useFrame(() => {
if (atmosphereRef.current) {
atmosphereRef.current.updateByDate(date)
}
})
if (!atmosphereTextures) {
return <Html center>Generating Atmosphere...</Html>
}
return (
<>
<PerspectiveCamera makeDefault far={1e6} near={10} />
<OrbitControls ref={controlsRef} makeDefault minDistance={100} />
<Atmosphere ref={atmosphereRef} textures={atmosphereTextures}>
<Sky />
<Stars />
<EffectComposer enableNormalPass frameBufferType={THREE.HalfFloatType} multisampling={8}>
<Clouds
qualityPreset='high'
coverage={0.4}
localWeatherTexture={localWeatherTexture}
shapeTexture={shapeTexture}
shapeDetailTexture={shapeDetailTexture}
turbulenceTexture={turbulenceTexture}
/>
<AerialPerspective sky sunLight skyLight />
<ToneMapping mode={ToneMappingMode.AGX} />
</EffectComposer>
</Atmosphere>
<ForegroundScene />
</>
)
}

View File

@@ -0,0 +1,9 @@
export function setupCounter(element: HTMLButtonElement) {
let counter = 0
const setCounter = (count: number) => {
counter = count
element.innerHTML = `count is ${counter}`
}
element.addEventListener('click', () => setCounter(counter + 1))
setCounter(0)
}

7
atmosphere/src/index.css Normal file
View File

@@ -0,0 +1,7 @@
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
}

24
atmosphere/src/main.ts Normal file
View File

@@ -0,0 +1,24 @@
import './style.css'
import typescriptLogo from './typescript.svg'
import viteLogo from '/vite.svg'
import { setupCounter } from './counter.ts'
document.querySelector<HTMLDivElement>('#root')!.innerHTML = `
<div>
<a href="https://vite.dev" target="_blank">
<img src="${viteLogo}" class="logo" alt="Vite logo" />
</a>
<a href="https://www.typescriptlang.org/" target="_blank">
<img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
</a>
<h1>Vite + TypeScript</h1>
<div class="card">
<button id="counter" type="button"></button>
</div>
<p class="read-the-docs">
Click on the Vite and TypeScript logos to learn more
</p>
</div>
`
setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)

10
atmosphere/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

96
atmosphere/src/style.css Normal file
View File

@@ -0,0 +1,96 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #3178c6aa);
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
atmosphere/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

33
atmosphere/tsconfig.json Normal file
View File

@@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts"
]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})

View File

@@ -12,7 +12,13 @@ $ npm run start
# npm install three three-stdlib @types/three @react-three/fiber @react-three/fiber @react-three/drei @react-three/postprocessing # npm install three three-stdlib @types/three @react-three/fiber @react-three/fiber @react-three/drei @react-three/postprocessing
``` ```
## tips
- `?ms=0`
- `?g=moon`
## ref ## ref
- galaxy.glb : https://sketchfab.com/3d-models/need-some-space-d6521362b37b48e3a82bce4911409303 - galaxy.glb : https://sketchfab.com/3d-models/need-some-space-d6521362b37b48e3a82bce4911409303
- tsx : https://gist.github.com/artokun/fb7f0c68a01ba5d9813abb3ccce254c4 - tsx : https://gist.github.com/artokun/fb7f0c68a01ba5d9813abb3ccce254c4

1
holo/.env Normal file
View File

@@ -0,0 +1 @@
GENERATE_SOURCEMAP=false

24
holo/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*-lock.json

52
holo/package.json Normal file
View File

@@ -0,0 +1,52 @@
{
"name": "holo",
"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"
]
}
}

BIN
holo/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

43
holo/public/index.html Normal file
View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href=".%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href=".%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href=".%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
holo/public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
holo/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
holo/public/manifest.json Normal file
View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
holo/public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

20
holo/readme.md Normal file
View File

@@ -0,0 +1,20 @@
react-three-fiber + three-vrm
```sh
$ npm i
$ npm run start
```
## install
```sh
# npx create-react-app vrm1 --template typescript
# npm install three three-stdlib @types/three @react-three/fiber @react-three/fiber @react-three/drei @react-three/postprocessing
```
## three-vrm
`vrm 1.0`
unity export

39
holo/src/App.css Normal file
View File

@@ -0,0 +1,39 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

9
holo/src/App.test.tsx Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

16
holo/src/App.tsx Normal file
View File

@@ -0,0 +1,16 @@
import React from 'react'
import VRMModelCanvas from './pages/vrm'
import ScreenTimeCanvas from './pages/time'
import ScreenApiCanvas from './pages/api'
const App = () => {
return (
<>
<VRMModelCanvas/>
<ScreenTimeCanvas/>
<ScreenApiCanvas/>
</>
)
}
export default App;

39
holo/src/index.css Normal file
View File

@@ -0,0 +1,39 @@
body {
height: 100%;
margin: 0;
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
html {
height: 100%;
}
.time {
transform:scale(-1,1);
position: absolute;
top: 10px;
padding: 10px;
z-index: 100;
color: #e9ff00;
font-size: 30px;
text-align: center;
}
.api {
transform:scale(-1,1);
position: absolute;
top: 50px;
padding: 10px;
z-index: 100;
color: #e9ff00;
font-size: 25px;
text-align: center;
}

19
holo/src/index.tsx Normal file
View File

@@ -0,0 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

1
holo/src/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

58
holo/src/pages/api.tsx Normal file
View File

@@ -0,0 +1,58 @@
//import React, { useState, useEffect } from 'react';
//import axios from 'axios';
//interface Post {
// id: number;
// username: string;
// planet: number;
//}
//const ScreenApiCanvas: React.FC = () => {
// const [posts, setPosts] = useState<Post[]>([]);
// const [loading, setLoading] = useState(true);
// const [error, setError] = useState<string | null>(null);
// const searchParams = new URLSearchParams(window.location.search);
// var id = searchParams.get('id') ?? '2';
// var url = 'https://api.syui.ai/users/' + id;
// useEffect(() => {
// const fetchPosts = async () => {
// try {
// const response = await axios.get<Post[]>(url);
// setPosts(response.data);
// setLoading(false);
// } catch (err) {
// setError('error network api');
// setLoading(false);
// }
// };
//
// fetchPosts();
// }, [url]);
//
// if (loading) return <div>読み込み中...</div>;
// if (error) return <div>{error}</div>;
//
// return (
// <div className="api">
// <ul>
// {posts.map(post => (
// <li key={post.id}>{post.username} / M {post.planet}</li>
// ))}
// </ul>
// </div>
// );
//};
import React from 'react';
const ScreenApiCanvas: React.FC = () => {
const searchParams = new URLSearchParams(window.location.search);
var m = searchParams.get('m') ?? '0';
var u = searchParams.get('u') ?? 'ai';
return (
<div className="api">
<p>{u} / M{m}</p>
</div>
);
};
export default ScreenApiCanvas;

37
holo/src/pages/time.tsx Normal file
View File

@@ -0,0 +1,37 @@
import React, { useState, useEffect } from 'react';
//function reverseString(str: string): string {
// return str.split('').reverse().join('');
//}
const ScreenTimeCanvas: React.FC = () => {
const [currentDateTime, setCurrentDateTime] = useState<string>('');
//const [reversedDateTime, setReversedDateTime] = useState<string>('');
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 (
<div className="time">
<p>{currentDateTime}</p>
</div>
);
};
export default ScreenTimeCanvas;

85
holo/src/pages/vrm.tsx Normal file
View File

@@ -0,0 +1,85 @@
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";
import { VRMSpringBoneManager } from '@pixiv/three-vrm-springbone';
interface ModelProps {
url: string
url_anim: string
scale: number
position: [number, number, number]
rotation: [number, number, number]
}
const VRMModel: React.FC<ModelProps> = ({ url, url_anim, position, rotation, scale }) => {
const [vrm, setVrm] = useState<VRM | null>(null);
const mixerRef = useRef<THREE.AnimationMixer | null>(null);
const springBoneManagerRef = useRef<VRMSpringBoneManager | null>(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);
springBoneManagerRef.current = vrmModel.springBoneManager as VRMSpringBoneManager;
springBoneManagerRef.current?.reset();
setVrm(vrmModel);
if (vrmModel) {
vrmModel.scene.rotation.set(...rotation);
vrmModel.scene.position.set(...position);
vrmModel.scene.scale.setScalar(scale);
}
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, position, scale, rotation]);
useFrame((state, delta) => {
if (mixerRef.current) mixerRef.current.update(delta);
if (springBoneManagerRef.current) springBoneManagerRef.current.update(delta);
if (vrm) vrm.update(delta);
});
return vrm ? <primitive object={vrm.scene} /> : null;
};
export const VRMModelCanvas = () => {
return (
<div style={{ height: '100vh', width: '100vw' }}>
<Canvas
shadows
gl={{
//toneMapping: THREE.ACESFilmicToneMapping,
//toneMapping: THREE.ReinhardToneMapping,
toneMapping: THREE.NeutralToneMapping,
toneMappingExposure: 1,
alpha: true,
powerPreference: "high-performance",
antialias: true,
//stencil: false,
//depth: false
}}
camera={{ position: [1.2, 0, 0] }}>
<color attach="background" args={["#000"]} /> {/* Light gray background */}
<OrbitControls dampingFactor={0.05} rotateSpeed={0.1} zoomSpeed={0.5}/>
<ambientLight intensity={10} />
<pointLight position={[10, 10, 10]} />
<VRMModel url="./models/t.vrm" url_anim="./models/default.vrma" position={[0, -0.6, 0]} rotation={[0, 89.5, 0]} scale={1} />
</Canvas>
</div>
)
}
export default VRMModelCanvas;

1
holo/src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
holo/src/setupTests.ts Normal file
View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

26
holo/tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}