title | author | date | header-includes |
---|---|---|---|
3D Rendering |
150015686 |
April 2018 |
\addtolength{\oddsidemargin}{-.875in}
\addtolength{\evensidemargin}{-.875in}
\addtolength{\textwidth}{1.75in}
\addtolength{\topmargin}{-.875in}
\addtolength{\textheight}{1.75in}
\usepackage{float}
\let\origfigure\figure
\let\endorigfigure\endfigure
\renewenvironment{figure}[1][2] {
\expandafter\origfigure\expandafter[H]
} {
\endorigfigure
}
|
The aim of this project is to create a tool to be used to interpolate between a number of different face models with constant mesh by providing a helpful user interface for choosing the face models to be interpolated and also to display the resulting model.
The first step of this project was to ingest the given data set and convert it into a format that could be easily used later on.
The data is provided in a format that requires some processing to extract the actual shape and texture of the model: an average shape and texture is defined, upon which offsets need to be added to arrive to the final model.
More precisely: $$ V_i = V_0 + \mathit{wv}_i * \mathit{Voff}_i $$ $$ T_i = T_0 + \mathit{wt}_i * \mathit{Toff}_i $$
Where
All faces are read and processed at startup and saved in memory for easy access.
The first step in rendering the models on the screen was set up the boilerplate
necessary to give access to the means of drawing on a window. For this project,
we used Java Swing to create a component and override it's
paintComponent(Graphics g)
method, giving us access to the Graphics
object
which can be used to draw on the window.
To test that the face was loaded correctly, we first just drew the triangles on the screen as a wireframe. The resulting render was satisfactory (see Renders)) confirming that the faces were loaded appropriately.
During the implementation of the wire frame renderer, we noticed the following facts about the given model:
- the face was centered at (0,0) on the x and y axes
- the scale of the face was quite large, with the values of the vertices being
around the order of
$10^5$ - the face was aligned with the xy plane
These factors had to be taken into account for the final render:
- the screen space had to be translated, as the default settings has the 0,0 point on the top left of the screen
- the face had to be scaled down, so that it could fit into screen space (which was set to only 500 px).
- due to the face's alignment, a quick ortographic projection could be achieved
by just ignoring the
$z$ component of the vertices.
The next step was to add colour to the drawn triangles. We defined the colour of each of the triangles to be constant on its surface (no interpolation) and to be equal to the average colour of the vertices defining the triangle. With this we got the first colour render. And with this, the next issue to solve was discovered. Some of the triangles were being drawn in the wrong order, leading to artifacts and overall a bad render quality.
Fortunately, this is a problem that has been already solved in many different ways. One of these ways is to use something called the Painter's algorithm. The algorithm naively solves the problem by drawing the triangles in the order of their distance from the projection plane (z=0 in our case) from furthest to nearest. This approach is known to have issues when drawing complex scenes, as it orders entire triangles from furthest to nearest and therefore there is the possiblity of drawing intersecting triangles incorrectly.
The result (see Renders) was satisfactory, allowing us to move to the next step.
The lighting for the scene uses Phong's reflection model with a directional
light in the
An ambient source of light was added as well, to make the scene more bright. With this addition, the resulting values of the computation had to be clamped to the correct range, as some areas of the image, especially those perpendicular to the viewing direction (e.g the forehead) were oversaturated and caused exceptions with the Graphics backend.
Additionally, the colour and direction of lights can be configured, as specified in the Configuration section.
For all this time, the render only drew one face in the middle of the screen. But the final project requires a number of other elements on the screen.
For this, we took inspiration from the Processing graphics library and designed a renderer that allows the user to apply transformations to the scene, draw elements on the screen and also save and restore transformations with the help of a stack.
The renderer provides the basic transformations:
- scale
- translate
- rotate in all 3 axes
These are then used to compose the final UI and also allows us to display more than just one face on the screen.
To showcase the rotation transformations, the composed face is rotated by 10 degrees on the Y axis. For further tinkering see the Configuration section.
The lighting is applyed after all the transformations, therefore always keeping the light direction as defined.
Rotation has been exposed to the user using the following key bindings:
J
andL
to rotate around the Y axisI
andK
to rotate around the X axis
The face composition works in two steps.
The face compositor holds in memory a vector of the same size as the number of
faces. Each value will be in the range
$$ F_{\mathit{composed}} = \sum^{i=0}{i < \mathit{no_faces}} \frac{C_i}{\sum^{j=0}{j<\mathit{no_faces}}{C}} * F_i$$
Where
To actually set these values the user is presented with a triangle, with each point of the traingle being annotated with a face and its id. When the user clicks on the triangle, the weights of the faces present are adjusted by converting the click of the user into barycentric coordinates relative to the triangle using the following formulae:
where
The user is then allowed to change the faces at the vertices of the triangle as such:
- to change the currently selected face (the one whose id is in square brackets), press left or right arrow.
- to change the currently selected index, press A or D.
To expose some of the functionality and allow for customisation, a config file
can be found (app.properties
) with the following configurable properties:
offsetAmplificationFactor
(default:1
) - a factor used to amplify the vertex offsets when generating the faces. This was used to amplify the differences between faces, which can make the result of the interpolation more visibledataPath
(default:data\
) - the path to the folder with the data filesfaceCount
(default:4
) - the number of faces to be loaded (min:3
, max:199
)ambientColor
(default:0.2, 0.2, 0.2
) - the colour of the ambient in RGB components between 0 and 1directLightDirection
(default:0,0,1
) - the direction of directional light (x,y,z). The direction will be normalizeddirectLightColor
(default:1,1,1
) - the colour of the directional light in RGB components between 0 and 1scaleX
(default:1
) - scale factor to be applied in the x directionscaleY
(default:1
) - scale factor to be applied in the y directionscaleZ
(default:1
) - scale factor to be applied in the z directiontranslateX
(default:0
) - translation to be applied in the x directiontranslateY
(default:0
) - translation to be applied in the y directiontranslateZ
(default:0
) - translation to be applied in the z directionrotateX
(default:0
) - rotation to be applied on the x axis (in degrees)rotateY
(default:-10
) - rotation to be applied on the y axis (in degrees)rotateZ
(default:0
) - rotation to be applied on the z axis (in degrees)
By this point it became quite obvious that rendering the final UI at a decent FPS is but a dream, but nontheless some performance improvements were attempted, driven by the use of a profiler.
Before the start of the optimization, this is a snapshot of the usual time to render the scene:
And here is the result of profiling the drawing. Do note that the time is not relevant as profiling hits performance. We're mostly interested at the ratios:
We know that we cannot optimize the actual drawing, as it is done by the awt library. But we see that a large proportion of the time is taken by calculating normals, and more accurately by calculating the magnitude of a vector. Additonally the collect call is used to transform the vertices of a triangle into scren space using a matrix multiplication.
From here we see that the weak link of the pipeline is the linear algebra library. Due to the fact that it is designed to work for any matrices and vectors, some operations have some overhead which quickly sums up when we are doing a few hundred thousand multiplications. Therefore extended the given matrices and vectors to improve performance for our use case which is 4x4 matrices and 4-vectors, while also being compatible with the rest of the library.
After this optimization step has been completed, here is the result of the profiling:
\pagebreak
And here is the average rendering time for the scene:
As you can see, the optimization reduced the time for rendering the scene from ~6 seconds per frame to 1.5 seconds per frame. While this is not close to real time, it is still a lot better.
The profiling now shows an almost perfect result, given the approach we are taking with the time to render being split between the actual draw calls to the graphics backend and the sorting of the triangles.
The sorting of the triangles could be replaces with a different approach, such as z-buffering, but the Graphics library we are using doesn't support direct pixel manipulation.
For optimizing the system overall, a lot of the work could be passed on to the GPU, both for the actual rendering pipeline and for the face composition, as GPU's are optimized for matrix and vector manipulations.
The basic specification was fully achieved, with the system successfully composing 3 faces using a triangle as a user input. The composed face is displayed using flat shading and an ortographic projection.
The advanced specification was also implemented, with the user being able to select which faces to compose from and with the system remembering the weights of the previously used faces. The user is able to preview the faces between which he is interpolation, but not when selecting the face to be used in the interpolation. This was a design choice, as with the system's current performance, rendering 100 faces would take quite a long time.
Some of the highly advanced requirements have also been implemented, with the user being able to change the direction and colour of the light, the addition of ambient lighting, as well as the ability to transform the generated face by rotating, scaling and translating it on the screen using configuration parameters. All these transformations can be done at run time as well, but for the project only the x and y axis rotations have been bound to keys.
This was an interesting practical to implement. I am overall happy with the submission. I wish I had started the practical using a library like OpenGL, as it would be more close to what the state of the art is and also produces an a lot faster system.
\pagebreak
\appendix
Modify the config as you wish...
Only 10 faces have been provided in the submission. If you want to use more,
please change the dataPath
property.
In the project folder run:
./gradlew run