// 3D objects
//
// Copyright (C) 2003-2019 Sam Varner
//
// This file is part of Vamos Automotive Simulator.
//
// Vamos is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Vamos is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Vamos. If not, see <http://www.gnu.org/licenses/>.
#include "Ac3d.hpp"
#include "../geometry/Constants.hpp"
#include "../geometry/Three_Matrix.hpp"
#include "../geometry/Three_Vector.hpp"
#include "../geometry/Two_Vector.hpp"
#include "Texture_Image.hpp"
#include <algorithm>
#include <cassert>
#include <cstdlib>
#include <fstream>
#include <functional>
#include <sstream>
using namespace Vamos_Geometry;
using namespace Vamos_Media;
using V_Vertex = std::vector<Vertex>;
namespace
{
/// Convert the AC3D version in the file's header to an integer.
int get_version_number(char ver)
{
if (!isxdigit(ver))
{
std::ostringstream message;
message << "The version number " << ver << "is not a hexadecimal character.";
throw Malformed_Ac3d_File(message.str());
}
int n;
std::stringstream ss;
ss << ver;
ss >> std::hex >> n;
return n;
}
bool read_header(std::istream& is)
{
std::string header;
is >> header;
return header.size() >= 5
&& header.substr(0, 4) == "AC3D"
&& get_version_number(header[4]) > 0;
}
/// Read as element and return it as a string. Quotes are stripped if present.
std::string get_quoted(std::istream& is)
{
std::string word;
is >> word;
if (word.front() != '\"')
return word; // Single word, not quoted.
if (word.back() == '\"')
return word.substr(1, word.size() - 1); // Single word, quoted.
std::string rest;
std::getline(is, rest, '\"');
return word.substr(1) + rest;
}
void read_material_parameters(std::istream& is, std::string label, GLfloat* value, size_t length)
{
std::string actual_label;
is >> actual_label;
if (actual_label != label)
throw Malformed_Ac3d_File("Expected \"" + label + "\".");
for (size_t i = 0; i < length; ++i)
is >> value[i];
}
Three_Matrix read_matrix(std::istream& is)
{
Three_Matrix mat;
for (size_t i = 0; i < 3; i++)
for (size_t j = 0; j < 3; j++)
is >> mat[i][j];
return mat;
}
Three_Vector read_vector(std::istream& is)
{
Three_Vector vec;
is >> vec.x >> vec.y >> vec.z;
return vec;
}
std::unique_ptr<char[]> read_data(std::istream& is)
{
size_t length;
is >> length;
auto buffer = std::make_unique<char[]>(length + 1);
// Throw out the newline after the data length.
is.get();
is.get(buffer.get(), length);
buffer[length] = '\0';
return buffer;
}
Ac3d_Surface* read_surface(std::istream& is, double scale,
const Three_Vector& location, const Three_Matrix& rotation,
const V_Vertex& vertices, const Ac3d::V_Material& materials)
{
std::string label;
is >> label;
if (label != "SURF")
throw Malformed_Ac3d_File("Expected A SURF section.");
std::string surf_type;
is >> surf_type;
auto* surf = new Ac3d_Surface(surf_type, scale, location, rotation);
int mat = -1;
is >> label;
if (label == "mat")
{
is >> mat >> label;
surf->set_material(materials[mat].get());
}
if (label == "refs")
{
size_t num;
is >> num;
if (num == 3)
surf->set_figure_type(TRIANGLE);
else if (num == 4)
surf->set_figure_type(QUADRILATERAL);
std::vector<Vertex> verts;
for (size_t i = 0; i < num; i++)
{
size_t ref;
is >> ref;
verts.push_back(vertices[ref]);
is >> verts.back().texture_x >> verts.back().texture_y;
}
surf->add_vertices(verts);
}
else
throw Malformed_Ac3d_File("Expected a mat or refs section.");
if (mat == -1)
throw Malformed_Ac3d_File("Expected a mat section.");
return surf;
}
Ac3d_Object* read_object(std::string file, std::istream& is, double scale,
const Three_Vector& translation, const Three_Vector& rotation,
const Ac3d::V_Material& materials)
{
std::string type;
is >> type; // ignored
auto* obj = new Ac3d_Object(translation, rotation);
auto vertices = V_Vertex();
std::string label;
while (is >> label)
{
if (label == "name")
get_quoted(is); // ignored
else if (label == "data")
read_data(is); // ignored
else if (label == "texture")
{
std::string dir = file.substr(0, file.find_last_of("/") + 1);
std::string file = get_quoted(is);
obj->set_texture_image(dir + file);
}
else if (label == "texrep")
{
double x, y;
is >> x >> y; // ignored
}
else if (label == "rot")
obj->set_rotation(read_matrix(is));
else if (label == "loc")
obj->set_location(read_vector(is));
else if (label == "url")
get_quoted(is); // ignored
else if (label == "numvert")
{
size_t num;
is >> num;
for (size_t i = 0; i < num; i++)
vertices.emplace_back(read_vector(is));
}
else if (label == "numsurf")
{
size_t num;
is >> num;
for (size_t i = 0; i < num; i++)
obj->add_surface(read_surface(is, scale, obj->get_location(), obj->get_rotation(),
vertices, materials));
}
else if (label == "kids")
{
size_t n_kids;
is >> n_kids;
for (size_t i = 0; i < n_kids; i++)
{
is >> label;
if (label != "OBJECT")
throw Malformed_Ac3d_File("An OBJECT line must follow a kids line.");
obj->add_kid(read_object(file, is, scale, translation, rotation, materials));
}
break;
}
else
{
std::cerr << "Ac3d::read_object(): Unrecognized OBJECT data: " << label << std::endl;
continue;
}
}
return obj;
}
Ac3d_Material* read_material(std::istream& is)
{
get_quoted(is); // Ignore the name.
auto mat = new Ac3d_Material{0};
read_material_parameters(is, "rgb", mat->color.data(), 3);
read_material_parameters(is, "amb", mat->ambient.data(), 3);
read_material_parameters(is, "emis", mat->emission.data(), 3);
read_material_parameters(is, "spec", mat->specular.data(), 3);
read_material_parameters(is, "shi", &(mat->shininess), 1);
read_material_parameters(is, "trans", &(mat->transparency), 1);
return mat;
}
void set_gl_properties(GLenum face, const Ac3d_Material& mat)
{
glColor4f(mat.color[0], mat.color[1], mat.color[2], 1.0 - mat.transparency);
glMaterialfv(face, GL_AMBIENT, mat.ambient.data());
glMaterialfv(face, GL_EMISSION, mat.emission.data());
glMaterialfv(face, GL_SPECULAR, mat.specular.data());
glMaterialfv(face, GL_SHININESS, &mat.shininess);
}
} // namespace
//----------------------------------------------------------------------------------------
Ac3d_Surface::Ac3d_Surface(std::string figure_type_code, double scale, const Three_Vector& offset,
const Three_Matrix& rotation)
: m_scale(scale),
m_offset(offset),
m_rotation(rotation)
{
std::istringstream is(figure_type_code);
is.setf(std::ios_base::hex, std::ios_base::basefield);
int code;
is >> code;
m_figure_type = Figure_Type(code & 0x0f);
if (m_figure_type != POLYGON && m_figure_type != CLOSED_LINE && m_figure_type != LINE)
throw Malformed_Ac3d_File("Unrecognized figure type");
m_shaded = code & 0x10;
m_two_sided = code & 0x20;
}
void Ac3d_Surface::build() const
{
if (m_vertices.empty())
return;
glPushAttrib(GL_ENABLE_BIT);
{
if (m_two_sided)
glDisable(GL_CULL_FACE);
else
glEnable(GL_CULL_FACE);
glBegin(get_gl_figure_type());
{
set_material_properties();
draw_figure();
}
glEnd();
}
glPopAttrib();
}
void Ac3d_Surface::rearrange_vertices(const std::vector<size_t>& index)
{
V_Vertex old = m_vertices;
for (size_t i = 0; i < index.size(); ++i)
m_vertices[i] = old[index[i]];
}
void Ac3d_Surface::add_vertex(const Vertex& vert)
{
m_vertices.push_back(vert);
}
void Ac3d_Surface::add_vertices(const std::vector<Vertex>& verts)
{
Three_Vector normal = Three_Vector::Z;
if (verts.size() >= 3)
{
const auto& p0 = verts.front().coordinates;
const auto& p1 = verts[1].coordinates;
const auto& p2 = verts.back().coordinates;
auto r1 = p1 - p0;
auto r2 = p2 - p0;
normal = (r1.cross(r2)).unit();
}
for (const auto& v : verts)
{
m_vertices.push_back(v);
m_vertices.back().normal = normal;
}
}
GLenum Ac3d_Surface::get_gl_figure_type() const
{
const size_t n_vertices = m_vertices.size();
switch (m_figure_type)
{
case LINE:
return GL_LINE_STRIP;
case CLOSED_LINE:
return GL_LINE_LOOP;
case TRIANGLE:
assert(n_vertices == 3);
return GL_TRIANGLES;
case TRIANGLE_STRIP:
assert(n_vertices > 3);
return GL_TRIANGLE_STRIP;
case TRIANGLE_FAN:
assert(n_vertices > 3);
return GL_TRIANGLE_FAN;
case QUADRILATERAL:
assert(n_vertices == 4);
return GL_QUADS;
case QUADRILATERAL_STRIP:
assert(n_vertices >= 4);
assert(n_vertices % 2 == 0);
return GL_QUAD_STRIP;
case POLYGON:
assert(n_vertices > 4);
return GL_POLYGON;
default:
throw Malformed_Ac3d_File("Unrecognized figure type");
}
}
void Ac3d_Surface::set_material_properties() const
{
GLenum face = m_two_sided ? GL_FRONT_AND_BACK : GL_FRONT;
glColorMaterial(face, GL_AMBIENT_AND_DIFFUSE);
glEnable(GL_COLOR_MATERIAL);
assert(mp_material);
set_gl_properties(face, *mp_material);
}
void Ac3d_Surface::draw_figure() const
{
auto norm = m_rotation * m_vertices.front().normal;
for (const auto& vertex : m_vertices)
{
glTexCoord2f(vertex.texture_x, vertex.texture_y);
if (m_shaded)
norm = m_rotation * vertex.normal.unit();
glNormal3d(norm.x, norm.y, norm.z);
// Rotate and translate the vertex.
const auto& pos = m_offset + m_scale * m_rotation * vertex.coordinates;
glVertex3f(pos.x, pos.y, pos.z);
}
}
//----------------------------------------------------------------------------------------
void V_Surface::push_back(Ac3d_Surface* surface)
{
if (!surface->is_shaded() || !join_surface(surface))
m_surfaces.push_back(surface);
}
bool V_Surface::join_surface(const Ac3d_Surface* surface)
{
if (m_surfaces.empty())
return false;
const auto last = m_surfaces.back();
if (surface->get_material() != last->get_material())
return false;
if (surface->get_figure_type() != QUADRILATERAL
&& surface->get_figure_type() != TRIANGLE)
return false;
const auto& new_vertices = surface->get_vertices();
const auto& old_vertices = last->get_vertices();
const size_t n_old_vertices = old_vertices.size();
const auto new_type = surface->get_figure_type();
const auto old_type = last->get_figure_type();
if (new_type == QUADRILATERAL)
{
if (old_type == QUADRILATERAL)
{
for (size_t i1 = 0; i1 < n_old_vertices; i1++)
{
size_t i2 = (i1 + 1) % n_old_vertices;
if (join_quadrilateral_to_edge(i1, i2, old_vertices, new_vertices))
return true;
}
}
else if (old_type == QUADRILATERAL_STRIP)
return join_quadrilateral_to_edge(n_old_vertices - 1, n_old_vertices - 2, old_vertices,
new_vertices);
}
if (new_type == TRIANGLE)
{
if (old_type == TRIANGLE)
{
for (size_t i1 = 0; i1 < n_old_vertices; i1++)
{
size_t i2 = (i1 + 1) % n_old_vertices;
if (join_triangle_to_edge(i1, i2, old_vertices, new_vertices))
return true;
}
}
else if (old_type == TRIANGLE_STRIP)
return join_triangle_to_edge(n_old_vertices - 2, n_old_vertices - 1, old_vertices,
new_vertices);
else if (old_type == TRIANGLE_FAN)
return join_triangle_to_edge(0, n_old_vertices - 1, old_vertices, new_vertices);
}
return false;
}
bool V_Surface::join_quadrilateral_to_edge(size_t index1, size_t index2,
const V_Vertex& old_vertices,
const V_Vertex& new_vertices)
{
const auto n_new_vertices = new_vertices.size();
const auto& v1 = old_vertices[index1];
const auto& v2 = old_vertices[index2];
for (size_t j1 = 0; j1 < n_new_vertices; j1++)
{
auto j2 = (j1 + 1) % n_new_vertices;
// The common vertices are on opposite sides, so j1 connects with i2 and j2 with
// i1.
if (new_vertices[j1].coordinates == v2.coordinates
&& new_vertices[j2].coordinates == v1.coordinates)
return join_quadrilateral(new_vertices, index1, index2, j1, j2);
}
return false;
}
bool V_Surface::join_triangle_to_edge(size_t index1, size_t index2, const V_Vertex& old_vertices,
const V_Vertex& new_vertices)
{
const auto n_new_vertices = new_vertices.size();
const auto& v1 = old_vertices[index1];
const auto& v2 = old_vertices[index2];
auto vert_equal = [=](size_t i1, size_t i2) {
return new_vertices[i1].coordinates == v1.coordinates
&& new_vertices[i2].coordinates == v2.coordinates;
};
const auto old_type = m_surfaces.back()->get_figure_type();
for (size_t j1 = 0; j1 < n_new_vertices; j1++)
{
auto j2 = (j1 + 1) % n_new_vertices;
bool even = old_vertices.size() % 2 == 0;
// For a triangle strip with odd vertices, the common vertices are on opposite
// sides, so j1 connects with i2 and j2 with i1.
bool match = (even && old_type == TRIANGLE_STRIP) || old_type == TRIANGLE_FAN
? vert_equal(j1, j2) : vert_equal(j2, j1);
match = vert_equal(j2, j1);
if (match)
{
auto new_type = (old_type == TRIANGLE && index2 == 0)
|| (old_type == TRIANGLE_FAN && index1 == 0)
? TRIANGLE_FAN : TRIANGLE_STRIP;
return join_triangle(new_vertices, j1, j2, new_type);
}
}
return false;
}
bool V_Surface::join_quadrilateral(const V_Vertex& new_vertices, size_t olddex1, size_t olddex2,
size_t newdex1, size_t newdex2)
{
auto n_new_vertices = new_vertices.size();
auto newdex3 = (newdex1 + 2) % n_new_vertices;
auto newdex4 = (newdex1 + 3) % n_new_vertices;
auto& last = m_surfaces.back();
if (last->get_figure_type() == QUADRILATERAL)
{
auto olddex3 = (olddex1 + 2) % n_new_vertices;
auto olddex4 = (olddex1 + 3) % n_new_vertices;
last->rearrange_vertices({olddex3, olddex4, olddex2, olddex1});
last->set_figure_type(QUADRILATERAL_STRIP);
m_last_vertex1 = newdex3;
m_last_vertex2 = newdex4;
}
else if (m_last_vertex1 != newdex3 || m_last_vertex2 != newdex4)
return false;
last->add_vertex(new_vertices[newdex4]);
last->add_vertex(new_vertices[newdex3]);
return true;
}
bool V_Surface::join_triangle(const V_Vertex& new_vertices, size_t newdex1, size_t newdex2,
Figure_Type new_type)
{
auto n_new_vertices = new_vertices.size();
auto newdex3 = (newdex1 + 2) % n_new_vertices;
auto& last = m_surfaces.back();
auto old_type = last->get_figure_type();
if (old_type == TRIANGLE)
{
last->set_figure_type(new_type);
last->add_vertex(new_vertices[newdex3]);
return true;
}
else if (new_type != old_type)
return false;
last->add_vertex(new_vertices[newdex3]);
return true;
}
void V_Surface::build() const
{
for (auto* surface : m_surfaces)
surface->build();
}
//----------------------------------------------------------------------------------------
Ac3d_Object::Ac3d_Object(const Three_Vector& translation, const Three_Vector& rotation)
: m_location(translation)
{
m_rotation.identity();
// Conversion from Blender to AC3D puts the z-axis in the
// y-direction. Compensate by...
// ...adding 90 to the x-rotation...
m_rotation.rotate(Three_Vector::X * (rotation.x + half_pi));
// ...using the z rotation for y...
m_rotation.rotate(Three_Vector::Y * rotation.z);
// ...using the -y rotation for z.
m_rotation.rotate(Three_Vector::Z * -rotation.y);
}
void Ac3d_Object::build() const
{
if (mp_texture)
{
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
mp_texture->activate();
}
else
glDisable(GL_TEXTURE_2D);
m_surfaces.build();
glDisable(GL_TEXTURE_2D);
for (const auto& kid : m_kids)
kid->build();
glEnable(GL_TEXTURE_2D);
}
void Ac3d_Object::set_texture_image(std::string file)
{
mp_texture.reset(new Texture_Image(file));
}
void Ac3d_Object::set_location(const Three_Vector& loc)
{
m_location = loc;
}
void Ac3d_Object::set_rotation(const Three_Matrix& rot)
{
m_rotation *= rot;
}
void Ac3d_Object::add_surface(Ac3d_Surface* surf)
{
m_surfaces.push_back(surf);
}
//----------------------------------------------------------------------------------------
Ac3d::Ac3d(std::string file, double scale, const Three_Vector& translation,
const Three_Vector& rotation)
{
std::ifstream is(file);
if (!is)
throw No_File(file);
if (!read_header(is))
throw Not_An_Ac3d_File(file + " does not have an AC3D header");
std::string label;
while (is >> label)
{
if (label == "MATERIAL")
m_materials.emplace_back(read_material(is));
else if (label == "OBJECT")
m_objects.emplace_back(
read_object(file, is, scale, translation, rotation, m_materials));
else if (label.front() == '#')
continue;
else
throw Malformed_Ac3d_File("Not part of an object definition");
}
}
GLuint Ac3d::build()
{
GLuint id = glGenLists(1);
glNewList(id, GL_COMPILE);
for (const auto& obj : m_objects)
obj->build();
glEndList();
return id;
}