From d8cc3904e6f4e6e0849c3a7df56f22ecaf43b5ad Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:28:45 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20stereo=20camera=20rig=20on=20net=20line?= =?UTF-8?q?=20with=20160=C2=B0=20FOV=20projection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cameras at net line (X=6.7), 1m outside court (Y=-1), 1m height - Stereo pair 6cm apart, each rotated 28° outward - FOV cones projected onto court surface showing coverage - cam0 (blue) looks left, cam1 (pink) looks right Co-Authored-By: Claude Opus 4.6 --- src/web/templates/index.html | 119 ++++++++++++++++++++++++++++------- 1 file changed, 97 insertions(+), 22 deletions(-) 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 =====================