17
0
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:
2026-10-08 18:52:51 +02:00
parent 917c8f7285
commit e661b9f1c7
6 changed files with 900 additions and 2 deletions

View File

@@ -112,6 +112,7 @@ set(SOURCES
"EU07.cpp"
"export_e3d_standalone.cpp"
"export_btf_standalone.cpp"
"nodebank_previews_standalone.cpp"
"world/Event.cpp"
"world/EvLaunch.cpp"
"utilities/Float3d.cpp"

View File

@@ -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);
int export_btf_standalone(std::string in, std::string out);
int generate_nodebank_previews(int Argc, char *Argv[]);
#include <ctime>
#include <string>
@@ -99,6 +100,14 @@ int main(int argc, char *argv[])
int dynamic = std::stoi(std::string(argv[5]));
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>]
else if (argc >= 4 && std::string(argv[1]) == "-btf")
{

View 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);
}

View File

@@ -3982,7 +3982,7 @@ void opengl33_renderer::Render(TSubModel *Submodel)
{
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;
@@ -4998,7 +4998,7 @@ void opengl33_renderer::Render_Alpha_text(TSubModel *Submodel)
void opengl33_renderer::Render_Alpha(TSubModel *Submodel)
{
// 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;
@@ -5549,6 +5549,414 @@ void opengl33_renderer::MakeScreenshot()
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)
{
glDebug("Update_Lights");

View File

@@ -130,6 +130,7 @@ class opengl33_renderer : public gfx_renderer {
std::string const &
info_stats() const 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();
// 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
GLFWwindow *m_window{nullptr}; // main window
gfx::geometrybank_manager m_geometry;
@@ -547,6 +558,19 @@ class opengl33_renderer : public gfx_renderer {
std::unique_ptr<vr_interface> vr;
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;
class opengl33_imgui_renderer : public imgui_renderer

View File

@@ -93,6 +93,10 @@ public:
// imgui renderer
virtual class imgui_renderer *GetImguiRenderer() = 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