Echo Writes Code

main.cpp

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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
#include "crucible/core.hpp"

#include <chrono>
#include <cmath>
#include <concepts>
#include <cstdint>
#include <random>
#include <thread>
#include <utility>

namespace console_cube
{
	class Transform
	{
	public:
		constexpr Transform() = default;

		constexpr Transform(crucible::vector<float> translation, crucible::quaternion<float> rotation, crucible::vector<float> scale);

		[[nodiscard]] constexpr auto matrix() const -> crucible::matrix<float>;

		[[nodiscard]] constexpr auto inverse_matrix() const -> crucible::matrix<float>;

		constexpr auto set_translation(float tx, float ty, float tz) -> void;

		constexpr auto set_translation(crucible::vector<float> translation) -> void;

		constexpr auto set_rotation(float angle, float axis_x, float axis_y, float axis_z) -> void;

		constexpr auto set_rotation(float angle, crucible::vector<float> axis) -> void;

		constexpr auto set_rotation(crucible::quaternion<float> rotation) -> void;

		constexpr auto set_scale(float sx, float sy, float sz) -> void;

		constexpr auto set_scale(crucible::vector<float> scale) -> void;

		constexpr auto translate_by(float tx, float ty, float tz) -> void;

		constexpr auto translate_by(crucible::vector<float> translation) -> void;

		constexpr auto rotate_by(float angle, float axis_x, float axis_y, float axis_z) -> void;

		constexpr auto rotate_by(float angle, crucible::vector<float> axis) -> void;

		constexpr auto rotate_by(crucible::quaternion<float> rotation) -> void;

		constexpr auto scale_by(float sx, float sy, float sz) -> void;

		constexpr auto scale_by(crucible::vector<float> scale) -> void;

	private:
		crucible::vector<float> m_translation { 0, 0, 0 };

		crucible::quaternion<float> m_rotation { 1, 0, 0, 0 };

		crucible::vector<float> m_scale { 1, 1, 1 };
	};

	constexpr auto Transform::matrix() const -> crucible::matrix<float>
	{
		auto const translation { crucible::make_translation_matrix(m_translation) };
		auto const rotation { crucible::make_rotation_matrix(m_rotation) };
		auto const scale { crucible::make_scale_matrix(m_scale) };
		return translation * rotation * scale;
	}

	constexpr auto Transform::inverse_matrix() const -> crucible::matrix<float>
	{
		auto const translation { crucible::make_translation_matrix(-m_translation) };
		auto const rotation { crucible::make_rotation_matrix(m_rotation.conjugate()) };
		auto const scale { crucible::make_scale_matrix(1.0f / m_scale.x(), 1.0f / m_scale.y(), 1.0f / m_scale.z()) };
		return translation * rotation * scale;
	}

	constexpr auto Transform::set_translation(float const tx, float const ty, float const tz) -> void
	{
		m_translation = crucible::make_vector(tx, ty, tz);
	}

	constexpr auto Transform::set_translation(crucible::vector<float> const translation) -> void
	{
		m_translation = translation;
	}

	constexpr auto Transform::set_rotation(float const angle, float const axis_x, float const axis_y, float const axis_z) -> void
	{
		m_rotation = crucible::make_orientation_quaternion(angle, axis_x, axis_y, axis_z);
	}

	constexpr auto Transform::set_rotation(float const angle, crucible::vector<float> const axis) -> void
	{
		m_rotation = crucible::make_orientation_quaternion(angle, axis);
	}

	constexpr auto Transform::set_rotation(crucible::quaternion<float> const rotation) -> void
	{
		m_rotation = rotation;
	}

	constexpr auto Transform::set_scale(float const sx, float const sy, float const sz) -> void
	{
		m_scale = crucible::make_vector(sx, sy, sz);
	}

	constexpr auto Transform::set_scale(crucible::vector<float> const scale) -> void
	{
		m_scale = scale;
	}

	constexpr auto Transform::translate_by(float const tx, float const ty, float const tz) -> void
	{
		m_translation += crucible::make_vector(tx, ty, tz);
	}

	constexpr auto Transform::translate_by(crucible::vector<float> const translation) -> void
	{
		m_translation += translation;
	}

	constexpr auto Transform::rotate_by(float const angle, float const axis_x, float const axis_y, float const axis_z) -> void
	{
		m_rotation = crucible::make_orientation_quaternion(angle, axis_x, axis_y, axis_z).rotate(m_rotation);
	}

	constexpr auto Transform::rotate_by(float const angle, crucible::vector<float> const axis) -> void
	{
		m_rotation = crucible::make_orientation_quaternion(angle, axis).rotate(m_rotation);
	}

	constexpr auto Transform::rotate_by(crucible::quaternion<float> const rotation) -> void
	{
		m_rotation = rotation.rotate(m_rotation);
	}

	constexpr auto Transform::scale_by(float const sx, float const sy, float const sz) -> void
	{
		m_scale = crucible::make_vector(m_scale.x() * sx, m_scale.y() * sy, m_scale.z() * sz);
	}

	constexpr auto Transform::scale_by(crucible::vector<float> const scale) -> void
	{
		m_scale = crucible::make_vector(m_scale.x() * scale.x(), m_scale.y() * scale.y(), m_scale.z() * scale.z());
	}

	class Entity
	{
	public:
		[[nodiscard]] auto transform() const & -> Transform const &;

		auto set_translation(float tx, float ty, float tz) -> void;

		auto set_translation(crucible::vector<float> translation) -> void;

		auto set_rotation(float angle, float axis_x, float axis_y, float axis_z) -> void;

		auto set_rotation(float angle, crucible::vector<float> axis) -> void;

		auto set_rotation(crucible::quaternion<float> rotation) -> void;

		auto set_scale(float sx, float sy, float sz) -> void;

		auto set_scale(crucible::vector<float> scale) -> void;

		auto translate_by(float tx, float ty, float tz) -> void;

		auto translate_by(crucible::vector<float> translation) -> void;

		auto rotate_by(float angle, float axis_x, float axis_y, float axis_z) -> void;

		auto rotate_by(float angle, crucible::vector<float> axis) -> void;

		auto rotate_by(crucible::quaternion<float> rotation) -> void;

		auto scale_by(float sx, float sy, float sz) -> void;

		auto scale_by(crucible::vector<float> scale) -> void;

	protected:
		// We don't have any pure virtual methods, but you still shouldn't construct a plain Entity
		virtual ~Entity() = default;

	private:
		Transform m_transform;
	};

	auto Entity::transform() const & -> Transform const &
	{
		return m_transform;
	}

	auto Entity::set_translation(float const tx, float const ty, float const tz) -> void
	{
		m_transform.set_translation(tx, ty, tz);
	}

	auto Entity::set_translation(crucible::vector<float> const translation) -> void
	{
		m_transform.set_translation(translation);
	}

	auto Entity::set_rotation(float const angle, float const axis_x, float const axis_y, float const axis_z) -> void
	{
		m_transform.set_rotation(angle, axis_x, axis_y, axis_z);
	}

	auto Entity::set_rotation(float const angle, crucible::vector<float> const axis) -> void
	{
		m_transform.set_rotation(angle, axis);
	}

	auto Entity::set_rotation(crucible::quaternion<float> const rotation) -> void
	{
		m_transform.set_rotation(rotation);
	}

	auto Entity::set_scale(float const sx, float const sy, float const sz) -> void
	{
		m_transform.set_scale(sx, sy, sz);
	}

	auto Entity::set_scale(crucible::vector<float> const scale) -> void
	{
		m_transform.set_scale(scale);
	}

	auto Entity::translate_by(float const tx, float const ty, float const tz) -> void
	{
		m_transform.translate_by(tx, ty, tz);
	}

	auto Entity::translate_by(crucible::vector<float> const translation) -> void
	{
		m_transform.translate_by(translation);
	}

	auto Entity::rotate_by(float const angle, float const axis_x, float const axis_y, float const axis_z) -> void
	{
		m_transform.rotate_by(angle, axis_x, axis_y, axis_z);
	}

	auto Entity::rotate_by(float const angle, crucible::vector<float> const axis) -> void
	{
		m_transform.rotate_by(angle, axis);
	}

	auto Entity::rotate_by(crucible::quaternion<float> const rotation) -> void
	{
		m_transform.rotate_by(rotation);
	}

	auto Entity::scale_by(float const sx, float const sy, float const sz) -> void
	{
		m_transform.scale_by(sx, sy, sz);
	}

	auto Entity::scale_by(crucible::vector<float> const scale) -> void
	{
		m_transform.scale_by(scale);
	}

	class Camera : public Entity
	{
	public:
		[[nodiscard]] auto matrix() const -> crucible::matrix<float>;

		auto orthographic(float width, float height, float near, float far) -> void;

		auto perspective(float width, float height, float near, float far) -> void;

	private:
		crucible::matrix<float> m_projection;
	};

	auto Camera::matrix() const -> crucible::matrix<float>
	{
		return m_projection * transform().inverse_matrix();
	}

	auto Camera::orthographic(float const width, float const height, float const near, float const far) -> void
	{
		m_projection = crucible::make_orthographic_matrix(width, height, near, far);
	}

	auto Camera::perspective(float const width, float const height, float const near, float const far) -> void
	{
		m_projection = crucible::make_perspective_matrix(width, height, near, far);
	}

	class Console_Renderer
	{
	public:
		Console_Renderer(std::size_t width, std::size_t height);

		auto render() const -> void;

		auto clear() -> void;

		auto draw_triangles(crucible::matrix<float> transform, crucible::heap_buffer<crucible::vector<float>> const &vertices, crucible::heap_buffer<std::uint32_t> const &indices) -> void;

		auto draw_line(crucible::vector<float> from, crucible::vector<float> to) -> void;

		auto draw_pixel(std::int32_t x, std::int32_t y, float value) -> void;

	private:
		std::size_t m_width { 0 };

		std::size_t m_height { 0 };

		crucible::heap_buffer<float> m_pixels;
	};

	Console_Renderer::Console_Renderer(std::size_t const width, std::size_t const height) :
		m_width { width },
		m_height { height },
		// TODO: make_heap_buffer_with_size() + supply initial value instead of default
		m_pixels { crucible::make_heap_buffer<float>(width * height) }
	{
		clear();
	}

	auto Console_Renderer::render() const -> void
	{
		auto &console { crucible::console() };

		crucible::iterate(m_pixels)
			.map([](auto const pixel) {
				constexpr crucible::string_view EMPTY_CELL { "\x1b[90m..\x1b[0m" };
				constexpr crucible::string_view FAR_CELL {"\x1b[1;95m░░\x1b[0m" };
				constexpr crucible::string_view MID_CELL {"\x1b[1;95m▒▒\x1b[0m" };
				constexpr crucible::string_view NEAR_CELL { "\x1b[1;95m▓▓\x1b[0m" };
				constexpr crucible::string_view NEAR_CLIPPED_CELL { "\x1b[1;95mnn\x1b[0m" };
				constexpr crucible::string_view FAR_CLIPPED_CELL { "\x1b[35mff\x1b[0m" };

				if (pixel < -1.0f) {
					return NEAR_CLIPPED_CELL;
				} else if (pixel < -0.4f) {
					return NEAR_CELL;
				} else if (pixel < 0.4f) {
					return MID_CELL;
				} else if (pixel <= 1.0f) {
					return FAR_CELL;
				} else if (pixel < std::numeric_limits<float>::max()) {
					return FAR_CLIPPED_CELL;
				} else {
					return EMPTY_CELL;
				}
			})
			.examine([this, &console, next_cell = 0] (auto const cell) mutable {
				console.write_out(cell);

				++next_cell;
				if (next_cell % m_width == 0) {
					console.write_out_line();
				}
			}).consume();

		// TODO: make this a Console API
		console.format_out("\x1b%[{}F", m_height);
	}

	auto Console_Renderer::clear() -> void
	{
		// TODO: constant-init, zero-init, and reset for array-like types
		crucible::iterate(crucible::make_range<std::size_t>(0, m_pixels.size()))
			.examine([&](std::size_t const i) {
				m_pixels[i] = std::numeric_limits<float>::max();
			}).consume();
	}

	auto Console_Renderer::draw_triangles(crucible::matrix<float> const transform, crucible::heap_buffer<crucible::vector<float>> const &vertices, crucible::heap_buffer<std::uint32_t> const &indices) -> void
	{
		CRUCIBLE_ASSERT_EQ(indices.size() % 3, 0);

		auto const normalized_vertices {
			crucible::iterate(vertices)
				.map([&transform](auto const vertex) { return transform * vertex; })
				.map([](auto const vertex) { return vertex / vertex.w(); })
				.materialize<crucible::heap_buffer<crucible::vector<float>>>()
		};

		// TODO: iterate by groups of N
		// TODO: less cumbersome range syntax
		crucible::iterate(crucible::make_range<std::size_t>(0, indices.size(), 3))
			.examine([&](std::size_t const i) {
				auto const p0 { indices[i] };
				auto const p1 { indices[i + 1] };
				auto const p2 { indices[i + 2] };

				draw_line(normalized_vertices[p0], normalized_vertices[p1]);
				draw_line(normalized_vertices[p0], normalized_vertices[p2]);
				draw_line(normalized_vertices[p1], normalized_vertices[p2]);
			}).consume();
	}

	auto Console_Renderer::draw_line(crucible::vector<float> const from, crucible::vector<float> const to) -> void
	{
		auto lerp { crucible::make_lerp(from, to) };

		crucible::iterate(crucible::make_range(0.0f, 1.0f, 0.0001f))
			.examine([&](float const alpha) {
				auto const point { lerp.sample(alpha) };

				auto const pixel_x { static_cast<std::int32_t>(m_width * (point.x() + 1.0f) / 2.0f) };

				// Subtract because we need to mirror the pixel_y axis (world space has min=bottom max=top, but screen space has min=top max=bottom)
				// m_height - 1 because when we flip the axis, rounding towards 0 becomes rounding away from 0, so we're off by 1
				auto const pixel_y { static_cast<std::int32_t>(m_height - 1) - static_cast<std::int32_t>(m_height * (point.y() + 1.0f) / 2.0f) };

				// We do clipping here because we don't have a great way to discard single vertices, so we do it on pixels instead
				if (pixel_x < 0 || pixel_x >= static_cast<std::int32_t>(m_width)) {
					return;
				}

				if (pixel_y < 0 || pixel_y >= static_cast<std::int32_t>(m_height)) {
					return;
				}

				// Use the point's Z coordinate as the pixel value, because it matches the pixel format exactly
				float const pixel_value { point.z() };

				draw_pixel(pixel_x, pixel_y, pixel_value);
			}).consume();
	}

	auto Console_Renderer::draw_pixel(std::int32_t const x, std::int32_t const y, float const value) -> void
	{
		/*
		CRUCIBLE_ASSERT_GE(x, 0);
		CRUCIBLE_ASSERT_LT(x, m_width);
		CRUCIBLE_ASSERT_GE(y, 0);
		CRUCIBLE_ASSERT_LT(y, m_height);
		*/

		// Only draw a new pixel if it's "closer" (lower value) than whatever was here before
		float const previous { m_pixels[y * m_width + x] };
		m_pixels[y * m_width + x] = std::min(value, previous);
	}

	class Mesh : public Entity
	{
	public:
		Mesh() = default;

		Mesh(crucible::heap_buffer<crucible::vector<float>> vertices, crucible::heap_buffer<std::uint32_t> indices);

		auto render_with(Console_Renderer &renderer, Camera const &camera) -> void;

	private:
		crucible::heap_buffer<crucible::vector<float>> m_vertices;

		crucible::heap_buffer<std::uint32_t> m_indices;
	};

	Mesh::Mesh(crucible::heap_buffer<crucible::vector<float>> vertices, crucible::heap_buffer<std::uint32_t> indices) :
		m_vertices { std::move(vertices) },
		m_indices { std::move(indices) }
	{}

	auto Mesh::render_with(Console_Renderer &renderer, Camera const &camera) -> void
	{
		renderer.draw_triangles(camera.matrix() * transform().matrix(), m_vertices, m_indices);
	}

	class Random_Number
	{
	public:
		Random_Number();

		[[nodiscard]] auto sample() -> float;

	private:
		std::mt19937 m_rng;
		std::uniform_real_distribution<float> m_generator { -1.0f, 1.0f };
	};

	Random_Number::Random_Number() :
		m_rng { (std::random_device {})() }
	{}

	auto Random_Number::sample() -> float
	{
		return m_generator(m_rng);
	}

	class Random_Vector
	{
	public:
		Random_Vector();

		[[nodiscard]] auto sample() const -> crucible::vector<float>;

	private:
		std::random_device rng_seed;
		std::mt19937 m_rng { rng_seed() };
		std::uniform_real_distribution<float> m_generator { -1.0f, 1.0f };
	};

	class Random_Versor
	{
	public:
		Random_Versor();

		[[nodiscard]] auto sample() const -> crucible::quaternion<float>;

	private:
		std::random_device rng_seed;
		std::mt19937 m_rng { rng_seed() };
		std::uniform_real_distribution<float> m_angle_generator { 0.0f, std::numbers::pi_v<float> };
		std::uniform_real_distribution<float> m_axis_component_generator { -1.0f, 1.0f };
	};

	// TODO: this could be a view
	auto const CUBE_VERTICES {
		crucible::make_heap_buffer({
			crucible::vector<float> { 0.5f, 0.5f, -0.5f }, // top right near
			crucible::vector<float> { 0.5f, -0.5f, -0.5f }, // bottom right near
			crucible::vector<float> { -0.5f, 0.5f, -0.5f }, // top left near
			crucible::vector<float> { -0.5f, -0.5f, -0.5f }, // bottom left near
			crucible::vector<float> { 0.5f, 0.5f, 0.5f }, // top right far
			crucible::vector<float> { 0.5f, -0.5f, 0.5f }, // bottom right far
			crucible::vector<float> { -0.5f, 0.5f, 0.5f }, // top left far
			crucible::vector<float> { -0.5f, -0.5f, 0.5f } // bottom left far
		})
	};

	// TODO: this could be a view
	auto const CUBE_INDICES {
		crucible::make_heap_buffer<std::uint32_t>({
			0, 1, 2, // front face northeast pointing
			1, 2, 3, // front face southwest facing
			4, 5, 6, // back face northeast facing
			5, 6, 7, // back face southwest facing
			0, 2, 4, // top face southeast facing
			2, 4, 6, // top face northwest facing
			1, 3, 5, // bottom face northeast facing
			3, 5, 7 // bottom face southwest facing
		})
	};

	auto boot(crucible::immutable_view<crucible::string_view> const /* arguments */) -> crucible::exit_status
	{
		// These values are tweakable (within reason)
		constexpr std::uint32_t FRAMES_PER_SECOND { 24 };
		constexpr std::int32_t FRAMEBUFFER_WIDTH { 40 };
		constexpr std::int32_t FRAMEBUFFER_HEIGHT { 30 };

		Console_Renderer renderer { FRAMEBUFFER_WIDTH, FRAMEBUFFER_HEIGHT };

		Camera camera;
		camera.perspective(2.0f, 2.0f, 1.0f, 1000.0f);

		Mesh cube { CUBE_VERTICES, CUBE_INDICES };
		cube.translate_by(0.0f, 0.0f, 2.0f);
		cube.scale_by(2.0f, 1.0f, 1.0f);

		bool running { true };
		int frames = 0;

		auto t0 { std::chrono::steady_clock::now() };
		float dt { 0 };

		std::random_device rng_seed;
		std::mt19937 rng_state { rng_seed() };
		std::uniform_real_distribution<float> angle_rng { 0.0f, std::numbers::pi_v<float> };
		std::uniform_real_distribution<float> axis_rng { -1.0f, 1.0f };

		float const rotation_angle { std::numbers::pi_v<float> };
		auto rotation_axis { crucible::make_unit_vector(axis_rng(rng_state), axis_rng(rng_state), axis_rng(rng_state)) };

		auto rotate_from { crucible::make_identity_quaternion<float>() };
		auto rotate_to { crucible::make_orientation_quaternion(rotation_angle, rotation_axis) };
		auto slerp { crucible::make_slerp(rotate_from, rotate_to) };

		float current_slerp_time { 0 };
		constexpr float total_slerp_time { 5 };

		while (running) {
			constexpr std::chrono::milliseconds sleepytime { 1000 / FRAMES_PER_SECOND };

			// Display the current frame, then sleep for sleepytime
			renderer.render();
			std::this_thread::sleep_for(sleepytime);

			// Reset the framebuffer
			renderer.clear();

			// Update timer, figure out the dt for the current frame
			auto const t1 { std::chrono::steady_clock::now() };
			dt = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count() / 1000.0f;
			t0 = t1;

			// Update the rotating cube animation
			current_slerp_time += dt;

			if (current_slerp_time >= total_slerp_time) {
				current_slerp_time = 0;
				rotation_axis = crucible::make_unit_vector(axis_rng(rng_state), axis_rng(rng_state), axis_rng(rng_state));
				rotate_from = rotate_to;
				rotate_to = rotate_from.rotate(crucible::make_orientation_quaternion(rotation_angle, rotation_axis));
				slerp = crucible::make_slerp(rotate_from, rotate_to);
			}

			float const slerp_factor { current_slerp_time / total_slerp_time };
			auto const current_rotation { slerp.sample(slerp_factor) };
			cube.set_rotation(current_rotation);

			// Render the cube!
			// We want (perspective or orthographic) X view X (translation X rotation X scale)
			// Associativity doesn't matter, it's just easier to visualize that way c:
			cube.render_with(renderer, camera);

			if (frames > 0) {
				--frames;
				running = frames > 0;
			}
		}

		return crucible::exit_status::success;
	}
}

CRUCIBLE_MAIN(console_cube::boot)