// galaxy.cpp // // Copyright (C) 2001-2009, the Celestia Development Team // Original version by Chris Laurel, Fridger Schrempp, and Toti // // 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 "render.h" #include "astro.h" #include "galaxy.h" #include "vecgl.h" #include "texture.h" #include #include #include #include #include #include #include #include #include #include using namespace Eigen; using namespace std; using namespace celmath; using namespace celestia; static int width = 128, height = 128; static const unsigned int GALAXY_POINTS = 3500; static bool formsInitialized = false; static GalacticForm** spiralForms = nullptr; static GalacticForm** ellipticalForms = nullptr; static GalacticForm* irregularForm = nullptr; static Texture* galaxyTex = nullptr; static Texture* colorTex = nullptr; static void InitializeForms(); static GalacticForm* buildGalacticForms(const fs::path& filename); float Galaxy::lightGain = 0.0f; bool operator< (const Blob& b1, const Blob& b2) { return (b1.position.squaredNorm() < b2.position.squaredNorm()); } typedef vector > BlobVector; class GalacticForm { public: BlobVector* blobs; Vector3f scale; }; struct GalaxyTypeName { const char* name; Galaxy::GalaxyType type; }; static GalaxyTypeName GalaxyTypeNames[] = { { "S0", Galaxy::S0 }, { "Sa", Galaxy::Sa }, { "Sb", Galaxy::Sb }, { "Sc", Galaxy::Sc }, { "SBa", Galaxy::SBa }, { "SBb", Galaxy::SBb }, { "SBc", Galaxy::SBc }, { "E0", Galaxy::E0 }, { "E1", Galaxy::E1 }, { "E2", Galaxy::E2 }, { "E3", Galaxy::E3 }, { "E4", Galaxy::E4 }, { "E5", Galaxy::E5 }, { "E6", Galaxy::E6 }, { "E7", Galaxy::E7 }, { "Irr", Galaxy::Irr }, }; static void GalaxyTextureEval(float u, float v, float /*w*/, unsigned char *pixel) { float r = 0.9f - (float) sqrt(u * u + v * v ); if (r < 0) r = 0; auto pixVal = (int) (r * 255.99f); #ifdef HDR_COMPRESS pixel[0] = 127; pixel[1] = 127; pixel[2] = 127; #else pixel[0] = 255;//65; pixel[1] = 255;//64; pixel[2] = 255;//65; #endif pixel[3] = pixVal; } static void ColorTextureEval(float u, float v, float /*w*/, unsigned char *pixel) { unsigned int i = (u*0.5f + 0.5f)*255.99f; // [-1, 1] -> [0, 255] // generic Hue profile as deduced from true-color imaging for spirals // Hue in degrees float hue = 25 * tanh(0.0615f * (27 - i)); if (i >= 28) hue += 220; //convert Hue to RGB float r, g, b; DeepSkyObject::hsv2rgb(&r, &g, &b, hue, 0.20f, 1.0f); pixel[0] = (unsigned char) (r * 255.99f); pixel[1] = (unsigned char) (g * 255.99f); pixel[2] = (unsigned char) (b * 255.99f); } float Galaxy::getDetail() const { return detail; } void Galaxy::setDetail(float d) { detail = d; } void Galaxy::setCustomTmpName(const string& tmpNameStr) { if (customTmpName == nullptr) customTmpName = new string(tmpNameStr); else *customTmpName = tmpNameStr; } string Galaxy::getCustomTmpName() const { if (customTmpName == nullptr) return ""; else return *customTmpName; } const char* Galaxy::getType() const { return GalaxyTypeNames[(int) type].name; } void Galaxy::setType(const string& typeStr) { type = Galaxy::Irr; auto iter = std::find_if(begin(GalaxyTypeNames), end(GalaxyTypeNames), [typeStr](GalaxyTypeName& g) { return g.name == typeStr; }); if (iter != end(GalaxyTypeNames)) type = iter->type; if (!formsInitialized) InitializeForms(); if (customTmpName != nullptr) { form = buildGalacticForms(fs::path("models") / *customTmpName); } else { switch (type) { case S0: case Sa: case Sb: case Sc: case SBa: case SBb: case SBc: form = spiralForms[type - S0]; break; case E0: case E1: case E2: case E3: case E4: case E5: case E6: case E7: form = ellipticalForms[type - E0]; //form = nullptr; break; case Irr: form = irregularForm; break; } } } string Galaxy::getDescription() const { return fmt::sprintf(_("Galaxy (Hubble type: %s)"), getType()); } GalacticForm* Galaxy::getForm() const { return form; } const char* Galaxy::getObjTypeName() const { return "galaxy"; } // TODO: This value is just a guess. // To be optimal, it should actually be computed: static const float RADIUS_CORRECTION = 0.025f; static const float MAX_SPIRAL_THICKNESS = 0.06f; bool Galaxy::pick(const Ray3d& ray, double& distanceToPicker, double& cosAngleToBoundCenter) const { if (!isVisible()) return false; // The ellipsoid should be slightly larger to compensate for the fact // that blobs are considered points when galaxies are built, but have size // when they are drawn. float yscale = (type < E0 )? MAX_SPIRAL_THICKNESS: form->scale.y() + RADIUS_CORRECTION; Vector3d ellipsoidAxes(getRadius()*(form->scale.x() + RADIUS_CORRECTION), getRadius()* yscale, getRadius()*(form->scale.z() + RADIUS_CORRECTION)); Matrix3d rotation = getOrientation().cast().toRotationMatrix(); return testIntersection(Ray3d(ray.origin - getPosition(), ray.direction).transform(rotation), Ellipsoidd(ellipsoidAxes), distanceToPicker, cosAngleToBoundCenter); } bool Galaxy::load(AssociativeArray* params, const fs::path& resPath) { double detail = 1.0; params->getNumber("Detail", detail); setDetail((float) detail); string customTmpName; if(params->getString("CustomTemplate", customTmpName)) setCustomTmpName(customTmpName); string typeName; params->getString("Type", typeName); setType(typeName); return DeepSkyObject::load(params, resPath); } void Galaxy::render(const Vector3f& offset, const Quaternionf& viewerOrientation, float brightness, float pixelSize, const Matrices& m, const Renderer* renderer) { if (form == nullptr) { //renderGalaxyEllipsoid(offset, viewerOrientation, brightness, pixelSize); } else { renderGalaxyPointSprites(offset, viewerOrientation, brightness, pixelSize, m, renderer); } } struct GalaxyVertex { EIGEN_MAKE_ALIGNED_OPERATOR_NEW; Vector4f position; Matrix texCoord; // texCoord.x = x, texCoord.y = y, texCoord.z = color index, texCoord.w = alpha }; static void draw(const GalaxyVertex *v, size_t count, const GLushort *indices) { glVertexAttribPointer(CelestiaGLProgram::VertexCoordAttributeIndex, 4, GL_FLOAT, GL_FALSE, sizeof(GalaxyVertex), &v->position); glVertexAttribPointer(CelestiaGLProgram::TextureCoord0AttributeIndex, 4, GL_SHORT, GL_FALSE, sizeof(GalaxyVertex), &v->texCoord); glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_SHORT, indices); } static GalaxyVertex *g_vertices = nullptr; static GLushort *g_indices = nullptr; constexpr const size_t maxPoints = 8192; // 256k buffer void Galaxy::renderGalaxyPointSprites(const Vector3f& offset, const Quaternionf& viewerOrientation, float brightness, float pixelSize, const Matrices& ms, const Renderer* renderer) { if (form == nullptr) return; /* We'll first see if the galaxy's apparent size is big enough to be noticeable on screen; if it's not we'll break right here, avoiding all the overhead of the matrix transformations and GL state changes: */ float distanceToDSO = offset.norm() - getRadius(); if (distanceToDSO < 0) distanceToDSO = 0; float minimumFeatureSize = pixelSize * distanceToDSO; float size = 2 * getRadius(); if (size < minimumFeatureSize) return; auto *prog = renderer->getShaderManager().getShader("galaxy"); if (prog == nullptr) return; if (galaxyTex == nullptr) { galaxyTex = CreateProceduralTexture(width, height, GL_RGBA, GalaxyTextureEval); } assert(galaxyTex != nullptr); glActiveTexture(GL_TEXTURE0); galaxyTex->bind(); if (colorTex == nullptr) { colorTex = CreateProceduralTexture(256, 1, GL_RGBA, ColorTextureEval, Texture::EdgeClamp, Texture::NoMipMaps); } assert(colorTex != nullptr); glActiveTexture(GL_TEXTURE1); colorTex->bind(); Matrix3f viewMat = viewerOrientation.conjugate().toRotationMatrix(); Vector4f v0(Vector4f::Zero()); Vector4f v1(Vector4f::Zero()); Vector4f v2(Vector4f::Zero()); Vector4f v3(Vector4f::Zero()); v0.head(3) = viewMat * Vector3f(-1, -1, 0) * size; v1.head(3) = viewMat * Vector3f( 1, -1, 0) * size; v2.head(3) = viewMat * Vector3f( 1, 1, 0) * size; v3.head(3) = viewMat * Vector3f(-1, 1, 0) * size; Quaternionf orientation = getOrientation().conjugate(); Matrix3f mScale = form->scale.asDiagonal() * size; Matrix3f mLinear = orientation.toRotationMatrix() * mScale; Matrix4f m = Matrix4f::Identity(); m.topLeftCorner(3,3) = mLinear; m.block<3,1>(0, 3) = offset; int pow2 = 1; BlobVector* points = form->blobs; unsigned int nPoints = (unsigned int) (points->size() * clamp(getDetail())); // corrections to avoid excessive brightening if viewed e.g. edge-on float brightness_corr = 1.0f; float cosi; if (type < E0 || type > E3) //all galaxies, except ~round elliptics { cosi = (orientation * Vector3f::UnitY()).dot(offset) / offset.norm(); brightness_corr = std::sqrt(std::abs(cosi)); if (brightness_corr < 0.2f) brightness_corr = 0.2f; } if (type > E3) // only elliptics with higher ellipticities { cosi = (orientation * Vector3f::UnitX()).dot(offset) / offset.norm(); brightness_corr = brightness_corr * std::abs(cosi); if (brightness_corr < 0.45f) brightness_corr = 0.45f; } Matrix4f mv = vecgl::translate(*ms.modelview, Vector3f(-offset)); const float btot = ((type > SBc) && (type < Irr)) ? 2.5f : 5.0f; const float spriteScaleFactor = 1.0f / 1.55f; glEnableVertexAttribArray(CelestiaGLProgram::VertexCoordAttributeIndex); glEnableVertexAttribArray(CelestiaGLProgram::TextureCoord0AttributeIndex); if (g_vertices == nullptr) g_vertices = new GalaxyVertex[maxPoints]; if (g_indices == nullptr) g_indices = new GLushort[(maxPoints / 4 + 1) * 6]; GLushort j = 0; prog->use(); prog->mat4Param("MVPMatrix") = (*ms.projection) * mv; prog->samplerParam("galaxyTex") = 0; prog->samplerParam("colorTex") = 1; size_t vertex = 0, index = 0; for (unsigned int i = 0; i < nPoints; ++i) { if ((i & pow2) != 0) { pow2 <<= 1; size *= spriteScaleFactor; v0 *= spriteScaleFactor; v1 *= spriteScaleFactor; v2 *= spriteScaleFactor; v3 *= spriteScaleFactor; if (size < minimumFeatureSize) break; } const Blob& b = (*points)[i]; Vector4f p = m * b.position; float br = b.brightness / 255.0f; float screenFrac = size / p.norm(); if (screenFrac < 0.1f) { float a = (4.0f * lightGain + 1.0f) * btot * (0.1f - screenFrac) * brightness_corr * brightness * br; short alpha = (short) (a * 65535.99f); short color = (short) b.colorIndex; g_vertices[vertex++] = { p + v0, { 0, 0, color, alpha } }; g_vertices[vertex++] = { p + v1, { 1, 0, color, alpha } }; g_vertices[vertex++] = { p + v2, { 1, 1, color, alpha } }; g_vertices[vertex++] = { p + v3, { 0, 1, color, alpha } }; g_indices[index++] = j; g_indices[index++] = j + 1; g_indices[index++] = j + 2; g_indices[index++] = j; g_indices[index++] = j + 2; g_indices[index++] = j + 3; j += 4; if (vertex + 4 > maxPoints) { draw(g_vertices, index, g_indices); index = 0; vertex = 0; j = 0; } } } if (index > 0) draw(g_vertices, index, g_indices); glDisableVertexAttribArray(CelestiaGLProgram::VertexCoordAttributeIndex); glDisableVertexAttribArray(CelestiaGLProgram::TextureCoord0AttributeIndex); glUseProgram(0); glActiveTexture(GL_TEXTURE0); } #if 0 void Galaxy::renderGalaxyEllipsoid(const Vec3f& offset, const Quatf&, float, float pixelSize) { float discSizeInPixels = pixelSize * getRadius() / offset.length(); unsigned int nRings = (unsigned int) (discSizeInPixels / 4.0f); unsigned int nSlices = (unsigned int) (discSizeInPixels / 4.0f); nRings = max(nRings, 100u); nSlices = max(nSlices, 100u); VertexProcessor* vproc = context.getVertexProcessor(); if (vproc == nullptr) return; //int e = min(max((int) type - (int) E0, 0), 7); Vec3f scale = Vec3f(1.0f, 0.9f, 1.0f) * getRadius(); Vec3f eyePos_obj = -offset * (~getOrientation()).toMatrix3(); vproc->enable(); vproc->use(vp::ellipticalGalaxy); vproc->parameter(vp::EyePosition, eyePos_obj); vproc->parameter(vp::Scale, scale); vproc->parameter(vp::InverseScale, Vec3f(1.0f / scale.x, 1.0f / scale.y, 1.0f / scale.z)); vproc->parameter((vp::Parameter) 23, eyePos_obj.length() / scale.x, 0.0f, 0.0f, 0.0f); glRotate(getOrientation()); glDisable(GL_TEXTURE_2D); glColor4f(1.0f, 1.0f, 1.0f, 0.3f); for (unsigned int i = 0; i < nRings; i++) { float phi0 = (float) PI * ((float) i / (float) nRings - 0.5f); float phi1 = (float) PI * ((float) (i + 1) / (float) nRings - 0.5f); glBegin(GL_QUAD_STRIP); for (unsigned int j = 0; j <= nSlices; j++) { float theta = (float) (PI * 2) * (float) j / (float) nSlices; float sinTheta = (float) sin(theta); float cosTheta = (float) cos(theta); glVertex3f((float) cos(phi0) * cosTheta * scale.x, (float) sin(phi0) * scale.y, (float) cos(phi0) * sinTheta * scale.z); glVertex3f((float) cos(phi1) * cosTheta * scale.x, (float) sin(phi1) * scale.y, (float) cos(phi1) * sinTheta * scale.z); } glEnd(); } glEnable(GL_TEXTURE_2D); vproc->disable(); } #endif uint64_t Galaxy::getRenderMask() const { return Renderer::ShowGalaxies; } unsigned int Galaxy::getLabelMask() const { return Renderer::GalaxyLabels; } void Galaxy::increaseLightGain() { lightGain += 0.05f; if (lightGain > 1.0f) lightGain = 1.0f; } void Galaxy::decreaseLightGain() { lightGain -= 0.05f; if (lightGain < 0.0f) lightGain = 0.0f; } float Galaxy::getLightGain() { return lightGain; } void Galaxy::setLightGain(float lg) { lightGain = clamp(lg); } GalacticForm* buildGalacticForms(const fs::path& filename) { Blob b; BlobVector* galacticPoints = new BlobVector; // Load templates in standard .png format int width, height, rgb, j = 0, kmin = 9; unsigned char value; float h = 0.75f; Image* img; img = LoadPNGImage(filename); if (img == nullptr) { cout<<"\nThe galaxy template *** "<getWidth(); height = img->getHeight(); rgb = img->getComponents(); for (int i = 0; i < width * height; i++) { value = img->getPixels()[rgb * i]; if (value > 10) { float x, y, z, r2, yy, prob; z = floor(i /(float) width); x = (i - width * z - 0.5f * (width - 1)) / (float) width; z = (0.5f * (height - 1) - z) / (float) height; x += sfrand() * 0.008f; z += sfrand() * 0.008f; r2 = x * x + z * z; if (filename != "models/E0.png") { float y0 = 0.5f * MAX_SPIRAL_THICKNESS * sqrt((float)value/256.0f) * exp(- 5.0f * r2); float B, yr; B = (r2 > 0.35f)? 1.0f: 0.75f; // the darkness of the "dust lane", 0 < B < 1 float p0 = 1.0f - B * exp(-h * h); // the uniform reference probability, envelopping prob*p0. do { // generate "thickness" y of spirals with emulation of a dust lane // in galctic plane (y=0) yr = sfrand() * h; prob = (1.0f - B * exp(-yr * yr))/p0; } while (frand() > prob); b.brightness = value * prob; y = y0 * yr / h; } else { // generate spherically symmetric distribution from E0.png do { yy = sfrand(); float ry2 = 1.0f - yy * yy; prob = ry2 > 0? sqrt(ry2): 0.0f; } while (frand() > prob); y = yy * sqrt(0.25f - r2) ; b.brightness = value; kmin = 12; } b.position = Vector4f(x, y, z, 1.0f); unsigned int rr = (unsigned int) (b.position.head(3).norm() * 511); b.colorIndex = rr < 256? rr: 255; galacticPoints->push_back(b); j++; } } delete img; galacticPoints->reserve(j); // sort to start with the galaxy center region (x^2 + y^2 + z^2 ~ 0), such that // the biggest (brightest) sprites will be localized there! sort(galacticPoints->begin(), galacticPoints->end()); // reshuffle the galaxy points randomly...except the first kmin+1 in the center! // the higher that number the stronger the central "glow" std::random_device rng; std::mt19937 urng(rng()); shuffle(galacticPoints->begin() + kmin, galacticPoints->end(), urng); auto* galacticForm = new GalacticForm(); galacticForm->blobs = galacticPoints; galacticForm->scale = Vector3f::Ones(); return galacticForm; } void InitializeForms() { // Spiral Galaxies, 7 classical Hubble types spiralForms = new GalacticForm*[7]; spiralForms[Galaxy::S0] = buildGalacticForms("models/S0.png"); spiralForms[Galaxy::Sa] = buildGalacticForms("models/Sa.png"); spiralForms[Galaxy::Sb] = buildGalacticForms("models/Sb.png"); spiralForms[Galaxy::Sc] = buildGalacticForms("models/Sc.png"); spiralForms[Galaxy::SBa] = buildGalacticForms("models/SBa.png"); spiralForms[Galaxy::SBb] = buildGalacticForms("models/SBb.png"); spiralForms[Galaxy::SBc] = buildGalacticForms("models/SBc.png"); // Elliptical Galaxies , 8 classical Hubble types, E0..E7, // // To save space: generate spherical E0 template from S0 disk // via rescaling by (1.0f, 3.8f, 1.0f). ellipticalForms = new GalacticForm*[8]; for (unsigned int eform = 0; eform <= 7; ++eform) { float ell = 1.0f - (float) eform / 8.0f; // note the correct x,y-alignment of 'ell' scaling!! // build all elliptical templates from rescaling E0 ellipticalForms[eform] = buildGalacticForms("models/E0.png"); if (*ellipticalForms) ellipticalForms[eform]->scale = Vector3f(ell, ell, 1.0f); // account for reddening of ellipticals rel.to spirals if (*ellipticalForms) { unsigned int nPoints = (unsigned int) (ellipticalForms[eform]->blobs->size()); for (unsigned int i = 0; i < nPoints; ++i) { (*ellipticalForms[eform]->blobs)[i].colorIndex = (unsigned int) ceil(0.76f * (*ellipticalForms[eform]->blobs)[i].colorIndex); } } } //Irregular Galaxies unsigned int galaxySize = GALAXY_POINTS, ip = 0; Blob b; Vector3f p; BlobVector* irregularPoints = new BlobVector; irregularPoints->reserve(galaxySize); while (ip < galaxySize) { p = Vector3f(sfrand(), sfrand(), sfrand()); float r = p.norm(); if (r < 1) { float prob = (1 - r) * (fractalsum(Vector3f(p.x() + 5, p.y() + 5, p.z() + 5), 8) + 1) * 0.5f; if (frand() < prob) { b.position = Vector4f(p.x(), p.y(), p.z(), 1.0f); b.brightness = 64u; auto rr = (unsigned int) (r * 511); b.colorIndex = rr < 256 ? rr : 255; irregularPoints->push_back(b); ++ip; } } } irregularForm = new GalacticForm(); irregularForm->blobs = irregularPoints; irregularForm->scale = Vector3f::Constant(0.5f); formsInitialized = true; } ostream& operator<<(ostream& s, const Galaxy::GalaxyType& sc) { return s << GalaxyTypeNames[static_cast(sc)].name; }