2D STL 三角面片提取轮廓

1. 提取轮廓算法

1.1 读取2D STL 三角面片边信息

在由三角面片组成的多边形中,轮廓边之会出现一次,而重合的边会出现两次,根据这个,就可以读取边的信息,如图

而三角面片STL格式如下, 其中facet normal是平面的法向量,在2D平面这个法向量可以忽略

vertex表示三角形的三个顶点

facet normal 0 0 1
 outer loop
  vertex 0 0 0
  vertex 1 0 0
  vertex 0 1 0
 endloop
endfacet

那么基本算法就可以这样设计
1. 读取2D STL 文件的三个顶点,将三个顶点组成边,记录边出现的次数(使用unordered_map)

2. 遍历所有组成的边,出现次数为2的说明是重复边,舍去

算法是简单的,下面看实现细节

首先是2D点数据结构实现,细节上需要注意赋值运算符=和相等运算符==。同时由于后面的设计需要对点使用unordered_map,因此需要设计operator()

struct Point2D
{
	double x, y;
	Point2D() : x(0.0), y(0.0){}
    Point2D(double x_, double y_) : x(x_), y(y_){}
	Point2D(const Point2D& p) : x(p.x), y(p.y){}

	Point2D& operator=(const Point2D& p)
	{
		x = p.x;
		y = p.y;
		return *this;
    }
};

bool operator==(const Point2D& p1, const Point2D& p2)
{
    return (p1.x == p2.x && p1.y == p2.y);
}

// 用于哈希 Point2D
namespace std {
    template<>
    struct hash<Point2D> {
        std::size_t operator()(const Point2D& p) const {
            return std::hash<double>()(p.x) ^ std::hash<double>()(p.y);
        }
    };
}

接着是2D线段数据结构的实现,由于线段需要使用unordered_map,因此需要设计operator()和==运算符。需要注意,线段是没有方向的,因此==运算符需要考虑两种情况

struct Segment2D
{
    Point2D P1, P2;
    Segment2D() : P1(), P2(){};
    Segment2D(const Point2D& _P1, const Point2D& _P2){
        P1 = _P1; P2 = _P2;
    }

    Segment2D(const Segment2D& segment){
        P1 = segment.P1;
        P2 = segment.P2;
    }
    Segment2D& operator=(const Segment2D& segment){
        P1 = segment.P1;
        P2 = segment.P2;
        return *this;
    }
};


bool operator==(const Segment2D& segment1, const Segment2D& segment2)
{
    return (segment1.P1 == segment2.P1 && segment1.P2 == segment2.P2) ||
           (segment1.P1 == segment2.P2 && segment1.P2 == segment2.P1);
}

// 特化 std::hash 模板
namespace std {
template<>
struct hash<Segment2D> {
    size_t operator()(const Segment2D& s) const {
        // 假设我们用两个点的坐标组合来生成哈希值
        auto h1 = std::hash<double>{}(s.P1.x);
        auto h2 = std::hash<double>{}(s.P1.y);
        auto h3 = std::hash<double>{}(s.P2.x);
        auto h4 = std::hash<double>{}(s.P2.y);

        // 使用 XOR 组合哈希值
        return (h1 ^ (h2 << 1)) ^ (h3 ^ (h4 << 1));
    }
};
}

然后是2D 线段提取的完整实现,这里的话我是以ASCII码形式读取的STL文件,感兴趣的小伙伴也可以改成读取二进制形式的STL

void readSTL2D(const std::string& filepath, std::vector<Point2D>& points, std::vector<Segment2D>& segments2D)
{
    printf("Reading file \" %s \"...\n", filepath.c_str());
    std::ifstream input(filepath);
    if (!input.is_open())
    {
        printf("\nError: %s, %d.\n", __FILE__, __LINE__);
        printf("Can not open file:\"%s\" .\n", filepath.c_str());
        exit(EXIT_FAILURE);
    }
    std::string s;
    while (input >> s)
    {
        if (s == "vertex")
        {
            double x, y, z;
            input >> x >> y >> z;
            points.push_back(Point2D(x, y));
        }
    }

    if (points.size() % 3 != 0){
        printf("Error %s, %d\ninput 2D stl is not illegal.\n", __FILE__, __LINE__);
        exit(EXIT_FAILURE);
    }

    std::unordered_map<Segment2D, int> umap_segments2D;
    for (int i=0; i<points.size(); i+=3){
        const Point2D& p1 = points[i + 0];
        const Point2D& p2 = points[i + 1];
        const Point2D& p3 = points[i + 2];

        Segment2D seg1(p1, p2);
        Segment2D seg2(p2, p3);
        Segment2D seg3(p3, p1);

        umap_segments2D[seg1]++;
        umap_segments2D[seg2]++;
        umap_segments2D[seg3]++;
    }

    for (auto it = umap_segments2D.begin(); it != umap_segments2D.end(); ++it)
    {
        if (it->second != 1) continue;
        segments2D.push_back(it->first); 
    }
    printf("segments2D.size() = %zd\n", segments2D.size());
}

1.2 提取轮廓

在2.1中提取了线段之后,根据线段提取出每一段轮廓,包括外部和内部。

思路如下:

1. 选取一个起始线段,根据线段的两个端点,寻找下一个线段,将找过的节点进行记录。

当线段的最后一个点和起点重合时,那么这个回路轮廓就算找到了。

2. 找到这个轮廓之后,使用并查集算法,迭代访问当前节点的下一个节点,将每个节点记录到数组中,这样就完成了轮廓的提取

3. 再选取其他的线段作为其实线段,判断一下该线段的两个端点是否被记录,没有被记录那么就是别的轮廓,再按照1、2中的操作就行

1、2 步骤中的算法如下:

polygons 用于保存轮廓,start_seg_id即步骤1中提到的选取起始线段

void addOutLineWithStartSegId(std::vector<std::vector<Point2D>>& polygons, 
    int start_seg_id, const std::vector<Segment2D>& segments, std::unordered_map<Point2D, Point2D>& umap)
{
    Point2D start_p = segments[start_seg_id].P1;
    Point2D next_p = segments[start_seg_id].P2;
    if (umap.find(start_p) != umap.end() || umap.find(next_p) != umap.end()) return;
    umap[start_p] = next_p;
    int i = 1;


    for (int j = 0; j<segments.size(); ++j)
    {
        if (j == start_seg_id) continue;
        Point2D P1 = segments[j].P1;
        Point2D P2 = segments[j].P2;

        if (P1 == next_p && umap.find(P2) == umap.end())
        {
            umap[next_p] = P2;
            next_p = P2;
            j = -1;
        }
        else if (P2 == next_p && umap.find(P1) == umap.end())
        {
            umap[next_p] = P1;
            next_p = P1;
            j = -1;
        }
        else if (P1 == next_p && P2 == start_p)
        {
            umap[P1] = P2; 
            break;
        }
        else if (P2 == next_p && P1 == start_p)
        {
            umap[P2] = P1; 
            break;
        }
    }

    std::vector<Point2D> polygon;
    polygon.push_back(start_p);
    next_p = umap[start_p];
    while ( !(next_p == start_p)){
        polygon.push_back(next_p);
        next_p = umap[next_p];
    }
    polygons.push_back(polygon);
}

3 步骤实现如下,由于要区分外部轮廓和内部轮廓,所以需要首先计算最外边的线段轮廓

void linkEdgesWithMap(
    std::vector<std::vector<Point2D>>& polygons, 
    const std::vector<Segment2D>& segments, 
    bool is_print)
{
    std::unordered_map<Point2D, Point2D> umap;
    
    double max_X_axis = std::max(segments[0].P1.x, segments[0].P2.x);
    int outer_start_seg_id = 0;
    for (int i = 1; i<segments.size(); ++i)
    {
        const Point2D& P1 = segments[i].P1;
        const Point2D& P2 = segments[i].P2;
        if (P1.x > max_X_axis){
            max_X_axis = P1.x;
            outer_start_seg_id = i;
        }
        if (P2.x > max_X_axis)
        {
            max_X_axis = P2.x;
            outer_start_seg_id = i;
        }
    }
    addOutLineWithStartSegId(polygons, outer_start_seg_id, segments, umap);


    for (int i=0; i<segments.size(); ++i)
    {
        if (i == outer_start_seg_id) continue;
        addOutLineWithStartSegId(polygons, i, segments, umap);
    }
    if (umap.size() != segments.size())
    {
        printf("umap_size = %zd, segments.size() = %zd\n", umap.size(), segments.size());
        printf("error: %d, %s\n", __LINE__, __FILE__);
        printf("there is some error in the input model, check it if is closed.\n");
        exit(EXIT_FAILURE);
    }

    if (is_print)
    {
        for (int i=0; i<polygons.size(); ++i)
        {
            printf("\n\n------------------------Polygon %d----------------------\n", i);
            printf("polygon point size: %zd\n", polygons[i].size());
            int j = 0;
            for (const Point2D& point : polygons[i])
            {
                if (j != polygons[i].size()-1) printf("point(%lf, %lf)->", point.x, point.y);
                else printf("point(%lf, %lf)", point.x, point.y);
                if ((j+1) % 3 == 0) printf("\n");
                j++;
            }
            printf("\n");
        }
    }
}

1.3 完整实现如下

#include <unordered_map>
#include <cstdlib>
#include <cstdio>
#include <vector>
#include <string>
#include <iostream>
#include <fstream>


struct Point2D
{
	double x, y;
	Point2D() : x(0.0), y(0.0){}
    Point2D(double x_, double y_) : x(x_), y(y_){}
	Point2D(const Point2D& p) : x(p.x), y(p.y){}

	Point2D& operator=(const Point2D& p)
	{
		x = p.x;
		y = p.y;
		return *this;
    }
};

bool operator==(const Point2D& p1, const Point2D& p2)
{
    return (p1.x == p2.x && p1.y == p2.y);
}

// 用于哈希 Point2D
namespace std {
    template<>
    struct hash<Point2D> {
        std::size_t operator()(const Point2D& p) const {
            return std::hash<double>()(p.x) ^ std::hash<double>()(p.y);
        }
    };
}

struct Segment2D
{
    Point2D P1, P2;
    Segment2D() : P1(), P2(){};
    Segment2D(const Point2D& _P1, const Point2D& _P2){
        P1 = _P1; P2 = _P2;
    }

    Segment2D(const Segment2D& segment){
        P1 = segment.P1;
        P2 = segment.P2;
    }
    Segment2D& operator=(const Segment2D& segment){
        P1 = segment.P1;
        P2 = segment.P2;
        return *this;
    }
};


bool operator==(const Segment2D& segment1, const Segment2D& segment2)
{
    return (segment1.P1 == segment2.P1 && segment1.P2 == segment2.P2) ||
           (segment1.P1 == segment2.P2 && segment1.P2 == segment2.P1);
}

// 特化 std::hash 模板
namespace std {
template<>
struct hash<Segment2D> {
    size_t operator()(const Segment2D& s) const {
        // 假设我们用两个点的坐标组合来生成哈希值
        auto h1 = std::hash<double>{}(s.P1.x);
        auto h2 = std::hash<double>{}(s.P1.y);
        auto h3 = std::hash<double>{}(s.P2.x);
        auto h4 = std::hash<double>{}(s.P2.y);

        // 使用 XOR 组合哈希值
        return (h1 ^ (h2 << 1)) ^ (h3 ^ (h4 << 1));
    }
};
}


void readSTL2D(const std::string& filepath, std::vector<Point2D>& points, std::vector<Segment2D>& segments2D);
void linkEdgesWithMap(std::vector<std::vector<Point2D>>& polygons, 
    const std::vector<Segment2D>& segments, bool is_print = false);
void addOutLineWithStartSegId(std::vector<std::vector<Point2D>>& polygons, 
    int start_seg_id, const std::vector<Segment2D>& segments, std::unordered_map<Point2D, Point2D>& umap);

int main()
{
    // const std::string input =  "../test_cubic_huanti.stl";
    const std::string input = "../Hollow_out_square.stl";
    std::vector<Point2D> points;
    std::vector<Segment2D> segments2D;
    readSTL2D(input, points, segments2D);
    std::vector<std::vector<Point2D>> polygons;
    linkEdgesWithMap(polygons, segments2D, true);
}


void readSTL2D(const std::string& filepath, std::vector<Point2D>& points, std::vector<Segment2D>& segments2D)
{
    printf("Reading file \" %s \"...\n", filepath.c_str());
    std::ifstream input(filepath);
    if (!input.is_open())
    {
        printf("\nError: %s, %d.\n", __FILE__, __LINE__);
        printf("Can not open file:\"%s\" .\n", filepath.c_str());
        exit(EXIT_FAILURE);
    }
    std::string s;
    while (input >> s)
    {
        if (s == "vertex")
        {
            double x, y, z;
            input >> x >> y >> z;
            points.push_back(Point2D(x, y));
        }
    }

    if (points.size() % 3 != 0){
        printf("Error %s, %d\ninput 2D stl is not illegal.\n", __FILE__, __LINE__);
        exit(EXIT_FAILURE);
    }

    std::unordered_map<Segment2D, int> umap_segments2D;
    for (int i=0; i<points.size(); i+=3){
        const Point2D& p1 = points[i + 0];
        const Point2D& p2 = points[i + 1];
        const Point2D& p3 = points[i + 2];

        Segment2D seg1(p1, p2);
        Segment2D seg2(p2, p3);
        Segment2D seg3(p3, p1);

        umap_segments2D[seg1]++;
        umap_segments2D[seg2]++;
        umap_segments2D[seg3]++;
    }

    for (auto it = umap_segments2D.begin(); it != umap_segments2D.end(); ++it)
    {
        if (it->second != 1) continue;
        segments2D.push_back(it->first); 
    }
    printf("segments2D.size() = %zd\n", segments2D.size());
}


void addOutLineWithStartSegId(std::vector<std::vector<Point2D>>& polygons, 
    int start_seg_id, const std::vector<Segment2D>& segments, std::unordered_map<Point2D, Point2D>& umap)
{
    Point2D start_p = segments[start_seg_id].P1;
    Point2D next_p = segments[start_seg_id].P2;
    if (umap.find(start_p) != umap.end() || umap.find(next_p) != umap.end()) return;
    umap[start_p] = next_p;
    int i = 1;


    for (int j = 0; j<segments.size(); ++j)
    {
        if (j == start_seg_id) continue;
        Point2D P1 = segments[j].P1;
        Point2D P2 = segments[j].P2;

        if (P1 == next_p && umap.find(P2) == umap.end())
        {
            umap[next_p] = P2;
            next_p = P2;
            j = -1;
        }
        else if (P2 == next_p && umap.find(P1) == umap.end())
        {
            umap[next_p] = P1;
            next_p = P1;
            j = -1;
        }
        else if (P1 == next_p && P2 == start_p)
        {
            umap[P1] = P2; 
            break;
        }
        else if (P2 == next_p && P1 == start_p)
        {
            umap[P2] = P1; 
            break;
        }
    }

    std::vector<Point2D> polygon;
    polygon.push_back(start_p);
    next_p = umap[start_p];
    while ( !(next_p == start_p)){
        polygon.push_back(next_p);
        next_p = umap[next_p];
    }
    polygons.push_back(polygon);
}



void linkEdgesWithMap(
    std::vector<std::vector<Point2D>>& polygons, 
    const std::vector<Segment2D>& segments, 
    bool is_print)
{
    std::unordered_map<Point2D, Point2D> umap;
    
    double max_X_axis = std::max(segments[0].P1.x, segments[0].P2.x);
    int outer_start_seg_id = 0;
    for (int i = 1; i<segments.size(); ++i)
    {
        const Point2D& P1 = segments[i].P1;
        const Point2D& P2 = segments[i].P2;
        if (P1.x > max_X_axis){
            max_X_axis = P1.x;
            outer_start_seg_id = i;
        }
        if (P2.x > max_X_axis)
        {
            max_X_axis = P2.x;
            outer_start_seg_id = i;
        }
    }
    addOutLineWithStartSegId(polygons, outer_start_seg_id, segments, umap);


    for (int i=0; i<segments.size(); ++i)
    {
        if (i == outer_start_seg_id) continue;
        addOutLineWithStartSegId(polygons, i, segments, umap);
    }
    if (umap.size() != segments.size())
    {
        printf("umap_size = %zd, segments.size() = %zd\n", umap.size(), segments.size());
        printf("error: %d, %s\n", __LINE__, __FILE__);
        printf("there is some error in the input model, check it if is closed.\n");
        exit(EXIT_FAILURE);
    }

    if (is_print)
    {
        for (int i=0; i<polygons.size(); ++i)
        {
            printf("\n\n------------------------Polygon %d----------------------\n", i);
            printf("polygon point size: %zd\n", polygons[i].size());
            int j = 0;
            for (const Point2D& point : polygons[i])
            {
                if (j != polygons[i].size()-1) printf("point(%lf, %lf)->", point.x, point.y);
                else printf("point(%lf, %lf)", point.x, point.y);
                if ((j+1) % 3 == 0) printf("\n");
                j++;
            }
            printf("\n");
        }
    }
}

测试了两个stl文件, test_cubic_huanti.stl内容如下

输出结果如下:

第二个文件为Hollow_out_square.stl

test_cubic_huanti.stl

solid 
facet normal 0 0 1
 outer loop
  vertex 0 0 0
  vertex 1 0 0
  vertex 0 1 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 0 1 0
  vertex 1 1 0
  vertex 1 0 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 1 0 0
  vertex 2 0 0
  vertex 1 1 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 1 1 0
  vertex 2 1 0
  vertex 2 0 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 2 0 0
  vertex 3 0 0
  vertex 2 1 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 3 0 0
  vertex 3 1 0
  vertex 2 1 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 0 1 0
  vertex 1 1 0
  vertex 0 2 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 0 2 0
  vertex 1 1 0
  vertex 1 2 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 2 2 0
  vertex 2 1 0
  vertex 3 1 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 3 1 0
  vertex 3 2 0
  vertex 2 2 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 0 2 0
  vertex 0 3 0
  vertex 1 2 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 0 3 0
  vertex 1 3 0
  vertex 1 2 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 1 2 0
  vertex 1 3 0
  vertex 2 2 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 1 3 0
  vertex 2 3 0
  vertex 2 2 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 2 2 0
  vertex 2 3 0
  vertex 3 2 0
 endloop
endfacet
facet normal -0 0 1
 outer loop
  vertex 2 3 0
  vertex 3 3 0
  vertex 3 2 0
 endloop
endfacet
endsolid 

Hollow_out_square.stl 

solid square_with_holes
  facet normal 0 0 1
    outer loop
      vertex 0 0 0
      vertex 0 1 0
      vertex 1 0 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 1 0 0
      vertex 0 1 0
      vertex 1 1 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 0 1 0
      vertex 1 1 0
      vertex 1 2 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 1 2 0
      vertex 0 1 0
      vertex 0 2 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 0 2 0
      vertex 1 2 0
      vertex 0 3 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 1 2 0
      vertex 1 3 0
      vertex 0 3 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 0 3 0
      vertex 1 4 0
      vertex 1 3 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 1 4 0
      vertex 0 3 0
      vertex 0 4 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 0 4 0
      vertex 1 4 0
      vertex 0 5 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 1 4 0
      vertex 0 5 0
      vertex 1 5 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 1 4 0
      vertex 1 5 0
      vertex 2 5 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 1 4 0
      vertex 2 5 0
      vertex 2 4 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 2 4 0
      vertex 2 5 0
      vertex 3 4 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 3 4 0
      vertex 2 5 0
      vertex 3 5 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 3 4 0
      vertex 3 5 0
      vertex 4 5 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 3 4 0
      vertex 4 5 0
      vertex 4 4 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 4 4 0
      vertex 4 5 0
      vertex 5 4 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 4 5 0
      vertex 5 5 0
      vertex 5 4 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 4 3 0
      vertex 4 4 0
      vertex 5 4 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 4 3 0
      vertex 5 4 0
      vertex 5 3 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 4 2 0
      vertex 4 3 0
      vertex 5 2 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 5 2 0
      vertex 4 3 0
      vertex 5 3 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 4 1 0
      vertex 4 2 0
      vertex 5 2 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 4 1 0
      vertex 5 2 0
      vertex 5 1 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 5 0 0
      vertex 4 1 0
      vertex 5 1 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 5 0 0
      vertex 4 0 0
      vertex 4 1 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 4 0 0
      vertex 3 0 0
      vertex 4 1 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 3 0 0
      vertex 3 1 0
      vertex 4 1 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 3 0 0
      vertex 2 1 0
      vertex 3 1 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 2 0 0
      vertex 2 1 0
      vertex 3 0 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 2 0 0
      vertex 1 0 0
      vertex 2 1 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 1 0 0
      vertex 1 1 0
      vertex 2 1 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 3 1 0
      vertex 2 1 0
      vertex 3 2 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 2 1 0
      vertex 2 2 0
      vertex 3 2 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 1 2 0
      vertex 2 3 0
      vertex 2 2 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 1 2 0
      vertex 1 3 0
      vertex 2 3 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 2 2 0
      vertex 2 3 0
      vertex 3 2 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 3 2 0
      vertex 2 3 0
      vertex 3 3 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 3 2 0
      vertex 3 3 0
      vertex 4 3 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 3 2 0
      vertex 4 3 0
      vertex 4 2 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 2 3 0
      vertex 3 4 0
      vertex 3 3 0
    endloop
  endfacet
  facet normal 0 0 1
    outer loop
      vertex 2 3 0
      vertex 2 4 0
      vertex 3 4 0
    endloop
  endfacet
endsolid square_with_holes

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值