Skip to content

Commit

Permalink
light: LIGHTING_E5BGR9 + HDR .lit support (#427)
Browse files Browse the repository at this point in the history
* light: Add support for LIGHTING_E5BGR9.

This is not really finished. Currently mutually exclusive with
regular RGBLIGHTING, and not tested with external .lit file.

* light: replace HDR_PackResult with a version following the OpenGL spec

- add HDR_UnpackE5BRG9 for unpacking
- caller is now responsible for scaling 128 -> 1
- expose in header for tests

* light: move HDR_PackE5BRG9/HDR_UnpackE5BRG9 to common/litfile.hh

* build: std::powf not available in gcc

* common: move LoadLitFile to litfile.hh

* common: LoadLitFile: return hdr variant as well

* lightpreview: add an Exposure slider

* lightpreview: support hdr .lit's, wip

* tests: refactoring to set up testing for hdr lit's

* tests: add test for -hdr and -bspxhdr

* tests: fix LM_Sample to deal with styles correctly

* lightpreview: fix glsl error

* lightpreview: show dialog box for glsl compile errors

---------

Co-authored-by: Daniel Svensson <[email protected]>
  • Loading branch information
ericwa and dsvensson authored May 27, 2024
1 parent ccb502d commit 30868ea
Show file tree
Hide file tree
Showing 21 changed files with 915 additions and 195 deletions.
2 changes: 2 additions & 0 deletions common/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ add_library(common STATIC
cmdlib.cc
decompile.cc
entdata.cc
litfile.cc
log.cc
mathlib.cc
parser.cc
Expand All @@ -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
Expand Down
136 changes: 101 additions & 35 deletions common/bspinfo.cc
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
#include <common/json.hh>
#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)
Expand Down Expand Up @@ -224,24 +224,40 @@ static faceextents_t get_face_extents(const mbsp_t &bsp, const bspxentries_t &bs
(float)nth_bit(reinterpret_cast<const char *>(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<uint8_t> &litdata, bool use_bspx, bool use_decoupled)
full_atlas_t build_lightmap_atlas(const mbsp_t &bsp, const bspxentries_t &bspx, const std::vector<uint8_t> &litdata,
const std::vector<uint32_t> &hdr_litdata, bool use_bspx, bool use_decoupled)
{
struct face_rect
{
const mface_t *face;
faceextents_t extents;
int32_t lightofs;
std::optional<img::texture> texture = std::nullopt;

// lightmap data for this face
int width = 0, height = 0;
std::vector<qvec4b> rgba8_samples;
std::vector<uint32_t> 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<const uint32_t *>(it->second.data());
is_hdr = true;
} else if (!litdata.empty()) {
is_lit = true;
is_rgb = true;
lightdata_source = litdata.data();
Expand Down Expand Up @@ -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;

Expand All @@ -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;

Expand All @@ -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++;
}
}
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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<float> 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);
}

Expand Down
79 changes: 56 additions & 23 deletions common/bsputils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t> *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) {
Expand All @@ -1100,14 +1113,22 @@ qvec3b LM_Sample(const mbsp_t *bsp, const std::vector<uint8_t> *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<lit1_t>(*lit))
throw std::runtime_error("not implemented");

const uint8_t *lit_data = std::get_if<lit1_t>(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],
Expand All @@ -1121,31 +1142,43 @@ qvec3b LM_Sample(const mbsp_t *bsp, const std::vector<uint8_t> *lit, const facee
}
}

std::vector<uint8_t> 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::endian::little>;

std::array<char, 4> ident;
stream >= ident;
if (ident != std::array<char, 4>{'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<uint8_t> 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_hdr>(*lit)) {
packed_samples = std::get_if<lit_hdr>(lit)->samples.data();
} else if (bspx) {
if (auto it = bspx->find("LIGHTING_E5BGR9"); it != bspx->end()) {
// FIXME: alignment ignored
packed_samples = reinterpret_cast<const uint32_t *>(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<int, std::vector<int>> &cluster_to_leafnums)
Expand Down
Loading

0 comments on commit 30868ea

Please sign in to comment.