211 lines
11 KiB
XML
211 lines
11 KiB
XML
= Implementation details
|
|
|
|
During this thesis, a lot of software has been developed, and for this software to be successful and efficient, we chose appropriate languages.
|
|
When it comes to 3D streaming systems, we need two kind of software.
|
|
|
|
- *Interactive applications* which can run on as many devices as possible so we can easily conduct user studies. For this context, we chose the *JavaScript* language, since it can run on many devices and it has great support for WebGL.
|
|
- *Native applications* which can run fast on desktop devices, in order to prepare data, run simulations and evaluate our ideas. For this context, we chose the *Rust* language, which is a somewhat recent language that provides both the efficiency of C and C++ and the safety of functional languages.
|
|
|
|
== JavaScript
|
|
|
|
=== THREE.js
|
|
|
|
On the web browser, it is now possible to perform 3D rendering by using WebGL.
|
|
However, WebGL is very low level and it can be painful to write code, even to render a simple triangle.
|
|
For example, #link("https://www.tutorialspoint.com/webgl/webgl_drawing_a_triangle.htm")[this tutorial]'s code contains 121 lines of JavaScript, 46 being code (not comments or empty lines) to render a simple, non-textured triangle.
|
|
For this reason, it seems unreasonable to build a system like the one we are describing in raw WebGL.
|
|
There are many libraires that wrap WebGL code and that help people building 3D interfaces, and
|
|
#link("https://threejs.org")[THREE.js] is a very popular one (56617 stars on github, making it the 35th most starred repository on GitHub as of November 26th, 2019) // \footnote{\url{https://web.archive.org/web/20191126151645/https://gitstar-ranking.com/mrdoob/three.js}}). // TODO footnote
|
|
THREE.js acts as a 3D engine built on WebGL.
|
|
It provides classes to deal with everything we need:
|
|
- the *Renderer* class contains all the WebGL code needed to render a scene on the web page;
|
|
- the *Object* class contains all the boilerplate needed to manage the tree structure of the content, it contains a transform (translation and rotation) and it can have children that are other objects;
|
|
- the *Scene* class is the root object, it contains all of the objects we want to render and it is passed as argument to the render function;
|
|
- the *Geometry* and *BufferGeometry* classes are the classes that hold the vertex buffers, we will discuss it more in the next paragraph;
|
|
- the *Material* class is the class that holds the properties used to render geometry (the most important information being the texture), there are many classes derived from Material, and the developer can choose what material they want for their objects;
|
|
- the *Mesh* class is the class that links the geometry and the material, it derives the Object class and can thus be added to a scene and rendered.
|
|
A snippet of the basic usage of these classes is given in @three-hello-world. // TODO
|
|
|
|
#figure(
|
|
align(left,
|
|
```javascript
|
|
// Computes the aspect ratio of the window.
|
|
let aspectRatio = window.innerWidth / window.innerHeight;
|
|
|
|
// Creates a camera and sets its parameters and position.
|
|
let camera = new THREE.PerspectiveCamera(70, aspectRatio, 0.01, 10);
|
|
camera.position.z = 1;
|
|
|
|
// Creates the scene that contains our objects.
|
|
let scene = new THREE.Scene();
|
|
|
|
// Creates a geometry (vertices and faces) corresponding to a cube.
|
|
let geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
|
|
|
|
// Creates a material that paints the faces depending on their normal.
|
|
let material = new THREE.MeshNormalMaterial();
|
|
|
|
// Creates a mesh that associates the geometry with the material.
|
|
let mesh = new THREE.Mesh(geometry, material);
|
|
|
|
// Adds the mesh to the scene.
|
|
scene.add(mesh);
|
|
|
|
// Creates the renderer and append its canvas to the DOM.
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
// Renders the scene with the camera.
|
|
renderer.render(scene, camera);
|
|
```
|
|
),
|
|
caption: [A THREE.js _hello world_]
|
|
)<three-hello-world>
|
|
|
|
=== Geometries
|
|
|
|
Geometries are the classes that hold the vertices, texture coordinates, normals and faces.
|
|
THREE.js proposes two classes for handling geometries:
|
|
- the *Geometry* class, which is made to be developer friendly and allows easy editing but can suffer from performance issues;
|
|
- the *BufferGeometry* class, which is harder to use for a developer, but allows better performance since the developer controls how data is transmitted to the GPU.
|
|
|
|
|
|
== Rust
|
|
|
|
In this section, we explain the specificities of Rust and why it is an adequate language for writing efficient native software safely.
|
|
|
|
=== Borrow checker
|
|
|
|
Rust is a system programming language focused on safety.
|
|
It is made to be efficient (and effectively has performances comparable to C // TODO \footnote{\url{https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/rust.html}} or C++\footnote{\url{https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/rust-gpp.html}})
|
|
but with some extra features.
|
|
C++ users might see it as a language like C++ but that forbids undefined behaviours.// TODO \footnote{in Rust, when you need to execute code that might lead to undefined behaviours, you need to put it inside an \texttt{unsafe} block. Many operations are not available outside an \texttt{unsafe} block (e.g., dereferencing a pointer, or mutating a static variable). The idea is that you can use \texttt{unsafe} blocks when you require it, but you should avoid it as much as possible and when you do it, you must be particularly careful.}
|
|
The most powerful concept from Rust is _ownership_.
|
|
Basically, every value has a variable that we call its _owner_.
|
|
To be able to use a value, you must either be its owner or borrow it.
|
|
There are two types of borrow, the immutable borrow and the mutable borrow (roughly equivalent to references in C++).
|
|
The compiler comes with the _borrow checker_ which makes sure you only use variables that you are allowed to use.
|
|
For example, the owner can only use the value if it is not being borrowed, and it is only possible to either mutably borrow a value once, or immutably borrow a value many times.
|
|
At first, the borrow checker seems particularly efficient to detect bugs in concurrent software, but in fact, it is also decisive in non concurrent code.
|
|
Consider the piece of C++ code in Snippets X and Y. // TODO
|
|
|
|
#figure(
|
|
align(left,
|
|
```cpp
|
|
auto vec = std::vector<int> {1, 2, 3};
|
|
for (auto value: vec)
|
|
vec.push_back(value);
|
|
)
|
|
```
|
|
),
|
|
caption: [Undefined behaviour with for each syntax],
|
|
)<undefined-behaviour-cpp>
|
|
|
|
#figure(
|
|
align(left,
|
|
```cpp
|
|
auto vec = std::vector<int> {1, 2, 3};
|
|
for (auto it = std::begin(vec); it < std::end(vec); it++)
|
|
vec.push_back(*it);
|
|
```
|
|
),
|
|
caption: [Undefined behaviour with iterator syntax],
|
|
)<undefined-behaviour-it-cpp>
|
|
|
|
This loop should go endlessly because the vector grows in size as we add elements in the loop.
|
|
But the most important thing here is that since we add elements to the vector, it will eventually need to be reallocated, and that reallocation will invalidate the iterator, meaning that the following iteration will provoke an undefined behaviour.
|
|
The equivalent code in Rust is in @undefined-behaviour-rs and @undefined-behaviour-it-rs. // TODO
|
|
|
|
#columns(2, gutter: 11pt)[
|
|
#v(0.8cm)
|
|
#figure(
|
|
align(left,
|
|
```rust
|
|
let mut vec = vec![1, 2, 3];
|
|
for value in &vec {
|
|
vec.push(value);
|
|
}
|
|
```
|
|
),
|
|
caption: [Rust version of @undefined-behaviour-cpp],
|
|
)<undefined-behaviour-rs>
|
|
|
|
#colbreak()
|
|
|
|
#figure(
|
|
align(left,
|
|
```rust
|
|
let mut vec = vec![1, 2, 3];
|
|
let iter = vec.iter();
|
|
loop {
|
|
match iter.next() {
|
|
Some(x) => vec.push(x),
|
|
None => break,
|
|
}
|
|
}
|
|
```
|
|
),
|
|
caption: [Rust version of @undefined-behaviour-it-cpp],
|
|
)<undefined-behaviour-it-rs>
|
|
]
|
|
|
|
|
|
What happens is that the iterator needs to borrow the vector.
|
|
Because it is borrowed, it can no longer be borrowed as mutable since mutating it could invalidate the other borrowers.
|
|
And effectively, the borrow checker will crash the compiler with the error in Snippet X. // TODO
|
|
|
|
#figure(
|
|
align(left,
|
|
```
|
|
error[E0502]: cannot borrow `vec` as mutable because it is also borrowed as immutable
|
|
--> src/main.rs:4:9
|
|
|
|
|
3 | for value in &vec {
|
|
| ----
|
|
| |
|
|
| immutable borrow occurs here
|
|
| immutable borrow later used here
|
|
4 | vec.push(*value);
|
|
| ^^^^^^^^^^^^^^^^ mutable borrow occurs here
|
|
```
|
|
),
|
|
caption: [Error given by the compiler on @undefined-behaviour-rs],
|
|
)
|
|
|
|
This example is one of the many examples of how powerful the borrow checker is: in Rust code, there can be no dangling reference, and all the segmentation faults coming from them are detected by the compiler.
|
|
The borrow checker may seem like an enemy to newcomers because it often rejects code that seem correct, but once they get used to it, they understand what is the problem with their code and either fix the problem easily, or realize that the whole architecture is wrong and understand why.
|
|
|
|
It is probably for those reasons that Rust is the _most loved programming language_ according to the Stack Overflow
|
|
Developer Survey // TODO in~\citeyear{so-survey-2016}, \citeyear{so-survey-2017}, \citeyear{so-survey-2018} and~\citeyear{so-survey-2019}.
|
|
|
|
=== Tooling
|
|
|
|
Moreover, Rust comes with many programs that help developers.
|
|
- #link("https://github.com/rust-lang/rust")[*`rustc`*] is the Rust compiler. It is comfortable due to the clarity and precise explanations of its error messages.
|
|
- #link("https://github.com/rust-lang/cargo")[*`cargo`*] is the official Rust's project and package manager. It manages compilation, dependencies, documentation, tests, etc.
|
|
- #link("https://github.com/racer-rust/racer")[*`racer`*], #link("https://github.com/rust-lang/rls")[*`rls`*] (Rust
|
|
Language Server) and #link("https://github.com/rust-analyzer/rust-analyzer")[*`rust-analyzer`*] are software that manage automatic compilation to display errors in code editors as well as providing semantic code completion.
|
|
- #link("https://github.com/rust-lang/rustfmt")[*`rustfmt`*] auto formats code.
|
|
- #link("https://github.com/rust-lang/rust-clippy")[*`clippy`*] is a linter that detects unidiomatic code and suggests modifications.
|
|
|
|
=== Glium
|
|
|
|
When we need to perform rendering for 3D content analysis or for evaluation, we use the #link("https://github.com/glium/glium")[`glium`] library.
|
|
Glium has many advantages over using raw OpenGL calls.
|
|
Its objectives are:
|
|
|
|
- to be easy to use: it exposes functions that are higher level than raw OpenGL calls, but still low enough level to let the developer free;
|
|
- to be safe: debugging OpenGL code can be a nightmare, and glium does its best to use the borrow checker to its advantage to avoid OpenGL bugs;
|
|
- to be fast: the binary produced use optimized OpenGL functions calls;
|
|
- to be compatible: glium seeks to support the latest versions of OpenGL functions and falls back to older functions if the most recent ones are not supported on the device.
|
|
|
|
=== Conclusion
|
|
|
|
In our work, many tasks will consist in 3D content analysis, reorganization, rendering and evaluation.
|
|
Many of these tasks require long computations, lasting from hours to entire days.
|
|
To perform them, we need a programming language that has good performances.
|
|
In addition, the extra features that Rust provides ease tremendously development, and this is why we use Rust for all tasks that do not require having a web interface.
|
|
|
|
|