1use crate::{Camera, ClipDepth, Color, MeshCache, RenderItem, Vec3, Vec4, vec2};
2
3pub const MAX_DIMENSION: u32 = 8192;
5pub const MAX_PIXELS: usize = 16_777_216;
6const CLIP_VERTEX_CAPACITY: usize = 12;
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum RenderError {
9 InvalidDimensions,
10 InvalidCamera,
11 InvalidSettings,
12}
13impl std::fmt::Display for RenderError {
14 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15 write!(f, "software renderer: {self:?}")
16 }
17}
18impl std::error::Error for RenderError {}
19
20pub struct Framebuffer {
21 pub(crate) width: u32,
22 pub(crate) height: u32,
23 pixels: Vec<[u8; 4]>,
24 depth: Vec<f32>,
25}
26impl Framebuffer {
27 pub fn new(width: u32, height: u32) -> Result<Self, RenderError> {
28 let count = (width as usize)
29 .checked_mul(height as usize)
30 .ok_or(RenderError::InvalidDimensions)?;
31 if width == 0
32 || height == 0
33 || width > MAX_DIMENSION
34 || height > MAX_DIMENSION
35 || count > MAX_PIXELS
36 {
37 return Err(RenderError::InvalidDimensions);
38 }
39 Ok(Self {
40 width,
41 height,
42 pixels: vec![[0, 0, 0, 255]; count],
43 depth: vec![f32::INFINITY; count],
44 })
45 }
46 pub const fn width(&self) -> u32 {
47 self.width
48 }
49 pub const fn height(&self) -> u32 {
50 self.height
51 }
52 pub fn pixels_mut(&mut self) -> &mut [[u8; 4]] {
54 &mut self.pixels
55 }
56 pub fn pixels(&self) -> &[[u8; 4]] {
57 &self.pixels
58 }
59 pub fn depths(&self) -> &[f32] {
60 &self.depth
61 }
62 pub fn pixel(&self, x: u32, y: u32) -> Option<[u8; 4]> {
63 if x >= self.width || y >= self.height {
64 None
65 } else {
66 Some(self.pixels[(y * self.width + x) as usize])
67 }
68 }
69 pub fn clear(&mut self, color: Color) {
70 self.pixels.fill(color.to_rgba());
71 self.depth.fill(f32::INFINITY);
72 }
73}
74#[derive(Clone, Copy, Debug)]
75pub struct Fog {
76 pub color: Color,
77 pub start: f32,
78 pub end: f32,
79}
80#[derive(Clone, Copy, Debug)]
81pub struct RenderSettings {
82 pub clear_color: Color,
83 pub light_direction: Vec3,
85 pub ambient: f32,
86 pub diffuse: f32,
87 pub fog: Option<Fog>,
88 pub cull_backfaces: bool,
89}
90impl Default for RenderSettings {
91 fn default() -> Self {
92 Self {
93 clear_color: Color::from_hex(0x293945),
94 light_direction: Vec3::new(-0.4, 0.8, 0.3),
95 ambient: 0.35,
96 diffuse: 0.65,
97 fog: None,
98 cull_backfaces: true,
99 }
100 }
101}
102impl RenderSettings {
103 fn valid(self) -> bool {
104 self.clear_color.to_vec().is_finite()
105 && self.light_direction.is_finite()
106 && self.ambient.is_finite()
107 && self.diffuse.is_finite()
108 && self.ambient >= 0.0
109 && self.diffuse >= 0.0
110 && self.fog.is_none_or(|f| {
111 f.color.to_vec().is_finite()
112 && f.start.is_finite()
113 && f.end.is_finite()
114 && f.start >= 0.0
115 && f.end > f.start
116 })
117 }
118}
119#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
120pub struct RenderStats {
121 pub submitted_instances: u64,
122 pub visible_instances: u64,
123 pub culled_instances: u64,
124 pub invalid_instances: u64,
125 pub submitted_triangles: u64,
126 pub clipped_triangles: u64,
127 pub rasterized_triangles: u64,
128 pub shaded_pixels: u64,
129}
130#[derive(Clone, Copy, Default)]
131struct ClipVertex {
132 clip: Vec4,
133 world: Vec3,
134 color: Vec4,
135}
136impl ClipVertex {
137 fn lerp(self, other: Self, t: f32) -> Self {
138 Self {
139 clip: self.clip.lerp(other.clip, t),
140 world: self.world.lerp(other.world, t),
141 color: self.color.lerp(other.color, t),
142 }
143 }
144}
145
146pub struct SoftwareRenderer {
147 pub framebuffer: Framebuffer,
148 pub settings: RenderSettings,
149 scratch: Vec<ClipVertex>,
150}
151impl SoftwareRenderer {
152 pub fn new(width: u32, height: u32) -> Result<Self, RenderError> {
153 Ok(Self {
154 framebuffer: Framebuffer::new(width, height)?,
155 settings: RenderSettings::default(),
156 scratch: Vec::new(),
157 })
158 }
159 pub fn render(
162 &mut self,
163 cache: &MeshCache,
164 items: &[RenderItem],
165 camera: Camera,
166 ) -> Result<RenderStats, RenderError> {
167 if !self.settings.valid() {
168 return Err(RenderError::InvalidSettings);
169 }
170 if !camera.position.is_finite() {
171 return Err(RenderError::InvalidCamera);
172 }
173 let frustum = camera.frustum().ok_or(RenderError::InvalidCamera)?;
174 self.framebuffer.clear(self.settings.clear_color);
175 let mut stats = RenderStats::default();
176 let light = self.settings.light_direction.normalize_or_zero();
177 for item in items {
178 stats.submitted_instances += 1;
179 let Some(mesh) = cache.get(item.mesh) else {
180 stats.invalid_instances += 1;
181 continue;
182 };
183 if !item.transform.is_finite() || !item.transform.row(3).abs_diff_eq(Vec4::W, 1.0e-5) {
184 stats.invalid_instances += 1;
185 continue;
186 }
187 let Some(inverse) = item.transform.try_inverse() else {
188 stats.invalid_instances += 1;
189 continue;
190 };
191 let bounds = cache.bounds(item.mesh).expect("same validated mesh handle");
192 if !frustum.intersects_bounds(bounds, item.transform) {
193 stats.culled_instances += 1;
194 continue;
195 }
196 stats.visible_instances += 1;
197 let normal_matrix = inverse.transpose();
198 self.scratch.clear();
199 self.scratch.reserve(mesh.vertices.len());
200 for vertex in &mesh.vertices {
201 let world = item.transform.transform_point3(vertex.position);
202 let normal = normal_matrix
203 .transform_vector3(vertex.normal)
204 .normalize_or_zero();
205 let lighting =
206 self.settings.ambient + self.settings.diffuse * normal.dot(light).max(0.0);
207 let color = Vec4::new(
208 vertex.color.r * lighting,
209 vertex.color.g * lighting,
210 vertex.color.b * lighting,
211 vertex.color.a,
212 );
213 self.scratch.push(ClipVertex {
214 clip: camera.view_projection * world.extend(1.0),
215 world,
216 color,
217 });
218 }
219 let mirrored = item.transform.determinant() < 0.0;
220 for indices in mesh.indices.as_chunks::<3>().0 {
221 stats.submitted_triangles += 1;
222 let triangle = [
223 self.scratch[indices[0] as usize],
224 self.scratch[indices[1] as usize],
225 self.scratch[indices[2] as usize],
226 ];
227 let (mut polygon, count) = clip_triangle(triangle, camera.clip_depth);
228 if count < 3 {
229 continue;
230 }
231 if mirrored {
232 polygon[..count].reverse();
233 }
234 stats.clipped_triangles += (count - 2) as u64;
235 for i in 1..count - 1 {
236 raster_triangle(
237 &mut self.framebuffer,
238 [polygon[0], polygon[i], polygon[i + 1]],
239 self.settings,
240 camera,
241 &mut stats,
242 );
243 }
244 }
245 }
246 Ok(stats)
247 }
248}
249fn clip_distance(vertex: ClipVertex, plane: usize, depth: ClipDepth) -> f32 {
250 let p = vertex.clip;
251 match plane {
252 0 => p.w + p.x,
253 1 => p.w - p.x,
254 2 => p.w + p.y,
255 3 => p.w - p.y,
256 4 => {
257 if depth == ClipDepth::ZeroToOne {
258 p.z
259 } else {
260 p.w + p.z
261 }
262 }
263 5 => p.w - p.z,
264 _ => p.w - 1.0e-6,
265 }
266}
267fn clip_triangle(
269 triangle: [ClipVertex; 3],
270 depth: ClipDepth,
271) -> ([ClipVertex; CLIP_VERTEX_CAPACITY], usize) {
272 let mut a = [ClipVertex::default(); CLIP_VERTEX_CAPACITY];
273 let mut b = a;
274 a[..3].copy_from_slice(&triangle);
275 let mut count = 3;
276 if triangle
277 .iter()
278 .any(|v| !v.clip.is_finite() || !v.world.is_finite() || !v.color.is_finite())
279 {
280 return (a, 0);
281 }
282 for plane in 0..7 {
283 let mut next = 0;
284 for i in 0..count {
285 let current = a[i];
286 let previous = a[(i + count - 1) % count];
287 let dc = clip_distance(current, plane, depth);
288 let dp = clip_distance(previous, plane, depth);
289 if (dc >= 0.0) != (dp >= 0.0) {
290 let ratio = (f64::from(dp) / (f64::from(dp) - f64::from(dc))) as f32;
291 if next >= CLIP_VERTEX_CAPACITY {
292 return (a, 0);
293 }
294 b[next] = previous.lerp(current, ratio.clamp(0.0, 1.0));
295 next += 1;
296 }
297 if dc >= 0.0 {
298 if next >= CLIP_VERTEX_CAPACITY {
299 return (a, 0);
300 }
301 b[next] = current;
302 next += 1;
303 }
304 }
305 if next < 3 {
306 return (a, 0);
307 }
308 std::mem::swap(&mut a, &mut b);
309 count = next;
310 }
311 (a, count)
312}
313fn raster_triangle(
314 frame: &mut Framebuffer,
315 triangle: [ClipVertex; 3],
316 settings: RenderSettings,
317 camera: Camera,
318 stats: &mut RenderStats,
319) {
320 let inverse_w = triangle.map(|v| v.clip.w.recip());
321 let ndc = std::array::from_fn::<_, 3, _>(|i| triangle[i].clip.truncate() * inverse_w[i]);
322 let screen = ndc.map(|p| {
323 vec2(
324 (p.x * 0.5 + 0.5) * frame.width as f32,
325 (0.5 - p.y * 0.5) * frame.height as f32,
326 )
327 });
328 let area = (screen[1] - screen[0]).perp_dot(screen[2] - screen[0]);
329 if !area.is_finite() || area.abs() < 1.0e-6 || (settings.cull_backfaces && area >= 0.0) {
330 return;
331 }
332 let min = screen[0].min(screen[1]).min(screen[2]).floor();
333 let max = screen[0].max(screen[1]).max(screen[2]).ceil();
334 let min_x = (min.x as i64).clamp(0, i64::from(frame.width) - 1) as u32;
335 let max_x = (max.x as i64).clamp(0, i64::from(frame.width) - 1) as u32;
336 let min_y = (min.y as i64).clamp(0, i64::from(frame.height) - 1) as u32;
337 let max_y = (max.y as i64).clamp(0, i64::from(frame.height) - 1) as u32;
338 stats.rasterized_triangles += 1;
339 for y in min_y..=max_y {
340 for x in min_x..=max_x {
341 let sample = vec2(x as f32 + 0.5, y as f32 + 0.5);
342 let bary = [
343 (screen[2] - screen[1]).perp_dot(sample - screen[1]) / area,
344 (screen[0] - screen[2]).perp_dot(sample - screen[2]) / area,
345 (screen[1] - screen[0]).perp_dot(sample - screen[0]) / area,
346 ];
347 if bary.iter().any(|&w| w < 0.0) {
348 continue;
349 }
350 let depth = bary[0] * ndc[0].z + bary[1] * ndc[1].z + bary[2] * ndc[2].z;
351 let depth = if camera.clip_depth == ClipDepth::NegativeOneToOne {
352 depth * 0.5 + 0.5
353 } else {
354 depth
355 };
356 let index = (y * frame.width + x) as usize;
357 if !depth.is_finite()
358 || !(0.0..=1.00001).contains(&depth)
359 || depth >= frame.depth[index]
360 {
361 continue;
362 }
363 let weights = std::array::from_fn::<_, 3, _>(|i| bary[i] * inverse_w[i]);
364 let sum = weights.iter().sum::<f32>();
365 if !sum.is_finite() || sum <= 0.0 {
366 continue;
367 }
368 let mut color = Vec4::ZERO;
369 let mut position = Vec3::ZERO;
370 for i in 0..3 {
371 let weight = weights[i] / sum;
372 color += triangle[i].color * weight;
373 position += triangle[i].world * weight;
374 }
375 if color.w <= 0.0 {
376 continue;
377 }
378 if let Some(fog) = settings.fog {
379 let amount = ((position.distance(camera.position) - fog.start)
380 / (fog.end - fog.start))
381 .clamp(0.0, 1.0);
382 color = color.lerp(fog.color.to_vec(), amount);
383 }
384 color.w = 1.0;
385 frame.pixels[index] = Color::from(color).to_rgba();
386 frame.depth[index] = depth.clamp(0.0, 1.0);
387 stats.shaded_pixels += 1;
388 }
389 }
390}