#ifndef VEC3_H
#define VEC3_H
#include <cmath>
class Vec3
{
public:
float x, y, z;
Vec3(float x_ = 0.0f, float y_ = 0.0f, float z_ = 0.0f)
: x(x_), y(y_), z(z_) {}
Vec3 operator-(const Vec3 &v) const
{
return Vec3(this->x - v.x, this->y - v.y, this->z - v.z);
}
Vec3 operator+(const Vec3 &v) const
{
return Vec3(this->x + v.x, this->y + v.y, this->z + v.z);
}
float length_squared() const
{
return this->x * this->x + this->y * this->y + this->z * this->z;
}
float length() const
{
return std::sqrt(length_squared());
}
};
inline float dot(Vec3 v1, Vec3 v2)
{
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
}
inline Vec3 cross(Vec3 v1, Vec3 v2)
{
return Vec3(
v1.y * v2.z - v1.z * v2.y,
v1.z * v2.x - v1.x * v2.z,
v1.x * v2.y - v1.y * v2.x);
}
class Triangle
{
public:
Vec3 a;
Vec3 b;
Vec3 c;
Triangle(Vec3 v1, Vec3 v2, Vec3 v3) : a(v1), b(v2), c(v3) {}
};
#endif // VEC3_H
일단 벡터 클래스와, 벡터 3개를 이용한 Triangle클래스를 만들어줌 ㅇㅇ
벡터클래스에는 연산자 오버로딩, dot, cross 연산을 정의해놓음ㅇㅇ
일단 2차원에서 진행된다는 점을 잊지 말 것!
Triangle t1(Vec3(7,45,0), Vec3(35,100,0), Vec3(45,60,0));
Triangle t2(Vec3(120,35,0), Vec3(90,5,0), Vec3(45,110,0));
Triangle t3(Vec3(115,83,0), Vec3(80,90,0), Vec3(85,120,0));
draw_triangle(t1.a.x, t1.a.y, t1.b.x, t1.b.y, t1.c.x, t1.c.y, framebuffer, red);
draw_triangle(t2.a.x, t2.a.y, t2.b.x, t2.b.y, t2.c.x, t2.c.y, framebuffer, white);
draw_triangle(t3.a.x, t3.a.y, t3.b.x, t3.b.y, t3.c.x, t3.c.y, framebuffer, green);
이렇게 3개의 triangle을 그리도록 했음
draw_triangle은
void line(int ax, int ay, int bx, int by, TGAImage &framebuffer, TGAColor color)
{
bool is_x_smaller = (std::abs(ax - bx) < std::abs(ay - by));
if (is_x_smaller) { std::swap(ax, ay); std::swap(bx, by); }
if (ax > bx) { std::swap(ax, bx); std::swap(ay, by); }
int dx = bx - ax;
int dy = std::abs(by - ay);
int idiff = 0;
int y = ay;
int ystep = (by > ay) ? 1 : -1;
for (int x = ax; x <= bx; x++)
{
if (is_x_smaller) framebuffer.set(y, x, color);
else framebuffer.set(x, y, color);
idiff += 2 * dy;
if (idiff > dx)
{
y += ystep;
idiff -= 2 * dx;
}
}
}
void draw_triangle(int ax, int ay, int bx, int by, int cx, int cy, TGAImage &framebuffer, TGAColor color)
{
line(ax, ay, bx, by, framebuffer, color);
line(bx, by, cx, cy, framebuffer, color);
line(cx, cy, ax, ay, framebuffer, color);
}
이렇게 작동함
왜 저런 코드가 나왓는지 궁금하면
나만의 tiny renderer 만들기 (1) - Bresenham Algorithm 참고!
이런 삼각형 3개가 만들어지지

여기에 이제 특정 픽셀이 삼각형 내부인지를 판별해야함
이걸 쉽게 구할 수 있는 방식이 있음
그게 바로 외적 공식의 기하학적 특성을 이용한거임

두 벡터의 외적된 벡터의 절대값(길이)는
두 벡터가 이루는 평행사변형의 크기와 같음
가 되는거임
![]() | ![]() | ![]() |
|---|
또, 이렇게
점 p에서 삼각형 각 꼭짓점까지 만들 수 있는 새로운 삼각형 3개의 넓이의 합 == 기존 삼각형의 넓이
= 세 삼각형 넓이의 합 - 기존 삼각형 넓이 == 0
라면, 점 p는 삼각형 내부에 있는거임
여기서 외적의 성질을 이용하면 삼각형 넓이가 아닌
두 벡터의 외적 길이인 평행사변형의 크기를 이용해도 문제가 없음
따라서 먼저 다음과 같은 넓이 구하는 코드 + 점 p의 위치가 삼각형 내부에 있는지 판별하는 메서드를 만들어줌
float get_triangle_area(const Triangle &t)
{
auto ba = t.b - t.a;
auto ca = t.c - t.a;
auto cr = cross(ba, ca);
return cr.length();
}
bool is_inside_triangle(const float area, const Vec3 &p, const Triangle &t)
{
Triangle pt1(Vec3(p), Vec3(t.a), Vec3(t.b));
Triangle pt2(Vec3(p), Vec3(t.b), Vec3(t.c));
Triangle pt3(Vec3(p), Vec3(t.c), Vec3(t.a));
auto pt1_area = get_triangle_area(pt1);
auto pt2_area = get_triangle_area(pt2);
auto pt3_area = get_triangle_area(pt3);
float diff = area - (pt1_area + pt2_area + pt3_area);
if (0 <= diff && diff < 1e-4) return true;
return false;
}
점 p를 이용해 만들 수 있는 삼각형을 만들고,
해당 삼각형의 외적과 외적의 길이를 구하여 평행사변형의 넓이를 구함
왜
std::abs와 같은 절대값 처리가 없음?쉬움
오른손 좌표계냐 왼손 좌표계냐에 따라 외적의 방향이 달라지지?
두 벡터가 오른손 좌표계에서 외적이 특정 방향(d)이었다면
해당 두 벡터는 왼손 좌표계에서 외적은 -d 가 됨근데, 벡터의 길이를 구하는 로직은 자기 자신의 모든 좌표를 제곱하는 것과 같음
float length_squared() const { return this->x * this->x + this->y * this->y + this->z * this->z; }그러니까 외적의 특정 축이 음수였다고 하더라도,
길이를 구할때는 무조건 양수가 나오게 되는거임
이제 전체 main.cpp를 봐보자
int main(int argc, char** argv)
{
TGAImage framebuffer(width, height, TGAImage::RGB);
Triangle t1(Vec3(7,45,0), Vec3(35,100,0), Vec3(45,60,0));
Triangle t2(Vec3(120,35,0), Vec3(90,5,0), Vec3(45,110,0));
Triangle t3(Vec3(115,83,0), Vec3(80,90,0), Vec3(85,120,0));
draw_triangle(t1.a.x, t1.a.y, t1.b.x, t1.b.y, t1.c.x, t1.c.y, framebuffer, red);
draw_triangle(t2.a.x, t2.a.y, t2.b.x, t2.b.y, t2.c.x, t2.c.y, framebuffer, white);
draw_triangle(t3.a.x, t3.a.y, t3.b.x, t3.b.y, t3.c.x, t3.c.y, framebuffer, green);
float t1_area = get_triangle_area(t1);
float t2_area = get_triangle_area(t2);
float t3_area = get_triangle_area(t3);
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
Vec3 p(j, i, 0);
if (is_inside_triangle(t1_area, p, t1))
{
framebuffer.set(j, i, red);
}
if (is_inside_triangle(t2_area, p, t2))
{
framebuffer.set(j, i, white);
}
if (is_inside_triangle(t3_area, p, t3))
{
framebuffer.set(j, i, green);
}
}
}
framebuffer.write_tga_file("framebuffer.tga");
return 0;
}
height와 width를 전부 돌면서
삼각형 t1,t2,t3와 점 p가 어떤 관계인지를 파악하고
그걸 framebuffer에 넣어 출력하는거임

그럼 이렇게 내부가 칠해진 삼각형이 나오게 됨~~ㄷㄷㄷㄷ
지금 코드는 모든 width와 height의 픽셀에 대하여 해당 픽셀이 삼각형 내부인지 아닌지를 판별함
근데, 사진에서 잘 보면 삼각형 어디에도 절대 포함될 수 없는 픽셀들이 존재함
빨간 삼각형을 기준으로 봐보자

이렇게 빗금칠 된 공간은 절대로 삼각형 내부에 들어갈 수 없음
그래서 저부분을 빼고 계산을 해준다는 개념이 바운딩 박스임
먼저 모든 삼각형을 하나의 객체처럼 관리하기 위해 배열에 넣어줌
std::vector<std::pair<Triangle, TGAColor>> triangles; //add
int main(int argc, char** argv)
{
TGAImage framebuffer(width, height, TGAImage::RGB);
Triangle t1(Vec3(7,45,0), Vec3(35,100,0), Vec3(45,60,0));
Triangle t2(Vec3(120,35,0), Vec3(90,5,0), Vec3(45,110,0));
Triangle t3(Vec3(115,83,0), Vec3(80,90,0), Vec3(85,120,0));
triangles.push_back(std::make_pair(t1, red));
triangles.push_back(std::make_pair(t2, white));
triangles.push_back(std::make_pair(t3, green));
//...
}
그리고 특정 점 p가 삼각형 내부인지 판별하기전에
해당 삼각형의 min, max x-y의 범위에서 loop를 돌도록 하면 됨
따라서 전체 main은 다음처럼 바뀜
std::vector<std::pair<Triangle, TGAColor>> triangles;
int main(int argc, char** argv)
{
TGAImage framebuffer(width, height, TGAImage::RGB);
Triangle t1(Vec3(7,45,0), Vec3(35,100,0), Vec3(45,60,0));
Triangle t2(Vec3(120,35,0), Vec3(90,5,0), Vec3(45,110,0));
Triangle t3(Vec3(115,83,0), Vec3(80,90,0), Vec3(85,120,0));
triangles.push_back(std::make_pair(t1, red));
triangles.push_back(std::make_pair(t2, white));
triangles.push_back(std::make_pair(t3, green));
draw_triangle(t1.a.x, t1.a.y, t1.b.x, t1.b.y, t1.c.x, t1.c.y, framebuffer, red);
draw_triangle(t2.a.x, t2.a.y, t2.b.x, t2.b.y, t2.c.x, t2.c.y, framebuffer, white);
draw_triangle(t3.a.x, t3.a.y, t3.b.x, t3.b.y, t3.c.x, t3.c.y, framebuffer, green);
for (int ti = 0; ti < triangles.size(); ++ti)
{
auto p = triangles[ti];
Triangle t = p.first;
TGAColor color = p.second;
//box bounding
int minX = std::min(std::min(t.a.x, t.b.x), t.c.x);
int minY = std::min(std::min(t.a.y, t.b.y), t.c.y);
int maxX = std::max(std::max(t.a.x, t.b.x), t.c.x);
int maxY = std::max(std::max(t.a.y, t.b.y), t.c.y);
float area = get_triangle_area(t);
for (int j = minY; j <= maxY; ++j)
{
for (int i = minX; i <= maxX; ++i)
{
Vec3 point(i, j, 0);
if (is_inside_triangle(area, point, t))
{
framebuffer.set(i, j, color);
}
}
}
}
framebuffer.write_tga_file("framebuffer.tga");
return 0;
}

잘 출력댐
박스 바운딩 전 | 박스 바운딩 후 |
|---|
3개의 삼각형을 그리는데도 차이가 심함!
그러니까 바운딩 박스 최적화를 잘 사용해보자
지금 삼각형 내부를 판별하는 코드는 문제가 있음

대충 어떤 모델을 렌더링한 결과임
잘 보면 이상한 부분이 많음

이게 이렇게 렌더링되는 이유는
먼저 공부중인 코스에 맞춰 코드를 수정함
float get_triangle_area(Triangle t)
{
return 0.5 * (
(t.b.y-t.a.y)*(t.b.x+t.a.x) +
(t.c.y-t.b.y)*(t.c.x+t.b.x) +
(t.a.y-t.c.y)*(t.a.x+t.c.x)
);
}
void triangle(Triangle t, TGAImage &framebuffer, TGAColor color)
{
int bbminx = std::min(std::min(t.a.x, t.b.x), t.c.x);
int bbminy = std::min(std::min(t.a.y, t.b.y), t.c.y);
int bbmaxx = std::max(std::max(t.a.x, t.b.x), t.c.x);
int bbmaxy = std::max(std::max(t.a.y, t.b.y), t.c.y);
double total_area = get_triangle_area(t);
#pragma omp parallel for
for (int x=bbminx; x<=bbmaxx; x++)
{
for (int y=bbminy; y<=bbmaxy; y++)
{
double alpha = get_triangle_area(Triangle(Vec3(x,y,0), Vec3(t.b.x, t.b.y, 0), Vec3(t.c.x, t.c.y, 0))) / total_area;
double beta = get_triangle_area(Triangle(Vec3(x,y,0), Vec3(t.c.x, t.c.y, 0), Vec3(t.a.x, t.a.y, 0))) / total_area;
double gamma =get_triangle_area(Triangle(Vec3(x,y,0), Vec3(t.a.x, t.a.y, 0), Vec3(t.b.x, t.b.y, 0))) / total_area;
if (alpha<0 || beta<0 || gamma<0) continue;
framebuffer.set(x, y, color);
}
}
}
이렇게 코드를 바꿈
is_inside_triangle는 삭제하고 위처럼 코드를 수정함
중요한건
return 0.5 * (
(t.b.y-t.a.y)*(t.b.x+t.a.x) +
(t.c.y-t.b.y)*(t.c.x+t.b.x) +
(t.a.y-t.c.y)*(t.a.x+t.c.x)
);
이부분임
지금은 2차원 평면이므로, 모든 좌표의 z좌표가 0임
두 3차원 벡터의 외적공식은 다음과 같음
이때 벡터는 각각
그리고 z축과 관련된 벡터의 요소는 0임
따라서 의 외적은 다음과 같음(z축과 관련된 항 모두 제거)
이걸 분배법칙을 이용해 전개해보면 다음과 같음
이때 두 스칼라의 곱셈법칙은 교환법칙이 성립함
좀 어지러우니 식을 정리해보자
여기서 트릭 하나 발동
라는 식 한개를 정의해보자
이항정리를 하면 0이 되는걸 알수있음
그럼 이 식은 합이 0이니, 위의 식에 넣어도 0이니, 값에 변화가 없음을 알 수 있음
와 같이 두개를 더한거나 아닌거나 기존의 외적과 같은 값이라는거임
그럼 이제 분배법칙을 이용하기 위해 식을 조금 정리해보자
처럼 전개 가능함(개빡세네 에휴...)
그럼 이제 각 요소의 곱셈자리마다 분배법칙을 이용할 수 있음
로 식을 변환할 수 있음
그럼 이제 다시 곱셈법칙을 이용해서 처리할 수 있는데
로 전개가 완료됨

원래대로 잘 출력됨!
이제 백페이스 컬링 할차례
void triangle(Triangle t, TGAImage &framebuffer, TGAColor color)
{
int bbminx = std::min(std::min(t.a.x, t.b.x), t.c.x);
int bbminy = std::min(std::min(t.a.y, t.b.y), t.c.y);
int bbmaxx = std::max(std::max(t.a.x, t.b.x), t.c.x);
int bbmaxy = std::max(std::max(t.a.y, t.b.y), t.c.y);
double total_area = get_area(t);
#pragma omp parallel for
for (int x=bbminx; x<=bbmaxx; x++)
{
for (int y=bbminy; y<=bbmaxy; y++)
{
double alpha = get_area(Triangle(Vec3(x,y,0), Vec3(t.b.x, t.b.y, 0), Vec3(t.c.x, t.c.y, 0))) / total_area;
double beta = get_area(Triangle(Vec3(x,y,0), Vec3(t.c.x, t.c.y, 0), Vec3(t.a.x, t.a.y, 0))) / total_area;
double gamma =get_area(Triangle(Vec3(x,y,0), Vec3(t.a.x, t.a.y, 0), Vec3(t.b.x, t.b.y, 0))) / total_area;
if (alpha<0 || beta<0 || gamma<0) continue;
framebuffer.set(x, y, color);
}
}
}
이코드를 잘 봐보자
점 a,b,c가 있을때
오른손 좌표계를 기준으로 함

처럼 됨.
이때 외적 벡터의 z는 양수가 됨

하지만 이렇게 좌표가 있다면
오른손 좌표계 기준으로
외적 벡터의 z는 음수가 됨
이렇게 좌표계에 맞춰서
원하는 좌표계를 정했다면
정면이 아닌 z축을 가지는 삼각형은 렌더링을 패싱하면 되는거임
void triangle(Triangle t, TGAImage &framebuffer, TGAColor color)
{
int bbminx = std::min(std::min(t.a.x, t.b.x), t.c.x);
int bbminy = std::min(std::min(t.a.y, t.b.y), t.c.y);
int bbmaxx = std::max(std::max(t.a.x, t.b.x), t.c.x);
int bbmaxy = std::max(std::max(t.a.y, t.b.y), t.c.y);
double total_area = get_area(t);
//add
if (total_area < 0) return;
#pragma omp parallel for
for (int x=bbminx; x<=bbmaxx; x++)
{
for (int y=bbminy; y<=bbmaxy; y++)
{
double alpha = get_area(Triangle(Vec3(x,y,0), Vec3(t.b.x, t.b.y, 0), Vec3(t.c.x, t.c.y, 0))) / total_area;
double beta = get_area(Triangle(Vec3(x,y,0), Vec3(t.c.x, t.c.y, 0), Vec3(t.a.x, t.a.y, 0))) / total_area;
double gamma =get_area(Triangle(Vec3(x,y,0), Vec3(t.a.x, t.a.y, 0), Vec3(t.b.x, t.b.y, 0))) / total_area;
if (alpha<0 || beta<0 || gamma<0) continue;
framebuffer.set(x, y, color);
}
}
}
그게 add 주석 부분임
졸라 간단스


이런 렌더링이 됨
하지만 아직 문제가 있음
뒷면에 해당되는 삼각형
예를들어 입안, 옷 안쪽은 무시되거나 다른 삼각형 위에 렌더링이 됨
이건 다음에 해결하도록~~~
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <ctime>
#include "tgaimage.h"
#include <iostream>
#include <fstream>
#include <string>
#include "Model.h"
#include "Vec3.h"
constexpr TGAColor white = {255, 255, 255, 255};
constexpr TGAColor green = { 0, 255, 0, 255};
constexpr TGAColor red = { 0, 0, 255, 255};
constexpr TGAColor blue = {255, 128, 64, 255};
constexpr TGAColor yellow = { 0, 200, 255, 255};
constexpr int width = 1024;
constexpr int height = 1024;
float get_triangle_area(Triangle t)
{
return 0.5 * (
(t.b.y-t.a.y)*(t.b.x+t.a.x) +
(t.c.y-t.b.y)*(t.c.x+t.b.x) +
(t.a.y-t.c.y)*(t.a.x+t.c.x)
);
}
void triangle(Triangle t, TGAImage &framebuffer, TGAColor color)
{
int bbminx = std::min(std::min(t.a.x, t.b.x), t.c.x);
int bbminy = std::min(std::min(t.a.y, t.b.y), t.c.y);
int bbmaxx = std::max(std::max(t.a.x, t.b.x), t.c.x);
int bbmaxy = std::max(std::max(t.a.y, t.b.y), t.c.y);
double total_area = get_triangle_area(t);
//if (total_area < 0) return;
#pragma omp parallel for
for (int x=bbminx; x<=bbmaxx; x++)
{
for (int y=bbminy; y<=bbmaxy; y++)
{
double alpha = get_triangle_area(Triangle(Vec3(x,y,0), Vec3(t.b.x, t.b.y, 0), Vec3(t.c.x, t.c.y, 0))) / total_area;
double beta = get_triangle_area(Triangle(Vec3(x,y,0), Vec3(t.c.x, t.c.y, 0), Vec3(t.a.x, t.a.y, 0))) / total_area;
double gamma =get_triangle_area(Triangle(Vec3(x,y,0), Vec3(t.a.x, t.a.y, 0), Vec3(t.b.x, t.b.y, 0))) / total_area;
if (alpha<0 || beta<0 || gamma<0) continue;
framebuffer.set(x, y, color);
}
}
}
std::tuple<int,int> project(Vec3 v)
{
return { static_cast<int>(std::round((v.x + 1.) * width / 2)),
static_cast<int>(std::round((v.y + 1.) * height / 2)) };
}
std::vector<std::pair<Triangle, TGAColor>> triangles;
int main(int argc, char** argv)
{
if (argc != 2)
{
std::cerr << "Err Usage: " << argv[0] << " obj/model.obj" << std::endl;
return 1;
}
Model model(argv[1]);
TGAImage framebuffer(width, height, TGAImage::RGB);
// triangle(Triangle(Vec3(7, 45, 0), Vec3(35, 100, 0), Vec3(45, 60, 0)), framebuffer, red);
// triangle(Triangle(Vec3(120, 35, 0), Vec3(90, 5, 0), Vec3(45, 110, 0)), framebuffer, white);
// triangle(Triangle(Vec3(115, 83, 0), Vec3(80, 90, 0), Vec3(85, 120, 0)), framebuffer, green);
Vec3 camera_pos(0,0, 1);
for (int i = 0; i < model.nfaces(); i++)
{
auto [ax, ay] = project(model.vert(i, 0));
auto [bx, by] = project(model.vert(i, 1));
auto [cx, cy] = project(model.vert(i, 2));
TGAColor rnd;
for (int c = 0; c < 3; c++) rnd[c] = std::rand() % 255;
triangle(Triangle(Vec3(ax, ay, 0), Vec3(bx, by, 0), Vec3(cx, cy, 0)), framebuffer, rnd);
}
framebuffer.write_tga_file("framebuffer.tga");
return 0;
}
#ifndef VEC3_H
#define VEC3_H
#include <cmath>
class Vec3
{
public:
float x, y, z;
Vec3(float x_ = 0.0f, float y_ = 0.0f, float z_ = 0.0f)
: x(x_), y(y_), z(z_) {}
Vec3 operator-(const Vec3 &v) const
{
return Vec3(this->x - v.x, this->y - v.y, this->z - v.z);
}
Vec3 operator+(const Vec3 &v) const
{
return Vec3(this->x + v.x, this->y + v.y, this->z + v.z);
}
float& operator[](int i)
{
if (i == 0) return x;
if (i == 1) return y;
return z;
}
const float& operator[](int i) const
{
if (i == 0) return x;
if (i == 1) return y;
return z;
}
float length_squared() const
{
return this->x * this->x + this->y * this->y + this->z * this->z;
}
float length() const
{
return std::sqrt(length_squared());
}
};
inline float dot(Vec3 v1, Vec3 v2)
{
return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
}
inline Vec3 cross(Vec3 v1, Vec3 v2)
{
return Vec3(
v1.y * v2.z - v1.z * v2.y,
v1.z * v2.x - v1.x * v2.z,
v1.x * v2.y - v1.y * v2.x);
}
class Triangle
{
public:
Vec3 a;
Vec3 b;
Vec3 c;
Triangle(Vec3 v1, Vec3 v2, Vec3 v3) : a(v1), b(v2), c(v3) {}
};
#endif // VEC3_H
#ifndef MODEL_H
#define MODEL_H
#include <string>
#include <vector>
class Vec3;
class Model
{
std::vector<Vec3> verts = {};
std::vector<int> facet_vrt = {};
public:
Model(const std::string filename);
int nverts() const;
int nfaces() const;
Vec3 vert(const int i) const;
Vec3 vert(const int iface, const int nthvert) const;
};
#endif // MODEL_H
#include <fstream>
#include <iostream>
#include <sstream>
#include "model.h"
#include "Vec3.h"
Model::Model(const std::string filename) {
std::ifstream in;
in.open(filename, std::ifstream::in);
if (in.fail()) return;
std::string line;
while (!in.eof()) {
std::getline(in, line);
std::istringstream iss(line.c_str());
char trash;
if (!line.compare(0, 2, "v ")) {
iss >> trash;
Vec3 v;
for (int i : {0,1,2}) iss >> v[i];
verts.push_back(v);
} else if (!line.compare(0, 2, "f ")) {
int f,t,n, cnt = 0;
iss >> trash;
while (iss >> f >> trash >> t >> trash >> n) {
facet_vrt.push_back(--f);
cnt++;
}
if (3!=cnt) {
std::cerr << "Error: the obj file is supposed to be triangulated" << std::endl;
return;
}
}
}
std::cerr << "# v# " << nverts() << " f# " << nfaces() << std::endl;
}
int Model::nverts() const { return verts.size(); }
int Model::nfaces() const { return facet_vrt.size()/3; }
Vec3 Model::vert(const int i) const {
return verts[i];
}
Vec3 Model::vert(const int iface, const int nthvert) const {
return verts[facet_vrt[iface*3+nthvert]];
}