// 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 MACOSX_PB #include #endif #endif /* _WIN32 */ #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" 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; // 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; static const Color compassColor(0.4f, 0.4f, 1.0f); static const float CoronaHeight = 0.2f; static bool buggyVertexProgramEmulation = true; struct SphericalCoordLabel { string label; float ra; float dec; SphericalCoordLabel() : ra(0), dec(0) {}; SphericalCoordLabel(float _ra, float _dec) : ra(_ra), dec(_dec) { } }; static int nCoordLabels = 44; static SphericalCoordLabel* coordLabels = NULL; static const int MaxSkyRings = 32; static const int MaxSkySlices = 180; static const int MinSkySlices = 30; 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.125f, 0.167f, 0.2f); Color Renderer::EquatorialGridLabelColor(0.095f, 0.196f, 0.096f); 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.12f, 0.18f); Color Renderer::BoundaryColor (0.1f, 0.006f, 0.066f); Color Renderer::EquatorialGridColor (0.071f, 0.114f, 0.073f); #if 0 struct DisplayDevice { float faintestVisibleLevel; }; struct Detector { float saturationLevel; }; DisplayDevice displayDevice = { 0.1f, }; Detector detector = { 1.0f, }; #endif // 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), minOrbitSize(MinOrbitSizeForLabel), distanceLimit(1.0e6f), minFeatureSize(MinFeatureSizeForLabel), locationFilter(~0u), colorTemp(NULL), videoSync(false) { 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); Vec3f u(0, 0, 1); #if 0 Vec3f n(0, 0, 1); // Experimental illumination function float c = v * n; if (c < 0.0f) { u = v; } else { c = (1 - ((1 - c))) * 1.0f; u = v + (c * n); u.normalize(); } #else u = v; #endif pixel[0] = 128 + (int) (127 * u.x); pixel[1] = 128 + (int) (127 * u.y); pixel[2] = 128 + (int) (127 * u.z); } 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 bool operator<(const Renderer::Label& a, const Renderer::Label& b) { // Operation is reversed because -z axis points into the screen return a.position.z > b.position.z; } // Depth comparison for orbit paths bool operator<(const Renderer::OrbitPathListEntry& a, const Renderer::OrbitPathListEntry& b) { // Operation is reversed because -z axis points into the screen return a.centerZ - a.radius > b.centerZ - b.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_EXT_texture_cube_map")) { // normalizationTex = CreateNormalizationCubeMap(64); normalizationTex = CreateProceduralCubeMap(64, GL_RGB, IllumMapEval); } // Create labels for celestial sphere { char buf[10]; int i; coordLabels = new SphericalCoordLabel[nCoordLabels]; for (i = 0; i < 12; i++) { coordLabels[i].ra = float(i * 2); coordLabels[i].dec = 0; sprintf(buf, "%dh", i * 2); coordLabels[i].label = string(buf); } coordLabels[12] = SphericalCoordLabel(0, -80); coordLabels[13] = SphericalCoordLabel(0, -70); coordLabels[14] = SphericalCoordLabel(0, -60); coordLabels[15] = SphericalCoordLabel(0, -50); coordLabels[16] = SphericalCoordLabel(0, -40); coordLabels[17] = SphericalCoordLabel(0, -30); coordLabels[18] = SphericalCoordLabel(0, -20); coordLabels[19] = SphericalCoordLabel(0, -10); coordLabels[20] = SphericalCoordLabel(0, 10); coordLabels[21] = SphericalCoordLabel(0, 20); coordLabels[22] = SphericalCoordLabel(0, 30); coordLabels[23] = SphericalCoordLabel(0, 40); coordLabels[24] = SphericalCoordLabel(0, 50); coordLabels[25] = SphericalCoordLabel(0, 60); coordLabels[26] = SphericalCoordLabel(0, 70); coordLabels[27] = SphericalCoordLabel(0, 80); for (i = 28; i < nCoordLabels; i++) { coordLabels[i].ra = 12; coordLabels[i].dec = coordLabels[i - 16].dec; } for (i = 12; i < nCoordLabels; i++) { char buf[10]; sprintf(buf, "%d", (int) coordLabels[i].dec); coordLabels[i].label = 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; } } // 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; } float Renderer::getFaintestAM45deg() { return faintestAutoMag45deg; } unsigned int Renderer::getResolution() { return textureResolution; } void Renderer::setResolution(unsigned int resolution) { if (resolution < TEXTURE_RESOLUTION) textureResolution = resolution; } TextureFont* Renderer::getFont(FontStyle fs) const { return font[(int) fs]; } void Renderer::setFont(FontStyle fs, TextureFont* txf) { font[(int) fs] = txf; } void Renderer::setRenderMode(int _renderMode) { renderMode = _renderMode; } int Renderer::getRenderFlags() const { return renderFlags; } void Renderer::setRenderFlags(int _renderFlags) { renderFlags = _renderFlags; } int Renderer::getLabelMode() const { return labelMode; } void Renderer::setLabelMode(int _labelMode) { labelMode = _labelMode; } int Renderer::getOrbitMask() const { return orbitMask; } void Renderer::setOrbitMask(int mask) { orbitMask = mask; } const ColorTemperatureTable* Renderer::getStarColorTable() const { return colorTemp; } void Renderer::setStarColorTable(const ColorTemperatureTable* ct) { colorTemp = ct; } bool Renderer::getVideoSync() const { return videoSync; } void Renderer::setVideoSync(bool sync) { videoSync = sync; } float Renderer::getAmbientLightLevel() const { return ambientLightLevel; } void Renderer::setAmbientLightLevel(float level) { ambientLightLevel = level; } float Renderer::getMinimumFeatureSize() const { return minFeatureSize; } void Renderer::setMinimumFeatureSize(float pixels) { minFeatureSize = pixels; } 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; } float Renderer::getDistanceLimit() const { return distanceLimit; } void Renderer::setDistanceLimit(float distanceLimit_) { distanceLimit = distanceLimit_; } bool Renderer::getFragmentShaderEnabled() const { return fragmentShaderEnabled; } void Renderer::setFragmentShaderEnabled(bool enable) { fragmentShaderEnabled = enable && fragmentShaderSupported(); } bool Renderer::fragmentShaderSupported() const { return context->bumpMappingSupported(); } bool Renderer::getVertexShaderEnabled() const { return vertexShaderEnabled; } void Renderer::setVertexShaderEnabled(bool enable) { vertexShaderEnabled = enable && vertexShaderSupported(); } 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 = Point3f((float) p.x, (float) p.y, (float) p.z); samp.t = t; samples->push_back(samp); }; }; void renderOrbitColor(int classification, bool selected, float opacity) { Color orbitColor; if (selected) { // Highlight the orbit of the selected object in red orbitColor = Renderer::SelectionOrbitColor; } else { 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()); } void Renderer::renderOrbit(const OrbitPathListEntry& orbitPath, double t) { Body* body = orbitPath.body; // Ugly cast here because orbit cache needs to be rewritten to use an STL map Body* cacheKey = body != NULL ? body : reinterpret_cast(const_cast(orbitPath.star)); vector* trajectory = NULL; for (vector::const_iterator iter = orbitCache.begin(); iter != orbitCache.end(); iter++) { if ((*iter)->body == cacheKey) { (*iter)->keep = true; trajectory = &((*iter)->trajectory); break; } } const Orbit* orbit = NULL; if (body != NULL) orbit = body->getOrbit(); else orbit = orbitPath.star->getOrbit(); // If it's not in the cache already if (trajectory == NULL) { CachedOrbit* cachedOrbit = NULL; // Search the cache an see if we can reuse an old orbit for (vector::const_iterator iter = orbitCache.begin(); iter != orbitCache.end(); iter++) { if ((*iter)->body == NULL) { cachedOrbit = *iter; cachedOrbit->trajectory.clear(); break; } } // If we can't reuse an old orbit, allocate a new one. bool reuse = true; if (cachedOrbit == NULL) { cachedOrbit = new CachedOrbit(); reuse = false; } 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->body = cacheKey; cachedOrbit->keep = true; OrbitSampler sampler(&cachedOrbit->trajectory); orbit->sample(startTime, orbit->getPeriod(), nSamples, sampler); trajectory = &cachedOrbit->trajectory; // If the orbit is new, put it back in the cache if (!reuse) orbitCache.insert(orbitCache.end(), cachedOrbit); } glPushMatrix(); glTranslate(orbitPath.origin); if (body != NULL) { Quatd orientation(1.0); if (body->getOrbitFrame() != NULL) { orientation = body->getOrbitFrame()->getOrientation(t); } else if (body->getOrbitBarycenter() != NULL) { orientation = body->getOrbitBarycenter()->getEclipticalToEquatorial(t); } glRotate(~orientation); } bool highlight; if (body != NULL) highlight = highlightObject.body() == body; else highlight = highlightObject.star() == orbitPath.star; renderOrbitColor(body != NULL ? body->getClassification() : Body::Stellar, highlight, orbitPath.opacity); // Actually render the orbit if (orbit->isPeriodic()) glBegin(GL_LINE_LOOP); else glBegin(GL_LINE_STRIP); if ((renderFlags & ShowPartialTrajectories) == 0 || orbit->isPeriodic()) { // Show the complete trajectory // TODO: for much greater efficiency, use vertex arrays rather than // immediate commands to draw orbit paths. for (vector::const_iterator p = trajectory->begin(); p != trajectory->end(); p++) { glVertex3f(p->pos.x, p->pos.y, p->pos.z); } } else { vector::const_iterator p; // Show the portion of the trajectory travelled up to this point for (p = trajectory->begin(); p != trajectory->end() && p->t < t; p++) { glVertex3f(p->pos.x, p->pos.y, p->pos.z); } // If we're midway through a non-periodic trajectory, we will need // to render a partial orbit segment. if (p != trajectory->end()) { Point3d pos = orbit->positionAtTime(t); glVertex3f((float) pos.x, (float) pos.y, (float) pos.z); } } 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); Renderer::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) { if (rle.body != NULL) { if (rle.isCometTail) { renderCometTail(*rle.body, rle.position, rle.distance, rle.appMag, observer.getTime(), cameraOrientation, lightSourceLists[rle.solarSysIndex], nearPlaneDistance, farPlaneDistance); } else { renderPlanet(*rle.body, rle.position, rle.distance, rle.appMag, observer, cameraOrientation, lightSourceLists[rle.solarSysIndex], nearPlaneDistance, farPlaneDistance); } } else if (rle.star != NULL) { renderStar(*rle.star, rle.position, rle.distance, rle.appMag, cameraOrientation, observer.getTime(), nearPlaneDistance, farPlaneDistance); } } void Renderer::render(const Observer& observer, const Universe& universe, float faintestMagNight, const Selection& sel) { // Get the observer's time double now = observer.getTime(); // 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.getOrientation(); // 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); unsigned int solarSysIndex = 0; for (vector::const_iterator iter = nearStars.begin(); iter != nearStars.end(); iter++) { const Star* sun = *iter; SolarSystem* solarSystem = universe.getSolarSystem(sun); if (solarSystem != NULL) { setupLightSources(nearStars, *sun, now, lightSourceLists[solarSysIndex]); buildRenderLists(*sun, solarSystem->getPlanets(), observer, now, solarSysIndex, (labelMode & (BodyLabelMask)) != 0); solarSysIndex++; } 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(); float oblateness = iter->body->getOblateness(); Vec3f recipSemiAxes(1.0f, 1.0f / (1.0f - oblateness), 1.0f); 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 (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) { glColor(ConstellationColor); 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) { glColor(BoundaryColor); 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(); // Clear the keep flag for all orbits in the cache; if they're not // used when rendering this frame, they'll get marked for // recycling. if ((renderFlags & ShowOrbits) != 0 && orbitMask != 0) { for (vector::const_iterator iter = orbitCache.begin(); iter != orbitCache.end(); iter++) (*iter)->keep = false; } 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.getOrientation()).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; if (iter->body != NULL) { if (iter->isCometTail) { radius = iter->radius; cullRadius = radius; convex = false; } else { 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)); } } } else if (iter->star != NULL) { radius = iter->star->getRadius(); cullRadius = radius * (1.0f + CoronaHeight); } // 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 the oblateness float eradius = radius; if (iter->body != NULL) eradius *= 1.0f - iter->body->getOblateness(); 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); 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