* Project a world-space point to 2D screen (canvas pixel) coordinates * through this camera's view + perspective projection (perspective divide * included). The origin is top-left with **y down**, matching where * geometry at `world` rasterizes and the engine's 2D draw space — so the * resul
( world: Vector3d, out: Vector2d = new Vector2d(), )
| 743 | * @returns the screen-space pixel coordinates, or `null` if behind the camera |
| 744 | */ |
| 745 | worldToScreen( |
| 746 | world: Vector3d, |
| 747 | out: Vector2d = new Vector2d(), |
| 748 | ): Vector2d | null { |
| 749 | // projection × view — built exactly like `_rebuildFrustumPlanes`: |
| 750 | // rotate (pitch then yaw), translate by -pos, then pre-multiply by the |
| 751 | // frustum projection. |
| 752 | _viewMatrix.identity(); |
| 753 | if (this.pitch !== 0) { |
| 754 | _viewMatrix.rotate(-this.pitch, AXIS_X); |
| 755 | } |
| 756 | if (this.yaw !== 0) { |
| 757 | _viewMatrix.rotate(-this.yaw, AXIS_Y); |
| 758 | } |
| 759 | _viewMatrix.translate(-this.pos.x, -this.pos.y, -this.depth); |
| 760 | _viewProjection.copy(this.frustum.projectionMatrix); |
| 761 | _viewProjection.multiply(_viewMatrix); |
| 762 | |
| 763 | // clip-space w (column-major): reject points at/behind the camera before |
| 764 | // the perspective divide would mirror them. |
| 765 | const m = _viewProjection.val; |
| 766 | const w = m[3] * world.x + m[7] * world.y + m[11] * world.z + m[15]; |
| 767 | if (w <= 0) { |
| 768 | return null; |
| 769 | } |
| 770 | |
| 771 | // `Matrix3d.apply` divides by the clip-space w → normalized device |
| 772 | // coordinates in [-1, 1]. |
| 773 | _wsPoint.set(world.x, world.y, world.z); |
| 774 | _viewProjection.apply(_wsPoint); |
| 775 | |
| 776 | // NDC → screen pixels. NDC +y points up, screen +y points down, so the |
| 777 | // y axis is flipped. |
| 778 | out.set( |
| 779 | (_wsPoint.x * 0.5 + 0.5) * this.width, |
| 780 | (1 - (_wsPoint.y * 0.5 + 0.5)) * this.height, |
| 781 | ); |
| 782 | return out; |
| 783 | } |
| 784 | |
| 785 | /** |
| 786 | * Recompute the frustum's six bounding planes from the current |