aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/main.js
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-03 15:16:11 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-03 15:16:11 +0900
commit20e722e5ab241d0a2153a3fdcff8fc8fddf1361e (patch)
treeecc8ddeb0b7ce4e47f30d43c5b77d30c7540eb02 /public/bluebey-studio/src/main.js
parentc6d1df09c66bcfe8388651866d26ed5d05b4d2d2 (diff)
public/bluebey-studio: 見えない壁を2枚にし、ぶるべーに追従させる
見えない壁は2枚まで置けるようにした(壁1・壁2)。板の位置はぶるべーからの相対で持ち、ぶるべーを動かすと壁も一緒に動くようにした。 地面(影を受ける板)が深度を書かないようにし、ぶるべーが地面より下に来ても体やテクスチャが隠れないようにした。
Diffstat (limited to 'public/bluebey-studio/src/main.js')
-rw-r--r--public/bluebey-studio/src/main.js73
1 files changed, 51 insertions, 22 deletions
diff --git a/public/bluebey-studio/src/main.js b/public/bluebey-studio/src/main.js
index c7e6501..f2434b9 100644
--- a/public/bluebey-studio/src/main.js
+++ b/public/bluebey-studio/src/main.js
@@ -128,9 +128,12 @@ async function init() {
const ambient = new THREE.HemisphereLight(0xffffff, 0xd9d0f2, 0.9);
scene.add(key, key.target, fill, ambient);
+ // A shadow catcher, not a solid floor: `depthWrite: false` keeps the plane from
+ // 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.ShadowMaterial({ opacity: 0.22, transparent: true }),
+ new THREE.ShadowMaterial({ opacity: 0.22, transparent: true, depthWrite: false }),
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
@@ -389,6 +392,14 @@ async function init() {
// A clip plane that hides part of the character, so it can look half buried
// in a wall (see src/clip.js). Applied in `refresh`, with the render settings.
const clip = createClipper({ scene, renderer, model });
+ // A wall's position is stored relative to the character, so every time the
+ // walls are applied they are shifted by the body's own offset - that is what
+ // makes them travel with ぶるべー when it is moved.
+ const characterOffset = () => {
+ 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());
// --------------------------------------------------------------- backdrop
const backdrop = createBackdrop({
@@ -755,7 +766,7 @@ async function init() {
let propDrag = null;
// The 見えない壁 uses the same machinery: `wallDrag` keeps the grabbed point's
// offset from the wall's anchor so grabbing a corner does not make it jump.
- const wallDrag = { pointerId: null, offset: new THREE.Vector3() };
+ const wallDrag = { pointerId: null, key: 'wall', offset: new THREE.Vector3() };
/** The pointer in normalised device coordinates, i.e. what a raycaster wants. */
function pointerNdc(event) {
@@ -789,10 +800,19 @@ async function init() {
* so with it off a huge invisible plane cannot swallow every orbit drag.
*/
function wallAt(event) {
- if (state.render.wall?.on !== true || state.render.wall.guide !== true) return null;
propRaycaster.setFromCamera(pointerNdc(event), view.camera);
- const hits = propRaycaster.intersectObject(clip.guide, false);
- return hits.length ? hits[0].point.clone() : null;
+ let best = null;
+ for (let index = 0; index < clip.guides.length; index += 1) {
+ const key = index === 0 ? 'wall' : 'wall2';
+ const wall = state.render[key];
+ if (wall?.on !== true || wall?.guide !== true) continue;
+ const hits = propRaycaster.intersectObject(clip.guides[index], false);
+ if (!hits.length) continue;
+ if (!best || hits[0].distance < best.distance) {
+ best = { index, key, point: hits[0].point.clone() };
+ }
+ }
+ return best;
}
function onPropPointerDown(event) {
@@ -831,10 +851,18 @@ async function init() {
event.stopPropagation();
event.preventDefault();
view.camera.getWorldDirection(propNormal);
- propPlane.setFromNormalAndCoplanarPoint(propNormal.negate(), onWall);
- const wall = state.render.wall;
+ propPlane.setFromNormalAndCoplanarPoint(propNormal.negate(), onWall.point);
+ const wall = state.render[onWall.key] ?? (state.render[onWall.key] = defaultState().render[onWall.key]);
+ const offset = characterOffset();
wallDrag.pointerId = event.pointerId;
- wallDrag.offset.set(wall.x ?? 0, wall.y ?? 0, wall.z ?? 0).sub(onWall);
+ 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.y ?? 0) - (onWall.point.y - offset.y),
+ (wall.z ?? 0) - (onWall.point.z - offset.z),
+ );
stage.style.cursor = 'grabbing';
app.needsRender = true;
}
@@ -848,14 +876,15 @@ async function init() {
event.stopPropagation();
propRaycaster.setFromCamera(pointerNdc(event), view.camera);
if (!propRaycaster.ray.intersectPlane(propPlane, propPoint)) return;
- const next = propPoint.add(wallDrag.offset);
- const wall = state.render.wall;
- // The same ranges the panel's sliders offer, so a drag and a number always
- // describe the same place.
- wall.x = round2(clampNumber(next.x, -4, 4));
- wall.y = round2(clampNumber(next.y, -2, 4));
- wall.z = round2(clampNumber(next.z, -4, 4));
- clip.apply(wall);
+ const offset = characterOffset();
+ 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.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));
+ applyWalls();
app.needsRender = true;
return;
}
@@ -1055,7 +1084,7 @@ async function init() {
return [
ground,
look.contact,
- clip.guide,
+ ...clip.guides,
rig.helper,
model.parts.eyeMesh,
model.parts.mouthMesh,
@@ -1068,7 +1097,7 @@ async function init() {
// 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
// its depth is applied.
- outline.occlude([clip.occluder]);
+ outline.occlude(clip.occluders);
/**
* The parts the screen-space pass draws, with the label each one writes.
@@ -1459,7 +1488,7 @@ async function init() {
look.apply(state.render, { line });
applyProps();
// 見えない壁: the one setting that changes what is drawn rather than how.
- clip.apply(state.render.wall);
+ applyWalls();
const azimuth = state.render.lightAzimuth * DEG;
const elevation = state.render.lightElevation * DEG;
@@ -1762,14 +1791,14 @@ async function init() {
/** Hide the gizmo and helpers so they never end up in an export. */
async function withHiddenHelpers(fn) {
const wasVisible = rig.helper.visible;
- const guideWasVisible = clip.guide.visible;
+ const guidesWereVisible = clip.guides.map((guide) => guide.visible);
rig.helper.visible = false;
- clip.guide.visible = false;
+ clip.guides.forEach((guide) => { guide.visible = false; });
try {
return await fn();
} finally {
rig.setGizmoVisible(wasVisible);
- clip.guide.visible = guideWasVisible;
+ clip.guides.forEach((guide, index) => { guide.visible = guidesWereVisible[index]; });
app.needsRender = true;
}
}