nori/src/parser.cpp

320 lines
14 KiB
C++

/*
This file is part of Nori, a simple educational ray tracer
Copyright (c) 2015 by Wenzel Jakob
Nori is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License Version 3
as published by the Free Software Foundation.
Nori 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, see <http://www.gnu.org/licenses/>.
*/
#include <nori/parser.h>
#include <nori/proplist.h>
#include <Eigen/Geometry>
#include <pugixml.hpp>
#include <fstream>
#include <set>
NORI_NAMESPACE_BEGIN
NoriObject *loadFromXML(const std::string &filename) {
/* Load the XML file using 'pugi' (a tiny self-contained XML parser implemented in C++) */
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(filename.c_str());
/* Helper function: map a position offset in bytes to a more readable line/column value */
auto offset = [&](ptrdiff_t pos) -> std::string {
std::fstream is(filename);
char buffer[1024];
int line = 0, linestart = 0, offset = 0;
while (is.good()) {
is.read(buffer, sizeof(buffer));
for (int i = 0; i < is.gcount(); ++i) {
if (buffer[i] == '\n') {
if (offset + i >= pos)
return tfm::format("line %i, col %i", line + 1, pos - linestart);
++line;
linestart = offset + i;
}
}
offset += (int) is.gcount();
}
return "byte offset " + std::to_string(pos);
};
if (!result) /* There was a parser / file IO error */
throw NoriException("Error while parsing \"%s\": %s (at %s)", filename, result.description(), offset(result.offset));
/* Set of supported XML tags */
enum ETag {
/* Object classes */
EScene = NoriObject::EScene,
EMesh = NoriObject::EMesh,
EBSDF = NoriObject::EBSDF,
EPhaseFunction = NoriObject::EPhaseFunction,
EEmitter = NoriObject::EEmitter,
EMedium = NoriObject::EMedium,
ECamera = NoriObject::ECamera,
EIntegrator = NoriObject::EIntegrator,
ESampler = NoriObject::ESampler,
ETest = NoriObject::ETest,
EReconstructionFilter = NoriObject::EReconstructionFilter,
/* Properties */
EBoolean = NoriObject::EClassTypeCount,
EInteger,
EFloat,
EString,
EPoint,
EVector,
EColor,
ETransform,
ETranslate,
EMatrix,
ERotate,
EScale,
ELookAt,
EInvalid
};
/* Create a mapping from tag names to tag IDs */
std::map<std::string, ETag> tags;
tags["scene"] = EScene;
tags["mesh"] = EMesh;
tags["bsdf"] = EBSDF;
tags["emitter"] = EEmitter;
tags["camera"] = ECamera;
tags["medium"] = EMedium;
tags["phase"] = EPhaseFunction;
tags["integrator"] = EIntegrator;
tags["sampler"] = ESampler;
tags["rfilter"] = EReconstructionFilter;
tags["test"] = ETest;
tags["boolean"] = EBoolean;
tags["integer"] = EInteger;
tags["float"] = EFloat;
tags["string"] = EString;
tags["point"] = EPoint;
tags["vector"] = EVector;
tags["color"] = EColor;
tags["transform"] = ETransform;
tags["translate"] = ETranslate;
tags["matrix"] = EMatrix;
tags["rotate"] = ERotate;
tags["scale"] = EScale;
tags["lookat"] = ELookAt;
/* Helper function to check if attributes are fully specified */
auto check_attributes = [&](const pugi::xml_node &node, std::set<std::string> attrs) {
for (auto attr : node.attributes()) {
auto it = attrs.find(attr.name());
if (it == attrs.end())
throw NoriException("Error while parsing \"%s\": unexpected attribute \"%s\" in \"%s\" at %s",
filename, attr.name(), node.name(), offset(node.offset_debug()));
attrs.erase(it);
}
if (!attrs.empty())
throw NoriException("Error while parsing \"%s\": missing attribute \"%s\" in \"%s\" at %s",
filename, *attrs.begin(), node.name(), offset(node.offset_debug()));
};
Eigen::Affine3f transform;
/* Helper function to parse a Nori XML node (recursive) */
std::function<NoriObject *(pugi::xml_node &, PropertyList &, int)> parseTag = [&](
pugi::xml_node &node, PropertyList &list, int parentTag) -> NoriObject * {
/* Skip over comments */
if (node.type() == pugi::node_comment || node.type() == pugi::node_declaration)
return nullptr;
if (node.type() != pugi::node_element)
throw NoriException(
"Error while parsing \"%s\": unexpected content at %s",
filename, offset(node.offset_debug()));
/* Look up the name of the current element */
auto it = tags.find(node.name());
if (it == tags.end())
throw NoriException("Error while parsing \"%s\": unexpected tag \"%s\" at %s",
filename, node.name(), offset(node.offset_debug()));
int tag = it->second;
/* Perform some safety checks to make sure that the XML tree really makes sense */
bool hasParent = parentTag != EInvalid;
bool parentIsObject = hasParent && parentTag < NoriObject::EClassTypeCount;
bool currentIsObject = tag < NoriObject::EClassTypeCount;
bool parentIsTransform = parentTag == ETransform;
bool currentIsTransformOp = tag == ETranslate || tag == ERotate || tag == EScale || tag == ELookAt || tag == EMatrix;
if (!hasParent && !currentIsObject)
throw NoriException("Error while parsing \"%s\": root element \"%s\" must be a Nori object (at %s)",
filename, node.name(), offset(node.offset_debug()));
if (parentIsTransform != currentIsTransformOp)
throw NoriException("Error while parsing \"%s\": transform nodes "
"can only contain transform operations (at %s)",
filename, offset(node.offset_debug()));
if (hasParent && !parentIsObject && !(parentIsTransform && currentIsTransformOp))
throw NoriException("Error while parsing \"%s\": node \"%s\" requires a Nori object as parent (at %s)",
filename, node.name(), offset(node.offset_debug()));
if (tag == EScene)
node.append_attribute("type") = "scene";
else if (tag == ETransform)
transform.setIdentity();
PropertyList propList;
std::vector<NoriObject *> children;
for (pugi::xml_node &ch: node.children()) {
NoriObject *child = parseTag(ch, propList, tag);
if (child)
children.push_back(child);
}
NoriObject *result = nullptr;
try {
if (currentIsObject) {
check_attributes(node, { "type" });
/* This is an object, first instantiate it */
result = NoriObjectFactory::createInstance(
node.attribute("type").value(),
propList
);
if (result->getClassType() != (int) tag) {
throw NoriException(
"Unexpectedly constructed an object "
"of type <%s> (expected type <%s>): %s",
NoriObject::classTypeName(result->getClassType()),
NoriObject::classTypeName((NoriObject::EClassType) tag),
result->toString());
}
/* Add all children */
for (auto ch: children) {
result->addChild(ch);
ch->setParent(result);
}
/* Activate / configure the object */
result->activate();
} else {
/* This is a property */
switch (tag) {
case EString: {
check_attributes(node, { "name", "value" });
list.setString(node.attribute("name").value(), node.attribute("value").value());
}
break;
case EFloat: {
check_attributes(node, { "name", "value" });
list.setFloat(node.attribute("name").value(), toFloat(node.attribute("value").value()));
}
break;
case EInteger: {
check_attributes(node, { "name", "value" });
list.setInteger(node.attribute("name").value(), toInt(node.attribute("value").value()));
}
break;
case EBoolean: {
check_attributes(node, { "name", "value" });
list.setBoolean(node.attribute("name").value(), toBool(node.attribute("value").value()));
}
break;
case EPoint: {
check_attributes(node, { "name", "value" });
list.setPoint(node.attribute("name").value(), Point3f(toVector3f(node.attribute("value").value())));
}
break;
case EVector: {
check_attributes(node, { "name", "value" });
list.setVector(node.attribute("name").value(), Vector3f(toVector3f(node.attribute("value").value())));
}
break;
case EColor: {
check_attributes(node, { "name", "value" });
list.setColor(node.attribute("name").value(), Color3f(toVector3f(node.attribute("value").value()).array()));
}
break;
case ETransform: {
check_attributes(node, { "name" });
list.setTransform(node.attribute("name").value(), transform.matrix());
}
break;
case ETranslate: {
check_attributes(node, { "value" });
Eigen::Vector3f v = toVector3f(node.attribute("value").value());
transform = Eigen::Translation<float, 3>(v.x(), v.y(), v.z()) * transform;
}
break;
case EMatrix: {
check_attributes(node, { "value" });
std::vector<std::string> tokens = tokenize(node.attribute("value").value());
if (tokens.size() != 16)
throw NoriException("Expected 16 values");
Eigen::Matrix4f matrix;
for (int i=0; i<4; ++i)
for (int j=0; j<4; ++j)
matrix(i, j) = toFloat(tokens[i*4+j]);
transform = Eigen::Affine3f(matrix) * transform;
}
break;
case EScale: {
check_attributes(node, { "value" });
Eigen::Vector3f v = toVector3f(node.attribute("value").value());
transform = Eigen::DiagonalMatrix<float, 3>(v) * transform;
}
break;
case ERotate: {
check_attributes(node, { "angle", "axis" });
float angle = degToRad(toFloat(node.attribute("angle").value()));
Eigen::Vector3f axis = toVector3f(node.attribute("axis").value()).normalized();
transform = Eigen::AngleAxis<float>(angle, axis) * transform;
}
break;
case ELookAt: {
check_attributes(node, { "origin", "target", "up" });
Eigen::Vector3f origin = toVector3f(node.attribute("origin").value());
Eigen::Vector3f target = toVector3f(node.attribute("target").value());
Eigen::Vector3f up = toVector3f(node.attribute("up").value());
Vector3f dir = (target - origin).normalized();
Vector3f left = up.normalized().cross(dir).normalized();
Vector3f newUp = dir.cross(left).normalized();
Eigen::Matrix4f trafo;
trafo << left, newUp, dir, origin,
0, 0, 0, 1;
transform = Eigen::Affine3f(trafo) * transform;
}
break;
default: throw NoriException("Unhandled element \"%s\"", node.name());
};
}
} catch (const NoriException &e) {
throw NoriException("Error while parsing \"%s\": %s (at %s)", filename,
e.what(), offset(node.offset_debug()));
}
return result;
};
PropertyList list;
return parseTag(*doc.begin(), list, EInvalid);
}
NORI_NAMESPACE_END