summaryrefslogtreecommitdiff
path: root/src/scene.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/scene.cc')
-rw-r--r--src/scene.cc258
1 files changed, 258 insertions, 0 deletions
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 <math.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include <Eigen/Core>
+#include <algorithm>
+#include <chrono>
+#include <iostream>
+#include <vector>
+
+#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<const uint8_t> 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<uint8_t> 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<const uint8_t> 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<std::chrono::days>(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<std::chrono::milliseconds>(
+ date::clock_cast<date::tai_clock>(now)));
+ Eigen::Vector3f to_sun{
+ static_cast<float>(cos(noon_latitude) * cos(noon_longitude)),
+ static_cast<float>(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