Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-pointer-ray-origin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-three-map": patch
---

Fix hover/raycast inaccuracy by deriving the pointer-event ray origin from the cursor position. The ray origin was being unprojected from a fixed NDC point that ignored the cursor, producing a skewed ray whose error grew towards the edges of the map.
Comment thread
RodrigoHamuy marked this conversation as resolved.
Outdated
19 changes: 19 additions & 0 deletions src/core/compute-ray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Matrix4, Ray } from "three";

/**
* Builds a raycaster ray from the pointer position by unprojecting NDC points
* through the inverted projection*view matrix.
*
* Both the origin (near plane) and direction (towards the far plane) are derived
* from the pointer, so the resulting ray is the geometrically correct line
* through the camera eye for any cursor position — the same near→far scheme
* three.js uses in `Raycaster.setFromCamera`.
*/
export function computeRay(ray: Ray, pointerX: number, pointerY: number, projViewInv: Matrix4) {
ray.origin.set(pointerX, pointerY, -1).applyMatrix4(projViewInv);
ray.direction
.set(pointerX, pointerY, 1)
.applyMatrix4(projViewInv)
.sub(ray.origin)
.normalize();
}
8 changes: 2 additions & 6 deletions src/core/events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Canvas, events as fiberEvents } from "@react-three/fiber";
import { Matrix4 } from "three";
import { computeRay } from "./compute-ray";
Comment thread
RodrigoHamuy marked this conversation as resolved.
Outdated

/** projection * view matrix inverted */
const projViewInv = new Matrix4()
Expand All @@ -22,12 +23,7 @@ export const events: Events = (store) => {
if (state.camera.userData.projByViewInv) projViewInv.fromArray(state.camera.userData.projByViewInv);

state.raycaster.camera = state.camera;
state.raycaster.ray.origin.setScalar(0).applyMatrix4(projViewInv);
state.raycaster.ray.direction
.set(state.pointer.x, state.pointer.y, 1)
.applyMatrix4(projViewInv)
.sub(state.raycaster.ray.origin)
.normalize();
computeRay(state.raycaster.ray, state.pointer.x, state.pointer.y, projViewInv);
Comment thread
RodrigoHamuy marked this conversation as resolved.
Outdated

},
};
Expand Down
40 changes: 40 additions & 0 deletions src/test/compute-ray.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Matrix4, PerspectiveCamera, Ray, Vector3 } from "three";
import { expect, it } from "vitest";
import { computeRay } from "../core/compute-ray";

/**
* For a perspective camera every pointer ray must pass through the camera eye.
* `computeRay` satisfies this for any cursor position by deriving the origin
* from the pointer; the previous fixed origin (`setScalar(0)`) only did so at
* the dead centre of the view, which is the hover/raycast offset bug.
*/
it("computeRay builds a ray that passes through the perspective camera eye", () => {
const camera = new PerspectiveCamera(60, 1.5, 0.1, 100);
camera.position.set(3, 4, 5);
camera.lookAt(0, 0, 0);
camera.updateMatrixWorld(true);
camera.updateProjectionMatrix();

// projection * view, inverted — the matrix events.ts unprojects through
const projViewInv = new Matrix4()
.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse)
.invert();

const ray = new Ray();
// an off-centre pointer, where the old fixed origin was wrong
computeRay(ray, 0.6, -0.3, projViewInv);

// direction is unit length
expect(ray.direction.length()).toBeCloseTo(1, 6);

// eye is collinear with the ray: (eye - origin) is parallel to direction
const eye = camera.position.clone();
const toEye = eye.clone().sub(ray.origin).normalize();
expect(Math.abs(toEye.dot(ray.direction))).toBeCloseTo(1, 5);

// regression guard: the old origin (unprojected NDC centre) is NOT on the
// ray, so it produced a skewed ray that missed the eye.
const oldOrigin = new Vector3().setScalar(0).applyMatrix4(projViewInv);
const toEyeOld = eye.clone().sub(oldOrigin).normalize();
expect(Math.abs(toEyeOld.dot(ray.direction))).toBeLessThan(0.999);
});