diff --git a/src/AcDream.Core/Selection/WorldPicker.cs b/src/AcDream.Core/Selection/WorldPicker.cs
new file mode 100644
index 0000000..ae3f0cb
--- /dev/null
+++ b/src/AcDream.Core/Selection/WorldPicker.cs
@@ -0,0 +1,55 @@
+using System.Numerics;
+using AcDream.Core.World;
+
+namespace AcDream.Core.Selection;
+
+///
+/// Mouse-to-entity picker. Pure static functions; no state, no DI.
+///
+/// - turns a pixel + view/projection into a world-space ray.
+/// - ray-sphere intersects against entity candidates and returns the nearest hit's ServerGuid.
+///
+/// Used by GameWindow.OnInputAction to wire SelectLeft / SelectDblLeft / UseSelected to InteractRequests.BuildUse.
+///
+public static class WorldPicker
+{
+ ///
+ /// Unprojects a pixel coordinate to a world-space ray using the supplied
+ /// view + projection matrices (System.Numerics row-vector convention,
+ /// composed as view * projection — same as the rest of acdream's camera
+ /// pipeline; see GameWindow.cs:6445 FrustumPlanes.FromViewProjection).
+ ///
+ ///
+ /// (origin = world point on the near plane, direction = normalized
+ /// world-space ray direction). Returns (Vector3.Zero, Vector3.Zero)
+ /// if the view-projection composition is singular.
+ ///
+ public static (Vector3 Origin, Vector3 Direction) BuildRay(
+ float mouseX, float mouseY,
+ float viewportW, float viewportH,
+ Matrix4x4 view, Matrix4x4 projection)
+ {
+ // Pixel -> NDC. y flipped: top-left pixel maps to ndc.y = +1.
+ float ndcX = (2f * mouseX) / viewportW - 1f;
+ float ndcY = 1f - (2f * mouseY) / viewportH;
+
+ var vp = view * projection;
+ if (!Matrix4x4.Invert(vp, out var invVp))
+ return (Vector3.Zero, Vector3.Zero);
+
+ // Unproject near (ndc.z = -1) and far (ndc.z = +1) clip points.
+ var nearClip = new Vector4(ndcX, ndcY, -1f, 1f);
+ var farClip = new Vector4(ndcX, ndcY, +1f, 1f);
+ var n4 = Vector4.Transform(nearClip, invVp);
+ var f4 = Vector4.Transform(farClip, invVp);
+ if (n4.W == 0f || f4.W == 0f)
+ return (Vector3.Zero, Vector3.Zero);
+
+ var nearWorld = new Vector3(n4.X, n4.Y, n4.Z) / n4.W;
+ var farWorld = new Vector3(f4.X, f4.Y, f4.Z) / f4.W;
+ var dir = farWorld - nearWorld;
+ if (dir.LengthSquared() < 1e-10f)
+ return (Vector3.Zero, Vector3.Zero);
+ return (nearWorld, Vector3.Normalize(dir));
+ }
+}
diff --git a/tests/AcDream.Core.Tests/Selection/WorldPickerTests.cs b/tests/AcDream.Core.Tests/Selection/WorldPickerTests.cs
new file mode 100644
index 0000000..5f55161
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Selection/WorldPickerTests.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Numerics;
+using AcDream.Core.Selection;
+using Xunit;
+
+namespace AcDream.Core.Tests.Selection;
+
+public class WorldPickerTests
+{
+ private const float Epsilon = 0.01f;
+
+ private static (Matrix4x4 View, Matrix4x4 Projection) MakeIdentityCamera()
+ {
+ var view = Matrix4x4.Identity;
+ var proj = Matrix4x4.CreatePerspectiveFieldOfView(
+ fieldOfView: MathF.PI / 3f,
+ aspectRatio: 16f / 9f,
+ nearPlaneDistance: 0.1f,
+ farPlaneDistance: 100f);
+ return (view, proj);
+ }
+
+ [Fact]
+ public void BuildRay_CenterOfViewport_ReturnsForwardRay()
+ {
+ var (view, proj) = MakeIdentityCamera();
+ const float vpW = 1920f, vpH = 1080f;
+
+ var (_, direction) = WorldPicker.BuildRay(
+ mouseX: vpW / 2f, mouseY: vpH / 2f,
+ viewportW: vpW, viewportH: vpH,
+ view, proj);
+
+ // Right-handed perspective + identity view -> camera looks down -Z.
+ // Center pixel ray = (0, 0, -1) within float epsilon.
+ Assert.True(MathF.Abs(direction.X) < Epsilon, $"direction.X = {direction.X}");
+ Assert.True(MathF.Abs(direction.Y) < Epsilon, $"direction.Y = {direction.Y}");
+ Assert.True(direction.Z < -0.99f, $"direction.Z = {direction.Z}");
+ }
+
+ [Fact]
+ public void BuildRay_OffsetMouseRight_DeflectsRayPositiveX()
+ {
+ var (view, proj) = MakeIdentityCamera();
+ const float vpW = 1920f, vpH = 1080f;
+
+ var (_, direction) = WorldPicker.BuildRay(
+ mouseX: vpW * 0.75f, mouseY: vpH / 2f,
+ viewportW: vpW, viewportH: vpH,
+ view, proj);
+
+ Assert.True(direction.X > 0.1f, $"direction.X = {direction.X} (expected > 0.1)");
+ }
+}