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
#include "crucible/core.hpp"

#include <chrono>
#include <cmath>
#include <cstdint>
#include <thread>

namespace console_cube
{
	constexpr auto FRAMEBUFFER_SIZE { crucible::core::make_vector_2d<std::uint32_t>(40, 30) };
	constexpr std::uint32_t FRAMES_PER_SECOND { 24 };
	constexpr crucible::core::String_View EMPTY_CELL { ".." };
	constexpr crucible::core::String_View FULL_CELL { "##" };

	// TODO: make it possible to make these views
	static auto const CUBE_VERTICES { crucible::core::make_heap_buffer({
		crucible::core::make_vector_4d(0.5f, 0.5f, -0.5f, 1.0f), // top right near
		crucible::core::make_vector_4d(0.5f, -0.5f, -0.5f, 1.0f), // bottom right near
		crucible::core::make_vector_4d(-0.5f, 0.5f, -0.5f, 1.0f), // top left near
		crucible::core::make_vector_4d(-0.5f, -0.5f, -0.5f, 1.0f), // bottom left near
		crucible::core::make_vector_4d(0.5f, 0.5f, 0.5f, 1.0f), // top right far
		crucible::core::make_vector_4d(0.5f, -0.5f, 0.5f, 1.0f), // bottom right far
		crucible::core::make_vector_4d(-0.5f, 0.5f, 0.5f, 1.0f), // top left far
		crucible::core::make_vector_4d(-0.5f, -0.5f, 0.5f, 1.0f) // bottom left far
	}) };

	static auto const CUBE_TRIANGLES { crucible::core::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
	}) };

	struct Matrix
	{
		float m00 { 0 };
		float m01 { 0 };
		float m02 { 0 };
		float m03 { 0 };

		float m10 { 0 };
		float m11 { 0 };
		float m12 { 0 };
		float m13 { 0 };

		float m20 { 0 };
		float m21 { 0 };
		float m22 { 0 };
		float m23 { 0 };

		float m30 { 0 };
		float m31 { 0 };
		float m32 { 0 };
		float m33 { 0 };
	};

	auto multiply_matrix_vector(Matrix const m, crucible::core::Vector4D<float> const v) -> crucible::core::Vector4D<float>
	{
		crucible::core::Vector4D<float> result;
		result.set_x(m.m00 * v.x() + m.m01 * v.y() + m.m02 * v.z() + m.m03 * v.w());
		result.set_y(m.m10 * v.x() + m.m11 * v.y() + m.m12 * v.z() + m.m13 * v.w());
		result.set_z(m.m20 * v.x() + m.m21 * v.y() + m.m22 * v.z() + m.m23 * v.w());
		result.set_w(m.m30 * v.x() + m.m31 * v.y() + m.m32 * v.z() + m.m33 * v.w());
		return result;
	}

	auto transform_vertices(Matrix const m, crucible::core::Immutable_View<crucible::core::Vector4D<float>> const vertices) -> crucible::core::Heap_Buffer<crucible::core::Vector4D<float>>
	{
		return crucible::core::iterate(vertices)
			.map([&](crucible::core::Vector4D<float> const vertex) {
				return multiply_matrix_vector(m, vertex);
			})
			.materialize<crucible::core::Heap_Buffer<crucible::core::Vector4D<float>>>();
	}

	// TODO: bring quaternions and matrices into the math library
	struct Quaternion
	{
		float i { 0.0f };
		float j { 0.0f };
		float k { 0.0f };
		float w { 0.0f };
	};

	auto axis_angle_quaternion(crucible::core::Vector3D<float> const axis, float const angle) -> Quaternion
	{
		Quaternion q;
		q.i = axis.x() * std::sin(angle / 2);
		q.j = axis.y() * std::sin(angle / 2);
		q.k = axis.z() * std::sin(angle / 2);
		q.w = std::cos(angle / 2);
		return q;
	}

	auto quaternion_to_rotation_matrix(Quaternion const q) -> Matrix
	{
		Matrix m;

		m.m00 = 2 * (q.i * q.i + q.j * q.j) - 1;
		m.m01 = 2 * (q.j * q.k - q.i * q.w);
		m.m02 = 2 * (q.j * q.w + q.i * q.k);
		m.m03 = 0;

		m.m10 = 2 * (q.j * q.k + q.i * q.w);
		m.m11 = 2 * (q.i * q.i + q.k * q.k) - 1;
		m.m12 = 2 * (q.k * q.w - q.i * q.j);
		m.m13 = 0;

		m.m20 = 2 * (q.j * q.w - q.i * q.k);
		m.m21 = 2 * (q.k * q.w + q.i * q.j);
		m.m22 = 2 * (q.i * q.i + q.w * q.w) - 1;
		m.m23 = 0;

		m.m30 = 0;
		m.m31 = 0;
		m.m32 = 0;
		m.m33 = 1;

		return m;
	}

	auto present(crucible::core::Immutable_View<float> const framebuffer, std::size_t const frame) -> void
	{
		auto &console { crucible::core::console() };
		std::size_t next_cell { 0 };

		console
			.format_out_line("Frame {}, FPS target = {}", frame, FRAMES_PER_SECOND)
			.write_out_line();

		crucible::core::iterate(framebuffer)
			.map([](auto const pixel) {
				if (pixel < 0.5f) {
					return EMPTY_CELL;
				} else {
					return FULL_CELL;
				}
			})
			.examine([&](auto const cell) {
				console.write_out(cell);

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

		// Move the cursor back to the top for the next frame. +2 for the status line and blank line.
		// TODO: make this a Console API
		console.format_out("\x1b%[{}F", FRAMEBUFFER_SIZE.y() + 2);
	}

	auto clear(crucible::core::Mutable_View<float> framebuffer) -> void
	{
		// TODO: constant-init and zero-init for array-like types
		crucible::core::iterate(crucible::core::make_range<std::uint32_t>(0, FRAMEBUFFER_SIZE.x() * FRAMEBUFFER_SIZE.y()))
			.examine([&](auto const i) {
				framebuffer[i] = 0.0f;
			}).consume();
	}

	auto model_to_screen(crucible::core::Immutable_View<crucible::core::Vector4D<float>> const model_vertices) -> crucible::core::Heap_Buffer<crucible::core::Vector2D<float>>
	{
		// TODO: vector componentwise division
		constexpr crucible::core::Vector2D<std::uint32_t> FRAMEBUFFER_HALFSIZE { FRAMEBUFFER_SIZE.x() / 2, FRAMEBUFFER_SIZE.y() / 2 };

		return crucible::core::iterate(model_vertices)
			.map([&](crucible::core::Vector4D<float> const v) {
				float const x { (v.x() + 1.0f) * FRAMEBUFFER_HALFSIZE.x() };
				// size.y() - transformed vert because we need to mirror the y axis (model has its min=bottom max=top, but screen has min=top max=bottom)
				float const y { FRAMEBUFFER_SIZE.y() - (v.y() + 1.0f) * FRAMEBUFFER_HALFSIZE.y() };
				return crucible::core::make_vector_2d(x, y);
			}).materialize<crucible::core::Heap_Buffer<crucible::core::Vector2D<float>>>();
	}

	auto draw_line(crucible::core::Mutable_View<float> framebuffer, crucible::core::Immutable_View<crucible::core::Vector2D<float>> const screen_vertices, std::uint32_t from, std::uint32_t to) -> void
	{
		crucible::core::iterate(crucible::core::make_range(0.0f, 1.0f, 0.01f))
			.examine([&](float const t) {
				auto const x { static_cast<std::size_t>(screen_vertices[from].x() * (1 - t) + screen_vertices[to].x() * t) };
				auto const y { static_cast<std::size_t>(screen_vertices[from].y() * (1 - t) + screen_vertices[to].y() * t) };

				// Clip anything outside the framebuffer
				if (x < 0 || x >= FRAMEBUFFER_SIZE.x()) {
					return;
				}

				if (y < 0 || y >= FRAMEBUFFER_SIZE.y()) {
					return;
				}

				framebuffer[y * FRAMEBUFFER_SIZE.x() + x] = 1.0f; // Note: this is where the pixel is written to the framebuffer
			}).consume();
	}

	auto rasterize(crucible::core::Mutable_View<float> framebuffer, crucible::core::Immutable_View<crucible::core::Vector2D<float>> const screen_vertices, crucible::core::Immutable_View<std::uint32_t> const triangles) -> void
	{
		// TODO: iterate by groups of N
		// TODO: less cumbersome range syntax
		crucible::core::iterate(crucible::core::make_range<std::size_t>(0, triangles.size(), 3))
			.examine([&](std::size_t const i) {
				auto const p1 { triangles[i] };
				auto const p2 { triangles[i + 1] };
				auto const p3 { triangles[i + 2] };

				draw_line(framebuffer, screen_vertices, p1, p2);
				draw_line(framebuffer, screen_vertices, p1, p3);
				draw_line(framebuffer, screen_vertices, p2, p3);
			}).consume();
	}

	auto boot(crucible::core::Immutable_View<crucible::core::String_View> const /* arguments */) -> crucible::core::Exit_Status
	{
		constexpr std::chrono::milliseconds sleepytime { 1000 / FRAMES_PER_SECOND };

		auto framebuffer { crucible::core::make_heap_buffer<float>(FRAMEBUFFER_SIZE.x() * FRAMEBUFFER_SIZE.y()) };
		bool running { true };
		int frames = 0;
		std::size_t current_frame { 0 };

		clear(framebuffer.view());

		auto const rotation_axis { crucible::core::normalize_vector(crucible::core::make_vector_3d(1.0f, 1.0f, 0.0f)) };
		auto t0 { std::chrono::steady_clock::now() };
		float t { 0 };

		while (running) {
			present(framebuffer, current_frame);
			std::this_thread::sleep_for(sleepytime);

			auto const t1 { std::chrono::steady_clock::now() };
			t += std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count() / 1000.0f;
			t0 = t1;

			clear(framebuffer);
			auto const model_vertices { CUBE_VERTICES };
			auto const quaternion { axis_angle_quaternion(rotation_axis, t) };
			auto const rotation_matrix { quaternion_to_rotation_matrix(quaternion) };
			auto const view_vertices { transform_vertices(rotation_matrix, model_vertices) };
			auto const screen_vertices { model_to_screen(view_vertices) };
			rasterize(framebuffer, screen_vertices, CUBE_TRIANGLES);

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

			++current_frame;
		}

		return crucible::core::Exit_Status::Success;
	}
}

CRUCIBLE_CORE_MAIN(console_cube::boot)