fix: correct FOV projection origin from camera position on court edge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -661,62 +661,66 @@ function addStereocameras(scene) {
|
|||||||
var deg2rad = Math.PI / 180;
|
var deg2rad = Math.PI / 180;
|
||||||
var halfFov = hfov / 2;
|
var halfFov = hfov / 2;
|
||||||
|
|
||||||
function buildCoveragePolygon(cx, angleDeg) {
|
function drawCoverage(cx, angleDeg, color) {
|
||||||
var centerAngle = 90 + angleDeg;
|
var centerAngle = 90 + angleDeg;
|
||||||
var leftAngle = centerAngle + halfFov;
|
var leftAngle = centerAngle + halfFov;
|
||||||
var rightAngle = centerAngle - halfFov;
|
var rightAngle = centerAngle - halfFov;
|
||||||
var ox = cx, oy = baseY;
|
var ox = cx, oy = baseY;
|
||||||
|
|
||||||
function rayToCourtEdge(aDeg) {
|
// Cast ray, return farthest point on court boundary
|
||||||
|
function castRay(aDeg) {
|
||||||
var rad = aDeg * deg2rad;
|
var rad = aDeg * deg2rad;
|
||||||
var dx = Math.cos(rad);
|
var dx = Math.cos(rad);
|
||||||
var dy = Math.sin(rad);
|
var dy = Math.sin(rad);
|
||||||
if (dy <= 0) return null;
|
if (dy <= 0) return null;
|
||||||
|
|
||||||
var tMax = 50;
|
// t where ray enters court (Y=0)
|
||||||
if (dy > 1e-9) tMax = Math.min(tMax, (courtMaxY - oy) / dy);
|
|
||||||
if (dx > 1e-9) tMax = Math.min(tMax, (courtMaxX - ox) / dx);
|
|
||||||
else if (dx < -1e-9) tMax = Math.min(tMax, (courtMinX - ox) / dx);
|
|
||||||
|
|
||||||
var tEnter = (courtMinY - oy) / dy;
|
var tEnter = (courtMinY - oy) / dy;
|
||||||
if (tMax <= tEnter) return null;
|
// t where ray exits court
|
||||||
var t = Math.max(tEnter, 0.01);
|
var tExit = (courtMaxY - oy) / dy;
|
||||||
t = tMax;
|
// clip X bounds
|
||||||
var px = ox + dx * t;
|
if (dx > 1e-9) tExit = Math.min(tExit, (courtMaxX - ox) / dx);
|
||||||
var py = oy + dy * t;
|
else if (dx < -1e-9) tExit = Math.min(tExit, (courtMinX - ox) / dx);
|
||||||
|
|
||||||
|
if (tExit <= tEnter) return null;
|
||||||
|
|
||||||
|
var px = ox + dx * tExit;
|
||||||
|
var py = oy + dy * tExit;
|
||||||
px = Math.max(courtMinX, Math.min(courtMaxX, px));
|
px = Math.max(courtMinX, Math.min(courtMaxX, px));
|
||||||
py = Math.max(courtMinY, Math.min(courtMaxY, py));
|
py = Math.max(courtMinY, Math.min(courtMaxY, py));
|
||||||
return new THREE.Vector3(px, py, 0.015);
|
return new THREE.Vector3(px, py, 0.015);
|
||||||
}
|
}
|
||||||
|
|
||||||
var points = [];
|
// Camera position projected onto court near edge (Y=0)
|
||||||
|
var camOnCourt = new THREE.Vector3(
|
||||||
|
Math.max(courtMinX, Math.min(courtMaxX, cx)), courtMinY, 0.015
|
||||||
|
);
|
||||||
|
|
||||||
|
// Far edge points
|
||||||
|
var farPoints = [];
|
||||||
var steps = 64;
|
var steps = 64;
|
||||||
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);
|
||||||
var pt = rayToCourtEdge(a);
|
var pt = castRay(a);
|
||||||
if (pt) points.push(pt);
|
if (pt) farPoints.push(pt);
|
||||||
}
|
}
|
||||||
return points;
|
if (farPoints.length < 2) return;
|
||||||
}
|
|
||||||
|
|
||||||
function drawCoverageFan(points, color) {
|
// Draw triangle fan from camera court position to far edge
|
||||||
if (points.length < 2) return;
|
var mat = new THREE.MeshBasicMaterial({
|
||||||
var origin = new THREE.Vector3(points[0].x, courtMinY, 0.015);
|
color: color, transparent: true, opacity: 0.35,
|
||||||
for (var i = 0; i < points.length - 1; i++) {
|
side: THREE.DoubleSide, depthWrite: false
|
||||||
|
});
|
||||||
|
for (var i = 0; i < farPoints.length - 1; i++) {
|
||||||
var triGeo = new THREE.BufferGeometry().setFromPoints([
|
var triGeo = new THREE.BufferGeometry().setFromPoints([
|
||||||
origin, points[i], points[i + 1]
|
camOnCourt, farPoints[i], farPoints[i + 1]
|
||||||
]);
|
]);
|
||||||
scene.add(new THREE.Mesh(triGeo, new THREE.MeshBasicMaterial({
|
scene.add(new THREE.Mesh(triGeo, mat));
|
||||||
color: color, transparent: true, opacity: 0.35, side: THREE.DoubleSide,
|
|
||||||
depthWrite: false
|
|
||||||
})));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var poly0 = buildCoveragePolygon(cam0x, -camAngle);
|
drawCoverage(cam0x, -camAngle, 0x4488ff); // blue
|
||||||
var poly1 = buildCoveragePolygon(cam1x, camAngle);
|
drawCoverage(cam1x, camAngle, 0xff44aa); // pink
|
||||||
drawCoverageFan(poly0, 0x4488ff); // blue
|
|
||||||
drawCoverageFan(poly1, 0xff44aa); // pink
|
|
||||||
// overlap blends to purple naturally
|
// overlap blends to purple naturally
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user