/*
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 .
*/
#include
#include
#include
#define EIGEN_DONT_PARALLELIZE
#define EIGEN_NO_DEBUG
#include
#include
MTS_NAMESPACE_BEGIN
/* Some statistics counters */
static StatsCounter statsStepFailed(
"Specular manifold", "Retries (step failed)");
static StatsCounter statsStepTooFar(
"Specular manifold", "Retries (step increased distance)");
static StatsCounter statsStepSuccess(
"Specular manifold", "Successful steps");
static StatsCounter statsAvgIterations(
"Specular manifold", "Avg. iterations per walk", EAverage);
static StatsCounter statsAvgIterationsSuccess(
"Specular manifold", "Avg. iterations per successful walk", EAverage);
static StatsCounter statsAvgManifoldSize(
"Specular manifold", "Avg. manifold size", EAverage);
static StatsCounter statsSuccessfulWalks(
"Specular manifold", "Successful walks", EPercentage);
static StatsCounter statsMediumSuccess(
"Specular manifold", "Successful walks w/ media", EPercentage);
static StatsCounter statsNonManifold(
"Specular manifold", "Non-manifold", EPercentage);
static StatsCounter statsUpdateFailed(
"Specular manifold", "Update failed");
static StatsCounter statsMaxManifold(
"Specular manifold", "Max. manifold size", EMaximumValue);
SpecularManifold::SpecularManifold(const Scene *scene, int maxIterations)
: m_scene(scene) {
m_maxIterations = maxIterations > 0 ? maxIterations :
MTS_MANIFOLD_MAX_ITERATIONS;
}
bool SpecularManifold::init(const Path &path, int start, int end) {
int step = start < end ? 1 : -1;
if (path.vertex(start)->isSupernode())
start += step;
if (path.vertex(end)->isSupernode())
end -= step;
const PathVertex
*vs = path.vertex(start),
*ve = path.vertex(end);
/* Create the initial vertex that is pinned in position by default */
SimpleVertex v(EPinnedPosition, vs->getPosition());
/* When the endpoint is on an orthographic camera or directional light
source, switch to a directionally pinned vertex instead */
if (vs->getType() & (PathVertex::ESensorSample | PathVertex::EEmitterSample)) {
const PositionSamplingRecord &pRec
= vs->getPositionSamplingRecord();
uint32_t type = static_cast(pRec.object)->getType()
& (AbstractEmitter::EDeltaDirection | AbstractEmitter::EDeltaPosition);
if (type == AbstractEmitter::EDeltaDirection) {
v.type = EPinnedDirection;
v.gn = v.n = pRec.n;
coordinateSystem(pRec.n, v.dpdu, v.dpdv);
}
}
m_time = vs->getTime();
m_vertices.clear();
m_vertices.push_back(v);
for (int i=start + step; i != end; i += step) {
const PathVertex
*pred = path.vertex(i-step),
*vertex = path.vertex(i),
*succ = path.vertex(i+step);
if (vertex->isSurfaceInteraction()) {
const Intersection &its = vertex->getIntersection();
const BSDF *bsdf = its.getBSDF();
v.p = its.p;
v.gn = its.geoFrame.n;
v.n = its.shFrame.n;
v.dpdu = its.dpdu;
v.dpdv = its.dpdv;
v.object = bsdf;
v.degenerate = !vertex->isConnectable();
const Shape *shape = its.instance != NULL ? its.instance : its.shape;
shape->getNormalDerivative(its, v.dndu, v.dndv);
/* Turn into an orthonormal parameterization at 'p' */
Float invLen = 1 / v.dpdu.length();
v.dpdu *= invLen;
v.dndu *= invLen;
Float dp = dot(v.dpdu, v.dpdv);
Vector dpdv = v.dpdv - dp * v.dpdu;
Vector dndv = v.dndv - dp * v.dndu;
invLen = 1 / dpdv.length();
v.dpdv = dpdv * invLen;
v.dndv = dndv * invLen;
Vector wPred = pred->getPosition() - v.p;
Vector wSucc = succ->getPosition() - v.p;
if (dot(v.gn, wPred) * dot(v.gn, wSucc) < 0) {
v.type = ERefraction;
v.eta = bsdf->getEta();
} else {
v.type = EReflection;
v.eta = 1.0f;
}
} else if (vertex->isMediumInteraction()) {
const MediumSamplingRecord &mRec = vertex->getMediumSamplingRecord();
Vector wi = pred->getPosition() - mRec.p;
Float invLength = 1.0f / wi.length();
wi *= invLength;
v.p = mRec.p;
v.gn = v.n = Normal(0.0f);
Vector s, t;
coordinateSystem(wi, s, t);
v.dpdu = s;
v.dpdv = t;
v.dndu = s * invLength;
v.dndv = t * invLength;
v.object = mRec.getPhaseFunction();
v.eta = 1.0f;
v.degenerate = false;
v.type = EMedium;
} else {
Log(EError, "Unknown vertex type! : %s", vertex->toString().c_str());
}
m_vertices.push_back(v);
}
v = SimpleVertex(EMovable, ve->getPosition());
m_vertices.push_back(v);
#if MTS_MANIFOLD_DEBUG == 1
cout << "==========================================" << endl;
cout << "Initialized specular manifold: " << toString() << endl;
#endif
return true;
}
bool SpecularManifold::computeTangents() {
const int n = static_cast(m_vertices.size() - 1);
m_vertices[0].Tp.setZero();
m_vertices[m_vertices.size()-1].Tp.setIdentity();
if (m_vertices.size() == 2) /* Nothing to do */
return true;
/* Matrix assembly stage */
for (int i=0; i std::abs(wi.y)) {
Float tl = 1.0f / std::sqrt(wi.x * wi.x + wi.z * wi.z);
t = Vector(wi.z * tl, 0.0f, -wi.x * tl);
dt_dpred_u = Vector(dwi_dpred_u.z*tl, 0.0f, -dwi_dpred_u.x*tl);
dt_dpred_v = Vector(dwi_dpred_v.z*tl, 0.0f, -dwi_dpred_v.x*tl);
dt_dcur_u = Vector(dwi_dcur_u.z*tl, 0.0f, -dwi_dcur_u.x*tl);
dt_dcur_v = Vector(dwi_dcur_v.z*tl, 0.0f, -dwi_dcur_v.x*tl);
} else {
Float tl = 1.0f / std::sqrt(wi.y * wi.y + wi.z * wi.z);
t = Vector(0.0f, wi.z * tl, -wi.y * tl);
dt_dpred_u = Vector(0.0f, dwi_dpred_u.z*tl, -dwi_dpred_u.y*tl);
dt_dpred_v = Vector(0.0f, dwi_dpred_v.z*tl, -dwi_dpred_v.y*tl);
dt_dcur_u = Vector(0.0f, dwi_dcur_u.z*tl, -dwi_dcur_u.y*tl);
dt_dcur_v = Vector(0.0f, dwi_dcur_v.z*tl, -dwi_dcur_v.y*tl);
}
dt_dpred_u -= t * dot(t, dt_dpred_u);
dt_dpred_v -= t * dot(t, dt_dpred_v);
dt_dcur_u -= t * dot(t, dt_dcur_u);
dt_dcur_v -= t * dot(t, dt_dcur_v);
Vector s = cross(t, wi);
Vector ds_dpred_u = cross(dt_dpred_u, wi) + cross(t, dwi_dpred_u);
Vector ds_dpred_v = cross(dt_dpred_v, wi) + cross(t, dwi_dpred_v);
Vector ds_dcur_u = cross(dt_dcur_u, wi) + cross(t, dwi_dcur_u);
Vector ds_dcur_v = cross(dt_dcur_v, wi) + cross(t, dwi_dcur_v);
/* Some tangential projections */
Vector2
t_cur_dpdu (dot(v[ 0].dpdu, s), dot(v[ 0].dpdu, t)),
t_cur_dpdv (dot(v[ 0].dpdv, s), dot(v[ 0].dpdv, t)),
t_next_dpdu(dot(v[ 1].dpdu, s), dot(v[ 1].dpdu, t)),
t_next_dpdv(dot(v[ 1].dpdv, s), dot(v[ 1].dpdv, t)),
t_wo = Vector2(dot(wo, s), dot(wo, t));
v[0].a = Matrix2x2(
Vector2(dot(ds_dpred_u, wo), dot(dt_dpred_u, wo)),
Vector2(dot(ds_dpred_v, wo), dot(dt_dpred_v, wo))
);
v[0].b = Matrix2x2(
(t_wo * dot(wo, v[0].dpdu) - t_cur_dpdu) * ilo +
Vector2(dot(ds_dcur_u, wo), dot(dt_dcur_u, wo)),
(t_wo * dot(wo, v[0].dpdv) - t_cur_dpdv) * ilo +
Vector2(dot(ds_dcur_v, wo), dot(dt_dcur_v, wo)));
v[0].c = Matrix2x2(
(t_next_dpdu - t_wo * dot(wo, v[1].dpdu)) * ilo,
(t_next_dpdv - t_wo * dot(wo, v[1].dpdv)) * ilo);
v[0].m = Vector(dot(s, wo), dot(t, wo), dot(wi, wo));
} else {
Log(EError, "Unknown vertex type!");
}
}
/* Find the tangent space with respect to translation of the last
vertex. For this, we must solve a tridiagonal system. The following is
simplified version of the block tridiagonal LU factorization algorithm
for this specific problem */
Matrix2x2 Li;
if (!m_vertices[0].b.invert(Li))
return false;
for (int i=0; i < n - 1; ++i) {
m_vertices[i].u = Li * m_vertices[i].c;
Matrix2x2 temp = m_vertices[i+1].b - m_vertices[i+1].a * m_vertices[i].u;
if (!temp.invert(Li))
return false;
}
m_vertices[n-1].Tp = -Li * m_vertices[n-1].c;
for (int i=n-2; i>=0; --i)
m_vertices[i].Tp = -m_vertices[i].u * m_vertices[i+1].Tp;
return true;
}
bool SpecularManifold::project(const Vector &d) {
const SimpleVertex &last = m_vertices[m_vertices.size()-1];
Float du = dot(d, last.dpdu), dv = dot(d, last.dpdv);
Ray ray(Point(0.0f), Vector(1.0f), 0); // make gcc happy
Intersection its;
m_proposal.clear();
for (size_t i=0; irayIntersect(ray, its))
return false;
Vector n = its.shFrame.n,
s = its.dpdu, t;
s = normalize(s - n * dot(n, s));
t = cross(n, s);
Vector m = s * vertex.m[0] + t * vertex.m[1] + n * vertex.m[2];
ray.setOrigin(its.p);
ray.setDirection(reflect(-ray.d, m));
} else if (vertex.type == ERefraction) {
if (!m_scene->rayIntersect(ray, its))
return false;
Vector n = its.shFrame.n,
s = its.dpdu, t;
s = normalize(s - n * dot(n, s));
t = cross(n, s);
Vector m = s * vertex.m[0] + t * vertex.m[1] + n * vertex.m[2];
Vector refracted = refract(-ray.d, m, its.shape->getBSDF()->getEta());
if (refracted.isZero())
return false;
ray.setOrigin(its.p);
ray.setDirection(refracted);
} else if (vertex.type == EMedium) {
Float length = (m_vertices[i].p - m_vertices[i-1].p).length(),
invLength = 1.0f / length;
/* Check for occlusion */
if (m_scene->rayIntersect(Ray(ray, Epsilon, length)))
return false;
vertex.p = ray(length);
vertex.n = Vector(0.0f);
Vector wi = -ray.d, s, t;
coordinateSystem(wi, s, t);
vertex.dpdu = s;
vertex.dpdv = t;
vertex.dndu = s * invLength;
vertex.dndv = t * invLength;
ray.setOrigin(vertex.p);
ray.setDirection(s * vertex.m[0] + t * vertex.m[1] + wi * vertex.m[2]);
} else {
Log(EError, "Unsupported vertex type!");
}
if (vertex.type != EMedium) {
if (vertex.object != its.shape->getBSDF())
return false;
vertex.p = its.p;
vertex.n = its.shFrame.n;
vertex.gn = its.geoFrame.n;
vertex.dpdu = its.dpdu;
vertex.dpdv = its.dpdv;
const Shape *shape = its.instance != NULL ? its.instance : its.shape;
shape->getNormalDerivative(its,
vertex.dndu, vertex.dndv);
/* Turn into an orthonormal parameterization at 'p' */
Float invLen = 1 / vertex.dpdu.length();
vertex.dpdu *= invLen; vertex.dndu *= invLen;
Float dp = dot(vertex.dpdu, vertex.dpdv);
Vector dpdv = vertex.dpdv - dp * vertex.dpdu;
Vector dndv = vertex.dndv - dp * vertex.dndu;
invLen = 1 / dpdv.length();
vertex.dpdv = dpdv * invLen;
vertex.dndv = dndv * invLen;
}
}
return true;
}
bool SpecularManifold::move(const Point &target, const Normal &n) {
SimpleVertex &last = m_vertices[m_vertices.size()-1];
#if MTS_MANIFOLD_DEBUG == 1
cout << "moveTo(" << last.p.toString() << " => " << target.toString() << ", n=" << n.toString() << ")" << endl;
#endif
if (m_vertices.size() == 2 && m_vertices[0].type == EPinnedPosition) {
/* Nothing to do */
return true;
}
bool medium = false;
for (size_t i=0; i " << newDist << endl;
#endif
if (newDist > dist) {
++statsStepTooFar;
#if MTS_MANIFOLD_DEBUG == 1
cout << "-> Rejecting!" << endl;
#endif
goto failure;
}
#if MTS_MANIFOLD_DEBUG == 1
cout << "-> Accepting!" << endl;
#endif
++statsStepSuccess;
m_proposal.swap(m_vertices);
/* Increase the step size */
stepSize = std::min((Float) 1.0f, stepSize * 2.0f);
continue;
failure:
/* Reduce the step size */
stepSize /= 2.0f;
}
#if MTS_MANIFOLD_DEBUG == 1
cout << "Exceeded the max. iteration count!" << endl;
#endif
return false;
}
bool SpecularManifold::update(Path &path, int start, int end) {
int step;
ETransportMode mode;
if (start < end) {
step = 1; mode = EImportance;
} else {
step = -1; mode = ERadiance;
}
int last = (int) m_vertices.size() - 2;
if (m_vertices[0].type == EPinnedDirection)
last = std::max(last, 1);
for (int j=0, i=start; j < last; ++j, i += step) {
const SimpleVertex
&v = m_vertices[j],
&vn = m_vertices[j+1];
PathVertex
*pred = path.vertexOrNull(i-step),
*vertex = path.vertex(i),
*succ = path.vertex(i+step);
int predEdgeIdx = (mode == EImportance) ? i-step : i-step-1;
PathEdge *predEdge = path.edgeOrNull(predEdgeIdx),
*succEdge = path.edge(predEdgeIdx + step);
Vector d = vn.p - v.p;
Float length = d.length();
d /= length;
PathVertex::EVertexType desiredType = vn.type == EMedium ?
PathVertex::EMediumInteraction : PathVertex::ESurfaceInteraction;
if (v.type == EPinnedDirection) {
/* Create a fake vertex and use it to call sampleDirect(). This is
kind of terrible -- a nicer API is needed to cleanly support this */
PathVertex temp;
temp.type = PathVertex::EMediumInteraction;
temp.degenerate = false;
temp.measure = EArea;
MediumSamplingRecord &mRec = temp.getMediumSamplingRecord();
mRec.time = m_time;
mRec.p = vn.p;
if (temp.sampleDirect(m_scene, NULL, vertex, succEdge, succ, mode).isZero()) {
#if MTS_MANIFOLD_DEBUG == 1
cout << "update(): failed in sampleDirect()!" << endl;
#endif
++statsUpdateFailed;
return false;
}
if (m_vertices.size() >= 3) {
PathVertex *succ2 = path.vertex(i+2*step);
PathEdge *succ2Edge = path.edge(predEdgeIdx + 2*step);
if (!succ->sampleNext(m_scene, NULL, vertex, succEdge, succ2Edge, succ2, mode)) {
#if MTS_MANIFOLD_DEBUG == 1
cout << "update(): failed in sampleNext() / pinned direction!" << endl;
#endif
++statsUpdateFailed;
return false;
}
}
i += step;
} else if (!v.degenerate) {
if (!vertex->perturbDirection(m_scene,
pred, predEdge, succEdge, succ, d,
length, desiredType, mode)) {
#if MTS_MANIFOLD_DEBUG == 1
cout << "update(): failed in perturbDirection()" << endl;
#endif
++statsUpdateFailed;
return false;
}
Float relerr = (vn.p - succ->getPosition()).length() /
std::max(std::max(std::abs(vn.p.x),
std::abs(vn.p.y)), std::abs(vn.p.z));
if (relerr > 1e-3f) {
// be extra-cautious
#if MTS_MANIFOLD_DEBUG == 1
cout << "update(): failed, relative error of perturbDirection() too high:" << relerr << endl;
#endif
++statsUpdateFailed;
return false;
}
} else {
unsigned int compType;
if (v.type == ERefraction)
compType = v.eta != 1 ? BSDF::EDeltaTransmission : (BSDF::ENull | BSDF::EDeltaTransmission);
else
compType = BSDF::EDeltaReflection;
if (!vertex->propagatePerturbation(m_scene,
pred, predEdge, succEdge, succ, compType,
length, desiredType, mode)) {
#if MTS_MANIFOLD_DEBUG == 1
cout << "update(): failed in propagatePerturbation()" << endl;
#endif
++statsUpdateFailed;
return false;
}
Float relerr = (vn.p - succ->getPosition()).length() /
std::max(std::max(std::abs(vn.p.x),
std::abs(vn.p.y)), std::abs(vn.p.z));
if (relerr > 1e-3f) {
// be extra-cautious
#if MTS_MANIFOLD_DEBUG == 1
cout << "update(): failed, relative error of propagatePerturbation() too high:" << relerr << endl;
#endif
++statsUpdateFailed;
return false;
}
}
}
return true;
}
Float SpecularManifold::det(const Path &path, int a, int b, int c) {
int k = path.length();
if (a == 0 || a == k)
std::swap(a, c);
int step = b > a ? 1 : -1, nGlossy = 0, nSpecular = 0;
for (int i=a + step; i != c; i += step) {
if (path.vertex(i)->isConnectable())
++nGlossy;
else
++nSpecular;
}
if (nGlossy <= 1) /* No glossy materials -- we don't need this derivative */
return 1.0f;
bool success = init(path, a, c);
BDAssert(success);
int b_idx = std::abs(b-a);
SimpleVertex &vb = m_vertices[b_idx];
const PathVertex *pb = path.vertex(b);
if (pb->isMediumInteraction()) {
vb.n = Vector(path.edge(a < b ? (b-1) : b)->d);
} else {
vb.n = pb->getShadingNormal();
}
coordinateSystem(vb.n, vb.dpdu, vb.dpdv);
if (!computeTangents()) {
Log(EWarn, "Could not compute tangents!");
return 0.0f;
}
m_vertices[b_idx].a.setZero();
m_vertices[b_idx].b.setIdentity();
m_vertices[b_idx].c.setZero();
if (nSpecular == 0) {
/* The chain only consists of glossy vertices -- simply compute the
determinant of the block tridiagonal matrix A.
See D.K. Salkuyeh, Comments on "A note on a three-term recurrence for a
tridiagonal matrix", Appl. Math. Comput. 176 (2006) 442-444. */
Matrix2x2 Di(0.0f), D = m_vertices[1].b;
Float det = D.det();
for (size_t i=2; i A(2*(nGlossy + nSpecular), 2*(nGlossy + nSpecular));
A.setZero();
for (int j=0, i=0; j= 0) {
A(2*i, 2*(j-1)) = m_vertices[j+1].a(0,0);
A(2*i, 2*(j-1)+1) = m_vertices[j+1].a(0,1);
A(2*i+1, 2*(j-1)) = m_vertices[j+1].a(1,0);
A(2*i+1, 2*(j-1)+1) = m_vertices[j+1].a(1,1);
}
A(2*i, 2*j) = m_vertices[j+1].b(0,0);
A(2*i, 2*j+1) = m_vertices[j+1].b(0,1);
A(2*i+1, 2*j) = m_vertices[j+1].b(1,0);
A(2*i+1, 2*j+1) = m_vertices[j+1].b(1,1);
if (j+1 < nGlossy + nSpecular) {
A(2*i, 2*(j+1)) = m_vertices[j+1].c(0,0);
A(2*i, 2*(j+1)+1) = m_vertices[j+1].c(0,1);
A(2*i+1, 2*(j+1)) = m_vertices[j+1].c(1,0);
A(2*i+1, 2*(j+1)+1) = m_vertices[j+1].c(1,1);
}
++i;
}
/* Compute the inverse and "cross out" irrelevant columns and rows */
Eigen::Matrix Ai = A.inverse();
for (int i=0; i(2*i, 2*i).setIdentity();
}
return std::abs(Ai.determinant());
}
}
Float SpecularManifold::multiG(const Path &path, int a, int b) {
if (a == 0)
++a;
else if (a == path.length())
--a;
if (b == 0)
++b;
else if (b == path.length())
--b;
int step = b > a ? 1 : -1;
while (!path.vertex(b)->isConnectable())
b -= step;
while (!path.vertex(a)->isConnectable())
a += step;
Float result = 1;
BDAssert(path.vertex(a)->isConnectable() && path.vertex(b)->isConnectable());
for (int i = a + step, start = a; i != b + step; i += step) {
if (path.vertex(i)->isConnectable()) {
result *= G(path, start, i);
start = i;
}
}
return result;
}
Float SpecularManifold::G(const Path &path, int a, int b) {
if (std::abs(a-b) == 1) {
if (a > b)
std::swap(a, b);
return path.edge(a)->evalCached(path.vertex(a),
path.vertex(b), PathEdge::EGeometricTerm)[0];
}
Assert(path.vertex(a)->isConnectable());
Assert(path.vertex(b)->isConnectable());
int step = b > a ? 1 : -1;
bool success = init(path, a, b);
BDAssert(success);
SimpleVertex &last = m_vertices[m_vertices.size()-1];
const PathVertex *vb = path.vertex(b);
if (!vb->isOnSurface()) {
last.n = Vector(path.edge(a < b ? (b-1) : b)->d);
} else {
last.n = vb->getShadingNormal();
}
coordinateSystem(last.n, last.dpdu, last.dpdv);
statsNonManifold.incrementBase();
if (!computeTangents()) {
++statsNonManifold;
Log(EWarn, "SpecularManifold::evalG(): non-manifold configuration!");
return 0;
}
Float result;
if (m_vertices[0].type == EPinnedDirection) {
result = cross(m_vertices[0].map(1, 0), m_vertices[0].map(0, 1)).length();
} else if (m_vertices[0].type == EPinnedPosition) {
Vector d = m_vertices[1].p - m_vertices[0].p;
Float lengthSqr = d.lengthSquared(), invLength = 1/std::sqrt(lengthSqr);
result = cross(m_vertices[1].map(1, 0), m_vertices[1].map(0, 1)).length() / lengthSqr;
if (path.vertex(a)->isOnSurface())
result *= absDot(d, path.vertex(a)->getShadingNormal()) * invLength;
if (path.vertex(a+step)->isOnSurface())
result *= absDot(d, path.vertex(a+step)->getShadingNormal()) * invLength;
} else {
Log(EError, "Invalid vertex type!");
return 0;
}
return result;
}
std::string SpecularManifold::SimpleVertex::toString() const {
std::ostringstream oss;
oss << "SimpleVertex[" << endl
<< " type = ";
switch (type) {
case EPinnedPosition: oss << "pinnedPosition"; break;
case EPinnedDirection: oss << "pinnedDirection"; break;
case EReflection: oss << "reflection"; break;
case ERefraction: oss << "refraction"; break;
case EMedium: oss << "medium"; break;
case EMovable: oss << "movable"; break;
default: SLog(EError, "Unknown vertex type!");
}
oss << "," << endl
<< " p = " << p.toString() << "," << endl
<< " n = " << n.toString() << "," << endl
<< " m = " << m.toString() << "," << endl
<< " dpdu = " << dpdu.toString() << "," << endl
<< " dpdv = " << dpdv.toString() << "," << endl
<< " dndu = " << dndu.toString() << "," << endl
<< " dndv = " << dndv.toString() << "," << endl
<< " eta = " << eta << "," << endl
<< " object = " << (object ? indent(object->toString()).c_str() : "null") << endl
<< "]";
return oss.str();
}
std::string SpecularManifold::toString() const {
std::ostringstream oss;
oss << "SpecularManifold[" << endl;
for (size_t i=0; i " << indent(m_vertices[i].toString());
if (i+1 < m_vertices.size())
oss << ",";
oss << endl;
}
oss << "]";
return oss.str();
}
MTS_IMPLEMENT_CLASS(SpecularManifold, false, Object)
MTS_NAMESPACE_END