3
// Copyright (C) 2000, Chris Laurel <claurel@shatters.net>
5
// This program is free software; you can redistribute it and/or
6
// modify it under the terms of the GNU General Public License
7
// as published by the Free Software Foundation; either version 2
8
// of the License, or (at your option) any later version.
23
#include <fmt/format.h>
25
#include <celutil/binaryread.h>
26
#include <celutil/logger.h>
29
namespace util = celestia::util;
34
enum class M3DChunkType : std::uint16_t
41
IntPercentage = 0x0030,
42
FloatPercentage = 0x0031,
45
BackgroundColor = 0x1200,
51
TriangleMesh = 0x4100,
53
PointFlagArray = 0x4111,
55
MeshMaterialGroup = 0x4130,
56
MeshTextureCoords = 0x4140,
57
MeshSmoothGroup = 0x4150,
61
MaterialName = 0xa000,
62
MaterialAmbient = 0xa010,
63
MaterialDiffuse = 0xa020,
64
MaterialSpecular = 0xa030,
65
MaterialShininess = 0xa040,
66
MaterialShin2Pct = 0xa041,
67
MaterialTransparency = 0xa050,
68
MaterialXpfall = 0xa052,
69
MaterialRefblur = 0xa053,
70
MaterialSelfIllum = 0xa084,
71
MaterialWiresize = 0xa087,
72
MaterialXpfallin = 0xa08a,
73
MaterialShading = 0xa100,
74
MaterialTexmap = 0xa200,
75
MaterialMapname = 0xa300,
76
MaterialEntry = 0xafff,
82
constexpr auto chunkHeaderSize = static_cast<std::int32_t>(sizeof(M3DChunkType) + sizeof(std::int32_t));
84
} // end unnamed namespace
88
struct fmt::formatter<M3DChunkType>
90
constexpr auto parse(const format_parse_context& ctx) const -> decltype(ctx.begin()) {
91
// we should validate the format here but exceptions are disabled
95
template<typename FormatContext>
96
auto format(const M3DChunkType& chunkType, FormatContext& ctx) -> decltype(ctx.out()) {
97
return format_to(ctx.out(), "{:04x}", static_cast<std::uint16_t>(chunkType));
105
bool readChunkType(std::istream& in, M3DChunkType& chunkType)
108
if (!util::readLE<std::uint16_t>(in, value)) { return false; }
109
chunkType = static_cast<M3DChunkType>(value);
114
bool readString(std::istream& in, std::int32_t& contentSize, std::string& value)
116
if (contentSize == 0)
122
std::size_t max_length = std::min(64, contentSize);
123
value.resize(max_length);
124
in.getline(value.data(), max_length, '\0');
127
GetLogger()->error("Error occurred reading string\n");
131
value.resize(in.gcount() - 1);
132
contentSize -= in.gcount();
137
bool skipChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize)
139
GetLogger()->debug("Skipping {} bytes of unknown/unexpected chunk type {}\n", contentSize, chunkType);
140
in.ignore(contentSize);
141
if (in.good() || in.eof()) { return true; }
143
GetLogger()->error("Error skipping {} bytes of unknown/unexpected chunk type {}\n", contentSize, chunkType);
148
bool skipTrailing(std::istream& in, std::int32_t contentSize)
152
GetLogger()->error("Negative trailing chunk size {} detected", contentSize);
156
if (contentSize == 0) { return true; }
158
GetLogger()->debug("Skipping {} trailing bytes\n", contentSize);
159
in.ignore(contentSize);
160
if (in.good() || in.eof()) { return true; }
162
GetLogger()->error("Error skipping {} trailing bytes\n", contentSize);
167
template<typename T, typename ProcessFunc>
168
bool readChunks(std::istream& in, std::int32_t contentSize, T& obj, ProcessFunc processChunk)
170
while (contentSize > chunkHeaderSize)
174
GetLogger()->warn("Unexpected EOF detected, stopping processing\n");
178
M3DChunkType chunkType;
179
if (!readChunkType(in, chunkType))
181
GetLogger()->error("Failed to read chunk type\n");
185
GetLogger()->debug("Found chunk type {}\n", chunkType);
187
std::int32_t chunkSize;
188
if (!util::readLE<std::int32_t>(in, chunkSize))
190
GetLogger()->error("Failed to read chunk size\n", chunkType);
193
else if (chunkSize < chunkHeaderSize)
195
GetLogger()->error("Chunk size {} too small to include header\n", chunkSize);
198
else if (chunkSize > contentSize)
200
GetLogger()->error("Chunk size {} exceeds remaining content size {} of outer chunk\n", chunkSize, contentSize);
204
if (!processChunk(in, chunkType, chunkSize - chunkHeaderSize, obj))
206
GetLogger()->debug("Failed to process inner chunk\n");
210
contentSize -= chunkSize;
213
return skipTrailing(in, contentSize);
217
bool readPointArray(std::istream& in, std::int32_t contentSize, M3DTriangleMesh& triMesh)
219
constexpr auto headerSize = static_cast<std::int32_t>(sizeof(std::uint16_t));
220
if (contentSize < headerSize)
222
GetLogger()->error("Content size {} too small to include point array count\n", contentSize);
226
std::uint16_t nPoints;
227
if (!util::readLE<std::uint16_t>(in, nPoints))
229
GetLogger()->error("Failed to read point array count\n");
233
auto pointsCount = static_cast<std::int32_t>(nPoints);
234
std::int32_t expectedSize = headerSize + pointsCount * static_cast<std::int32_t>(3 * sizeof(float));
235
if (contentSize < expectedSize)
237
GetLogger()->error("Content size {} too small to include point array with {} entries", contentSize);
241
for (std::int32_t i = 0; i < pointsCount; ++i)
243
Eigen::Vector3f vertex;
244
if (!util::readLE<float>(in, vertex.x())
245
|| !util::readLE<float>(in, vertex.y())
246
|| !util::readLE<float>(in, vertex.z()))
248
GetLogger()->error("Failed to read entry {} of point array\n", i);
252
triMesh.addVertex(vertex);
255
return skipTrailing(in, contentSize - expectedSize);
259
bool readTextureCoordArray(std::istream& in, std::int32_t contentSize, M3DTriangleMesh& triMesh)
261
constexpr auto headerSize = static_cast<std::int32_t>(sizeof(std::uint16_t));
262
if (contentSize < headerSize)
264
GetLogger()->error("Content size {} too small to include texture coord array count\n", contentSize);
268
std::uint16_t nTexCoords;
269
if (!util::readLE<std::uint16_t>(in, nTexCoords))
271
GetLogger()->error("Failed to read texture coord array count\n");
275
auto texCoordsCount = static_cast<std::int32_t>(nTexCoords);
276
std::int32_t expectedSize = headerSize + texCoordsCount * static_cast<std::int32_t>(2 * sizeof(float));
277
if (contentSize < expectedSize)
279
GetLogger()->error("Content size {} too small to include texture coord array with {} entries\n", contentSize, nTexCoords);
283
for (std::int32_t i = 0; i < texCoordsCount; ++i)
285
Eigen::Vector2f texCoord;
286
if (!util::readLE<float>(in, texCoord.x())
287
|| !util::readLE<float>(in, texCoord.y()))
289
GetLogger()->error("Failed to read entry {} of texture coord array\n", i);
293
texCoord.y() = -texCoord.y();
294
triMesh.addTexCoord(texCoord);
297
return skipTrailing(in, contentSize - expectedSize);
301
bool readMeshMaterialGroup(std::istream& in, std::int32_t contentSize, M3DTriangleMesh& triMesh)
303
M3DMeshMaterialGroup matGroup;
304
if (!readString(in, contentSize, matGroup.materialName)) { return false; }
305
constexpr auto headerSize = static_cast<std::int32_t>(sizeof(std::uint16_t));
306
if (contentSize < headerSize)
308
GetLogger()->error("Remaining content size {} too small to include material group face array count\n", contentSize);
312
std::uint16_t nFaces;
313
if (!util::readLE<std::uint16_t>(in, nFaces))
315
GetLogger()->error("Failed to read material group face array count\n");
319
auto faceCount = static_cast<std::int32_t>(nFaces);
320
std::int32_t expectedSize = headerSize + faceCount * static_cast<std::int32_t>(sizeof(std::uint16_t));
321
if (contentSize < expectedSize)
323
GetLogger()->error("Remaining content size {} too small to include material group face array with {} entries\n", contentSize, nFaces);
327
for (std::int32_t i = 0; i < faceCount; ++i)
329
std::uint16_t faceIndex;
330
if (!util::readLE(in, faceIndex))
332
GetLogger()->error("Failed to read entry {} of material group face array\n", i);
336
matGroup.faces.push_back(faceIndex);
339
triMesh.addMeshMaterialGroup(std::move(matGroup));
340
return skipTrailing(in, contentSize - expectedSize);
344
bool readMeshSmoothGroup(std::istream& in, std::int32_t contentSize, M3DTriangleMesh& triMesh)
346
auto faceCount = static_cast<std::int32_t>(triMesh.getFaceCount());
347
std::int32_t expectedSize = faceCount * static_cast<std::int32_t>(sizeof(std::int32_t));
348
if (contentSize < expectedSize)
350
GetLogger()->error("Content size {} too small to include smoothing group array with {} entries\n", contentSize, faceCount);
354
for (std::int32_t i = 0; i < faceCount; ++i)
356
std::uint32_t groups;
357
if (!util::readLE<std::uint32_t>(in, groups))
359
GetLogger()->error("Failed to read entry {} of smoothing group array\n", i);
363
triMesh.addSmoothingGroups(groups);
366
return skipTrailing(in, contentSize - expectedSize);
370
bool processFaceArrayChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, M3DTriangleMesh& triMesh)
374
case M3DChunkType::MeshMaterialGroup:
375
GetLogger()->debug("Processing MeshMaterialGroup chunk\n");
376
return readMeshMaterialGroup(in, contentSize, triMesh);
378
case M3DChunkType::MeshSmoothGroup:
379
GetLogger()->debug("Processing MeshSmoothGroup chunk\n");
380
return readMeshSmoothGroup(in, contentSize, triMesh);
383
return skipChunk(in, chunkType, contentSize);
388
bool readFaceArray(std::istream& in, std::int32_t contentSize, M3DTriangleMesh& triMesh)
390
constexpr auto headerSize = static_cast<std::int32_t>(sizeof(std::uint16_t));
391
if (contentSize < headerSize)
393
GetLogger()->error("Content size {} too small to include face array count\n", contentSize);
397
std::uint16_t nFaces;
398
if (!util::readLE<std::uint16_t>(in, nFaces))
400
GetLogger()->error("Failed to read face array count\n");
404
auto faceCount = static_cast<std::int32_t>(nFaces);
405
std::int32_t expectedSize = headerSize + faceCount * static_cast<std::int32_t>(4 * sizeof(std::uint16_t));
406
if (contentSize < expectedSize)
408
GetLogger()->error("Content size {} too small to include face array with {} entries\n", contentSize, nFaces);
412
for (std::int32_t i = 0; i < faceCount; ++i)
414
std::uint16_t v0, v1, v2, flags;
415
if (!util::readLE<std::uint16_t>(in, v0)
416
|| !util::readLE<std::uint16_t>(in, v1)
417
|| !util::readLE<std::uint16_t>(in, v2)
418
|| !util::readLE<std::uint16_t>(in, flags))
420
GetLogger()->error("Failed to read entry {} of face array\n", i);
424
triMesh.addFace(v0, v1, v2);
427
if (expectedSize < contentSize)
429
return readChunks(in, contentSize - expectedSize, triMesh, processFaceArrayChunk);
436
bool readMeshMatrix(std::istream& in, std::int32_t contentSize, M3DTriangleMesh& triMesh)
438
constexpr auto expectedSize = static_cast<std::int32_t>(sizeof(float) * 12);
439
if (contentSize < expectedSize)
441
GetLogger()->error("Content size {} too small to include mesh matrix\n", contentSize);
445
std::array<float, 12> elements;
446
for (std::size_t i = 0; i < 12; ++i)
448
if (!util::readLE<float>(in, elements[i]))
450
GetLogger()->error("Failed to read element {} of mesh matrix\n", i);
455
Eigen::Matrix4f matrix;
456
matrix << elements[0], elements[1], elements[2], 0,
457
elements[3], elements[4], elements[5], 0,
458
elements[6], elements[7], elements[8], 0,
459
elements[9], elements[10], elements[11], 1;
460
triMesh.setMatrix(matrix);
462
return skipTrailing(in, contentSize - expectedSize);
466
bool processTriangleMeshChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, M3DTriangleMesh& triMesh)
470
case M3DChunkType::PointArray:
471
GetLogger()->debug("Processing PointArray chunk\n");
472
return readPointArray(in, contentSize, triMesh);
474
case M3DChunkType::MeshTextureCoords:
475
GetLogger()->debug("Processing MeshTextureCoords chunk\n");
476
return readTextureCoordArray(in, contentSize, triMesh);
478
case M3DChunkType::FaceArray:
479
GetLogger()->debug("Processing FaceArray chunk\n");
480
return readFaceArray(in, contentSize, triMesh);
482
case M3DChunkType::MeshMatrix:
483
GetLogger()->debug("Processing MeshMatrix chunk\n");
484
return readMeshMatrix(in, contentSize, triMesh);
487
return skipChunk(in, chunkType, contentSize);
492
bool processModelChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, M3DModel& model)
494
if (chunkType != M3DChunkType::TriangleMesh)
496
return skipChunk(in, chunkType, contentSize);
499
GetLogger()->debug("Processing TriangleMesh chunk\n");
500
M3DTriangleMesh triMesh;
501
if (!readChunks(in, contentSize, triMesh, processTriangleMeshChunk)) { return false; }
502
model.addTriMesh(std::move(triMesh));
507
bool readColor24(std::istream& in, std::int32_t contentSize, M3DColor& color)
511
GetLogger()->warn("Content size {} too small to include 24-bit color\n", contentSize);
512
return skipTrailing(in, contentSize);
515
std::array<char, 3> rgb;
516
if (!in.read(rgb.data(), rgb.size()).good())
518
GetLogger()->error("Error reading Color24 RGB values");
522
color.red = static_cast<float>(static_cast<std::uint8_t>(rgb[0])) / 255.0f;
523
color.green = static_cast<float>(static_cast<std::uint8_t>(rgb[1])) / 255.0f;
524
color.blue = static_cast<float>(static_cast<std::uint8_t>(rgb[2])) / 255.0f;
526
return skipTrailing(in, contentSize - 3);
530
bool readColorFloat(std::istream& in, std::int32_t contentSize, M3DColor& color)
532
constexpr auto expectedSize = static_cast<std::int32_t>(sizeof(float) * 3);
533
GetLogger()->debug("Processing ColorFloat chunk\n");
534
if (contentSize < expectedSize)
536
GetLogger()->warn("Content size {} too small to include float color\n", contentSize);
537
return skipTrailing(in, contentSize);
540
if (!util::readLE<float>(in, color.red)
541
|| !util::readLE<float>(in, color.green)
542
|| !util::readLE<float>(in, color.blue))
544
GetLogger()->error("Error reading ColorFloat RGB values");
548
return skipTrailing(in, contentSize - expectedSize);
552
bool processColorChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, M3DColor& color)
556
case M3DChunkType::Color24:
557
GetLogger()->debug("Processing Color24 chunk\n");
558
return readColor24(in, contentSize, color);
560
case M3DChunkType::ColorFloat:
561
GetLogger()->debug("Processing ColorFloat chunk\n");
562
return readColorFloat(in, contentSize, color);
565
GetLogger()->warn("Unknown color chunk type {}\n", chunkType);
566
return skipChunk(in, chunkType, contentSize);
571
bool readIntPercentage(std::istream& in, std::int32_t contentSize, float& percentage)
573
constexpr auto expectedSize = static_cast<std::int32_t>(sizeof(std::int16_t));
574
if (contentSize < expectedSize)
576
GetLogger()->warn("Content size {} too small to include integer perecentage\n", contentSize);
577
return skipTrailing(in, contentSize);
581
if (!util::readLE<std::int16_t>(in, value))
583
GetLogger()->error("Error reading IntPercentage\n");
587
percentage = static_cast<float>(value);
588
return skipTrailing(in, contentSize - expectedSize);
592
bool readFloatPercentage(std::istream& in, std::int32_t contentSize, float& percentage)
594
constexpr auto expectedSize = static_cast<std::int32_t>(sizeof(float));
595
if (contentSize < expectedSize)
597
GetLogger()->warn("Content size {} too small to include float percentage\n", contentSize);
598
return skipTrailing(in, contentSize);
601
if (!util::readLE<float>(in, percentage))
603
GetLogger()->error("Error reading FloatPercentage\n");
607
return skipTrailing(in, contentSize - expectedSize);
611
bool processPercentageChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, float& percentage)
615
case M3DChunkType::IntPercentage:
616
GetLogger()->debug("Processing IntPercentage chunk\n");
617
return readIntPercentage(in, contentSize, percentage);
619
case M3DChunkType::FloatPercentage:
620
GetLogger()->debug("Processing FloatPercentage chunk\n");
621
readFloatPercentage(in, contentSize, percentage);
624
GetLogger()->warn("Unknown percentage {}\n", chunkType);
625
return skipChunk(in, chunkType, contentSize);
630
bool processTexmapChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, M3DMaterial& material)
632
if (chunkType != M3DChunkType::MaterialMapname)
634
return skipChunk(in, chunkType, contentSize);
637
GetLogger()->debug("Processing MaterialMapname chunk\n");
639
if (!readString(in, contentSize, name)) { return false; }
640
material.setTextureMap(std::move(name));
641
return skipTrailing(in, contentSize);
645
bool readMaterialName(std::istream& in, std::int32_t contentSize, M3DMaterial& material)
648
if (!readString(in, contentSize, name)) { return false; }
649
material.setName(std::move(name));
650
return skipTrailing(in, contentSize);
654
template<typename Setter>
655
bool readMaterialColor(std::istream& in, std::int32_t contentSize, Setter setter)
658
if (!readChunks(in, contentSize, color, processColorChunk)) { return false; }
664
template<typename Setter>
665
bool readMaterialPercentage(std::istream& in, std::int32_t contentSize, Setter setter)
667
float percentage = 0.0f;
668
if (!readChunks(in, contentSize, percentage, processPercentageChunk)) { return false; }
674
bool processMaterialChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, M3DMaterial& material)
678
case M3DChunkType::MaterialName:
679
GetLogger()->debug("Processing MaterialName chunk\n");
680
return readMaterialName(in, contentSize, material);
682
case M3DChunkType::MaterialAmbient:
683
GetLogger()->debug("Processing MaterialAmbient chunk\n");
684
return readMaterialColor(in, contentSize, [&material](M3DColor color) { material.setAmbientColor(color); });
686
case M3DChunkType::MaterialDiffuse:
687
GetLogger()->debug("Processing MaterialDiffuse chunk\n");
688
return readMaterialColor(in, contentSize, [&material](M3DColor color) { material.setDiffuseColor(color); });
690
case M3DChunkType::MaterialSpecular:
691
GetLogger()->debug("Processing MaterialSpecular chunk\n");
692
return readMaterialColor(in, contentSize, [&material](M3DColor color) { material.setSpecularColor(color); });
694
case M3DChunkType::MaterialShininess:
695
GetLogger()->debug("Processing MaterialShininess chunk\n");
696
return readMaterialPercentage(in, contentSize, [&material](float percentage) { material.setShininess(percentage); });
698
case M3DChunkType::MaterialTransparency:
699
GetLogger()->debug("Processing MaterialTransparency chunk\n");
700
return readMaterialPercentage(in, contentSize,
701
[&material](float percentage) { material.setOpacity(1.0f - percentage / 100.0f); });
703
case M3DChunkType::MaterialTexmap:
704
GetLogger()->debug("Processing MaterialTexmap chunk\n");
705
return readChunks(in, contentSize, material, processTexmapChunk);
708
return skipChunk(in, chunkType, contentSize);
713
bool readNamedObject(std::istream& in, std::int32_t contentSize, M3DScene& scene)
716
if (!readString(in, contentSize, name)) { return false; }
718
model.setName(std::move(name));
719
if (!readChunks(in, contentSize, model, processModelChunk)) { return false; }
720
scene.addModel(std::move(model));
725
bool readMaterialEntry(std::istream& in, std::int32_t contentSize, M3DScene& scene)
727
M3DMaterial material;
728
if (!readChunks(in, contentSize, material, processMaterialChunk)) { return false; }
729
scene.addMaterial(std::move(material));
734
bool readBackgroundColor(std::istream& in, std::int32_t contentSize, M3DScene& scene)
737
if (!readChunks(in, contentSize, color, processColorChunk)) { return false; }
738
scene.setBackgroundColor(color);
743
bool processMeshdataChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, M3DScene& scene)
747
case M3DChunkType::NamedObject:
748
GetLogger()->debug("Processing NamedObject chunk\n");
749
return readNamedObject(in, contentSize, scene);
751
case M3DChunkType::MaterialEntry:
752
GetLogger()->debug("Processing MaterialEntry chunk\n");
753
return readMaterialEntry(in, contentSize, scene);
755
case M3DChunkType::BackgroundColor:
756
GetLogger()->debug("Processing BackgroundColor chunk\n");
757
return readBackgroundColor(in, contentSize, scene);
760
return skipChunk(in, chunkType, contentSize);
765
bool processTopLevelChunk(std::istream& in, M3DChunkType chunkType, std::int32_t contentSize, M3DScene& scene)
767
if (chunkType != M3DChunkType::Meshdata)
769
return skipChunk(in, chunkType, contentSize);
772
GetLogger()->debug("Processing Meshdata chunk\n");
773
return readChunks(in, contentSize, scene, processMeshdataChunk);
776
} // end unnamed namespace
779
std::unique_ptr<M3DScene> Read3DSFile(std::istream& in)
781
if (M3DChunkType chunkType; !readChunkType(in, chunkType) || chunkType != M3DChunkType::Magic)
783
GetLogger()->error("Read3DSFile: Wrong magic number in header\n");
787
std::int32_t chunkSize;
788
if (!util::readLE<std::int32_t>(in, chunkSize) || chunkSize < chunkHeaderSize)
790
GetLogger()->error("Read3DSFile: Error reading 3DS file top level chunk size\n");
794
GetLogger()->verbose("3DS file, {} bytes\n", chunkSize + chunkHeaderSize);
796
auto scene = std::make_unique<M3DScene>();
797
if (!readChunks(in, chunkSize - chunkHeaderSize, *scene, processTopLevelChunk))
806
std::unique_ptr<M3DScene> Read3DSFile(const fs::path& filename)
808
std::ifstream in(filename, std::ios::in | std::ios::binary);
811
GetLogger()->error("Read3DSFile: Error opening {}\n", filename);
815
std::unique_ptr<M3DScene> scene = Read3DSFile(in);