* Orient the quad toward the camera when billboarding; otherwise defer to the * standard Mesh world projection (a fixed-orientation quad). * @ignore
(offsetX, offsetY, offsetZ)
| 676 | * @ignore |
| 677 | */ |
| 678 | _projectVerticesWorld(offsetX, offsetY, offsetZ) { |
| 679 | const mode = this.billboard; |
| 680 | const cam = this._billboardCam; |
| 681 | if (mode === false || mode === "none" || cam === null) { |
| 682 | super._projectVerticesWorld(offsetX, offsetY, offsetZ); |
| 683 | return; |
| 684 | } |
| 685 | |
| 686 | // camera basis (world space): right / up / forward. Note `getBasis` |
| 687 | // returns a math basis (+Y up) while render space is Y-down — each mode |
| 688 | // below resolves that sign so the card lands right-way-up. |
| 689 | cam.getBasis(_right, _up, _fwd); |
| 690 | |
| 691 | if (mode === "cylindrical" || mode === true) { |
| 692 | // stay upright: up = world up (-Y in render space), right = horizontal |
| 693 | // axis = forward × up (this order makes the front face the camera). |
| 694 | // Falls back to the camera right when looking straight up/down |
| 695 | // (forward ∥ world up → degenerate cross product). |
| 696 | _up.copy(WORLD_UP); |
| 697 | _right.copy(_fwd).cross(WORLD_UP); |
| 698 | if (_right.length() < 1e-4) { |
| 699 | cam.getRight(_right); |
| 700 | } else { |
| 701 | _right.normalize(); |
| 702 | } |
| 703 | } else { |
| 704 | // spherical: face the camera on all axes. `getBasis` returns a math |
| 705 | // (+Y up) basis, but render space is Y-down — so the camera's "up" |
| 706 | // actually points screen-DOWN. Negate it to get the screen-up axis |
| 707 | // (matching WORLD_UP at head-on), then right = forward × up to match |
| 708 | // the cylindrical handedness so the front face — not the mirrored |
| 709 | // back — points at the camera. |
| 710 | _up.negateSelf(); |
| 711 | _right.copy(_fwd).cross(_up); |
| 712 | if (_right.length() < 1e-4) { |
| 713 | cam.getRight(_right); |
| 714 | } else { |
| 715 | _right.normalize(); |
| 716 | } |
| 717 | } |
| 718 | |
| 719 | // emit the 4 corners as center ± right·localX ± up·localY (the local |
| 720 | // offsets are the baked ±hw / ±hh quad coordinates) |
| 721 | const out = this.vertices; |
| 722 | const src = this.originalVertices; |
| 723 | const rx = _right.x; |
| 724 | const ry = _right.y; |
| 725 | const rz = _right.z; |
| 726 | const ux = _up.x; |
| 727 | const uy = _up.y; |
| 728 | const uz = _up.z; |
| 729 | for (let i = 0; i < 4; i++) { |
| 730 | const i3 = i * 3; |
| 731 | const lx = src[i3]; |
| 732 | const ly = src[i3 + 1]; |
| 733 | out[i3] = offsetX + rx * lx + ux * ly; |
| 734 | out[i3 + 1] = offsetY + ry * lx + uy * ly; |
| 735 | out[i3 + 2] = offsetZ + rz * lx + uz * ly; |