From fa3411f87c72f249168b00e6338027512ce7e894 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:39:45 +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 | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) (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 60507eb..01ee85a 100644 --- a/public/bluebey-studio/src/main.js +++ b/public/bluebey-studio/src/main.js @@ -12,6 +12,7 @@ import { toast } from './ui.js'; import { History } from './history.js'; import { createLook } from './look.js'; import { createClipper } from './clip.js'; +import { buildHat } from './hats.js'; import { createBackdrop, backdropStyle } from './background.js'; import { drawCaption, layoutCaption } from './caption.js'; import { loadFont, captionFontStack } from './textOutlines.js'; @@ -360,6 +361,53 @@ async function init() { const snotMeshes = snotBubble ? [...snotBubble.children] : []; applySnotBubble(snotBubble, state.face.eyes.snot); + // The hat rides the `master` bone like the snot bubble does: it is parented + // there, placed at the top of the head in that bone's local frame, and turned + // so it is world-aligned at rest (the bone's own rest rotation cancelled once). + // The character's front is +z, so the hats are modelled facing +z. + const hatMount = (() => { + const master = model.bones.find((entry) => entry.name.replace(/[.\s]/g, '').toLowerCase() === 'master'); + if (!master) return null; + model.root.updateMatrixWorld(true); + const bone = master.bone; + const group = new THREE.Group(); + group.name = 'hat'; + group.position.copy(bone.worldToLocal(new THREE.Vector3(0, model.bounds.max.y - 0.06, 0))); + group.quaternion.copy(bone.getWorldQuaternion(new THREE.Quaternion()).invert()); + bone.add(group); + return group; + })(); + + let hatKindCurrent = null; + let hatObject = null; + const hatMeshes = []; + /** 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 (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?.(); + app.needsRender = true; + } + applyHat(state.face.hat?.kind ?? 'none'); + const halfHeight = model.size.y * 0.5; view.frame(model.size, halfHeight); @@ -1186,10 +1234,12 @@ async function init() { model.parts.mouthMesh, ...(model.parts.hairMesh ? [model.parts.hairMesh] : []), ...snotMeshes, + ...hatMeshes, ...styles.outlineMeshes, ]; } outline.exclude(outlineExclusion()); + refreshOutlineExclusion = () => outline.exclude(outlineExclusion()); // The wall is *not* excluded: it is handed to the pass as an occluder, so the // labels behind it are culled as well. Excluded, the leaves and the nose // buried in the wall kept their outlines, because the labels are read before @@ -1383,6 +1433,8 @@ async function init() { look.contact.position.x = container.position.x; look.contact.position.z = container.position.z; + applyHat(state.face.hat?.kind ?? 'none'); + if (snotBubble) { const snot = state.face.eyes.snot; const sig = `${snot.enabled}|${snot.size}|${snot.offsetX}|${snot.offsetY}|${snot.offsetZ}|${snot.color}`; -- cgit v1.3.1