//// FSTReader.cpp////// Created by Clement on 3/26/15.// Copyright 2015 High Fidelity, Inc.// Copyright 2023 Overte e.V.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// #include "FSTReader.h" #include <QBuffer>#include <QEventLoop>#include <QNetworkReply>#include <QNetworkRequest> #include <NetworkAccessManager.h>#include <NetworkingConstants.h>#include <SharedUtil.h> const QStringList SINGLE_VALUE_PROPERTIES { NAME_FIELD, FILENAME_FIELD, TEXDIR_FIELD, SCRIPT_FIELD, WAIT_FOR_WEARABLES_FIELD, COMMENT_FIELD }; hifi::VariantMultiHash FSTReader::parseMapping(QIODevice* device) { hifi::VariantMultiHash properties; QByteArray line; while (!(line = device->readLine()).isEmpty()) { if ((line = line.trimmed()).startsWith('#')) { continue; // comment } QList<QByteArray> sections = line.split('='); if (sections.size() < 2) { continue; } // We can have URLs like: // filename = https://www.dropbox.com/scl/fi/xxx/avatar.fbx?rlkey=xxx&dl=1\n // These confuse the parser due to the presence of = in the URL. // // SINGLE_VALUE_PROPERTIES contains a list of things that may be URLs or contain an = // for some other reason, and that we know for sure contain only a single value after // the first =. // // Really though, we should just use JSON instead. QByteArray name = sections.at(0).trimmed(); bool isSingleValue = SINGLE_VALUE_PROPERTIES.contains(name); if (sections.size() == 2 || isSingleValue) { // As per the above, we can have '=' signs inside of URLs, so instead of // using the split string, just use everything after the first '='. QString value = line.mid(line.indexOf("=")+1).trimmed(); properties.insert(name, value); } else if (sections.size() == 3) { QVariantHash heading = properties.value(name).toHash(); heading.insert(sections.at(1).trimmed(), sections.at(2).trimmed()); properties.insert(name, heading); } else if (sections.size() >= 4) { QVariantHash heading = properties.value(name).toHash(); QVariantList contents; for (int i = 2; i < sections.size(); i++) { contents.append(sections.at(i).trimmed()); } heading.insert(sections.at(1).trimmed(), contents); properties.insert(name, heading); } } return properties;} static void removeBlendshape(QVariantHash& bs, const QString& key) { if (bs.contains(key)) { bs.remove(key); }} static void splitBlendshapes(hifi::VariantMultiHash& bs, const QString& key, const QString& leftKey, const QString& rightKey) { if (bs.contains(key) && !(bs.contains(leftKey) || bs.contains(rightKey))) { // key has been split into leftKey and rightKey blendshapes QVariantList origShapes = bs.values(key); QVariantList halfShapes; for (int i = 0; i < origShapes.size(); i++) { QVariantList origShape = origShapes[i].toList(); QVariantList halfShape; halfShape.append(origShape[0]); halfShape.append(QVariant(0.5f * origShape[1].toFloat())); bs.insert(leftKey, halfShape); bs.insert(rightKey, halfShape); } }} // convert legacy blendshapes to arkit blendshapesstatic void fixUpLegacyBlendshapes(hifi::VariantMultiHash & properties) { hifi::VariantMultiHash bs = properties.value("bs").toHash(); // These blendshapes have no ARKit equivalent, so we remove them. removeBlendshape(bs, "JawChew"); removeBlendshape(bs, "ChinLowerRaise"); removeBlendshape(bs, "ChinUpperRaise"); removeBlendshape(bs, "LipsUpperOpen"); removeBlendshape(bs, "LipsLowerOpen"); // These blendshapes are split in ARKit, we replace them with their left and right sides with a weight of 1/2. splitBlendshapes(bs, "LipsUpperUp", "MouthUpperUp_L", "MouthUpperUp_R"); splitBlendshapes(bs, "LipsLowerDown", "MouthLowerDown_L", "MouthLowerDown_R"); splitBlendshapes(bs, "Sneer", "NoseSneer_L", "NoseSneer_R"); // re-insert new mutated bs hash into mapping properties. properties.insert("bs", bs);} hifi::VariantMultiHash FSTReader::readMapping(const QByteArray& data) { QBuffer buffer(const_cast<QByteArray*>(&data)); buffer.open(QIODevice::ReadOnly); hifi::VariantMultiHash mapping = FSTReader::parseMapping(&buffer); fixUpLegacyBlendshapes(mapping); return mapping;} void FSTReader::writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it) { QByteArray key = it.key().toUtf8() + " = "; QVariantHash hashValue = it.value().toHash(); if (hashValue.isEmpty()) { buffer.write(key + it.value().toByteArray() + "\n"); return; } for (QVariantHash::const_iterator second = hashValue.constBegin(); second != hashValue.constEnd(); second++) { QByteArray extendedKey = key + second.key().toUtf8(); QVariantList listValue = second.value().toList(); if (listValue.isEmpty()) { buffer.write(extendedKey + " = " + second.value().toByteArray() + "\n"); continue; } buffer.write(extendedKey); for (QVariantList::const_iterator third = listValue.constBegin(); third != listValue.constEnd(); third++) { buffer.write(" = " + third->toByteArray()); } buffer.write("\n"); }} QByteArray FSTReader::writeMapping(const hifi::VariantMultiHash& mapping) { static const QStringList PREFERED_ORDER = QStringList() << NAME_FIELD << TYPE_FIELD << SCALE_FIELD << FILENAME_FIELD << TEXDIR_FIELD << SCRIPT_FIELD << JOINT_FIELD << BLENDSHAPE_FIELD << JOINT_INDEX_FIELD; QBuffer buffer; buffer.open(QIODevice::WriteOnly); for (auto key : PREFERED_ORDER) { auto it = mapping.find(key); if (it != mapping.constEnd()) { if (key == SCRIPT_FIELD) { // writeVariant does not handle strings added using insertMulti. for (auto multi : mapping.values(key)) { buffer.write(key.toUtf8()); buffer.write(" = "); buffer.write(multi.toByteArray()); buffer.write("\n"); } } else { writeVariant(buffer, it); } } } for (auto it = mapping.constBegin(); it != mapping.constEnd(); it++) { if (!PREFERED_ORDER.contains(it.key())) { writeVariant(buffer, it); } } return buffer.data();} QHash<FSTReader::ModelType, QString> FSTReader::_typesToNames;QString FSTReader::getNameFromType(ModelType modelType) { if (_typesToNames.size() == 0) { _typesToNames[ENTITY_MODEL] = "entity"; _typesToNames[HEAD_MODEL] = "head"; _typesToNames[BODY_ONLY_MODEL] = "body"; _typesToNames[HEAD_AND_BODY_MODEL] = "body+head"; } return _typesToNames[modelType];} QHash<QString, FSTReader::ModelType> FSTReader::_namesToTypes;FSTReader::ModelType FSTReader::getTypeFromName(const QString& name) { if (_namesToTypes.size() == 0) { _namesToTypes["entity"] = ENTITY_MODEL; _namesToTypes["head"] = HEAD_MODEL ; _namesToTypes["body"] = BODY_ONLY_MODEL; _namesToTypes["body+head"] = HEAD_AND_BODY_MODEL; } return _namesToTypes[name];} FSTReader::ModelType FSTReader::predictModelType(const hifi::VariantMultiHash& mapping) { QVariantHash joints; if (mapping.contains("joint") && mapping.value("joint").type() == QVariant::Hash) { joints = mapping.value("joint").toHash(); } // if the mapping includes the type hint... then we trust the mapping if (mapping.contains(TYPE_FIELD)) { return FSTReader::getTypeFromName(mapping.value(TYPE_FIELD).toString()); } // check for blendshapes bool hasBlendshapes = mapping.contains(BLENDSHAPE_FIELD); // a Head needs to have these minimum fields... //joint = jointEyeLeft = EyeL = 1 //joint = jointEyeRight = EyeR = 1 //joint = jointNeck = Head = 1 bool hasHeadMinimum = joints.contains("jointNeck") && joints.contains("jointEyeLeft") && joints.contains("jointEyeRight"); // a Body needs to have these minimum fields... //joint = jointRoot = Hips //joint = jointLean = Spine //joint = jointNeck = Neck //joint = jointHead = HeadTop_End bool hasBodyMinimumJoints = joints.contains("jointRoot") && joints.contains("jointLean") && joints.contains("jointNeck") && joints.contains("jointHead"); bool isLikelyHead = hasBlendshapes || hasHeadMinimum; if (isLikelyHead && hasBodyMinimumJoints) { return HEAD_AND_BODY_MODEL; } if (isLikelyHead) { return HEAD_MODEL; } if (hasBodyMinimumJoints) { return BODY_ONLY_MODEL; } return ENTITY_MODEL;} QVector<QString> FSTReader::getScripts(const QUrl& url, const hifi::VariantMultiHash& mapping) { auto fstMapping = mapping.isEmpty() ? downloadMapping(url.toString()) : mapping; QVector<QString> scriptPaths; if (!fstMapping.value(SCRIPT_FIELD).isNull()) { auto scripts = fstMapping.values(SCRIPT_FIELD).toVector(); for (auto &script : scripts) { QString scriptPath = script.toString(); if (QUrl(scriptPath).isRelative()) { if (scriptPath.at(0) == '/') { scriptPath = scriptPath.right(scriptPath.length() - 1); } scriptPath = url.resolved(QUrl(scriptPath)).toString(); } scriptPaths.push_back(scriptPath); } } return scriptPaths;} hifi::VariantMultiHash FSTReader::downloadMapping(const QString& url) { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest networkRequest = QNetworkRequest(url); networkRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); networkRequest.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::OVERTE_USER_AGENT); QNetworkReply* reply = networkAccessManager.get(networkRequest); QEventLoop loop; QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); loop.exec(); QByteArray fstContents = reply->readAll(); delete reply; return FSTReader::readMapping(fstContents);}
//
// FSTReader.cpp
//
//
// Created by Clement on 3/26/15.
// Copyright 2015 High Fidelity, Inc.
// Copyright 2023 Overte e.V.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "FSTReader.h"
#include <QBuffer>
#include <QEventLoop>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <NetworkAccessManager.h>
#include <NetworkingConstants.h>
#include <SharedUtil.h>
const QStringList SINGLE_VALUE_PROPERTIES { NAME_FIELD, FILENAME_FIELD, TEXDIR_FIELD, SCRIPT_FIELD, WAIT_FOR_WEARABLES_FIELD, COMMENT_FIELD };
hifi::VariantMultiHash FSTReader::parseMapping(QIODevice* device) {
hifi::VariantMultiHash properties;
QByteArray line;
while (!(line = device->readLine()).isEmpty()) {
if ((line = line.trimmed()).startsWith('#')) {
continue; // comment
}
QList<QByteArray> sections = line.split('=');
if (sections.size() < 2) {
continue;
}
// We can have URLs like:
// filename = https://www.dropbox.com/scl/fi/xxx/avatar.fbx?rlkey=xxx&dl=1\n
// These confuse the parser due to the presence of = in the URL.
//
// SINGLE_VALUE_PROPERTIES contains a list of things that may be URLs or contain an =
// for some other reason, and that we know for sure contain only a single value after
// the first =.
//
// Really though, we should just use JSON instead.
QByteArray name = sections.at(0).trimmed();
bool isSingleValue = SINGLE_VALUE_PROPERTIES.contains(name);
if (sections.size() == 2 || isSingleValue) {
// As per the above, we can have '=' signs inside of URLs, so instead of
// using the split string, just use everything after the first '='.
QString value = line.mid(line.indexOf("=")+1).trimmed();
properties.insert(name, value);
} else if (sections.size() == 3) {
QVariantHash heading = properties.value(name).toHash();
heading.insert(sections.at(1).trimmed(), sections.at(2).trimmed());
properties.insert(name, heading);
} else if (sections.size() >= 4) {
QVariantHash heading = properties.value(name).toHash();
QVariantList contents;
for (int i = 2; i < sections.size(); i++) {
contents.append(sections.at(i).trimmed());
}
heading.insert(sections.at(1).trimmed(), contents);
properties.insert(name, heading);
}
}
return properties;
}
static void removeBlendshape(QVariantHash& bs, const QString& key) {
if (bs.contains(key)) {
bs.remove(key);
}
}
static void splitBlendshapes(hifi::VariantMultiHash& bs, const QString& key, const QString& leftKey, const QString& rightKey) {
if (bs.contains(key) && !(bs.contains(leftKey) || bs.contains(rightKey))) {
// key has been split into leftKey and rightKey blendshapes
QVariantList origShapes = bs.values(key);
QVariantList halfShapes;
for (int i = 0; i < origShapes.size(); i++) {
QVariantList origShape = origShapes[i].toList();
QVariantList halfShape;
halfShape.append(origShape[0]);
halfShape.append(QVariant(0.5f * origShape[1].toFloat()));
bs.insert(leftKey, halfShape);
bs.insert(rightKey, halfShape);
}
}
}
// convert legacy blendshapes to arkit blendshapes
static void fixUpLegacyBlendshapes(hifi::VariantMultiHash & properties) {
hifi::VariantMultiHash bs = properties.value("bs").toHash();
// These blendshapes have no ARKit equivalent, so we remove them.
removeBlendshape(bs, "JawChew");
removeBlendshape(bs, "ChinLowerRaise");
removeBlendshape(bs, "ChinUpperRaise");
removeBlendshape(bs, "LipsUpperOpen");
removeBlendshape(bs, "LipsLowerOpen");
// These blendshapes are split in ARKit, we replace them with their left and right sides with a weight of 1/2.
splitBlendshapes(bs, "LipsUpperUp", "MouthUpperUp_L", "MouthUpperUp_R");
splitBlendshapes(bs, "LipsLowerDown", "MouthLowerDown_L", "MouthLowerDown_R");
splitBlendshapes(bs, "Sneer", "NoseSneer_L", "NoseSneer_R");
// re-insert new mutated bs hash into mapping properties.
properties.insert("bs", bs);
}
hifi::VariantMultiHash FSTReader::readMapping(const QByteArray& data) {
QBuffer buffer(const_cast<QByteArray*>(&data));
buffer.open(QIODevice::ReadOnly);
hifi::VariantMultiHash mapping = FSTReader::parseMapping(&buffer);
fixUpLegacyBlendshapes(mapping);
return mapping;
}
void FSTReader::writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it) {
QByteArray key = it.key().toUtf8() + " = ";
QVariantHash hashValue = it.value().toHash();
if (hashValue.isEmpty()) {
buffer.write(key + it.value().toByteArray() + "\n");
return;
}
for (QVariantHash::const_iterator second = hashValue.constBegin(); second != hashValue.constEnd(); second++) {
QByteArray extendedKey = key + second.key().toUtf8();
QVariantList listValue = second.value().toList();
if (listValue.isEmpty()) {
buffer.write(extendedKey + " = " + second.value().toByteArray() + "\n");
continue;
}
buffer.write(extendedKey);
for (QVariantList::const_iterator third = listValue.constBegin(); third != listValue.constEnd(); third++) {
buffer.write(" = " + third->toByteArray());
}
buffer.write("\n");
}
}
QByteArray FSTReader::writeMapping(const hifi::VariantMultiHash& mapping) {
static const QStringList PREFERED_ORDER = QStringList() << NAME_FIELD << TYPE_FIELD << SCALE_FIELD << FILENAME_FIELD
<< TEXDIR_FIELD << SCRIPT_FIELD << JOINT_FIELD
<< BLENDSHAPE_FIELD << JOINT_INDEX_FIELD;
QBuffer buffer;
buffer.open(QIODevice::WriteOnly);
for (auto key : PREFERED_ORDER) {
auto it = mapping.find(key);
if (it != mapping.constEnd()) {
if (key == SCRIPT_FIELD) { // writeVariant does not handle strings added using insertMulti.
for (auto multi : mapping.values(key)) {
buffer.write(key.toUtf8());
buffer.write(" = ");
buffer.write(multi.toByteArray());
buffer.write("\n");
}
} else {
writeVariant(buffer, it);
}
}
}
for (auto it = mapping.constBegin(); it != mapping.constEnd(); it++) {
if (!PREFERED_ORDER.contains(it.key())) {
writeVariant(buffer, it);
}
}
return buffer.data();
}
QHash<FSTReader::ModelType, QString> FSTReader::_typesToNames;
QString FSTReader::getNameFromType(ModelType modelType) {
if (_typesToNames.size() == 0) {
_typesToNames[ENTITY_MODEL] = "entity";
_typesToNames[HEAD_MODEL] = "head";
_typesToNames[BODY_ONLY_MODEL] = "body";
_typesToNames[HEAD_AND_BODY_MODEL] = "body+head";
}
return _typesToNames[modelType];
}
QHash<QString, FSTReader::ModelType> FSTReader::_namesToTypes;
FSTReader::ModelType FSTReader::getTypeFromName(const QString& name) {
if (_namesToTypes.size() == 0) {
_namesToTypes["entity"] = ENTITY_MODEL;
_namesToTypes["head"] = HEAD_MODEL ;
_namesToTypes["body"] = BODY_ONLY_MODEL;
_namesToTypes["body+head"] = HEAD_AND_BODY_MODEL;
}
return _namesToTypes[name];
}
FSTReader::ModelType FSTReader::predictModelType(const hifi::VariantMultiHash& mapping) {
QVariantHash joints;
if (mapping.contains("joint") && mapping.value("joint").type() == QVariant::Hash) {
joints = mapping.value("joint").toHash();
}
// if the mapping includes the type hint... then we trust the mapping
if (mapping.contains(TYPE_FIELD)) {
return FSTReader::getTypeFromName(mapping.value(TYPE_FIELD).toString());
}
// check for blendshapes
bool hasBlendshapes = mapping.contains(BLENDSHAPE_FIELD);
// a Head needs to have these minimum fields...
//joint = jointEyeLeft = EyeL = 1
//joint = jointEyeRight = EyeR = 1
//joint = jointNeck = Head = 1
bool hasHeadMinimum = joints.contains("jointNeck") && joints.contains("jointEyeLeft") && joints.contains("jointEyeRight");
// a Body needs to have these minimum fields...
//joint = jointRoot = Hips
//joint = jointLean = Spine
//joint = jointNeck = Neck
//joint = jointHead = HeadTop_End
bool hasBodyMinimumJoints = joints.contains("jointRoot") && joints.contains("jointLean") && joints.contains("jointNeck")
&& joints.contains("jointHead");
bool isLikelyHead = hasBlendshapes || hasHeadMinimum;
if (isLikelyHead && hasBodyMinimumJoints) {
return HEAD_AND_BODY_MODEL;
}
if (isLikelyHead) {
return HEAD_MODEL;
}
if (hasBodyMinimumJoints) {
return BODY_ONLY_MODEL;
}
return ENTITY_MODEL;
}
QVector<QString> FSTReader::getScripts(const QUrl& url, const hifi::VariantMultiHash& mapping) {
auto fstMapping = mapping.isEmpty() ? downloadMapping(url.toString()) : mapping;
QVector<QString> scriptPaths;
if (!fstMapping.value(SCRIPT_FIELD).isNull()) {
auto scripts = fstMapping.values(SCRIPT_FIELD).toVector();
for (auto &script : scripts) {
QString scriptPath = script.toString();
if (QUrl(scriptPath).isRelative()) {
if (scriptPath.at(0) == '/') {
scriptPath = scriptPath.right(scriptPath.length() - 1);
}
scriptPath = url.resolved(QUrl(scriptPath)).toString();
}
scriptPaths.push_back(scriptPath);
}
}
return scriptPaths;
}
hifi::VariantMultiHash FSTReader::downloadMapping(const QString& url) {
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest = QNetworkRequest(url);
networkRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::OVERTE_USER_AGENT);
QNetworkReply* reply = networkAccessManager.get(networkRequest);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
QByteArray fstContents = reply->readAll();
delete reply;
return FSTReader::readMapping(fstContents);
}