22#include <tiny_obj_loader.h>
29#include <unordered_map>
40#include "spdlog/spdlog.h"
58 static_assert(
sizeof(
PackedVertex) == 56,
"PackedVertex must be 56 bytes (14 floats)");
66 bool operator==(
const VertexKey& o)
const
68 return vi == o.vi && ni == o.ni && ti == o.ti;
74 size_t operator()(
const VertexKey& k)
const
76 size_t h = std::hash<int>()(k.vi);
77 h ^= std::hash<int>()(k.ni) + 0x9e3779b9 + (h << 6) + (h >> 2);
78 h ^= std::hash<int>()(k.ti) + 0x9e3779b9 + (h << 6) + (h >> 2);
85 void generateTangents(std::vector<PackedVertex>& vertices,
const std::vector<uint32_t>& indices)
87 const size_t vertexCount = vertices.size();
88 if (vertexCount == 0)
return;
90 std::vector<Vector3> tan1(vertexCount,
Vector3(0.0f, 0.0f, 0.0f));
91 std::vector<Vector3> tan2(vertexCount,
Vector3(0.0f, 0.0f, 0.0f));
93 auto accumulateTriangle = [&](uint32_t i0, uint32_t i1, uint32_t i2) {
94 if (i0 >= vertexCount || i1 >= vertexCount || i2 >= vertexCount)
return;
96 const auto& v0 = vertices[i0];
97 const auto& v1 = vertices[i1];
98 const auto& v2 = vertices[i2];
100 const float du1 = v1.u - v0.u;
101 const float dv1 = v1.v - v0.v;
102 const float du2 = v2.u - v0.u;
103 const float dv2 = v2.v - v0.v;
105 const float det = du1 * dv2 - dv1 * du2;
106 if (std::abs(det) <= 1e-8f)
return;
108 const float invDet = 1.0f / det;
109 const Vector3 e1(v1.px - v0.px, v1.py - v0.py, v1.pz - v0.pz);
110 const Vector3 e2(v2.px - v0.px, v2.py - v0.py, v2.pz - v0.pz);
112 const Vector3 sdir = (e1 * dv2 - e2 * dv1) * invDet;
113 const Vector3 tdir = (e2 * du1 - e1 * du2) * invDet;
115 tan1[i0] += sdir; tan1[i1] += sdir; tan1[i2] += sdir;
116 tan2[i0] += tdir; tan2[i1] += tdir; tan2[i2] += tdir;
119 if (!indices.empty()) {
120 for (
size_t i = 0; i + 2 < indices.size(); i += 3)
121 accumulateTriangle(indices[i], indices[i + 1], indices[i + 2]);
123 for (uint32_t i = 0; i + 2 <
static_cast<uint32_t
>(vertexCount); i += 3)
124 accumulateTriangle(i, i + 1, i + 2);
127 for (
size_t i = 0; i < vertexCount; ++i) {
128 const Vector3 n(vertices[i].nx, vertices[i].ny, vertices[i].nz);
130 if (t.lengthSquared() <= 1e-8f) {
131 t = std::abs(n.getY()) < 0.999f
132 ? n.cross(
Vector3(0.0f, 1.0f, 0.0f))
133 : n.cross(
Vector3(1.0f, 0.0f, 0.0f));
137 const float handedness = (n.cross(t).dot(tan2[i]) < 0.0f) ? -1.0f : 1.0f;
139 vertices[i].tx = t.getX();
140 vertices[i].ty = t.getY();
141 vertices[i].tz = t.getZ();
142 vertices[i].tw = handedness;
148 void tangentFromNormal(
float nx,
float ny,
float nz,
149 float& tx,
float& ty,
float& tz,
float& tw)
163 void generateSmoothNormals(
164 const tinyobj::attrib_t& attrib,
165 const tinyobj::mesh_t& mesh,
166 std::vector<float>& outNormals)
168 const size_t totalFaceVertices = mesh.indices.size();
169 outNormals.resize(totalFaceVertices * 3, 0.0f);
175 unsigned int smoothingGroup;
176 bool operator==(
const SmoothKey& o)
const {
177 return vertexIndex == o.vertexIndex && smoothingGroup == o.smoothingGroup;
180 struct SmoothKeyHash {
181 size_t operator()(
const SmoothKey& k)
const {
182 size_t h = std::hash<int>()(k.vertexIndex);
183 h ^= std::hash<unsigned int>()(k.smoothingGroup) + 0x9e3779b9 + (h << 6) + (h >> 2);
188 std::unordered_map<SmoothKey, Vector3, SmoothKeyHash> accum;
191 const size_t faceCount = mesh.num_face_vertices.size();
192 std::vector<size_t> faceOffsets(faceCount);
195 for (
size_t f = 0; f < faceCount; ++f) {
196 faceOffsets[f] = offset;
197 offset += mesh.num_face_vertices[f];
202 for (
size_t f = 0; f < faceCount; ++f) {
203 const unsigned int fv = mesh.num_face_vertices[f];
204 if (fv < 3)
continue;
206 const size_t base = faceOffsets[f];
207 const unsigned int sg = (f < mesh.smoothing_group_ids.size())
208 ? mesh.smoothing_group_ids[f] : 0;
211 const auto& i0 = mesh.indices[base + 0];
212 const auto& i1 = mesh.indices[base + 1];
213 const auto& i2 = mesh.indices[base + 2];
215 Vector3 p0(attrib.vertices[3 * i0.vertex_index + 0],
216 attrib.vertices[3 * i0.vertex_index + 1],
217 attrib.vertices[3 * i0.vertex_index + 2]);
218 Vector3 p1(attrib.vertices[3 * i1.vertex_index + 0],
219 attrib.vertices[3 * i1.vertex_index + 1],
220 attrib.vertices[3 * i1.vertex_index + 2]);
221 Vector3 p2(attrib.vertices[3 * i2.vertex_index + 0],
222 attrib.vertices[3 * i2.vertex_index + 1],
223 attrib.vertices[3 * i2.vertex_index + 2]);
225 Vector3 faceNormal = (p1 - p0).cross(p2 - p0);
230 float len = faceNormal.length();
232 faceNormal = faceNormal * (1.0f / len);
234 faceNormal =
Vector3(0.0f, 1.0f, 0.0f);
236 for (
unsigned int j = 0; j < fv; ++j) {
237 outNormals[(base + j) * 3 + 0] = faceNormal.getX();
238 outNormals[(base + j) * 3 + 1] = faceNormal.getY();
239 outNormals[(base + j) * 3 + 2] = faceNormal.getZ();
243 for (
unsigned int j = 0; j < fv; ++j) {
244 int vi = mesh.indices[base + j].vertex_index;
245 SmoothKey key{vi, sg};
246 accum[key] += faceNormal;
252 for (
size_t f = 0; f < faceCount; ++f) {
253 const unsigned int fv = mesh.num_face_vertices[f];
254 const size_t base = faceOffsets[f];
255 const unsigned int sg = (f < mesh.smoothing_group_ids.size())
256 ? mesh.smoothing_group_ids[f] : 0;
258 if (sg == 0)
continue;
260 for (
unsigned int j = 0; j < fv; ++j) {
261 int vi = mesh.indices[base + j].vertex_index;
262 SmoothKey key{vi, sg};
263 auto it = accum.find(key);
264 if (it != accum.end()) {
266 outNormals[(base + j) * 3 + 0] = n.
getX();
267 outNormals[(base + j) * 3 + 1] = n.getY();
268 outNormals[(base + j) * 3 + 2] = n.getZ();
270 outNormals[(base + j) * 3 + 0] = 0.0f;
271 outNormals[(base + j) * 3 + 1] = 1.0f;
272 outNormals[(base + j) * 3 + 2] = 0.0f;
280 std::shared_ptr<Texture> loadObjTexture(
282 const std::string& texname,
283 const std::filesystem::path& basedir,
284 std::unordered_map<std::string, std::shared_ptr<Texture>>& cache)
286 if (texname.empty())
return nullptr;
288 auto it = cache.find(texname);
289 if (it != cache.end())
return it->second;
291 auto texPath = basedir / texname;
292 if (!std::filesystem::exists(texPath)) {
293 spdlog::warn(
"OBJ texture not found: {}", texPath.string());
294 cache[texname] =
nullptr;
299 stbi_set_flip_vertically_on_load(
true);
301 stbi_uc* pixels = stbi_load(texPath.string().c_str(), &w, &h, &comp, 4);
303 spdlog::warn(
"OBJ texture decode failed: {}", texPath.string());
304 cache[texname] =
nullptr;
309 opts.
width =
static_cast<uint32_t
>(w);
310 opts.height =
static_cast<uint32_t
>(h);
312 opts.mipmaps =
false;
318 auto texture = std::make_shared<Texture>(device, opts);
319 const size_t dataSize =
static_cast<size_t>(w) * h * 4;
320 texture->setLevelData(0, pixels, dataSize);
321 stbi_image_free(pixels);
324 spdlog::info(
"OBJ texture loaded: {} ({}x{})", texname, w, h);
325 cache[texname] = texture;
331 float roughnessFromShininess(
float ns)
333 ns = std::clamp(ns, 0.0f, 1000.0f);
334 return 1.0f - std::sqrt(ns / 1000.0f);
337 bool hasPbrData(
const tinyobj::material_t& mat)
339 return mat.roughness > 0.0f || mat.metallic > 0.0f ||
340 !mat.roughness_texname.empty() || !mat.metallic_texname.empty() ||
341 !mat.normal_texname.empty();
344 std::shared_ptr<StandardMaterial> convertMtlMaterial(
345 const tinyobj::material_t& mtl,
347 const std::filesystem::path& basedir,
348 std::unordered_map<std::string, std::shared_ptr<Texture>>& texCache,
349 std::vector<std::shared_ptr<Texture>>& ownedTextures)
351 auto material = std::make_shared<StandardMaterial>();
352 material->setName(mtl.name.empty() ?
"obj-material" : mtl.name);
354 const bool usePbr = hasPbrData(mtl);
358 Color baseColor(mtl.diffuse[0], mtl.diffuse[1], mtl.diffuse[2], mtl.dissolve);
359 material->setDiffuse(baseColor);
360 material->setBaseColorFactor(baseColor);
361 material->setMetalness(mtl.metallic);
362 material->setMetallicFactor(mtl.metallic);
363 material->setGloss(1.0f - mtl.roughness);
364 material->setRoughnessFactor(mtl.roughness);
367 Color diffuse(mtl.diffuse[0], mtl.diffuse[1], mtl.diffuse[2], mtl.dissolve);
368 material->setDiffuse(diffuse);
369 material->setBaseColorFactor(diffuse);
370 material->setSpecular(
Color(mtl.specular[0], mtl.specular[1], mtl.specular[2], 1.0f));
372 float roughness = roughnessFromShininess(mtl.shininess);
373 material->setGloss(1.0f - roughness);
374 material->setRoughnessFactor(roughness);
377 float kdLum = 0.2126f * mtl.diffuse[0] + 0.7152f * mtl.diffuse[1] + 0.0722f * mtl.diffuse[2];
378 float ksLum = 0.2126f * mtl.specular[0] + 0.7152f * mtl.specular[1] + 0.0722f * mtl.specular[2];
379 float metallic = 0.0f;
380 if (kdLum < 0.04f && ksLum > 0.5f) {
382 }
else if (ksLum > 0.25f) {
383 float ksMax = std::max({mtl.specular[0], mtl.specular[1], mtl.specular[2]});
384 float ksMin = std::min({mtl.specular[0], mtl.specular[1], mtl.specular[2]});
385 float sat = (ksMax > 0.001f) ? (ksMax - ksMin) / ksMax : 0.0f;
386 if (sat > 0.2f) metallic = 0.8f;
388 material->setMetalness(metallic);
389 material->setMetallicFactor(metallic);
392 material->setUseMetalness(
true);
393 material->setOpacity(mtl.dissolve);
396 if (mtl.emission[0] > 0.0f || mtl.emission[1] > 0.0f || mtl.emission[2] > 0.0f) {
397 material->setEmissive(
Color(mtl.emission[0], mtl.emission[1], mtl.emission[2], 1.0f));
398 material->setEmissiveFactor(
Color(mtl.emission[0], mtl.emission[1], mtl.emission[2], 1.0f));
402 if (mtl.dissolve < 0.99f) {
404 material->setTransparent(
true);
411 auto loadAndOwn = [&](
const std::string& name) ->
Texture* {
412 auto tex = loadObjTexture(device, name, basedir, texCache);
413 if (tex) ownedTextures.push_back(tex);
418 if (
auto* tex = loadAndOwn(mtl.diffuse_texname)) {
419 material->setDiffuseMap(tex);
420 material->setBaseColorTexture(tex);
421 material->setHasBaseColorTexture(
true);
425 std::string normalTexName = mtl.normal_texname.empty() ? mtl.bump_texname : mtl.normal_texname;
426 if (
auto* tex = loadAndOwn(normalTexName)) {
427 material->setNormalMap(tex);
428 material->setNormalTexture(tex);
429 material->setHasNormalTexture(
true);
430 if (!mtl.bump_texname.empty() && mtl.normal_texname.empty()) {
431 material->setBumpiness(mtl.bump_texopt.bump_multiplier);
432 material->setNormalScale(mtl.bump_texopt.bump_multiplier);
437 if (
auto* tex = loadAndOwn(mtl.ambient_texname)) {
438 material->setAoMap(tex);
439 material->setOcclusionTexture(tex);
440 material->setHasOcclusionTexture(
true);
444 if (
auto* tex = loadAndOwn(mtl.emissive_texname)) {
445 material->setEmissiveMap(tex);
446 material->setEmissiveTexture(tex);
447 material->setHasEmissiveTexture(
true);
451 if (
auto* tex = loadAndOwn(mtl.alpha_texname)) {
452 material->setOpacityMap(tex);
464 const std::string& path,
465 const std::shared_ptr<GraphicsDevice>& device,
469 spdlog::error(
"OBJ parse failed: graphics device is null");
474 const std::filesystem::path objPath(path);
475 const std::filesystem::path basedir = config.
mtlSearchPath.empty()
476 ? objPath.parent_path()
480 tinyobj::ObjReaderConfig readerConfig;
481 readerConfig.triangulate =
true;
482 readerConfig.vertex_color =
true;
483 readerConfig.mtl_search_path = basedir.string();
485 tinyobj::ObjReader reader;
486 if (!reader.ParseFromFile(path, readerConfig)) {
487 spdlog::error(
"OBJ parse failed [{}]: {}", path, reader.Error());
490 if (!reader.Warning().empty()) {
491 spdlog::warn(
"OBJ parse warning [{}]: {}", path, reader.Warning());
494 const auto& attrib = reader.GetAttrib();
495 const auto& shapes = reader.GetShapes();
496 const auto& materials = reader.GetMaterials();
498 const bool hasNormals = !attrib.normals.empty();
499 const bool hasTexcoords = !attrib.texcoords.empty();
501 spdlog::info(
"OBJ loaded [{}]: {} vertices, {} normals, {} texcoords, "
502 "{} shapes, {} materials",
504 attrib.vertices.size() / 3,
505 attrib.normals.size() / 3,
506 attrib.texcoords.size() / 2,
512 auto container = std::make_unique<GlbContainerResource>();
513 std::unordered_map<std::string, std::shared_ptr<Texture>> texCache;
514 std::vector<std::shared_ptr<Texture>> ownedTextures;
516 std::vector<std::shared_ptr<Material>> objMaterials;
517 if (materials.empty()) {
519 auto mat = std::make_shared<StandardMaterial>();
520 mat->setName(
"obj-default");
521 mat->setDiffuse(
Color(0.8f, 0.8f, 0.8f, 1.0f));
522 mat->setBaseColorFactor(
Color(0.8f, 0.8f, 0.8f, 1.0f));
523 mat->setMetalness(0.0f);
524 mat->setMetallicFactor(0.0f);
526 mat->setRoughnessFactor(0.5f);
527 mat->setUseMetalness(
true);
528 objMaterials.push_back(mat);
530 for (
const auto& mtl : materials) {
531 objMaterials.push_back(
532 convertMtlMaterial(mtl, device.get(), basedir, texCache, ownedTextures));
538 constexpr int BYTES_PER_VERTEX =
static_cast<int>(
sizeof(
PackedVertex));
539 auto vertexFormat = std::make_shared<VertexFormat>(BYTES_PER_VERTEX,
true,
false);
541 size_t meshPayloadIndex = 0;
543 for (
size_t shapeIdx = 0; shapeIdx < shapes.size(); ++shapeIdx) {
544 const auto& shape = shapes[shapeIdx];
545 const auto& mesh = shape.mesh;
548 const size_t faceCount = mesh.num_face_vertices.size();
549 std::vector<size_t> faceOffsets(faceCount);
552 for (
size_t f = 0; f < faceCount; ++f) {
553 faceOffsets[f] = offset;
554 offset += mesh.num_face_vertices[f];
559 std::vector<float> generatedNormals;
561 generateSmoothNormals(attrib, mesh, generatedNormals);
565 std::unordered_map<int, std::vector<size_t>> materialFaceGroups;
566 for (
size_t f = 0; f < faceCount; ++f) {
567 int matId = (f < mesh.material_ids.size()) ? mesh.material_ids[f] : -1;
568 if (matId < 0 || matId >=
static_cast<int>(objMaterials.size())) matId = 0;
569 materialFaceGroups[matId].push_back(f);
573 for (
auto& [matId, faceIndices] : materialFaceGroups) {
574 std::unordered_map<VertexKey, uint32_t, VertexKeyHash> vertexMap;
575 std::vector<PackedVertex> vertices;
576 std::vector<uint32_t> indices;
577 float minX = std::numeric_limits<float>::max();
578 float minY = std::numeric_limits<float>::max();
579 float minZ = std::numeric_limits<float>::max();
580 float maxX = std::numeric_limits<float>::lowest();
581 float maxY = std::numeric_limits<float>::lowest();
582 float maxZ = std::numeric_limits<float>::lowest();
584 for (
size_t fi : faceIndices) {
585 const size_t base = faceOffsets[fi];
586 const unsigned int fv = mesh.num_face_vertices[fi];
588 for (
unsigned int j = 0; j < fv; ++j) {
589 const size_t idx = base + j;
590 const auto& objIdx = mesh.indices[idx];
593 float px = attrib.vertices[3 * objIdx.vertex_index + 0];
594 float py = attrib.vertices[3 * objIdx.vertex_index + 1];
595 float pz = attrib.vertices[3 * objIdx.vertex_index + 2];
608 if (hasNormals && objIdx.normal_index >= 0) {
609 nx = attrib.normals[3 * objIdx.normal_index + 0];
610 ny = attrib.normals[3 * objIdx.normal_index + 1];
611 nz = attrib.normals[3 * objIdx.normal_index + 2];
617 float len = std::sqrt(nx * nx + ny * ny + nz * nz);
619 float inv = 1.0f / len;
620 nx *= inv; ny *= inv; nz *= inv;
622 }
else if (!generatedNormals.empty()) {
623 nx = generatedNormals[idx * 3 + 0];
624 ny = generatedNormals[idx * 3 + 1];
625 nz = generatedNormals[idx * 3 + 2];
631 nx = 0.0f; ny = 1.0f; nz = 0.0f;
635 float u = 0.0f, vt = 0.0f;
636 if (hasTexcoords && objIdx.texcoord_index >= 0) {
637 u = attrib.texcoords[2 * objIdx.texcoord_index + 0];
638 vt = attrib.texcoords[2 * objIdx.texcoord_index + 1];
644 VertexKey key{objIdx.vertex_index, objIdx.normal_index, objIdx.texcoord_index};
645 auto it = vertexMap.find(key);
646 if (it != vertexMap.end()) {
647 indices.push_back(it->second);
649 uint32_t newIdx =
static_cast<uint32_t
>(vertices.size());
652 float tx, ty, tz, tw;
653 tangentFromNormal(nx, ny, nz, tx, ty, tz, tw);
656 vert.px = px; vert.py = py; vert.pz = pz;
657 vert.nx = nx; vert.ny = ny; vert.nz = nz;
658 vert.u = u; vert.v = vt;
659 vert.tx = tx; vert.ty = ty; vert.tz = tz; vert.tw = tw;
660 vert.u1 = u; vert.v1 = vt;
662 vertices.push_back(vert);
663 vertexMap[key] = newIdx;
664 indices.push_back(newIdx);
666 minX = std::min(minX, px); minY = std::min(minY, py); minZ = std::min(minZ, pz);
667 maxX = std::max(maxX, px); maxY = std::max(maxY, py); maxZ = std::max(maxZ, pz);
673 size_t last = indices.size();
674 std::swap(indices[last - 2], indices[last - 1]);
678 if (vertices.empty())
continue;
682 generateTangents(vertices, indices);
687 const int vertexCount =
static_cast<int>(vertices.size());
688 std::vector<uint8_t> vertexBytes(vertices.size() *
sizeof(
PackedVertex));
689 std::memcpy(vertexBytes.data(), vertices.data(), vertexBytes.size());
693 vbOpts.
data = std::move(vertexBytes);
694 auto vb = device->createVertexBuffer(vertexFormat, vertexCount, vbOpts);
696 const int indexCount =
static_cast<int>(indices.size());
700 std::vector<uint8_t> indexBytes;
702 indexBytes.resize(indices.size() *
sizeof(uint16_t));
703 auto* dst =
reinterpret_cast<uint16_t*
>(indexBytes.data());
704 for (
size_t i = 0; i < indices.size(); ++i) {
705 dst[i] =
static_cast<uint16_t
>(indices[i]);
708 indexBytes.resize(indices.size() *
sizeof(uint32_t));
709 std::memcpy(indexBytes.data(), indices.data(), indexBytes.size());
711 auto ib = device->createIndexBuffer(idxFmt, indexCount, indexBytes);
713 auto meshResource = std::make_shared<Mesh>();
714 meshResource->setVertexBuffer(vb);
715 meshResource->setIndexBuffer(ib, 0);
721 prim.
count = indexCount;
723 meshResource->setPrimitive(prim, 0);
727 (minX + maxX) * 0.5f,
728 (minY + maxY) * 0.5f,
729 (minZ + maxZ) * 0.5f);
731 (maxX - minX) * 0.5f,
732 (maxY - minY) * 0.5f,
733 (maxZ - minZ) * 0.5f);
734 meshResource->setAabb(bounds);
739 payload.
mesh = meshResource;
740 int safeMat = std::clamp(matId, 0,
static_cast<int>(objMaterials.size()) - 1);
741 payload.
material = objMaterials[
static_cast<size_t>(safeMat)];
742 container->addMeshPayload(payload);
751 if (shapes.size() == 1) {
754 rootNode.
name = objPath.stem().string();
756 for (
size_t i = 0; i < meshPayloadIndex; ++i) {
759 container->addNodePayload(rootNode);
760 container->addRootNodeIndex(0);
763 size_t payloadCursor = 0;
764 for (
size_t shapeIdx = 0; shapeIdx < shapes.size(); ++shapeIdx) {
765 const auto& shape = shapes[shapeIdx];
766 const auto& mesh = shape.mesh;
769 std::unordered_map<int, bool> matGroups;
770 for (
size_t f = 0; f < mesh.num_face_vertices.size(); ++f) {
771 int matId = (f < mesh.material_ids.size()) ? mesh.material_ids[f] : 0;
772 if (matId < 0 || matId >=
static_cast<int>(objMaterials.size())) matId = 0;
773 matGroups[matId] =
true;
775 size_t groupCount = matGroups.size();
778 node.
name = shape.name.empty()
779 ?
"shape_" + std::to_string(shapeIdx)
782 for (
size_t i = 0; i < groupCount && payloadCursor < meshPayloadIndex; ++i) {
785 container->addNodePayload(node);
786 container->addRootNodeIndex(
static_cast<int>(shapeIdx));
791 for (
auto& tex : ownedTextures) {
792 container->addOwnedTexture(tex);
795 spdlog::info(
"OBJ parse complete [{}]: {} mesh payloads, {} materials, {} textures",
796 path, meshPayloadIndex, objMaterials.size(), ownedTextures.size());
Axis-Aligned Bounding Box defined by center and half-extents.
void setHalfExtents(const Vector3 &halfExtents)
void setCenter(const Vector3 ¢er)
Abstract GPU interface for resource creation, state management, and draw submission.
static std::unique_ptr< GlbContainerResource > parse(const std::string &path, const std::shared_ptr< GraphicsDevice > &device, const ObjParserConfig &config=ObjParserConfig{})
GPU texture resource supporting 2D, cubemap, volume, and array formats with mipmap management.
RGBA color with floating-point components in [0, 1].
std::shared_ptr< Mesh > mesh
std::shared_ptr< Material > material
std::vector< size_t > meshPayloadIndices
Configuration options for OBJ loading.
bool flipYZ
If true, swap Y and Z axes (Z-up CAD -> Y-up engine) and negate new Z.
float uniformScale
Uniform scale applied to all vertex positions (e.g., 0.001 for mm -> m).
bool generateNormals
Generate smooth normals when the OBJ file has no normals.
bool flipWinding
If true, reverse face winding order.
bool generateTangents
Generate tangents for normal mapping.
std::string mtlSearchPath
Override MTL search path (empty = same directory as .obj file).
Describes how vertex and index data should be interpreted for a draw call.
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