// Rotate satellite position back to planet's frame let planetRotationQuat = rotateQuaternion({ x: 0, y: 0, z: 0, w: 1 }, { x: 0, y: 1, z: 0 }, rotationAngle); let satellitePosInPlanetFrame = rotateVectorByQuaternion(satellitePos, planetRotationQuatInverse);
letP2 = satellitePosInPlanetFrame; // Satellite position in planet's frame let d = dist3D(P1, P2); // Distance between planet center and satellite
// Check for intersection if (d > R1 + R2 || d < abs(R1 - R2)) { return; // No intersection }
// Calculate intersection circle let a = (R1 * R1 - R2 * R2 + d * d) / (2 * d); let h = sqrt(R1 * R1 - a * a);
// Center of intersection circle let P_c = { x: P1.x + a * (P2.x - P1.x) / d, y: P1.y + a * (P2.y - P1.y) / d, z: P1.z + a * (P2.z - P1.z) / d };
// Normal vector let n = { x: (P2.x - P1.x) / d, y: (P2.y - P1.y) / d, z: (P2.z - P1.z) / d };
// Orthogonal basis let arbitraryVector = { x: 1, y: 0, z: 0 }; if (abs(n.x) > 0.99) arbitraryVector = { x: 0, y: 1, z: 0 }; let u = crossProduct(n, arbitraryVector); u = normalize(u); let v = crossProduct(n, u);
// Sample points around the intersection circle let numPoints = 100; let angleStep = TWO_PI / numPoints; let frontVertices = []; let backVertices = [];
for (let i = 0; i <= numPoints; i++) { let angle = i * angleStep; let point = { x: P_c.x + h * (cos(angle) * u.x + sin(angle) * v.x), y: P_c.y + h * (cos(angle) * u.y + sin(angle) * v.y), z: P_c.z + h * (cos(angle) * u.z + sin(angle) * v.z) };
// Rotate back to world frame let rotatedPoint = rotateVectorByQuaternion(point, planetRotationQuat);
// Project to 2D let screenX = centerX + rotatedPoint.z; let screenY = centerY - rotatedPoint.y;
// Determine front or back let normal = normalize(rotatedPoint); let viewDir = { x: -1, y: 0, z: 0 }; // Viewer direction let dotProduct = dot(normal, viewDir);
// Draw front area if (frontVertices.length > 2) { fill(150, 150, 150, 100); // Semi-transparent gray stroke(0); beginShape(); for (let v of frontVertices) vertex(v.x, v.y); endShape(CLOSE); noFill(); }
// Draw back area if (backVertices.length > 2) { fill(255, 255, 255, 0); // Transparent stroke(0); drawingContext.setLineDash([5, 5]); // Dashed line beginShape(); for (let v of backVertices) vertex(v.x, v.y); endShape(CLOSE); drawingContext.setLineDash([]); noFill(); } }
关键步骤总结:
求交判定:判断两球是否相交(不相交/包含时直接返回)。
求交圆参数:计算交圆中心 P_c 与半径 h。
构造正交基:在交圆平面内构造 u, v 两个正交方向,方便绕圈采样。
采样 + 投影:把交圆上的点旋转回世界系并投影到 2D。
前后分组渲染:用法线与视线方向的点积判定点属于前景还是背面,然后用不同风格绘制。
结语
在 2D 画布里做一个“像 3D 一样”的行星战斗系统,本质上是数学与图形编程的协作:四元数让旋转顺滑、投影让空间扁平化、遮挡逻辑让画面可信、几何求交让范围可视化不穿帮。