374 lines
17 KiB
C++
374 lines
17 KiB
C++
#include "Client.hpp"
|
|
|
|
#include "render/index.hpp"
|
|
#include "render/UI.hpp"
|
|
|
|
#include "control/InputMap.hpp"
|
|
#include "world/index.hpp"
|
|
#include "core/world/Elements.hpp"
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
#include "core/geometry/math.hpp"
|
|
#include <Tracy.hpp>
|
|
|
|
#undef LOG_PREFIX
|
|
#define LOG_PREFIX "Client: "
|
|
|
|
Client::Client(config::client::options& options, world::AbstractServerFactory* srvContainer):
|
|
options(options) { state.srvContainer = srvContainer; }
|
|
Client::~Client() {
|
|
LOG_W("Stopped");
|
|
}
|
|
|
|
void Client::run() {
|
|
if (!render::Load(window, options.preferVulkan, options.renderer, options.window))
|
|
return;
|
|
|
|
InputMap inputs(window.getPtr(), options.keys);
|
|
auto pipeline = render::Renderer::Get();
|
|
pipeline->LightInvDir = glm::normalize(glm::vec3(-.5f, 2, -2));
|
|
render::Renderer::Get()->loadUI(window);
|
|
|
|
const auto applyOptions = [&] (const render::UI::Actions& actions) {
|
|
if (actions && render::UI::Actions::FPS) {
|
|
window.setTargetFPS(options.window.targetFPS);
|
|
pipeline->setVSync(options.window.targetFPS < Window::MIN_FPS);
|
|
}
|
|
if (actions && render::UI::Actions::FullScreen) {
|
|
window.setFullscreen(options.window.fullscreen);
|
|
}
|
|
if(actions && render::UI::Actions::ClearColor) {
|
|
pipeline->setClearColor(options.renderer.clear_color);
|
|
}
|
|
if(actions && render::UI::Actions::RendererSharders) {
|
|
pipeline->reloadShaders(options.renderer.voxel);
|
|
}
|
|
if(actions && render::UI::Actions::RendererTextures) {
|
|
pipeline->reloadTextures(options.renderer.getTextures());
|
|
}
|
|
if(actions && render::UI::Actions::FillMode) {
|
|
pipeline->setFillMode(options.renderer.wireframe);
|
|
}
|
|
};
|
|
|
|
const auto play = [&] {
|
|
auto *localHandle = state.srvContainer && (!options.connection.has_value() || state.useSrv) ? state.srvContainer->run() : nullptr;
|
|
|
|
Controllable player(window.getPtr(), inputs, options.control);
|
|
Camera camera(&state.position.absolute.position, &player, options.camera);
|
|
|
|
auto world = world::client::Load(state.login, options.connection, localHandle, options.world, options.contouring);
|
|
state.contouring = world->getContouring();
|
|
world->onTeleport = [&](const world::relative_transform &rel, const world::transform &abs) {
|
|
state.position.relative = rel;
|
|
state.position.absolute = abs;
|
|
};
|
|
world->onMessage = [&](const std::string &text) {
|
|
const auto yellow = std::make_optional(0xFF43F5F5);
|
|
state.console.lines.push_back(state::state::line{text, text[0] == '>' ? yellow : std::nullopt});
|
|
};
|
|
|
|
do
|
|
{
|
|
window.startFrame();
|
|
FrameMark;
|
|
{ // Update
|
|
ZoneScopedN("Update");
|
|
static double lastTime = window.getTime();
|
|
const double partTime = window.getTime();
|
|
const float deltaTime = partTime - lastTime;
|
|
inputs.toggle(state.capture_mouse, Input::Mouse);
|
|
inputs.toggle(options.debugMenu.bar, Input::Debug);
|
|
|
|
player.capture(state.capture_mouse, !render::UI::IsFocus(), deltaTime);
|
|
if(player.velocity != glm::vec3(0))
|
|
{
|
|
auto &position = state.position.relative.relative.position;
|
|
const world::cell_pos old = position;
|
|
if (options.control.collide)
|
|
{
|
|
const auto rp = world->tryMovePlayer(state.position.relative, player.velocity);
|
|
state.position.relative.parent = rp.parent;
|
|
position = rp.relative.position;
|
|
}
|
|
else
|
|
{
|
|
position += player.velocity;
|
|
}
|
|
state.position.absolute = world->getAbsolute(state.position.relative);
|
|
|
|
//TODO: throttle
|
|
if(old != (world::cell_pos)position)
|
|
{
|
|
world->emit(world::action::Move(world::relative_pos{state.position.relative.parent, position}));
|
|
}
|
|
}
|
|
camera.update();
|
|
pipeline->lookFrom(camera);
|
|
// FIXME: Get from world
|
|
pipeline->LightInvDir = glm::vec3(glm::rotate(glm::mat4(1), deltaTime * .1f, glm::vec3(1, .5, .1)) * glm::vec4(pipeline->LightInvDir, 0));
|
|
|
|
{
|
|
state.look_at = world->raycast(camera.getRay());
|
|
state.can_fill = [&] {
|
|
if(const auto target = std::get_if<world::Universe::voxel_query_target>(&state.look_at)) {
|
|
if (world::Elements::Is<world::Elements::Type::Area>(target->pos.node)) {
|
|
const auto &tool = options.editor.tool;
|
|
return world->isRangeFree(target->pos, world::action::Volume{tool.shape, (uint8_t)tool.radius});
|
|
}
|
|
}
|
|
return false;
|
|
}();
|
|
}
|
|
if (state.capture_mouse)
|
|
{
|
|
if (const auto target = std::get_if<world::Universe::voxel_query_target>(&state.look_at))
|
|
{
|
|
ZoneScopedN("Edit");
|
|
const auto &tool = options.editor.tool;
|
|
constexpr world::Voxel::material_t AIR = 0;
|
|
if (inputs.isPressing(Mouse::Left))
|
|
world->emit(world::action::FillShape(
|
|
target->pos, world::Voxel(AIR, tool.emptyAir * world::Voxel::DENSITY_MAX), tool.shape, tool.radius));
|
|
else if (const auto voxel = world::Voxel(tool.material, world::Voxel::DENSITY_MAX);
|
|
inputs.isPressing(Mouse::Right) && (state.can_fill || !voxel.is_solid()))
|
|
world->emit(world::action::FillShape(
|
|
target->pos, voxel, tool.shape, tool.radius));
|
|
} //TODO: else interact
|
|
if (inputs.isDown(Input::Throw))
|
|
{
|
|
//FIXME: register entity type world->addEntity(entity_id(0), {state.position * options.voxel_density, glm::vec3(10, 0, 0)});
|
|
}
|
|
}
|
|
world->update(state.position.absolute.position, deltaTime);
|
|
inputs.saveKeys();
|
|
lastTime = partTime;
|
|
}
|
|
|
|
{
|
|
ZoneScopedN("UI");
|
|
const auto actions = render::UI::Get()->draw(options, state, reports);
|
|
applyOptions(actions);
|
|
if(actions && render::UI::Actions::World)
|
|
{
|
|
//FIXME: server options world->setOptions(options.world);
|
|
}
|
|
if(actions && render::UI::Actions::Camera)
|
|
{
|
|
camera.setOptions(options.camera);
|
|
}
|
|
if(actions && render::UI::Actions::Control)
|
|
{
|
|
player.setOptions(options.control);
|
|
}
|
|
if(actions && render::UI::Actions::Message)
|
|
{
|
|
char *s = state.console.buffer.data();
|
|
//Strtrim(s);
|
|
//if (s[0])
|
|
// ExecCommand(s);
|
|
world->emit(world::action::Message(s));
|
|
strcpy(s, "");
|
|
}
|
|
if (inputs.isDown(Input::Quit))
|
|
{
|
|
state.playing = false;
|
|
}
|
|
}
|
|
renderFrame(*pipeline, options.culling >= 0 ? std::make_optional(camera.getFrustum()) : std::nullopt,
|
|
options.culling > 0 ? std::make_optional(player.getAngles()): std::nullopt, *world);
|
|
|
|
{ // Swap buffers
|
|
ZoneScopedN("Swap");
|
|
pipeline->swapBuffer(window);
|
|
inputs.poll();
|
|
}
|
|
|
|
window.waitTargetFPS();
|
|
} while (!window.shouldClose() && world->isRunning() && state.playing);
|
|
|
|
player.capture(false, false, 0); // Restore mouse
|
|
options.contouring = state.contouring->getOptions();
|
|
state.contouring = nullptr;
|
|
world.reset();
|
|
};
|
|
|
|
Controllable player(window.getPtr(), inputs, options.control);
|
|
Camera camera(&state.position.absolute.position, &player, options.camera);
|
|
do { // Main menu
|
|
window.startFrame();
|
|
inputs.saveKeys();
|
|
player.capture(true, false, 0, true);
|
|
camera.update();
|
|
pipeline->lookFrom(camera);
|
|
const auto actions = render::UI::Get()->draw(options, state, reports);
|
|
applyOptions(actions);
|
|
if (actions && render::UI::Actions::Quit) {
|
|
window.close();
|
|
}
|
|
if (state.playing) {
|
|
play();
|
|
state.playing = false;
|
|
}
|
|
pipeline->beginFrame();
|
|
{ //MAYBE: menu pipeline
|
|
pipeline->beginTerrainPass(true);
|
|
pipeline->beginUniquePass();
|
|
pipeline->beginInstancedPass();
|
|
pipeline->beginIndicatorPass();
|
|
if (options.renderer.voxel.transparency) {
|
|
pipeline->beginTerrainPass(false);
|
|
}
|
|
pipeline->postProcess();
|
|
}
|
|
render::UI::Get()->render();
|
|
pipeline->endFrame();
|
|
pipeline->swapBuffer(window);
|
|
inputs.poll();
|
|
window.waitTargetFPS();
|
|
} while (!window.shouldClose());
|
|
|
|
inputs.exportKeys(options.keys);
|
|
|
|
render::Renderer::Unload();
|
|
window.destroy();
|
|
}
|
|
|
|
void Client::renderFrame(render::Renderer& pipeline, const std::optional<geometry::Frustum>& frustum, const std::optional<std::pair<float, float>>& angles, const world::client::Universe& world) {
|
|
ZoneScopedN("Render");
|
|
pipeline.beginFrame();
|
|
|
|
reports.models_count = 0;
|
|
reports.tris_count = 0;
|
|
const auto cull = [&]() -> culler {
|
|
if (frustum.has_value())
|
|
return {frustum.value()};
|
|
else if (angles.has_value()) {
|
|
std::vector<glm::vec3> occlusion;
|
|
const auto ratio = options.culling * 2;
|
|
occlusion.reserve(glm::pow2(ratio * 2 - 1));
|
|
const auto [ch, cv] = angles.value();
|
|
const auto max_v = tan(options.camera.fov / 2.), max_h = Window::RATIO * max_v;
|
|
for(int iv = -ratio + 1; iv < ratio; iv++) {
|
|
const auto v = cv + max_v * iv / ratio;
|
|
for(int ih = -ratio + 1; ih < ratio; ih++) {
|
|
const auto h = ch + max_h * ih / ratio;
|
|
occlusion.emplace_back(cos(v) * sin(h), sin(v), cos(v) * cos(h));
|
|
}
|
|
}
|
|
return {occlusion};
|
|
} else
|
|
return {};
|
|
}();
|
|
const auto offset = Camera::Split(state.position.absolute.position).first;
|
|
|
|
renderTerrainPass(pipeline, cull, offset, true); // Solid areas
|
|
{
|
|
const auto self = world.getPlayerId();
|
|
const auto elements = world.getElements();
|
|
{ // Unique elements (parts)
|
|
const auto pass = pipeline.beginUniquePass();
|
|
const auto draw = [&](const glm::mat4& model, render::Model *const buffer) {
|
|
reports.models_count++;
|
|
reports.tris_count += pass(buffer, model);
|
|
};
|
|
state.contouring->getUniqueModels(draw, *elements, frustum, offset);
|
|
}
|
|
{ // Instanced elements (instances)
|
|
const auto pass = pipeline.beginInstancedPass();
|
|
const auto draw = [&](const std::vector<glm::mat4> &models, render::Model *const buffer) {
|
|
reports.models_count += models.size();
|
|
reports.tris_count += pass(buffer, models);
|
|
};
|
|
state.contouring->getInstancedModels(draw, *elements, frustum, offset, self);
|
|
}
|
|
}
|
|
{ // Indicators
|
|
const auto pass = pipeline.beginIndicatorPass();
|
|
constexpr float ALPHA = .5;
|
|
if(const auto target = std::get_if<world::Universe::voxel_query_target>(&state.look_at)) {
|
|
const auto tf = world.getAbsolute(world::relative_transform{target->pos.node, world::transform(target->pos.voxel)});
|
|
const auto box = faabb::FromCenter(glm::vec3(0), glm::vec3(options.editor.tool.radius));
|
|
const auto obbMat = glm::scale(glm::translate(glm::translate(glm::mat4(1),
|
|
glm::vec3(tf.position - (world::world_pos)offset)) *
|
|
glm::toMat4(tf.rotation), box.min), box.getSize());
|
|
const auto color = [&] {
|
|
if (world::Elements::Is<world::Elements::Type::Area>(target->pos.node)) {
|
|
if (state.can_fill)
|
|
return glm::vec4(1, 1, 1, ALPHA);
|
|
else if (world::Voxel::IsSolid(options.editor.tool.material))
|
|
return glm::vec4(1, 0, 0, ALPHA);
|
|
else
|
|
return glm::vec4(1, .5, 0, ALPHA);
|
|
|
|
}
|
|
return glm::vec4(.2, .2, 1, ALPHA);
|
|
} ();
|
|
reports.models_count++;
|
|
reports.tris_count += pass(obbMat, options.editor.tool.shape, color);
|
|
}
|
|
// TODO: handle ray_element_target
|
|
// MAYBE: hover entity
|
|
if (state.indicators.aabb || state.indicators.obb) {
|
|
const auto elements = world.getElements();
|
|
elements->nodes.iter([&](const world::node_id&, const world::Node& node) {
|
|
if (!node.content)
|
|
return;
|
|
|
|
const auto& tf = node.absolute;
|
|
const auto box = faabb::From(node.content->getBoundingBox(*elements));
|
|
if (state.indicators.obb) {
|
|
reports.models_count++;
|
|
const auto obbMat = glm::scale(glm::translate(glm::translate(glm::mat4(1),
|
|
glm::vec3(tf.position - (world::world_pos)offset)) *
|
|
glm::toMat4(tf.rotation), box.min), box.getSize());
|
|
reports.tris_count += pass(obbMat, world::action::Shape::Cube, glm::vec4(1, 1, 1, ALPHA));
|
|
}
|
|
if (state.indicators.aabb) {
|
|
reports.models_count++;
|
|
const auto col = box.rotate(tf.rotation);
|
|
const auto aabbMat = glm::scale(glm::translate(glm::mat4(1),
|
|
glm::vec3(tf.position - (world::world_pos)offset) + col.min), col.getSize());
|
|
reports.tris_count += pass(aabbMat, world::action::Shape::Cube, glm::vec4(ALPHA));
|
|
}
|
|
});
|
|
}
|
|
if (state.indicators.chunk) {
|
|
const auto elements = world.getElements();
|
|
for (const auto& ref: elements->areas) {
|
|
const auto id = elements->withFlag(ref);
|
|
const auto node = elements->findArea(id.val);
|
|
if (state.indicators.chunk) {
|
|
for (const auto& ck: node->get()->getChunks()) {
|
|
const auto pos = node->absolute.computeChild(glm::multiply(ck.first));
|
|
if (glm::length2(glm::vec3(pos - state.position.absolute.position)) > glm::pow2(world::CHUNK_LENGTH * 2))
|
|
continue;
|
|
|
|
reports.models_count++;
|
|
const auto obbMat = glm::scale(glm::translate(glm::mat4(1), glm::vec3(pos - (world::world_pos)offset)) *
|
|
glm::toMat4(node->absolute.rotation), glm::vec3(world::CHUNK_LENGTH));
|
|
reports.tris_count += pass(obbMat, world::action::Shape::Cube, glm::vec4(1, .5, 0, ALPHA));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (options.renderer.voxel.transparency) {
|
|
renderTerrainPass(pipeline, cull, offset, false); // Transparent areas
|
|
}
|
|
pipeline.postProcess();
|
|
render::UI::Get()->render();
|
|
pipeline.endFrame();
|
|
}
|
|
void Client::renderTerrainPass(render::Renderer& pipeline, const culler& culler, const world::cell_pos& offset, bool solid) {
|
|
const auto pass = pipeline.beginTerrainPass(solid);
|
|
const auto draw = [&](glm::mat4 model, render::LodModel *const buffer, const contouring::Abstract::area_info &area, const world::voxel_pos &area_offset) {
|
|
reports.models_count++;
|
|
reports.tris_count += pass(buffer, model, /*big precision lost*/glm::vec4(area_offset, area.radius), area.curvature);
|
|
};
|
|
if (const auto occlusion = std::get_if<std::vector<glm::vec3>>(&culler)) {
|
|
state.contouring->getTerrainModels(draw, state.position.absolute.position, options.camera.far_dist, *occlusion, offset, solid);
|
|
} else {
|
|
const auto frustum = std::get_if<geometry::Frustum>(&culler);
|
|
state.contouring->getTerrainModels(draw, frustum ? std::make_optional(*frustum) : std::nullopt, offset, solid);
|
|
}
|
|
} |