diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 13c6e9f6..3dde1907 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(common STATIC cmdlib.cc decompile.cc entdata.cc + litfile.cc log.cc mathlib.cc parser.cc @@ -36,6 +37,7 @@ add_library(common STATIC ../include/common/decompile.hh ../include/common/entdata.h ../include/common/iterators.hh + ../include/common/litfile.hh ../include/common/log.hh ../include/common/mathlib.hh ../include/common/numeric_cast.hh diff --git a/common/bspinfo.cc b/common/bspinfo.cc index 932441a7..7fa8c1e8 100644 --- a/common/bspinfo.cc +++ b/common/bspinfo.cc @@ -29,10 +29,10 @@ #include #include "common/fs.hh" #include "common/imglib.hh" +#include "common/litfile.hh" #define STB_IMAGE_WRITE_STATIC #define STB_IMAGE_WRITE_IMPLEMENTATION -#define STBI_WRITE_NO_STDIO #include "../3rdparty/stb_image_write.h" static std::string hex_string(const uint8_t *bytes, const size_t count) @@ -224,24 +224,40 @@ static faceextents_t get_face_extents(const mbsp_t &bsp, const bspxentries_t &bs (float)nth_bit(reinterpret_cast(bspx.at("LMSHIFT").data())[&face - bsp.dfaces.data()])}; } -full_atlas_t build_lightmap_atlas(const mbsp_t &bsp, const bspxentries_t &bspx, const std::vector &litdata, bool use_bspx, bool use_decoupled) +full_atlas_t build_lightmap_atlas(const mbsp_t &bsp, const bspxentries_t &bspx, const std::vector &litdata, + const std::vector &hdr_litdata, bool use_bspx, bool use_decoupled) { struct face_rect { const mface_t *face; faceextents_t extents; int32_t lightofs; - std::optional texture = std::nullopt; + + // lightmap data for this face + int width = 0, height = 0; + std::vector rgba8_samples; + std::vector e5brg9_samples; + size_t atlas = 0; size_t x = 0, y = 0; }; constexpr size_t atlas_size = 512; - const uint8_t *lightdata_source; - bool is_rgb; - bool is_lit; - if (!litdata.empty()) { + bool is_hdr = false; + const uint32_t *hdr_lightdata_source = nullptr; // 1 packed uint32 (e5brg9) per sample + const uint8_t *lightdata_source = nullptr; // either greyscale (1 byte per sample) or rgb (3 bytes per sample) + bool is_rgb = false; + bool is_lit = false; + + if (!hdr_litdata.empty()) { + hdr_lightdata_source = hdr_litdata.data(); + is_hdr = true; + } else if (auto it = bspx.find("LIGHTING_E5BGR9"); it != bspx.end()) { + // FIXME: alignment ignored + hdr_lightdata_source = reinterpret_cast(it->second.data()); + is_hdr = true; + } else if (!litdata.empty()) { is_lit = true; is_rgb = true; lightdata_source = litdata.data(); @@ -352,7 +368,7 @@ full_atlas_t build_lightmap_atlas(const mbsp_t &bsp, const bspxentries_t &bspx, } // calculate final atlas texture size - img::texture full_atlas; + single_style_atlas_t full_atlas; size_t sqrt_count = ceil(sqrt(atlasses.size())); size_t trimmed_width = 0, trimmed_height = 0; @@ -379,9 +395,12 @@ full_atlas_t build_lightmap_atlas(const mbsp_t &bsp, const bspxentries_t &bspx, } } - full_atlas.width = full_atlas.meta.width = trimmed_width; - full_atlas.height = full_atlas.meta.height = trimmed_height; - full_atlas.pixels.resize(full_atlas.width * full_atlas.height); + full_atlas.width = trimmed_width; + full_atlas.height = trimmed_height; + if (is_hdr) + full_atlas.e5brg9_samples.resize(full_atlas.width * full_atlas.height); + else + full_atlas.rgba8_samples.resize(full_atlas.width * full_atlas.height); full_atlas_t result; @@ -408,23 +427,42 @@ full_atlas_t build_lightmap_atlas(const mbsp_t &bsp, const bspxentries_t &bspx, continue; } - auto in_pixel = - lightdata_source + ((is_lit ? 3 : 1) * rect.lightofs) + (rect.extents.numsamples() * (is_rgb ? 3 : 1) * style_index); + if (!is_hdr) { + auto in_pixel = + lightdata_source + ((is_lit ? 3 : 1) * rect.lightofs) + + (rect.extents.numsamples() * (is_rgb ? 3 : 1) * style_index); + + for (size_t y = 0; y < rect.extents.height(); y++) { + for (size_t x = 0; x < rect.extents.width(); x++) { + size_t ox = rect.x + x; + size_t oy = rect.y + y; + + auto &out_pixel = full_atlas.rgba8_samples[(oy * full_atlas.width) + ox]; + out_pixel[3] = 255; + + if (is_rgb) { + out_pixel[0] = *in_pixel++; + out_pixel[1] = *in_pixel++; + out_pixel[2] = *in_pixel++; + } else { + out_pixel[0] = out_pixel[1] = out_pixel[2] = *in_pixel++; + } + } + } + } else { + // hdr - for (size_t y = 0; y < rect.extents.height(); y++) { - for (size_t x = 0; x < rect.extents.width(); x++) { - size_t ox = rect.x + x; - size_t oy = rect.y + y; + auto in_pixel = + hdr_lightdata_source + rect.lightofs + + (rect.extents.numsamples() * style_index); - auto &out_pixel = full_atlas.pixels[(oy * full_atlas.width) + ox]; - out_pixel[3] = 255; + for (size_t y = 0; y < rect.extents.height(); y++) { + for (size_t x = 0; x < rect.extents.width(); x++) { + size_t ox = rect.x + x; + size_t oy = rect.y + y; - if (is_rgb) { - out_pixel[0] = *in_pixel++; - out_pixel[1] = *in_pixel++; - out_pixel[2] = *in_pixel++; - } else { - out_pixel[0] = out_pixel[1] = out_pixel[2] = *in_pixel++; + auto &out_pixel = full_atlas.e5brg9_samples[(oy * full_atlas.width) + ox]; + out_pixel = *in_pixel++; } } } @@ -439,7 +477,11 @@ full_atlas_t build_lightmap_atlas(const mbsp_t &bsp, const bspxentries_t &bspx, // copy out the atlas texture result.style_to_lightmap_atlas[i] = full_atlas; - memset(full_atlas.pixels.data(), 0, sizeof(*full_atlas.pixels.data()) * full_atlas.pixels.size()); + if (!full_atlas.rgba8_samples.empty()) + memset(full_atlas.rgba8_samples.data(), 0, full_atlas.rgba8_samples.size()); + + if (!full_atlas.e5brg9_samples.empty()) + memset(full_atlas.e5brg9_samples.data(), 0, full_atlas.e5brg9_samples.size() * 4); } auto ExportLightmapUVs = [&full_atlas, &result](const mbsp_t *bsp, const face_rect &face) { @@ -477,7 +519,8 @@ static void export_obj_and_lightmaps(const mbsp_t &bsp, const bspxentries_t &bsp fs::path obj_path, const fs::path &lightmaps_path_base) { // FIXME: pass in .lit - const auto atlas = build_lightmap_atlas(bsp, bspx, {}, use_bspx, use_decoupled); + // FIXME: pass in hdr .lit + const auto atlas = build_lightmap_atlas(bsp, bspx, {}, {}, use_bspx, use_decoupled); if (atlas.facenum_to_lightmap_uvs.empty()) { return; @@ -486,17 +529,40 @@ static void export_obj_and_lightmaps(const mbsp_t &bsp, const bspxentries_t &bsp // e.g. mapname.bsp.lm const std::string stem = lightmaps_path_base.stem().string(); - // write .png's, one per style + // write .png's (or .hdr's, if e5bgr9 lightmaps), one per style for (const auto &[i, full_atlas] : atlas.style_to_lightmap_atlas) { + const bool is_hdr = !full_atlas.e5brg9_samples.empty(); auto lightmaps_path = lightmaps_path_base; - lightmaps_path.replace_filename(stem + "_" + std::to_string(i) + ".png"); + std::string extension = is_hdr ? ".hdr" : ".png"; + lightmaps_path.replace_filename(stem + "_" + std::to_string(i) + extension); + std::ofstream strm(lightmaps_path, std::ofstream::out | std::ofstream::binary); - stbi_write_png_to_func( - [](void *context, void *data, int size) { - std::ofstream &strm = *((std::ofstream *)context); - strm.write((const char *)data, size); - }, - &strm, full_atlas.width, full_atlas.height, 4, full_atlas.pixels.data(), full_atlas.width * 4); + + if (is_hdr) { + std::vector temp; // rgb components + + // unpack from e5bgr9 to 3x float + for (uint32_t sample : full_atlas.e5brg9_samples) { + qvec3f rgb = HDR_UnpackE5BRG9(sample); + temp.push_back(rgb[0]); + temp.push_back(rgb[1]); + temp.push_back(rgb[2]); + } + + stbi_write_hdr_to_func( + [](void *context, void *data, int size) { + std::ofstream &strm = *((std::ofstream *) context); + strm.write((const char *) data, size); + }, + &strm, full_atlas.width, full_atlas.height, 3, temp.data()); + } else { + stbi_write_png_to_func( + [](void *context, void *data, int size) { + std::ofstream &strm = *((std::ofstream *) context); + strm.write((const char *) data, size); + }, + &strm, full_atlas.width, full_atlas.height, 4, full_atlas.rgba8_samples.data(), full_atlas.width * 4); + } logging::print("wrote {}\n", lightmaps_path); } diff --git a/common/bsputils.cc b/common/bsputils.cc index 2b048009..28f6db7a 100644 --- a/common/bsputils.cc +++ b/common/bsputils.cc @@ -1085,10 +1085,23 @@ qvec3f faceextents_t::LMCoordToWorld(qvec2f lm) const } /** - * Samples the lightmap at an integer coordinate - * FIXME: this doesn't deal with styles at all + * Returns an offset, in samples, from the start of the face's lightmaps to the location of the given style data. + * Returns -1 if the face doesn't have lightmaps for that style. */ -qvec3b LM_Sample(const mbsp_t *bsp, const std::vector *lit, const faceextents_t &faceextents, +static int StyleOffset(int style, const mface_t *face, const faceextents_t &faceextents) +{ + for (int i = 0; i < face->styles.size(); ++i) { + if (face->styles[i] == style) { + return i * faceextents.width() * faceextents.height(); + } + } + return -1; +} + +/** + * Samples the lightmap at an integer coordinate in style 0 + */ +qvec3b LM_Sample(const mbsp_t *bsp, const mface_t *face, const lit_variant_t *lit, const faceextents_t &faceextents, int byte_offset_of_face, qvec2i coord) { if (byte_offset_of_face == -1) { @@ -1100,14 +1113,22 @@ qvec3b LM_Sample(const mbsp_t *bsp, const std::vector *lit, const facee Q_assert(coord[0] < faceextents.width()); Q_assert(coord[1] < faceextents.height()); - int pixel = coord[0] + (coord[1] * faceextents.width()); + int style_offset = StyleOffset(0, face, faceextents); + if (style_offset == -1) { + return {0, 0, 0}; + } + + int pixel = style_offset + coord[0] + (coord[1] * faceextents.width()); assert(byte_offset_of_face >= 0); const uint8_t *data = bsp->dlightdata.data(); if (lit) { - const uint8_t *lit_data = lit->data(); + if (!std::holds_alternative(*lit)) + throw std::runtime_error("not implemented"); + + const uint8_t *lit_data = std::get_if(lit)->rgbdata.data(); return qvec3f{lit_data[(3 * byte_offset_of_face) + (pixel * 3) + 0], lit_data[(3 * byte_offset_of_face) + (pixel * 3) + 1], @@ -1121,31 +1142,43 @@ qvec3b LM_Sample(const mbsp_t *bsp, const std::vector *lit, const facee } } -std::vector LoadLitFile(const fs::path &path) +qvec3f LM_Sample_HDR(const mbsp_t *bsp, const mface_t *face, + const faceextents_t &faceextents, + int byte_offset_of_face, qvec2i coord, + const lit_variant_t *lit, const bspxentries_t *bspx) { - std::ifstream stream(path, std::ios_base::in | std::ios_base::binary); - stream >> endianness; - - std::array ident; - stream >= ident; - if (ident != std::array{'Q', 'L', 'I', 'T'}) { - throw std::runtime_error("invalid lit ident"); + if (byte_offset_of_face == -1) { + return {0, 0, 0}; } - int version; - stream >= version; - if (version != 1) { - throw std::runtime_error("invalid lit version"); + Q_assert(coord[0] >= 0); + Q_assert(coord[1] >= 0); + Q_assert(coord[0] < faceextents.width()); + Q_assert(coord[1] < faceextents.height()); + + int style_offset = StyleOffset(0, face, faceextents); + if (style_offset == -1) { + return {0, 0, 0}; } - std::vector litdata; - while (stream.good()) { - uint8_t b; - stream >= b; - litdata.push_back(b); + int pixel = style_offset + coord[0] + (coord[1] * faceextents.width()); + + assert(byte_offset_of_face >= 0); + + const uint32_t *packed_samples = nullptr; + if (lit && std::holds_alternative(*lit)) { + packed_samples = std::get_if(lit)->samples.data(); + } else if (bspx) { + if (auto it = bspx->find("LIGHTING_E5BGR9"); it != bspx->end()) { + // FIXME: alignment ignored + packed_samples = reinterpret_cast(it->second.data()); + } } - return litdata; + if (!packed_samples) + throw std::runtime_error("LM_Sample_HDR requires either an HDR .lit file or BSPX lump"); + + return HDR_UnpackE5BRG9(packed_samples[byte_offset_of_face + pixel]); } static void AddLeafs(const mbsp_t *bsp, int nodenum, std::map> &cluster_to_leafnums) diff --git a/common/litfile.cc b/common/litfile.cc new file mode 100644 index 00000000..c6e2b065 --- /dev/null +++ b/common/litfile.cc @@ -0,0 +1,165 @@ +/* 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 + +// 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); +} + +/** + * Packs a float3 into a 32-bit integer. + * + * Follows the OpenGL 4.6 Core spec, section 8.5.2 Encoding of Special Internal Formats. + * + * See HDR_UnpackE5BRG9 for the format description. + */ +uint32_t HDR_PackE5BRG9(qvec3f rgb) +{ + constexpr int N = 9; // bits per component + constexpr int B = 15; // exponent bias + constexpr int Emax = 31; // max allowed exponent bias value + + // slightly under 2^16 + constexpr float max_representable = \ + (static_cast((1 << N) - 1) / static_cast(1 << N)) * \ + static_cast(1 << (Emax - B)); + + // clamp inputs + const float r = std::max(0.0f, std::min(rgb[0], max_representable)); + const float g = std::max(0.0f, std::min(rgb[1], max_representable)); + const float b = std::max(0.0f, std::min(rgb[2], max_representable)); + + const float max_comp = std::max(std::max(r, g), b); + + // avoid division by 0 below if the input is (0, 0, 0) + if (max_comp == 0.0f) + return 0; + + // preliminary shared exponent + const int prelim_exponent = std::max(-B - 1, (int)std::floor(std::log2(max_comp))) + 1 + B; + + // refined shared exponent + const int max_s = (int)std::floor((max_comp / std::pow(2.0f, prelim_exponent - B - N)) + 0.5f); + + int refined_exponent = std::clamp((max_s < (1 << N)) ? prelim_exponent : prelim_exponent + 1, 0, 0x1f); + + const float scale = std::pow(2.0f, refined_exponent - B - N); + + int r_integer = std::clamp((int)std::floor((r / scale) + 0.5), 0, 0x1ff); + int g_integer = std::clamp((int)std::floor((g / scale) + 0.5), 0, 0x1ff); + int b_integer = std::clamp((int)std::floor((b / scale) + 0.5), 0, 0x1ff); + + return (refined_exponent << 27) | (b_integer << 18) | (g_integer << 9) | (r_integer << 0); +} + +/** + * Takes a e5bgr9 value as used in the LIGHTING_E5BGR9 lump and unpacks it into a float3 + * in the order (red, green, blue). + * + * The packed format is, from highest-order to lowest-order bits: + * + * - top 5 bits: biased_exponent in [0, 31] + * - next 9 bits: blue_int in [0, 511] + * - next 9 bits: green_int in [0, 511] + * - bottom 9 bits: red_int in [0, 511] + * + * the conversion to floating point goes like: + * + * blue_float = 2^(biased_exponent - 24) * blue_int + * + * this is following OpenGL 4.6 Core spec, section 8.25 Shared Exponent Texture Color Conversion + */ +qvec3f HDR_UnpackE5BRG9(uint32_t packed) +{ + // grab the top 5 bits. this is a value in [0, 31]. + const uint32_t biased_exponent = packed >> 27; + // the actual exponent gets remapped to the range [-24, 7]. + const int exponent = static_cast(biased_exponent) - 24; + + const uint32_t blue_int = (packed >> 18) & 0x1ff; + const uint32_t green_int = (packed >> 9) & 0x1ff; + const uint32_t red_int = packed & 0x1ff; + + const float multiplier = std::pow(2.0f, static_cast(exponent)); + + return qvec3f(red_int, green_int, blue_int) * multiplier; +} + +lit_variant_t LoadLitFile(const fs::path &path) +{ + std::ifstream stream(path, std::ios_base::in | std::ios_base::binary); + if (!stream.good()) { + return { lit_none() }; + } + + stream >> endianness; + + std::array ident; + stream >= ident; + if (ident != std::array{'Q', 'L', 'I', 'T'}) { + throw std::runtime_error("invalid lit ident"); + } + + int version; + stream >= version; + if (version == LIT_VERSION) { + std::vector litdata; + while (stream.good()) { + uint8_t b; + stream >= b; + litdata.push_back(b); + } + return {lit1_t{.rgbdata = std::move(litdata)}}; + } else if (version == LIT_VERSION_E5BGR9) { + std::vector litdata; + while (stream.good()) { + uint32_t sample; + stream >= sample; + litdata.push_back(sample); + } + return {lit_hdr{.samples = std::move(litdata)}}; + } + + throw std::runtime_error("invalid lit version"); +} diff --git a/docs/light.rst b/docs/light.rst index 2454c6c3..88f861de 100644 --- a/docs/light.rst +++ b/docs/light.rst @@ -331,6 +331,14 @@ Experimental options Writes both rgb and directions data *only* into the bsp itself. +.. option:: -hdr + + Write .lit file with e5bgr9 data. + +.. option:: -bspxhdr + + Writes e5bgr9 data into the bsp itself. + .. option:: -novanilla Fallback scaled lighting will be omitted. Standard grey lighting will diff --git a/include/common/bspinfo.hh b/include/common/bspinfo.hh index 1fc1267c..b15a6da6 100644 --- a/include/common/bspinfo.hh +++ b/include/common/bspinfo.hh @@ -31,6 +31,15 @@ struct bspdata_t; struct mbsp_t; struct mface_t; +struct single_style_atlas_t +{ + int width = 0, height = 0; + + // only one of these will be populated + std::vector rgba8_samples; + std::vector e5brg9_samples; +}; + struct full_atlas_t { /** @@ -38,9 +47,10 @@ struct full_atlas_t */ std::map> facenum_to_lightmap_uvs; - std::map style_to_lightmap_atlas; + std::map style_to_lightmap_atlas; }; -full_atlas_t build_lightmap_atlas(const mbsp_t &bsp, const bspxentries_t &bspx, const std::vector &litdata, bool use_bspx, bool use_decoupled); +full_atlas_t build_lightmap_atlas(const mbsp_t &bsp, const bspxentries_t &bspx, const std::vector &litdata, + const std::vector &hdr_litdata, bool use_bspx, bool use_decoupled); void serialize_bsp(const bspdata_t &bspdata, const mbsp_t &bsp, const fs::path &name); diff --git a/include/common/bsputils.hh b/include/common/bsputils.hh index 173076ae..aeb9eafb 100644 --- a/include/common/bsputils.hh +++ b/include/common/bsputils.hh @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -174,8 +175,13 @@ public: qvec3f LMCoordToWorld(qvec2f lm) const; }; -qvec3b LM_Sample(const mbsp_t *bsp, const std::vector *lit, const faceextents_t &faceextents, +qvec3b LM_Sample(const mbsp_t *bsp, const mface_t *face, const lit_variant_t *lit, const faceextents_t &faceextents, int byte_offset_of_face, qvec2i coord); -std::vector LoadLitFile(const fs::path &path); + +qvec3f LM_Sample_HDR(const mbsp_t *bsp, + const mface_t *face, + const faceextents_t &faceextents, + int byte_offset_of_face, qvec2i coord, + const lit_variant_t *lit = nullptr, const bspxentries_t *bspx = nullptr); std::map> ClusterToLeafnumsMap(const mbsp_t *bsp); diff --git a/include/common/litfile.hh b/include/common/litfile.hh new file mode 100644 index 00000000..0188cb9c --- /dev/null +++ b/include/common/litfile.hh @@ -0,0 +1,75 @@ +/* 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. +*/ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +constexpr int32_t LIT_VERSION = 1; +constexpr int32_t LIT_VERSION_E5BGR9 = (0x00010000 | LIT_VERSION); + +struct litheader_t +{ + struct v1_t + { + std::array ident = {'Q', 'L', 'I', 'T'}; + int version; + + // serialize for streams + void stream_write(std::ostream &s) const; + void stream_read(std::istream &s); + }; + struct v2_t + { + int numsurfs; + int lmsamples; + + // serialize for streams + void stream_write(std::ostream &s) const; + void stream_read(std::istream &s); + }; + + v1_t v1; + v2_t v2; +}; + +uint32_t HDR_PackE5BRG9(qvec3f rgb); +qvec3f HDR_UnpackE5BRG9(uint32_t packed); + +struct lit1_t { + // 3 bytes (r,g,b) per sample + std::vector rgbdata; +}; + +struct lit_hdr { + // 1 packed e5bgr9 uint32_t per sample + std::vector samples; +}; + +struct lit_none {}; + +using lit_variant_t = std::variant; + +lit_variant_t LoadLitFile(const fs::path &path); diff --git a/include/light/light.hh b/include/light/light.hh index f2302a05..279301fb 100644 --- a/include/light/light.hh +++ b/include/light/light.hh @@ -171,7 +171,9 @@ enum class lightfile external = 1, bspx = 2, both = external | bspx, - lit2 = 4 + lit2 = 4, + hdr = 8, + bspxhdr = 16, }; /* tracelist is a std::vector of pointers to modelinfo_t to use for LOS tests */ @@ -393,6 +395,8 @@ public: setting_func bspxlux; setting_func bspxonly; setting_func bspx; + setting_func hdr; + setting_func bspxhdr; setting_scalar world_units_per_luxel; setting_bool litonly; setting_bool nolights; diff --git a/include/light/write.hh b/include/light/write.hh index 9d685bec..f10081aa 100644 --- a/include/light/write.hh +++ b/include/light/write.hh @@ -28,33 +28,6 @@ struct mbsp_t; struct bspdata_t; -constexpr int32_t LIT_VERSION = 1; - -struct litheader_t -{ - struct v1_t - { - std::array ident = {'Q', 'L', 'I', 'T'}; - int version; - - // serialize for streams - void stream_write(std::ostream &s) const; - void stream_read(std::istream &s); - }; - struct v2_t - { - int numsurfs; - int lmsamples; - - // serialize for streams - void stream_write(std::ostream &s) const; - void stream_read(std::istream &s); - }; - - v1_t v1; - v2_t v2; -}; - constexpr size_t MAXLIGHTMAPSSUP = 16; constexpr uint16_t INVALID_LIGHTSTYLE = 0xffffu; @@ -67,7 +40,9 @@ struct facesup_t twosided extent; }; -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 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, const std::vector &hdr_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/light.cc b/light/light.cc index 344500de..d0a0780f 100644 --- a/light/light.cc +++ b/light/light.cc @@ -335,6 +335,20 @@ light_settings::light_settings() return true; }, &experimental_group, "writes both rgb and directions data into the bsp itself"}, + hdr{this, "hdr", + [&](const std::string &, parser_base_t &, source) { + write_litfile |= lightfile::external; + write_litfile |= lightfile::hdr; + return true; + }, + &experimental_group, "write .lit file with e5bgr9 data"}, + bspxhdr{this, "bspxhdr", + [&](const std::string &, parser_base_t &, source) { + write_litfile |= lightfile::hdr; + write_litfile |= lightfile::bspxhdr; + return true; + }, + &experimental_group, "writes e5bgr9 data into the bsp itself"}, world_units_per_luxel{ this, "world_units_per_luxel", 0, 0, 1024, &output_group, "enables output of DECOUPLED_LM BSPX lump"}, litonly{this, "litonly", false, &output_group, "only write .lit file, don't modify BSP"}, diff --git a/light/ltface.cc b/light/ltface.cc index b7fc2189..815cb16b 100644 --- a/light/ltface.cc +++ b/light/ltface.cc @@ -2453,14 +2453,6 @@ static void LightPoint_ScaleAndClamp(qvec3f &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); - } } static void LightPoint_ScaleAndClamp(lightgrid_samples_t &result) diff --git a/light/write.cc b/light/write.cc index 6d5c83ed..ba541c1e 100644 --- a/light/write.cc +++ b/light/write.cc @@ -23,32 +23,10 @@ #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) +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, const std::vector &hdr_filebase) { litheader_t header; @@ -80,8 +58,13 @@ void WriteLitFile(const mbsp_t *bsp, const std::vector &facesup, cons } 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); + } else { + if (version == LIT_VERSION_E5BGR9) { + litfile.write((const char *)hdr_filebase.data(), bsp->dlightdata.size() * 4); + } 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) @@ -365,14 +348,16 @@ static std::vector BoxBlurImage(const std::vector &input, int w, return res; } +static constexpr float HDR_ONE = 128.0f; // logical value for 1.0 lighting (quake's overbrights give 255). + /** * - 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 lightmap_t *lm, const int actual_width, const int actual_height, + uint8_t *out, uint8_t *lit, uint8_t *lux, uint8_t *hdr, 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(); @@ -413,7 +398,24 @@ static void WriteSingleLightmap(const mbsp_t *bsp, const mface_t *face, const li const int sampleindex = (input_sample_t * actual_width) + input_sample_s; if (lit || out) { - const qvec4f &color = output_color.at(sampleindex); + qvec4f color = output_color.at(sampleindex); + + if (hdr) { + uint32_t c = HDR_PackE5BRG9(color / HDR_ONE); + // Write uint32 in little-endian + *hdr++ = c & 0xFF; + *hdr++ = (c >> 8) & 0xFF; + *hdr++ = (c >> 16) & 0xFF; + *hdr++ = (c >> 24) & 0xFF; + } + + // clamp + // FIXME: should this be a brightness clamp? + const float maxcolor = qv::max(color); + + if (maxcolor > 255.0f) { + color *= (255.0f / maxcolor); + } if (lit) { *lit++ = color[0]; @@ -465,7 +467,8 @@ static void WriteSingleLightmap(const mbsp_t *bsp, const mface_t *face, const li * - 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) + const lightmap_t *lm, const int output_width, const int output_height, + uint8_t *out, uint8_t *lit, uint8_t *lux, uint8_t *hdr) { // this is the lightmap data in the "decoupled" coordinate system std::vector fullres = LightmapColorsToGLMVector(lightsurf, lm); @@ -501,11 +504,28 @@ static void WriteSingleLightmap_FromDecoupled(const mbsp_t *bsp, const mface_t * const float coord_frac_y = decoupled_lm_coord[1] - coord_floor_y; // 2D bilinear interpolation - const qvec4f color = + 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 (hdr) { + uint32_t c = HDR_PackE5BRG9(color / HDR_ONE); + // Write uint32 in little-endian + *hdr++ = c & 0xFF; + *hdr++ = (c >> 8) & 0xFF; + *hdr++ = (c >> 16) & 0xFF; + *hdr++ = (c >> 24) & 0xFF; + } + + // clamp + // FIXME: should this be a brightness clamp? + const float maxcolor = qv::max(color); + + if (maxcolor > 255.0f) { + color *= (255.0f / maxcolor); + } + if (lit || out) { if (lit) { *lit++ = color[0]; @@ -567,14 +587,6 @@ inline void LightFace_ScaleAndClamp(lightsurf_t *lightsurf) 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); - } } } } @@ -607,9 +619,9 @@ static float Lightmap_MaxBrightness(const lightmap_t *lm, const lightsurf_t *lig 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) +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, std::vector &hdr_filebase) { lightmapdict_t &lightmaps = lightsurf->lightmapsByStyle; const int actual_width = extents.width(); @@ -625,7 +637,7 @@ static void SaveLitOnlyLightmapSurface(const mbsp_t *bsp, mface_t *face, return; } - uint8_t *out = nullptr, *lit = nullptr, *lux = nullptr; + uint8_t *out = nullptr, *lit = nullptr, *lux = nullptr, *hdr = nullptr; Q_assert(face->lightofs >= 0); @@ -641,6 +653,10 @@ static void SaveLitOnlyLightmapSurface(const mbsp_t *bsp, mface_t *face, lux = lux_filebase.data() + (face->lightofs * 3); } + if (!hdr_filebase.empty()) { + hdr = hdr_filebase.data() + (face->lightofs * 4); + } + // NOTE: file_p et. al. are not updated, since we're not dynamically allocating the lightmaps for (int mapnum = 0; mapnum < MAXLIGHTMAPS; mapnum++) { @@ -654,7 +670,7 @@ static void SaveLitOnlyLightmapSurface(const mbsp_t *bsp, mface_t *face, for (const lightmap_t &lm : lightmaps) { if (lm.style == style) { WriteSingleLightmap( - bsp, face, lightsurf, &lm, actual_width, actual_height, out, lit, lux, output_extents); + bsp, face, lightsurf, &lm, actual_width, actual_height, out, lit, lux, hdr, output_extents); break; } } @@ -669,6 +685,9 @@ static void SaveLitOnlyLightmapSurface(const mbsp_t *bsp, mface_t *face, if (lux) { lux += (size * 3); } + if (hdr) { + hdr += (size * 4); + } } } @@ -718,9 +737,10 @@ int CalculateLightmapStyles(const mbsp_t *bsp, mface_t *face, facesup_t *facesup 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) + // skip lightmaps where all samples have brightness below 1 unless rendering float lightmaps + // HACK: don't do this on Q2. seems if all styles are 0xff, + // the face is drawn fullbright instead of black (Q1) + if (bsp->loadversion->game->id != GAME_QUAKE_II && !(light_options.write_litfile & lightfile::hdr)) { const float maxb = Lightmap_MaxBrightness(&lightmap, lightsurf); if (maxb < 1) continue; @@ -787,9 +807,8 @@ int CalculateLightmapStyles(const mbsp_t *bsp, mface_t *face, facesup_t *facesup 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 faceextents_t &output_extents, std::vector &filebase, std::vector &lit_filebase, + std::vector &lux_filebase, std::vector &hdr_filebase, lightmap_intermediate_data_t &id) { const int output_width = output_extents.width(); const int output_height = output_extents.height(); @@ -830,7 +849,7 @@ void SaveLightmapSurface(const mbsp_t *bsp, mface_t *face, facesup_t *facesup, } } - uint8_t *out = nullptr, *lit = nullptr, *lux = nullptr; + uint8_t *out = nullptr, *lit = nullptr, *lux = nullptr, *hdr = nullptr; if (!filebase.empty()) { out = filebase.data() + id.lightofs; @@ -844,6 +863,10 @@ void SaveLightmapSurface(const mbsp_t *bsp, mface_t *face, facesup_t *facesup, lux = lux_filebase.data() + (id.lightofs * 3); } + if (!hdr_filebase.empty()) { + hdr = hdr_filebase.data() + (id.lightofs * 4); + } + int lightofs; // Q2/HL native colored lightmaps @@ -881,10 +904,14 @@ void SaveLightmapSurface(const mbsp_t *bsp, mface_t *face, facesup_t *facesup, Q_assert((lux - lux_filebase.data()) + (size * 3 * id.sorted.size()) <= lux_filebase.size()); } + if (hdr) { + Q_assert((hdr - hdr_filebase.data()) + (size * 4 * id.sorted.size()) <= hdr_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); + WriteSingleLightmap(bsp, face, lightsurf, lm, actual_width, actual_height, out, lit, lux, hdr, output_extents); if (out) { out += size; @@ -895,6 +922,9 @@ void SaveLightmapSurface(const mbsp_t *bsp, mface_t *face, facesup_t *facesup, if (lux) { lux += (size * 3); } + if (hdr) { + hdr += (size * 4); + } } // write vanilla lightmap if -world_units_per_luxel is in use but not -novanilla @@ -915,6 +945,10 @@ void SaveLightmapSurface(const mbsp_t *bsp, mface_t *face, facesup_t *facesup, lux = lux_filebase.data() + (id.vanilla_lightofs * 3); } + if (!hdr_filebase.empty()) { + hdr = hdr_filebase.data() + (id.vanilla_lightofs * 4); + } + // Q2/HL native colored lightmaps if (bsp->loadversion->game->has_rgb_lightmap) { lightofs = lit - lit_filebase.data(); @@ -927,7 +961,7 @@ void SaveLightmapSurface(const mbsp_t *bsp, mface_t *face, facesup_t *facesup, 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); + lightsurf->vanilla_extents.height(), out, lit, lux, hdr); if (out) { out += vanilla_size; @@ -938,6 +972,9 @@ void SaveLightmapSurface(const mbsp_t *bsp, mface_t *face, facesup_t *facesup, if (lux) { lux += (vanilla_size * 3); } + if (hdr) { + hdr += (vanilla_size * 4); + } } } } @@ -952,7 +989,7 @@ void SaveLightmapSurfaces(bspdata_t *bspdata, const fs::path &source) fully_transparent_lightmaps = 0; // lightmap data storage - std::vector filebase, lit_filebase, lux_filebase; + std::vector filebase, lit_filebase, lux_filebase, hdr_filebase; if (light_options.litonly.value()) { @@ -971,7 +1008,11 @@ void SaveLightmapSurfaces(bspdata_t *bspdata, const fs::path &source) if (light_options.write_luxfile) { lux_filebase.resize(filebase.size() * 3); } - + + if (light_options.write_litfile & lightfile::hdr) { + hdr_filebase.resize(filebase.size() * 4); + } + logging::parallel_for(static_cast(0), bsp->dfaces.size(), [&](size_t i) { auto &surf = LightSurfaces()[i]; @@ -983,7 +1024,8 @@ void SaveLightmapSurfaces(bspdata_t *bspdata, const fs::path &source) auto f = &bsp->dfaces[i]; - SaveLitOnlyLightmapSurface(bsp, f, &surf, surf.extents, surf.extents, filebase, lit_filebase, lux_filebase); + SaveLitOnlyLightmapSurface( + bsp, f, &surf, surf.extents, surf.extents, filebase, lit_filebase, lux_filebase, hdr_filebase); }); } else { std::atomic_size_t lightmap_size = 0; @@ -1039,7 +1081,12 @@ void SaveLightmapSurfaces(bspdata_t *bspdata, const fs::path &source) lux_filebase.resize(lightmap_size * 3); } - logging::print(logging::flag::STAT, "lightmap size (total): {}\n", filebase.size() + lit_filebase.size() + lux_filebase.size()); + if (light_options.write_litfile & lightfile::hdr) { + hdr_filebase.resize(lightmap_size * 4); + } + + logging::print(logging::flag::STAT, "lightmap size (total): {}\n", + filebase.size() + lit_filebase.size() + lux_filebase.size() + hdr_filebase.size()); logging::parallel_for(static_cast(0), bsp->dfaces.size(), [&](size_t i) { auto &surf = LightSurfaces()[i]; @@ -1052,24 +1099,28 @@ void SaveLightmapSurfaces(bspdata_t *bspdata, const fs::path &source) 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]); + SaveLightmapSurface(bsp, f, nullptr, &facesup_decoupled_global[i], &surf, surf.extents, surf.extents, + filebase, lit_filebase, lux_filebase, hdr_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]); + SaveLightmapSurface(bsp, f, nullptr, nullptr, &surf, surf.extents, surf.extents, filebase, lit_filebase, + lux_filebase, hdr_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]); + SaveLightmapSurface(bsp, f, &faces_sup[i], nullptr, &surf, surf.extents, surf.extents, filebase, + lit_filebase, lux_filebase, hdr_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]); + SaveLightmapSurface(bsp, f, nullptr, nullptr, &surf, surf.extents, surf.vanilla_extents, filebase, + lit_filebase, lux_filebase, hdr_filebase, intermediate_data[i]); + SaveLightmapSurface(bsp, f, &faces_sup[i], nullptr, &surf, surf.extents, surf.extents, filebase, + lit_filebase, lux_filebase, hdr_filebase, intermediate_data[i]); } }); } @@ -1077,7 +1128,7 @@ void SaveLightmapSurfaces(bspdata_t *bspdata, const fs::path &source) logging::print("Lighting Completed.\n\n"); if (light_options.write_litfile == lightfile::lit2) { - WriteLitFile(bsp, faces_sup, source, 2, lit_filebase, lux_filebase); + WriteLitFile(bsp, faces_sup, source, 2, lit_filebase, lux_filebase, hdr_filebase); return; // run away before any files are written } @@ -1093,12 +1144,14 @@ void SaveLightmapSurfaces(bspdata_t *bspdata, const fs::path &source) bspdata->bspx.entries.erase("RGBLIGHTING"); bspdata->bspx.entries.erase("LIGHTINGDIR"); + bspdata->bspx.entries.erase("LIGHTING_E5BGR9"); // lit/lux files (or their BSPX equivalents) - only write in games that lack RGB lightmaps. // (technically we could allow .lux in Q2 mode, but no engines support it.) if (!bsp->loadversion->game->has_rgb_lightmap) { if (light_options.write_litfile & lightfile::external) { - WriteLitFile(bsp, faces_sup, source, LIT_VERSION, lit_filebase, lux_filebase); + int version = light_options.write_litfile & lightfile::hdr ? LIT_VERSION_E5BGR9 : LIT_VERSION; + WriteLitFile(bsp, faces_sup, source, version, lit_filebase, lux_filebase, hdr_filebase); } if (light_options.write_litfile & lightfile::bspx) { lit_filebase.resize(bsp->dlightdata.size() * 3); @@ -1111,5 +1164,9 @@ void SaveLightmapSurfaces(bspdata_t *bspdata, const fs::path &source) lux_filebase.resize(bsp->dlightdata.size() * 3); bspdata->bspx.transfer("LIGHTINGDIR", lux_filebase); } + if (light_options.write_litfile & lightfile::bspxhdr) { + hdr_filebase.resize(bsp->dlightdata.size() * 4); + bspdata->bspx.transfer("LIGHTING_E5BGR9", hdr_filebase); + } } } diff --git a/lightpreview/glview.cpp b/lightpreview/glview.cpp index 80f71135..d3eb150b 100644 --- a/lightpreview/glview.cpp +++ b/lightpreview/glview.cpp @@ -34,6 +34,7 @@ See file, 'COPYING', for details. #include #include #include +#include #include #include @@ -192,6 +193,8 @@ uniform bool fullbright; uniform bool drawnormals; uniform bool drawflat; uniform float style_scalars[256]; +uniform float brightness; +uniform float lightmap_scale; // extra scale factor for remapping 0-1 SDR lightmaps to 0-2 void main() { if (drawnormals) { @@ -221,8 +224,11 @@ void main() { } } - // 2.0 for overbright - color = vec4(texcolor * lmcolor * 2.0, opacity); + // if we're using SDR lightmaps (0-255 components mapped to 0..1 by OpenGL, + // lightmap_scale == 2.0 to remap 0..1 to 0..2). + // + // HDR lightmaps are used as-is with lightmap_scale == 1. + color = vec4(texcolor * lmcolor * lightmap_scale, opacity) * pow(2.0, brightness); } } )"; @@ -289,6 +295,8 @@ uniform bool fullbright; uniform bool drawnormals; uniform bool drawflat; uniform float style_scalars[256]; +uniform float brightness; +uniform float lightmap_scale; // extra scale factor for remapping 0-1 SDR lightmaps to 0-2 uniform vec3 eye_origin; @@ -313,8 +321,8 @@ void main() { lmcolor += texture(lightmap_sampler, vec3(lightmap_uv, float(style))).rgb * style_scalars[style]; } - // 2.0 for overbright - color = vec4(lmcolor * 2.0, 1.0); + // see lightmap_scale documentation above + color = vec4(lmcolor * lightmap_scale, 1.0); } else { @@ -322,6 +330,7 @@ void main() { vec3 dir = normalize(fragment_world_pos - eye_origin); color = vec4(texture(texture_sampler, dir).rgb, 1.0); } + color = color * pow(2.0, brightness); } } )"; @@ -507,6 +516,28 @@ void GLView::handleLoggedMessage(const QOpenGLDebugMessage &debugMessage) #endif } +void GLView::error(const QString &context, const QString &context2, const QString &log) +{ + QMessageBox errorMessage( + QMessageBox::Critical, + tr("GLSL Error"), tr("%1: %2:\n\n%3").arg(context).arg(context2).arg(log), QMessageBox::Ok, this); + + errorMessage.exec(); +} + +void GLView::setupProgram(const QString &context, QOpenGLShaderProgram *dest, const char *vert, const char *frag) +{ + if (!dest->addShaderFromSourceCode(QOpenGLShader::Vertex, vert)) { + error(context, "vertex shader",dest->log()); + } + if (!dest->addShaderFromSourceCode(QOpenGLShader::Fragment, frag)) { + error(context, "fragment shader",dest->log()); + } + if (!dest->link()) { + error(context, "link",dest->log()); + } +} + void GLView::initializeGL() { initializeOpenGLFunctions(); @@ -521,24 +552,16 @@ void GLView::initializeGL() // set up shader m_program = new QOpenGLShaderProgram(); - m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, s_vertShader); - m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, s_fragShader); - assert(m_program->link()); + setupProgram("m_program", m_program, s_vertShader, s_fragShader); m_skybox_program = new QOpenGLShaderProgram(); - m_skybox_program->addShaderFromSourceCode(QOpenGLShader::Vertex, s_skyboxVertShader); - m_skybox_program->addShaderFromSourceCode(QOpenGLShader::Fragment, s_skyboxFragShader); - assert(m_skybox_program->link()); + setupProgram("m_skybox_program", m_skybox_program, s_skyboxVertShader,s_skyboxFragShader); m_program_simple = new QOpenGLShaderProgram(); - m_program_simple->addShaderFromSourceCode(QOpenGLShader::Vertex, s_vertShader_Simple); - m_program_simple->addShaderFromSourceCode(QOpenGLShader::Fragment, s_fragShader_Simple); - assert(m_program_simple->link()); + setupProgram("m_program_simple", m_program_simple, s_vertShader_Simple, s_fragShader_Simple); m_program_wireframe = new QOpenGLShaderProgram(); - m_program_wireframe->addShaderFromSourceCode(QOpenGLShader::Vertex, s_vertShader_Wireframe); - m_program_wireframe->addShaderFromSourceCode(QOpenGLShader::Fragment, s_fragShader_Wireframe); - assert(m_program_wireframe->link()); + setupProgram("m_program_wireframe", m_program_wireframe, s_vertShader_Wireframe, s_fragShader_Wireframe); m_program->bind(); m_program_mvp_location = m_program->uniformLocation("MVP"); @@ -552,6 +575,8 @@ void GLView::initializeGL() m_program_drawnormals_location = m_program->uniformLocation("drawnormals"); m_program_drawflat_location = m_program->uniformLocation("drawflat"); m_program_style_scalars_location = m_program->uniformLocation("style_scalars"); + m_program_brightness_location = m_program->uniformLocation("brightness"); + m_program_lightmap_scale_location = m_program->uniformLocation("lightmap_scale"); m_program->release(); m_skybox_program->bind(); @@ -566,6 +591,8 @@ void GLView::initializeGL() m_skybox_program_drawnormals_location = m_skybox_program->uniformLocation("drawnormals"); m_skybox_program_drawflat_location = m_skybox_program->uniformLocation("drawflat"); m_skybox_program_style_scalars_location = m_skybox_program->uniformLocation("style_scalars"); + m_skybox_program_brightness_location = m_skybox_program->uniformLocation("brightness"); + m_skybox_program_lightmap_scale_location = m_skybox_program->uniformLocation("lightmap_scale"); m_skybox_program->release(); m_program_wireframe->bind(); @@ -654,6 +681,8 @@ void GLView::paintGL() m_program->setUniformValue(m_program_fullbright_location, m_fullbright); m_program->setUniformValue(m_program_drawnormals_location, m_drawNormals); m_program->setUniformValue(m_program_drawflat_location, m_drawFlat); + m_program->setUniformValue(m_program_brightness_location, m_brightness); + m_program->setUniformValue(m_program_lightmap_scale_location, m_is_hdr_lightmap ? 1.0f : 2.0f); m_skybox_program->bind(); m_skybox_program->setUniformValue(m_skybox_program_mvp_location, MVP); @@ -666,6 +695,8 @@ void GLView::paintGL() m_skybox_program->setUniformValue(m_skybox_program_fullbright_location, m_fullbright); m_skybox_program->setUniformValue(m_skybox_program_drawnormals_location, m_drawNormals); m_skybox_program->setUniformValue(m_skybox_program_drawflat_location, m_drawFlat); + m_skybox_program->setUniformValue(m_skybox_program_brightness_location, m_brightness); + m_skybox_program->setUniformValue(m_skybox_program_lightmap_scale_location, m_is_hdr_lightmap ? 1.0f : 2.0f); // resolves whether to render a particular drawcall as opaque auto draw_as_opaque = [&](const drawcall_t &draw) -> bool { @@ -1033,6 +1064,12 @@ void GLView::setShowBmodels(bool bmodels) update(); } +void GLView::setBrightness(float brightness) +{ + m_brightness = brightness; + update(); +} + void GLView::takeScreenshot(QString destPath, int w, int h) { // update aspect ratio @@ -1183,18 +1220,34 @@ void GLView::renderBSP(const QString &file, const mbsp_t &bsp, const bspxentries // FIXME: empty map access if there are no lightmaps const auto &lm_tex = lightmap.style_to_lightmap_atlas.begin()->second; + m_is_hdr_lightmap = false; + for (auto &[style_index, style_atlas] : lightmap.style_to_lightmap_atlas) { + if (!style_atlas.e5brg9_samples.empty()) { + m_is_hdr_lightmap = true; + break; + } + } + lightmap_texture = std::make_shared(QOpenGLTexture::Target2DArray); lightmap_texture->setSize(lm_tex.width, lm_tex.height); lightmap_texture->setLayers(highest_depth + 1); - lightmap_texture->setFormat(QOpenGLTexture::TextureFormat::RGBA8_UNorm); + if (m_is_hdr_lightmap) + lightmap_texture->setFormat(QOpenGLTexture::TextureFormat::RGB9E5); + else + lightmap_texture->setFormat(QOpenGLTexture::TextureFormat::RGBA8_UNorm); lightmap_texture->setAutoMipMapGenerationEnabled(false); lightmap_texture->setMagnificationFilter(QOpenGLTexture::Linear); lightmap_texture->setMinificationFilter(QOpenGLTexture::Linear); lightmap_texture->allocateStorage(); - for (auto &style : lightmap.style_to_lightmap_atlas) { - lightmap_texture->setData(0, style.first, QOpenGLTexture::RGBA, QOpenGLTexture::UInt8, - reinterpret_cast(style.second.pixels.data())); + for (auto &[style_index, style_atlas] : lightmap.style_to_lightmap_atlas) { + if (m_is_hdr_lightmap) { + lightmap_texture->setData(0, style_index, QOpenGLTexture::RGB, QOpenGLTexture::UInt32_RGB9_E5, + reinterpret_cast(style_atlas.e5brg9_samples.data())); + } else { + lightmap_texture->setData(0, style_index, QOpenGLTexture::RGBA, QOpenGLTexture::UInt8, + reinterpret_cast(style_atlas.rgba8_samples.data())); + } } } diff --git a/lightpreview/glview.h b/lightpreview/glview.h index 17cb3b48..6c0003d6 100644 --- a/lightpreview/glview.h +++ b/lightpreview/glview.h @@ -117,6 +117,7 @@ class GLView : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core QOpenGLTexture::Filter m_filter = QOpenGLTexture::Linear; bool m_drawTranslucencyAsOpaque = false; bool m_showBmodels = true; + float m_brightness = 0.0f; QOpenGLVertexArrayObject m_vao; QOpenGLBuffer m_vbo; @@ -157,6 +158,7 @@ class GLView : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core std::shared_ptr placeholder_texture; std::shared_ptr lightmap_texture; + bool m_is_hdr_lightmap = false; /** * 1D texture, one uint8 per face. * @@ -193,6 +195,8 @@ class GLView : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core int m_program_drawnormals_location = 0; int m_program_drawflat_location = 0; int m_program_style_scalars_location = 0; + int m_program_brightness_location = 0; + int m_program_lightmap_scale_location = 0; // uniform locations (skybox program) int m_skybox_program_mvp_location = 0; @@ -206,6 +210,8 @@ class GLView : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core int m_skybox_program_drawnormals_location = 0; int m_skybox_program_drawflat_location = 0; int m_skybox_program_style_scalars_location = 0; + int m_skybox_program_brightness_location = 0; + int m_skybox_program_lightmap_scale_location = 0; // uniform locations (wireframe program) int m_program_wireframe_mvp_location = 0; @@ -250,9 +256,13 @@ class GLView : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core const bool &getKeepOrigin() const { return m_keepOrigin; } void setDrawTranslucencyAsOpaque(bool drawopaque); void setShowBmodels(bool bmodels); + void setBrightness(float brightness); void takeScreenshot(QString destPath, int w, int h); +private: + void error(const QString &context, const QString &context2, const QString &log); + void setupProgram(const QString &context, QOpenGLShaderProgram *dest, const char *vert, const char *frag); protected: void initializeGL() override; void paintGL() override; diff --git a/lightpreview/mainwindow.cpp b/lightpreview/mainwindow.cpp index 40abec54..caf36ab3 100644 --- a/lightpreview/mainwindow.cpp +++ b/lightpreview/mainwindow.cpp @@ -32,6 +32,7 @@ See file, 'COPYING', for details. #include #include #include +#include #include #include #include @@ -52,6 +53,7 @@ See file, 'COPYING', for details. #include #include +#include #include #include #include @@ -327,6 +329,20 @@ void MainWindow::createPropertiesSidebar() formLayout->addRow(lightstyles_group); + // brightness slider + auto *brightnessSlider = new QSlider(Qt::Horizontal); + brightnessSlider->setMinimum(-100); + brightnessSlider->setMaximum(100); + brightnessSlider->setSliderPosition(0); + + auto *brightnessLabel = new QLabel(QString("0.0")); + auto *brightnessLayout = new QHBoxLayout(); + auto *brightnessReset = new QPushButton(tr("Reset")); + brightnessLayout->addWidget(brightnessSlider, 1); + brightnessLayout->addWidget(brightnessLabel, 0); + brightnessLayout->addWidget(brightnessReset, 0); + formLayout->addRow(tr("Exposure"), brightnessLayout); + // wrap formLayout in a scroll area auto *form = new QWidget(); form->setLayout(formLayout); @@ -396,6 +412,14 @@ void MainWindow::createPropertiesSidebar() connect(glView, &GLView::cameraMoved, this, &MainWindow::displayCameraPositionInfo); connect(show_bmodels, &QAbstractButton::toggled, this, [this](bool checked) { glView->setShowBmodels(checked); }); + connect(brightnessSlider, &QAbstractSlider::valueChanged, this, [this,brightnessLabel](int value){ + float brightness = value / 10.0f; + brightnessLabel->setText(QString::fromLatin1("%1").arg(brightness, 0, 'f', 2)); + glView->setBrightness(brightness); + }); + connect(brightnessReset, &QAbstractButton::pressed, this, [this, brightnessSlider](){ + brightnessSlider->setValue(0); + }); // set up load timer m_fileReloadTimer = std::make_unique(); @@ -931,11 +955,21 @@ int MainWindow::compileMap(const QString &file, bool is_reload) auto lit_path = fs_path; lit_path.replace_extension(".lit"); + m_hdr_litdata = {}; + m_litdata = {}; + try { - m_litdata = LoadLitFile(lit_path); + auto lit_variant = LoadLitFile(lit_path); + + if (auto* lit1_ptr = std::get_if(&lit_variant)) { + m_litdata = std::move(lit1_ptr->rgbdata); + } else if (auto* lit_hdr_ptr = std::get_if(&lit_variant)) { + m_hdr_litdata = std::move(lit_hdr_ptr->samples); + } } catch (const std::runtime_error &error) { logging::print("error loading lit: {}", error.what()); m_litdata = {}; + m_hdr_litdata = {}; } return 0; @@ -959,7 +993,7 @@ void MainWindow::compileThreadExited() auto ents = EntData_Parse(bsp); // build lightmap atlas - auto atlas = build_lightmap_atlas(bsp, m_bspdata.bspx.entries, m_litdata, false, bspx_decoupled_lm->isChecked()); + auto atlas = build_lightmap_atlas(bsp, m_bspdata.bspx.entries, m_litdata, m_hdr_litdata, false, bspx_decoupled_lm->isChecked()); glView->renderBSP(m_mapFile, bsp, m_bspdata.bspx.entries, ents, atlas, render_settings, bspx_normals->isChecked()); diff --git a/lightpreview/mainwindow.h b/lightpreview/mainwindow.h index 112cb806..05335857 100644 --- a/lightpreview/mainwindow.h +++ b/lightpreview/mainwindow.h @@ -74,6 +74,7 @@ class MainWindow : public QMainWindow QString m_mapFile; bspdata_t m_bspdata; std::vector m_litdata; + std::vector m_hdr_litdata; settings::common_settings render_settings; qint64 m_fileSize = -1; ETLogTab m_activeLogTab = ETLogTab::TAB_LIGHTPREVIEW; diff --git a/testmaps/q1_hdrtest.map b/testmaps/q1_hdrtest.map new file mode 100644 index 00000000..f919693c --- /dev/null +++ b/testmaps/q1_hdrtest.map @@ -0,0 +1,85 @@ +// Game: Quake +// Format: Valve +// entity 0 +{ +"classname" "worldspawn" +"wad" "deprecated/free_wad.wad" +"_tb_def" "builtin:Quake.fgd" +// brush 0 +{ +( -416 -128 32 ) ( -416 -127 32 ) ( -416 -128 33 ) bolt3 [ 0 1 0 32 ] [ 0 0 -1 0 ] 0 1 1 +( -416 -208 32 ) ( -416 -208 33 ) ( -415 -208 32 ) bolt3 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( -416 -128 32 ) ( -415 -128 32 ) ( -416 -127 32 ) bolt3 [ 1 0 0 0 ] [ 0 -1 0 -32 ] 0 1 1 +( 128 272 48 ) ( 128 273 48 ) ( 129 272 48 ) bolt3 [ 1 0 0 0 ] [ 0 -1 0 -32 ] 0 1 1 +( 128 272 48 ) ( 129 272 48 ) ( 128 272 49 ) bolt3 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 128 272 48 ) ( 128 272 49 ) ( 128 273 48 ) bolt3 [ 0 1 0 32 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 1 +{ +( -416 -128 288 ) ( -416 -127 288 ) ( -416 -128 289 ) bolt3 [ 0 1 0 32 ] [ 0 0 -1 0 ] 0 1 1 +( -416 -208 288 ) ( -416 -208 289 ) ( -415 -208 288 ) bolt3 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( -416 -128 288 ) ( -415 -128 288 ) ( -416 -127 288 ) bolt3 [ 1 0 0 0 ] [ 0 -1 0 -32 ] 0 1 1 +( 128 272 304 ) ( 128 273 304 ) ( 129 272 304 ) bolt3 [ 1 0 0 0 ] [ 0 -1 0 -32 ] 0 1 1 +( 128 272 304 ) ( 129 272 304 ) ( 128 272 305 ) bolt3 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 128 272 304 ) ( 128 272 305 ) ( 128 273 304 ) bolt3 [ 0 1 0 32 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 2 +{ +( -432 -128 272 ) ( -432 -127 272 ) ( -432 -128 273 ) bolt3 [ 0 1 0 32 ] [ 0 0 -1 0 ] 0 1 1 +( -432 -208 272 ) ( -432 -208 273 ) ( -431 -208 272 ) bolt3 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( -432 -128 48 ) ( -431 -128 48 ) ( -432 -127 48 ) bolt3 [ 1 0 0 0 ] [ 0 -1 0 -32 ] 0 1 1 +( 112 272 288 ) ( 112 273 288 ) ( 113 272 288 ) bolt3 [ 1 0 0 0 ] [ 0 -1 0 -32 ] 0 1 1 +( 112 272 288 ) ( 113 272 288 ) ( 112 272 289 ) bolt3 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( -416 272 288 ) ( -416 272 289 ) ( -416 273 288 ) bolt3 [ 0 1 0 32 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 3 +{ +( 128 -128 272 ) ( 128 -127 272 ) ( 128 -128 273 ) bolt3 [ 0 1 0 32 ] [ 0 0 -1 0 ] 0 1 1 +( 128 -208 272 ) ( 128 -208 273 ) ( 129 -208 272 ) bolt3 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 128 -128 48 ) ( 129 -128 48 ) ( 128 -127 48 ) bolt3 [ 1 0 0 0 ] [ 0 -1 0 -32 ] 0 1 1 +( 672 272 288 ) ( 672 273 288 ) ( 673 272 288 ) bolt3 [ 1 0 0 0 ] [ 0 -1 0 -32 ] 0 1 1 +( 672 272 288 ) ( 673 272 288 ) ( 672 272 289 ) bolt3 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 144 272 288 ) ( 144 272 289 ) ( 144 273 288 ) bolt3 [ 0 1 0 32 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 4 +{ +( -416 -144 272 ) ( -416 -143 272 ) ( -416 -144 273 ) bolt3 [ 0 1 0 32 ] [ 0 0 -1 0 ] 0 1 1 +( 96 -224 272 ) ( 96 -224 273 ) ( 97 -224 272 ) bolt3 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 96 -144 48 ) ( 97 -144 48 ) ( 96 -143 48 ) bolt3 [ 1 0 0 0 ] [ 0 -1 0 -32 ] 0 1 1 +( 640 256 288 ) ( 640 257 288 ) ( 641 256 288 ) bolt3 [ 1 0 0 0 ] [ 0 -1 0 -32 ] 0 1 1 +( 640 -208 288 ) ( 641 -208 288 ) ( 640 -208 289 ) bolt3 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 128 256 288 ) ( 128 256 289 ) ( 128 257 288 ) bolt3 [ 0 1 0 32 ] [ 0 0 -1 0 ] 0 1 1 +} +// brush 5 +{ +( -416 256 48 ) ( -416 257 48 ) ( -416 256 49 ) bolt3 [ 0 1 0 32 ] [ 0 0 -1 0 ] 0 1 1 +( -432 256 48 ) ( -432 256 49 ) ( -431 256 48 ) bolt3 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( -432 256 48 ) ( -431 256 48 ) ( -432 257 48 ) bolt3 [ 1 0 0 0 ] [ 0 -1 0 -32 ] 0 1 1 +( 128 272 288 ) ( 128 273 288 ) ( 129 272 288 ) bolt3 [ 1 0 0 0 ] [ 0 -1 0 -32 ] 0 1 1 +( 128 272 64 ) ( 129 272 64 ) ( 128 272 65 ) bolt3 [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1 +( 128 272 64 ) ( 128 272 65 ) ( 128 273 64 ) bolt3 [ 0 1 0 32 ] [ 0 0 -1 0 ] 0 1 1 +} +} +// entity 1 +{ +"classname" "info_player_start" +"origin" "-292 0 72" +"angle" "90" +} +// entity 2 +{ +"classname" "light" +"origin" "-360 200 56" +"light" "15" +"delay" "2" +"_color" "0.75 0.65 0.44" +} +// entity 3 +{ +"classname" "light" +"origin" "88 -176 260" +"light" "15" +"delay" "2" +"_color" "0 1 0.7" +"style" "5" +} diff --git a/tests/test_light.cc b/tests/test_light.cc index 0f00c30b..ab076c9c 100644 --- a/tests/test_light.cc +++ b/tests/test_light.cc @@ -10,6 +10,7 @@ #include #include +#include TEST_SUITE("mathlib") { @@ -1022,3 +1023,49 @@ TEST_SUITE("settings") CHECK(LF_INVERSE2 == light.formula.value()); } } + + + +TEST_SUITE("light formats") +{ + TEST_CASE("e5bgr9 pack (511, 1, 0)") + { + uint32_t packed = HDR_PackE5BRG9(qvec3f{511.0f, 1.0f, 0.0f}); + + // e | b | g | r + uint32_t expected = (24 << 27) | (0 << 18) | (1 << 9) | (511 << 0); + CHECK(packed == expected); + + qvec3f roundtripped = HDR_UnpackE5BRG9(expected); + CHECK(roundtripped[0] == 511); + CHECK(roundtripped[1] == 1); + CHECK(roundtripped[2] == 0); + } + + TEST_CASE("e5bgr9 pack (1'000'000, 0, 0)") + { + uint32_t packed = HDR_PackE5BRG9(qvec3f{1'000'000.0f, 0.0f, 0.0f}); + + // e | b | g | r + uint32_t expected = (0x1f << 27) | (0 << 18) | (0 << 9) | (0x1ff << 0); + CHECK(packed == expected); + + qvec3f roundtripped = HDR_UnpackE5BRG9(packed); + CHECK(roundtripped[0] == 65408.0f); + CHECK(roundtripped[1] == 0.0f); + CHECK(roundtripped[2] == 0.0f); + } + + TEST_CASE("e5bgr9 pack (0.1, 0.01, 0.001)") + { + qvec3f in = qvec3f{0.1, 0.01, 0.001}; + uint32_t packed = HDR_PackE5BRG9(in); + + qvec3f roundtripped = HDR_UnpackE5BRG9(packed); + qvec3f error = qv::abs(in - roundtripped); + + CHECK(error[0] < 0.000098); + CHECK(error[1] < 0.00001); + CHECK(error[2] < 0.000025); + } +} diff --git a/tests/test_ltface.cc b/tests/test_ltface.cc index 4bac7920..ff11c219 100644 --- a/tests/test_ltface.cc +++ b/tests/test_ltface.cc @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -117,9 +118,11 @@ testresults_lit_t QbspVisLight_Q1( auto lit_path = fs::path(test_quake_maps_dir) / name.filename(); lit_path.replace_extension(".lit"); - std::vector litdata = LoadLitFile(lit_path); + auto lit_variant = LoadLitFile(lit_path); - return testresults_lit_t{.bsp = res.bsp, .bspx = res.bspx, .lit = litdata}; + return testresults_lit_t{.bsp = std::move(res.bsp), + .bspx = std::move(res.bspx), + .lit = std::move(lit_variant)}; } testresults_t QbspVisLight_Q2( @@ -249,7 +252,7 @@ TEST_CASE("emissive cube artifacts") auto lm_coord = extents.worldToLMCoord(pos); - auto sample = LM_Sample(&bsp, nullptr, extents, lm_info.offset, lm_coord); + auto sample = LM_Sample(&bsp, floor, nullptr, extents, lm_info.offset, lm_coord); CHECK(sample[0] >= previous_sample[0]); // logging::print("world: {} lm_coord: {} sample: {} lm size: {}x{}\n", pos, lm_coord, sample, lm_info.lmwidth, @@ -295,7 +298,7 @@ TEST_CASE("-novanilla + -world_units_per_luxel") template static void CheckFaceLuxels( - const mbsp_t &bsp, const mface_t &face, L &&lambda, const std::vector *lit = nullptr) + const mbsp_t &bsp, const mface_t &face, L &&lambda, const lit_variant_t *lit = nullptr) { // FIXME: assumes no DECOUPLED_LM lump @@ -303,7 +306,7 @@ static void CheckFaceLuxels( for (int x = 0; x < extents.width(); ++x) { for (int y = 0; y < extents.height(); ++y) { - const qvec3b sample = LM_Sample(&bsp, lit, extents, face.lightofs, {x, y}); + const qvec3b sample = LM_Sample(&bsp, &face, lit, extents, face.lightofs, {x, y}); INFO("sample ", x, ", ", y); lambda(sample); } @@ -316,7 +319,7 @@ static void CheckFaceLuxelsNonBlack(const mbsp_t &bsp, const mface_t &face) } static void CheckFaceLuxelAtPoint(const mbsp_t *bsp, const dmodelh2_t *model, const qvec3b &expected_color, - const qvec3d &point, const qvec3d &normal = {0, 0, 0}, const std::vector *lit = nullptr, + const qvec3d &point, const qvec3d &normal = {0, 0, 0}, const lit_variant_t *lit = nullptr, const bspxentries_t *bspx = nullptr) { auto *face = BSP_FindFaceAtPoint(bsp, model, point, normal); @@ -338,7 +341,7 @@ static void CheckFaceLuxelAtPoint(const mbsp_t *bsp, const dmodelh2_t *model, co const auto coord = extents.worldToLMCoord(point); const auto int_coord = qvec2i(round(coord[0]), round(coord[1])); - const qvec3b sample = LM_Sample(bsp, lit, extents, offset, int_coord); + const qvec3b sample = LM_Sample(bsp, face, lit, extents, offset, int_coord); INFO("world point: ", point); INFO("lm coord: ", coord[0], ", ", coord[1]); INFO("lm int_coord: ", int_coord[0], ", ", int_coord[1]); @@ -351,6 +354,43 @@ static void CheckFaceLuxelAtPoint(const mbsp_t *bsp, const dmodelh2_t *model, co CHECK(delta[2] <= 1); } +static void CheckFaceLuxelAtPoint_HDR(const mbsp_t *bsp, const dmodelh2_t *model, const qvec3f &expected_color, + const qvec3f &allowed_delta, + const qvec3d &point, const qvec3d &normal = {0, 0, 0}, const lit_variant_t *lit = nullptr, + const bspxentries_t *bspx = nullptr) +{ + auto *face = BSP_FindFaceAtPoint(bsp, model, point, normal); + REQUIRE(face); + + faceextents_t extents; + int offset; + + if (bspx && bspx->find("DECOUPLED_LM") != bspx->end()) { + auto lm_info = BSPX_DecoupledLM(*bspx, Face_GetNum(bsp, face)); + extents = faceextents_t(*face, *bsp, lm_info.lmwidth, lm_info.lmheight, lm_info.world_to_lm_space); + offset = lm_info.offset; + } else { + // vanilla lightmap + extents = faceextents_t(*face, *bsp, LMSCALE_DEFAULT); + offset = face->lightofs; + } + + const auto coord = extents.worldToLMCoord(point); + const auto int_coord = qvec2i(round(coord[0]), round(coord[1])); + + const qvec3f sample = LM_Sample_HDR(bsp, face, extents, offset, int_coord, lit, bspx); + INFO("world point: ", point); + INFO("lm coord: ", coord[0], ", ", coord[1]); + INFO("lm int_coord: ", int_coord[0], ", ", int_coord[1]); + INFO("face num: ", Face_GetNum(bsp, face)); + INFO("actual sample: ", sample[0], " ", sample[1], " ", sample[2]); + + qvec3f delta = qv::abs(sample - expected_color); + CHECK(delta[0] <= allowed_delta[0]); + CHECK(delta[1] <= allowed_delta[1]); + CHECK(delta[2] <= allowed_delta[2]); +} + TEST_CASE("emissive lights") { auto [bsp, bspx] = QbspVisLight_Q2("q2_light_flush.map", {}); @@ -1062,3 +1102,43 @@ TEST_CASE("hl_light_black") // confirmed that this renders as expected (black lightmaps) in the Dec 2023 HL build } } + +TEST_CASE("q1 hdr") +{ + // center of the room on the floor. + // in the non-HDR lightmap this is pure black (0, 0, 0), but in the HDR one it's still receiving a bit of light + const qvec3f testpoint = {0, 0, 48}; + const qvec3f testnormal = {0, 0, 1}; + const qvec3f expected_hdr_color = {0.00215912, 0.0018692, 0.00126648}; + + SUBCASE("lit") + { + auto [bsp, bspx, lit] = QbspVisLight_Q1("q1_hdrtest.map", {"-hdr"}); + + CHECK(bspx.empty()); + CHECK(std::holds_alternative(lit)); + + // check hdr .lit file + CheckFaceLuxelAtPoint_HDR(&bsp, &bsp.dmodels[0], expected_hdr_color, {1e-5, 1e-5, 1e-5}, testpoint, testnormal, + &lit, &bspx); + + // check internal lightmap - greyscale, since Q1 + CheckFaceLuxelAtPoint(&bsp, &bsp.dmodels[0], {0, 0, 0}, testpoint, testnormal); + } + + SUBCASE("bspx") + { + auto [bsp, bspx, lit] = QbspVisLight_Q1("q1_hdrtest.map", {"-bspxhdr"}); + + CHECK(bspx.size() == 1); + CHECK(bspx.find("LIGHTING_E5BGR9") != bspx.end()); + CHECK(std::holds_alternative(lit)); + + // check hdr BSPX lump + CheckFaceLuxelAtPoint_HDR(&bsp, &bsp.dmodels[0], expected_hdr_color, {1e-5, 1e-5, 1e-5}, testpoint, testnormal, + &lit, &bspx); + + // check internal lightmap - greyscale, since Q1 + CheckFaceLuxelAtPoint(&bsp, &bsp.dmodels[0], {0, 0, 0}, testpoint, testnormal); + } +} \ No newline at end of file diff --git a/tests/test_qbsp.hh b/tests/test_qbsp.hh index c380944c..cad96497 100644 --- a/tests/test_qbsp.hh +++ b/tests/test_qbsp.hh @@ -1,8 +1,11 @@ #include #include +#include + #include #include #include +#include class mapbrush_t; struct mapface_t; @@ -39,7 +42,7 @@ struct testresults_lit_t { mbsp_t bsp; bspxentries_t bspx; - std::vector lit; + lit_variant_t lit; }; enum class runvis_t