void SkyGrid::render(Renderer& renderer, const Observer& observer, int windowWidth, int windowHeight) { // 90 degree rotation about the x-axis used to transform coordinates // to Celestia's system. Quatd xrot90 = Quatd::xrotation(-PI / 2.0); double vfov = observer.getFOV(); double viewAspectRatio = (double) windowWidth / (double) windowHeight; // Calculate the cosine of half the maximum field of view. We'll use this for // fast testing of marker visibility. The stored field of view is the // vertical field of view; we want the field of view as measured on the // diagonal between viewport corners. double h = tan(vfov / 2); double w = h * viewAspectRatio; double diag = sqrt(1.0 + square(h) + square(h * viewAspectRatio)); double cosHalfFov = 1.0 / diag; double halfFov = acos(cosHalfFov); float polarCrossSize = (float) (POLAR_CROSS_SIZE * halfFov); // We want to avoid drawing more of the grid than we have to. The following code // determines the region of the grid intersected by the view frustum. We're // interested in the minimum and maximum phi and theta of the visible patch // of the celestial sphere. // Find the minimum and maximum theta (longitude) by finding the smallest // longitude range containing all corners of the view frustum. // View frustum corners Vec3d c0(-w, -h, -1.0); Vec3d c1( w, -h, -1.0); Vec3d c2(-w, h, -1.0); Vec3d c3( w, h, -1.0); Quatd cameraOrientation = observer.getOrientation(); Mat3d r = (cameraOrientation * xrot90 * ~m_orientation * ~xrot90).toMatrix3(); // Transform the frustum corners by the camera and grid // rotations. c0 = toStandardCoords(c0 * r); c1 = toStandardCoords(c1 * r); c2 = toStandardCoords(c2 * r); c3 = toStandardCoords(c3 * r); double thetaC0 = atan2(c0.y, c0.x); double thetaC1 = atan2(c1.y, c1.x); double thetaC2 = atan2(c2.y, c2.x); double thetaC3 = atan2(c3.y, c3.x); // Compute the minimum longitude range containing the corners; slightly // tricky because of the wrapping at PI/-PI. double minTheta = thetaC0; double maxTheta = thetaC1; double maxDiff = 0.0; updateAngleRange(thetaC0, thetaC1, &maxDiff, &minTheta, &maxTheta); updateAngleRange(thetaC0, thetaC2, &maxDiff, &minTheta, &maxTheta); updateAngleRange(thetaC0, thetaC3, &maxDiff, &minTheta, &maxTheta); updateAngleRange(thetaC1, thetaC2, &maxDiff, &minTheta, &maxTheta); updateAngleRange(thetaC1, thetaC3, &maxDiff, &minTheta, &maxTheta); updateAngleRange(thetaC2, thetaC3, &maxDiff, &minTheta, &maxTheta); if (std::fabs(maxTheta - minTheta) < PI) { if (minTheta > maxTheta) std::swap(minTheta, maxTheta); } else { if (maxTheta > minTheta) std::swap(minTheta, maxTheta); } maxTheta = minTheta + maxDiff; // Calculate the normals to the view frustum planes; we'll use these to // when computing intersection points with the parallels and meridians of the // grid. Coordinate labels will be drawn at the intersection points. Vec3d frustumNormal[4]; frustumNormal[0] = Vec3d( 0, 1, -h); frustumNormal[1] = Vec3d( 0, -1, -h); frustumNormal[2] = Vec3d( 1, 0, -w); frustumNormal[3] = Vec3d(-1, 0, -w); { for (int i = 0; i < 4; i++) { frustumNormal[i].normalize(); frustumNormal[i] = toStandardCoords(frustumNormal[i] * r); } } Vec3d viewCenter(0.0, 0.0, -1.0); viewCenter = toStandardCoords(viewCenter * r); double centerDec; if (fabs(viewCenter.z) < 1.0) centerDec = std::asin(viewCenter.z); else if (viewCenter.z < 0.0) centerDec = -PI / 2.0; else centerDec = PI / 2.0; double minDec = centerDec - halfFov; double maxDec = centerDec + halfFov; if (maxDec >= PI / 2.0) { // view cone contains north pole maxDec = PI / 2.0; minTheta = -PI; maxTheta = PI; } else if (minDec <= -PI / 2.0) { // view cone contains south pole minDec = -PI / 2.0; minTheta = -PI; maxTheta = PI; } double idealParallelSpacing = 2.0 * halfFov / MAX_VISIBLE_ARCS; double idealMeridianSpacing = idealParallelSpacing; // Adjust the spacing between meridians based on how close the view direction // is to the poles; the density of meridians increases as we approach the pole, // so we want to increase the angular distance between meridians. #if 1 // Choose spacing based on the minimum declination (closest to zero) double minAbsDec = std::min(std::fabs(minDec), std::fabs(maxDec)); if (minDec * maxDec <= 0.0f) // Check if min and max straddle the equator minAbsDec = 0.0f; idealMeridianSpacing /= cos(minAbsDec); #else // Choose spacing based on the maximum declination (closest to pole) double maxAbsDec = std::max(std::fabs(minDec), std::fabs(maxDec)); idealMeridianSpacing /= max(cos(PI / 2.0 - 5.0 * idealParallelSpacing), cos(maxAbsDec)); #endif int totalLongitudeUnits = HOUR_MIN_SEC_TOTAL; if (m_longitudeUnits == LongitudeDegrees) totalLongitudeUnits = DEG_MIN_SEC_TOTAL * 2; int raIncrement = meridianSpacing(idealMeridianSpacing); int decIncrement = parallelSpacing(idealParallelSpacing); int startRa = (int) std::ceil (totalLongitudeUnits * (minTheta / (PI * 2.0f)) / (float) raIncrement) * raIncrement; int endRa = (int) std::floor(totalLongitudeUnits * (maxTheta / (PI * 2.0f)) / (float) raIncrement) * raIncrement; int startDec = (int) std::ceil (DEG_MIN_SEC_TOTAL * (minDec / PI) / (float) decIncrement) * decIncrement; int endDec = (int) std::floor(DEG_MIN_SEC_TOTAL * (maxDec / PI) / (float) decIncrement) * decIncrement; // Get the orientation at single precision Quatd q = xrot90 * m_orientation * ~xrot90; Quatf orientationf((float) q.w, (float) q.x, (float) q.y, (float) q.z); glColor(m_lineColor); // Render the parallels glPushMatrix(); glRotate(xrot90 * ~m_orientation * ~xrot90); // Radius of sphere is arbitrary, with the constraint that it shouldn't // intersect the near or far plane of the view frustum. glScalef(1000.0f, 1000.0f, 1000.0f); double arcStep = (maxTheta - minTheta) / (double) ARC_SUBDIVISIONS; double theta0 = minTheta; for (int dec = startDec; dec <= endDec; dec += decIncrement) { double phi = PI * (double) dec / (double) DEG_MIN_SEC_TOTAL; double cosPhi = cos(phi); double sinPhi = sin(phi); glBegin(GL_LINE_STRIP); for (int j = 0; j <= ARC_SUBDIVISIONS; j++) { double theta = theta0 + j * arcStep; float x = (float) (cosPhi * std::cos(theta)); float y = (float) (cosPhi * std::sin(theta)); float z = (float) sinPhi; glVertex3f(x, z, -y); // convert to Celestia coords } glEnd(); // Place labels at the intersections of the view frustum planes // and the parallels. Vec3d center(0.0, 0.0, sinPhi); Vec3d axis0(cosPhi, 0.0, 0.0); Vec3d axis1(0.0, cosPhi, 0.0); for (int k = 0; k < 4; k += 2) { Vec3d isect0(0.0, 0.0, 0.0); Vec3d isect1(0.0, 0.0, 0.0); Renderer::LabelAlignment hAlign = getCoordLabelHAlign(k); Renderer::LabelVerticalAlignment vAlign = getCoordLabelVAlign(k); if (planeCircleIntersection(frustumNormal[k], center, axis0, axis1, &isect0, &isect1)) { string labelText = latitudeLabel(dec, decIncrement); Point3f p0((float) isect0.x, (float) isect0.z, (float) -isect0.y); Point3f p1((float) isect1.x, (float) isect1.z, (float) -isect1.y); #ifdef DEBUG_LABEL_PLACEMENT glPointSize(5.0); glBegin(GL_POINTS); glColor4f(1.0f, 0.0f, 0.0f, 1.0f); glVertex3f(p0.x, p0.y, p0.z); glVertex3f(p1.x, p1.y, p1.z); glColor(m_lineColor); glEnd(); #endif Mat3f m = conjugate(observer.getOrientationf()).toMatrix3(); p0 = p0 * orientationf.toMatrix3(); p1 = p1 * orientationf.toMatrix3(); if ((p0 * m).z < 0.0) { renderer.addBackgroundAnnotation(NULL, labelText, m_labelColor, p0, hAlign, vAlign); } if ((p1 * m).z < 0.0) { renderer.addBackgroundAnnotation(NULL, labelText, m_labelColor, p1, hAlign, vAlign); } } } } // Draw the meridians // Render meridians only to the last latitude circle; this looks better // than spokes radiating from the pole. double maxMeridianAngle = PI / 2.0 * (1.0 - 2.0 * (double) decIncrement / (double) DEG_MIN_SEC_TOTAL); minDec = std::max(minDec, -maxMeridianAngle); maxDec = std::min(maxDec, maxMeridianAngle); arcStep = (maxDec - minDec) / (double) ARC_SUBDIVISIONS; double phi0 = minDec; double cosMaxMeridianAngle = cos(maxMeridianAngle); for (int ra = startRa; ra <= endRa; ra += raIncrement) { double theta = 2.0 * PI * (double) ra / (double) totalLongitudeUnits; double cosTheta = cos(theta); double sinTheta = sin(theta); glBegin(GL_LINE_STRIP); for (int j = 0; j <= ARC_SUBDIVISIONS; j++) { double phi = phi0 + j * arcStep; float x = (float) (cos(phi) * cosTheta); float y = (float) (cos(phi) * sinTheta); float z = (float) sin(phi); glVertex3f(x, z, -y); // convert to Celestia coords } glEnd(); // Place labels at the intersections of the view frustum planes // and the meridians. Vec3d center(0.0, 0.0, 0.0); Vec3d axis0(cosTheta, sinTheta, 0.0); Vec3d axis1(0.0, 0.0, 1.0); for (int k = 1; k < 4; k += 2) { Vec3d isect0(0.0, 0.0, 0.0); Vec3d isect1(0.0, 0.0, 0.0); Renderer::LabelAlignment hAlign = getCoordLabelHAlign(k); Renderer::LabelVerticalAlignment vAlign = getCoordLabelVAlign(k); if (planeCircleIntersection(frustumNormal[k], center, axis0, axis1, &isect0, &isect1)) { string labelText = longitudeLabel(ra, raIncrement); Point3f p0((float) isect0.x, (float) isect0.z, (float) -isect0.y); Point3f p1((float) isect1.x, (float) isect1.z, (float) -isect1.y); #ifdef DEBUG_LABEL_PLACEMENT glPointSize(5.0); glBegin(GL_POINTS); glColor4f(1.0f, 0.0f, 0.0f, 1.0f); glVertex3f(p0.x, p0.y, p0.z); glVertex3f(p1.x, p1.y, p1.z); glColor(m_lineColor); glEnd(); #endif Mat3f m = conjugate(observer.getOrientationf()).toMatrix3(); p0 = p0 * orientationf.toMatrix3(); p1 = p1 * orientationf.toMatrix3(); if ((p0 * m).z < 0.0 && axis0 * isect0 >= cosMaxMeridianAngle) { renderer.addBackgroundAnnotation(NULL, labelText, m_labelColor, p0, hAlign, vAlign); } if ((p1 * m).z < 0.0 && axis0 * isect1 >= cosMaxMeridianAngle) { renderer.addBackgroundAnnotation(NULL, labelText, m_labelColor, p1, hAlign, vAlign); } } } } // Draw crosses indicating the north and south poles glBegin(GL_LINES); glVertex3f(-polarCrossSize, 1.0f, 0.0f); glVertex3f( polarCrossSize, 1.0f, 0.0f); glVertex3f(0.0f, 1.0f, -polarCrossSize); glVertex3f(0.0f, 1.0f, polarCrossSize); glVertex3f(-polarCrossSize, -1.0f, 0.0f); glVertex3f( polarCrossSize, -1.0f, 0.0f); glVertex3f(0.0f, -1.0f, -polarCrossSize); glVertex3f(0.0f, -1.0f, polarCrossSize); glEnd(); glPopMatrix(); }