aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/bluebey-studio/src/main.js')
-rw-r--r--public/bluebey-studio/src/main.js52
1 files changed, 52 insertions, 0 deletions
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}`;