diff --git a/src/web/templates/index.html b/src/web/templates/index.html
index 677b9e9..24cae10 100644
--- a/src/web/templates/index.html
+++ b/src/web/templates/index.html
@@ -507,7 +507,7 @@ function initCourtScene() {
courtScene.add(new THREE.AmbientLight(0xffffff, 0.8));
// Physical camera marker: 1m from net, center, 1m height
- addCameraMarker(courtScene, 7.7, 3.05, 1);
+ addStereocameras(courtScene);
// Load existing calibration cameras
fetch('/api/calibration/data')
@@ -608,7 +608,7 @@ function initTrajectoryScene() {
trajScene.add(new THREE.AmbientLight(0xffffff, 0.8));
// Physical camera marker: 1m from net, center, 1m height
- addCameraMarker(trajScene, 7.7, 3.05, 1);
+ addStereocameras(trajScene);
function animateTraj() {
requestAnimationFrame(animateTraj);
@@ -626,31 +626,106 @@ function initTrajectoryScene() {
});
}
-// ===================== Camera marker =====================
-function addCameraMarker(scene, x, y, z) {
- // Camera body (box)
- var body = new THREE.Mesh(
- new THREE.BoxGeometry(0.2, 0.15, 0.15),
- new THREE.MeshBasicMaterial({ color: 0x44aaff })
- );
- body.position.set(x, y, z);
- scene.add(body);
+// ===================== Stereo camera rig with FOV projection =====================
+function addStereocameras(scene) {
+ // 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 stereoGap = 0.06; // 6cm between cameras
+ var camAngle = 28; // degrees each camera is rotated outward
+ var hfov = 160; // horizontal FOV degrees
- // Lens (cone pointing toward net at x=6.7)
- var lens = new THREE.Mesh(
- new THREE.ConeGeometry(0.06, 0.15, 8),
- new THREE.MeshBasicMaterial({ color: 0x2288dd })
- );
- lens.rotation.z = Math.PI / 2; // point along -X toward net
- lens.position.set(x - 0.17, y, z);
- scene.add(lens);
+ var cam0x = baseX - stereoGap / 2; // left cam
+ var cam1x = baseX + stereoGap / 2; // right cam
- // Pole from ground to camera
+ // Draw each camera
+ function drawCamBody(cx, cy, cz, color) {
+ var body = new THREE.Mesh(
+ new THREE.BoxGeometry(0.12, 0.08, 0.08),
+ new THREE.MeshBasicMaterial({ color: color })
+ );
+ body.position.set(cx, cy, cz);
+ scene.add(body);
+ }
+
+ drawCamBody(cam0x, baseY, baseZ, 0x44aaff); // cam0 blue
+ drawCamBody(cam1x, baseY, baseZ, 0xff44aa); // cam1 pink
+
+ // Pole/mount
var poleGeo = new THREE.BufferGeometry().setFromPoints([
- new THREE.Vector3(x, y, 0),
- new THREE.Vector3(x, y, z)
+ new THREE.Vector3(baseX, baseY, 0),
+ new THREE.Vector3(baseX, baseY, baseZ)
]);
scene.add(new THREE.Line(poleGeo, new THREE.LineBasicMaterial({ color: 0x666666 })));
+
+ // FOV projection on ground plane (z=0.02)
+ // Base look direction is +Y (into the court)
+ // cam0 rotated -28° (toward -X), cam1 rotated +28° (toward +X)
+ var deg2rad = Math.PI / 180;
+ var halfFov = hfov / 2;
+ var projDist = 16; // ray length for projection
+
+ function drawFov(cx, cy, cz, angleDeg, color) {
+ var centerAngle = 90 - angleDeg; // 90° = +Y direction, offset by camera rotation
+ var leftAngle = centerAngle + halfFov;
+ var rightAngle = centerAngle - halfFov;
+
+ // Project rays to ground: from (cx, cy, cz) in direction, find where z=0
+ function rayToGround(angleDeg2) {
+ var rad = angleDeg2 * deg2rad;
+ var dx = Math.cos(rad);
+ var dy = Math.sin(rad);
+ // Extend ray to projDist in XY, at ground level
+ return new THREE.Vector3(cx + dx * projDist, cy + dy * projDist, 0.02);
+ }
+
+ var camPos = new THREE.Vector3(cx, cy, cz);
+ var groundCenter = new THREE.Vector3(cx, cy, 0.02);
+
+ // Draw FOV edges
+ 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++) {
+ var a = rightAngle + (leftAngle - rightAngle) * (i / steps);
+ fanPoints.push(rayToGround(a));
+ }
+
+ for (var i = 1; i < fanPoints.length - 1; i++) {
+ var triGeo = new THREE.BufferGeometry().setFromPoints([
+ fanPoints[0], fanPoints[i], fanPoints[i + 1]
+ ]);
+ var triMesh = new THREE.Mesh(triGeo, new THREE.MeshBasicMaterial({
+ color: color, transparent: true, opacity: 0.08, side: THREE.DoubleSide
+ }));
+ scene.add(triMesh);
+ }
+
+ // FOV arc line on ground
+ var arcPoints = [];
+ for (var i = 0; i <= steps; i++) {
+ var a = rightAngle + (leftAngle - rightAngle) * (i / steps);
+ arcPoints.push(rayToGround(a));
+ }
+ 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)
+ drawFov(cam0x, baseY, baseZ, -camAngle, 0x44aaff);
+ // cam1: rotated +28° (looking slightly right / toward +X)
+ drawFov(cam1x, baseY, baseZ, camAngle, 0xff44aa);
}
// ===================== Draw court lines =====================