MTL loading, Lots of changes

This commit is contained in:
MrLetsplay 2022-10-09 23:12:37 +02:00
parent 1676867489
commit fbb7980ce7
19 changed files with 423 additions and 27 deletions

4
DEPENDENCIES.md Normal file
View File

@ -0,0 +1,4 @@
# Dependencies
- OpenGL, GLEW, GLFW
- stb_image, stb_image_write
- microtar (included)

View File

@ -6,6 +6,12 @@
#include <GL/glew.h> #include <GL/glew.h>
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
#include <glm/gtc/type_ptr.hpp> #include <glm/gtc/type_ptr.hpp>
#include <chrono>
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb/stb_image.h>
#include <stb/stb_image_write.h>
#include "internal.h" #include "internal.h"
#include "errordialog.h" #include "errordialog.h"
@ -145,6 +151,9 @@ int init() {
glfwSetInputMode(kekData.window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); glfwSetInputMode(kekData.window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
glClearColor(0.1f, 0.3f, 0.1f, 0.0f); glClearColor(0.1f, 0.3f, 0.1f, 0.0f);
//glfwSwapInterval(0);
stbi_set_flip_vertically_on_load(true);
kekData.activeCamera = new Camera(); kekData.activeCamera = new Camera();
kekData.shader = new Shader("shader/mesh/vertex.glsl", "shader/mesh/fragment.glsl"); kekData.shader = new Shader("shader/mesh/vertex.glsl", "shader/mesh/fragment.glsl");
@ -154,6 +163,8 @@ int init() {
int start() { int start() {
while(!glfwWindowShouldClose(kekData.window)) { while(!glfwWindowShouldClose(kekData.window)) {
auto start = std::chrono::high_resolution_clock::now();
// Clear the screen // Clear the screen
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glViewport(0, 0, kekData.screenWidth, kekData.screenHeight); glViewport(0, 0, kekData.screenWidth, kekData.screenHeight);
@ -204,14 +215,17 @@ int start() {
glUniformMatrix4fv(glGetUniformLocation(kekData.shader->id, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(kekData.shader->id, "view"), 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(glGetUniformLocation(kekData.shader->id, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); glUniformMatrix4fv(glGetUniformLocation(kekData.shader->id, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
glUniform3fv(glGetUniformLocation(kekData.shader->id, "cameraPos"), 1, glm::value_ptr(position)); glUniform3fv(glGetUniformLocation(kekData.shader->id, "cameraPos"), 1, glm::value_ptr(position));
glm::mat4 model = glm::mat4(1.0f);
glUniformMatrix4fv(glGetUniformLocation(kekData.shader->id, "model"), 1, GL_FALSE, glm::value_ptr(model));
if(kekData.activeScene) kekData.activeScene->draw(kekData.shader); if(kekData.activeScene) kekData.activeScene->draw(kekData.shader);
// Swap buffers and poll window events // Swap buffers and poll window events
glfwSwapBuffers(kekData.window); glfwSwapBuffers(kekData.window);
glfwPollEvents(); glfwPollEvents();
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<float> secsTaken = end - start;
//std::cout << "FT: " << secsTaken.count() * 1000 << std::endl;
//std::cout << "FR: " << (1.0f / secsTaken.count()) << std::endl;
} }
return KEK_SUCCESS; return KEK_SUCCESS;

View File

@ -1,5 +1,10 @@
#include "gameobject.h" #include "gameobject.h"
#include <GL/glew.h>
#include <glm/gtc/type_ptr.hpp>
#include "internal.h"
namespace kek { namespace kek {
GameObject::GameObject() { GameObject::GameObject() {
@ -17,6 +22,10 @@ void GameObject::addMesh(Mesh *mesh) {
} }
void GameObject::draw(Shader *shader) { void GameObject::draw(Shader *shader) {
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, position) * glm::mat4_cast(rotation);
glUniformMatrix4fv(glGetUniformLocation(kekData.shader->id, "model"), 1, GL_FALSE, glm::value_ptr(model));
for(Mesh *mesh : meshes) { for(Mesh *mesh : meshes) {
mesh->draw(shader); mesh->draw(shader);
} }

View File

@ -12,9 +12,17 @@ Vertex::Vertex(glm::vec3 pos, glm::vec3 normal, glm::vec2 texCoords) {
this->texCoords = texCoords; this->texCoords = texCoords;
} }
Mesh::Mesh(std::vector<Vertex> vertices, std::vector<uint32_t> indices) { Material::Material(std::shared_ptr<Texture> ambient, std::shared_ptr<Texture> diffuse, std::shared_ptr<Texture> specular, float shininess) {
this->ambient = ambient;
this->diffuse = diffuse;
this->specular = specular;
this->shininess = shininess;
}
Mesh::Mesh(std::vector<Vertex> vertices, std::vector<uint32_t> indices, Material *material) {
this->vertices = vertices; this->vertices = vertices;
this->indices = indices; this->indices = indices;
this->material = material;
glGenVertexArrays(1, &vao); glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo); glGenBuffers(1, &vbo);
@ -52,6 +60,7 @@ Mesh::~Mesh() {
glDeleteBuffers(1, &ebo); glDeleteBuffers(1, &ebo);
glDeleteBuffers(1, &vbo); glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao); glDeleteVertexArrays(1, &vao);
delete material;
} }
void Mesh::draw(Shader *shader) { void Mesh::draw(Shader *shader) {
@ -59,14 +68,19 @@ void Mesh::draw(Shader *shader) {
glBindBuffer(GL_ARRAY_BUFFER, vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
/*if(!light) { // TODO: check if props exist GLint ambient = glGetUniformLocation(shader->id, "material.ambient");
material->diffuse->use(GL_TEXTURE0); // FIXME: is -1
glUniform1i(glGetUniformLocation(shader->id, "material.diffuse"), 0); if(ambient != -1) { // Material can be passed to shader
material->ambient->use(GL_TEXTURE0);
material->specular->use(GL_TEXTURE1); glUniform1i(ambient, 0);
glUniform1i(glGetUniformLocation(shader->id, "material.specular"), 1); material->diffuse->use(GL_TEXTURE1);
glUniform1i(glGetUniformLocation(shader->id, "material.diffuse"), 1);
material->specular->use(GL_TEXTURE2);
glUniform1i(glGetUniformLocation(shader->id, "material.specular"), 2);
glUniform1f(glGetUniformLocation(shader->id, "material.shininess"), material->shininess); glUniform1f(glGetUniformLocation(shader->id, "material.shininess"), material->shininess);
}*/ }else {
std::cerr << "oof" << std::endl;
}
// Bind vertices + texture coordinates // Bind vertices + texture coordinates
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0); glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);

View File

@ -6,6 +6,7 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include "types.h" #include "types.h"
#include "resource.h"
namespace kek::ObjParser { namespace kek::ObjParser {
@ -24,7 +25,9 @@ static bool readFaceVertex(std::istringstream &stream, uint32_t *outVertex, uint
return true; return true;
} }
Mesh *parse(MemoryBuffer *buf) { Mesh *parseMesh(MemoryBuffer *buf, std::string folderPath) {
std::map<std::string, Material *> materials;
std::vector<glm::vec3> vertexPositions; std::vector<glm::vec3> vertexPositions;
std::vector<glm::vec3> vertexNormals; std::vector<glm::vec3> vertexNormals;
std::vector<glm::vec2> vertexTexCoords; std::vector<glm::vec2> vertexTexCoords;
@ -32,6 +35,8 @@ Mesh *parse(MemoryBuffer *buf) {
std::vector<Vertex> vertices; std::vector<Vertex> vertices;
std::vector<uint32_t> indices; std::vector<uint32_t> indices;
Material *material = nullptr;
std::istream stream(buf); std::istream stream(buf);
std::string line; std::string line;
@ -68,12 +73,111 @@ Mesh *parse(MemoryBuffer *buf) {
indices.push_back(indices.size()); indices.push_back(indices.size());
vertices.push_back(Vertex(vertexPositions[v3 - 1], vertexNormals[v3N - 1], vertexTexCoords[v3T - 1])); vertices.push_back(Vertex(vertexPositions[v3 - 1], vertexNormals[v3N - 1], vertexTexCoords[v3T - 1]));
indices.push_back(indices.size()); indices.push_back(indices.size());
}else if(cmd == "mtllib") {
std::string name;
str >> name;
std::string mtllibPath = folderPath + name;
std::map<std::string, Material *> mats = loadMaterials(mtllibPath);
materials.insert(mats.begin(), mats.end());
}else if(cmd == "usemtl") {
std::string mtlName;
str >> mtlName;
auto it = materials.find(mtlName);
if(it == materials.end()) {
std::cerr << "Invalid material specified: " << mtlName << std::endl;
throw std::exception();
}
material = it->second;
}else { }else {
std::cout << "Ignoring unknown OBJ command: " << cmd << std::endl; std::cout << "Ignoring unknown/unsupported OBJ command: " << cmd << std::endl;
} }
} }
return new Mesh(vertices, indices); for(auto it = materials.begin(); it != materials.end(); it++) {
if(it->second != material) delete it->second;
}
if(!material) {
std::shared_ptr<Texture> colorTex = Texture::generateColor(glm::vec3(1.0f, 0.0f, 0.0f));
material = new Material(colorTex, colorTex, colorTex, 1.0f);
}
return new Mesh(vertices, indices, material);
}
Mesh *loadMesh(std::string path) {
std::size_t n = path.find('/');
std::string folderPath = n == std::string::npos ? "" : path.substr(n + 1);
MemoryBuffer *buf = Resource::loadResource(path);
Mesh *mesh = parseMesh(buf, folderPath);
delete buf;
return mesh;
}
std::map<std::string, Material *> parseMaterials(MemoryBuffer *buf, std::string folderPath) {
std::map<std::string, Material *> mats;
std::string name;
std::shared_ptr<Texture> ambient;
std::shared_ptr<Texture> diffuse;
std::shared_ptr<Texture> specular;
float shininess = 0.0f;
std::istream stream(buf);
std::string line;
while(std::getline(stream, line)) {
std::istringstream str(line);
std::string cmd;
str >> cmd;
if(cmd == "#") continue;
if(cmd == "newmtl") {
if(name.length() > 0) {
Material *mat = new Material(ambient, diffuse, specular, shininess);
mats.emplace(name, mat);
}
str >> name;
}else if(cmd == "Ka") {
float r, g, b;
str >> r >> g >> b;
ambient = Texture::generateColor(glm::vec3(r, g, b));
}else if(cmd == "Kd") {
float r, g, b;
str >> r >> g >> b;
diffuse = Texture::generateColor(glm::vec3(r, g, b));
}else if(cmd == "Ks") {
float r, g, b;
str >> r >> g >> b;
specular = Texture::generateColor(glm::vec3(r, g, b));
}else if(cmd == "Ns") {
float s;
str >> s;
shininess = s;
}else {
std::cout << "Ignoring unknown/unsupported MTL command: " << cmd << std::endl;
}
}
if(name.length() > 0) {
Material *mat = new Material(ambient, diffuse, specular, shininess);
mats.emplace(name, mat);
}
return mats;
}
std::map<std::string, Material *> loadMaterials(std::string path) {
std::size_t n = path.find('/');
std::string folderPath = n == std::string::npos ? "" : path.substr(n + 1);
MemoryBuffer *buf = Resource::loadResource(path);
std::map<std::string, Material *> mats = parseMaterials(buf, folderPath);
delete buf;
return mats;
} }
} }

View File

@ -32,6 +32,7 @@ static GLuint compileShader(GLenum type, std::string path) {
} }
std::string code = stream.str(); std::string code = stream.str();
std::cout << code << std::endl;
delete buf; delete buf;
const char *src = code.c_str(); const char *src = code.c_str();

View File

@ -0,0 +1,83 @@
#include "texture.h"
#include <stb/stb_image.h>
#include <stb/stb_image_write.h>
#include "resource.h"
#include "internal.h"
namespace kek {
static unsigned int allocTexture(unsigned char *data, int width, int height, float borderColor[4]) {
GLuint id;
glGenTextures(1, &id);
glBindTexture(GL_TEXTURE_2D, id);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
//glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
return id;
}
Texture::Texture(std::string texturePath) {
this->path = texturePath;
int width, height, nrChannels;
MemoryBuffer *buf = Resource::loadResource(texturePath);
if(!buf) {
std::cout << "Failed to load texture: " << texturePath << std::endl;
throw std::exception();
}
unsigned char *data = stbi_load_from_memory((stbi_uc *) buf->buffer, buf->length, &width, &height, &nrChannels, 0);
delete buf;
float borderColor[] = { 1.0f, 0.0f, 1.0f, 1.0f };
allocTexture(data, width, height, borderColor);
stbi_image_free(data);
}
Texture::Texture(GLuint id) {
this->id = id;
}
Texture::~Texture() {
glDeleteTextures(1, &id);
}
void Texture::use(GLenum texture) {
glActiveTexture(texture);
glBindTexture(GL_TEXTURE_2D, id);
}
std::shared_ptr<Texture> Texture::load(std::string texturePath) {
std::shared_ptr<Texture> ret;
auto iter = kekData.loadedTextures.find(texturePath);
if(iter != kekData.loadedTextures.end()) ret = iter->second.lock();
if(!ret) {
ret = std::make_shared<Texture>(texturePath);
kekData.loadedTextures.emplace(texturePath, ret);
}
return ret;
}
std::shared_ptr<Texture> Texture::generateColor(glm::vec3 color) {
unsigned char *data = (unsigned char *) malloc(3 * sizeof(unsigned char));
data[0] = (unsigned char) (color.x * 255);
data[1] = (unsigned char) (color.y * 255);
data[2] = (unsigned char) (color.z * 255);
float borderColor[4] = { color.x, color.y, color.z, 1.0f };
GLuint tex = allocTexture(data, 1, 1, borderColor);
free(data);
return std::make_shared<Texture>(tex);
}
}

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <GL/glew.h>
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
#include "utils.h" #include "utils.h"

View File

@ -1,10 +1,13 @@
#pragma once #pragma once
#include <map>
#include <memory>
#include <vector>
#include "input.h" #include "input.h"
#include "camera.h" #include "camera.h"
#include "scene.h" #include "scene.h"
#include "texture.h"
#include <map>
namespace kek { namespace kek {
@ -21,6 +24,8 @@ struct KekData {
int screenWidth; int screenWidth;
int screenHeight; int screenHeight;
std::map<std::string, std::weak_ptr<Texture>> loadedTextures;
}; };
extern KekData kekData; extern KekData kekData;

View File

@ -1,11 +1,13 @@
#pragma once #pragma once
#include "shader.h"
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <vector> #include <vector>
#include <cstdint> #include <cstdint>
#include <iostream> #include <iostream>
#include <memory>
#include "shader.h"
#include "texture.h"
namespace kek { namespace kek {
@ -18,15 +20,28 @@ struct Vertex {
}; };
struct Material {
public:
std::shared_ptr<Texture> ambient;
std::shared_ptr<Texture> diffuse;
std::shared_ptr<Texture> specular;
float shininess;
Material(std::shared_ptr<Texture> ambient, std::shared_ptr<Texture> diffuse, std::shared_ptr<Texture> specular, float shininess);
};
class Mesh { class Mesh {
public: public:
std::vector<Vertex> vertices; std::vector<Vertex> vertices;
std::vector<uint32_t> indices; std::vector<uint32_t> indices;
Material *material;
unsigned int vao, vbo, ebo; unsigned int vao, vbo, ebo;
Mesh(std::vector<Vertex> vertices, std::vector<uint32_t> indices); Mesh(std::vector<Vertex> vertices, std::vector<uint32_t> indices, Material *material);
~Mesh(); ~Mesh();

View File

@ -1,12 +1,18 @@
#pragma once #pragma once
#include <vector> #include <map>
#include "types.h" #include "types.h"
#include "mesh.h" #include "mesh.h"
namespace kek::ObjParser { namespace kek::ObjParser {
Mesh *parse(MemoryBuffer *buf); Mesh *parseMesh(MemoryBuffer *buf, std::string folderPath);
Mesh *loadMesh(std::string path);
std::map<std::string, Material *> parseMaterials(MemoryBuffer *buf, std::string folderPath);
std::map<std::string, Material *> loadMaterials(std::string path);
} }

View File

@ -0,0 +1,33 @@
#pragma once
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <vector>
#include <string>
#include <memory>
namespace kek {
class Texture {
public:
GLuint id;
std::string path;
Texture(std::string texturePath);
Texture(GLuint id);
~Texture();
void use(GLenum texture);
static std::shared_ptr<Texture> load(std::string texturePath);
static std::shared_ptr<Texture> generateColor(glm::vec3 color);
};
}

View File

@ -0,0 +1,25 @@
vec3 calcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir, vec2 texCoord, float shadow) {
vec3 lightDir = normalize(light.position - fragPos);
vec3 halfwayDir = normalize(lightDir + viewDir);
// Ambient
vec3 ambient = light.ambient * vec3(texture(material.diffuse, texCoord));
// Diffuse
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, texCoord));
// Specular
float spec = pow(max(dot(normal, halfwayDir), 0.0), material.shininess);
vec3 specular = light.specular * spec * vec3(texture(material.specular, texCoord));
// Attenuation
float dist = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * dist + light.quadratic * (dist * dist));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return ambient + (1.0 - shadow) * (diffuse + specular);
}

View File

@ -1,4 +1,5 @@
struct Material { struct Material {
sampler2D ambient;
sampler2D diffuse; sampler2D diffuse;
sampler2D specular; sampler2D specular;
float shininess; float shininess;

View File

@ -1,19 +1,28 @@
#version 330 core #version 330 core
!include types.glsl
in VS_OUT { in VS_OUT {
vec3 fragmentPosition; vec3 fragmentPosition;
vec2 textureCoordinate; vec2 textureCoordinate;
vec3 normal; vec3 normal;
} fs_in; } fs_in;
!include types.glsl
uniform Material material;
uniform vec3 cameraPos; uniform vec3 cameraPos;
out vec4 color; out vec4 color;
!include lightcalc.glsl
void main() { void main() {
//vec3 norm = normalize(fs_in.normal); vec3 norm = normalize(fs_in.normal);
//vec3 viewDir = normalize(cameraPos - fs_in.fragmentPosition); vec3 viewDir = normalize(cameraPos - fs_in.fragmentPosition);
color = length(fs_in.fragmentPosition) / 2 * vec4(1.0); //color = length(fs_in.fragmentPosition) / 2 * vec4(1.0);
//color = vec4(norm, 1.0);
color = texture(material.diffuse, fs_in.textureCoordinate);
//PointLight light = {};
} }

View File

@ -10,7 +10,7 @@ int main(int argc, char **argv) {
Engine::init(); Engine::init();
MemoryBuffer *buf = Resource::loadResource("object/sphere/Sphere.obj"); MemoryBuffer *buf = Resource::loadResource("object/sphere/Sphere.obj");
Mesh *mesh = ObjParser::parse(buf); Mesh *mesh = ObjParser::parseMesh(buf, "object/sphere/");
delete buf; delete buf;
GameObject *test = new GameObject(); GameObject *test = new GameObject();
@ -18,6 +18,20 @@ int main(int argc, char **argv) {
Scene *scene = new Scene(); Scene *scene = new Scene();
scene->addObject(test); scene->addObject(test);
MemoryBuffer *buf2 = Resource::loadResource("object/cube_colored/Cube.obj");
Mesh *mesh2 = ObjParser::parseMesh(buf2, "object/cube_colored/");
delete buf2;
for(int i = 0; i < 1; i++) {
GameObject *test2 = new GameObject();
test2->addMesh(new Mesh(mesh2->vertices, mesh2->indices, mesh2->material));
test2->moveTo(glm::vec3(0.0f, 5.0f, 3 * i));
scene->addObject(test2);
}
delete mesh2;
Engine::setActiveScene(scene); Engine::setActiveScene(scene);
Engine::start(); Engine::start();

View File

@ -0,0 +1,12 @@
# Blender 3.3.0 MTL File: 'None'
# www.blender.org
newmtl Material
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.071425 0.000000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2

View File

@ -0,0 +1,46 @@
# Blender 3.3.0
# www.blender.org
mtllib Cube.mtl
o Cube
v 1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 1.000000 -1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 1.000000
vn -0.0000 1.0000 -0.0000
vn -0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn -0.0000 -1.0000 -0.0000
vn 1.0000 -0.0000 -0.0000
vn -0.0000 -0.0000 -1.0000
vt 0.625000 0.500000
vt 0.375000 0.500000
vt 0.625000 0.750000
vt 0.375000 0.750000
vt 0.875000 0.500000
vt 0.625000 0.250000
vt 0.125000 0.500000
vt 0.375000 0.250000
vt 0.875000 0.750000
vt 0.625000 1.000000
vt 0.625000 0.000000
vt 0.375000 0.000000
vt 0.375000 1.000000
vt 0.125000 0.750000
s 0
usemtl Material
f 5/5/1 3/3/1 1/1/1
f 3/3/2 8/13/2 4/4/2
f 7/11/3 6/8/3 8/12/3
f 2/2/4 8/14/4 6/7/4
f 1/1/5 4/4/5 2/2/5
f 5/6/6 2/2/6 6/8/6
f 5/5/1 7/9/1 3/3/1
f 3/3/2 7/10/2 8/13/2
f 7/11/3 5/6/3 6/8/3
f 2/2/4 4/4/4 8/14/4
f 1/1/5 3/3/5 4/4/5
f 5/6/6 1/1/6 2/2/6