764 lines
26 KiB
C++
764 lines
26 KiB
C++
// skygrid.cpp
|
|
//
|
|
// Celestial longitude/latitude grids.
|
|
//
|
|
// Copyright (C) 2008-2009, the Celestia Development Team
|
|
// Initial version by Chris Laurel, <claurel@gmail.com>
|
|
//
|
|
// This program is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU General Public License
|
|
// as published by the Free Software Foundation; either version 2
|
|
// of the License, or (at your option) any later version.
|
|
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
#include <algorithm>
|
|
#include <celutil/util.h>
|
|
#include <celmath/geomutil.h>
|
|
#include "render.h"
|
|
#include "vecgl.h"
|
|
#include "skygrid.h"
|
|
|
|
using namespace Eigen;
|
|
using namespace std;
|
|
using namespace celmath;
|
|
using namespace celestia;
|
|
|
|
|
|
// #define DEBUG_LABEL_PLACEMENT
|
|
|
|
// The maximum number of parallels or meridians that will be visible
|
|
const double MAX_VISIBLE_ARCS = 10.0;
|
|
|
|
// Number of line segments used to approximate one arc of the celestial sphere
|
|
const int ARC_SUBDIVISIONS = 100;
|
|
|
|
// Size of the cross indicating the north and south poles
|
|
const double POLAR_CROSS_SIZE = 0.01;
|
|
|
|
// Grid line spacing tables
|
|
static const int MSEC = 1;
|
|
static const int SEC = 1000;
|
|
static const int MIN = 60 * SEC;
|
|
static const int DEG = 60 * MIN;
|
|
static const int HR = 60 * MIN;
|
|
|
|
static const int HOUR_MIN_SEC_TOTAL = 24 * HR;
|
|
static const int DEG_MIN_SEC_TOTAL = 180 * DEG;
|
|
|
|
static const int HOUR_MIN_SEC_SPACING[] =
|
|
{
|
|
2*HR,
|
|
1*HR,
|
|
30*MIN,
|
|
15*MIN,
|
|
10*MIN,
|
|
5*MIN,
|
|
3*MIN,
|
|
2*MIN,
|
|
1*MIN,
|
|
30*SEC,
|
|
15*SEC,
|
|
10*SEC,
|
|
5*SEC,
|
|
3*SEC,
|
|
2*SEC,
|
|
1*SEC,
|
|
500*MSEC,
|
|
200*MSEC,
|
|
100*MSEC
|
|
};
|
|
|
|
static const int DEG_MIN_SEC_SPACING[] =
|
|
{
|
|
30*DEG,
|
|
15*DEG,
|
|
10*DEG,
|
|
5*DEG,
|
|
3*DEG,
|
|
2*DEG,
|
|
1*DEG,
|
|
30*MIN,
|
|
15*MIN,
|
|
10*MIN,
|
|
5*MIN,
|
|
3*MIN,
|
|
2*MIN,
|
|
1*MIN,
|
|
30*SEC,
|
|
15*SEC,
|
|
10*SEC,
|
|
5*SEC,
|
|
3*SEC,
|
|
2*SEC,
|
|
1*SEC,
|
|
500*MSEC,
|
|
200*MSEC,
|
|
100*MSEC
|
|
};
|
|
|
|
|
|
// Alternate spacing tables
|
|
#if 0
|
|
// Max step between spacings is 5x; all spacings are
|
|
// integer multiples of subsequent spacings.
|
|
static const int HOUR_MIN_SEC_SPACING[] =
|
|
{
|
|
2*HR, 1*HR, 30*MIN, 10*MIN, 5*MIN,
|
|
1*MIN, 30*SEC, 10*SEC, 5*SEC, 1*SEC,
|
|
500*MSEC, 100*MSEC
|
|
};
|
|
|
|
static const int DEG_MIN_SEC_SPACING[] =
|
|
{
|
|
30*DEG, 10*DEG, 5*DEG, 1*DEG, 30*MIN,
|
|
10*MIN, 5*MIN, 1*MIN, 30*SEC, 10*SEC,
|
|
5*SEC, 1*SEC, 500*MSEC, 100*MSEC
|
|
};
|
|
#endif
|
|
|
|
#if 0
|
|
// Max step between spacings is 3x
|
|
static const int HOUR_MIN_SEC_STEPS[] =
|
|
{
|
|
2*HR, 1*HR, 30*MIN, 10*MIN, 5*MIN, 2*MIN+30*SEC,
|
|
1*MIN, 30*SEC, 10*SEC, 5*SEC, 2*SEC+500*MSEC, 1*SEC,
|
|
500*MSEC, 200*MSEC, 100*MSEC, 50*MSEC, 20*MSEC, 10*MSEC
|
|
};
|
|
|
|
static const int DEG_MIN_SEC_STEPS[] =
|
|
{
|
|
30*DEG, 10*DEG, 5*DEG, 2*DEG+30*MIN, 1*DEG, 30*MIN,
|
|
10*MIN, 5*MIN, 2*MIN+30*SEC, 1*MIN, 30*SEC, 10*SEC,
|
|
5*SEC, 2*SEC+500*MSEC, 1*SEC, 500*MSEC, 200*MSEC, 100*MSEC,
|
|
50*MSEC, 20*MSEC, 10*MSEC
|
|
};
|
|
#endif
|
|
|
|
|
|
static Vector3d
|
|
toStandardCoords(const Vector3d& v)
|
|
{
|
|
return Vector3d(v.x(), -v.z(), v.y());
|
|
}
|
|
|
|
// Compute the difference between two angles in [-PI, PI]
|
|
template<class T> static T
|
|
angleDiff(T a, T b)
|
|
{
|
|
T diff = std::fabs(a - b);
|
|
if (diff > PI)
|
|
return (T) (2.0 * PI - diff);
|
|
else
|
|
return diff;
|
|
}
|
|
|
|
template<class T> static T
|
|
min4(T a, T b, T c, T d)
|
|
{
|
|
return std::min(a, std::min(b, std::min(c, d)));
|
|
}
|
|
|
|
|
|
static void updateAngleRange(double a, double b, double* maxDiff, double* minAngle, double* maxAngle)
|
|
{
|
|
if (angleDiff(a, b) > *maxDiff)
|
|
{
|
|
*maxDiff = angleDiff(a, b);
|
|
*minAngle = a;
|
|
*maxAngle = b;
|
|
}
|
|
}
|
|
|
|
|
|
// Get the horizontal alignment for the coordinate label along the specified frustum plane
|
|
static Renderer::LabelAlignment
|
|
getCoordLabelHAlign(int planeIndex)
|
|
{
|
|
switch (planeIndex)
|
|
{
|
|
case 2:
|
|
return Renderer::AlignLeft;
|
|
case 3:
|
|
return Renderer::AlignRight;
|
|
default:
|
|
return Renderer::AlignCenter;
|
|
}
|
|
}
|
|
|
|
|
|
// Get the vertical alignment for the coordinate label along the specified frustum plane
|
|
static Renderer::LabelVerticalAlignment
|
|
getCoordLabelVAlign(int planeIndex)
|
|
{
|
|
return planeIndex == 1 ? Renderer::VerticalAlignTop : Renderer::VerticalAlignBottom;
|
|
}
|
|
|
|
|
|
// Find the intersection of a circle and the plane with the specified normal and
|
|
// containing the origin. The circle is defined parametrically by:
|
|
// center + cos(t)*u + sin(t)*u
|
|
// u and v are orthogonal vectors with magnitudes equal to the radius of the
|
|
// circle.
|
|
// Return true if there are two solutions.
|
|
template<typename T> static bool planeCircleIntersection(const Matrix<T, 3, 1>& planeNormal,
|
|
const Matrix<T, 3, 1>& center,
|
|
const Matrix<T, 3, 1>& u,
|
|
const Matrix<T, 3, 1>& v,
|
|
Matrix<T, 3, 1>* sol0,
|
|
Matrix<T, 3, 1>* sol1)
|
|
{
|
|
// Any point p on the plane must satisfy p*N = 0. Thus the intersection points
|
|
// satisfy (center + cos(t)U + sin(t)V)*N = 0
|
|
// This simplifies to an equation of the form:
|
|
// a*cos(t)+b*sin(t)+c = 0, with a=N*U, b=N*V, and c=N*center
|
|
T a = u.dot(planeNormal);
|
|
T b = v.dot(planeNormal);
|
|
T c = center.dot(planeNormal);
|
|
|
|
// The solution is +-acos((-ac +- sqrt(a^2+b^2-c^2))/(a^2+b^2))
|
|
T s = a * a + b * b;
|
|
if (s == 0.0)
|
|
{
|
|
// No solution; plane containing circle is parallel to test plane
|
|
return false;
|
|
}
|
|
|
|
if (s - c * c <= 0)
|
|
{
|
|
// One or no solutions; no need to distinguish between these
|
|
// cases for our purposes.
|
|
return false;
|
|
}
|
|
|
|
// No need to actually call acos to get the solution, since we're just
|
|
// going to plug it into sin and cos anyhow.
|
|
T r = b * std::sqrt(s - c * c);
|
|
T cosTheta0 = (-a * c + r) / s;
|
|
T cosTheta1 = (-a * c - r) / s;
|
|
T sinTheta0 = std::sqrt(1 - cosTheta0 * cosTheta0);
|
|
T sinTheta1 = std::sqrt(1 - cosTheta1 * cosTheta1);
|
|
|
|
*sol0 = center + cosTheta0 * u + sinTheta0 * v;
|
|
*sol1 = center + cosTheta1 * u + sinTheta1 * v;
|
|
|
|
// Check that we've chosen a solution that produces a point on the
|
|
// plane. If not, we need to use the -acos solution.
|
|
if (std::abs(sol0->dot(planeNormal)) > 1.0e-8)
|
|
{
|
|
*sol0 = center + cosTheta0 * u - sinTheta0 * v;
|
|
}
|
|
|
|
if (std::abs(sol1->dot(planeNormal)) > 1.0e-8)
|
|
{
|
|
*sol1 = center + cosTheta1 * u - sinTheta1 * v;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// Get the a string with a label for the specified latitude. Both
|
|
// the latitude and latitudeStep are given in milliarcseconds.
|
|
string
|
|
SkyGrid::latitudeLabel(int latitude, int latitudeStep) const
|
|
{
|
|
// Produce a sexigesimal string
|
|
ostringstream out;
|
|
if (latitude < 0)
|
|
out << '-';
|
|
out << std::abs(latitude / DEG) << UTF8_DEGREE_SIGN;
|
|
if (latitudeStep % DEG != 0)
|
|
{
|
|
out << ' ' << setw(2) << setfill('0') << std::abs((latitude / MIN) % 60) << '\'';
|
|
if (latitudeStep % MIN != 0)
|
|
{
|
|
out << ' ' << setw(2) << setfill('0') << std::abs((latitude / SEC) % 60);
|
|
if (latitudeStep % SEC != 0)
|
|
out << '.' << setw(3) << setfill('0') << latitude % SEC;
|
|
out << '"';
|
|
}
|
|
}
|
|
|
|
return out.str();
|
|
}
|
|
|
|
|
|
// Get the a string with a label for the specified longitude. Both
|
|
// the longitude and longitude are given in milliarcseconds.
|
|
string
|
|
SkyGrid::longitudeLabel(int longitude, int longitudeStep) const
|
|
{
|
|
int totalUnits = HOUR_MIN_SEC_TOTAL;
|
|
int baseUnit = HR;
|
|
const char* baseUnitSymbol = "h";
|
|
char minuteSymbol = 'm';
|
|
char secondSymbol = 's';
|
|
|
|
if (m_longitudeUnits == LongitudeDegrees)
|
|
{
|
|
totalUnits = DEG_MIN_SEC_TOTAL * 2;
|
|
baseUnit = DEG;
|
|
baseUnitSymbol = UTF8_DEGREE_SIGN;
|
|
minuteSymbol = '\'';
|
|
secondSymbol = '"';
|
|
}
|
|
|
|
// Produce a sexigesimal string
|
|
ostringstream out;
|
|
if (longitude < 0)
|
|
longitude += totalUnits;
|
|
|
|
// Reverse the labels if the longitude increases clockwise (e.g. for
|
|
// horizontal coordinate grids, where azimuth is defined to increase
|
|
// eastward from due north.
|
|
if (m_longitudeDirection == IncreasingClockwise)
|
|
longitude = (totalUnits - longitude) % totalUnits;
|
|
|
|
out << longitude / baseUnit << baseUnitSymbol;
|
|
if (longitudeStep % baseUnit != 0)
|
|
{
|
|
out << ' ' << setw(2) << setfill('0') << (longitude / MIN) % 60 << minuteSymbol;
|
|
if (longitudeStep % MIN != 0)
|
|
{
|
|
out << ' ' << setw(2) << setfill('0') << (longitude / SEC) % 60;
|
|
if (longitudeStep % SEC != 0)
|
|
out << '.' << setw(3) << setfill('0') << longitude % SEC;
|
|
out << secondSymbol;
|
|
}
|
|
}
|
|
|
|
return out.str();
|
|
}
|
|
|
|
|
|
// Compute the angular step between parallels
|
|
int
|
|
SkyGrid::parallelSpacing(double idealSpacing) const
|
|
{
|
|
// We want to use parallels and meridian spacings that are nice multiples of hours, degrees,
|
|
// minutes, or seconds. Choose spacings from a table. We take the table entry that gives
|
|
// the spacing closest to but not less than the ideal spacing.
|
|
int spacing = DEG_MIN_SEC_TOTAL;
|
|
|
|
// Scan the tables to find the best spacings
|
|
unsigned int tableSize = sizeof(DEG_MIN_SEC_SPACING) / sizeof(DEG_MIN_SEC_SPACING[0]);
|
|
for (unsigned int i = 0; i < tableSize; i++)
|
|
{
|
|
if (PI * (double) DEG_MIN_SEC_SPACING[i] / (double) DEG_MIN_SEC_TOTAL < idealSpacing)
|
|
break;
|
|
spacing = DEG_MIN_SEC_SPACING[i];
|
|
}
|
|
|
|
return spacing;
|
|
}
|
|
|
|
|
|
// Compute the angular step between meridians
|
|
int
|
|
SkyGrid::meridianSpacing(double idealSpacing) const
|
|
{
|
|
const int* spacingTable = HOUR_MIN_SEC_SPACING;
|
|
unsigned int tableSize = sizeof(HOUR_MIN_SEC_SPACING) / sizeof(HOUR_MIN_SEC_SPACING[0]);
|
|
int totalUnits = HOUR_MIN_SEC_TOTAL;
|
|
|
|
// Use degree spacings if the latitude units are degrees instead of hours
|
|
if (m_longitudeUnits == LongitudeDegrees)
|
|
{
|
|
spacingTable = DEG_MIN_SEC_SPACING;
|
|
tableSize = sizeof(DEG_MIN_SEC_SPACING) / sizeof(DEG_MIN_SEC_SPACING[0]);
|
|
totalUnits = DEG_MIN_SEC_TOTAL * 2;
|
|
}
|
|
|
|
int spacing = totalUnits;
|
|
|
|
for (unsigned int i = 0; i < tableSize; i++)
|
|
{
|
|
if (2 * PI * (double) spacingTable[i] / (double) totalUnits < idealSpacing)
|
|
break;
|
|
spacing = spacingTable[i];
|
|
}
|
|
|
|
return spacing;
|
|
}
|
|
|
|
|
|
void
|
|
SkyGrid::render(Renderer& renderer,
|
|
const Observer& observer,
|
|
int windowWidth,
|
|
int windowHeight)
|
|
{
|
|
ShaderProperties shadprop;
|
|
shadprop.texUsage = ShaderProperties::VertexColors | ShaderProperties::LineAsTriangles;
|
|
shadprop.lightModel = ShaderProperties::UnlitModel;
|
|
auto *prog = renderer.getShaderManager().getShader(shadprop);
|
|
if (prog == nullptr)
|
|
return;
|
|
|
|
// 90 degree rotation about the x-axis used to transform coordinates
|
|
// to Celestia's system.
|
|
Quaterniond xrot90 = 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);
|
|
|
|
auto 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
|
|
Vector3d c0(-w, -h, -1.0);
|
|
Vector3d c1( w, -h, -1.0);
|
|
Vector3d c2(-w, h, -1.0);
|
|
Vector3d c3( w, h, -1.0);
|
|
|
|
Quaterniond cameraOrientation = observer.getOrientation();
|
|
Matrix3d r = (cameraOrientation * xrot90 * m_orientation.conjugate() * xrot90.conjugate()).toRotationMatrix().transpose();
|
|
|
|
// Transform the frustum corners by the camera and grid
|
|
// rotations.
|
|
c0 = toStandardCoords(r * c0);
|
|
c1 = toStandardCoords(r * c1);
|
|
c2 = toStandardCoords(r * c2);
|
|
c3 = toStandardCoords(r * c3);
|
|
|
|
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.
|
|
Vector3d frustumNormal[4];
|
|
frustumNormal[0] = Vector3d( 0, 1, -h);
|
|
frustumNormal[1] = Vector3d( 0, -1, -h);
|
|
frustumNormal[2] = Vector3d( 1, 0, -w);
|
|
frustumNormal[3] = Vector3d(-1, 0, -w);
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
frustumNormal[i] = toStandardCoords(r * frustumNormal[i].normalized());
|
|
}
|
|
|
|
Vector3d viewCenter(-Vector3d::UnitZ());
|
|
viewCenter = toStandardCoords(r * viewCenter);
|
|
|
|
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
|
|
Quaterniond q = xrot90 * m_orientation * xrot90.conjugate();
|
|
Quaternionf orientationf = q.cast<float>();
|
|
|
|
prog->use();
|
|
glVertexAttrib(CelestiaGLProgram::ColorAttributeIndex, m_lineColor);
|
|
|
|
// Radius of sphere is arbitrary, with the constraint that it shouldn't
|
|
// intersect the near or far plane of the view frustum.
|
|
Matrix4f m = renderer.getModelViewMatrix() *
|
|
vecgl::rotate((xrot90 * m_orientation.conjugate() * xrot90.conjugate()).cast<float>()) *
|
|
vecgl::scale(1000.0f);
|
|
prog->setMVPMatrices(renderer.getProjectionMatrix(), m);
|
|
prog->lineWidthX = renderer.getLineWidthX();
|
|
prog->lineWidthY = renderer.getLineWidthY();
|
|
|
|
double arcStep = (maxTheta - minTheta) / (double) ARC_SUBDIVISIONS;
|
|
double theta0 = minTheta;
|
|
|
|
vector<LineStripEnd> buffer;
|
|
buffer.reserve(2 * (ARC_SUBDIVISIONS + 2));
|
|
glEnableVertexAttribArray(CelestiaGLProgram::VertexCoordAttributeIndex);
|
|
glEnableVertexAttribArray(CelestiaGLProgram::NextVCoordAttributeIndex);
|
|
glEnableVertexAttribArray(CelestiaGLProgram::ScaleFactorAttributeIndex);
|
|
glVertexAttribPointer(CelestiaGLProgram::VertexCoordAttributeIndex,
|
|
3, GL_FLOAT, GL_FALSE, sizeof(LineStripEnd), &buffer[0].point);
|
|
glVertexAttribPointer(CelestiaGLProgram::NextVCoordAttributeIndex,
|
|
3, GL_FLOAT, GL_FALSE, sizeof(LineStripEnd), &buffer[2].point);
|
|
glVertexAttribPointer(CelestiaGLProgram::ScaleFactorAttributeIndex,
|
|
1, GL_FLOAT, GL_FALSE, sizeof(LineStripEnd), &buffer[0].scale);
|
|
|
|
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);
|
|
|
|
for (int j = 0; j <= ARC_SUBDIVISIONS + 1; j++)
|
|
{
|
|
double theta = theta0 + j * arcStep;
|
|
auto x = (float) (cosPhi * std::cos(theta));
|
|
auto y = (float) (cosPhi * std::sin(theta));
|
|
auto z = (float) sinPhi;
|
|
Vector3f position = {x, z, -y}; // convert to Celestia coords
|
|
buffer[2 * j] = {position, -0.5f};
|
|
buffer[2 * j + 1] = {position, 0.5f};
|
|
}
|
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 2 * (ARC_SUBDIVISIONS + 1));
|
|
|
|
// Place labels at the intersections of the view frustum planes
|
|
// and the parallels.
|
|
Vector3d center(0.0, 0.0, sinPhi);
|
|
Vector3d axis0(cosPhi, 0.0, 0.0);
|
|
Vector3d axis1(0.0, cosPhi, 0.0);
|
|
for (int k = 0; k < 4; k += 2)
|
|
{
|
|
Vector3d isect0(Vector3d::Zero());
|
|
Vector3d isect1(Vector3d::Zero());
|
|
Renderer::LabelAlignment hAlign = getCoordLabelHAlign(k);
|
|
Renderer::LabelVerticalAlignment vAlign = getCoordLabelVAlign(k);
|
|
|
|
if (planeCircleIntersection(frustumNormal[k], center, axis0, axis1,
|
|
&isect0, &isect1))
|
|
{
|
|
string labelText = latitudeLabel(dec, decIncrement);
|
|
|
|
Vector3f p0((float) isect0.x(), (float) isect0.z(), (float) -isect0.y());
|
|
Vector3f 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);
|
|
glVertex3fv(p0.data());
|
|
glVertex3fv(p1.data());
|
|
glColor(m_lineColor);
|
|
glEnd();
|
|
#endif
|
|
|
|
Matrix3f m = observer.getOrientationf().toRotationMatrix();
|
|
p0 = orientationf.conjugate() * p0;
|
|
p1 = orientationf.conjugate() * p1;
|
|
|
|
if ((m * p0).z() < 0.0)
|
|
{
|
|
renderer.addBackgroundAnnotation(nullptr, labelText, m_labelColor, p0, hAlign, vAlign);
|
|
}
|
|
|
|
if ((m * p1).z() < 0.0)
|
|
{
|
|
renderer.addBackgroundAnnotation(nullptr, 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);
|
|
|
|
for (int j = 0; j <= ARC_SUBDIVISIONS + 1; j++)
|
|
{
|
|
double phi = phi0 + j * arcStep;
|
|
auto x = (float) (cos(phi) * cosTheta);
|
|
auto y = (float) (cos(phi) * sinTheta);
|
|
auto z = (float) sin(phi);
|
|
Vector3f position = {x, z, -y}; // convert to Celestia coords
|
|
buffer[2 * j] = {position, -0.5f};
|
|
buffer[2 * j + 1] = {position, 0.5f};
|
|
}
|
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 2 * (ARC_SUBDIVISIONS + 1));
|
|
|
|
// Place labels at the intersections of the view frustum planes
|
|
// and the meridians.
|
|
Vector3d center(0.0, 0.0, 0.0);
|
|
Vector3d axis0(cosTheta, sinTheta, 0.0);
|
|
Vector3d axis1(0.0, 0.0, 1.0);
|
|
for (int k = 1; k < 4; k += 2)
|
|
{
|
|
Vector3d isect0(0.0, 0.0, 0.0);
|
|
Vector3d 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);
|
|
|
|
Vector3f p0((float) isect0.x(), (float) isect0.z(), (float) -isect0.y());
|
|
Vector3f 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);
|
|
glVertex3fv(p0.data());
|
|
glVertex3fv(p1.data());
|
|
glColor(m_lineColor);
|
|
glEnd();
|
|
#endif
|
|
|
|
Matrix3f m = observer.getOrientationf().toRotationMatrix();
|
|
p0 = orientationf.conjugate() * p0;
|
|
p1 = orientationf.conjugate() * p1;
|
|
|
|
if ((m * p0).z() < 0.0 && axis0.dot(isect0) >= cosMaxMeridianAngle)
|
|
{
|
|
renderer.addBackgroundAnnotation(nullptr, labelText, m_labelColor, p0, hAlign, vAlign);
|
|
}
|
|
|
|
if ((m * p1).z() < 0.0 && axis0.dot(isect1) >= cosMaxMeridianAngle)
|
|
{
|
|
renderer.addBackgroundAnnotation(nullptr, labelText, m_labelColor, p1, hAlign, vAlign);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw crosses indicating the north and south poles
|
|
array<float, 112> lineAsTriangleVertices = {
|
|
-polarCrossSize, 1.0f, 0.0f, polarCrossSize, 1.0f, 0.0f, -0.5,
|
|
-polarCrossSize, 1.0f, 0.0f, polarCrossSize, 1.0f, 0.0f, 0.5,
|
|
|
|
polarCrossSize, 1.0f, 0.0f, -polarCrossSize, 1.0f, 0.0f, -0.5,
|
|
polarCrossSize, 1.0f, 0.0f, -polarCrossSize, 1.0f, 0.0f, 0.5,
|
|
|
|
0.0f, 1.0f, -polarCrossSize, 0.0f, 1.0f, polarCrossSize, -0.5,
|
|
0.0f, 1.0f, -polarCrossSize, 0.0f, 1.0f, polarCrossSize, 0.5,
|
|
|
|
0.0f, 1.0f, polarCrossSize, 0.0f, 1.0f, -polarCrossSize, -0.5,
|
|
0.0f, 1.0f, polarCrossSize, 0.0f, 1.0f, -polarCrossSize, 0.5,
|
|
|
|
-polarCrossSize, -1.0f, 0.0f, polarCrossSize, -1.0f, 0.0f, -0.5,
|
|
-polarCrossSize, -1.0f, 0.0f, polarCrossSize, -1.0f, 0.0f, 0.5,
|
|
|
|
polarCrossSize, -1.0f, 0.0f, -polarCrossSize, -1.0f, 0.0f, -0.5,
|
|
polarCrossSize, -1.0f, 0.0f, -polarCrossSize, -1.0f, 0.0f, 0.5,
|
|
|
|
0.0f, -1.0f, -polarCrossSize, 0.0f, -1.0f, polarCrossSize, -0.5,
|
|
0.0f, -1.0f, -polarCrossSize, 0.0f, -1.0f, polarCrossSize, 0.5,
|
|
|
|
0.0f, -1.0f, polarCrossSize, 0.0f, -1.0f, -polarCrossSize, -0.5,
|
|
0.0f, -1.0f, polarCrossSize, 0.0f, -1.0f, -polarCrossSize, 0.5,
|
|
};
|
|
constexpr array<short, 24> lineAsTriangleIndcies = {
|
|
0, 1, 2, 2, 3, 0,
|
|
4, 5, 6, 6, 7, 4,
|
|
8, 9, 10, 10, 11, 8,
|
|
12, 13, 14, 14, 15, 12
|
|
};
|
|
|
|
glVertexAttribPointer(CelestiaGLProgram::VertexCoordAttributeIndex,
|
|
3, GL_FLOAT, GL_FALSE, sizeof(float) * 7, lineAsTriangleVertices.data());
|
|
glVertexAttribPointer(CelestiaGLProgram::NextVCoordAttributeIndex,
|
|
3, GL_FLOAT, GL_FALSE, sizeof(float) * 7, lineAsTriangleVertices.data() + 3);
|
|
glVertexAttribPointer(CelestiaGLProgram::ScaleFactorAttributeIndex,
|
|
1, GL_FLOAT, GL_FALSE, sizeof(float) * 7, lineAsTriangleVertices.data() + 6);
|
|
glDrawElements(GL_TRIANGLES, lineAsTriangleIndcies.size(), GL_UNSIGNED_SHORT, lineAsTriangleIndcies.data());
|
|
|
|
glDisableVertexAttribArray(CelestiaGLProgram::VertexCoordAttributeIndex);
|
|
glDisableVertexAttribArray(CelestiaGLProgram::NextVCoordAttributeIndex);
|
|
glDisableVertexAttribArray(CelestiaGLProgram::ScaleFactorAttributeIndex);
|
|
}
|