3055 lines
107 KiB
C++
3055 lines
107 KiB
C++
/* 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 <light/light.hh>
|
|
#include <light/phong.hh>
|
|
#include <light/bounce.hh>
|
|
#include <light/surflight.hh> //mxd
|
|
#include <light/entities.hh>
|
|
#include <light/trace.hh>
|
|
#include <light/ltface.hh>
|
|
|
|
#include <common/log.hh>
|
|
#include <common/bsputils.hh>
|
|
#include <common/qvec.hh>
|
|
|
|
#include <atomic>
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <fstream>
|
|
#include <fmt/ostream.h>
|
|
|
|
using namespace std;
|
|
|
|
std::atomic<uint32_t> total_light_rays, total_light_ray_hits, total_samplepoints;
|
|
std::atomic<uint32_t> total_bounce_rays, total_bounce_ray_hits;
|
|
std::atomic<uint32_t> total_surflight_rays, total_surflight_ray_hits; // mxd
|
|
std::atomic<uint32_t> fully_transparent_lightmaps;
|
|
bool warned_about_light_map_overflow, warned_about_light_style_overflow;
|
|
|
|
/* Debug helper - move elsewhere? */
|
|
void PrintFaceInfo(const mface_t *face, const mbsp_t *bsp)
|
|
{
|
|
const mtexinfo_t *tex = &bsp->texinfo[face->texinfo];
|
|
const char *texname = Face_TextureName(bsp, face);
|
|
|
|
logging::print("face {}, texture {}, {} edges; vectors:\n"
|
|
"{}\n",
|
|
Face_GetNum(bsp, face), texname, face->numedges, tex->vecs);
|
|
|
|
for (int i = 0; i < face->numedges; i++) {
|
|
int edge = bsp->dsurfedges[face->firstedge + i];
|
|
int vert = Face_VertexAtIndex(bsp, face, i);
|
|
const qvec3f &point = GetSurfaceVertexPoint(bsp, face, i);
|
|
const qvec3f norm = GetSurfaceVertexNormal(bsp, face, i).normal;
|
|
logging::print("{} {:3} ({:3.3}, {:3.3}, {:3.3}) :: normal ({:3.3}, {:3.3}, {:3.3}) :: edge {}\n",
|
|
i ? " " : " verts ", vert, point[0], point[1], point[2], norm[0], norm[1], norm[2], edge);
|
|
}
|
|
}
|
|
|
|
class position_t
|
|
{
|
|
public:
|
|
bool m_unoccluded;
|
|
const mface_t *m_actualFace;
|
|
qvec3f m_position;
|
|
qvec3f m_interpolatedNormal;
|
|
|
|
position_t(qvec3f position)
|
|
: m_unoccluded(false), m_actualFace(nullptr), m_position(position), m_interpolatedNormal({})
|
|
{
|
|
}
|
|
|
|
position_t(const mface_t *actualFace, const qvec3f &position, const qvec3f &interpolatedNormal)
|
|
: m_unoccluded(true), m_actualFace(actualFace), m_position(position),
|
|
m_interpolatedNormal(interpolatedNormal){};
|
|
};
|
|
|
|
static constexpr float sampleOffPlaneDist = 1.0f;
|
|
|
|
static float TexSpaceDist(const mbsp_t *bsp, const mface_t *face, const qvec3f &p0, const qvec3f &p1)
|
|
{
|
|
const qvec2f p0_tex = Face_WorldToTexCoord(bsp, face, p0);
|
|
const qvec2f p1_tex = Face_WorldToTexCoord(bsp, face, p1);
|
|
|
|
return qv::length(p1_tex - p0_tex);
|
|
}
|
|
|
|
/*
|
|
|
|
Why this is so complicated:
|
|
|
|
- vanilla tools just did a trace from the face centroid to the desired location of the sample point.
|
|
This doesn't work because we want to allow pillars blocking parts of the face. (func_detail_wall).
|
|
|
|
- must avoid solutions that leak light through walls (e.g. having an interior wall
|
|
pick up light from outside), and avoid light leaks e.g. in V-shaped sconces
|
|
that have a light inside the "V".
|
|
|
|
- it's critical to allow sample points to extend onto neighbouring faces,
|
|
both for phong shading to look good, as well as general light quality
|
|
|
|
*/
|
|
static position_t PositionSamplePointOnFace(
|
|
const mbsp_t *bsp, const mface_t *face, const bool phongShaded, const qvec3f &point, const qvec3f &modelOffset);
|
|
|
|
std::vector<const mface_t *> NeighbouringFaces_old(const mbsp_t *bsp, const mface_t *face)
|
|
{
|
|
std::vector<const mface_t *> result;
|
|
for (int i = 0; i < face->numedges; i++) {
|
|
const mface_t *smoothed = Face_EdgeIndexSmoothed(bsp, face, i);
|
|
if (smoothed != nullptr && smoothed != face) {
|
|
result.push_back(smoothed);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
position_t CalcPointNormal(const mbsp_t *bsp, const mface_t *face, const qvec3f &origPoint, bool phongShaded,
|
|
float face_lmscale, int recursiondepth, const qvec3f &modelOffset)
|
|
{
|
|
const auto &facecache = FaceCacheForFNum(Face_GetNum(bsp, face));
|
|
const qvec4f &surfplane = facecache.plane();
|
|
const auto &points = facecache.points();
|
|
const auto &edgeplanes = facecache.edgePlanes();
|
|
// const auto &neighbours = facecache.neighbours();
|
|
|
|
// check for degenerate face
|
|
if (points.empty() || edgeplanes.empty())
|
|
return position_t(origPoint);
|
|
|
|
// project `point` onto the surface plane, then lift it off again
|
|
const qvec3f point = GLM_ProjectPointOntoPlane(surfplane, origPoint) + (qvec3f(surfplane) * sampleOffPlaneDist);
|
|
|
|
// check if in face..
|
|
if (GLM_EdgePlanes_PointInside(edgeplanes, point)) {
|
|
return PositionSamplePointOnFace(bsp, face, phongShaded, point, modelOffset);
|
|
}
|
|
|
|
#if 0
|
|
// fixme: handle "not possible to compute"
|
|
const qvec3f centroid = Face_Centroid(bsp, face);
|
|
|
|
for (const neighbour_t &n : neighbours) {
|
|
/*
|
|
|
|
check if in XXX area:
|
|
|
|
"in1" "in2"
|
|
\XXXX|
|
|
\XXX|
|
|
|--|----|
|
|
|\ | |
|
|
| * | * = centroid
|
|
|------/
|
|
|
|
*/
|
|
|
|
const qvec3f in1_normal = qv::cross(qv::normalize(n.p0 - centroid), facecache.normal());
|
|
const qvec3f in2_normal = qv::cross(facecache.normal(), qv::normalize(n.p1 - centroid));
|
|
const qvec4f in1 = GLM_MakePlane(in1_normal, n.p0);
|
|
const qvec4f in2 = GLM_MakePlane(in2_normal, n.p1);
|
|
|
|
const float in1_dist = GLM_DistAbovePlane(in1, point);
|
|
const float in2_dist = GLM_DistAbovePlane(in2, point);
|
|
if (in1_dist >= 0 && in2_dist >= 0) {
|
|
const auto &n_facecache = FaceCacheForFNum(Face_GetNum(bsp, n.face));
|
|
const qvec4f &n_surfplane = n_facecache.plane();
|
|
const auto &n_edgeplanes = n_facecache.edgePlanes();
|
|
|
|
// project `point` onto the surface plane, then lift it off again
|
|
const qvec3f n_point = GLM_ProjectPointOntoPlane(n_surfplane, origPoint) + (qvec3f(n_surfplane) * sampleOffPlaneDist);
|
|
|
|
// check if in face..
|
|
if (GLM_EdgePlanes_PointInside(n_edgeplanes, n_point)) {
|
|
return PositionSamplePointOnFace(bsp, n.face, phongShaded, n_point, modelOffset);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// not in any triangle. among the edges this point is _behind_,
|
|
// search for the one that the point is least past the endpoints of the edge
|
|
{
|
|
int bestplane = -1;
|
|
float bestdist = FLT_MAX;
|
|
|
|
for (int i = 0; i < face->numedges; i++) {
|
|
const qvec3f &v0 = points.at(i);
|
|
const qvec3f &v1 = points.at((i + 1) % points.size());
|
|
|
|
const auto edgeplane = GLM_MakeInwardFacingEdgePlane(v0, v1, surfplane);
|
|
if (!edgeplane.first)
|
|
continue; // degenerate edge
|
|
|
|
const float planedist = GLM_DistAbovePlane(edgeplane.second, point);
|
|
if (planedist < POINT_EQUAL_EPSILON) {
|
|
// behind this plane. check whether we're between the endpoints.
|
|
|
|
const qvec3f v0v1 = v1 - v0;
|
|
const float v0v1dist = qv::length(v0v1);
|
|
|
|
const float t = FractionOfLine(v0, v1, point); // t=0 for point=v0, t=1 for point=v1
|
|
|
|
float edgedist;
|
|
if (t < 0)
|
|
edgedist = fabs(t) * v0v1dist;
|
|
else if (t > 1)
|
|
edgedist = t * v0v1dist;
|
|
else
|
|
edgedist = 0;
|
|
|
|
if (edgedist < bestdist) {
|
|
bestplane = i;
|
|
bestdist = edgedist;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestplane != -1) {
|
|
// FIXME: Also need to handle non-smoothed but same plane
|
|
const mface_t *smoothed = Face_EdgeIndexSmoothed(bsp, face, bestplane);
|
|
if (smoothed) {
|
|
// try recursive search
|
|
if (recursiondepth < 3) {
|
|
// call recursively to look up normal in the adjacent face
|
|
return CalcPointNormal(
|
|
bsp, smoothed, point, phongShaded, face_lmscale, recursiondepth + 1, modelOffset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Try snapping to poly
|
|
|
|
const pair<int, qvec3f> closest = GLM_ClosestPointOnPolyBoundary(points, point);
|
|
const float texSpaceDist = TexSpaceDist(bsp, face, closest.second, point);
|
|
|
|
if (texSpaceDist <= face_lmscale) {
|
|
// Snap it to the face edge. Add the 1 unit off plane.
|
|
const qvec3f snapped = closest.second + (qvec3f(surfplane) * sampleOffPlaneDist);
|
|
return PositionSamplePointOnFace(bsp, face, phongShaded, snapped, modelOffset);
|
|
}
|
|
|
|
// This point is too far from the polygon to be visible in game, so don't bother calculating lighting for it.
|
|
// Dont contribute to interpolating.
|
|
// We could safely colour it in pink for debugging.
|
|
return position_t(point);
|
|
}
|
|
|
|
// Dump points to a .map file
|
|
static void CalcPoints_Debug(const lightsurf_t *surf, const mbsp_t *bsp)
|
|
{
|
|
std::ofstream f("calcpoints.map");
|
|
|
|
for (int t = 0; t < surf->height; t++) {
|
|
for (int s = 0; s < surf->width; s++) {
|
|
const int i = t * surf->width + s;
|
|
const qvec3d &point = surf->points[i];
|
|
const qvec3d mangle = qv::mangle_from_vec(qvec3d(surf->normals[i]));
|
|
|
|
f << "{\n";
|
|
f << "\"classname\" \"light\"\n";
|
|
fmt::print(f, "\"origin\" \"{}\"\n", point);
|
|
fmt::print(f, "\"mangle\" \"{}\"\n", mangle);
|
|
fmt::print(f, "\"face\" \"{}\"\n", surf->realfacenums[i]);
|
|
fmt::print(f, "\"occluded\" \"{}\"\n", (int)surf->occluded[i]);
|
|
fmt::print(f, "\"s\" \"{}\"\n", s);
|
|
fmt::print(f, "\"t\" \"{}\"\n", t);
|
|
f << "}\n";
|
|
}
|
|
}
|
|
|
|
logging::print("wrote face {}'s sample points ({}x{}) to calcpoints.map\n", Face_GetNum(bsp, surf->face),
|
|
surf->width, surf->height);
|
|
|
|
PrintFaceInfo(surf->face, bsp);
|
|
}
|
|
|
|
/// Checks if the point is in any solid (solid or sky leaf)
|
|
/// 1. the world
|
|
/// 2. any shadow-casting bmodel
|
|
/// 3. the `self` model (regardless of whether it's selfshadowing)
|
|
///
|
|
/// This is used for marking sample points as occluded.
|
|
bool Light_PointInAnySolid(const mbsp_t *bsp, const dmodelh2_t *self, const qvec3d &point)
|
|
{
|
|
if (Light_PointInSolid(bsp, self, point))
|
|
return true;
|
|
|
|
if (Light_PointInWorld(bsp, point))
|
|
return true;
|
|
|
|
for (const auto &modelinfo : tracelist) {
|
|
if (Light_PointInSolid(bsp, modelinfo->model, point)) {
|
|
// Only mark occluded if the bmodel is fully opaque
|
|
if (modelinfo->alpha.value() == 1.0f)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// precondition: `point` is on the same plane as `face` and within the bounds.
|
|
static position_t PositionSamplePointOnFace(
|
|
const mbsp_t *bsp, const mface_t *face, const bool phongShaded, const qvec3f &point, const qvec3f &modelOffset)
|
|
{
|
|
const auto &facecache = FaceCacheForFNum(Face_GetNum(bsp, face));
|
|
const auto &points = facecache.points();
|
|
const auto &normals = facecache.normals();
|
|
const auto &edgeplanes = facecache.edgePlanes();
|
|
const auto &plane = facecache.plane();
|
|
|
|
if (edgeplanes.empty()) {
|
|
// degenerate polygon
|
|
return position_t(point);
|
|
}
|
|
|
|
const float planedist = GLM_DistAbovePlane(plane, point);
|
|
if (!(fabs(planedist - sampleOffPlaneDist) <= 0.1)) {
|
|
// something is wrong?
|
|
return position_t(point);
|
|
}
|
|
|
|
const float insideDist = GLM_EdgePlanes_PointInsideDist(edgeplanes, point);
|
|
if (insideDist < -POINT_EQUAL_EPSILON) {
|
|
// Non-convex polygon
|
|
return position_t(point);
|
|
}
|
|
|
|
const modelinfo_t *mi = ModelInfoForFace(bsp, Face_GetNum(bsp, face));
|
|
if (mi == nullptr) {
|
|
// missing model ("skip" faces) don't get lighting
|
|
return position_t(point);
|
|
}
|
|
|
|
// Get the point normal
|
|
qvec3f pointNormal;
|
|
if (phongShaded) {
|
|
const auto interpNormal = GLM_InterpolateNormal(points, normals, point);
|
|
// We already know the point is in the face, so this should always succeed
|
|
if (!interpNormal.first)
|
|
return position_t(point);
|
|
pointNormal = interpNormal.second;
|
|
} else {
|
|
pointNormal = plane;
|
|
}
|
|
|
|
const bool inSolid = Light_PointInAnySolid(bsp, mi->model, point + modelOffset);
|
|
if (inSolid) {
|
|
// Check distance to border
|
|
const float distanceInside = GLM_EdgePlanes_PointInsideDist(edgeplanes, point);
|
|
if (distanceInside < 1.0f) {
|
|
// Point is too close to the border. Try nudging it inside.
|
|
const auto &shrunk = facecache.pointsShrunkBy1Unit();
|
|
if (!shrunk.empty()) {
|
|
const pair<int, qvec3f> closest = GLM_ClosestPointOnPolyBoundary(shrunk, point);
|
|
const qvec3f newPoint = closest.second + (qvec3f(plane) * sampleOffPlaneDist);
|
|
if (!Light_PointInAnySolid(bsp, mi->model, newPoint + modelOffset))
|
|
return position_t(face, newPoint, pointNormal);
|
|
}
|
|
}
|
|
|
|
return position_t(point);
|
|
}
|
|
|
|
return position_t(face, point, pointNormal);
|
|
}
|
|
|
|
/*
|
|
* =================
|
|
* CalcPoints
|
|
* For each texture aligned grid point, back project onto the plane
|
|
* to get the world xyz value of the sample point
|
|
* =================
|
|
*/
|
|
static void CalcPoints(
|
|
const modelinfo_t *modelinfo, const qvec3d &offset, lightsurf_t *surf, const mbsp_t *bsp, const mface_t *face)
|
|
{
|
|
const settings::worldspawn_keys &cfg = *surf->cfg;
|
|
|
|
surf->width = surf->extents.width() * light_options.extra.value();
|
|
surf->height = surf->extents.height() * light_options.extra.value();
|
|
|
|
const float starts = -0.5 + (0.5 / light_options.extra.value());
|
|
const float startt = -0.5 + (0.5 / light_options.extra.value());
|
|
const float st_step = 1.0f / light_options.extra.value();
|
|
|
|
/* Allocate surf->points */
|
|
size_t num_points = surf->width * surf->height;
|
|
surf->points.resize(num_points);
|
|
surf->normals.resize(num_points);
|
|
surf->occluded.resize(num_points);
|
|
surf->realfacenums.resize(num_points);
|
|
|
|
const auto points = GLM_FacePoints(bsp, face);
|
|
const auto edgeplanes = GLM_MakeInwardFacingEdgePlanes(points);
|
|
|
|
for (int t = 0; t < surf->height; t++) {
|
|
for (int s = 0; s < surf->width; s++) {
|
|
const int i = t * surf->width + s;
|
|
qvec3d &point = surf->points[i];
|
|
qvec3d &norm = surf->normals[i];
|
|
int &realfacenum = surf->realfacenums[i];
|
|
|
|
const vec_t us = starts + s * st_step;
|
|
const vec_t ut = startt + t * st_step;
|
|
|
|
point = surf->extents.LMCoordToWorld(qvec2f(us, ut)) + surf->plane.normal; // one unit in front of face
|
|
|
|
// do this before correcting the point, so we can wrap around the inside of pipes
|
|
const bool phongshaded = (surf->curved && cfg.phongallowed.value());
|
|
const auto res = CalcPointNormal(bsp, face, point, phongshaded, surf->lightmapscale, 0, offset);
|
|
|
|
surf->occluded[i] = !res.m_unoccluded;
|
|
realfacenum = res.m_actualFace != nullptr ? Face_GetNum(bsp, res.m_actualFace) : -1;
|
|
point = res.m_position + offset;
|
|
norm = res.m_interpolatedNormal;
|
|
}
|
|
}
|
|
|
|
if (dump_facenum == Face_GetNum(bsp, face)) {
|
|
CalcPoints_Debug(surf, bsp);
|
|
}
|
|
}
|
|
|
|
static bool Mod_LeafPvs(const mbsp_t *bsp, const mleaf_t *leaf, uint8_t *out)
|
|
{
|
|
const size_t num_pvsclusterbytes = DecompressedVisSize(bsp);
|
|
|
|
// init to all visible
|
|
memset(out, 0xFF, num_pvsclusterbytes);
|
|
|
|
if (bsp->loadversion->game->id == GAME_QUAKE_II) {
|
|
if (leaf->cluster < 0) {
|
|
return false;
|
|
}
|
|
|
|
if (leaf->cluster >= bsp->dvis.bit_offsets.size() ||
|
|
bsp->dvis.get_bit_offset(VIS_PVS, leaf->cluster) >= bsp->dvis.bits.size()) {
|
|
logging::print("Mod_LeafPvs: invalid visofs for cluster {}\n", leaf->cluster);
|
|
return false;
|
|
}
|
|
|
|
Mod_Q1BSP_DecompressVis(bsp->dvis.bits.data() + bsp->dvis.get_bit_offset(VIS_PVS, leaf->cluster),
|
|
bsp->dvis.bits.data() + bsp->dvis.bits.size(), out, out + num_pvsclusterbytes);
|
|
} else {
|
|
if (leaf->visofs < 0) {
|
|
return false;
|
|
}
|
|
|
|
const ptrdiff_t leafnum = (leaf - bsp->dleafs.data());
|
|
|
|
// this is confusing.. "visleaf numbers" are the leaf number minus 1.
|
|
// they also don't go as high, bsp->dmodels[0].visleafs instead of bsp->numleafs
|
|
const int visleaf = leafnum - 1;
|
|
if (visleaf < 0 || visleaf >= bsp->dmodels[0].visleafs) {
|
|
return false;
|
|
}
|
|
|
|
if (leaf->visofs >= bsp->dvis.bits.size()) {
|
|
logging::print("Mod_LeafPvs: invalid visofs for leaf {}\n", leafnum);
|
|
return false;
|
|
}
|
|
|
|
Mod_Q1BSP_DecompressVis(bsp->dvis.bits.data() + leaf->visofs, bsp->dvis.bits.data() + bsp->dvis.bits.size(),
|
|
out, out + num_pvsclusterbytes);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// returns true if pvs can see leaf
|
|
static bool Pvs_LeafVisible(const mbsp_t *bsp, const std::vector<uint8_t> &pvs, const mleaf_t *leaf)
|
|
{
|
|
if (bsp->loadversion->game->id == GAME_QUAKE_II) {
|
|
if (leaf->cluster < 0) {
|
|
return false;
|
|
}
|
|
|
|
if (leaf->cluster >= bsp->dvis.bit_offsets.size() ||
|
|
bsp->dvis.get_bit_offset(VIS_PVS, leaf->cluster) >= bsp->dvis.bits.size()) {
|
|
logging::print("Pvs_LeafVisible: invalid visofs for cluster {}\n", leaf->cluster);
|
|
return false;
|
|
}
|
|
|
|
return !!(pvs[leaf->cluster >> 3] & (1 << (leaf->cluster & 7)));
|
|
} else {
|
|
const int leafnum = (leaf - bsp->dleafs.data());
|
|
const int visleaf = leafnum - 1;
|
|
|
|
if (visleaf < 0 || visleaf >= bsp->dmodels[0].visleafs) {
|
|
logging::print("WARNING: bad/empty vis data on leaf?");
|
|
return false;
|
|
}
|
|
|
|
return !!(pvs[visleaf >> 3] & (1 << (visleaf & 7)));
|
|
}
|
|
}
|
|
|
|
static void CalcPvs(const mbsp_t *bsp, lightsurf_t *lightsurf)
|
|
{
|
|
const int pvssize = DecompressedVisSize(bsp);
|
|
const mleaf_t *lastleaf = nullptr;
|
|
|
|
// set defaults
|
|
lightsurf->pvs.clear();
|
|
|
|
if (!bsp->dvis.bits.size()) {
|
|
return;
|
|
}
|
|
|
|
// set lightsurf->pvs
|
|
uint8_t *pointpvs = (uint8_t *)alloca(pvssize);
|
|
lightsurf->pvs.resize(pvssize);
|
|
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
const mleaf_t *leaf = Light_PointInLeaf(bsp, lightsurf->points[i]);
|
|
|
|
/* most/all of the surface points are probably in the same leaf */
|
|
if (leaf == lastleaf)
|
|
continue;
|
|
|
|
lastleaf = leaf;
|
|
|
|
/* copy the pvs for this leaf into pointpvs */
|
|
Mod_LeafPvs(bsp, leaf, pointpvs);
|
|
|
|
if (bsp->loadversion->game->contents_are_liquid({leaf->contents})) {
|
|
// hack for when the sample point might be in an opaque liquid, blocking vis,
|
|
// but we typically want light to pass through these.
|
|
// see also VisCullEntity() which handles the case when the light emitter is in liquid.
|
|
for (int j = 0; j < pvssize; j++) {
|
|
lightsurf->pvs[j] |= 0xff;
|
|
}
|
|
break;
|
|
}
|
|
|
|
/* merge the pvs for this sample point into lightsurf->pvs */
|
|
for (int j = 0; j < pvssize; j++) {
|
|
lightsurf->pvs[j] |= pointpvs[j];
|
|
}
|
|
}
|
|
}
|
|
|
|
static std::unique_ptr<lightsurf_t> Lightsurf_Init(const modelinfo_t *modelinfo, const settings::worldspawn_keys &cfg,
|
|
const mface_t *face, const mbsp_t *bsp, const facesup_t *facesup,
|
|
const bspx_decoupled_lm_perface *facesup_decoupled)
|
|
{
|
|
auto spaceToWorld = TexSpaceToWorld(bsp, face);
|
|
|
|
/* Check for invalid texture axes */
|
|
if (std::isnan(spaceToWorld.at(0, 0))) {
|
|
logging::print("Bad texture axes on face:\n");
|
|
PrintFaceInfo(face, bsp);
|
|
return nullptr;
|
|
}
|
|
|
|
auto lightsurf = std::make_unique<lightsurf_t>();
|
|
lightsurf->cfg = &cfg;
|
|
lightsurf->modelinfo = modelinfo;
|
|
lightsurf->bsp = bsp;
|
|
lightsurf->face = face;
|
|
|
|
/* if liquid doesn't have the TEX_SPECIAL flag set, the map was qbsp'ed with
|
|
* lit water in mind. In that case receive light from both top and bottom.
|
|
* (lit will only be rendered in compatible engines, but degrades gracefully.)
|
|
*/
|
|
lightsurf->twosided = Face_IsTranslucent(bsp, face);
|
|
|
|
// pick the larger of the two scales
|
|
lightsurf->lightmapscale =
|
|
(facesup && facesup->lmscale < modelinfo->lightmapscale) ? facesup->lmscale : modelinfo->lightmapscale;
|
|
|
|
const surfflags_t &extended_flags = extended_texinfo_flags[face->texinfo];
|
|
lightsurf->curved = extended_flags.phong_angle != 0 || Q2_FacePhongValue(bsp, face);
|
|
|
|
// nodirt
|
|
if (modelinfo->dirt.isChanged()) {
|
|
lightsurf->nodirt = (modelinfo->dirt.value() == -1);
|
|
} else {
|
|
lightsurf->nodirt = extended_flags.no_dirt;
|
|
}
|
|
|
|
|
|
// minlight
|
|
if (modelinfo->minlight.isChanged()) {
|
|
lightsurf->minlight = modelinfo->minlight.value();
|
|
} else {
|
|
lightsurf->minlight = extended_flags.minlight;
|
|
}
|
|
|
|
// minlightMottle
|
|
if (modelinfo->minlightMottle.isChanged()) {
|
|
lightsurf->minlightMottle = modelinfo->minlightMottle.value();
|
|
} else if (light_options.minlightMottle.isChanged()) {
|
|
lightsurf->minlightMottle = light_options.minlightMottle.value();
|
|
} else {
|
|
// default value depends on game
|
|
if (bsp->loadversion->game->id == GAME_QUAKE_II) {
|
|
lightsurf->minlightMottle = true;
|
|
} else {
|
|
lightsurf->minlightMottle = false;
|
|
}
|
|
}
|
|
|
|
// Q2 uses a 0-1 range for minlight
|
|
if (bsp->loadversion->game->id == GAME_QUAKE_II) {
|
|
lightsurf->minlight *= 128.f;
|
|
}
|
|
|
|
// maxlight
|
|
if (modelinfo->maxlight.isChanged()) {
|
|
lightsurf->maxlight = modelinfo->maxlight.value();
|
|
} else {
|
|
lightsurf->maxlight = extended_flags.maxlight;
|
|
}
|
|
|
|
// lightcolorscale
|
|
if (modelinfo->lightcolorscale.isChanged()) {
|
|
lightsurf->lightcolorscale = modelinfo->lightcolorscale.value();
|
|
} else {
|
|
lightsurf->lightcolorscale = extended_flags.lightcolorscale;
|
|
}
|
|
|
|
// Q2 uses a 0-1 range for minlight
|
|
if (bsp->loadversion->game->id == GAME_QUAKE_II) {
|
|
lightsurf->maxlight *= 128.f;
|
|
}
|
|
|
|
// minlight_color
|
|
if (modelinfo->minlight_color.isChanged()) {
|
|
lightsurf->minlight_color = modelinfo->minlight_color.value();
|
|
} else {
|
|
// if modelinfo mincolor not set, use the one from the .texinfo file
|
|
if (lightsurf->minlight > 0 && qv::emptyExact(extended_flags.minlight_color)) {
|
|
lightsurf->minlight_color = {255.0};
|
|
} else {
|
|
lightsurf->minlight_color = extended_flags.minlight_color;
|
|
}
|
|
}
|
|
|
|
/* never receive dirtmapping on lit liquids */
|
|
if (Face_IsTranslucent(bsp, face)) {
|
|
lightsurf->nodirt = true;
|
|
}
|
|
|
|
/* handle glass alpha */
|
|
if (modelinfo->alpha.value() < 1) {
|
|
/* skip culling of rays coming from the back side of the face */
|
|
lightsurf->twosided = true;
|
|
}
|
|
|
|
/* Set up the plane, not including model offset */
|
|
qplane3d &plane = lightsurf->plane;
|
|
if (face->side) {
|
|
plane = -bsp->dplanes[face->planenum];
|
|
} else {
|
|
plane = bsp->dplanes[face->planenum];
|
|
}
|
|
|
|
const mtexinfo_t *tex = &bsp->texinfo[face->texinfo];
|
|
lightsurf->snormal = qv::normalize(tex->vecs.row(0).xyz());
|
|
lightsurf->tnormal = -qv::normalize(tex->vecs.row(1).xyz());
|
|
|
|
/* Set up the surface points */
|
|
if (light_options.world_units_per_luxel.isChanged()) {
|
|
lightsurf->extents = faceextents_t(*face, *bsp, world_units_per_luxel_t{},
|
|
light_options.world_units_per_luxel.value());
|
|
} else {
|
|
lightsurf->extents = faceextents_t(*face, *bsp, lightsurf->lightmapscale);
|
|
}
|
|
lightsurf->vanilla_extents = faceextents_t(*face, *bsp, LMSCALE_DEFAULT);
|
|
|
|
CalcPoints(modelinfo, modelinfo->offset, lightsurf.get(), bsp, face);
|
|
|
|
/* Correct the plane for the model offset (must be done last,
|
|
calculation of face extents / points needs the uncorrected plane) */
|
|
qvec3d planepoint = (plane.normal * plane.dist) + modelinfo->offset;
|
|
plane.dist = qv::dot(plane.normal, planepoint);
|
|
|
|
/* Correct bounding sphere */
|
|
lightsurf->extents.origin += modelinfo->offset;
|
|
lightsurf->extents.bounds = lightsurf->extents.bounds.translate(modelinfo->offset);
|
|
|
|
/* Allocate occlusion array */
|
|
lightsurf->occlusion.resize(lightsurf->points.size());
|
|
|
|
lightsurf->intersection_stream.resize(lightsurf->points.size());
|
|
lightsurf->occlusion_stream.resize(lightsurf->points.size());
|
|
|
|
/* Setup vis data */
|
|
CalcPvs(bsp, lightsurf.get());
|
|
|
|
return lightsurf;
|
|
}
|
|
|
|
static void Lightmap_AllocOrClear(lightmap_t *lightmap, const lightsurf_t *lightsurf)
|
|
{
|
|
if (!lightmap->samples.size()) {
|
|
/* first use of this lightmap, allocate the storage for it. */
|
|
lightmap->samples.resize(lightsurf->points.size());
|
|
} else if (lightmap->style != INVALID_LIGHTSTYLE) {
|
|
/* clear only the data that is going to be merged to it. there's no point clearing more */
|
|
std::fill_n(lightmap->samples.begin(), lightsurf->points.size(), lightsample_t{});
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
static const lightmap_t *Lightmap_ForStyle_ReadOnly(const lightsurf_t *lightsurf, const int style)
|
|
{
|
|
for (const auto &lm : lightsurf->lightmapsByStyle) {
|
|
if (lm.style == style)
|
|
return &lm;
|
|
}
|
|
return nullptr;
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
* Lightmap_ForStyle
|
|
*
|
|
* If lightmap with given style has already been allocated, return it.
|
|
* 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)
|
|
{
|
|
for (auto &lm : *lightmaps) {
|
|
if (lm.style == style)
|
|
return &lm;
|
|
}
|
|
|
|
// no exact match, check for an unsaved one
|
|
for (auto &lm : *lightmaps) {
|
|
if (lm.style == INVALID_LIGHTSTYLE) {
|
|
Lightmap_AllocOrClear(&lm, lightsurf);
|
|
return &lm;
|
|
}
|
|
}
|
|
|
|
// add a new one to the vector (invalidates existing lightmap_t pointers)
|
|
lightmap_t &newLightmap = lightmaps->emplace_back();
|
|
newLightmap.style = INVALID_LIGHTSTYLE;
|
|
Lightmap_AllocOrClear(&newLightmap, lightsurf);
|
|
return &newLightmap;
|
|
}
|
|
|
|
static void Lightmap_ClearAll(lightmapdict_t *lightmaps)
|
|
{
|
|
for (auto &lm : *lightmaps) {
|
|
lm.style = INVALID_LIGHTSTYLE;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Lightmap_Save
|
|
*
|
|
* As long as we have space for the style, mark as allocated,
|
|
* otherwise emit a warning.
|
|
*/
|
|
static void Lightmap_Save(
|
|
lightmapdict_t *lightmaps, const lightsurf_t *lightsurf, lightmap_t *lightmap, const int style)
|
|
{
|
|
if (lightmap->style == INVALID_LIGHTSTYLE) {
|
|
lightmap->style = style;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* ============================================================================
|
|
* FACE LIGHTING
|
|
* ============================================================================
|
|
*/
|
|
|
|
// returns the light contribution at a given distance, without regard for angle
|
|
vec_t GetLightValue(const settings::worldspawn_keys &cfg, const light_t *entity, vec_t dist)
|
|
{
|
|
const float light = entity->light.value();
|
|
|
|
// mxd. Apply falloff?
|
|
const float lightdistance = entity->falloff.value();
|
|
if (lightdistance > 0.0f) {
|
|
if (entity->getFormula() == LF_LINEAR) {
|
|
// Light can affect surface?
|
|
if (lightdistance > dist)
|
|
return light * (1.0f - (dist / lightdistance));
|
|
else
|
|
return 0.0f; // Surface is unaffected
|
|
}
|
|
}
|
|
|
|
if (entity->getFormula() == LF_INFINITE || entity->getFormula() == LF_LOCALMIN)
|
|
return light;
|
|
|
|
vec_t value = cfg.scaledist.value() * entity->atten.value() * dist;
|
|
|
|
switch (entity->getFormula()) {
|
|
case LF_INVERSE: return light / (value / LF_SCALE);
|
|
case LF_INVERSE2A:
|
|
value += LF_SCALE;
|
|
/* Fall through */
|
|
case LF_INVERSE2: return light / ((value * value) / (LF_SCALE * LF_SCALE));
|
|
case LF_LINEAR:
|
|
if (light > 0)
|
|
return (light - value > 0) ? light - value : 0;
|
|
else
|
|
return (light + value < 0) ? light + value : 0;
|
|
default: Error("Internal error: unknown light formula");
|
|
}
|
|
}
|
|
|
|
static float GetLightValueWithAngle(const settings::worldspawn_keys &cfg, const light_t *entity, const qvec3d &surfnorm,
|
|
const qvec3d &surfpointToLightDir, float dist, bool twosided)
|
|
{
|
|
vec_t angle = qv::dot(surfpointToLightDir, surfnorm);
|
|
if (entity->bleed.value() || twosided) {
|
|
if (angle < 0) {
|
|
angle = -angle; // ericw -- support "_bleed" option
|
|
}
|
|
}
|
|
|
|
/* Light behind sample point? Zero contribution, period.
|
|
see: https://github.com/ericwa/ericw-tools/issues/181 */
|
|
if (angle < 0) {
|
|
return 0;
|
|
}
|
|
|
|
/* Apply anglescale */
|
|
angle = (1.0 - entity->anglescale.value()) + (entity->anglescale.value() * angle);
|
|
|
|
/* Check spotlight cone */
|
|
float spotscale = 1;
|
|
if (entity->spotlight) {
|
|
const vec_t falloff = qv::dot(entity->spotvec, surfpointToLightDir);
|
|
if (falloff > entity->spotfalloff) {
|
|
return 0;
|
|
}
|
|
if (falloff > entity->spotfalloff2) {
|
|
/* Interpolate between the two spotlight falloffs */
|
|
spotscale = falloff - entity->spotfalloff2;
|
|
spotscale /= entity->spotfalloff - entity->spotfalloff2;
|
|
spotscale = 1.0 - spotscale;
|
|
}
|
|
}
|
|
|
|
float add = GetLightValue(cfg, entity, dist) * angle * spotscale;
|
|
return add;
|
|
}
|
|
|
|
static void Matrix4x4_CM_Transform4(const std::array<vec_t, 16> &matrix, const qvec4d &vector, qvec4d &product)
|
|
{
|
|
product[0] = matrix[0] * vector[0] + matrix[4] * vector[1] + matrix[8] * vector[2] + matrix[12] * vector[3];
|
|
product[1] = matrix[1] * vector[0] + matrix[5] * vector[1] + matrix[9] * vector[2] + matrix[13] * vector[3];
|
|
product[2] = matrix[2] * vector[0] + matrix[6] * vector[1] + matrix[10] * vector[2] + matrix[14] * vector[3];
|
|
product[3] = matrix[3] * vector[0] + matrix[7] * vector[1] + matrix[11] * vector[2] + matrix[15] * vector[3];
|
|
}
|
|
static bool Matrix4x4_CM_Project(const qvec3d &in, qvec3d &out, const std::array<vec_t, 16> &modelviewproj)
|
|
{
|
|
bool result = true;
|
|
|
|
qvec4d v;
|
|
Matrix4x4_CM_Transform4(modelviewproj, {in, 1}, v);
|
|
|
|
v[0] /= v[3];
|
|
v[1] /= v[3];
|
|
if (v[2] < 0)
|
|
result = false; // too close to the view
|
|
v[2] /= v[3];
|
|
|
|
out[0] = (1 + v[0]) / 2;
|
|
out[1] = (1 + v[1]) / 2;
|
|
out[2] = (1 + v[2]) / 2;
|
|
if (out[2] > 1)
|
|
result = false; // beyond far clip plane
|
|
return result;
|
|
}
|
|
|
|
static bool LightFace_SampleMipTex(
|
|
const img::texture *tex, const std::array<vec_t, 16> &projectionmatrix, const qvec3d &point, qvec3f &result)
|
|
{
|
|
// okay, yes, this is weird, yes we're using a vec3_t for a coord...
|
|
// this is because we're treating it like a cubemap. why? no idea.
|
|
float weight[4];
|
|
qvec4b pi[4];
|
|
|
|
qvec3d coord;
|
|
if (!Matrix4x4_CM_Project(point, coord, projectionmatrix) || coord[0] <= 0 || coord[0] >= 1 || coord[1] <= 0 ||
|
|
coord[1] >= 1) {
|
|
result = {};
|
|
return false; // mxd
|
|
}
|
|
|
|
float sfrac = (coord[0]) * (tex->meta.width - 1); // mxd. We are sampling sbase+1 pixels, so multiplying by
|
|
// tex->width will result in an 1px overdraw, same for tbase
|
|
const int sbase = sfrac;
|
|
sfrac -= sbase;
|
|
float tfrac = (1 - coord[1]) * (tex->meta.height - 1);
|
|
const int tbase = tfrac;
|
|
tfrac -= tbase;
|
|
|
|
pi[0] = tex->pixels[((sbase + 0) % tex->meta.width) + (tex->meta.width * ((tbase + 0) % tex->meta.height))];
|
|
weight[0] = (1 - sfrac) * (1 - tfrac);
|
|
pi[1] = tex->pixels[((sbase + 1) % tex->meta.width) + (tex->meta.width * ((tbase + 0) % tex->meta.height))];
|
|
weight[1] = (sfrac) * (1 - tfrac);
|
|
pi[2] = tex->pixels[((sbase + 0) % tex->meta.width) + (tex->meta.width * ((tbase + 1) % tex->meta.height))];
|
|
weight[2] = (1 - sfrac) * (tfrac);
|
|
pi[3] = tex->pixels[((sbase + 1) % tex->meta.width) + (tex->meta.width * ((tbase + 1) % tex->meta.height))];
|
|
weight[3] = (sfrac) * (tfrac);
|
|
result = pi[0].xyz() * weight[0] + pi[1].xyz() * weight[1] + pi[2].xyz() * weight[2] + pi[3].xyz() * weight[3];
|
|
result *= 2;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void GetLightContrib(const settings::worldspawn_keys &cfg, const light_t *entity, const qvec3d &surfnorm,
|
|
const qvec3d &surfpoint, bool twosided, qvec3f &color_out, qvec3d &surfpointToLightDir_out,
|
|
qvec3d &normalmap_addition_out, float *dist_out)
|
|
{
|
|
float dist = GetDir(surfpoint, entity->origin.value(), surfpointToLightDir_out);
|
|
if (dist < 0.1) {
|
|
// Catch 0 distance between sample point and light (produces infinite brightness / nan's) and causes
|
|
// problems later
|
|
dist = 0.1f;
|
|
surfpointToLightDir_out = {0, 0, 1};
|
|
}
|
|
const float add = GetLightValueWithAngle(cfg, entity, surfnorm, surfpointToLightDir_out, dist, twosided);
|
|
|
|
/* write out the final color */
|
|
if (entity->projectedmip) {
|
|
qvec3f col;
|
|
if (LightFace_SampleMipTex(entity->projectedmip, entity->projectionmatrix, surfpoint, col)) {
|
|
// mxd. Modulate by light color...
|
|
const auto &entcol = entity->color.value();
|
|
for (int i = 0; i < 3; i++)
|
|
col[i] *= entcol[i] * (1.0f / 255.0f);
|
|
}
|
|
|
|
color_out = col * add * (1.0f / 255.0f);
|
|
} else {
|
|
color_out = entity->color.value() * add * (1.0f / 255.0f);
|
|
}
|
|
|
|
// write normalmap contrib
|
|
normalmap_addition_out = surfpointToLightDir_out * add;
|
|
|
|
*dist_out = dist;
|
|
}
|
|
|
|
constexpr vec_t SQR(vec_t x)
|
|
{
|
|
return x * x;
|
|
}
|
|
|
|
// CHECK: naming? why clamp*min*?
|
|
constexpr void Light_ClampMin(lightsample_t &sample, const vec_t light, const qvec3d &color)
|
|
{
|
|
for (int i = 0; i < 3; i++) {
|
|
sample.color[i] = std::max(sample.color[i], (float) (color[i] * (light / 255.0f)));
|
|
}
|
|
}
|
|
|
|
constexpr vec_t fraction(const vec_t &min, const vec_t &val, const vec_t &max)
|
|
{
|
|
if (val >= max)
|
|
return 1.0;
|
|
if (val <= min)
|
|
return 0.0;
|
|
|
|
return (val - min) / (max - min);
|
|
}
|
|
|
|
/*
|
|
* ============
|
|
* Dirt_GetScaleFactor
|
|
*
|
|
* returns scale factor for dirt/ambient occlusion
|
|
* ============
|
|
*/
|
|
inline vec_t Dirt_GetScaleFactor(const settings::worldspawn_keys &cfg, vec_t occlusion, const light_t *entity,
|
|
const vec_t entitydist, const lightsurf_t *surf)
|
|
{
|
|
vec_t light_dirtgain = cfg.dirtGain.value();
|
|
vec_t light_dirtscale = cfg.dirtScale.value();
|
|
bool usedirt;
|
|
|
|
/* is dirt processing disabled entirely? */
|
|
if (!dirt_in_use)
|
|
return 1.0f;
|
|
if (surf && surf->nodirt)
|
|
return 1.0f;
|
|
|
|
/* should this light be affected by dirt? */
|
|
if (entity) {
|
|
if (entity->dirt.value() == -1) {
|
|
usedirt = false;
|
|
} else if (entity->dirt.value() == 1) {
|
|
usedirt = true;
|
|
} else {
|
|
usedirt = cfg.globalDirt.value();
|
|
}
|
|
} else {
|
|
/* no entity is provided, assume the caller wants dirt */
|
|
usedirt = true;
|
|
}
|
|
|
|
/* if not, quit */
|
|
if (!usedirt)
|
|
return 1.0;
|
|
|
|
/* override the global scale and gain values with the light-specific
|
|
values, if present */
|
|
if (entity) {
|
|
if (entity->dirtgain.value())
|
|
light_dirtgain = entity->dirtgain.value();
|
|
if (entity->dirtscale.value())
|
|
light_dirtscale = entity->dirtscale.value();
|
|
}
|
|
|
|
/* early out */
|
|
if (occlusion <= 0.0f) {
|
|
return 1.0f;
|
|
}
|
|
|
|
/* apply gain (does this even do much? heh) */
|
|
float outDirt = pow(occlusion, light_dirtgain);
|
|
if (outDirt > 1.0f) {
|
|
outDirt = 1.0f;
|
|
}
|
|
|
|
/* apply scale */
|
|
outDirt *= light_dirtscale;
|
|
if (outDirt > 1.0f) {
|
|
outDirt = 1.0f;
|
|
}
|
|
|
|
/* lerp based on distance to light */
|
|
if (entity) {
|
|
// From 0 to _dirt_off_radius units, no dirt.
|
|
// From _dirt_off_radius to _dirt_on_radius, the dirt linearly ramps from 0 to full, and after _dirt_on_radius,
|
|
// it's full dirt.
|
|
|
|
if (entity->dirt_on_radius.isChanged() && entity->dirt_off_radius.isChanged()) {
|
|
|
|
const float onRadius = entity->dirt_on_radius.value();
|
|
const float offRadius = entity->dirt_off_radius.value();
|
|
|
|
if (entitydist < offRadius) {
|
|
outDirt = 0.0;
|
|
} else if (entitydist >= offRadius && entitydist < onRadius) {
|
|
const float frac = fraction(offRadius, entitydist, onRadius);
|
|
outDirt = frac * outDirt;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* return to sender */
|
|
return 1.0f - outDirt;
|
|
}
|
|
|
|
/*
|
|
* ================
|
|
* CullLight
|
|
*
|
|
* Returns true if the given light doesn't reach lightsurf.
|
|
* ================
|
|
*/
|
|
inline bool CullLight(const light_t *entity, const lightsurf_t *lightsurf)
|
|
{
|
|
const settings::worldspawn_keys &cfg = *lightsurf->cfg;
|
|
|
|
if (light_options.visapprox.value() == visapprox_t::RAYS &&
|
|
entity->bounds.disjoint(lightsurf->extents.bounds, 0.001) &&
|
|
entity->light_channel_mask.value() == CHANNEL_MASK_DEFAULT &&
|
|
entity->shadow_channel_mask.value() == CHANNEL_MASK_DEFAULT) {
|
|
// EstimateVisibleBoundsAtPoint uses CHANNEL_MASK_DEFAULT
|
|
// for its rays, so only cull lights that are also going to be using
|
|
// CHANNEL_MASK_DEFAULT for rendering
|
|
return true;
|
|
}
|
|
|
|
qvec3d distvec = entity->origin.value() - lightsurf->extents.origin;
|
|
const float dist = qv::length(distvec) - lightsurf->extents.radius;
|
|
|
|
/* light is inside surface bounding sphere => can't cull */
|
|
if (dist < 0) {
|
|
return false;
|
|
}
|
|
|
|
/* return true if the light level at the closest point on the
|
|
surface bounding sphere to the light source is <= fadegate.
|
|
need fabs to handle antilights. */
|
|
return fabs(GetLightValue(cfg, entity, dist)) <= light_options.gate.value();
|
|
}
|
|
|
|
static bool VisCullEntity(const mbsp_t *bsp, const std::vector<uint8_t> &pvs, const mleaf_t *entleaf)
|
|
{
|
|
if (pvs.empty()) {
|
|
return false;
|
|
}
|
|
if (entleaf == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
if (bsp->loadversion->game->contents_are_solid({entleaf->contents}) ||
|
|
bsp->loadversion->game->contents_are_sky({entleaf->contents}) ||
|
|
bsp->loadversion->game->contents_are_liquid({entleaf->contents})) {
|
|
// the liquid case is because entleaf->contents might be in an opaque liquid,
|
|
// which we typically want light to pass through, but visdata would report that
|
|
// there's no visibility across the opaque liquid. so, skip culling and do the raytracing.
|
|
return false;
|
|
}
|
|
|
|
return !Pvs_LeafVisible(bsp, pvs, entleaf);
|
|
}
|
|
|
|
/*
|
|
* ================
|
|
* LightFace_Entity
|
|
* ================
|
|
*/
|
|
static void LightFace_Entity(
|
|
const mbsp_t *bsp, const light_t *entity, lightsurf_t *lightsurf, lightmapdict_t *lightmaps)
|
|
{
|
|
const settings::worldspawn_keys &cfg = *lightsurf->cfg;
|
|
const modelinfo_t *modelinfo = lightsurf->modelinfo;
|
|
const qplane3d *plane = &lightsurf->plane;
|
|
|
|
/* vis cull */
|
|
if (light_options.visapprox.value() == visapprox_t::VIS
|
|
&& entity->light_channel_mask.value() == CHANNEL_MASK_DEFAULT
|
|
&& entity->shadow_channel_mask.value() == CHANNEL_MASK_DEFAULT
|
|
&& VisCullEntity(bsp, lightsurf->pvs, entity->leaf)) {
|
|
return;
|
|
}
|
|
|
|
const vec_t planedist = plane->distance_to(entity->origin.value());
|
|
|
|
/* don't bother with lights behind the surface.
|
|
|
|
if the surface is curved, the light may be behind the surface, but it may
|
|
still have a line of sight to a samplepoint, and that sample point's
|
|
normal may be facing such that it receives some light, so we can't use this
|
|
test in the curved case.
|
|
*/
|
|
if (planedist < 0 && !entity->bleed.value() && !lightsurf->curved && !lightsurf->twosided) {
|
|
return;
|
|
}
|
|
|
|
/* sphere cull surface and light */
|
|
if (CullLight(entity, lightsurf)) {
|
|
return;
|
|
}
|
|
|
|
// check lighting channels
|
|
if (!(entity->light_channel_mask.value() & lightsurf->modelinfo->object_channel_mask.value())) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Check it for real
|
|
*/
|
|
raystream_occlusion_t &rs = lightsurf->occlusion_stream;
|
|
rs.clearPushedRays();
|
|
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
const qvec3d &surfpoint = lightsurf->points[i];
|
|
const qvec3d &surfnorm = lightsurf->normals[i];
|
|
|
|
if (lightsurf->occluded[i])
|
|
continue;
|
|
|
|
qvec3d surfpointToLightDir;
|
|
float surfpointToLightDist;
|
|
qvec3f color;
|
|
qvec3d normalcontrib;
|
|
|
|
GetLightContrib(cfg, entity, surfnorm, surfpoint, lightsurf->twosided, color, surfpointToLightDir,
|
|
normalcontrib, &surfpointToLightDist);
|
|
|
|
const float occlusion =
|
|
Dirt_GetScaleFactor(cfg, lightsurf->occlusion[i], entity, surfpointToLightDist, lightsurf);
|
|
color *= occlusion;
|
|
|
|
/* Quick distance check first */
|
|
if (fabs(LightSample_Brightness(color)) <= light_options.gate.value()) {
|
|
continue;
|
|
}
|
|
|
|
rs.pushRay(i, surfpoint, surfpointToLightDir, surfpointToLightDist, &color, &normalcontrib);
|
|
}
|
|
|
|
// don't need closest hit, just checking for occlusion between light and surface point
|
|
rs.tracePushedRaysOcclusion(modelinfo, entity->shadow_channel_mask.value());
|
|
total_light_rays += rs.numPushedRays();
|
|
|
|
int cached_style = entity->style.value();
|
|
lightmap_t *cached_lightmap = Lightmap_ForStyle(lightmaps, cached_style, lightsurf);
|
|
|
|
const int N = rs.numPushedRays();
|
|
for (int j = 0; j < N; j++) {
|
|
if (rs.getPushedRayOccluded(j)) {
|
|
continue;
|
|
}
|
|
|
|
total_light_ray_hits++;
|
|
|
|
int i = rs.getPushedRayPointIndex(j);
|
|
|
|
// check if we hit a dynamic shadow caster (only applies to style 0 lights)
|
|
//
|
|
// note, this still works even though we're doing an occlusion trace - closest
|
|
// hit doesn't matter. All that matters is whether there is a real (solid) occluder
|
|
// between the ray start and end.
|
|
//
|
|
// If there is, the light is fully blocked and we bail out above, regardless of any
|
|
// dynamic shadow casters that also might be along the ray.
|
|
//
|
|
// If not, then we are guaranteed to detect the dynamic shadow caster in the ray filter
|
|
// (if any), and handle it here.
|
|
int desired_style = entity->style.value();
|
|
if (desired_style == 0) {
|
|
desired_style = rs.getPushedRayDynamicStyle(j);
|
|
}
|
|
|
|
// if necessary, switch which lightmap we are writing to.
|
|
if (desired_style != cached_style) {
|
|
cached_style = desired_style;
|
|
cached_lightmap = Lightmap_ForStyle(lightmaps, cached_style, lightsurf);
|
|
}
|
|
|
|
lightsample_t &sample = cached_lightmap->samples[i];
|
|
|
|
sample.color += rs.getPushedRayColor(j);
|
|
sample.direction += rs.getPushedRayNormalContrib(j);
|
|
|
|
Lightmap_Save(lightmaps, lightsurf, cached_lightmap, cached_style);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* =============
|
|
* LightFace_Sky
|
|
* =============
|
|
*/
|
|
static void LightFace_Sky(const sun_t *sun, lightsurf_t *lightsurf, lightmapdict_t *lightmaps)
|
|
{
|
|
const settings::worldspawn_keys &cfg = *lightsurf->cfg;
|
|
const modelinfo_t *modelinfo = lightsurf->modelinfo;
|
|
const qplane3d *plane = &lightsurf->plane;
|
|
|
|
// FIXME: Normalized sun vector should be stored in the sun_t. Also clarify which way the vector points (towards or
|
|
// away..)
|
|
// FIXME: Much of this is copied/pasted from LightFace_Entity, should probably be merged
|
|
qvec3d incoming = qv::normalize(sun->sunvec);
|
|
|
|
/* Don't bother if surface facing away from sun */
|
|
const vec_t dp = qv::dot(incoming, plane->normal);
|
|
if (dp < -LIGHT_ANGLE_EPSILON && !lightsurf->curved && !lightsurf->twosided) {
|
|
return;
|
|
}
|
|
|
|
// check lighting channels (currently sunlight is always on CHANNEL_MASK_DEFAULT)
|
|
if (!(lightsurf->modelinfo->object_channel_mask.value() & CHANNEL_MASK_DEFAULT)) {
|
|
return;
|
|
}
|
|
|
|
/* Check each point... */
|
|
raystream_intersection_t &rs = lightsurf->intersection_stream;
|
|
rs.clearPushedRays();
|
|
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
const qvec3d &surfpoint = lightsurf->points[i];
|
|
const qvec3d &surfnorm = lightsurf->normals[i];
|
|
|
|
if (lightsurf->occluded[i])
|
|
continue;
|
|
|
|
vec_t angle = qv::dot(incoming, surfnorm);
|
|
if (lightsurf->twosided) {
|
|
if (angle < 0) {
|
|
angle = -angle;
|
|
}
|
|
}
|
|
|
|
angle = max(0.0, angle);
|
|
|
|
angle = (1.0 - sun->anglescale) + sun->anglescale * angle;
|
|
float value = angle * sun->sunlight;
|
|
if (sun->dirt) {
|
|
value *= Dirt_GetScaleFactor(cfg, lightsurf->occlusion[i], NULL, 0.0, lightsurf);
|
|
}
|
|
|
|
qvec3f color = sun->sunlight_color * (value / 255.0);
|
|
|
|
/* Quick distance check first */
|
|
if (fabs(LightSample_Brightness(color)) <= light_options.gate.value()) {
|
|
continue;
|
|
}
|
|
|
|
qvec3d normalcontrib = incoming * value;
|
|
|
|
rs.pushRay(i, surfpoint, incoming, MAX_SKY_DIST, &color, &normalcontrib);
|
|
}
|
|
|
|
// We need to check if the first hit face is a sky face, so we need
|
|
// to test intersection (not occlusion)
|
|
rs.tracePushedRaysIntersection(modelinfo, CHANNEL_MASK_DEFAULT);
|
|
|
|
/* if sunlight is set, use a style 0 light map */
|
|
int cached_style = sun->style;
|
|
lightmap_t *cached_lightmap = Lightmap_ForStyle(lightmaps, cached_style, lightsurf);
|
|
|
|
const int N = rs.numPushedRays();
|
|
total_light_rays += N;
|
|
|
|
for (int j = 0; j < N; j++) {
|
|
if (rs.getPushedRayHitType(j) != hittype_t::SKY) {
|
|
continue;
|
|
}
|
|
|
|
// check if we hit the wrong texture
|
|
if (sun->suntexture_value) {
|
|
const triinfo *face = rs.getPushedRayHitFaceInfo(j);
|
|
if (sun->suntexture_value != face->texture) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const int i = rs.getPushedRayPointIndex(j);
|
|
|
|
// check if we hit a dynamic shadow caster
|
|
int desired_style = sun->style;
|
|
if (desired_style == 0) {
|
|
desired_style = rs.getPushedRayDynamicStyle(j);
|
|
}
|
|
|
|
// if necessary, switch which lightmap we are writing to.
|
|
if (desired_style != cached_style) {
|
|
cached_style = desired_style;
|
|
cached_lightmap = Lightmap_ForStyle(lightmaps, cached_style, lightsurf);
|
|
}
|
|
|
|
lightsample_t &sample = cached_lightmap->samples[i];
|
|
|
|
sample.color += rs.getPushedRayColor(j);
|
|
sample.direction += rs.getPushedRayNormalContrib(j);
|
|
total_light_ray_hits++;
|
|
|
|
Lightmap_Save(lightmaps, lightsurf, cached_lightmap, cached_style);
|
|
}
|
|
}
|
|
|
|
// Mottle
|
|
|
|
static int mod_round_to_neg_inf(int x, int y) {
|
|
assert(y > 0);
|
|
if (x >= 0) {
|
|
return x % y;
|
|
}
|
|
// e.g. with mod_round_to_neg_inf(-7, 3) we want +2
|
|
const int temp = (-x) % y;
|
|
return y - temp;
|
|
}
|
|
|
|
constexpr int mottle_texsize = 256;
|
|
|
|
/**
|
|
* integers 0 through 255 shuffled with Python:
|
|
*
|
|
* import random
|
|
* a = list(range(0, 256))
|
|
* random.shuffle(a)
|
|
*/
|
|
static constexpr uint8_t MottlePermutation256[] = {11, 255, 250, 82, 217, 9, 144, 93, 136, 153, 55, 71, 73, 204, 96,
|
|
180, 126, 8, 50, 46, 113, 91, 238, 143, 30, 215, 191, 243, 65, 58, 208, 33, 86, 1, 182, 118, 83, 115, 207, 52, 94,
|
|
112, 205, 48, 99, 254, 117, 101, 157, 140, 72, 242, 244, 154, 10, 135, 155, 168, 125, 183, 148, 116, 187, 166, 25,
|
|
156, 177, 231, 165, 57, 221, 105, 28, 211, 127, 41, 142, 253, 146, 87, 122, 229, 162, 137, 194, 174, 167, 15, 220,
|
|
26, 235, 3, 39, 80, 88, 42, 202, 12, 97, 53, 70, 123, 170, 110, 214, 192, 173, 84, 169, 188, 64, 102, 147, 158, 100,
|
|
69, 213, 193, 43, 20, 13, 237, 171, 103, 32, 190, 223, 150, 131, 206, 85, 124, 163, 18, 139, 132, 79, 29, 216, 232,
|
|
178, 74, 24, 141, 201, 181, 152, 4, 7, 159, 134, 212, 226, 245, 164, 239, 47, 66, 27, 40, 197, 81, 78, 219, 228,
|
|
241, 121, 23, 120, 230, 76, 252, 199, 184, 45, 203, 161, 89, 16, 21, 119, 5, 209, 196, 68, 130, 195, 176, 225, 233,
|
|
128, 22, 248, 179, 249, 61, 108, 138, 145, 31, 49, 107, 56, 172, 224, 210, 6, 160, 189, 104, 200, 44, 175, 133, 77,
|
|
62, 106, 92, 186, 227, 14, 38, 247, 37, 17, 222, 36, 75, 129, 185, 251, 240, 54, 151, 2, 98, 149, 0, 63, 218, 60,
|
|
198, 19, 59, 90, 246, 234, 67, 51, 109, 95, 236, 35, 34, 114, 111};
|
|
|
|
/**
|
|
* Return a noise texture value from 0-47.
|
|
*
|
|
* Vanilla Q2 tools just called (rand() % 48) per-luxel, which generates seams
|
|
* and scales in size with lightmap scale.
|
|
*
|
|
* Replacement code uses "value noise", generating a tiling 3D texture
|
|
* in world space.
|
|
*/
|
|
static float Mottle(const qvec3d &position)
|
|
{
|
|
#if 0
|
|
return rand() % 48;
|
|
#else
|
|
const float world_to_tex = 1/16.0f;
|
|
|
|
qvec3d texspace_pos = position * world_to_tex;
|
|
|
|
int coord_floor_x = static_cast<int>(floor(texspace_pos[0]));
|
|
int coord_floor_y = static_cast<int>(floor(texspace_pos[1]));
|
|
int coord_floor_z = static_cast<int>(floor(texspace_pos[2]));
|
|
|
|
float coord_frac_x = static_cast<float>(texspace_pos[0] - coord_floor_x);
|
|
float coord_frac_y = static_cast<float>(texspace_pos[1] - coord_floor_y);
|
|
float coord_frac_z = static_cast<float>(texspace_pos[2] - coord_floor_z);
|
|
|
|
assert(coord_frac_x >= 0 && coord_frac_x <= 1);
|
|
assert(coord_frac_y >= 0 && coord_frac_y <= 1);
|
|
assert(coord_frac_z >= 0 && coord_frac_z <= 1);
|
|
|
|
coord_floor_x = mod_round_to_neg_inf(coord_floor_x, mottle_texsize);
|
|
coord_floor_y = mod_round_to_neg_inf(coord_floor_y, mottle_texsize);
|
|
coord_floor_z = mod_round_to_neg_inf(coord_floor_z, mottle_texsize);
|
|
|
|
assert(coord_floor_x >= 0 && coord_floor_x < mottle_texsize);
|
|
assert(coord_floor_y >= 0 && coord_floor_y < mottle_texsize);
|
|
assert(coord_floor_z >= 0 && coord_floor_z < mottle_texsize);
|
|
|
|
// look up sample in the 3d texture at an integer coordinate
|
|
auto tex = [](int x, int y, int z) -> uint8_t {
|
|
int v;
|
|
v = MottlePermutation256[x % 256];
|
|
v = MottlePermutation256[(v + y) % 256];
|
|
v = MottlePermutation256[(v + z) % 256];
|
|
return v;
|
|
};
|
|
|
|
// 3D bilinear interpolation
|
|
float res = mix(
|
|
mix(
|
|
mix(tex(coord_floor_x, coord_floor_y, coord_floor_z),
|
|
tex(coord_floor_x + 1, coord_floor_y, coord_floor_z),
|
|
coord_frac_x),
|
|
mix(tex(coord_floor_x, coord_floor_y + 1, coord_floor_z),
|
|
tex(coord_floor_x + 1, coord_floor_y + 1, coord_floor_z),
|
|
coord_frac_x),
|
|
coord_frac_y),
|
|
mix(
|
|
mix(tex(coord_floor_x, coord_floor_y, coord_floor_z + 1),
|
|
tex(coord_floor_x + 1, coord_floor_y, coord_floor_z + 1),
|
|
coord_frac_x),
|
|
mix(tex(coord_floor_x, coord_floor_y + 1, coord_floor_z + 1),
|
|
tex(coord_floor_x + 1, coord_floor_y + 1, coord_floor_z + 1),
|
|
coord_frac_x),
|
|
coord_frac_y),
|
|
coord_frac_z);
|
|
|
|
return (res / 255.0f) * 48.0f;
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* ============
|
|
* LightFace_Min
|
|
* ============
|
|
*/
|
|
static void LightFace_Min(const mbsp_t *bsp, const mface_t *face, const qvec3d &color, vec_t light,
|
|
lightsurf_t *lightsurf, lightmapdict_t *lightmaps, int32_t style)
|
|
{
|
|
const settings::worldspawn_keys &cfg = *lightsurf->cfg;
|
|
|
|
const surfflags_t &extended_flags = extended_texinfo_flags[face->texinfo];
|
|
if (extended_flags.no_minlight) {
|
|
return; /* this face is excluded from minlight */
|
|
}
|
|
|
|
/* Find the lightmap */
|
|
lightmap_t *lightmap = Lightmap_ForStyle(lightmaps, style, lightsurf);
|
|
|
|
bool hit = false;
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
lightsample_t &sample = lightmap->samples[i];
|
|
|
|
vec_t value = light;
|
|
if (cfg.minlightDirt.value()) {
|
|
value *= Dirt_GetScaleFactor(cfg, lightsurf->occlusion[i], NULL, 0.0, lightsurf);
|
|
}
|
|
if (cfg.addminlight.value()) {
|
|
sample.color += color * (value / 255.0);
|
|
} else {
|
|
if (lightsurf->minlightMottle) {
|
|
value += Mottle(lightsurf->points[i]);
|
|
}
|
|
Light_ClampMin(sample, value, color);
|
|
}
|
|
|
|
hit = true;
|
|
}
|
|
|
|
if (hit) {
|
|
Lightmap_Save(lightmaps, lightsurf, lightmap, style);
|
|
}
|
|
}
|
|
|
|
static void LightFace_LocalMin(const mbsp_t *bsp, const mface_t *face,
|
|
lightsurf_t *lightsurf, lightmapdict_t *lightmaps)
|
|
{
|
|
const settings::worldspawn_keys &cfg = *lightsurf->cfg;
|
|
const modelinfo_t *modelinfo = lightsurf->modelinfo;
|
|
|
|
const surfflags_t &extended_flags = extended_texinfo_flags[face->texinfo];
|
|
if (extended_flags.no_minlight) {
|
|
return; /* this face is excluded from minlight */
|
|
}
|
|
|
|
// FIXME: Refactor this?
|
|
if (lightsurf->modelinfo->lightignore.value() || extended_flags.light_ignore)
|
|
return;
|
|
|
|
/* Cast rays for local minlight entities */
|
|
for (const auto &entity : GetLights()) {
|
|
if (entity->getFormula() != LF_LOCALMIN) {
|
|
continue;
|
|
}
|
|
if (entity->nostaticlight.value()) {
|
|
continue;
|
|
}
|
|
|
|
if (CullLight(entity.get(), lightsurf)) {
|
|
continue;
|
|
}
|
|
|
|
raystream_occlusion_t &rs = lightsurf->occlusion_stream;
|
|
rs.clearPushedRays();
|
|
|
|
lightmap_t *lightmap = Lightmap_ForStyle(lightmaps, entity->style.value(), lightsurf);
|
|
|
|
bool hit = false;
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
if (lightsurf->occluded[i])
|
|
continue;
|
|
|
|
const lightsample_t &sample = lightmap->samples[i];
|
|
const qvec3d &surfpoint = lightsurf->points[i];
|
|
if (cfg.addminlight.value() || LightSample_Brightness(sample.color) < entity->light.value()) {
|
|
qvec3d surfpointToLightDir;
|
|
const vec_t surfpointToLightDist = GetDir(surfpoint, entity->origin.value(), surfpointToLightDir);
|
|
|
|
rs.pushRay(i, surfpoint, surfpointToLightDir, surfpointToLightDist);
|
|
}
|
|
}
|
|
|
|
// local minlight just needs occlusion, not closest hit
|
|
rs.tracePushedRaysOcclusion(modelinfo, CHANNEL_MASK_DEFAULT);
|
|
total_light_rays += rs.numPushedRays();
|
|
|
|
const int N = rs.numPushedRays();
|
|
for (int j = 0; j < N; j++) {
|
|
if (rs.getPushedRayOccluded(j)) {
|
|
continue;
|
|
}
|
|
|
|
int i = rs.getPushedRayPointIndex(j);
|
|
vec_t value = entity->light.value();
|
|
lightsample_t &sample = lightmap->samples[i];
|
|
|
|
value *= Dirt_GetScaleFactor(
|
|
cfg, lightsurf->occlusion[i], entity.get(), 0.0 /* TODO: pass distance */, lightsurf);
|
|
if (cfg.addminlight.value()) {
|
|
sample.color += entity->color.value() * (value / 255.0);
|
|
} else {
|
|
Light_ClampMin(sample, value, entity->color.value());
|
|
}
|
|
|
|
hit = true;
|
|
total_light_ray_hits++;
|
|
}
|
|
|
|
if (hit) {
|
|
Lightmap_Save(lightmaps, lightsurf, lightmap, entity->style.value());
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* =============
|
|
* LightFace_DirtDebug
|
|
* =============
|
|
*/
|
|
static void LightFace_DirtDebug(const lightsurf_t *lightsurf, lightmapdict_t *lightmaps)
|
|
{
|
|
const settings::worldspawn_keys &cfg = *lightsurf->cfg;
|
|
/* use a style 0 light map */
|
|
lightmap_t *lightmap = Lightmap_ForStyle(lightmaps, 0, lightsurf);
|
|
|
|
/* Overwrite each point with the dirt value for that sample... */
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
lightsample_t &sample = lightmap->samples[i];
|
|
const float light = 255 * Dirt_GetScaleFactor(cfg, lightsurf->occlusion[i], nullptr, 0.0, lightsurf);
|
|
sample.color = {light};
|
|
}
|
|
|
|
Lightmap_Save(lightmaps, lightsurf, lightmap, 0);
|
|
}
|
|
|
|
/*
|
|
* =============
|
|
* LightFace_PhongDebug
|
|
* =============
|
|
*/
|
|
static void LightFace_PhongDebug(const lightsurf_t *lightsurf, lightmapdict_t *lightmaps)
|
|
{
|
|
/* use a style 0 light map */
|
|
lightmap_t *lightmap = Lightmap_ForStyle(lightmaps, 0, lightsurf);
|
|
|
|
/* Overwrite each point with the normal for that sample... */
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
lightsample_t &sample = lightmap->samples[i];
|
|
// scale from [-1..1] to [0..1], then multiply by 255
|
|
sample.color = lightsurf->normals[i];
|
|
|
|
for (auto &v : sample.color) {
|
|
v = abs(v) * 255;
|
|
}
|
|
}
|
|
|
|
Lightmap_Save(lightmaps, lightsurf, lightmap, 0);
|
|
}
|
|
|
|
static void LightFace_DebugMottle(const lightsurf_t *lightsurf, lightmapdict_t *lightmaps)
|
|
{
|
|
/* use a style 0 light map */
|
|
lightmap_t *lightmap = Lightmap_ForStyle(lightmaps, 0, lightsurf);
|
|
|
|
/* Overwrite each point with the mottle noise for that sample... */
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
lightsample_t &sample = lightmap->samples[i];
|
|
// mottle is meant to be applied on top of minlight, so add some here
|
|
// for preview purposes.
|
|
const float minlight = 20.0f;
|
|
sample.color = qvec3f(minlight + Mottle(lightsurf->points[i]));
|
|
}
|
|
|
|
Lightmap_Save(lightmaps, lightsurf, lightmap, 0);
|
|
}
|
|
|
|
// mxd. Surface light falloff. Returns color in [0,255]
|
|
inline qvec3f SurfaceLight_ColorAtDist(
|
|
const settings::worldspawn_keys &cfg, const float &surf_scale, const float &intensity, const qvec3d &color, const float &dist, const float &hotspot_clamp)
|
|
{
|
|
// Exponential falloff
|
|
const float d = max(dist, hotspot_clamp); // Clamp away hotspots, also avoid division by 0...
|
|
const float scaledintensity = intensity * surf_scale;
|
|
const float scale = (1.0f / (d * d));
|
|
|
|
return color * scaledintensity * scale;
|
|
}
|
|
|
|
// dir: vpl -> sample point direction
|
|
// mxd. returns color in [0,255]
|
|
inline qvec3f GetSurfaceLighting(const settings::worldspawn_keys &cfg, const surfacelight_t *vpl, const qvec3f &dir,
|
|
const float dist, const qvec3f &normal, const vec_t &standard_scale, const vec_t &sky_scale, const float &hotspot_clamp)
|
|
{
|
|
qvec3f result;
|
|
float dotProductFactor = 1.0f;
|
|
|
|
float dp1 = qv::dot(vpl->surfnormal, dir);
|
|
const qvec3f sp_vpl = dir * -1.0f;
|
|
float dp2 = qv::dot(sp_vpl, normal);
|
|
|
|
if (!vpl->omnidirectional) {
|
|
if (dp1 < -LIGHT_ANGLE_EPSILON)
|
|
return {0}; // sample point behind vpl
|
|
if (dp2 < -LIGHT_ANGLE_EPSILON)
|
|
return {0}; // vpl behind sample face
|
|
|
|
// Rescale a bit to brighten the faces nearly-perpendicular to the surface light plane...
|
|
if (vpl->rescale) {
|
|
dp1 = 0.5f + dp1 * 0.5f;
|
|
dp2 = 0.5f + dp2 * 0.5f;
|
|
}
|
|
|
|
dotProductFactor = dp1 * dp2;
|
|
} else {
|
|
// used for sky face surface lights
|
|
dotProductFactor = dp2;
|
|
}
|
|
|
|
// Get light contribution
|
|
result = SurfaceLight_ColorAtDist(cfg, vpl->omnidirectional ? sky_scale : standard_scale, vpl->intensity, vpl->color, dist, hotspot_clamp);
|
|
|
|
// Apply angle scale
|
|
const qvec3f resultscaled = result * dotProductFactor;
|
|
|
|
Q_assert(!std::isnan(resultscaled[0]) && !std::isnan(resultscaled[1]) && !std::isnan(resultscaled[2]));
|
|
return resultscaled;
|
|
}
|
|
|
|
static bool // mxd
|
|
SurfaceLight_SphereCull(const surfacelight_t *vpl, const lightsurf_t *lightsurf, const vec_t &bouncelight_gate, const float &hotspot_clamp)
|
|
{
|
|
if (light_options.visapprox.value() == visapprox_t::RAYS &&
|
|
vpl->bounds.disjoint(lightsurf->extents.bounds, 0.001)) {
|
|
return true;
|
|
}
|
|
|
|
const settings::worldspawn_keys &cfg = *lightsurf->cfg;
|
|
const qvec3f dir = qvec3f(lightsurf->extents.origin) - qvec3f(vpl->pos); // vpl -> sample point
|
|
const float dist = qv::length(dir) + lightsurf->extents.radius;
|
|
|
|
// Get light contribution
|
|
const qvec3f color = SurfaceLight_ColorAtDist(cfg, vpl->omnidirectional ? cfg.surflightskyscale.value() : cfg.surflightscale.value(), vpl->totalintensity, vpl->color, dist, hotspot_clamp);
|
|
|
|
return qv::gate(color, (float) bouncelight_gate);
|
|
}
|
|
|
|
static void // mxd
|
|
LightFace_SurfaceLight(const mbsp_t *bsp, lightsurf_t *lightsurf, lightmapdict_t *lightmaps, const std::vector<surfacelight_t> &surface_lights, const vec_t &standard_scale, const vec_t &sky_scale, const float &hotspot_clamp)
|
|
{
|
|
const settings::worldspawn_keys &cfg = *lightsurf->cfg;
|
|
const float surflight_gate = 0.01f;
|
|
|
|
// check lighting channels (currently surface lights are always on CHANNEL_MASK_DEFAULT)
|
|
if (!(lightsurf->modelinfo->object_channel_mask.value() & CHANNEL_MASK_DEFAULT)) {
|
|
return;
|
|
}
|
|
|
|
for (const surfacelight_t &vpl : surface_lights) {
|
|
if (SurfaceLight_SphereCull(&vpl, lightsurf, surflight_gate, hotspot_clamp))
|
|
continue;
|
|
|
|
raystream_occlusion_t &rs = lightsurf->occlusion_stream;
|
|
|
|
for (int c = 0; c < vpl.points.size(); c++) {
|
|
if (light_options.visapprox.value() == visapprox_t::VIS &&
|
|
VisCullEntity(bsp, lightsurf->pvs, vpl.leaves[c])) {
|
|
continue;
|
|
}
|
|
|
|
rs.clearPushedRays();
|
|
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
if (lightsurf->occluded[i])
|
|
continue;
|
|
|
|
const qvec3d &lightsurf_pos = lightsurf->points[i];
|
|
const qvec3d &lightsurf_normal = lightsurf->normals[i];
|
|
|
|
qvec3f pos = vpl.points[c];
|
|
qvec3f dir = lightsurf_pos - pos;
|
|
float dist = qv::length(dir);
|
|
|
|
if (dist == 0.0f)
|
|
dir = lightsurf_normal;
|
|
else
|
|
dir /= dist;
|
|
|
|
const qvec3f indirect = GetSurfaceLighting(cfg, &vpl, dir, dist, lightsurf_normal, standard_scale, sky_scale, hotspot_clamp);
|
|
if (!qv::gate(indirect, surflight_gate)) { // Each point contributes very little to the final result
|
|
rs.pushRay(i, pos, dir, dist, &indirect);
|
|
}
|
|
}
|
|
|
|
if (!rs.numPushedRays())
|
|
continue;
|
|
|
|
total_surflight_rays += rs.numPushedRays();
|
|
rs.tracePushedRaysOcclusion(lightsurf->modelinfo, CHANNEL_MASK_DEFAULT);
|
|
|
|
const int lightmapstyle = vpl.style;
|
|
lightmap_t *lightmap = Lightmap_ForStyle(lightmaps, lightmapstyle, lightsurf);
|
|
|
|
bool hit = false;
|
|
const int numrays = rs.numPushedRays();
|
|
for (int j = 0; j < numrays; j++) {
|
|
if (rs.getPushedRayOccluded(j))
|
|
continue;
|
|
|
|
const int i = rs.getPushedRayPointIndex(j);
|
|
qvec3f indirect = rs.getPushedRayColor(j);
|
|
|
|
Q_assert(!std::isnan(indirect[0]));
|
|
|
|
// Use dirt scaling on the surface lighting.
|
|
const vec_t dirtscale = Dirt_GetScaleFactor(cfg, lightsurf->occlusion[i], nullptr, 0.0, lightsurf);
|
|
indirect *= dirtscale;
|
|
|
|
lightsample_t &sample = lightmap->samples[i];
|
|
sample.color += indirect;
|
|
|
|
hit = true;
|
|
++total_surflight_ray_hits;
|
|
}
|
|
|
|
// If surface light contributed anything, save.
|
|
if (hit)
|
|
Lightmap_Save(lightmaps, lightsurf, lightmap, lightmapstyle);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void LightFace_OccludedDebug(lightsurf_t *lightsurf, lightmapdict_t *lightmaps)
|
|
{
|
|
Q_assert(light_options.debugmode == debugmodes::debugoccluded);
|
|
|
|
/* use a style 0 light map */
|
|
lightmap_t *lightmap = Lightmap_ForStyle(lightmaps, 0, lightsurf);
|
|
|
|
/* Overwrite each point, red=occluded, green=ok */
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
lightsample_t &sample = lightmap->samples[i];
|
|
if (lightsurf->occluded[i]) {
|
|
sample.color = {255, 0, 0};
|
|
} else {
|
|
sample.color = {0, 255, 0};
|
|
}
|
|
// N.B.: Mark it as un-occluded now, to disable special handling later in the -extra/-extra4 downscaling code
|
|
lightsurf->occluded[i] = false;
|
|
}
|
|
|
|
Lightmap_Save(lightmaps, lightsurf, lightmap, 0);
|
|
}
|
|
|
|
static void LightFace_DebugNeighbours(lightsurf_t *lightsurf, lightmapdict_t *lightmaps)
|
|
{
|
|
Q_assert(light_options.debugmode == debugmodes::debugneighbours);
|
|
|
|
/* use a style 0 light map */
|
|
lightmap_t *lightmap = Lightmap_ForStyle(lightmaps, 0, lightsurf);
|
|
|
|
// std::vector<neighbour_t> neighbours = NeighbouringFaces_new(lightsurf->bsp, BSP_GetFace(lightsurf->bsp,
|
|
// dump_facenum)); bool found = false; for (auto &f : neighbours) {
|
|
// if (f.face == lightsurf->face)
|
|
// found = true;
|
|
// }
|
|
|
|
bool has_sample_on_dumpface = false;
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
if (lightsurf->realfacenums[i] == dump_facenum) {
|
|
has_sample_on_dumpface = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Overwrite each point */
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
lightsample_t &sample = lightmap->samples[i];
|
|
const int &sample_face = lightsurf->realfacenums[i];
|
|
|
|
if (sample_face == dump_facenum) {
|
|
/* Red - the sample is on the selected face */
|
|
sample.color = {255, 0, 0};
|
|
} else if (has_sample_on_dumpface) {
|
|
/* Green - the face has some samples on the selected face */
|
|
sample.color = {0, 255, 0};
|
|
} else {
|
|
sample.color = {};
|
|
}
|
|
// N.B.: Mark it as un-occluded now, to disable special handling later in the -extra/-extra4 downscaling code
|
|
lightsurf->occluded[i] = false;
|
|
}
|
|
|
|
Lightmap_Save(lightmaps, lightsurf, lightmap, 0);
|
|
}
|
|
|
|
/* Dirtmapping borrowed from q3map2, originally by RaP7oR */
|
|
|
|
constexpr size_t DIRT_NUM_ANGLE_STEPS = 16;
|
|
constexpr size_t DIRT_NUM_ELEVATION_STEPS = 3;
|
|
constexpr size_t DIRT_NUM_VECTORS = (DIRT_NUM_ANGLE_STEPS * DIRT_NUM_ELEVATION_STEPS);
|
|
|
|
static qvec3d dirtVectors[DIRT_NUM_VECTORS];
|
|
int numDirtVectors = 0;
|
|
|
|
/*
|
|
* ============
|
|
* SetupDirt
|
|
*
|
|
* sets up dirtmap (ambient occlusion)
|
|
* ============
|
|
*/
|
|
void SetupDirt(settings::worldspawn_keys &cfg)
|
|
{
|
|
// check if needed
|
|
|
|
if (!cfg.globalDirt.value() && cfg.globalDirt.getSource() == settings::source::COMMANDLINE) {
|
|
// HACK: "-dirt 0" disables all dirtmapping even if we would otherwise use it.
|
|
dirt_in_use = false;
|
|
return;
|
|
}
|
|
|
|
if (cfg.globalDirt.value() || cfg.minlightDirt.value() || cfg.sunlight_dirt.boolValue() ||
|
|
cfg.sunlight2_dirt.boolValue()) {
|
|
dirt_in_use = true;
|
|
}
|
|
|
|
if (!dirt_in_use) {
|
|
// check entities, maybe only a few lights use it
|
|
for (const auto &light : GetLights()) {
|
|
if (light->dirt.boolValue()) {
|
|
dirt_in_use = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!dirt_in_use) {
|
|
// dirtmapping is not used by this map.
|
|
return;
|
|
}
|
|
|
|
/* note it */
|
|
logging::funcheader();
|
|
|
|
/* calculate angular steps */
|
|
constexpr float angleStep = (float)DEG2RAD(360.0f / DIRT_NUM_ANGLE_STEPS);
|
|
const float elevationStep = (float)DEG2RAD(cfg.dirtAngle.value() / DIRT_NUM_ELEVATION_STEPS);
|
|
|
|
/* iterate angle */
|
|
float angle = 0.0f;
|
|
numDirtVectors = 0;
|
|
for (int i = 0; i < DIRT_NUM_ANGLE_STEPS; i++, angle += angleStep) {
|
|
/* iterate elevation */
|
|
float elevation = elevationStep * 0.5f;
|
|
for (int j = 0; j < DIRT_NUM_ELEVATION_STEPS; j++, elevation += elevationStep) {
|
|
dirtVectors[numDirtVectors][0] = sin(elevation) * cos(angle);
|
|
dirtVectors[numDirtVectors][1] = sin(elevation) * sin(angle);
|
|
dirtVectors[numDirtVectors][2] = cos(elevation);
|
|
numDirtVectors++;
|
|
}
|
|
}
|
|
|
|
/* emit some statistics */
|
|
logging::print("{:9} dirtmap vectors\n", numDirtVectors);
|
|
}
|
|
|
|
// from q3map2
|
|
static void GetUpRtVecs(const qvec3d &normal, qvec3d &myUp, qvec3d &myRt)
|
|
{
|
|
/* check if the normal is aligned to the world-up */
|
|
if (normal[0] == 0.0f && normal[1] == 0.0f) {
|
|
if (normal[2] == 1.0f) {
|
|
myRt = {1.0, 0.0, 0.0};
|
|
myUp = {0.0, 1.0, 0.0};
|
|
} else if (normal[2] == -1.0f) {
|
|
myRt = {-1.0, 0.0, 0.0};
|
|
myUp = {0.0, 1.0, 0.0};
|
|
}
|
|
} else {
|
|
constexpr qvec3d worldUp{0, 0, 1};
|
|
myRt = qv::normalize(qv::cross(normal, worldUp));
|
|
myUp = qv::normalize(qv::cross(myRt, normal));
|
|
}
|
|
}
|
|
|
|
// from q3map2
|
|
inline qvec3d TransformToTangentSpace(
|
|
const qvec3d &normal, const qvec3d &myUp, const qvec3d &myRt, const qvec3d &inputvec)
|
|
{
|
|
return myRt * inputvec[0] + myUp * inputvec[1] + normal * inputvec[2];
|
|
}
|
|
|
|
// from q3map2
|
|
inline qvec3d GetDirtVector(const settings::worldspawn_keys &cfg, int i)
|
|
{
|
|
Q_assert(i < numDirtVectors);
|
|
|
|
if (cfg.dirtMode.value() == 1) {
|
|
/* get random vector */
|
|
float angle = Random() * DEG2RAD(360.0f);
|
|
float elevation = Random() * DEG2RAD(cfg.dirtAngle.value());
|
|
return {cos(angle) * sin(elevation), sin(angle) * sin(elevation), cos(elevation)};
|
|
}
|
|
|
|
return dirtVectors[i];
|
|
}
|
|
|
|
/*
|
|
* ============
|
|
* LightFace_CalculateDirt
|
|
* ============
|
|
*/
|
|
static void LightFace_CalculateDirt(lightsurf_t *lightsurf)
|
|
{
|
|
const settings::worldspawn_keys &cfg = *lightsurf->cfg;
|
|
|
|
Q_assert(dirt_in_use);
|
|
|
|
// batch implementation:
|
|
|
|
thread_local static std::vector<qvec3d> myUps, myRts;
|
|
|
|
myUps.resize(lightsurf->points.size());
|
|
myRts.resize(lightsurf->points.size());
|
|
|
|
// init
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
lightsurf->occlusion[i] = 0;
|
|
}
|
|
|
|
// this stuff is just per-point
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
GetUpRtVecs(lightsurf->normals[i], myUps[i], myRts[i]);
|
|
}
|
|
|
|
for (int j = 0; j < numDirtVectors; j++) {
|
|
raystream_intersection_t &rs = lightsurf->intersection_stream;
|
|
rs.clearPushedRays();
|
|
|
|
// fill in input buffers
|
|
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
if (lightsurf->occluded[i])
|
|
continue;
|
|
|
|
qvec3d dirtvec = GetDirtVector(cfg, j);
|
|
qvec3d dir = TransformToTangentSpace(lightsurf->normals[i], myUps[i], myRts[i], dirtvec);
|
|
|
|
rs.pushRay(i, lightsurf->points[i], dir, cfg.dirtDepth.value());
|
|
}
|
|
|
|
// trace the batch. need closest hit for dirt, so intersection.
|
|
//
|
|
// use the model's own channel mask as the shadow mask, e.g. so a model in channel 2's AO rays will only hit
|
|
// other things in channel 2
|
|
rs.tracePushedRaysIntersection(lightsurf->modelinfo, lightsurf->modelinfo->object_channel_mask.value());
|
|
|
|
// accumulate hitdists
|
|
for (int k = 0; k < rs.numPushedRays(); k++) {
|
|
const int i = rs.getPushedRayPointIndex(k);
|
|
if (rs.getPushedRayHitType(k) == hittype_t::SOLID) {
|
|
vec_t dist = rs.getPushedRayHitDist(k);
|
|
lightsurf->occlusion[i] += min(cfg.dirtDepth.value(), dist);
|
|
} else {
|
|
lightsurf->occlusion[i] += cfg.dirtDepth.value();
|
|
}
|
|
}
|
|
}
|
|
|
|
// process the results.
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
vec_t avgHitdist = lightsurf->occlusion[i] / (float)numDirtVectors;
|
|
lightsurf->occlusion[i] = 1 - (avgHitdist / cfg.dirtDepth.value());
|
|
}
|
|
}
|
|
|
|
// 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->points.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()) {
|
|
vec_t maxcolor = qv::max(color);
|
|
// FIXME: for colored lighting, this doesn't seem to generate the right values...
|
|
vec_t maxval = (lightsurf->maxlight ? lightsurf->maxlight : cfg.maxlight.value()) * 2.0;
|
|
|
|
if (maxcolor > maxval) {
|
|
color *= (maxval / maxcolor);
|
|
}
|
|
}
|
|
|
|
// color scaling
|
|
if (lightsurf->lightcolorscale != 1.0) {
|
|
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.0) {
|
|
for (auto &c : color) {
|
|
c = pow(c / 255.0f, 1.0 / cfg.lightmapgamma.value()) * 255.0f;
|
|
}
|
|
}
|
|
|
|
// clamp
|
|
// FIXME: should this be a brightness clamp?
|
|
vec_t maxcolor = qv::max(color);
|
|
|
|
if (maxcolor > 255.0) {
|
|
color *= (255.0 / 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->points.size(); j++) {
|
|
avgb += LightSample_Brightness(lm->samples[j].color);
|
|
}
|
|
avgb /= lightsurf->points.size();
|
|
return avgb;
|
|
}
|
|
|
|
static float Lightmap_MaxBrightness(const lightmap_t *lm, const lightsurf_t *lightsurf)
|
|
{
|
|
float maxb = 0;
|
|
for (int j = 0; j < lightsurf->points.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)
|
|
{
|
|
qfile_t file = SafeOpenWrite(fname);
|
|
|
|
// see: http://netpbm.sourceforge.net/doc/ppm.html
|
|
fmt::print(file.get(), "P6 {} {} 255 ", width, height);
|
|
int bytes = width * height * 3;
|
|
Q_assert(bytes == SafeWrite(file, rgbdata, bytes));
|
|
}
|
|
|
|
static void DumpFullSizeLightmap(const mbsp_t *bsp, const lightsurf_t *lightsurf)
|
|
{
|
|
const lightmap_t *lm = Lightmap_ForStyle_ReadOnly(lightsurf, 0);
|
|
if (lm != nullptr) {
|
|
int fnum = Face_GetNum(bsp, lightsurf->face);
|
|
|
|
std::vector<uint8_t> rgbdata;
|
|
for (int i = 0; i < lightsurf->numpoints; i++) {
|
|
const qvec3d &color = lm->samples[i].color;
|
|
for (int j = 0; j < 3; j++) {
|
|
int intval = static_cast<int>(clamp(color[j], 0.0, 255.0));
|
|
rgbdata.push_back(static_cast<uint8_t>(intval));
|
|
}
|
|
}
|
|
|
|
const int oversampled_width = (lightsurf->texsize[0] + 1) * settings::extra.value();
|
|
const int oversampled_height = (lightsurf->texsize[1] + 1) * settings::extra.value();
|
|
|
|
Q_assert(lightsurf->numpoints == (oversampled_height * oversampled_width));
|
|
|
|
WritePPM(fmt::format("face{:04}.ppm", fnum), oversampled_width, oversampled_height, rgbdata.data());
|
|
}
|
|
}
|
|
|
|
static void DumpGLMVector(std::string fname, std::vector<qvec3f> vec, int width, int height)
|
|
{
|
|
std::vector<uint8_t> rgbdata;
|
|
for (int y = 0; y < height; y++) {
|
|
for (int x = 0; x < width; x++) {
|
|
const qvec3f &sample = vec.at((y * width) + x);
|
|
for (int j = 0; j < 3; j++) {
|
|
int intval = static_cast<int>(clamp(sample[j], 0.0f, 255.0f));
|
|
rgbdata.push_back(static_cast<uint8_t>(intval));
|
|
}
|
|
}
|
|
}
|
|
Q_assert(rgbdata.size() == (width * height * 3));
|
|
WritePPM(fname, width, height, rgbdata.data());
|
|
}
|
|
|
|
static void DumpDownscaledLightmap(const mbsp_t *bsp, const mface_t *face, int w, int h, const qvec3d *colors)
|
|
{
|
|
int fnum = Face_GetNum(bsp, face);
|
|
|
|
std::vector<uint8_t> rgbdata;
|
|
for (int i = 0; i < (w * h); i++) {
|
|
for (int j = 0; j < 3; j++) {
|
|
int intval = static_cast<int>(clamp(colors[i][j], 0.0, 255.0));
|
|
rgbdata.push_back(static_cast<uint8_t>(intval));
|
|
}
|
|
}
|
|
|
|
WritePPM(fmt::format("face-small{:04}.ppm", fnum), w, h, rgbdata.data());
|
|
}
|
|
#endif
|
|
|
|
static std::vector<qvec4f> LightmapColorsToGLMVector(const lightsurf_t *lightsurf, const lightmap_t *lm)
|
|
{
|
|
std::vector<qvec4f> res;
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
const qvec3d &color = lm->samples[i].color;
|
|
const float alpha = lightsurf->occluded[i] ? 0.0f : 1.0f;
|
|
res.emplace_back(color[0], color[1], color[2], alpha);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
static std::vector<qvec4f> LightmapNormalsToGLMVector(const lightsurf_t *lightsurf, const lightmap_t *lm)
|
|
{
|
|
std::vector<qvec4f> res;
|
|
for (int i = 0; i < lightsurf->points.size(); i++) {
|
|
const qvec3d &color = lm->samples[i].direction;
|
|
const float alpha = lightsurf->occluded[i] ? 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<qvec4f> IntegerDownsampleImage(const std::vector<qvec4f> &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<qvec4f> res(static_cast<size_t>(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<qvec4f> FloodFillTransparent(const std::vector<qvec4f> &input, int w, int h)
|
|
{
|
|
// transparent pixels take the average of their neighbours.
|
|
|
|
std::vector<qvec4f> 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<qvec4f> HighlightSeams(const std::vector<qvec4f> &input, int w, int h)
|
|
{
|
|
std::vector<qvec4f> 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<qvec4f> BoxBlurImage(const std::vector<qvec4f> &input, int w, int h, int radius)
|
|
{
|
|
std::vector<qvec4f> 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 = clamp(x + x0, 0, w - 1);
|
|
const int y1 = 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;
|
|
}
|
|
|
|
bool Face_IsLightmapped(const mbsp_t *bsp, const mface_t *face)
|
|
{
|
|
const mtexinfo_t *texinfo = Face_Texinfo(bsp, face);
|
|
|
|
if (texinfo == nullptr)
|
|
return false;
|
|
|
|
// Q2RTX should light nodraw faces
|
|
if (light_options.q2rtx.value() && (texinfo->flags.native & Q2_SURF_NODRAW)) {
|
|
return true;
|
|
}
|
|
|
|
return bsp->loadversion->game->surf_is_lightmapped(texinfo->flags);
|
|
}
|
|
|
|
/**
|
|
* - 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<qvec4f> 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<qvec4f> output_color =
|
|
IntegerDownsampleImage(fullres, oversampled_width, oversampled_height, light_options.extra.value());
|
|
std::optional<std::vector<qvec4f>> 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 = max({color[0], color[1], color[2]});
|
|
if (light < 0)
|
|
light = 0;
|
|
if (light > 255)
|
|
light = 255;
|
|
*out++ = light;
|
|
}
|
|
}
|
|
|
|
if (lux) {
|
|
qvec3d direction = output_dir->at(sampleindex).xyz();
|
|
qvec3d 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<qvec4f> 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 = clamp(x, 0, lightsurf->width - 1);
|
|
const int y_clamped = 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 = 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<std::pair<float, const lightmap_t *>> 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->points[0]);
|
|
} else {
|
|
logging::print(
|
|
"WARNING: a face has exceeded max light style id ({}). Use -verbose to find which faces.\n",
|
|
maxstyle, lightsurf->points[0]);
|
|
}
|
|
warned_about_light_style_overflow = true;
|
|
}
|
|
logging::print(logging::flag::VERBOSE, "WARNING: Style {} too high on face near {}\n", lightmap.style,
|
|
lightsurf->points[0]);
|
|
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;
|
|
std::fill(lightsurf->occluded.begin(), lightsurf->occluded.end(), 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<const lightmap_t *> 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->points[0]);
|
|
} else {
|
|
logging::print(
|
|
"WARNING: a face has exceeded max light styles ({}). Use -verbose to find which faces.\n",
|
|
maxfstyles, lightsurf->points[0]);
|
|
}
|
|
warned_about_light_map_overflow = true;
|
|
}
|
|
logging::print(logging::flag::VERBOSE,
|
|
"WARNING: {} light styles (max {}) on face near {}; styles: ", sortable.size(), maxfstyles,
|
|
lightsurf->points[0]);
|
|
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<int>(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
|
|
{
|
|
const char *texname = Face_TextureName(bsp, face);
|
|
Q_assert(Face_IsLightmapped(bsp, face));
|
|
Q_assert(Q_strcasecmp(texname, "skip") != 0);
|
|
Q_assert(Q_strcasecmp(texname, "trigger") != 0);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<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)
|
|
{
|
|
/* Find the correct model offset */
|
|
const modelinfo_t *modelinfo = ModelInfoForFace(bsp, Face_GetNum(bsp, face));
|
|
if (modelinfo == nullptr) {
|
|
return nullptr;
|
|
}
|
|
|
|
/* don't bother with degenerate faces */
|
|
if (face->numedges < 3)
|
|
return nullptr;
|
|
|
|
if (!Face_IsLightmapped(bsp, face))
|
|
return nullptr;
|
|
|
|
const char *texname = Face_TextureName(bsp, face);
|
|
|
|
/* don't save lightmaps for "trigger" texture */
|
|
if (!Q_strcasecmp(texname, "trigger"))
|
|
return nullptr;
|
|
|
|
/* don't save lightmaps for "skip" texture */
|
|
if (!Q_strcasecmp(texname, "skip"))
|
|
return nullptr;
|
|
|
|
return Lightsurf_Init(modelinfo, cfg, face, bsp, facesup, facesup_decoupled);
|
|
}
|
|
|
|
/*
|
|
* ============
|
|
* LightFace
|
|
* ============
|
|
*/
|
|
void DirectLightFace(const mbsp_t *bsp, lightsurf_t &lightsurf, const settings::worldspawn_keys &cfg)
|
|
{
|
|
auto face = lightsurf.face;
|
|
const modelinfo_t *modelinfo = ModelInfoForFace(bsp, Face_GetNum(bsp, face));
|
|
|
|
lightmapdict_t *lightmaps = &lightsurf.lightmapsByStyle;
|
|
|
|
/* calculate dirt (ambient occlusion) but don't use it yet */
|
|
if (dirt_in_use && (light_options.debugmode != debugmodes::phong))
|
|
LightFace_CalculateDirt(&lightsurf);
|
|
|
|
/*
|
|
* The lighting procedure is: cast all positive lights, fix
|
|
* minlight levels, then cast all negative lights. Finally, we
|
|
* clamp any values that may have gone negative.
|
|
*/
|
|
|
|
if (light_options.debugmode == debugmodes::none) {
|
|
|
|
total_samplepoints += lightsurf.points.size();
|
|
|
|
const surfflags_t &extended_flags = extended_texinfo_flags[face->texinfo];
|
|
|
|
/* positive lights */
|
|
if (!(modelinfo->lightignore.value() || extended_flags.light_ignore)) {
|
|
for (const auto &entity : GetLights()) {
|
|
if (entity->getFormula() == LF_LOCALMIN)
|
|
continue;
|
|
if (entity->nostaticlight.value())
|
|
continue;
|
|
if (entity->light.value() > 0)
|
|
LightFace_Entity(bsp, entity.get(), &lightsurf, lightmaps);
|
|
}
|
|
for (const sun_t &sun : GetSuns())
|
|
if (sun.sunlight > 0)
|
|
LightFace_Sky(&sun, &lightsurf, lightmaps);
|
|
|
|
// mxd. Add surface lights...
|
|
// FIXME: negative surface lights
|
|
LightFace_SurfaceLight(bsp, &lightsurf, lightmaps, GetSurfaceLights(), cfg.surflightscale.value(), cfg.surflightskyscale.value(), 16.0f);
|
|
}
|
|
|
|
float minlight = 0;
|
|
qvec3d minlight_color;
|
|
|
|
// first, check for global minlight; this only affects style 0
|
|
if (lightsurf.minlight > cfg.minlight.value()) {
|
|
minlight = lightsurf.minlight;
|
|
minlight_color = lightsurf.minlight_color;
|
|
} else {
|
|
minlight = cfg.minlight.value();
|
|
minlight_color = cfg.minlight_color.value();
|
|
}
|
|
|
|
if (minlight) {
|
|
LightFace_Min(bsp, face, minlight_color, minlight, &lightsurf, lightmaps, 0);
|
|
}
|
|
|
|
if (auto value = IsSurfaceLitFace(bsp, face)) {
|
|
auto *entity = std::get<3>(value.value());
|
|
const float surface_minlight_scale = entity ? entity->surface_minlight_scale.value() : 64.f;
|
|
minlight = std::get<0>(value.value()) * surface_minlight_scale;
|
|
minlight_color = std::get<2>(value.value());
|
|
LightFace_Min(bsp, face, minlight_color, minlight, &lightsurf, lightmaps, std::get<1>(value.value()));
|
|
}
|
|
|
|
LightFace_LocalMin(bsp, face, &lightsurf, lightmaps);
|
|
|
|
/* negative lights */
|
|
if (!(modelinfo->lightignore.value() || extended_flags.light_ignore)) {
|
|
for (const auto &entity : GetLights()) {
|
|
if (entity->getFormula() == LF_LOCALMIN)
|
|
continue;
|
|
if (entity->nostaticlight.value())
|
|
continue;
|
|
if (entity->light.value() < 0)
|
|
LightFace_Entity(bsp, entity.get(), &lightsurf, lightmaps);
|
|
}
|
|
for (const sun_t &sun : GetSuns())
|
|
if (sun.sunlight < 0)
|
|
LightFace_Sky(&sun, &lightsurf, lightmaps);
|
|
}
|
|
}
|
|
|
|
/* replace lightmaps with AO for debugging */
|
|
if (light_options.debugmode == debugmodes::dirt)
|
|
LightFace_DirtDebug(&lightsurf, lightmaps);
|
|
|
|
if (light_options.debugmode == debugmodes::phong)
|
|
LightFace_PhongDebug(&lightsurf, lightmaps);
|
|
|
|
if (light_options.debugmode == debugmodes::debugoccluded)
|
|
LightFace_OccludedDebug(&lightsurf, lightmaps);
|
|
|
|
if (light_options.debugmode == debugmodes::debugneighbours)
|
|
LightFace_DebugNeighbours(&lightsurf, lightmaps);
|
|
|
|
if (light_options.debugmode == debugmodes::mottle)
|
|
LightFace_DebugMottle(&lightsurf, lightmaps);
|
|
}
|
|
|
|
/*
|
|
* ============
|
|
* LightFace
|
|
* ============
|
|
*/
|
|
void IndirectLightFace(const mbsp_t *bsp, lightsurf_t &lightsurf, const settings::worldspawn_keys &cfg)
|
|
{
|
|
auto face = lightsurf.face;
|
|
const modelinfo_t *modelinfo = ModelInfoForFace(bsp, Face_GetNum(bsp, face));
|
|
lightmapdict_t *lightmaps = &lightsurf.lightmapsByStyle;
|
|
|
|
if (light_options.debugmode == debugmodes::none) {
|
|
const surfflags_t &extended_flags = extended_texinfo_flags[face->texinfo];
|
|
|
|
/* positive lights */
|
|
if (!(modelinfo->lightignore.value() || extended_flags.light_ignore)) {
|
|
|
|
/* add bounce lighting */
|
|
// note: scale here is just to keep it close-ish to the old code
|
|
LightFace_SurfaceLight(bsp, &lightsurf, lightmaps, BounceLights(), cfg.bouncescale.value() * 0.5, cfg.bouncescale.value(), 128.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ResetLtFace()
|
|
{
|
|
total_light_rays = 0;
|
|
total_light_ray_hits = 0;
|
|
total_samplepoints = 0;
|
|
|
|
total_bounce_rays = 0;
|
|
total_bounce_ray_hits = 0;
|
|
total_surflight_rays = 0;
|
|
total_surflight_ray_hits = 0;
|
|
|
|
fully_transparent_lightmaps = 0;
|
|
|
|
warned_about_light_map_overflow = false;
|
|
warned_about_light_style_overflow = false;
|
|
}
|