-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest.txt
More file actions
225 lines (166 loc) · 12.1 KB
/
Copy pathtest.txt
File metadata and controls
225 lines (166 loc) · 12.1 KB
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
3DRenderer Overview
3DRenderer is a minimalist C++ software rasterization engine – a full 3D rendering pipeline implemented from scratch without any GPU API (OpenGL/DirectX/Vulkan)
github.com
. It is inspired by TinyRenderer and uses only libraries for window/output (SFML) and image loading (stb_image)
github.com
. The core is a custom math library (templated vectors/matrices) and an OBJ parser to load mesh geometry. The author notes the engine implements a complete 3D raster pipeline with features like back-face culling, perspective-correct interpolation, a Z-buffer for hidden-surface removal, and multiple shading modes (flat, Gouraud, Phong, and normal-mapped shading)
github.com
. Importantly, the renderer is parallelized with OpenMP and uses C++ virtual methods to make the vertex/fragment “shaders” extendable
github.com
.
Mesh Loading and Data Structures
OBJ model parsing: The engine reads standard *.obj files to obtain mesh data (vertices, texture coordinates, normals, face indices). Parsed data is stored in mesh or model objects (lists of vertices and faces). (The README explicitly lists an “Obj model parser” as a feature
github.com
.)
Math library: A custom templated vector/matrix library handles all 3D math (3- and 4-component vectors, 4×4 matrices, etc.)
github.com
. All transformations (translation, rotation, projection) use these math types. This lightweight math system replaces external libs like GLM.
Camera control: The engine provides a simple orbit‐style camera (rotating around an axis) with configurable parameters
github.com
. Internally, the camera maintains a view matrix (from position/orientation) and a projection matrix (perspective projection). The view matrix transforms world-space vertices into camera space; the projection matrix then maps camera-space 3D points into clip/NDC space. (In practice, the code will multiply each vertex by Model, View, and Projection matrices to get clip coordinates.)
Transformation Pipeline
Each 3D vertex passes through a standard sequence of linear transforms before rasterization:
Model Transform: Positions/rotates the object in world space. The code likely maintains one model matrix per object to account for its position/orientation/scale.
View Transform: Converts from world space to camera (eye) space. The camera’s view matrix (often a LookAt or orbit‐matrix) is applied. This makes the scene relative to the camera.
Projection: A 4×4 perspective-projection matrix (derived from a field-of-view, aspect ratio, near/far planes) projects camera-space coordinates into clip space. After multiplying by the projection matrix, a homogeneous divide (divide by W) yields normalized device coordinates.
After projection the coordinates are in clip/normalized space where X,Y map to [-1,1] screen bounds. This pipeline of (Model × View × Projection) is handled in what the code calls a vertex shader (implemented via a virtual C++ method)
github.com
. The README indicates the renderer uses perspective-correct interpolation across triangles
github.com
, meaning it properly divides attributes by depth before interpolation to avoid distortion.
##Camera and View/Projection Matrices
The camera system allows rotating the view around the scene (an orbit camera). Each frame the engine updates a view matrix based on the camera’s position/orientation. Combined with a fixed projection matrix, this produces the View and Projection matrices of the pipeline. Because the engine uses its own math library, the code will explicitly build or update these matrices (for example using a look-at or rotation matrix). All world-space vertex positions are multiplied by View × Projection (and by the object’s Model matrix) to compute screen coordinates. In effect, 3D points are transformed to 2D screen points as in a typical OpenGL/Vulkan pipeline, just done manually in C++.
Rasterization and Fragment Processing
With transformed vertices in 2D, the engine performs triangle rasterization entirely in software:
Back-face culling: Before rasterizing, each triangle’s orientation is tested (e.g. via cross product of edges in screen space). Triangles facing away from the camera are skipped. This is explicitly mentioned as a feature
github.com
.
Triangle setup and scan conversion: For each visible triangle, the code determines its screen-space bounding box or spans. It then loops over pixel locations within the triangle. At each pixel, it computes barycentric coordinates to interpolate per-vertex attributes (depth, normals, texture UVs, etc.). The README notes “flat, Gouraud, Phong and normal map shaders” are implemented
github.com
, implying that interpolated data (like normals or colors) is used. Because perspective-correct interpolation is enabled
github.com
, the engine divides barycentric weights by depth to get correct attribute interpolation under perspective.
Depth (Z) buffering: A 2D depth buffer holds the closest depth seen so far for each pixel. As each fragment (pixel) is generated, its depth is compared to the existing Z-buffer value. If nearer, the pixel is shaded and the buffer is updated. Hidden-surface (back) removal via a Z-buffer is explicitly listed
github.com
.
Shading: The engine uses a polymorphic shader system. A vertex shader stage transforms positions (as above), and a fragment shader stage computes final pixel color. Different shader classes implement different lighting models:
Flat shading: One color per triangle (using the triangle’s normal and a light direction).
Gouraud shading: Vertex lighting is computed (e.g. via dot(normal, light)), and colors are interpolated across the triangle.
Phong shading: Surface normals are interpolated per-pixel, and lighting (diffuse + specular) is computed in the fragment shader. This yields smoother highlights.
Normal-mapped shading: A normal texture (normal map) perturbs the interpolated normal. The code supports tangent-space normal mapping
github.com
, meaning it likely computes a tangent/bitangent at each vertex and uses the normal map to alter the normal before lighting.
All shaders presumably use the custom math library for vector operations. The README mentions “vertex and fragment shaders using C++ virtual functions”
github.com
, indicating an inheritance structure like Shader base class with virtual methods for vertex/fragment processing.
Parallelization: The rasterization loops (over triangles or pixels) are parallelized with OpenMP
github.com
. This typically means the triangle processing loop or the per-pixel loop inside a triangle is split across threads. This speeds up the CPU-based rasterizer.
After all triangles are drawn into an image buffer (with depth testing), the final color buffer is ready.
Rendering Loop and Output
The engine uses SFML to create a window and display the rendered image
github.com
. A typical frame loop in src/Main.cpp or similar would: handle user input (e.g. camera rotation), clear the color/depth buffers, invoke the rasterizer to draw all meshes with the chosen shader, then update the SFML texture or pixel array and display it on screen. The README explicitly lists “SFML Backend for graphics output” as an engine feature
github.com
. In summary, the program continuously: load/transform mesh data, rasterize triangles into a pixel buffer (with Z-buffer and shading), and present that buffer as an image.
Component Interaction (Input to Final Image)
Putting it all together, the data flow is:
Input: An OBJ model file (and optional texture/normal maps) is parsed into mesh data (vertices, normals, UVs).
Preparation: For each mesh, precompute any needed per-vertex data (e.g. transformed normals or tangents).
Per-frame setup: Compute the camera’s view and projection matrices. For each object, compute its model matrix (if movable).
Vertex processing: For each triangle, apply Model→View→Projection transforms to its 3D vertices (in a vertex shader). Perform back-face culling using transformed normals.
Clipping/Culling: (Optionally, if implemented) clip triangles against the view frustum. Skipped triangles or parts outside screen are discarded.
Rasterization: For each remaining triangle, iterate over its covered pixels. For each pixel, compute depth and interpolated attributes (color, normal, UV).
Depth test: Compare pixel depth to the Z-buffer; if closer, proceed, else skip.
Fragment shading: Run the fragment shader to compute the pixel’s color. This may involve sampling a texture or normal map and doing a lighting calculation (flat/Gouraud/Phong) based on interpolated normals and a light direction. Perspective-correct interpolation ensures the lighting and texturing look correct in perspective
github.com
.
Output: Write the shaded color to the framebuffer and update the Z-buffer. After all triangles are processed, the final image is drawn to the screen via SFML.
Throughout this process, the major components (mesh loader, camera, transform math, shader logic, rasterizer, and output system) work together. The result is a rendered 2D image of the 3D scene.
Key References: The project’s README confirms its pipeline features: it “implements a 3D software rasterization pipeline” with backface culling, perspective-correct shading, Z-buffering, and multiple shader models
github.com
. It also notes the use of an OBJ loader, custom math library, and SFML for display
github.com
. These features match the classic steps outlined above.
This code has both good practices and some areas that could be improved. Let me break it down:
## **Good Practices:**
- Template-based design for reusability
- Specialized implementations for common cases (2D, 3D, 4D)
- Bounds checking with `assert`
- Const correctness
- Clear separation of interface and implementation
- Useful typedefs at the end
## **Areas for Improvement:**
### 1. **Missing Include**
```cpp
#include <cassert> // You're using assert but not including it
#include <cmath> // You're using std::sqrt but not including it
```
### 2. **Inconsistent Initialization**
```cpp
// Current - uninitialized members
Vector<3, T>::Vector() {} // x, y, z are uninitialized!
// Better - value initialization
Vector<3, T>::Vector() : x(T()), y(T()), z(T()) {}
```
### 3. **Mixed Access Patterns**
```cpp
// Sometimes you use data[i], sometimes x,y,z directly
// This can be confusing and less efficient
```
### 4. **Missing Common Operations**
```cpp
// Should have:
bool operator==(const Vector& other) const;
bool operator!=(const Vector& other) const;
Vector& operator+=(const Vector& other);
Vector& operator-=(const Vector& other);
// etc.
```
### 5. **Potential Performance Issues**
```cpp
// Returning by value in operators (normal for vectors)
// But missing move semantics for C++11 and later
```
### 6. **Template Organization**
The commented-out declarations at the top are confusing. Either:
- Remove them entirely, or
- Put them in a separate "fwd.decl" file
## **Suggested Improvements:**
### **Fix Constructor Initialization:**
```cpp
template <typename T>
Vector<3, T>::Vector() : x(T()), y(T()), z(T()) {}
template <typename T>
Vector<3, T>::Vector(T X, T Y, T Z) : x(X), y(Y), z(Z) {}
```
### **Add Common Operations:**
```cpp
template <int N, typename T>
bool operator==(const Vector<N, T>& left, const Vector<N, T>& right) {
for (int i = 0; i < N; i++) {
if (left[i] != right[i]) return false;
}
return true;
}
```
### **Consider a Unified Approach:**
Instead of specializing for 2D, 3D, 4D, you could use a single template with named accessors:
```cpp
template <int N, typename T>
class Vector {
private:
T data[N];
public:
// Generic access
T& operator[](size_t index) { return data[index]; }
// Named access for common cases
T& x() requires (N >= 1) { return data[0]; }
T& y() requires (N >= 2) { return data[1]; }
T& z() requires (N >= 3) { return data[2]; }
T& w() requires (N >= 4) { return data[3]; }
};
```
## **Verdict:**
It's not "bad practice" overall - it's actually quite decent! The template design is good, but it needs some polishing for production use. The main issues are the missing includes and uninitialized values, which can cause hard-to-debug problems.
The code shows good understanding of templates and operator overloading, just needs some refinement!