일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- ray tracing
- 레이 트레이싱
- AR Foundation Throwing
- 렌더링
- 평면에 물체 던지기
- AR Throwing Game
- AR 던지기 게임
- Ray Tracing in One Weekend
- AR 게임
- ar game
- Today
- Total
요리조리
폴리곤 메시 렌더링하기 (Rendering Polygon Meshes)_1 본문
사용하는 c++ 함수:
- std::move
- std::vector
- static_cast
- assert
필요한 구조체, 클래스 정의하기 (Defining Structures and Classes)
메시 클래스를 정의하기 앞서, 메시 오브젝트를 구성하는 요소들(e.g., vertex, face)의 타입부터 정의하자. 요소들은 구조체를 이용하여 새로운 데이터타입으로 정의한다.
struct Vertex
{
Vec3 position;
Vec3 normal;
}
struct Face
{
int vertices[3];
int normals[3];
}
이제, Vertex와 Face 타입의 변수를 요소, 즉 속성(attribute)으로 갖는 메시 클래스를 정의하자. 이때, 이 속성들은 private 접근 제한자를 두었기 때문에 이에 접근하기 위한 setter(쓰는), getter(읽는) 메소드들을 정의해야 한다.
class Mesh
{
public:
explicit Mesh(vector<Vertex> vertices, vector<Face> faces);
private:
vector<Vertex> m_vertices;
vector<Face> m_faces;
}
void Mesh::setVertices(vector<Vertex> vertices)
{
m_vertices = std::move(vertices); // Data transfer
// updateBoundingBox();
}
void Mesh::setFaces(vector<Face> faces)
{
m_faces = std::move(faces); // Data transfer
}
///
오브젝트 파일 로딩하기 (Loading object(.obj) files)
폴리곤 메시 오브젝트 파일 (일반적으로 .obj 파일)을 읽어서 원하는 형식으로 데이터를 저장하는 과정이 필요하다. 오브젝트 파일을 읽는 과정을 이해하기 위해 아래 페이지를 참고하였다. 해당 페이지에서는 피라미드 메시의 오브젝트 파일을 읽는 방법을 설명하고 있다.
https://winterbloed.be/reading-an-obj-file-in-processing/
Reading an .obj file in Processing
One of the most useful skills in creative coding is reading all kinds of data and files. And reading Wavefront .obj files in Processing is definitely one of the more fun applications.
winterbloed.be
오브젝트 파일 형식 (Object File Format)
오브젝트 파일 형식은 사실상 텍스트 (.txt) 파일이다. 파일의 각 라인은 메시를 구성하고 있는 요소(vertex, vertex normal, face, etc.) 하나의 정보를 저장하고 있다. 이때, 각 라인의 맨 앞에 있는 문자열은 해당 라인이 저장하고 있는 요소의 타입을 알려준다. 예를 들면, 문자열 "v"로 시작하는 줄은 vertex 정보를, "f"로 시작하는 줄은 face 정보를 저장한다는 것을 의미한다. 따라서 파일을 라인별로 읽을 때, 각 라인의 맨 앞 문자열을 읽고 해당 타입의 데이터를 처리하게끔 한다. 또한, 타입별로 각 라인의 순서(order)는 해당 타입 데이터의 고유한 인덱스가 된다. 가령, "v"로 시작하는 라인들 중 3번째로 등장하는 라인은 인덱스가 3인 vertex 데이터이다. 위에서 참고한 페이지에서는 피라미드 메시의 오브젝트 파일을 처리하고 있는데, 그 형식은 아래 그림과 같다.

- v: vertex line
- vn: vertex normal
- vt: vertex texture
- f: face
오브젝트 파일 형식에 대한 더욱 구체적인 정보는 아래 사이트에서 참고하시라.
https://www.cs.cmu.edu/~mbz/personal/graphics/obj.html
https://www.cs.cmu.edu/~mbz/personal/graphics/obj.html
NOTE: This page was havested from googles cache of royriggs OBJ page which has been shut down due to a laps of domain name renewal. OBJ FILE FORMAT Poser 3 only uses a subset of the full OBJ file format. Here's everything you need to know about OBJ files i
www.cs.cmu.edu
오브젝트 파일 읽기 (Reading Object (.obj) File)
먼저, 오브젝트 (.obj) 파일을 읽어보자.
C언어의 파일 입출력 방식 (fopen, fclose)을 사용하든, C++언어의 파일 입출력 방식 (open, close)를 사용하든 상관 없다. 여기서는 C++언어 방식을 사용할 것이다.
(C언어의 방식은 아래 사이트를 참고하면 좋다.)
https://jhnyang.tistory.com/196
[C언어] 파일입출력 -스트림(STREAM)에 대한 이해, fopen, fclose
[C/ C++ 프로그래밍 기초 목차] 오랜만에 사용해보는 기본스티커 ㅎㅎㅎ 안녕하세요. 즐거운(?) 주말이예요 ㅎㅎ 오늘은 파일입출력에 관한 포스팅을 들고 왔숩니다. 오랜만에 옛날에 배운걸 더듬
jhnyang.tistory.com
1. 파일 입출력을 하기 위해 <fstream> 헤더 라이브러리의 스트림 객체를 생성해준다. 이때, 파일을 읽기만 할 것이므로 ifstream 객체를 생성한다.
- ifstream: 파일 읽기만 하는 경우
- ofstream: 파일 쓰기만 하는 경우
- fstream: 파일 읽기, 쓰기를 모두 하는 경우
std::open() 함수를 사용하여 파일을 연다(외부 파일과 프로그램 사이에 스트림에 연결한다).
#include <fstream>
using namespace std;
int main(void) {
readObjFile("pyramid.obj");
}
bool readObjFile(string filename) {
// ifstream file(filename);
ifstream file;
file.open(filename);
}
이때, 파일을 읽지 못하는 경우를 대비해 예외처리를 해준다. ifstream 클래스의 멤버함수인 fail() 함수를 사용하거나 그와 반대되는 is_open() 함수를 사용하면 된다.
#include <fstream>
using namespace std;
int main(void) {
readObjFile("pyramid.obj");
}
bool readObjFile(string filename) {
// Open the object file
// ifstream file(filename);
ifstream file;
file.open(filename);
if(file.fail()) { // If it fails to read the file (== !file.is_open())
// printf("Cannot open the object file.\n");
cerr << "Cannot open the object file." << endl;
return false;
}
return true;
}
2. 파일이 제대로 열렸다면, std::getline() 함수를 사용하여 파일을 라인 바이 라인으로 읽는다. 한 라인을 string 변수에 저장하고, 해당 문자열을 공백을 기준으로 나누어 데이터를 저장한다. 이 과정은 단순히 라인마다 공백을 기준으로 데이터를 분리하여 데이터의 타입에 해당되는 벡터(vector)에 저장하는 과정이다. 이때 저장된 데이터는 날것의 데이터(raw data)이므로, 추후에 이 데이터를 렌더링에 사용할 수 있는 형식의 데이터로 가공하는 과정이 필요하다.
(이때, string을 인자로 넣는 std::getline() 함수 말고도 char 배열을 인자로 넣는 std::istream::getline() 함수를 사용할 수도 있다. 자세한 내용은 https://jhnyang.tistory.com/107를 참고하면 된다.)
- 라인의 첫번째 substring은 라인의 타입을 결정한다. 해당 서브 문자열을 type 변수에 저장한다.
- 라인의 나머지 substring은 타입에 따라 특정 값들을 저장한다 (e.g., vertex나 vertex normal 타입의 경우, x, y, z dimension 값들을 저장하고, face 타입의 경우, 해당 face를 구성하는 vertex와 vertex normal들의 index 값들을 저장한다).

fstream 클래스의 멤버함수인 find()와 substr() 함수를 이용하여, 공백을 기준으로 문자열을 substring으로 나누어 저장하자.
- line.find(separator, cur_pos): cur_pos 위치에서부터 라인을 탐색하여 최초의 separator(char 타입)이 나오는 위치(인덱스)를 반환한다. 이때, separator가 발견되지 않는 경우, string::npos를 반환한다.
- line.substr(pos1, pos2): pos1 위치에서부터 pos2 위치까지의 substring을 반환한다.
- std::stof(str): string 타입 인자를 float 타입으로 변환한 값을 반환한다.
// parsing
while(getline(file, line))
{
// Determine the line type
int cur_pos = 0;
int pos = line.find(" ", cur_pos);
int len;
line_type = line.substr(0, pos); // substring before space
line_rest = line.substr(pos+1, line.length()); // substring after space
cout << line_type << " ";
// 1) Vertex line
if(line_type == "v") // v x y z
{
float e[DIMENSION]; // x y z
for(int i=0; i<DIMENSION; i++)
{
pos = line_rest.find(" ", cur_pos); // index of the first " ", starting from cur_pos
len = pos - cur_pos; // length of the current element
e[i] = stof(line_rest.substr(cur_pos, len));
cur_pos = pos + 1; // Move on to the next element
cout << e[i] << " "; // debugging
}
raw_vertices.emplace_back(e[0], e[1], e[2]); // add the current vertex data to the collection
}
else if(line_type == "vn") // vn x y z
{
float e[DIMENSION]; // x y z
for(int i=0; i<DIMENSION; i++)
{
pos = line_rest.find(" ", cur_pos); // index of the first " ", starting from cur_pos
len = pos - cur_pos; // length of the current element
e[i] = stof(line_rest.substr(cur_pos, len));
cur_pos = pos + 1; // Move on to the next element
cout << e[i] << " "; // debugging
}
raw_normals.emplace_back(e[0], e[1], e[2]); // add the current vertex data to the collection
}
else if(line_type == "f") // f v1 v2 v3 vn1 vn2 vn3
{
bool has_only_vertices = false;
int v_e[MAX_POLYGON];
int n_e[MAX_POLYGON];
// Vertex indices
for(int i=0; i<MAX_POLYGON && pos >= 0; i++)
{
// v
pos = line_rest.find("/", cur_pos); // index of the first " ", starting from cur_pos
if(pos == string::npos)
{
has_only_vertices = true;
pos = line_rest.find(" ", cur_pos); // If there is no delimeter "/" found
}
len = pos - cur_pos; // length of the current element
v_e[i] = stoi(line_rest.substr(cur_pos, len));
cur_pos = pos + 1; // Move on to the next element
if(has_only_vertices)
{
cout << v_e[i] << " "; // debugging
continue;
}
// vt
pos = line_rest.find("/", cur_pos);
cur_pos = pos + 1;
// vn
pos = line_rest.find(" ", cur_pos); // index of the first " ", starting from cur_pos
len = pos - cur_pos; // length of the current element
n_e[i] = stoi(line_rest.substr(cur_pos, len));
cur_pos = pos + 1;
cout << v_e[i] << "//" << n_e[i] << " "; // debugging
}
v_elements.push_back(v_e[0] - 1);
v_elements.push_back(v_e[1] - 1);
v_elements.push_back(v_e[2] - 1);
n_elements.push_back(n_e[0] - 1);
n_elements.push_back(n_e[1] - 1);
n_elements.push_back(n_e[2] - 1);
}
cout << endl;
}
// Close the file
file.close();
cow.obj 파일을 라인별로 읽은 결과를 텍스트 파일로 출력해 보았다. 모든 라인들을 제대로 파싱하여 처리하였음을 확인할 수 있다.
3. 날것의 데이터를 렌더링에 사용할 수 있는 데이터로 가공한다. 이전 단계에서는 단순히 파일의 모든 라인들에 있는 데이터를 추출하여 한데 모아 저장(vertex는 vertex끼리, normal은 normal끼리, face는 face끼리 분류하여 저장)하였다면, 이제는 해당 데이터를 바탕으로 mesh를 구성하는 요소인 정점(vertex)과 면(face)의 벡터를 각각 생성해준다.
// Refine the raw data
// the numbers of mesh vertices and mesh normals
int v_num = raw_vertices.size();
int n_num = raw_normals.size();
int max_num = std::max(v_num, n_num);
// the total numbers of vertices and normals that make up mesh faces
int f_v_num = v_elements.size();
int f_n_num = n_elements.size();
// 1) Vertex Data
// Initialize vertex vector
vertices.clear();
vertices.reserve(raw_vertices.size()) // Resize the vector
// Generate the vector
for(int i = 0; i < max_num; i++)
{
Vec3 cur_v;
Vec3 cur_n;
// Check index range before data transfer
if (i < v_num) cur_v = raw_vertices[i];
if (i < n_num) cur_n = raw_normals[i];
vertices.emplace_back() // Add new vertex element into the vector
}
// 2) Face Data
// Initialize face vector
faces.clear();
faces.reserve(max_num/MAX_POLYGON); // Resize the vector (3 vertices compose one face(triangle))
// Generate the vector
for (int i = 0; i < max_num; i += MAX_POLYGON)
{
int cur_v[MAX_POLYGON] = {0, 0, 0};
int cur_n[MAX_POLYGON] = {-1, -1, -1};
// Check index range before data transfer
if (i+2 < f_v_num) // three vertices of the triangle
{
cur_v[0] = v_elements[i];
cur_v[1] = v_elements[i+1];
cur_v[2] = v_elements[i+2];
}
if (i+2 < f_n_num) // three normals at the vertices
{
cur_n[0] = n_elements[i];
cur_n[1] = n_elements[i+1];
cur_n[2] = n_elements[i+2];
}
faces.emplace_back(cur_v, cur_n); // Add new face element into the vector
}
'연구실 인턴 > Ray Tracing' 카테고리의 다른 글
Bounding Volume Hierarchy (BVH)로 렌더링 가속화하기 (0) | 2023.02.13 |
---|---|
폴리곤 메시 렌더링하기 (Rendering Polygon Meshes)_2 (0) | 2023.02.01 |