diff --git a/include/light/light.hh b/include/light/light.hh index 8ae51f4a..b5a6728b 100644 --- a/include/light/light.hh +++ b/include/light/light.hh @@ -429,24 +429,20 @@ public: extern settings::light_settings light_options; -extern std::vector filebase; -extern std::vector lit_filebase; -extern std::vector lux_filebase; - const std::unordered_map> &UncompressedVis(); bool IsOutputtingSupplementaryData(); -std::span &LightSurfaces(); +std::span &LightSurfaces(); std::vector &EmissiveLightSurfaces(); extern std::vector extended_texinfo_flags; +lightmap_t *Lightmap_ForStyle(lightmapdict_t *lightmaps, const int style, const lightsurf_t *lightsurf); + // public functions void FixupGlobalSettings(); -void GetFileSpace(uint8_t **lightdata, uint8_t **colordata, uint8_t **deluxdata, int size); -void GetFileSpace_PreserveOffsetInBsp(uint8_t **lightdata, uint8_t **colordata, uint8_t **deluxdata, int lightofs); const modelinfo_t *ModelInfoForModel(const mbsp_t *bsp, int modelnum); /** * returns nullptr for "skip" faces diff --git a/include/light/ltface.hh b/include/light/ltface.hh index 3053f3f7..2ba8636e 100644 --- a/include/light/ltface.hh +++ b/include/light/ltface.hh @@ -42,7 +42,7 @@ extern std::atomic total_light_rays, total_light_ray_hits, total_sampl extern std::atomic total_bounce_rays, total_bounce_ray_hits; extern std::atomic total_surflight_rays, total_surflight_ray_hits; // mxd #endif -extern std::atomic fully_transparent_lightmaps; +extern std::atomic fully_transparent_lightmaps; // write.cc void PrintFaceInfo(const mface_t *face, const mbsp_t *bsp); // FIXME: remove light param. add normal param and dir params. @@ -55,10 +55,6 @@ bool Face_IsEmissive(const mbsp_t *bsp, const mface_t *face); void DirectLightFace(const mbsp_t *bsp, lightsurf_t &lightsurf, const settings::worldspawn_keys &cfg); void IndirectLightFace(const mbsp_t *bsp, lightsurf_t &lightsurf, const settings::worldspawn_keys &cfg, size_t bounce_depth); void PostProcessLightFace(const mbsp_t *bsp, lightsurf_t &lightsurf, const settings::worldspawn_keys &cfg); -void FinishLightmapSurface(const mbsp_t *bsp, lightsurf_t *lightsurf); -void SaveLightmapSurface(const mbsp_t *bsp, mface_t *face, facesup_t *facesup, - bspx_decoupled_lm_perface *facesup_decoupled, lightsurf_t *lightsurf, const faceextents_t &extents, - const faceextents_t &output_extents); struct lightgrid_sample_t { diff --git a/include/light/litfile.hh b/include/light/write.hh similarity index 86% rename from include/light/litfile.hh rename to include/light/write.hh index 5a104728..9d685bec 100644 --- a/include/light/litfile.hh +++ b/include/light/write.hh @@ -26,6 +26,7 @@ #include struct mbsp_t; +struct bspdata_t; constexpr int32_t LIT_VERSION = 1; @@ -66,5 +67,7 @@ struct facesup_t twosided extent; }; -void WriteLitFile(const mbsp_t *bsp, const std::vector &facesup, const fs::path &filename, int version); -void WriteLuxFile(const mbsp_t *bsp, const fs::path &filename, int version); +void WriteLitFile(const mbsp_t *bsp, const std::vector &facesup, const fs::path &filename, int version, const std::vector &lit_filebase, const std::vector &lux_filebase); +void WriteLuxFile(const mbsp_t *bsp, const fs::path &filename, int version, const std::vector &lux_filebase); + +void SaveLightmapSurfaces(bspdata_t *bspdata, const fs::path &source); diff --git a/light/CMakeLists.txt b/light/CMakeLists.txt index 6e2e829a..197127f4 100644 --- a/light/CMakeLists.txt +++ b/light/CMakeLists.txt @@ -10,11 +10,10 @@ set(LIGHT_INCLUDES ../include/light/surflight.hh ../include/light/ltface.hh ../include/light/trace.hh - ../include/light/litfile.hh) + ../include/light/write.hh) set(LIGHT_SOURCES entities.cc - litfile.cc ltface.cc trace.cc light.cc @@ -22,6 +21,7 @@ set(LIGHT_SOURCES phong.cc bounce.cc surflight.cc + write.cc ${LIGHT_INCLUDES}) if (embree_FOUND) diff --git a/light/entities.cc b/light/entities.cc index d1627c63..66201da2 100644 --- a/light/entities.cc +++ b/light/entities.cc @@ -27,7 +27,7 @@ #include #include -#include +#include #include #include #include diff --git a/light/light.cc b/light/light.cc index eedf80d4..a971fcf9 100644 --- a/light/light.cc +++ b/light/light.cc @@ -29,7 +29,7 @@ #include //mxd #include #include -#include // for facesup_t +#include // for facesup_t #include #include @@ -65,7 +65,7 @@ static std::span light_surfaces_span; // light_surfaces filtered down to just the emissive ones static std::vector emissive_light_surfaces; -std::span &LightSurfaces() +std::span &LightSurfaces() { return light_surfaces_span; } @@ -86,35 +86,14 @@ static void UpdateEmissiveLightSurfacesList() } } -static std::vector faces_sup; // lit2/bspx stuff -static std::vector facesup_decoupled_global; +std::vector faces_sup; // lit2/bspx stuff +std::vector facesup_decoupled_global; bool IsOutputtingSupplementaryData() { return !faces_sup.empty(); } -/// start of lightmap data -std::vector filebase; -/// offset of start of free space after data (should be kept a multiple of 4) -static int file_p; -/// offset of end of free space for lightmap data -static int file_end; - -/// start of litfile data -std::vector lit_filebase; -/// offset of start of free space after litfile data (should be kept a multiple of 12) -static int lit_file_p; -/// offset of end of space for litfile data -static int lit_file_end; - -/// start of luxfile data -std::vector lux_filebase; -/// offset of start of free space after luxfile data (should be kept a multiple of 12) -static int lux_file_p; -/// offset of end of space for luxfile data -static int lux_file_end; - static std::unordered_map> all_uncompressed_vis; const std::unordered_map> &UncompressedVis() @@ -554,82 +533,6 @@ void FixupGlobalSettings() } } -static std::mutex light_mutex; - -/* - * Return space for the lightmap and colourmap at the same time so it can - * be done in a thread-safe manner. - * - * size is the number of greyscale pixels = number of bytes to allocate - * and return in *lightdata - */ -void GetFileSpace(uint8_t **lightdata, uint8_t **colordata, uint8_t **deluxdata, int size) -{ - light_mutex.lock(); - - *lightdata = *colordata = *deluxdata = nullptr; - - if (!filebase.empty()) { - *lightdata = filebase.data() + file_p; - } - if (!lit_filebase.empty()) { - *colordata = lit_filebase.data() + lit_file_p; - } - if (!lux_filebase.empty()) { - *deluxdata = lux_filebase.data() + lux_file_p; - } - - // if size isn't a multiple of 4, round up to the next multiple of 4 - if ((size % 4) != 0) { - size += (4 - (size % 4)); - } - - // increment the next writing offsets, aligning them to 4 uint8_t boundaries (file_p) - // and 12-uint8_t boundaries (lit_file_p/lux_file_p) - if (!filebase.empty()) { - file_p += size; - } - if (!lit_filebase.empty()) { - lit_file_p += 3 * size; - } - if (!lux_filebase.empty()) { - lux_file_p += 3 * size; - } - - light_mutex.unlock(); - - if (file_p > file_end) - FError("overrun"); - - if (lit_file_p > lit_file_end) - FError("overrun"); -} - -/** - * Special version of GetFileSpace for when we're relighting a .bsp and can't modify it. - * In this case the offsets are already known. - */ -void GetFileSpace_PreserveOffsetInBsp(uint8_t **lightdata, uint8_t **colordata, uint8_t **deluxdata, int lightofs) -{ - Q_assert(lightofs >= 0); - - *lightdata = *colordata = *deluxdata = nullptr; - - if (!filebase.empty()) { - *lightdata = filebase.data() + lightofs; - } - - if (colordata && !lit_filebase.empty()) { - *colordata = lit_filebase.data() + (lightofs * 3); - } - - if (deluxdata && !lux_filebase.empty()) { - *deluxdata = lux_filebase.data() + (lightofs * 3); - } - - // NOTE: file_p et. al. are not updated, since we're not dynamically allocating the lightmaps -} - const modelinfo_t *ModelInfoForModel(const mbsp_t *bsp, int modelnum) { return modelinfo.at(modelnum); @@ -734,44 +637,6 @@ static void CreateLightmapSurfaces(mbsp_t *bsp) }); } -static void SaveLightmapSurfaces(mbsp_t *bsp) -{ - logging::funcheader(); - logging::parallel_for(static_cast(0), bsp->dfaces.size(), [&bsp](size_t i) { - auto &surf = light_surfaces[i]; - - if (surf.samples.empty()) { - return; - } - - FinishLightmapSurface(bsp, &surf); - - auto f = &bsp->dfaces[i]; - const modelinfo_t *face_modelinfo = ModelInfoForFace(bsp, i); - - if (!facesup_decoupled_global.empty()) { - SaveLightmapSurface( - bsp, f, nullptr, &facesup_decoupled_global[i], &surf, surf.extents, surf.extents); - } else if (faces_sup.empty()) { - SaveLightmapSurface(bsp, f, nullptr, nullptr, &surf, surf.extents, surf.extents); - } else if (light_options.novanilla.value() || faces_sup[i].lmscale == face_modelinfo->lightmapscale) { - if (faces_sup[i].lmscale == face_modelinfo->lightmapscale) { - f->lightofs = faces_sup[i].lightofs; - } else { - f->lightofs = -1; - } - SaveLightmapSurface(bsp, f, &faces_sup[i], nullptr, &surf, surf.extents, surf.extents); - for (int j = 0; j < MAXLIGHTMAPS; j++) { - f->styles[j] = - faces_sup[i].styles[j] == INVALID_LIGHTSTYLE ? INVALID_LIGHTSTYLE_OLD : faces_sup[i].styles[j]; - } - } else { - SaveLightmapSurface(bsp, f, nullptr, nullptr, &surf, surf.extents, surf.vanilla_extents); - SaveLightmapSurface(bsp, f, &faces_sup[i], nullptr, &surf, surf.extents, surf.extents); - } - }); -} - static void ClearLightmapSurfaces() { logging::funcheader(); @@ -854,47 +719,18 @@ static void FindModelInfo(const mbsp_t *bsp) Q_assert(modelinfo.size() == bsp->dmodels.size()); } -// FIXME: in theory can't we calculate the exact amount of -// storage required? we'd have to expand it by 4 to account for -// lightstyles though -static constexpr size_t MAX_MAP_LIGHTING = 0x8000000; - /* * ============= * LightWorld * ============= */ -static void LightWorld(bspdata_t *bspdata, bool forcedscale) +static void LightWorld(bspdata_t *bspdata, const fs::path &source, bool forcedscale) { logging::funcheader(); mbsp_t &bsp = std::get(bspdata->bsp); ClearLightmapSurfaces(); - filebase.clear(); - lit_filebase.clear(); - lux_filebase.clear(); - - if (!bsp.loadversion->game->has_rgb_lightmap) { - /* greyscale data stored in a separate buffer */ - filebase.resize(MAX_MAP_LIGHTING); - file_p = 0; - file_end = MAX_MAP_LIGHTING; - } - - if (bsp.loadversion->game->has_rgb_lightmap || light_options.write_litfile) { - /* litfile data stored in a separate buffer */ - lit_filebase.resize(MAX_MAP_LIGHTING * 3); - lit_file_p = 0; - lit_file_end = (MAX_MAP_LIGHTING * 3); - } - - if (light_options.write_luxfile) { - /* lux data stored in a separate buffer */ - lux_filebase.resize(MAX_MAP_LIGHTING * 3); - lux_file_p = 0; - lux_file_end = (MAX_MAP_LIGHTING * 3); - } if (forcedscale) { bspdata->bspx.entries.erase("LMSHIFT"); @@ -996,23 +832,7 @@ static void LightWorld(bspdata_t *bspdata, bool forcedscale) }); } - SaveLightmapSurfaces(&bsp); - - logging::print("Lighting Completed.\n\n"); - - // Transfer greyscale lightmap (or color lightmap for Q2/HL) to the bsp and update lightdatasize - if (!light_options.litonly.value()) { - if (bsp.loadversion->game->has_rgb_lightmap) { - bsp.dlightdata.resize(lit_file_p); - memcpy(bsp.dlightdata.data(), lit_filebase.data(), bsp.dlightdata.size()); - } else { - bsp.dlightdata.resize(file_p); - memcpy(bsp.dlightdata.data(), filebase.data(), bsp.dlightdata.size()); - } - } else { - // NOTE: bsp.lightdatasize is already valid in the -litonly case - } - logging::print("lightdatasize: {}\n", bsp.dlightdata.size()); + SaveLightmapSurfaces(bspdata, source); // kill this stuff if its somehow found. bspdata->bspx.entries.erase("LMSTYLE16"); @@ -1476,18 +1296,6 @@ static void ResetLight() faces_sup.clear(); facesup_decoupled_global.clear(); - filebase.clear(); - file_p = 0; - file_end = 0; - - lit_filebase.clear(); - lit_file_p = 0; - lit_file_end = 0; - - lux_filebase.clear(); - lux_file_p = 0; - lux_file_end = 0; - all_uncompressed_vis.clear(); modelinfo.clear(); tracelist.clear(); @@ -1629,7 +1437,7 @@ int light_main(int argc, const char **argv) SetupDirt(light_options); - LightWorld(&bspdata, light_options.lightmap_scale.is_changed()); + LightWorld(&bspdata, source, light_options.lightmap_scale.is_changed()); LightGrid(&bspdata); @@ -1642,30 +1450,9 @@ int light_main(int argc, const char **argv) WriteNormals(bsp, bspdata); } - /*invalidate any bspx lighting info early*/ - bspdata.bspx.entries.erase("RGBLIGHTING"); - bspdata.bspx.entries.erase("LIGHTINGDIR"); - if (light_options.write_litfile == lightfile::lit2) { - WriteLitFile(&bsp, faces_sup, source, 2); return 0; // run away before any files are written } - - /*fixme: add a new per-surface offset+lmscale lump for compat/versitility?*/ - if (light_options.write_litfile & lightfile::external) { - WriteLitFile(&bsp, faces_sup, source, LIT_VERSION); - } - if (light_options.write_litfile & lightfile::bspx) { - lit_filebase.resize(bsp.dlightdata.size() * 3); - bspdata.bspx.transfer("RGBLIGHTING", lit_filebase); - } - if (light_options.write_luxfile & lightfile::external) { - WriteLuxFile(&bsp, source, LIT_VERSION); - } - if (light_options.write_luxfile & lightfile::bspx) { - lux_filebase.resize(bsp.dlightdata.size() * 3); - bspdata.bspx.transfer("LIGHTINGDIR", lux_filebase); - } } /* -novanilla + internal lighting = no grey lightmap */ diff --git a/light/litfile.cc b/light/litfile.cc deleted file mode 100644 index 5f080599..00000000 --- a/light/litfile.cc +++ /dev/null @@ -1,102 +0,0 @@ -/* Copyright (C) 2002-2006 Kevin Shanahan - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - - See file, 'COPYING', for details. -*/ - -#include - -#include - -#include - -#include -#include -#include - -// litheader_t::v1_t - -void litheader_t::v1_t::stream_write(std::ostream &s) const -{ - s <= std::tie(ident, version); -} - -void litheader_t::v1_t::stream_read(std::istream &s) -{ - s >= std::tie(ident, version); -} - -// litheader_t::v2_t - -void litheader_t::v2_t::stream_write(std::ostream &s) const -{ - s <= std::tie(numsurfs, lmsamples); -} - -void litheader_t::v2_t::stream_read(std::istream &s) -{ - s >= std::tie(numsurfs, lmsamples); -} - -void WriteLitFile(const mbsp_t *bsp, const std::vector &facesup, const fs::path &filename, int version) -{ - litheader_t header; - - fs::path litname = filename; - litname.replace_extension("lit"); - - header.v1.version = version; - header.v2.numsurfs = bsp->dfaces.size(); - header.v2.lmsamples = bsp->dlightdata.size(); - - logging::print("Writing {}\n", litname); - std::ofstream litfile(litname, std::ios_base::out | std::ios_base::binary); - litfile <= header.v1; - if (version == 2) { - unsigned int i, j; - litfile <= header.v2; - for (i = 0; i < bsp->dfaces.size(); i++) { - litfile <= facesup[i].lightofs; - for (int j = 0; j < 4; j++) { - litfile <= facesup[i].styles[j]; - } - for (int j = 0; j < 2; j++) { - litfile <= facesup[i].extent[j]; - } - j = 0; - while (nth_bit(j) < facesup[i].lmscale) - j++; - litfile <= (uint8_t)j; - } - litfile.write((const char *)lit_filebase.data(), bsp->dlightdata.size() * 3); - litfile.write((const char *)lux_filebase.data(), bsp->dlightdata.size() * 3); - } else - litfile.write((const char *)lit_filebase.data(), bsp->dlightdata.size() * 3); -} - -void WriteLuxFile(const mbsp_t *bsp, const fs::path &filename, int version) -{ - litheader_t header; - - fs::path luxname = filename; - luxname.replace_extension("lux"); - - header.v1.version = version; - - std::ofstream luxfile(luxname, std::ios_base::out | std::ios_base::binary); - luxfile <= header.v1; - luxfile.write((const char *)lux_filebase.data(), bsp->dlightdata.size() * 3); -} diff --git a/light/ltface.cc b/light/ltface.cc index b800ec1c..0d8cb25d 100644 --- a/light/ltface.cc +++ b/light/ltface.cc @@ -26,7 +26,7 @@ #include #include #include -#include // for facesup_t +#include // for facesup_t #include #include @@ -45,8 +45,6 @@ std::atomic total_light_rays, total_light_ray_hits, total_samplepoints std::atomic total_bounce_rays, total_bounce_ray_hits; std::atomic total_surflight_rays, total_surflight_ray_hits; // mxd #endif -std::atomic fully_transparent_lightmaps; -static bool warned_about_light_map_overflow, warned_about_light_style_overflow; thread_local static raystream_occlusion_t occlusion_stream; thread_local static raystream_intersection_t intersection_stream; @@ -760,7 +758,7 @@ static const lightmap_t *Lightmap_ForStyle_ReadOnly(const lightsurf_t *lightsurf * Otherwise, return the next available map. A new map is not marked as * allocated since it may not be kept if no lights hit. */ -static lightmap_t *Lightmap_ForStyle(lightmapdict_t *lightmaps, const int style, const lightsurf_t *lightsurf) +lightmap_t *Lightmap_ForStyle(lightmapdict_t *lightmaps, const int style, const lightsurf_t *lightsurf) { for (auto &lm : *lightmaps) { if (lm.style == style) @@ -2422,56 +2420,6 @@ static void LightFace_CalculateDirt(lightsurf_t *lightsurf) } } -// clamps negative values. applies gamma and rangescale. clamps values over 255 -// N.B. we want to do this before smoothing / downscaling, so huge values don't mess up the averaging. -inline void LightFace_ScaleAndClamp(lightsurf_t *lightsurf) -{ - const settings::worldspawn_keys &cfg = *lightsurf->cfg; - - for (lightmap_t &lightmap : lightsurf->lightmapsByStyle) { - for (int i = 0; i < lightsurf->samples.size(); i++) { - qvec3f &color = lightmap.samples[i].color; - - /* Fix any negative values */ - color = qv::max(color, {0}); - - // before any other scaling, apply maxlight - if (lightsurf->maxlight || cfg.maxlight.value()) { - float maxcolor = qv::max(color); - // FIXME: for colored lighting, this doesn't seem to generate the right values... - float maxval = (lightsurf->maxlight ? lightsurf->maxlight : cfg.maxlight.value()) * 2.0f; - - if (maxcolor > maxval) { - color *= (maxval / maxcolor); - } - } - - // color scaling - if (lightsurf->lightcolorscale != 1.0f) { - qvec3f grayscale{qv::max(color)}; - color = mix(grayscale, color, lightsurf->lightcolorscale); - } - - /* Scale and handle gamma adjustment */ - color *= cfg.rangescale.value(); - - if (cfg.lightmapgamma.value() != 1.0f) { - for (auto &c : color) { - c = pow(c / 255.0f, 1.0f / cfg.lightmapgamma.value()) * 255.0f; - } - } - - // clamp - // FIXME: should this be a brightness clamp? - float maxcolor = qv::max(color); - - if (maxcolor > 255.0f) { - color *= (255.0f / maxcolor); - } - } - } -} - /** * Same as above LightFace_ScaleAndClamp, but for lightgrid * TODO: share code with above @@ -2521,34 +2469,6 @@ static void LightPoint_ScaleAndClamp(lightgrid_samples_t &result) } } -void FinishLightmapSurface(const mbsp_t *bsp, lightsurf_t *lightsurf) -{ - /* Apply gamma, rangescale, and clamp */ - LightFace_ScaleAndClamp(lightsurf); -} - -static float Lightmap_AvgBrightness(const lightmap_t *lm, const lightsurf_t *lightsurf) -{ - float avgb = 0; - for (int j = 0; j < lightsurf->samples.size(); j++) { - avgb += LightSample_Brightness(lm->samples[j].color); - } - avgb /= lightsurf->samples.size(); - return avgb; -} - -static float Lightmap_MaxBrightness(const lightmap_t *lm, const lightsurf_t *lightsurf) -{ - float maxb = 0; - for (int j = 0; j < lightsurf->samples.size(); j++) { - const float b = LightSample_Brightness(lm->samples[j].color); - if (b > maxb) { - maxb = b; - } - } - return maxb; -} - #if 0 static void WritePPM(const fs::path &fname, int width, int height, const uint8_t *rgbdata) { @@ -2616,251 +2536,6 @@ static void DumpDownscaledLightmap(const mbsp_t *bsp, const mface_t *face, int w } #endif -static std::vector LightmapColorsToGLMVector(const lightsurf_t *lightsurf, const lightmap_t *lm) -{ - std::vector res; - for (int i = 0; i < lightsurf->samples.size(); i++) { - const qvec3f &color = lm->samples[i].color; - const float alpha = lightsurf->samples[i].occluded ? 0.0f : 1.0f; - res.emplace_back(color[0], color[1], color[2], alpha); - } - return res; -} - -static std::vector LightmapNormalsToGLMVector(const lightsurf_t *lightsurf, const lightmap_t *lm) -{ - std::vector res; - for (int i = 0; i < lightsurf->samples.size(); i++) { - const qvec3f &color = lm->samples[i].direction; - const float alpha = lightsurf->samples[i].occluded ? 0.0f : 1.0f; - res.emplace_back(color[0], color[1], color[2], alpha); - } - return res; -} - -// Special handling of alpha channel: -// - "alpha channel" is expected to be 0 or 1. This gets set to 0 if the sample -// point is occluded (bmodel sticking outside of the world, or inside a shadow- -// casting bmodel that is overlapping a world face), otherwise it's 1. -// -// - If alpha is 0 the sample doesn't contribute to the filter kernel. -// - If all the samples in the filter kernel have alpha=0, write a sample with alpha=0 -// (but still average the colors, important so that minlight still works properly -// for bmodels that go outside of the world). -static std::vector IntegerDownsampleImage(const std::vector &input, int w, int h, int factor) -{ - Q_assert(factor >= 1); - if (factor == 1) - return input; - - const int outw = w / factor; - const int outh = h / factor; - - std::vector res(static_cast(outw * outh)); - - for (int y = 0; y < outh; y++) { - for (int x = 0; x < outw; x++) { - - float totalWeight = 0.0f; - qvec3f totalColor{}; - - // These are only used if all the samples in the kernel have alpha = 0 - float totalWeightIgnoringOcclusion = 0.0f; - qvec3f totalColorIgnoringOcclusion{}; - - const int extraradius = 0; - const int kernelextent = factor + (2 * extraradius); - - for (int y0 = 0; y0 < kernelextent; y0++) { - for (int x0 = 0; x0 < kernelextent; x0++) { - const int x1 = (x * factor) - extraradius + x0; - const int y1 = (y * factor) - extraradius + y0; - - // check if the kernel goes outside of the source image - if (x1 < 0 || x1 >= w) - continue; - if (y1 < 0 || y1 >= h) - continue; - - // read the input sample - const float weight = 1.0f; - const qvec4f &inSample = input.at((y1 * w) + x1); - - totalColorIgnoringOcclusion += qvec3f(inSample) * weight; - totalWeightIgnoringOcclusion += weight; - - // Occluded sample points don't contribute to the filter - if (inSample[3] == 0.0f) - continue; - - totalColor += qvec3f(inSample) * weight; - totalWeight += weight; - } - } - - const int outIndex = (y * outw) + x; - if (totalWeight > 0.0f) { - const qvec3f tmp = totalColor / totalWeight; - const qvec4f resultColor = qvec4f(tmp[0], tmp[1], tmp[2], 1.0f); - res[outIndex] = resultColor; - } else { - const qvec3f tmp = totalColorIgnoringOcclusion / totalWeightIgnoringOcclusion; - const qvec4f resultColor = qvec4f(tmp[0], tmp[1], tmp[2], 0.0f); - res[outIndex] = resultColor; - } - } - } - - return res; -} - -static std::vector FloodFillTransparent(const std::vector &input, int w, int h) -{ - // transparent pixels take the average of their neighbours. - - std::vector res(input); - - while (1) { - int unhandled_pixels = 0; - - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - const int i = (y * w) + x; - const qvec4f &inSample = res.at(i); - - if (inSample[3] == 0) { - // average the neighbouring non-transparent samples - - int opaque_neighbours = 0; - qvec3f neighbours_sum{}; - for (int y0 = -1; y0 <= 1; y0++) { - for (int x0 = -1; x0 <= 1; x0++) { - const int x1 = x + x0; - const int y1 = y + y0; - - if (x1 < 0 || x1 >= w) - continue; - if (y1 < 0 || y1 >= h) - continue; - - const qvec4f neighbourSample = res.at((y1 * w) + x1); - if (neighbourSample[3] == 1) { - opaque_neighbours++; - neighbours_sum += qvec3f(neighbourSample); - } - } - } - - if (opaque_neighbours > 0) { - neighbours_sum *= (1.0f / (float)opaque_neighbours); - res.at(i) = qvec4f(neighbours_sum[0], neighbours_sum[1], neighbours_sum[2], 1.0f); - - // this sample is now opaque - } else { - unhandled_pixels++; - - // all neighbours are transparent. need to perform more iterations (or the whole lightmap is - // transparent). - } - } - } - } - - if (unhandled_pixels == input.size()) { - // logging::funcprint("warning, fully transparent lightmap\n"); - fully_transparent_lightmaps++; - break; - } - - if (unhandled_pixels == 0) - break; // all done - } - - return res; -} - -static std::vector HighlightSeams(const std::vector &input, int w, int h) -{ - std::vector res(input); - - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - const int i = (y * w) + x; - const qvec4f &inSample = res.at(i); - - if (inSample[3] == 0) { - res.at(i) = qvec4f(255, 0, 0, 1); - } - } - } - - return res; -} - -static std::vector BoxBlurImage(const std::vector &input, int w, int h, int radius) -{ - std::vector res(input.size()); - - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - - float totalWeight = 0.0f; - qvec3f totalColor{}; - - // These are only used if all the samples in the kernel have alpha = 0 - float totalWeightIgnoringOcclusion = 0.0f; - qvec3f totalColorIgnoringOcclusion{}; - - for (int y0 = -radius; y0 <= radius; y0++) { - for (int x0 = -radius; x0 <= radius; x0++) { - const int x1 = std::clamp(x + x0, 0, w - 1); - const int y1 = std::clamp(y + y0, 0, h - 1); - - // check if the kernel goes outside of the source image - - // 2017-09-16: this is a hack, but clamping the - // x/y instead of discarding the samples outside of the - // kernel looks better in some cases: - // https://github.com/ericwa/ericw-tools/issues/171 -#if 0 - if (x1 < 0 || x1 >= w) - continue; - if (y1 < 0 || y1 >= h) - continue; -#endif - - // read the input sample - const float weight = 1.0f; - const qvec4f &inSample = input.at((y1 * w) + x1); - - totalColorIgnoringOcclusion += qvec3f(inSample) * weight; - totalWeightIgnoringOcclusion += weight; - - // Occluded sample points don't contribute to the filter - if (inSample[3] == 0.0f) - continue; - - totalColor += qvec3f(inSample) * weight; - totalWeight += weight; - } - } - - const int outIndex = (y * w) + x; - if (totalWeight > 0.0f) { - const qvec3f tmp = totalColor / totalWeight; - const qvec4f resultColor = qvec4f(tmp[0], tmp[1], tmp[2], 1.0f); - res[outIndex] = resultColor; - } else { - const qvec3f tmp = totalColorIgnoringOcclusion / totalWeightIgnoringOcclusion; - const qvec4f resultColor = qvec4f(tmp[0], tmp[1], tmp[2], 0.0f); - res[outIndex] = resultColor; - } - } - } - - return res; -} - // check if the given face can actually store luxel data bool Face_IsLightmapped(const mbsp_t *bsp, const mface_t *face) { @@ -2892,425 +2567,6 @@ bool Face_IsEmissive(const mbsp_t *bsp, const mface_t *face) return bsp->loadversion->game->surf_is_emissive(texinfo->flags, texname); } -/** - * - Writes (actual_width * actual_height) bytes to `out` - * - Writes (actual_width * actual_height * 3) bytes to `lit` - * - Writes (actual_width * actual_height * 3) bytes to `lux` - */ -static void WriteSingleLightmap(const mbsp_t *bsp, const mface_t *face, const lightsurf_t *lightsurf, - const lightmap_t *lm, const int actual_width, const int actual_height, uint8_t *out, uint8_t *lit, uint8_t *lux, - const faceextents_t &output_extents) -{ - const int oversampled_width = actual_width * light_options.extra.value(); - const int oversampled_height = actual_height * light_options.extra.value(); - - // allocate new float buffers for the output colors and directions - // these are the actual output width*height, without oversampling. - - std::vector fullres = LightmapColorsToGLMVector(lightsurf, lm); - - if (light_options.highlightseams.value()) { - fullres = HighlightSeams(fullres, oversampled_width, oversampled_height); - } - - // removes all transparent pixels by averaging from adjacent pixels - fullres = FloodFillTransparent(fullres, oversampled_width, oversampled_height); - - if (light_options.soft.value() > 0) { - fullres = BoxBlurImage(fullres, oversampled_width, oversampled_height, light_options.soft.value()); - } - - const std::vector output_color = - IntegerDownsampleImage(fullres, oversampled_width, oversampled_height, light_options.extra.value()); - std::optional> output_dir; - - if (lux) { - output_dir = IntegerDownsampleImage(LightmapNormalsToGLMVector(lightsurf, lm), oversampled_width, - oversampled_height, light_options.extra.value()); - } - - // copy from the float buffers to byte buffers in .bsp / .lit / .lux - const int output_width = output_extents.width(); - const int output_height = output_extents.height(); - - for (int t = 0; t < output_height; t++) { - for (int s = 0; s < output_width; s++) { - const int input_sample_s = (s / (float)output_width) * actual_width; - const int input_sample_t = (t / (float)output_height) * actual_height; - const int sampleindex = (input_sample_t * actual_width) + input_sample_s; - - if (lit || out) { - const qvec4f &color = output_color.at(sampleindex); - - if (lit) { - *lit++ = color[0]; - *lit++ = color[1]; - *lit++ = color[2]; - } - - if (out) { - /* Take the max() of the 3 components to get the value to write to the - .bsp lightmap. this avoids issues with some engines - that require the lit and internal lightmap to have the same - intensity. (MarkV, some QW engines) - - This must be max(), see LightNormalize in MarkV 1036. - */ - float light = std::max({color[0], color[1], color[2]}); - if (light < 0) - light = 0; - if (light > 255) - light = 255; - *out++ = light; - } - } - - if (lux) { - qvec3f direction = output_dir->at(sampleindex).xyz(); - qvec3f temp = {qv::dot(direction, lightsurf->snormal), qv::dot(direction, lightsurf->tnormal), - qv::dot(direction, lightsurf->plane.normal)}; - - if (qv::emptyExact(temp)) - temp = {0, 0, 1}; - else - qv::normalizeInPlace(temp); - - int v = (temp[0] + 1) * 128; - *lux++ = (v > 255) ? 255 : v; - v = (temp[1] + 1) * 128; - *lux++ = (v > 255) ? 255 : v; - v = (temp[2] + 1) * 128; - *lux++ = (v > 255) ? 255 : v; - } - } - } -} - -/** - * - Writes (output_width * output_height) bytes to `out` - * - Writes (output_width * output_height * 3) bytes to `lit` - * - Writes (output_width * output_height * 3) bytes to `lux` - */ -static void WriteSingleLightmap_FromDecoupled(const mbsp_t *bsp, const mface_t *face, const lightsurf_t *lightsurf, - const lightmap_t *lm, const int output_width, const int output_height, uint8_t *out, uint8_t *lit, uint8_t *lux) -{ - // this is the lightmap data in the "decoupled" coordinate system - std::vector fullres = LightmapColorsToGLMVector(lightsurf, lm); - - // maps a luxel in the vanilla lightmap to the corresponding position in the decoupled lightmap - const qmat4x4f vanillaLMToDecoupled = - lightsurf->extents.worldToLMMatrix * lightsurf->vanilla_extents.lmToWorldMatrix; - - // samples the "decoupled" lightmap at an integer coordinate, with clamping - auto tex = [&lightsurf, &fullres](int x, int y) -> qvec4f { - const int x_clamped = std::clamp(x, 0, lightsurf->width - 1); - const int y_clamped = std::clamp(y, 0, lightsurf->height - 1); - - const int sampleindex = (y_clamped * lightsurf->width) + x_clamped; - assert(sampleindex >= 0); - assert(sampleindex < fullres.size()); - - return fullres[sampleindex]; - }; - - for (int t = 0; t < output_height; t++) { - for (int s = 0; s < output_width; s++) { - // convert from vanilla lm coord to decoupled lm coord - qvec2f decoupled_lm_coord = vanillaLMToDecoupled * qvec4f(s, t, 0, 1); - - decoupled_lm_coord = decoupled_lm_coord * light_options.extra.value(); - - // split into integer/fractional part for bilinear interpolation - const int coord_floor_x = (int)decoupled_lm_coord[0]; - const int coord_floor_y = (int)decoupled_lm_coord[1]; - - const float coord_frac_x = decoupled_lm_coord[0] - coord_floor_x; - const float coord_frac_y = decoupled_lm_coord[1] - coord_floor_y; - - // 2D bilinear interpolation - const qvec4f color = - mix(mix(tex(coord_floor_x, coord_floor_y), tex(coord_floor_x + 1, coord_floor_y), coord_frac_x), - mix(tex(coord_floor_x, coord_floor_y + 1), tex(coord_floor_x + 1, coord_floor_y + 1), coord_frac_x), - coord_frac_y); - - if (lit || out) { - if (lit) { - *lit++ = color[0]; - *lit++ = color[1]; - *lit++ = color[2]; - } - - if (out) { - // FIXME: implement - *out++ = 0; - } - } - - if (lux) { - // FIXME: implement - *lux++ = 0; - *lux++ = 0; - *lux++ = 0; - } - } - } -} - -void SaveLightmapSurface(const mbsp_t *bsp, mface_t *face, facesup_t *facesup, - bspx_decoupled_lm_perface *facesup_decoupled, lightsurf_t *lightsurf, const faceextents_t &extents, - const faceextents_t &output_extents) -{ - lightmapdict_t &lightmaps = lightsurf->lightmapsByStyle; - const int actual_width = extents.width(); - const int actual_height = extents.height(); - const int output_width = output_extents.width(); - const int output_height = output_extents.height(); - const int size = output_extents.numsamples(); - - if (light_options.litonly.value()) { - // special case for writing a .lit for a bsp without modifying the bsp. - // involves looking at which styles were written to the bsp in the previous lighting run, and then - // writing the same styles to the same offsets in the .lit file. - - if (face->lightofs == -1) { - // nothing to write for this face - return; - } - - uint8_t *out, *lit, *lux; - GetFileSpace_PreserveOffsetInBsp(&out, &lit, &lux, face->lightofs); - - for (int mapnum = 0; mapnum < MAXLIGHTMAPS; mapnum++) { - const int style = face->styles[mapnum]; - - if (style == 255) { - break; // all done for this face - } - - // see if we have computed lighting for this style - for (const lightmap_t &lm : lightmaps) { - if (lm.style == style) { - WriteSingleLightmap( - bsp, face, lightsurf, &lm, actual_width, actual_height, out, lit, lux, output_extents); - break; - } - } - // if we didn't find a matching lightmap, just don't write anything - - if (out) { - out += size; - } - if (lit) { - lit += (size * 3); - } - if (lux) { - lux += (size * 3); - } - } - - return; - } - - size_t maxfstyles = std::min((size_t)light_options.facestyles.value(), facesup ? MAXLIGHTMAPSSUP : MAXLIGHTMAPS); - int maxstyle = facesup ? INVALID_LIGHTSTYLE : INVALID_LIGHTSTYLE_OLD; - - // intermediate collection for sorting lightmaps - std::vector> sortable; - - for (const lightmap_t &lightmap : lightmaps) { - // skip un-saved lightmaps - if (lightmap.style == INVALID_LIGHTSTYLE) - continue; - if (lightmap.style > maxstyle || (facesup && lightmap.style > INVALID_LIGHTSTYLE_OLD)) { - if (!warned_about_light_style_overflow) { - if (IsOutputtingSupplementaryData()) { - logging::print( - "INFO: a face has exceeded max light style id ({});\n LMSTYLE16 will be output to hold the non-truncated data.\n Use -verbose to find which faces.\n", - maxstyle, lightsurf->samples[0].point); - } else { - logging::print( - "WARNING: a face has exceeded max light style id ({}). Use -verbose to find which faces.\n", - maxstyle, lightsurf->samples[0].point); - } - warned_about_light_style_overflow = true; - } - logging::print(logging::flag::VERBOSE, "WARNING: Style {} too high on face near {}\n", lightmap.style, - lightsurf->samples[0].point); - continue; - } - - // skip lightmaps where all samples have brightness below 1 - if (bsp->loadversion->game->id != GAME_QUAKE_II) { // HACK: don't do this on Q2. seems if all styles are 0xff, - // the face is drawn fullbright instead of black (Q1) - const float maxb = Lightmap_MaxBrightness(&lightmap, lightsurf); - if (maxb < 1) - continue; - } - - const float avgb = Lightmap_AvgBrightness(&lightmap, lightsurf); - sortable.emplace_back(avgb, &lightmap); - } - - // HACK: in Q2, if lightofs is -1, then it's drawn fullbright, - // so we can't optimize away unused portions of the lightmap. - if (bsp->loadversion->game->id == GAME_QUAKE_II) { - if (!sortable.size()) { - lightmap_t *lm = Lightmap_ForStyle(&lightmaps, 0, lightsurf); - lm->style = 0; - for (auto &sample : lightsurf->samples) { - sample.occluded = false; - } - sortable.emplace_back(0, lm); - } - } - - // sort in descending order of average brightness - std::sort(sortable.begin(), sortable.end()); - std::reverse(sortable.begin(), sortable.end()); - - std::vector sorted; - for (const auto &pair : sortable) { - if (sorted.size() == maxfstyles) { - if (!warned_about_light_map_overflow) { - if (IsOutputtingSupplementaryData()) { - logging::print( - "INFO: a face has exceeded max light styles ({});\n LMSTYLE/LMSTYLE16 will be output to hold the non-truncated data.\n Use -verbose to find which faces.\n", - maxfstyles, lightsurf->samples[0].point); - } else { - logging::print( - "WARNING: a face has exceeded max light styles ({}). Use -verbose to find which faces.\n", - maxfstyles, lightsurf->samples[0].point); - } - warned_about_light_map_overflow = true; - } - logging::print(logging::flag::VERBOSE, - "WARNING: {} light styles (max {}) on face near {}; styles: ", sortable.size(), maxfstyles, - lightsurf->samples[0].point); - for (auto &p : sortable) { - logging::print(logging::flag::VERBOSE, "{} ", p.second->style); - } - logging::print(logging::flag::VERBOSE, "\n"); - break; - } - - sorted.push_back(pair.second); - } - - /* final number of lightmaps */ - const int numstyles = static_cast(sorted.size()); - Q_assert(numstyles <= MAXLIGHTMAPSSUP); - - if (bsp->loadversion->game->id == GAME_QUAKE_II) { - Q_assert(numstyles > 0); - } - - /* update face info (either core data or supplementary stuff) */ - if (facesup) { - facesup->extent[0] = output_width; - facesup->extent[1] = output_height; - int mapnum; - for (mapnum = 0; mapnum < numstyles && mapnum < MAXLIGHTMAPSSUP; mapnum++) { - facesup->styles[mapnum] = sorted.at(mapnum)->style; - } - for (; mapnum < MAXLIGHTMAPSSUP; mapnum++) { - facesup->styles[mapnum] = INVALID_LIGHTSTYLE; - } - facesup->lmscale = lightsurf->lightmapscale; - } else { - int mapnum; - for (mapnum = 0; mapnum < numstyles && mapnum < MAXLIGHTMAPS; mapnum++) { - face->styles[mapnum] = sorted.at(mapnum)->style; - } - for (; mapnum < MAXLIGHTMAPS; mapnum++) { - face->styles[mapnum] = INVALID_LIGHTSTYLE_OLD; - } - - if (facesup_decoupled) { - facesup_decoupled->lmwidth = output_width; - facesup_decoupled->lmheight = output_height; - for (size_t i = 0; i < 2; ++i) { - facesup_decoupled->world_to_lm_space.set_row(i, output_extents.worldToLMMatrix.row(i)); - } - } - } - - if (!numstyles) - return; - - uint8_t *out, *lit, *lux; - GetFileSpace(&out, &lit, &lux, size * numstyles); - - int lightofs; - - // Q2/HL native colored lightmaps - if (bsp->loadversion->game->has_rgb_lightmap) { - lightofs = lit - lit_filebase.data(); - } else { - lightofs = out - filebase.data(); - } - - if (facesup_decoupled) { - facesup_decoupled->offset = lightofs; - face->lightofs = -1; - } else if (facesup) { - facesup->lightofs = lightofs; - } else { - face->lightofs = lightofs; - } - - // sanity check that we don't save a lightmap for a non-lightmapped face - { - Q_assert(Face_IsLightmapped(bsp, face)); - } - - for (int mapnum = 0; mapnum < numstyles; mapnum++) { - const lightmap_t *lm = sorted.at(mapnum); - - WriteSingleLightmap(bsp, face, lightsurf, lm, actual_width, actual_height, out, lit, lux, output_extents); - - if (out) { - out += size; - } - if (lit) { - lit += (size * 3); - } - if (lux) { - lux += (size * 3); - } - } - - // write vanilla lightmap if -world_units_per_luxel is in use but not -novanilla - if (facesup_decoupled && !light_options.novanilla.value()) { - // FIXME: duplicates some code from above - GetFileSpace(&out, &lit, &lux, lightsurf->vanilla_extents.numsamples() * numstyles); - - // Q2/HL native colored lightmaps - if (bsp->loadversion->game->has_rgb_lightmap) { - lightofs = lit - lit_filebase.data(); - } else { - lightofs = out - filebase.data(); - } - face->lightofs = lightofs; - - for (int mapnum = 0; mapnum < numstyles; mapnum++) { - const lightmap_t *lm = sorted.at(mapnum); - - WriteSingleLightmap_FromDecoupled(bsp, face, lightsurf, lm, lightsurf->vanilla_extents.width(), - lightsurf->vanilla_extents.height(), out, lit, lux); - - if (out) { - out += lightsurf->vanilla_extents.numsamples(); - } - if (lit) { - lit += (lightsurf->vanilla_extents.numsamples() * 3); - } - if (lux) { - lux += (lightsurf->vanilla_extents.numsamples() * 3); - } - } - } -} - lightsurf_t CreateLightmapSurface(const mbsp_t *bsp, const mface_t *face, const facesup_t *facesup, const bspx_decoupled_lm_perface *facesup_decoupled, const settings::worldspawn_keys &cfg) { @@ -3707,9 +2963,4 @@ void ResetLtFace() total_surflight_rays = 0; total_surflight_ray_hits = 0; #endif - - fully_transparent_lightmaps = 0; - - warned_about_light_map_overflow = false; - warned_about_light_style_overflow = false; } diff --git a/light/write.cc b/light/write.cc new file mode 100644 index 00000000..c6ca357c --- /dev/null +++ b/light/write.cc @@ -0,0 +1,1109 @@ +/* Copyright (C) 1996-1997 Id Software, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + See file, 'COPYING', for details. +*/ + +#include +#include +#include + +#include +#include + +// litheader_t::v1_t + +void litheader_t::v1_t::stream_write(std::ostream &s) const +{ + s <= std::tie(ident, version); +} + +void litheader_t::v1_t::stream_read(std::istream &s) +{ + s >= std::tie(ident, version); +} + +// litheader_t::v2_t + +void litheader_t::v2_t::stream_write(std::ostream &s) const +{ + s <= std::tie(numsurfs, lmsamples); +} + +void litheader_t::v2_t::stream_read(std::istream &s) +{ + s >= std::tie(numsurfs, lmsamples); +} + +void WriteLitFile(const mbsp_t *bsp, const std::vector &facesup, const fs::path &filename, int version, const std::vector &lit_filebase, const std::vector &lux_filebase) +{ + litheader_t header; + + fs::path litname = filename; + litname.replace_extension("lit"); + + header.v1.version = version; + header.v2.numsurfs = bsp->dfaces.size(); + header.v2.lmsamples = bsp->dlightdata.size(); + + logging::print("Writing {}\n", litname); + std::ofstream litfile(litname, std::ios_base::out | std::ios_base::binary); + litfile <= header.v1; + if (version == 2) { + unsigned int i, j; + litfile <= header.v2; + for (i = 0; i < bsp->dfaces.size(); i++) { + litfile <= facesup[i].lightofs; + for (int j = 0; j < 4; j++) { + litfile <= facesup[i].styles[j]; + } + for (int j = 0; j < 2; j++) { + litfile <= facesup[i].extent[j]; + } + j = 0; + while (nth_bit(j) < facesup[i].lmscale) + j++; + litfile <= (uint8_t)j; + } + litfile.write((const char *)lit_filebase.data(), bsp->dlightdata.size() * 3); + litfile.write((const char *)lux_filebase.data(), bsp->dlightdata.size() * 3); + } else + litfile.write((const char *)lit_filebase.data(), bsp->dlightdata.size() * 3); +} + +void WriteLuxFile(const mbsp_t *bsp, const fs::path &filename, int version, const std::vector &lux_filebase) +{ + litheader_t header; + + fs::path luxname = filename; + luxname.replace_extension("lux"); + + header.v1.version = version; + + std::ofstream luxfile(luxname, std::ios_base::out | std::ios_base::binary); + luxfile <= header.v1; + luxfile.write((const char *)lux_filebase.data(), bsp->dlightdata.size() * 3); +} + + +/* + * Return space for the lightmap and colourmap at the same time so it can + * be done in a thread-safe manner. + * + * size is the number of greyscale pixels = number of bytes to allocate + * and return in *lightdata + */ +static inline int GetFileSpace(std::atomic_size_t &offset, size_t size) +{ + // if size isn't a multiple of 4, round up to the next multiple of 4 + size_t v = offset.fetch_add(size + (4 - (size % 4))); + + // early check + if (v > std::numeric_limits::max()) + FError("exceeded max lightmap space"); + + return v; +} + +std::atomic fully_transparent_lightmaps; +static bool warned_about_light_map_overflow, warned_about_light_style_overflow; + +static std::vector LightmapColorsToGLMVector(const lightsurf_t *lightsurf, const lightmap_t *lm) +{ + std::vector res; + for (int i = 0; i < lightsurf->samples.size(); i++) { + const qvec3f &color = lm->samples[i].color; + const float alpha = lightsurf->samples[i].occluded ? 0.0f : 1.0f; + res.emplace_back(color[0], color[1], color[2], alpha); + } + return res; +} + +static std::vector LightmapNormalsToGLMVector(const lightsurf_t *lightsurf, const lightmap_t *lm) +{ + std::vector res; + for (int i = 0; i < lightsurf->samples.size(); i++) { + const qvec3f &color = lm->samples[i].direction; + const float alpha = lightsurf->samples[i].occluded ? 0.0f : 1.0f; + res.emplace_back(color[0], color[1], color[2], alpha); + } + return res; +} + +// Special handling of alpha channel: +// - "alpha channel" is expected to be 0 or 1. This gets set to 0 if the sample +// point is occluded (bmodel sticking outside of the world, or inside a shadow- +// casting bmodel that is overlapping a world face), otherwise it's 1. +// +// - If alpha is 0 the sample doesn't contribute to the filter kernel. +// - If all the samples in the filter kernel have alpha=0, write a sample with alpha=0 +// (but still average the colors, important so that minlight still works properly +// for bmodels that go outside of the world). +static std::vector IntegerDownsampleImage(const std::vector &input, int w, int h, int factor) +{ + Q_assert(factor >= 1); + if (factor == 1) + return input; + + const int outw = w / factor; + const int outh = h / factor; + + std::vector res(static_cast(outw * outh)); + + for (int y = 0; y < outh; y++) { + for (int x = 0; x < outw; x++) { + + float totalWeight = 0.0f; + qvec3f totalColor{}; + + // These are only used if all the samples in the kernel have alpha = 0 + float totalWeightIgnoringOcclusion = 0.0f; + qvec3f totalColorIgnoringOcclusion{}; + + const int extraradius = 0; + const int kernelextent = factor + (2 * extraradius); + + for (int y0 = 0; y0 < kernelextent; y0++) { + for (int x0 = 0; x0 < kernelextent; x0++) { + const int x1 = (x * factor) - extraradius + x0; + const int y1 = (y * factor) - extraradius + y0; + + // check if the kernel goes outside of the source image + if (x1 < 0 || x1 >= w) + continue; + if (y1 < 0 || y1 >= h) + continue; + + // read the input sample + const float weight = 1.0f; + const qvec4f &inSample = input.at((y1 * w) + x1); + + totalColorIgnoringOcclusion += qvec3f(inSample) * weight; + totalWeightIgnoringOcclusion += weight; + + // Occluded sample points don't contribute to the filter + if (inSample[3] == 0.0f) + continue; + + totalColor += qvec3f(inSample) * weight; + totalWeight += weight; + } + } + + const int outIndex = (y * outw) + x; + if (totalWeight > 0.0f) { + const qvec3f tmp = totalColor / totalWeight; + const qvec4f resultColor = qvec4f(tmp[0], tmp[1], tmp[2], 1.0f); + res[outIndex] = resultColor; + } else { + const qvec3f tmp = totalColorIgnoringOcclusion / totalWeightIgnoringOcclusion; + const qvec4f resultColor = qvec4f(tmp[0], tmp[1], tmp[2], 0.0f); + res[outIndex] = resultColor; + } + } + } + + return res; +} + +static std::vector FloodFillTransparent(const std::vector &input, int w, int h) +{ + // transparent pixels take the average of their neighbours. + + std::vector res(input); + + while (1) { + int unhandled_pixels = 0; + + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + const int i = (y * w) + x; + const qvec4f &inSample = res.at(i); + + if (inSample[3] == 0) { + // average the neighbouring non-transparent samples + + int opaque_neighbours = 0; + qvec3f neighbours_sum{}; + for (int y0 = -1; y0 <= 1; y0++) { + for (int x0 = -1; x0 <= 1; x0++) { + const int x1 = x + x0; + const int y1 = y + y0; + + if (x1 < 0 || x1 >= w) + continue; + if (y1 < 0 || y1 >= h) + continue; + + const qvec4f neighbourSample = res.at((y1 * w) + x1); + if (neighbourSample[3] == 1) { + opaque_neighbours++; + neighbours_sum += qvec3f(neighbourSample); + } + } + } + + if (opaque_neighbours > 0) { + neighbours_sum *= (1.0f / (float)opaque_neighbours); + res.at(i) = qvec4f(neighbours_sum[0], neighbours_sum[1], neighbours_sum[2], 1.0f); + + // this sample is now opaque + } else { + unhandled_pixels++; + + // all neighbours are transparent. need to perform more iterations (or the whole lightmap is + // transparent). + } + } + } + } + + if (unhandled_pixels == input.size()) { + // logging::funcprint("warning, fully transparent lightmap\n"); + fully_transparent_lightmaps++; + break; + } + + if (unhandled_pixels == 0) + break; // all done + } + + return res; +} + +static std::vector HighlightSeams(const std::vector &input, int w, int h) +{ + std::vector res(input); + + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + const int i = (y * w) + x; + const qvec4f &inSample = res.at(i); + + if (inSample[3] == 0) { + res.at(i) = qvec4f(255, 0, 0, 1); + } + } + } + + return res; +} + +static std::vector BoxBlurImage(const std::vector &input, int w, int h, int radius) +{ + std::vector res(input.size()); + + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + + float totalWeight = 0.0f; + qvec3f totalColor{}; + + // These are only used if all the samples in the kernel have alpha = 0 + float totalWeightIgnoringOcclusion = 0.0f; + qvec3f totalColorIgnoringOcclusion{}; + + for (int y0 = -radius; y0 <= radius; y0++) { + for (int x0 = -radius; x0 <= radius; x0++) { + const int x1 = std::clamp(x + x0, 0, w - 1); + const int y1 = std::clamp(y + y0, 0, h - 1); + + // check if the kernel goes outside of the source image + + // 2017-09-16: this is a hack, but clamping the + // x/y instead of discarding the samples outside of the + // kernel looks better in some cases: + // https://github.com/ericwa/ericw-tools/issues/171 +#if 0 + if (x1 < 0 || x1 >= w) + continue; + if (y1 < 0 || y1 >= h) + continue; +#endif + + // read the input sample + const float weight = 1.0f; + const qvec4f &inSample = input.at((y1 * w) + x1); + + totalColorIgnoringOcclusion += qvec3f(inSample) * weight; + totalWeightIgnoringOcclusion += weight; + + // Occluded sample points don't contribute to the filter + if (inSample[3] == 0.0f) + continue; + + totalColor += qvec3f(inSample) * weight; + totalWeight += weight; + } + } + + const int outIndex = (y * w) + x; + if (totalWeight > 0.0f) { + const qvec3f tmp = totalColor / totalWeight; + const qvec4f resultColor = qvec4f(tmp[0], tmp[1], tmp[2], 1.0f); + res[outIndex] = resultColor; + } else { + const qvec3f tmp = totalColorIgnoringOcclusion / totalWeightIgnoringOcclusion; + const qvec4f resultColor = qvec4f(tmp[0], tmp[1], tmp[2], 0.0f); + res[outIndex] = resultColor; + } + } + } + + return res; +} + +/** + * - Writes (actual_width * actual_height) bytes to `out` + * - Writes (actual_width * actual_height * 3) bytes to `lit` + * - Writes (actual_width * actual_height * 3) bytes to `lux` + */ +static void WriteSingleLightmap(const mbsp_t *bsp, const mface_t *face, const lightsurf_t *lightsurf, + const lightmap_t *lm, const int actual_width, const int actual_height, uint8_t *out, uint8_t *lit, uint8_t *lux, + const faceextents_t &output_extents) +{ + const int oversampled_width = actual_width * light_options.extra.value(); + const int oversampled_height = actual_height * light_options.extra.value(); + + // allocate new float buffers for the output colors and directions + // these are the actual output width*height, without oversampling. + + std::vector fullres = LightmapColorsToGLMVector(lightsurf, lm); + + if (light_options.highlightseams.value()) { + fullres = HighlightSeams(fullres, oversampled_width, oversampled_height); + } + + // removes all transparent pixels by averaging from adjacent pixels + fullres = FloodFillTransparent(fullres, oversampled_width, oversampled_height); + + if (light_options.soft.value() > 0) { + fullres = BoxBlurImage(fullres, oversampled_width, oversampled_height, light_options.soft.value()); + } + + const std::vector output_color = + IntegerDownsampleImage(fullres, oversampled_width, oversampled_height, light_options.extra.value()); + std::optional> output_dir; + + if (lux) { + output_dir = IntegerDownsampleImage(LightmapNormalsToGLMVector(lightsurf, lm), oversampled_width, + oversampled_height, light_options.extra.value()); + } + + // copy from the float buffers to byte buffers in .bsp / .lit / .lux + const int output_width = output_extents.width(); + const int output_height = output_extents.height(); + + for (int t = 0; t < output_height; t++) { + for (int s = 0; s < output_width; s++) { + const int input_sample_s = (s / (float)output_width) * actual_width; + const int input_sample_t = (t / (float)output_height) * actual_height; + const int sampleindex = (input_sample_t * actual_width) + input_sample_s; + + if (lit || out) { + const qvec4f &color = output_color.at(sampleindex); + + if (lit) { + *lit++ = color[0]; + *lit++ = color[1]; + *lit++ = color[2]; + } + + if (out) { + /* Take the max() of the 3 components to get the value to write to the + .bsp lightmap. this avoids issues with some engines + that require the lit and internal lightmap to have the same + intensity. (MarkV, some QW engines) + + This must be max(), see LightNormalize in MarkV 1036. + */ + float light = std::max({color[0], color[1], color[2]}); + if (light < 0) + light = 0; + if (light > 255) + light = 255; + *out++ = light; + } + } + + if (lux) { + qvec3f direction = output_dir->at(sampleindex).xyz(); + qvec3f temp = {qv::dot(direction, lightsurf->snormal), qv::dot(direction, lightsurf->tnormal), + qv::dot(direction, lightsurf->plane.normal)}; + + if (qv::emptyExact(temp)) + temp = {0, 0, 1}; + else + qv::normalizeInPlace(temp); + + int v = (temp[0] + 1) * 128; + *lux++ = (v > 255) ? 255 : v; + v = (temp[1] + 1) * 128; + *lux++ = (v > 255) ? 255 : v; + v = (temp[2] + 1) * 128; + *lux++ = (v > 255) ? 255 : v; + } + } + } +} + +/** + * - Writes (output_width * output_height) bytes to `out` + * - Writes (output_width * output_height * 3) bytes to `lit` + * - Writes (output_width * output_height * 3) bytes to `lux` + */ +static void WriteSingleLightmap_FromDecoupled(const mbsp_t *bsp, const mface_t *face, const lightsurf_t *lightsurf, + const lightmap_t *lm, const int output_width, const int output_height, uint8_t *out, uint8_t *lit, uint8_t *lux) +{ + // this is the lightmap data in the "decoupled" coordinate system + std::vector fullres = LightmapColorsToGLMVector(lightsurf, lm); + + // maps a luxel in the vanilla lightmap to the corresponding position in the decoupled lightmap + const qmat4x4f vanillaLMToDecoupled = + lightsurf->extents.worldToLMMatrix * lightsurf->vanilla_extents.lmToWorldMatrix; + + // samples the "decoupled" lightmap at an integer coordinate, with clamping + auto tex = [&lightsurf, &fullres](int x, int y) -> qvec4f { + const int x_clamped = std::clamp(x, 0, lightsurf->width - 1); + const int y_clamped = std::clamp(y, 0, lightsurf->height - 1); + + const int sampleindex = (y_clamped * lightsurf->width) + x_clamped; + assert(sampleindex >= 0); + assert(sampleindex < fullres.size()); + + return fullres[sampleindex]; + }; + + for (int t = 0; t < output_height; t++) { + for (int s = 0; s < output_width; s++) { + // convert from vanilla lm coord to decoupled lm coord + qvec2f decoupled_lm_coord = vanillaLMToDecoupled * qvec4f(s, t, 0, 1); + + decoupled_lm_coord = decoupled_lm_coord * light_options.extra.value(); + + // split into integer/fractional part for bilinear interpolation + const int coord_floor_x = (int)decoupled_lm_coord[0]; + const int coord_floor_y = (int)decoupled_lm_coord[1]; + + const float coord_frac_x = decoupled_lm_coord[0] - coord_floor_x; + const float coord_frac_y = decoupled_lm_coord[1] - coord_floor_y; + + // 2D bilinear interpolation + const qvec4f color = + mix(mix(tex(coord_floor_x, coord_floor_y), tex(coord_floor_x + 1, coord_floor_y), coord_frac_x), + mix(tex(coord_floor_x, coord_floor_y + 1), tex(coord_floor_x + 1, coord_floor_y + 1), coord_frac_x), + coord_frac_y); + + if (lit || out) { + if (lit) { + *lit++ = color[0]; + *lit++ = color[1]; + *lit++ = color[2]; + } + + if (out) { + // FIXME: implement + *out++ = 0; + } + } + + if (lux) { + // FIXME: implement + *lux++ = 0; + *lux++ = 0; + *lux++ = 0; + } + } + } +} + +// clamps negative values. applies gamma and rangescale. clamps values over 255 +// N.B. we want to do this before smoothing / downscaling, so huge values don't mess up the averaging. +inline void LightFace_ScaleAndClamp(lightsurf_t *lightsurf) +{ + const settings::worldspawn_keys &cfg = *lightsurf->cfg; + + for (lightmap_t &lightmap : lightsurf->lightmapsByStyle) { + for (int i = 0; i < lightsurf->samples.size(); i++) { + qvec3f &color = lightmap.samples[i].color; + + /* Fix any negative values */ + color = qv::max(color, {0}); + + // before any other scaling, apply maxlight + if (lightsurf->maxlight || cfg.maxlight.value()) { + float maxcolor = qv::max(color); + // FIXME: for colored lighting, this doesn't seem to generate the right values... + float maxval = (lightsurf->maxlight ? lightsurf->maxlight : cfg.maxlight.value()) * 2.0f; + + if (maxcolor > maxval) { + color *= (maxval / maxcolor); + } + } + + // color scaling + if (lightsurf->lightcolorscale != 1.0f) { + qvec3f grayscale{qv::max(color)}; + color = mix(grayscale, color, lightsurf->lightcolorscale); + } + + /* Scale and handle gamma adjustment */ + color *= cfg.rangescale.value(); + + if (cfg.lightmapgamma.value() != 1.0f) { + for (auto &c : color) { + c = pow(c / 255.0f, 1.0f / cfg.lightmapgamma.value()) * 255.0f; + } + } + + // clamp + // FIXME: should this be a brightness clamp? + float maxcolor = qv::max(color); + + if (maxcolor > 255.0f) { + color *= (255.0f / maxcolor); + } + } + } +} + +void FinishLightmapSurface(const mbsp_t *bsp, lightsurf_t *lightsurf) +{ + /* Apply gamma, rangescale, and clamp */ + LightFace_ScaleAndClamp(lightsurf); +} + +static float Lightmap_AvgBrightness(const lightmap_t *lm, const lightsurf_t *lightsurf) +{ + float avgb = 0; + for (int j = 0; j < lightsurf->samples.size(); j++) { + avgb += LightSample_Brightness(lm->samples[j].color); + } + avgb /= lightsurf->samples.size(); + return avgb; +} + +static float Lightmap_MaxBrightness(const lightmap_t *lm, const lightsurf_t *lightsurf) +{ + float maxb = 0; + for (int j = 0; j < lightsurf->samples.size(); j++) { + const float b = LightSample_Brightness(lm->samples[j].color); + if (b > maxb) { + maxb = b; + } + } + return maxb; +} + +static void SaveLitOnlyLightmapSurface(const mbsp_t *bsp, mface_t *face, + lightsurf_t *lightsurf, const faceextents_t &extents, + const faceextents_t &output_extents, std::vector &filebase, std::vector &lit_filebase, std::vector &lux_filebase) +{ + lightmapdict_t &lightmaps = lightsurf->lightmapsByStyle; + const int actual_width = extents.width(); + const int actual_height = extents.height(); + const int size = output_extents.numsamples(); + + // special case for writing a .lit for a bsp without modifying the bsp. + // involves looking at which styles were written to the bsp in the previous lighting run, and then + // writing the same styles to the same offsets in the .lit file. + + if (face->lightofs == -1) { + // nothing to write for this face + return; + } + + uint8_t *out = nullptr, *lit = nullptr, *lux = nullptr; + + Q_assert(face->lightofs >= 0); + + if (!filebase.empty()) { + out = filebase.data() + face->lightofs; + } + + if (!lit_filebase.empty()) { + lit = lit_filebase.data() + (face->lightofs * 3); + } + + if (!lux_filebase.empty()) { + lux = lux_filebase.data() + (face->lightofs * 3); + } + + // NOTE: file_p et. al. are not updated, since we're not dynamically allocating the lightmaps + + for (int mapnum = 0; mapnum < MAXLIGHTMAPS; mapnum++) { + const int style = face->styles[mapnum]; + + if (style == 255) { + break; // all done for this face + } + + // see if we have computed lighting for this style + for (const lightmap_t &lm : lightmaps) { + if (lm.style == style) { + WriteSingleLightmap( + bsp, face, lightsurf, &lm, actual_width, actual_height, out, lit, lux, output_extents); + break; + } + } + // if we didn't find a matching lightmap, just don't write anything + + if (out) { + out += size; + } + if (lit) { + lit += (size * 3); + } + if (lux) { + lux += (size * 3); + } + } +} + +// data stored from Calculate into Save +struct lightmap_intermediate_data_t +{ + std::vector sorted; + int lightofs = -1, vanilla_lightofs = -1; +}; + +// temp +extern std::vector faces_sup; // lit2/bspx stuff +extern std::vector facesup_decoupled_global; + +int CalculateLightmapStyles(const mbsp_t *bsp, mface_t *face, facesup_t *facesup, + lightsurf_t *lightsurf, const faceextents_t &extents, + std::atomic_size_t &lightmap_size, + lightmap_intermediate_data_t &id) +{ + lightmapdict_t &lightmaps = lightsurf->lightmapsByStyle; + + size_t maxfstyles = std::min((size_t)light_options.facestyles.value(), facesup ? MAXLIGHTMAPSSUP : MAXLIGHTMAPS); + int maxstyle = facesup ? INVALID_LIGHTSTYLE : INVALID_LIGHTSTYLE_OLD; + + // intermediate collection for sorting lightmaps + std::vector> sortable; + + for (const lightmap_t &lightmap : lightmaps) { + // skip un-saved lightmaps + if (lightmap.style == INVALID_LIGHTSTYLE) + continue; + if (lightmap.style > maxstyle || (facesup && lightmap.style > INVALID_LIGHTSTYLE_OLD)) { + if (!warned_about_light_style_overflow) { + if (IsOutputtingSupplementaryData()) { + logging::print( + "INFO: a face has exceeded max light style id ({});\n LMSTYLE16 will be output to hold the non-truncated data.\n Use -verbose to find which faces.\n", + maxstyle, lightsurf->samples[0].point); + } else { + logging::print( + "WARNING: a face has exceeded max light style id ({}). Use -verbose to find which faces.\n", + maxstyle, lightsurf->samples[0].point); + } + warned_about_light_style_overflow = true; + } + logging::print(logging::flag::VERBOSE, "WARNING: Style {} too high on face near {}\n", lightmap.style, + lightsurf->samples[0].point); + continue; + } + + // skip lightmaps where all samples have brightness below 1 + if (bsp->loadversion->game->id != GAME_QUAKE_II) { // HACK: don't do this on Q2. seems if all styles are 0xff, + // the face is drawn fullbright instead of black (Q1) + const float maxb = Lightmap_MaxBrightness(&lightmap, lightsurf); + if (maxb < 1) + continue; + } + + const float avgb = Lightmap_AvgBrightness(&lightmap, lightsurf); + sortable.emplace_back(avgb, &lightmap); + } + + // HACK: in Q2, if lightofs is -1, then it's drawn fullbright, + // so we can't optimize away unused portions of the lightmap. + if (bsp->loadversion->game->id == GAME_QUAKE_II) { + if (!sortable.size()) { + lightmap_t *lm = Lightmap_ForStyle(&lightmaps, 0, lightsurf); + lm->style = 0; + for (auto &sample : lightsurf->samples) { + sample.occluded = false; + } + sortable.emplace_back(0, lm); + } + } + + // sort in descending order of average brightness + std::sort(sortable.begin(), sortable.end()); + std::reverse(sortable.begin(), sortable.end()); + + for (const auto &pair : sortable) { + if (id.sorted.size() == maxfstyles) { + if (!warned_about_light_map_overflow) { + if (IsOutputtingSupplementaryData()) { + logging::print( + "INFO: a face has exceeded max light styles ({});\n LMSTYLE/LMSTYLE16 will be output to hold the non-truncated data.\n Use -verbose to find which faces.\n", + maxfstyles, lightsurf->samples[0].point); + } else { + logging::print( + "WARNING: a face has exceeded max light styles ({}). Use -verbose to find which faces.\n", + maxfstyles, lightsurf->samples[0].point); + } + warned_about_light_map_overflow = true; + } + logging::print(logging::flag::VERBOSE, + "WARNING: {} light styles (max {}) on face near {}; styles: ", sortable.size(), maxfstyles, + lightsurf->samples[0].point); + for (auto &p : sortable) { + logging::print(logging::flag::VERBOSE, "{} ", p.second->style); + } + logging::print(logging::flag::VERBOSE, "\n"); + break; + } + + id.sorted.push_back(pair.second); + } + + /* final number of lightmaps */ + const int numstyles = static_cast(id.sorted.size()); + Q_assert(numstyles <= MAXLIGHTMAPSSUP); + + if (bsp->loadversion->game->id == GAME_QUAKE_II) { + Q_assert(numstyles > 0); + } + + return numstyles; +} + +void SaveLightmapSurface(const mbsp_t *bsp, mface_t *face, facesup_t *facesup, + bspx_decoupled_lm_perface *facesup_decoupled, lightsurf_t *lightsurf, const faceextents_t &extents, + const faceextents_t &output_extents, + std::vector &filebase, std::vector &lit_filebase, std::vector &lux_filebase, + lightmap_intermediate_data_t &id) +{ + const int output_width = output_extents.width(); + const int output_height = output_extents.height(); + + Q_assert(id.lightofs >= 0); + + /* update face info (either core data or supplementary stuff) */ + if (facesup) { + facesup->extent[0] = output_width; + facesup->extent[1] = output_height; + int mapnum; + for (mapnum = 0; mapnum < id.sorted.size() && mapnum < MAXLIGHTMAPSSUP; mapnum++) { + facesup->styles[mapnum] = id.sorted.at(mapnum)->style; + } + for (; mapnum < MAXLIGHTMAPSSUP; mapnum++) { + facesup->styles[mapnum] = INVALID_LIGHTSTYLE; + } + facesup->lmscale = lightsurf->lightmapscale; + } else { + int mapnum; + for (mapnum = 0; mapnum < id.sorted.size() && mapnum < MAXLIGHTMAPS; mapnum++) { + face->styles[mapnum] = id.sorted.at(mapnum)->style; + } + for (; mapnum < MAXLIGHTMAPS; mapnum++) { + face->styles[mapnum] = INVALID_LIGHTSTYLE_OLD; + } + + if (facesup_decoupled) { + facesup_decoupled->lmwidth = output_width; + facesup_decoupled->lmheight = output_height; + for (size_t i = 0; i < 2; ++i) { + facesup_decoupled->world_to_lm_space.set_row(i, output_extents.worldToLMMatrix.row(i)); + } + } + } + + uint8_t *out = nullptr, *lit = nullptr, *lux = nullptr; + + if (!filebase.empty()) { + out = filebase.data() + id.lightofs; + } + + if (!lit_filebase.empty()) { + lit = lit_filebase.data() + (id.lightofs * 3); + } + + if (!lux_filebase.empty()) { + lux = lux_filebase.data() + (id.lightofs * 3); + } + + int lightofs; + + // Q2/HL native colored lightmaps + if (bsp->loadversion->game->has_rgb_lightmap) { + lightofs = lit - lit_filebase.data(); + } else { + lightofs = out - filebase.data(); + } + + if (facesup_decoupled) { + facesup_decoupled->offset = lightofs; + face->lightofs = -1; + } else if (facesup) { + facesup->lightofs = lightofs; + } else { + face->lightofs = lightofs; + } + + // sanity check that we don't save a lightmap for a non-lightmapped face + Q_assert(Face_IsLightmapped(bsp, face)); + + const int actual_width = extents.width(); + const int actual_height = extents.height(); + const int size = output_extents.numsamples(); + + if (out) { + Q_assert((out - filebase.data()) + (size * id.sorted.size()) <= filebase.size()); + } + + if (lit) { + Q_assert((lit - lit_filebase.data()) + (size * 3 * id.sorted.size()) <= lit_filebase.size()); + } + + if (lux) { + Q_assert((lux - lux_filebase.data()) + (size * 3 * id.sorted.size()) <= lux_filebase.size()); + } + + for (int mapnum = 0; mapnum < id.sorted.size(); mapnum++) { + const lightmap_t *lm = id.sorted.at(mapnum); + + WriteSingleLightmap(bsp, face, lightsurf, lm, actual_width, actual_height, out, lit, lux, output_extents); + + if (out) { + out += size; + } + if (lit) { + lit += (size * 3); + } + if (lux) { + lux += (size * 3); + } + } + + // write vanilla lightmap if -world_units_per_luxel is in use but not -novanilla + if (facesup_decoupled && !light_options.novanilla.value()) { + size_t vanilla_size = lightsurf->vanilla_extents.numsamples(); + + Q_assert(id.vanilla_lightofs >= 0); + + if (!filebase.empty()) { + out = filebase.data() + id.vanilla_lightofs; + } + + if (!lit_filebase.empty()) { + lit = lit_filebase.data() + (id.vanilla_lightofs * 3); + } + + if (!lux_filebase.empty()) { + lux = lux_filebase.data() + (id.vanilla_lightofs * 3); + } + + // Q2/HL native colored lightmaps + if (bsp->loadversion->game->has_rgb_lightmap) { + lightofs = lit - lit_filebase.data(); + } else { + lightofs = out - filebase.data(); + } + face->lightofs = lightofs; + + for (int mapnum = 0; mapnum < id.sorted.size(); mapnum++) { + const lightmap_t *lm = id.sorted.at(mapnum); + + WriteSingleLightmap_FromDecoupled(bsp, face, lightsurf, lm, lightsurf->vanilla_extents.width(), + lightsurf->vanilla_extents.height(), out, lit, lux); + + if (out) { + out += vanilla_size; + } + if (lit) { + lit += (vanilla_size * 3); + } + if (lux) { + lux += (vanilla_size * 3); + } + } + } +} + +void SaveLightmapSurfaces(bspdata_t *bspdata, const fs::path &source) +{ + mbsp_t *bsp = &std::get(bspdata->bsp); + + logging::funcheader(); + + warned_about_light_map_overflow = warned_about_light_style_overflow = false; + fully_transparent_lightmaps = 0; + + // lightmap data storage + std::vector filebase, lit_filebase, lux_filebase; + + if (light_options.litonly.value()) { + + if (bsp->dlightdata.empty()) { + Error("no light data, but litonly was specified"); + } else if (bsp->loadversion->game->has_rgb_lightmap) { + Error("litonly is only useful for non-RGB lightmap games (Quake)"); + } + + filebase.resize(bsp->dlightdata.size()); + + if (light_options.write_litfile) { + lit_filebase.resize(filebase.size() * 3); + } + + if (light_options.write_luxfile) { + lux_filebase.resize(filebase.size() * 3); + } + + logging::parallel_for(static_cast(0), bsp->dfaces.size(), [&](size_t i) { + auto &surf = LightSurfaces()[i]; + + if (surf.samples.empty()) { + return; + } + + FinishLightmapSurface(bsp, &surf); + + auto f = &bsp->dfaces[i]; + + SaveLitOnlyLightmapSurface(bsp, f, &surf, surf.extents, surf.extents, filebase, lit_filebase, lux_filebase); + }); + } else { + std::atomic_size_t lightmap_size = 0; + std::vector intermediate_data; + intermediate_data.resize(bsp->dfaces.size()); + + // calculate finish lightmaps and calculate lightofs for each face. + // the lightofs will be set to the size in bytes. + logging::parallel_for(static_cast(0), bsp->dfaces.size(), [&](size_t i) { + auto &surf = LightSurfaces()[i]; + + if (surf.samples.empty()) { + return; + } + + FinishLightmapSurface(bsp, &surf); + + auto f = &bsp->dfaces[i]; + const modelinfo_t *face_modelinfo = ModelInfoForFace(bsp, i); + int num_styles; + + if (!facesup_decoupled_global.empty()) { + num_styles = CalculateLightmapStyles( + bsp, f, nullptr, &surf, surf.extents, lightmap_size, intermediate_data[i]); + + if (!light_options.novanilla.value()) { + intermediate_data[i].vanilla_lightofs = GetFileSpace(lightmap_size, surf.vanilla_extents.numsamples() * num_styles); + } + } else if (faces_sup.empty()) { + num_styles = CalculateLightmapStyles(bsp, f, nullptr, &surf, surf.extents, lightmap_size, intermediate_data[i]); + } else if (light_options.novanilla.value() || faces_sup[i].lmscale == face_modelinfo->lightmapscale) { + num_styles = CalculateLightmapStyles(bsp, f, &faces_sup[i], &surf, surf.extents, lightmap_size, intermediate_data[i]); + } else { + num_styles = CalculateLightmapStyles(bsp, f, nullptr, &surf, surf.extents, lightmap_size, intermediate_data[i]); + intermediate_data[i].vanilla_lightofs = GetFileSpace(lightmap_size, surf.vanilla_extents.numsamples() * num_styles); + } + + if (num_styles) { + intermediate_data[i].lightofs = GetFileSpace(lightmap_size, surf.extents.numsamples() * num_styles); + } + }); + + // allocate required space + if (!bsp->loadversion->game->has_rgb_lightmap) { + filebase.resize(lightmap_size); + } + + if (bsp->loadversion->game->has_rgb_lightmap || light_options.write_litfile) { + lit_filebase.resize(lightmap_size * 3); + } + + if (light_options.write_luxfile) { + lux_filebase.resize(lightmap_size * 3); + } + + logging::print(logging::flag::STAT, "lightmap size (total): {}\n", filebase.size() + lit_filebase.size() + lux_filebase.size()); + + logging::parallel_for(static_cast(0), bsp->dfaces.size(), [&](size_t i) { + auto &surf = LightSurfaces()[i]; + + if (surf.samples.empty()) { + return; + } + + auto f = &bsp->dfaces[i]; + const modelinfo_t *face_modelinfo = ModelInfoForFace(bsp, i); + + if (!facesup_decoupled_global.empty()) { + SaveLightmapSurface( + bsp, f, nullptr, &facesup_decoupled_global[i], &surf, surf.extents, surf.extents, filebase, lit_filebase, lux_filebase, intermediate_data[i]); + } else if (faces_sup.empty()) { + SaveLightmapSurface(bsp, f, nullptr, nullptr, &surf, surf.extents, surf.extents, filebase, lit_filebase, lux_filebase, intermediate_data[i]); + } else if (light_options.novanilla.value() || faces_sup[i].lmscale == face_modelinfo->lightmapscale) { + if (faces_sup[i].lmscale == face_modelinfo->lightmapscale) { + f->lightofs = faces_sup[i].lightofs; + } else { + f->lightofs = -1; + } + SaveLightmapSurface(bsp, f, &faces_sup[i], nullptr, &surf, surf.extents, surf.extents, filebase, lit_filebase, lux_filebase, intermediate_data[i]); + for (int j = 0; j < MAXLIGHTMAPS; j++) { + f->styles[j] = + faces_sup[i].styles[j] == INVALID_LIGHTSTYLE ? INVALID_LIGHTSTYLE_OLD : faces_sup[i].styles[j]; + } + } else { + SaveLightmapSurface(bsp, f, nullptr, nullptr, &surf, surf.extents, surf.vanilla_extents, filebase, lit_filebase, lux_filebase, intermediate_data[i]); + SaveLightmapSurface(bsp, f, &faces_sup[i], nullptr, &surf, surf.extents, surf.extents, filebase, lit_filebase, lux_filebase, intermediate_data[i]); + } + }); + } + + logging::print("Lighting Completed.\n\n"); + + // Transfer greyscale lightmap (or color lightmap for Q2/HL) to the bsp and update lightdatasize + // NOTE: bsp.lightdatasize is already valid in the -litonly case + if (!light_options.litonly.value()) { + if (bsp->loadversion->game->has_rgb_lightmap) { + bsp->dlightdata = std::move(lit_filebase); + } else { + bsp->dlightdata = std::move(filebase); + } + } + + bspdata->bspx.entries.erase("RGBLIGHTING"); + bspdata->bspx.entries.erase("LIGHTINGDIR"); + + if (light_options.write_litfile == lightfile::lit2) { + WriteLitFile(bsp, faces_sup, source, 2, lit_filebase, lux_filebase); + return; // run away before any files are written + } + + /*fixme: add a new per-surface offset+lmscale lump for compat/versitility?*/ + if (light_options.write_litfile & lightfile::external) { + WriteLitFile(bsp, faces_sup, source, LIT_VERSION, lit_filebase, lux_filebase); + } + if (light_options.write_litfile & lightfile::bspx) { + lit_filebase.resize(bsp->dlightdata.size() * 3); + bspdata->bspx.transfer("RGBLIGHTING", lit_filebase); + } + if (light_options.write_luxfile & lightfile::external) { + WriteLuxFile(bsp, source, LIT_VERSION, lux_filebase); + } + if (light_options.write_luxfile & lightfile::bspx) { + lux_filebase.resize(bsp->dlightdata.size() * 3); + bspdata->bspx.transfer("LIGHTINGDIR", lux_filebase); + } +} \ No newline at end of file