ericw-tools/light/trace_embree.cc

677 lines
22 KiB
C++

/* Copyright (C) 2016 Eric Wasylishen
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/bounce.hh>
#include <light/trace_embree.hh>
#include <light/ltface.hh>
#include <common/bsputils.hh>
#include <common/polylib.hh>
#include <vector>
#include <cassert>
#include <climits>
#include <cstdlib>
#include <limits>
using namespace std;
using namespace polylib;
sceneinfo skygeom; // sky. always occludes.
sceneinfo solidgeom; // solids. always occludes.
sceneinfo filtergeom; // conditional occluders.. needs to run ray intersection filter
/**
* Returns 1.0 unless a custom alpha value is set.
* The priority is: "_light_alpha" (read from extended_texinfo_flags), then "alpha"
*/
static float Face_Alpha(const modelinfo_t *modelinfo, const mface_t *face)
{
const surfflags_t &extended_flags = extended_texinfo_flags[face->texinfo];
// for _light_alpha, 0 is considered unset
if (extended_flags.light_alpha) {
return extended_flags.light_alpha;
}
// next check modelinfo alpha (defaults to 1.0)
return modelinfo->alpha.value();
}
sceneinfo CreateGeometry(
const mbsp_t *bsp, RTCDevice g_device, RTCScene scene, const std::vector<const mface_t *> &faces)
{
// count triangles
int numtris = 0;
for (const mface_t *face : faces) {
if (face->numedges < 3)
continue;
numtris += (face->numedges - 2);
}
unsigned int geomID;
RTCGeometry geom_0 = rtcNewGeometry(g_device, RTC_GEOMETRY_TYPE_TRIANGLE);
// we're not using masks, but they need to be set to something or else all rays miss
// if embree is compiled with them
rtcSetGeometryMask(geom_0, 1);
rtcSetGeometryBuildQuality(geom_0, RTC_BUILD_QUALITY_MEDIUM);
rtcSetGeometryTimeStepCount(geom_0, 1);
geomID = rtcAttachGeometry(scene, geom_0);
rtcReleaseGeometry(geom_0);
struct Vertex
{
float point[4];
}; // 4th element is padding
struct Triangle
{
int v0, v1, v2;
};
// fill in vertices
Vertex *vertices = (Vertex *)rtcSetNewGeometryBuffer(
geom_0, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3, 4 * sizeof(float), bsp->dvertexes.size());
size_t i = 0;
for (auto &dvertex : bsp->dvertexes) {
Vertex *vert = &vertices[i++];
for (int j = 0; j < 3; j++) {
vert->point[j] = dvertex[j];
}
}
sceneinfo s;
s.geomID = geomID;
// fill in triangles
Triangle *triangles = (Triangle *)rtcSetNewGeometryBuffer(
geom_0, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, 3 * sizeof(int), numtris);
int tri_index = 0;
for (const mface_t *face : faces) {
if (face->numedges < 3)
continue;
// NOTE: can be null for "skip" faces
const modelinfo_t *modelinfo = ModelInfoForFace(bsp, Face_GetNum(bsp, face));
for (int j = 2; j < face->numedges; j++) {
Triangle *tri = &triangles[tri_index];
tri->v0 = Face_VertexAtIndex(bsp, face, j - 1);
tri->v1 = Face_VertexAtIndex(bsp, face, j);
tri->v2 = Face_VertexAtIndex(bsp, face, 0);
tri_index++;
triinfo info;
info.face = face;
info.modelinfo = modelinfo;
info.texinfo = &bsp->texinfo[face->texinfo];
info.texture = Face_Texture(bsp, face);
info.shadowworldonly = modelinfo->shadowworldonly.boolValue();
info.shadowself = modelinfo->shadowself.boolValue();
info.switchableshadow = modelinfo->switchableshadow.boolValue();
info.switchshadstyle = modelinfo->switchshadstyle.value();
info.alpha = Face_Alpha(modelinfo, face);
// mxd
if (bsp->loadversion->game->id == GAME_QUAKE_II) {
const int surf_flags = Face_ContentsOrSurfaceFlags(bsp, face);
info.is_fence =
((surf_flags & Q2_SURF_TRANSLUCENT) ==
Q2_SURF_TRANSLUCENT); // KMQuake 2-specific. Use texture alpha chanel when both flags are set.
info.is_glass = !info.is_fence && (surf_flags & Q2_SURF_TRANSLUCENT);
if (info.is_glass) {
info.alpha = (surf_flags & Q2_SURF_TRANS33 ? 0.33f : 0.66f);
}
} else {
const char *name = Face_TextureName(bsp, face);
info.is_fence = (name[0] == '{');
info.is_glass = (info.alpha < 1.0f);
}
s.triInfo.push_back(info);
}
}
rtcCommitGeometry(geom_0);
return s;
}
static void CreateGeometryFromWindings(RTCDevice g_device, RTCScene scene, const std::vector<winding_t> &windings)
{
if (windings.empty())
return;
// count triangles
int numtris = 0;
int numverts = 0;
for (const auto &winding : windings) {
Q_assert(winding.size() >= 3);
numtris += (winding.size() - 2);
numverts += winding.size();
}
unsigned int geomID;
RTCGeometry geom_1 = rtcNewGeometry(g_device, RTC_GEOMETRY_TYPE_TRIANGLE);
rtcSetGeometryBuildQuality(geom_1, RTC_BUILD_QUALITY_MEDIUM);
rtcSetGeometryMask(geom_1, 1);
rtcSetGeometryTimeStepCount(geom_1, 1);
geomID = rtcAttachGeometry(scene, geom_1);
rtcReleaseGeometry(geom_1);
struct Vertex
{
float point[4];
}; // 4th element is padding
struct Triangle
{
int v0, v1, v2;
};
// fill in vertices
Vertex *vertices = (Vertex *)rtcSetNewGeometryBuffer(
geom_1, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3, 4 * sizeof(float), numverts);
{
int vert_index = 0;
for (const auto &winding : windings) {
for (int j = 0; j < winding.size(); j++) {
for (int k = 0; k < 3; k++) {
vertices[vert_index + j].point[k] = winding.at(j)[k];
}
}
vert_index += winding.size();
}
}
// fill in triangles
Triangle *triangles = (Triangle *)rtcSetNewGeometryBuffer(
geom_1, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, 3 * sizeof(int), numtris);
int tri_index = 0;
int vert_index = 0;
for (const auto &winding : windings) {
for (int j = 2; j < winding.size(); j++) {
Triangle *tri = &triangles[tri_index];
tri->v0 = vert_index + (j - 1);
tri->v1 = vert_index + j;
tri->v2 = vert_index + 0;
tri_index++;
}
vert_index += winding.size();
}
Q_assert(vert_index == numverts);
Q_assert(tri_index == numtris);
rtcCommitGeometry(geom_1);
}
RTCDevice device;
RTCScene scene;
static const mbsp_t *bsp_static;
void ErrorCallback(void *userptr, const RTCError code, const char *str)
{
fmt::print("RTC Error {}: {}\n", code, str);
}
const triinfo &Embree_LookupTriangleInfo(unsigned int geomID, unsigned int primID)
{
const sceneinfo &info = Embree_SceneinfoForGeomID(geomID);
return info.triInfo.at(primID);
}
inline qvec3f Embree_RayEndpoint(RTCRayN *ray, const qvec3f &dir, size_t N, size_t i)
{
qvec3f org{RTCRayN_org_x(ray, N, i), RTCRayN_org_y(ray, N, i), RTCRayN_org_z(ray, N, i)};
float &tfar = RTCRayN_tfar(ray, N, i);
return org + (dir * tfar);
}
enum class filtertype_t
{
INTERSECTION,
OCCLUSION
};
static void AddGlassToRay(RTCIntersectContext *context, unsigned rayIndex, float opacity, const qvec3d &glasscolor);
static void AddDynamicOccluderToRay(RTCIntersectContext *context, unsigned rayIndex, int style);
// called to evaluate transparency
template<filtertype_t filtertype>
static void Embree_FilterFuncN(const struct RTCFilterFunctionNArguments *args)
{
int *const valid = args->valid;
RTCIntersectContext *const context = args->context;
struct RTCRayN *const ray = args->ray;
struct RTCHitN *const potentialHit = args->hit;
const unsigned int N = args->N;
const int VALID = -1;
const int INVALID = 0;
const ray_source_info *rsi = static_cast<const ray_source_info *>(context);
for (size_t i = 0; i < N; i++) {
if (valid[i] != VALID) {
// we only need to handle valid rays
continue;
}
const unsigned &rayID = RTCRayN_id(ray, N, i);
const unsigned &geomID = RTCHitN_geomID(potentialHit, N, i);
const unsigned &primID = RTCHitN_primID(potentialHit, N, i);
// unpack ray index
const unsigned rayIndex = rayID;
const modelinfo_t *source_modelinfo = rsi->self;
const triinfo &hit_triinfo = Embree_LookupTriangleInfo(geomID, primID);
if (!hit_triinfo.modelinfo) {
// we hit a "skip" face with no associated model
// reject hit (???)
valid[i] = INVALID;
continue;
}
if (hit_triinfo.shadowworldonly) {
// we hit "_shadowworldonly" "1" geometry. Ignore the hit unless we are from world.
if (!source_modelinfo || !source_modelinfo->isWorld()) {
// reject hit
valid[i] = INVALID;
continue;
}
}
if (hit_triinfo.shadowself) {
// only casts shadows on itself
if (source_modelinfo != hit_triinfo.modelinfo) {
// reject hit
valid[i] = INVALID;
continue;
}
}
if (hit_triinfo.switchableshadow) {
// we hit a dynamic shadow caster. reject the hit, but store the
// info about what we hit.
const int style = hit_triinfo.switchshadstyle;
AddDynamicOccluderToRay(context, rayIndex, style);
// reject hit
valid[i] = INVALID;
continue;
}
float alpha = hit_triinfo.alpha;
// test fence textures and glass
if (hit_triinfo.is_fence || hit_triinfo.is_glass) {
qvec3f rayDir =
qv::normalize(qvec3f{RTCRayN_dir_x(ray, N, i), RTCRayN_dir_y(ray, N, i), RTCRayN_dir_z(ray, N, i)});
qvec3f hitpoint = Embree_RayEndpoint(ray, rayDir, N, i);
const qvec4b sample = SampleTexture(hit_triinfo.face, hit_triinfo.texinfo, hit_triinfo.texture, bsp_static,
hitpoint); // mxd. Palette index -> color_rgba
if (hit_triinfo.is_glass) {
// hit glass...
// mxd. Adjust alpha by texture alpha?
if (sample[3] < 255)
alpha = sample[3] / 255.0f;
qvec3f potentialHitGeometryNormal = qv::normalize(qvec3f{RTCHitN_Ng_x(potentialHit, N, i),
RTCHitN_Ng_y(potentialHit, N, i), RTCHitN_Ng_z(potentialHit, N, i)});
const float raySurfaceCosAngle = qv::dot(rayDir, potentialHitGeometryNormal);
// only pick up the color of the glass on the _exiting_ side of the glass.
// (we currently trace "backwards", from surface point --> light source)
if (raySurfaceCosAngle < 0) {
AddGlassToRay(context, rayIndex, alpha, sample.xyz() * (1.0 / 255.0));
}
// reject hit
valid[i] = INVALID;
continue;
}
if (hit_triinfo.is_fence) {
if (sample[3] < 255) {
// reject hit
valid[i] = INVALID;
continue;
}
}
}
// accept hit
// (just need to leave the `valid` value set to VALID)
}
}
// building faces for skip-textured bmodels
qplane3d Node_Plane(const mbsp_t *bsp, const bsp2_dnode_t *node, bool side)
{
qplane3d plane = bsp->dplanes[node->planenum];
if (side) {
return -plane;
}
return plane;
}
/**
* `planes` all of the node planes that bound this leaf, facing inward.
*/
static void Leaf_MakeFaces(
const mbsp_t *bsp, const mleaf_t *leaf, const std::vector<qplane3d> &planes, std::vector<winding_t> &result)
{
for (const qplane3d &plane : planes) {
// flip the inward-facing split plane to get the outward-facing plane of the face we're constructing
qplane3d faceplane = -plane;
std::optional<winding_t> winding = winding_t::from_plane(faceplane, 10e6);
// clip `winding` by all of the other planes
for (const qplane3d &plane2 : planes) {
if (&plane2 == &plane)
continue;
auto clipped = winding->clip(plane2);
// discard the back, continue clipping the front part
winding = clipped[0];
// check if everything was clipped away
if (!winding)
break;
}
if (winding) {
// logging::print("WARNING: winding clipped away\n");
} else {
result.push_back(std::move(*winding));
}
}
}
void MakeFaces_r(const mbsp_t *bsp, const int nodenum, std::vector<qplane3d> *planes, std::vector<winding_t> &result)
{
if (nodenum < 0) {
const int leafnum = -nodenum - 1;
const mleaf_t *leaf = &bsp->dleafs[leafnum];
if ((bsp->loadversion->game->id == GAME_QUAKE_II) ? (leaf->contents & Q2_CONTENTS_SOLID)
: leaf->contents == CONTENTS_SOLID) {
Leaf_MakeFaces(bsp, leaf, *planes, result);
}
return;
}
const bsp2_dnode_t *node = &bsp->dnodes[nodenum];
// go down the front side
planes->push_back(Node_Plane(bsp, node, false));
MakeFaces_r(bsp, node->children[0], planes, result);
planes->pop_back();
// go down the back side
planes->push_back(Node_Plane(bsp, node, true));
MakeFaces_r(bsp, node->children[1], planes, result);
planes->pop_back();
}
static void MakeFaces(const mbsp_t *bsp, const dmodelh2_t *model, std::vector<winding_t> &result)
{
std::vector<qplane3d> planes;
MakeFaces_r(bsp, model->headnode[0], &planes, result);
Q_assert(planes.empty());
}
void Embree_TraceInit(const mbsp_t *bsp)
{
bsp_static = bsp;
Q_assert(device == nullptr);
std::vector<const mface_t *> skyfaces, solidfaces, filterfaces;
// check all modelinfos
for (size_t mi = 0; mi < bsp->dmodels.size(); mi++) {
const modelinfo_t *model = ModelInfoForModel(bsp, mi);
const bool isWorld = model->isWorld();
const bool shadow = model->shadow.boolValue();
const bool shadowself = model->shadowself.boolValue();
const bool shadowworldonly = model->shadowworldonly.boolValue();
const bool switchableshadow = model->switchableshadow.boolValue();
if (!(isWorld || shadow || shadowself || shadowworldonly || switchableshadow))
continue;
for (int i = 0; i < model->model->numfaces; i++) {
const mface_t *face = BSP_GetFace(bsp, model->model->firstface + i);
// check for TEX_NOSHADOW
const surfflags_t &extended_flags = extended_texinfo_flags[face->texinfo];
if (extended_flags.no_shadow)
continue;
// handle switchableshadow
if (switchableshadow) {
filterfaces.push_back(face);
continue;
}
const int contents_or_surf_flags = Face_ContentsOrSurfaceFlags(bsp, face); // mxd
const mtexinfo_t *texinfo = Face_Texinfo(bsp, face);
const bool is_q2 = bsp->loadversion->game->id == GAME_QUAKE_II;
// mxd. Skip NODRAW faces, but not SKY ones (Q2's sky01.wal has both flags set)
if (is_q2 && (contents_or_surf_flags & Q2_SURF_NODRAW) && !(contents_or_surf_flags & Q2_SURF_SKY))
continue;
// handle glass / water
const float alpha = Face_Alpha(model, face);
if (alpha < 1.0f ||
(is_q2 && (contents_or_surf_flags & Q2_SURF_TRANSLUCENT))) { // mxd. Both fence and transparent textures
// are done using SURF_TRANS flags in Q2
filterfaces.push_back(face);
continue;
}
// fence
const char *texname = Face_TextureName(bsp, face);
if (texname[0] == '{') {
filterfaces.push_back(face);
continue;
}
// handle sky
if (is_q2) {
// Q2: arghrad compat: sky faces only emit sunlight if:
// sky flag set, light flag set, value nonzero
if ((contents_or_surf_flags & Q2_SURF_SKY) != 0 &&
(!light_options.arghradcompat.value() ||
((contents_or_surf_flags & Q2_SURF_LIGHT) != 0 && texinfo->value != 0))) {
skyfaces.push_back(face);
continue;
}
} else {
// Q1
if (!Q_strncasecmp("sky", texname, 3)) {
skyfaces.push_back(face);
continue;
}
}
// liquids
if (/* texname[0] == '*' */ ContentsOrSurfaceFlags_IsTranslucent(bsp, contents_or_surf_flags)) { // mxd
if (!isWorld) {
// world liquids never cast shadows; shadow casting bmodel liquids do
solidfaces.push_back(face);
}
continue;
}
// solid faces
if (isWorld || shadow) {
solidfaces.push_back(face);
} else {
// shadowself or shadowworldonly
Q_assert(shadowself || shadowworldonly);
filterfaces.push_back(face);
}
}
}
/* Special handling of skip-textured bmodels */
std::vector<winding_t> skipwindings;
for (const modelinfo_t *model : tracelist) {
if (model->model->numfaces == 0) {
MakeFaces(bsp, model->model, skipwindings);
}
}
device = rtcNewDevice(NULL);
rtcSetDeviceErrorFunction(
device, ErrorCallback, nullptr); // mxd. Changed from rtcDeviceSetErrorFunction to silence compiler warning...
// log version
const size_t ver_maj = rtcGetDeviceProperty(device, RTC_DEVICE_PROPERTY_VERSION_MAJOR);
const size_t ver_min = rtcGetDeviceProperty(device, RTC_DEVICE_PROPERTY_VERSION_MINOR);
const size_t ver_pat = rtcGetDeviceProperty(device, RTC_DEVICE_PROPERTY_VERSION_PATCH);
logging::funcprint("Embree version: {}.{}.{}\n", ver_maj, ver_min, ver_pat);
scene = rtcNewScene(device);
rtcSetSceneFlags(scene, RTC_SCENE_FLAG_NONE);
rtcSetSceneBuildQuality(scene, RTC_BUILD_QUALITY_HIGH);
skygeom = CreateGeometry(bsp, device, scene, skyfaces);
solidgeom = CreateGeometry(bsp, device, scene, solidfaces);
filtergeom = CreateGeometry(bsp, device, scene, filterfaces);
CreateGeometryFromWindings(device, scene, skipwindings);
rtcSetGeometryIntersectFilterFunction(
rtcGetGeometry(scene, filtergeom.geomID), Embree_FilterFuncN<filtertype_t::INTERSECTION>);
rtcSetGeometryOccludedFilterFunction(
rtcGetGeometry(scene, filtergeom.geomID), Embree_FilterFuncN<filtertype_t::OCCLUSION>);
rtcCommitScene(scene);
logging::funcprint("\n");
logging::print("\t{} sky faces\n", skyfaces.size());
logging::print("\t{} solid faces\n", solidfaces.size());
logging::print("\t{} filtered faces\n", filterfaces.size());
logging::print("\t{} shadow-casting skip faces\n", skipwindings.size());
}
static RTCRayHit SetupRay_StartStop(const qvec3d &start, const qvec3d &stop)
{
qvec3d dir = stop - start;
vec_t dist = qv::normalizeInPlace(dir);
return SetupRay(0, start, dir, dist);
}
// public
hitresult_t TestLight(const qvec3d &start, const qvec3d &stop, const modelinfo_t *self)
{
RTCRay ray = SetupRay_StartStop(start, stop).ray;
ray_source_info ctx2(nullptr, self);
rtcOccluded1(scene, &ctx2, &ray);
if (ray.tfar < 0.0f)
return {false, 0}; // fully occluded
// no obstruction (or a switchable shadow obstruction only)
return {true, ctx2.singleRayShadowStyle};
}
// public
hitresult_t TestSky(const qvec3d &start, const qvec3d &dirn, const modelinfo_t *self, const mface_t **face_out)
{
// trace from the sample point towards the sun, and
// return true if we hit a sky poly.
qvec3d dir_normalized = qv::normalize(dirn);
RTCRayHit ray = SetupRay(0, start, dir_normalized, MAX_SKY_DIST);
ray_source_info ctx2(nullptr, self);
rtcIntersect1(scene, &ctx2, &ray);
bool hit_sky = (ray.hit.geomID == skygeom.geomID);
if (face_out) {
if (hit_sky) {
const sceneinfo &si = Embree_SceneinfoForGeomID(ray.hit.geomID);
*face_out = si.triInfo.at(ray.hit.primID).face;
} else {
*face_out = nullptr;
}
}
return {hit_sky, ctx2.singleRayShadowStyle};
}
static void AddGlassToRay(RTCIntersectContext *context, unsigned rayIndex, float opacity, const qvec3d &glasscolor)
{
ray_source_info *ctx = static_cast<ray_source_info *>(context);
raystream_embree_common_t *rs = ctx->raystream;
if (rs == nullptr) {
// FIXME: remove this.. once all ray casts use raystreams
// happens for bounce lights, e.g. Embree_TestSky
return;
}
// clamp opacity
opacity = clamp(opacity, 0.0f, 1.0f);
Q_assert(rayIndex < rs->_numrays);
// multiply ray color by glass color
qvec3d tinted = rs->_ray_colors[rayIndex] * glasscolor;
// use the lerped color between original ray color and fully tinted, based on opacity
rs->_ray_colors[rayIndex] = mix(tinted, rs->_ray_colors[rayIndex], opacity);
}
static void AddDynamicOccluderToRay(RTCIntersectContext *context, unsigned rayIndex, int style)
{
ray_source_info *ctx = static_cast<ray_source_info *>(context);
raystream_embree_common_t *rs = ctx->raystream;
if (rs != nullptr) {
rs->_ray_dynamic_styles[rayIndex] = style;
} else {
// TestLight case
ctx->singleRayShadowStyle = style;
}
}