30#include <unordered_map>
39#include "spdlog/spdlog.h"
56 static_assert(
sizeof(
PackedVertex) == 56,
"PackedVertex must be 56 bytes (14 floats)");
70 void tangentFromNormal(
float nx,
float ny,
float nz,
71 float& tx,
float& ty,
float& tz,
float& tw)
84 Vector3 computeFaceNormal(
float v0x,
float v0y,
float v0z,
85 float v1x,
float v1y,
float v1z,
86 float v2x,
float v2y,
float v2z)
88 Vector3 e1(v1x - v0x, v1y - v0y, v1z - v0z);
89 Vector3 e2(v2x - v0x, v2y - v0y, v2z - v0z);
93 return n * (1.0f / len);
95 return Vector3(0.0f, 1.0f, 0.0f);
100 bool isBinaryStl(
const std::vector<uint8_t>& data)
102 if (data.size() < 84)
return false;
105 uint32_t triCount = 0;
106 std::memcpy(&triCount, data.data() + 80,
sizeof(uint32_t));
109 const size_t expectedSize = 80 + 4 +
static_cast<size_t>(triCount) * 50;
110 return data.size() == expectedSize;
115 bool parseBinaryStl(
const std::vector<uint8_t>& data,
116 std::vector<StlTriangle>& triangles,
117 std::string& solidName)
119 if (data.size() < 84) {
120 spdlog::error(
"STL binary file too small ({} bytes)", data.size());
125 solidName.assign(
reinterpret_cast<const char*
>(data.data()), 80);
127 auto end = solidName.find_last_not_of(std::string(
"\0 \t\r\n", 5));
128 solidName = (end != std::string::npos) ? solidName.substr(0, end + 1) :
"";
130 if (solidName.size() >= 6 && solidName.substr(0, 6) ==
"solid ") {
131 solidName = solidName.substr(6);
134 uint32_t triCount = 0;
135 std::memcpy(&triCount, data.data() + 80,
sizeof(uint32_t));
137 const size_t expectedSize = 80 + 4 +
static_cast<size_t>(triCount) * 50;
138 if (data.size() < expectedSize) {
139 spdlog::error(
"STL binary file truncated: expected {} bytes, got {}", expectedSize, data.size());
143 triangles.resize(triCount);
144 const uint8_t* ptr = data.data() + 84;
146 for (uint32_t i = 0; i < triCount; ++i) {
148 std::memcpy(&triangles[i], ptr, 48);
157 bool parseAsciiStl(
const std::vector<uint8_t>& data,
158 std::vector<StlTriangle>& triangles,
159 std::string& solidName)
161 std::string text(
reinterpret_cast<const char*
>(data.data()), data.size());
162 std::istringstream stream(text);
167 if (token !=
"solid") {
168 spdlog::error(
"STL ASCII: expected 'solid', got '{}'", token);
171 std::getline(stream, solidName);
173 auto start = solidName.find_first_not_of(
" \t\r\n");
174 auto end = solidName.find_last_not_of(
" \t\r\n");
175 solidName = (start != std::string::npos) ? solidName.substr(start, end - start + 1) :
"";
177 while (stream >> token) {
178 if (token ==
"endsolid")
break;
180 if (token ==
"facet") {
185 stream >> tri.nx >> tri.ny >> tri.nz;
188 stream >> token >> token;
191 stream >> token >> tri.v0x >> tri.v0y >> tri.v0z;
192 stream >> token >> tri.v1x >> tri.v1y >> tri.v1z;
193 stream >> token >> tri.v2x >> tri.v2y >> tri.v2z;
201 triangles.push_back(tri);
205 return !triangles.empty();
214 bool operator==(
const PositionKey& o)
const
216 return ix == o.ix && iy == o.iy && iz == o.iz;
220 struct PositionKeyHash
222 size_t operator()(
const PositionKey& k)
const
224 size_t h = std::hash<int32_t>()(k.ix);
225 h ^= std::hash<int32_t>()(k.iy) + 0x9e3779b9 + (h << 6) + (h >> 2);
226 h ^= std::hash<int32_t>()(k.iz) + 0x9e3779b9 + (h << 6) + (h >> 2);
231 PositionKey quantize(
float x,
float y,
float z,
float invEpsilon)
234 static_cast<int32_t
>(std::round(x * invEpsilon)),
235 static_cast<int32_t
>(std::round(y * invEpsilon)),
236 static_cast<int32_t
>(std::round(z * invEpsilon))
243 const std::vector<StlTriangle>& triangles,
245 std::vector<PackedVertex>& outVertices,
246 std::vector<uint32_t>& outIndices,
247 float& minX,
float& minY,
float& minZ,
248 float& maxX,
float& maxY,
float& maxZ)
250 const size_t triCount = triangles.size();
251 outVertices.reserve(triCount * 3);
252 outIndices.reserve(triCount * 3);
254 for (
size_t i = 0; i < triCount; ++i) {
255 const auto& tri = triangles[i];
258 float positions[3][3] = {
259 {tri.v0x * config.uniformScale, tri.v0y * config.uniformScale, tri.v0z * config.uniformScale},
260 {tri.v1x * config.uniformScale, tri.v1y * config.uniformScale, tri.v1z * config.uniformScale},
261 {tri.v2x * config.uniformScale, tri.v2y * config.uniformScale, tri.v2z * config.uniformScale}
266 for (
auto& p : positions) {
267 std::swap(p[1], p[2]);
273 Vector3 faceN = computeFaceNormal(
274 positions[0][0], positions[0][1], positions[0][2],
275 positions[1][0], positions[1][1], positions[1][2],
276 positions[2][0], positions[2][1], positions[2][2]);
278 float fnx = faceN.getX();
279 float fny = faceN.getY();
280 float fnz = faceN.getZ();
283 float tx, ty, tz, tw;
284 if (config.generateTangents) {
285 tangentFromNormal(fnx, fny, fnz, tx, ty, tz, tw);
287 tx = 1.0f; ty = 0.0f; tz = 0.0f; tw = 1.0f;
291 int order[3] = {0, 1, 2};
292 if (config.flipWinding) {
293 std::swap(order[1], order[2]);
297 for (
int j = 0; j < 3; ++j) {
300 vert.px = positions[oi][0];
301 vert.py = positions[oi][1];
302 vert.pz = positions[oi][2];
315 auto idx =
static_cast<uint32_t
>(outVertices.size());
316 outVertices.push_back(vert);
317 outIndices.push_back(idx);
319 minX = std::min(minX, vert.px);
320 minY = std::min(minY, vert.py);
321 minZ = std::min(minZ, vert.pz);
322 maxX = std::max(maxX, vert.px);
323 maxY = std::max(maxY, vert.py);
324 maxZ = std::max(maxZ, vert.pz);
331 void buildSmoothMesh(
332 const std::vector<StlTriangle>& triangles,
334 std::vector<PackedVertex>& outVertices,
335 std::vector<uint32_t>& outIndices,
336 float& minX,
float& minY,
float& minZ,
337 float& maxX,
float& maxY,
float& maxZ)
339 const size_t triCount = triangles.size();
340 const float cosCrease = std::cos(config.creaseAngle * 3.14159265358979f / 180.0f);
343 struct TransformedTri {
348 std::vector<TransformedTri> tris(triCount);
350 for (
size_t i = 0; i < triCount; ++i) {
351 const auto& src = triangles[i];
353 tris[i].pos[0][0] = src.v0x * config.uniformScale;
354 tris[i].pos[0][1] = src.v0y * config.uniformScale;
355 tris[i].pos[0][2] = src.v0z * config.uniformScale;
356 tris[i].pos[1][0] = src.v1x * config.uniformScale;
357 tris[i].pos[1][1] = src.v1y * config.uniformScale;
358 tris[i].pos[1][2] = src.v1z * config.uniformScale;
359 tris[i].pos[2][0] = src.v2x * config.uniformScale;
360 tris[i].pos[2][1] = src.v2y * config.uniformScale;
361 tris[i].pos[2][2] = src.v2z * config.uniformScale;
364 for (
auto& p : tris[i].pos) {
365 std::swap(p[1], p[2]);
370 tris[i].faceNormal = computeFaceNormal(
371 tris[i].pos[0][0], tris[i].pos[0][1], tris[i].pos[0][2],
372 tris[i].pos[1][0], tris[i].pos[1][1], tris[i].pos[1][2],
373 tris[i].pos[2][0], tris[i].pos[2][1], tris[i].pos[2][2]);
379 const float epsilon = 1e-5f;
380 const float invEpsilon = 1.0f / epsilon;
384 std::unordered_map<PositionKey, uint32_t, PositionKeyHash> weldMap;
386 struct WeldedVertex {
388 std::vector<std::pair<size_t, int>> incidents;
390 std::vector<WeldedVertex> welded;
393 std::vector<std::array<uint32_t, 3>> triWeldedIdx(triCount);
395 for (
size_t i = 0; i < triCount; ++i) {
396 for (
int c = 0; c < 3; ++c) {
397 float px = tris[i].pos[c][0];
398 float py = tris[i].pos[c][1];
399 float pz = tris[i].pos[c][2];
401 PositionKey key = quantize(px, py, pz, invEpsilon);
402 auto it = weldMap.find(key);
404 if (it != weldMap.end()) {
407 wIdx =
static_cast<uint32_t
>(welded.size());
409 wv.px = px; wv.py = py; wv.pz = pz;
410 welded.push_back(std::move(wv));
413 welded[wIdx].incidents.emplace_back(i, c);
414 triWeldedIdx[i][c] = wIdx;
418 spdlog::debug(
"STL smooth normals: {} triangles, {} raw vertices, {} welded vertices",
419 triCount, triCount * 3, welded.size());
429 std::vector<Vector3> cornerNormals(triCount * 3,
Vector3(0.0f, 1.0f, 0.0f));
431 for (
auto& wv : welded) {
432 if (wv.incidents.empty())
continue;
434 if (wv.incidents.size() == 1) {
436 auto [ti, ci] = wv.incidents[0];
437 cornerNormals[ti * 3 + ci] = tris[ti].faceNormal;
443 Vector3 accumulated{0.0f, 0.0f, 0.0f};
444 std::vector<std::pair<size_t, int>> members;
446 std::vector<SmoothGroup> groups;
448 for (
auto [ti, ci] : wv.incidents) {
449 const Vector3& fn = tris[ti].faceNormal;
450 bool assigned =
false;
452 for (
auto& group : groups) {
455 float dot = groupDir.
dot(fn);
456 if (dot >= cosCrease) {
460 group.accumulated += fn;
461 group.members.emplace_back(ti, ci);
468 SmoothGroup newGroup;
469 newGroup.accumulated = fn;
470 newGroup.members.emplace_back(ti, ci);
471 groups.push_back(std::move(newGroup));
476 for (
auto& group : groups) {
478 for (
auto [ti, ci] : group.members) {
479 cornerNormals[ti * 3 + ci] = smoothN;
491 int16_t qnx, qny, qnz;
493 bool operator==(
const VertexKey& o)
const {
494 return weldedIdx == o.weldedIdx &&
495 qnx == o.qnx && qny == o.qny && qnz == o.qnz;
499 struct VertexKeyHash {
500 size_t operator()(
const VertexKey& k)
const {
501 size_t h = std::hash<uint32_t>()(k.weldedIdx);
502 h ^= std::hash<int16_t>()(k.qnx) + 0x9e3779b9 + (h << 6) + (h >> 2);
503 h ^= std::hash<int16_t>()(k.qny) + 0x9e3779b9 + (h << 6) + (h >> 2);
504 h ^= std::hash<int16_t>()(k.qnz) + 0x9e3779b9 + (h << 6) + (h >> 2);
509 auto quantizeNormal = [](
float v) -> int16_t {
510 return static_cast<int16_t
>(std::round(v * 32767.0f));
513 std::unordered_map<VertexKey, uint32_t, VertexKeyHash> vertexMap;
514 outVertices.reserve(welded.size());
515 outIndices.reserve(triCount * 3);
517 for (
size_t i = 0; i < triCount; ++i) {
518 int order[3] = {0, 1, 2};
519 if (config.flipWinding) {
520 std::swap(order[1], order[2]);
523 for (
int j = 0; j < 3; ++j) {
525 uint32_t wIdx = triWeldedIdx[i][c];
526 const Vector3& n = cornerNormals[i * 3 + c];
529 quantizeNormal(n.getX()),
530 quantizeNormal(n.getY()),
531 quantizeNormal(n.getZ())};
533 auto it = vertexMap.find(key);
534 if (it != vertexMap.end()) {
535 outIndices.push_back(it->second);
537 auto idx =
static_cast<uint32_t
>(outVertices.size());
539 float nx = n.getX(), ny = n.getY(), nz = n.getZ();
540 float tx, ty, tz, tw;
541 if (config.generateTangents) {
542 tangentFromNormal(nx, ny, nz, tx, ty, tz, tw);
544 tx = 1.0f; ty = 0.0f; tz = 0.0f; tw = 1.0f;
547 const auto& wv = welded[wIdx];
550 vert.px = wv.px; vert.py = wv.py; vert.pz = wv.pz;
551 vert.nx = nx; vert.ny = ny; vert.nz = nz;
552 vert.u = 0.0f; vert.v = 0.0f;
553 vert.tx = tx; vert.ty = ty; vert.tz = tz; vert.tw = tw;
554 vert.u1 = 0.0f; vert.v1 = 0.0f;
556 outVertices.push_back(vert);
557 vertexMap[key] = idx;
558 outIndices.push_back(idx);
560 minX = std::min(minX, vert.px);
561 minY = std::min(minY, vert.py);
562 minZ = std::min(minZ, vert.pz);
563 maxX = std::max(maxX, vert.px);
564 maxY = std::max(maxY, vert.py);
565 maxZ = std::max(maxZ, vert.pz);
576 const std::string& path,
577 const std::shared_ptr<GraphicsDevice>& device,
581 spdlog::error(
"STL parse failed: graphics device is null");
586 std::ifstream file(path, std::ios::binary | std::ios::ate);
587 if (!file.is_open()) {
588 spdlog::error(
"STL parse failed: cannot open '{}'", path);
592 const auto fileSize = file.tellg();
594 spdlog::error(
"STL parse failed: empty file '{}'", path);
598 std::vector<uint8_t> data(
static_cast<size_t>(fileSize));
600 file.read(
reinterpret_cast<char*
>(data.data()), fileSize);
604 std::vector<StlTriangle> triangles;
605 std::string solidName;
607 if (isBinaryStl(data)) {
608 if (!parseBinaryStl(data, triangles, solidName)) {
609 spdlog::error(
"STL binary parse failed: '{}'", path);
613 if (!parseAsciiStl(data, triangles, solidName)) {
614 spdlog::error(
"STL ASCII parse failed: '{}'", path);
619 if (triangles.empty()) {
620 spdlog::warn(
"STL file has no triangles: '{}'", path);
624 spdlog::info(
"STL loaded [{}]: {} triangles, format={}, solid='{}'",
625 path, triangles.size(),
626 isBinaryStl(data) ?
"binary" :
"ASCII",
630 std::vector<PackedVertex> vertices;
631 std::vector<uint32_t> indices;
632 float minX = std::numeric_limits<float>::max();
633 float minY = std::numeric_limits<float>::max();
634 float minZ = std::numeric_limits<float>::max();
635 float maxX = std::numeric_limits<float>::lowest();
636 float maxY = std::numeric_limits<float>::lowest();
637 float maxZ = std::numeric_limits<float>::lowest();
640 buildSmoothMesh(triangles, config, vertices, indices,
641 minX, minY, minZ, maxX, maxY, maxZ);
643 buildFlatMesh(triangles, config, vertices, indices,
644 minX, minY, minZ, maxX, maxY, maxZ);
647 if (vertices.empty()) {
648 spdlog::error(
"STL parse produced no vertices: '{}'", path);
652 spdlog::info(
"STL mesh [{}]: {} vertices, {} indices ({}x reduction from raw)",
653 path, vertices.size(), indices.size(),
654 static_cast<float>(triangles.size() * 3) /
static_cast<float>(vertices.size()));
658 constexpr int BYTES_PER_VERTEX =
static_cast<int>(
sizeof(
PackedVertex));
659 auto vertexFormat = std::make_shared<VertexFormat>(BYTES_PER_VERTEX,
true,
false);
661 const int vertexCount =
static_cast<int>(vertices.size());
662 std::vector<uint8_t> vertexBytes(vertices.size() *
sizeof(
PackedVertex));
663 std::memcpy(vertexBytes.data(), vertices.data(), vertexBytes.size());
667 vbOpts.
data = std::move(vertexBytes);
668 auto vb = device->createVertexBuffer(vertexFormat, vertexCount, vbOpts);
670 const int indexCount =
static_cast<int>(indices.size());
673 std::vector<uint8_t> indexBytes;
675 indexBytes.resize(indices.size() *
sizeof(uint16_t));
676 auto* dst =
reinterpret_cast<uint16_t*
>(indexBytes.data());
677 for (
size_t i = 0; i < indices.size(); ++i) {
678 dst[i] =
static_cast<uint16_t
>(indices[i]);
681 indexBytes.resize(indices.size() *
sizeof(uint32_t));
682 std::memcpy(indexBytes.data(), indices.data(), indexBytes.size());
684 auto ib = device->createIndexBuffer(idxFmt, indexCount, indexBytes);
688 auto meshResource = std::make_shared<Mesh>();
689 meshResource->setVertexBuffer(vb);
690 meshResource->setIndexBuffer(ib, 0);
696 prim.
count = indexCount;
698 meshResource->setPrimitive(prim, 0);
702 (minX + maxX) * 0.5f,
703 (minY + maxY) * 0.5f,
704 (minZ + maxZ) * 0.5f);
706 (maxX - minX) * 0.5f,
707 (maxY - minY) * 0.5f,
708 (maxZ - minZ) * 0.5f);
709 meshResource->setAabb(bounds);
713 auto material = std::make_shared<StandardMaterial>();
714 material->setName(solidName.empty() ?
"stl-default" : solidName);
717 material->setMetalness(config.
metalness);
718 material->setMetallicFactor(config.
metalness);
719 material->setGloss(1.0f - config.
roughness);
720 material->setRoughnessFactor(config.
roughness);
721 material->setUseMetalness(
true);
726 auto container = std::make_unique<GlbContainerResource>();
729 payload.
mesh = meshResource;
731 container->addMeshPayload(payload);
735 rootNode.
name = solidName.empty()
736 ? std::filesystem::path(path).stem().string()
740 container->addNodePayload(rootNode);
741 container->addRootNodeIndex(0);
743 spdlog::info(
"STL parse complete [{}]: {} vertices, {} indices, bounds=[{:.3f},{:.3f},{:.3f}]-[{:.3f},{:.3f},{:.3f}]",
744 path, vertices.size(), indices.size(),
745 minX, minY, minZ, maxX, maxY, maxZ);
Axis-Aligned Bounding Box defined by center and half-extents.
void setHalfExtents(const Vector3 &halfExtents)
void setCenter(const Vector3 ¢er)
static std::unique_ptr< GlbContainerResource > parse(const std::string &path, const std::shared_ptr< GraphicsDevice > &device, const StlParserConfig &config=StlParserConfig{})
RGBA color with floating-point components in [0, 1].
std::shared_ptr< Mesh > mesh
std::shared_ptr< Material > material
std::vector< size_t > meshPayloadIndices
Describes how vertex and index data should be interpreted for a draw call.
Configuration options for STL loading.
float diffuseR
Default material diffuse color (STL has no material data).
float metalness
Default material metalness (0 = dielectric, 1 = metal).
bool generateSmoothNormals
float roughness
Default material roughness (0 = mirror, 1 = matte).
3D vector for positions, directions, and normals with multi-backend SIMD acceleration.
Vector3 cross(const Vector3 &other) const
Vector3 normalized() const
float dot(const Vector3 &other) const
std::vector< uint8_t > data