add vrm mode
This commit is contained in:
21
src/App.jsx
21
src/App.jsx
@@ -5,6 +5,7 @@ import AtmosphereScene from './AtmosphereScene';
|
|||||||
import AvatarScene from './AvatarScene';
|
import AvatarScene from './AvatarScene';
|
||||||
import { LOCATIONS, teleportTo } from './worldState';
|
import { LOCATIONS, teleportTo } from './worldState';
|
||||||
import { adminMode, onAdminChange } from './controls/KeyInput';
|
import { adminMode, onAdminChange } from './controls/KeyInput';
|
||||||
|
import ControlPanel from './ui/ControlPanel';
|
||||||
|
|
||||||
const ACTION_SEQUENCE = ['attack', 'skill', 'jump', 'fly_dodge', 'damage'];
|
const ACTION_SEQUENCE = ['attack', 'skill', 'jump', 'fly_dodge', 'damage'];
|
||||||
const TELEPORT_ANIM = 'fly_dodge';
|
const TELEPORT_ANIM = 'fly_dodge';
|
||||||
@@ -16,6 +17,9 @@ export default function App() {
|
|||||||
const [animState, setAnimState] = useState({ name: 'fly_idle', count: 0 });
|
const [animState, setAnimState] = useState({ name: 'fly_idle', count: 0 });
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const [view, setView] = useState('avatar'); // 'avatar' | 'nasa'
|
const [view, setView] = useState('avatar'); // 'avatar' | 'nasa'
|
||||||
|
const [timeScale, setTimeScale] = useState(100);
|
||||||
|
const [camSpeed, setCamSpeed] = useState(0.05);
|
||||||
|
const [vrmModel, setVrmModel] = useState('ai.vrm');
|
||||||
const actionIndexRef = useRef(0);
|
const actionIndexRef = useRef(0);
|
||||||
const teleportIndexRef = useRef(0);
|
const teleportIndexRef = useRef(0);
|
||||||
const countRef = useRef(0);
|
const countRef = useRef(0);
|
||||||
@@ -50,6 +54,7 @@ export default function App() {
|
|||||||
} else if (e.code === 'KeyS') {
|
} else if (e.code === 'KeyS') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
playAnim('skill');
|
playAnim('skill');
|
||||||
|
setVrmModel(prev => prev === 'ai.vrm' ? 'ai_mode.vrm' : 'ai.vrm');
|
||||||
}
|
}
|
||||||
}, [playAnim, view]);
|
}, [playAnim, view]);
|
||||||
|
|
||||||
@@ -90,6 +95,7 @@ export default function App() {
|
|||||||
|
|
||||||
const doSkill = useCallback(() => {
|
const doSkill = useCallback(() => {
|
||||||
playAnim('skill');
|
playAnim('skill');
|
||||||
|
setVrmModel(prev => prev === 'ai.vrm' ? 'ai_mode.vrm' : 'ai.vrm');
|
||||||
}, [playAnim]);
|
}, [playAnim]);
|
||||||
|
|
||||||
const appStartRef = useRef(Date.now());
|
const appStartRef = useRef(Date.now());
|
||||||
@@ -154,17 +160,28 @@ export default function App() {
|
|||||||
{/* Atmosphere background */}
|
{/* Atmosphere background */}
|
||||||
<div style={{ ...layerStyle, zIndex: 0, opacity: isNasa ? 0 : 1, transition: 'opacity 1.5s ease' }}>
|
<div style={{ ...layerStyle, zIndex: 0, opacity: isNasa ? 0 : 1, transition: 'opacity 1.5s ease' }}>
|
||||||
<Canvas gl={{ alpha: true, antialias: true }}>
|
<Canvas gl={{ alpha: true, antialias: true }}>
|
||||||
<AtmosphereScene />
|
<AtmosphereScene timeScale={timeScale} />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Avatar layer */}
|
{/* Avatar layer */}
|
||||||
<div style={{ ...layerStyle, zIndex: 1, pointerEvents: 'none', opacity: isNasa ? 0 : 1, transition: 'opacity 1.5s ease' }}>
|
<div style={{ ...layerStyle, zIndex: 1, pointerEvents: 'none', opacity: isNasa ? 0 : 1, transition: 'opacity 1.5s ease' }}>
|
||||||
<div style={{ width: '100%', height: '100%', pointerEvents: isNasa ? 'none' : 'auto' }}>
|
<div style={{ width: '100%', height: '100%', pointerEvents: isNasa ? 'none' : 'auto' }}>
|
||||||
<AvatarScene selectedAnimation={animState} onZoomOut={handleZoomOut} />
|
<AvatarScene selectedAnimation={animState} onZoomOut={handleZoomOut} camSpeed={camSpeed} vrmModel={vrmModel} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Control panel */}
|
||||||
|
{!isNasa && (
|
||||||
|
<ControlPanel
|
||||||
|
timeScale={timeScale}
|
||||||
|
onTimeScaleChange={setTimeScale}
|
||||||
|
camSpeed={camSpeed}
|
||||||
|
onCamSpeedChange={setCamSpeed}
|
||||||
|
onSkill={doSkill}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Admin controls */}
|
{/* Admin controls */}
|
||||||
{isAdmin && !isNasa && (
|
{isAdmin && !isNasa && (
|
||||||
<div style={{ position: 'absolute', top: 16, left: 16, zIndex: 2, display: 'flex', gap: 8, alignItems: 'center' }}>
|
<div style={{ position: 'absolute', top: 16, left: 16, zIndex: 2, display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
|
|||||||
@@ -69,11 +69,13 @@ function GoogleMaps3DTiles() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AtmosphereScene() {
|
export default function AtmosphereScene({ timeScale: timeScaleProp }) {
|
||||||
const { gl } = useThree();
|
const { gl } = useThree();
|
||||||
const sunRef = useRef();
|
const sunRef = useRef();
|
||||||
const atmosphereRef = useRef();
|
const atmosphereRef = useRef();
|
||||||
const dateRef = useRef(new Date(INITIAL_DATE));
|
const dateRef = useRef(new Date(INITIAL_DATE));
|
||||||
|
const timeScaleRef = useRef(timeScaleProp ?? TIME_SCALE);
|
||||||
|
timeScaleRef.current = timeScaleProp ?? TIME_SCALE;
|
||||||
const [weather, setWeather] = useState(WEATHER_PRESETS[1]);
|
const [weather, setWeather] = useState(WEATHER_PRESETS[1]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -93,7 +95,7 @@ export default function AtmosphereScene() {
|
|||||||
|
|
||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
const currentDate = dateRef.current;
|
const currentDate = dateRef.current;
|
||||||
currentDate.setTime(currentDate.getTime() + delta * TIME_SCALE * 1000);
|
currentDate.setTime(currentDate.getTime() + delta * timeScaleRef.current * 1000);
|
||||||
|
|
||||||
if (atmosphereRef.current) {
|
if (atmosphereRef.current) {
|
||||||
atmosphereRef.current.updateByDate(currentDate);
|
atmosphereRef.current.updateByDate(currentDate);
|
||||||
|
|||||||
@@ -31,12 +31,13 @@ const CAM_APPROACH_SPEED = 0.08; // 接近の補間速度
|
|||||||
|
|
||||||
// --- Components ---
|
// --- Components ---
|
||||||
|
|
||||||
function RotatingStage({ children }) {
|
function RotatingStage({ children, speed }) {
|
||||||
const groupRef = useRef(null);
|
const groupRef = useRef(null);
|
||||||
|
|
||||||
useFrame(({ clock }) => {
|
useFrame(({ clock }) => {
|
||||||
if (groupRef.current) {
|
if (groupRef.current) {
|
||||||
const angle = clock.getElapsedTime() * STAGE_ROTATE_SPEED;
|
const s = speed != null ? speed : STAGE_ROTATE_SPEED;
|
||||||
|
const angle = clock.getElapsedTime() * s;
|
||||||
groupRef.current.rotation.y = angle;
|
groupRef.current.rotation.y = angle;
|
||||||
worldState.stageRotationY = angle;
|
worldState.stageRotationY = angle;
|
||||||
}
|
}
|
||||||
@@ -111,7 +112,7 @@ function CameraDistanceMonitor({ onDistanceChange }) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AvatarScene({ selectedAnimation: animState, onZoomOut }) {
|
export default function AvatarScene({ selectedAnimation: animState, onZoomOut, camSpeed: camSpeedProp, vrmModel }) {
|
||||||
const selectedAnimation = animState;
|
const selectedAnimation = animState;
|
||||||
const animName = animState?.name || animState;
|
const animName = animState?.name || animState;
|
||||||
const [effect, setEffect] = useState(null);
|
const [effect, setEffect] = useState(null);
|
||||||
@@ -141,8 +142,8 @@ export default function AvatarScene({ selectedAnimation: animState, onZoomOut })
|
|||||||
<ambientLight intensity={1.0} />
|
<ambientLight intensity={1.0} />
|
||||||
<spotLight position={[0, 2, -2]} intensity={3} color="#ffdcb4" />
|
<spotLight position={[0, 2, -2]} intensity={3} color="#ffdcb4" />
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<RotatingStage>
|
<RotatingStage speed={camSpeedProp}>
|
||||||
<VrmCharacter selectedAnimation={selectedAnimation} />
|
<VrmCharacter key={vrmModel} selectedAnimation={selectedAnimation} vrmModel={vrmModel} />
|
||||||
{effect && <EnergySphere type={effect.type} position={[0, 1.0, 0]} scale={6} />}
|
{effect && <EnergySphere type={effect.type} position={[0, 1.0, 0]} scale={6} />}
|
||||||
</RotatingStage>
|
</RotatingStage>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import * as THREE from 'three';
|
|||||||
const SPHERE_PRESETS = {
|
const SPHERE_PRESETS = {
|
||||||
sun: {
|
sun: {
|
||||||
layers: [
|
layers: [
|
||||||
{ radius: 0.3, segments: 64, size: 0.015, opacity: 0.8 },
|
{ radius: 0.3, segments: 64, size: 0.015, opacity: 0.5 },
|
||||||
{ radius: 0.22, segments: 48, size: 0.02, opacity: 0.5 },
|
{ radius: 0.22, segments: 48, size: 0.02, opacity: 0.3 },
|
||||||
{ radius: 0.38, segments: 40, size: 0.008, opacity: 0.3 },
|
{ radius: 0.38, segments: 40, size: 0.008, opacity: 0.2 },
|
||||||
],
|
],
|
||||||
lightColor: '#ffaa22',
|
lightColor: '#ffaa22',
|
||||||
lightIntensity: 3,
|
lightIntensity: 1.5,
|
||||||
rotateSpeed: 1.5,
|
rotateSpeed: 1.5,
|
||||||
pulseSpeed: 2.0,
|
pulseSpeed: 2.0,
|
||||||
pulseRange: 0.25,
|
pulseRange: 0.25,
|
||||||
@@ -22,11 +22,11 @@ const SPHERE_PRESETS = {
|
|||||||
},
|
},
|
||||||
moon: {
|
moon: {
|
||||||
layers: [
|
layers: [
|
||||||
{ radius: 0.2, segments: 48, size: 0.01, opacity: 0.6 },
|
{ radius: 0.2, segments: 48, size: 0.01, opacity: 0.4 },
|
||||||
{ radius: 0.26, segments: 32, size: 0.006, opacity: 0.25 },
|
{ radius: 0.26, segments: 32, size: 0.006, opacity: 0.15 },
|
||||||
],
|
],
|
||||||
lightColor: '#aaccff',
|
lightColor: '#aaccff',
|
||||||
lightIntensity: 2,
|
lightIntensity: 1.0,
|
||||||
rotateSpeed: 0.8,
|
rotateSpeed: 0.8,
|
||||||
pulseSpeed: 1.0,
|
pulseSpeed: 1.0,
|
||||||
pulseRange: 0.12,
|
pulseRange: 0.12,
|
||||||
@@ -140,7 +140,7 @@ function EnergySphere({ type = 'sun', position = [0, 1.5, 0], scale = 1 }) {
|
|||||||
/>
|
/>
|
||||||
</group>
|
</group>
|
||||||
))}
|
))}
|
||||||
<pointLight position={[0, 0, 0]} intensity={preset.lightIntensity} color={preset.lightColor} distance={5} />
|
<pointLight position={[0, 0, 0]} intensity={preset.lightIntensity} color={preset.lightColor} distance={3} />
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import * as THREE from 'three';
|
|||||||
import { keys, adminMode } from './controls/KeyInput';
|
import { keys, adminMode } from './controls/KeyInput';
|
||||||
|
|
||||||
const BASE_URL = import.meta.env.BASE_URL;
|
const BASE_URL = import.meta.env.BASE_URL;
|
||||||
const VRM_URL = `${BASE_URL}model/ai.vrm`;
|
|
||||||
|
|
||||||
export const VRMA_FILES = [
|
export const VRMA_FILES = [
|
||||||
{ name: 'idle', label: '待機', file: 'idle.vrma', loop: true },
|
{ name: 'idle', label: '待機', file: 'idle.vrma', loop: true },
|
||||||
@@ -54,14 +53,15 @@ function trimClip(clip, duration, addLoopPose) {
|
|||||||
return clip;
|
return clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VrmCharacter({ selectedAnimation: animState }) {
|
export default function VrmCharacter({ selectedAnimation: animState, vrmModel = 'ai.vrm' }) {
|
||||||
const selectedAnimation = animState?.name || animState;
|
const selectedAnimation = animState?.name || animState;
|
||||||
const mixerRef = useRef(null);
|
const mixerRef = useRef(null);
|
||||||
const vrmRef = useRef(null);
|
const vrmRef = useRef(null);
|
||||||
const actionsRef = useRef({});
|
const actionsRef = useRef({});
|
||||||
const currentActionRef = useRef(null);
|
const currentActionRef = useRef(null);
|
||||||
|
|
||||||
const gltf = useLoader(GLTFLoader, VRM_URL, (loader) => {
|
const vrmUrl = `${BASE_URL}model/${vrmModel}`;
|
||||||
|
const gltf = useLoader(GLTFLoader, vrmUrl, (loader) => {
|
||||||
loader.register((parser) => new VRMLoaderPlugin(parser));
|
loader.register((parser) => new VRMLoaderPlugin(parser));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
85
src/ui/ControlPanel.jsx
Normal file
85
src/ui/ControlPanel.jsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const containerStyle = {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
zIndex: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
padding: '10px 12px',
|
||||||
|
background: 'rgba(0,0,0,0.3)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
minWidth: 140,
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelStyle = {
|
||||||
|
color: 'rgba(255,255,255,0.7)',
|
||||||
|
fontSize: '11px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sliderStyle = {
|
||||||
|
width: '100%',
|
||||||
|
height: 4,
|
||||||
|
appearance: 'none',
|
||||||
|
background: 'rgba(255,255,255,0.2)',
|
||||||
|
borderRadius: 2,
|
||||||
|
outline: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnStyle = {
|
||||||
|
padding: '4px 10px',
|
||||||
|
background: 'rgba(255,255,255,0.1)',
|
||||||
|
color: 'rgba(255,255,255,0.7)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '11px',
|
||||||
|
width: '100%',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ControlPanel({ timeScale, onTimeScaleChange, camSpeed, onCamSpeedChange, onSkill }) {
|
||||||
|
return (
|
||||||
|
<div style={containerStyle}>
|
||||||
|
<div>
|
||||||
|
<div style={labelStyle}>
|
||||||
|
<span>Time</span>
|
||||||
|
<span>{timeScale}x</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={5000}
|
||||||
|
step={50}
|
||||||
|
value={timeScale}
|
||||||
|
onChange={(e) => onTimeScaleChange(Number(e.target.value))}
|
||||||
|
style={sliderStyle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={labelStyle}>
|
||||||
|
<span>Camera</span>
|
||||||
|
<span>{camSpeed.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={1}
|
||||||
|
max={20}
|
||||||
|
step={1}
|
||||||
|
value={camSpeed * 100}
|
||||||
|
onChange={(e) => onCamSpeedChange(Number(e.target.value) / 100)}
|
||||||
|
style={sliderStyle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button style={btnStyle} onClick={() => onSkill?.()}>
|
||||||
|
Skill
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user