/* 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 #include #include #include #include #include #include #include #include #include #include #define MTS_FILEFORMAT_HEADER 0x041C #define MTS_FILEFORMAT_VERSION_V3 0x0003 #define MTS_FILEFORMAT_VERSION_V4 0x0004 MTS_NAMESPACE_BEGIN TriMesh::TriMesh(const std::string &name, size_t triangleCount, size_t vertexCount, bool hasNormals, bool hasTexcoords, bool hasVertexColors, bool flipNormals, bool faceNormals) : Shape(Properties()), m_triangleCount(triangleCount), m_vertexCount(vertexCount), m_flipNormals(flipNormals), m_faceNormals(faceNormals) { m_name = name; m_triangles = new Triangle[m_triangleCount]; m_positions = new Point[m_vertexCount]; m_normals = hasNormals ? new Normal[m_vertexCount] : NULL; m_texcoords = hasTexcoords ? new Point2[m_vertexCount] : NULL; m_colors = hasVertexColors ? new Color3[m_vertexCount] : NULL; m_tangents = NULL; m_surfaceArea = m_invSurfaceArea = -1; m_mutex = new Mutex(); } TriMesh::TriMesh(const Properties &props) : Shape(props), m_triangles(NULL), m_positions(NULL), m_normals(NULL), m_texcoords(NULL), m_tangents(NULL), m_colors(NULL) { /* 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); m_triangles = NULL; m_surfaceArea = m_invSurfaceArea = -1; m_mutex = new Mutex(); } TriMesh::TriMesh(Stream *stream, int index) : Shape(Properties()), m_triangles(NULL), m_positions(NULL), m_normals(NULL), m_texcoords(NULL), m_tangents(NULL), m_colors(NULL) { m_mutex = new Mutex(); loadCompressed(stream, index); } /* Flags used to identify available data during serialization */ enum ETriMeshFlags { EHasNormals = 0x0001, EHasTexcoords = 0x0002, EHasTangents = 0x0004, // unused EHasColors = 0x0008, EFaceNormals = 0x0010, ESinglePrecision = 0x1000, EDoublePrecision = 0x2000 }; TriMesh::TriMesh(Stream *stream, InstanceManager *manager) : Shape(stream, manager), m_tangents(NULL) { m_name = stream->readString(); m_aabb = AABB(stream); uint32_t flags = stream->readUInt(); m_vertexCount = stream->readSize(); m_triangleCount = stream->readSize(); m_positions = new Point[m_vertexCount]; stream->readFloatArray(reinterpret_cast(m_positions), m_vertexCount * sizeof(Point)/sizeof(Float)); m_faceNormals = flags & EFaceNormals; if (flags & EHasNormals) { m_normals = new Normal[m_vertexCount]; stream->readFloatArray(reinterpret_cast(m_normals), m_vertexCount * sizeof(Normal)/sizeof(Float)); } else { m_normals = NULL; } if (flags & EHasTexcoords) { m_texcoords = new Point2[m_vertexCount]; stream->readFloatArray(reinterpret_cast(m_texcoords), m_vertexCount * sizeof(Point2)/sizeof(Float)); } else { m_texcoords = NULL; } if (flags & EHasColors) { m_colors = new Color3[m_vertexCount]; stream->readFloatArray(reinterpret_cast(m_colors), m_vertexCount * sizeof(Color3)/sizeof(Float)); } else { m_colors = NULL; } m_triangles = new Triangle[m_triangleCount]; stream->readUIntArray(reinterpret_cast(m_triangles), m_triangleCount * sizeof(Triangle)/sizeof(uint32_t)); m_flipNormals = false; m_surfaceArea = m_invSurfaceArea = -1; m_mutex = new Mutex(); configure(); } static void readHelper(Stream *stream, bool fileDoublePrecision, Float *target, size_t count, size_t nelems) { #if defined(SINGLE_PRECISION) bool hostDoublePrecision = false; #else bool hostDoublePrecision = true; #endif size_t size = count * nelems; if (fileDoublePrecision == hostDoublePrecision) { /* Precision matches - load directly into memory */ stream->readFloatArray(target, size); } else if (fileDoublePrecision) { /* Double -> Single conversion */ double *temp = new double[size]; stream->readDoubleArray(temp, size); for (size_t i=0; i Double conversion */ float *temp = new float[size]; stream->readSingleArray(temp, size); for (size_t i=0; i stream = _stream; if (stream->getByteOrder() != Stream::ELittleEndian) Log(EError, "Tried to unserialize a shape from a stream, " "which was not previously set to little endian byte order!"); const short version = readHeader(stream); if (index != 0) { const size_t offset = readOffset(stream, version, index); stream->seek(offset); stream->skip(sizeof(short) * 2); // Skip the header } stream = new ZStream(stream); stream->setByteOrder(Stream::ELittleEndian); uint32_t flags = stream->readUInt(); if (version == MTS_FILEFORMAT_VERSION_V4) m_name = stream->readString(); m_vertexCount = stream->readSize(); m_triangleCount = stream->readSize(); bool fileDoublePrecision = flags & EDoublePrecision; m_faceNormals = flags & EFaceNormals; if (m_positions) delete[] m_positions; m_positions = new Point[m_vertexCount]; readHelper(stream, fileDoublePrecision, reinterpret_cast(m_positions), m_vertexCount, sizeof(Point)/sizeof(Float)); if (m_normals) delete[] m_normals; if (flags & EHasNormals) { m_normals = new Normal[m_vertexCount]; readHelper(stream, fileDoublePrecision, reinterpret_cast(m_normals), m_vertexCount, sizeof(Normal)/sizeof(Float)); } else { m_normals = NULL; } if (m_texcoords) delete[] m_texcoords; if (flags & EHasTexcoords) { m_texcoords = new Point2[m_vertexCount]; readHelper(stream, fileDoublePrecision, reinterpret_cast(m_texcoords), m_vertexCount, sizeof(Point2)/sizeof(Float)); } else { m_texcoords = NULL; } if (m_colors) delete[] m_colors; if (flags & EHasColors) { m_colors = new Color3[m_vertexCount]; readHelper(stream, fileDoublePrecision, reinterpret_cast(m_colors), m_vertexCount, sizeof(Color3)/sizeof(Float)); } else { m_colors = NULL; } m_triangles = new Triangle[m_triangleCount]; stream->readUIntArray(reinterpret_cast(m_triangles), m_triangleCount * sizeof(Triangle)/sizeof(uint32_t)); m_surfaceArea = m_invSurfaceArea = -1; m_flipNormals = false; } short TriMesh::readHeader(Stream *stream) { short format = stream->readShort(); if (format == 0x1C04) { Log(EError, "Encountered a geometry file generated by an old " "version of Mitsuba. Please re-import the scene to update this file " "to the current format."); } if (format != MTS_FILEFORMAT_HEADER) { Log(EError, "Encountered an invalid file format!"); } short version = stream->readShort(); if (version != MTS_FILEFORMAT_VERSION_V3 && version != MTS_FILEFORMAT_VERSION_V4) { Log(EError, "Encountered an incompatible file version!"); } return version; } size_t TriMesh::readOffset(Stream *stream, short version, int idx) { const size_t streamSize = stream->getSize(); /* Determine the position of the requested substream. This is stored at the end of the file */ stream->seek(streamSize - sizeof(uint32_t)); uint32_t count = stream->readUInt(); if (idx < 0 || idx > (int) count) { Log(EError, "Unable to unserialize mesh, " "shape index is out of range! (requested %i out of 0..%i)", idx, count-1); } // Seek to the correct position if (version == MTS_FILEFORMAT_VERSION_V4) { stream->seek(stream->getSize() - sizeof(uint64_t) * (count-idx) - sizeof(uint32_t)); return stream->readSize(); } else { Assert(version == MTS_FILEFORMAT_VERSION_V3); stream->seek(stream->getSize() - sizeof(uint32_t) * (count-idx + 1)); return stream->readUInt(); } } int TriMesh::readOffsetDictionary(Stream *stream, short version, std::vector& outOffsets) { const size_t streamSize = stream->getSize(); stream->seek(streamSize - sizeof(uint32_t)); const uint32_t count = stream->readUInt(); // Check if the stream is large enough to contain that number of meshes const size_t minSize = sizeof(uint32_t) + count * ( 2*sizeof(uint16_t) // Header + sizeof(uint32_t) // Flags + sizeof(char) // Name + 2*sizeof(uint64_t) // Number of vertices and triangles + 3*sizeof(float) // One vertex + 3*sizeof(uint32_t)); // One triangle if (streamSize >= minSize) { outOffsets.resize(count); if (version == MTS_FILEFORMAT_VERSION_V4) { stream->seek(stream->getSize() - sizeof(uint64_t) * count - sizeof(uint32_t)); if (typeid(size_t) == typeid(uint64_t)) { stream->readArray(&outOffsets[0], count); } else { for (size_t i = 0; i < count; ++i) outOffsets[i] = stream->readSize(); } } else { stream->seek(stream->getSize() - sizeof(uint32_t) * (count + 1)); Assert(version == MTS_FILEFORMAT_VERSION_V3); if (typeid(size_t) == typeid(uint32_t)) { stream->readArray(&outOffsets[0], count); } else { for (size_t i = 0; i < count; ++i) { outOffsets[i] = (size_t) stream->readUInt(); } } } return count; } else { Log(EDebug, "The serialized mesh does not contain a valid dictionary"); return -1; } } TriMesh::~TriMesh() { if (m_positions) delete[] m_positions; if (m_normals) delete[] m_normals; if (m_texcoords) delete[] m_texcoords; if (m_tangents) delete[] m_tangents; if (m_colors) delete[] m_colors; if (m_triangles) delete[] m_triangles; } std::string TriMesh::getName() const { return m_name; } AABB TriMesh::getAABB() const { return m_aabb; } Float TriMesh::pdfPosition(const PositionSamplingRecord &pRec) const { return m_invSurfaceArea; } void TriMesh::configure() { Shape::configure(); if (!m_aabb.isValid()) { /* Most shape objects should compute the AABB while loading the geometry -- but let's be on the safe side */ for (size_t i=0; igetType() & BSDF::EAnisotropic) || m_bsdf->usesRayDifferentials())) computeUVTangents(); /* For manifold exploration: always compute UV tangents when a glossy material is involved. TODO: find a way to avoid this expense (compute on demand?) */ if (hasBSDF() && (m_bsdf->getType() & BSDF::EGlossy)) computeUVTangents(); } void TriMesh::prepareSamplingTable() { if (m_triangleCount == 0) { Log(EError, "Encountered an empty triangle mesh!"); return; } LockGuard guard(m_mutex); if (m_surfaceArea < 0) { /* Generate a PDF for sampling wrt. area */ m_areaDistr.reserve(m_triangleCount); for (size_t i=0; i(this)->prepareSamplingTable(); return m_surfaceArea; } void TriMesh::samplePosition(PositionSamplingRecord &pRec, const Point2 &_sample) const { if (EXPECT_NOT_TAKEN(m_surfaceArea < 0)) const_cast(this)->prepareSamplingTable(); Point2 sample(_sample); size_t index = m_areaDistr.sampleReuse(sample.y); pRec.p = m_triangles[index].sample(m_positions, m_normals, m_texcoords, pRec.n, pRec.uv, sample); pRec.pdf = m_invSurfaceArea; pRec.measure = EArea; } struct Vertex { Point p; Point2 uv; Color3 col; inline Vertex() : p(0.0f), uv(0.0f), col(0.0f) { } }; /// For using vertices as keys in an associative structure struct vertex_key_order : public std::binary_function { static int compare(const Vertex &v1, const Vertex &v2) { if (v1.p.x < v2.p.x) return -1; else if (v1.p.x > v2.p.x) return 1; if (v1.p.y < v2.p.y) return -1; else if (v1.p.y > v2.p.y) return 1; if (v1.p.z < v2.p.z) return -1; else if (v1.p.z > v2.p.z) return 1; if (v1.uv.x < v2.uv.x) return -1; else if (v1.uv.x > v2.uv.x) return 1; if (v1.uv.y < v2.uv.y) return -1; else if (v1.uv.y > v2.uv.y) return 1; for (int i=0; i v2.col[i]) return 1; } return 0; } bool operator()(const Vertex &v1, const Vertex &v2) const { return compare(v1, v2) < 0; } }; /// Used in \ref TriMesh::rebuildTopology() struct TopoData { size_t idx; /// Triangle index bool clustered; /// Has the tri-vert. pair been assigned to a cluster? inline TopoData() { } inline TopoData(size_t idx, bool clustered) : idx(idx), clustered(clustered) { } }; void TriMesh::rebuildTopology(Float maxAngle) { typedef std::multimap MMap; typedef std::pair MPair; const Float dpThresh = std::cos(degToRad(maxAngle)); if (m_normals) { delete[] m_normals; m_normals = NULL; } if (m_tangents) { delete[] m_tangents; m_tangents = NULL; } Log(EInfo, "Rebuilding the topology of \"%s\" (" SIZE_T_FMT " triangles, " SIZE_T_FMT " vertices, max. angle = %f)", m_name.c_str(), m_triangleCount, m_vertexCount, maxAngle); ref timer = new Timer(); MMap vertexToFace; std::vector newPositions; std::vector newTexcoords; std::vector newColors; std::vector faceNormals(m_triangleCount); Triangle *newTriangles = new Triangle[m_triangleCount]; newPositions.reserve(m_vertexCount); if (m_texcoords != NULL) newTexcoords.reserve(m_vertexCount); if (m_colors != NULL) newColors.reserve(m_vertexCount); /* Create an associative list and precompute a few things */ for (size_t i=0; ifirst); MMap::iterator end = vertexToFace.upper_bound(it->first); /* Perform a greedy clustering of normals */ for (MMap::iterator it2 = start; it2 != end; it2++) { const Vertex &v = it2->first; const TopoData &t1 = it2->second; Normal n1(faceNormals[t1.idx]); if (t1.clustered) continue; uint32_t vertexIdx = (uint32_t) newPositions.size(); newPositions.push_back(v.p); if (m_texcoords) newTexcoords.push_back(v.uv); if (m_colors) newColors.push_back(v.col); for (MMap::iterator it3 = it2; it3 != end; ++it3) { TopoData &t2 = it3->second; if (t2.clustered) continue; Normal n2(faceNormals[t2.idx]); if (n1 == n2 || dot(n1, n2) > dpThresh) { const Triangle &tri = m_triangles[t2.idx]; Triangle &newTri = newTriangles[t2.idx]; for (int i=0; i<3; ++i) { if (m_positions[tri.idx[i]] == v.p) newTri.idx[i] = vertexIdx; } t2.clustered = true; } } } it = end; } for (size_t i=0; igetMilliseconds(), m_vertexCount); configure(); } void TriMesh::computeNormals(bool force) { int invalidNormals = 0; if (m_faceNormals) { if (m_normals) { delete[] m_normals; m_normals = NULL; } if (m_flipNormals) { /* Change the winding order */ for (size_t i=0; i 0) Log(EWarn, "\"%s\": Unable to generate %i vertex normals", m_name.c_str(), invalidNormals); } void TriMesh::computeUVTangents() { // int degenerate = 0; if (!m_texcoords) { bool anisotropic = hasBSDF() && m_bsdf->getType() & BSDF::EAnisotropic; if (anisotropic) Log(EError, "\"%s\": computeUVTangents(): texture coordinates " "are required to generate tangent vectors. If you want to render with an anisotropic " "material, please make sure that all associated shapes have valid texture coordinates.", getName().c_str()); return; } if (m_tangents) return; m_tangents = new TangentSpace[m_triangleCount]; memset(m_tangents, 0, sizeof(TangentSpace)*m_triangleCount); for (size_t i=0; i 0) Log(EWarn, "\"%s\": computeTangentSpace(): Mesh contains %i " "degenerate triangles!", getName().c_str(), degenerate); #endif } void TriMesh::getNormalDerivative(const Intersection &its, Vector &dndu, Vector &dndv, bool shadingFrame) const { if (!shadingFrame || !m_normals) { dndu = dndv = Vector(0.0f); } else { Assert(its.primIndex < m_triangleCount); const Triangle &tri = m_triangles[its.primIndex]; uint32_t idx0 = tri.idx[0], idx1 = tri.idx[1], idx2 = tri.idx[2]; const Point &p0 = m_positions[idx0], &p1 = m_positions[idx1], &p2 = m_positions[idx2]; /* Recompute the barycentric coordinates, since 'its.uv' may have been overwritten with coordinates of the texture "parameterization". */ Vector rel = its.p - p0, du = p1 - p0, dv = p2 - p0; Float b1 = dot(du, rel), b2 = dot(dv, rel), /* Normal equations */ a11 = dot(du, du), a12 = dot(du, dv), a22 = dot(dv, dv), det = a11 * a22 - a12 * a12; if (det == 0) { dndu = dndv = Vector(0.0f); return; } Float invDet = 1.0f / det, u = ( a22 * b1 - a12 * b2) * invDet, v = (-a12 * b1 + a11 * b2) * invDet, w = 1 - u - v; const Normal &n0 = m_normals[idx0], &n1 = m_normals[idx1], &n2 = m_normals[idx2]; /* Now compute the derivative of "normalize(u*n1 + v*n2 + (1-u-v)*n0)" with respect to [u, v] in the local triangle parameterization. Since d/du [f(u)/|f(u)|] = [d/du f(u)]/|f(u)| - f(u)/|f(u)|^3 , this results in */ Normal N(u * n1 + v * n2 + w * n0); Float il = 1.0f / N.length(); N *= il; dndu = (n1 - n0) * il; dndu -= N * dot(N, dndu); dndv = (n2 - n0) * il; dndv -= N * dot(N, dndv); if (m_tangents) { /* Compute derivatives with respect to a specified texture UV parameterization. */ const Point2 &uv0 = m_texcoords[idx0], &uv1 = m_texcoords[idx1], &uv2 = m_texcoords[idx2]; Vector2 duv1 = uv1 - uv0, duv2 = uv2 - uv0; det = duv1.x * duv2.y - duv1.y * duv2.x; if (det == 0) { dndu = dndv = Vector(0.0f); return; } invDet = 1.0f / det; Vector dndu_ = ( duv2.y * dndu - duv1.y * dndv) * invDet; Vector dndv_ = (-duv2.x * dndu + duv1.x * dndv) * invDet; dndu = dndu_; dndv = dndv_; } } } ref TriMesh::createTriMesh() { return this; } void TriMesh::serialize(Stream *stream, InstanceManager *manager) const { Shape::serialize(stream, manager); uint32_t flags = 0; if (m_normals) flags |= EHasNormals; if (m_texcoords) flags |= EHasTexcoords; if (m_colors) flags |= EHasColors; if (m_faceNormals) flags |= EFaceNormals; stream->writeString(m_name); m_aabb.serialize(stream); stream->writeUInt(flags); stream->writeSize(m_vertexCount); stream->writeSize(m_triangleCount); stream->writeFloatArray(reinterpret_cast(m_positions), m_vertexCount * sizeof(Point)/sizeof(Float)); if (m_normals) stream->writeFloatArray(reinterpret_cast(m_normals), m_vertexCount * sizeof(Normal)/sizeof(Float)); if (m_texcoords) stream->writeFloatArray(reinterpret_cast(m_texcoords), m_vertexCount * sizeof(Point2)/sizeof(Float)); if (m_colors) stream->writeFloatArray(reinterpret_cast(m_colors), m_vertexCount * sizeof(Color3)/sizeof(Float)); stream->writeUIntArray(reinterpret_cast(m_triangles), m_triangleCount * sizeof(Triangle)/sizeof(uint32_t)); } ref TriMesh::fromBlender(const std::string &name, size_t faceCount, void *_facePtr, size_t vertexCount, void *_vertexPtr, void *_uvPtr, void *_colPtr, short mat_nr) { struct MFace { uint32_t v[4]; int16_t mat_nr; uint8_t edcode, flag; }; struct MVert { float co[3]; int16_t no[3]; uint8_t flag, bweight; }; struct MCol { uint8_t a, r, g, b; }; struct MLoopUV { float uv[2]; int32_t flag; }; MFace *facePtr = (MFace *) _facePtr; MVert *vertexPtr = (MVert *) _vertexPtr; MCol *colPtr = (MCol *) _colPtr; MLoopUV *uvPtr = (MLoopUV *) _uvPtr; boost::unordered_map vertexMap; uint32_t triangleCtr = 0, vertexCtr = 0; for (int i=0; i triMesh = new TriMesh(name, triangleCtr, vertexCtr, true, uvPtr != NULL, colPtr != NULL); uint32_t *triangles = (uint32_t *) triMesh->getTriangles(); Point *vertexPositions = (Point *) triMesh->getVertexPositions(); Normal *vertexNormals = (Normal *) triMesh->getVertexNormals(); Color3 *vertexColors = (Color3 *) triMesh->getVertexColors(); Point2 *vertexTexcoords = (Point2 *) triMesh->getVertexTexcoords(); for (int i=0; i::iterator it = vertexMap.begin(); it != vertexMap.end(); ++it) { const MVert &vertex = vertexPtr[it->first]; uint32_t idx = it->second; vertexPositions[idx] = Point3(vertex.co[0], vertex.co[1], vertex.co[2]); vertexNormals[idx] = normalize(Normal( vertex.no[0] * normalScale, vertex.no[1] * normalScale, vertex.no[2] * normalScale )); if (uvPtr) { const MLoopUV &uv = uvPtr[it->first]; vertexTexcoords[idx] = Point2(uv.uv[0], uv.uv[1]); } if (colPtr) { const MCol &col = colPtr[it->first]; vertexColors[idx] = Color3( col.r * rgbScale, col.g * rgbScale, col.b * rgbScale ); } } return triMesh; } void TriMesh::writeOBJ(const fs::path &path) const { fs::ofstream os(path); os << "o " << m_name << endl; for (size_t i=0; i stream = _stream; if (stream->getByteOrder() != Stream::ELittleEndian) Log(EError, "Tried to unserialize a shape from a stream, " "which was not previously set to little endian byte order!"); stream->writeShort(MTS_FILEFORMAT_HEADER); stream->writeShort(MTS_FILEFORMAT_VERSION_V4); stream = new ZStream(stream); #if defined(SINGLE_PRECISION) uint32_t flags = ESinglePrecision; #else uint32_t flags = EDoublePrecision; #endif if (m_normals) flags |= EHasNormals; if (m_texcoords) flags |= EHasTexcoords; if (m_colors) flags |= EHasColors; if (m_faceNormals) flags |= EFaceNormals; stream->writeUInt(flags); stream->writeString(m_name); stream->writeSize(m_vertexCount); stream->writeSize(m_triangleCount); stream->writeFloatArray(reinterpret_cast(m_positions), m_vertexCount * sizeof(Point)/sizeof(Float)); if (m_normals) stream->writeFloatArray(reinterpret_cast(m_normals), m_vertexCount * sizeof(Normal)/sizeof(Float)); if (m_texcoords) stream->writeFloatArray(reinterpret_cast(m_texcoords), m_vertexCount * sizeof(Point2)/sizeof(Float)); if (m_colors) stream->writeFloatArray(reinterpret_cast(m_colors), m_vertexCount * sizeof(Color3)/sizeof(Float)); stream->writeUIntArray(reinterpret_cast(m_triangles), m_triangleCount * sizeof(Triangle)/sizeof(uint32_t)); } size_t TriMesh::getPrimitiveCount() const { return m_triangleCount; } size_t TriMesh::getEffectivePrimitiveCount() const { return m_triangleCount; } std::string TriMesh::toString() const { std::ostringstream oss; oss << getClass()->getName() << "[" << endl << " name = \"" << m_name<< "\"," << endl << " triangleCount = " << m_triangleCount << "," << endl << " vertexCount = " << m_vertexCount << "," << endl << " faceNormals = " << (m_faceNormals ? "true" : "false") << "," << endl << " hasNormals = " << (m_normals ? "true" : "false") << "," << endl << " hasTexcoords = " << (m_texcoords ? "true" : "false") << "," << endl << " hasTangents = " << (m_tangents ? "true" : "false") << "," << endl << " hasColors = " << (m_colors ? "true" : "false") << "," << endl << " surfaceArea = " << m_surfaceArea << "," << endl << " aabb = " << m_aabb.toString() << "," << endl << " bsdf = " << indent(m_bsdf.toString()) << "," << endl; if (isMediumTransition()) oss << " interiorMedium = " << indent(m_interiorMedium.toString()) << "," << endl << " exteriorMedium = " << indent(m_exteriorMedium.toString()) << "," << endl; oss << " subsurface = " << indent(m_subsurface.toString()) << "," << endl << " emitter = " << indent(m_emitter.toString()) << endl << "]"; return oss.str(); } MTS_IMPLEMENT_CLASS_S(TriMesh, false, Shape) MTS_NAMESPACE_END