#include "Environment.h" #include "GvsPreprocessor.h" #include "GlRenderer.h" #include "VssRay.h" #include "ViewCellsManager.h" #include "Triangle3.h" #include "IntersectableWrapper.h" #include "Plane3.h" #include "RayCaster.h" #include "Exporter.h" #include "SamplingStrategy.h" #include "BvHierarchy.h" namespace GtpVisibilityPreprocessor { #define GVS_DEBUG 1 struct VizStruct { Polygon3 *enlargedTriangle; Triangle3 originalTriangle; VssRay *ray; }; static const float MIN_DIST = 0.01f; static vector vizContainer; GvsPreprocessor::GvsPreprocessor(): Preprocessor(), mSamplingType(SamplingStrategy::VIEWCELL_BASED_DISTRIBUTION), //mSamplingType(SamplingStrategy::DIRECTION_BASED_DISTRIBUTION), mNumViewCells(0), mCurrentViewCell(NULL) { Environment::GetSingleton()->GetIntValue("GvsPreprocessor.totalSamples", mTotalSamples); Environment::GetSingleton()->GetIntValue("GvsPreprocessor.initialSamples", mInitialSamples); Environment::GetSingleton()->GetIntValue("GvsPreprocessor.samplesPerPass", mSamplesPerPass); Environment::GetSingleton()->GetFloatValue("GvsPreprocessor.epsilon", mEps); Environment::GetSingleton()->GetFloatValue("GvsPreprocessor.threshold", mThreshold); Environment::GetSingleton()->GetBoolValue("GvsPreprocessor.perViewCell", mPerViewCell); char gvsStatsLog[100]; Environment::GetSingleton()->GetStringValue("GvsPreprocessor.stats", gvsStatsLog); mGvsStatsStream.open(gvsStatsLog); Debug << "Gvs preprocessor options" << endl; Debug << "number of total samples: " << mTotalSamples << endl; Debug << "number of initial samples: " << mInitialSamples << endl; Debug << "number of samples per pass: " << mSamplesPerPass << endl; Debug << "threshold: " << mThreshold << endl; Debug << "epsilon: " << mEps << endl; Debug << "stats: " << gvsStatsLog << endl; if (1) mOnlyRandomSampling = false; else mOnlyRandomSampling = true; mGvsStats.Reset(); } GvsPreprocessor::~GvsPreprocessor() { // clean ray queue while (!mRayQueue.empty()) { // handle next ray VssRay *ray = mRayQueue.top(); mRayQueue.pop(); delete ray; } } bool GvsPreprocessor::NextViewCell() { if (mViewCellsManager->GetNumViewCells() == mNumViewCells) return false; // no more view cells if (1) mCurrentViewCell = mViewCellsManager->GetViewCell(mNumViewCells); else { // HACK int idx = (int)RandomValue(0.0f, (float)mViewCellsManager->GetNumViewCells() - 0.5f); mCurrentViewCell = mViewCellsManager->GetViewCell(idx); } if (!mCurrentViewCell->GetMesh()) mViewCellsManager->CreateMesh(mCurrentViewCell); ++ mNumViewCells; return true; } bool GvsPreprocessor::CheckDiscontinuity(const VssRay ¤tRay, const Triangle3 &hitTriangle, const VssRay &oldRay) { // the predicted hitpoint: we expect to hit the same mesh again const Vector3 predictedHit = CalcPredictedHitPoint(currentRay, hitTriangle, oldRay); const float predictedLen = Magnitude(predictedHit - currentRay.mOrigin); const float len = Magnitude(currentRay.mTermination - currentRay.mOrigin); // distance large => this is likely to be a discontinuity #if 1 if ((predictedLen - len) > mThreshold) #else // rather use relative distance if ((predictedLen / len) > mThreshold) #endif { //cout << "r"; // apply reverse sampling to find the gap VssRay *newRay = ReverseSampling(currentRay, hitTriangle, oldRay); if (!newRay) return false; // set flag for visualization newRay->mFlags |= VssRay::ReverseSample; // if ray is not further processed => delete ray if (!HandleRay(newRay)) { delete newRay; } else if (0 && GVS_DEBUG && (mVssRays.size() < 9)) { mVssRays.push_back(new VssRay(oldRay)); mVssRays.push_back(new VssRay(currentRay)); mVssRays.push_back(new VssRay(*newRay)); } return true; } return false; } bool GvsPreprocessor::HandleRay(VssRay *vssRay) { // store the rays + the intersected view cells const bool storeRaysForViz = false; //GVS_DEBUG; if (!mPerViewCell) { mViewCellsManager->ComputeSampleContribution(*vssRay, true, storeRaysForViz, true); } else { mViewCellsManager->ComputeSampleContribution(*vssRay, true, mCurrentViewCell, true); } // some pvs contribution for this ray? if (!vssRay->mPvsContribution) return false; if (GVS_DEBUG) mVssRays.push_back(new VssRay(*vssRay)); // add new ray to ray queue mRayQueue.push(vssRay); if (storeRaysForViz) { VssRay *nray = new VssRay(*vssRay); nray->mFlags = vssRay->mFlags; // store ray in contributing view cell ViewCellContainer::const_iterator vit, vit_end = vssRay->mViewCells.end(); for (vit = vssRay->mViewCells.begin(); vit != vit_end; ++ vit) { (*vit)->GetOrCreateRays()->push_back(nray); } } ++ mGvsStats.mTotalContribution; return true; } /** Creates 3 new vertices for triangle vertex with specified index. */ void GvsPreprocessor::CreateDisplacedVertices(VertexContainer &vertices, const Triangle3 &hitTriangle, const VssRay &ray, const int index) const { const int indexU = (index + 1) % 3; const int indexL = (index == 0) ? 2 : index - 1; const Vector3 a = hitTriangle.mVertices[index] - ray.GetOrigin(); const Vector3 b = hitTriangle.mVertices[indexU] - hitTriangle.mVertices[index]; const Vector3 c = hitTriangle.mVertices[index] - hitTriangle.mVertices[indexL]; const float len = Magnitude(a); const Vector3 dir1 = Normalize(CrossProd(a, b)); //N((pi-xp)×(pi+1- pi)); const Vector3 dir2 = Normalize(CrossProd(a, c)); // N((pi-xp)×(pi- pi-1)) const Vector3 dir3 = DotProd(dir2, dir1) > 0 ? // N((pi-xp)×di,i-1+di,i+1×(pi-xp)) Normalize(dir2 + dir1) : Normalize(CrossProd(a, dir1) + CrossProd(dir2, a)); // compute the new three hit points // pi, i + 1: pi+ e·|pi-xp|·di, j const Vector3 pt1 = hitTriangle.mVertices[index] + mEps * len * dir1; // pi, i - 1: pi+ e·|pi-xp|·di, j const Vector3 pt2 = hitTriangle.mVertices[index] + mEps * len * dir2; // pi, i: pi+ e·|pi-xp|·di, j const Vector3 pt3 = hitTriangle.mVertices[index] + mEps * len * dir3; vertices.push_back(pt2); vertices.push_back(pt3); vertices.push_back(pt1); } void GvsPreprocessor::EnlargeTriangle(VertexContainer &vertices, const Triangle3 &hitTriangle, const VssRay &ray) const { CreateDisplacedVertices(vertices, hitTriangle, ray, 0); CreateDisplacedVertices(vertices, hitTriangle, ray, 1); CreateDisplacedVertices(vertices, hitTriangle, ray, 2); } Vector3 GvsPreprocessor::CalcPredictedHitPoint(const VssRay &newRay, const Triangle3 &hitTriangle, const VssRay &oldRay) const { // find the intersection of the plane induced by the // hit triangle with the new ray Plane3 plane(hitTriangle.GetNormal(), hitTriangle.mVertices[0]); const Vector3 hitPt = plane.FindIntersection(newRay.mTermination, newRay.mOrigin); return hitPt; } static bool EqualVisibility(const VssRay &a, const VssRay &b) { return a.mTerminationObject == b.mTerminationObject; } int GvsPreprocessor::SubdivideEdge(const Triangle3 &hitTriangle, const Vector3 &p1, const Vector3 &p2, const VssRay &ray1, const VssRay &ray2, const VssRay &oldRay) { // cast reverse rays if necessary CheckDiscontinuity(ray1, hitTriangle, oldRay); CheckDiscontinuity(ray2, hitTriangle, oldRay); if (EqualVisibility(ray1, ray2) || (Magnitude(p1 - p2) <= MIN_DIST)) { return 0; } // the new subdivision point const Vector3 p = (p1 + p2) * 0.5f; // cast ray into the new point SimpleRay sray(oldRay.mOrigin, p - oldRay.mOrigin, SamplingStrategy::GVS, 1.0f); VssRay *newRay = mRayCaster->CastRay(sray, mViewCellsManager->GetViewSpaceBox(), mPerViewCell); if (!newRay) return 0; newRay->mFlags |= VssRay::BorderSample; // add new ray to queue const bool enqueued = HandleRay(newRay); // subdivide further const int samples1 = SubdivideEdge(hitTriangle, p1, p, ray1, *newRay, oldRay); const int samples2 = SubdivideEdge(hitTriangle, p, p2, *newRay, ray2, oldRay); // this ray will not be further processed if (!enqueued) delete newRay; return samples1 + samples2 + 1; } int GvsPreprocessor::AdaptiveBorderSampling(const VssRay ¤tRay) { Intersectable *tObj = currentRay.mTerminationObject; Triangle3 hitTriangle; // other types not implemented yet if (tObj->Type() == Intersectable::TRIANGLE_INTERSECTABLE) { hitTriangle = dynamic_cast(tObj)->GetItem(); } else { cout << "not yet implemented" << endl; } VertexContainer enlargedTriangle; /// create 3 new hit points for each vertex EnlargeTriangle(enlargedTriangle, hitTriangle, currentRay); /// create rays from sample points and handle them SimpleRayContainer simpleRays; simpleRays.reserve(9); VertexContainer::const_iterator vit, vit_end = enlargedTriangle.end(); for (vit = enlargedTriangle.begin(); vit != vit_end; ++ vit) { const Vector3 rayDir = (*vit) - currentRay.GetOrigin(); SimpleRay sr(currentRay.GetOrigin(), rayDir, SamplingStrategy::GVS, 1.0f); simpleRays.AddRay(sr); } if (0) { // visualize enlarged triangles VizStruct dummy; dummy.enlargedTriangle = new Polygon3(enlargedTriangle); dummy.originalTriangle = hitTriangle; vizContainer.push_back(dummy); } // cast rays to triangle vertices and determine visibility VssRayContainer vssRays; // don't cast double rays as we need only the forward rays const bool castDoubleRays = false; // cannot prune invalid rays because we have to compare adjacent rays. const bool pruneInvalidRays = false; // keep origin for per view cell sampling CastRays(simpleRays, vssRays, castDoubleRays, pruneInvalidRays, mPerViewCell); // set flags VssRayContainer::const_iterator rit, rit_end = vssRays.end(); for (rit = vssRays.begin(); rit != rit_end; ++ rit) { (*rit)->mFlags |= VssRay::BorderSample; } // handle rays EnqueueRays(vssRays); const int n = (int)vssRays.size(); int castRays = n; // recursivly subdivide each edge for (int i = 0; i < n; ++ i) { castRays += SubdivideEdge(hitTriangle, enlargedTriangle[i], enlargedTriangle[(i + 1) % n], *vssRays[i], *vssRays[(i + 1) % n], currentRay); } mGvsStats.mBorderSamples += castRays; return castRays; } Vector3 GvsPreprocessor::GetPassingPoint(const VssRay ¤tRay, const Triangle3 &occluder, const VssRay &oldRay) const { //-- The plane p = (xp, hit(x), hit(xold)) is intersected //-- with the newly found occluder (xold is the previous ray from //-- which x was generated). On the intersecting line, we select a point //-- pnew which lies just outside of the new triangle so the ray //-- just passes through the gap const Plane3 plane(currentRay.GetOrigin(), currentRay.GetTermination(), oldRay.GetTermination()); Vector3 pt1, pt2; const bool intersects = occluder.GetPlaneIntersection(plane, pt1, pt2); if (!intersects) cerr << "big error!! no intersection" << endl; // get the intersection point on the old ray const Plane3 triPlane(occluder.GetNormal(), occluder.mVertices[0]); const float t = triPlane.FindT(oldRay.mOrigin, oldRay.mTermination); const Vector3 pt3 = oldRay.mOrigin + t * (oldRay.mTermination - oldRay.mOrigin); // Evaluate new hitpoint just outside the triangle Vector3 newPoint; const float eps = mEps; // the point is chosen to be on the side closer to the original ray if (Distance(pt1, pt3) < Distance(pt2, pt3)) { newPoint = pt1 + eps * (pt1 - pt2); } else { newPoint = pt2 + eps * (pt2 - pt1); } //cout << "passing point: " << newPoint << endl << endl; return newPoint; } VssRay *GvsPreprocessor::ReverseSampling(const VssRay ¤tRay, const Triangle3 &hitTriangle, const VssRay &oldRay) { // get triangle occluding the path to the hit mesh Triangle3 occluder; Intersectable *tObj = currentRay.mTerminationObject; // q: why can this happen? if (!tObj) return NULL; // other types not implemented yet if (tObj->Type() == Intersectable::TRIANGLE_INTERSECTABLE) occluder = dynamic_cast(tObj)->GetItem(); else cout << "not yet implemented" << endl; // get a point which is passing just outside of the occluder const Vector3 newPoint = GetPassingPoint(currentRay, occluder, oldRay); const Vector3 predicted = CalcPredictedHitPoint(currentRay, hitTriangle, oldRay); Vector3 newDir, newOrigin; //-- Construct the mutated ray with xnew, //-- dir = predicted(x)- pnew as direction vector newDir = predicted - newPoint; // take xnew, p = intersect(viewcell, line(pnew, predicted(x)) as origin ? // difficult to say!! const float offset = 0.5f; newOrigin = newPoint - newDir * offset; ////////////// //-- for per view cell sampling, we must check for intersection //-- with the current view cell if (mPerViewCell) { // send ray to view cell static Ray ray; ray.Clear(); ray.Init(newOrigin, -newDir, Ray::LOCAL_RAY); //ray.Init(newOrigin, -newDir, Ray::LINE_SEGMENT); //cout << "z"; // check if ray intersects view cell if (!mCurrentViewCell->CastRay(ray)) return NULL; Ray::Intersection &hit = ray.intersections[0]; //cout << "q"; // the ray starts from the view cell newOrigin = ray.Extrap(hit.mT); } const SimpleRay simpleRay(newOrigin, newDir, SamplingStrategy::GVS, 1.0f); VssRay *reverseRay = mRayCaster->CastRay(simpleRay, mViewCellsManager->GetViewSpaceBox(), mPerViewCell); ++ mGvsStats.mReverseSamples; return reverseRay; } int GvsPreprocessor::CastInitialSamples(const int numSamples, const int sampleType) { const long startTime = GetTime(); // generate simple rays SimpleRayContainer simpleRays; ViewCellBasedDistribution vcStrat(*this, mCurrentViewCell); GenerateRays(numSamples, vcStrat, simpleRays); //cout << "sr: " << simpleRays.size() << endl; // generate vss rays VssRayContainer samples; const bool castDoubleRays = false; const bool pruneInvalidRays = true; const bool keepOrigin = mPerViewCell; CastRays(simpleRays, samples, castDoubleRays, pruneInvalidRays, keepOrigin); // add to ray queue EnqueueRays(samples); //Debug << "generated " << numSamples << " samples in " << TimeDiff(startTime, GetTime()) * 1e-3 << " secs" << endl; return (int)simpleRays.size(); } void GvsPreprocessor::EnqueueRays(VssRayContainer &samples) { // add samples to ray queue VssRayContainer::const_iterator vit, vit_end = samples.end(); for (vit = samples.begin(); vit != vit_end; ++ vit) { HandleRay(*vit); } } int GvsPreprocessor::ProcessQueue() { int castSamples = 0; ++ mGvsStats.mGvsPass; while (!mRayQueue.empty() )//&& (mGvsStats.mTotalSamples + castSamples < mTotalSamples) ) { // handle next ray VssRay *ray = mRayQueue.top(); mRayQueue.pop(); if (!ray) cout << "Error!!!!!" << endl; const int newSamples = AdaptiveBorderSampling(*ray); castSamples += newSamples; //cout << newSamples << " "; delete ray; } return castSamples; } void ExportVssRays(Exporter *exporter, const VssRayContainer &vssRays) { VssRayContainer vcRays, vcRays2, vcRays3; VssRayContainer::const_iterator rit, rit_end = vssRays.end(); // prepare some rays for output for (rit = vssRays.begin(); rit != rit_end; ++ rit) { //const float p = RandomValue(0.0f, (float)vssRays.size()); if (1)//(p < raysOut) { if ((*rit)->mFlags & VssRay::BorderSample) { vcRays.push_back(*rit); } else if ((*rit)->mFlags & VssRay::ReverseSample) { vcRays2.push_back(*rit); } else { vcRays3.push_back(*rit); } } } //exporter->ExportRays(vcRays, RgbColor(1, 0, 0)); exporter->ExportRays(vcRays2, RgbColor(0, 1, 0)); //exporter->ExportRays(vcRays3, RgbColor(1, 1, 1)); } void GvsPreprocessor::VisualizeViewCell(ViewCell *vc) { Intersectable::NewMail(); Material m; char str[64]; sprintf(str, "pass%06d.wrl", mNumViewCells); Exporter *exporter = Exporter::GetExporter(str); if (!exporter) return; ObjectPvsIterator pit = vc->GetPvs().GetIterator(); // output PVS of view cell while (pit.HasMoreEntries()) { ObjectPvsEntry entry = pit.Next(); Intersectable *intersect = entry.mObject; if (intersect->Mailed()) continue; intersect->Mail(); //m.mDiffuseColor = RgbColor(1, 0, 0); m = RandomMaterial(); exporter->SetForcedMaterial(m); exporter->ExportIntersectable(intersect); } cout << "vssrays: " << (int)mVssRays.size() << endl; ExportVssRays(exporter, mVssRays); ///////////////// //-- export view cell geometry exporter->SetWireframe(); m.mDiffuseColor = RgbColor(0, 1, 0); exporter->SetForcedMaterial(m); mViewCellsManager->ExportViewCellGeometry(exporter, vc, NULL, NULL); //exporter->SetFilled(); DEL_PTR(exporter); } void GvsPreprocessor::VisualizeViewCells() { char str[64]; sprintf(str, "tmp/pass%06d_%04d-", mNumViewCells, mPass); // visualization if (mGvsStats.mPassContribution > 0) { const bool exportRays = true; const bool exportPvs = true; mViewCellsManager->ExportSingleViewCells(mObjects, 10, false, exportPvs, exportRays, 1000, str); } // remove pass samples ViewCellContainer::const_iterator vit, vit_end = mViewCellsManager->GetViewCells().end(); for (vit = mViewCellsManager->GetViewCells().begin(); vit != vit_end; ++ vit) { (*vit)->DelRayRefs(); } } void GvsPreprocessor::ProcessViewCell() { mGvsStats.mPerViewCellSamples = 0; int oldContribution = 0; int passSamples = 0; while (mGvsStats.mPerViewCellSamples < mTotalSamples) { // Ray queue empty => // cast a number of uniform samples to fill ray queue int newSamples = CastInitialSamples(mInitialSamples, mSamplingType); if (!mOnlyRandomSampling) newSamples += ProcessQueue(); passSamples += newSamples; mGvsStats.mPerViewCellSamples += newSamples; if (0 && passSamples % (mSamplesPerPass + 1) == mSamplesPerPass) { ++ mPass; mGvsStats.mPassContribution = mGvsStats.mTotalContribution - oldContribution; //////// //-- stats mGvsStats.mPass = mPass; mGvsStats.Stop(); mGvsStats.Print(mGvsStatsStream); //cout << "\nPass " << mPass << " #samples: " << mGvsStats.mTotalSamples << " of " << mTotalSamples << endl; // reset oldContribution = mGvsStats.mTotalContribution; mGvsStats.mPassContribution = 0; passSamples = 0; } } } void GvsPreprocessor::PerViewCellComputation() { const int maxViewCells = 25; while (mNumViewCells < maxViewCells && NextViewCell()) { cout << "processing view cell " << mNumViewCells << endl; // compute the pvs of the current view cell ProcessViewCell(); // exchange triangle pvs with objects UpdatePvs(mCurrentViewCell); if (GVS_DEBUG) { VisualizeViewCell(mCurrentViewCell); CLEAR_CONTAINER(mVssRays); } //////// //-- stats mGvsStats.mViewCells = mNumViewCells;//mPass; mGvsStats.mTotalPvs += mCurrentViewCell->GetPvs().GetSize(); mGvsStats.mTotalSamples += mGvsStats.mPerViewCellSamples; mGvsStats.Stop(); mGvsStats.Print(mGvsStatsStream); } } void GvsPreprocessor::UpdatePvs(ViewCell *currentViewCell) { ObjectPvs newPvs; BvhLeaf::NewMail(); ObjectPvsIterator pit = currentViewCell->GetPvs().GetIterator(); // output PVS of view cell while (pit.HasMoreEntries()) { ObjectPvsEntry entry = pit.Next(); Intersectable *intersect = entry.mObject; BvhLeaf *bv = intersect->mBvhLeaf; if (!bv || bv->Mailed()) continue; bv->Mail(); //m.mDiffuseColor = RgbColor(1, 0, 0); newPvs.AddSampleDirty(bv, 1.0f); } newPvs.SimpleSort(); currentViewCell->SetPvs(newPvs); } void GvsPreprocessor::GlobalComputation() { int passSamples = 0; int oldContribution = 0; while (mGvsStats.mTotalSamples < mTotalSamples) { // Ray queue empty => // cast a number of uniform samples to fill ray queue int newSamples = CastInitialSamples(mInitialSamples, mSamplingType); if (!mOnlyRandomSampling) newSamples += ProcessQueue(); passSamples += newSamples; mGvsStats.mTotalSamples += newSamples; if (passSamples % (mSamplesPerPass + 1) == mSamplesPerPass) { ++ mPass; mGvsStats.mPassContribution = mGvsStats.mTotalContribution - oldContribution; //////// //-- stats //cout << "\nPass " << mPass << " #samples: " << mGvsStats.mTotalSamples << " of " << mTotalSamples << endl; mGvsStats.mPass = mPass; mGvsStats.Stop(); mGvsStats.Print(mGvsStatsStream); // reset oldContribution = mGvsStats.mTotalContribution; mGvsStats.mPassContribution = 0; passSamples = 0; if (GVS_DEBUG) VisualizeViewCells(); } } } bool GvsPreprocessor::ComputeVisibility() { cout << "Gvs Preprocessor started\n" << flush; const long startTime = GetTime(); //Randomize(0); mGvsStats.Reset(); mGvsStats.Start(); if (!mLoadViewCells) { /// construct the view cells from the scratch ConstructViewCells(); // reset pvs already gathered during view cells construction mViewCellsManager->ResetPvs(); cout << "finished view cell construction" << endl; } else if (0) { //-- test successful view cells loading by exporting them again VssRayContainer dummies; mViewCellsManager->Visualize(mObjects, dummies); mViewCellsManager->ExportViewCells("test.xml.gz", mViewCellsManager->GetExportPvs(), mObjects); } mGvsStats.Stop(); mGvsStats.Print(mGvsStatsStream); if (mPerViewCell) { PerViewCellComputation(); } else { GlobalComputation(); } cout << "cast " << 2 * mGvsStats.mTotalSamples / (1e3f * TimeDiff(startTime, GetTime())) << "M rays/s" << endl; if (GVS_DEBUG) { Visualize(); CLEAR_CONTAINER(mVssRays); } return true; } void GvsPreprocessor::Visualize() { Exporter *exporter = Exporter::GetExporter("gvs.wrl"); if (!exporter) return; vector::const_iterator vit, vit_end = vizContainer.end(); for (vit = vizContainer.begin(); vit != vit_end; ++ vit) { exporter->SetWireframe(); exporter->ExportPolygon((*vit).enlargedTriangle); //Material m; exporter->SetFilled(); Polygon3 poly = Polygon3((*vit).originalTriangle); exporter->ExportPolygon(&poly); } VssRayContainer::const_iterator rit, rit_end = mVssRays.end(); for (rit = mVssRays.begin(); rit != rit_end; ++ rit) { Intersectable *obj = (*rit)->mTerminationObject; exporter->ExportIntersectable(obj); } ExportVssRays(exporter, mVssRays); delete exporter; } void GvsStatistics::Print(ostream &app) const { app << "#Pass\n" << mPass << endl; app << "#Time\n" << Time() << endl; app << "#TotalSamples\n" << mTotalSamples << endl; app << "#ScDiff\n" << mPassContribution << endl; app << "#SamplesContri\n" << mTotalContribution << endl; app << "#ReverseSamples\n" << mReverseSamples << endl; app << "#BorderSamples\n" << mBorderSamples << endl; app << "#GvsRuns\n" << mGvsPass << endl; app << "#ViewCells\n" << mViewCells << endl; app << "#TotalPvs\n" << mTotalPvs << endl; app << "#PerViewCellSamples\n" << mPerViewCellSamples << endl << endl; } }