source: trunk/VUT/GtpVisibilityPreprocessor/src/SamplingPreprocessor.cpp @ 335

Revision 335, 18.1 KB checked in by mattausch, 19 years ago (diff)

reprogrammed splitplane evaluation. updated default.env

Line 
1#include "SceneGraph.h"
2#include "KdTree.h"
3#include "SamplingPreprocessor.h"
4#include "X3dExporter.h"
5#include "Environment.h"
6#include "MutualVisibility.h"
7#include "Polygon3.h"
8#include "ViewCell.h"
9
10SamplingPreprocessor::SamplingPreprocessor(): mPass(0), mSampleRays(NULL)
11{
12  // this should increase coherence of the samples
13  environment->GetIntValue("Sampling.samplesPerPass", mSamplesPerPass);
14  environment->GetIntValue("Sampling.totalSamples", mTotalSamples);
15  environment->GetIntValue("BspTree.Construction.samples", mBspConstructionSamples);
16
17  mKdPvsDepth = 100;
18  mStats.open("stats.log");
19
20}
21
22SamplingPreprocessor::~SamplingPreprocessor()
23{
24        CLEAR_CONTAINER(mSampleRays);
25}
26
27void
28SamplingPreprocessor::SetupRay(Ray &ray,
29                                                           const Vector3 &point,
30                                                           const Vector3 &direction,
31                                                           const int type)
32{
33  ray.intersections.clear();
34  ray.leaves.clear();
35  ray.meshes.clear();
36  ray.viewCells.clear();
37
38  //  cout<<point<<" "<<direction<<endl;
39  ray.Init(point, direction, type);
40}
41
42KdNode *
43SamplingPreprocessor::GetNodeForPvs(KdLeaf *leaf)
44{
45  KdNode *node = leaf;
46  while (node->mParent && node->mDepth > mKdPvsDepth)
47    node = node->mParent;
48  return node;
49}
50
51bool
52SamplingPreprocessor::BuildBspTree()
53{
54        // delete old tree
55        DEL_PTR(mBspTree);
56        mBspTree = new BspTree(&mUnbounded);
57
58        ObjectContainer objects;
59       
60        switch (BspTree::sConstructionMethod)
61        {
62        case BspTree::FROM_INPUT_VIEW_CELLS:
63                mBspTree->SetGenerateViewCells(false);
64                mBspTree->Construct(mViewCells);
65                break;
66        case BspTree::FROM_SCENE_GEOMETRY:
67                DeleteViewCells(); // we generate new view cells
68                mBspTree->SetGenerateViewCells(true);
69                mSceneGraph->CollectObjects(&objects);
70                mBspTree->Construct(objects);
71                Debug << "collecting view cells" << endl;
72                mBspTree->CollectViewCells(mViewCells);
73                break;
74        case BspTree::FROM_RAYS:
75                DeleteViewCells(); // we generate new view cells
76                mBspTree->SetGenerateViewCells(true);
77                mBspTree->Construct(mSampleRays);
78                Debug << "collecting view cells" << endl;
79                mBspTree->CollectViewCells(mViewCells);
80                break;
81        default:
82                Debug << "Error: Method not available\n";
83                break;
84        }
85       
86       
87        return true;
88}
89
90int
91SamplingPreprocessor::AddNodeSamples(Intersectable *object,
92                                                                                                                                                 const Ray &ray
93                                                                                                                                                 )
94{
95  int contributingSamples = 0;
96  int j;
97  for (j=0; j < ray.leaves.size(); j++) {
98    KdNode *node = GetNodeForPvs( ray.leaves[j] );
99    contributingSamples += object->mKdPvs.AddSample(node);
100  }
101   
102  if (mPass > 10)
103    for (j=1; j < ((int)ray.leaves.size() - 1); j++) {
104      ray.leaves[j]->AddPassingRay(ray, contributingSamples ? 1 : 0);
105  }
106 
107  return contributingSamples;
108}
109
110
111int SamplingPreprocessor::AddObjectSamples(Intersectable *obj, const Ray &ray)
112{
113        int contributingSamples = 0;
114        int j;
115 
116        // object can be seen from the view cell => add to view cell pvs
117        for (j=0; j < ray.viewCells.size(); ++ j)
118        {       // if ray not in unbounded space
119                if (ray.viewCells[j] != &mUnbounded)
120                        contributingSamples += ray.viewCells[j]->GetPvs().AddSample(obj);
121        }
122 
123        // rays passing through this viewcell
124        if (mPass > 1)
125                for (j=1; j < ((int)ray.viewCells.size() - 1); ++ j)
126                {
127                        if (ray.viewCells[j] != &mUnbounded)
128                                ray.viewCells[j]->AddPassingRay(ray, contributingSamples ? 1 : 0);
129                }
130 
131        return contributingSamples;
132}
133
134
135void
136SamplingPreprocessor::HoleSamplingPass()
137{
138  vector<KdLeaf *> leaves;
139  mKdTree->CollectLeaves(leaves);
140 
141  // go through all the leaves and evaluate their passing contribution
142  for (int i=0 ; i < leaves.size(); i++) {
143    KdLeaf *leaf = leaves[i];
144    cout<<leaf->mPassingRays<<endl;
145  }
146}
147
148
149int
150SamplingPreprocessor::CastRay(Intersectable *object, Ray &ray)
151{
152        int sampleContributions = 0;
153
154        // cast ray to KD tree to find intersection with other objects
155        mKdTree->CastRay(ray);
156       
157        if (mViewCellsType == BSP_VIEW_CELLS)
158        {
159                // cast ray to BSP tree to get intersection with view cells
160                if (mBspTree)
161                {
162                        mBspTree->CastRay(ray);
163               
164                        sampleContributions += AddObjectSamples(object, ray);
165                               
166                        if (!ray.intersections.empty()) // second intersection found
167                        {
168                                sampleContributions +=
169                                        AddObjectSamples(ray.intersections[0].mObject, ray);
170                        }
171                }
172        }
173        else
174        {
175                if (ray.leaves.size()) {
176                        sampleContributions += AddNodeSamples(object, ray);
177               
178                        if (ray.intersections.size()) {
179                                sampleContributions += AddNodeSamples(ray.intersections[0].mObject, ray);
180                        }
181                }
182        }       
183       
184        return sampleContributions;
185}
186
187//  void
188//  SamplingPreprocessor::AvsGenerateRandomRay(Ray &ray)
189//  {
190//    int objId = RandomValue(0, mObjects.size());
191//    Intersectable *object = objects[objId];
192//    object->GetRandomSurfacePoint(point, normal);
193//    direction = UniformRandomVector(normal);
194//    SetupRay(ray, point, direction);
195//  }
196
197//  void
198//  SamplingPreprocessor::AvsHandleRay(Ray &ray)
199//  {
200//    int sampleContributions = 0;
201
202//    mKdTree->CastRay(ray);
203 
204//    if (ray.leaves.size()) {
205//      sampleContributions += AddNodeSamples(object, ray, pass);
206   
207//      if (ray.intersections.size()) {
208//        sampleContributions += AddNodeSamples(ray.intersections[0].mObject, ray, pass);
209//      }
210//    }
211//  }
212
213//  void
214//  SamplingPreprocessor::AvsBorderSampling(Ray &ray)
215//  {
216 
217
218//  }
219
220//  void
221//  SamplingPreprocessor::AvsPass()
222//  {
223//    Ray ray;
224//    while (1) {
225//      AvsGenerateRay(ray);
226//      HandleRay(ray);
227//      while ( !mRayQueue.empty() ) {
228//        Ray ray = mRayQueue.pop();
229//        mRayQueue.pop();
230//        AdaptiveBorderSampling(ray);
231//      }
232//    }
233 
234 
235
236//  }
237
238
239int
240SamplingPreprocessor::CastEdgeSamples(
241                                                                                                                                                        Intersectable *object,
242                                                                                                                                                        const Vector3 &point,
243                                                                                                                                                        MeshInstance *mi,
244                                                                                                                                                        const int samples
245                                                                                                                                                        )
246{
247        Ray ray;
248        int maxTries = samples*10;
249        int i;
250        int rays = 0;
251        int edgeSamplesContributions = 0;
252        for (i=0; i < maxTries && rays < samples; i++) {
253                // pickup a random face of each mesh
254                Mesh *mesh = mi->GetMesh();
255                int face = RandomValue(0, mesh->mFaces.size()-1);
256               
257                Polygon3 poly(mesh->mFaces[face], mesh);
258                poly.Scale(1.001);
259                // now extend a random edge of the face
260                int edge = RandomValue(0, poly.mVertices.size()-1);
261                float t = RandomValue(0.0f,1.0f);
262                Vector3 target = t*poly.mVertices[edge] + (1.0f-t)*poly.mVertices[(edge + 1)%
263                                                                                                                                                                                                                                                                                 poly.mVertices.size()];
264                SetupRay(ray, point, target - point, Ray::LOCAL_RAY);
265                if (!mesh->CastRay(ray, mi)) {
266                        // the rays which intersect the mesh have been discarded since they are not tangent
267                        // to the mesh
268                        rays++;
269                        edgeSamplesContributions += CastRay(object, ray);
270                }
271        }
272        return edgeSamplesContributions;
273}
274
275bool
276SamplingPreprocessor::ComputeVisibility()
277{
278 
279  // pickup an object
280  ObjectContainer objects;
281 
282  mSceneGraph->CollectObjects(&objects);
283
284  Vector3 point, normal, direction;
285  Ray ray;
286
287  long startTime = GetTime();
288 
289  int i;
290  int totalSamples = 0;
291
292  int pvsOut = Min((int)objects.size(), 10);
293
294  vector<Ray> rays[10];
295
296  vector<Ray> vcRays[5];
297  ViewCellContainer pvsViewCells;
298  vector<Ray> viewCellRays; // used for BSP tree construction
299
300  while (totalSamples < mTotalSamples) {
301                int passContributingSamples = 0;
302                int passSampleContributions = 0;
303                int passSamples = 0;
304                int index = 0;
305                Real maxTime = 0;
306                int maxTimeIdx = 0;
307
308                cout << "totalSamples: "  << totalSamples << endl;
309
310                for (i = 0; i < objects.size(); i++) {
311                                               
312                        KdNode *nodeToSample = NULL;
313                        Intersectable *object = objects[i];
314               
315                        int pvsSize = 0;
316                        if (mViewCellsType == KD_VIEW_CELLS)
317                                pvsSize = object->mKdPvs.GetSize();
318                                               
319                        if (0 && pvsSize) {
320                                // mail all nodes from the pvs
321                                Intersectable::NewMail();
322                                KdPvsMap::iterator i = object->mKdPvs.mEntries.begin();
323                         
324                                for (; i != object->mKdPvs.mEntries.end(); i++) {
325                                        KdNode *node = (*i).first;
326                                        node->Mail();
327                                }
328                               
329                                int maxTries = 2*pvsSize;
330                                Debug << "Finding random neighbour" << endl;   
331                                for (int tries = 0; tries < 10; tries++) {
332                                        index = RandomValue(0, pvsSize - 1);
333                                        KdPvsData data;
334                                        KdNode *node;
335                                        object->mKdPvs.GetData(index, node, data);
336                                        nodeToSample = mKdTree->FindRandomNeighbor(node, true);
337                                        if (nodeToSample)
338                                                break;
339                                }
340                        }
341                       
342                        if (0 && pvsSize && mPass == 1000 ) {
343                                // mail all nodes from the pvs
344                                Intersectable::NewMail();
345                                KdPvsMap::iterator i = object->mKdPvs.mEntries.begin();
346                                for (; i != object->mKdPvs.mEntries.end(); i++) {
347                                        KdNode *node = (*i).first;
348                                        node->Mail();
349                                }
350                                Debug << "Get all neighbours from PVS" << endl;
351                                vector<KdNode *> invisibleNeighbors;
352                                // get all neighbors of all PVS nodes
353                                i = object->mKdPvs.mEntries.begin();
354                                for (; i != object->mKdPvs.mEntries.end(); i++) {
355                                        KdNode *node = (*i).first;
356                                        mKdTree->FindNeighbors(node, invisibleNeighbors, true);
357                                        AxisAlignedBox3 box = object->GetBox();
358                                        for (int j=0; j < invisibleNeighbors.size(); j++) {
359                                                int visibility = ComputeBoxVisibility(mSceneGraph,
360                                                                                                                                                                                                        mKdTree,
361                                                                                                                                                                                                        box,
362                                                                                                                                                                                                        mKdTree->GetBox(invisibleNeighbors[j]),
363                                                                                                                                                                                                        1e-6f);
364                                                //            exit(0);
365                                        }
366                                        // now rank all the neighbors according to probability that a new
367                                        // sample creates some contribution
368                                }
369                        }
370                       
371                        object->GetRandomSurfacePoint(point, normal);
372                       
373                        long samplesPerObjStart = GetTime();
374
375                        bool viewcellSample = true;
376                        int sampleContributions;
377
378                        if (viewcellSample) {
379                                nodeToSample = mKdTree->GetRandomLeaf(Plane3(normal, point));
380                                       
381                                for (int k=0; k < mSamplesPerPass; k++) {
382                                        if (nodeToSample) {
383                                                int maxTries = 5;
384                                               
385                                                for (int tries = 0; tries < maxTries; tries++) {
386                                                        direction = mKdTree->GetBox(nodeToSample).GetRandomPoint() - point;
387                                                       
388                                                        if (DotProd(direction, normal) > Limits::Small)
389                                                                break;                                                 
390                                                }
391                                               
392                                                if (tries == maxTries)
393                                                        direction = UniformRandomVector(normal);
394                                        }
395                                        else {
396                                                direction = UniformRandomVector(normal);
397                                        }
398                                       
399                                        // construct a ray
400                                        SetupRay(ray, point, direction, Ray::LOCAL_RAY);
401                                       
402                                        sampleContributions = CastRay(object, ray);
403
404                                        //-- CORR matt: put block inside loop
405                                        if (sampleContributions) {
406                                                passContributingSamples++;
407                                                passSampleContributions += sampleContributions;
408                                        }
409
410                                        if ( i < pvsOut )
411                                                rays[i].push_back(ray);
412               
413                                        if (!ray.intersections.empty()) {
414                                                // check whether we can add this to the rays
415                                                for (int j = 0; j < pvsOut; j++) {
416                                                        if (objects[j] == ray.intersections[0].mObject) {
417                                                                rays[j].push_back(ray);
418                                                        }
419                                                }
420                                        }
421                                        //-------------------
422
423                                        if (mViewCellsType == BSP_VIEW_CELLS)
424                                        {
425                        // save rays for bsp tree construction
426                                                if ((BspTree::sConstructionMethod = BspTree::FROM_RAYS) &&
427                                                        (totalSamples < mBspConstructionSamples))
428                                                {
429                                                        mSampleRays.push_back(new Ray(ray));
430                                                }
431                                                else
432                                                {
433                            // construct BSP tree using the samples
434                                                        if (!mBspTree)
435                                                        {
436                                                                BuildBspTree();
437
438                                                                cout << "generated " << (int)mViewCells.size() << " view cells" << endl;
439                                                                passContributingSamples += mBspTree->GetStat().contributingSamples;
440                                                                passSampleContributions += mBspTree->GetStat().pvs;
441
442                                                                BspTreeStatistics(Debug);       
443                                                                Export("vc_bsptree.x3d", false, false, true);
444                                                        }
445                                                                                                       
446                                                        // some random view cells for output
447                                                        if (pvsViewCells.empty())
448                                                        {
449                                                                int vcPvsOut = Min((int)mViewCells.size(), 5);
450                                                       
451                                                                for (int j = 0; j < vcPvsOut; ++ j)
452                                                                {
453                                                                        int idx = Random((int)mViewCells.size());
454                                                                        Debug << "output view cell=" << idx << endl;
455                                                                        pvsViewCells.push_back(mViewCells[Random((int)mViewCells.size())]);
456                                                                }
457                                                        }
458                                                        else
459                                                        {
460                                                                // check whether we can add the current ray to the rays
461                                                                for (int k = 0; k < (int)ray.viewCells.size(); ++ k)
462                                                                        for (int j = 0; j < (int)pvsViewCells.size(); ++ j)
463                                                                                if (pvsViewCells[j] == ray.viewCells[k])
464                                                                                        vcRays[j].push_back(ray);                                                       
465                                                        }
466                                                }                               
467                                        }
468               
469                                }
470                        } else {
471                                // edge samples
472                                // get random visible mesh
473                                //                              object->GetRandomVisibleMesh(Plane3(normal, point));
474                        }
475                               
476                        // measure maximal time for samples per object
477                        Real t = TimeDiff(samplesPerObjStart, GetTime());
478
479                        if (t > maxTime)
480                        {
481                                maxTime = t;
482                                maxTimeIdx = i;
483                        }
484       
485                        // CORR matt: must add all samples
486                        passSamples += mSamplesPerPass;
487                }
488       
489                totalSamples += passSamples;
490               
491                //    if (pass>10)
492                //      HoleSamplingPass();
493   
494                mPass++;
495
496                int pvsSize = 0;
497       
498                if (mViewCellsType == BSP_VIEW_CELLS) {
499                        for (i=0; i < mViewCells.size(); i++) {
500                                ViewCell *vc = mViewCells[i];
501                                pvsSize += vc->GetPvs().GetSize();
502                        }
503                } else  {
504                        for (i=0; i < objects.size(); i++) {
505                                Intersectable *object = objects[i];
506                                pvsSize += object->mKdPvs.GetSize();
507                        }
508                }
509
510                Debug << "maximal time needed for pass: " << maxTime << " (object " << maxTimeIdx << ")" << endl;
511
512                float avgRayContrib = (passContributingSamples > 0) ?
513                        passSampleContributions/(float)passContributingSamples : 0;
514
515                cout << "#Pass " << mPass << " : t = " << TimeDiff(startTime, GetTime())*1e-3 << "s" << endl;
516                cout << "#TotalSamples=" << totalSamples/1000
517                                 << "k   #SampleContributions=" << passSampleContributions << " ("
518                                 << 100*passContributingSamples/(float)passSamples<<"%)" << " avgPVS="
519                                 << pvsSize/(float)objects.size() << endl
520                                 << "avg ray contrib=" << avgRayContrib << endl;
521               
522                mStats <<
523                        "#Pass\n" <<mPass<<endl<<
524                        "#Time\n" << TimeDiff(startTime, GetTime())*1e-3 << endl<<
525                        "#TotalSamples\n" << totalSamples<< endl<<
526                        "#SampleContributions\n" << passSampleContributions << endl <<
527                        "#PContributingSamples\n"<<100*passContributingSamples/(float)passSamples<<endl <<
528                        "#AvgPVS\n"<< pvsSize/(float)objects.size() << endl <<
529                        "#AvgRayContrib\n" << avgRayContrib << endl;
530        }
531
532        if (mViewCellsType == KD_VIEW_CELLS)   
533                cout << "#totalPvsSize=" << mKdTree->CollectLeafPvs() << endl;
534 
535  //  HoleSamplingPass();
536  if (0) {
537    Exporter *exporter = Exporter::GetExporter("ray-density.x3d");
538    exporter->SetExportRayDensity(true);
539    exporter->ExportKdTree(*mKdTree);
540
541        if (mViewCellsType == BSP_VIEW_CELLS)   
542                exporter->ExportBspTree(*mBspTree);
543
544    delete exporter;
545  }
546 
547  bool exportRays = false;
548  if (exportRays) {
549    Exporter *exporter = NULL;
550    exporter = Exporter::GetExporter("sample-rays.x3d");
551    exporter->SetWireframe();
552    exporter->ExportKdTree(*mKdTree);
553        exporter->ExportBspTree(*mBspTree);
554
555    for (i=0; i < pvsOut; i++)
556      exporter->ExportRays(rays[i], 1000, RgbColor(1, 0, 0));
557    exporter->SetFilled();
558         
559    delete exporter;
560  }
561 
562  if (1) {
563  if (mViewCellsType == BSP_VIEW_CELLS)
564  {
565          bool exportSplits = false;
566          environment->GetBoolValue("BspTree.exportSplits", exportSplits);
567
568          // export the bsp splits
569          if (exportSplits)
570          {
571                  Exporter *exporter = Exporter::GetExporter("bsp_splits.x3d");
572
573                  if (exporter)
574                  {     
575                          Material m;
576                          m.mDiffuseColor = RgbColor(1, 0, 0);
577                          exporter->SetForcedMaterial(m);
578                          exporter->SetWireframe();
579                          exporter->ExportBspSplits(*mBspTree);
580
581                          // take forced material, else big scenes cannot be viewed
582                          m.mDiffuseColor = RgbColor(0, 1, 0);
583                          exporter->SetForcedMaterial(m);
584                          exporter->SetFilled();
585
586                          exporter->ResetForcedMaterial();
587
588                          if (1)
589                          {
590                                  Material m;//= RandomMaterial();
591                                  m.mDiffuseColor = RgbColor(0, 0, 1);
592                                  exporter->SetForcedMaterial(m);
593                 
594                                  for (int j = 0; j < objects.size(); ++ j)
595                                           exporter->ExportIntersectable(objects[j]);
596                       
597                                  delete exporter;
598                          }
599                  }
600          }
601
602          for (int j = 0; j < pvsViewCells.size(); ++ j)
603          {
604                  ViewCell *vc = pvsViewCells[j];
605
606                  Intersectable::NewMail();
607                  char s[64]; sprintf(s, "bsp-pvs%04d.x3d", j);
608
609                  Exporter *exporter = Exporter::GetExporter(s);
610                  exporter->SetFilled();
611
612                  ViewCellPvsMap::iterator it = vc->GetPvs().mEntries.begin();
613
614                  Material m;//= RandomMaterial();
615                  m.mDiffuseColor = RgbColor(0, 1, 0);
616                  exporter->SetForcedMaterial(m);
617
618                  exporter->ExportViewCell(vc);
619
620                  Debug << j << ": pvs size=" << (int)vc->GetPvs().GetSize()
621                            << ", piercing rays=" << (int)vcRays[j].size() << endl;
622
623                  exporter->SetWireframe();
624
625                  // export view cells
626                  m.mDiffuseColor = RgbColor(1, 0, 1);
627                  exporter->SetForcedMaterial(m);
628                  exporter->ExportViewCells(mViewCells);
629                       
630                  // export rays piercing this view cell
631                  exporter->ExportRays(vcRays[j], 1000, RgbColor(0, 1, 0));
632
633                  m.mDiffuseColor = RgbColor(1, 0, 0);
634                  exporter->SetForcedMaterial(m);
635
636                  // output pvs of view cell
637                  for (; it != vc->GetPvs().mEntries.end(); ++ it)
638                  {
639                          Intersectable *intersect = (*it).first;
640                          if (!intersect->Mailed())
641                          {
642                                  exporter->ExportIntersectable(intersect);
643                                  intersect->Mail();
644                          }                     
645                  }
646               
647                  // output rest of the objects
648                  if (1)
649                  {
650                          Material m;//= RandomMaterial();
651                          m.mDiffuseColor = RgbColor(0, 0, 1);
652                          exporter->SetForcedMaterial(m);
653
654                          for (int j = 0; j < objects.size(); ++ j)
655                                  if (!objects[j]->Mailed())
656                                  {
657                                          //if (j == 2198)m.mDiffuseColor = RgbColor(1, 0, 1);
658                                          //else m.mDiffuseColor = RgbColor(1, 1, 0);
659                                          exporter->SetForcedMaterial(m);
660                                          exporter->ExportIntersectable(objects[j]);
661                                          objects[j]->Mail();
662                                  }
663                  }
664                  DEL_PTR(exporter);
665                }
666  } 
667
668   for (int k=0; k < pvsOut; k++) {
669      Intersectable *object = objects[k];
670      char s[64];
671      sprintf(s, "sample-pvs%04d.x3d", k);
672      Exporter *exporter = Exporter::GetExporter(s);
673      exporter->SetWireframe();
674
675       
676          KdPvsMap::iterator i = object->mKdPvs.mEntries.begin();
677          Intersectable::NewMail();
678                 
679          // avoid adding the object to the list
680          object->Mail();
681          ObjectContainer visibleObjects;
682
683          for (; i != object->mKdPvs.mEntries.end(); i++)
684          {
685                  KdNode *node = (*i).first;
686                  exporter->ExportBox(mKdTree->GetBox(node));
687                  mKdTree->CollectObjects(node, visibleObjects);
688          }
689
690          exporter->ExportRays(rays[k], 1000, RgbColor(0, 1, 0));
691          exporter->SetFilled();
692
693          for (int j = 0; j < visibleObjects.size(); j++)
694                  exporter->ExportIntersectable(visibleObjects[j]);
695       
696
697          Material m;
698          m.mDiffuseColor = RgbColor(1, 0, 0);
699          exporter->SetForcedMaterial(m);
700          exporter->ExportIntersectable(object);
701
702          delete exporter;
703    }
704  }
705 
706  return true;
707}
Note: See TracBrowser for help on using the repository browser.