/* * JoeyLib 3D * Copyright (C) 2019 Scott Duensing * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. */ // The code in this file is heavily influenced by // "Black Art of 3D Game Programming" by Andre LaMothe // Waite Group Press - ISBN 1-57169-004-2 // https://archive.org/details/BlackArt3DEBook #include #include #include #include #include "stretchy_buffer.h" #include "j3d.h" #ifdef JOEY_IIGS segment "j3d"; #define M_PI 3.1415926 #define fabsf fabs #else #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpadded" #endif // Module global data static float sin_table[361]; static float cos_table[361]; static int clipMinX = 0; static int clipMinY = 0; static int clipMaxX = 319; static int clipMaxY = 199; static int viewDistance = 250; #define ASPECT_RATIO (float)0.8 #define INVERSE_ASPECT_RATIO (float)1.25 #define HALF_SCREEN_WIDTH 160 #define HALF_SCREEN_HEIGHT 100 void _j3DrawWireframePair(j3ObjectT *o, int v1, int v2) { float x1; float y1; float z1; float x2; float y2; float z2; int ix1; int iy1; int ix2; int iy2; j3VertexT v; v = o->verticies[v1].camera; x1 = v.x; y1 = v.y; z1 = v.z; v = o->verticies[v2].camera; x2 = v.x; y2 = v.y; z2 = v.z; x1 = HALF_SCREEN_WIDTH + x1 * viewDistance / z1; y1 = HALF_SCREEN_HEIGHT - ASPECT_RATIO * y1 * viewDistance / z1; x2 = HALF_SCREEN_WIDTH + x2 * viewDistance / z2; y2 = HALF_SCREEN_HEIGHT - ASPECT_RATIO * y2 * viewDistance / z2; ix1 = (int)x1; iy1 = (int)y1; ix2 = (int)x2; iy2 = (int)y2; if (_j3UtilClipLine(&ix1, &iy1, &ix2, &iy2)) { jlDrawLine((jint16)ix1, (jint16)iy1, (jint16)ix2, (jint16)iy2); } } void _j3DrawWireframe(j3ObjectT *o) { int t; // Current triangle for (t=0; ttriangleCount; t++) { _j3DrawWireframePair(o, o->triangles[t].index[0], o->triangles[t].index[1]); _j3DrawWireframePair(o, o->triangles[t].index[1], o->triangles[t].index[2]); _j3DrawWireframePair(o, o->triangles[t].index[2], o->triangles[t].index[0]); } } void j3MathMatrix4x4Identity(j3Matrix4x4T result) { result[0][0] = 1; result[0][1] = 0; result[0][2] = 0; result[0][3] = 0; result[1][0] = 0; result[1][1] = 1; result[1][2] = 0; result[1][3] = 0; result[2][0] = 0; result[2][1] = 0; result[2][2] = 1; result[2][3] = 0; result[3][0] = 0; result[3][1] = 0; result[3][2] = 0; result[3][3] = 1; } void j3MathMatrix4x4Mult(j3Matrix4x4T a, j3Matrix4x4T b, j3Matrix4x4T result) { result[0][0] = a[0][0] * b[0][0] + a[0][1] * b[1][0] + a[0][2] * b[2][0]; result[0][1] = a[0][0] * b[0][1] + a[0][1] * b[1][1] + a[0][2] * b[2][1]; result[0][2] = a[0][0] * b[0][2] + a[0][1] * b[1][2] + a[0][2] * b[2][2]; result[0][3] = 0; result[1][0] = a[1][0] * b[0][0] + a[1][1] * b[1][0] + a[1][2] * b[2][0]; result[1][1] = a[1][0] * b[0][1] + a[1][1] * b[1][1] + a[1][2] * b[2][1]; result[1][2] = a[1][0] * b[0][2] + a[1][1] * b[1][2] + a[1][2] * b[2][2]; result[1][3] = 0; result[2][0] = a[2][0] * b[0][0] + a[2][1] * b[1][0] + a[2][2] * b[2][0]; result[2][1] = a[2][0] * b[0][1] + a[2][1] * b[1][1] + a[2][2] * b[2][1]; result[2][2] = a[2][0] * b[0][2] + a[2][1] * b[1][2] + a[2][2] * b[2][2]; result[2][3] = 0; result[3][0] = a[3][0] * b[0][0] + a[3][1] * b[1][0] + a[3][2] * b[2][0] + b[3][0]; result[3][1] = a[3][0] * b[0][1] + a[3][1] * b[1][1] + a[3][2] * b[2][1] + b[3][1]; result[3][2] = a[3][0] * b[0][2] + a[3][1] * b[1][2] + a[3][2] * b[2][2] + b[3][2]; result[3][3] = 1; } void _j3ObjectReset(j3ObjectT *o) { o->position.x = 0; o->position.y = 0; o->position.z = 0; o->rotation.x = 0; o->rotation.y = 0; o->rotation.z = 0; o->scale.x = 1; o->scale.y = 1; o->scale.z = 1; } void _j3ObjectUpdate(j3ObjectT *o) { int x; int y; int z; int i; int axis = 0; j3Matrix4x4T rotateX; j3Matrix4x4T rotateY; j3Matrix4x4T rotateZ; j3Matrix4x4T final; j3Matrix4x4T temp; // === ROTATION === x = (int)o->rotation.x; y = (int)o->rotation.y; z = (int)o->rotation.z; j3MathMatrix4x4Identity(final); // What angles are we rotating on? By knowing this we can optimize some. if (x) axis += 4; if (y) axis += 2; if (z) axis += 1; switch (axis) { case 1: // Final matrix = z final[0][0] = cos_table[z]; final[0][1] = sin_table[z]; final[1][0] = -sin_table[z]; final[1][1] = cos_table[z]; for (i=0; ivertexCount; i++) { o->verticies[i].world.x = o->verticies[i].local.x * final[0][0] + o->verticies[i].local.y * final[1][0]; o->verticies[i].world.y = o->verticies[i].local.x * final[0][1] + o->verticies[i].local.y * final[1][1]; o->verticies[i].world.z = o->verticies[i].local.z; } break; case 2: // Final matrix = y final[0][0] = cos_table[y]; final[0][2] = -sin_table[y]; final[2][0] = sin_table[y]; final[2][2] = cos_table[y]; for (i=0; ivertexCount; i++) { o->verticies[i].world.x = o->verticies[i].local.x * final[0][0] + o->verticies[i].local.z * final[2][0]; o->verticies[i].world.y = o->verticies[i].local.y; o->verticies[i].world.z = o->verticies[i].local.x * final[0][2] + o->verticies[i].local.z * final[2][2]; } break; case 3: // Final matrix = y * z final[0][0] = cos_table[y] * cos_table[z]; final[0][1] = cos_table[y] * sin_table[z]; final[0][2] = -sin_table[y]; final[1][0] = -sin_table[z]; final[1][1] = cos_table[z]; final[2][0] = sin_table[y] * cos_table[z]; final[2][1] = sin_table[y] * sin_table[z]; final[2][2] = cos_table[y]; for (i=0; ivertexCount; i++) { o->verticies[i].world.x = o->verticies[i].local.x * final[0][0] + o->verticies[i].local.y * final[1][0] + o->verticies[i].local.z * final[2][0]; o->verticies[i].world.y = o->verticies[i].local.x * final[0][1] + o->verticies[i].local.y * final[1][1] + o->verticies[i].local.z * final[2][1]; o->verticies[i].world.z = o->verticies[i].local.x * final[0][2] + o->verticies[i].local.z * final[2][2]; } break; case 4: // Final matrix = x final[1][1] = cos_table[x]; final[1][2] = sin_table[x]; final[2][1] = -sin_table[x]; final[2][2] = cos_table[x]; for (i=0; ivertexCount; i++) { o->verticies[i].world.x = o->verticies[i].local.x; o->verticies[i].world.y = o->verticies[i].local.y * final[1][1] + o->verticies[i].local.z * final[2][1]; o->verticies[i].world.z = o->verticies[i].local.y * final[1][2] + o->verticies[i].local.z * final[2][2]; } break; case 5: // Final matrix = x * z final[0][0] = cos_table[z]; final[0][1] = sin_table[z]; final[1][0] = -cos_table[x]*sin_table[z]; final[1][1] = cos_table[x]*cos_table[z]; final[1][2] = sin_table[x]; final[2][0] = sin_table[x]*sin_table[z]; final[2][1] = -sin_table[x]*cos_table[z]; final[2][2] = cos_table[x]; for (i=0; ivertexCount; i++) { o->verticies[i].world.x = o->verticies[i].local.x * final[0][0] + o->verticies[i].local.y * final[1][0] + o->verticies[i].local.z * final[2][0]; o->verticies[i].world.y = o->verticies[i].local.x * final[0][1] + o->verticies[i].local.y * final[1][1] + o->verticies[i].local.z * final[2][1]; o->verticies[i].world.z = o->verticies[i].local.y * final[1][2] + o->verticies[i].local.z * final[2][2]; } break; case 6: // Final matrix = x * y final[0][0] = cos_table[y]; final[0][2] = -sin_table[y]; final[1][0] = sin_table[x] * sin_table[y]; final[1][1] = cos_table[x]; final[1][2] = sin_table[x] * cos_table[y]; final[2][0] = cos_table[x] * sin_table[y]; final[2][1] = -sin_table[x]; final[2][2] = cos_table[x] * cos_table[y]; for (i=0; ivertexCount; i++) { o->verticies[i].world.x = o->verticies[i].local.x * final[0][0] + o->verticies[i].local.y * final[1][0] + o->verticies[i].local.z * final[2][0]; o->verticies[i].world.y = o->verticies[i].local.y * final[1][1] + o->verticies[i].local.z * final[2][1]; o->verticies[i].world.z = o->verticies[i].local.x * final[0][2] + o->verticies[i].local.y * final[1][2] + o->verticies[i].local.z * final[2][2]; } break; case 7: // Final matrix = x * y * z j3MathMatrix4x4Identity(rotateX); rotateX[1][1] = cos_table[x]; rotateX[1][2] = sin_table[x]; rotateX[2][1] = -sin_table[x]; rotateX[2][2] = cos_table[x]; j3MathMatrix4x4Identity(rotateY); rotateY[0][0] = cos_table[y]; rotateY[0][2] = -sin_table[y]; rotateY[2][0] = sin_table[y]; rotateY[2][2] = cos_table[y]; j3MathMatrix4x4Identity(rotateZ); rotateZ[0][0] = cos_table[z]; rotateZ[0][1] = sin_table[z]; rotateZ[1][0] = -sin_table[z]; rotateZ[1][1] = cos_table[z]; j3MathMatrix4x4Mult(rotateX, rotateZ, temp); j3MathMatrix4x4Mult(temp, rotateZ, final); for (i=0; ivertexCount; i++) { o->verticies[i].world.x = o->verticies[i].local.x * final[0][0] + o->verticies[i].local.y * final[1][0] + o->verticies[i].local.z * final[2][0]; o->verticies[i].world.y = o->verticies[i].local.x * final[0][1] + o->verticies[i].local.y * final[1][1] + o->verticies[i].local.z * final[2][1]; o->verticies[i].world.z = o->verticies[i].local.x * final[0][2] + o->verticies[i].local.y * final[1][2] + o->verticies[i].local.z * final[2][2]; } break; default: break; } // === SCALE & TRANSLATION === for (i=0; ivertexCount; i++) { o->verticies[i].world.x = o->verticies[i].world.x * o->scale.x + o->position.x; o->verticies[i].world.y = o->verticies[i].world.y * o->scale.y + o->position.y; o->verticies[i].world.z = o->verticies[i].world.z * o->scale.z + o->position.z; } // === CAMERA SPACE === //***TODO*** Move this? for (i=0; ivertexCount; i++) { o->verticies[i].camera = o->verticies[i].world; } } bool _j3UtilClipLine(int *x1, int *y1, int *x2, int *y2) { int xi; int yi; float dx; float dy; bool point1 = false; // End points visible? bool point2 = false; bool rightEdge = false; // Which edges are the endpoints beyond? bool leftEdge = false; bool topEdge = false; bool bottomEdge = false; bool success = false; // Did we successfully clip this line? // Is the line completely visible? point1 = ((*x1 >= clipMinX) && (*x1 <= clipMaxX) && (*y1 >= clipMinY) && (*y1 <= clipMaxY)); point2 = ((*x2 >= clipMinX) && (*x2 <= clipMaxX) && (*y2 >= clipMinY) && (*y2 <= clipMaxY)); if (point1 && point2) { return(true); } // Is the line is completely invisible? if (!point1 && !point2) { // Test to see if each endpoint is on the same side of one of // the bounding planes created by each clipping region boundary if (((*x1clipMaxX) && (*x2>clipMaxX)) || // right ((*y1clipMaxY) && (*y2>clipMaxY))) { // below // No need to draw line return(false); } // if we got here we have the special case where the line cuts into and // out of the clipping region //return(false); } // Either endpoint is in clipping region if (point1 || (!point1 && !point2)) { // Find deltas dx = *x2 - *x1; dy = *y2 - *y1; // Find which boundary line needs to be clipped against if (*x2 > clipMaxX) { // Flag right edge rightEdge = true; // Find intersection with right edge if (fabsf(dx) > FLT_EPSILON) { // dx != 0 yi = (int)((float)0.5 + (dy / dx) * (clipMaxX - *x1) + *y1); } else { yi = -1; // Invalidate intersection } } else if (*x2 < clipMinX) { // Flag left edge leftEdge = true; // Find intersection with left edge if (fabsf(dx) > FLT_EPSILON) { // dx != 0 yi = (int)((float)0.5 + (dy / dx) * (clipMinX - *x1) + *y1); } else { yi = -1; // Invalidate intersection } } if (*y2 > clipMaxY) { // Flag bottom edge bottomEdge = true; // Find intersection with right edge if (fabsf(dy) > FLT_EPSILON) { // dy != 0 xi = (int)((float)0.5 + (dx / dy) * (clipMaxY - *y1) + *x1); } else { xi = -1; // Invalidate inntersection } } else if (*y2 < clipMinY) { // Flag top edge topEdge = true; // Find intersection with top edge if (fabsf(dy) > FLT_EPSILON) { // dy != 0 xi = (int)((float)0.5 + (dx / dy) * (clipMinY - *y1) + *x1); } else { xi = -1; // Invalidate intersection } } // We know where the line passed through // FInd which edge is the proper intersection if (rightEdge && (yi >= clipMinY && yi <= clipMaxY)) { *x2 = clipMaxX; *y2 = yi; success = true; } else if (leftEdge && (yi >= clipMinY && yi <= clipMaxY)) { *x2 = clipMinX; *y2 = yi; success = true; } if (bottomEdge && (xi >= clipMinX && xi <= clipMaxX)) { *x2 = xi; *y2 = clipMaxY; success = true; } else if (topEdge && (xi >= clipMinX && xi <= clipMaxX)) { *x2 = xi; *y2 = clipMinY; success = true; } } // Reset edge flags rightEdge = false; leftEdge = false; topEdge = false; bottomEdge = false; // Test second endpoint if (point2 || (!point1 && !point2)) { // Find deltas dx = *x1 - *x2; dy = *y1 - *y2; // Find which boundary line needs to be clipped against if (*x1 > clipMaxX) { // Flag right edge rightEdge = true; // Find intersection with right edge if (fabsf(dx) > FLT_EPSILON) { // dx != 0 yi = (int)((float)0.5 + (dy / dx) * (clipMaxX - *x2) + *y2); } else { yi = -1; // Invalidate inntersection } } else if (*x1 < clipMinX) { // Flag left edge leftEdge = true; // Find intersection with left edge if (fabsf(dx) > FLT_EPSILON) { // dx != 0 yi = (int)((float)0.5 + (dy / dx) * (clipMinX - *x2) + *y2); } else { yi = -1; // Invalidate intersection } } if (*y1 > clipMaxY) { // Flag bottom edge bottomEdge = true; // Find intersection with right edge if (fabsf(dy) > FLT_EPSILON) { // dy != 0 xi = (int)((float)0.5 + (dx / dy) * (clipMaxY - *y2) + *x2); } else { xi = -1; // invalidate inntersection } } else if (*y1 < clipMinY) { // Flag top edge topEdge = true; // Find intersection with top edge if (fabsf(dy) > FLT_EPSILON) { // dy != 0 xi = (int)((float)0.5 + (dx / dy) * (clipMinY - *y2) + *x2); } else { xi = -1; // invalidate inntersection } } // We know where the line passed through // Find which edge is the proper intersection if (rightEdge && (yi >= clipMinY && yi <= clipMaxY)) { *x1 = clipMaxX; *y1 = yi; success = true; } else if (leftEdge && (yi >= clipMinY && yi <= clipMaxY)) { *x1 = clipMinX; *y1 = yi; success = true; } if (bottomEdge && (xi >= clipMinX && xi <= clipMaxX)) { *x1 = xi; *y1 = clipMaxY; success = true; } else if (topEdge && (xi >= clipMinX && xi <= clipMaxX)) { *x1 = xi; *y1 = clipMinY; success = true; } } return(success); } void j3UtilShutdown(void) { // Nothing yet } void j3UtilStartup(void) { int euler; float radian; // Build look-up tables for speed later for (euler=0; euler<361; euler++) { radian = ((float)M_PI * (float)euler / (float)180); sin_table[euler] = (float)sin((double)radian); cos_table[euler] = (float)cos((double)radian); } } void _j3WorldFree(j3WorldT **world) { int x; for (x=0; xobjects); x++) { sb_free((*world)->objects[x].triangles); sb_free((*world)->objects[x].verticies); } sb_free((*world)->objects); jlFree(*world); } bool _j3WorldLoad(j3WorldT **world, char *file) { int r; int x; char token[1024]; char *c; FILE *in; j3CoordinatesT v; j3ObjectT o; j3TriangleT t; in = fopen(jlUtilMakePathname(file, "obj"), "rt"); // Did we find the file? if (in == NULL) { // Nope. return false; } // Do we have a world? if (*world != NULL) { // Free this one j3WorldFree(world); } // Allocate world object *world = (j3WorldT *)jlMalloc(sizeof(j3WorldT)); // Initialize world object & create an initial object to read data into (*world)->objects = NULL; o.vertexCount = 0; o.triangleCount = 0; o.verticies = NULL; o.triangles = NULL; _j3ObjectReset(&o); while (true) { // Read next token r = fscanf(in, "%s", token); // End of file? if (r == EOF) { break; } // Vertex? if (strcmp(token, "v" ) == 0) { r = fscanf(in, "%f %f %f\n", &v.local.x, &v.local.y, &v.local.z); sb_push(o.verticies, v); o.vertexCount++; } // Face? if (strcmp(token, "f" ) == 0) { for (x=0; x<3; x++) { // Fetch 'x'th vertex index r = fscanf(in, "%s", token); c = strstr(token, "/"); if (c) c[0] = 0; r = atoi(token); t.index[x] = r - 1; } fscanf(in, "\n"); sb_push(o.triangles, t); o.triangleCount++; } } //***TODO*** support multiple objects - not sure how I want to do this yet sb_push((*world)->objects, o); // Finished! Clean up. fclose(in); return true; } #ifndef JOEY_IIGS #pragma GCC diagnostic pop #endif