fix: project camera FOV coverage directly onto court surface
- FOV clipped to court boundaries (0,0)-(13.4,6.1) - Colored overlay shows which court areas each camera covers - Dashed center line shows each camera's look direction - cam0 (blue) -28° from +Y, cam1 (pink) +28° from +Y Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -626,106 +626,133 @@ function initTrajectoryScene() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== Stereo camera rig with FOV projection =====================
|
// ===================== Stereo camera rig with court coverage =====================
|
||||||
function addStereocameras(scene) {
|
function addStereocameras(scene) {
|
||||||
// Position: net line (X=6.7), 1m outside court edge (Y=-1), 1m height (Z=1)
|
// Position: net line (X=6.7), 1m outside court edge (Y=-1), 1m height (Z=1)
|
||||||
var baseX = 6.7, baseY = -1, baseZ = 1;
|
var baseX = 6.7, baseY = -1, baseZ = 1;
|
||||||
var stereoGap = 0.06; // 6cm between cameras
|
var stereoGap = 0.06; // 6cm between cameras
|
||||||
var camAngle = 28; // degrees each camera is rotated outward
|
var camAngle = 28; // degrees each camera is rotated outward from straight +Y
|
||||||
var hfov = 160; // horizontal FOV degrees
|
var hfov = 160; // horizontal FOV degrees
|
||||||
|
|
||||||
var cam0x = baseX - stereoGap / 2; // left cam
|
var cam0x = baseX - stereoGap / 2;
|
||||||
var cam1x = baseX + stereoGap / 2; // right cam
|
var cam1x = baseX + stereoGap / 2;
|
||||||
|
|
||||||
// Draw each camera
|
// Small camera bodies
|
||||||
function drawCamBody(cx, cy, cz, color) {
|
function drawCamBody(cx, color) {
|
||||||
var body = new THREE.Mesh(
|
var body = new THREE.Mesh(
|
||||||
new THREE.BoxGeometry(0.12, 0.08, 0.08),
|
new THREE.BoxGeometry(0.12, 0.08, 0.08),
|
||||||
new THREE.MeshBasicMaterial({ color: color })
|
new THREE.MeshBasicMaterial({ color: color })
|
||||||
);
|
);
|
||||||
body.position.set(cx, cy, cz);
|
body.position.set(cx, baseY, baseZ);
|
||||||
scene.add(body);
|
scene.add(body);
|
||||||
}
|
}
|
||||||
|
drawCamBody(cam0x, 0x44aaff);
|
||||||
|
drawCamBody(cam1x, 0xff44aa);
|
||||||
|
|
||||||
drawCamBody(cam0x, baseY, baseZ, 0x44aaff); // cam0 blue
|
// Pole
|
||||||
drawCamBody(cam1x, baseY, baseZ, 0xff44aa); // cam1 pink
|
|
||||||
|
|
||||||
// Pole/mount
|
|
||||||
var poleGeo = new THREE.BufferGeometry().setFromPoints([
|
var poleGeo = new THREE.BufferGeometry().setFromPoints([
|
||||||
new THREE.Vector3(baseX, baseY, 0),
|
new THREE.Vector3(baseX, baseY, 0),
|
||||||
new THREE.Vector3(baseX, baseY, baseZ)
|
new THREE.Vector3(baseX, baseY, baseZ)
|
||||||
]);
|
]);
|
||||||
scene.add(new THREE.Line(poleGeo, new THREE.LineBasicMaterial({ color: 0x666666 })));
|
scene.add(new THREE.Line(poleGeo, new THREE.LineBasicMaterial({ color: 0x666666 })));
|
||||||
|
|
||||||
// FOV projection on ground plane (z=0.02)
|
// Court boundaries for clipping
|
||||||
// Base look direction is +Y (into the court)
|
var courtMinX = 0, courtMaxX = 13.4, courtMinY = 0, courtMaxY = 6.1;
|
||||||
// cam0 rotated -28° (toward -X), cam1 rotated +28° (toward +X)
|
|
||||||
var deg2rad = Math.PI / 180;
|
var deg2rad = Math.PI / 180;
|
||||||
var halfFov = hfov / 2;
|
var halfFov = hfov / 2;
|
||||||
var projDist = 16; // ray length for projection
|
|
||||||
|
|
||||||
function drawFov(cx, cy, cz, angleDeg, color) {
|
function drawCourtCoverage(cx, angleDeg, color) {
|
||||||
var centerAngle = 90 - angleDeg; // 90° = +Y direction, offset by camera rotation
|
// Center direction: +Y (90°) rotated by angleDeg
|
||||||
|
var centerAngle = 90 + angleDeg; // +angle = toward +X, -angle = toward -X
|
||||||
var leftAngle = centerAngle + halfFov;
|
var leftAngle = centerAngle + halfFov;
|
||||||
var rightAngle = centerAngle - halfFov;
|
var rightAngle = centerAngle - halfFov;
|
||||||
|
|
||||||
// Project rays to ground: from (cx, cy, cz) in direction, find where z=0
|
// Cast ray from camera ground pos, clip to court rect
|
||||||
function rayToGround(angleDeg2) {
|
var ox = cx, oy = baseY;
|
||||||
|
|
||||||
|
function rayCourtIntersect(angleDeg2) {
|
||||||
var rad = angleDeg2 * deg2rad;
|
var rad = angleDeg2 * deg2rad;
|
||||||
var dx = Math.cos(rad);
|
var dx = Math.cos(rad);
|
||||||
var dy = Math.sin(rad);
|
var dy = Math.sin(rad);
|
||||||
// Extend ray to projDist in XY, at ground level
|
if (Math.abs(dx) < 1e-9 && Math.abs(dy) < 1e-9) return null;
|
||||||
return new THREE.Vector3(cx + dx * projDist, cy + dy * projDist, 0.02);
|
|
||||||
|
var tMin = 0.01, tMax = 50;
|
||||||
|
// Clip to court Y bounds
|
||||||
|
if (dy > 1e-9) {
|
||||||
|
tMax = Math.min(tMax, (courtMaxY - oy) / dy);
|
||||||
|
} else if (dy < -1e-9) {
|
||||||
|
tMax = Math.min(tMax, (courtMinY - oy) / dy);
|
||||||
|
}
|
||||||
|
// Clip to court X bounds
|
||||||
|
if (dx > 1e-9) {
|
||||||
|
var tx = (courtMaxX - ox) / dx;
|
||||||
|
tMax = Math.min(tMax, tx);
|
||||||
|
} else if (dx < -1e-9) {
|
||||||
|
var tx = (courtMinX - ox) / dx;
|
||||||
|
tMax = Math.min(tMax, tx);
|
||||||
|
}
|
||||||
|
// Must enter court (Y >= 0)
|
||||||
|
if (dy > 1e-9) {
|
||||||
|
tMin = Math.max(tMin, (courtMinY - oy) / dy);
|
||||||
|
} else if (dy <= 0) {
|
||||||
|
return null; // ray goes away from court
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tMax <= tMin) return null;
|
||||||
|
return new THREE.Vector3(ox + dx * tMax, oy + dy * tMax, 0.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
var camPos = new THREE.Vector3(cx, cy, cz);
|
// Build coverage polygon on court surface
|
||||||
var groundCenter = new THREE.Vector3(cx, cy, 0.02);
|
var points = [];
|
||||||
|
var origin = new THREE.Vector3(ox, Math.max(oy, courtMinY), 0.02);
|
||||||
// Draw FOV edges
|
var steps = 48;
|
||||||
var edgeMat = new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0.6 });
|
|
||||||
|
|
||||||
var leftPt = rayToGround(leftAngle);
|
|
||||||
var rightPt = rayToGround(rightAngle);
|
|
||||||
var centerPt = rayToGround(centerAngle);
|
|
||||||
|
|
||||||
// Edge lines from camera to ground projection
|
|
||||||
[leftPt, rightPt, centerPt].forEach(function(pt) {
|
|
||||||
var geo = new THREE.BufferGeometry().setFromPoints([camPos, pt]);
|
|
||||||
scene.add(new THREE.Line(geo, edgeMat));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fill FOV area on ground with semi-transparent triangle fan
|
|
||||||
var fanPoints = [groundCenter.clone()];
|
|
||||||
var steps = 24;
|
|
||||||
for (var i = 0; i <= steps; i++) {
|
for (var i = 0; i <= steps; i++) {
|
||||||
var a = rightAngle + (leftAngle - rightAngle) * (i / steps);
|
var a = rightAngle + (leftAngle - rightAngle) * (i / steps);
|
||||||
fanPoints.push(rayToGround(a));
|
var pt = rayCourtIntersect(a);
|
||||||
|
if (pt) {
|
||||||
|
// Clamp to court bounds
|
||||||
|
pt.x = Math.max(courtMinX, Math.min(courtMaxX, pt.x));
|
||||||
|
pt.y = Math.max(courtMinY, Math.min(courtMaxY, pt.y));
|
||||||
|
points.push(pt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 1; i < fanPoints.length - 1; i++) {
|
if (points.length < 2) return;
|
||||||
|
|
||||||
|
// Draw coverage edge line on court
|
||||||
|
var edgeGeo = new THREE.BufferGeometry().setFromPoints(points);
|
||||||
|
scene.add(new THREE.Line(edgeGeo, new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0.7 })));
|
||||||
|
|
||||||
|
// Fill coverage area as triangle fan from camera ground position
|
||||||
|
var fanOrigin = new THREE.Vector3(ox, courtMinY, 0.02);
|
||||||
|
for (var i = 0; i < points.length - 1; i++) {
|
||||||
var triGeo = new THREE.BufferGeometry().setFromPoints([
|
var triGeo = new THREE.BufferGeometry().setFromPoints([
|
||||||
fanPoints[0], fanPoints[i], fanPoints[i + 1]
|
fanOrigin, points[i], points[i + 1]
|
||||||
]);
|
]);
|
||||||
var triMesh = new THREE.Mesh(triGeo, new THREE.MeshBasicMaterial({
|
scene.add(new THREE.Mesh(triGeo, new THREE.MeshBasicMaterial({
|
||||||
color: color, transparent: true, opacity: 0.08, side: THREE.DoubleSide
|
color: color, transparent: true, opacity: 0.12, side: THREE.DoubleSide
|
||||||
}));
|
})));
|
||||||
scene.add(triMesh);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FOV arc line on ground
|
// Center line on court
|
||||||
var arcPoints = [];
|
var centerPt = rayCourtIntersect(centerAngle);
|
||||||
for (var i = 0; i <= steps; i++) {
|
if (centerPt) {
|
||||||
var a = rightAngle + (leftAngle - rightAngle) * (i / steps);
|
centerPt.x = Math.max(courtMinX, Math.min(courtMaxX, centerPt.x));
|
||||||
arcPoints.push(rayToGround(a));
|
centerPt.y = Math.max(courtMinY, Math.min(courtMaxY, centerPt.y));
|
||||||
|
var clGeo = new THREE.BufferGeometry().setFromPoints([
|
||||||
|
new THREE.Vector3(ox, courtMinY, 0.02), centerPt
|
||||||
|
]);
|
||||||
|
scene.add(new THREE.Line(clGeo, new THREE.LineDashedMaterial({
|
||||||
|
color: color, dashSize: 0.2, gapSize: 0.1
|
||||||
|
})));
|
||||||
|
scene.children[scene.children.length - 1].computeLineDistances();
|
||||||
}
|
}
|
||||||
var arcGeo = new THREE.BufferGeometry().setFromPoints(arcPoints);
|
|
||||||
scene.add(new THREE.Line(arcGeo, new THREE.LineBasicMaterial({ color: color, transparent: true, opacity: 0.3 })));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cam0: rotated -28° (looking slightly left / toward -X)
|
// cam0: rotated -28° (looks slightly toward -X)
|
||||||
drawFov(cam0x, baseY, baseZ, -camAngle, 0x44aaff);
|
drawCourtCoverage(cam0x, -camAngle, 0x44aaff);
|
||||||
// cam1: rotated +28° (looking slightly right / toward +X)
|
// cam1: rotated +28° (looks slightly toward +X)
|
||||||
drawFov(cam1x, baseY, baseZ, camAngle, 0xff44aa);
|
drawCourtCoverage(cam1x, camAngle, 0xff44aa);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== Draw court lines =====================
|
// ===================== Draw court lines =====================
|
||||||
|
|||||||
Reference in New Issue
Block a user