mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 00:29:19 +02:00
feat(editor): -generate-nodebank-previews mode: png previews of the node bank models, or of the entries given with -entries, drawn offscreen and without a window by the full renderer: square, -resolution 1024 by default, the model in perspective from the front, side and above, fitted to the image less -margin pixels (64 by default) on white background, lit, with materials and with self shadows unless -no-shadows; saved in textures/previews/nodebank under the path of the model with the skin; software rendering of mesa when the graphics driver gives no gl 3.3 or with -software
This commit is contained in:
@@ -112,6 +112,7 @@ set(SOURCES
|
|||||||
"EU07.cpp"
|
"EU07.cpp"
|
||||||
"export_e3d_standalone.cpp"
|
"export_e3d_standalone.cpp"
|
||||||
"export_btf_standalone.cpp"
|
"export_btf_standalone.cpp"
|
||||||
|
"nodebank_previews_standalone.cpp"
|
||||||
"world/Event.cpp"
|
"world/Event.cpp"
|
||||||
"world/EvLaunch.cpp"
|
"world/EvLaunch.cpp"
|
||||||
"utilities/Float3d.cpp"
|
"utilities/Float3d.cpp"
|
||||||
|
|||||||
9
EU07.cpp
9
EU07.cpp
@@ -34,6 +34,7 @@ Stele, firleju, szociu, hunter, ZiomalCl, OLI_EU and others
|
|||||||
|
|
||||||
void export_e3d_standalone(std::string in, std::string out, int flags, bool dynamic);
|
void export_e3d_standalone(std::string in, std::string out, int flags, bool dynamic);
|
||||||
int export_btf_standalone(std::string in, std::string out);
|
int export_btf_standalone(std::string in, std::string out);
|
||||||
|
int generate_nodebank_previews(int Argc, char *Argv[]);
|
||||||
|
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -99,6 +100,14 @@ int main(int argc, char *argv[])
|
|||||||
int dynamic = std::stoi(std::string(argv[5]));
|
int dynamic = std::stoi(std::string(argv[5]));
|
||||||
export_e3d_standalone(in, out, flags, dynamic);
|
export_e3d_standalone(in, out, flags, dynamic);
|
||||||
}
|
}
|
||||||
|
// standalone generation of preview images of the node bank models: -generate-nodebank-previews [options]
|
||||||
|
else if (argc >= 2 && std::string(argv[1]) == "-generate-nodebank-previews")
|
||||||
|
{
|
||||||
|
auto const result{generate_nodebank_previews(argc, argv)};
|
||||||
|
fflush(stdout);
|
||||||
|
fflush(stderr);
|
||||||
|
std::_Exit(result);
|
||||||
|
}
|
||||||
// standalone conversion of text terrain file to binary format: -btf -s <text file> [-o <binary file>]
|
// standalone conversion of text terrain file to binary format: -btf -s <text file> [-o <binary file>]
|
||||||
else if (argc >= 4 && std::string(argv[1]) == "-btf")
|
else if (argc >= 4 && std::string(argv[1]) == "-btf")
|
||||||
{
|
{
|
||||||
|
|||||||
452
nodebank_previews_standalone.cpp
Normal file
452
nodebank_previews_standalone.cpp
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
|
||||||
|
#include "model/AnimModel.h"
|
||||||
|
#include "rendering/renderer.h"
|
||||||
|
#include "scene/scenenode.h"
|
||||||
|
#include "utilities/Globals.h"
|
||||||
|
#include "utilities/Logs.h"
|
||||||
|
#include "utilities/parser.h"
|
||||||
|
#include "utilities/utilities.h"
|
||||||
|
#include <png.h>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
// standalone generation of preview images of the models of the node bank of the scenery editor:
|
||||||
|
// -generate-nodebank-previews [-entries "node ... endmodel" ...] [-resolution <pixels>] [-margin <pixels>] [-no-shadows] [-software]
|
||||||
|
// each image is a square png on white background, saved in textures/previews/nodebank under the path of the model
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
std::string const preview_folder{"textures/previews/nodebank/"};
|
||||||
|
|
||||||
|
struct preview_settings
|
||||||
|
{
|
||||||
|
std::vector<std::string> entries; // node definitions to draw. the whole node bank if empty
|
||||||
|
int resolution{1024};
|
||||||
|
int margin{64};
|
||||||
|
bool shadows{true};
|
||||||
|
bool software{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
// prints a line on the console the generator was started from, and puts it in the log
|
||||||
|
void report(std::string const &Text, bool const Error = false)
|
||||||
|
{
|
||||||
|
std::fprintf((Error ? stderr : stdout), "%s\n", Text.c_str());
|
||||||
|
std::fflush(Error ? stderr : stdout);
|
||||||
|
if (Error)
|
||||||
|
{
|
||||||
|
// goes to the log as well
|
||||||
|
ErrorLog(Text);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteLog(Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_number(char const *Text, int &Value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::size_t used{0};
|
||||||
|
auto const value{std::stoi(Text, &used)};
|
||||||
|
if (Text[used] != '\0')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Value = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (std::exception const &)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_arguments(int const Argc, char *Argv[], preview_settings &Settings)
|
||||||
|
{
|
||||||
|
for (int i = 2; i < Argc; ++i)
|
||||||
|
{
|
||||||
|
std::string const token{Argv[i]};
|
||||||
|
if (token == "-entries")
|
||||||
|
{
|
||||||
|
while ((i + 1 < Argc) && (Argv[i + 1][0] != '-'))
|
||||||
|
{
|
||||||
|
Settings.entries.emplace_back(Argv[++i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (token == "-resolution")
|
||||||
|
{
|
||||||
|
if ((i + 1 >= Argc) || (false == parse_number(Argv[++i], Settings.resolution)) || (Settings.resolution < 16))
|
||||||
|
{
|
||||||
|
report("-resolution needs a number of pixels, 16 or more", true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (token == "-margin")
|
||||||
|
{
|
||||||
|
if ((i + 1 >= Argc) || (false == parse_number(Argv[++i], Settings.margin)) || (Settings.margin < 0))
|
||||||
|
{
|
||||||
|
report("-margin needs a number of pixels, 0 or more", true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (token == "-no-shadows")
|
||||||
|
{
|
||||||
|
Settings.shadows = false;
|
||||||
|
}
|
||||||
|
else if (token == "-software")
|
||||||
|
{
|
||||||
|
Settings.software = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
report("unknown option " + token, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Settings.margin * 2 >= Settings.resolution)
|
||||||
|
{
|
||||||
|
report("the margin leaves no room for the model at this resolution", true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// node definitions of the node bank, read the way the node bank panel of the editor reads them
|
||||||
|
std::vector<std::string> nodebank_entries()
|
||||||
|
{
|
||||||
|
std::vector<std::string> entries;
|
||||||
|
std::ifstream file("nodebank.txt", std::ios_base::in | std::ios_base::binary);
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(file, line))
|
||||||
|
{
|
||||||
|
if (line.size() < 4)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto const nodestart{line.find("node")};
|
||||||
|
if (nodestart == std::string::npos)
|
||||||
|
{
|
||||||
|
// group header
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto entry{line.substr(nodestart)};
|
||||||
|
while ((false == entry.empty()) && ((entry.back() == '\r') || (entry.back() == '\n')))
|
||||||
|
{
|
||||||
|
entry.pop_back();
|
||||||
|
}
|
||||||
|
entries.emplace_back(entry);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates the hidden window with the gl context for the renderer. on a computer without a graphics driver able to provide
|
||||||
|
// gl 3.3 the software rasterizer of mesa can take over: on linux mesa picks it by itself, on windows its opengl32.dll
|
||||||
|
// (with the libraries it needs) is looked for in the mesa folder
|
||||||
|
GLFWwindow *create_context(bool const Software)
|
||||||
|
{
|
||||||
|
if (Software)
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
auto const folder{std::filesystem::absolute("mesa")};
|
||||||
|
::SetDllDirectoryW(folder.wstring().c_str());
|
||||||
|
#else
|
||||||
|
::setenv("LIBGL_ALWAYS_SOFTWARE", "1", 1);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
if (glfwInit() == GLFW_FALSE)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||||
|
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
|
||||||
|
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||||
|
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
|
||||||
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||||
|
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||||
|
if (Global.gfx_gldebug)
|
||||||
|
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
|
||||||
|
auto *window{glfwCreateWindow(64, 64, "eu07 previews", nullptr, nullptr)};
|
||||||
|
if (window == nullptr)
|
||||||
|
{
|
||||||
|
glfwTerminate();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
glfwMakeContextCurrent(window);
|
||||||
|
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
|
||||||
|
{
|
||||||
|
glfwDestroyWindow(window);
|
||||||
|
glfwTerminate();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
// characters which can be safely used in a file name
|
||||||
|
std::string file_name_part(std::string Text)
|
||||||
|
{
|
||||||
|
for (auto &character : Text)
|
||||||
|
{
|
||||||
|
auto const c{static_cast<unsigned char>(character)};
|
||||||
|
if ((false == std::isalnum(c)) && (c != '_') && (c != '-') && (c != '.') && (c != '+') && (c != '(') && (c != ')'))
|
||||||
|
{
|
||||||
|
character = '_';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((Text.empty()) || (Text == ".") || (Text == ".."))
|
||||||
|
{
|
||||||
|
Text = "_";
|
||||||
|
}
|
||||||
|
return Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// path of the image of the node with specified model and skin: the path of the model in the preview folder, the skin added to the name
|
||||||
|
std::string preview_path(std::string Model, std::string const &Skin)
|
||||||
|
{
|
||||||
|
replace_slashes(Model);
|
||||||
|
erase_extension(Model);
|
||||||
|
std::string path;
|
||||||
|
std::size_t start{0};
|
||||||
|
while (start <= Model.size())
|
||||||
|
{
|
||||||
|
auto end{Model.find('/', start)};
|
||||||
|
if (end == std::string::npos)
|
||||||
|
{
|
||||||
|
end = Model.size();
|
||||||
|
}
|
||||||
|
if (end > start)
|
||||||
|
{
|
||||||
|
if (false == path.empty())
|
||||||
|
{
|
||||||
|
path += '/';
|
||||||
|
}
|
||||||
|
path += file_name_part(Model.substr(start, end - start));
|
||||||
|
}
|
||||||
|
start = end + 1;
|
||||||
|
}
|
||||||
|
if ((false == Skin.empty()) && (Skin != "none"))
|
||||||
|
{
|
||||||
|
auto skin{Skin};
|
||||||
|
replace_slashes(skin);
|
||||||
|
erase_extension(skin);
|
||||||
|
std::replace(skin.begin(), skin.end(), '/', '_');
|
||||||
|
path += '@' + file_name_part(skin);
|
||||||
|
}
|
||||||
|
return preview_folder + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool save_png(std::string const &Path, std::vector<std::uint8_t> const &Image, int const Size)
|
||||||
|
{
|
||||||
|
std::error_code error;
|
||||||
|
std::filesystem::create_directories(std::filesystem::path(Path).parent_path(), error);
|
||||||
|
png_image png;
|
||||||
|
std::memset(&png, 0, sizeof(png_image));
|
||||||
|
png.version = PNG_IMAGE_VERSION;
|
||||||
|
png.width = Size;
|
||||||
|
png.height = Size;
|
||||||
|
png.format = PNG_FORMAT_RGB;
|
||||||
|
return (png_image_write_to_file(&png, Path.c_str(), 0, Image.data(), Size * 3, nullptr) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// draws the preview of a single node bank entry. returns: true on success
|
||||||
|
bool generate_preview(std::string const &Definition, preview_settings const &Settings, std::set<std::string> &Saved, std::string &Outcome)
|
||||||
|
{
|
||||||
|
// a line of the node bank can be given as it is, with the label in front of the definition
|
||||||
|
auto const nodestart{Definition.find("node")};
|
||||||
|
if (nodestart == std::string::npos)
|
||||||
|
{
|
||||||
|
Outcome = "not a node definition";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto const entry{Definition.substr(nodestart)};
|
||||||
|
cParser parser(entry, cParser::buffer_TEXT);
|
||||||
|
parser.getTokens(); // node
|
||||||
|
scene::node_data nodedata;
|
||||||
|
parser.getTokens(4);
|
||||||
|
parser >> nodedata.range_max >> nodedata.range_min >> nodedata.name >> nodedata.type;
|
||||||
|
if (nodedata.name == "none")
|
||||||
|
{
|
||||||
|
nodedata.name.clear();
|
||||||
|
}
|
||||||
|
if (nodedata.type != "model")
|
||||||
|
{
|
||||||
|
Outcome = "skipped, node of type " + nodedata.type + " isn't a model";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// placement in the scenery is of no use here, the rotation however is a part of the look of the node
|
||||||
|
glm::dvec3 location;
|
||||||
|
glm::vec3 rotation{0.f};
|
||||||
|
parser.getTokens(4);
|
||||||
|
parser >> location.x >> location.y >> location.z >> rotation.y;
|
||||||
|
// NOTE: instances are kept, as the rest of the simulator assumes scenery nodes live until the exit
|
||||||
|
auto *instance = new TAnimModel(nodedata);
|
||||||
|
instance->Angles(rotation);
|
||||||
|
instance->Load(&parser, false);
|
||||||
|
if (instance->pModel == nullptr)
|
||||||
|
{
|
||||||
|
Outcome = "model not found";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
instance->location(glm::dvec3{0.0});
|
||||||
|
|
||||||
|
// the model and skin, which give the name of the image, are the first two tokens after the rotation
|
||||||
|
cParser names(entry, cParser::buffer_TEXT);
|
||||||
|
names.getTokens(9, false);
|
||||||
|
auto const model{names.getToken<std::string>()};
|
||||||
|
auto const skin{names.getToken<std::string>(false)};
|
||||||
|
auto path{preview_path(model, skin)};
|
||||||
|
if (Saved.contains(path))
|
||||||
|
{
|
||||||
|
// another entry with the same model and skin, but other lights or angles
|
||||||
|
auto variant{2};
|
||||||
|
while (Saved.contains(path + "~" + std::to_string(variant)))
|
||||||
|
{
|
||||||
|
++variant;
|
||||||
|
}
|
||||||
|
path += "~" + std::to_string(variant);
|
||||||
|
}
|
||||||
|
path += ".png";
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> image;
|
||||||
|
if (false == GfxRenderer->Render_Preview(instance, Settings.resolution, Settings.margin, Settings.shadows, image))
|
||||||
|
{
|
||||||
|
Outcome = "nothing to draw";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (false == save_png(path, image, Settings.resolution))
|
||||||
|
{
|
||||||
|
Outcome = "failed to save " + path;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Saved.emplace(path.substr(0, path.size() - 4));
|
||||||
|
Outcome = path;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int generate_nodebank_previews(int Argc, char *Argv[])
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
// the simulator is a windows application, so the console it was started from has to be joined to report progress
|
||||||
|
if (::AttachConsole(ATTACH_PARENT_PROCESS))
|
||||||
|
{
|
||||||
|
FILE *stream{nullptr};
|
||||||
|
freopen_s(&stream, "CONOUT$", "w", stdout);
|
||||||
|
freopen_s(&stream, "CONOUT$", "w", stderr);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// settings of the simulator, for the paths and the graphics options which aren't set for the previews below
|
||||||
|
auto const inipath{user_config_path("eu07.ini")};
|
||||||
|
if (false == inipath.empty() && std::filesystem::exists(inipath))
|
||||||
|
{
|
||||||
|
Global.LoadIniFile(inipath.string());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Global.LoadIniFile("eu07.ini");
|
||||||
|
}
|
||||||
|
// the progress goes to the console, the log keeps the details
|
||||||
|
Global.iWriteLogEnabled &= ~2;
|
||||||
|
std::thread logservice(LogService);
|
||||||
|
|
||||||
|
auto const finish = [&](int const Result) {
|
||||||
|
// give the log service time to write out what's left
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||||
|
Global.applicationQuitOrder = true;
|
||||||
|
logservice.join();
|
||||||
|
return Result;
|
||||||
|
};
|
||||||
|
|
||||||
|
preview_settings settings;
|
||||||
|
if (false == parse_arguments(Argc, Argv, settings))
|
||||||
|
{
|
||||||
|
report("usage: " + std::string(Argv[0]) + " -generate-nodebank-previews [-entries \"node ... endmodel\" ...] [-resolution <pixels>] [-margin <pixels>] [-no-shadows] [-software]", true);
|
||||||
|
return finish(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the full renderer, drawing offscreen
|
||||||
|
Global.GfxRenderer = "default";
|
||||||
|
Global.LegacyRenderer = false;
|
||||||
|
Global.NvRenderer = false;
|
||||||
|
Global.BasicRenderer = false;
|
||||||
|
Global.gfx_usegles = false;
|
||||||
|
Global.gfx_skippipeline = false;
|
||||||
|
Global.gfx_postfx_motionblur_enabled = false;
|
||||||
|
Global.gfx_postfx_chromaticaberration_enabled = false;
|
||||||
|
Global.gfx_postfx_ssao_enabled = false;
|
||||||
|
Global.gfx_envmap_enabled = false;
|
||||||
|
Global.gfx_shadowmap_enabled = settings.shadows;
|
||||||
|
Global.shadowtune.map_size = std::max(Global.shadowtune.map_size, 2048u);
|
||||||
|
Global.iMultisampling = 0;
|
||||||
|
Global.vr = false;
|
||||||
|
Global.extra_viewports.clear();
|
||||||
|
Global.VSync = false;
|
||||||
|
Global.fb_size = {64, 64};
|
||||||
|
Global.window_size = {64, 64};
|
||||||
|
Global.gfx_framebuffer_width = 64;
|
||||||
|
Global.gfx_framebuffer_height = 64;
|
||||||
|
Global.gfx_texture_streaming = false;
|
||||||
|
Global.gfx_texture_releasedistance = 0.f;
|
||||||
|
// summer daylight, which picks the seasonal variants of models and materials and keeps the lamps off
|
||||||
|
Global.Season = "summer:";
|
||||||
|
Global.Overcast = 0.f;
|
||||||
|
Global.fLuminance = 1.0;
|
||||||
|
|
||||||
|
auto *window{create_context(settings.software)};
|
||||||
|
if ((window == nullptr) && (false == settings.software))
|
||||||
|
{
|
||||||
|
report("no gl 3.3 context from the graphics driver, trying the software renderer");
|
||||||
|
settings.software = true;
|
||||||
|
window = create_context(true);
|
||||||
|
}
|
||||||
|
if (window == nullptr)
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
report("failed to create gl 3.3 context. for computers without a capable graphics driver put opengl32.dll of mesa (with the libraries it comes with) in the mesa folder", true);
|
||||||
|
#else
|
||||||
|
report("failed to create gl 3.3 context. without a display run it through xvfb-run", true);
|
||||||
|
#endif
|
||||||
|
return finish(1);
|
||||||
|
}
|
||||||
|
GfxRenderer = gfx_renderer_factory::get_instance()->create("modern");
|
||||||
|
if ((!GfxRenderer) || (false == GfxRenderer->Init(window)))
|
||||||
|
{
|
||||||
|
report("failed to initialize the renderer", true);
|
||||||
|
return finish(1);
|
||||||
|
}
|
||||||
|
report(std::string("renderer: ") + reinterpret_cast<char const *>(glGetString(GL_RENDERER)));
|
||||||
|
|
||||||
|
auto const entries{settings.entries.empty() ? nodebank_entries() : settings.entries};
|
||||||
|
if (entries.empty())
|
||||||
|
{
|
||||||
|
report("no entries to draw, nodebank.txt is missing or empty", true);
|
||||||
|
return finish(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string> saved;
|
||||||
|
auto failures{0};
|
||||||
|
for (std::size_t idx = 0; idx < entries.size(); ++idx)
|
||||||
|
{
|
||||||
|
std::string outcome;
|
||||||
|
auto success{false};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
success = generate_preview(entries[idx], settings, saved, outcome);
|
||||||
|
}
|
||||||
|
catch (std::exception const &Error)
|
||||||
|
{
|
||||||
|
// a broken entry shouldn't stop the others
|
||||||
|
outcome = Error.what();
|
||||||
|
}
|
||||||
|
if (false == success)
|
||||||
|
{
|
||||||
|
++failures;
|
||||||
|
}
|
||||||
|
report("[" + std::to_string(idx + 1) + "/" + std::to_string(entries.size()) + "] " + entries[idx] + (success ? " -> " : ": ") + outcome, (false == success));
|
||||||
|
}
|
||||||
|
report(std::to_string(saved.size()) + " previews saved in " + preview_folder + (failures > 0 ? ", " + std::to_string(failures) + " entries failed" : ""));
|
||||||
|
|
||||||
|
return finish(failures > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
@@ -3982,7 +3982,7 @@ void opengl33_renderer::Render(TSubModel *Submodel)
|
|||||||
{
|
{
|
||||||
glDebug("Render TSubModel");
|
glDebug("Render TSubModel");
|
||||||
|
|
||||||
if ((Submodel->iVisible) && (TSubModel::fSquareDist >= Submodel->fSquareMinDist) && (TSubModel::fSquareDist < Submodel->fSquareMaxDist))
|
if ((Submodel->iVisible) && (TSubModel::fSquareDist >= Submodel->fSquareMinDist) && (TSubModel::fSquareDist < Submodel->fSquareMaxDist) && (false == m_previewing || Submodel->fVisible > 0.f))
|
||||||
{
|
{
|
||||||
glm::mat4 future_stack = model_ubs.future;
|
glm::mat4 future_stack = model_ubs.future;
|
||||||
|
|
||||||
@@ -4998,7 +4998,7 @@ void opengl33_renderer::Render_Alpha_text(TSubModel *Submodel)
|
|||||||
void opengl33_renderer::Render_Alpha(TSubModel *Submodel)
|
void opengl33_renderer::Render_Alpha(TSubModel *Submodel)
|
||||||
{
|
{
|
||||||
// renderowanie przezroczystych przez DL
|
// renderowanie przezroczystych przez DL
|
||||||
if ((Submodel->iVisible) && (TSubModel::fSquareDist >= Submodel->fSquareMinDist) && (TSubModel::fSquareDist < Submodel->fSquareMaxDist))
|
if ((Submodel->iVisible) && (TSubModel::fSquareDist >= Submodel->fSquareMinDist) && (TSubModel::fSquareDist < Submodel->fSquareMaxDist) && (false == m_previewing || Submodel->fVisible > 0.f))
|
||||||
{
|
{
|
||||||
glm::mat4 future_stack = model_ubs.future;
|
glm::mat4 future_stack = model_ubs.future;
|
||||||
|
|
||||||
@@ -5549,6 +5549,414 @@ void opengl33_renderer::MakeScreenshot()
|
|||||||
screenshot_manager::make_screenshot();
|
screenshot_manager::make_screenshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// model previews
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// direction from the previewed model to the camera. models are made facing -z (the front view of 3ds max),
|
||||||
|
// so the camera looks at their front, right side and top
|
||||||
|
glm::dvec3 const preview_viewdirection{-0.6123724, 0.5, -0.6123724};
|
||||||
|
// vertical field of view of the preview camera, in degrees
|
||||||
|
float const preview_fieldofview{30.f};
|
||||||
|
// brightness of the hdr background, past the white point of the tonemapping, so translucent parts blend with white
|
||||||
|
float const preview_background{16.f};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void opengl33_renderer::Preview_Bounds(TSubModel const *Submodel, glm::dmat4 const &Transform, glm::dvec3 &Min, glm::dvec3 &Max) const
|
||||||
|
{
|
||||||
|
// same walk as the one of Render(TSubModel *) bar the range checks, which leave out parts only for the bounds to be smaller
|
||||||
|
for (auto const *submodel{Submodel}; submodel != nullptr; submodel = submodel->Next)
|
||||||
|
{
|
||||||
|
if ((submodel->iVisible == 0) || (submodel->fVisible <= 0.f))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto transform{Transform};
|
||||||
|
if ((submodel->iFlags & 0xC000) && (submodel->fMatrix != nullptr))
|
||||||
|
{
|
||||||
|
transform *= glm::dmat4{glm::make_mat4(submodel->fMatrix->readArray())};
|
||||||
|
}
|
||||||
|
if ((submodel->eType < TP_ROTATOR) && (submodel->m_geometry.handle != null_handle))
|
||||||
|
{
|
||||||
|
for (auto const &vertex : m_geometry.vertices(submodel->m_geometry.handle))
|
||||||
|
{
|
||||||
|
auto const point{glm::dvec3{transform * glm::dvec4{glm::dvec3{vertex.position}, 1.0}}};
|
||||||
|
Min = glm::min(Min, point);
|
||||||
|
Max = glm::max(Max, point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Preview_Bounds(submodel->Child, transform, Min, Max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool opengl33_renderer::Preview_Targets(int const Size)
|
||||||
|
{
|
||||||
|
if (m_preview.size == Size)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
m_preview = preview_targets();
|
||||||
|
|
||||||
|
m_preview.hdr_tex = std::make_unique<opengl_texture>();
|
||||||
|
m_preview.hdr_tex->alloc_rendertarget(GL_RGBA16F, GL_RGBA, Size, Size, 1, 1, GL_CLAMP_TO_EDGE);
|
||||||
|
m_preview.hdr_rbds = std::make_unique<gl::renderbuffer>();
|
||||||
|
m_preview.hdr_rbds->alloc(GL_DEPTH32F_STENCIL8, Size, Size);
|
||||||
|
m_preview.hdr_fb = std::make_unique<gl::framebuffer>();
|
||||||
|
m_preview.hdr_fb->attach(*m_preview.hdr_tex, GL_COLOR_ATTACHMENT0);
|
||||||
|
m_preview.hdr_fb->attach(*m_preview.hdr_rbds, GL_DEPTH_STENCIL_ATTACHMENT);
|
||||||
|
m_preview.hdr_fb->setup_drawing(1);
|
||||||
|
if (false == m_preview.hdr_fb->is_complete())
|
||||||
|
{
|
||||||
|
ErrorLog("preview framebuffer setup failed");
|
||||||
|
m_preview = preview_targets();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// the tonemapping writes srgb values, either by itself or through the srgb target
|
||||||
|
m_preview.ldr_tex = std::make_unique<opengl_texture>();
|
||||||
|
m_preview.ldr_tex->alloc_rendertarget((Global.gfx_shadergamma ? GL_RGBA8 : GL_SRGB8_ALPHA8), GL_RGBA, Size, Size, 1, 1, GL_CLAMP_TO_EDGE);
|
||||||
|
m_preview.ldr_fb = std::make_unique<gl::framebuffer>();
|
||||||
|
m_preview.ldr_fb->attach(*m_preview.ldr_tex, GL_COLOR_ATTACHMENT0);
|
||||||
|
m_preview.ldr_fb->setup_drawing(1);
|
||||||
|
if (false == m_preview.ldr_fb->is_complete())
|
||||||
|
{
|
||||||
|
ErrorLog("preview framebuffer setup failed");
|
||||||
|
m_preview = preview_targets();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
gl::framebuffer::unbind();
|
||||||
|
|
||||||
|
m_preview.size = Size;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void opengl33_renderer::Preview_Model(TAnimModel *Instance, float const Squaredistance, bool const Alpha)
|
||||||
|
{
|
||||||
|
// the placement of Render(TAnimModel *), without its range and visibility tests
|
||||||
|
::glPushMatrix();
|
||||||
|
auto const position{Instance->location() - m_renderpass.pass_camera.position()};
|
||||||
|
::glTranslated(position.x, position.y, position.z);
|
||||||
|
auto const &angle{Instance->vAngle};
|
||||||
|
if (angle.y != 0.0)
|
||||||
|
::glRotated(angle.y, 0.f, 1.f, 0.f);
|
||||||
|
if (angle.x != 0.0)
|
||||||
|
::glRotated(angle.x, 1.f, 0.f, 0.f);
|
||||||
|
if (angle.z != 0.0)
|
||||||
|
::glRotated(angle.z, 0.f, 0.f, 1.f);
|
||||||
|
auto const &scale{Instance->Scale()};
|
||||||
|
::glScalef(scale.x, scale.y, scale.z);
|
||||||
|
if (Alpha)
|
||||||
|
Render_Alpha(Instance->pModel, Instance->Material(), Squaredistance);
|
||||||
|
else
|
||||||
|
Render(Instance->pModel, Instance->Material(), Squaredistance);
|
||||||
|
::glPopMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
void opengl33_renderer::Preview_Color(TAnimModel *Instance, float const Squaredistance, int const Size, glm::dvec3 const &Eye, glm::mat4 const &View, glm::mat4 const &Projection, bool const Shadows)
|
||||||
|
{
|
||||||
|
m_renderpass.draw_mode = rendermode::color;
|
||||||
|
m_renderpass.pass_camera.position() = Eye;
|
||||||
|
m_renderpass.pass_camera.modelview() = View;
|
||||||
|
m_renderpass.pass_camera.projection() = Projection;
|
||||||
|
m_renderpass.viewport_camera.position() = Eye;
|
||||||
|
setup_matrices();
|
||||||
|
|
||||||
|
scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION);
|
||||||
|
scene_ubs.inv_view = glm::inverse(glm::mat4{glm::mat3{View}});
|
||||||
|
scene_ubs.time = 0.f;
|
||||||
|
for (auto idx = 0; idx < m_shadowpass.size(); ++idx)
|
||||||
|
{
|
||||||
|
// everything is in the first cascade
|
||||||
|
scene_ubs.cascade_end[idx] = std::numeric_limits<float>::max();
|
||||||
|
}
|
||||||
|
scene_ubo->update(scene_ubs);
|
||||||
|
scene_ubo->bind_uniform();
|
||||||
|
// the sun is the only light, and there's no fog
|
||||||
|
auto const &modelview{OpenGLMatrices.data(GL_MODELVIEW)};
|
||||||
|
light_ubs.ambient = m_sunlight.ambient;
|
||||||
|
light_ubs.fog_color = glm::vec3{1.f};
|
||||||
|
light_ubs.lights_count = 1;
|
||||||
|
light_ubs.lights[0].type = gl::light_element_ubs::DIR;
|
||||||
|
light_ubs.lights[0].dir = modelview * glm::vec4(m_sunlight.direction, 0.0f);
|
||||||
|
light_ubs.lights[0].color = m_sunlight.diffuse;
|
||||||
|
light_ubs.lights[0].ambient = 0.0f;
|
||||||
|
light_ubs.lights[0].intensity = 1.0f;
|
||||||
|
light_ubo->update(light_ubs);
|
||||||
|
model_ubs.fog_density = 0.f;
|
||||||
|
model_ubs.future = glm::mat4();
|
||||||
|
model_ubs.emission = 0.f;
|
||||||
|
if (Shadows)
|
||||||
|
{
|
||||||
|
setup_shadow_bind_map();
|
||||||
|
setup_shadow_color(colors::shadow);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setup_shadow_unbind_map();
|
||||||
|
setup_shadow_color(colors::white);
|
||||||
|
}
|
||||||
|
setup_env_map(nullptr);
|
||||||
|
|
||||||
|
m_preview.hdr_fb->bind();
|
||||||
|
glViewport(0, 0, Size, Size);
|
||||||
|
glClearColor(preview_background, preview_background, preview_background, 1.f);
|
||||||
|
glClearStencil(0);
|
||||||
|
glStencilMask(0xFF);
|
||||||
|
m_preview.hdr_fb->clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||||
|
if (!Global.gfx_usegles)
|
||||||
|
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
|
// every fragment which made it past the alpha test marks the pixel as covered, translucent ones included
|
||||||
|
glEnable(GL_STENCIL_TEST);
|
||||||
|
glStencilFunc(GL_ALWAYS, 1, 0xFF);
|
||||||
|
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
||||||
|
|
||||||
|
setup_drawing(false);
|
||||||
|
Preview_Model(Instance, Squaredistance, false);
|
||||||
|
setup_drawing(true);
|
||||||
|
Preview_Model(Instance, Squaredistance, true);
|
||||||
|
setup_drawing(false);
|
||||||
|
|
||||||
|
glDisable(GL_STENCIL_TEST);
|
||||||
|
setup_shadow_unbind_map();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool opengl33_renderer::Render_Preview(TAnimModel *Instance, int const Size, int const Margin, bool const Shadows, std::vector<std::uint8_t> &Image)
|
||||||
|
{
|
||||||
|
if ((Instance == nullptr) || (Instance->pModel == nullptr) || (Size <= 0) || (Margin < 0) || (Margin * 2 >= Size))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// the image is drawn at double size and scaled down, for antialiased edges which blend with the white background
|
||||||
|
GLint maxtexturesize{0}, maxrenderbuffersize{0};
|
||||||
|
GLint maxviewport[2]{0, 0};
|
||||||
|
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxtexturesize);
|
||||||
|
glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxrenderbuffersize);
|
||||||
|
glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxviewport);
|
||||||
|
auto const maxsize{std::min({maxtexturesize, maxrenderbuffersize, maxviewport[0], maxviewport[1]})};
|
||||||
|
if (Size > maxsize)
|
||||||
|
{
|
||||||
|
ErrorLog("preview size " + std::to_string(Size) + " exceeds the limit of the graphics driver, " + std::to_string(maxsize));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto const supersampling{(Size * 2 <= maxsize ? 2 : 1)};
|
||||||
|
auto const targetsize{Size * supersampling};
|
||||||
|
if (false == Preview_Targets(targetsize))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
glfwMakeContextCurrent(m_window);
|
||||||
|
opengl_texture::reset_unit_cache();
|
||||||
|
gfx::opengl33_vaogeometrybank::reset();
|
||||||
|
m_previewing = true;
|
||||||
|
// lights and visibility of the submodels, for this instance
|
||||||
|
Instance->RaAnimate(m_framestamp);
|
||||||
|
Instance->RaPrepare();
|
||||||
|
|
||||||
|
// bounding sphere of the model, which the camera is placed to fit in its view
|
||||||
|
glm::dvec3 boundsmin{std::numeric_limits<double>::max()};
|
||||||
|
glm::dvec3 boundsmax{std::numeric_limits<double>::lowest()};
|
||||||
|
Preview_Bounds(Instance->pModel->Root, glm::translate(glm::dmat4{1.0}, Instance->location()) * glm::dmat4{Instance->rotation_scale()}, boundsmin, boundsmax);
|
||||||
|
if (boundsmin.x > boundsmax.x)
|
||||||
|
{
|
||||||
|
m_previewing = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto const centre{(boundsmin + boundsmax) * 0.5};
|
||||||
|
auto const radius{std::max(0.01, glm::length(boundsmax - boundsmin) * 0.5)};
|
||||||
|
auto const fieldofview{glm::radians(preview_fieldofview)};
|
||||||
|
auto const distance{radius / std::sin(fieldofview * 0.5)};
|
||||||
|
auto const eye{centre + glm::normalize(preview_viewdirection) * distance};
|
||||||
|
glm::mat4 const view{glm::lookAt(eye, centre, glm::dvec3{0.0, 1.0, 0.0})};
|
||||||
|
// the sphere is given some slack for parts which aren't where their vertices are, like billboards turning to the camera
|
||||||
|
auto const znear{static_cast<float>(std::max(distance - radius * 1.5, distance * 0.05))};
|
||||||
|
auto const zfar{static_cast<float>(distance + radius * 1.5)};
|
||||||
|
auto const projection{perspective_projection_raw(fieldofview, 1.f, znear, zfar)};
|
||||||
|
// level of detail as seen from the camera, like in Render(TAnimModel *)
|
||||||
|
auto const squaredistance{static_cast<float>(glm::length2(Instance->location() - eye) / Global.fDistanceFactor)};
|
||||||
|
|
||||||
|
// key light from above the left shoulder of the viewer, so the visible sides of the model differ in tone
|
||||||
|
auto const forward{glm::normalize(centre - eye)};
|
||||||
|
auto const right{glm::normalize(glm::cross(forward, glm::dvec3{0.0, 1.0, 0.0}))};
|
||||||
|
auto const tosun{glm::normalize(-forward * 0.4 + glm::dvec3{0.0, 1.0, 0.0} - right * 0.9)};
|
||||||
|
m_sunlight.direction = glm::vec3{-tosun};
|
||||||
|
m_sunlight.diffuse = glm::vec4{1.0f, 0.9686275f, 0.9411765f, 1.f};
|
||||||
|
m_sunlight.ambient = glm::vec4{0.65f, 0.66f, 0.68f, 1.f};
|
||||||
|
m_sunlight.factor = 1.f;
|
||||||
|
|
||||||
|
auto const shadows{Shadows && Global.gfx_shadowmap_enabled && (m_shadow_fb != nullptr)};
|
||||||
|
if (shadows)
|
||||||
|
{
|
||||||
|
// one map covering the whole model, seen from the sun
|
||||||
|
m_renderpass.draw_mode = rendermode::shadows;
|
||||||
|
m_renderpass.pass_camera.position() = centre;
|
||||||
|
m_renderpass.pass_camera.modelview() = glm::mat4{glm::lookAt(centre, centre + glm::dvec3{m_sunlight.direction}, glm::dvec3{0.0, 1.0, 0.0})};
|
||||||
|
auto const extent{static_cast<float>(radius * 1.5)};
|
||||||
|
m_renderpass.pass_camera.projection() = ortho_projection(-extent, extent, -extent, extent, -extent, extent);
|
||||||
|
m_renderpass.viewport_camera.position() = eye;
|
||||||
|
setup_matrices();
|
||||||
|
scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION);
|
||||||
|
scene_ubo->update(scene_ubs);
|
||||||
|
|
||||||
|
glViewport(0, 0, m_shadowbuffersize, m_shadowbuffersize);
|
||||||
|
m_shadow_fb->attach(*m_shadow_tex, GL_DEPTH_ATTACHMENT, 0);
|
||||||
|
m_shadow_fb->clear(GL_DEPTH_BUFFER_BIT);
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
|
setup_drawing(false);
|
||||||
|
Preview_Model(Instance, squaredistance, false);
|
||||||
|
m_shadow_fb->unbind();
|
||||||
|
for (auto &pass : m_shadowpass)
|
||||||
|
{
|
||||||
|
pass = m_renderpass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the view is cropped to the part of the image the model covers: a look from a bit further first, to find it,
|
||||||
|
// then a closer one at the full size, as thin parts can show up only there, to refine the crop to a pixel.
|
||||||
|
// the crop maps the area to the image less the margins
|
||||||
|
std::vector<std::uint8_t> coverage;
|
||||||
|
auto const fitsize{targetsize};
|
||||||
|
auto const area{1.0 - 2.0 * Margin / Size}; // half of the extent of the area for the model, in normalized device coordinates
|
||||||
|
glm::dvec2 cropcentre{0.0};
|
||||||
|
auto cropscale{0.75};
|
||||||
|
auto tight{false}; // whether the crop was fitted to the model
|
||||||
|
auto const cropped = [&]() {
|
||||||
|
return glm::mat4{glm::scale(glm::dmat4{1.0}, glm::dvec3{cropscale, cropscale, 1.0}) * glm::translate(glm::dmat4{1.0}, glm::dvec3{-cropcentre, 0.0})} * projection;
|
||||||
|
};
|
||||||
|
for (auto attempt = 0; attempt < 6; ++attempt)
|
||||||
|
{
|
||||||
|
auto const passsize{(tight ? fitsize : std::min(fitsize, 512))};
|
||||||
|
Preview_Color(Instance, squaredistance, passsize, eye, view, cropped(), shadows);
|
||||||
|
coverage.resize(static_cast<std::size_t>(passsize) * passsize);
|
||||||
|
m_preview.hdr_fb->bind();
|
||||||
|
glReadPixels(0, 0, passsize, passsize, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, coverage.data());
|
||||||
|
|
||||||
|
glm::ivec2 pixelmin{passsize, passsize}, pixelmax{-1, -1};
|
||||||
|
for (auto y = 0; y < passsize; ++y)
|
||||||
|
{
|
||||||
|
auto const *row{coverage.data() + static_cast<std::size_t>(y) * passsize};
|
||||||
|
for (auto x = 0; x < passsize; ++x)
|
||||||
|
{
|
||||||
|
if (row[x] != 0)
|
||||||
|
{
|
||||||
|
pixelmin = glm::min(pixelmin, glm::ivec2{x, y});
|
||||||
|
pixelmax = glm::max(pixelmax, glm::ivec2{x, y});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pixelmax.x < 0)
|
||||||
|
{
|
||||||
|
if (attempt == 0)
|
||||||
|
{
|
||||||
|
// nothing to see
|
||||||
|
m_previewing = false;
|
||||||
|
m_renderpass.draw_mode = rendermode::none;
|
||||||
|
gl::framebuffer::unbind();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// the crop missed the model, start over with the plain view
|
||||||
|
cropcentre = glm::dvec2{0.0};
|
||||||
|
cropscale = 0.5;
|
||||||
|
tight = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// covered area in normalized device coordinates of the uncropped view
|
||||||
|
auto const tondc = [&](glm::ivec2 const &Pixel) { return (glm::dvec2{-1.0} + 2.0 * glm::dvec2{Pixel} / static_cast<double>(passsize)) / cropscale + cropcentre; };
|
||||||
|
auto const ndcmin{tondc(pixelmin)};
|
||||||
|
auto const ndcmax{tondc(pixelmax + 1)};
|
||||||
|
cropcentre = (ndcmin + ndcmax) * 0.5;
|
||||||
|
// a model fitted to the area keeps clear of the image edges, unless the margin is too thin to tell
|
||||||
|
auto const clipped{(pixelmin.x == 0) || (pixelmin.y == 0) || (pixelmax.x == passsize - 1) || (pixelmax.y == passsize - 1)};
|
||||||
|
if (clipped && ((false == tight) || (Margin * passsize / Size >= 2)))
|
||||||
|
{
|
||||||
|
// part of the model is out of the view, look again from further away
|
||||||
|
cropscale *= 0.5;
|
||||||
|
tight = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cropscale = 2.0 * area / std::max(ndcmax.x - ndcmin.x, ndcmax.y - ndcmin.y);
|
||||||
|
if (tight)
|
||||||
|
{
|
||||||
|
// measured through a fitted crop, which makes it accurate to a pixel of the fitting pass
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tight = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the image itself
|
||||||
|
Preview_Color(Instance, squaredistance, targetsize, eye, view, cropped(), shadows);
|
||||||
|
glViewport(0, 0, targetsize, targetsize);
|
||||||
|
if (!Global.gfx_usegles && !Global.gfx_shadergamma)
|
||||||
|
glEnable(GL_FRAMEBUFFER_SRGB);
|
||||||
|
m_pfx_tonemapping->apply(*m_preview.hdr_tex, m_preview.ldr_fb.get());
|
||||||
|
if (!Global.gfx_usegles && !Global.gfx_shadergamma)
|
||||||
|
glDisable(GL_FRAMEBUFFER_SRGB);
|
||||||
|
opengl_texture::reset_unit_cache();
|
||||||
|
// the post processing binds its own vertex array, which the geometry banks don't know about
|
||||||
|
gfx::opengl33_vaogeometrybank::reset();
|
||||||
|
|
||||||
|
auto const targetpixels{static_cast<std::size_t>(targetsize) * targetsize};
|
||||||
|
std::vector<std::uint8_t> colors(targetpixels * 4);
|
||||||
|
m_preview.ldr_fb->bind();
|
||||||
|
glReadPixels(0, 0, targetsize, targetsize, GL_RGBA, GL_UNSIGNED_BYTE, colors.data());
|
||||||
|
coverage.resize(targetpixels);
|
||||||
|
m_preview.hdr_fb->bind();
|
||||||
|
glReadPixels(0, 0, targetsize, targetsize, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, coverage.data());
|
||||||
|
gl::framebuffer::unbind();
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
|
||||||
|
m_previewing = false;
|
||||||
|
m_renderpass.draw_mode = rendermode::none;
|
||||||
|
|
||||||
|
// uncovered pixels are white, the samples are averaged in linear space and the rows flipped to go from the top
|
||||||
|
static auto const tolinear{[]() {
|
||||||
|
std::array<float, 256> table;
|
||||||
|
for (auto idx = 0; idx < 256; ++idx)
|
||||||
|
{
|
||||||
|
auto const value{idx / 255.f};
|
||||||
|
table[idx] = (value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f));
|
||||||
|
}
|
||||||
|
return table;
|
||||||
|
}()};
|
||||||
|
auto const tosrgb = [](float const Value) {
|
||||||
|
auto const value{std::clamp(Value, 0.f, 1.f)};
|
||||||
|
auto const encoded{(value <= 0.0031308f ? value * 12.92f : 1.055f * std::pow(value, 1.f / 2.4f) - 0.055f)};
|
||||||
|
return static_cast<std::uint8_t>(std::lround(encoded * 255.f));
|
||||||
|
};
|
||||||
|
auto const samplecount{static_cast<float>(supersampling * supersampling)};
|
||||||
|
Image.resize(static_cast<std::size_t>(Size) * Size * 3);
|
||||||
|
for (auto y = 0; y < Size; ++y)
|
||||||
|
{
|
||||||
|
auto *output{Image.data() + static_cast<std::size_t>(y) * Size * 3};
|
||||||
|
auto const sourcey{(Size - 1 - y) * supersampling};
|
||||||
|
for (auto x = 0; x < Size; ++x)
|
||||||
|
{
|
||||||
|
glm::vec3 color{0.f};
|
||||||
|
for (auto sy = 0; sy < supersampling; ++sy)
|
||||||
|
{
|
||||||
|
for (auto sx = 0; sx < supersampling; ++sx)
|
||||||
|
{
|
||||||
|
auto const sample{static_cast<std::size_t>(sourcey + sy) * targetsize + x * supersampling + sx};
|
||||||
|
if (coverage[sample] != 0)
|
||||||
|
{
|
||||||
|
auto const *rgba{colors.data() + sample * 4};
|
||||||
|
color += glm::vec3{tolinear[rgba[0]], tolinear[rgba[1]], tolinear[rgba[2]]};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
color += glm::vec3{1.f};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color /= samplecount;
|
||||||
|
output[x * 3 + 0] = tosrgb(color.r);
|
||||||
|
output[x * 3 + 1] = tosrgb(color.g);
|
||||||
|
output[x * 3 + 2] = tosrgb(color.b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void opengl33_renderer::Update_Lights(light_array &Lights)
|
void opengl33_renderer::Update_Lights(light_array &Lights)
|
||||||
{
|
{
|
||||||
glDebug("Update_Lights");
|
glDebug("Update_Lights");
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ class opengl33_renderer : public gfx_renderer {
|
|||||||
std::string const &
|
std::string const &
|
||||||
info_stats() const override;
|
info_stats() const override;
|
||||||
void MakeScreenshot() override;
|
void MakeScreenshot() override;
|
||||||
|
bool Render_Preview( TAnimModel *Instance, int const Size, int const Margin, bool const Shadows, std::vector<std::uint8_t> &Image ) override;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -334,6 +335,16 @@ class opengl33_renderer : public gfx_renderer {
|
|||||||
|
|
||||||
void draw_debug_ui();
|
void draw_debug_ui();
|
||||||
|
|
||||||
|
// model previews
|
||||||
|
// extends provided bounds with the geometry of the submodel and its siblings and children visible in a preview
|
||||||
|
void Preview_Bounds( TSubModel const *Submodel, glm::dmat4 const &Transform, glm::dvec3 &Min, glm::dvec3 &Max ) const;
|
||||||
|
// prepares the offscreen targets of model previews for square images of specified size
|
||||||
|
bool Preview_Targets( int const Size );
|
||||||
|
// draws the instance on its own with the current render pass settings
|
||||||
|
void Preview_Model( TAnimModel *Instance, float const Squaredistance, bool const Alpha );
|
||||||
|
// draws the lit instance into the hdr preview target, marking in its stencil the pixels covered by the model
|
||||||
|
void Preview_Color( TAnimModel *Instance, float const Squaredistance, int const Size, glm::dvec3 const &Eye, glm::mat4 const &View, glm::mat4 const &Projection, bool const Shadows );
|
||||||
|
|
||||||
// members
|
// members
|
||||||
GLFWwindow *m_window{nullptr}; // main window
|
GLFWwindow *m_window{nullptr}; // main window
|
||||||
gfx::geometrybank_manager m_geometry;
|
gfx::geometrybank_manager m_geometry;
|
||||||
@@ -547,6 +558,19 @@ class opengl33_renderer : public gfx_renderer {
|
|||||||
std::unique_ptr<vr_interface> vr;
|
std::unique_ptr<vr_interface> vr;
|
||||||
bool debug_ui_active = false;
|
bool debug_ui_active = false;
|
||||||
|
|
||||||
|
// offscreen targets of model previews
|
||||||
|
struct preview_targets
|
||||||
|
{
|
||||||
|
int size { 0 };
|
||||||
|
std::unique_ptr<opengl_texture> hdr_tex;
|
||||||
|
std::unique_ptr<gl::renderbuffer> hdr_rbds; // depth and stencil, the stencil marks pixels covered by the model
|
||||||
|
std::unique_ptr<gl::framebuffer> hdr_fb;
|
||||||
|
std::unique_ptr<opengl_texture> ldr_tex;
|
||||||
|
std::unique_ptr<gl::framebuffer> ldr_fb;
|
||||||
|
} m_preview;
|
||||||
|
// set while a preview is drawn: submodels faded out entirely are skipped, instead of being drawn invisible over the stencil
|
||||||
|
bool m_previewing { false };
|
||||||
|
|
||||||
static bool renderer_register;
|
static bool renderer_register;
|
||||||
|
|
||||||
class opengl33_imgui_renderer : public imgui_renderer
|
class opengl33_imgui_renderer : public imgui_renderer
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ public:
|
|||||||
// imgui renderer
|
// imgui renderer
|
||||||
virtual class imgui_renderer *GetImguiRenderer() = 0;
|
virtual class imgui_renderer *GetImguiRenderer() = 0;
|
||||||
virtual void MakeScreenshot() = 0;
|
virtual void MakeScreenshot() = 0;
|
||||||
|
// draws the model of a scenery instance on its own into a square image of Size pixels on white background, the model
|
||||||
|
// seen in perspective from the front, side and above and filling the image less Margin pixels at each edge.
|
||||||
|
// Image receives rgb rows, top row first. returns false if the backend can't make previews or there's nothing to draw
|
||||||
|
virtual auto Render_Preview( TAnimModel *Instance, int const Size, int const Margin, bool const Shadows, std::vector<std::uint8_t> &Image ) -> bool { return false; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class gfx_renderer_factory
|
class gfx_renderer_factory
|
||||||
|
|||||||
Reference in New Issue
Block a user