From fbaf2cc671fed23b555e218499301af0f50327b0 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:44:38 +0900 Subject: public/bluebey-studio: 移動範囲と影・壁の追従、看板の文字 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移動スライダーの範囲を広げ、キーライトと影カメラがぶるべーに追従するようにした(遠くへ動かしても影が切れない)。地面も広げた。 足もとの丸い影は既定で非表示にし、「足もとの丸い影」で切り替えられるようにした。原点に取り残されないよう、ぶるべーに追従させる。 見えない壁は体の向き(master のY回転と見る先の体向き)にも追従するようにした。 「舞台」をやめ、「小物」「擬音」「見えない壁」をそれぞれ独立したメニューにした。看板に文字を書けるようにした(複数行可)。 --- public/bluebey-studio/src/main.js | 56 ++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 9 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 f2434b9..051554d 100644 --- a/public/bluebey-studio/src/main.js +++ b/public/bluebey-studio/src/main.js @@ -15,7 +15,7 @@ import { createClipper } from './clip.js'; import { createBackdrop, backdropStyle } from './background.js'; import { drawCaption, layoutCaption } from './caption.js'; import { loadFont, captionFontStack } from './textOutlines.js'; -import { buildProp, PROP_DEFAULTS, disposeProp } from './props.js'; +import { buildProp, PROP_DEFAULTS, disposeProp, applyPropText } from './props.js'; import { rollAll, decodeSeed } from './gacha.js'; import { encodeState, decodeState } from './urlState.js'; import { createMouthFlap, levelToMouth } from './mouthFlap.js'; @@ -132,7 +132,7 @@ async function init() { // ever hiding the character, so moving ぶるべー below it no longer makes the body // (or its textures) vanish - the shadow still falls on it, but it never occludes. const ground = new THREE.Mesh( - new THREE.PlaneGeometry(60, 60), + new THREE.PlaneGeometry(200, 200), new THREE.ShadowMaterial({ opacity: 0.22, transparent: true, depthWrite: false }), ); ground.rotation.x = -Math.PI / 2; @@ -367,6 +367,15 @@ async function init() { shadowCamera.updateProjectionMatrix(); key.target.position.set(0, halfHeight, 0); + // The light's offset from the character, so the key light (and therefore its + // shadow camera) can follow the character around: moving far from the origin + // used to leave the shadow behind and cut it off at the shadow camera's edge. + const keyOffset = new THREE.Vector3(0, model.size.y * 3, model.size.y * 3); + function updateKeyLight() { + key.target.position.set(container.position.x, halfHeight + container.position.y, container.position.z); + key.position.copy(key.target.position).add(keyOffset); + } + // ------------------------------------------------------------- controllers const styles = new Styles({ meshes: model.parts.body }); const face = new Face({ @@ -399,7 +408,18 @@ async function init() { const root = state.pose?.root; return { x: root?.[0] ?? 0, y: root?.[1] ?? 0, z: root?.[2] ?? 0 }; }; - const applyWalls = () => clip.apply([state.render.wall, state.render.wall2], characterOffset()); + const applyWalls = () => clip.apply( + [state.render.wall, state.render.wall2], + characterOffset(), + wallYaw(), + ); + // The character's own turn about Y: the `master` bone's Y (Shift+drag) plus any + // body yaw the 見る先 mode added. The walls take this too, so turning the + // character with Shift+drag no longer slides the hidden region around. + const wallYaw = () => { + const masterY = state.pose?.bones?.master?.[1] ?? 0; + return (masterY + (container.rotation.y * 180) / Math.PI) * DEG; + }; // --------------------------------------------------------------- backdrop const backdrop = createBackdrop({ @@ -744,6 +764,9 @@ async function init() { group.position.set(item.x ?? def.x ?? 0, item.y ?? def.y ?? 0, item.z ?? def.z ?? 0); group.rotation.y = ((item.rotY ?? def.rotY ?? 0) * DEG); propRoot.add(group); + // 看板 only: paint the saved text onto the freshly built face (no-op for + // every other prop, which has no writing surface). + applyPropText(group, item.text); } app.needsRender = true; } @@ -854,14 +877,19 @@ async function init() { propPlane.setFromNormalAndCoplanarPoint(propNormal.negate(), onWall.point); const wall = state.render[onWall.key] ?? (state.render[onWall.key] = defaultState().render[onWall.key]); const offset = characterOffset(); + const yaw = wallYaw(); + const cos = Math.cos(yaw); + const sin = Math.sin(yaw); + const px = onWall.point.x - offset.x; + const pz = onWall.point.z - offset.z; wallDrag.pointerId = event.pointerId; wallDrag.key = onWall.key; // Keep the grab point's offset *in the wall's own (character-relative) frame*, // so grabbing a corner does not make the wall jump. wallDrag.offset.set( - (wall.x ?? 0) - (onWall.point.x - offset.x), + (wall.x ?? 0) - (cos * px - sin * pz), (wall.y ?? 0) - (onWall.point.y - offset.y), - (wall.z ?? 0) - (onWall.point.z - offset.z), + (wall.z ?? 0) - (sin * px + cos * pz), ); stage.style.cursor = 'grabbing'; app.needsRender = true; @@ -877,13 +905,18 @@ async function init() { propRaycaster.setFromCamera(pointerNdc(event), view.camera); if (!propRaycaster.ray.intersectPlane(propPlane, propPoint)) return; const offset = characterOffset(); + const yaw = wallYaw(); + const cos = Math.cos(yaw); + const sin = Math.sin(yaw); + const px = propPoint.x - offset.x; + const pz = propPoint.z - offset.z; const wall = state.render[wallDrag.key] ?? (state.render[wallDrag.key] = defaultState().render[wallDrag.key]); // Back into the wall's character-relative frame, then the same ranges the // panel's sliders offer, so a drag and a number always describe the same // place. - wall.x = round2(clampNumber(propPoint.x - offset.x + wallDrag.offset.x, -4, 4)); + wall.x = round2(clampNumber(cos * px - sin * pz + wallDrag.offset.x, -4, 4)); wall.y = round2(clampNumber(propPoint.y - offset.y + wallDrag.offset.y, -2, 4)); - wall.z = round2(clampNumber(propPoint.z - offset.z + wallDrag.offset.z, -4, 4)); + wall.z = round2(clampNumber(sin * px + cos * pz + wallDrag.offset.z, -4, 4)); applyWalls(); app.needsRender = true; return; @@ -1281,6 +1314,10 @@ async function init() { const posed = animator.pose(state.pose); if (!rig.controls.dragging) rig.applyPose(posed); container.position.set(posed.root[0] ?? 0, posed.root[1] ?? 0, posed.root[2] ?? 0); + updateKeyLight(); + // The contact blob rides under the character, so it never lingers at the origin. + look.contact.position.x = container.position.x; + look.contact.position.z = container.position.z; if (snotBubble) { const snot = state.face.eyes.snot; @@ -1493,11 +1530,12 @@ async function init() { const azimuth = state.render.lightAzimuth * DEG; const elevation = state.render.lightElevation * DEG; const distance = model.size.y * 3; - key.position.set( + keyOffset.set( Math.cos(elevation) * Math.sin(azimuth) * distance, - Math.sin(elevation) * distance + halfHeight, + Math.sin(elevation) * distance, Math.cos(elevation) * Math.cos(azimuth) * distance, ); + updateKeyLight(); key.intensity = state.render.lightIntensity; ambient.intensity = state.render.ambient; renderer.toneMappingExposure = state.render.exposure; -- cgit v1.3.1