diff options
| author | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-03 15:16:11 +0900 |
|---|---|---|
| committer | Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> | 2026-10-03 15:16:11 +0900 |
| commit | 20e722e5ab241d0a2153a3fdcff8fc8fddf1361e (patch) | |
| tree | ecc8ddeb0b7ce4e47f30d43c5b77d30c7540eb02 /public/bluebey-studio/src/main.js | |
| parent | c6d1df09c66bcfe8388651866d26ed5d05b4d2d2 (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.js | 73 |
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; } } |
