843 lines
27 KiB
C++
843 lines
27 KiB
C++
/*
|
|
This file is part of Mitsuba, a physically based rendering system.
|
|
|
|
Copyright (c) 2007-2012 by Wenzel Jakob and others.
|
|
|
|
Mitsuba 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.
|
|
|
|
Mitsuba 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 <mitsuba/render/trimesh.h>
|
|
#include <mitsuba/core/plugin.h>
|
|
#include <mitsuba/core/fresolver.h>
|
|
#include <mitsuba/core/timer.h>
|
|
#include <mitsuba/render/emitter.h>
|
|
#include <mitsuba/render/bsdf.h>
|
|
#include <mitsuba/render/subsurface.h>
|
|
#include <mitsuba/render/medium.h>
|
|
#include <mitsuba/render/sensor.h>
|
|
#include <mitsuba/hw/basicshader.h>
|
|
#include <set>
|
|
|
|
MTS_NAMESPACE_BEGIN
|
|
|
|
/*!\plugin{obj}{Wavefront OBJ mesh loader}
|
|
* \order{5}
|
|
* \parameters{
|
|
* \parameter{filename}{\String}{
|
|
* Filename of the OBJ file that should be loaded
|
|
* }
|
|
* \parameter{faceNormals}{\Boolean}{
|
|
* When set to \code{true}, any existing or computed vertex normals are
|
|
* discarded and \emph{face normals} will instead be used during rendering.
|
|
* This gives the rendered object a faceted appearance.\default{\code{false}}
|
|
* }
|
|
* \parameter{maxSmoothAngle}{\Float}{
|
|
* When specified, Mitsuba will discard all vertex normals in the input mesh and rebuild
|
|
* them in a way that is sensitive to the presence of creases and corners. For more
|
|
* details on this parameter, see below. Disabled by default.
|
|
* }
|
|
* \parameter{flipNormals}{\Boolean}{
|
|
* Optional flag to flip all normals. \default{\code{false}, i.e.
|
|
* the normals are left unchanged}.
|
|
* }
|
|
* \parameter{flipTexCoords}{\Boolean}{
|
|
* Treat the vertical component of the texture as inverted? Most OBJ files use
|
|
* this convention. \default{\code{true}}
|
|
* }
|
|
* \parameter{toWorld}{\Transform\Or\Animation}{
|
|
* Specifies an optional linear object-to-world transformation.
|
|
* \default{none (i.e. object space $=$ world space)}
|
|
* }
|
|
* \parameter{shapeIndex}{\Integer}{
|
|
* When the file contains multiple meshes, this parameter can
|
|
* be used to select a single one. \default{\code{-1}, \mbox{i.e. load all}}
|
|
* }
|
|
* \parameter{collapse}{\Boolean}{
|
|
* Collapse all meshes into a single shape \default{\code{false}}
|
|
* }
|
|
* }
|
|
* \renderings{
|
|
* \label{fig:rungholt}
|
|
* \bigrendering{An example scene with both geometry and materials imported using the Wavefront OBJ mesh loader
|
|
* (Neu Rungholt model courtesy of \texttt{kescha}, converted from Minecraft to OBJ by Morgan McGuire)}{shape_obj}
|
|
* }
|
|
*
|
|
* This plugin implements a simple loader for Wavefront OBJ files. It handles
|
|
* meshes containing triangles and quadrilaterals, and it also imports vertex normals
|
|
* and texture coordinates.
|
|
*
|
|
* Loading an ordinary OBJ file is as simple as writing:
|
|
* \begin{xml}
|
|
* <shape type="obj">
|
|
* <string name="filename" value="myShape.obj"/>
|
|
* </shape>
|
|
* \end{xml}
|
|
* \paragraph{Material import:}
|
|
* When the OBJ file references a Wavefront material description (a \code{.mtl} file),
|
|
* Mitsuba attempts to reproduce the material within and associate it with the shape.
|
|
* This is restricted to fairly basic materials and textures, hence in most cases
|
|
* it will be preferable to override this behavior by specifying an
|
|
* explicit Mitsuba BSDF that should be used instead.
|
|
* This can be done by passing it as a child argument, e.g.
|
|
* \begin{xml}
|
|
* <shape type="obj">
|
|
* <string name="filename" value="myShape.obj"/>
|
|
* <bsdf type="roughplastic">
|
|
* <rgb name="diffuseReflectance" value="0.2, 0.6, 0.3"/>
|
|
* </bsdf>
|
|
* </shape>
|
|
* \end{xml}
|
|
* The \code{mtl} material attributes that are automatically handled by Mitsuba include:
|
|
* \begin{itemize}
|
|
* \item Diffuse and glossy materials (optionally textured)
|
|
* \item Smooth glass and metal
|
|
* \item Textured transparency
|
|
* \item Bump maps
|
|
* \end{itemize}
|
|
*
|
|
* In some cases, OBJ files contain \emph{multiple} objects with different associated
|
|
* materials. In this case, the materials can be overwritten individually, by specifying
|
|
* the corresponding names. For instance, if the OBJ file contains two materials named
|
|
* \code{Glass} and \code{Water}, these can be overwritten as follows
|
|
* \begin{xml}
|
|
* <shape type="obj">
|
|
* <string name="filename" value="myShape.obj"/>
|
|
* <bsdf name="Glass" type="dielectric">
|
|
* <float name="intIOR" value="1.5"/>
|
|
* </bsdf>
|
|
* <bsdf name="Water" type="dielectric">
|
|
* <float name="intIOR" value="1.333"/>
|
|
* </bsdf>
|
|
* </shape>
|
|
* \end{xml}
|
|
* \paragraph{The \code{maxSmoothAngle} parameter:}
|
|
* \label{sec:maxSmoothAngle}
|
|
* When given a mesh without vertex normals, Mitsuba will by default
|
|
* create a smoothly varying normal field over the entire shape. This can produce
|
|
* undesirable output when the input mesh contains regions that are intentionally
|
|
* not smooth (i.e. corners, creases). Meshes that do include vertex
|
|
* normals sometimes incorrectly interpolate normals over such regions, leading to
|
|
* much the same problem.
|
|
*
|
|
* The \code{maxSmoothAngle} parameter can be issued to force inspection of the dihedral angle associated with
|
|
* each edge in the input mesh and disable normal interpolation locally where this angle exceeds
|
|
* a certain threshold value. A reasonable value might be something like \code{30} (degrees).
|
|
* The underlying analysis is somewhat costly and hence this parameter should only be used when it is
|
|
* actually needed (i.e. when the mesh contains creases or edges and does not come with
|
|
* valid vertex normals).
|
|
*
|
|
* \remarks{
|
|
* \item Importing geometry via OBJ files should only be used as an absolutely
|
|
* last resort. Due to inherent limitations of this format, the files tend to be unreasonably
|
|
* large, and parsing them requires significant amounts of memory and processing power. What's worse
|
|
* is that the internally stored data is often truncated, causing a loss of precision.
|
|
*
|
|
* If possible, use the \pluginref{ply} or \pluginref{serialized} plugins instead. For convenience, it
|
|
* is also possible to convert legacy OBJ files into \code{.serialized} files using the \code{mtsimport}
|
|
* utility. Using the resulting output will significantly accelerate the scene loading time.
|
|
* }
|
|
*/
|
|
class WavefrontOBJ : public Shape {
|
|
public:
|
|
struct OBJTriangle {
|
|
int p[3];
|
|
int n[3];
|
|
int uv[3];
|
|
|
|
inline OBJTriangle() {
|
|
memset(this, 0, sizeof(OBJTriangle));
|
|
}
|
|
};
|
|
|
|
bool fetch_line(std::istream &is, std::string &line) {
|
|
/// Fetch a line from the stream, while handling line breaks with backslashes
|
|
if (!std::getline(is, line))
|
|
return false;
|
|
if (line == "")
|
|
return true;
|
|
int lastCharacter = (int) line.size() - 1;
|
|
while (lastCharacter >= 0 &&
|
|
(line[lastCharacter] == '\r' ||
|
|
line[lastCharacter] == '\n' ||
|
|
line[lastCharacter] == '\t' ||
|
|
line[lastCharacter] == ' '))
|
|
lastCharacter--;
|
|
|
|
if (lastCharacter >= 0 && line[lastCharacter] == '\\') {
|
|
std::string nextLine;
|
|
fetch_line(is, nextLine);
|
|
line = line.substr(0, lastCharacter) + nextLine;
|
|
} else {
|
|
line.resize(lastCharacter+1);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
WavefrontOBJ(const Properties &props) : Shape(props) {
|
|
ref<FileResolver> fileResolver = Thread::getThread()->getFileResolver()->clone();
|
|
fs::path path = fileResolver->resolve(props.getString("filename"));
|
|
|
|
m_name = path.stem().string();
|
|
|
|
/* By default, any existing normals will be used for
|
|
rendering. If no normals are found, Mitsuba will
|
|
automatically generate smooth vertex normals.
|
|
Setting the 'faceNormals' parameter instead forces
|
|
the use of face normals, which will result in a faceted
|
|
appearance.
|
|
*/
|
|
m_faceNormals = props.getBoolean("faceNormals", false);
|
|
|
|
/* Causes all normals to be flipped */
|
|
m_flipNormals = props.getBoolean("flipNormals", false);
|
|
|
|
/* Collapse all contained shapes / groups into a single object? */
|
|
m_collapse = props.getBoolean("collapse", false);
|
|
|
|
/* Causes all texture coordinates to be vertically flipped */
|
|
bool flipTexCoords = props.getBoolean("flipTexCoords", true);
|
|
|
|
/// When the file contains multiple meshes, this index specifies which one to load
|
|
int shapeIndex = props.getInteger("shapeIndex", -1);
|
|
|
|
/* Object-space -> World-space transformation */
|
|
Transform objectToWorld = props.getTransform("toWorld", Transform());
|
|
|
|
/* Load the geometry */
|
|
Log(EInfo, "Loading geometry from \"%s\" ..", path.filename().string().c_str());
|
|
fs::ifstream is(path);
|
|
if (is.bad() || is.fail())
|
|
Log(EError, "Wavefront OBJ file '%s' not found!", path.string().c_str());
|
|
|
|
fileResolver->prependPath(fs::absolute(path).parent_path());
|
|
|
|
ref<Timer> timer = new Timer();
|
|
std::string buf;
|
|
std::vector<Point> vertices;
|
|
std::vector<Normal> normals;
|
|
std::vector<Point2> texcoords;
|
|
std::vector<OBJTriangle> triangles;
|
|
std::string name = m_name, line;
|
|
std::set<std::string> geomNames;
|
|
std::vector<Vertex> vertexBuffer;
|
|
fs::path materialLibrary;
|
|
int geomIndex = 0;
|
|
bool nameBeforeGeometry = false;
|
|
std::string materialName;
|
|
|
|
while (is.good() && !is.eof() && fetch_line(is, line)) {
|
|
std::istringstream iss(line);
|
|
if (!(iss >> buf))
|
|
continue;
|
|
|
|
if (buf == "v") {
|
|
/* Parse + transform vertices */
|
|
Point p;
|
|
iss >> p.x >> p.y >> p.z;
|
|
vertices.push_back(p);
|
|
} else if (buf == "vn") {
|
|
Normal n;
|
|
iss >> n.x >> n.y >> n.z;
|
|
normals.push_back(n);
|
|
} else if (buf == "g" && !m_collapse) {
|
|
std::string targetName;
|
|
std::string newName = trim(line.substr(1, line.length()-1));
|
|
|
|
/* There appear to be two different conventions
|
|
for specifying object names in OBJ file -- try
|
|
to detect which one is being used */
|
|
if (nameBeforeGeometry)
|
|
// Save geometry under the previously specified name
|
|
targetName = name;
|
|
else
|
|
targetName = newName;
|
|
|
|
if (triangles.size() > 0) {
|
|
/// make sure that we have unique names
|
|
if (geomNames.find(targetName) != geomNames.end())
|
|
targetName = formatString("%s_%i", targetName.c_str(), geomIndex);
|
|
geomIndex += 1;
|
|
geomNames.insert(targetName);
|
|
if (shapeIndex < 0 || geomIndex-1 == shapeIndex)
|
|
createMesh(targetName, vertices, normals, texcoords,
|
|
triangles, materialName, objectToWorld, vertexBuffer);
|
|
triangles.clear();
|
|
} else {
|
|
nameBeforeGeometry = true;
|
|
}
|
|
name = newName;
|
|
} else if (buf == "usemtl") {
|
|
/* Flush if necessary */
|
|
if (triangles.size() > 0 && !m_collapse) {
|
|
/// make sure that we have unique names
|
|
if (geomNames.find(name) != geomNames.end())
|
|
name = formatString("%s_%i", name.c_str(), geomIndex);
|
|
geomIndex += 1;
|
|
geomNames.insert(name);
|
|
if (shapeIndex < 0 || geomIndex-1 == shapeIndex)
|
|
createMesh(name, vertices, normals, texcoords,
|
|
triangles, materialName, objectToWorld, vertexBuffer);
|
|
triangles.clear();
|
|
name = m_name;
|
|
}
|
|
|
|
materialName = trim(line.substr(6, line.length()-1));
|
|
} else if (buf == "mtllib") {
|
|
materialLibrary = fileResolver->resolve(trim(line.substr(6, line.length()-1)));
|
|
} else if (buf == "vt") {
|
|
Float u, v;
|
|
iss >> u >> v;
|
|
if (flipTexCoords)
|
|
v = 1-v;
|
|
texcoords.push_back(Point2(u, v));
|
|
} else if (buf == "f") {
|
|
std::string tmp;
|
|
OBJTriangle t;
|
|
iss >> tmp; parse(t, 0, tmp);
|
|
iss >> tmp; parse(t, 1, tmp);
|
|
iss >> tmp; parse(t, 2, tmp);
|
|
triangles.push_back(t);
|
|
/* Handle n-gons assuming a convex shape */
|
|
while (iss >> tmp) {
|
|
t.p[1] = t.p[2];
|
|
t.uv[1] = t.uv[2];
|
|
t.n[1] = t.n[2];
|
|
parse(t, 2, tmp);
|
|
triangles.push_back(t);
|
|
}
|
|
} else {
|
|
/* Ignore */
|
|
}
|
|
}
|
|
if (geomNames.find(name) != geomNames.end())
|
|
/// make sure that we have unique names
|
|
name = formatString("%s_%i", m_name.c_str(), geomIndex);
|
|
|
|
if (shapeIndex < 0 || geomIndex-1 == shapeIndex)
|
|
createMesh(name, vertices, normals, texcoords,
|
|
triangles, materialName, objectToWorld, vertexBuffer);
|
|
|
|
if (props.hasProperty("maxSmoothAngle")) {
|
|
if (m_faceNormals)
|
|
Log(EError, "The properties 'maxSmoothAngle' and 'faceNormals' "
|
|
"can't be specified at the same time!");
|
|
Float maxSmoothAngle = props.getFloat("maxSmoothAngle");
|
|
for (size_t i=0; i<m_meshes.size(); ++i)
|
|
m_meshes[i]->rebuildTopology(maxSmoothAngle);
|
|
}
|
|
|
|
if (!materialLibrary.empty())
|
|
loadMaterialLibrary(fileResolver, materialLibrary);
|
|
|
|
Log(EInfo, "Done with \"%s\" (took %i ms)", path.filename().string().c_str(), timer->getMilliseconds());
|
|
}
|
|
|
|
WavefrontOBJ(Stream *stream, InstanceManager *manager) : Shape(stream, manager) {
|
|
m_aabb = AABB(stream);
|
|
m_name = stream->readString();
|
|
uint32_t meshCount = stream->readUInt();
|
|
m_meshes.resize(meshCount);
|
|
|
|
for (uint32_t i=0; i<meshCount; ++i) {
|
|
m_meshes[i] = static_cast<TriMesh *>(manager->getInstance(stream));
|
|
m_meshes[i]->incRef();
|
|
}
|
|
}
|
|
|
|
void serialize(Stream *stream, InstanceManager *manager) const {
|
|
Shape::serialize(stream, manager);
|
|
|
|
m_aabb.serialize(stream);
|
|
stream->writeString(m_name);
|
|
stream->writeUInt((uint32_t) m_meshes.size());
|
|
for (size_t i=0; i<m_meshes.size(); ++i)
|
|
manager->serialize(stream, m_meshes[i]);
|
|
}
|
|
|
|
void parse(OBJTriangle &t, int i, const std::string &str) {
|
|
std::vector<std::string> tokens = tokenize(str, "/");
|
|
if (tokens.size() == 1) {
|
|
t.p[i] = atoi(tokens[0].c_str());
|
|
} else if (tokens.size() == 2) {
|
|
if (str.find("//") == std::string::npos) {
|
|
t.p[i] = atoi(tokens[0].c_str());
|
|
t.uv[i] = atoi(tokens[1].c_str());
|
|
} else {
|
|
t.p[i] = atoi(tokens[0].c_str());
|
|
t.n[i] = atoi(tokens[1].c_str());
|
|
}
|
|
} else if (tokens.size() == 3) {
|
|
t.p[i] = atoi(tokens[0].c_str());
|
|
t.uv[i] = atoi(tokens[1].c_str());
|
|
t.n[i] = atoi(tokens[2].c_str());
|
|
} else {
|
|
Log(EError, "Invalid OBJ face format!");
|
|
}
|
|
}
|
|
|
|
Texture *loadTexture(const FileResolver *fileResolver,
|
|
std::map<std::string, Texture *> &cache,
|
|
const fs::path &mtlPath, std::string filename) {
|
|
/* Prevent Linux/OSX fs::path handling issues for DAE files created on Windows */
|
|
for (size_t i=0; i<filename.length(); ++i) {
|
|
if (filename[i] == '\\')
|
|
filename[i] = '/';
|
|
}
|
|
|
|
if (cache.find(filename) != cache.end())
|
|
return cache[filename];
|
|
|
|
fs::path path = fileResolver->resolve(filename);
|
|
if (!fs::exists(path)) {
|
|
path = fileResolver->resolve(fs::path(filename).filename());
|
|
if (!fs::exists(path)) {
|
|
Log(EWarn, "Unable to find texture \"%s\" referenced from \"%s\"!",
|
|
path.string().c_str(), mtlPath.string().c_str());
|
|
return new ConstantSpectrumTexture(Spectrum(0.0f));
|
|
}
|
|
}
|
|
Properties props("bitmap");
|
|
props.setString("filename", path.string());
|
|
props.setFloat("gamma", 1.0f);
|
|
ref<Texture> texture = static_cast<Texture *> (PluginManager::getInstance()->
|
|
createObject(MTS_CLASS(Texture), props));
|
|
texture->configure();
|
|
texture->incRef();
|
|
cache[filename] = texture;
|
|
return texture;
|
|
}
|
|
|
|
void loadMaterialLibrary(const FileResolver *fileResolver, const fs::path &mtlPath) {
|
|
if (!fs::exists(mtlPath)) {
|
|
Log(EWarn, "Could not find referenced material library '%s'",
|
|
mtlPath.string().c_str());
|
|
return;
|
|
}
|
|
|
|
Log(EInfo, "Loading OBJ materials from \"%s\" ..", mtlPath.filename().string().c_str());
|
|
fs::ifstream is(mtlPath);
|
|
if (is.bad() || is.fail())
|
|
Log(EError, "Unexpected I/O error while accessing material file '%s'!",
|
|
mtlPath.string().c_str());
|
|
std::string buf, line;
|
|
std::string mtlName;
|
|
ref<Texture> specular, diffuse, exponent, bump, mask;
|
|
int illum = 0;
|
|
specular = new ConstantSpectrumTexture(Spectrum(0.0f));
|
|
diffuse = new ConstantSpectrumTexture(Spectrum(0.0f));
|
|
exponent = new ConstantFloatTexture(0.0f);
|
|
std::map<std::string, Texture *> cache;
|
|
|
|
while (is.good() && !is.eof() && fetch_line(is, line)) {
|
|
std::istringstream iss(line);
|
|
if (!(iss >> buf))
|
|
continue;
|
|
|
|
if (buf == "newmtl") {
|
|
if (mtlName != "")
|
|
addMaterial(mtlName, diffuse, specular, exponent, bump, mask, illum);
|
|
|
|
mtlName = trim(line.substr(6, line.length()-6));
|
|
|
|
specular = new ConstantSpectrumTexture(Spectrum(0.0f));
|
|
diffuse = new ConstantSpectrumTexture(Spectrum(0.0f));
|
|
exponent = new ConstantFloatTexture(0.0f);
|
|
mask = NULL;
|
|
bump = NULL;
|
|
illum = 0;
|
|
} else if (buf == "Kd") {
|
|
Float r, g, b;
|
|
iss >> r >> g >> b;
|
|
Spectrum value;
|
|
value.fromSRGB(r, g, b);
|
|
diffuse = new ConstantSpectrumTexture(value);
|
|
} else if (buf == "map_Kd") {
|
|
std::string filename;
|
|
iss >> filename;
|
|
diffuse = loadTexture(fileResolver, cache, mtlPath, filename);
|
|
} else if (buf == "Ks") {
|
|
Float r, g, b;
|
|
iss >> r >> g >> b;
|
|
Spectrum value;
|
|
value.fromSRGB(r, g, b);
|
|
specular = new ConstantSpectrumTexture(value);
|
|
} else if (buf == "map_Ks") {
|
|
std::string filename;
|
|
iss >> filename;
|
|
specular = loadTexture(fileResolver, cache, mtlPath, filename);
|
|
} else if (buf == "bump") {
|
|
std::string filename;
|
|
iss >> filename;
|
|
bump = loadTexture(fileResolver, cache, mtlPath, filename);
|
|
} else if (buf == "map_d") {
|
|
std::string filename;
|
|
iss >> filename;
|
|
mask = loadTexture(fileResolver, cache, mtlPath, filename);
|
|
} else if (buf == "d" /* || buf == "Tr" */) {
|
|
Float value;
|
|
iss >> value;
|
|
if (value == 1)
|
|
mask = NULL;
|
|
else
|
|
mask = new ConstantFloatTexture(value);
|
|
} else if (buf == "Ns") {
|
|
Float value;
|
|
iss >> value;
|
|
exponent = new ConstantFloatTexture(value);
|
|
} else if (buf == "illum") {
|
|
iss >> illum;
|
|
} else {
|
|
/* Ignore */
|
|
}
|
|
}
|
|
|
|
addMaterial(mtlName, diffuse, specular, exponent, bump, mask, illum);
|
|
|
|
for (std::map<std::string, Texture *>::iterator it = cache.begin();
|
|
it != cache.end(); ++it)
|
|
it->second->decRef();
|
|
}
|
|
|
|
void addMaterial(const std::string &name, Texture *diffuse, Texture *specular,
|
|
Texture *exponent, Texture *bump, Texture *mask, int model) {
|
|
ref<BSDF> bsdf;
|
|
Properties props;
|
|
|
|
if (model == 2 && (specular->getMaximum().isZero() || exponent->getMaximum().isZero()))
|
|
model = 1;
|
|
|
|
if (model == 2) {
|
|
props.setPluginName("phong");
|
|
|
|
bsdf = static_cast<BSDF *> (PluginManager::getInstance()->
|
|
createObject(MTS_CLASS(BSDF), props));
|
|
bsdf->addChild("diffuseReflectance", diffuse);
|
|
bsdf->addChild("specularReflectance", specular);
|
|
bsdf->addChild("exponent", exponent);
|
|
} else if (model == 4 || model == 6 || model == 7 || model == 9) {
|
|
props.setPluginName("dielectric");
|
|
bsdf = static_cast<BSDF *> (PluginManager::getInstance()->
|
|
createObject(MTS_CLASS(BSDF), props));
|
|
} else if (model == 5 || model == 8) {
|
|
props.setPluginName("conductor");
|
|
props.setString("material", "Al");
|
|
bsdf = static_cast<BSDF *> (PluginManager::getInstance()->
|
|
createObject(MTS_CLASS(BSDF), props));
|
|
} else {
|
|
props.setPluginName("diffuse");
|
|
bsdf = static_cast<BSDF *> (PluginManager::getInstance()->
|
|
createObject(MTS_CLASS(BSDF), props));
|
|
bsdf->addChild("reflectance", diffuse);
|
|
}
|
|
|
|
bsdf->configure();
|
|
|
|
if (bump) {
|
|
props = Properties("bumpmap");
|
|
ref<BSDF> bumpBSDF = static_cast<BSDF *> (PluginManager::getInstance()->
|
|
createObject(MTS_CLASS(BSDF), props));
|
|
|
|
bumpBSDF->addChild(bump);
|
|
bumpBSDF->addChild(bsdf);
|
|
bumpBSDF->configure();
|
|
bsdf = bumpBSDF;
|
|
}
|
|
|
|
if (mask) {
|
|
props = Properties("mask");
|
|
ref<BSDF> maskedBSDF = static_cast<BSDF *> (PluginManager::getInstance()->
|
|
createObject(MTS_CLASS(BSDF), props));
|
|
|
|
maskedBSDF->addChild("opacity", mask);
|
|
maskedBSDF->addChild(bsdf);
|
|
maskedBSDF->configure();
|
|
bsdf = maskedBSDF;
|
|
}
|
|
|
|
bsdf->setID(name);
|
|
addChild(name, bsdf, false);
|
|
}
|
|
|
|
struct Vertex {
|
|
Point p;
|
|
Normal n;
|
|
Point2 uv;
|
|
};
|
|
|
|
/// For using vertices as keys in an associative structure
|
|
struct vertex_key_order : public
|
|
std::binary_function<Vertex, Vertex, bool> {
|
|
public:
|
|
bool operator()(const Vertex &v1, const Vertex &v2) const {
|
|
if (v1.p.x < v2.p.x) return true;
|
|
else if (v1.p.x > v2.p.x) return false;
|
|
if (v1.p.y < v2.p.y) return true;
|
|
else if (v1.p.y > v2.p.y) return false;
|
|
if (v1.p.z < v2.p.z) return true;
|
|
else if (v1.p.z > v2.p.z) return false;
|
|
if (v1.n.x < v2.n.x) return true;
|
|
else if (v1.n.x > v2.n.x) return false;
|
|
if (v1.n.y < v2.n.y) return true;
|
|
else if (v1.n.y > v2.n.y) return false;
|
|
if (v1.n.z < v2.n.z) return true;
|
|
else if (v1.n.z > v2.n.z) return false;
|
|
if (v1.uv.x < v2.uv.x) return true;
|
|
else if (v1.uv.x > v2.uv.x) return false;
|
|
if (v1.uv.y < v2.uv.y) return true;
|
|
else if (v1.uv.y > v2.uv.y) return false;
|
|
return false;
|
|
}
|
|
};
|
|
|
|
void createMesh(const std::string &name,
|
|
const std::vector<Point> &vertices,
|
|
const std::vector<Normal> &normals,
|
|
const std::vector<Point2> &texcoords,
|
|
const std::vector<OBJTriangle> &triangles,
|
|
const std::string &materialName,
|
|
const Transform &objectToWorld,
|
|
std::vector<Vertex> &vertexBuffer) {
|
|
if (triangles.size() == 0)
|
|
return;
|
|
typedef std::map<Vertex, uint32_t, vertex_key_order> VertexMapType;
|
|
VertexMapType vertexMap;
|
|
|
|
vertexBuffer.reserve(vertices.size());
|
|
size_t numMerged = 0;
|
|
AABB aabb;
|
|
bool hasTexcoords = false;
|
|
bool hasNormals = false;
|
|
ref<Timer> timer = new Timer();
|
|
vertexBuffer.clear();
|
|
|
|
/* Collapse the mesh into a more usable form */
|
|
Triangle *triangleArray = new Triangle[triangles.size()];
|
|
for (uint32_t i=0; i<triangles.size(); i++) {
|
|
Triangle tri;
|
|
for (uint32_t j=0; j<3; j++) {
|
|
int vertexId = triangles[i].p[j];
|
|
int normalId = triangles[i].n[j];
|
|
int uvId = triangles[i].uv[j];
|
|
uint32_t key;
|
|
|
|
Vertex vertex;
|
|
if (vertexId < 0)
|
|
vertexId += (int) vertices.size() + 1;
|
|
if (normalId < 0)
|
|
normalId += (int) normals.size() + 1;
|
|
if (uvId < 0)
|
|
uvId += (int) texcoords.size() + 1;
|
|
|
|
if (vertexId > (int) vertices.size() || vertexId <= 0)
|
|
Log(EError, "Out of bounds: tried to access vertex %i (max: %i)", vertexId, (int) vertices.size());
|
|
|
|
vertex.p = objectToWorld(vertices[vertexId-1]);
|
|
aabb.expandBy(vertex.p);
|
|
|
|
if (normalId != 0) {
|
|
if (normalId > (int) normals.size() || normalId < 0)
|
|
Log(EError, "Out of bounds: tried to access normal %i (max: %i)", normalId, (int) normals.size());
|
|
vertex.n = objectToWorld(normals[normalId-1]);
|
|
if (!vertex.n.isZero())
|
|
vertex.n = normalize(vertex.n);
|
|
hasNormals = true;
|
|
} else {
|
|
vertex.n = Normal(0.0f);
|
|
}
|
|
|
|
if (uvId != 0) {
|
|
if (uvId > (int) texcoords.size() || uvId < 0)
|
|
Log(EError, "Out of bounds: tried to access uv %i (max: %i)", uvId, (int) texcoords.size());
|
|
vertex.uv = texcoords[uvId-1];
|
|
hasTexcoords = true;
|
|
} else {
|
|
vertex.uv = Point2(0.0f);
|
|
}
|
|
|
|
VertexMapType::iterator it = vertexMap.find(vertex);
|
|
if (it != vertexMap.end()) {
|
|
key = it->second;
|
|
numMerged++;
|
|
} else {
|
|
key = (uint32_t) vertexBuffer.size();
|
|
vertexMap[vertex] = key;
|
|
vertexBuffer.push_back(vertex);
|
|
}
|
|
|
|
tri.idx[j] = key;
|
|
}
|
|
triangleArray[i] = tri;
|
|
}
|
|
|
|
ref<TriMesh> mesh = new TriMesh(name,
|
|
triangles.size(), vertexBuffer.size(),
|
|
hasNormals, hasTexcoords, false,
|
|
m_flipNormals, m_faceNormals);
|
|
|
|
std::copy(triangleArray, triangleArray+triangles.size(), mesh->getTriangles());
|
|
|
|
Point *target_positions = mesh->getVertexPositions();
|
|
Normal *target_normals = mesh->getVertexNormals();
|
|
Point2 *target_texcoords = mesh->getVertexTexcoords();
|
|
|
|
mesh->getAABB() = aabb;
|
|
|
|
for (size_t i=0; i<vertexBuffer.size(); i++) {
|
|
*target_positions++ = vertexBuffer[i].p;
|
|
if (hasNormals)
|
|
*target_normals++ = vertexBuffer[i].n;
|
|
if (hasTexcoords)
|
|
*target_texcoords++ = vertexBuffer[i].uv;
|
|
}
|
|
|
|
mesh->incRef();
|
|
m_materialAssignment.push_back(materialName);
|
|
m_meshes.push_back(mesh);
|
|
Log(EInfo, "%s: " SIZE_T_FMT " triangles, " SIZE_T_FMT
|
|
" vertices (merged " SIZE_T_FMT " vertices).", name.c_str(),
|
|
triangles.size(), vertexBuffer.size(), numMerged);
|
|
}
|
|
|
|
virtual ~WavefrontOBJ() {
|
|
for (size_t i=0; i<m_meshes.size(); ++i)
|
|
m_meshes[i]->decRef();
|
|
}
|
|
|
|
void configure() {
|
|
Shape::configure();
|
|
|
|
m_aabb.reset();
|
|
for (size_t i=0; i<m_meshes.size(); ++i) {
|
|
m_meshes[i]->configure();
|
|
m_aabb.expandBy(m_meshes[i]->getAABB());
|
|
}
|
|
}
|
|
|
|
void addChild(const std::string &name, ConfigurableObject *child) {
|
|
addChild(name, child, true);
|
|
}
|
|
|
|
void addChild(const std::string &name, ConfigurableObject *child, bool warn) {
|
|
const Class *cClass = child->getClass();
|
|
if (cClass->derivesFrom(MTS_CLASS(BSDF))) {
|
|
Shape::addChild(name, child);
|
|
Assert(m_meshes.size() > 0);
|
|
if (name == "") {
|
|
for (size_t i=0; i<m_meshes.size(); ++i)
|
|
m_meshes[i]->addChild(name, child);
|
|
} else {
|
|
bool found = false;
|
|
for (size_t i=0; i<m_meshes.size(); ++i) {
|
|
if (m_materialAssignment[i] == name) {
|
|
found = true;
|
|
m_meshes[i]->addChild(name, child);
|
|
}
|
|
}
|
|
if (!found && warn)
|
|
Log(EWarn, "Attempted to register the material named "
|
|
"'%s', which does not occur in the OBJ file!", name.c_str());
|
|
}
|
|
m_bsdf->setParent(NULL);
|
|
} else if (cClass->derivesFrom(MTS_CLASS(Emitter))) {
|
|
if (m_meshes.size() > 1)
|
|
Log(EError, "Cannot attach an emitter to an OBJ file "
|
|
"containing multiple objects!");
|
|
m_emitter = static_cast<Emitter *>(child);
|
|
child->setParent(m_meshes[0]);
|
|
m_meshes[0]->addChild(name, child);
|
|
} else if (cClass->derivesFrom(MTS_CLASS(Sensor))) {
|
|
if (m_meshes.size() > 1)
|
|
Log(EError, "Cannot attach an sensor to an OBJ file "
|
|
"containing multiple objects!");
|
|
m_sensor = static_cast<Sensor *>(child);
|
|
child->setParent(m_meshes[0]);
|
|
m_meshes[0]->addChild(name, child);
|
|
} else if (cClass->derivesFrom(MTS_CLASS(Subsurface))) {
|
|
Assert(m_subsurface == NULL);
|
|
m_subsurface = static_cast<Subsurface *>(child);
|
|
for (size_t i=0; i<m_meshes.size(); ++i) {
|
|
child->setParent(m_meshes[i]);
|
|
m_meshes[i]->addChild(name, child);
|
|
}
|
|
} else if (cClass->derivesFrom(MTS_CLASS(Medium))) {
|
|
Shape::addChild(name, child);
|
|
for (size_t i=0; i<m_meshes.size(); ++i)
|
|
m_meshes[i]->addChild(name, child);
|
|
} else {
|
|
Shape::addChild(name, child);
|
|
}
|
|
}
|
|
|
|
bool isCompound() const {
|
|
return true;
|
|
}
|
|
|
|
Shape *getElement(int index) {
|
|
if (index >= (int) m_meshes.size())
|
|
return NULL;
|
|
Shape *shape = m_meshes[index];
|
|
BSDF *bsdf = shape->getBSDF();
|
|
Emitter *emitter = shape->getEmitter();
|
|
Subsurface *subsurface = shape->getSubsurface();
|
|
if (bsdf)
|
|
bsdf->setParent(shape);
|
|
if (emitter)
|
|
emitter->setParent(shape);
|
|
if (subsurface)
|
|
subsurface->setParent(shape);
|
|
return shape;
|
|
}
|
|
|
|
std::string getName() const {
|
|
return m_name;
|
|
}
|
|
|
|
AABB getAABB() const {
|
|
return m_aabb;
|
|
}
|
|
|
|
Float getSurfaceArea() const {
|
|
Float sa = 0;
|
|
for (size_t i=0; i<m_meshes.size(); ++i)
|
|
sa += m_meshes[i]->getSurfaceArea();
|
|
return sa;
|
|
}
|
|
|
|
size_t getPrimitiveCount() const {
|
|
size_t result = 0;
|
|
for (size_t i=0; i<m_meshes.size(); ++i)
|
|
result += m_meshes[i]->getPrimitiveCount();
|
|
return result;
|
|
}
|
|
|
|
size_t getEffectivePrimitiveCount() const {
|
|
size_t result = 0;
|
|
for (size_t i=0; i<m_meshes.size(); ++i)
|
|
result += m_meshes[i]->getEffectivePrimitiveCount();
|
|
return result;
|
|
}
|
|
|
|
MTS_DECLARE_CLASS()
|
|
private:
|
|
std::vector<TriMesh *> m_meshes;
|
|
std::vector<std::string> m_materialAssignment;
|
|
bool m_flipNormals, m_faceNormals;
|
|
std::string m_name;
|
|
AABB m_aabb;
|
|
bool m_collapse;
|
|
};
|
|
|
|
MTS_IMPLEMENT_CLASS_S(WavefrontOBJ, false, Shape)
|
|
MTS_EXPORT_PLUGIN(WavefrontOBJ, "OBJ triangle mesh loader");
|
|
MTS_NAMESPACE_END
|