/* 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 #include #include #include #include #include #include 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 static RTCDevice device; RTCScene scene; static const mbsp_t *bsp_static; void ResetEmbree() { skygeom = {}; solidgeom = {}; filtergeom = {}; if (scene) { rtcReleaseScene(scene); scene = nullptr; } if (device) { rtcReleaseDevice(device); device = nullptr; } bsp_static = nullptr; } /** * Returns 1.0 unless a custom alpha value is set. * The priority is: "_light_alpha" (read from extended_texinfo_flags), then "alpha", then Q2 surface flags */ static float Face_Alpha(const mbsp_t *bsp, const modelinfo_t *modelinfo, const mface_t *face) { const surfflags_t &extended_flags = extended_texinfo_flags[face->texinfo]; const int surf_flags = Face_ContentsOrSurfaceFlags(bsp, face); const bool is_q2 = bsp->loadversion->game->id == GAME_QUAKE_II; if (extended_flags.light_alpha) { return *extended_flags.light_alpha; } // next check "alpha" key (q1) if (modelinfo->alpha.is_changed()) { return modelinfo->alpha.value(); } // next handle q2 surface flags if (is_q2) { if (surf_flags & Q2_SURF_TRANS33) { return 0.33f; } if (surf_flags & Q2_SURF_TRANS66) { return 0.66f; } } // no alpha requested return 1.0f; } sceneinfo CreateGeometry( const mbsp_t *bsp, RTCDevice g_device, RTCScene scene, const std::vector &faces) { 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; }; // temprary buffers used while gathering faces std::vector vertices_temp; std::vector tris_temp; sceneinfo s; s.geomID = geomID; auto add_vert = [&](const qvec3f &pos) { vertices_temp.push_back({.point{pos[0], pos[1], pos[2], 0.0f}}); }; // FIXME: reuse vertices auto add_tri = [&](const mface_t *face, int bsp_vert0, int bsp_vert1, int bsp_vert2, const modelinfo_t *modelinfo) { const qvec3f final_pos0 = Vertex_GetPos(bsp, bsp_vert0) + modelinfo->offset; const qvec3f final_pos1 = Vertex_GetPos(bsp, bsp_vert1) + modelinfo->offset; const qvec3f final_pos2 = Vertex_GetPos(bsp, bsp_vert2) + modelinfo->offset; // push the 3 vertices int first_vert_index = vertices_temp.size(); add_vert(final_pos0); add_vert(final_pos1); add_vert(final_pos2); int tri_index = tris_temp.size(); tris_temp.push_back({first_vert_index, first_vert_index + 1, first_vert_index + 2}); const surfflags_t &extended_flags = extended_texinfo_flags[face->texinfo]; triinfo info; info.face = face; info.modelinfo = modelinfo; info.texinfo = &bsp->texinfo[face->texinfo]; info.texture = Face_Texture(bsp, face); // FIXME: don't these need to check extended_flags? info.shadowworldonly = modelinfo->shadowworldonly.boolValue(); info.shadowself = modelinfo->shadowself.boolValue(); info.switchableshadow = modelinfo->switchableshadow.boolValue(); info.switchshadstyle = modelinfo->switchshadstyle.value(); info.channelmask = extended_flags.object_channel_mask.value_or(modelinfo->object_channel_mask.value()); info.alpha = Face_Alpha(bsp, 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_ALPHATEST; info.is_glass = !info.is_fence && (surf_flags & (Q2_SURF_TRANS33 | Q2_SURF_TRANS66)); } 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); }; auto add_face = [&](const mface_t *face, const modelinfo_t *modelinfo) { if (face->numedges < 3) return; for (int j = 2; j < face->numedges; j++) { int bsp_vert0 = Face_VertexAtIndex(bsp, face, j - 1); int bsp_vert1 = Face_VertexAtIndex(bsp, face, j); int bsp_vert2 = Face_VertexAtIndex(bsp, face, 0); add_tri(face, bsp_vert0, bsp_vert1, bsp_vert2, modelinfo); } }; for (const mface_t *face : faces) { // NOTE: can be null for "skip" faces const modelinfo_t *modelinfo = ModelInfoForFace(bsp, Face_GetNum(bsp, face)); if (modelinfo) { add_face(face, modelinfo); } } // copy vertices, triangles from temporary buffers to embree-managed memory Vertex *vertices = (Vertex *)rtcSetNewGeometryBuffer( geom_0, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3, 4 * sizeof(float), vertices_temp.size()); Triangle *triangles = (Triangle *)rtcSetNewGeometryBuffer( geom_0, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, 3 * sizeof(int), tris_temp.size()); memcpy(vertices, vertices_temp.data(), sizeof(Vertex) * vertices_temp.size()); memcpy(triangles, tris_temp.data(), sizeof(Triangle) * tris_temp.size()); rtcCommitGeometry(geom_0); return s; } static void CreateGeometryFromWindings(RTCDevice g_device, RTCScene scene, const std::vector &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(); } 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); 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); } 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); } 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 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(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.channelmask & rsi->shadowmask)) { // reject hit valid[i] = INVALID; continue; } 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) } } /** * For use with all rays coming from a model with non-default channel mask */ static void PerRay_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; auto *rsi = static_cast(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 triinfo &hit_triinfo = Embree_LookupTriangleInfo(geomID, primID); if (!(hit_triinfo.channelmask & rsi->shadowmask)) { // 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 modelinfo_t *modelinfo, const mleaf_t *leaf, const std::vector &planes, std::vector &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 = winding_t::from_plane(faceplane, 10e6); // clip `winding` by all of the other planes for (const qplane3d &plane2 : planes) { if (&plane2 == &plane) continue; // discard the back, continue clipping the front part winding = winding->clip_front(plane2); // check if everything was clipped away if (!winding) break; } if (!winding) { // logging::print("WARNING: winding clipped away\n"); } else { result.push_back(winding->translate(modelinfo->offset)); } } } void MakeFaces_r(const mbsp_t *bsp, const modelinfo_t *modelinfo, const int nodenum, std::vector *planes, std::vector &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, modelinfo, 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, modelinfo, node->children[0], planes, result); planes->pop_back(); // go down the back side planes->push_back(Node_Plane(bsp, node, true)); MakeFaces_r(bsp, modelinfo, node->children[1], planes, result); planes->pop_back(); } static void MakeFaces( const mbsp_t *bsp, const modelinfo_t *modelinfo, const dmodelh2_t *model, std::vector &result) { std::vector planes; MakeFaces_r(bsp, modelinfo, 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 skyfaces, solidfaces, filterfaces; // check all modelinfos for (size_t mi = 0; mi < bsp->dmodels.size(); mi++) { const modelinfo_t *model = ModelInfoForModel(bsp, mi); // check reasons that a bmodel can be shadow casting 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(); const bool has_custom_channel_mask = (model->object_channel_mask.value() != CHANNEL_MASK_DEFAULT); if (!(isWorld || shadow || shadowself || shadowworldonly || switchableshadow || has_custom_channel_mask)) 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; } // non-default channel mask if (model->object_channel_mask.value() != CHANNEL_MASK_DEFAULT || extended_flags.object_channel_mask.value_or(CHANNEL_MASK_DEFAULT) != CHANNEL_MASK_DEFAULT) { 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(bsp, model, face); if (alpha < 1.0f || (is_q2 && (contents_or_surf_flags & (Q2_SURF_ALPHATEST | Q2_SURF_TRANS33 | Q2_SURF_TRANS66)))) { 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 skipwindings; for (const modelinfo_t *modelinfo : tracelist) { if (modelinfo->model->numfaces == 0) { MakeFaces(bsp, modelinfo, modelinfo->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); // we're using RTCIntersectContext::filter so it's required that we set // RTC_SCENE_FLAG_CONTEXT_FILTER_FUNCTION rtcSetSceneFlags(scene, RTC_SCENE_FLAG_CONTEXT_FILTER_FUNCTION); 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); rtcSetGeometryOccludedFilterFunction(rtcGetGeometry(scene, filtergeom.geomID), Embree_FilterFuncN); 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 void AddGlassToRay(RTCIntersectContext *context, unsigned rayIndex, float opacity, const qvec3d &glasscolor) { ray_source_info *ctx = static_cast(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); rs->_ray_hit_glass[rayIndex] = true; rs->_ray_glass_color[rayIndex] = glasscolor; rs->_ray_glass_opacity[rayIndex] = opacity; } static void AddDynamicOccluderToRay(RTCIntersectContext *context, unsigned rayIndex, int style) { ray_source_info *ctx = static_cast(context); raystream_embree_common_t *rs = ctx->raystream; if (rs != nullptr) { rs->_ray_dynamic_styles[rayIndex] = style; } } ray_source_info::ray_source_info(raystream_embree_common_t *raystream_, const modelinfo_t *self_, int shadowmask_) : raystream(raystream_), self(self_), shadowmask(shadowmask_) { rtcInitIntersectContext(this); flags = RTC_INTERSECT_CONTEXT_FLAG_COHERENT; if (shadowmask != CHANNEL_MASK_DEFAULT) { // non-default shadow mask means we have to use the slow path filter = PerRay_FilterFuncN; } }