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 { LOCATIONS, teleportTo } from './worldState';
|
||||
import { adminMode, onAdminChange } from './controls/KeyInput';
|
||||
import ControlPanel from './ui/ControlPanel';
|
||||
|
||||
const ACTION_SEQUENCE = ['attack', 'skill', 'jump', 'fly_dodge', 'damage'];
|
||||
const TELEPORT_ANIM = 'fly_dodge';
|
||||
@@ -16,6 +17,9 @@ export default function App() {
|
||||
const [animState, setAnimState] = useState({ name: 'fly_idle', count: 0 });
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
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 teleportIndexRef = useRef(0);
|
||||
const countRef = useRef(0);
|
||||
@@ -50,6 +54,7 @@ export default function App() {
|
||||
} else if (e.code === 'KeyS') {
|
||||
e.preventDefault();
|
||||
playAnim('skill');
|
||||
setVrmModel(prev => prev === 'ai.vrm' ? 'ai_mode.vrm' : 'ai.vrm');
|
||||
}
|
||||
}, [playAnim, view]);
|
||||
|
||||
@@ -90,6 +95,7 @@ export default function App() {
|
||||
|
||||
const doSkill = useCallback(() => {
|
||||
playAnim('skill');
|
||||
setVrmModel(prev => prev === 'ai.vrm' ? 'ai_mode.vrm' : 'ai.vrm');
|
||||
}, [playAnim]);
|
||||
|
||||
const appStartRef = useRef(Date.now());
|
||||
@@ -154,17 +160,28 @@ export default function App() {
|
||||
{/* Atmosphere background */}
|
||||
<div style={{ ...layerStyle, zIndex: 0, opacity: isNasa ? 0 : 1, transition: 'opacity 1.5s ease' }}>
|
||||
<Canvas gl={{ alpha: true, antialias: true }}>
|
||||
<AtmosphereScene />
|
||||
<AtmosphereScene timeScale={timeScale} />
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
{/* Avatar layer */}
|
||||
<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' }}>
|
||||
<AvatarScene selectedAnimation={animState} onZoomOut={handleZoomOut} />
|
||||
<AvatarScene selectedAnimation={animState} onZoomOut={handleZoomOut} camSpeed={camSpeed} vrmModel={vrmModel} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Control panel */}
|
||||
{!isNasa && (
|
||||
<ControlPanel
|
||||
timeScale={timeScale}
|
||||
onTimeScaleChange={setTimeScale}
|
||||
camSpeed={camSpeed}
|
||||
onCamSpeedChange={setCamSpeed}
|
||||
onSkill={doSkill}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Admin controls */}
|
||||
{isAdmin && !isNasa && (
|
||||
<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 sunRef = useRef();
|
||||
const atmosphereRef = useRef();
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -93,7 +95,7 @@ export default function AtmosphereScene() {
|
||||
|
||||
useFrame((_, delta) => {
|
||||
const currentDate = dateRef.current;
|
||||
currentDate.setTime(currentDate.getTime() + delta * TIME_SCALE * 1000);
|
||||
currentDate.setTime(currentDate.getTime() + delta * timeScaleRef.current * 1000);
|
||||
|
||||
if (atmosphereRef.current) {
|
||||
atmosphereRef.current.updateByDate(currentDate);
|
||||
|
||||
@@ -31,12 +31,13 @@ const CAM_APPROACH_SPEED = 0.08; // 接近の補間速度
|
||||
|
||||
// --- Components ---
|
||||
|
||||
function RotatingStage({ children }) {
|
||||
function RotatingStage({ children, speed }) {
|
||||
const groupRef = useRef(null);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
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;
|
||||
worldState.stageRotationY = angle;
|
||||
}
|
||||
@@ -111,7 +112,7 @@ function CameraDistanceMonitor({ onDistanceChange }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function AvatarScene({ selectedAnimation: animState, onZoomOut }) {
|
||||
export default function AvatarScene({ selectedAnimation: animState, onZoomOut, camSpeed: camSpeedProp, vrmModel }) {
|
||||
const selectedAnimation = animState;
|
||||
const animName = animState?.name || animState;
|
||||
const [effect, setEffect] = useState(null);
|
||||
@@ -141,8 +142,8 @@ export default function AvatarScene({ selectedAnimation: animState, onZoomOut })
|
||||
<ambientLight intensity={1.0} />
|
||||
<spotLight position={[0, 2, -2]} intensity={3} color="#ffdcb4" />
|
||||
<Suspense fallback={null}>
|
||||
<RotatingStage>
|
||||
<VrmCharacter selectedAnimation={selectedAnimation} />
|
||||
<RotatingStage speed={camSpeedProp}>
|
||||
<VrmCharacter key={vrmModel} selectedAnimation={selectedAnimation} vrmModel={vrmModel} />
|
||||
{effect && <EnergySphere type={effect.type} position={[0, 1.0, 0]} scale={6} />}
|
||||
</RotatingStage>
|
||||
</Suspense>
|
||||
|
||||
@@ -5,12 +5,12 @@ import * as THREE from 'three';
|
||||
const SPHERE_PRESETS = {
|
||||
sun: {
|
||||
layers: [
|
||||
{ radius: 0.3, segments: 64, size: 0.015, opacity: 0.8 },
|
||||
{ radius: 0.22, segments: 48, size: 0.02, opacity: 0.5 },
|
||||
{ radius: 0.38, segments: 40, size: 0.008, opacity: 0.3 },
|
||||
{ radius: 0.3, segments: 64, size: 0.015, opacity: 0.5 },
|
||||
{ radius: 0.22, segments: 48, size: 0.02, opacity: 0.3 },
|
||||
{ radius: 0.38, segments: 40, size: 0.008, opacity: 0.2 },
|
||||
],
|
||||
lightColor: '#ffaa22',
|
||||
lightIntensity: 3,
|
||||
lightIntensity: 1.5,
|
||||
rotateSpeed: 1.5,
|
||||
pulseSpeed: 2.0,
|
||||
pulseRange: 0.25,
|
||||
@@ -22,11 +22,11 @@ const SPHERE_PRESETS = {
|
||||
},
|
||||
moon: {
|
||||
layers: [
|
||||
{ radius: 0.2, segments: 48, size: 0.01, opacity: 0.6 },
|
||||
{ radius: 0.26, segments: 32, size: 0.006, opacity: 0.25 },
|
||||
{ radius: 0.2, segments: 48, size: 0.01, opacity: 0.4 },
|
||||
{ radius: 0.26, segments: 32, size: 0.006, opacity: 0.15 },
|
||||
],
|
||||
lightColor: '#aaccff',
|
||||
lightIntensity: 2,
|
||||
lightIntensity: 1.0,
|
||||
rotateSpeed: 0.8,
|
||||
pulseSpeed: 1.0,
|
||||
pulseRange: 0.12,
|
||||
@@ -140,7 +140,7 @@ function EnergySphere({ type = 'sun', position = [0, 1.5, 0], scale = 1 }) {
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import * as THREE from 'three';
|
||||
import { keys, adminMode } from './controls/KeyInput';
|
||||
|
||||
const BASE_URL = import.meta.env.BASE_URL;
|
||||
const VRM_URL = `${BASE_URL}model/ai.vrm`;
|
||||
|
||||
export const VRMA_FILES = [
|
||||
{ name: 'idle', label: '待機', file: 'idle.vrma', loop: true },
|
||||
@@ -54,14 +53,15 @@ function trimClip(clip, duration, addLoopPose) {
|
||||
return clip;
|
||||
}
|
||||
|
||||
export default function VrmCharacter({ selectedAnimation: animState }) {
|
||||
export default function VrmCharacter({ selectedAnimation: animState, vrmModel = 'ai.vrm' }) {
|
||||
const selectedAnimation = animState?.name || animState;
|
||||
const mixerRef = useRef(null);
|
||||
const vrmRef = useRef(null);
|
||||
const actionsRef = useRef({});
|
||||
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));
|
||||
});
|
||||
|
||||
|
||||
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