aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/face.js
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-04 00:06:06 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-04 00:06:06 +0900
commitc81104129a9ba6e26aa33a00770f76a6aa97b2a9 (patch)
tree6329d9b3cd779c7373fd20e63f7db0ff6d4f45ac /public/bluebey-studio/src/face.js
parentcb0cce903f8ea3ad4943cabab83b74a40d87fc00 (diff)
bluebey-studio: 髪の板(hair-plate)で頭の模様を後ろまで回す
モデルに追加された hair-plate(頭をぐるりと覆う円筒UVの板)を読み込み、頭の模様をその板にも描くようにした。これで横や後ろから見ても髪が見える。髪の板が無いモデルは従来どおり前面の板に描く。
Diffstat (limited to 'public/bluebey-studio/src/face.js')
-rw-r--r--public/bluebey-studio/src/face.js35
1 files changed, 31 insertions, 4 deletions
diff --git a/public/bluebey-studio/src/face.js b/public/bluebey-studio/src/face.js
index 0813c54..de8a31c 100644
--- a/public/bluebey-studio/src/face.js
+++ b/public/bluebey-studio/src/face.js
@@ -1,5 +1,5 @@
import * as THREE from 'three';
-import { drawEyes, drawMouth, canvasLayout } from './faceArt.js';
+import { drawEyes, drawMouth, drawHeadCoverWrap, canvasLayout } from './faceArt.js';
/**
* Owns the two procedural textures (eyes and mouth) and keeps them in sync with
@@ -16,14 +16,19 @@ import { drawEyes, drawMouth, canvasLayout } from './faceArt.js';
* that ships inside the GLB.
*/
export class Face {
- constructor({ eyeMesh, mouthMesh, originals }) {
+ constructor({ eyeMesh, mouthMesh, hairMesh = null, originals }) {
this.eyeMesh = eyeMesh;
this.mouthMesh = mouthMesh;
+ // 頭の模様 can be carried by a plate that wraps right round the head (the
+ // `hair-plate`), so hair is visible from behind as well. Older models have no
+ // such plate and draw the covering on the front one instead.
+ this.hairMesh = hairMesh;
// Rewrite each plate's UVs so its own range fills 0..1, and work out the
// canvas that keeps the artwork exactly where the 2022 textures put it.
this.eyeLayout = remapPlate(eyeMesh);
this.mouthLayout = remapPlate(mouthMesh);
+ this.hairLayout = hairMesh ? remapPlate(hairMesh) : null;
// The hand-drawn eye textures bake the face colour into their background,
// which is invisible in the lit "real" style but shows up as a flat purple
@@ -43,11 +48,14 @@ export class Face {
this.beardImages = {};
this.eyeCanvas = makeCanvas(this.eyeLayout);
this.mouthCanvas = makeCanvas(this.mouthLayout);
+ this.hairCanvas = this.hairLayout ? makeCanvas(this.hairLayout) : null;
this.eyeCtx = this.eyeCanvas.getContext('2d');
this.mouthCtx = this.mouthCanvas.getContext('2d');
+ this.hairCtx = this.hairCanvas ? this.hairCanvas.getContext('2d') : null;
this.eyeTexture = makeTexture(this.eyeCanvas);
this.mouthTexture = makeTexture(this.mouthCanvas);
+ this.hairTexture = this.hairCanvas ? makeTexture(this.hairCanvas) : null;
this.params = null;
this.styleMode = 'paint';
@@ -64,7 +72,8 @@ export class Face {
// artwork the way they should - it only ever hid the old planes because
// those did not reach far enough down the head.
const orders = new Map([[eyeMesh, 2], [mouthMesh, 1]]);
- for (const mesh of [eyeMesh, mouthMesh]) {
+ if (hairMesh) orders.set(hairMesh, 3);
+ for (const mesh of [eyeMesh, mouthMesh, ...(hairMesh ? [hairMesh] : [])]) {
const material = mesh.material;
material.polygonOffset = true;
material.polygonOffsetFactor = -1;
@@ -106,6 +115,10 @@ export class Face {
this.mouthMesh.material.map = mouthMap;
this.mouthMesh.material.needsUpdate = true;
}
+ if (this.hairMesh && this.hairMesh.material.map !== this.hairTexture) {
+ this.hairMesh.material.map = this.hairTexture;
+ this.hairMesh.material.needsUpdate = true;
+ }
}
/**
@@ -210,7 +223,10 @@ export class Face {
// ほっぺ and 頭の模様 are shared by the whole face, so they travel with
// the other shared eye fields (see `drawEyes` in src/faceArt.js).
cheeks: p.eyes.cheeks,
- headMark: p.eyes.headMark,
+ // When the model has a wrapping hair plate the covering is drawn there
+ // instead (below), so it is switched off on the front plate to keep it
+ // from being drawn twice.
+ headMark: this.hairMesh ? { shape: 'off' } : p.eyes.headMark,
// ひげ is shared by the whole face as well. (鼻ちょうちん is a real 3D
// object hung off the nose - see src/main.js - so it is not drawn here.)
beard: p.eyes.beard,
@@ -242,6 +258,17 @@ export class Face {
this.mouthTexture.needsUpdate = true;
}
+ if (this.hairMesh) {
+ const layout = this.hairLayout;
+ const ctx = this.hairCtx;
+ ctx.clearRect(0, 0, layout.width, layout.height);
+ ctx.save();
+ ctx.translate(layout.offsetX, layout.offsetY);
+ drawHeadCoverWrap(ctx, p.eyes.headMark ?? {}, layout);
+ ctx.restore();
+ this.hairTexture.needsUpdate = true;
+ }
+
this.dirty = false;
}
}