THREE.Raycaster: Unsupported camera type: Camera

What "THREE.Raycaster: Unsupported camera type" means in three.js, the branch of Raycaster.setFromCamera() that prints it, which cameras and values reach it, how to build the ray yourself from a projection matrix, and what the paprika3d plugin's eval and observe replies show before and after.

three.js logs this console error from Raycaster.setFromCamera() when the camera it was given is neither a PerspectiveCamera nor an OrthographicCamera. The word after the colon is the camera's type property, so it reads Camera for a plain THREE.Camera, CubeCamera for a cube camera and undefined for an object that is not a camera at all. The raycaster's ray is left as it was, so the next intersectObjects() tests whatever ray the raycaster held before, and clicks pick nothing or the wrong object.

Where three.js prints it

setFromCamera() in src/core/Raycaster.js (three r186) has one branch per supported projection and an error for the rest:

setFromCamera( coords, camera ) {

	if ( camera.isPerspectiveCamera ) {

		this.ray.origin.setFromMatrixPosition( camera.matrixWorld );
		this.ray.direction.set( coords.x, coords.y, 0.5 ).unproject( camera ).sub( this.ray.origin ).normalize();
		this.camera = camera;

	} else if ( camera.isOrthographicCamera ) {

		this.ray.origin.set( coords.x, coords.y, camera.projectionMatrix.elements[ 14 ] ).unproject( camera ); // set origin in plane of camera
		this.ray.direction.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld );
		this.camera = camera;

	} else {

		error( 'Raycaster: Unsupported camera type: ' + camera.type );

	}

}

The test is on the isPerspectiveCamera and isOrthographicCamera flags, not on the matrix. A camera whose projection matrix is a valid perspective matrix still lands in the else branch when the object is a base Camera. ArrayCamera, which WebXR uses, extends PerspectiveCamera and passes.

Why the camera is not supported

A page that reproduces it

This page draws three crates through a bare Camera that receives a perspective matrix, standing in for a map layer's matrix, and turns the clicked crate red:

const camera = new THREE.Camera();
camera.projectionMatrix.copy(new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100).projectionMatrix);
// ...
raycaster.setFromCamera(pointer, camera); // a base Camera: neither branch matches
const hit = raycaster.intersectObjects(crates)[0];
if (hit) hit.object.material.color.set(0xff5533);

paprika3d is a Claude Code plugin and MCP server that opens a three.js page in its own headless Chrome on a paused clock. The calls below ran on 2026-10-28 with the stable build, on a machine with no GPU; the run folder becomes <project>, and the replies are cut to the lines this page explains, word for word.

look {}

On screen (3):
crate* ×3 8.1% [250, 322, 1029, 477]
Never visible: none

eval {"code": "renderer.domElement.dispatchEvent(new PointerEvent(\"pointerdown\", { clientX: 950, clientY: 400 })); return scene.children.map(c => c.name + \" #\" + c.material.color.getHexString())"}

["crate0 #5588cc","crate1 #5588cc","crate2 #5588cc"]
errors: THREE.Raycaster: Unsupported camera type: Camera

observe {}

{"entities":{"crate0":{"type":"Mesh","position":[-2,0,0]},"crate1":{"type":"Mesh","position":[0,0,0]},"crate2":{"type":"Mesh","position":[2,0,0]}},"harness":{"frame":1,"renderer":{"calls":3,"triangles":36,"textures":0,"geometries":3,"programs":2},"scene":{"objects":3,"meshes":3,"lights":0,"cameras":0},"errors":["THREE.Raycaster: Unsupported camera type: Camera"],"changed":null}}

look draws the first frame, so the crates have their world matrices, and shows all three on screen; the box [250, 322, 1029, 477] is the pixel area they cover together, which puts the right crate under x 950. The eval sends the page's own handler a press at that point. The error comes back on its errors: line, and the returned colours show that no crate changed: the raycaster kept its default ray from the origin along minus z, which starts inside the middle crate and meets no front face. observe keeps the error in harness.errors for later calls. The replies show the camera's type but not where the camera came from; for a map layer, the place to look is the code that builds the camera in the layer's setup.

The fix

When the page owns the camera, make it a PerspectiveCamera and keep setFromCamera(). When the matrix comes from outside, build the ray with unproject(), which works on any camera: it multiplies by projectionMatrixInverse and then by matrixWorld. A base Camera never fills projectionMatrixInverse itself, so invert it each time the projection is copied in:

<!-- verify -->
<!doctype html>
<html><head><meta charset="utf-8"><title>Pick</title>
<style>html,body{margin:0;overflow:hidden}canvas{display:block}</style></head>
<body>
<script type="importmap">{"imports":{"three":"./node_modules/three/build/three.module.js","three/addons/":"./node_modules/three/examples/jsm/"}}</script>
<script type="module">
import * as THREE from 'three';

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1d2128);

// a camera that has nothing but a projection matrix, as a map or AR layer hands it over
const camera = new THREE.Camera();
const setProjection = () => {
  camera.projectionMatrix.copy(new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100).projectionMatrix);
  camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert(); // unproject() reads the inverse
};
setProjection();
camera.position.set(0, 0, 6);
camera.updateMatrixWorld();

const crates = [-2, 0, 2].map((x, i) => {
  const m = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 0x5588cc }));
  m.position.x = x;
  m.name = 'crate' + i;
  scene.add(m);
  return m;
});

const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
renderer.domElement.addEventListener('pointerdown', e => {
  pointer.set(e.clientX / innerWidth * 2 - 1, -(e.clientY / innerHeight) * 2 + 1);
  // setFromCamera() handles perspective and orthographic cameras; for a bare matrix, unproject two depths instead
  const near = new THREE.Vector3(pointer.x, pointer.y, -1).unproject(camera);
  const far = new THREE.Vector3(pointer.x, pointer.y, 1).unproject(camera);
  raycaster.set(near, far.sub(near).normalize());
  const hit = raycaster.intersectObjects(crates)[0];
  if (hit) hit.object.material.color.set(0xff5533);
});

addEventListener('resize', () => {
  renderer.setSize(innerWidth, innerHeight);
  setProjection();
});
renderer.setAnimationLoop(() => renderer.render(scene, camera));
</script>
</body></html>

The same press now turns the right crate red, and nothing is logged:

eval {"code": "renderer.domElement.dispatchEvent(new PointerEvent(\"pointerdown\", { clientX: 950, clientY: 400 })); return scene.children.map(c => c.name + \" #\" + c.material.color.getHexString())"}

["crate0 #5588cc","crate1 #5588cc","crate2 #ff5533"]

observe {}

{"entities":{"crate0":{"type":"Mesh","position":[-2,0,0]},"crate1":{"type":"Mesh","position":[0,0,0]},"crate2":{"type":"Mesh","position":[2,0,0]}},"harness":{"frame":1,"renderer":{"calls":3,"triangles":36,"textures":0,"geometries":3,"programs":2},"scene":{"objects":3,"meshes":3,"lights":0,"cameras":0},"errors":[],"changed":null}}

Points at depth -1 and 1 in normalized device coordinates lie on the near and far planes, so the ray between them passes through the pixel under the pointer for a perspective or an orthographic matrix alike. If the outside library moves the camera through the matrix it hands over (a combined view-projection matrix, as map layers usually pass), leave matrixWorld at identity and invert that combined matrix instead; the ray then comes out in world space directly.

Where people ask about it

These threads quote the message; each count was read on 2026-10-28.