/* 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 /** * 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; // for "_light_alpha", 0 is considered unset if (extended_flags.light_alpha) { return extended_flags.light_alpha; } // next check "alpha" key (q1) if (modelinfo->alpha.isChanged()) { 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) { // 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(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); } } 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); } 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 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.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 &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(std::move(*winding)); } } } void MakeFaces_r(const mbsp_t *bsp, 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, 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 &result) { std::vector 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 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(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 *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); 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 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(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; // lerp ray color between original ray color and fully tinted by the glass texture color, based on the glass opacity rs->_ray_colors[rayIndex] = mix(rs->_ray_colors[rayIndex], tinted, 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; } else { // TestLight case ctx->singleRayShadowStyle = style; } }