phd-typst/foreword/3d-model.typ

168 lines
7.4 KiB
Plaintext
Raw Normal View History

2023-04-14 17:27:59 +01:00
A 3D streaming system is a system that progressively collects 3D data.
The previous chapter voluntarily remained vague about what \emph{3D data} actually are.
This chapter presents in detail the 3D data we consider and how they are rendered.
We also give insights about interaction and streaming by comparing the 3D setting to the video one.
= What is a 3D model?
The 3D models we are interested in are sets of textured meshes, which can potentially be arranged in a scene graph.
Such models can typically contain the following:
- *Vertices*, which are 3D points,
- *Faces*, which are polygons defined from vertices (most of the time, they are triangles),
- *Textures*, which are images that can be used to paint faces in order to add visual richness,
- *Texture coordinates*, which are information added to a face, describing how the texture should be painted over it,
- *Normals*, which are 3D vectors that can give information about light behaviour on a face.
The Wavefront OBJ is a format that describes all these elements in text format.
A 3D model encoded in the OBJ format typically consists in two files: the material file (`.mtl`) and the object file (`.obj`).
The material file declares all the materials that the object file will reference.
A material consists in name, and other photometric properties such as ambient, diffuse and specular colors, as well as texture maps, which are images that are painted on faces.
Each face corresponds to a material.
2023-04-20 10:44:45 +01:00
A simple material file is visible on @cubemtl.
2023-04-14 17:27:59 +01:00
The object file declares the 3D content of the objects.
It declares vertices, texture coordinates and normals from coordinates (e.g. `v 1.0 2.0 3.0` for a vertex, `vt 1.0 2.0` for a texture coordinate, `vn 1.0 2.0 3.0` for a normal).
These elements are numbered starting from 1.
Faces are declared by using the indices of these elements. A face is a polygon with an arbitrary number of vertices and can be declared in multiple manners:
- `f 1 2 3` defines a triangle face that joins the first, the second and the third declared vertices;
- `f 1/1 2/3 3/4` defines a similar triangle but with texture coordinates, the first texture coordinate is associated to the first vertex, the third texture coordinate is associated to the second vertex, and the fourth texture coordinate is associated with the third vertex;
- `f 1//1 2//3 3//4` defines a similar triangle but referencing normals instead of texture coordinates;
- `f 1/1/1 2/3/3 3/4/4` defines a triangle with both texture coordinates and normals.
An object file can include materials from a material file (`mtllib path.mtl`) and apply the materials that it declares to faces.
A material is applied by using the `usemtl` keyword, followed by the name of the material to use.
The faces declared after a `usemtl` are painted using the material in question.
2023-04-20 10:44:45 +01:00
An example of object file is visible on @cube.
2023-04-14 17:27:59 +01:00
2023-04-20 10:44:45 +01:00
#figure(
grid(
columns: (1fr, 0.2fr, 1fr),
align(center + horizon)[
#figure(
align(left,
raw(
read("../assets/introduction/cube.obj"),
block: true,
)
),
caption: [An object file describing a cube]
)<cubeobj>
],
[],
align(center + horizon)[
#figure(
align(left,
raw(
read("../assets/introduction/materials.mtl"),
block: true,
)
),
caption: [A material file describing a material]
)<cubemtl>
#figure(
align(left,
image("../assets/introduction/cube.png", width: 100%)
),
caption: [A rendering of the cube]
)
],
),
caption: [The OBJ representation of a cube and its render]
)<cube>
2023-04-14 17:27:59 +01:00
== Rendering a 3D model
A typical 3D renderer follows Algorithm X. // TODO
// \begin{algorithm}[th]
// \SetKwData{Material}{material}
// \SetKwData{Object}{object}
// \SetKwData{Geometry}{geometry}
// \SetKwData{Materials}{all\_materials}
// \SetKwData{Object}{object}
// \SetKwData{Scene}{scene}
// \SetKwData{True}{true}
// \SetKwFunction{LoadGeometry}{load\_geometry}
// \SetKwFunction{LoadMaterial}{load\_material}
// \SetKwFunction{BindMaterial}{bind\_material}
// \SetKwFunction{Draw}{draw}
//
// \tcc{Initialization}
// \For{$\Object\in\Scene$}{%
// \LoadGeometry{\Object.\Geometry}\;
// \LoadMaterial{\Object.\Material}\;
// }
// \BlankLine%
// \BlankLine%
// \tcc{Render loop}
// \While{\True}{%
// \For{$\Object\in\Scene$}{%
// \BindMaterial{\Object.\Material}\;
// \Draw{\Object.\Geometry}\;
// }
// }
//
// \caption{A rendering algorithm\label{f:renderer}}
// \end{algorithm}
//
The first task the renderer needs to perform is sending the data to the GPU: this is done in the loading loop during an initialization step.
This step can be slow, but it is generally acceptable since it only occurs once at the beginning of the program.
Then, the renderer starts the rendering loop: at each frame, it renders the whole scene: for each object, it binds the corresponding material to the GPU and then renders the object.
During the rendering loop, there are two things to consider regarding performances:
- the more faces in a geometry, the slower the `draw` call;
- the more objects in the scene, the more overhead caused by the CPU/GPU communication at each step of the loop.
The way the loop works forces objects with different materials to be rendered separately.
An efficient renderer keeps the number of objects in a scene low to avoid introducing overhead.
However, an important feature of 3D engines regarding performance is frustum culling.
The frustum is the viewing volume of the camera.
Frustum culling consists in skipping the objects that are outside the viewing volume of the camera in the rendering loop.
Algorithm X is a variation of Algorithm Y with frustum culling. // TODO
// \begin{algorithm}[th]
// \SetKwData{Texture}{texture}
// \SetKwData{Object}{object}
// \SetKwData{Geometry}{geometry}
// \SetKwData{Textures}{all\_textures}
// \SetKwData{Object}{object}
// \SetKwData{Scene}{scene}
// \SetKwData{True}{true}
// \SetKwData{CameraFrustum}{camera\_frustum}
// \SetKwFunction{LoadGeometry}{load\_geometry}
// \SetKwFunction{LoadTexture}{load\_texture}
// \SetKwFunction{BindTexture}{bind\_texture}
// \SetKwFunction{Draw}{draw}
//
// \tcc{Initialization}
// \For{$\Object\in\Scene$}{%
// \LoadGeometry{\Object.\Geometry}\;
// \LoadTexture{\Object.\Texture}\;
// }
// \BlankLine%
// \BlankLine%
// \tcc{Render loop}
// \While{\True}{%
// \For{$\Object\in\Scene$}{%
// \If{$\Object\cap\CameraFrustum\neq\emptyset$}{%
// \BindTexture{\Object.\Texture}\;
// \Draw{\Object.\Geometry}\;
// }
// }
// }
//
// \caption{A rendering algorithm with frustum culling\label{f:frustum-culling}}
// \end{algorithm}
A renderer that uses a single object avoids the overhead, but fails to benefit from frustum culling.
An optimized renderer needs to find a compromise between a too fine partition of the scene, which introduces overhead, and a too coarse partition, which introduces useless rendering.
- ensures to have objects that do not spread across the whole scene, since that would lead to a useless frustum culling, and many objects to avoid rendering the whole scene at each frame;
- but not too many objects to avoid suffering from the overhead.