// Copyright 2021, 2022 Benjamin Barenblat // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. #include "src/scene.h" #include #include #include #include #include #include #include #include #include "res/res.h" #include "src/astro.h" #include "src/gl/buffer.h" #include "src/gl/draw.h" #include "src/gl/shader.h" #include "src/gl/texture.h" #include "src/mesh.h" #include "src/webp.h" #include "third_party/abseil/absl/strings/string_view.h" #include "third_party/abseil/absl/types/span.h" #include "third_party/date/include/date/date.h" #include "third_party/date/include/date/tz.h" namespace glplanet { namespace { constexpr int kSectorsPerTurn = 64; constexpr int kSlices = 64; Eigen::Matrix4f ModelViewProjection(const double longitude_radians, const double latitude_radians) noexcept { // This is the precomputed result of the following transformations, applied in // order: // // 1. rotation around the model z axis (turning to longitude) // // 2. rotation around the model y axis (tilting to latitude) // // 3. transformation from model coordinates to view coordinates // // 4. perspective transformation // // This was computed with Maxima using the src/mvp.maxima script; see // documentation there for what the variables mean. const double sin_beta = sin(latitude_radians); const double cos_beta = cos(latitude_radians); const double sin_gamma = sin(-longitude_radians); const double cos_gamma = cos(-longitude_radians); constexpr double z = 12.0; // Compute the camera field of view. We would like this to be such that we can // see the full Earth. constexpr double kEarthRadius = 1.0; constexpr double tan_half_phix = (1.05 * kEarthRadius) / z; constexpr double tan_half_phiy = (1.05 * kEarthRadius) / z; constexpr double zn = z - kEarthRadius; constexpr double zf = z; // We can't see beyond the horizon. const double sin_beta_over_tan_half_phiy = sin_beta / tan_half_phiy; constexpr double zf_minus_zn = zf - zn; constexpr double clip_factor = (zf + zn) / zf_minus_zn; const double clip_factor_cos_beta = clip_factor * cos_beta; Eigen::Matrix4f mvp; mvp(0, 0) = sin_gamma / tan_half_phix; mvp(0, 1) = cos_gamma / tan_half_phix; mvp(0, 2) = 0; mvp(0, 3) = 0; mvp(1, 0) = -sin_beta_over_tan_half_phiy * cos_gamma; mvp(1, 1) = sin_beta_over_tan_half_phiy * sin_gamma; mvp(1, 2) = cos_beta / tan_half_phiy; mvp(1, 3) = 0; mvp(2, 0) = -clip_factor_cos_beta * cos_gamma; mvp(2, 1) = clip_factor_cos_beta * sin_gamma; mvp(2, 2) = -sin_beta * clip_factor; mvp(2, 3) = z * clip_factor - 2 * zf * zn / zf_minus_zn; mvp(3, 0) = -cos_beta * cos_gamma; mvp(3, 1) = cos_beta * sin_gamma; mvp(3, 2) = -sin_beta; mvp(3, 3) = z; return mvp; } void LoadMipmapsFromTableau(absl::Span webp, int max_width, int max_height, int column, gl::Texture2d& texture) { int x = column * max_width; if (x % 2 == 1) { // libwebp can't crop from an odd-numbered pixel. By convention, we've // pushed anything that would be on an odd boundary one pixel to the right // or down. ++x; } int y = 0; int width = max_width; int height = max_height; int level = 0; while (true) { std::vector mipmap = DecodeWebp(webp, x, y, width, height); texture.LoadSubimage(width, height, gl::Texture::PixelFormat::kRgb, gl::Texture::PixelType::kUnsignedByte, mipmap.data(), level); if (width == 1 && height == 1) { // We just loaded the last mipmap. break; } y += height; if (y % 2 == 1) { // libwebp can't crop from an odd-numbered pixel. See note above about // shifting one to the right or down. ++y; } width = std::max(width / 2, 1); height = std::max(height / 2, 1); ++level; } } void LoadStandardTexture(absl::Span webp, int column, gl::Texture2d& tex) { assert(tex.width() == 1024); assert(tex.height() == 512); LoadMipmapsFromTableau(webp, /*max_width=*/1024, /*max_height=*/512, column, tex); tex.SetWrap(gl::Texture2d::Dimension::kS, gl::Texture::Wrap::kRepeat); tex.SetWrap(gl::Texture2d::Dimension::kT, gl::Texture::Wrap::kClampToEdge); tex.SetMinFilter(gl::Texture::MinFilter::kLinearMipmapLinear); tex.SetMagFilter(gl::Texture::MagFilter::kLinear); } void ReportShaderWarnings(const char* description, absl::string_view log) noexcept { if (!log.empty()) { std::cerr << "glplanet: while " << description << ":\n" << log; } } } // namespace Scene::Scene(const Options& options) : planet_month_(0), planet_(gl::Texture::Format::kSrgb8, 1024, 512, 11), clouds_(gl::Texture::Format::kSrgb8, 1024, 512, 11), mvp_(ModelViewProjection(options.longitude_radians, options.latitude_radians)) { LoadStandardTexture(glplanet_resources::CloudsWebp(), /*column=*/0, clouds_); SetUpShaders(); LoadMesh(); } void Scene::SetUpShaders() { gl::VertexShader vertex_shader; vertex_shader.SetSource(glplanet_resources::VertexGlsl()); vertex_shader.Compile(); ReportShaderWarnings("compiling vertex shader", vertex_shader.compile_log()); program_.Attach(vertex_shader); gl::FragmentShader fragment_shader; fragment_shader.SetSource(glplanet_resources::FragmentGlsl()); fragment_shader.Compile(); ReportShaderWarnings("compiling fragment shader", fragment_shader.compile_log()); program_.Attach(fragment_shader); program_.SetFragmentDataLocation("out_color", 0); program_.Link(); ReportShaderWarnings("linking shader program", program_.link_log()); uniform_mvp_ = program_.active_uniform("mvp"); uniform_planet_ = program_.active_uniform("planet"); uniform_clouds_ = program_.active_uniform("clouds"); uniform_sun_direction_ = program_.active_uniform("sun_direction"); } void Scene::LoadMesh() { UvSphere mesh(kSectorsPerTurn, kSlices); vao_.SetVertexBuffer(gl::VertexBuffer(absl::MakeConstSpan(mesh.vertices), gl::Buffer::AccessFrequency::kStatic, gl::Buffer::AccessNature::kDraw)); vao_.SetVertexAttributeFormat( program_.active_vertex_attribute("cartesian_position"), 3, gl::VertexAttributeType::kFloat, offsetof(UvSphere::Coordinates, x)); vao_.SetVertexAttributeFormat( program_.active_vertex_attribute("vertex_texture_coordinate"), 2, gl::VertexAttributeType::kFloat, offsetof(UvSphere::Coordinates, u)); vao_.SetElementBuffer(gl::ElementBuffer(absl::MakeConstSpan(mesh.elements), gl::Buffer::AccessFrequency::kStatic, gl::Buffer::AccessNature::kDraw)); } void Scene::SetGlState() { gl::SetActiveShaderProgram(program_); gl::SetActiveShaderUniform(uniform_mvp_, mvp_); // TODO(bbarenblat@gmail.com): Assert that we have enough fragment shader // texture units (GL_MAX_TEXTURE_IMAGE_UNITS). gl::UseTextureUnit(0); gl::BindTexture2d(planet_); gl::SetActiveShaderUniform(uniform_planet_, 0); gl::UseTextureUnit(1); gl::BindTexture2d(clouds_); gl::SetActiveShaderUniform(uniform_clouds_, 1); gl::BindVertexArray(vao_); } void Scene::Draw(std::chrono::system_clock::time_point now) { if (date::month month = date::year_month_day(std::chrono::floor(now)) .month(); month != planet_month_) { // The month has changed since we last drew. Reload the planet texture to // reflect current snow levels. LoadStandardTexture(glplanet_resources::EarthWebp(), unsigned{month} - 1, planet_); planet_month_ = month; } const auto& [noon_longitude, noon_latitude] = HighNoonLocation(std::chrono::time_point_cast( date::clock_cast(now))); Eigen::Vector3f to_sun{ static_cast(cos(noon_latitude) * cos(noon_longitude)), static_cast(cos(noon_latitude) * sin(noon_longitude)), sinf(noon_latitude)}; gl::SetActiveShaderUniform(uniform_sun_direction_, to_sun); gl::DrawElements(vao_, gl::Primitive::kTriangles); } // namespace glplanet } // namespace glplanet