VTK----VTK数据结构详解3(代码篇)

上篇文章(VTK----VTK数据结构详解(计算机篇)-CSDN博客)从计算机数据结构(数组、链表等)的角度对数据数组数据对象数据属性的实现原理进行了说明,下面从代码的层面详细说明它们的使用及相关实现逻辑。

1 数据数组

vtkFloatArray为例,下面是它的使用及其VTK内部实现的代码:

vtkNew<vtkFloatArray> scalars;
scalars->InsertTuple1(0, 1);

void vtkDataArray::InsertTuple1(vtkIdType i, double value)
{
  int numComp = this->GetNumberOfComponents();
  if (numComp != 1)
  {
    vtkErrorMacro(
      "The number of components do not match the number requested: " << numComp << " != 1");
  }
  this->InsertTuple(i, &value);
}


template <class ValueTypeT>
void vtkAOSDataArrayTemplate<ValueTypeT>::InsertTuple(vtkIdType tupleIdx, const double* tuple)
{
  if (this->EnsureAccessToTuple(tupleIdx))
  {
    // See note in SetTuple about std::copy vs for loops on MSVC.
    const vtkIdType valueIdx = tupleIdx * this->NumberOfComponents;
    ValueTypeT* data = this->Buffer->GetBuffer() + valueIdx;
    for (int i = 0; i < this->NumberOfComponents; ++i)
    {
      data[i] = static_cast<ValueType>(tuple[i]);
    }
    this->MaxId = std::max(this->MaxId, valueIdx + this->NumberOfComponents - 1);
  }
}


template <class DerivedT, class ValueTypeT>
bool vtkGenericDataArray<DerivedT, ValueTypeT>::EnsureAccessToTuple(vtkIdType tupleIdx)
{
  if (tupleIdx < 0)
  {
    return false;
  }
  vtkIdType minSize = (1 + tupleIdx) * this->NumberOfComponents;
  vtkIdType expectedMaxId = minSize - 1;
  if (this->MaxId < expectedMaxId)
  {
    if (this->Size < minSize)
    {
      if (!this->Resize(tupleIdx + 1))
      {
        return false;
      }
    }
    this->MaxId = expectedMaxId;
  }
  return true;
}

从代码可以看出,插入值采用的是数组指针偏移的方式。插入前,通过EnsureAccessToTuple函数先检查是否需要Resize。插入时,指针偏移tupleIdx * NumberOfComponents,tupleIdx是当前准备在元组中插入的索引位置,NumberOfComponents是元组大小(可以理解为子一级的数组,子一级数组大小是固定值),上面的例子调用的是InsertTuple1,对应是一元组,所以NumberOfComponents等于1。在vtkDataArray.h中可以看到其提供了InsertTuple1、InsertTuple2、InsertTuple3、InsertTuple4、InsertTuple6、InsertTuple9这些接口分别用于插入对应大小的元组元素。

2 数据对象

vtkPolyData

上一篇文章提到vtkPolyData通过维护四个单独的列表(顶点(vertices)、线(lines)、多边形(polygons)和三角形带(triangle strips))来间接表示单元的类型,下面通过一个例子来看看它的使用。

#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);
VTK_MODULE_INIT(vtkRenderingFreeType);
VTK_MODULE_INIT(vtkRenderingVolumeOpenGL2);
#include <vtkActor.h>
#include <vtkNamedColors.h>
#include <vtkPolyData.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkSphereSource.h>
#include <vtkOpenGLRenderer.h>
#include <vtkOpenGLState.h>

int main(int, char*[])
{
  vtkNew<vtkNamedColors> colors;

  static double pts[25][3] = {
    {0.00, 1.00, 0.00   }, {0.00, 0.71, 0.71   }, {0.00, 0.00, 1.00   }, {0.00, -0.71, 0.71  }, {0.00, -1.00, 0.00  }, 
    {0.00, 1.00, 0.00   }, {0.71, 0.71, 0.00   }, {1.00, 0.00, 0.00   }, {0.71, -0.71, 0.00  }, {0.00, -1.00, 0.00  }, 
    {0.00, 1.00, 0.00   }, {0.00, 0.71, -0.71  }, {0.00, 0.00, -1.00  }, {0.00, -0.71, -0.71 }, {0.00, -1.00, 0.00  }, 
    {0.00, 1.00, 0.00   }, {-0.71, 0.71, 0.00  }, {-1.00, 0.00, 0.00  }, {-0.71, -0.71, 0.00 }, {0.00, -1.00, 0.00  }, 
    {0.00, 1.00, 0.00   }, {0.00, 0.71, 0.71   }, {0.00, 0.00, 1.00   }, {0.00, -0.71, 0.71  }, {0.00, -1.00, 0.00  }
  } ;
  
  static vtkIdType lines[80][2] = {
    {0, 5    }, {0, 1    }, {1, 6    }, {5, 6    }, {5, 1    }, {1, 6    }, {1, 2    }, {2, 7    }, 
    {6, 7    }, {6, 2    }, {2, 7    }, {2, 3    }, {3, 8    }, {7, 8    }, {7, 3    }, {3, 8    }, 
    {3, 4    }, {4, 9    }, {8, 9    }, {8, 4    }, {5, 10   }, {5, 6    }, {6, 11   }, {10, 11  }, 
    {10, 6   }, {6, 11   }, {6, 7    }, {7, 12   }, {11, 12  }, {11, 7   }, {7, 12   }, {7, 8    }, 
    {8, 13   }, {12, 13  }, {12, 8   }, {8, 13   }, {8, 9    }, {9, 14   }, {13, 14  }, {13, 9   }, 
    {10, 15  }, {10, 11  }, {11, 16  }, {15, 16  }, {15, 11  }, {11, 16  }, {11, 12  }, {12, 17  }, 
    {16, 17  }, {16, 12  }, {12, 17  }, {12, 13  }, {13, 18  }, {17, 18  }, {17, 13  }, {13, 18  }, 
    {13, 14  }, {14, 19  }, {18, 19  }, {18, 14  }, {15, 20  }, {15, 16  }, {16, 21  }, {20, 21  }, 
    {20, 16  }, {16, 21  }, {16, 17  }, {17, 22  }, {21, 22  }, {21, 17  }, {17, 22  }, {17, 18  }, 
    {18, 23  }, {22, 23  }, {22, 18  }, {18, 23  }, {18, 19  }, {19, 24  }, {23, 24  }, {23, 19  }
  };
  static vtkIdType strips[32][3] = {
    {0, 5, 1    }, {5, 1, 6    }, {1, 6, 2    }, {6, 2, 7    }, {2, 7, 3    }, {7, 3, 8    }, {3, 8, 4    }, {8, 4, 9    }, 
    {5, 10, 6   }, {10, 6, 11  }, {6, 11, 7   }, {11, 7, 12  }, {7, 12, 8   }, {12, 8, 13  }, {8, 13, 9   }, {13, 9, 14  }, 
    {10, 15, 11 }, {15, 11, 16 }, {11, 16, 12 }, {16, 12, 17 }, {12, 17, 13 }, {17, 13, 18 }, {13, 18, 14 }, {18, 14, 19 }, 
    {15, 20, 16 }, {20, 16, 21 }, {16, 21, 17 }, {21, 17, 22 }, {17, 22, 18 }, {22, 18, 23 }, {18, 23, 19 }, {23, 19, 24 }
  };
  vtkIdType numVerts = 25;
  vtkIdType numLines = 80;
  vtkIdType numStrips = 32;
  vtkIdType numCells = numVerts + numLines + numStrips;

  vtkIdType i;
  vtkPoints* points = vtkPoints::New();
  points->SetNumberOfPoints(25);
  for (i = 0; i < 25; i++)
  {
    points->InsertPoint(i, pts[i]);
  }
  vtkSmartPointer<vtkPolyData> poly = vtkSmartPointer<vtkPolyData>::New();
  poly->AllocateExact(numCells, numCells);
  poly->SetPoints(points);
  points->Delete();

  for (i = 0; i < numVerts; i++)
  {
    poly->InsertNextCell(VTK_VERTEX, 1, &i);
  }

  for (i = 0; i < numLines; i++)
  {
    poly->InsertNextCell(VTK_LINE, 2, lines[i]);
  }

  for (i = 0; i < numStrips; i++)
  {
    poly->InsertNextCell(VTK_TRIANGLE_STRIP, 3, strips[i]);
  }

  poly->BuildCells();

  vtkNew<vtkPolyDataMapper> mapper;
  mapper->SetInputData(poly);

  vtkNew<vtkActor> actor;
  actor->SetMapper(mapper);
  actor->GetProperty()->SetLineWidth(6);
  actor->GetProperty()->SetPointSize(25);
  actor->GetProperty()->SetRenderLinesAsTubes(1);
  actor->GetProperty()->SetRenderPointsAsSpheres(1);
  actor->GetProperty()->SetColor(colors->GetColor3d("Cornsilk").GetData());

  vtkNew<vtkRenderer> renderer;
  vtkNew<vtkRenderWindow> renderWindow;
  renderWindow->SetSize(500, 500);
  renderWindow->AddRenderer(renderer);
  vtkNew<vtkRenderWindowInteractor> renderWindowInteractor;
  renderWindowInteractor->SetRenderWindow(renderWindow);

  renderer->AddActor(actor);
  renderer->SetBackground(colors->GetColor3d("DarkGreen").GetData());
  renderWindow->Render();
  renderWindowInteractor->Start();
  getchar();
  return EXIT_SUCCESS;
}

运行效果图如下:

从图中可以看到绘制了点、线、由三角网渲染出来的面。

vtkUnstructuredGrid

vtkUnstructuredGrid可以表示所有单元类型(规则和不规则的),下面通过一个例子来看看它的使用。

#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);
VTK_MODULE_INIT(vtkRenderingFreeType);
VTK_MODULE_INIT(vtkRenderingVolumeOpenGL2);
#include <vtkActor.h>
#include <vtkCamera.h>
#include <vtkCellType.h>
#include <vtkDataSetMapper.h>
#include <vtkNamedColors.h>
#include <vtkNew.h>
#include <vtkPoints.h>
#include <vtkProperty.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkUnstructuredGrid.h>

int main(int, char*[])
{
  int i;
  static double x[27][3] = {
      {0, 0, 0}, {1, 0, 0}, {2, 0, 0}, {0, 1, 0}, {1, 1, 0}, {2, 1, 0},
      {0, 0, 1}, {1, 0, 1}, {2, 0, 1}, {0, 1, 1}, {1, 1, 1}, {2, 1, 1},
      {0, 1, 2}, {1, 1, 2}, {2, 1, 2}, {0, 1, 3}, {1, 1, 3}, {2, 1, 3},
      {0, 1, 4}, {1, 1, 4}, {2, 1, 4}, {0, 1, 5}, {1, 1, 5}, {2, 1, 5},
      {0, 1, 6}, {1, 1, 6}, {2, 1, 6}};
  static vtkIdType pts[12][8] = {
      {0, 1, 4, 3, 6, 7, 10, 9},      {1, 2, 5, 4, 7, 8, 11, 10},
      {6, 10, 9, 12, 0, 0, 0, 0},     {8, 11, 10, 14, 0, 0, 0, 0},
      {16, 17, 14, 13, 12, 15, 0, 0}, {18, 15, 19, 16, 20, 17, 0, 0},
      {22, 23, 20, 19, 0, 0, 0, 0},   {21, 22, 18, 0, 0, 0, 0, 0},
      {22, 19, 18, 0, 0, 0, 0, 0},    {23, 26, 0, 0, 0, 0, 0, 0},
      {21, 24, 0, 0, 0, 0, 0, 0},     {25, 0, 0, 0, 0, 0, 0, 0}};

  vtkNew<vtkNamedColors> colors;

  vtkNew<vtkRenderer> renderer;

  vtkNew<vtkRenderWindow> renWin;
  renWin->AddRenderer(renderer);
  vtkNew<vtkRenderWindowInteractor> iren;
  iren->SetRenderWindow(renWin);

  vtkNew<vtkPoints> points;
  for (i = 0; i < 27; i++) points->InsertPoint(i, x[i]);

  vtkNew<vtkUnstructuredGrid> ugrid;
  ugrid->Allocate(100);
  ugrid->InsertNextCell(VTK_HEXAHEDRON, 8, pts[0]);
  ugrid->InsertNextCell(VTK_HEXAHEDRON, 8, pts[1]);
  ugrid->InsertNextCell(VTK_TETRA, 4, pts[2]);
  ugrid->InsertNextCell(VTK_TETRA, 4, pts[3]);
  ugrid->InsertNextCell(VTK_POLYGON, 6, pts[4]);
  ugrid->InsertNextCell(VTK_TRIANGLE_STRIP, 6, pts[5]);
  ugrid->InsertNextCell(VTK_QUAD, 4, pts[6]);
  ugrid->InsertNextCell(VTK_TRIANGLE, 3, pts[7]);
  ugrid->InsertNextCell(VTK_TRIANGLE, 3, pts[8]);
  ugrid->InsertNextCell(VTK_LINE, 2, pts[9]);
  ugrid->InsertNextCell(VTK_LINE, 2, pts[10]);
  ugrid->InsertNextCell(VTK_VERTEX, 1, pts[11]);

  ugrid->SetPoints(points);

  vtkNew<vtkDataSetMapper> ugridMapper;
  ugridMapper->SetInputData(ugrid);

  vtkNew<vtkActor> ugridActor;
  ugridActor->SetMapper(ugridMapper);
  ugridActor->GetProperty()->SetColor(colors->GetColor3d("Peacock").GetData());
  ugridActor->GetProperty()->EdgeVisibilityOn();

  renderer->AddActor(ugridActor);
  renderer->SetBackground(colors->GetColor3d("Beige").GetData());

  renderer->ResetCamera();
  renderer->GetActiveCamera()->Elevation(60.0);
  renderer->GetActiveCamera()->Azimuth(30.0);
  renderer->GetActiveCamera()->Dolly(1.2);

  renWin->SetSize(640, 480);
  renWin->SetWindowName("UGrid)");

  // interact with data
  renWin->Render();

  iren->Start();

  getchar();

  return EXIT_SUCCESS;
}

运行效果图如下:

3 数据模型

VTK中数据模型由单元(cell)点(point)组成,点对应vtkPoints类,单元对应vtkCellArray类。

vtkPoints

vtkPoints用来表示和操作3D点。vtkPoints的数据模型是可通过(点或单元)id访问的vx-vy-vz三元组数组。

vtkPoints::vtkPoints(int dataType)
{
  this->Data = vtkFloatArray::New();
  this->Data->Register(this);
  this->Data->Delete();
  this->SetDataType(dataType);

  this->Data->SetNumberOfComponents(3);
  this->Data->SetName("Points");

  this->Bounds[0] = this->Bounds[2] = this->Bounds[4] = VTK_DOUBLE_MAX;
  this->Bounds[1] = this->Bounds[3] = this->Bounds[5] = -VTK_DOUBLE_MAX;
}

从vtkPoints类构造函数中的代码可以看出,它创建了一个vtkFloatArray数据数组,NumberOfComponents为3表示其为一个三元组。

  void InsertPoint(vtkIdType id, const float x[3]) VTK_EXPECTS(0 <= id)
  {
    this->Data->InsertTuple(id, x);
  }
  void InsertPoint(vtkIdType id, const double x[3]) VTK_EXPECTS(0 <= id)
  {
    this->Data->InsertTuple(id, x);
  }

  vtkIdType InsertNextPoint(const float x[3]) { return this->Data->InsertNextTuple(x); }
  vtkIdType InsertNextPoint(const double x[3]) { return this->Data->InsertNextTuple(x); }
  vtkIdType InsertNextPoint(double x, double y, double z);

InsertPoint和InsertNextPoint内部调用的是数据数组的InsertTuple和InsertNextTuple函数。

vtkCellArray

vtkCellArray用来表示单元内部连接性的对象。其将数据集的拓扑结构存为显示连接性(connectivity)表,表中列出组成每个单元的点ID。

在vtkCellArray内部,连接性表表示为两个数组:Offsets(偏移)Connectivity(连接性)。Offsets是一个[numCells+1]值的数组,指示Connectivity数组中每个单元点开始的索引,最后一个值始终是Connectivity数组的长度。Connectivity数组存每个单元的点ID列表。因此,对于由两个三角形、一个四边形和一条线组成的数据集,内部数组将如下所示:

Topology:
---------
Cell 0: Triangle | point ids: {0, 1, 2}
Cell 1: Triangle | point ids: {5, 7, 2}
Cell 2: Quad     | point ids: {3, 4, 6, 7}
Cell 4: Line     | point ids: {5, 8}
 
vtkCellArray (current):
-----------------------
Offsets:      {0, 3, 6, 10, 12}
Connectivity: {0, 1, 2, 5, 7, 2, 3, 4, 6, 7, 5, 8}

虽然此类提供了遍历方法(旧的InitTraversal()、GetNextCell()方法和较新的方法GetCellAtId()),但这些方法通常不是线程安全的。最好使用本地线程安全的vtkCellArrayInterator对象,可以通过以下方式获取该对象:

auto iter = vtk::TakeSmartPointer(cellArray->NewIterator());
for (iter->GoToFirstCell(); !iter->IsDoneWithTraversal(); iter->GoToNextCell())
{
  // do work with iter
}

(但请注意,根据内部存的类型和结构,由于额外的数据复制,单元数组迭代器可能比直接遍历单元数组慢得多。另外,如果vtkCellArray内部存储被修改,迭代器可能变得无效。)

vtkCellArray内部的数组可以存32或64位的值,尽管大多数API更喜欢使用vtkIdType来对应数组中的元素。允许将64位存储于32位vtkIdType一起使用,但太大而无法容纳32位有符号整数的值在通过API访问时将被截断。(特定的内部存储类型对性能有影响,具体取决于vtkIdType。如果内部存储等效于vtkIdType,则返回指向点"id"数组的指针的方法可以共享内部存储,否则必须执行复制内存)。

/*--------------------------------------------------------------------------*/
/* Choose an implementation for vtkIdType.  */
#define VTK_HAS_ID_TYPE
#ifdef VTK_USE_64BIT_IDS
#if VTK_SIZEOF_LONG_LONG == 8
typedef long long vtkIdType;
#define VTK_ID_TYPE_IMPL VTK_LONG_LONG
#define VTK_SIZEOF_ID_TYPE VTK_SIZEOF_LONG_LONG
#define VTK_ID_MIN VTK_LONG_LONG_MIN
#define VTK_ID_MAX VTK_LONG_LONG_MAX
#define VTK_ID_TYPE_PRId "lld"
#elif VTK_SIZEOF_LONG == 8
typedef long vtkIdType;
#define VTK_ID_TYPE_IMPL VTK_LONG
#define VTK_SIZEOF_ID_TYPE VTK_SIZEOF_LONG
#define VTK_ID_MIN VTK_LONG_MIN
#define VTK_ID_MAX VTK_LONG_MAX
#define VTK_ID_TYPE_PRId "ld"
#else
#error "VTK_USE_64BIT_IDS is ON but no 64-bit integer type is available."
#endif
#else
typedef int vtkIdType;
#define VTK_ID_TYPE_IMPL VTK_INT
#define VTK_SIZEOF_ID_TYPE VTK_SIZEOF_INT
#define VTK_ID_MIN VTK_INT_MIN
#define VTK_ID_MAX VTK_INT_MAX
#define VTK_ID_TYPE_PRId "d"
#endif

InsertNextCell

InsertNextCell是用来插入单元的函数,各种数据对象中都有实现该接口,用以构造本对象中的各种类型的单元。下面我们通过vtkPolyData中InsertNextCell函数的内部实现代码看下它是如何构造单元的。

vtkIdType vtkPolyData::InsertNextCell(int type, vtkIdList* pts)
{
  return this->InsertNextCell(type, static_cast<int>(pts->GetNumberOfIds()), pts->GetPointer(0));
}

vtkIdType vtkPolyData::InsertNextCell(int type, int npts, const vtkIdType ptsIn[])
{
...
  // Insert next cell into the lookup map:
  TaggedCellId& tag = this->Cells->InsertNextCell(VTKCellType(type));
  vtkCellArray* cells = this->GetCellArrayInternal(tag);

  // Validate and update the internal cell id:
  const vtkIdType internalCellId = cells->InsertNextCell(npts, pts);
  if (internalCellId < 0)
  {
    vtkErrorMacro("Internal error: Invalid cell id (" << internalCellId << ").");
    return -1;
  }
...

  // Return the dataset cell id:
  return this->Cells->GetNumberOfCells() - 1;
}

inline vtkCellArray* vtkPolyData::GetCellArrayInternal(vtkPolyData::TaggedCellId tag)
{
  switch (tag.GetTarget())
  {
    case vtkPolyData_detail::Target::Verts:
      return this->Verts;
    case vtkPolyData_detail::Target::Lines:
      return this->Lines;
    case vtkPolyData_detail::Target::Polys:
      return this->Polys;
    case vtkPolyData_detail::Target::Strips:
      return this->Strips;
  }
  return nullptr; // unreachable
}

vtkPolyData::InsertNextCell函数中调用:

vtkCellArray* cells = this->GetCellArrayInternal(tag);

获取当前要插入单元的类型对应的vtkCellArray。然后调用:

const vtkIdType internalCellId = cells->InsertNextCell(npts, pts);

插入单元到vtkCellArray。

inline vtkIdType vtkCellArray::InsertNextCell(vtkIdType npts, const vtkIdType* pts)
  VTK_SIZEHINT(pts, npts)
{
  return this->Visit(vtkCellArray_detail::InsertNextCellImpl{}, npts, pts);
}

template <typename Functor, typename... Args,
	typename = typename std::enable_if<!ReturnsVoid<Functor, Args...>::value>::type>
GetReturnType<Functor, Args...> Visit(Functor&& functor, Args&&... args)
{
	if (this->Storage.Is64Bit())
	{
	  // If you get an error on the next line, a call to Visit(functor, Args...)
	  // is being called with arguments that do not match the functor's call
	  // signature. See the Visit documentation for details.
	  return functor(this->Storage.GetArrays64(), std::forward<Args>(args)...);
	}
	else
	{
	  // If you get an error on the next line, a call to Visit(functor, Args...)
	  // is being called with arguments that do not match the functor's call
	  // signature. See the Visit documentation for details.
	  return functor(this->Storage.GetArrays32(), std::forward<Args>(args)...);
	}
}

Visit(Functor&& functor, Args&&... args)函数的第一个参数是一个InsertNextCellImpl对象(&&可以理解为std::move操作),第二个参数是一个可变参数列表。

functor(this->Storage.GetArrays64(), std::forward<Args>(args)...);调用的是InsertNextCellImpl中()操作符函数:

struct InsertNextCellImpl
{
  // Insert full cell
  template <typename CellStateT>
  vtkIdType operator()(CellStateT& state, const vtkIdType npts, const vtkIdType pts[])
  {
    using ValueType = typename CellStateT::ValueType;
    auto* conn = state.GetConnectivity();
    auto* offsets = state.GetOffsets();

    const vtkIdType cellId = offsets->GetNumberOfValues() - 1;

    offsets->InsertNextValue(static_cast<ValueType>(conn->GetNumberOfValues() + npts));

    for (vtkIdType i = 0; i < npts; ++i)
    {
      conn->InsertNextValue(static_cast<ValueType>(pts[i]));
    }

    return cellId;
  }
...
};

operator()函数的第一个参数通过this->Storage.GetArrays64()获得。

  struct Storage
  {
    // Union type that switches 32 and 64 bit array storage
    union ArraySwitch {
      ArraySwitch() = default;  // handled by Storage
      ~ArraySwitch() = default; // handle by Storage
      VisitState<ArrayType32>* Int32;
      VisitState<ArrayType64>* Int64;
    };

    Storage()
    {
#ifdef VTK_USE_MEMKIND
      this->Arrays =
        static_cast<ArraySwitch*>(vtkObjectBase::GetCurrentMallocFunction()(sizeof(ArraySwitch)));
#else
      this->Arrays = new ArraySwitch;
#endif

      // Default to the compile-time setting:
#ifdef VTK_USE_64BIT_IDS

      this->Arrays->Int64 = new VisitState<ArrayType64>;
      this->StorageIs64Bit = true;

#else // VTK_USE_64BIT_IDS

      this->Arrays->Int32 = new VisitState<ArrayType32>;
      this->StorageIs64Bit = false;

#endif // VTK_USE_64BIT_IDS
#ifdef VTK_USE_MEMKIND
      if (vtkObjectBase::GetUsingMemkind())
      {
        this->IsInMemkind = true;
      }
#else
      (void)this->IsInMemkind; // comp warning workaround
#endif
    }

...
    // Get the VisitState for 32-bit arrays
    VisitState<ArrayType32>& GetArrays32()
    {
      assert(!this->StorageIs64Bit);
      return *this->Arrays->Int32;
    }

    const VisitState<ArrayType32>& GetArrays32() const
    {
      assert(!this->StorageIs64Bit);
      return *this->Arrays->Int32;
    }

    // Get the VisitState for 64-bit arrays
    VisitState<ArrayType64>& GetArrays64()
    {
      assert(this->StorageIs64Bit);
      return *this->Arrays->Int64;
    }

    const VisitState<ArrayType64>& GetArrays64() const
    {
      assert(this->StorageIs64Bit);
      return *this->Arrays->Int64;
    }

  private:
    // Access restricted to ensure proper union construction/destruction thru
    // API.
    ArraySwitch* Arrays;
    bool StorageIs64Bit;
    bool IsInMemkind = false;
  };

Storage内通过联合体(union)管理一个32位或64位的VisitState。

  template <typename ArrayT>
  struct VisitState
  {
    using ArrayType = ArrayT;
    using ValueType = typename ArrayType::ValueType;
    using CellRangeType = decltype(vtk::DataArrayValueRange<1>(std::declval<ArrayType>()));

    // We can't just use is_same here, since binary compatible representations
    // (e.g. int and long) are distinct types. Instead, ensure that ValueType
    // is a signed integer the same size as vtkIdType.
    // If this value is true, ValueType pointers may be safely converted to
    // vtkIdType pointers via reinterpret cast.
    static constexpr bool ValueTypeIsSameAsIdType = std::is_integral<ValueType>::value &&
      std::is_signed<ValueType>::value && (sizeof(ValueType) == sizeof(vtkIdType));

    ArrayType* GetOffsets() { return this->Offsets; }
    const ArrayType* GetOffsets() const { return this->Offsets; }

    ArrayType* GetConnectivity() { return this->Connectivity; }
    const ArrayType* GetConnectivity() const { return this->Connectivity; }

...

    friend class vtkCellArray;

  protected:
    VisitState()
    {
      this->Connectivity = vtkSmartPointer<ArrayType>::New();
      this->Offsets = vtkSmartPointer<ArrayType>::New();
      this->Offsets->InsertNextValue(0);
      if (vtkObjectBase::GetUsingMemkind())
      {
        this->IsInMemkind = true;
      }
    }

    vtkSmartPointer<ArrayType> Connectivity;
    vtkSmartPointer<ArrayType> Offsets;

...
  };

回到InsertNextCellImpl的operator()函数。我们可以看到,函数中根据InsertNextCell传入的单元类型对应的点数(npts)在进行遍历,依次插入单元对应的点的索引集合;将conn->GetNumberOfValues() + npts(当前已经插入的索引值总数+要插入单元的元组大小)的值作为偏移值插入Offsets数组中。

4 数据属性

vtkPointData 和 vtkCellData

上一篇文章讲到数据属性通常与点和单元关联关联的(也可以通过GetFieldData()关联到整个数据模型),VTK中使用vtkPointData和vtkCellData分别表示数据点和单元的属性,它们都是vtkFieldData的子类。下面我们通过一个例子来看下它们的使用。

#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);
VTK_MODULE_INIT(vtkRenderingFreeType);
VTK_MODULE_INIT(vtkRenderingVolumeOpenGL2);
#include <vtkActor.h>
#include <vtkArrowSource.h>
#include <vtkCamera.h>
#include <vtkGlyph3D.h>
#include <vtkNamedColors.h>
#include <vtkNew.h>
#include <vtkPolyData.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkCubeSource.h>
#include <vtkCellArray.h>
#include <vtkFloatArray.h>
#include <vtkPointData.h>
#include <vtkCellData.h>
#include <vtkPoints.h>

int main(int, char*[])
{
  vtkNew<vtkNamedColors> colors;

  std::array<std::array<double, 3>, 8> pts = {{{{0, 0, 0}},
                                               {{1, 0, 0}},
                                               {{1, 1, 0}},
                                               {{0, 1, 0}},
                                               {{0, 0, 1}},
                                               {{1, 0, 1}},
                                               {{1, 1, 1}},
                                               {{0, 1, 1}}}};
  // The ordering of the corner points on each face.
  std::array<std::array<vtkIdType, 4>, 6> ordering = {{{{0, 3, 2, 1}},
                                                       {{4, 5, 6, 7}},
                                                       {{0, 1, 5, 4}},
                                                       {{1, 2, 6, 5}},
                                                       {{2, 3, 7, 6}},
                                                       {{3, 0, 4, 7}}}};

  std::array<std::array<double, 3>, 8> vertex_normals;     
  for(auto i = 0; i < 8; ++i)
  {
    for(auto j = 0; j < 3; ++j)
    {
      vertex_normals[i][j] = pts[i][j] - 0.5;
    }
  }       

  // We'll create the building blocks of polydata including data attributes.
  vtkNew<vtkPolyData> cube;
  vtkNew<vtkPoints> points;
  vtkNew<vtkCellArray> polys;
  vtkNew<vtkFloatArray> vertex_normals_array;
  vtkNew<vtkFloatArray> scalar_array;

  // Load the point, cell, and data attributes.
  for (auto i = 0ul; i < pts.size(); ++i)
  {
    points->InsertPoint(i, pts[i].data());
  }

  for (auto&& i : ordering)
  {
    polys->InsertNextCell(vtkIdType(i.size()), i.data());
  }

  vertex_normals_array->SetNumberOfComponents(3);
  for(auto i = 0; i < vertex_normals.size(); ++i)
  {
    vertex_normals_array->InsertNextTuple3(vertex_normals[i][0], 
                                           vertex_normals[i][1], 
                                           vertex_normals[i][2]);
  }

  scalar_array->SetNumberOfComponents(1);
  for(auto i = 0; i < ordering.size(); ++i)
  {
    scalar_array->InsertNextTuple1(i);
  }
  // We now assign the pieces to the vtkPolyData.
  cube->SetPoints(points);
  cube->SetPolys(polys);
  cube->GetPointData()->SetNormals(vertex_normals_array);
  cube->GetCellData()->SetScalars(scalar_array);

  vtkNew<vtkPolyData> input;
  input->ShallowCopy(cube);
  vtkNew<vtkArrowSource> arrowSource;
  vtkNew<vtkGlyph3D> glyph3D;
  glyph3D->SetSourceConnection(arrowSource->GetOutputPort());
  glyph3D->SetInputData(input);
  glyph3D->ScalingOn();
  glyph3D->SetVectorModeToUseNormal();
  glyph3D->SetScaleFactor(0.25);
  glyph3D->Update();

  // Visualize
  vtkNew<vtkPolyDataMapper> mapper;
  mapper->SetInputConnection(glyph3D->GetOutputPort());
  vtkNew<vtkPolyDataMapper> cube_mapper;
  cube_mapper->SetInputData(cube);
  cube_mapper->SetScalarRange(cube->GetScalarRange());

  vtkNew<vtkActor> actor;
  actor->SetMapper(mapper);
  actor->GetProperty()->SetColor(colors->GetColor3d("Gold").GetData());
  vtkNew<vtkActor> cube_actor;
  cube_actor->SetMapper(cube_mapper);

  vtkNew<vtkRenderer> renderer;
  vtkNew<vtkRenderWindow> renderWindow;
  renderWindow->AddRenderer(renderer);
  renderWindow->SetWindowName("OrientedGlyphs");

  vtkNew<vtkRenderWindowInteractor> renderWindowInteractor;
  renderWindowInteractor->SetRenderWindow(renderWindow);

  renderer->AddActor(actor);
  renderer->AddActor(cube_actor);
  renderer->SetBackground(colors->GetColor3d("DarkGreen").GetData());

  renderWindow->Render();
  renderWindowInteractor->Start();
  getchar();
  return EXIT_SUCCESS;
}

运行效果如下:

上面的代码中:

cube->GetPointData()->SetNormals(vertex_normals_array);将法向量数据数组设置给点,所以从图中可以看到箭头是在点的位置进行显示。

cube->GetCellData()->SetScalars(scalar_array);将标量数据数组设置给单元,可以看到每个单元(面)根据标量显示不同的颜色。

数据属性不仅仅只包含标量、向量,其所能支持的类型包括以下这些:

  // Always keep NUM_ATTRIBUTES as the last entry
  enum AttributeTypes
  {
    SCALARS = 0,
    VECTORS = 1,
    NORMALS = 2,
    TCOORDS = 3,
    TENSORS = 4,
    GLOBALIDS = 5,
    PEDIGREEIDS = 6,
    EDGEFLAG = 7,
    TANGENTS = 8,
    RATIONALWEIGHTS = 9,
    HIGHERORDERDEGREES = 10,
    NUM_ATTRIBUTES
  };

Ghost属性

考虑如下图所示的提取外表面(face)的操作。外表面操作用于识别没有本地邻居的所有面。当我们把这些面片放置一起时,我们发现一些面被错误地识别为外部面。当两个相邻的单元被放置在单独的处理中时,这些面的错误识别就会发生。

上图提取外部面结果错误是因为处理中丢失了一些重要的全局信息,这些单独的处理过程不需要所有的数据,但需要一些不属于它们自己数据,即它们需要知道相邻的其他分区中的单元。

这个问题可以通过引入Ghost单元来解决这个局部/全局问题。Ghost单元是属于数据的一个分区并重复在其他分区上的单元。Ghost单元的引入是通过领域信息进行的,并按层次进行组织。对于给定的分区,与分区中的单元相邻但不属于分区本身的任何单元都是Ghost单元1。与不属于层次1或原始分区层次1的Ghost单元相邻的任何单元都处于层次2。递归定义更深的层次。

将Ghost引用到提取外表面的示例中,效果如下图,图中某些面仍然被分类为外表面,但是,所有这些面都附着在Ghost单元上。这些Ghost面很容易被剔除,最终结果就是合适的外表面。

下面通过一个例子来看看VTK中Ghost单元的使用:

#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);
VTK_MODULE_INIT(vtkRenderingFreeType);
VTK_MODULE_INIT(vtkRenderingVolumeOpenGL2);
#include "vtkActor.h"
#include "vtkCellData.h"
#include "vtkCellType.h"
#include "vtkDataSetSurfaceFilter.h"
#include "vtkGeometryFilter.h"
#include "vtkNew.h"
#include "vtkPoints.h"
#include "vtkPolyDataMapper.h"
#include "vtkRegressionTestImage.h"
#include "vtkRenderWindow.h"
#include "vtkRenderWindowInteractor.h"
#include "vtkRenderer.h"
#include "vtkSmartPointer.h"
#include "vtkUnsignedCharArray.h"
#include "vtkUnstructuredGrid.h"
#include "vtkFloatArray.h"
#include "vtkColorTransferFunction.h"
#include "vtkTextActor.h"
#include "vtkTextProperty.h"
#include "vtkCamera.h"
#include "vtkCallbackCommand.h"
#include "vtkRendererCollection.h"
#include "vtkActor2DCollection.h"

void CallbackFunction(vtkObject* caller, long unsigned int eventId,
                      void* clientData, void* callData)
{
  vtkRenderWindowInteractor* iren = static_cast<vtkRenderWindowInteractor*>(caller);
  vtkRenderer* renderer = iren->GetRenderWindow()->GetRenderers()->GetFirstRenderer();
  if(clientData == nullptr)
    return;
  auto points = static_cast<vtkPoints*>(clientData);
  auto ac = renderer->GetActors2D();
  vtkActor2D* anActor;
  vtkCollectionSimpleIterator ait;
  for (ac->InitTraversal(ait); (anActor = ac->GetNextActor2D(ait));)
  {
    auto ta = vtkTextActor::SafeDownCast(anActor);
    if(ta == nullptr) continue;
    std::string text = ta->GetInput();
    int idx = std::stoi(text);
    auto pt = points->GetPoint(idx);
    renderer->WorldToDisplay(pt[0], pt[1], pt[2]);
    ta->SetDisplayPosition(pt[0], pt[1]);
  }
}

int main(int argc, char* argv[])
{
  vtkNew<vtkPoints> points;
  points->InsertPoint(0, 0, 0, 0);
  points->InsertPoint(1, 1, 0, 0);
  points->InsertPoint(2, 0.5, 1, 0);
  points->InsertPoint(3, 0.5, 0.5, 1);
  points->InsertPoint(4, 0.5, -1, 0);
  points->InsertPoint(5, 0.5, -0.5, 1);

  vtkIdType v[3][4] = { { 0, 1, 2, 3 }, { 0, 4, 1, 5 }, { 5, 3, 1, 0 } };
  //vtkIdType v[3][4] = { { 0, 1, 2, 3 }, { 5, 3, 1, 0 }, { 0, 4, 1, 5 } };

  vtkSmartPointer<vtkUnstructuredGrid> grid = vtkSmartPointer<vtkUnstructuredGrid>::New();
  grid->InsertNextCell(VTK_TETRA, 4, v[0]);
  grid->InsertNextCell(VTK_TETRA, 4, v[1]);
  grid->InsertNextCell(VTK_TETRA, 4, v[2]);
  grid->SetPoints(points);

  vtkNew<vtkFloatArray> cell_scalar_array;
  cell_scalar_array->SetNumberOfComponents(1);
  cell_scalar_array->InsertNextTuple1(0);
  cell_scalar_array->InsertNextTuple1(3);
  cell_scalar_array->InsertNextTuple1(7);

  grid->GetCellData()->SetScalars(cell_scalar_array);
  
  // vtkNew<vtkUnsignedCharArray> ghosts;
  // ghosts->InsertNextValue(0);
  // ghosts->InsertNextValue(1);
  // ghosts->InsertNextValue(2);
  // ghosts->SetName(vtkDataSetAttributes::GhostArrayName()); 
  // grid->GetCellData()->AddArray(ghosts);

  // this filter removes the ghost cells
  vtkNew<vtkGeometryFilter> surfaces;
  surfaces->SetInputData(grid);
  surfaces->Update(); 

	vtkNew<vtkColorTransferFunction> clrTransferFunc;
	clrTransferFunc->SetColorSpaceToRGB();
  clrTransferFunc->AddRGBPoint(0, 1, 0, 0);
  clrTransferFunc->AddRGBPoint(3, 0, 1, 0);
  clrTransferFunc->AddRGBPoint(7, 0, 1, 1);

  vtkNew<vtkPolyDataMapper> mapper;
  mapper->SetInputConnection(surfaces->GetOutputPort());
  mapper->SetScalarRange(grid->GetScalarRange());
  mapper->SetLookupTable(clrTransferFunc);

  vtkNew<vtkActor> actor;
  actor->SetMapper(mapper);

  vtkNew<vtkCamera> camera;
  camera->SetPosition(0, 0, 5);
  camera->SetFocalPoint(0, 0, 0);

  vtkNew<vtkRenderer> renderer;
  renderer->SetActiveCamera(camera);
  renderer->ResetCamera();
  renderer->AddActor(actor);

  vtkNew<vtkRenderWindow> renwin;
  renwin->AddRenderer(renderer);
  renwin->SetSize(500, 500);

  vtkNew<vtkRenderWindowInteractor> iren;
  iren->SetRenderWindow(renwin);
  iren->Initialize();

  vtkNew<vtkCallbackCommand> callback;
  callback->SetCallback(CallbackFunction);
  callback->SetClientData(points);
  iren->AddObserver(vtkCommand::LeftButtonPressEvent, callback);

  for(int i = 0; i < points->GetNumberOfPoints(); ++i)
  {
    auto pt = points->GetPoint(i);
    std::string text = std::to_string(i);
    vtkNew<vtkTextActor> ta;
    ta->SetInput(text.c_str());
    ta->GetTextProperty()->SetColor(0.5, 1.0, 0.0);
    renderer->WorldToDisplay(pt[0], pt[1], pt[2]);
    ta->SetDisplayPosition(pt[0], pt[1]);
    ta->GetTextProperty()->SetFontSize(32);
    renderer->AddActor(ta.Get());
  }

  renwin->Render();
  iren->Start();

  getchar();
  return EXIT_SUCCESS;
}

没应用ghost单元代码的情况下,效果如下图:

应用ghost单元代码的情况下,效果如下图:

可以看到vtkGeometryFilter移除了第二个Ghost单元。将第二个单元和第三个单元顺序换一下的效果图如下:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/576648.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ssm082基于java斗车交易系统设计与实现+vue

斗车交易系统 摘 要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识&#xff0c;科学化的管理&…

12.Blender 界面介绍(上)及物体基础编辑操作

设置语言 首先在菜单栏打开编辑-Preferences-界面-翻译&#xff0c;可以修改语言 这里使用的是Steam上下载的4.1版本 工具栏 左边的工具栏&#xff0c;按T就会出现&#xff0c;再按T就会隐藏 右边的工具栏是按N&#xff0c;按N显示&#xff0c;再按N隐藏 旋转画面 长按鼠…

C语言面试题之相交链表

相交链表 实例要求 1、给定两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。2、如果两个链表不存在相交节点&#xff0c;返回 null 。示例&#xff1a; 实例分析 可以使用两种方法&#xff1a;哈希表方法和双指针方法。哈希表方法…

Golang基础7-并发编程

并发编程 https://www.cnblogs.com/Survivalist/p/11527949.html 进程和线程、协程的区别_线程协程进程的区别-CSDN博客 Golang中的并发编程是一个重点&#xff0c;我们要了解Golang中的并发Goroutine因此需要先理解进程、线程、之后再理解协程。 进程&#xff1a;操作系统进…

某翻译平台翻译接口逆向之webpack学习

逆向网址 aHR0cHM6Ly9mYW55aS55b3VkYW8uY29tLw 逆向链接 aHR0cHM6Ly9mYW55aS55b3VkYW8uY29tLyMv 逆向接口 aHR0cHM6Ly9kaWN0LnlvdWRhby5jb20vd2VidHJhbnNsYXRl 逆向过程 请求方式 POST 逆向参数 sign c168e4cb76169e90f82d28118dbd24d2 接口请求结果解密 过程分析 根据XHR…

免费获取!遗传算法+多目标规划算法+自适应神经模糊系统程序代码!

前言 遗传算法&#xff08;Genetic Algorithm&#xff0c;GA&#xff09;最早是由美国的 John holland于20世纪70年代提出&#xff0c;该算法是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型&#xff0c;通过数学的方式&#xff0c;将问题的求解过程转…

全国省级金融发展水平数据集(2000-2022年)

01、数据简介 金融发展水平是一个国家或地区经济实力和国际竞争力的重要体现。它反映了金融体系的成熟程度和发展水平&#xff0c;是衡量一个国家或地区经济发展质量的重要指标。金融发展水平的提高&#xff0c;意味着金融体系能够更好地服务实体经济&#xff0c;推动经济增长…

3.7设计模式——Observer 观察者模式(行为型)

意图 定义对象间的一种一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于他的对象都得到通知并被自动更新。 结构 Subject&#xff08;目标&#xff09;知道它的观察者&#xff0c;可以有任意多个观察者观察同一个目标&#xff0c;提供注册和删…

模块三:二分——153.寻找旋转排序数组中的最小值

文章目录 题目描述算法原理解法一&#xff1a;暴力查找解法二&#xff1a;二分查找疑问 代码实现解法一&#xff1a;暴力查找解法二&#xff1a;CJava 题目描述 题目链接&#xff1a;153.寻找旋转排序数组中的最小值 根据题目的要求时间复杂度为O(log N)可知需要使用二分查找…

电子负载仪的远端控制

前言 最近研究了电子负载仪的远端控制&#xff08;区别于前面板控制&#xff09;&#xff0c;主要是用于程序控制&#xff0c;避免繁琐复杂的人工控制&#xff0c;举了南京嘉拓和艾维泰科的例子。 有纰漏请指出&#xff0c;转载请说明。 学习交流请发邮件 1280253714qq.com …

基于JavaWEB的学生考勤管理系统(含论文)

本系统是用Java语言写的&#xff0c;基于JavaWEB的学生考勤管理系统 主要有三大模块&#xff0c;学生&#xff0c;教师和管理员模块&#xff0c;功能如下&#xff1a; 学生模块 教师模块&#xff1a; 管理员模块

深入了解Semaphore、CountDownLatch等实用工具的用法

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一个人虽可以走的更快&#xff0c;但一群人可以走的更远。 我是一名后…

4月26(信息差)

&#x1f30d; 1170万台 华为跃升重回首位&#xff01;苹果跌至第五位 &#x1f384;工业软件大事件 —— OGG 1.0 发布&#xff0c;华为贡献全部源代码 ✨ 苹果发布 OpenELM&#xff1a;专为在设备端运行而设计的小型开源 AI 模型 1.FisheyeDetNet&#xff1a;首个基于鱼眼相…

LED驱动模块RSC6218A 5W-18W迷你高效驱动电源应用-REASUNOS(瑞森半导体)

一、LED驱动模块RSC6218A REASUNOS(瑞森半导体)通过持续投入研发&#xff0c;提升LLC应用技术&#xff0c;集成控制芯片与功率转换&#xff0c;成功推出新一代产品RSC6218A WSOP-16&#xff0c;延续瑞森LLC拓扑方案&#xff0c;时机趋势完全迎合我国双碳政策&#xff0c;电气特…

【Web】DASCTF X GFCTF 2024|四月开启第一局 题解(全)

目录 EasySignin cool_index SuiteCRM web1234 法一、条件竞争(没成功) 法二、session反序列化 EasySignin 先随便注册个账号登录&#xff0c;然后拿bp抓包改密码(username改成admin) 然后admin / 1234567登录 康好康的图片功能可以打SSRF&#xff0c;不能直接读本地文…

Docker网络模式与cgroup资源控制

前言 在 Docker 中&#xff0c;网络模式和 cgroup 资源控制作为关键功能&#xff0c;对于容器的性能优化和资源管理起着至关重要的作用。本文将介绍 Docker 的网络模式和cgroup资源控制&#xff0c;探讨不同网络模式的特点以及如何利用 cgroup 资源控制机制来有效管理容器的资…

不使用加减运算符实现整数加和减

文章目录 进位 进位 加粗 最近想出了不适用运算符实现加与减 首先按位与找出的是需不需要进位 按位与是两边同时为1,则为1,那么如果两边同时为1的话,是不是就该进位?所以我们用按位与来判断是否需要进位 然后再按位异或找出不同的位数 按位异或是两边不相等,也就是1 和 0的时…

SpringBoot源码阅读2-自动配置

SpringBoot源码阅读2-自动配置 在传统的Spring应用中&#xff0c;开发者需要手动配置一系列Web应用的核心组件&#xff0c;例如DispatcherServlet用于处理请求分发、ViewResolver用于视图解析、CharacterEncodingFilter用于字符编码过滤等。 然而在SpringBoot中只要引入了spr…

力扣HOT100 - 994. 腐烂的橘子

解题思路&#xff1a; 因为要记录轮数&#xff08;分钟数&#xff09;&#xff0c;所以不能一口气遍历到底&#xff0c;所以不能用深搜&#xff08;bfs&#xff09;&#xff0c;而要用广搜&#xff08;bfs&#xff0c;层序遍历&#xff09;。 先记录下新鲜橘子数&#xff0c;…

github+PicGo+obsidian来作为你的免费高效可靠图床吧

前提 一直以来 博客的图床问题都是个大问题 ,如何找到一个 可靠并且 方便的搭建方式 非常重要 今天介绍一种 githubpicGoobsidian的搭建方式 准备github库 生成个人github token 找到个人 设置 生成一个新token 或者已经有的直接用 新生成的token 需要记录下来 这可能是你最后…
最新文章