From 629dfd09777d1abb3cdecd8fc58d18234d1063cf Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:08:30 +0900 Subject: bluebey-studio: 帽子の高さ・傾き、髪の下地書き出し、目の形メニューの再構成 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/bluebey-studio/src/main.js | 67 ++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 19 deletions(-) (limited to 'public/bluebey-studio/src/main.js') diff --git a/public/bluebey-studio/src/main.js b/public/bluebey-studio/src/main.js index 01ee85a..d4866c7 100644 --- a/public/bluebey-studio/src/main.js +++ b/public/bluebey-studio/src/main.js @@ -381,30 +381,50 @@ async function init() { let hatKindCurrent = null; let hatObject = null; const hatMeshes = []; + /** Last transform applied, so the render loop only wakes when it changes. */ + const hatXform = { height: null, tiltX: null, tiltZ: null }; /** Set once the outline pass exists, so a hat change can refresh the exclude set. */ let refreshOutlineExclusion = null; function applyHat(kind) { - if (kind === hatKindCurrent) return; - hatKindCurrent = kind; if (!hatMount) return; + if (kind !== hatKindCurrent) { + hatKindCurrent = kind; + if (hatObject) { + hatMount.remove(hatObject); + hatObject.traverse((o) => { + o.geometry?.dispose(); + if (Array.isArray(o.material)) o.material.forEach((m) => m.dispose()); + else o.material?.dispose(); + }); + hatObject = null; + } + hatMeshes.length = 0; + const built = buildHat(kind); + if (built) { + hatMount.add(built); + hatObject = built; + built.traverse((o) => { if (o.isMesh) hatMeshes.push(o); }); + } + refreshOutlineExclusion?.(); + } + + // 高さ and 傾き. The mount is world-aligned (see above), so +y is up, +z is the + // character's front and +x its right: a plain translation and rotation read + // the way the sliders say. + const hat = state.face.hat ?? {}; + const height = hat.height ?? 0; + const tiltX = hat.tiltX ?? 0; + const tiltZ = hat.tiltZ ?? 0; if (hatObject) { - hatMount.remove(hatObject); - hatObject.traverse((o) => { - o.geometry?.dispose(); - if (Array.isArray(o.material)) o.material.forEach((m) => m.dispose()); - else o.material?.dispose(); - }); - hatObject = null; + hatObject.position.y = height; + hatObject.rotation.set((tiltX * Math.PI) / 180, 0, (tiltZ * Math.PI) / 180); } - hatMeshes.length = 0; - const built = buildHat(kind); - if (built) { - hatMount.add(built); - hatObject = built; - built.traverse((o) => { if (o.isMesh) hatMeshes.push(o); }); + if (height !== hatXform.height || tiltX !== hatXform.tiltX || tiltZ !== hatXform.tiltZ) { + hatXform.height = height; + hatXform.tiltX = tiltX; + hatXform.tiltZ = tiltZ; + app.needsRender = true; } - refreshOutlineExclusion?.(); - app.needsRender = true; } applyHat(state.face.hat?.kind ?? 'none'); @@ -1932,14 +1952,23 @@ async function init() { // `face.flush(now, 26)`), so force any pending one through first or the map // would show the expression before last. face.flush(performance.now(), 0); - return exporter.buildFaceMap(face, kind); + return kind === 'hair' ? exporter.buildHairMap(face) : exporter.buildFaceMap(face, kind); } async function saveFaceMap(kind) { const canvas = faceMapCanvas(kind); + if (!canvas) { + toast('このモデルには髪のプレートがありません'); + return; + } const blob = await exporter.canvasToBlob(canvas); - const label = kind === 'eyes' ? '目' : '口'; + const label = kind === 'eyes' ? '目' : kind === 'mouth' ? '口' : '髪'; exporter.downloadBlob(blob, `bluebey-face-${kind}-${exporter.timestamp()}.png`); + if (kind === 'hair') { + toast(`${label}の下地を書き出しました(${canvas.width}×${canvas.height})。` + + 'この画像に前髪を描いて渡してください'); + return; + } toast(`${label}の下地を書き出しました(${canvas.width}×${canvas.height})。` + `この画像に描いて「${label}の画像を読み込む(PNG)」で読み込めます`); } -- cgit v1.3.1