diff --git a/README.md b/README.md index 1129a70bc..d5580d173 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,12 @@ fastgltf supports glTF 2.0: fastgltf supports a number of glTF extensions: - [x] EXT_meshopt_compression - [x] EXT_texture_webp +- [x] KHR_lights_punctual - [x] KHR_texture_basisu - [x] KHR_texture_transform - [x] KHR_mesh_quantization - [x] MSFT_texture_dds - + fastgltf brings many utilities: - [x] SIMD powered base64 buffer decoder - [x] Can decompose transform matrices, so you only ever have translation, rotation, and scale. diff --git a/src/fastgltf.cpp b/src/fastgltf.cpp index 6cc2a646a..5780fc177 100644 --- a/src/fastgltf.cpp +++ b/src/fastgltf.cpp @@ -154,6 +154,7 @@ static constexpr std::array, 8 { fg::extensions::EXT_mesh_gpu_instancing, fg::Extensions::EXT_mesh_gpu_instancing }, { fg::extensions::EXT_meshopt_compression, fg::Extensions::EXT_meshopt_compression }, { fg::extensions::EXT_texture_webp, fg::Extensions::EXT_texture_webp }, + { fg::extensions::KHR_lights_punctual, fg::Extensions::KHR_lights_punctual }, { fg::extensions::KHR_mesh_quantization, fg::Extensions::KHR_mesh_quantization }, { fg::extensions::KHR_texture_basisu, fg::Extensions::KHR_texture_basisu }, { fg::extensions::KHR_texture_transform, fg::Extensions::KHR_texture_transform }, @@ -599,7 +600,16 @@ fg::Error fg::glTF::parse(Category categories) { } parsedAsset->defaultScene = static_cast(defaultScene); continue; - } else if (hashedKey == force_consteval || hashedKey == force_consteval || hashedKey == force_consteval) { + } else if (hashedKey == force_consteval) { + dom::object extensionsObject; + if (object.value.get_object().get(extensionsObject) != SUCCESS) { + errorCode = Error::InvalidGltf; + return errorCode; + } + + parseExtensions(extensionsObject); + continue; + } else if (hashedKey == force_consteval || hashedKey == force_consteval) { continue; } @@ -1155,6 +1165,37 @@ void fg::glTF::parseCameras(simdjson::dom::array& cameras) { } } +void fg::glTF::parseExtensions(simdjson::dom::object& extensionsObject) { + using namespace simdjson; + + for (auto extensionValue : extensionsObject) { + dom::object extensionObject; + if (auto error = extensionValue.value.get_object().get(extensionObject); error != SUCCESS) { + if (error == INCORRECT_TYPE) { + continue; // We want to ignore + } else { + SET_ERROR_RETURN(Error::InvalidGltf) + } + } + + auto hash = crc32(extensionValue.key); + switch (hash) { + case force_consteval: { + if (!hasBit(extensions, Extensions::KHR_lights_punctual)) + break; + + dom::array lightsArray; + if (auto error = extensionObject["lights"].get_array().get(lightsArray); error == SUCCESS) { + parseLights(lightsArray); + } else if (error != NO_SUCH_FIELD) { + SET_ERROR_RETURN(Error::InvalidGltf) + } + break; + } + } + } +} + void fg::glTF::parseImages(simdjson::dom::array& images) { using namespace simdjson; @@ -1213,6 +1254,95 @@ void fg::glTF::parseImages(simdjson::dom::array& images) { } } +void fg::glTF::parseLights(simdjson::dom::array& lights) { + using namespace simdjson; + + parsedAsset->lights.reserve(lights.size()); + for (auto lightValue : lights) { + dom::object lightObject; + if (lightValue.get_object().get(lightObject) != SUCCESS) { + SET_ERROR_RETURN(Error::InvalidGltf); + } + Light light = {}; + + std::string_view type; + if (lightObject["type"].get_string().get(type) == SUCCESS) { + switch (crc32(type)) { + case force_consteval: { + light.type = LightType::Directional; + break; + } + case force_consteval: { + light.type = LightType::Spot; + break; + } + case force_consteval: { + light.type = LightType::Point; + break; + } + default: { + SET_ERROR_RETURN(Error::InvalidGltf) + } + } + } else { + SET_ERROR_RETURN(Error::InvalidGltf) + } + + if (light.type == LightType::Spot) { + dom::object spotObject; + if (lightObject["spot"].get_object().get(spotObject) != SUCCESS) { + SET_ERROR_RETURN(Error::InvalidGltf) + } + + double innerConeAngle; + if (lightObject["innerConeAngle"].get_double().get(innerConeAngle) != SUCCESS) { + SET_ERROR_RETURN(Error::InvalidGltf) + } + light.innerConeAngle = static_cast(innerConeAngle); + + double outerConeAngle; + if (lightObject["outerConeAngle"].get_double().get(outerConeAngle) != SUCCESS) { + SET_ERROR_RETURN(Error::InvalidGltf) + } + light.outerConeAngle = static_cast(outerConeAngle); + } + + dom::array colorArray; + if (lightObject["color"].get_array().get(colorArray) == SUCCESS) { + if (colorArray.size() != 3) { + SET_ERROR_RETURN(Error::InvalidGltf) + } + for (auto i = 0; i < colorArray.size(); ++i) { + double color; + if (colorArray.at(i).get_double().get(color) == SUCCESS) { + light.color[i] = static_cast(color); + } else { + SET_ERROR_RETURN(Error::InvalidGltf) + } + } + } + + double intensity; + if (lightObject["intensity"].get_double().get(intensity) == SUCCESS) { + light.intensity = static_cast(intensity); + } else { + light.intensity = 0.0; + } + + double range; + if (lightObject["range"].get_double().get(range) == SUCCESS) { + light.range = static_cast(range); + } + + std::string_view name; + if (lightObject["name"].get_string().get(name) == SUCCESS) { + light.name = std::string { name }; + } + + parsedAsset->lights.emplace_back(std::move(light)); + } +} + void fg::glTF::parseMaterials(simdjson::dom::array& materials) { using namespace simdjson; @@ -1570,14 +1700,22 @@ void fg::glTF::parseNodes(simdjson::dom::array& nodes) { node.transform = trs; } - // name is optional. - { - std::string_view name; - if (nodeObject["name"].get_string().get(name) == SUCCESS) { - node.name = std::string { name }; + dom::object extensionsObject; + if (nodeObject["extensions"].get_object().get(extensionsObject) == SUCCESS) { + dom::object lightsObject; + if (extensionsObject[extensions::KHR_lights_punctual].get_object().get(lightsObject) == SUCCESS) { + uint64_t light; + if (lightsObject["light"].get_uint64().get(light) == SUCCESS) { + node.lightsIndex = static_cast(light); + } } } + std::string_view name; + if (nodeObject["name"].get_string().get(name) == SUCCESS) { + node.name = std::string { name }; + } + parsedAsset->nodes.emplace_back(std::move(node)); } } diff --git a/src/fastgltf_parser.hpp b/src/fastgltf_parser.hpp index ddf20e2e0..f4cffa8f2 100644 --- a/src/fastgltf_parser.hpp +++ b/src/fastgltf_parser.hpp @@ -18,6 +18,7 @@ // fwd namespace simdjson::dom { class array; + class object; class parser; } @@ -67,6 +68,9 @@ namespace fastgltf { // See https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_meshopt_compression/README.md EXT_meshopt_compression = 1 << 5, + // See https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_lights_punctual/README.md + KHR_lights_punctual = 1 << 6, + // See https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_mesh_gpu_instancing/README.md EXT_mesh_gpu_instancing = 1 << 7, @@ -186,6 +190,7 @@ namespace fastgltf { constexpr std::string_view EXT_mesh_gpu_instancing = "EXT_mesh_gpu_instancing"; constexpr std::string_view EXT_meshopt_compression = "EXT_meshopt_compression"; constexpr std::string_view EXT_texture_webp = "EXT_texture_webp"; + constexpr std::string_view KHR_lights_punctual = "KHR_lights_punctual"; constexpr std::string_view KHR_mesh_quantization = "KHR_mesh_quantization"; constexpr std::string_view KHR_texture_basisu = "KHR_texture_basisu"; constexpr std::string_view KHR_texture_transform = "KHR_texture_transform"; @@ -234,7 +239,9 @@ namespace fastgltf { void parseBuffers(simdjson::dom::array& array); void parseBufferViews(simdjson::dom::array& array); void parseCameras(simdjson::dom::array& array); + void parseExtensions(simdjson::dom::object& extensionsObject); void parseImages(simdjson::dom::array& array); + void parseLights(simdjson::dom::array& array); void parseMaterials(simdjson::dom::array& array); void parseMeshes(simdjson::dom::array& array); void parseNodes(simdjson::dom::array& array); diff --git a/src/fastgltf_types.hpp b/src/fastgltf_types.hpp index 859e00259..1298a0411 100644 --- a/src/fastgltf_types.hpp +++ b/src/fastgltf_types.hpp @@ -169,6 +169,12 @@ namespace fastgltf { Quaternion, Exponential, }; + + enum class LightType : uint8_t { + Directional, + Spot, + Point, + }; // clang-format on #pragma endregion @@ -655,6 +661,7 @@ namespace fastgltf { std::optional meshIndex; std::optional skinIndex; std::optional cameraIndex; + std::optional lightsIndex; SmallVector children; SmallVector weights; @@ -844,6 +851,22 @@ namespace fastgltf { std::string name; }; + struct Light { + LightType type; + /** RGB light color in linear space. */ + std::array color; + + /** Point and spot lights use candela (lm/sr) while directional use lux (lm/m^2) */ + float intensity; + /** Range for point and spot lights. If not present, range is infinite. */ + std::optional range; + + std::optional innerConeAngle; + std::optional outerConeAngle; + + std::string name; + }; + struct Asset { /** * This will only ever have no value if Options::DontRequireValidAssetMember was specified. @@ -856,6 +879,7 @@ namespace fastgltf { std::vector bufferViews; std::vector cameras; std::vector images; + std::vector lights; std::vector materials; std::vector meshes; std::vector nodes; diff --git a/tests/basic_test.cpp b/tests/basic_test.cpp index 5366defa1..e13dc8dac 100644 --- a/tests/basic_test.cpp +++ b/tests/basic_test.cpp @@ -520,3 +520,30 @@ TEST_CASE("Validate morph target parsing", "[gltf-loader]") { REQUIRE(primitive.targets[1].contains("POSITION")); REQUIRE(primitive.targets[1]["POSITION"] == 3); } + +TEST_CASE("Test KHR_lights_punctual", "[gltf-loader]") { + auto lightsLamp = sampleModels / "2.0" / "LightsPunctualLamp" / "glTF"; + fastgltf::GltfDataBuffer jsonData; + jsonData.loadFromFile(lightsLamp / "LightsPunctualLamp.gltf"); + + fastgltf::Parser parser(fastgltf::Extensions::KHR_lights_punctual); + auto model = parser.loadGLTF(&jsonData, lightsLamp); + REQUIRE(parser.getError() == fastgltf::Error::None); + REQUIRE(model->parse(fastgltf::Category::All) == fastgltf::Error::None); + + auto asset = model->getParsedAsset(); + REQUIRE(asset->lights.size() == 5); + REQUIRE(asset->nodes.size() > 4); + + auto& nodes = asset->nodes; + REQUIRE(nodes[3].lightsIndex.has_value()); + REQUIRE(nodes[3].lightsIndex.value() == 0); + + auto& lights = asset->lights; + REQUIRE(lights[0].name == "Point"); + REQUIRE(lights[0].type == fastgltf::LightType::Point); + REQUIRE(lights[0].intensity == 15.0f); + REQUIRE(glm::epsilonEqual(lights[0].color[0], 1.0f, glm::epsilon())); + REQUIRE(glm::epsilonEqual(lights[0].color[1], 0.63187497854232788f, glm::epsilon())); + REQUIRE(glm::epsilonEqual(lights[0].color[2], 0.23909975588321689f, glm::epsilon())); +}