mirror of
https://github.com/retroplasma/earth-reverse-engineering.git
synced 2025-12-08 18:02:00 +00:00
494 lines
15 KiB
C++
494 lines
15 KiB
C++
#include <fstream>
|
|
#include <sys/stat.h>
|
|
#ifdef _WIN32
|
|
#include <direct.h>
|
|
#endif
|
|
|
|
#include "SDL.h"
|
|
#ifdef EMSCRIPTEN
|
|
# include <GLES2/gl2.h>
|
|
# include "emscripten.h"
|
|
#else
|
|
# if defined(_WIN32) || defined(__linux__)
|
|
# include <glad/glad.h>
|
|
# include "gl2/src/glad.c"
|
|
# else
|
|
# define GL_GLEXT_PROTOTYPES
|
|
# endif
|
|
#endif
|
|
|
|
#include <math.h>
|
|
#include <Eigen/Dense>
|
|
using namespace Eigen;
|
|
#include "rocktree_util.h"
|
|
|
|
#include <SDL_opengl.h>
|
|
|
|
SDL_Window* sdl_window;
|
|
|
|
static rocktree_t *_planetoid = NULL;
|
|
|
|
void loadPlanet() {
|
|
auto planetoid = new rocktree_t();
|
|
planetoid->downloaded = false;
|
|
_planetoid = planetoid;
|
|
|
|
getPlanetoid([=](std::unique_ptr<PlanetoidMetadata> _metadata) {
|
|
if (!_metadata) fprintf(stderr, "%s", "no planetoid\n"), abort();
|
|
populatePlanetoid(planetoid, std::move(_metadata));
|
|
|
|
auto bulk = planetoid->root_bulk;
|
|
assert(bulk->dl_state == dl_state_stub);
|
|
bulk->setStartedDownloading();
|
|
getBulk(bulk->request, bulk, [=](auto _) { /* todo rm cb */ });
|
|
});
|
|
}
|
|
|
|
void initGL(gl_ctx_t &ctx) {
|
|
glEnable(GL_DEPTH_TEST);
|
|
glEnable(GL_CULL_FACE);
|
|
ctx.program = makeShader(
|
|
"uniform mat4 transform;"
|
|
"uniform vec2 uv_offset;"
|
|
"uniform vec2 uv_scale;"
|
|
"uniform bool octant_mask[8];"
|
|
"attribute vec3 position;"
|
|
"attribute float octant;"
|
|
"attribute vec2 texcoords;"
|
|
"varying vec2 v_texcoords;"
|
|
"void main() {"
|
|
" float mask = octant_mask[int(octant)] ? 0.0 : 1.0;"
|
|
" v_texcoords = (texcoords + uv_offset) * uv_scale * mask;"
|
|
" gl_Position = transform * vec4(position, 1.0) * mask;"
|
|
"}",
|
|
|
|
"#ifdef GL_ES\n"
|
|
"precision mediump float;\n"
|
|
"#endif\n"
|
|
"uniform sampler2D texture;"
|
|
"varying vec2 v_texcoords;"
|
|
"void main() {"
|
|
" gl_FragColor = vec4(texture2D(texture, v_texcoords).rgb, 1.0);"
|
|
"}"
|
|
);
|
|
glUseProgram(ctx.program);
|
|
ctx.transform_loc = glGetUniformLocation(ctx.program, "transform");
|
|
ctx.uv_offset_loc = glGetUniformLocation(ctx.program, "uv_offset");
|
|
ctx.uv_scale_loc = glGetUniformLocation(ctx.program, "uv_scale");
|
|
ctx.octant_mask_loc = glGetUniformLocation(ctx.program, "octant_mask");
|
|
ctx.texture_loc = glGetUniformLocation(ctx.program, "texture");
|
|
ctx.position_loc = glGetAttribLocation(ctx.program, "position");
|
|
ctx.octant_loc = glGetAttribLocation(ctx.program, "octant");
|
|
ctx.texcoords_loc = glGetAttribLocation(ctx.program, "texcoords");
|
|
|
|
glEnableVertexAttribArray(ctx.position_loc);
|
|
glEnableVertexAttribArray(ctx.octant_loc);
|
|
glEnableVertexAttribArray(ctx.texcoords_loc);
|
|
}
|
|
|
|
Uint64 NOW = SDL_GetPerformanceCounter();
|
|
Uint64 LAST = 0;
|
|
double deltaTime = 0;
|
|
|
|
void drawPlanet(gl_ctx_t &ctx) {
|
|
auto planetoid = _planetoid;
|
|
if (!planetoid) return;
|
|
if (!planetoid->downloaded) return;
|
|
if (planetoid->root_bulk->dl_state != dl_state_downloaded) return;
|
|
auto current_bulk = planetoid->root_bulk;
|
|
auto planet_radius = planetoid->radius;
|
|
|
|
Matrix4d projection, viewprojection;
|
|
|
|
int width, height;
|
|
SDL_GL_GetDrawableSize(sdl_window, &width, &height);
|
|
glViewport(0, 0, width, height);
|
|
auto sky = 0x83b5fc;
|
|
glClearColor((sky>>16 & 0xff) / 255.0f, (sky>>8 & 0xff) / 255.0f, (sky & 0xff) / 255.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|
|
|
auto mouse_state = SDL_GetMouseState(NULL, NULL);
|
|
auto state = SDL_GetKeyboardState(NULL);
|
|
auto key_up_pressed = state[SDL_SCANCODE_W];
|
|
auto key_left_pressed = state[SDL_SCANCODE_A];
|
|
auto key_down_pressed = state[SDL_SCANCODE_S];
|
|
auto key_right_pressed = state[SDL_SCANCODE_D];
|
|
auto key_boost_pressed = state[SDL_SCANCODE_LSHIFT] || state[SDL_SCANCODE_RSHIFT];
|
|
auto mouse_pressed = mouse_state & SDL_BUTTON(SDL_BUTTON_LEFT);
|
|
|
|
// from lat/lon
|
|
//static Vector3d ecef = { ...https://www.oc.nps.edu/oc2902w/coord/llhxyz.htm };
|
|
//static auto ecef_norm = ecef.normalized();
|
|
//static Vector3d eye = (ecef_norm * (planet_radius + 10000));
|
|
|
|
// nyc
|
|
static Vector3d eye = { 1329866.230289, -4643494.267515, 4154677.131562 };
|
|
static Vector3d direction = { 0.219862, 0.419329, 0.312226 };
|
|
|
|
// print position every 2 seconds
|
|
{
|
|
static double ms = 0;
|
|
ms += deltaTime;
|
|
if (ms > 2000) {
|
|
ms = 0;
|
|
printf("pos: %f %f %f, dir: %f %f %f\n", eye.x(), eye.y(), eye.z(), direction.x(), direction.y(), direction.z());
|
|
}
|
|
}
|
|
|
|
// up is the vec from the planetoid's center towards the sky
|
|
auto up = eye.normalized();
|
|
|
|
// projection
|
|
float aspect_ratio = (float)width / (float)height;
|
|
float fov = 0.25f * (float)M_PI;
|
|
auto altitude = eye.norm() - planet_radius;
|
|
auto horizon = sqrt( altitude * (2*planet_radius + altitude) );
|
|
auto near = horizon > 370000 ? altitude / 2 : 50;
|
|
auto far = horizon;
|
|
if (near >= far) near = far - 1;
|
|
if (isnan(far) || far < near) far = near + 1;
|
|
projection = perspective(fov, aspect_ratio, near, far);
|
|
|
|
// rotation
|
|
int mouse_x, mouse_y;
|
|
SDL_GetRelativeMouseState(&mouse_x, &mouse_y);
|
|
double yaw = mouse_x * 0.001;
|
|
double pitch = -mouse_y * 0.001;
|
|
auto overhead = direction.dot(-up);
|
|
if ((overhead > 0.99 && pitch < 0) || (overhead < -0.99 && pitch > 0))
|
|
pitch = 0;
|
|
auto pitch_axis = direction.cross(up);
|
|
auto yaw_axis = direction.cross(pitch_axis);
|
|
pitch_axis.normalize();
|
|
AngleAxisd roll_angle(0, Vector3d::UnitZ());
|
|
AngleAxisd yaw_angle(yaw, yaw_axis);
|
|
AngleAxisd pitch_angle(pitch, pitch_axis);
|
|
auto quat = roll_angle * yaw_angle * pitch_angle;
|
|
auto rotation = quat.matrix();
|
|
direction = (rotation * direction).normalized();
|
|
|
|
// movement
|
|
auto speed_amp = fmin(2600, powf(fmax(0, (altitude - 500)/10000)+1, 1.337)) / 6;
|
|
auto mag = 100*(deltaTime/17.0)*(1+key_boost_pressed*4) * speed_amp;
|
|
auto sideways = direction.cross(up).normalized();
|
|
auto forwards = direction * mag;
|
|
auto backwards = -direction * mag;
|
|
auto left = -sideways * mag;
|
|
auto right = sideways * mag;
|
|
auto new_eye = eye + key_up_pressed * forwards
|
|
+ key_down_pressed * backwards
|
|
+ key_left_pressed * left
|
|
+ key_right_pressed * right;
|
|
auto pot_altitude = new_eye.norm() - planet_radius;
|
|
if (pot_altitude < 1000 * 1000 * 10) {
|
|
eye = new_eye;
|
|
}
|
|
|
|
auto view = lookAt(eye, eye + direction, up);
|
|
viewprojection = projection * view;
|
|
|
|
auto frustum_planes = getFrustumPlanes(viewprojection); // for obb culling
|
|
|
|
const std::string octs[] = { "0", "1", "2", "3", "4", "5", "6", "7" };
|
|
std::vector<std::pair<std::string, rocktree_t::bulk_t *>> valid = { std::make_pair("", current_bulk) };
|
|
decltype(valid) next_valid;
|
|
std::map<std::string, rocktree_t::node_t *> potential_nodes;
|
|
//std::multimap<double, rocktree_t::node_t *> dist_nodes;
|
|
|
|
// todo: improve download order
|
|
// todo: abort emscripten_fetch_close() https://emscripten.org/docs/api_reference/fetch.html
|
|
// and/or emscripten coroutine fetch semaphore
|
|
// todo: purge branches less aggressively
|
|
// todo: workers instead of shared mem https://emscripten.org/docs/api_reference/emscripten.h.html#worker-api
|
|
|
|
std::map<std::string, rocktree_t::bulk_t *> potential_bulks;
|
|
|
|
// node culling and level of detail using breadth-first search
|
|
for (;;) {
|
|
for(auto cur2 : valid) {
|
|
auto cur = cur2.first;
|
|
auto bulk = cur2.second;
|
|
|
|
if (cur.size() > 0 && cur.size() % 4 == 0) {
|
|
auto rel = cur.substr (floor((cur.size() - 1) / 4) * 4, 4);
|
|
auto bulk_kv = bulk->bulks.find(rel);
|
|
auto has_bulk = bulk_kv != bulk->bulks.end();
|
|
if (!has_bulk) continue;
|
|
auto b = bulk_kv->second.get();
|
|
potential_bulks[cur] = b;
|
|
if (b->dl_state == dl_state_stub) {
|
|
b->setStartedDownloading();
|
|
getBulk(b->request, b, [=](auto) {});
|
|
}
|
|
if (b->dl_state != dl_state_downloaded) continue;
|
|
bulk = b;
|
|
}
|
|
potential_bulks[cur] = bulk;
|
|
|
|
for(auto o : octs) {
|
|
auto nxt = cur + o;
|
|
auto nxt_rel = nxt.substr (floor((nxt.size() - 1) / 4) * 4, 4);
|
|
auto node_kv = bulk->nodes.find(nxt_rel);
|
|
if (node_kv == bulk->nodes.end()) // node at "nxt" doesn't exist
|
|
continue;
|
|
auto node = node_kv->second.get();
|
|
|
|
// cull outside frustum using obb
|
|
// todo: check if it could cull more
|
|
if (obb_frustum_outside == classifyObbFrustum(&node->obb, frustum_planes)) {
|
|
continue;
|
|
}
|
|
|
|
// level of detail
|
|
/*{
|
|
auto obb_center = node->obb.center;
|
|
auto obb_max_diameter = fmax(fmax(node->obb.extents[0], node->obb.extents[1]), node->obb.extents[2]);
|
|
|
|
auto t = Affine3d().Identity();
|
|
t.translate(Vector3d(obb_center.x(), obb_center.y(), obb_center.z()));
|
|
t.scale(obb_max_diameter);
|
|
Matrix4d viewprojection_d;
|
|
for(auto i = 0; i < 16; i++) viewprojection_d.data()[i] = viewprojection.data()[i];
|
|
auto m = viewprojection_d * t;
|
|
auto s = m(3, 3);
|
|
if (s < 0) s = -s; // ?
|
|
auto diameter_in_clipspace = 2 * (obb_max_diameter / s); // *2 because clip space is -1 to +1
|
|
auto amplify = 4; // todo: meters per texel
|
|
if (diameter_in_clipspace < 0.5 / amplify) {
|
|
continue;
|
|
}
|
|
}*/
|
|
|
|
{
|
|
auto t = Affine3d().Identity();
|
|
t.translate(eye + (eye-node->obb.center).norm() * direction);
|
|
auto m = viewprojection * t;
|
|
auto s = m(3, 3);
|
|
auto texels_per_meter = 1.0f / node->meters_per_texel;
|
|
auto wh = 768; // width < height ? width : height;
|
|
auto r = (2.0*(1.0/s)) * wh;
|
|
if (texels_per_meter > r) continue;
|
|
}
|
|
|
|
next_valid.push_back(std::make_pair(nxt, bulk));
|
|
|
|
if (node->can_have_data) {
|
|
potential_nodes[nxt] = node;
|
|
//auto d = (node->obb.center - eye).squaredNorm();
|
|
//dist_nodes[d] = node;
|
|
//dist_nodes.insert(std::make_pair (d, node));
|
|
}
|
|
}
|
|
}
|
|
if (next_valid.size() == 0) break;
|
|
valid = next_valid;
|
|
next_valid.clear();
|
|
}
|
|
|
|
for (auto kv = potential_nodes.begin(); kv != potential_nodes.end(); ++kv) { // normal order
|
|
//for (auto kv = potential_nodes.rbegin(); kv != potential_nodes.rend(); ++kv) { // reverse order
|
|
//for (auto kv = dist_nodes.rbegin(); kv != dist_nodes.rend(); ++kv) { // reverse order
|
|
//for (auto kv = dist_nodes.begin(); kv != dist_nodes.end(); ++kv) { // normal order
|
|
auto node = kv->second;
|
|
if (node->dl_state == dl_state_stub) {
|
|
node->setStartedDownloading();
|
|
getNode(node->request, node, [node](auto) {});
|
|
}
|
|
}
|
|
|
|
// unbuffer and obsolete nodes
|
|
std::vector<rocktree_t::bulk_t*> x = {current_bulk};
|
|
auto buf_cnt = 0, obs_n_cnt = 0, total_n = 0;
|
|
while(!x.empty()) {
|
|
auto cur_bulk = x[0]; x.erase(x.begin());
|
|
// prepare next iteration
|
|
for (auto &kv : cur_bulk->bulks) {
|
|
auto b = kv.second.get();
|
|
if (b->dl_state != dl_state_downloaded) continue;
|
|
x.emplace(x.begin(), b);
|
|
}
|
|
// current iteration
|
|
for (auto &kv : cur_bulk->nodes) {
|
|
auto n = kv.second.get();
|
|
if (n->dl_state != dl_state_downloaded) continue;
|
|
|
|
// just count buffers
|
|
for (auto &m : n->meshes) { if (m.buffered) { buf_cnt++; break;}}
|
|
|
|
total_n++;
|
|
auto p = n->request.node_key().path();
|
|
auto has = potential_nodes.find(p) != potential_nodes.end();
|
|
if (!has) {
|
|
// node is obsolete
|
|
obs_n_cnt++;
|
|
|
|
// unbuffer
|
|
for (auto &mesh : n->meshes) {
|
|
if (mesh.buffered) unbufferMesh(mesh);
|
|
}
|
|
// clean up
|
|
n->_data = nullptr;
|
|
n->matrix_globe_from_mesh = Matrix4d::Zero();
|
|
n->meshes.clear();
|
|
n->setDeleted();
|
|
}
|
|
}
|
|
}
|
|
|
|
// post order dfs purge obsolete bulks
|
|
auto total_b = 0, obs_b_cnt = 0;
|
|
std::function<void(rocktree_t::bulk_t *)> po;
|
|
po = [&po, &potential_bulks, &obs_b_cnt, &total_b](rocktree_t::bulk_t * b){
|
|
for (auto &kv : b->bulks){
|
|
auto b = kv.second.get();
|
|
if (b->dl_state == dl_state_downloaded)
|
|
po(b);
|
|
}
|
|
total_b++;
|
|
auto p = b->request.node_key().path();
|
|
auto has = potential_bulks.find(p) != potential_bulks.end();
|
|
if (!has) {
|
|
if (b->busy_ctr == 0) {
|
|
b->nodes.clear();
|
|
b->bulks.clear();
|
|
b->setDeleted();
|
|
}
|
|
}
|
|
};
|
|
|
|
po(current_bulk);
|
|
|
|
// log stuff about buffers
|
|
{
|
|
static double ms = 0;
|
|
ms += deltaTime;
|
|
if (ms > 2000) {
|
|
ms = 0;
|
|
printf("buffered: %d, tot_n: %d, tot_b: %d, pot_n: %lu, pot_b: %lu, obs n: %d, obs b: %d\n",
|
|
buf_cnt, total_n, total_b, potential_nodes.size(), potential_bulks.size(), obs_n_cnt, obs_b_cnt
|
|
);
|
|
}
|
|
}
|
|
|
|
// 8-bit octant mask flags of nodes
|
|
std::map<std::string, uint8_t> mask_map;
|
|
|
|
for (auto kv = potential_nodes.rbegin(); kv != potential_nodes.rend(); ++kv) { // reverse order
|
|
auto full_path = kv->first;
|
|
auto node = kv->second;
|
|
auto level = strlen(full_path.c_str());
|
|
assert(level > 0);
|
|
assert(node->can_have_data);
|
|
if (node->dl_state != dl_state_downloaded) continue;
|
|
|
|
// set octant mask of previous node
|
|
auto octant = (int)(full_path[level - 1] - '0');
|
|
auto prev = full_path.substr (0, level - 1);
|
|
mask_map[prev] |= 1 << octant;
|
|
|
|
// skip if node is masked completely
|
|
if (mask_map[full_path] == 0xff) continue;
|
|
|
|
// float transform matrix
|
|
Matrix4d transform = viewprojection * node->matrix_globe_from_mesh;
|
|
Matrix4f transform_float;
|
|
for(auto i = 0; i < 16; ++i) transform_float.data()[i] = (float)(transform.data()[i]);
|
|
|
|
// buffer, bind, draw
|
|
glUniformMatrix4fv(ctx.transform_loc, 1, GL_FALSE, transform_float.data());
|
|
for (auto &mesh : node->meshes) {
|
|
if (!mesh.buffered) bufferMesh(mesh);
|
|
bindAndDrawMesh(mesh, mask_map[full_path], ctx);
|
|
}
|
|
//bufs[full_path] = node;
|
|
}
|
|
}
|
|
|
|
bool quit = false;
|
|
void mainloop(gl_ctx_t &ctx) {
|
|
SDL_Event sdl_event;
|
|
while (SDL_PollEvent(&sdl_event)) {
|
|
switch (sdl_event.type) {
|
|
case SDL_QUIT:
|
|
quit = true;
|
|
break;
|
|
case SDL_KEYDOWN:
|
|
if (sdl_event.key.keysym.sym == SDLK_ESCAPE) quit = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
LAST = NOW;
|
|
NOW = SDL_GetPerformanceCounter();
|
|
deltaTime = (double)((NOW - LAST)*1000 / (double)SDL_GetPerformanceFrequency());
|
|
|
|
drawPlanet(ctx);
|
|
SDL_GL_SwapWindow(sdl_window);
|
|
}
|
|
|
|
int main(int argc, char* argv[]) {
|
|
|
|
int video_width = 1024;
|
|
int video_height = 768;
|
|
|
|
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
|
|
fprintf(stderr, "Couldn't init SDL2: %s\n", SDL_GetError());
|
|
exit(1);
|
|
}
|
|
#ifdef EMSCRIPTEN
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
|
|
SDL_GL_CONTEXT_PROFILE_ES);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
|
#else
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
|
|
SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
|
|
#endif
|
|
sdl_window = SDL_CreateWindow("Earth Client",
|
|
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
|
video_width, video_height,
|
|
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
|
|
if (!sdl_window) {
|
|
fprintf(stderr, "Couldn't create window: %s\n", SDL_GetError());
|
|
exit(1);
|
|
}
|
|
SDL_GLContext gl_context = SDL_GL_CreateContext(sdl_window);
|
|
if (!gl_context) {
|
|
fprintf(stderr, "Couldn't create OpenGL context: %s\n", SDL_GetError());
|
|
exit(1);
|
|
}
|
|
SDL_GL_SetSwapInterval(1);
|
|
|
|
#if defined(_WIN32) || defined(__linux__)
|
|
// init glad
|
|
if (!gladLoadGL()) {
|
|
fprintf(stderr, "Failed to init glad\n");
|
|
exit(1);
|
|
}
|
|
#endif
|
|
|
|
auto ctx = new gl_ctx_t();
|
|
|
|
initGL(*ctx);
|
|
loadPlanet();
|
|
|
|
SDL_SetRelativeMouseMode(SDL_TRUE);
|
|
|
|
#ifdef EMSCRIPTEN
|
|
emscripten_set_main_loop_arg([](void* _ctx){
|
|
auto ctx = (gl_ctx_t *)_ctx;
|
|
mainloop(*ctx);
|
|
}, (void *)ctx, 0, 1);
|
|
#else
|
|
while (!quit) mainloop(*ctx);
|
|
#endif
|
|
|
|
SDL_GL_DeleteContext(gl_context);
|
|
SDL_DestroyWindow(sdl_window);
|
|
SDL_Quit();
|
|
delete ctx;
|
|
|
|
return 0;
|
|
} |