// render.cpp // // Copyright (C) 2001-2007, Chris Laurel // // 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 #include #include #include #ifndef _WIN32 #ifndef TARGET_OS_MAC #include #endif #endif /* _WIN32 */ #define REFMARKS 1 #include #include #include #include #include #include #include "gl.h" #include "astro.h" #include "glext.h" #include "vecgl.h" #include "glshader.h" #include "shadermanager.h" #include "spheremesh.h" #include "lodspheremesh.h" #include "model.h" #include "regcombine.h" #include "vertexprog.h" #include "texmanager.h" #include "meshmanager.h" #include "render.h" #include "renderinfo.h" #include "renderglsl.h" #if REFMARKS #include "axisarrow.h" #endif using namespace std; #define FOV 45.0f #define NEAR_DIST 0.5f #define FAR_DIST 1.0e9f // This should be in the GL headers, but where? #ifndef GL_COLOR_SUM_EXT #define GL_COLOR_SUM_EXT 0x8458 #endif static const float STAR_DISTANCE_LIMIT = 1.0e6f; static const int REF_DISTANCE_TO_SCREEN = 400; //[mm] // Distance from the Sun at which comet tails will start to fade out static const float COMET_TAIL_ATTEN_DIST_SOL = astro::AUtoKilometers(5.0f); static const int StarVertexListSize = 1024; // Fractional pixel offset used when rendering text as texture mapped // quads to ensure consistent mapping of texels to pixels. static const float PixelOffset = 0.125f; // These two values constrain the near and far planes of the view frustum // when rendering planet and object meshes. The near plane will never be // closer than MinNearPlaneDistance, and the far plane is set so that far/near // will not exceed MaxFarNearRatio. static const float MinNearPlaneDistance = 0.0001f; // km static const float MaxFarNearRatio = 2000000.0f; static const float RenderDistance = 50.0f; // Star disc size in pixels static const float BaseStarDiscSize = 5.0f; static const float MaxScaledDiscStarSize = 8.0f; static const float GlareOpacity = 0.65f; static const float MinRelativeOccluderRadius = 0.005f; static const float CubeCornerToCenterDistance = (float) sqrt(3.0); // The minimum apparent size of an objects orbit in pixels before we display // a label for it. This minimizes label clutter. static const float MinOrbitSizeForLabel = 20.0f; // The minimum apparent size of a surface feature in pixels before we display // a label for it. static const float MinFeatureSizeForLabel = 20.0f; /* The maximum distance of the observer to the origin of coordinates before asterism lines and labels start to linearly fade out (in uLY) */ static const float MaxAsterismLabelsConstDist = 6.0e6f; static const float MaxAsterismLinesConstDist = 6.0e8f; /* The maximum distance of the observer to the origin of coordinates before asterisms labels and lines fade out completely (in uLY) */ static const float MaxAsterismLabelsDist = 2.0e7f; static const float MaxAsterismLinesDist = 6.52e10f; // Maximum size of a solar system in light years. Features beyond this distance // will not necessarily be rendered correctly. This limit is used for // visibility culling of solar systems. static const float MaxSolarSystemSize = 1.0f; // Static meshes and textures used by all instances of Simulation static bool commonDataInitialized = false; LODSphereMesh* g_lodSphere = NULL; static Texture* normalizationTex = NULL; static Texture* starTex = NULL; static Texture* glareTex = NULL; static Texture* shadowTex = NULL; static Texture* gaussianDiscTex = NULL; static Texture* gaussianGlareTex = NULL; // Shadow textures are scaled down slightly to leave some extra blank pixels // near the border. This keeps axis aligned streaks from appearing on hardware // that doesn't support clamp to border color. static const float ShadowTextureScale = 15.0f / 16.0f; static Texture* eclipseShadowTextures[4]; static Texture* shadowMaskTexture = NULL; static Texture* penumbraFunctionTexture = NULL; Texture* rectToSphericalTexture = NULL; static const Color compassColor(0.4f, 0.4f, 1.0f); static const float CoronaHeight = 0.2f; static bool buggyVertexProgramEmulation = true; // Controls for the number of circles displayed on celestial coordinate spheres static const unsigned int CoordSphereRADivisions = 24; static const unsigned int CoordSphereDecDivisions = 18; // Celestial grid labels static const float RALabelSpacing = 1.0f; // hours between each RA label static const float DecLabelSpacing = 10.0f; // degrees between each declination labels static const float DecLabelRASpacing = 6.0f; // hours between meridians with declination labels static const unsigned int RALabelCount = (unsigned int) (24.0f / RALabelSpacing); static const unsigned int DecLabelCount = (unsigned int) ((int) (90.0f / DecLabelSpacing) - 1) * 2 + 1; static string* RACoordLabels; static string* DecCoordLabels; static const int MaxSkyRings = 32; static const int MaxSkySlices = 180; static const int MinSkySlices = 30; // Size at which the orbit cache will be flushed of old orbit paths static const unsigned int OrbitCacheCullThreshold = 200; // Age in frames at which unused orbit paths may be eliminated from the cache static const uint32 OrbitCacheRetireAge = 16; Color Renderer::StarLabelColor (0.471f, 0.356f, 0.682f); Color Renderer::PlanetLabelColor (0.407f, 0.333f, 0.964f); Color Renderer::MoonLabelColor (0.231f, 0.733f, 0.792f); Color Renderer::AsteroidLabelColor (0.596f, 0.305f, 0.164f); Color Renderer::CometLabelColor (0.768f, 0.607f, 0.227f); Color Renderer::SpacecraftLabelColor (0.93f, 0.93f, 0.93f); Color Renderer::LocationLabelColor (0.24f, 0.89f, 0.43f); Color Renderer::GalaxyLabelColor (0.0f, 0.45f, 0.5f); Color Renderer::NebulaLabelColor (0.541f, 0.764f, 0.278f); Color Renderer::OpenClusterLabelColor (0.239f, 0.572f, 0.396f); Color Renderer::ConstellationLabelColor (0.225f, 0.301f, 0.36f); Color Renderer::EquatorialGridLabelColor(0.32f, 0.44f, 0.36f); Color Renderer::StarOrbitColor (0.5f, 0.5f, 0.8f); Color Renderer::PlanetOrbitColor (0.3f, 0.323f, 0.833f); Color Renderer::MoonOrbitColor (0.08f, 0.407f, 0.392f); Color Renderer::AsteroidOrbitColor (0.58f, 0.152f, 0.08f); Color Renderer::CometOrbitColor (0.639f, 0.487f, 0.168f); Color Renderer::SpacecraftOrbitColor (0.4f, 0.4f, 0.4f); Color Renderer::SelectionOrbitColor (1.0f, 0.0f, 0.0f); Color Renderer::ConstellationColor (0.0f, 0.24f, 0.36f); Color Renderer::BoundaryColor (0.24f, 0.10f, 0.12f); Color Renderer::EquatorialGridColor (0.19f, 0.25f, 0.19f); // Some useful unit conversions inline float mmToInches(float mm) { return mm * (1.0f / 25.4f); } inline float inchesToMm(float in) { return in * 25.4f; } // Fade function for objects that shouldn't be shown when they're too small // on screen such as orbit paths and some object labels. The will fade linearly // from invisible at minSize pixels to full visibility at opaqueScale*minSize. inline float sizeFade(float screenSize, float minScreenSize, float opaqueScale) { return min(1.0f, (screenSize - minScreenSize) / (minScreenSize * (opaqueScale - 1))); } Renderer::Renderer() : context(0), windowWidth(0), windowHeight(0), fov(FOV), screenDpi(96), corrFac(1.12f), faintestAutoMag45deg(7.0f), renderMode(GL_FILL), labelMode(NoLabels), renderFlags(ShowStars | ShowPlanets), orbitMask(Body::Planet | Body::Moon | Body::Stellar), ambientLightLevel(0.1f), fragmentShaderEnabled(false), vertexShaderEnabled(false), brightnessBias(0.0f), saturationMagNight(1.0f), saturationMag(1.0f), starStyle(FuzzyPointStars), starVertexBuffer(NULL), pointStarVertexBuffer(NULL), glareVertexBuffer(NULL), useVertexPrograms(false), useRescaleNormal(false), usePointSprite(false), textureResolution(medres), useNewStarRendering(false), frameCount(0), lastOrbitCacheFlush(0), minOrbitSize(MinOrbitSizeForLabel), distanceLimit(1.0e6f), minFeatureSize(MinFeatureSizeForLabel), locationFilter(~0u), colorTemp(NULL), videoSync(false), settingsChanged(true) { starVertexBuffer = new StarVertexBuffer(2048); pointStarVertexBuffer = new PointStarVertexBuffer(2048); glareVertexBuffer = new PointStarVertexBuffer(2048); skyVertices = new SkyVertex[MaxSkySlices * (MaxSkyRings + 1)]; skyIndices = new uint32[(MaxSkySlices + 1) * 2 * MaxSkyRings]; skyContour = new SkyContourPoint[MaxSkySlices + 1]; colorTemp = GetStarColorTable(ColorTable_Enhanced); for (int i = 0; i < (int) FontCount; i++) { font[i] = NULL; } } Renderer::~Renderer() { if (starVertexBuffer != NULL) delete starVertexBuffer; if (pointStarVertexBuffer != NULL) delete pointStarVertexBuffer; delete[] skyVertices; delete[] skyIndices; delete[] skyContour; } Renderer::DetailOptions::DetailOptions() : ringSystemSections(100), orbitPathSamplePoints(100), shadowTextureSize(256), eclipseTextureSize(128) { } static void StarTextureEval(float u, float v, float, unsigned char *pixel) { float r = 1 - (float) sqrt(u * u + v * v); if (r < 0) r = 0; else if (r < 0.5f) r = 2.0f * r; else r = 1; int pixVal = (int) (r * 255.99f); pixel[0] = pixVal; pixel[1] = pixVal; pixel[2] = pixVal; } static void GlareTextureEval(float u, float v, float, unsigned char *pixel) { float r = 0.9f - (float) sqrt(u * u + v * v); if (r < 0) r = 0; int pixVal = (int) (r * 255.99f); pixel[0] = 65; pixel[1] = 64; pixel[2] = 65; pixel[3] = pixVal; } static void ShadowTextureEval(float u, float v, float, unsigned char *pixel) { float r = (float) sqrt(u * u + v * v); // Leave some white pixels around the edges to the shadow doesn't // 'leak'. We'll also set the maximum mip map level for this texture to 3 // so we don't have problems with the edge texels at high mip map levels. int pixVal = r < 15.0f / 16.0f ? 0 : 255; pixel[0] = pixVal; pixel[1] = pixVal; pixel[2] = pixVal; } //! Lookup function for eclipse penumbras--the input is the amount of overlap // between the occluder and sun disc, and the output is the fraction of // full brightness. static void PenumbraFunctionEval(float u, float, float, unsigned char *pixel) { u = (u + 1.0f) * 0.5f; // Using the cube root produces a good visual result unsigned char pixVal = (unsigned char) (::pow((double) u, 0.33) * 255.99); pixel[0] = pixVal; } // ShadowTextureFunction is a function object for creating shadow textures // used for rendering eclipses. class ShadowTextureFunction : public TexelFunctionObject { public: ShadowTextureFunction(float _umbra) : umbra(_umbra) {}; virtual void operator()(float u, float v, float w, unsigned char* pixel); float umbra; }; void ShadowTextureFunction::operator()(float u, float v, float, unsigned char* pixel) { float r = (float) sqrt(u * u + v * v); int pixVal = 255; // Leave some white pixels around the edges to the shadow doesn't // 'leak'. We'll also set the maximum mip map level for this texture to 3 // so we don't have problems with the edge texels at high mip map levels. r = r / (15.0f / 16.0f); if (r < 1) { // The pixel value should depend on the area of the sun which is // occluded. We just fudge it here and use the square root of the // radius. if (r <= umbra) pixVal = 0; else pixVal = (int) (sqrt((r - umbra) / (1 - umbra)) * 255.99f); } pixel[0] = pixVal; pixel[1] = pixVal; pixel[2] = pixVal; }; class ShadowMaskTextureFunction : public TexelFunctionObject { public: ShadowMaskTextureFunction() {}; virtual void operator()(float u, float v, float w, unsigned char* pixel); float dummy; }; void ShadowMaskTextureFunction::operator()(float u, float, float, unsigned char* pixel) { unsigned char a = u > 0.0f ? 255 : 0; pixel[0] = a; pixel[1] = a; pixel[2] = a; pixel[3] = a; } static void IllumMapEval(float x, float y, float z, unsigned char* pixel) { Vec3f v(x, y, z); pixel[0] = 128 + (int) (127 * v.x); pixel[1] = 128 + (int) (127 * v.y); pixel[2] = 128 + (int) (127 * v.z); } #if 0 // Not used yet. // The RectToSpherical map converts XYZ coordinates to UV coordinates // via a cube map lookup. However, a lot of GPUs don't support linear // interpolation of textures with > 8 bits per component, which is // inadequate precision for storing texture coordinates. To work around // this, we'll store the u and v texture coordinates with two 8 bit // coordinates each: rg for u, ba for v. The coordinates are unpacked // as: u = r * 255/256 + g * 1/255 // v = b * 255/256 + a * 1/255 // This gives an effective precision of 16 bits for each texture coordinate. static void RectToSphericalMapEval(float x, float y, float z, unsigned char* pixel) { // Compute spherical coodinates (r is always 1) double phi = asin(y); double theta = atan2(z, -x); // Convert to texture coordinates double u = (theta / PI + 1.0) * 0.5; double v = (-phi / PI + 0.5); // Pack texture coordinates in red/green and blue/alpha // u = red + green/256 // v = blue* + alpha/256 uint16 rg = (uint16) (u * 65535.99); uint16 ba = (uint16) (v * 65535.99); pixel[0] = rg >> 8; pixel[1] = rg & 0xff; pixel[2] = ba >> 8; pixel[3] = ba & 0xff; } #endif static void BuildGaussianDiscMipLevel(unsigned char* mipPixels, unsigned int log2size, float fwhm, float power) { unsigned int size = 1 << log2size; float sigma = fwhm / 2.3548f; float isig2 = 1.0f / (2.0f * sigma * sigma); float s = 1.0f / (sigma * (float) sqrt(2.0 * PI)); for (unsigned int i = 0; i < size; i++) { float y = (float) i - size / 2; for (unsigned int j = 0; j < size; j++) { float x = (float) j - size / 2; float r2 = x * x + y * y; float f = s * (float) exp(-r2 * isig2) * power; mipPixels[i * size + j] = (unsigned char) (255.99f * min(f, 1.0f)); } } } static void BuildGlareMipLevel(unsigned char* mipPixels, unsigned int log2size, float scale, float base) { unsigned int size = 1 << log2size; for (unsigned int i = 0; i < size; i++) { float y = (float) i - size / 2; for (unsigned int j = 0; j < size; j++) { float x = (float) j - size / 2; float r = (float) sqrt(x * x + y * y); float f = (float) pow(base, r * scale); mipPixels[i * size + j] = (unsigned char) (255.99f * min(f, 1.0f)); } } } #if 0 // An alternate glare function, based roughly on results in Spencer, G. et al, // 1995, "Physically-Based Glare Effects for Digital Images" static void BuildGlareMipLevel2(unsigned char* mipPixels, unsigned int log2size, float scale) { unsigned int size = 1 << log2size; for (unsigned int i = 0; i < size; i++) { float y = (float) i - size / 2; for (unsigned int j = 0; j < size; j++) { float x = (float) j - size / 2; float r = (float) sqrt(x * x + y * y); float f = 0.3f / (0.3f + r * r * scale * scale * 100); /* if (i == 0 || j == 0 || i == size - 1 || j == size - 1) f = 1.0f; */ mipPixels[i * size + j] = (unsigned char) (255.99f * min(f, 1.0f)); } } } #endif static Texture* BuildGaussianDiscTexture(unsigned int log2size) { unsigned int size = 1 << log2size; Image* img = new Image(GL_LUMINANCE, size, size, log2size + 1); for (unsigned int mipLevel = 0; mipLevel <= log2size; mipLevel++) { float fwhm = (float) pow(2.0f, (float) (log2size - mipLevel)) * 0.3f; BuildGaussianDiscMipLevel(img->getMipLevel(mipLevel), log2size - mipLevel, fwhm, (float) pow(2.0f, (float) (log2size - mipLevel))); } ImageTexture* texture = new ImageTexture(*img, Texture::BorderClamp, Texture::DefaultMipMaps); texture->setBorderColor(Color(0.0f, 0.0f, 0.0f, 0.0f)); delete img; return texture; } static Texture* BuildGaussianGlareTexture(unsigned int log2size) { unsigned int size = 1 << log2size; Image* img = new Image(GL_LUMINANCE, size, size, log2size + 1); for (unsigned int mipLevel = 0; mipLevel <= log2size; mipLevel++) { /* // Optional gaussian glare float fwhm = (float) pow(2.0f, (float) (log2size - mipLevel)) * 0.15f; float power = (float) pow(2.0f, (float) (log2size - mipLevel)) * 0.15f; BuildGaussianDiscMipLevel(img->getMipLevel(mipLevel), log2size - mipLevel, fwhm, power); */ BuildGlareMipLevel(img->getMipLevel(mipLevel), log2size - mipLevel, 25.0f / (float) pow(2.0f, (float) (log2size - mipLevel)), 0.66f); /* BuildGlareMipLevel2(img->getMipLevel(mipLevel), log2size - mipLevel, 1.0f / (float) pow(2.0f, (float) (log2size - mipLevel))); */ } ImageTexture* texture = new ImageTexture(*img, Texture::BorderClamp, Texture::DefaultMipMaps); texture->setBorderColor(Color(0.0f, 0.0f, 0.0f, 0.0f)); delete img; return texture; } // Depth comparison function for render list entries bool operator<(const RenderListEntry& a, const RenderListEntry& b) { // Operation is reversed because -z axis points into the screen return a.centerZ - a.radius > b.centerZ - b.radius; } // Depth comparison for labels // Note that it's essential to declare this operator as a member // function of Renderer::Label; if it's not a class member, C++'s // argument dependent lookup will not find the operator when it's // used as a predicate for STL algorithms. bool Renderer::Label::operator<(const Label& l) const { // Operation is reversed because -z axis points into the screen return position.z > l.position.z; } // Depth comparison for orbit paths bool Renderer::OrbitPathListEntry::operator<(const Renderer::OrbitPathListEntry& o) const { // Operation is reversed because -z axis points into the screen return centerZ - radius > o.centerZ - o.radius; } bool Renderer::init(GLContext* _context, int winWidth, int winHeight, DetailOptions& _detailOptions) { context = _context; detailOptions = _detailOptions; // Initialize static meshes and textures common to all instances of Renderer if (!commonDataInitialized) { g_lodSphere = new LODSphereMesh(); starTex = CreateProceduralTexture(64, 64, GL_RGB, StarTextureEval); glareTex = LoadTextureFromFile("textures/flare.jpg"); if (glareTex == NULL) glareTex = CreateProceduralTexture(64, 64, GL_RGB, GlareTextureEval); // Max mipmap level doesn't work reliably on all graphics // cards. In particular, Rage 128 and TNT cards resort to software // rendering when this feature is enabled. The only workaround is to // disable mipmapping completely unless texture border clamping is // supported, which solves the problem much more elegantly than all // the mipmap level nonsense. // shadowTex->setMaxMipMapLevel(3); Texture::AddressMode shadowTexAddress = Texture::EdgeClamp; Texture::MipMapMode shadowTexMip = Texture::NoMipMaps; useClampToBorder = context->extensionSupported("GL_ARB_texture_border_clamp"); if (useClampToBorder) { shadowTexAddress = Texture::BorderClamp; shadowTexMip = Texture::DefaultMipMaps; } shadowTex = CreateProceduralTexture(detailOptions.shadowTextureSize, detailOptions.shadowTextureSize, GL_RGB, ShadowTextureEval, shadowTexAddress, shadowTexMip); shadowTex->setBorderColor(Color::White); if (gaussianDiscTex == NULL) gaussianDiscTex = BuildGaussianDiscTexture(8); if (gaussianGlareTex == NULL) gaussianGlareTex = BuildGaussianGlareTexture(9); // Create the eclipse shadow textures { for (int i = 0; i < 4; i++) { ShadowTextureFunction func(i * 0.25f); eclipseShadowTextures[i] = CreateProceduralTexture(detailOptions.eclipseTextureSize, detailOptions.eclipseTextureSize, GL_RGB, func, shadowTexAddress, shadowTexMip); if (eclipseShadowTextures[i] != NULL) { // eclipseShadowTextures[i]->setMaxMipMapLevel(2); eclipseShadowTextures[i]->setBorderColor(Color::White); } } } // Create the shadow mask texture { ShadowMaskTextureFunction func; shadowMaskTexture = CreateProceduralTexture(128, 2, GL_RGBA, func); //shadowMaskTexture->bindName(); } // Create a function lookup table in a texture for use with // fragment program eclipse shadows. penumbraFunctionTexture = CreateProceduralTexture(512, 1, GL_LUMINANCE, PenumbraFunctionEval, Texture::EdgeClamp); if (context->extensionSupported("GL_ARB_texture_cube_map")) { normalizationTex = CreateProceduralCubeMap(64, GL_RGB, IllumMapEval); #if ADVANCED_CLOUD_SHADOWS rectToSphericalTexture = CreateProceduralCubeMap(128, GL_RGBA, RectToSphericalMapEval); #endif } // Create labels for celestial sphere RACoordLabels = new string[RALabelCount]; DecCoordLabels = new string[DecLabelCount]; unsigned int i; for (i = 0; i < RALabelCount; i++) { float ra = i * RALabelSpacing; char buf[32]; int hours = (int) ra; int minutes = (int) ((ra - hours) * 60); sprintf(buf, "%dh %02dm", hours, minutes); RACoordLabels[i] = string(buf); } for (i = 0; i < DecLabelCount; i++) { float dec = ((int) i - (int) DecLabelCount / 2) * DecLabelSpacing; char buf[32]; sprintf(buf, "%g%s", dec, UTF8_DEGREE_SIGN); DecCoordLabels[i] = string(buf); } commonDataInitialized = true; } #if 0 if (context->extensionSupported("GL_ARB_multisample")) { int nSamples = 0; int sampleBuffers = 0; int enabled = (int) glIsEnabled(GL_MULTISAMPLE_ARB); glGetIntegerv(GL_SAMPLE_BUFFERS_ARB, &sampleBuffers); glGetIntegerv(GL_SAMPLES_ARB, &nSamples); clog << "AA samples: " << nSamples << ", enabled=" << (int) enabled << ", sample buffers=" << (sampleBuffers) << "\n"; glEnable(GL_MULTISAMPLE_ARB); } #endif if (context->extensionSupported("GL_EXT_rescale_normal")) { // We need this enabled because we use glScale, but only // with uniform scale factors. DPRINTF(1, "Renderer: EXT_rescale_normal supported.\n"); useRescaleNormal = true; glEnable(GL_RESCALE_NORMAL_EXT); } if (context->extensionSupported("GL_ARB_point_sprite")) { DPRINTF(1, "Renderer: point sprites supported.\n"); usePointSprite = true; } if (context->extensionSupported("GL_EXT_separate_specular_color")) { glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL_EXT, GL_SEPARATE_SPECULAR_COLOR_EXT); } // Ugly renderer-specific bug workarounds follow . . . char* glRenderer = (char*) glGetString(GL_RENDERER); if (glRenderer != NULL) { // Fog is broken with vertex program emulation in most versions of // the GF 1 and 2 drivers; we need to detect this and disable // vertex programs which output fog coordinates if (strstr(glRenderer, "GeForce3") != NULL || strstr(glRenderer, "GeForce4") != NULL) { buggyVertexProgramEmulation = false; } if (strstr(glRenderer, "Savage4") != NULL || strstr(glRenderer, "ProSavage") != NULL) { // S3 Savage4 drivers appear to rescale normals without reporting // EXT_rescale_normal. Lighting will be messed up unless // we set the useRescaleNormal flag. useRescaleNormal = true; } #ifdef TARGET_OS_MAC if (strstr(glRenderer, "Radeon 9600") != NULL || strstr(glRenderer, "Radeon 9700") != NULL || strstr(glRenderer, "Radeon 9800") != NULL || strstr(glRenderer, "Radeon X600") != NULL || strstr(glRenderer, "Radeon X800") != NULL) { // Some ATI drivers on the Mac appear to limit point sprite size. // This causes an abrupt size transition when going from billboards // to sprites. Rather than incur overhead accounting for the size limit, // do not use sprites on these renderers. // Affected cards: 9550-9800, X300, X600, X1050, X850XT etc // Renderer strings are not unique. usePointSprite = false; } #endif } // More ugly hacks; according to Matt Craighead at NVIDIA, an NVIDIA // OpenGL driver that reports version 1.3.1 or greater will have working // fog in emulated vertex programs. char* glVersion = (char*) glGetString(GL_VERSION); if (glVersion != NULL) { int major = 0, minor = 0, extra = 0; int nScanned = sscanf(glVersion, "%d.%d.%d", &major, &minor, &extra); if (nScanned >= 2) { if (major > 1 || minor > 3 || (minor == 3 && extra >= 1)) buggyVertexProgramEmulation = false; } } glLoadIdentity(); glEnable(GL_CULL_FACE); glCullFace(GL_BACK); glEnable(GL_COLOR_MATERIAL); glEnable(GL_LIGHTING); glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE); // LEQUAL rather than LESS required for multipass rendering glDepthFunc(GL_LEQUAL); resize(winWidth, winHeight); return true; } void Renderer::resize(int width, int height) { windowWidth = width; windowHeight = height; // glViewport(windowWidth, windowHeight); } float Renderer::calcPixelSize(float fovY, float windowHeight) { return 2 * (float) tan(degToRad(fovY / 2.0)) / (float) windowHeight; } void Renderer::setFieldOfView(float _fov) { fov = _fov; corrFac = (0.12f * fov/FOV * fov/FOV + 1.0f); } int Renderer::getScreenDpi() const { return screenDpi; } void Renderer::setScreenDpi(int _dpi) { screenDpi = _dpi; } void Renderer::setFaintestAM45deg(float _faintestAutoMag45deg) { faintestAutoMag45deg = _faintestAutoMag45deg; markSettingsChanged(); } float Renderer::getFaintestAM45deg() { return faintestAutoMag45deg; } unsigned int Renderer::getResolution() { return textureResolution; } void Renderer::setResolution(unsigned int resolution) { if (resolution < TEXTURE_RESOLUTION) textureResolution = resolution; //markSettingsChanged(); } TextureFont* Renderer::getFont(FontStyle fs) const { return font[(int) fs]; } void Renderer::setFont(FontStyle fs, TextureFont* txf) { font[(int) fs] = txf; markSettingsChanged(); } void Renderer::setRenderMode(int _renderMode) { renderMode = _renderMode; markSettingsChanged(); } int Renderer::getRenderFlags() const { return renderFlags; } void Renderer::setRenderFlags(int _renderFlags) { renderFlags = _renderFlags; markSettingsChanged(); } int Renderer::getLabelMode() const { return labelMode; } void Renderer::setLabelMode(int _labelMode) { labelMode = _labelMode; markSettingsChanged(); } int Renderer::getOrbitMask() const { return orbitMask; } void Renderer::setOrbitMask(int mask) { orbitMask = mask; markSettingsChanged(); } const ColorTemperatureTable* Renderer::getStarColorTable() const { return colorTemp; } void Renderer::setStarColorTable(const ColorTemperatureTable* ct) { colorTemp = ct; markSettingsChanged(); } bool Renderer::getVideoSync() const { return videoSync; } void Renderer::setVideoSync(bool sync) { videoSync = sync; markSettingsChanged(); } float Renderer::getAmbientLightLevel() const { return ambientLightLevel; } void Renderer::setAmbientLightLevel(float level) { ambientLightLevel = level; markSettingsChanged(); } float Renderer::getMinimumFeatureSize() const { return minFeatureSize; } void Renderer::setMinimumFeatureSize(float pixels) { minFeatureSize = pixels; markSettingsChanged(); } float Renderer::getMinimumOrbitSize() const { return minOrbitSize; } // Orbits and labels are only rendered when the orbit of the object // occupies some minimum number of pixels on screen. void Renderer::setMinimumOrbitSize(float pixels) { minOrbitSize = pixels; markSettingsChanged(); } float Renderer::getDistanceLimit() const { return distanceLimit; } void Renderer::setDistanceLimit(float distanceLimit_) { distanceLimit = distanceLimit_; markSettingsChanged(); } bool Renderer::getFragmentShaderEnabled() const { return fragmentShaderEnabled; } void Renderer::setFragmentShaderEnabled(bool enable) { fragmentShaderEnabled = enable && fragmentShaderSupported(); markSettingsChanged(); } bool Renderer::fragmentShaderSupported() const { return context->bumpMappingSupported(); } bool Renderer::getVertexShaderEnabled() const { return vertexShaderEnabled; } void Renderer::setVertexShaderEnabled(bool enable) { vertexShaderEnabled = enable && vertexShaderSupported(); markSettingsChanged(); } bool Renderer::vertexShaderSupported() const { return useVertexPrograms; } void Renderer::addLabel(const char* text, Color color, const Point3f& pos, float depth) { double winX, winY, winZ; int view[4] = { 0, 0, 0, 0 }; view[0] = -windowWidth / 2; view[1] = -windowHeight / 2; view[2] = windowWidth; view[3] = windowHeight; depth = (float) (pos.x * modelMatrix[2] + pos.y * modelMatrix[6] + pos.z * modelMatrix[10]); if (gluProject(pos.x, pos.y, pos.z, modelMatrix, projMatrix, (const GLint*) view, &winX, &winY, &winZ) != GL_FALSE) { Label l; ReplaceGreekLetterAbbr(l.text, MaxLabelLength, text, strlen(text)); // Might be nice to use abbreviations instead of Greek letters // strncpy(l.text, text, MaxLabelLength); l.text[MaxLabelLength - 1] = '\0'; l.color = color; l.position = Point3f((float) winX, (float) winY, -depth); labels.insert(labels.end(), l); } } void Renderer::addLabel(const string& text, Color color, const Point3f& pos, float depth) { addLabel(text.c_str(), color, pos, depth); } void Renderer::addSortedLabel(const string& text, Color color, const Point3f& pos) { double winX, winY, winZ; int view[4] = { 0, 0, 0, 0 }; view[0] = -windowWidth / 2; view[1] = -windowHeight / 2; view[2] = windowWidth; view[3] = windowHeight; float depth = (float) (pos.x * modelMatrix[2] + pos.y * modelMatrix[6] + pos.z * modelMatrix[10]); if (gluProject(pos.x, pos.y, pos.z, modelMatrix, projMatrix, (const GLint*) view, &winX, &winY, &winZ) != GL_FALSE) { Label l; //l.text = ReplaceGreekLetterAbbr(_(text.c_str())); strncpy(l.text, text.c_str(), MaxLabelLength); l.text[MaxLabelLength - 1] = '\0'; l.color = color; l.position = Point3f((float) winX, (float) winY, -depth); depthSortedLabels.insert(depthSortedLabels.end(), l); } } void Renderer::clearLabels() { labels.clear(); } void Renderer::clearSortedLabels() { depthSortedLabels.clear(); } static void enableSmoothLines() { // glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_LINE_SMOOTH); glLineWidth(1.5f); } static void disableSmoothLines() { // glDisable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE); glDisable(GL_LINE_SMOOTH); glLineWidth(1.0f); } class OrbitSampler : public OrbitSampleProc { public: vector* samples; OrbitSampler(vector* _samples) : samples(_samples) {}; void sample(double t, const Point3d& p) { Renderer::OrbitSample samp; samp.pos = p; samp.t = t; samples->push_back(samp); }; }; void renderOrbitColor(const Body *body, bool selected, float opacity) { Color orbitColor; if (selected) { // Highlight the orbit of the selected object in red orbitColor = Renderer::SelectionOrbitColor; } else if (body != NULL && body->isOrbitColorOverridden()) { orbitColor = body->getOrbitColor(); } else { int classification; if (body != NULL) classification = body->getClassification(); else classification = Body::Stellar; switch (classification) { case Body::Moon: orbitColor = Renderer::MoonOrbitColor; break; case Body::Asteroid: orbitColor = Renderer::AsteroidOrbitColor; break; case Body::Comet: orbitColor = Renderer::CometOrbitColor; break; case Body::Spacecraft: orbitColor = Renderer::SpacecraftOrbitColor; break; case Body::Stellar: orbitColor = Renderer::StarOrbitColor; break; case Body::Planet: default: orbitColor = Renderer::PlanetOrbitColor; break; } } glColor(orbitColor, opacity * orbitColor.alpha()); } // Subdivide the orbit into sections and compute a bounding volume for each section. The bounding // volumes used are capsules, the set of all points less than some constant distance from a line // segment. static void computeOrbitSectionBoundingVolumes(Renderer::CachedOrbit& orbit) { const unsigned int MinOrbitSections = 6; const unsigned int MinSamplesPerSection = 32; // Determine the number of trajectory samples to include in each bounding volume; typically, // the last volume will contain any leftover samples. unsigned int nSections = max(orbit.trajectory.size() / MinSamplesPerSection, MinOrbitSections); unsigned int samplesPerSection = orbit.trajectory.size() / nSections; if (samplesPerSection <= 1) { if (orbit.trajectory.size() == 0) nSections = 0; else nSections = 1; } for (unsigned int i = 0; i < nSections; i++) { unsigned int nSamples; if (i != nSections - 1) nSamples = samplesPerSection; else nSamples = orbit.trajectory.size() - (nSections - 1) * samplesPerSection; Renderer::OrbitSection section; section.firstSample = samplesPerSection * i; unsigned int lastSample = min(orbit.trajectory.size() - 1, section.firstSample + nSamples + 1); // Set the initial axis and origin of the capsule bounding volume; they will be adjusted // to contain all points in the trajectory. The length of the axis may change, but the // direction will remain the same. Vec3d axis = orbit.trajectory[section.firstSample].pos - orbit.trajectory[lastSample].pos; Point3d orig = orbit.trajectory[section.firstSample].pos; double d = 1.0 / (axis * axis); double minT = 0.0; double maxT = 0.0; double maxDistSquared = 0.0; for (unsigned int j = section.firstSample; j <= lastSample; j++) { Point3d p = orbit.trajectory[j].pos; double t = ((p - orig) * axis) * d; Vec3d pointToAxis = p - (orig + axis * t); double distSquared = pointToAxis * pointToAxis; if (t < minT) minT = t; if (t > maxT) maxT = t; if (distSquared > maxDistSquared) maxDistSquared = distSquared; } section.boundingVolume.origin = orig + axis * minT; section.boundingVolume.axis = axis * (maxT - minT); // Make the bounding volume a bit thicker to avoid roundoff problems, and // to account cases when interpolation adds points slightly outside the // volume defined by the sampled points. section.boundingVolume.radius = sqrt(maxDistSquared) * 1.1f;; orbit.sections.push_back(section); } } static Point3d cubicInterpolate(const Point3d& p0, const Vec3d& v0, const Point3d& p1, const Vec3d& v1, double t) { return p0 + (((2.0 * (p0 - p1) + v1 + v0) * (t * t * t)) + ((3.0 * (p1 - p0) - 2.0 * v0 - v1) * (t * t)) + (v0 * t)); } static int splinesRendered = 0; static int orbitsRendered = 0; static int orbitsSkipped = 0; static int sectionsCulled = 0; static Point3d renderOrbitSplineSegment(const Renderer::OrbitSample& p0, const Renderer::OrbitSample& p1, const Renderer::OrbitSample& p2, const Renderer::OrbitSample& p3, double nearZ, double farZ, unsigned int subdivisions, int lastOutcode, bool drawLastSegment) { Vec3d v0 = (p2.pos - p0.pos) * ((p2.t - p1.t) / (p2.t - p0.t)); Vec3d v1 = (p3.pos - p1.pos) * ((p2.t - p1.t) / (p3.t - p1.t)); double dt = 1.0 / (double) subdivisions; if (drawLastSegment) subdivisions++; splinesRendered++; Point3d lastP = p1.pos; for (unsigned int i = 0; i < subdivisions; i++) { Point3d p = cubicInterpolate(p1.pos, v0, p2.pos, v1, i * dt); int outcode = (p.z > nearZ ? 1 : 0) | (p.z < farZ ? 2 : 0); if ((outcode | lastOutcode) == 0) { glVertex3d(p.x, p.y, p.z); } else if ((outcode & lastOutcode) == 0) { // Need to clip Point3d q0 = lastP; Point3d q1 = p; if (lastOutcode != 0) { glBegin(GL_LINE_STRIP); double t; if (lastOutcode == 1) t = (nearZ - lastP.z) / (p.z - lastP.z); else t = (farZ - lastP.z) / (p.z - lastP.z); q0 = lastP + t * (p - lastP); } if (outcode != 0) { double t; if (outcode == 1) t = (nearZ - lastP.z) / (p.z - lastP.z); else t = (farZ - lastP.z) / (p.z - lastP.z); q1 = lastP + t * (p - lastP); } glVertex3d(q0.x, q0.y, q0.z); glVertex3d(q1.x, q1.y, q1.z); if (outcode != 0) { glEnd(); } } lastOutcode = outcode; lastP = p; } return lastP; } #if 0 // Not yet used static Point3d renderOrbitSplineAdaptive(const Renderer::OrbitSample& p0, const Renderer::OrbitSample& p1, const Renderer::OrbitSample& p2, const Renderer::OrbitSample& p3, double nearZ, double farZ, unsigned int minSubdivisions, unsigned int maxSubdivisions, int lastOutcode, bool drawLastSegment) { Vec3d v0 = (p2.pos - p0.pos) * ((p2.t - p1.t) / (p2.t - p0.t)); Vec3d v1 = (p3.pos - p1.pos) * ((p2.t - p1.t) / (p3.t - p1.t)); double minDt = 1.0 / (double) maxSubdivisions; double maxDt = 1.0 / (double) minSubdivisions; double g = (p2.pos - p1.pos).length() * maxSubdivisions; double t = 0.0; splinesRendered += 10000; Point3d lastP = p1.pos; while (t < 1.0) { t += max(minDt, max(lastP.distanceFromOrigin() / g, maxDt)); if (drawLastSegment && t > 1.0) t = 1.0; else break; Point3d p = cubicInterpolate(p1.pos, v0, p2.pos, v1, t); int outcode = (p.z > nearZ ? 1 : 0) | (p.z < farZ ? 2 : 0); if ((outcode | lastOutcode) == 0) { glVertex3d(p.x, p.y, p.z); } else if ((outcode & lastOutcode) == 0) { // Need to clip Point3d q0 = lastP; Point3d q1 = p; if (lastOutcode != 0) { glBegin(GL_LINE_STRIP); double t; if (lastOutcode == 1) t = (nearZ - lastP.z) / (p.z - lastP.z); else t = (farZ - lastP.z) / (p.z - lastP.z); q0 = lastP + t * (p - lastP); } if (outcode != 0) { double t; if (outcode == 1) t = (nearZ - lastP.z) / (p.z - lastP.z); else t = (farZ - lastP.z) / (p.z - lastP.z); q1 = lastP + t * (p - lastP); } glVertex3d(q0.x, q0.y, q0.z); glVertex3d(q1.x, q1.y, q1.z); if (outcode != 0) { glEnd(); } } lastOutcode = outcode; lastP = p; } return lastP; } #endif static Point3d renderOrbitSection(const Orbit& orbit, Renderer::CachedOrbit& cachedOrbit, unsigned int sectionNumber, const Mat4d& modelview, Point3d lastP, int lastOutcode, double nearZ, double farZ, uint32 renderFlags) { vector& trajectory(cachedOrbit.trajectory); unsigned int nPoints = cachedOrbit.trajectory.size(); unsigned int firstPoint = cachedOrbit.sections[sectionNumber].firstSample + 1; unsigned int lastPoint; if (sectionNumber != cachedOrbit.sections.size() - 1) lastPoint = cachedOrbit.sections[sectionNumber + 1].firstSample; else lastPoint = nPoints - 1; double sectionRadius = cachedOrbit.sections[sectionNumber].boundingVolume.radius; double minSmoothZ = -sectionRadius * 8; double maxSmoothZ = nearZ + sectionRadius * 8; for (unsigned int i = firstPoint; i <= lastPoint; i++) { Point3d p = trajectory[i].pos * modelview; int outcode; // This segment of the orbit is very close to the camera and may appear // jagged. We'll add extra segments with cubic spline interpolation. // TODO: This calculation should depend on the field of view as well unsigned int splineSubdivisions = 0; if ((p.z > minSmoothZ || lastP.z > minSmoothZ) && !(p.z > maxSmoothZ && lastP.z > maxSmoothZ)) { double distFromEye = distanceToSegment(Point3d(0.0, 0.0, 0.0), lastP, p - lastP); if (distFromEye < sectionRadius) splineSubdivisions = 64; else if (distFromEye < sectionRadius * 8) splineSubdivisions = (unsigned int) (sectionRadius / distFromEye * 64); } if (splineSubdivisions > 1) { // Render this part of the orbit as a spline instead of a line segment Renderer::OrbitSample s0, s1, s2, s3; if (i > 1) { s0 = trajectory[i - 2]; } else if (orbit.isPeriodic()) { // Careful: use second to last sample, since first sample is duplicate of last s0 = trajectory[nPoints - 2]; s0.t -= orbit.getPeriod(); } else { s0 = trajectory[i - 1]; } if (i < trajectory.size() - 1) { s3 = trajectory[i + 1]; } else if (orbit.isPeriodic()) { // Careful: use second sample, since first sample is duplicate of last s3 = trajectory[1]; s3.t += orbit.getPeriod(); } else { s3 = trajectory[i]; } s1 = Renderer::OrbitSample(lastP, trajectory[i - 1].t); s2 = Renderer::OrbitSample(p, trajectory[i].t); s0.pos = s0.pos * modelview; s3.pos = s3.pos * modelview; bool drawLastSegment = i == nPoints - 1; p = renderOrbitSplineSegment(s0, s1, s2, s3, nearZ, farZ, splineSubdivisions, lastOutcode, drawLastSegment); outcode = (p.z > nearZ ? 1 : 0) | (p.z < farZ ? 2 : 0); } else { // Just draw a line segment // Compute the outcode mask for clipping: // 00 = between near and far // 01 = greater than nearZ (nearZ and farZ are always negative) // 10 = less than farZ // Given two outcodes o1 and o2 of line segment endpoints: // o1 | o2 == 0 means segment lies completely between near and far plans // o2 & o2 != 0 means segment lies completely outside planes // else we have to clip the line. outcode = (p.z > nearZ ? 1 : 0) | (p.z < farZ ? 2 : 0); if ((outcode | lastOutcode) == 0) { // Segment is completely between near and far planes glVertex3d(p.x, p.y, p.z); } else if ((outcode & lastOutcode) == 0) { // Need to clip Point3d q0 = lastP; Point3d q1 = p; // Clip against the enter plane if (lastOutcode != 0) { glBegin(GL_LINE_STRIP); double t; if (lastOutcode == 1) t = (nearZ - lastP.z) / (p.z - lastP.z); else t = (farZ - lastP.z) / (p.z - lastP.z); q0 = lastP + t * (p - lastP); } // Clip against the exit plane if (outcode != 0) { double t; if (outcode == 1) t = (nearZ - lastP.z) / (p.z - lastP.z); else t = (farZ - lastP.z) / (p.z - lastP.z); q1 = lastP + t * (p - lastP); } glVertex3d(q0.x, q0.y, q0.z); glVertex3d(q1.x, q1.y, q1.z); if (outcode != 0) { glEnd(); } } } lastOutcode = outcode; lastP = p; } return lastP; } void Renderer::renderOrbit(const OrbitPathListEntry& orbitPath, double t, const Quatf& cameraOrientationf, const Frustum& frustum, float nearDist, float farDist) { Body* body = orbitPath.body; Quatd cameraOrientation(cameraOrientationf.w, cameraOrientationf.x, cameraOrientationf.y, cameraOrientationf.z); double nearZ = -nearDist; // negate, becase z is into the screen in camera space double farZ = -farDist; // Ugly cast here because orbit cache uses the body pointer as a key Body* cacheKey = body != NULL ? body : reinterpret_cast(const_cast(orbitPath.star)); CachedOrbit* cachedOrbit = NULL; OrbitCache::iterator cached = orbitCache.find(cacheKey); if (cached != orbitCache.end()) { cachedOrbit = cached->second; cachedOrbit->lastUsed = frameCount; } const Orbit* orbit = NULL; if (body != NULL) orbit = body->getOrbit(); else orbit = orbitPath.star->getOrbit(); // If it's not in the cache already if (cachedOrbit == NULL) { double startTime = t; int nSamples = detailOptions.orbitPathSamplePoints; // Adjust the number of samples used for aperiodic orbits--these aren't // true orbits, but are sampled trajectories, generally of spacecraft. // Better control is really needed--some sort of adaptive sampling would // be ideal. if (!orbit->isPeriodic()) { double begin = 0.0, end = 0.0; orbit->getValidRange(begin, end); if (begin != end) { startTime = begin; nSamples = (int) (orbit->getPeriod() * 100.0); nSamples = max(min(nSamples, 1000), 100); } else { // If the orbit is aperiodic and doesn't have a // finite duration, we don't render it. A compromise // would be to pick some time window centered at the // current time, but we'd have to pick some arbitrary // duration. nSamples = 0; } } cachedOrbit = new CachedOrbit(); cachedOrbit->body = cacheKey; cachedOrbit->lastUsed = frameCount; OrbitSampler sampler(&cachedOrbit->trajectory); orbit->sample(startTime, orbit->getPeriod(), nSamples, sampler); // Add an extra sample to close a periodic orbit if (orbit->isPeriodic()) { if (!cachedOrbit->trajectory.empty()) { double lastSampleTime = cachedOrbit->trajectory[0].t + orbit->getPeriod(); cachedOrbit->trajectory.push_back(OrbitSample(cachedOrbit->trajectory[0].pos, lastSampleTime)); } } computeOrbitSectionBoundingVolumes(*cachedOrbit); // If the orbit cache is full, first try and eliminate some old orbits if (orbitCache.size() > OrbitCacheCullThreshold) { // Check for old orbits at most once per frame if (lastOrbitCacheFlush != frameCount) { for (OrbitCache::iterator iter = orbitCache.begin(); iter != orbitCache.end();) { // Tricky code to eliminate a node in the orbit cache without screwing // up the iterator. Should work in all STL implementations. if (frameCount - iter->second->lastUsed > OrbitCacheRetireAge) orbitCache.erase(iter++); else ++iter; } lastOrbitCacheFlush = frameCount; } } orbitCache[cacheKey] = cachedOrbit; } vector* trajectory = &cachedOrbit->trajectory; // The rest of the function isn't designed for empty trajectories if (trajectory->empty()) return; // We perform vertex tranformations on the CPU because double precision is necessary to // render orbits properly. Start by computing the modelview matrix, to transform orbit // vertices into camera space. Mat4d modelview; { Quatd orientation(1.0); if (body != NULL) { if (body->getOrbitFrame() != NULL) { orientation = body->getOrbitFrame()->getOrientation(t); } else if (body->getOrbitBarycenter() != NULL) { orientation = body->getOrbitBarycenter()->getEclipticalToEquatorial(t); } } // Equivalent to: // glRotate(cameraOrientation); // glTranslate(orbitPath.origin); // glRotate(~orientation); modelview = (orientation).toMatrix4() * Mat4d::translation(Point3d(orbitPath.origin.x, orbitPath.origin.y, orbitPath.origin.z)) * (~cameraOrientation).toMatrix4(); } glPushMatrix(); glLoadIdentity(); bool highlight; if (body != NULL) highlight = highlightObject.body() == body; else highlight = highlightObject.star() == orbitPath.star; renderOrbitColor(body, highlight, orbitPath.opacity); if ((renderFlags & ShowPartialTrajectories) == 0 || orbit->isPeriodic()) { // Show the complete trajectory Point3d p; p = (*trajectory)[0].pos * modelview; int outcode = (p.z > nearZ ? 1 : 0) | (p.z < farZ ? 2 : 0); if (outcode == 0) { glBegin(GL_LINE_STRIP); glVertex3d(p.x, p.y, p.z); } Point3d lastP = p; int lastOutcode = outcode; // The trajectory is subdivided into sections that each contain a number of samples. // Process each section in the trajectory, using its precomputed bounding volume to // quickly check for visibility. for (unsigned int i = 0; i < cachedOrbit->sections.size(); i++) { Capsuled& bv = cachedOrbit->sections[i].boundingVolume; Point3d orig = bv.origin * modelview; Vec3d axis = bv.axis * modelview; Capsulef bvf(Point3f((float) orig.x, (float) orig.y, (float) orig.z), Vec3f((float) axis.x, (float) axis.y, (float) axis.z), (float) bv.radius); // TODO: Create a fast path for the case when the bounding volume lies completely // within the frustum and clipping can be ignored. if (frustum.testCapsule(bvf) != Frustum::Outside) { lastP = renderOrbitSection(*orbit, *cachedOrbit, i, modelview, lastP, lastOutcode, nearZ, farZ, renderFlags); lastOutcode = (lastP.z > nearZ ? 1 : 0) | (lastP.z < farZ ? 2 : 0); } else { // The section was culled because it lies completely outside the view frustum, // but we still need to do some work to keep the begin/end state of the line // strip current. We just need to process the final point in the section. unsigned int lastSample; if (i < cachedOrbit->sections.size() - 1) lastSample = cachedOrbit->sections[i + 1].firstSample; else lastSample = cachedOrbit->trajectory.size() - 1; p = (*trajectory)[lastSample].pos * modelview; outcode = (p.z > nearZ ? 1 : 0) | (p.z < farZ ? 2 : 0); if ((outcode | lastOutcode) == 0) { // Segment is completely between near and far planes glVertex3d(p.x, p.y, p.z); } else if ((outcode & lastOutcode) == 0) { // Need to clip Point3d q0 = lastP; Point3d q1 = p; // Clip against the enter plane if (lastOutcode != 0) { glBegin(GL_LINE_STRIP); double t; if (lastOutcode == 1) t = (nearZ - lastP.z) / (p.z - lastP.z); else t = (farZ - lastP.z) / (p.z - lastP.z); q0 = lastP + t * (p - lastP); } // Clip against the exit plane if (outcode != 0) { double t; if (outcode == 1) t = (nearZ - lastP.z) / (p.z - lastP.z); else t = (farZ - lastP.z) / (p.z - lastP.z); q1 = lastP + t * (p - lastP); } glVertex3d(q0.x, q0.y, q0.z); glVertex3d(q1.x, q1.y, q1.z); if (outcode != 0) { glEnd(); } } lastP = p; lastOutcode = outcode; sectionsCulled++; } } if (lastOutcode == 0) { glEnd(); } } else { double endTime = t; bool endTimeReached = false; Point3d p; p = (*trajectory)[0].pos * modelview; int outcode = (p.z > nearZ ? 1 : 0) | (p.z < farZ ? 2 : 0); if (outcode == 0) { glBegin(GL_LINE_STRIP); glVertex3d(p.x, p.y, p.z); } Point3d lastP = p; int lastOutcode = outcode; unsigned int nPoints = trajectory->size(); for (unsigned int i = 1; i < nPoints && !endTimeReached; i++) { if ((*trajectory)[i].t > endTime) { p = orbit->positionAtTime(endTime) * modelview; endTimeReached = true; } else { p = (*trajectory)[i].pos * modelview; } outcode = (p.z > nearZ ? 1 : 0) | (p.z < farZ ? 2 : 0); if ((outcode | lastOutcode) == 0) { glVertex3d(p.x, p.y, p.z); } else if ((outcode & lastOutcode) == 0) { // Need to clip Point3d p0 = lastP; Point3d p1 = p; if (lastOutcode != 0) { glBegin(GL_LINE_STRIP); double t; if (lastOutcode == 1) t = (nearZ - lastP.z) / (p.z - lastP.z); else t = (farZ - lastP.z) / (p.z - lastP.z); p0 = lastP + t * (p - lastP); } if (outcode != 0) { double t; if (outcode == 1) t = (nearZ - lastP.z) / (p.z - lastP.z); else t = (farZ - lastP.z) / (p.z - lastP.z); p1 = lastP + t * (p - lastP); } glVertex3d(p0.x, p0.y, p0.z); glVertex3d(p1.x, p1.y, p1.z); if (outcode != 0) { glEnd(); } } lastOutcode = outcode; lastP = p; } if (lastOutcode == 0) { glEnd(); } } glPopMatrix(); } // Convert a position in the universal coordinate system to astrocentric // coordinates, taking into account possible orbital motion of the star. static Point3d astrocentricPosition(const UniversalCoord& pos, const Star& star, double t) { UniversalCoord starPos = star.getPosition(t); Vec3d v = pos - starPos; return Point3d(astro::microLightYearsToKilometers(v.x), astro::microLightYearsToKilometers(v.y), astro::microLightYearsToKilometers(v.z)); } void Renderer::autoMag(float& faintestMag) { float fieldCorr = 2.0f * FOV/(fov + FOV); faintestMag = (float) (faintestAutoMag45deg * sqrt(fieldCorr)); saturationMag = saturationMagNight * (1.0f + fieldCorr * fieldCorr); } // Set up the light sources for rendering a solar system. The positions of // all nearby stars are converted from universal to solar system coordinates. static void setupLightSources(const vector& nearStars, const Star& sun, double t, vector& lightSources) { UniversalCoord center = sun.getPosition(t); lightSources.clear(); for (vector::const_iterator iter = nearStars.begin(); iter != nearStars.end(); iter++) { if ((*iter)->getVisibility()) { Vec3d v = ((*iter)->getPosition(t) - center) * astro::microLightYearsToKilometers(1.0); LightSource ls; ls.position = Point3d(v.x, v.y, v.z); ls.luminosity = (*iter)->getLuminosity(); ls.radius = (*iter)->getRadius(); // If the star is sufficiently cool, change the light color // from white. Though our sun appears yellow, we still make // it and all hotter stars emit white light, as this is the // 'natural' light to which our eyes are accustomed. We also // assign a slight bluish tint to light from O and B type stars, // though these will almost never have planets for their light // to shine upon. float temp = (*iter)->getTemperature(); if (temp > 30000.0f) ls.color = Color(0.8f, 0.8f, 1.0f); else if (temp > 10000.0f) ls.color = Color(0.9f, 0.9f, 1.0f); else if (temp > 5400.0f) ls.color = Color(1.0f, 1.0f, 1.0f); else if (temp > 3900.0f) ls.color = Color(1.0f, 0.9f, 0.8f); else if (temp > 2000.0f) ls.color = Color(1.0f, 0.7f, 0.7f); else ls.color = Color(1.0f, 0.4f, 0.4f); lightSources.push_back(ls); } } } // Render an item from the render list // TODO: change the way the observer class works so that it is more efficient; // we should only have to recompute the position and attitude in universal // coordinates once per time step. Then, we wouldn't have to resort to passing // the camera orientation in order to avoid extra calculation. void Renderer::renderItem(const RenderListEntry& rle, const Observer& observer, const Quatf& cameraOrientation, float nearPlaneDistance, float farPlaneDistance) { switch (rle.renderableType) { case RenderListEntry::RenderableStar: assert(rle.star != NULL); renderStar(*rle.star, rle.position, rle.distance, rle.appMag, cameraOrientation, observer.getTime(), nearPlaneDistance, farPlaneDistance); break; case RenderListEntry::RenderableBody: assert(rle.body != NULL); renderPlanet(*rle.body, rle.position, rle.distance, rle.appMag, observer, cameraOrientation, *rle.lightSourceList, nearPlaneDistance, farPlaneDistance); break; case RenderListEntry::RenderableCometTail: assert(rle.body != NULL); renderCometTail(*rle.body, rle.position, observer.getTime(), *rle.lightSourceList, rle.discSizeInPixels); break; #if REFMARKS case RenderListEntry::RenderableBodyAxes: renderAxes(*rle.body, rle.position, rle.distance, observer.getTime(), nearPlaneDistance, farPlaneDistance, RenderListEntry::RenderableBodyAxes); break; case RenderListEntry::RenderableFrameAxes: renderAxes(*rle.body, rle.position, rle.distance, observer.getTime(), nearPlaneDistance, farPlaneDistance, RenderListEntry::RenderableFrameAxes); break; case RenderListEntry::RenderableSunDirection: renderSunDirection(*rle.body, rle.position, rle.distance, observer.getTime(), *rle.lightSourceList, nearPlaneDistance, farPlaneDistance); break; case RenderListEntry::RenderableVelocityVector: renderVelocityVector(*rle.body, rle.position, rle.distance, observer.getTime(), nearPlaneDistance, farPlaneDistance); break; #endif default: break; } } void Renderer::render(const Observer& observer, const Universe& universe, float faintestMagNight, const Selection& sel) { // Get the observer's time double now = observer.getTime(); frameCount++; settingsChanged = false; // Compute the size of a pixel setFieldOfView(radToDeg(observer.getFOV())); pixelSize = calcPixelSize(fov, (float) windowHeight); // Set up the projection we'll use for rendering stars. glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(fov, (float) windowWidth / (float) windowHeight, NEAR_DIST, FAR_DIST); // Set the modelview matrix glMatrixMode(GL_MODELVIEW); // Get the displayed surface texture set to use from the observer displayedSurface = observer.getDisplayedSurface(); locationFilter = observer.getLocationFilter(); if (usePointSprite && getGLContext()->getVertexProcessor() != NULL) { useNewStarRendering = true; } else { useNewStarRendering = false; } // Highlight the selected object highlightObject = sel; Quatf cameraOrientation = observer.getOrientationf(); // Set up the camera for star rendering; the units of this phase // are light years. Point3f observerPosLY = (Point3f) observer.getPosition(); observerPosLY.x *= 1e-6f; observerPosLY.y *= 1e-6f; observerPosLY.z *= 1e-6f; glPushMatrix(); glRotate(cameraOrientation); // Get the model matrix *before* translation. We'll use this for // positioning star and planet labels. glGetDoublev(GL_MODELVIEW_MATRIX, modelMatrix); glGetDoublev(GL_PROJECTION_MATRIX, projMatrix); clearLabels(); clearSortedLabels(); // Put all solar system bodies into the render list. Stars close and // large enough to have discernible surface detail are also placed in // renderList. renderList.clear(); orbitPathList.clear(); // See if we want to use AutoMag. if ((renderFlags & ShowAutoMag) != 0) { autoMag(faintestMag); } else { faintestMag = faintestMagNight; saturationMag = saturationMagNight; } faintestPlanetMag = faintestMag; if (renderFlags & ShowPlanets) { nearStars.clear(); universe.getNearStars(observer.getPosition(), 1.0f, nearStars); if (nearStars.size() > lightSourceLists.size()) { unsigned int expandElements = nearStars.size() - lightSourceLists.size(); for (unsigned int i = 0; i < expandElements; i++) { vector* ls = new vector(); lightSourceLists.push_back(ls); } } list* >::iterator lsIter = lightSourceLists.begin(); for (vector::const_iterator iter = nearStars.begin(); iter != nearStars.end(); iter++) { const Star* sun = *iter; SolarSystem* solarSystem = universe.getSolarSystem(sun); if (solarSystem != NULL) { vector* lightSources = *lsIter++; setupLightSources(nearStars, *sun, now, *lightSources); buildRenderLists(*sun, solarSystem->getPlanets(), observer, now, lightSources, (labelMode & (BodyLabelMask)) != 0); } addStarOrbitToRenderList(*sun, observer, now); } starTex->bind(); } Color skyColor(0.0f, 0.0f, 0.0f); // Scan through the render list to see if we're inside a planetary // atmosphere. If so, we need to adjust the sky color as well as the // limiting magnitude of stars (so stars aren't visible in the daytime // on planets with thick atmospheres.) if ((renderFlags & ShowAtmospheres) != 0) { for (vector::iterator iter = renderList.begin(); iter != renderList.end(); iter++) { if (iter->body != NULL && iter->body->getAtmosphere() != NULL) { // Compute the density of the atmosphere, and from that // the amount light scattering. It's complicated by the // possibility that the planet is oblate and a simple distance // to sphere calculation will not suffice. const Atmosphere* atmosphere = iter->body->getAtmosphere(); float radius = iter->body->getRadius(); Vec3f semiAxes = iter->body->getSemiAxes() * (1.0f / radius); Vec3f recipSemiAxes(1.0f / semiAxes.x, 1.0f / semiAxes.y, 1.0f / semiAxes.z); Mat3f A = Mat3f::scaling(recipSemiAxes); Vec3f eyeVec = iter->position - Point3f(0.0f, 0.0f, 0.0f); eyeVec *= (1.0f / radius); // Compute the orientation of the planet before axial rotation Quatd qd = iter->body->getEclipticalToEquatorial(now); Quatf q((float) qd.w, (float) qd.x, (float) qd.y, (float) qd.z); eyeVec = eyeVec * conjugate(q).toMatrix3(); // ellipDist is not the true distance from the surface unless // the planet is spherical. The quantity that we do compute // is the distance to the surface along a line from the eye // position to the center of the ellipsoid. float ellipDist = (float) sqrt((eyeVec * A) * (eyeVec * A)) - 1.0f; if (ellipDist < atmosphere->height / radius && atmosphere->height > 0.0f) { float density = 1.0f - ellipDist / (atmosphere->height / radius); if (density > 1.0f) density = 1.0f; Vec3f sunDir = iter->sun; Vec3f normal = Point3f(0.0f, 0.0f, 0.0f) - iter->position; sunDir.normalize(); normal.normalize(); float illumination = Math::clamp((sunDir * normal) + 0.2f); float lightness = illumination * density; faintestMag = faintestMag - 15.0f * lightness; saturationMag = saturationMag - 15.0f * lightness; } } } } // Now we need to determine how to scale the brightness of stars. The // brightness will be proportional to the apparent magnitude, i.e. // a logarithmic function of the stars apparent brightness. This mimics // the response of the human eye. We sort of fudge things here and // maintain a minimum range of six magnitudes between faintest visible // and saturation; this keeps stars from popping in or out as the sun // sets or rises. if (faintestMag - saturationMag >= 6.0f) brightnessScale = 1.0f / (faintestMag - saturationMag); else brightnessScale = 0.1667f; ambientColor = Color(ambientLightLevel, ambientLightLevel, ambientLightLevel); // Create the ambient light source. For realistic scenes in space, this // should be black. glAmbientLightColor(ambientColor); glClearColor(skyColor.red(), skyColor.green(), skyColor.blue(), 1); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); glDisable(GL_LIGHTING); glDepthMask(GL_FALSE); glEnable(GL_BLEND); glEnable(GL_TEXTURE_2D); if ((renderFlags & ShowCelestialSphere) != 0) { glColor(EquatorialGridColor); glDisable(GL_TEXTURE_2D); if ((renderFlags & ShowSmoothLines) != 0) enableSmoothLines(); renderCelestialSphere(observer); if ((renderFlags & ShowSmoothLines) != 0) disableSmoothLines(); glEnable(GL_BLEND); glEnable(GL_TEXTURE_2D); } if ((renderFlags & (ShowGalaxies | ShowNebulae | ShowOpenClusters)) != 0 && universe.getDSOCatalog() != NULL) { renderDeepSkyObjects(universe, observer, faintestMag); } // Translate the camera before rendering the stars glPushMatrix(); glTranslatef(-observerPosLY.x, -observerPosLY.y, -observerPosLY.z); // Render stars glBlendFunc(GL_SRC_ALPHA, GL_ONE); if ((renderFlags & ShowStars) != 0 && universe.getStarCatalog() != NULL) { if (useNewStarRendering) renderPointStars(*universe.getStarCatalog(), faintestMag, observer); else renderStars(*universe.getStarCatalog(), faintestMag, observer); } // Render asterisms if ((renderFlags & ShowDiagrams) != 0 && universe.getAsterisms() != NULL) { /* We'll linearly fade the lines as a function of the observer's distance to the origin of coordinates: */ float opacity = 1.0f; float dist = observerPosLY.distanceFromOrigin() * 1e6f; if (dist > MaxAsterismLinesConstDist) { opacity = clamp((MaxAsterismLinesConstDist - dist) / (MaxAsterismLinesDist - MaxAsterismLinesConstDist) + 1); } glColor(ConstellationColor, opacity); glDisable(GL_TEXTURE_2D); if ((renderFlags & ShowSmoothLines) != 0) enableSmoothLines(); AsterismList* asterisms = universe.getAsterisms(); for (AsterismList::const_iterator iter = asterisms->begin(); iter != asterisms->end(); iter++) { Asterism* ast = *iter; for (int i = 0; i < ast->getChainCount(); i++) { const Asterism::Chain& chain = ast->getChain(i); glBegin(GL_LINE_STRIP); for (Asterism::Chain::const_iterator iter = chain.begin(); iter != chain.end(); iter++) glVertex(*iter); glEnd(); } } if ((renderFlags & ShowSmoothLines) != 0) disableSmoothLines(); } if ((renderFlags & ShowBoundaries) != 0) { /* We'll linearly fade the boundaries as a function of the observer's distance to the origin of coordinates: */ float opacity = 1.0f; float dist = observerPosLY.distanceFromOrigin() * 1e6f; if (dist > MaxAsterismLabelsConstDist) { opacity = clamp((MaxAsterismLabelsConstDist - dist) / (MaxAsterismLabelsDist - MaxAsterismLabelsConstDist) + 1); } glColor(BoundaryColor, opacity); glDisable(GL_TEXTURE_2D); if ((renderFlags & ShowSmoothLines) != 0) enableSmoothLines(); if (universe.getBoundaries() != NULL) universe.getBoundaries()->render(); if ((renderFlags & ShowSmoothLines) != 0) disableSmoothLines(); } renderLabels(FontNormal, AlignLeft); clearLabels(); if ((labelMode & ConstellationLabels) != 0 && universe.getAsterisms() != NULL) { labelConstellations(*universe.getAsterisms(), observer); renderLabels(FontLarge, AlignCenter); clearLabels(); } glPopMatrix(); renderLabels(FontNormal, AlignLeft); glPolygonMode(GL_FRONT, (GLenum) renderMode); glPolygonMode(GL_BACK, (GLenum) renderMode); { Frustum frustum(degToRad(fov), (float) windowWidth / (float) windowHeight, MinNearPlaneDistance); Mat3f viewMat = conjugate(observer.getOrientationf()).toMatrix3(); // Remove objects from the render list that lie completely outside the // view frustum. vector::iterator notCulled = renderList.begin(); for (vector::iterator iter = renderList.begin(); iter != renderList.end(); iter++) { Point3f center = iter->position * viewMat; bool convex = true; float radius = 1.0f; float cullRadius = 1.0f; float cloudHeight = 0.0f; switch (iter->renderableType) { case RenderListEntry::RenderableStar: radius = iter->star->getRadius(); cullRadius = radius * (1.0f + CoronaHeight); break; case RenderListEntry::RenderableCometTail: radius = iter->radius; cullRadius = radius; convex = false; break; #if REFMARKS case RenderListEntry::RenderableBodyAxes: case RenderListEntry::RenderableFrameAxes: radius = iter->radius; cullRadius = radius; convex = false; break; #endif case RenderListEntry::RenderableBody: default: radius = iter->body->getBoundingRadius(); if (iter->body->getRings() != NULL) { radius = iter->body->getRings()->outerRadius; convex = false; } if (iter->body->getModel() != InvalidResource) convex = false; cullRadius = radius; if (iter->body->getAtmosphere() != NULL) { cullRadius += iter->body->getAtmosphere()->height; cloudHeight = max(iter->body->getAtmosphere()->cloudHeight, iter->body->getAtmosphere()->mieScaleHeight * (float) -log(AtmosphereExtinctionThreshold)); } break; } // Test the object's bounding sphere against the view frustum if (frustum.testSphere(center, cullRadius) != Frustum::Outside) { float nearZ = center.distanceFromOrigin() - radius; float maxSpan = (float) sqrt(square((float) windowWidth) + square((float) windowHeight)); nearZ = -nearZ * (float) cos(degToRad(fov / 2)) * ((float) windowHeight / maxSpan); if (nearZ > -MinNearPlaneDistance) iter->nearZ = -max(MinNearPlaneDistance, radius / 2000.0f); else iter->nearZ = nearZ; if (!convex) { iter->farZ = center.z - radius; if (iter->farZ / iter->nearZ > MaxFarNearRatio * 0.5f) iter->nearZ = iter->farZ / (MaxFarNearRatio * 0.5f); } else { // Make the far plane as close as possible float d = center.distanceFromOrigin(); // Account for ellipsoidal objects float eradius = radius; if (iter->body != NULL) { Vec3f semiAxes = iter->body->getSemiAxes(); float minSemiAxis = min(semiAxes.x, min(semiAxes.y, semiAxes.z)); eradius *= minSemiAxis / radius; } if (d > eradius) { iter->farZ = iter->centerZ - iter->radius; } else { // We're inside the bounding sphere (and, if the planet // is spherical, inside the planet.) iter->farZ = iter->nearZ * 2.0f; } if (cloudHeight > 0.0f) { // If there's a cloud layer, we need to move the // far plane out so that the clouds aren't clipped float cloudLayerRadius = eradius + cloudHeight; iter->farZ -= (float) sqrt(square(cloudLayerRadius) - square(eradius)); } } *notCulled = *iter; notCulled++; } } renderList.resize(notCulled - renderList.begin()); // The calls to buildRenderLists/renderStars filled renderList // with visible bodies. Sort it front to back, then // render each entry in reverse order (TODO: convenient, but not // ideal for performance; should render opaque objects front to // back, then translucent objects back to front. However, the // amount of overdraw in Celestia is typically low.) sort(renderList.begin(), renderList.end()); // Sort the labels sort(depthSortedLabels.begin(), depthSortedLabels.end()); // Sort the orbit paths sort(orbitPathList.begin(), orbitPathList.end()); int nEntries = renderList.size(); #define DEBUG_COALESCE 0 // Since we're rendering objects of a huge range of sizes spread over // vast distances, we can't just rely on the hardware depth buffer to // handle hidden surface removal without a little help. We'll partition // the depth buffer into spans that can be rendered without running // into terrible depth buffer precision problems. Typically, each body // with an apparent size greater than one pixel is allocated its own // depth buffer interval. However, this will not correctly handle // overlapping objects. If two objects overlap in depth, we must // assign them to the same interval. depthPartitions.clear(); int nIntervals = 0; float prevNear = -1e12f; // ~ 1 light year if (nEntries > 0) prevNear = renderList[nEntries - 1].farZ * 1.01f; int i; // Completely partition the depth buffer. Scan from back to front // through all the renderable items that passed the culling test. for (i = nEntries - 1; i >= 0; i--) { // Only consider renderables that will occupy more than one pixel. if (renderList[i].discSizeInPixels > 1) { if (nIntervals == 0 || renderList[i].farZ >= depthPartitions[nIntervals - 1].nearZ) { // This object spans a depth interval that's disjoint with // the current interval, so create a new one for it, and // another interval to fill the gap between the last // interval. DepthBufferPartition partition; partition.index = nIntervals; partition.nearZ = renderList[i].farZ; partition.farZ = prevNear; // Omit null intervals // TODO: Is this necessary? Shouldn't the >= test prevent this? if (partition.nearZ != partition.farZ) { depthPartitions.push_back(partition); nIntervals++; } partition.index = nIntervals; partition.nearZ = renderList[i].nearZ; partition.farZ = renderList[i].farZ; depthPartitions.push_back(partition); nIntervals++; prevNear = partition.nearZ; } else { // This object overlaps the current span; expand the // interval so that it completely contains the object. DepthBufferPartition& partition = depthPartitions[nIntervals - 1]; partition.nearZ = max(partition.nearZ, renderList[i].nearZ); partition.farZ = min(partition.farZ, renderList[i].farZ); prevNear = partition.nearZ; } } } // Scan the list of orbit paths and find the closest one. We'll need // adjust the nearest interval to accommodate it. float orbitPathNear = prevNear; for (i = 0; i < (int) orbitPathList.size(); i++) { const OrbitPathListEntry& o = orbitPathList[i]; float minNearDistance = min(-o.radius * 0.0001f, o.centerZ + o.radius); if (minNearDistance > orbitPathNear) orbitPathNear = minNearDistance; } #if DEBUG_COALESCE clog << "nEntries: " << nEntries << ", orbitPathNear: " << orbitPathNear << ", prevNear: " << prevNear << "\n"; #endif // If the nearest orbit path distance wasn't set, nothing should appear // in the frontmost depth buffer interval (so we can set the near plane // of the front interval to whatever we want as long as it's less than // the far plane distance. if (orbitPathNear == prevNear) orbitPathNear = 0.0f; // Add one last interval for the span from 0 to the front of the // nearest object { // TODO: closest object may not be at entry 0, since objects are // sorted by far distance. float closest = orbitPathNear; if (nEntries > 0) { closest = max(closest, renderList[0].nearZ); // Setting a the near plane distance to zero results in unreliable rendering, even // if we don't care about the depth buffer. Compromise and set the near plane // distance to a small fraction of distance to the nearest object. if (closest == 0.0f) { closest = renderList[0].nearZ * 0.01f; } } DepthBufferPartition partition; partition.index = nIntervals; partition.nearZ = closest; partition.farZ = prevNear; depthPartitions.push_back(partition); nIntervals++; } // If orbits are enabled, adjust the farthest partition so that it // can contain the orbit. if (!orbitPathList.empty()) { depthPartitions[0].farZ = min(depthPartitions[0].farZ, orbitPathList[orbitPathList.size() - 1].centerZ - orbitPathList[orbitPathList.size() - 1].radius); } // We want to avoid overpartitioning the depth buffer. In this stage, we coalesce // partitions that have small spans in the depth buffer. // TODO: Implement this step! vector