// Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details // Licensed under the terms of the GPL v3. See licenses/GPL-3.txt #include "Camera.h" #include "Body.h" #include "Frame.h" #include "Game.h" #include "Pi.h" #include "Planet.h" #include "Player.h" #include "Sfx.h" #include "Space.h" #include "galaxy/StarSystem.h" #include "graphics/TextureBuilder.h" using namespace Graphics; // if a body would render smaller than this many pixels, just ignore it static const float OBJECT_HIDDEN_PIXEL_THRESHOLD = 2.0f; // if a terrain object would render smaller than this many pixels, draw a billboard instead static const float BILLBOARD_PIXEL_THRESHOLD = 8.0f; CameraContext::CameraContext(float width, float height, float fovAng, float zNear, float zFar) : m_width(width), m_height(height), m_fovAng(fovAng), m_zNear(zNear), m_zFar(zFar), m_frustum(m_width, m_height, m_fovAng, m_zNear, m_zFar), m_frame(FrameId::Invalid), m_pos(0.0), m_orient(matrix3x3d::Identity()), m_camFrame(FrameId::Invalid) { } CameraContext::~CameraContext() { if (m_camFrame) EndFrame(); } void CameraContext::BeginFrame() { assert(m_frame.valid()); assert(!m_camFrame.valid()); // make temporary camera frame m_camFrame = Frame::CreateCameraFrame(m_frame); Frame *camFrame = Frame::GetFrame(m_camFrame); // move and orient it to the camera position camFrame->SetOrient(m_orient, Pi::game ? Pi::game->GetTime() : 0.0); camFrame->SetPosition(m_pos); // make sure old orient and interpolated orient (rendering orient) are not rubbish camFrame->ClearMovement(); camFrame->UpdateInterpTransform(1.0); // update root-relative pos/orient } void CameraContext::EndFrame() { assert(m_frame.valid()); assert(m_camFrame.valid()); Frame::DeleteCameraFrame(m_camFrame); m_camFrame = FrameId::Invalid; } void CameraContext::ApplyDrawTransforms(Graphics::Renderer *r) { r->SetProjection(matrix4x4f::InfinitePerspectiveMatrix(DEG2RAD(m_fovAng), m_width / m_height, m_zNear)); r->SetTransform(matrix4x4f::Identity()); } bool Camera::BodyAttrs::sort_BodyAttrs(const BodyAttrs &a, const BodyAttrs &b) { // both drawing last; distance order if (a.bodyFlags & Body::FLAG_DRAW_LAST && b.bodyFlags & Body::FLAG_DRAW_LAST) return a.camDist > b.camDist; // a drawing last; draw b first if (a.bodyFlags & Body::FLAG_DRAW_LAST) return false; // b drawing last; draw a first if (b.bodyFlags & Body::FLAG_DRAW_LAST) return true; // both in normal draw; distance order return a.camDist > b.camDist; } Camera::Camera(RefCountedPtr context, Graphics::Renderer *renderer) : m_context(context), m_renderer(renderer) { Graphics::MaterialDescriptor desc; desc.effect = Graphics::EFFECT_BILLBOARD; desc.textures = 1; m_billboardMaterial.reset(m_renderer->CreateMaterial(desc)); m_billboardMaterial->texture0 = Graphics::TextureBuilder::Billboard("textures/planet_billboard.dds").GetOrCreateTexture(m_renderer, "billboard"); } static void position_system_lights(Frame *camFrame, Frame *frame, std::vector &lights) { PROFILE_SCOPED() if (lights.size() > 3) return; SystemBody *body = frame->GetSystemBody(); // IsRotFrame check prevents double counting if (body && !frame->IsRotFrame() && (body->GetSuperType() == SystemBody::SUPERTYPE_STAR)) { vector3d lpos = frame->GetPositionRelTo(camFrame->GetId()); const double dist = lpos.Length() / AU; lpos *= 1.0 / dist; // normalize const Color &col = StarSystem::starRealColors[body->GetType()]; const Color lightCol(col[0], col[1], col[2], 0); vector3f lightpos(lpos.x, lpos.y, lpos.z); Graphics::Light light(Graphics::Light::LIGHT_DIRECTIONAL, lightpos, lightCol, lightCol); lights.push_back(Camera::LightSource(frame->GetBody(), light)); } for (FrameId kid : frame->GetChildren()) { Frame *kid_f = Frame::GetFrame(kid); position_system_lights(camFrame, kid_f, lights); } } void Camera::Update() { FrameId camFrame = m_context->GetTempFrame(); // evaluate each body and determine if/where/how to draw it m_sortedBodies.clear(); for (Body *b : Pi::game->GetSpace()->GetBodies()) { BodyAttrs attrs; attrs.body = b; attrs.billboard = false; // false by default // If the body wishes to be excluded from the draw, skip it. if (b->GetFlags() & Body::FLAG_DRAW_EXCLUDE) continue; // determine position and transform for draw // Frame::GetFrameTransform(b->GetFrame(), camFrame, attrs.viewTransform); // doesn't use interp coords, so breaks in some cases Frame *f = Frame::GetFrame(b->GetFrame()); attrs.viewTransform = f->GetInterpOrientRelTo(camFrame); attrs.viewTransform.SetTranslate(f->GetInterpPositionRelTo(camFrame)); attrs.viewCoords = attrs.viewTransform * b->GetInterpPosition(); // cull off-screen objects double rad = b->GetClipRadius(); if (!m_context->GetFrustum().TestPointInfinite(attrs.viewCoords, rad)) continue; attrs.camDist = attrs.viewCoords.Length(); attrs.bodyFlags = b->GetFlags(); // approximate pixel width (disc diameter) of body on screen const float pixSize = Graphics::GetScreenHeight() * 2.0 * rad / (attrs.camDist * Graphics::GetFovFactor()); // terrain objects are visible from distance but might not have any discernable features if (b->IsType(ObjectType::TERRAINBODY)) { if (pixSize < BILLBOARD_PIXEL_THRESHOLD) { attrs.billboard = true; // project the position vector3d pos; m_context->GetFrustum().TranslatePoint(attrs.viewCoords, pos); attrs.billboardPos = vector3f(pos); // limit the minimum billboard size for planets so they're always a little visible attrs.billboardSize = std::max(1.0f, pixSize); if (b->IsType(ObjectType::STAR)) { attrs.billboardColor = StarSystem::starRealColors[b->GetSystemBody()->GetType()]; } else if (b->IsType(ObjectType::PLANET)) { // XXX this should incorporate some lighting effect // (ie, colour of the illuminating star(s)) attrs.billboardColor = b->GetSystemBody()->GetAlbedo(); } else { attrs.billboardColor = Color::WHITE; } // this should always be the main star in the system - except for the star itself! if (!m_lightSources.empty() && !b->IsType(ObjectType::STAR)) { const Graphics::Light &light = m_lightSources[0].GetLight(); attrs.billboardColor *= light.GetDiffuse(); // colour the billboard a little with the Starlight } attrs.billboardColor.a = 255; // no alpha, these things are hard enough to see as it is } } else if (pixSize < OBJECT_HIDDEN_PIXEL_THRESHOLD) { continue; } m_sortedBodies.push_back(attrs); } // depth sort m_sortedBodies.sort(); } void Camera::Draw(const Body *excludeBody) { PROFILE_SCOPED() FrameId camFrameId = m_context->GetTempFrame(); FrameId rootFrameId = Pi::game->GetSpace()->GetRootFrame(); Frame *camFrame = Frame::GetFrame(camFrameId); Frame *rootFrame = Frame::GetFrame(rootFrameId); m_renderer->ClearScreen(); matrix4x4d trans2bg; Frame::GetFrameTransform(rootFrameId, camFrameId, trans2bg); trans2bg.ClearToRotOnly(); // Pick up to four suitable system light sources (stars) m_lightSources.clear(); m_lightSources.reserve(4); position_system_lights(camFrame, rootFrame, m_lightSources); if (m_lightSources.empty()) { // no lights means we're somewhere weird (eg hyperspace). fake one Graphics::Light light(Graphics::Light::LIGHT_DIRECTIONAL, vector3f(0.f), Color::WHITE, Color::WHITE); m_lightSources.push_back(LightSource(0, light)); } //fade space background based on atmosphere thickness and light angle float bgIntensity = 1.f; Frame *camParent = Frame::GetFrame(camFrame->GetParent()); if (camParent && camParent->IsRotFrame()) { //check if camera is near a planet Body *camParentBody = camParent->GetBody(); if (camParentBody && camParentBody->IsType(ObjectType::PLANET)) { Planet *planet = static_cast(camParentBody); const vector3f relpos(planet->GetInterpPositionRelTo(camFrameId)); double altitude(relpos.Length()); double pressure, density; planet->GetAtmosphericState(altitude, &pressure, &density); if (pressure >= 0.001) { //go through all lights to calculate something resembling light intensity float intensity = 0.f; const Body *pBody = Pi::game->GetPlayer(); for (Uint32 i = 0; i < m_lightSources.size(); i++) { // Set up data for eclipses. All bodies are assumed to be spheres. const LightSource &it = m_lightSources[i]; const vector3f lightDir(it.GetLight().GetPosition().Normalized()); intensity += ShadowedIntensity(i, pBody) * std::max(0.f, lightDir.Dot(-relpos.Normalized())) * (it.GetLight().GetDiffuse().GetLuminance() / 255.0f); } intensity = Clamp(intensity, 0.0f, 1.0f); //calculate background intensity with some hand-tweaked fuzz applied bgIntensity = Clamp(1.f - std::min(1.f, powf(density, 0.25f)) * (0.3f + powf(intensity, 0.25f)), 0.f, 1.f); } } } Pi::game->GetSpace()->GetBackground()->SetIntensity(bgIntensity); Pi::game->GetSpace()->GetBackground()->Draw(trans2bg); { std::vector rendererLights; rendererLights.reserve(m_lightSources.size()); for (size_t i = 0; i < m_lightSources.size(); i++) rendererLights.push_back(m_lightSources[i].GetLight()); m_renderer->SetLights(rendererLights.size(), &rendererLights[0]); } for (std::list::iterator i = m_sortedBodies.begin(); i != m_sortedBodies.end(); ++i) { BodyAttrs *attrs = &(*i); // explicitly exclude a single body if specified (eg player) if (attrs->body == excludeBody) continue; // draw something! if (attrs->billboard) { Graphics::Renderer::MatrixTicket mt(m_renderer, matrix4x4f::Identity()); m_billboardMaterial->diffuse = attrs->billboardColor; m_renderer->DrawPointSprites(1, &attrs->billboardPos, SfxManager::additiveAlphaState, m_billboardMaterial.get(), attrs->billboardSize); } else attrs->body->Render(m_renderer, this, attrs->viewCoords, attrs->viewTransform); } SfxManager::RenderAll(m_renderer, rootFrameId, camFrameId); } void Camera::CalcShadows(const int lightNum, const Body *b, std::vector &shadowsOut) const { // Set up data for eclipses. All bodies are assumed to be spheres. const Body *lightBody = m_lightSources[lightNum].GetBody(); if (!lightBody) return; const double lightRadius = lightBody->GetPhysRadius(); const vector3d bLightPos = lightBody->GetPositionRelTo(b); const vector3d lightDir = bLightPos.Normalized(); double bRadius; if (b->IsType(ObjectType::TERRAINBODY)) bRadius = b->GetSystemBody()->GetRadius(); else bRadius = b->GetPhysRadius(); // Look for eclipsing third bodies: for (const Body *b2 : Pi::game->GetSpace()->GetBodies()) { if (b2 == b || b2 == lightBody || !(b2->IsType(ObjectType::PLANET) || b2->IsType(ObjectType::STAR))) continue; double b2Radius = b2->GetSystemBody()->GetRadius(); vector3d b2pos = b2->GetPositionRelTo(b); const double perpDist = lightDir.Dot(b2pos); if (perpDist <= 0 || perpDist > bLightPos.Length()) // b2 isn't between b and lightBody; no eclipse continue; // Project to the plane perpendicular to lightDir, taking the line between the shadowed sphere // (b) and the light source as zero. Our calculations assume that the light source is at // infinity. All lengths are normalised such that b has radius 1. srad is then the radius of the // occulting sphere (b2), and lrad is the apparent radius of the light disc when considered to // be at the distance of b2, and projectedCentre is the normalised projected position of the // centre of b2 relative to the centre of b. The upshot is that from a point on b, with // normalised projected position p, the picture is of a disc of radius lrad being occulted by a // disc of radius srad centred at projectedCentre-p. To determine the light intensity at p, we // then just need to estimate the proportion of the light disc being occulted. const double srad = b2Radius / bRadius; const double lrad = (lightRadius / bLightPos.Length()) * perpDist / bRadius; if (srad / lrad < 0.01) { // any eclipse would have negligible effect - ignore continue; } const vector3d projectedCentre = (b2pos - perpDist * lightDir) / bRadius; if (projectedCentre.Length() < 1 + srad + lrad) { // some part of b is (partially) eclipsed Camera::Shadow shadow = { projectedCentre, static_cast(srad), static_cast(lrad) }; shadowsOut.push_back(shadow); } } } float discCovered(const float dist, const float rad) { // proportion of unit disc covered by a second disc of radius rad placed // dist from centre of first disc. // // WLOG, the second disc is displaced horizontally to the right. // xl = rightwards distance to intersection of the two circles. // xs = normalised leftwards distance from centre of second disc to intersection. // d = vertical distance to an intersection point // The clampings handle the cases where one disc contains the other. const float radsq = rad * rad; const float xl = Clamp((dist * dist + 1.f - radsq) / (2.f * std::max(0.001f, dist)), -1.f, 1.f); const float xs = Clamp((dist - xl) / std::max(0.001f, rad), -1.f, 1.f); const float d = sqrt(std::max(0.f, 1.f - xl * xl)); const float th = Clamp(acosf(xl), 0.f, float(M_PI)); const float th2 = Clamp(acosf(xs), 0.f, float(M_PI)); assert(!is_nan(d) && !is_nan(th) && !is_nan(th2)); // covered area can be calculated as the sum of segments from the two // discs plus/minus some triangles, and it works out as follows: return Clamp((th + radsq * th2 - dist * d) / float(M_PI), 0.f, 1.f); } static std::vector shadows; float Camera::ShadowedIntensity(const int lightNum, const Body *b) const { shadows.clear(); shadows.reserve(16); CalcShadows(lightNum, b, shadows); float product = 1.0; for (std::vector::const_iterator it = shadows.begin(), itEnd = shadows.end(); it != itEnd; ++it) product *= 1.0 - discCovered(it->centre.Length() / it->lrad, it->srad / it->lrad); return product; } // PrincipalShadows(b,n): returns the n biggest shadows on b in order of size void Camera::PrincipalShadows(const Body *b, const int n, std::vector &shadowsOut) const { shadows.clear(); shadows.reserve(16); for (size_t i = 0; i < 4 && i < m_lightSources.size(); i++) { CalcShadows(i, b, shadows); } shadowsOut.reserve(shadows.size()); std::sort(shadows.begin(), shadows.end()); std::vector::reverse_iterator it = shadows.rbegin(), itREnd = shadows.rend(); for (int i = 0; i < n; i++) { if (it == itREnd) break; shadowsOut.push_back(*(it++)); } }