1953 lines
54 KiB
C++
1953 lines
54 KiB
C++
// modelfile.cpp
|
|
//
|
|
// Copyright (C) 2004-2010, Celestia Development Team
|
|
// Original version by Chris Laurel <claurel@gmail.com>
|
|
//
|
|
// This program 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 2
|
|
// of the License, or (at your option) any later version.
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
|
|
#include <fmt/format.h>
|
|
#include <fmt/ostream.h>
|
|
|
|
#include <celutil/binaryread.h>
|
|
#include <celutil/binarywrite.h>
|
|
#include <celutil/logger.h>
|
|
#include <celutil/tokenizer.h>
|
|
#include "mesh.h"
|
|
#include "model.h"
|
|
#include "modelfile.h"
|
|
|
|
namespace celutil = celestia::util;
|
|
|
|
|
|
namespace cmod
|
|
{
|
|
namespace
|
|
{
|
|
constexpr std::size_t CEL_MODEL_HEADER_LENGTH = 16;
|
|
constexpr const char CEL_MODEL_HEADER_ASCII[] = "#celmodel__ascii";
|
|
constexpr const char CEL_MODEL_HEADER_BINARY[] = "#celmodel_binary";
|
|
|
|
// Material default values
|
|
constexpr Color DefaultDiffuse(0.0f, 0.0f, 0.0f);
|
|
constexpr Color DefaultSpecular(0.0f, 0.0f, 0.0f);
|
|
constexpr Color DefaultEmissive(0.0f, 0.0f, 0.0f);
|
|
constexpr float DefaultSpecularPower = 1.0f;
|
|
constexpr float DefaultOpacity = 1.0f;
|
|
constexpr BlendMode DefaultBlend = BlendMode::NormalBlend;
|
|
|
|
// Standard tokens for ASCII model loader
|
|
constexpr const char MeshToken[] = "mesh";
|
|
constexpr const char EndMeshToken[] = "end_mesh";
|
|
constexpr const char VertexDescToken[] = "vertexdesc";
|
|
constexpr const char EndVertexDescToken[] = "end_vertexdesc";
|
|
constexpr const char VerticesToken[] = "vertices";
|
|
constexpr const char MaterialToken[] = "material";
|
|
constexpr const char EndMaterialToken[] = "end_material";
|
|
|
|
// Binary file tokens
|
|
enum ModelFileToken
|
|
{
|
|
CMOD_Material = 1001,
|
|
CMOD_EndMaterial = 1002,
|
|
CMOD_Diffuse = 1003,
|
|
CMOD_Specular = 1004,
|
|
CMOD_SpecularPower = 1005,
|
|
CMOD_Opacity = 1006,
|
|
CMOD_Texture = 1007,
|
|
CMOD_Mesh = 1009,
|
|
CMOD_EndMesh = 1010,
|
|
CMOD_VertexDesc = 1011,
|
|
CMOD_EndVertexDesc = 1012,
|
|
CMOD_Vertices = 1013,
|
|
CMOD_Emissive = 1014,
|
|
CMOD_Blend = 1015,
|
|
};
|
|
|
|
enum ModelFileType
|
|
{
|
|
CMOD_Float1 = 1,
|
|
CMOD_Float2 = 2,
|
|
CMOD_Float3 = 3,
|
|
CMOD_Float4 = 4,
|
|
CMOD_String = 5,
|
|
CMOD_Uint32 = 6,
|
|
CMOD_Color = 7,
|
|
};
|
|
|
|
|
|
class ModelLoader
|
|
{
|
|
public:
|
|
explicit ModelLoader(HandleGetter& _handleGetter) : handleGetter(_handleGetter) {}
|
|
virtual ~ModelLoader() = default;
|
|
|
|
virtual std::unique_ptr<Model> load() = 0;
|
|
|
|
const std::string& getErrorMessage() const { return errorMessage; }
|
|
ResourceHandle getHandle(const fs::path& path) { return handleGetter(path); }
|
|
|
|
protected:
|
|
virtual void reportError(const std::string& msg) { errorMessage = msg; }
|
|
|
|
private:
|
|
std::string errorMessage{ };
|
|
HandleGetter& handleGetter;
|
|
};
|
|
|
|
|
|
class ModelWriter
|
|
{
|
|
public:
|
|
explicit ModelWriter(SourceGetter& _sourceGetter) : sourceGetter(_sourceGetter) {}
|
|
virtual ~ModelWriter() = default;
|
|
|
|
virtual bool write(const Model&) = 0;
|
|
|
|
fs::path getSource(ResourceHandle handle) { return sourceGetter(handle); }
|
|
|
|
private:
|
|
SourceGetter& sourceGetter;
|
|
};
|
|
|
|
|
|
/***** ASCII loader *****/
|
|
|
|
/*!
|
|
This is an approximate Backus Naur form for the contents of ASCII cmod
|
|
files. For brevity, the categories <unsigned_int> and <float> aren't
|
|
defined here--they have the obvious definitions.
|
|
\code
|
|
<modelfile> ::= <header> <model>
|
|
|
|
<header> ::= #celmodel__ascii
|
|
|
|
<model> ::= { <material_definition> } { <mesh_definition> }
|
|
|
|
<material_definition> ::= material
|
|
{ <material_attribute> }
|
|
end_material
|
|
|
|
<texture_semantic> ::= texture0 |
|
|
normalmap |
|
|
specularmap |
|
|
emissivemap
|
|
|
|
<texture> ::= <texture_semantic> <string>
|
|
|
|
<material_attribute> ::= diffuse <color> |
|
|
specular <color> |
|
|
emissive <color> |
|
|
specpower <float> |
|
|
opacity <float> |
|
|
blend <blendmode> |
|
|
<texture>
|
|
|
|
<color> ::= <float> <float> <float>
|
|
|
|
<string> ::= """ { letter } """
|
|
|
|
<blendmode> ::= normal | add | premultiplied
|
|
|
|
<mesh_definition> ::= mesh
|
|
<vertex_description>
|
|
<vertex_pool>
|
|
{ <prim_group> }
|
|
end_mesh
|
|
|
|
<vertex_description> ::= vertexdesc
|
|
{ <vertex_attribute> }
|
|
end_vertexdesc
|
|
|
|
<vertex_attribute> ::= <vertex_semantic> <vertex_format>
|
|
|
|
<vertex_semantic> ::= position | normal | color0 | color1 | tangent |
|
|
texcoord0 | texcoord1 | texcoord2 | texcoord3 |
|
|
pointsize
|
|
|
|
<vertex_format> ::= f1 | f2 | f3 | f4 | ub4
|
|
|
|
<vertex_pool> ::= vertices <count>
|
|
{ <float> }
|
|
|
|
<count> ::= <unsigned_int>
|
|
|
|
<prim_group> ::= <prim_group_type> <material_index> <count>
|
|
{ <unsigned_int> }
|
|
|
|
<prim_group_type> ::= trilist | tristrip | trifan |
|
|
linelist | linestrip | points |
|
|
sprites
|
|
|
|
<material_index> :: <unsigned_int> | -1
|
|
\endcode
|
|
*/
|
|
|
|
PrimitiveGroupType
|
|
parsePrimitiveGroupType(std::string_view name)
|
|
{
|
|
if (name == "trilist")
|
|
return PrimitiveGroupType::TriList;
|
|
if (name == "tristrip")
|
|
return PrimitiveGroupType::TriStrip;
|
|
if (name == "trifan")
|
|
return PrimitiveGroupType::TriFan;
|
|
if (name == "linelist")
|
|
return PrimitiveGroupType::LineList;
|
|
if (name == "linestrip")
|
|
return PrimitiveGroupType::LineStrip;
|
|
if (name == "points")
|
|
return PrimitiveGroupType::PointList;
|
|
if (name == "sprites")
|
|
return PrimitiveGroupType::SpriteList;
|
|
else
|
|
return PrimitiveGroupType::InvalidPrimitiveGroupType;
|
|
}
|
|
|
|
|
|
VertexAttributeSemantic
|
|
parseVertexAttributeSemantic(std::string_view name)
|
|
{
|
|
if (name == "position")
|
|
return VertexAttributeSemantic::Position;
|
|
if (name == "normal")
|
|
return VertexAttributeSemantic::Normal;
|
|
if (name == "color0")
|
|
return VertexAttributeSemantic::Color0;
|
|
if (name == "color1")
|
|
return VertexAttributeSemantic::Color1;
|
|
if (name == "tangent")
|
|
return VertexAttributeSemantic::Tangent;
|
|
if (name == "texcoord0")
|
|
return VertexAttributeSemantic::Texture0;
|
|
if (name == "texcoord1")
|
|
return VertexAttributeSemantic::Texture1;
|
|
if (name == "texcoord2")
|
|
return VertexAttributeSemantic::Texture2;
|
|
if (name == "texcoord3")
|
|
return VertexAttributeSemantic::Texture3;
|
|
if (name == "pointsize")
|
|
return VertexAttributeSemantic::PointSize;
|
|
return VertexAttributeSemantic::InvalidSemantic;
|
|
}
|
|
|
|
|
|
VertexAttributeFormat
|
|
parseVertexAttributeFormat(std::string_view name)
|
|
{
|
|
if (name == "f1")
|
|
return VertexAttributeFormat::Float1;
|
|
if (name == "f2")
|
|
return VertexAttributeFormat::Float2;
|
|
if (name == "f3")
|
|
return VertexAttributeFormat::Float3;
|
|
if (name == "f4")
|
|
return VertexAttributeFormat::Float4;
|
|
if (name == "ub4")
|
|
return VertexAttributeFormat::UByte4;
|
|
return VertexAttributeFormat::InvalidFormat;
|
|
}
|
|
|
|
|
|
TextureSemantic
|
|
parseTextureSemantic(std::string_view name)
|
|
{
|
|
if (name == "texture0")
|
|
return TextureSemantic::DiffuseMap;
|
|
if (name == "normalmap")
|
|
return TextureSemantic::NormalMap;
|
|
if (name == "specularmap")
|
|
return TextureSemantic::SpecularMap;
|
|
if (name == "emissivemap")
|
|
return TextureSemantic::EmissiveMap;
|
|
return TextureSemantic::InvalidTextureSemantic;
|
|
}
|
|
|
|
|
|
class AsciiModelLoader : public ModelLoader
|
|
{
|
|
public:
|
|
AsciiModelLoader(std::istream& _in, HandleGetter& _handleGetter) :
|
|
ModelLoader(_handleGetter),
|
|
tok(&_in)
|
|
{}
|
|
~AsciiModelLoader() override = default;
|
|
|
|
std::unique_ptr<Model> load() override;
|
|
void reportError(const std::string& msg) override;
|
|
|
|
bool loadMaterial(Material& material);
|
|
VertexDescription loadVertexDescription();
|
|
bool loadMesh(Mesh& mesh);
|
|
std::vector<VWord> loadVertices(const VertexDescription& vertexDesc,
|
|
unsigned int& vertexCount);
|
|
|
|
private:
|
|
Tokenizer tok;
|
|
};
|
|
|
|
|
|
void
|
|
AsciiModelLoader::reportError(const std::string& msg)
|
|
{
|
|
std::string s = fmt::format("{} (line {})", msg, tok.getLineNumber());
|
|
ModelLoader::reportError(s);
|
|
}
|
|
|
|
|
|
bool
|
|
AsciiModelLoader::loadMaterial(Material& material)
|
|
{
|
|
if (tok.nextToken() != Tokenizer::TokenName || tok.getStringValue() != MaterialToken)
|
|
{
|
|
reportError("Material definition expected");
|
|
return false;
|
|
}
|
|
|
|
material.diffuse = DefaultDiffuse;
|
|
material.specular = DefaultSpecular;
|
|
material.emissive = DefaultEmissive;
|
|
material.specularPower = DefaultSpecularPower;
|
|
material.opacity = DefaultOpacity;
|
|
|
|
while (tok.nextToken() == Tokenizer::TokenName && tok.getStringValue() != EndMaterialToken)
|
|
{
|
|
std::string property(tok.getStringValue());
|
|
TextureSemantic texType = parseTextureSemantic(property);
|
|
|
|
if (texType != TextureSemantic::InvalidTextureSemantic)
|
|
{
|
|
if (tok.nextToken() != Tokenizer::TokenString)
|
|
{
|
|
reportError("Texture name expected");
|
|
return false;
|
|
}
|
|
|
|
ResourceHandle tex = getHandle(tok.getStringValue());
|
|
material.setMap(texType, tex);
|
|
}
|
|
else if (property == "blend")
|
|
{
|
|
BlendMode blendMode = BlendMode::InvalidBlend;
|
|
|
|
if (tok.nextToken() == Tokenizer::TokenName)
|
|
{
|
|
std::string_view blendModeName = tok.getStringValue();
|
|
if (blendModeName == "normal")
|
|
blendMode = BlendMode::NormalBlend;
|
|
else if (blendModeName == "add")
|
|
blendMode = BlendMode::AdditiveBlend;
|
|
else if (blendModeName == "premultiplied")
|
|
blendMode = BlendMode::PremultipliedAlphaBlend;
|
|
}
|
|
|
|
if (blendMode == BlendMode::InvalidBlend)
|
|
{
|
|
reportError("Bad blend mode in material");
|
|
return false;
|
|
}
|
|
|
|
material.blend = blendMode;
|
|
}
|
|
else
|
|
{
|
|
// All non-texture material properties are 3-vectors except
|
|
// specular power and opacity
|
|
double data[3];
|
|
int nValues = 3;
|
|
if (property == "specpower" || property == "opacity")
|
|
{
|
|
nValues = 1;
|
|
}
|
|
|
|
for (int i = 0; i < nValues; i++)
|
|
{
|
|
if (tok.nextToken() != Tokenizer::TokenNumber)
|
|
{
|
|
reportError("Bad property value in material");
|
|
return false;
|
|
}
|
|
data[i] = tok.getNumberValue();
|
|
}
|
|
|
|
Color colorVal;
|
|
if (nValues == 3)
|
|
{
|
|
colorVal = Color(static_cast<float>(data[0]),
|
|
static_cast<float>(data[1]),
|
|
static_cast<float>(data[2]));
|
|
}
|
|
|
|
if (property == "diffuse")
|
|
{
|
|
material.diffuse = colorVal;
|
|
}
|
|
else if (property == "specular")
|
|
{
|
|
material.specular = colorVal;
|
|
}
|
|
else if (property == "emissive")
|
|
{
|
|
material.emissive = colorVal;
|
|
}
|
|
else if (property == "opacity")
|
|
{
|
|
material.opacity = static_cast<float>(data[0]);
|
|
}
|
|
else if (property == "specpower")
|
|
{
|
|
material.specularPower = static_cast<float>(data[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tok.getTokenType() != Tokenizer::TokenName)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
VertexDescription
|
|
AsciiModelLoader::loadVertexDescription()
|
|
{
|
|
if (tok.nextToken() != Tokenizer::TokenName || tok.getStringValue() != VertexDescToken)
|
|
{
|
|
reportError("Vertex description expected");
|
|
return {};
|
|
}
|
|
|
|
int maxAttributes = 16;
|
|
int nAttributes = 0;
|
|
unsigned int offset = 0;
|
|
std::vector<VertexAttribute> attributes;
|
|
attributes.reserve(maxAttributes);
|
|
|
|
while (tok.nextToken() == Tokenizer::TokenName && tok.getStringValue() != EndVertexDescToken)
|
|
{
|
|
if (nAttributes == maxAttributes)
|
|
{
|
|
// TODO: Should eliminate the attribute limit, though no real vertex
|
|
// will ever exceed it.
|
|
reportError("Attribute limit exceeded in vertex description");
|
|
return {};
|
|
}
|
|
|
|
VertexAttributeSemantic semantic = parseVertexAttributeSemantic(tok.getStringValue());
|
|
if (semantic == VertexAttributeSemantic::InvalidSemantic)
|
|
{
|
|
reportError(fmt::format("Invalid vertex attribute semantic '{}'", tok.getStringValue()));
|
|
return {};
|
|
}
|
|
|
|
if (tok.nextToken() != Tokenizer::TokenName)
|
|
{
|
|
reportError("Invalid vertex description");
|
|
return {};
|
|
}
|
|
|
|
VertexAttributeFormat format = parseVertexAttributeFormat(tok.getStringValue());
|
|
if (format == VertexAttributeFormat::InvalidFormat)
|
|
{
|
|
reportError(fmt::format("Invalid vertex attribute format '{}'", tok.getStringValue()));
|
|
return {};
|
|
}
|
|
|
|
attributes.emplace_back(semantic, format, offset);
|
|
|
|
offset += VertexAttribute::getFormatSizeWords(format);
|
|
nAttributes++;
|
|
}
|
|
|
|
if (tok.getTokenType() != Tokenizer::TokenName)
|
|
{
|
|
reportError("Invalid vertex description");
|
|
return {};
|
|
}
|
|
|
|
if (nAttributes == 0)
|
|
{
|
|
reportError("Vertex definitition cannot be empty");
|
|
return {};
|
|
}
|
|
|
|
return VertexDescription(std::move(attributes));
|
|
}
|
|
|
|
|
|
std::vector<VWord>
|
|
AsciiModelLoader::loadVertices(const VertexDescription& vertexDesc,
|
|
unsigned int& vertexCount)
|
|
{
|
|
if (tok.nextToken() != Tokenizer::TokenName && tok.getStringValue() != VerticesToken)
|
|
{
|
|
reportError("Vertex data expected");
|
|
return {};
|
|
}
|
|
|
|
if (tok.nextToken() != Tokenizer::TokenNumber || !tok.isInteger())
|
|
{
|
|
reportError("Vertex count expected");
|
|
return {};
|
|
}
|
|
|
|
std::int32_t num = tok.getIntegerValue();
|
|
if (num <= 0)
|
|
{
|
|
reportError("Bad vertex count for mesh");
|
|
return {};
|
|
}
|
|
|
|
vertexCount = static_cast<unsigned int>(num);
|
|
unsigned int stride = vertexDesc.strideBytes / sizeof(VWord);
|
|
unsigned int vertexDataSize = stride * vertexCount;
|
|
std::vector<VWord> vertexData(vertexDataSize);
|
|
|
|
unsigned int offset = 0;
|
|
double data[4];
|
|
for (unsigned int i = 0; i < vertexCount; i++, offset += stride)
|
|
{
|
|
assert(offset < vertexDataSize);
|
|
for (const auto& attr : vertexDesc.attributes)
|
|
{
|
|
VertexAttributeFormat vfmt = attr.format;
|
|
/*unsigned int nBytes = Mesh::getVertexAttributeSize(fmt); Unused*/
|
|
int readCount = 0;
|
|
switch (vfmt)
|
|
{
|
|
case VertexAttributeFormat::Float1:
|
|
readCount = 1;
|
|
break;
|
|
case VertexAttributeFormat::Float2:
|
|
readCount = 2;
|
|
break;
|
|
case VertexAttributeFormat::Float3:
|
|
readCount = 3;
|
|
break;
|
|
case VertexAttributeFormat::Float4:
|
|
case VertexAttributeFormat::UByte4:
|
|
readCount = 4;
|
|
break;
|
|
default:
|
|
assert(0);
|
|
return {};
|
|
}
|
|
|
|
for (int j = 0; j < readCount; j++)
|
|
{
|
|
if (tok.nextToken() != Tokenizer::TokenNumber)
|
|
{
|
|
reportError("Error in vertex data");
|
|
data[j] = 0.0;
|
|
}
|
|
else
|
|
{
|
|
data[j] = tok.getNumberValue();
|
|
}
|
|
// TODO: range check unsigned byte values
|
|
}
|
|
|
|
unsigned int base = offset + attr.offsetWords;
|
|
if (vfmt == VertexAttributeFormat::UByte4)
|
|
{
|
|
std::uint8_t c[4];
|
|
std::transform(data, data + readCount, c, [](double d) { return static_cast<std::uint8_t>(d); });
|
|
std::memcpy(vertexData.data() + base, c, readCount);
|
|
}
|
|
else
|
|
{
|
|
for (int k = 0; k < readCount; k++)
|
|
{
|
|
float value = static_cast<float>(data[k]);
|
|
std::memcpy(vertexData.data() + base + k, &value, sizeof(float));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return vertexData;
|
|
}
|
|
|
|
|
|
bool
|
|
AsciiModelLoader::loadMesh(Mesh& mesh)
|
|
{
|
|
if (tok.nextToken() != Tokenizer::TokenName && tok.getStringValue() != MeshToken)
|
|
{
|
|
reportError("Mesh definition expected");
|
|
return false;
|
|
}
|
|
|
|
VertexDescription vertexDesc = loadVertexDescription();
|
|
if (vertexDesc.attributes.empty())
|
|
return false;
|
|
|
|
unsigned int vertexCount = 0;
|
|
std::vector<VWord> vertexData = loadVertices(vertexDesc, vertexCount);
|
|
if (vertexData.empty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
mesh.setVertexDescription(std::move(vertexDesc));
|
|
mesh.setVertices(vertexCount, std::move(vertexData));
|
|
|
|
while (tok.nextToken() == Tokenizer::TokenName && tok.getStringValue() != EndMeshToken)
|
|
{
|
|
PrimitiveGroupType type = parsePrimitiveGroupType(tok.getStringValue());
|
|
if (type == PrimitiveGroupType::InvalidPrimitiveGroupType)
|
|
{
|
|
reportError(fmt::format("Bad primitive group type: {}", tok.getStringValue()));
|
|
return false;
|
|
}
|
|
|
|
if (tok.nextToken() != Tokenizer::TokenNumber || !tok.isInteger())
|
|
{
|
|
reportError("Material index expected in primitive group");
|
|
return false;
|
|
}
|
|
|
|
unsigned int materialIndex;
|
|
if (tok.getIntegerValue() == -1)
|
|
{
|
|
materialIndex = ~0u;
|
|
}
|
|
else
|
|
{
|
|
materialIndex = (unsigned int) tok.getIntegerValue();
|
|
}
|
|
|
|
if (tok.nextToken() != Tokenizer::TokenNumber || !tok.isInteger())
|
|
{
|
|
reportError("Index count expected in primitive group");
|
|
return false;
|
|
}
|
|
|
|
unsigned int indexCount = (unsigned int) tok.getIntegerValue();
|
|
|
|
std::vector<Index32> indices;
|
|
indices.reserve(indexCount);
|
|
|
|
for (unsigned int i = 0; i < indexCount; i++)
|
|
{
|
|
if (tok.nextToken() != Tokenizer::TokenNumber || !tok.isInteger())
|
|
{
|
|
reportError("Incomplete index list in primitive group");
|
|
return false;
|
|
}
|
|
|
|
unsigned int index = (unsigned int) tok.getIntegerValue();
|
|
if (index >= vertexCount)
|
|
{
|
|
reportError("Index out of range");
|
|
return false;
|
|
}
|
|
|
|
indices.push_back(index);
|
|
}
|
|
|
|
mesh.addGroup(type, materialIndex, std::move(indices));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
std::unique_ptr<Model>
|
|
AsciiModelLoader::load()
|
|
{
|
|
auto model = std::make_unique<Model>();
|
|
bool seenMeshes = false;
|
|
|
|
// Parse material and mesh definitions
|
|
for (Tokenizer::TokenType token = tok.nextToken(); token != Tokenizer::TokenEnd; token = tok.nextToken())
|
|
{
|
|
if (token == Tokenizer::TokenName)
|
|
{
|
|
std::string_view name = tok.getStringValue();
|
|
tok.pushBack();
|
|
|
|
if (name == "material")
|
|
{
|
|
if (seenMeshes)
|
|
{
|
|
reportError("Materials must be defined before meshes");
|
|
return nullptr;
|
|
}
|
|
|
|
Material material;
|
|
if (!loadMaterial(material))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
model->addMaterial(std::move(material));
|
|
}
|
|
else if (name == "mesh")
|
|
{
|
|
seenMeshes = true;
|
|
|
|
Mesh mesh;
|
|
if (!loadMesh(mesh))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
model->addMesh(std::move(mesh));
|
|
}
|
|
else
|
|
{
|
|
reportError(fmt::format("Error: Unknown block type {}", name));
|
|
return nullptr;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
reportError("Block name expected");
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return model;
|
|
}
|
|
|
|
|
|
/***** ASCII writer *****/
|
|
|
|
class AsciiModelWriter : public ModelWriter
|
|
{
|
|
public:
|
|
AsciiModelWriter(std::ostream& _out, SourceGetter& _sourceGetter) :
|
|
ModelWriter(_sourceGetter),
|
|
out(_out)
|
|
{}
|
|
~AsciiModelWriter() override = default;
|
|
|
|
bool write(const Model& /*model*/) override;
|
|
|
|
private:
|
|
bool writeMesh(const Mesh& /*mesh*/);
|
|
bool writeMaterial(const Material& /*material*/);
|
|
bool writeGroup(const PrimitiveGroup& /*group*/);
|
|
bool writeVertexDescription(const VertexDescription& /*desc*/);
|
|
bool writeVertices(const VWord* vertexData,
|
|
unsigned int nVertices,
|
|
unsigned int strideWords,
|
|
const VertexDescription& desc);
|
|
|
|
std::ostream& out;
|
|
};
|
|
|
|
|
|
bool
|
|
AsciiModelWriter::write(const Model& model)
|
|
{
|
|
fmt::print(out, "{}\n\n", CEL_MODEL_HEADER_ASCII);
|
|
if (!out.good()) { return false; }
|
|
|
|
for (unsigned int matIndex = 0; model.getMaterial(matIndex) != nullptr; matIndex++)
|
|
{
|
|
if (!writeMaterial(*model.getMaterial(matIndex))) { return false; }
|
|
if (!(out << '\n').good()) { return false; }
|
|
}
|
|
|
|
for (unsigned int meshIndex = 0; model.getMesh(meshIndex) != nullptr; meshIndex++)
|
|
{
|
|
if (!writeMesh(*model.getMesh(meshIndex))) { return false; }
|
|
if (!(out << '\n').good()) { return false; }
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool
|
|
AsciiModelWriter::writeGroup(const PrimitiveGroup& group)
|
|
{
|
|
switch (group.prim)
|
|
{
|
|
case PrimitiveGroupType::TriList:
|
|
fmt::print(out, "trilist"); break;
|
|
case PrimitiveGroupType::TriStrip:
|
|
fmt::print(out, "tristrip"); break;
|
|
case PrimitiveGroupType::TriFan:
|
|
fmt::print(out, "trifan"); break;
|
|
case PrimitiveGroupType::LineList:
|
|
fmt::print(out, "linelist"); break;
|
|
case PrimitiveGroupType::LineStrip:
|
|
fmt::print(out, "linestrip"); break;
|
|
case PrimitiveGroupType::PointList:
|
|
fmt::print(out, "points"); break;
|
|
case PrimitiveGroupType::SpriteList:
|
|
fmt::print(out, "sprites"); break;
|
|
default:
|
|
return false;
|
|
}
|
|
if (!out.good()) { return false; }
|
|
|
|
if (group.materialIndex == ~0u)
|
|
out << " -1";
|
|
else
|
|
fmt::print(out, " {}", group.materialIndex);
|
|
if (!out.good()) { return false; }
|
|
|
|
fmt::print(out, " {}\n", group.indices.size());
|
|
if (!out.good()) { return false;}
|
|
|
|
// Print the indices, twelve per line
|
|
for (unsigned int i = 0; i < group.indices.size(); i++)
|
|
{
|
|
fmt::print(out, "{}{}",
|
|
group.indices[i],
|
|
i % 12 == 11 || i == group.indices.size() - 1 ? '\n' : ' ');
|
|
if (!out.good()) { return false; }
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool
|
|
AsciiModelWriter::writeMesh(const Mesh& mesh)
|
|
{
|
|
if (!(out << "mesh\n").good()) { return false; }
|
|
|
|
if (!mesh.getName().empty())
|
|
{
|
|
fmt::print(out, "# {}\n", mesh.getName());
|
|
if (!out.good()) { return false; }
|
|
}
|
|
|
|
if (!writeVertexDescription(mesh.getVertexDescription())) { return false; }
|
|
if (!(out << '\n').good()) { return false; }
|
|
|
|
if (!writeVertices(mesh.getVertexData(),
|
|
mesh.getVertexCount(),
|
|
mesh.getVertexStrideWords(),
|
|
mesh.getVertexDescription()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!(out << '\n').good()) { return false; }
|
|
|
|
for (unsigned int groupIndex = 0; mesh.getGroup(groupIndex) != nullptr; groupIndex++)
|
|
{
|
|
if (!writeGroup(*mesh.getGroup(groupIndex))
|
|
|| !(out << '\n').good())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return (out << "end_mesh\n").good();
|
|
}
|
|
|
|
|
|
bool
|
|
AsciiModelWriter::writeVertices(const VWord* vertexData,
|
|
unsigned int nVertices,
|
|
unsigned int strideWords,
|
|
const VertexDescription& desc)
|
|
{
|
|
fmt::print(out, "vertices {}\n", nVertices);
|
|
if (!out.good()) { return false; }
|
|
|
|
for (unsigned int i = 0; i < nVertices; i++, vertexData += strideWords)
|
|
{
|
|
for (const auto& attr : desc.attributes)
|
|
{
|
|
const VWord* data = vertexData + attr.offsetWords;
|
|
float fdata[4];
|
|
|
|
switch (attr.format)
|
|
{
|
|
case VertexAttributeFormat::Float1:
|
|
std::memcpy(fdata, data, sizeof(float));
|
|
fmt::print(out, "{}", fdata[0]);
|
|
break;
|
|
case VertexAttributeFormat::Float2:
|
|
std::memcpy(fdata, data, sizeof(float) * 2);
|
|
fmt::print(out, "{} {}", fdata[0], fdata[1]);
|
|
break;
|
|
case VertexAttributeFormat::Float3:
|
|
std::memcpy(fdata, data, sizeof(float) * 3);
|
|
fmt::print(out, "{} {} {}", fdata[0], fdata[1], fdata[2]);
|
|
break;
|
|
case VertexAttributeFormat::Float4:
|
|
std::memcpy(fdata, data, sizeof(float) * 4);
|
|
fmt::print(out, "{} {} {} {}", fdata[0], fdata[1], fdata[2], fdata[3]);
|
|
break;
|
|
case VertexAttributeFormat::UByte4:
|
|
fmt::print(out, "{} {} {} {}", +data[0], +data[1], +data[2], +data[3]);
|
|
break;
|
|
default:
|
|
assert(0);
|
|
break;
|
|
}
|
|
if (!out.good() || !(out << ' ').good()) { return false; }
|
|
}
|
|
|
|
if (!(out << '\n').good()) { return false; }
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool
|
|
AsciiModelWriter::writeVertexDescription(const VertexDescription& desc)
|
|
{
|
|
if (!(out << "vertexdesc\n").good()) { return false; }
|
|
for (const auto& attr : desc.attributes)
|
|
{
|
|
// We should never have a vertex description with invalid
|
|
// fields . . .
|
|
|
|
switch (attr.semantic)
|
|
{
|
|
case VertexAttributeSemantic::Position:
|
|
out << "position";
|
|
break;
|
|
case VertexAttributeSemantic::Color0:
|
|
out << "color0";
|
|
break;
|
|
case VertexAttributeSemantic::Color1:
|
|
out << "color1";
|
|
break;
|
|
case VertexAttributeSemantic::Normal:
|
|
out << "normal";
|
|
break;
|
|
case VertexAttributeSemantic::Tangent:
|
|
out << "tangent";
|
|
break;
|
|
case VertexAttributeSemantic::Texture0:
|
|
out << "texcoord0";
|
|
break;
|
|
case VertexAttributeSemantic::Texture1:
|
|
out << "texcoord1";
|
|
break;
|
|
case VertexAttributeSemantic::Texture2:
|
|
out << "texcoord2";
|
|
break;
|
|
case VertexAttributeSemantic::Texture3:
|
|
out << "texcoord3";
|
|
break;
|
|
case VertexAttributeSemantic::PointSize:
|
|
out << "pointsize";
|
|
break;
|
|
default:
|
|
assert(0);
|
|
break;
|
|
}
|
|
|
|
if (!out.good() || !(out << ' ').good()) { return false; }
|
|
|
|
switch (attr.format)
|
|
{
|
|
case VertexAttributeFormat::Float1:
|
|
out << "f1";
|
|
break;
|
|
case VertexAttributeFormat::Float2:
|
|
out << "f2";
|
|
break;
|
|
case VertexAttributeFormat::Float3:
|
|
out << "f3";
|
|
break;
|
|
case VertexAttributeFormat::Float4:
|
|
out << "f4";
|
|
break;
|
|
case VertexAttributeFormat::UByte4:
|
|
out << "ub4";
|
|
break;
|
|
default:
|
|
assert(0);
|
|
break;
|
|
}
|
|
|
|
if (!out.good() || !(out << '\n').good()) { return false; }
|
|
}
|
|
return (out << "end_vertexdesc\n").good();
|
|
}
|
|
|
|
|
|
bool
|
|
AsciiModelWriter::writeMaterial(const Material& material)
|
|
{
|
|
if (!(out << "material\n").good()) { return false; }
|
|
if (material.diffuse != DefaultDiffuse)
|
|
{
|
|
fmt::print(out, "diffuse {} {} {}\n",
|
|
material.diffuse.red(),
|
|
material.diffuse.green(),
|
|
material.diffuse.blue());
|
|
if (!out.good()) { return false; }
|
|
}
|
|
|
|
if (material.emissive != DefaultEmissive)
|
|
{
|
|
fmt::print(out, "emissive {} {} {}\n",
|
|
material.emissive.red(),
|
|
material.emissive.green(),
|
|
material.emissive.blue());
|
|
if (!out.good()) { return false; }
|
|
}
|
|
|
|
if (material.specular != DefaultSpecular)
|
|
{
|
|
fmt::print(out, "specular {} {} {}\n",
|
|
material.specular.red(),
|
|
material.specular.green(),
|
|
material.specular.blue());
|
|
if (!out.good()) { return false; }
|
|
}
|
|
|
|
if (material.specularPower != DefaultSpecularPower)
|
|
{
|
|
fmt::print(out, "specpower {}\n", material.specularPower);
|
|
if (!out.good()) { return false; }
|
|
}
|
|
|
|
if (material.opacity != DefaultOpacity)
|
|
{
|
|
fmt::print(out, "opacity {}\n", material.opacity);
|
|
if (!out.good()) { return false; }
|
|
}
|
|
|
|
if (material.blend != DefaultBlend)
|
|
{
|
|
if (!(out << "blend ").good()) { return false; }
|
|
switch (material.blend)
|
|
{
|
|
case BlendMode::NormalBlend:
|
|
out << "normal";
|
|
break;
|
|
case BlendMode::AdditiveBlend:
|
|
out << "add";
|
|
break;
|
|
case BlendMode::PremultipliedAlphaBlend:
|
|
out << "premultiplied";
|
|
break;
|
|
default:
|
|
assert(0);
|
|
break;
|
|
}
|
|
|
|
if (!out.good() || !(out << '\n').good()) { return false; }
|
|
}
|
|
|
|
for (int i = 0; i < static_cast<int>(TextureSemantic::TextureSemanticMax); i++)
|
|
{
|
|
fs::path texSource;
|
|
if (material.maps[i] != InvalidResource)
|
|
{
|
|
texSource = getSource(material.maps[i]);
|
|
}
|
|
|
|
if (!texSource.empty())
|
|
{
|
|
switch (static_cast<TextureSemantic>(i))
|
|
{
|
|
case TextureSemantic::DiffuseMap:
|
|
out << "texture0";
|
|
break;
|
|
case TextureSemantic::NormalMap:
|
|
out << "normalmap";
|
|
break;
|
|
case TextureSemantic::SpecularMap:
|
|
out << "specularmap";
|
|
break;
|
|
case TextureSemantic::EmissiveMap:
|
|
out << "emissivemap";
|
|
break;
|
|
default:
|
|
assert(0);
|
|
}
|
|
|
|
if (!out.good()) { return false; }
|
|
|
|
fmt::print(out, " \"{}\"\n", texSource.string());
|
|
if (!out.good()) { return false; }
|
|
}
|
|
}
|
|
|
|
return (out << "end_material\n").good();
|
|
}
|
|
|
|
|
|
/***** Binary loader *****/
|
|
|
|
bool readToken(std::istream& in, ModelFileToken& value)
|
|
{
|
|
std::int16_t num;
|
|
if (!celutil::readLE<std::int16_t>(in, num)) { return false; }
|
|
value = static_cast<ModelFileToken>(num);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool readType(std::istream& in, ModelFileType& value)
|
|
{
|
|
std::int16_t num;
|
|
if (!celutil::readLE<std::int16_t>(in, num)) { return false; }
|
|
value = static_cast<ModelFileType>(num);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool readTypeFloat1(std::istream& in, float& f)
|
|
{
|
|
ModelFileType cmodType;
|
|
return readType(in, cmodType)
|
|
&& cmodType == CMOD_Float1
|
|
&& celutil::readLE<float>(in, f);
|
|
}
|
|
|
|
|
|
bool readTypeColor(std::istream& in, Color& c)
|
|
{
|
|
ModelFileType cmodType;
|
|
float r, g, b;
|
|
if (!readType(in, cmodType)
|
|
|| cmodType != CMOD_Color
|
|
|| !celutil::readLE<float>(in, r)
|
|
|| !celutil::readLE<float>(in, g)
|
|
|| !celutil::readLE<float>(in, b))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
c = Color(r, g, b);
|
|
return true;
|
|
}
|
|
|
|
|
|
bool readTypeString(std::istream& in, std::string& s)
|
|
{
|
|
ModelFileType cmodType;
|
|
uint16_t len;
|
|
if (!readType(in, cmodType)
|
|
|| cmodType != CMOD_String
|
|
|| !celutil::readLE<std::uint16_t>(in, len))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (len == 0)
|
|
{
|
|
s = "";
|
|
}
|
|
else
|
|
{
|
|
std::vector<char> buf(len);
|
|
if (!in.read(buf.data(), len).good()) { return false; }
|
|
s = std::string(buf.cbegin(), buf.cend());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ignoreValue(std::istream& in)
|
|
{
|
|
ModelFileType type;
|
|
if (!readType(in, type)) { return false; }
|
|
std::streamsize size = 0;
|
|
|
|
switch (type)
|
|
{
|
|
case CMOD_Float1:
|
|
size = 4;
|
|
break;
|
|
case CMOD_Float2:
|
|
size = 8;
|
|
break;
|
|
case CMOD_Float3:
|
|
size = 12;
|
|
break;
|
|
case CMOD_Float4:
|
|
size = 16;
|
|
break;
|
|
case CMOD_Uint32:
|
|
size = 4;
|
|
break;
|
|
case CMOD_Color:
|
|
size = 12;
|
|
break;
|
|
case CMOD_String:
|
|
{
|
|
std::uint16_t len;
|
|
if (!celutil::readLE<std::uint16_t>(in, len)) { return false; }
|
|
size = len;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
return in.ignore(size).good();
|
|
}
|
|
|
|
|
|
class BinaryModelLoader : public ModelLoader
|
|
{
|
|
public:
|
|
BinaryModelLoader(std::istream& _in, HandleGetter& _handleGetter) :
|
|
ModelLoader(_handleGetter),
|
|
in(_in)
|
|
{}
|
|
~BinaryModelLoader() override = default;
|
|
|
|
std::unique_ptr<Model> load() override;
|
|
void reportError(const std::string& /*msg*/) override;
|
|
|
|
bool loadMaterial(Material& material);
|
|
VertexDescription loadVertexDescription();
|
|
bool loadMesh(Mesh& mesh);
|
|
std::vector<VWord> loadVertices(const VertexDescription& vertexDesc,
|
|
unsigned int& vertexCount);
|
|
|
|
private:
|
|
std::istream& in;
|
|
};
|
|
|
|
|
|
void
|
|
BinaryModelLoader::reportError(const std::string& msg)
|
|
{
|
|
std::string s = fmt::format("{} (offset {})", msg, 0);
|
|
ModelLoader::reportError(s);
|
|
}
|
|
|
|
|
|
std::unique_ptr<Model>
|
|
BinaryModelLoader::load()
|
|
{
|
|
auto model = std::make_unique<Model>();
|
|
bool seenMeshes = false;
|
|
|
|
// Parse material and mesh definitions
|
|
for (;;)
|
|
{
|
|
ModelFileToken tok;
|
|
if (!readToken(in, tok))
|
|
{
|
|
if (in.eof()) { break; }
|
|
reportError("Failed to read token");
|
|
return nullptr;
|
|
}
|
|
|
|
if (tok == CMOD_Material)
|
|
{
|
|
if (seenMeshes)
|
|
{
|
|
reportError("Materials must be defined before meshes");
|
|
return nullptr;
|
|
}
|
|
|
|
Material material;
|
|
if (!loadMaterial(material))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
model->addMaterial(std::move(material));
|
|
}
|
|
else if (tok == CMOD_Mesh)
|
|
{
|
|
seenMeshes = true;
|
|
|
|
Mesh mesh;
|
|
if (!loadMesh(mesh))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
model->addMesh(std::move(mesh));
|
|
}
|
|
else
|
|
{
|
|
reportError("Error: Unknown block type in model");
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return model;
|
|
}
|
|
|
|
|
|
bool
|
|
BinaryModelLoader::loadMaterial(Material& material)
|
|
{
|
|
material.diffuse = DefaultDiffuse;
|
|
material.specular = DefaultSpecular;
|
|
material.emissive = DefaultEmissive;
|
|
material.specularPower = DefaultSpecularPower;
|
|
material.opacity = DefaultOpacity;
|
|
|
|
for (;;)
|
|
{
|
|
ModelFileToken tok;
|
|
if (!readToken(in, tok))
|
|
{
|
|
reportError("Error reading token type");
|
|
return false;
|
|
}
|
|
|
|
switch (tok)
|
|
{
|
|
case CMOD_Diffuse:
|
|
if (!readTypeColor(in, material.diffuse))
|
|
{
|
|
reportError("Incorrect type for diffuse color");
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case CMOD_Specular:
|
|
if (!readTypeColor(in, material.specular))
|
|
{
|
|
reportError("Incorrect type for specular color");
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case CMOD_Emissive:
|
|
if (!readTypeColor(in, material.emissive))
|
|
{
|
|
reportError("Incorrect type for emissive color");
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case CMOD_SpecularPower:
|
|
if (!readTypeFloat1(in, material.specularPower))
|
|
{
|
|
reportError("Float expected for specularPower");
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case CMOD_Opacity:
|
|
if (!readTypeFloat1(in, material.opacity))
|
|
{
|
|
reportError("Float expected for opacity");
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case CMOD_Blend:
|
|
{
|
|
std::int16_t blendMode;
|
|
if (!celutil::readLE<std::int16_t>(in, blendMode)
|
|
|| blendMode < 0 || blendMode >= static_cast<std::int16_t>(BlendMode::BlendMax))
|
|
{
|
|
reportError("Bad blend mode");
|
|
return false;
|
|
}
|
|
material.blend = static_cast<BlendMode>(blendMode);
|
|
}
|
|
break;
|
|
|
|
case CMOD_Texture:
|
|
{
|
|
std::int16_t texType;
|
|
if (!celutil::readLE<std::int16_t>(in, texType)
|
|
|| texType < 0 || texType >= static_cast<std::int16_t>(TextureSemantic::TextureSemanticMax))
|
|
{
|
|
reportError("Bad texture type");
|
|
return false;
|
|
}
|
|
|
|
std::string texfile;
|
|
if (!readTypeString(in, texfile))
|
|
{
|
|
reportError("String expected for texture filename");
|
|
return false;
|
|
}
|
|
|
|
if (texfile.empty())
|
|
{
|
|
reportError("Zero length texture name in material definition");
|
|
return false;
|
|
}
|
|
|
|
ResourceHandle tex = getHandle(texfile);
|
|
material.maps[texType] = tex;
|
|
}
|
|
break;
|
|
|
|
case CMOD_EndMaterial:
|
|
return true;
|
|
|
|
default:
|
|
// Skip unrecognized tokens
|
|
if (!ignoreValue(in))
|
|
{
|
|
return false;
|
|
}
|
|
} // switch
|
|
} // for
|
|
}
|
|
|
|
|
|
VertexDescription
|
|
BinaryModelLoader::loadVertexDescription()
|
|
{
|
|
ModelFileToken tok;
|
|
if (!readToken(in, tok) || tok != CMOD_VertexDesc)
|
|
{
|
|
reportError("Vertex description expected");
|
|
return {};
|
|
}
|
|
|
|
int maxAttributes = 16;
|
|
int nAttributes = 0;
|
|
unsigned int offset = 0;
|
|
std::vector<VertexAttribute> attributes;
|
|
attributes.reserve(maxAttributes);
|
|
|
|
for (;;)
|
|
{
|
|
std::int16_t tok;
|
|
if (!celutil::readLE<std::int16_t>(in, tok))
|
|
{
|
|
reportError("Could not read token");
|
|
return {};
|
|
}
|
|
|
|
if (tok == CMOD_EndVertexDesc)
|
|
{
|
|
break;
|
|
}
|
|
if (tok >= 0 && tok < static_cast<std::int16_t>(VertexAttributeSemantic::SemanticMax))
|
|
{
|
|
std::int16_t vfmt;
|
|
if (!celutil::readLE<std::int16_t>(in, vfmt)
|
|
|| vfmt < 0 || vfmt >= static_cast<std::int16_t>(VertexAttributeFormat::FormatMax))
|
|
{
|
|
reportError("Invalid vertex attribute type");
|
|
return {};
|
|
}
|
|
|
|
if (nAttributes == maxAttributes)
|
|
{
|
|
reportError("Too many attributes in vertex description");
|
|
return {};
|
|
}
|
|
|
|
attributes.emplace_back(static_cast<VertexAttributeSemantic>(tok),
|
|
static_cast<VertexAttributeFormat>(vfmt),
|
|
offset);
|
|
|
|
offset += VertexAttribute::getFormatSizeWords(attributes[nAttributes].format);
|
|
nAttributes++;
|
|
}
|
|
else
|
|
{
|
|
reportError("Invalid semantic in vertex description");
|
|
return {};
|
|
}
|
|
}
|
|
|
|
if (nAttributes == 0)
|
|
{
|
|
reportError("Vertex definitition cannot be empty");
|
|
return {};
|
|
}
|
|
|
|
return VertexDescription(std::move(attributes));
|
|
}
|
|
|
|
|
|
bool
|
|
BinaryModelLoader::loadMesh(Mesh& mesh)
|
|
{
|
|
VertexDescription vertexDesc = loadVertexDescription();
|
|
if (vertexDesc.attributes.empty()) { return false; }
|
|
|
|
unsigned int vertexCount = 0;
|
|
std::vector<VWord> vertexData = loadVertices(vertexDesc, vertexCount);
|
|
if (vertexData.empty()) { return false; }
|
|
|
|
mesh.setVertexDescription(std::move(vertexDesc));
|
|
mesh.setVertices(vertexCount, std::move(vertexData));
|
|
|
|
for (;;)
|
|
{
|
|
std::int16_t tok;
|
|
if (!celutil::readLE<std::int16_t>(in, tok))
|
|
{
|
|
reportError("Failed to read token type");
|
|
return false;
|
|
}
|
|
|
|
if (tok == CMOD_EndMesh)
|
|
{
|
|
break;
|
|
}
|
|
if (tok < 0 || tok >= static_cast<std::int16_t>(PrimitiveGroupType::PrimitiveTypeMax))
|
|
{
|
|
reportError("Bad primitive group type");
|
|
return false;
|
|
}
|
|
|
|
PrimitiveGroupType type = static_cast<PrimitiveGroupType>(tok);
|
|
std::uint32_t materialIndex, indexCount;
|
|
if (!celutil::readLE<std::uint32_t>(in, materialIndex)
|
|
|| !celutil::readLE<std::uint32_t>(in, indexCount))
|
|
{
|
|
reportError("Could not read primitive indices");
|
|
return false;
|
|
}
|
|
|
|
std::vector<Index32> indices;
|
|
indices.reserve(indexCount);
|
|
|
|
for (unsigned int i = 0; i < indexCount; i++)
|
|
{
|
|
std::uint32_t index;
|
|
if (!celutil::readLE<std::uint32_t>(in, index) || index >= vertexCount)
|
|
{
|
|
reportError("Index out of range");
|
|
return false;
|
|
}
|
|
|
|
indices.push_back(index);
|
|
}
|
|
|
|
mesh.addGroup(type, materialIndex, std::move(indices));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
std::vector<VWord>
|
|
BinaryModelLoader::loadVertices(const VertexDescription& vertexDesc,
|
|
unsigned int& vertexCount)
|
|
{
|
|
ModelFileToken tok;
|
|
if (!readToken(in, tok) || tok != CMOD_Vertices)
|
|
{
|
|
reportError("Vertex data expected");
|
|
return {};
|
|
}
|
|
|
|
if (!celutil::readLE<std::uint32_t>(in, vertexCount))
|
|
{
|
|
reportError("Vertex count expected");
|
|
return {};
|
|
}
|
|
|
|
unsigned int stride = vertexDesc.strideBytes / sizeof(VWord);
|
|
unsigned int vertexDataSize = stride * vertexCount;
|
|
std::vector<VWord> vertexData(vertexDataSize);
|
|
|
|
unsigned int offset = 0;
|
|
for (unsigned int i = 0; i < vertexCount; i++, offset += stride)
|
|
{
|
|
assert(offset < vertexDataSize);
|
|
for (const auto& attr : vertexDesc.attributes)
|
|
{
|
|
unsigned int base = offset + attr.offsetWords;
|
|
VertexAttributeFormat vfmt = attr.format;
|
|
float f[4];
|
|
/*int readCount = 0; Unused*/
|
|
switch (vfmt)
|
|
{
|
|
case VertexAttributeFormat::Float1:
|
|
if (!celutil::readLE<float>(in, f[0]))
|
|
{
|
|
reportError("Failed to read Float1");
|
|
return {};
|
|
}
|
|
std::memcpy(vertexData.data() + base, f, sizeof(float));
|
|
break;
|
|
case VertexAttributeFormat::Float2:
|
|
if (!celutil::readLE<float>(in, f[0])
|
|
|| !celutil::readLE<float>(in, f[1]))
|
|
{
|
|
reportError("Failed to read Float2");
|
|
return {};
|
|
}
|
|
std::memcpy(vertexData.data() + base, f, sizeof(float) * 2);
|
|
break;
|
|
case VertexAttributeFormat::Float3:
|
|
if (!celutil::readLE<float>(in, f[0])
|
|
|| !celutil::readLE<float>(in, f[1])
|
|
|| !celutil::readLE<float>(in, f[2]))
|
|
{
|
|
reportError("Failed to read Float3");
|
|
return {};
|
|
}
|
|
std::memcpy(vertexData.data() + base, f, sizeof(float) * 3);
|
|
break;
|
|
case VertexAttributeFormat::Float4:
|
|
if (!celutil::readLE<float>(in, f[0])
|
|
|| !celutil::readLE<float>(in, f[1])
|
|
|| !celutil::readLE<float>(in, f[2])
|
|
|| !celutil::readLE<float>(in, f[3]))
|
|
{
|
|
reportError("Failed to read Float4");
|
|
return {};
|
|
}
|
|
std::memcpy(vertexData.data() + base, f, sizeof(float) * 4);
|
|
break;
|
|
case VertexAttributeFormat::UByte4:
|
|
if (!celutil::readNative<std::uint32_t>(in, vertexData[base]))
|
|
{
|
|
reportError("failed to read UByte4");
|
|
return {};
|
|
}
|
|
break;
|
|
default:
|
|
assert(0);
|
|
return {};
|
|
}
|
|
}
|
|
}
|
|
|
|
return vertexData;
|
|
}
|
|
|
|
|
|
/***** Binary writer *****/
|
|
|
|
bool writeToken(std::ostream& out, ModelFileToken val)
|
|
{
|
|
return celutil::writeLE<std::int16_t>(out, static_cast<std::int16_t>(val));
|
|
}
|
|
|
|
|
|
bool writeType(std::ostream& out, ModelFileType val)
|
|
{
|
|
return celutil::writeLE<std::int16_t>(out, static_cast<std::int16_t>(val));
|
|
}
|
|
|
|
|
|
bool writeTypeFloat1(std::ostream& out, float f)
|
|
{
|
|
return writeType(out, CMOD_Float1) && celutil::writeLE<float>(out, f);
|
|
}
|
|
|
|
|
|
bool writeTypeColor(std::ostream& out, const Color& c)
|
|
{
|
|
return writeType(out, CMOD_Color)
|
|
&& celutil::writeLE<float>(out, c.red())
|
|
&& celutil::writeLE<float>(out, c.green())
|
|
&& celutil::writeLE<float>(out, c.blue());
|
|
}
|
|
|
|
|
|
bool writeTypeString(std::ostream& out, const std::string& s)
|
|
{
|
|
return s.length() <= INT16_MAX
|
|
&& writeType(out, CMOD_String)
|
|
&& celutil::writeLE<std::int16_t>(out, s.length())
|
|
&& out.write(s.c_str(), s.length()).good();
|
|
}
|
|
|
|
|
|
class BinaryModelWriter : public ModelWriter
|
|
{
|
|
public:
|
|
BinaryModelWriter(std::ostream& _out, SourceGetter& _sourceGetter) :
|
|
ModelWriter(_sourceGetter),
|
|
out(_out)
|
|
{}
|
|
~BinaryModelWriter() override = default;
|
|
|
|
bool write(const Model& /*model*/) override;
|
|
|
|
private:
|
|
bool writeMesh(const Mesh& /*mesh*/);
|
|
bool writeMaterial(const Material& /*material*/);
|
|
bool writeGroup(const PrimitiveGroup& /*group*/);
|
|
bool writeVertexDescription(const VertexDescription& /*desc*/);
|
|
bool writeVertices(const VWord* vertexData,
|
|
unsigned int nVertices,
|
|
unsigned int strideWords,
|
|
const VertexDescription& desc);
|
|
|
|
std::ostream& out;
|
|
};
|
|
|
|
|
|
bool
|
|
BinaryModelWriter::write(const Model& model)
|
|
{
|
|
if (!out.write(CEL_MODEL_HEADER_BINARY, CEL_MODEL_HEADER_LENGTH).good()) { return false; }
|
|
|
|
for (unsigned int matIndex = 0; model.getMaterial(matIndex) != nullptr; matIndex++)
|
|
{
|
|
if (!writeMaterial(*model.getMaterial(matIndex))) { return false; }
|
|
}
|
|
|
|
for (unsigned int meshIndex = 0; model.getMesh(meshIndex) != nullptr; meshIndex++)
|
|
{
|
|
if (!writeMesh(*model.getMesh(meshIndex))) { return false; }
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool
|
|
BinaryModelWriter::writeGroup(const PrimitiveGroup& group)
|
|
{
|
|
if (!celutil::writeLE<std::int16_t>(out, static_cast<std::int16_t>(group.prim))
|
|
|| !celutil::writeLE<std::uint32_t>(out, group.materialIndex)
|
|
|| !celutil::writeLE<std::uint32_t>(out, group.indices.size()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (auto index : group.indices)
|
|
{
|
|
if (!celutil::writeLE<std::uint32_t>(out, index)) { return false; }
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool
|
|
BinaryModelWriter::writeMesh(const Mesh& mesh)
|
|
{
|
|
if (!writeToken(out, CMOD_Mesh)
|
|
|| !writeVertexDescription(mesh.getVertexDescription())
|
|
|| !writeVertices(mesh.getVertexData(),
|
|
mesh.getVertexCount(),
|
|
mesh.getVertexStrideWords(),
|
|
mesh.getVertexDescription()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (unsigned int groupIndex = 0; mesh.getGroup(groupIndex) != nullptr; groupIndex++)
|
|
{
|
|
if (!writeGroup(*mesh.getGroup(groupIndex))) { return false; }
|
|
}
|
|
|
|
return writeToken(out, CMOD_EndMesh);
|
|
}
|
|
|
|
|
|
bool
|
|
BinaryModelWriter::writeVertices(const VWord* vertexData,
|
|
unsigned int nVertices,
|
|
unsigned int strideWords,
|
|
const VertexDescription& desc)
|
|
{
|
|
if (!writeToken(out, CMOD_Vertices) || !celutil::writeLE<std::uint32_t>(out, nVertices))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (unsigned int i = 0; i < nVertices; i++, vertexData += strideWords)
|
|
{
|
|
for (const auto& attr : desc.attributes)
|
|
{
|
|
const VWord* cdata = vertexData + attr.offsetWords;
|
|
float fdata[4];
|
|
|
|
bool result;
|
|
switch (attr.format)
|
|
{
|
|
case VertexAttributeFormat::Float1:
|
|
std::memcpy(fdata, cdata, sizeof(float));
|
|
result = celutil::writeLE<float>(out, fdata[0]);
|
|
break;
|
|
case VertexAttributeFormat::Float2:
|
|
std::memcpy(fdata, cdata, sizeof(float) * 2);
|
|
result = celutil::writeLE<float>(out, fdata[0])
|
|
&& celutil::writeLE<float>(out, fdata[1]);
|
|
break;
|
|
case VertexAttributeFormat::Float3:
|
|
std::memcpy(fdata, cdata, sizeof(float) * 3);
|
|
result = celutil::writeLE<float>(out, fdata[0])
|
|
&& celutil::writeLE<float>(out, fdata[1])
|
|
&& celutil::writeLE<float>(out, fdata[2]);
|
|
break;
|
|
case VertexAttributeFormat::Float4:
|
|
std::memcpy(fdata, cdata, sizeof(float) * 4);
|
|
result = celutil::writeLE<float>(out, fdata[0])
|
|
&& celutil::writeLE<float>(out, fdata[1])
|
|
&& celutil::writeLE<float>(out, fdata[2])
|
|
&& celutil::writeLE<float>(out, fdata[3]);
|
|
break;
|
|
case VertexAttributeFormat::UByte4:
|
|
result = celutil::writeNative<std::uint32_t>(out, *cdata);
|
|
break;
|
|
default:
|
|
assert(0);
|
|
result = false;
|
|
break;
|
|
}
|
|
|
|
if (!result) { return false; }
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool
|
|
BinaryModelWriter::writeVertexDescription(const VertexDescription& desc)
|
|
{
|
|
if (!writeToken(out, CMOD_VertexDesc)) { return false; }
|
|
|
|
for (const auto& attr : desc.attributes)
|
|
{
|
|
if (!celutil::writeLE<std::int16_t>(out, static_cast<std::int16_t>(attr.semantic))
|
|
|| !celutil::writeLE<std::int16_t>(out, static_cast<std::int16_t>(attr.format)))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return writeToken(out, CMOD_EndVertexDesc);
|
|
}
|
|
|
|
|
|
bool
|
|
BinaryModelWriter::writeMaterial(const Material& material)
|
|
{
|
|
if (!writeToken(out, CMOD_Material)) { return false; }
|
|
|
|
if (material.diffuse != DefaultDiffuse
|
|
&& (!writeToken(out, CMOD_Diffuse) || !writeTypeColor(out, material.diffuse)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (material.emissive != DefaultEmissive
|
|
&& (!writeToken(out, CMOD_Emissive) || !writeTypeColor(out, material.emissive)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (material.specular != DefaultSpecular
|
|
&& (!writeToken(out, CMOD_Specular) || !writeTypeColor(out, material.specular)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (material.specularPower != DefaultSpecularPower
|
|
&& (!writeToken(out, CMOD_SpecularPower) || !writeTypeFloat1(out, material.specularPower)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (material.opacity != DefaultOpacity
|
|
&& (!writeToken(out, CMOD_Opacity) || !writeTypeFloat1(out, material.opacity)))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (material.blend != DefaultBlend
|
|
&& (!writeToken(out, CMOD_Blend)
|
|
|| !celutil::writeLE<std::int16_t>(out, static_cast<std::int16_t>(material.blend))))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < static_cast<int>(TextureSemantic::TextureSemanticMax); i++)
|
|
{
|
|
if (material.maps[i] != InvalidResource)
|
|
{
|
|
fs::path texSource = getSource(material.maps[i]);
|
|
if (!texSource.empty()
|
|
&& (!writeToken(out, CMOD_Texture)
|
|
|| !celutil::writeLE<std::int16_t>(out, i)
|
|
|| !writeTypeString(out, texSource.string())))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return writeToken(out, CMOD_EndMaterial);
|
|
}
|
|
|
|
|
|
std::unique_ptr<ModelLoader>
|
|
OpenModel(std::istream& in, HandleGetter& getHandle)
|
|
{
|
|
char header[CEL_MODEL_HEADER_LENGTH];
|
|
if (!in.read(header, CEL_MODEL_HEADER_LENGTH).good())
|
|
{
|
|
celutil::GetLogger()->error("Could not read model header\n");
|
|
return nullptr;
|
|
}
|
|
|
|
if (std::strncmp(header, CEL_MODEL_HEADER_ASCII, CEL_MODEL_HEADER_LENGTH) == 0)
|
|
{
|
|
return std::make_unique<AsciiModelLoader>(in, getHandle);
|
|
}
|
|
if (std::strncmp(header, CEL_MODEL_HEADER_BINARY, CEL_MODEL_HEADER_LENGTH) == 0)
|
|
{
|
|
return std::make_unique<BinaryModelLoader>(in, getHandle);
|
|
}
|
|
else
|
|
{
|
|
celutil::GetLogger()->error("Model file has invalid header.\n");
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
|
|
} // end unnamed namespace
|
|
|
|
|
|
std::unique_ptr<Model>
|
|
LoadModel(std::istream& in, HandleGetter handleGetter)
|
|
{
|
|
std::unique_ptr<ModelLoader> loader = OpenModel(in, handleGetter);
|
|
if (loader == nullptr)
|
|
return nullptr;
|
|
|
|
std::unique_ptr<Model> model = loader->load();
|
|
if (model == nullptr)
|
|
{
|
|
celutil::GetLogger()->error("Error in model file: {}\n", loader->getErrorMessage());
|
|
}
|
|
|
|
return model;
|
|
}
|
|
|
|
|
|
bool
|
|
SaveModelAscii(const Model* model, std::ostream& out, SourceGetter sourceGetter)
|
|
{
|
|
if (model == nullptr) { return false; }
|
|
AsciiModelWriter writer(out, sourceGetter);
|
|
return writer.write(*model);
|
|
}
|
|
|
|
|
|
bool
|
|
SaveModelBinary(const Model* model, std::ostream& out, SourceGetter sourceGetter)
|
|
{
|
|
if (model == nullptr) { return false; }
|
|
BinaryModelWriter writer(out, sourceGetter);
|
|
return writer.write(*model);
|
|
}
|
|
} // end namespace cmod
|