scimesh 0.3.2
Headless CPU-only 3D software renderer for scientific mesh visualization
Loading...
Searching...
No Matches
rasterizer.cpp
Go to the documentation of this file.
3#include <algorithm>
4#include <cmath>
5
6#ifdef _OPENMP
7#include <omp.h>
8#endif
9
10namespace scimesh {
11
13 : width(w), height(h), z_buffer(w * h, 1.0f), normal_buffer(w * h, Vec3(0.0f)) {}
14
15void Rasterizer::clear(float clear_depth) {
16 std::fill(z_buffer.begin(), z_buffer.end(), clear_depth);
17 std::fill(normal_buffer.begin(), normal_buffer.end(), Vec3(0.0f));
18}
19
20void Rasterizer::shade_and_write(int x, int y, float depth,
21 const Color &color, const Vec3 &normal,
22 const Vec3 &light_direction, Image &output) {
23 if (x < 0 || x >= width || y < 0 || y >= height)
24 return;
25
26 int idx = y * width + x;
27 if (blend_mode || depth < z_buffer[idx]) {
28 Color shaded;
29 if (lights.empty()) {
30 shaded = shade_pixel(color, normal, light_direction,
32 } else {
33 shaded = shade_pixel_multi(color, normal, lights, ambient,
35 }
36
37 if (contrast != 1.0f) {
38 auto apply_contrast = [&](float v) {
39 return std::clamp((v - 0.5f) * contrast + 0.5f, 0.0f, 1.0f);
40 };
41 shaded.r = apply_contrast(shaded.r);
42 shaded.g = apply_contrast(shaded.g);
43 shaded.b = apply_contrast(shaded.b);
44 }
45
46 uint8_t r = static_cast<uint8_t>(std::clamp(shaded.r, 0.0f, 1.0f) * 255.0f);
47 uint8_t g = static_cast<uint8_t>(std::clamp(shaded.g, 0.0f, 1.0f) * 255.0f);
48 uint8_t b = static_cast<uint8_t>(std::clamp(shaded.b, 0.0f, 1.0f) * 255.0f);
49 uint8_t a = static_cast<uint8_t>(std::clamp(shaded.a, 0.0f, 1.0f) * 255.0f);
50
51 if (fog_enabled) {
52 float fog_fac = (depth - fog_start) / (fog_end - fog_start);
53 fog_fac = std::max(0.0f, std::min(1.0f, fog_fac));
54 r = static_cast<uint8_t>(r * (1.0f - fog_fac) + fog_color.r * 255.0f * fog_fac);
55 g = static_cast<uint8_t>(g * (1.0f - fog_fac) + fog_color.g * 255.0f * fog_fac);
56 b = static_cast<uint8_t>(b * (1.0f - fog_fac) + fog_color.b * 255.0f * fog_fac);
57 a = static_cast<uint8_t>(a * (1.0f - fog_fac) + fog_color.a * 255.0f * fog_fac);
58 }
59
60 if (blend_mode) {
61 uint8_t dr, dg, db, da;
62 output.get_pixel(x, y, dr, dg, db, da);
63 float src_a = a / 255.0f;
64 float inv_a = 1.0f - src_a;
65 r = static_cast<uint8_t>(r * src_a + dr * inv_a);
66 g = static_cast<uint8_t>(g * src_a + dg * inv_a);
67 b = static_cast<uint8_t>(b * src_a + db * inv_a);
68 a = static_cast<uint8_t>(a + da * inv_a);
69 } else {
70 z_buffer[idx] = depth;
71 normal_buffer[idx] = normal;
72 }
73
74 output.set_pixel(x, y, r, g, b, a);
75 }
76}
77
79 const Vec3 &screen_v0, const Color &color0, const Vec3 &normal0, const Vec2 &uv0,
80 const Vec3 &screen_v1, const Color &color1, const Vec3 &normal1, const Vec2 &uv1,
81 const Vec3 &screen_v2, const Color &color2, const Vec3 &normal2, const Vec2 &uv2,
82 bool backface_culling,
83 bool smooth_shading,
84 const Vec3 &light_direction,
85 bool wireframe,
86 const Color &wireframe_color,
87 Image &output) {
88
89 float area = (screen_v1.x - screen_v0.x) * (screen_v2.y - screen_v0.y) -
90 (screen_v2.x - screen_v0.x) * (screen_v1.y - screen_v0.y);
91
92 if (backface_culling && area > 0.0f)
93 return;
94
95 if (std::abs(area) < 1e-12f)
96 return;
97
98 float abs_area = std::abs(area);
99
100 float min_x = std::min({screen_v0.x, screen_v1.x, screen_v2.x});
101 float max_x = std::max({screen_v0.x, screen_v1.x, screen_v2.x});
102 float min_y = std::min({screen_v0.y, screen_v1.y, screen_v2.y});
103 float max_y = std::max({screen_v0.y, screen_v1.y, screen_v2.y});
104
105 int x_start = std::max(0, static_cast<int>(std::floor(min_x)));
106 int x_end = std::min(width - 1, static_cast<int>(std::ceil(max_x)));
107 int y_start = std::max(0, static_cast<int>(std::floor(min_y)));
108 int y_end = std::min(height - 1, static_cast<int>(std::ceil(max_y)));
109
110 float inv_area = 1.0f / area;
111
112 float wire_thresh = 0.0f;
113 if (wireframe) {
114 wire_thresh = 1.5f / std::sqrt(abs_area > 1e-9f ? abs_area : 1.0f);
115 if (wire_thresh > 0.5f) wire_thresh = 0.5f;
116 }
117
118#ifdef _OPENMP
119#pragma omp parallel for if((y_end - y_start) > 16) schedule(static)
120#endif
121 for (int y = y_start; y <= y_end; ++y) {
122 for (int x = x_start; x <= x_end; ++x) {
123 float px = static_cast<float>(x) + 0.5f;
124 float py = static_cast<float>(y) + 0.5f;
125
126 float w0 = ((screen_v1.x - px) * (screen_v2.y - py) -
127 (screen_v2.x - px) * (screen_v1.y - py)) * inv_area;
128 float w1 = ((screen_v2.x - px) * (screen_v0.y - py) -
129 (screen_v0.x - px) * (screen_v2.y - py)) * inv_area;
130 float w2 = 1.0f - w0 - w1;
131
132 if (w0 < 0.0f || w1 < 0.0f || w2 < 0.0f)
133 continue;
134
135 if (wireframe) {
136 if (w0 >= wire_thresh && w1 >= wire_thresh && w2 >= wire_thresh)
137 continue;
138 float depth = w0 * screen_v0.z + w1 * screen_v1.z + w2 * screen_v2.z;
139 int pidx = y * width + x;
140 output.set_pixel(x, y,
141 static_cast<uint8_t>(std::clamp(wireframe_color.r, 0.0f, 1.0f) * 255.0f),
142 static_cast<uint8_t>(std::clamp(wireframe_color.g, 0.0f, 1.0f) * 255.0f),
143 static_cast<uint8_t>(std::clamp(wireframe_color.b, 0.0f, 1.0f) * 255.0f),
144 static_cast<uint8_t>(std::clamp(wireframe_color.a, 0.0f, 1.0f) * 255.0f));
145 if (!blend_mode) {
146 z_buffer[pidx] = depth;
147 Vec3 wf_normal = w0 * normal0 + w1 * normal1 + w2 * normal2;
148 normal_buffer[pidx] = wf_normal;
149 }
150 continue;
151 }
152
153 float depth = w0 * screen_v0.z + w1 * screen_v1.z + w2 * screen_v2.z;
154
155 Color base_color;
156 Vec3 interp_normal;
157 if (smooth_shading) {
158 base_color = Color(
159 w0 * color0.r + w1 * color1.r + w2 * color2.r,
160 w0 * color0.g + w1 * color1.g + w2 * color2.g,
161 w0 * color0.b + w1 * color1.b + w2 * color2.b,
162 w0 * color0.a + w1 * color1.a + w2 * color2.a);
163 interp_normal = w0 * normal0 + w1 * normal1 + w2 * normal2;
164 } else {
165 base_color = color0;
166 interp_normal = normal0;
167 }
168
169 if (active_texture) {
170 Vec2 interp_uv = smooth_shading
171 ? w0 * uv0 + w1 * uv1 + w2 * uv2
172 : uv0;
173 Color tex = active_texture->sample_bilinear(interp_uv.x, interp_uv.y);
174 base_color = Color(base_color.r * tex.r, base_color.g * tex.g,
175 base_color.b * tex.b, base_color.a * tex.a);
176 }
177
178 shade_and_write(x, y, depth, base_color, interp_normal, light_direction, output);
179 }
180 }
181}
182
183void Rasterizer::rasterize_point(float screen_x, float screen_y, float depth,
184 float radius, const Color &color,
185 const Vec3 &normal, const Vec3 &light_direction,
186 Image &output) {
187 int cx = static_cast<int>(std::floor(screen_x));
188 int cy = static_cast<int>(std::floor(screen_y));
189 int r = static_cast<int>(std::ceil(radius));
190 float r_sq = radius * radius;
191
192 for (int dy = -r; dy <= r; ++dy) {
193 int py = cy + dy;
194 if (py < 0 || py >= height) continue;
195 for (int dx = -r; dx <= r; ++dx) {
196 int px = cx + dx;
197 if (px < 0 || px >= width) continue;
198 if (static_cast<float>(dx*dx + dy*dy) > r_sq) continue;
199 shade_and_write(px, py, depth, color, normal, light_direction, output);
200 }
201 }
202}
203
204
205// You'll need to pass your camera's near and far clipping planes (e.g., 0.1f and 100.0f)
206void Rasterizer::apply_ssao(Image &output, float z_near, float z_far) {
207 if (!ssao_enabled || width < 2 || height < 2) return;
208
209 // --- TUNABLE PARAMETERS (Now in linear world-units, e.g., meters) ---
210 // How far a sample must jut out to cast a shadow (fixes ground plane acne)
211 const float depth_bias = 0.05f;
212
213 // Max distance before we assume it's a different object (fixes skybox/cow halo)
214 const float max_occlusion_distance = 1.5f;
215
216 // Multiplier to convert world-radius to screen pixels
217 const float radius_scale = 10.0f;
218
219 // Fixed normalized sample directions (8 samples on a spiral)
220 const int ns = 8;
221 const float dirs[ns][2] = {
222 { 0.309f, 0.951f}, {-0.809f, 0.588f}, { 1.000f, -0.000f}, { 0.809f, -0.588f},
223 {-0.309f, -0.951f}, { 0.588f, 0.809f}, {-0.588f, 0.809f}, {-1.000f, -0.000f},
224 };
225
226 // Precalculate linearization constants outside the loop to save CPU cycles
227 const float depth_C = 2.0f * z_near * z_far;
228 const float depth_D = z_far + z_near;
229 const float depth_E = z_far - z_near;
230
231 // Fast inline lambda for depth linearization
232 auto linearize = [&](float raw_z) {
233 float z_ndc = 2.0f * raw_z - 1.0f;
234 return depth_C / (depth_D - z_ndc * depth_E);
235 };
236
237 for (int y = 0; y < height; ++y) {
238 for (int x = 0; x < width; ++x) {
239 int idx = y * width + x;
240 float center_depth_raw = z_buffer[idx];
241
242 // Skip background pixels
243 if (center_depth_raw >= 1.0f) continue;
244
245 // Convert center depth to actual world units
246 float center_depth = linearize(center_depth_raw);
247
248 Vec3 center_normal = normal_buffer[idx];
249 if (center_normal.x == 0.0f && center_normal.y == 0.0f && center_normal.z == 0.0f) continue;
250
251 center_normal = glm::normalize(center_normal);
252
253 // Scale the sampling radius based on distance (closer = bigger radius)
254 int screen_radius = static_cast<int>((ssao_radius * radius_scale) / center_depth);
255 screen_radius = std::max(1, std::min(screen_radius, 100)); // Clamp to sane bounds
256
257 float occlusion = 0.0f;
258 for (int s = 0; s < ns; ++s) {
259 int sx = x + static_cast<int>(dirs[s][0] * screen_radius);
260 int sy = y + static_cast<int>(dirs[s][1] * screen_radius);
261
262 // Bounds check
263 if (sx < 0 || sx >= width || sy < 0 || sy >= height) {
264 continue;
265 }
266
267 float sample_depth_raw = z_buffer[sy * width + sx];
268 float sample_depth = linearize(sample_depth_raw);
269
270 // Delta: Positive means the sample is CLOSER to the camera than the center
271 float depth_delta = center_depth - sample_depth;
272
273 // 1. Bias Check: Is it actually jutting out, or is it just a tilted flat plane?
274 if (depth_delta > depth_bias) {
275
276 // 2. Range Check: Smooth falloff to prevent halos across large gaps
277 float range_falloff = 1.0f - (depth_delta / max_occlusion_distance);
278
279 // Only add occlusion if it's within the max distance
280 if (range_falloff > 0.0f) {
281 // 3. Hemisphere check: only count occlusion from above the surface
282 float depth_ndc_delta = center_depth_raw - sample_depth_raw;
283 Vec3 offset_dir(
284 dirs[s][0] * screen_radius,
285 -dirs[s][1] * screen_radius,
286 -depth_ndc_delta * (width + height) * 0.1f);
287 offset_dir = glm::normalize(offset_dir);
288 float hem = glm::dot(center_normal, offset_dir);
289 if (hem > 0.0f) {
290 occlusion += range_falloff * hem;
291 }
292 }
293 }
294 }
295
296 // Calculate final AO and apply intensity
297 float ao = 1.0f - (ssao_intensity * (occlusion / static_cast<float>(ns)));
298 ao = std::max(0.0f, std::min(1.0f, ao));
299
300 // Apply to color buffer
301 if (ao < 1.0f) {
302 uint8_t r, g, b, a;
303 output.get_pixel(x, y, r, g, b, a);
304 r = static_cast<uint8_t>(r * ao);
305 g = static_cast<uint8_t>(g * ao);
306 b = static_cast<uint8_t>(b * ao);
307 output.set_pixel(x, y, r, g, b, a);
308 }
309 }
310 }
311}
312} // namespace scimesh
Low-level math utilities for the rendering pipeline.
glm::vec2 Vec2
2-component floating-point vector (xy).
Definition types.h:32
glm::vec3 Vec3
3-component floating-point vector (xyz).
Definition types.h:46
Color shade_pixel(const Color &base_color, const Vec3 &normal, const Vec3 &light_direction, const Color &specular_color=Color(0, 0, 0, 0), float shininess=0.0f)
Compute the shaded color of a pixel with a single directional light.
Definition math_utils.h:191
Color shade_pixel_multi(const Color &base_color, const Vec3 &normal, const std::vector< Light > &lights, float ambient, const Color &specular_color=Color(0, 0, 0, 0), float shininess=0.0f)
Compute the shaded color with multiple light sources.
Definition math_utils.h:231
The Rasterizer — the per-pixel rendering engine.
Vec3 normal2
Definition renderer.cpp:23
Vec3 screen_v2
Definition renderer.cpp:21
Vec3 normal1
Definition renderer.cpp:23
Vec3 normal0
Definition renderer.cpp:23
Vec3 screen_v1
Definition renderer.cpp:21
Vec2 uv1
Definition renderer.cpp:24
Color color0
Definition renderer.cpp:22
Vec2 uv0
Definition renderer.cpp:24
Color color1
Definition renderer.cpp:22
Vec3 screen_v0
Definition renderer.cpp:21
Vec2 uv2
Definition renderer.cpp:24
Color color2
Definition renderer.cpp:22
An RGBA color with floating-point components.
Definition types.h:88
float g
Green channel, [0, 1].
Definition types.h:90
float r
Red channel, [0, 1].
Definition types.h:89
float b
Blue channel, [0, 1].
Definition types.h:91
float a
Alpha (opacity) channel, [0, 1]. 1.0 = fully opaque.
Definition types.h:92
A 2D RGBA image buffer.
Definition image.h:87
void set_pixel(int x, int y, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
Set a single pixel's RGBA value.
Definition image.cpp:39
void get_pixel(int x, int y, uint8_t &r, uint8_t &g, uint8_t &b, uint8_t &a) const
Get a single pixel's RGBA value.
Definition image.cpp:49
Color sample_bilinear(float u, float v) const
Sample the image at texture coordinates (u, v) using bilinear interpolation.
Definition image.cpp:351
float ssao_radius
SSAO sample radius in pixels (default: 16).
Definition rasterizer.h:148
void clear(float clear_depth=1.0f)
Clear the depth and normal buffers.
void rasterize_point(float screen_x, float screen_y, float depth, float radius, const Color &color, const Vec3 &normal, const Vec3 &light_direction, Image &output)
Rasterize a single point (filled circle) into the output image.
float fog_end
Distance where fog is fully opaque.
Definition rasterizer.h:134
Color fog_color
The fog color (what distant objects blend into).
Definition rasterizer.h:137
bool fog_enabled
Enable depth fog (default: false).
Definition rasterizer.h:128
float fog_start
Distance where fog begins.
Definition rasterizer.h:131
Rasterizer(int w, int h)
Construct a rasterizer for the given output size.
void apply_ssao(Image &output, float z_near, float z_far)
Apply screen-space ambient occlusion to the output image.
int height
Output image height in pixels.
Definition rasterizer.h:53
Image * active_texture
Optional texture image for textured meshes.
Definition rasterizer.h:240
float ssao_intensity
SSAO darkening intensity (0.0–1.0, default: 0.8).
Definition rasterizer.h:151
float ambient
Ambient light level (0.0–1.0, default: 0.3).
Definition rasterizer.h:112
int width
Output image width in pixels.
Definition rasterizer.h:50
Color specular_color
Specular highlight color.
Definition rasterizer.h:91
std::vector< Light > lights
Light sources for Blinn-Phong shading.
Definition rasterizer.h:107
float shininess
Shininess exponent (Phong model).
Definition rasterizer.h:96
void rasterize_triangle(const Vec3 &screen_v0, const Color &color0, const Vec3 &normal0, const Vec2 &uv0, const Vec3 &screen_v1, const Color &color1, const Vec3 &normal1, const Vec2 &uv1, const Vec3 &screen_v2, const Color &color2, const Vec3 &normal2, const Vec2 &uv2, bool backface_culling, bool smooth_shading, const Vec3 &light_direction, bool wireframe, const Color &wireframe_color, Image &output)
Rasterize a single triangle into the output image.
bool ssao_enabled
Enable SSAO (default: false).
Definition rasterizer.h:145
std::vector< float > z_buffer
Z-buffer (depth buffer), one float per pixel.
Definition rasterizer.h:64
bool blend_mode
Enable alpha blending for transparent triangles (default: false).
Definition rasterizer.h:81
float contrast
Contrast adjustment (1.0 = no change).
Definition rasterizer.h:120
std::vector< Vec3 > normal_buffer
Normal buffer, one Vec3 per pixel.
Definition rasterizer.h:69