From 58d457d332cd8ce1639a720bc791413712efe6c1 Mon Sep 17 00:00:00 2001 From: tovjemam Date: Fri, 20 Mar 2026 22:24:10 +0100 Subject: [PATCH] Add various versions, readme and screenshot --- README.md | 15 + build_dos.bat | 7 + convert_cc.py | 51 ++++ reduce_cc.py | 20 ++ render.cpp | 400 +++++++++++++++++++++++++++ render.lua | 529 ++++++++++++++++++++++++++++++++++++ render_cc.lua | 616 ++++++++++++++++++++++++++++++++++++++++++ render_origo.lua | 522 +++++++++++++++++++++++++++++++++++ res/screen-dosbox.png | Bin 0 -> 30730 bytes 9 files changed, 2160 insertions(+) create mode 100644 README.md create mode 100644 build_dos.bat create mode 100644 convert_cc.py create mode 100644 reduce_cc.py create mode 100644 render.cpp create mode 100644 render.lua create mode 100644 render_cc.lua create mode 100644 render_origo.lua create mode 100644 res/screen-dosbox.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..3bcbe58 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# render +box, tree and teapot dumb software renderer + +![image](res/screen-dosbox.png) + +| name | lang | notes | +|---------------------|------------------------|---------------------------------------| +| `render.fcode` | Flowgorithm pseudocode | original, VT100/PPM output | +| `render.lua` | Lua | Lua port, VT100 output | +| `render_origo.lua` | Lua | unknown | +| `render_cc.lua` | Lua | ComputerCraft version, monitor output, requires processing with `convert_cc.py` and `reduce_cc.py` if too big to store on CC drive | +| `render.cpp` | C++ | C++ implementation, different structure, unknown state | +| `render_dos.c` | C | DOS, requires 32bit extender, output to VGA mode 13h (256 colors), much faster triangle rasterization alg. | + + diff --git a/build_dos.bat b/build_dos.bat new file mode 100644 index 0000000..f574b47 --- /dev/null +++ b/build_dos.bat @@ -0,0 +1,7 @@ +set WATCOM=c:\dev\render\wc +set EDPATH=%WATCOM%\eddat +set INCLUDE=%WATCOM%\h +set PATH=%PATH%;%WATCOM%\binnt;%WATCOM%\binw; + +wcl386 -3 -fpi87 -fp3 -os -d0 -mf -bt=dos -l=stub32x -fe=render.exe render_dos.c +pause diff --git a/convert_cc.py b/convert_cc.py new file mode 100644 index 0000000..87a2b3f --- /dev/null +++ b/convert_cc.py @@ -0,0 +1,51 @@ +def convert(input_filename): + with open(input_filename, "r") as f: + lines = [line.strip() for line in f if line.strip()] + + # --- Step 1: Parse header --- + idx = 0 + _ignored = int(lines[idx]) # First line, ignored + idx += 1 + + num_vertices = int(lines[idx]) + idx += 1 + + # --- Step 2: Extract vertex data --- + vertices = [] + for _ in range(num_vertices): + # Each vertex: x y z u v nx ny nz (8 numbers) + vertex = [lines[idx + i] for i in range(8)] + vertices.append(vertex) + idx += 8 + + # --- Step 3: Write mesh.txt --- + with open("mesh.txt", "w") as mesh_file: + mesh_file.write(str(num_vertices) + "\n") + for v in vertices: + for comp in v: + mesh_file.write(comp + "\n") + + # --- Step 4: Parse texture --- + width = int(lines[idx]); idx += 1 + height = int(lines[idx]); idx += 1 + + num_pixels = width * height + pixels = [] + for _ in range(num_pixels): + r = int(lines[idx]); idx += 1 + g = int(lines[idx]); idx += 1 + b = int(lines[idx]); idx += 1 + pixels.append((r, g, b)) + + # --- Step 5: Write texture.bin --- + with open("texture.bin", "wb") as tex_file: + for r, g, b in pixels: + tex_file.write(bytes([r, g, b])) + + +if __name__ == "__main__": + import sys + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ") + else: + convert(sys.argv[1]) diff --git a/reduce_cc.py b/reduce_cc.py new file mode 100644 index 0000000..70e7bce --- /dev/null +++ b/reduce_cc.py @@ -0,0 +1,20 @@ +def reduce_precision(input_file, output_file): + with open(input_file, "r") as infile, open(output_file, "w") as outfile: + for line in infile: + line = line.strip() + if line: # skip empty lines + try: + num = float(line) + if num.is_integer(): + outfile.write(f"{int(num)}\n") + else: + outfile.write(f"{num:.3f}\n") + except ValueError: + # If not a number, write unchanged + outfile.write(line + "\n") + +if __name__ == "__main__": + input_path = "data.txt" # original file + output_path = "data2.txt" # new file + reduce_precision(input_path, output_path) + \ No newline at end of file diff --git a/render.cpp b/render.cpp new file mode 100644 index 0000000..1895c1c --- /dev/null +++ b/render.cpp @@ -0,0 +1,400 @@ +// render.cpp : This file contains the 'main' function. Program execution begins and ends there. +// +#pragma comment(lib,"winmm.lib") + +#include +#include +#include +#include +#include + +//#include + +#include +#include + +#define RES_WIDTH 160 +#define RES_HEIGHT 120 +#define RES_PIXELS (RES_WIDTH * RES_HEIGHT) + +static glm::vec3 s_colorbuffer[RES_PIXELS]; +static float s_depthbuffer[RES_PIXELS]; +static char s_outputbuffer[5000000]; + +static float s_fps = 0.0f; + +struct Vertex { + glm::vec3 pos; + glm::vec2 texcoord; + glm::vec3 normal; +}; + +struct Texture { + std::vector> texels; + size_t width; + size_t height; + + glm::vec3 Sample(const glm::vec2& texcoord) const { + auto idx = (int)(texcoord.x * width) + (int)((1.0f - texcoord.y) * height) * width; + const auto& t = texels[idx]; + return glm::vec3((float)t.r / 255.0f, (float)t.g / 255.0f, (float)t.b / 255.0f); + } +}; + +static void ClearBuffers(float depth, const glm::vec3& clear_color) { + for (int i = 0; i < RES_PIXELS; ++i) { + s_colorbuffer[i] = clear_color; + s_depthbuffer[i] = depth; + } +} + +using Vec3ui8 = glm::vec<3, uint8_t>; + +static void OutputVT100() { + //printf("\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m"); + //printf("\x1b[1;1H"); + //printf("\x1b[2J"); + //std::cout << (char)27 << "[1;1H"; + + //std::string output; + //output.reserve(1000000); + + int offset = 0; + + //std::vector oss; + //oss.resize(RES_HEIGHT / 2); + + //output += "\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m\x1b[1;1H"; + + Vec3ui8 prevc1(0), prevc2(0); + +//#pragma omp parallel for + for (int y = 0; y <= RES_HEIGHT - 2; y += 2) { + //printf("\n"); + //output += '\n'; + + offset += snprintf(s_outputbuffer + offset, sizeof(s_outputbuffer), "\n"); + + for (int x = 0; x < RES_WIDTH; ++x) { + const auto& c1 = s_colorbuffer[x + y * RES_WIDTH]; + const auto& c2 = s_colorbuffer[x + (y + 1) * RES_WIDTH]; + + Vec3ui8 c1i = c1 * 255.0f; + Vec3ui8 c2i = c2 * 255.0f; + + if (prevc1 == c1i && prevc2 == c2i) { + //offset += snprintf(s_outputbuffer + offset, sizeof(s_outputbuffer), "\xDF"); + s_outputbuffer[offset++] = '\xDF'; + //s_outputbuffer[offset++] = 'W'; + s_outputbuffer[offset] = '\0'; + } + else { + offset += snprintf(s_outputbuffer + offset, sizeof(s_outputbuffer), "\x1b[38;2;%d;%d;%dm\x1b[48;2;%d;%d;%dm\xDF", c1i.r, c1i.g, c1i.b, c2i.r, c2i.g, c2i.b); + //offset += snprintf(s_outputbuffer + offset, sizeof(s_outputbuffer), "\x1b[38;2;%d;%d;%dmW", c1i.r, c1i.g, c1i.b, c2i.r, c2i.g, c2i.b); + } + + prevc1 = c1i; + prevc2 = c2i; + + + //printf("\x1b[38;2;%d;%d;%dm\x1b[48;2;%d;%d;%dm\xDF", c1i.r, c1i.g, c1i.b, c2i.r, c2i.g, c2i.b); + + //std::ostringstream os; + //auto& os = oss[y / 2]; + + //os << "\x1b[38;2;" << (int)c1i.r << ";" << (int)c1i.g << ";" << (int)c1i.b << "m" << "\x1b[48;2;" << (int)c2i.r << ";" << (int)c2i.g << ";" << (int)c2i.b << "m" << "\xDF"; + //output += os.str(); + } + + } + + printf("\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m"); + printf("\x1b[1;1H"); + + printf("%s\n", s_outputbuffer); + + //for (int i = 0; i < oss.size(); ++i) + // printf("%s\n", oss[i].str().c_str()); + + //output += "\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m"; + //printf("%s\n", output.c_str()); + + printf("\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m\n"); + + printf("%6d %4.1f FPS ", offset, s_fps); +} +// +//// Find the closest RGBx approximation of a 24-bit RGB color, for x = 0 or 1 +//glm::u8vec3 rgbx_approx(unsigned char red, unsigned char green, unsigned char blue, int x) { +// int threshold = (x + 1) * 255 / 3; +// glm::u8vec3 result; +// result.r = (red > threshold) ? 1 : 0; +// result.g = (green > threshold) ? 1 : 0; +// result.b = (blue > threshold) ? 1 : 0; +// return result; +//} +// +//// Convert a 4-bit RGBI color back to 24-bit RGB +//glm::u8vec3 rgbi_to_rgb24(int r, int g, int b, int i) { +// glm::u8vec3 result; +// result.r = ((2 * r + i) * 255) / 3; +// result.g = ((2 * g + i) * 255) / 3; +// result.b = ((2 * b + i) * 255) / 3; +// return result; +//} +// +//float color_distance(const glm::u8vec3& colorA, const glm::u8vec3& colorB) { +// int diffR = colorA.r - colorB.r; +// int diffG = colorA.g - colorB.g; +// int diffB = colorA.b - colorB.b; +// return static_cast((diffR * diffR) + (diffG * diffG) + (diffB * diffB)); +//} +// +//// Find the closest 4-bit RGBI approximation (by Euclidean distance) to a 24-bit RGB color +//std::pair rgbi_approx(unsigned char red, unsigned char green, unsigned char blue) { +// // Find best RGB0 and RGB1 approximations +// glm::u8vec3 rgb0 = rgbx_approx(red, green, blue, 0); +// glm::u8vec3 rgb1 = rgbx_approx(red, green, blue, 1); +// +// // Convert them back to 24-bit RGB +// glm::u8vec3 rgb24_0 = rgbi_to_rgb24(rgb0.r, rgb0.g, rgb0.b, 0); +// glm::u8vec3 rgb24_1 = rgbi_to_rgb24(rgb1.r, rgb1.g, rgb1.b, 1); +// +// // Calculate squared Euclidean distances +// float d0 = color_distance(glm::u8vec3(red, green, blue), rgb24_0); +// float d1 = color_distance(glm::u8vec3(red, green, blue), rgb24_1); +// +// if (d0 <= d1) { +// return std::make_pair(rgb0, 0); +// } +// else { +// return std::make_pair(rgb1, 1); +// } +//} +// +// +//void OutputWindows() { +// HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); +// +// COORD pos; +// pos.X = 0; +// pos.Y = 0; +// SetConsoleCursorPosition(hConsole, pos); +// +// const char palette[] = ".,-~:;=!*#$@"; +// int chars = sizeof(palette) - 1; +// +// for (int y = 0; y < RES_HEIGHT; ++y) { +// for (int x = 0; x < RES_WIDTH; ++x) { +// const auto& c1 = s_colorbuffer[x + y * RES_WIDTH]; +// Vec3ui8 c1i = c1 * 255.0f; +// +// auto [c, i] = rgbi_approx(c1i.r, c1i.g, c1i.b); +// +// DWORD attr = 0; +// if (c.r) attr |= FOREGROUND_RED; +// if (c.g) attr |= FOREGROUND_GREEN; +// if (c.b) attr |= FOREGROUND_BLUE; +// if (i) attr |= FOREGROUND_INTENSITY; +// SetConsoleTextAttribute(hConsole, attr); +// printf("X"); +// +// //float brightness = (c1.r + c1.g + c1.b) / 3.0f; +// //float b2 = glm::max(c1.r, glm::max(c1.g, c1.b)); +// +// //DWORD attr = 0; +// //if (c1.r / b2 > 0.5) attr |= FOREGROUND_RED; +// //if (c1.g / b2 > 0.5) attr |= FOREGROUND_GREEN; +// //if (c1.b / b2 > 0.5) attr |= FOREGROUND_BLUE; +// +// //if (b2 > 0.5) attr |= FOREGROUND_INTENSITY; +// +// //printf("%c", palette[(int)(chars * b2)]); +// +// +// } +// printf("\n"); +// } +// +// +//} + +static bool ProcessVertex(const glm::vec3& pos, const glm::mat4& mvp, glm::vec2& sc, float& z) { + auto cs = mvp * glm::vec4(pos.x, pos.y, pos.z, 1.0f); + + //if (cs.w < 0.0f) return true; + cs /= cs.w; + + sc.x = (cs.x + 1.0f) * 0.5f * (RES_WIDTH - 1); + sc.y = (1.0f - cs.y) * 0.5f * (RES_HEIGHT - 1); + + z = cs.z; + + return false; +} + +static bool IsRight(glm::vec2 a, glm::vec2 b, glm::vec2 c) { + return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) <= 0; +} + +glm::vec3 Barycentric(glm::vec2 p, glm::vec2 a, glm::vec2 b, glm::vec2 c) { + glm::vec2 v0 = b - a, v1 = c - a, v2 = p - a; + float d00 = glm::dot(v0, v0); + float d01 = glm::dot(v0, v1); + float d11 = glm::dot(v1, v1); + float d20 = glm::dot(v2, v0); + float d21 = glm::dot(v2, v1); + float denom = d00 * d11 - d01 * d01; + float v = (d11 * d20 - d01 * d21) / denom; + float w = (d00 * d21 - d01 * d20) / denom; + float u = 1.0f - v - w; + return glm::vec3(u, v, w); +} + +static void DrawTriangle(const Vertex& v0, const Vertex& v1, const Vertex& v2, const glm::mat4& mvp, const Texture& texture) { + glm::vec2 p0, p1, p2; + float z0, z1, z2; + + if (ProcessVertex(v0.pos, mvp, p0, z0) || ProcessVertex(v1.pos, mvp, p1, z1) || ProcessVertex(v2.pos, mvp, p2, z2)) + return; + + + auto aabbmin = glm::max(glm::min(glm::min(p0, p1), p2), glm::vec2(0, 0)); + auto aabbmax = glm::min(glm::max(glm::max(p0, p1), p2), glm::vec2(RES_WIDTH - 1, RES_HEIGHT - 1)); + + int ymin = aabbmin.y; + int ymax = aabbmax.y; + +//#pragma omp parallel for + for (int y = ymin; y <= ymax; y++) { + for (int x = aabbmin.x; x <= aabbmax.x; x++) { + glm::vec2 p(x, y); + + if (IsRight(p0, p1, p) && IsRight(p1, p2, p) && IsRight(p2, p0, p)) { + auto w = Barycentric(p, p0, p1, p2); + auto depth = w[0] * z0 + w[1] * z1 + w[2] * z2; + int idx = p.x + p.y * RES_WIDTH; + + if (depth < s_depthbuffer[idx] && depth > 0.0f) { + s_depthbuffer[idx] = depth; + + auto texcoord = w[0] * v0.texcoord + w[1] * v1.texcoord + w[2] * v2.texcoord; + auto normal = w[0] * v0.normal + w[1] * v1.normal + w[2] * v2.normal; + + glm::vec3 light_dir(-1, -2, -1); + + float diff = glm::max(glm::dot(glm::normalize(normal), -glm::normalize(light_dir)), 0.3f); + + s_colorbuffer[idx] = texture.Sample(texcoord) * diff; //glm::vec3(0, texcoord.x, texcoord.y); + //s_colorbuffer[idx] = glm::vec3(0, texcoord.x, texcoord.y); + + } + } + } + } +} + +int main() { + //omp_set_num_threads(4); + + std::ifstream data("data.txt"); + size_t waste, num_vertices; + std::vector vertices; + + data >> waste >> num_vertices; + vertices.resize(num_vertices); + + + for (int i = 0; i < num_vertices; ++i) { + auto& v = vertices[i]; + data >> v.pos.x >> v.pos.y >> v.pos.z >> v.texcoord.x >> v.texcoord.y >> v.normal.x >> v.normal.y >> v.normal.z; + } + + Texture texture; + + data >> texture.width >> texture.height; + texture.texels.resize(texture.width * texture.height); + + for (int i = 0; i < texture.texels.size(); ++i) { + auto& t = texture.texels[i]; + int r, g, b; + data >> r >> g >> b; + + t.r = r; t.g = g; t.b = b; + } + + + //HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + //SetConsoleMode(hConsole, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + //system(" "); + + float rot = 0.0f; + + + int lastfpstime = 0; + int frames = 0; + + while (true) { + frames++; + auto time = timeGetTime(); + if (lastfpstime + 1000 < time) { + lastfpstime = time; + s_fps = frames; + frames = 0; + } + + ClearBuffers(1.0f, glm::vec3(0.3f)); + + rot = (float)time * 0.001f; + + glm::vec3 campos; + campos.x = glm::sin(rot) * 7.0f; + campos.y = 3.0f; + campos.z = glm::cos(rot) * 7.0f; + + campos *= (glm::sin(rot * 0.3f) + 1.0f) * 0.5f; + + //rot += glm::pi() / 500.0f; + + + float aspect_ratio = (float)RES_WIDTH / (float)RES_HEIGHT; + auto view = glm::lookAt(campos, glm::vec3(0), glm::vec3(0, 1, 0)); + auto proj = glm::perspective(glm::radians(90.0f) / aspect_ratio, aspect_ratio, 0.1f, 100.0f); + + auto mvp = proj * view; + + for (int i = 0; i < vertices.size(); i += 3) { + const auto& v0 = vertices[i]; + const auto& v1 = vertices[i + 1]; + const auto& v2 = vertices[i + 2]; + + DrawTriangle(v0, v1, v2, mvp, texture); + + } + + //for (int y = 0; y < RES_HEIGHT; ++y) { + // for (int x = 0; x < RES_WIDTH; ++x) { + + // s_colorbuffer[x + RES_WIDTH * y] = glm::vec3((float)x / RES_WIDTH, (float)y / RES_HEIGHT, 0); + + // } + //} + + + OutputVT100(); + //OutputWindows(); + + } + +} + +// Run program: Ctrl + F5 or Debug > Start Without Debugging menu +// Debug program: F5 or Debug > Start Debugging menu + +// Tips for Getting Started: +// 1. Use the Solution Explorer window to add/manage files +// 2. Use the Team Explorer window to connect to source control +// 3. Use the Output window to see build output and other messages +// 4. Use the Error List window to view errors +// 5. Go to Project > Add New Item to create new code files, or Project > Add Existing Item to add existing code files to the project +// 6. In the future, to open this project again, go to File > Open > Project and select the .sln file diff --git a/render.lua b/render.lua new file mode 100644 index 0000000..f0bb113 --- /dev/null +++ b/render.lua @@ -0,0 +1,529 @@ +local fps = 0 +local math_floor = math.floor +local time = 0 + +local function ClearBuffers(meta, colorbuffer, depthbuffer, color, depth) + local i + for i = 0, meta[3] - 1 do + colorbuffer[1 + i * 3] = color[1] + colorbuffer[1 + i * 3 + 1] = color[2] + colorbuffer[1 + i * 3 + 2] = color[3] + + depthbuffer[1 + i] = depth + end +end + +local function DegToRad(deg) + local rad + rad = deg * 0.0174532925 + return rad +end + +local function Min(a, b) + local r + if a < b then + r = a + else + r = b + end + return r +end + +local function Min3(a, b, c) + local r + r = Min(Min(a, b), c) + return r +end + +local function Max(a, b) + local r + if a > b then + r = a + else + r = b + end + return r +end + +local function Max3(a, b, c) + local r + r = Max(Max(a, b), c) + return r +end + +local function IsOnRight(a, b, c) + local r + if (b[1] - a[1]) * (c[2] - a[2]) - (b[2] - a[2]) * (c[1] - a[1]) <= 0 then + r = true + else + r = false + end + return r +end + +local function Distance2(a, b) + local x, y, d + x = b[1] - a[1] + y = b[2] - a[2] + d = math.sqrt(x * x + y * y) + return d +end + +local function Normalize(a) + local l + l = math.sqrt(a[1] * a[1] + a[2] * a[2] + a[3] * a[3]) + a[1] = a[1] / l + a[2] = a[2] / l + a[3] = a[3] / l +end + +local function MapToRes(meta, a) + a[1] = a[1] / a[4] + a[2] = a[2] / a[4] + a[3] = a[3] / a[4] + + a[1] = (a[1] + 1.0) * 0.5 * (meta[1] - 1) + a[2] = (1.0 - a[2]) * 0.5 * (meta[2] - 1) +end + +local function Cross(result, a, b) + result[1] = a[2] * b[3] - a[3] * b[2] + result[2] = a[3] * b[1] - a[1] * b[3] + result[3] = a[1] * b[2] - a[2] * b[1] +end + +local function Dot(a, b) + local result + result = a[1] * b[1] + a[2] * b[2] + a[3] * b[3] + return result +end + +local function Dot2(a, b) + local result + result = a[1] * b[1] + a[2] * b[2] + return result +end + +local function VecCopy3(result, a) + result[1] = a[1] + result[2] = a[2] + result[3] = a[3] +end + +local function VecSubVec3(result, a, b) + result[1] = a[1] - b[1] + result[2] = a[2] - b[2] + result[3] = a[3] - b[3] +end + +local function MatMultMat(result, a, b) + result[1] = a[1] * b[1] + a[2] * b[5] + a[3] * b[9] + a[4] * b[13] + result[2] = a[1] * b[2] + a[2] * b[6] + a[3] * b[10] + a[4] * b[14] + result[3] = a[1] * b[3] + a[2] * b[7] + a[3] * b[11] + a[4] * b[15] + result[4] = a[1] * b[4] + a[2] * b[8] + a[3] * b[12] + a[4] * b[16] + result[5] = a[5] * b[1] + a[6] * b[5] + a[7] * b[9] + a[8] * b[13] + result[6] = a[5] * b[2] + a[6] * b[6] + a[7] * b[10] + a[8] * b[14] + result[7] = a[5] * b[3] + a[6] * b[7] + a[7] * b[11] + a[8] * b[15] + result[8] = a[5] * b[4] + a[6] * b[8] + a[7] * b[12] + a[8] * b[16] + result[9] = a[9] * b[1] + a[10] * b[5] + a[11] * b[9] + a[12] * b[13] + result[10] = a[9] * b[2] + a[10] * b[6] + a[11] * b[10] + a[12] * b[14] + result[11] = a[9] * b[3] + a[10] * b[7] + a[11] * b[11] + a[12] * b[15] + result[12] = a[9] * b[4] + a[10] * b[8] + a[11] * b[12] + a[12] * b[16] + result[13] = a[13] * b[1] + a[14] * b[5] + a[15] * b[9] + a[16] * b[13] + result[14] = a[13] * b[2] + a[14] * b[6] + a[15] * b[10] + a[16] * b[14] + result[15] = a[13] * b[3] + a[14] * b[7] + a[15] * b[11] + a[16] * b[15] + result[16] = a[13] * b[4] + a[14] * b[8] + a[15] * b[12] + a[16] * b[16] +end + +local function MatMultVec(result, a, v) + result[1] = a[1] * v[1] + a[2] * v[2] + a[3] * v[3] + a[4] * v[4] + result[2] = a[5] * v[1] + a[6] * v[2] + a[7] * v[3] + a[8] * v[4] + result[3] = a[9] * v[1] + a[10] * v[2] + a[11] * v[3] + a[12] * v[4] + result[4] = a[13] * v[1] + a[14] * v[2] + a[15] * v[3] + a[16] * v[4] +end + +local function MatMultVec3(result, a, v) + result[1] = a[1] * v[1] + a[2] * v[2] + a[3] * v[3] + a[4] + result[2] = a[5] * v[1] + a[6] * v[2] + a[7] * v[3] + a[8] + result[3] = a[9] * v[1] + a[10] * v[2] + a[11] * v[3] + a[12] + result[4] = a[13] * v[1] + a[14] * v[2] + a[15] * v[3] + a[16] +end + +local function LookAt(mat, campos, targetpos, upvec) + local forward = {} + local right = {} + local up = {} + + VecSubVec3(forward, targetpos, campos) + Normalize(forward) + + Cross(right, forward, upvec) + Normalize(right) + + Cross(up, right, forward) + + mat[1] = right[1] + mat[2] = right[2] + mat[3] = right[3] + mat[4] = -Dot(right, campos) + mat[5] = up[1] + mat[6] = up[2] + mat[7] = up[3] + mat[8] = -Dot(up, campos) + mat[9] = -forward[1] + mat[10] = -forward[2] + mat[11] = -forward[3] + mat[12] = Dot(forward, campos) + mat[13] = 0.0 + mat[14] = 0.0 + mat[15] = 0.0 + mat[16] = 1.0 +end + +local function Perspective(mat, fov, aspectratio, near, far) + local tanhalffovy + tanhalffovy = math.tan(fov * 0.5) + + mat[1] = 1 / (aspectratio * tanhalffovy) + mat[2] = 0.0 + mat[3] = 0.0 + mat[4] = 0.0 + mat[5] = 0.0 + mat[6] = 1 / tanhalffovy + mat[7] = 0.0 + mat[8] = 0.0 + mat[9] = 0.0 + mat[10] = 0.0 + mat[11] = -(far + near) / (far - near) + mat[12] = -(2 * far * near) / (far - near) + mat[13] = 0.0 + mat[14] = 0.0 + mat[15] = -1.0 + mat[16] = 0.0 +end + +local function OutputVT100(meta, colorbuffer) + print("\x1b[1;1H") + + local r1, g1, b1, r2, b2, g2, width, height, idx, rowoffset + width = meta[1] + height = meta[2] + + local out = {} + + local pr1, pg1, pb1, pr2, pg2, pb2 = 0, 0, 0, 0, 0, 0 + + for y = 0, height - 2, 2 do + table.insert(out, "\n") + for x = 0, width - 1 do + idx = (x + y * width) * 3 + rowoffset = width * 3 + + r1 = math_floor(colorbuffer[1 + idx] * 255) + g1 = math_floor(colorbuffer[1 + idx + 1] * 255) + b1 = math_floor(colorbuffer[1 + idx + 2] * 255) + + r2 = math_floor(colorbuffer[1 + idx + rowoffset] * 255) + g2 = math_floor(colorbuffer[1 + idx + 1 + rowoffset] * 255) + b2 = math_floor(colorbuffer[1 + idx + 2 + rowoffset] * 255) + + if r1 == pr1 and g1 == pg1 and b1 == pb1 and r2 == pr2 and g2 == pg2 and b2 == pb2 then + table.insert(out, "\xDF") + else + pr1, pg1, pb1, pr2, pg2, pb2 = r1, g1, b1, r2, g2, b2 + table.insert(out, string.format("\x1b[38;2;%d;%d;%dm\x1b[48;2;%d;%d;%dm\xDF", r1, g1, b1, r2, g2, b2)) + end + + + -- io.write("\x1b[38;2;" .. + -- r1 .. ";" .. g1 .. ";" .. b1 .. "m" .. "\x1b[48;2;" .. r2 .. ";" .. g2 .. ";" .. b2 .. "m" .. "\xDF") + end + end + + io.write(table.concat(out)) + + io.write(string.format("\n\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m%d FPS", fps)) +end + +local function DrawTriangle(meta, colorbuffer, depthbuffer, wp0, wp1, wp2, texture, c0, c1, c2, mvp, lightdir, n0, n1, n2) + local p0 = {} + local p1 = {} + local p2 = {} + + MatMultVec3(p0, mvp, wp0) + MatMultVec3(p1, mvp, wp1) + MatMultVec3(p2, mvp, wp2) + + if p0[4] ~= 0.0 and p1[4] ~= 0.0 and p2[4] ~= 0.0 then + MapToRes(meta, p0) + MapToRes(meta, p1) + MapToRes(meta, p2) + + local aabbmin = {} + local aabbmax = {} + + aabbmin[1] = Max(Min3(p0[1], p1[1], p2[1]), 0) + aabbmin[2] = Max(Min3(p0[2], p1[2], p2[2]), 0) + aabbmax[1] = Min(Max3(p0[1], p1[1], p2[1]), meta[1] - 1) + aabbmax[2] = Min(Max3(p0[2], p1[2], p2[2]), meta[2] - 1) + + local idx, tx, ty, ti + local p = {} + local depth + local w0, w1, w2, dot00, dot01, dot11, dot20, dot21, denom, d0, d1, d2 + + local v0 = {} + local v1 = {} + local v2 = {} + + local texcoord = {} + + local normal = {} + local diff + + for y = math_floor(aabbmin[2]), math_floor(aabbmax[2]) do + for x = math_floor(aabbmin[1]), math_floor(aabbmax[1]) do + p[1] = x + p[2] = y + + if IsOnRight(p0, p1, p) and IsOnRight(p1, p2, p) and IsOnRight(p2, p0, p) then + v0[1] = p1[1] - p0[1] + v0[2] = p1[2] - p0[2] + v1[1] = p2[1] - p0[1] + v1[2] = p2[2] - p0[2] + v2[1] = p[1] - p0[1] + v2[2] = p[2] - p0[2] + + dot00 = Dot2(v0, v0) + dot01 = Dot2(v0, v1) + dot11 = Dot2(v1, v1) + dot20 = Dot2(v2, v0) + dot21 = Dot2(v2, v1) + + denom = dot00 * dot11 - dot01 * dot01 + w1 = (dot11 * dot20 - dot01 * dot21) / denom + w2 = (dot00 * dot21 - dot01 * dot20) / denom + w0 = 1.0 - w1 - w2 + + depth = p0[3] * w0 + p1[3] * w1 + p2[3] * w2 + + idx = math_floor(x) + meta[1] * math_floor(y) + + if depth > 0.0 and depthbuffer[1 + idx] > depth then + depthbuffer[1 + idx] = depth + idx = idx * 3 + + texcoord[1] = w0 * c0[1] + w1 * c1[1] + w2 * c2[1] + texcoord[2] = w0 * c0[2] + w1 * c1[2] + w2 * c2[2] + + normal[1] = w0 * n0[1] + w1 * n1[1] + w2 * n2[1] + normal[2] = w0 * n0[2] + w1 * n1[2] + w2 * n2[2] + normal[3] = w0 * n0[3] + w1 * n1[3] + w2 * n2[3] + + Normalize(normal) + diff = Max(0.5, Dot(normal, lightdir)) + + tx = math_floor(texcoord[1] * meta[4]) + ty = math_floor((1 - texcoord[2]) * meta[5]) + ti = (tx + ty * meta[4]) * 3 + + + -- uz ne kokote + + + + colorbuffer[1 + idx] = texture[1 + ti] / 255 * diff + colorbuffer[1 + idx + 1] = texture[1 + ti + 1] / 255 * diff + colorbuffer[1 + idx + 2] = texture[1 + ti + 2] / 255 * diff + + OutputVT100(meta, colorbuffer) + + end + end + end + end + end +end + +local file = io.open("data.txt", "r") + +local function Input() + return file:read("*n") +end + +-- file:close() -- Close the file + +-- return number + +local function Main() + os.execute(" ") + + local meta = {} + + -- meta[1] = 175 + -- meta[2] = 120 + + meta[1] = 175 * 2 + meta[2] = 120 * 2 + + meta[3] = meta[1] * meta[2] + + local depthbuffer = {} + local colorbuffer = {} + + local background = {} + background[1] = 0.3 + background[2] = 0.3 + background[3] = 0.3 + + ClearBuffers(meta, colorbuffer, depthbuffer, background, 100.0) + + local lightdir = {} + lightdir[1] = .3 + lightdir[2] = 1 + lightdir[3] = .4 + Normalize(lightdir) + + local view = {} + local proj = {} + local mvp = {} + + local campos = {} + local targetpos = {} + local upvector = {} + + targetpos[1] = 0 + targetpos[2] = 0 + targetpos[3] = 0 + + upvector[1] = 0 + upvector[2] = 1 + upvector[3] = 0 + local aspectratio + aspectratio = 1.46 + + + Perspective(proj, DegToRad(90.0) / aspectratio, aspectratio, 0.1, 50.0) + + local p0 = {} + local p1 = {} + local p2 = {} + + local c0 = {} + local c1 = {} + local c2 = {} + + local n0 = {} + local n1 = {} + local n2 = {} + + local datasize, i, vertices + datasize = Input() + vertices = Input() + + local data = {} + for i = 1, datasize do + data[i] = Input() + end + + print("model rdy") + + meta[4] = Input() + meta[5] = Input() + + local tp + tp = meta[4] * meta[5] * 3 + + local texture = {} + + for i = 0, tp - 1 do + texture[1 + i] = Input() + end + + print("textura rdy") + + local temp + + local i0, i1, i2 + + local camangle + camangle = 0 + + local frames = 0 + local lastframereset = 0 + + while true do + frames = frames + 1 + + time = os.clock() * 1000 + + if time > lastframereset + 1000 then + fps = frames + frames = 0 + lastframereset = time + end + + + ClearBuffers(meta, colorbuffer, depthbuffer, background, 100.0) + + local camdis + camdis = 7.0 + + campos[1] = math.sin(camangle) * camdis + campos[2] = 0.8 * camdis + campos[3] = math.cos(camangle) * camdis + + + camangle = time * 0.0003 + + LookAt(view, campos, targetpos, upvector) + MatMultMat(mvp, proj, view) + + for i = 0, vertices - 3, 3 do + i0 = i * 8 + i1 = (i + 1) * 8 + i2 = (i + 2) * 8 + + p0[1] = data[1 + i0] + p0[2] = data[1 + i0 + 1] + p0[3] = data[1 + i0 + 2] + + p1[1] = data[1 + i1] + p1[2] = data[1 + i1 + 1] + p1[3] = data[1 + i1 + 2] + + p2[1] = data[1 + i2] + p2[2] = data[1 + i2 + 1] + p2[3] = data[1 + i2 + 2] + + + c0[1] = data[1 + i0 + 3] + c0[2] = data[1 + i0 + 4] + + c1[1] = data[1 + i1 + 3] + c1[2] = data[1 + i1 + 4] + + c2[1] = data[1 + i2 + 3] + c2[2] = data[1 + i2 + 4] + + n0[1] = data[1 + i0 + 5] + n0[2] = data[1 + i0 + 6] + n0[3] = data[1 + i0 + 7] + + n1[1] = data[1 + i1 + 5] + n1[2] = data[1 + i1 + 6] + n1[3] = data[1 + i1 + 7] + + n2[1] = data[1 + i2 + 5] + n2[2] = data[1 + i2 + 6] + n2[3] = data[1 + i2 + 7] + + DrawTriangle(meta, colorbuffer, depthbuffer, p0, p1, p2, texture, c0, c1, c2, mvp, lightdir, n0, n1, n2) + end + + + OutputVT100(meta, colorbuffer) + end +end + +Main() diff --git a/render_cc.lua b/render_cc.lua new file mode 100644 index 0000000..3003f57 --- /dev/null +++ b/render_cc.lua @@ -0,0 +1,616 @@ +local fps = 0 +local math_floor = math.floor +local time = 0 + +local function ClearBuffers(meta, colorbuffer, depthbuffer, color, depth) + local i + for i = 0, meta[3] - 1 do + colorbuffer[1 + i * 3] = color[1] + colorbuffer[1 + i * 3 + 1] = color[2] + colorbuffer[1 + i * 3 + 2] = color[3] + + depthbuffer[1 + i] = depth + end +end + +local function DegToRad(deg) + local rad + rad = deg * 0.0174532925 + return rad +end + +local function Min(a, b) + local r + if a < b then + r = a + else + r = b + end + return r +end + +local function Min3(a, b, c) + local r + r = Min(Min(a, b), c) + return r +end + +local function Max(a, b) + local r + if a > b then + r = a + else + r = b + end + return r +end + +local function Max3(a, b, c) + local r + r = Max(Max(a, b), c) + return r +end + +local function IsOnRight(a, b, c) + local r + if (b[1] - a[1]) * (c[2] - a[2]) - (b[2] - a[2]) * (c[1] - a[1]) <= 0 then + r = true + else + r = false + end + return r +end + +local function Distance2(a, b) + local x, y, d + x = b[1] - a[1] + y = b[2] - a[2] + d = math.sqrt(x * x + y * y) + return d +end + +local function Normalize(a) + local l + l = math.sqrt(a[1] * a[1] + a[2] * a[2] + a[3] * a[3]) + a[1] = a[1] / l + a[2] = a[2] / l + a[3] = a[3] / l +end + +local function MapToRes(meta, a) + a[1] = a[1] / a[4] + a[2] = a[2] / a[4] + a[3] = a[3] / a[4] + + a[1] = (a[1] + 1.0) * 0.5 * (meta[1] - 1) + a[2] = (1.0 - a[2]) * 0.5 * (meta[2] - 1) +end + +local function Cross(result, a, b) + result[1] = a[2] * b[3] - a[3] * b[2] + result[2] = a[3] * b[1] - a[1] * b[3] + result[3] = a[1] * b[2] - a[2] * b[1] +end + +local function Dot(a, b) + local result + result = a[1] * b[1] + a[2] * b[2] + a[3] * b[3] + return result +end + +local function Dot2(a, b) + local result + result = a[1] * b[1] + a[2] * b[2] + return result +end + +local function VecCopy3(result, a) + result[1] = a[1] + result[2] = a[2] + result[3] = a[3] +end + +local function VecSubVec3(result, a, b) + result[1] = a[1] - b[1] + result[2] = a[2] - b[2] + result[3] = a[3] - b[3] +end + +local function MatMultMat(result, a, b) + result[1] = a[1] * b[1] + a[2] * b[5] + a[3] * b[9] + a[4] * b[13] + result[2] = a[1] * b[2] + a[2] * b[6] + a[3] * b[10] + a[4] * b[14] + result[3] = a[1] * b[3] + a[2] * b[7] + a[3] * b[11] + a[4] * b[15] + result[4] = a[1] * b[4] + a[2] * b[8] + a[3] * b[12] + a[4] * b[16] + result[5] = a[5] * b[1] + a[6] * b[5] + a[7] * b[9] + a[8] * b[13] + result[6] = a[5] * b[2] + a[6] * b[6] + a[7] * b[10] + a[8] * b[14] + result[7] = a[5] * b[3] + a[6] * b[7] + a[7] * b[11] + a[8] * b[15] + result[8] = a[5] * b[4] + a[6] * b[8] + a[7] * b[12] + a[8] * b[16] + result[9] = a[9] * b[1] + a[10] * b[5] + a[11] * b[9] + a[12] * b[13] + result[10] = a[9] * b[2] + a[10] * b[6] + a[11] * b[10] + a[12] * b[14] + result[11] = a[9] * b[3] + a[10] * b[7] + a[11] * b[11] + a[12] * b[15] + result[12] = a[9] * b[4] + a[10] * b[8] + a[11] * b[12] + a[12] * b[16] + result[13] = a[13] * b[1] + a[14] * b[5] + a[15] * b[9] + a[16] * b[13] + result[14] = a[13] * b[2] + a[14] * b[6] + a[15] * b[10] + a[16] * b[14] + result[15] = a[13] * b[3] + a[14] * b[7] + a[15] * b[11] + a[16] * b[15] + result[16] = a[13] * b[4] + a[14] * b[8] + a[15] * b[12] + a[16] * b[16] +end + +local function MatMultVec(result, a, v) + result[1] = a[1] * v[1] + a[2] * v[2] + a[3] * v[3] + a[4] * v[4] + result[2] = a[5] * v[1] + a[6] * v[2] + a[7] * v[3] + a[8] * v[4] + result[3] = a[9] * v[1] + a[10] * v[2] + a[11] * v[3] + a[12] * v[4] + result[4] = a[13] * v[1] + a[14] * v[2] + a[15] * v[3] + a[16] * v[4] +end + +local function MatMultVec3(result, a, v) + result[1] = a[1] * v[1] + a[2] * v[2] + a[3] * v[3] + a[4] + result[2] = a[5] * v[1] + a[6] * v[2] + a[7] * v[3] + a[8] + result[3] = a[9] * v[1] + a[10] * v[2] + a[11] * v[3] + a[12] + result[4] = a[13] * v[1] + a[14] * v[2] + a[15] * v[3] + a[16] +end + +local function LookAt(mat, campos, targetpos, upvec) + local forward = {} + local right = {} + local up = {} + + VecSubVec3(forward, targetpos, campos) + Normalize(forward) + + Cross(right, forward, upvec) + Normalize(right) + + Cross(up, right, forward) + + mat[1] = right[1] + mat[2] = right[2] + mat[3] = right[3] + mat[4] = -Dot(right, campos) + mat[5] = up[1] + mat[6] = up[2] + mat[7] = up[3] + mat[8] = -Dot(up, campos) + mat[9] = -forward[1] + mat[10] = -forward[2] + mat[11] = -forward[3] + mat[12] = Dot(forward, campos) + mat[13] = 0.0 + mat[14] = 0.0 + mat[15] = 0.0 + mat[16] = 1.0 +end + +local function Perspective(mat, fov, aspectratio, near, far) + local tanhalffovy + tanhalffovy = math.tan(fov * 0.5) + + mat[1] = 1 / (aspectratio * tanhalffovy) + mat[2] = 0.0 + mat[3] = 0.0 + mat[4] = 0.0 + mat[5] = 0.0 + mat[6] = 1 / tanhalffovy + mat[7] = 0.0 + mat[8] = 0.0 + mat[9] = 0.0 + mat[10] = 0.0 + mat[11] = -(far + near) / (far - near) + mat[12] = -(2 * far * near) / (far - near) + mat[13] = 0.0 + mat[14] = 0.0 + mat[15] = -1.0 + mat[16] = 0.0 +end + +-- local function OutputVT100(meta, colorbuffer) +-- print("\x1b[1;1H") + +-- local r1, g1, b1, r2, b2, g2, width, height, idx, rowoffset +-- width = meta[1] +-- height = meta[2] + +-- local out = {} + +-- local pr1, pg1, pb1, pr2, pg2, pb2 = 0, 0, 0, 0, 0, 0 + +-- for y = 0, height - 2, 2 do +-- table.insert(out, "\n") +-- for x = 0, width - 1 do +-- idx = (x + y * width) * 3 +-- rowoffset = width * 3 + +-- r1 = math_floor(colorbuffer[1 + idx] * 255) +-- g1 = math_floor(colorbuffer[1 + idx + 1] * 255) +-- b1 = math_floor(colorbuffer[1 + idx + 2] * 255) + +-- r2 = math_floor(colorbuffer[1 + idx + rowoffset] * 255) +-- g2 = math_floor(colorbuffer[1 + idx + 1 + rowoffset] * 255) +-- b2 = math_floor(colorbuffer[1 + idx + 2 + rowoffset] * 255) + +-- if r1 == pr1 and g1 == pg1 and b1 == pb1 and r2 == pr2 and g2 == pg2 and b2 == pb2 then +-- table.insert(out, "\xDF") +-- else +-- pr1, pg1, pb1, pr2, pg2, pb2 = r1, g1, b1, r2, g2, b2 +-- table.insert(out, string.format("\x1b[38;2;%d;%d;%dm\x1b[48;2;%d;%d;%dm\xDF", r1, g1, b1, r2, g2, b2)) +-- end + + +-- -- io.write("\x1b[38;2;" .. +-- -- r1 .. ";" .. g1 .. ";" .. b1 .. "m" .. "\x1b[48;2;" .. r2 .. ";" .. g2 .. ";" .. b2 .. "m" .. "\xDF") +-- end +-- end + +-- io.write(table.concat(out)) + +-- io.write(string.format("\n\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m%d FPS", fps)) +-- end + +local monitor = peripheral.find("monitor") + +local function SetupMonitor() + monitor.setTextScale(0.5) + monitor.clear() + + monitor.setPaletteColor(colors.white, 0, 0, 0 ) -- 0 + monitor.setPaletteColor(colors.orange, 0, 0, 127 ) -- 1 + monitor.setPaletteColor(colors.magenta, 0, 127, 0 ) -- 2 + monitor.setPaletteColor(colors.lightBlue, 0, 127, 127 ) -- 3 + monitor.setPaletteColor(colors.yellow, 127, 0, 0 ) -- 4 + monitor.setPaletteColor(colors.lime, 127, 0, 127 ) -- 5 + monitor.setPaletteColor(colors.pink, 127, 127, 0 ) -- 6 + monitor.setPaletteColor(colors.gray, 127, 127, 127 ) -- 7 + + monitor.setPaletteColor(colors.lightGray, 127, 127, 127 ) -- 8 + monitor.setPaletteColor(colors.cyan, 127, 127, 255 ) -- 9 + monitor.setPaletteColor(colors.purple, 127, 255, 127 ) -- a + monitor.setPaletteColor(colors.blue, 127, 255, 255 ) -- b + monitor.setPaletteColor(colors.brown, 255, 127, 127 ) -- c + monitor.setPaletteColor(colors.green, 255, 127, 255 ) -- d + monitor.setPaletteColor(colors.red, 255, 255, 127 ) -- e + monitor.setPaletteColor(colors.black, 255, 255, 255 ) -- f +end + +local function OutputCCMonitor(meta, colorbuffer) + + local width = meta[1] + local height = meta[2] + + local chars = {} + local fcs = {} + local bcs = {} + local hex_digits = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" } + + for y = 0, height - 1 do + for x = 0, width - 1 do + idx = (x + y * width) * 3 + rowoffset = width * 3 + + local r = colorbuffer[1 + idx] + local g = colorbuffer[1 + idx + 1] + local b = colorbuffer[1 + idx + 2] + + local avg = (r + g + b) / 3 + + local c = 0 + local threshold = 1.2 + if b > avg * threshold then c = c + 1 end + if g > avg * threshold then c = c + 2 end + if r > avg * threshold then c = c + 4 end + + local bright_threshold = 0.5 + if r > bright_threshold or g > bright_threshold or b > bright_threshold then c = c + 8 end + + local xx = x + 1 + local cc = hex_digits[c + 1] + chars[xx] = " " + fcs[xx] = cc + bcs[xx] = cc + + end + + monitor.setCursorPos(1, y + 1) + monitor.blit(table.concat(chars), table.concat(fcs), table.concat(bcs)) + end + +end + +local function DrawTriangle(meta, colorbuffer, depthbuffer, wp0, wp1, wp2, texture, c0, c1, c2, mvp, lightdir, n0, n1, n2) + local p0 = {} + local p1 = {} + local p2 = {} + + MatMultVec3(p0, mvp, wp0) + MatMultVec3(p1, mvp, wp1) + MatMultVec3(p2, mvp, wp2) + + if p0[4] ~= 0.0 and p1[4] ~= 0.0 and p2[4] ~= 0.0 then + MapToRes(meta, p0) + MapToRes(meta, p1) + MapToRes(meta, p2) + + local aabbmin = {} + local aabbmax = {} + + aabbmin[1] = Max(Min3(p0[1], p1[1], p2[1]), 0) + aabbmin[2] = Max(Min3(p0[2], p1[2], p2[2]), 0) + aabbmax[1] = Min(Max3(p0[1], p1[1], p2[1]), meta[1] - 1) + aabbmax[2] = Min(Max3(p0[2], p1[2], p2[2]), meta[2] - 1) + + local idx, tx, ty, ti + local p = {} + local depth + local w0, w1, w2, dot00, dot01, dot11, dot20, dot21, denom, d0, d1, d2 + + local v0 = {} + local v1 = {} + local v2 = {} + + local texcoord = {} + + local normal = {} + local diff + + for y = math_floor(aabbmin[2]), math_floor(aabbmax[2]) do + for x = math_floor(aabbmin[1]), math_floor(aabbmax[1]) do + p[1] = x + p[2] = y + + if IsOnRight(p0, p1, p) and IsOnRight(p1, p2, p) and IsOnRight(p2, p0, p) then + v0[1] = p1[1] - p0[1] + v0[2] = p1[2] - p0[2] + v1[1] = p2[1] - p0[1] + v1[2] = p2[2] - p0[2] + v2[1] = p[1] - p0[1] + v2[2] = p[2] - p0[2] + + dot00 = Dot2(v0, v0) + dot01 = Dot2(v0, v1) + dot11 = Dot2(v1, v1) + dot20 = Dot2(v2, v0) + dot21 = Dot2(v2, v1) + + denom = dot00 * dot11 - dot01 * dot01 + w1 = (dot11 * dot20 - dot01 * dot21) / denom + w2 = (dot00 * dot21 - dot01 * dot20) / denom + w0 = 1.0 - w1 - w2 + + depth = p0[3] * w0 + p1[3] * w1 + p2[3] * w2 + + idx = math_floor(x) + meta[1] * math_floor(y) + + if depth > 0.0 and depthbuffer[1 + idx] > depth then + depthbuffer[1 + idx] = depth + idx = idx * 3 + + texcoord[1] = w0 * c0[1] + w1 * c1[1] + w2 * c2[1] + texcoord[2] = w0 * c0[2] + w1 * c1[2] + w2 * c2[2] + + normal[1] = w0 * n0[1] + w1 * n1[1] + w2 * n2[1] + normal[2] = w0 * n0[2] + w1 * n1[2] + w2 * n2[2] + normal[3] = w0 * n0[3] + w1 * n1[3] + w2 * n2[3] + + Normalize(normal) + diff = Max(0.5, Dot(normal, lightdir)) + + tx = math_floor(texcoord[1] * meta[4]) + ty = math_floor((1 - texcoord[2]) * meta[5]) + ti = (tx + ty * meta[4]) * 3 + + + -- uz ne kokote + + + + colorbuffer[1 + idx] = texture[1 + ti] / 255 * diff + colorbuffer[1 + idx + 1] = texture[1 + ti + 1] / 255 * diff + colorbuffer[1 + idx + 2] = texture[1 + ti + 2] / 255 * diff + + -- OutputVT100(meta, colorbuffer) + + end + end + end + end + end +end + +local file = assert(io.open("mesh.txt", "r")) + +local function Input() + return tonumber(file:read()) +end + +-- file:close() -- Close the file + +-- return number + +function load_texture(filename) + local file = assert(io.open(filename, "rb")) + local data = file:read("*all") + file:close() + + local tex = {} + for i = 1, #data do + tex[i] = string.byte(data, i) + end + + return tex +end + +local function Main() + SetupMonitor() + + local meta = {} + + -- meta[1] = 175 + -- meta[2] = 120 + + meta[1] = 79 + meta[2] = 38 + local charaspect = 1.5 + + meta[3] = meta[1] * meta[2] + + local depthbuffer = {} + local colorbuffer = {} + + local background = {} + background[1] = 0.3 + background[2] = 0.3 + background[3] = 0.3 + + ClearBuffers(meta, colorbuffer, depthbuffer, background, 100.0) + + local lightdir = {} + lightdir[1] = .3 + lightdir[2] = 1 + lightdir[3] = .4 + Normalize(lightdir) + + local view = {} + local proj = {} + local mvp = {} + + local campos = {} + local targetpos = {} + local upvector = {} + + targetpos[1] = 0 + targetpos[2] = 0 + targetpos[3] = 0 + + upvector[1] = 0 + upvector[2] = 1 + upvector[3] = 0 + local aspectratio + aspectratio = (meta[1] / meta[2]) / charaspect + + + Perspective(proj, DegToRad(90.0) / aspectratio, aspectratio, 0.1, 50.0) + + local p0 = {} + local p1 = {} + local p2 = {} + + local c0 = {} + local c1 = {} + local c2 = {} + + local n0 = {} + local n1 = {} + local n2 = {} + + local i, vertices + vertices = Input() + local datasize = vertices * 8 + + local data = {} + for i = 1, datasize do + data[i] = Input() + end + + print("model rdy") + + meta[4] = 256 -- tex width + meta[5] = 256 -- tex height + + -- local tp + -- tp = meta[4] * meta[5] * 3 + + -- local texture = {} + + -- for i = 0, tp - 1 do + -- texture[1 + i] = Input() + -- end + + local texture = load_texture("texture.bin") + + print("textura rdy") + + local temp + + local i0, i1, i2 + + local camangle + camangle = 0 + + local frames = 0 + local lastframereset = 0 + + while true do + frames = frames + 1 + + time = os.clock() * 1000 + + if time > lastframereset + 1000 then + fps = frames + frames = 0 + lastframereset = time + end + + + ClearBuffers(meta, colorbuffer, depthbuffer, background, 100.0) + + local camdis + camdis = 7.0 + + campos[1] = math.sin(camangle) * camdis + campos[2] = 0.8 * camdis + campos[3] = math.cos(camangle) * camdis + + + camangle = time * 0.0005 + + LookAt(view, campos, targetpos, upvector) + MatMultMat(mvp, proj, view) + + for i = 0, vertices - 3, 3 do + i0 = i * 8 + i1 = (i + 1) * 8 + i2 = (i + 2) * 8 + + p0[1] = data[1 + i0] + p0[2] = data[1 + i0 + 1] + p0[3] = data[1 + i0 + 2] + + p1[1] = data[1 + i1] + p1[2] = data[1 + i1 + 1] + p1[3] = data[1 + i1 + 2] + + p2[1] = data[1 + i2] + p2[2] = data[1 + i2 + 1] + p2[3] = data[1 + i2 + 2] + + + c0[1] = data[1 + i0 + 3] + c0[2] = data[1 + i0 + 4] + + c1[1] = data[1 + i1 + 3] + c1[2] = data[1 + i1 + 4] + + c2[1] = data[1 + i2 + 3] + c2[2] = data[1 + i2 + 4] + + n0[1] = data[1 + i0 + 5] + n0[2] = data[1 + i0 + 6] + n0[3] = data[1 + i0 + 7] + + n1[1] = data[1 + i1 + 5] + n1[2] = data[1 + i1 + 6] + n1[3] = data[1 + i1 + 7] + + n2[1] = data[1 + i2 + 5] + n2[2] = data[1 + i2 + 6] + n2[3] = data[1 + i2 + 7] + + DrawTriangle(meta, colorbuffer, depthbuffer, p0, p1, p2, texture, c0, c1, c2, mvp, lightdir, n0, n1, n2) + end + + + OutputCCMonitor(meta, colorbuffer) + + os.sleep(0.01) + end +end + +Main() diff --git a/render_origo.lua b/render_origo.lua new file mode 100644 index 0000000..6fc0db9 --- /dev/null +++ b/render_origo.lua @@ -0,0 +1,522 @@ +local fps = 0 + +local function ClearBuffers(meta, colorbuffer, depthbuffer, color, depth) + local i + for i = 0, meta[3] - 1 do + colorbuffer[1 + i * 3] = color[1] + colorbuffer[1 + i * 3 + 1] = color[2] + colorbuffer[1 + i * 3 + 2] = color[3] + + depthbuffer[1 + i] = depth + end +end + +local function DegToRad(deg) + local rad + rad = deg * 0.0174532925 + return rad +end + +local function Min(a, b) + local r + if a < b then + r = a + else + r = b + end + return r +end + +local function Min3(a, b, c) + local r + r = Min(Min(a, b), c) + return r +end + +local function Max(a, b) + local r + if a > b then + r = a + else + r = b + end + return r +end + +local function Max3(a, b, c) + local r + r = Max(Max(a, b), c) + return r +end + +local function IsOnRight(a, b, c) + local r + if (b[1] - a[1]) * (c[2] - a[2]) - (b[2] - a[2]) * (c[1] - a[1]) <= 0 then + r = true + else + r = false + end + return r +end + +local function Distance2(a, b) + local x, y, d + x = b[1] - a[1] + y = b[2] - a[2] + d = math.sqrt(x * x + y * y) + return d +end + +local function Normalize(a) + local l + l = math.sqrt(a[1] * a[1] + a[2] * a[2] + a[3] * a[3]) + a[1] = a[1] / l + a[2] = a[2] / l + a[3] = a[3] / l +end + +local function MapToRes(meta, a) + a[1] = a[1] / a[4] + a[2] = a[2] / a[4] + a[3] = a[3] / a[4] + + a[1] = (a[1] + 1.0) * 0.5 * (meta[1] - 1) + a[2] = (1.0 - a[2]) * 0.5 * (meta[2] - 1) +end + +local function Cross(result, a, b) + result[1] = a[2] * b[3] - a[3] * b[2] + result[2] = a[3] * b[1] - a[1] * b[3] + result[3] = a[1] * b[2] - a[2] * b[1] +end + +local function Dot(a, b) + local result + result = a[1] * b[1] + a[2] * b[2] + a[3] * b[3] + return result +end + +local function Dot2(a, b) + local result + result = a[1] * b[1] + a[2] * b[2] + return result +end + +local function VecCopy3(result, a) + result[1] = a[1] + result[2] = a[2] + result[3] = a[3] +end + +local function VecSubVec3(result, a, b) + result[1] = a[1] - b[1] + result[2] = a[2] - b[2] + result[3] = a[3] - b[3] +end + +local function MatMultMat(result, a, b) + result[1] = a[1] * b[1] + a[2] * b[5] + a[3] * b[9] + a[4] * b[13] + result[2] = a[1] * b[2] + a[2] * b[6] + a[3] * b[10] + a[4] * b[14] + result[3] = a[1] * b[3] + a[2] * b[7] + a[3] * b[11] + a[4] * b[15] + result[4] = a[1] * b[4] + a[2] * b[8] + a[3] * b[12] + a[4] * b[16] + result[5] = a[5] * b[1] + a[6] * b[5] + a[7] * b[9] + a[8] * b[13] + result[6] = a[5] * b[2] + a[6] * b[6] + a[7] * b[10] + a[8] * b[14] + result[7] = a[5] * b[3] + a[6] * b[7] + a[7] * b[11] + a[8] * b[15] + result[8] = a[5] * b[4] + a[6] * b[8] + a[7] * b[12] + a[8] * b[16] + result[9] = a[9] * b[1] + a[10] * b[5] + a[11] * b[9] + a[12] * b[13] + result[10] = a[9] * b[2] + a[10] * b[6] + a[11] * b[10] + a[12] * b[14] + result[11] = a[9] * b[3] + a[10] * b[7] + a[11] * b[11] + a[12] * b[15] + result[12] = a[9] * b[4] + a[10] * b[8] + a[11] * b[12] + a[12] * b[16] + result[13] = a[13] * b[1] + a[14] * b[5] + a[15] * b[9] + a[16] * b[13] + result[14] = a[13] * b[2] + a[14] * b[6] + a[15] * b[10] + a[16] * b[14] + result[15] = a[13] * b[3] + a[14] * b[7] + a[15] * b[11] + a[16] * b[15] + result[16] = a[13] * b[4] + a[14] * b[8] + a[15] * b[12] + a[16] * b[16] +end + +local function MatMultVec(result, a, v) + result[1] = a[1] * v[1] + a[2] * v[2] + a[3] * v[3] + a[4] * v[4] + result[2] = a[5] * v[1] + a[6] * v[2] + a[7] * v[3] + a[8] * v[4] + result[3] = a[9] * v[1] + a[10] * v[2] + a[11] * v[3] + a[12] * v[4] + result[4] = a[13] * v[1] + a[14] * v[2] + a[15] * v[3] + a[16] * v[4] +end + +local function MatMultVec3(result, a, v) + result[1] = a[1] * v[1] + a[2] * v[2] + a[3] * v[3] + a[4] + result[2] = a[5] * v[1] + a[6] * v[2] + a[7] * v[3] + a[8] + result[3] = a[9] * v[1] + a[10] * v[2] + a[11] * v[3] + a[12] + result[4] = a[13] * v[1] + a[14] * v[2] + a[15] * v[3] + a[16] +end + +local function LookAt(mat, campos, targetpos, upvec) + local forward = {} + local right = {} + local up = {} + + VecSubVec3(forward, targetpos, campos) + Normalize(forward) + + Cross(right, forward, upvec) + Normalize(right) + + Cross(up, right, forward) + + mat[1] = right[1] + mat[2] = right[2] + mat[3] = right[3] + mat[4] = -Dot(right, campos) + mat[5] = up[1] + mat[6] = up[2] + mat[7] = up[3] + mat[8] = -Dot(up, campos) + mat[9] = -forward[1] + mat[10] = -forward[2] + mat[11] = -forward[3] + mat[12] = Dot(forward, campos) + mat[13] = 0.0 + mat[14] = 0.0 + mat[15] = 0.0 + mat[16] = 1.0 +end + +local function Perspective(mat, fov, aspectratio, near, far) + local tanhalffovy + tanhalffovy = math.tan(fov * 0.5) + + mat[1] = 1 / (aspectratio * tanhalffovy) + mat[2] = 0.0 + mat[3] = 0.0 + mat[4] = 0.0 + mat[5] = 0.0 + mat[6] = 1 / tanhalffovy + mat[7] = 0.0 + mat[8] = 0.0 + mat[9] = 0.0 + mat[10] = 0.0 + mat[11] = -(far + near) / (far - near) + mat[12] = -(2 * far * near) / (far - near) + mat[13] = 0.0 + mat[14] = 0.0 + mat[15] = -1.0 + mat[16] = 0.0 +end + +local function DrawTriangle(meta, colorbuffer, depthbuffer, wp0, wp1, wp2, texture, c0, c1, c2, mvp, lightdir, n0, n1, n2) + local p0 = {} + local p1 = {} + local p2 = {} + + MatMultVec3(p0, mvp, wp0) + MatMultVec3(p1, mvp, wp1) + MatMultVec3(p2, mvp, wp2) + + if p0[4] ~= 0.0 and p1[4] ~= 0.0 and p2[4] ~= 0.0 then + MapToRes(meta, p0) + MapToRes(meta, p1) + MapToRes(meta, p2) + + local aabbmin = {} + local aabbmax = {} + + aabbmin[1] = Max(Min3(p0[1], p1[1], p2[1]), 0) + aabbmin[2] = Max(Min3(p0[2], p1[2], p2[2]), 0) + aabbmax[1] = Min(Max3(p0[1], p1[1], p2[1]), meta[1] - 1) + aabbmax[2] = Min(Max3(p0[2], p1[2], p2[2]), meta[2] - 1) + + local idx, tx, ty, ti + local p = {} + local depth + local w0, w1, w2, dot00, dot01, dot11, dot20, dot21, denom, d0, d1, d2 + + local v0 = {} + local v1 = {} + local v2 = {} + + local texcoord = {} + + local normal = {} + local diff + + for x = math.floor(aabbmin[1]), math.floor(aabbmax[1]) do + for y = math.floor(aabbmin[2]), math.floor(aabbmax[2]) do + p[1] = x + p[2] = y + + if IsOnRight(p0, p1, p) and IsOnRight(p1, p2, p) and IsOnRight(p2, p0, p) then + v0[1] = p1[1] - p0[1] + v0[2] = p1[2] - p0[2] + v1[1] = p2[1] - p0[1] + v1[2] = p2[2] - p0[2] + v2[1] = p[1] - p0[1] + v2[2] = p[2] - p0[2] + + dot00 = Dot2(v0, v0) + dot01 = Dot2(v0, v1) + dot11 = Dot2(v1, v1) + dot20 = Dot2(v2, v0) + dot21 = Dot2(v2, v1) + + denom = dot00 * dot11 - dot01 * dot01 + w1 = (dot11 * dot20 - dot01 * dot21) / denom + w2 = (dot00 * dot21 - dot01 * dot20) / denom + w0 = 1.0 - w1 - w2 + + depth = p0[3] * w0 + p1[3] * w1 + p2[3] * w2 + + idx = math.floor(x) + meta[1] * math.floor(y) + + if depth > 0.0 and depthbuffer[1 + idx] > depth then + depthbuffer[1 + idx] = depth + idx = idx * 3 + + texcoord[1] = w0 * c0[1] + w1 * c1[1] + w2 * c2[1] + texcoord[2] = w0 * c0[2] + w1 * c1[2] + w2 * c2[2] + + normal[1] = w0 * n0[1] + w1 * n1[1] + w2 * n2[1] + normal[2] = w0 * n0[2] + w1 * n1[2] + w2 * n2[2] + normal[3] = w0 * n0[3] + w1 * n1[3] + w2 * n2[3] + + Normalize(normal) + diff = Max(0.5, Dot(normal, lightdir)) + + tx = math.floor(texcoord[1] * meta[4]) + ty = math.floor((1 - texcoord[2]) * meta[5]) + ti = (tx + ty * meta[4]) * 3 + + + -- uz ne kokote + + + + colorbuffer[1 + idx] = texture[1 + ti] / 255 * diff + colorbuffer[1 + idx + 1] = texture[1 + ti + 1] / 255 * diff + colorbuffer[1 + idx + 2] = texture[1 + ti + 2] / 255 * diff + end + end + end + end + end +end + +local function OutputVT100(meta, colorbuffer) + print("\x1b[1;1H") + + local r1, g1, b1, r2, b2, g2, width, height, idx, rowoffset + width = meta[1] + height = meta[2] + + local out = {} + + local pr1, pg1, pb1, pr2, pg2, pb2 = 0, 0, 0, 0, 0, 0 + + for y = 0, height - 2, 2 do + table.insert(out, "\n") + for x = 0, width - 1 do + idx = (x + y * width) * 3 + rowoffset = width * 3 + + r1 = math.floor(colorbuffer[1 + idx] * 255) + g1 = math.floor(colorbuffer[1 + idx + 1] * 255) + b1 = math.floor(colorbuffer[1 + idx + 2] * 255) + + r2 = math.floor(colorbuffer[1 + idx + rowoffset] * 255) + g2 = math.floor(colorbuffer[1 + idx + 1 + rowoffset] * 255) + b2 = math.floor(colorbuffer[1 + idx + 2 + rowoffset] * 255) + + if r1 == pr1 and g1 == pg1 and b1 == pb1 and r2 == pr2 and g2 == pg2 and b2 == pb2 then + table.insert(out, "\xDF") + else + pr1, pg1, pb1, pr2, pg2, pb2 = r1, g1, b1, r2, g2, b2 + table.insert(out, string.format("\x1b[38;2;%d;%d;%dm\x1b[48;2;%d;%d;%dm\xDF", r1, g1, b1, r2, g2, b2)) + end + + + -- io.write("\x1b[38;2;" .. + -- r1 .. ";" .. g1 .. ";" .. b1 .. "m" .. "\x1b[48;2;" .. r2 .. ";" .. g2 .. ";" .. b2 .. "m" .. "\xDF") + end + end + + io.write(table.concat(out)) + + io.write(string.format("\n\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m%d FPS", fps)) +end + +local file = io.open("data.txt", "r") + +local function Input() + return file:read("*n") +end + +-- file:close() -- Close the file + +-- return number + +local function Main() + os.execute(" ") + + local meta = {} + + meta[1] = 175 + meta[2] = 120 + + + meta[3] = meta[1] * meta[2] + + local depthbuffer = {} + local colorbuffer = {} + + local background = {} + background[1] = 0.3 + background[2] = 0.3 + background[3] = 0.3 + + ClearBuffers(meta, colorbuffer, depthbuffer, background, 100.0) + + local lightdir = {} + lightdir[1] = .3 + lightdir[2] = 1 + lightdir[3] = .4 + Normalize(lightdir) + + local view = {} + local proj = {} + local mvp = {} + + local campos = {} + local targetpos = {} + local upvector = {} + + targetpos[1] = 0 + targetpos[2] = 0 + targetpos[3] = 0 + + upvector[1] = 0 + upvector[2] = 1 + upvector[3] = 0 + local aspectratio + aspectratio = 1.46 + + + Perspective(proj, DegToRad(90.0) / aspectratio, aspectratio, 0.1, 50.0) + + local p0 = {} + local p1 = {} + local p2 = {} + + local c0 = {} + local c1 = {} + local c2 = {} + + local n0 = {} + local n1 = {} + local n2 = {} + + local datasize, i, vertices + datasize = Input() + vertices = Input() + + local data = {} + for i = 1, datasize do + data[i] = Input() + end + + print("model rdy") + + meta[4] = Input() + meta[5] = Input() + + local tp + tp = meta[4] * meta[5] * 3 + + local texture = {} + + for i = 0, tp - 1 do + texture[1 + i] = Input() + end + + print("textura rdy") + + local temp + + local i0, i1, i2 + + local camangle + camangle = 0 + + local frames = 0 + local lastframereset = 0 + + while true do + frames = frames + 1 + + local time = os.clock() * 1000 + + if time > lastframereset + 1000 then + fps = frames + frames = 0 + lastframereset = time + end + + + ClearBuffers(meta, colorbuffer, depthbuffer, background, 100.0) + + local camdis + camdis = 7.0 + + campos[1] = math.sin(camangle) * camdis + campos[2] = 0.8 * camdis + campos[3] = math.cos(camangle) * camdis + + + camangle = camangle + math.pi / 50 + + LookAt(view, campos, targetpos, upvector) + MatMultMat(mvp, proj, view) + + for i = 0, vertices - 3, 3 do + i0 = i * 8 + i1 = (i + 1) * 8 + i2 = (i + 2) * 8 + + p0[1] = data[1 + i0] + p0[2] = data[1 + i0 + 1] + p0[3] = data[1 + i0 + 2] + + p1[1] = data[1 + i1] + p1[2] = data[1 + i1 + 1] + p1[3] = data[1 + i1 + 2] + + p2[1] = data[1 + i2] + p2[2] = data[1 + i2 + 1] + p2[3] = data[1 + i2 + 2] + + + c0[1] = data[1 + i0 + 3] + c0[2] = data[1 + i0 + 4] + + c1[1] = data[1 + i1 + 3] + c1[2] = data[1 + i1 + 4] + + c2[1] = data[1 + i2 + 3] + c2[2] = data[1 + i2 + 4] + + n0[1] = data[1 + i0 + 5] + n0[2] = data[1 + i0 + 6] + n0[3] = data[1 + i0 + 7] + + n1[1] = data[1 + i1 + 5] + n1[2] = data[1 + i1 + 6] + n1[3] = data[1 + i1 + 7] + + n2[1] = data[1 + i2 + 5] + n2[2] = data[1 + i2 + 6] + n2[3] = data[1 + i2 + 7] + + DrawTriangle(meta, colorbuffer, depthbuffer, p0, p1, p2, texture, c0, c1, c2, mvp, lightdir, n0, n1, n2) + end + + + OutputVT100(meta, colorbuffer) + end +end + +Main() diff --git a/res/screen-dosbox.png b/res/screen-dosbox.png new file mode 100644 index 0000000000000000000000000000000000000000..4a7ddc641e038a32e2edc89a5d7ae784b5db0a56 GIT binary patch literal 30730 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 literal 0 HcmV?d00001