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.js56
1 files changed, 47 insertions, 9 deletions
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;