-
Notifications
You must be signed in to change notification settings - Fork 36
/
Copy pathOBJLoader.cpp
422 lines (367 loc) · 19.1 KB
/
OBJLoader.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
// Copyright 2023 Autodesk, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "pch.h"
#include "Aurora/Foundation/Geometry.h"
#include "Loaders.h"
#include "SceneContents.h"
bool gEnableMaterialXMaterials = false;
uint32_t ImageCache::whitePixels[1] = { 0xFFFFFFFF };
#define PLASMA_HAS_TANGENTS 0
// A simple structure describing an OBJ index, which has sub-indices for vertex channels: position,
// normal, and texture coordinates.
struct OBJIndex
{
// Constructor, from a tinyobj::index_t.
OBJIndex(const tinyobj::index_t& index) :
Position(index.vertex_index), Normal(index.normal_index), TexCoord(index.texcoord_index)
{
}
// Equality operator, for use in a hash map.
bool operator==(const OBJIndex& other) const
{
return Position == other.Position && Normal == other.Normal && TexCoord == other.TexCoord;
}
// Indices, same as tinyobj::index_t.
int Position;
int Normal;
int TexCoord;
};
// A hash function object for OBJIndex.
struct hashOBJIndex
{
size_t operator()(const OBJIndex& index) const
{
// Get hash values for the individual indices...
hash<int> hasher;
size_t h1 = hasher(index.Position);
size_t h2 = hasher(index.Normal);
size_t h3 = hasher(index.TexCoord);
// ... and combine them in one recommended way.
return ((h1 ^ (h2 << 1)) >> 1) ^ (h3 << 1);
}
};
// An unordered (hash) map from an OBJIndex (with vertex channel indices) to a single index.
using OBJIndexMap = unordered_map<OBJIndex, uint32_t, hashOBJIndex>;
void loadMaterialXForOBJMaterials(bool enabled)
{
gEnableMaterialXMaterials = enabled;
}
// Loads a Wavefront OBJ file into the specified renderer and scene, from the specified file path.
bool loadOBJFile(Aurora::IRenderer* /*pRenderer*/, Aurora::IScene* pScene, const string& filePath,
SceneContents& sceneContents)
{
sceneContents.reset();
// Start a timer for reading the OBJ file.
Foundation::CPUTimer timer;
::infoMessage("Reading OBJ file \"" + filePath + "\"...");
// Load the OBJ file with tinyobjloader. If the load is not successful or has no shapes, do
// nothing else.
// NOTE: tinyobjloader currently does not support wide strings on Windows, so files with wide
// non-ASCII characters in their names will fail to load.
tinyobj::attrib_t attrib;
vector<tinyobj::shape_t> shapes;
vector<tinyobj::material_t> objMaterials;
string sWarnings;
string sErrors;
uint32_t objectCount = 0;
bool result =
tinyobj::LoadObj(&attrib, &shapes, &objMaterials, &sWarnings, &sErrors, filePath.c_str());
if (!result || shapes.empty())
{
return false;
}
// Report the file read time.
::infoMessage("... completed in " + to_string(static_cast<int>(timer.elapsed())) + " ms.");
// Start a timer for translating the OBJ scene data.
timer.reset();
::infoMessage("Translating OBJ scene data...");
// Parse the OBJ materials into a corresponding array of Aurora materials.
vector<Aurora::Path> lstMaterials;
lstMaterials.reserve(objMaterials.size());
ImageCache imageCache;
int mtlCount = 0;
for (auto& objMaterial : objMaterials)
{
// Flag is set try and load a materialX file with the OBJ material name.
if (gEnableMaterialXMaterials && !objMaterial.name.empty())
{
// Build MaterialX file path from OBJ material name and OBJ path.
std::string directory = std::filesystem::path(filePath).parent_path().u8string();
string mtlxFilename = directory + "/" + objMaterial.name + ".mtlx";
ifstream is(mtlxFilename, ifstream::binary);
if (is.good())
{
// If the file exists load it into a string.
string mtlString;
is.seekg(0, is.end);
size_t length = is.tellg();
is.seekg(0, is.beg);
mtlString.resize(length + 1);
is.read((char*)&mtlString[0], length);
// Create a material from the MaterialX string, and use it in place of the OBJ
// material.
Aurora::Path materialPath =
filePath + "-" + objMaterial.name + ":MaterialX-" + to_string(mtlCount++);
pScene->setMaterialType(materialPath, Names::MaterialTypes::kMaterialX, mtlString);
lstMaterials.push_back(materialPath);
AU_INFO("Found materialX document %s for OBJ material %s", mtlxFilename.c_str(),
objMaterial.name.c_str());
continue;
}
}
// Collect material properties.
// NOTE: This includes support for some of the properties in the PBR extension here:
// http://exocortex.com/blog/extending_wavefront_mtl_to_support_pbr
// See TinyOBJ parsing code for mapping from OBJ mtl values to ObjMaterial properties:
// https://github.com/tinyobjloader/tinyobjloader/blob/ee45fb41db95bf9563f2a41bc63adfa18475c2ee/tiny_obj_loader.h#L2127
// > We use the Standard Surface IOR default of 1.5 when no IOR is specified in the file,
// which is parsed as 1.0.
// > Transmission is derived from the "dissolve" property. This is normally intended for
// what Standard Surface calls "opacity" but transmission is more interesting.
// > The default transmittance is black, which is a useless tint color, so that value is
// converted to white.
vec3 baseColor = ::sRGBToLinear(make_vec3(objMaterial.diffuse));
float metalness = objMaterial.metallic;
vec3 specularColor = ::sRGBToLinear(make_vec3(objMaterial.specular));
float specularRoughness = objMaterial.roughness;
float specularIOR = objMaterial.ior == 1.0f ? 1.5f : objMaterial.ior;
float transmission = 1.0f - objMaterial.dissolve;
vec3 transmissionColor = ::sRGBToLinear(make_vec3(objMaterial.transmittance));
if (length(transmissionColor) == 0.0f)
{
transmissionColor = vec3(1.0f);
}
vec3 emissionColor = ::sRGBToLinear(make_vec3(objMaterial.emission));
vec3 opacity = vec3(1.0f); // objMaterial.dissolve (see NOTE above)
// Load the base color image file from the "map_Kd" file path.
// NOTE: Set the linearize flag, as these images typically use the sRGB color space.
Aurora::Path baseColorImage =
imageCache.getImage(objMaterial.diffuse_texname, pScene, true);
// Load the specular roughness image file from the "map_Pr" file path.
// NOTE: Don't set the linearize flag, as these images typically use the linear color space.
Aurora::Path specularRoughnessImage =
imageCache.getImage(objMaterial.roughness_texname, pScene, false);
// Load the emission color image file from the "map_Ke" file path.
// NOTE: Set the linearize flag, as these images typically use the sRGB color space.
Aurora::Path emissionColorImage =
imageCache.getImage(objMaterial.emissive_texname, pScene, true);
// Load the opacity image file from the "map_d" file path.
// NOTE: Don't set the linearize flag, as these images typically use the linear color space.
Aurora::Path opacityImage = imageCache.getImage(objMaterial.alpha_texname, pScene, false);
// Load the normal image file from either the "bump" or "norm" file path in the material.
// The file could be specified in either property depending on the exporter.
// NOTE: Don't set the linearize flag, as these images typically use the linear color space.
// Only normal maps (not height maps) are supported here.
string normalFilePath = objMaterial.normal_texname.empty() ? objMaterial.bump_texname
: objMaterial.normal_texname;
Aurora::Path normalImage = imageCache.getImage(normalFilePath, pScene, false);
// Set emission (the actual light emitted) to 1.0 if there is an emission color image or the
// emission color has any positive components.
float emission = (!emissionColorImage.empty() || emissionColor.length()) ? 1.0f : 0.0f;
// Create an Aurora material, assign the properties, and add the material to the list.
// clang-format off
Aurora::Properties properties =
{
{ "base_color", baseColor },
{ "metalness", metalness },
{ "specular_color", specularColor },
{ "specular_roughness", specularRoughness },
{ "specular_IOR", specularIOR },
{ "transmission", transmission },
{ "transmission_color", transmissionColor },
{ "emission", emission },
{ "emission_color", emissionColor },
{ "opacity", opacity },
{ "base_color_image", baseColorImage },
{ "specular_roughness_image", specularRoughnessImage },
{ "emission_color_image", emissionColorImage },
{ "opacity_image", opacityImage },
{ "normal_image", normalImage }
};
// clang-format on
Aurora::Path materialPath =
filePath + "-" + objMaterial.name + ":OBJFileMaterial-" + to_string(mtlCount++);
pScene->setMaterialProperties(materialPath, properties);
lstMaterials.push_back(materialPath);
};
// Get the data arrays from the tinyobj::attrib_t object.
const auto& srcPositions = attrib.vertices;
const auto& srcNormals = attrib.normals;
const auto& srcTexCoords = attrib.texcoords;
// Iterate the shapes, creating an Aurora geometry for each one and adding an instance of it
// to the scene.
bool hasMesh = false;
for (const auto& shape : shapes)
{
// Skip shapes that don't have a mesh (e.g. lines or points).
auto indexCount = static_cast<uint32_t>(shape.mesh.indices.size());
if (indexCount == 0)
{
continue;
}
hasMesh = true;
Aurora::Path sceneInstancePath =
filePath + "-" + shape.name + ":OBJFileInstance-" + to_string(objectCount);
Aurora::Path geomPath =
filePath + "-" + shape.name + ":OBJFileGeom-" + to_string(objectCount);
objectCount++;
SceneGeometryData& geometryData = sceneContents.addGeometry(geomPath);
auto& positions = geometryData.positions;
auto& normals = geometryData.normals;
auto& tex_coords = geometryData.texCoords;
auto& indices = geometryData.indices;
indices.reserve(indexCount);
bool bHasNormals = shape.mesh.indices[0].normal_index >= 0;
bool bHasTexCoords = shape.mesh.indices[0].texcoord_index >= 0;
// Iterate the "OBJ indices" in the shape, which consist of three sub-indices for individual
// position, normal, and texture coordinate values. These are used to define unique vertices
// (i.e. identical combinations of the three sub-indices) and populate the data arrays.
uint32_t nextIndex = 0;
OBJIndexMap objIndexMap;
for (const auto& objIndex : shape.mesh.indices)
{
// If this OBJ index is already in the map, add the corresponding unique index to the
// index array. The OBJIndex struct supports hashing to allow this.
if (objIndexMap.find(objIndex) != objIndexMap.end())
{
indices.push_back(objIndexMap[objIndex]);
}
// Else: Add the corresponding position, normal, and texture coordinate values to the
// their respective arrays. Also add the next index value to the index array and the OBJ
// index map.
else
{
// Add the position (XYZ) value, including updating the bounding box if needed.
const float* position = &srcPositions[objIndex.vertex_index * 3];
sceneContents.bounds.add(position);
positions.push_back(*position++);
positions.push_back(*position++);
positions.push_back(*position);
// Add the normal (XYZ) value, if needed.
if (bHasNormals)
{
// Normalize the normal, in case the source data has bad normals.
glm::vec3 normal =
glm::normalize(make_vec3(&srcNormals[objIndex.normal_index * 3]));
// Add the normal.
normals.push_back(normal.x);
normals.push_back(normal.y);
normals.push_back(normal.z);
}
// Add the texture coordinate (UV) value, if needed.
if (bHasTexCoords)
{
const float* tex_coord = &srcTexCoords[objIndex.texcoord_index * 2];
tex_coords.push_back(*tex_coord++);
tex_coords.push_back(1.0f - *tex_coord); // flip V
}
// Add the next index to the index array and the OBJ index map, for the current OBJ
// index.
indices.push_back(nextIndex);
objIndexMap[objIndex] = nextIndex;
nextIndex++;
}
}
// Calculate the vertex count.
uint32_t vertexCount = static_cast<uint32_t>(positions.size()) / 3;
// Do we have tangents ? Default to false.
bool bHasTangents = false;
// Create normals if they are not available.
if (!bHasNormals)
{
normals.resize(positions.size());
Foundation::calculateNormals(
vertexCount, positions.data(), indexCount / 3, indices.data(), normals.data());
}
// Create tangents if texture coordinates are available.
#if PLASMA_HAS_TANGENTS
auto& tangents = geometryData.tangents;
if (bHasTexCoords)
{
tangents.resize(normals.size());
Foundation::calculateTangents(vertexCount, positions.data(), normals.data(),
tex_coords.data(), indexCount / 3, indices.data(), tangents.data());
bHasTangents = true;
}
#endif
Aurora::GeometryDescriptor& geomDesc = geometryData.descriptor;
geomDesc.type = Aurora::PrimitiveType::Triangles;
geomDesc.vertexDesc.attributes[Aurora::Names::VertexAttributes::kPosition] =
Aurora::AttributeFormat::Float3;
geomDesc.vertexDesc.attributes[Aurora::Names::VertexAttributes::kNormal] =
Aurora::AttributeFormat::Float3;
geomDesc.vertexDesc.attributes[Aurora::Names::VertexAttributes::kTexCoord0] =
Aurora::AttributeFormat::Float2;
if (bHasTangents)
geomDesc.vertexDesc.attributes[Aurora::Names::VertexAttributes::kTangent] =
Aurora::AttributeFormat::Float3;
geomDesc.vertexDesc.count = vertexCount;
geomDesc.indexCount = indexCount;
geomDesc.getAttributeData = [geomPath, bHasTangents, &sceneContents](
Aurora::AttributeDataMap& buffers, size_t /* firstVertex*/,
size_t /* vertexCount*/, size_t /* firstIndex*/,
size_t /* indexCount*/) {
SceneGeometryData& geometryData = sceneContents.geometry[geomPath];
buffers[Aurora::Names::VertexAttributes::kPosition].address =
geometryData.positions.data();
buffers[Aurora::Names::VertexAttributes::kPosition].size =
geometryData.positions.size() * sizeof(float);
buffers[Aurora::Names::VertexAttributes::kPosition].stride = sizeof(vec3);
buffers[Aurora::Names::VertexAttributes::kNormal].address = geometryData.normals.data();
buffers[Aurora::Names::VertexAttributes::kNormal].size =
geometryData.normals.size() * sizeof(float);
buffers[Aurora::Names::VertexAttributes::kNormal].stride = sizeof(vec3);
buffers[Aurora::Names::VertexAttributes::kTexCoord0].address =
geometryData.texCoords.data();
buffers[Aurora::Names::VertexAttributes::kTexCoord0].size =
geometryData.texCoords.size() * sizeof(float);
buffers[Aurora::Names::VertexAttributes::kTexCoord0].stride = sizeof(vec2);
// Fill in the tangent data, if we have them.
if (bHasTangents)
{
buffers[Aurora::Names::VertexAttributes::kTangent].address =
geometryData.tangents.data();
buffers[Aurora::Names::VertexAttributes::kTangent].size =
geometryData.tangents.size() * sizeof(float);
buffers[Aurora::Names::VertexAttributes::kTangent].stride = sizeof(vec3);
}
buffers[Aurora::Names::VertexAttributes::kIndices].address =
geometryData.indices.data();
buffers[Aurora::Names::VertexAttributes::kIndices].size =
geometryData.indices.size() * sizeof(int);
buffers[Aurora::Names::VertexAttributes::kIndices].stride = sizeof(int);
return true;
};
pScene->setGeometryDescriptor(geomPath, geomDesc);
// Create an Aurora geometry object and get the associated material. Add an instance with
// that geometry and material to the scene.
// NOTE: An OBJ file can have different materials for each face (triangle) in a mesh. Here
// we only use the material assigned to the *first* face for the entire mesh. If no material
// is specified, use a null pointer which indicates Aurora should use a default material.
int material_id = shape.mesh.material_ids[0];
// Add instance to the scene.
Aurora::InstanceDefinition instDef = { sceneInstancePath,
{ { Aurora::Names::InstanceProperties::kTransform, mat4() } } };
if (material_id >= 0)
instDef.properties[Aurora::Names::InstanceProperties::kMaterial] =
lstMaterials[material_id];
pScene->addInstance(sceneInstancePath, geomPath, instDef.properties);
sceneContents.instances.push_back({ instDef, geomPath });
sceneContents.vertexCount += static_cast<uint32_t>(geomDesc.vertexDesc.count);
sceneContents.triangleCount += static_cast<uint32_t>(geomDesc.indexCount / 3);
}
// Report the translation time.
::infoMessage("... completed in " + to_string(static_cast<int>(timer.elapsed())) + " ms.");
return hasMesh;
}