/
Go3D.h
221 lines (182 loc) · 9.3 KB
/
Go3D.h
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
#pragma once
namespace partII
{
// Frame buffer dimensions
static const auto g_scWidth = 1280u;
static const auto g_scHeight = 720u;
#define ARR_SIZE(x) (sizeof(x) / sizeof(x[0]))
// Silence macro redefinition warnings
#undef TO_RASTER
// Transform a given vertex in clip-space [-w,w] to raster-space [0, {w|h}]
#define TO_RASTER(v) glm::vec4((g_scWidth * (v.x + v.w) / 2), (g_scHeight * (v.w - v.y) / 2), v.z, v.w)
void OutputFrame(const std::vector<glm::vec3>& frameBuffer, const char* filename)
{
assert(frameBuffer.size() >= (g_scWidth * g_scHeight));
FILE* pFile = nullptr;
fopen_s(&pFile, filename, "w");
fprintf(pFile, "P3\n%d %d\n%d\n ", g_scWidth, g_scHeight, 255);
for (auto i = 0; i < g_scWidth * g_scHeight; ++i)
{
// Write out color values clamped to [0, 255]
uint32_t r = static_cast<uint32_t>(255 * glm::clamp(frameBuffer[i].r, 0.0f, 1.0f));
uint32_t g = static_cast<uint32_t>(255 * glm::clamp(frameBuffer[i].g, 0.0f, 1.0f));
uint32_t b = static_cast<uint32_t>(255 * glm::clamp(frameBuffer[i].b, 0.0f, 1.0f));
fprintf(pFile, "%d %d %d ", r, g, b);
}
fclose(pFile);
}
void InitializeSceneObjects(std::vector<glm::mat4>& objects)
{
// Construct a scene of few cubes randomly positioned
const glm::mat4 identity(1.f);
glm::mat4 M0 = glm::translate(identity, glm::vec3(0, 0, 2.f));
M0 = glm::rotate(M0, glm::radians(45.f), glm::vec3(0, 1, 0));
glm::mat4 M1 = glm::translate(identity, glm::vec3(-3.75f, 0, 0));
M1 = glm::rotate(M1, glm::radians(30.f), glm::vec3(1, 0, 0));
glm::mat4 M2 = glm::translate(identity, glm::vec3(3.75f, 0, 0));
M2 = glm::rotate(M2, glm::radians(60.f), glm::vec3(0, 1, 0));
glm::mat4 M3 = glm::translate(identity, glm::vec3(0, 0, -2.f));
M3 = glm::rotate(M3, glm::radians(90.f), glm::vec3(0, 0, 1));
// Change the order of cubes being rendered, see how it changes with and without depth test
objects.push_back(M0);
objects.push_back(M1);
objects.push_back(M2);
objects.push_back(M3);
}
glm::vec4 VS(const glm::vec3& pos, const glm::mat4& M, const glm::mat4& V, const glm::mat4& P)
{
return (P * V * M * glm::vec4(pos, 1.0f));
}
bool EvaluateEdgeFunction(const glm::vec3& E, const glm::vec3& sample)
{
// Interpolate edge function at given sample
float result = (E.x * sample.x) + (E.y * sample.y) + E.z;
// Apply tie-breaking rules on shared vertices in order to avoid double-shading fragments
if (result > 0.0f) return true;
else if (result < 0.0f) return false;
if (E.x > 0.f) return true;
else if (E.x < 0.0f) return false;
if ((E.x == 0.0f) && (E.y < 0.0f)) return false;
else return true;
}
void Go3D()
{
// Setup vertices & indices to draw an indexed cube
glm::vec3 vertices[] =
{
{ 1.0f, -1.0f, -1.0f },
{ 1.0f, -1.0f, 1.0f },
{ -1.0f, -1.0f, 1.0f },
{ -1.0f, -1.0f, -1.0f },
{ 1.0f, 1.0f, -1.0f },
{ 1.0f, 1.0f, 1.0f },
{ -1.0f, 1.0f, 1.0f },
{ -1.0f, 1.0f, -1.0f },
};
uint32_t indices[] =
{
// 6 faces of cube * 2 triangles per-face * 3 vertices per-triangle = 36 indices
1,3,0, 7,5,4, 4,1,0, 5,2,1, 2,7,3, 0,7,4, 1,2,3, 7,6,5, 4,5,1, 5,6,2, 2,6,7, 0,3,7
};
// Use per-face colors
glm::vec3 colors[] =
{
glm::vec3(0, 0, 1),
glm::vec3(0, 1, 0),
glm::vec3(0, 1, 1),
glm::vec3(1, 1, 1),
glm::vec3(1, 0, 1),
glm::vec3(1, 1, 0)
};
// Allocate and clear the frame buffer before starting to render to it
std::vector<glm::vec3> frameBuffer(g_scWidth * g_scHeight, glm::vec3(0, 0, 0)); // clear color black = vec3(0, 0, 0)
// Allocate and clear the depth buffer (not z-buffer but 1/w-buffer in this part!) to 0.0
std::vector<float> depthBuffer(g_scWidth * g_scHeight, 0.0);
// Let's draw multiple objects
std::vector<glm::mat4> objects;
InitializeSceneObjects(objects);
// Build view & projection matrices (right-handed sysem)
float nearPlane = 0.1f;
float farPlane = 100.f;
glm::vec3 eye(0, 3.75, 6.5);
glm::vec3 lookat(0, 0, 0);
glm::vec3 up(0, 1, 0);
glm::mat4 view = glm::lookAt(eye, lookat, up);
glm::mat4 proj = glm::perspective(glm::radians(60.f), static_cast<float>(g_scWidth) / static_cast<float>(g_scHeight), nearPlane, farPlane);
// Loop over objects in the scene
for (size_t n = 0; n < objects.size(); n++)
{
// Loop over triangles in a given object and rasterize them one by one
for (uint32_t idx = 0; idx < ARR_SIZE(indices) / 3; idx++)
{
// We're gonna fetch object-space vertices from the vertex buffer indexed by the values in index buffer
// and pass them directly to each VS invocation
const glm::vec3& v0 = vertices[indices[idx * 3]];
const glm::vec3& v1 = vertices[indices[idx * 3 + 1]];
const glm::vec3& v2 = vertices[indices[idx * 3 + 2]];
// Invoke function for each vertex of the triangle to transform them from object-space to clip-space (-w, w)
glm::vec4 v0Clip = VS(v0, objects[n], view, proj);
glm::vec4 v1Clip = VS(v1, objects[n], view, proj);
glm::vec4 v2Clip = VS(v2, objects[n], view, proj);
// Apply viewport transformation
// Notice that we haven't applied homogeneous division and are still utilizing homogeneous coordinates
glm::vec4 v0Homogen = TO_RASTER(v0Clip);
glm::vec4 v1Homogen = TO_RASTER(v1Clip);
glm::vec4 v2Homogen = TO_RASTER(v2Clip);
// Base vertex matrix
glm::mat3 M =
{
// Notice that glm is itself column-major)
{ v0Homogen.x, v1Homogen.x, v2Homogen.x},
{ v0Homogen.y, v1Homogen.y, v2Homogen.y},
{ v0Homogen.w, v1Homogen.w, v2Homogen.w},
};
// Singular vertex matrix (det(M) == 0.0) means that the triangle has zero area,
// which in turn means that it's a degenerate triangle which should not be rendered anyways,
// whereas (det(M) > 0) implies a back-facing triangle so we're going to skip such primitives
float det = glm::determinant(M);
if (det >= 0.0f)
continue;
// Compute the inverse of vertex matrix to use it for setting up edge & constant functions
M = inverse(M);
// Set up edge functions based on the vertex matrix
glm::vec3 E0 = M[0];
glm::vec3 E1 = M[1];
glm::vec3 E2 = M[2];
// Calculate constant function to interpolate 1/w
glm::vec3 C = M * glm::vec3(1, 1, 1);
// Start rasterizing by looping over pixels to output a per-pixel color
for (auto y = 0; y < g_scHeight; y++)
{
for (auto x = 0; x < g_scWidth; x++)
{
// Sample location at the center of each pixel
glm::vec3 sample = { x + 0.5f, y + 0.5f, 1.0f };
// Evaluate edge functions at current fragment
bool inside0 = EvaluateEdgeFunction(E0, sample);
bool inside1 = EvaluateEdgeFunction(E1, sample);
bool inside2 = EvaluateEdgeFunction(E2, sample);
// If sample is "inside" of all three half-spaces bounded by the three edges of the triangle, it's 'on' the triangle
if (inside0 && inside1 && inside2)
{
// Interpolate 1/w at current fragment
float oneOverW = (C.x * sample.x) + (C.y * sample.y) + C.z;
// Perform depth test with interpolated 1/w value
// Notice that the greater 1/w value is, the closer the object would be to our virtual camera,
// hence "less_than_equal" comparison is actually oneOverW >= depthBuffer[sample] and not oneOverW <= depthBuffer[sample] here
if (oneOverW >= depthBuffer[x + y * g_scWidth])
{
// Depth test passed; update depth buffer value
depthBuffer[x + y * g_scWidth] = oneOverW;
// Write new color at this fragment
frameBuffer[x + y * g_scWidth] = colors[indices[3 * idx] % 6];
}
}
}
}
}
}
// Rendering of one frame is finished, output a .PPM file of the contents of our frame buffer to see what we actually just rendered
OutputFrame(frameBuffer, "../render_go_3d.ppm");
}
}