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; }}
|
||||
onReady={() => setLive2dReady(true)}
|
||||
isSpeaking={isKiraSpeaking}
|
||||
outfit={currentOutfit}
|
||||
/>
|
||||
|
||||
<Particles type={currentScene.particles ?? 'none'} />
|
||||
|
||||
@@ -4,6 +4,7 @@ interface Props {
|
||||
onKiraReady?: (model: any) => void;
|
||||
onReady?: () => void;
|
||||
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);
|
||||
}
|
||||
|
||||
export default function Live2DStage({ onKiraReady, onReady }: Props) {
|
||||
export default function Live2DStage({ onKiraReady, onReady, outfit }: Props) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const kiraModelRef = useRef<any>(null);
|
||||
const clothTexIdxRef = useRef<number>(-1);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
@@ -96,6 +99,22 @@ export default function Live2DStage({ onKiraReady, onReady }: Props) {
|
||||
(kiraModel as any).isInteractive = () => false;
|
||||
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.expression('Normal'); } catch {}
|
||||
|
||||
@@ -135,6 +154,42 @@ export default function Live2DStage({ onKiraReady, onReady }: Props) {
|
||||
// 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 (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
|
||||
Reference in New Issue
Block a user