aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/clip.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/bluebey-studio/src/clip.js')
-rw-r--r--public/bluebey-studio/src/clip.js202
1 files changed, 0 insertions, 202 deletions
diff --git a/public/bluebey-studio/src/clip.js b/public/bluebey-studio/src/clip.js
deleted file mode 100644
index 4fdcfd9..0000000
--- a/public/bluebey-studio/src/clip.js
+++ /dev/null
@@ -1,202 +0,0 @@
-import * as THREE from 'three';
-
-/**
- * 見えない壁 (the invisible wall): a clipping plane that hides whatever falls
- * behind it, so the character can be buried in the wall and only the rest of the
- * body shows.
- *
- * The wall is a *finite* rectangle: an invisible quad that writes depth and no
- * colour, drawn before everything else in the opaque pass, so the character
- * behind it is culled by the depth test. That is what a real wall does, and it
- * is the only way to bound the effect - a `renderer.clippingPlanes` entry is an
- * infinite half-space, so it can never be limited to a rectangle.
- *
- * Nothing needs to agree per-material: the depth buffer does the work, so the
- * body, the outline hulls, the face plates, the ink pass and the offscreen
- * renders an export uses are all hidden by the same wall.
- *
- * `wallPlane` is pure (no three.js maths), so the geometry can be unit-tested
- * without a renderer.
- */
-
-const DEG = Math.PI / 180;
-
-/**
- * A hair toward the kept side. The guide sits exactly on the cut, and a plane
- * on its own boundary is half inside the discarded half - nudging it along the
- * normal keeps all of it on the visible side.
- */
-const GUIDE_EPSILON = 0.002;
-
-/**
- * The plane a wall setting lies in.
- *
- * The wall is a *finite* quad, so this no longer decides which half of space is
- * hidden - the quad's own depth does that. What `apply` needs from here is the
- * normal, which is the direction the quad faces (and so the direction the wall
- * lies along), plus the constant of the plane through the point it sits on.
- *
- * By default the wall lies in the XY plane through `(x, y, z)`; `yaw` turns it
- * about Y and `tilt` leans it about X afterwards.
- *
- * @param {{x?:number,y?:number,z?:number,yaw?:number,tilt?:number}} [wall]
- * @returns {{normal:[number,number,number], constant:number}}
- */
-export function wallPlane(wall = {}) {
- const yaw = (wall.yaw ?? 0) * DEG;
- const tilt = (wall.tilt ?? 0) * DEG;
- const cosYaw = Math.cos(yaw);
- const sinYaw = Math.sin(yaw);
- const cosTilt = Math.cos(tilt);
- const sinTilt = Math.sin(tilt);
-
- // Start from +Z, turn about Y, then lean about the (already turned) X axis.
- let nx = sinYaw;
- let ny = -cosYaw * sinTilt;
- let nz = cosYaw * cosTilt;
-
- const length = Math.hypot(nx, ny, nz) || 1;
- nx /= length;
- ny /= length;
- nz /= length;
-
- // Constant so the plane passes through (x, y, z): dot(n, p) + c = 0.
- const constant = -(nx * (wall.x ?? 0) + ny * (wall.y ?? 0) + nz * (wall.z ?? 0));
- return { normal: [nx, ny, nz], constant };
-}
-
-/** The faint plane shown while placing the wall. Hidden from every export. */
-/**
- * The wall itself: an invisible, *finite* quad that writes depth but no colour.
- *
- * It is drawn before everything else in the opaque pass (`renderOrder`), so the
- * character behind it is culled by the depth test. That is what a real wall does,
- * and - unlike a clip plane, which is an infinite half-space - it only hides what
- * the rectangle actually covers. So the size sliders are the wall's real size.
- */
-function makeWall() {
- const material = new THREE.MeshBasicMaterial({
- colorWrite: false,
- side: THREE.DoubleSide,
- toneMapped: false,
- });
- const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);
- mesh.name = 'wall';
- // Before everything, including the silhouette-only hulls (renderOrder -1), so
- // its depth hides a nose buried in the wall (see styles.js hullMaterialFor).
- mesh.renderOrder = -2;
- mesh.visible = false;
- mesh.castShadow = false;
- mesh.receiveShadow = false;
- return mesh;
-}
-
-function makeGuide() {
- const geometry = new THREE.PlaneGeometry(1, 1);
- const material = new THREE.MeshBasicMaterial({
- color: 0x8a4fe0,
- transparent: true,
- opacity: 0.16,
- side: THREE.DoubleSide,
- depthWrite: false,
- toneMapped: false,
- });
- const mesh = new THREE.Mesh(geometry, material);
- mesh.name = 'wall-guide';
- mesh.userData.isHelper = true;
- mesh.visible = false;
-
- // An edge, so the plane's extent is legible even where the fill is faint.
- const edge = new THREE.LineSegments(
- new THREE.EdgesGeometry(geometry),
- new THREE.LineBasicMaterial({ color: 0x8a4fe0, transparent: true, opacity: 0.55, toneMapped: false }),
- );
- edge.userData.isHelper = true;
- mesh.add(edge);
- return mesh;
-}
-
-export function createClipper({ scene, renderer, model }) {
- // A little larger than the character: big enough to read as a wall, small
- // enough not to cover the whole viewport.
- const guideSpan = Math.max(model.size.x, model.size.y, model.size.z) * 1.5;
- const FROM = new THREE.Vector3(0, 0, 1); // the plane the quad starts in
-
- function makeSlot() {
- const occluder = makeWall();
- const guide = makeGuide();
- scene.add(guide);
- scene.add(occluder);
- return { occluder, guide, normal: new THREE.Vector3(0, 0, 1) };
- }
- // Two walls. Each slot keeps its own rectangle, so the two are independent.
- const slots = [makeSlot(), makeSlot()];
-
- /**
- * @param {Array<object>} walls up to two `state.render.wall` settings, in slot
- * order.
- * @param {{x?:number,y?:number,z?:number}} [offset] the character's own
- * translation. A wall's position is stored *relative to the character*, so
- * adding this makes the walls travel with the body when it is moved.
- * @param {number} [yaw] the character's own turn about Y, in radians. The stored
- * position is rotated by it (and added to the wall's own `yaw`), so a wall
- * stays glued to the body when the character is turned.
- *
- * The guide is drawn only when `wall.guide` is on, which is also when the wall
- * can be grabbed in the viewport. Off, the wall is invisible: the cut still
- * applies, so you can see the character half-hidden with nothing in the way.
- */
- function apply(walls, offset, yaw = 0) {
- const list = Array.isArray(walls) ? walls : [walls];
- const ox = offset?.x ?? 0;
- const oy = offset?.y ?? 0;
- const oz = offset?.z ?? 0;
- const cos = Math.cos(yaw);
- const sin = Math.sin(yaw);
- const yawDeg = (yaw * 180) / Math.PI;
- slots.forEach((slot, index) => {
- const wall = list[index];
- if (!wall || wall.on !== true) {
- slot.occluder.visible = false;
- slot.guide.visible = false;
- return;
- }
- // The stored position is relative to the character, so it turns with the
- // body: rotate (x, z) about Y by the character's yaw, then add the offset.
- const lx = wall.x ?? 0;
- const lz = wall.z ?? 0;
- const x = cos * lx + sin * lz + ox;
- const y = (wall.y ?? 0) + oy;
- const z = -sin * lx + cos * lz + oz;
- const { normal } = wallPlane({ yaw: (wall.yaw ?? 0) + yawDeg, tilt: wall.tilt, x, y, z });
- slot.normal.set(normal[0], normal[1], normal[2]);
- const span = guideSpan * Math.max(0.05, wall.size ?? 1);
- // The wall and its guide are the same rectangle, turned to face along the
- // plane's normal: the wall is the occluder, the guide is the tinted copy
- // that is shown only while placing.
- for (const mesh of [slot.occluder, slot.guide]) {
- mesh.quaternion.setFromUnitVectors(FROM, slot.normal);
- mesh.scale.setScalar(span);
- }
- slot.occluder.position.set(x, y, z);
- slot.occluder.visible = true;
- slot.guide.position.set(x, y, z).addScaledVector(slot.normal, GUIDE_EPSILON);
- slot.guide.visible = wall.guide === true;
- });
- }
-
- return {
- guides: slots.map((slot) => slot.guide),
- occluders: slots.map((slot) => slot.occluder),
- apply,
- dispose() {
- for (const slot of slots) {
- for (const mesh of [slot.guide, slot.occluder]) {
- mesh.geometry.dispose();
- mesh.material.dispose();
- mesh.removeFromParent();
- }
- }
- },
- };
-}