diff --git a/common/bsputils.cc b/common/bsputils.cc index 28f6db7a..d3d3046d 100644 --- a/common/bsputils.cc +++ b/common/bsputils.cc @@ -1099,10 +1099,10 @@ static int StyleOffset(int style, const mface_t *face, const faceextents_t &face } /** - * Samples the lightmap at an integer coordinate in style 0 + * Samples the lightmap at an integer coordinate in the given style */ qvec3b LM_Sample(const mbsp_t *bsp, const mface_t *face, const lit_variant_t *lit, const faceextents_t &faceextents, - int byte_offset_of_face, qvec2i coord) + int byte_offset_of_face, qvec2i coord, int style) { if (byte_offset_of_face == -1) { return {0, 0, 0}; @@ -1113,7 +1113,7 @@ qvec3b LM_Sample(const mbsp_t *bsp, const mface_t *face, const lit_variant_t *li Q_assert(coord[0] < faceextents.width()); Q_assert(coord[1] < faceextents.height()); - int style_offset = StyleOffset(0, face, faceextents); + int style_offset = StyleOffset(style, face, faceextents); if (style_offset == -1) { return {0, 0, 0}; } diff --git a/docs/light.rst b/docs/light.rst index 88f861de..c26896e0 100644 --- a/docs/light.rst +++ b/docs/light.rst @@ -665,6 +665,11 @@ If used on func_detail* or func_group, a full qbsp pass need to be run. through the switchable shadow casters, regardless of whether the shadow is off or on. + .. seealso:: + + The light entity key :light-key:`_switchableshadow_target` allows using switchable + shadows in ID1 Quake, without custom QC, although the setup is more awkward. + .. bmodel-key:: "_dirt" "n" For brush models, -1 prevents dirtmapping on the brush model. Useful @@ -918,6 +923,21 @@ Point Lights Set to 1 to make the light compiler ignore this entity (prevents it from casting any light). e.g. could be useful with rtlights. +.. light-key:: "_switchableshadow_target" "name" + + Calculate lighting with and without bmodels with a "targetname" equal to "name", + and stores the resulting switchable shadow data in a light style which is stored in this light + entity's "style" key. + + You should give this light a :light-key:`targetname` and typically set "spawnflags" "1" (start off). + + Implies :light-key:`_nostaticlight` (this entity itself does not cast any light). + + .. hint:: + + If your mod supports it, you should prefer to use bmodel key :bmodel-key:`_switchableshadow` + to enable switchable shadows. + Spotlights ---------- diff --git a/include/common/bsputils.hh b/include/common/bsputils.hh index aeb9eafb..5f33c0bb 100644 --- a/include/common/bsputils.hh +++ b/include/common/bsputils.hh @@ -176,7 +176,7 @@ public: }; qvec3b LM_Sample(const mbsp_t *bsp, const mface_t *face, const lit_variant_t *lit, const faceextents_t &faceextents, - int byte_offset_of_face, qvec2i coord); + int byte_offset_of_face, qvec2i coord, int style = 0); qvec3f LM_Sample_HDR(const mbsp_t *bsp, const mface_t *face, diff --git a/include/light/entities.hh b/include/light/entities.hh index 568dd8e5..707c7917 100644 --- a/include/light/entities.hh +++ b/include/light/entities.hh @@ -113,6 +113,7 @@ public: settings::setting_int32 light_channel_mask; settings::setting_int32 shadow_channel_mask; settings::setting_bool nonudge; + settings::setting_string switchableshadow_target; light_t(); @@ -143,6 +144,10 @@ std::vector> &GetLights(); const std::vector &GetEntdicts(); std::vector &GetSuns(); std::vector &GetRadLights(); +/** + * Returns the light entity that has "_switchableshadow_target" set to the given value, or nullptr. + */ +light_t *LightWithSwitchableShadowTargetValue(const std::string &target); const std::vector> &GetSurfaceLightTemplates(); diff --git a/light/entities.cc b/light/entities.cc index 572edb13..516cea00 100644 --- a/light/entities.cc +++ b/light/entities.cc @@ -42,6 +42,7 @@ static std::vector> lightstyleForTargetname; static std::vector> surfacelight_templates; static std::ofstream surflights_dump_file; static fs::path surflights_dump_filename; +static std::map lights_by_switchableshadow_target; /** * Resets global data in this file @@ -58,6 +59,7 @@ void ResetLightEntities() surfacelight_templates.clear(); surflights_dump_file = {}; surflights_dump_filename.clear(); + lights_by_switchableshadow_target.clear(); } std::vector> &GetLights() @@ -80,6 +82,16 @@ std::vector &GetRadLights() return radlights; } +light_t *LightWithSwitchableShadowTargetValue(const std::string &target) +{ + auto it = lights_by_switchableshadow_target.find(target); + + if (it == lights_by_switchableshadow_target.end()) + return nullptr; + + return it->second; +} + /* surface lights */ static void MakeSurfaceLights(const mbsp_t *bsp); @@ -121,7 +133,8 @@ light_t::light_t() surflight_atten{this, "surflight_atten", 1.f}, light_channel_mask{this, "light_channel_mask", CHANNEL_MASK_DEFAULT}, shadow_channel_mask{this, "shadow_channel_mask", CHANNEL_MASK_DEFAULT}, - nonudge{this, "nonudge", false} + nonudge{this, "nonudge", false}, + switchableshadow_target{this, "switchableshadow_target", ""} { } @@ -1062,6 +1075,14 @@ void LoadEntities(const settings::worldspawn_keys &cfg, const mbsp_t *bsp) entity->projfov.value(), entity->projectionmatrix); } + // vanilla-compatible switchable shadows + const std::string &switchableshadow_target = entity->switchableshadow_target.value(); + if (!switchableshadow_target.empty()) { + entity->nostaticlight.set_value(true, settings::source::DEFAULT); + + lights_by_switchableshadow_target[switchableshadow_target] = entity.get(); + } + CheckEntityFields(bsp, cfg, entity.get()); } } diff --git a/light/light.cc b/light/light.cc index ff7e3575..4d06d0b0 100644 --- a/light/light.cc +++ b/light/light.cc @@ -725,6 +725,13 @@ static void FindModelInfo(const mbsp_t *bsp) // apply settings info->set_settings(*entdict, settings::source::MAP); + // vanilla-compatible switchable shadows + if (auto *light = LightWithSwitchableShadowTargetValue(entdict->get("targetname"))) { + // take the "style" key from this light entity and enable switchable shadows on ourself + info->switchableshadow.set_value(true, settings::source::DEFAULT); + info->switchshadstyle.set_value(light->style.value(), settings::source::DEFAULT); + } + /* Check if this model will cast shadows (shadow => shadowself) */ if (info->switchableshadow.boolValue()) { Q_assert(info->switchshadstyle.value() != 0); diff --git a/testmaps/q1_light_switchableshadow_target.map b/testmaps/q1_light_switchableshadow_target.map new file mode 100644 index 00000000..86a18fc8 --- /dev/null +++ b/testmaps/q1_light_switchableshadow_target.map @@ -0,0 +1,184 @@ +// Game: Quake +// Format: Standard +// entity 0 +{ +"classname" "worldspawn" +"_tb_textures" "textures/e1u1" +"wad" "deprecated/free_wad.wad" +// brush 0 +{ +( 480 1088 928 ) ( 480 1089 928 ) ( 480 1088 929 ) bolt14 0 32 0 1 1 +( 704 1088 928 ) ( 704 1088 929 ) ( 705 1088 928 ) bolt14 0 32 0 1 1 +( 704 1088 928 ) ( 705 1088 928 ) ( 704 1089 928 ) bolt14 0 0 0 1 1 +( 944 1472 944 ) ( 944 1473 944 ) ( 945 1472 944 ) bolt14 0 0 0 1 1 +( 944 1488 944 ) ( 945 1488 944 ) ( 944 1488 945 ) bolt14 0 32 0 1 1 +( 1056 1472 944 ) ( 1056 1472 945 ) ( 1056 1473 944 ) bolt14 0 32 0 1 1 +} +// brush 1 +{ +( 480 1088 1248 ) ( 480 1089 1248 ) ( 480 1088 1249 ) bolt14 0 96 0 1 1 +( 704 1072 1248 ) ( 704 1072 1249 ) ( 705 1072 1248 ) bolt14 0 96 0 1 1 +( 704 1088 1248 ) ( 705 1088 1248 ) ( 704 1089 1248 ) bolt14 0 0 0 1 1 +( 944 1472 1264 ) ( 944 1473 1264 ) ( 945 1472 1264 ) bolt14 0 0 0 1 1 +( 944 1488 1264 ) ( 945 1488 1264 ) ( 944 1488 1265 ) bolt14 0 96 0 1 1 +( 1056 1472 1264 ) ( 1056 1472 1265 ) ( 1056 1473 1264 ) bolt14 0 96 0 1 1 +} +// brush 2 +{ +( 480 1072 928 ) ( 480 1073 928 ) ( 480 1072 929 ) bolt14 16 32 0 1 1 +( 704 1072 928 ) ( 704 1072 929 ) ( 705 1072 928 ) bolt14 0 32 0 1 1 +( 704 1072 928 ) ( 705 1072 928 ) ( 704 1073 928 ) bolt14 0 -16 0 1 1 +( 944 1456 1248 ) ( 944 1457 1248 ) ( 945 1456 1248 ) bolt14 0 -16 0 1 1 +( 944 1088 944 ) ( 945 1088 944 ) ( 944 1088 945 ) bolt14 0 32 0 1 1 +( 1056 1456 944 ) ( 1056 1456 945 ) ( 1056 1457 944 ) bolt14 16 32 0 1 1 +} +// brush 3 +{ +( 480 1392 928 ) ( 480 1393 928 ) ( 480 1392 929 ) bolt14 -48 32 0 1 1 +( 832 1488 928 ) ( 832 1488 929 ) ( 833 1488 928 ) bolt14 -128 32 0 1 1 +( 832 1392 928 ) ( 833 1392 928 ) ( 832 1393 928 ) bolt14 -128 48 0 1 1 +( 1072 1776 1248 ) ( 1072 1777 1248 ) ( 1073 1776 1248 ) bolt14 -128 48 0 1 1 +( 1072 1504 944 ) ( 1073 1504 944 ) ( 1072 1504 945 ) bolt14 -128 32 0 1 1 +( 1056 1392 928 ) ( 1056 1392 929 ) ( 1056 1393 928 ) bolt14 -48 32 0 1 1 +} +// brush 4 +{ +( 1056 1088 1056 ) ( 1056 1089 1056 ) ( 1056 1088 1057 ) bolt14 0 32 0 1 1 +( 736 1088 1056 ) ( 736 1088 1057 ) ( 737 1088 1056 ) bolt14 -32 32 0 1 1 +( 736 1088 928 ) ( 737 1088 928 ) ( 736 1089 928 ) bolt14 -32 0 0 1 1 +( 976 1472 1248 ) ( 976 1473 1248 ) ( 977 1472 1248 ) bolt14 -32 0 0 1 1 +( 976 1488 1072 ) ( 977 1488 1072 ) ( 976 1488 1073 ) bolt14 -32 32 0 1 1 +( 1072 1472 1072 ) ( 1072 1472 1073 ) ( 1072 1473 1072 ) bolt14 0 32 0 1 1 +} +// brush 5 +{ +( 464 1088 1056 ) ( 464 1089 1056 ) ( 464 1088 1057 ) bolt14 0 32 0 1 1 +( 144 1072 1056 ) ( 144 1072 1057 ) ( 145 1072 1056 ) bolt14 48 32 0 1 1 +( 144 1088 928 ) ( 145 1088 928 ) ( 144 1089 928 ) bolt14 48 0 0 1 1 +( 384 1472 1248 ) ( 384 1473 1248 ) ( 385 1472 1248 ) bolt14 48 0 0 1 1 +( 384 1488 1072 ) ( 385 1488 1072 ) ( 384 1488 1073 ) bolt14 48 32 0 1 1 +( 480 1472 1072 ) ( 480 1472 1073 ) ( 480 1473 1072 ) bolt14 0 32 0 1 1 +} +// brush 6 +{ +( 704 1088 944 ) ( 704 1089 944 ) ( 704 1088 945 ) bolt10 0 0 0 1 1 +( 704 1088 944 ) ( 704 1088 945 ) ( 705 1088 944 ) bolt10 0 0 0 1 1 +( 704 1088 944 ) ( 705 1088 944 ) ( 704 1089 944 ) bolt10 0 0 0 1 1 +( 720 1216 1008 ) ( 720 1217 1008 ) ( 721 1216 1008 ) bolt10 0 0 0 1 1 +( 720 1216 960 ) ( 721 1216 960 ) ( 720 1216 961 ) bolt10 0 0 0 1 1 +( 720 1216 960 ) ( 720 1216 961 ) ( 720 1217 960 ) bolt10 0 0 0 1 1 +} +// brush 7 +{ +( 704 1088 944 ) ( 704 1089 944 ) ( 704 1088 945 ) bolt10 0 0 0 1 1 +( 720 1344 960 ) ( 720 1344 961 ) ( 721 1344 960 ) bolt10 0 0 0 1 1 +( 704 1088 944 ) ( 705 1088 944 ) ( 704 1089 944 ) bolt10 0 0 0 1 1 +( 720 1216 1008 ) ( 720 1217 1008 ) ( 721 1216 1008 ) bolt10 0 0 0 1 1 +( 720 1488 960 ) ( 721 1488 960 ) ( 720 1488 961 ) bolt10 0 0 0 1 1 +( 720 1216 960 ) ( 720 1216 961 ) ( 720 1217 960 ) bolt10 0 0 0 1 1 +} +// brush 8 +{ +( 704 1088 944 ) ( 704 1089 944 ) ( 704 1088 945 ) bolt10 0 0 0 1 1 +( 704 1088 944 ) ( 704 1088 945 ) ( 705 1088 944 ) bolt10 0 0 0 1 1 +( 720 1216 1008 ) ( 721 1216 1008 ) ( 720 1217 1008 ) bolt10 0 0 0 1 1 +( 720 1216 1016 ) ( 720 1217 1016 ) ( 721 1216 1016 ) bolt10 0 0 0 1 1 +( 720 1488 960 ) ( 721 1488 960 ) ( 720 1488 961 ) bolt10 0 0 0 1 1 +( 720 1216 960 ) ( 720 1216 961 ) ( 720 1217 960 ) bolt10 0 0 0 1 1 +} +} +// entity 1 +{ +"classname" "info_player_start" +"origin" "848 1280 968" +"angle" "180" +} +// entity 2 +{ +"classname" "light" +"origin" "760 1280 1048" +"light" "1000" +"targetname" "toggle_door1_shadow" +"spawnflags" "1" +"_switchableshadow_target" "door1" +} +// entity 3 +{ +"classname" "light" +"origin" "568 1264 952" +"_color" "1 0 0" +"light" "500" +} +// entity 4 +{ +"classname" "func_door" +"targetname" "door1" +"spawnflags" "32" +"angle" "-2" +"speed" "1000" +// brush 0 +{ +( 704 1320 944 ) ( 704 1321 944 ) ( 704 1320 945 ) bolt10 -8 0 0 1 1 +( 704 1320 944 ) ( 704 1320 945 ) ( 705 1320 944 ) bolt10 0 0 0 1 1 +( 704 1320 944 ) ( 705 1320 944 ) ( 704 1321 944 ) bolt10 0 8 0 1 1 +( 720 1448 1008 ) ( 720 1449 1008 ) ( 721 1448 1008 ) bolt10 0 8 0 1 1 +( 720 1336 960 ) ( 721 1336 960 ) ( 720 1336 961 ) bolt10 0 0 0 1 1 +( 720 1448 960 ) ( 720 1448 961 ) ( 720 1449 960 ) bolt10 -8 0 0 1 1 +} +// brush 1 +{ +( 704 1224 944 ) ( 704 1225 944 ) ( 704 1224 945 ) bolt10 -8 0 0 1 1 +( 704 1224 944 ) ( 704 1224 945 ) ( 705 1224 944 ) bolt10 0 0 0 1 1 +( 704 1224 944 ) ( 705 1224 944 ) ( 704 1225 944 ) bolt10 0 8 0 1 1 +( 720 1352 1008 ) ( 720 1353 1008 ) ( 721 1352 1008 ) bolt10 0 8 0 1 1 +( 720 1240 960 ) ( 721 1240 960 ) ( 720 1240 961 ) bolt10 0 0 0 1 1 +( 720 1352 960 ) ( 720 1352 961 ) ( 720 1353 960 ) bolt10 -8 0 0 1 1 +} +// brush 2 +{ +( 704 1256 944 ) ( 704 1257 944 ) ( 704 1256 945 ) bolt10 -8 0 0 1 1 +( 704 1256 944 ) ( 704 1256 945 ) ( 705 1256 944 ) bolt10 0 0 0 1 1 +( 704 1256 944 ) ( 705 1256 944 ) ( 704 1257 944 ) bolt10 0 8 0 1 1 +( 720 1384 1008 ) ( 720 1385 1008 ) ( 721 1384 1008 ) bolt10 0 8 0 1 1 +( 720 1272 960 ) ( 721 1272 960 ) ( 720 1272 961 ) bolt10 0 0 0 1 1 +( 720 1384 960 ) ( 720 1384 961 ) ( 720 1385 960 ) bolt10 -8 0 0 1 1 +} +// brush 3 +{ +( 704 1288 944 ) ( 704 1289 944 ) ( 704 1288 945 ) bolt10 -8 0 0 1 1 +( 704 1288 944 ) ( 704 1288 945 ) ( 705 1288 944 ) bolt10 0 0 0 1 1 +( 704 1288 944 ) ( 705 1288 944 ) ( 704 1289 944 ) bolt10 0 8 0 1 1 +( 720 1416 1008 ) ( 720 1417 1008 ) ( 721 1416 1008 ) bolt10 0 8 0 1 1 +( 720 1304 960 ) ( 721 1304 960 ) ( 720 1304 961 ) bolt10 0 0 0 1 1 +( 720 1416 960 ) ( 720 1416 961 ) ( 720 1417 960 ) bolt10 -8 0 0 1 1 +} +} +// entity 5 +{ +"classname" "func_button" +"target" "switch1" +"angle" "-2" +// brush 0 +{ +( 752 1288 952 ) ( 752 1289 952 ) ( 752 1288 953 ) swire2 -56 0 0 1 1 +( 792 1272 944 ) ( 791 1272 944 ) ( 792 1272 945 ) swire2 48 0 180 1 -1 +( 792 1248 944 ) ( 792 1249 944 ) ( 791 1248 944 ) swire2 -56 -48 90 1 1 +( 744 1288 952 ) ( 743 1288 952 ) ( 744 1289 952 ) +0switch -8 -16 90 1 1 +( 744 1288 952 ) ( 744 1288 953 ) ( 743 1288 952 ) swire2 48 0 180 1 -1 +( 784 1248 944 ) ( 784 1248 945 ) ( 784 1249 944 ) swire2 -56 0 0 1 1 +} +} +// entity 6 +{ +"classname" "trigger_relay" +"origin" "760 1256 1000" +"targetname" "switch1" +"target" "door1" +} +// entity 7 +{ +"classname" "trigger_relay" +"origin" "760 1320 1000" +"targetname" "switch1" +"target" "toggle_door1_shadow" +} diff --git a/tests/test_ltface.cc b/tests/test_ltface.cc index 8d8e3492..a191f2f9 100644 --- a/tests/test_ltface.cc +++ b/tests/test_ltface.cc @@ -319,7 +319,7 @@ static void CheckFaceLuxelsNonBlack(const mbsp_t &bsp, const mface_t &face) static void CheckFaceLuxelAtPoint(const mbsp_t *bsp, const dmodelh2_t *model, const qvec3b &expected_color, const qvec3d &point, const qvec3d &normal = {0, 0, 0}, const lit_variant_t *lit = nullptr, - const bspxentries_t *bspx = nullptr) + const bspxentries_t *bspx = nullptr, int style = 0) { auto *face = BSP_FindFaceAtPoint(bsp, model, point, normal); ASSERT_TRUE(face); @@ -340,7 +340,7 @@ static void CheckFaceLuxelAtPoint(const mbsp_t *bsp, const dmodelh2_t *model, co const auto coord = extents.worldToLMCoord(point); const auto int_coord = qvec2i(round(coord[0]), round(coord[1])); - const qvec3b sample = LM_Sample(bsp, face, lit, extents, offset, int_coord); + const qvec3b sample = LM_Sample(bsp, face, lit, extents, offset, int_coord, style); SCOPED_TRACE(fmt::format("world point: {}", point)); SCOPED_TRACE(fmt::format("lm coord: {}", coord)); SCOPED_TRACE(fmt::format("lm int_coord: {}", int_coord)); @@ -1145,4 +1145,35 @@ TEST(ltfaceQ1, hdr) // check internal lightmap - greyscale, since Q1 CheckFaceLuxelAtPoint(&bsp, &bsp.dmodels[0], {0, 0, 0}, testpoint, testnormal); } +} + +TEST(ltfaceQ1, switchableshadowTarget) +{ + SCOPED_TRACE("Vanilla-compatible switchable shadows"); + + auto [bsp, bspx, lit] = QbspVisLight_Q1("q1_light_switchableshadow_target.map", {}); + + // find the light controlling the switchable shadow + auto entdicts = EntData_Parse(bsp); + + auto it = std::find_if(entdicts.begin(), entdicts.end(), [](const entdict_t& dict) -> bool { + return dict.get("_switchableshadow_target") == "door1"; + }); + ASSERT_NE(it, entdicts.end()); + + ASSERT_TRUE(it->has("style")); + int switchable_style = it->get_int("style"); + + ASSERT_EQ(32, switchable_style); + + const qvec3f not_in_shadow {792, 1240, 944}; + const qvec3f in_shadow {792, 1264, 944}; + + // not in shadow - should be lit up red in style 0, and black in style 32 + CheckFaceLuxelAtPoint(&bsp, &bsp.dmodels[0], {68, 0, 0}, not_in_shadow, {0, 0, 1}, &lit, &bspx, 0); + CheckFaceLuxelAtPoint(&bsp, &bsp.dmodels[0], {0, 0, 0}, not_in_shadow, {0, 0, 1}, &lit, &bspx, 32); + + // in (switchable) shadow - should be black in style 0, and red in style 32 + CheckFaceLuxelAtPoint(&bsp, &bsp.dmodels[0], {0, 0, 0}, in_shadow, {0, 0, 1}, &lit, &bspx, 0); + CheckFaceLuxelAtPoint(&bsp, &bsp.dmodels[0], {68, 0, 0}, in_shadow, {0, 0, 1}, &lit, &bspx, 32); } \ No newline at end of file