A building loaded with its materials
What it shows: the Sponza atrium, an OBJ model of 40 000 faces (66 000 triangles once its
quads are split) that uses 20 materials, 19 of them with a texture image. mesh_load_file_obj_advanced reads the model
and its .mtl file, and returns it split into parts, one per material; the scene converts
them into a list of mesh_drawable and draws them in a loop. The camera starts inside the
building.
Concepts combined in this example:
mesh_load_file_obj_advanced(directory, filename), which also loads the texture images named in the material file;mesh_obj_advanced_loader::convert_to_mesh_drawable, which sends every part to the GPU;- a
std::vector<mesh_drawable>drawn with onedrawper part; - an OBJ file without normals: the normals are computed at loading, one per triangle.
Source: examples/09_mesh_loading
of the public repository epita_ani3d_lab_code-2027, run unchanged in the demo above. For
the steps in your own project (where to put the files, how to fix a .mtl file, how to
scale and rotate the parts), see
Load an OBJ model made of several textured parts;
for a single model without materials,
Load a 3D model from an OBJ file.
| File | Role |
|---|---|
src/scene.hpp, src/scene.cpp |
the scene: loads the model, draws its parts |
assets/sponza.zip |
the Sponza model (OBJ, MTL, JPG images): unzip it in assets/ |
assets/f1.zip |
a second model (a Formula 1 car, an OBJ of 25 MB once unzipped), loaded by a commented-out line |
The models are shipped as zip files: unzip them in assets/ before running the example
natively (the comment in initialize() says so). The demo above uses a lighter copy of
Sponza (rounded coordinates, smaller images, see demos/mesh-loading/origin.txt) and leaves
out the car. The scene also declares a timer, tree, ground and grass drawables and two
GUI fields (number_of_instances, scaling_grass) that no code uses: leftovers of a larger
scene.
1. Load and convert
// src/scene.cpp - initialize // Unzip the files in assets/ before runing the code //auto struct_shape = mesh_load_file_obj_advanced(project::path + "assets/f1/", "F1GenV3Backup.obj"); auto struct_shape = mesh_load_file_obj_advanced(project::path + "assets/sponza/", "sponza.obj"); shapes = mesh_obj_advanced_loader::convert_to_mesh_drawable(struct_shape);
To display the car instead, swap the two comments. The model is used as it is in the file:
no scaling, no rotation. Sponza measures about 35 x 17 x 16 units and its vertical axis is
y, while the camera of the scene (camera_controller_orbit, placed by look_at with the up
vector {0,0,1}) treats z as vertical: the building appears lying on its side, and the
camera, at {4,4,1.5}, starts within its walls. Zoom out (right button drag) to see it
whole. The how-to rotates and scales the parts to get an upright building of a few units.
2. What the loader builds
mesh_load_file_obj_advanced parses the file with the tinyobjloader library, then:
- loads each texture once: for every material of the
.mtlfile with a diffuse image ("map_Kd"), the imagedirectory + nameis loaded on the GPU inGL_REPEATmode; a material without image gets the default white texture; - cuts the faces into parts: it walks through the faces of each shape returned by the
parser (the "g" groups of the file, for Sponza) and starts a new part every time the material changes. Each part is a
mesh_obj_advanced_loader::shape_element_node: ameshand the texture of its material. Several parts with the same material share the same texture, since a texture copy only copies its OpenGL id; - does not share vertices: the faces are triangulated by the parser, and every triangle gets three new vertices, with the position, the uv and, if the file has some, the normal of its corners.
convert_to_mesh_drawable then calls fill_empty_field and initialize_data_on_gpu on
each mesh, and assigns its texture. Sponza has no "vn" line: fill_empty_field computes the
normals from the triangles, and since no vertex is shared, each triangle gets its own
normal. The building is therefore lit with flat facets, which suits its mostly flat walls.
Only the diffuse image of each material is used: the colors ("Kd"), the bump maps and the
other images of the .mtl file are ignored.
3. Draw all the parts
// src/scene.cpp - display_frame for (int k = 0; k < shapes.size(); ++k) draw(shapes[k], environment); if (gui.display_wireframe) { for (int k = 0; k < shapes.size(); ++k) draw_wireframe(shapes[k], environment); }
One draw call per part. The "Wireframe" checkbox draws the triangles in blue over the
model natively; in the browser draw_wireframe draws nothing (see
Display the wireframe).
Going further
- Print
shapes.size()to see how many parts the material changes produce. - Rotate and scale all the parts in the same loop (
model.rotation,model.scaling), as in the how-to, to stand the building upright. - Give each part a random
material.colorto see where the parts are cut. - Load your own model: export it from Blender as OBJ with its materials, keep the images
next to the
.mtlfile, and check the image names in the "map_Kd" lines.