From d0e18bdb7924c71cdca8dd983711171d87ef28be Mon Sep 17 00:00:00 2001 From: Benjamin Barenblat Date: Mon, 17 Jan 2022 23:12:32 -0500 Subject: glplanet, an OpenGL-based planetary renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit glplanet draws Earth like it currently appears from space, putting nighttime areas in shadow and daytime areas in light. It’s modeled after Xplanet (http://xplanet.sourceforge.net/), but whereas Xplanet is entirely a CPU-resident program, glplanet draws using OpenGL. It’s thus much less resource-intensive, particularly when using high-resolution textures. --- src/scene.cc | 258 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 src/scene.cc (limited to 'src/scene.cc') diff --git a/src/scene.cc b/src/scene.cc new file mode 100644 index 0000000..4f3c9a7 --- /dev/null +++ b/src/scene.cc @@ -0,0 +1,258 @@ +// 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 -- cgit v1.2.3