feat(wardrobe): swap Live2D outfit textures via wardrobe buttons
Epsilon model has 3 texture sheets: body (00), hair (01), clothes (02). Outfit PNGs in /outfits/ replace texture_02 at runtime via PIXI BaseTexture swap. Live2DStage now accepts outfit prop and swaps on change. App passes currentOutfit to Live2DStage.
This commit is contained in:
@@ -110,6 +110,7 @@ export default function App() {
|
|||||||
onKiraReady={(model) => { kiraModelRef.current = model; }}
|
onKiraReady={(model) => { kiraModelRef.current = model; }}
|
||||||
onReady={() => setLive2dReady(true)}
|
onReady={() => setLive2dReady(true)}
|
||||||
isSpeaking={isKiraSpeaking}
|
isSpeaking={isKiraSpeaking}
|
||||||
|
outfit={currentOutfit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Particles type={currentScene.particles ?? 'none'} />
|
<Particles type={currentScene.particles ?? 'none'} />
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ interface Props {
|
|||||||
onKiraReady?: (model: any) => void;
|
onKiraReady?: (model: any) => void;
|
||||||
onReady?: () => void;
|
onReady?: () => void;
|
||||||
isSpeaking?: boolean;
|
isSpeaking?: boolean;
|
||||||
|
outfit?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,8 +47,10 @@ function positionCat(cat: any) {
|
|||||||
cat.position.set(r.left + r.width / 2, r.top + r.height / 2 + 10);
|
cat.position.set(r.left + r.width / 2, r.top + r.height / 2 + 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Live2DStage({ onKiraReady, onReady }: Props) {
|
export default function Live2DStage({ onKiraReady, onReady, outfit }: Props) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const kiraModelRef = useRef<any>(null);
|
||||||
|
const clothTexIdxRef = useRef<number>(-1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
@@ -96,6 +99,22 @@ export default function Live2DStage({ onKiraReady, onReady }: Props) {
|
|||||||
(kiraModel as any).isInteractive = () => false;
|
(kiraModel as any).isInteractive = () => false;
|
||||||
app.stage.addChild(kiraModel as any);
|
app.stage.addChild(kiraModel as any);
|
||||||
|
|
||||||
|
kiraModelRef.current = kiraModel;
|
||||||
|
|
||||||
|
// Find the clothing texture index (texture_02 = the outfit sheet)
|
||||||
|
try {
|
||||||
|
const core = (kiraModel as any).internalModel?.coreModel;
|
||||||
|
if (core) {
|
||||||
|
const drawCount = core.getDrawableCount();
|
||||||
|
// texture_02 is the last texture, find which drawable uses it
|
||||||
|
// We'll swap the PIXI texture directly on the model's textures array
|
||||||
|
const textures = (kiraModel as any).internalModel?.textures;
|
||||||
|
if (textures && textures.length >= 3) {
|
||||||
|
clothTexIdxRef.current = 2; // texture_02 is the outfit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
try { kiraModel.motion('Idle'); } catch {}
|
try { kiraModel.motion('Idle'); } catch {}
|
||||||
try { kiraModel.expression('Normal'); } catch {}
|
try { kiraModel.expression('Normal'); } catch {}
|
||||||
|
|
||||||
@@ -135,6 +154,42 @@ export default function Live2DStage({ onKiraReady, onReady }: Props) {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Outfit swap effect
|
||||||
|
useEffect(() => {
|
||||||
|
const model = kiraModelRef.current;
|
||||||
|
const texIdx = clothTexIdxRef.current;
|
||||||
|
if (!model || texIdx < 0 || !outfit) return;
|
||||||
|
|
||||||
|
const outfitSrc = `/live2d/models/kira/outfits/${outfit}.png`;
|
||||||
|
|
||||||
|
const swapTexture = async () => {
|
||||||
|
try {
|
||||||
|
const textures = (model as any).internalModel?.textures;
|
||||||
|
if (!textures || !textures[texIdx]) return;
|
||||||
|
|
||||||
|
const PIXI = await import('pixi.js');
|
||||||
|
const newBase = PIXI.BaseTexture.from(outfitSrc);
|
||||||
|
|
||||||
|
const doSwap = () => {
|
||||||
|
if (newBase.valid) {
|
||||||
|
textures[texIdx].baseTexture = newBase;
|
||||||
|
textures[texIdx].update();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newBase.valid) {
|
||||||
|
doSwap();
|
||||||
|
} else {
|
||||||
|
newBase.once('loaded', doSwap);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Live2DStage] outfit swap failed:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
swapTexture();
|
||||||
|
}, [outfit]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
|
|||||||
Reference in New Issue
Block a user