fix: rasterized court coverage with 3 distinct colors

- Blue = cam0 only, Pink = cam1 only, Purple = overlap
- Grid-based fill, no dashed lines or edge artifacts
- White court lines on top

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruslan Bakiev
2026-03-07 11:42:02 +07:00
parent d34c25a6d7
commit 72e6d1c191

View File

@@ -661,98 +661,54 @@ function addStereocameras(scene) {
var deg2rad = Math.PI / 180; var deg2rad = Math.PI / 180;
var halfFov = hfov / 2; var halfFov = hfov / 2;
function drawCourtCoverage(cx, angleDeg, color) { // Rasterize coverage onto a grid, then draw colored quads
// Center direction: +Y (90°) rotated by angleDeg var gridRes = 0.2; // 20cm per cell
var centerAngle = 90 + angleDeg; // +angle = toward +X, -angle = toward -X var cols = Math.ceil((courtMaxX - courtMinX) / gridRes);
var leftAngle = centerAngle + halfFov; var rows = Math.ceil((courtMaxY - courtMinY) / gridRes);
var rightAngle = centerAngle - halfFov; // 0 = no coverage, 1 = cam0 only, 2 = cam1 only, 3 = both
var grid = new Array(cols * rows).fill(0);
// Cast ray from camera ground pos, clip to court rect function markCoverage(cx, angleDeg, bit) {
var ox = cx, oy = baseY; var centerAngle = (90 + angleDeg) * deg2rad;
var halfFovRad = halfFov * deg2rad;
var leftAngle = centerAngle + halfFovRad;
var rightAngle = centerAngle - halfFovRad;
function rayCourtIntersect(angleDeg2) { for (var r = 0; r < rows; r++) {
var rad = angleDeg2 * deg2rad; for (var c = 0; c < cols; c++) {
var dx = Math.cos(rad); var cellX = courtMinX + (c + 0.5) * gridRes;
var dy = Math.sin(rad); var cellY = courtMinY + (r + 0.5) * gridRes;
if (Math.abs(dx) < 1e-9 && Math.abs(dy) < 1e-9) return null; var dx = cellX - cx;
var dy = cellY - baseY;
var tMin = 0.01, tMax = 50; var angle = Math.atan2(dy, dx);
// Clip to court Y bounds if (angle >= rightAngle && angle <= leftAngle) {
if (dy > 1e-9) { grid[r * cols + c] |= bit;
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);
}
// Build coverage polygon on court surface
var points = [];
var origin = new THREE.Vector3(ox, Math.max(oy, courtMinY), 0.02);
var steps = 48;
for (var i = 0; i <= steps; i++) {
var a = rightAngle + (leftAngle - rightAngle) * (i / steps);
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);
}
}
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([
fanOrigin, points[i], points[i + 1]
]);
scene.add(new THREE.Mesh(triGeo, new THREE.MeshBasicMaterial({
color: color, transparent: true, opacity: 0.4, side: THREE.DoubleSide
})));
}
// Center line on court
var centerPt = rayCourtIntersect(centerAngle);
if (centerPt) {
centerPt.x = Math.max(courtMinX, Math.min(courtMaxX, centerPt.x));
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();
} }
} }
// cam0: rotated -28° (looks slightly toward -X) markCoverage(cam0x, -camAngle, 1);
drawCourtCoverage(cam0x, -camAngle, 0x44aaff); markCoverage(cam1x, camAngle, 2);
// cam1: rotated +28° (looks slightly toward +X)
drawCourtCoverage(cam1x, camAngle, 0xff44aa); // Colors: cam0 only = blue, cam1 only = pink, overlap = purple
var colors = { 1: 0x4488ff, 2: 0xff44aa, 3: 0xaa44ff };
var opacities = { 1: 0.35, 2: 0.35, 3: 0.5 };
for (var r = 0; r < rows; r++) {
for (var c = 0; c < cols; c++) {
var val = grid[r * cols + c];
if (val === 0) continue;
var x1 = courtMinX + c * gridRes;
var y1 = courtMinY + r * gridRes;
var quad = new THREE.PlaneGeometry(gridRes, gridRes);
var mesh = new THREE.Mesh(quad, new THREE.MeshBasicMaterial({
color: colors[val], transparent: true, opacity: opacities[val], side: THREE.DoubleSide
}));
mesh.position.set(x1 + gridRes / 2, y1 + gridRes / 2, 0.015);
scene.add(mesh);
}
}
} }
// ===================== Draw court lines ===================== // ===================== Draw court lines =====================