/* * 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 //***TODO*** // Not all faces are rendering until they rotate out and back in // Some fields like objectCount are juint16 but should be jint16 #include #include #include #include "j3d.h" #include "j3dtbls.h" #ifdef JOEY_IIGS segment "j3d"; #define M_PI 3.1415926 #define fabsf fabs #define FLT_EPSILON 1.19209290E-07F #else #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpadded" #endif typedef int (*j3qsortCastT)(const void *, const void *); // Private Prototypes void _j3DrawTriangleBottom(jint16 x1, jint16 y1, jint16 x2,jint16 y2, jint16 x3, jint16 y3); void _j3DrawTriangleTop(jint16 x1, jint16 y1, jint16 x2,jint16 y2, jint16 x3, jint16 y3); void _j3ObjectUpdateNormalLength(j3ObjectT *object, juint16 triangle); void _j3ObjectUpdate(j3WorldT *w, jint16 index); int _j3PolyCompare(j3PolyListT *arg1, j3PolyListT *arg2); bool _j3UtilClipLine(jint16 *x1, jint16 *y1, jint16 *x2, jint16 *y2); // Module global data static jint16 _j3VarClipMinX = 0; static jint16 _j3VarClipMinY = 0; static jint16 _j3VarClipMinZ = 100; static jint16 _j3VarClipMaxX = 319; static jint16 _j3VarClipMaxY = 199; static jint16 _j3VarClipMaxZ = 3000; static jint16 _j3VarViewDistance = 200; static jreal _j3VarAmbientLight = 6; static j3Matrix4x4T _j3VarCameraMatrix; static j3VertexT _j3VarCameraLocation; static j3FacingT _j3VarCameraAngle; static j3DirtynessT _j3VarCameraHowDirty = DIRTYNESS_ALL; static j3VertexT _j3VarSunLocation; #define ASPECT_RATIO (jreal)0.8 #define INVERSE_ASPECT_RATIO (jreal)1.25 #define HALF_SCREEN_WIDTH 160 #define HALF_SCREEN_HEIGHT 100 void j3CameraMove(jreal x, jreal y, jreal z) { _j3VarCameraLocation.x += x; _j3VarCameraLocation.y += y; _j3VarCameraLocation.z += z; _j3VarCameraHowDirty |= DIRTYNESS_TRANSLATE; } void j3CameraMoveTo(jreal x, jreal y, jreal z) { _j3VarCameraLocation.x = x; _j3VarCameraLocation.y = y; _j3VarCameraLocation.z = z; _j3VarCameraHowDirty |= DIRTYNESS_TRANSLATE; } void j3CameraRotate(jreal x, jreal y, jreal z) { _j3VarCameraAngle.x += x; _j3VarCameraAngle.y += y; _j3VarCameraAngle.z += z; _j3VarCameraHowDirty |= DIRTYNESS_ROTATE; } void j3CameraRotateTo(jreal x, jreal y, jreal z) { _j3VarCameraAngle.x = (jint16)x; _j3VarCameraAngle.y = (jint16)y; _j3VarCameraAngle.z = (jint16)z; _j3VarCameraHowDirty |= DIRTYNESS_ROTATE; } void j3CameraSetClippingBounds(jint16 x1, jint16 y1, jint16 x2, jint16 y2) { _j3VarClipMinX = x1; j3MathCheckBounds(_j3VarClipMinX, 0, 319); _j3VarClipMinY = y1; j3MathCheckBounds(_j3VarClipMinY, 0, 199); _j3VarClipMaxX = x2; j3MathCheckBounds(_j3VarClipMaxX, 0, 319); _j3VarClipMaxY = y2; j3MathCheckBounds(_j3VarClipMaxY, 0, 199); } void j3DrawTriangle2D(jint16 x1, jint16 y1, jint16 x2,jint16 y2, jint16 x3, jint16 y3, jint16 color) { jint16 tempX; jint16 tempY; jint16 newX; // Horizontal or vertical lines? if ((x1 == x2 && x2 == x3) || (y1 == y2 && y2 == y3)) { return; } // Sort points in ascending Y order if (y2 < y1) { tempX = x2; tempY = y2; x2 = x1; y2 = y1; x1 = tempX; y1 = tempY; } // p1 and p2 are now in order if (y3 < y1) { tempX = x3; tempY = y3; x3 = x1; y3 = y1; x1 = tempX; y1 = tempY; } // Finally check y3 against y2 if (y3 < y2) { tempX = x3; tempY = y3; x3 = x2; y3 = y2; x2 = tempX; y2 = tempY; } // end if // Trivial rejection tests if (y3 < _j3VarClipMinY || y1 > _j3VarClipMaxY || (x1 < _j3VarClipMinX && x2 < _j3VarClipMinX && x3 < _j3VarClipMinX) || (x1 > _j3VarClipMaxX && x2 > _j3VarClipMaxX && x3 > _j3VarClipMaxX)) { return; } jlDrawColorSet((byte)color); // Is top of triangle flat? if (y1 == y2) { _j3DrawTriangleTop(x1, y1, x2, y2, x3, y3); } else if (y2 == y3) { _j3DrawTriangleBottom(x1, y1, x2, y2, x3, y3); } else { // General triangle that's needs to be broken up along long edge newX = x1 + (jint16)((jreal)(y2 - y1) * (jreal)(x3 - x1) / (jreal)(y3 - y1)); // Draw each sub-triangle _j3DrawTriangleBottom(x1, y1, newX, y2, x2, y2); _j3DrawTriangleTop(x2, y2, newX, y2, x3, y3); } } void _j3DrawTriangleBottom(jint16 x1, jint16 y1, jint16 x2,jint16 y2, jint16 x3, jint16 y3) { jreal dxRight; jreal dxLeft; jreal xs; jreal xe; jreal height; jint16 tempX; jint16 tempY; jint16 right; jint16 left; (void)y2; // Test order of x1 and x2 if (x3 < x2) { tempX = x2; x2 = x3; x3 = tempX; } // Compute deltas height = y3 - y1; dxLeft = (x2 - x1) / height; dxRight = (x3 - x1) / height; // Set starting points xs = (jreal)x1; xe = (jreal)x1 + (jreal)0.5; // Perform y clipping if (y1 < _j3VarClipMinY) { // Compute new xs and ys xs = xs + dxLeft * (jreal)(-y1 + _j3VarClipMinY); xe = xe + dxRight * (jreal)(-y1 + _j3VarClipMinY); // Reset y1 y1 = _j3VarClipMinY; } if (y3 > _j3VarClipMaxY) { y3 = _j3VarClipMaxY; } // Test if x clipping is needed if (x1 >= _j3VarClipMinX && x1 <= _j3VarClipMaxX && x2 >= _j3VarClipMinX && x2 <= _j3VarClipMaxX && x3 >= _j3VarClipMinX && x3 <= _j3VarClipMaxX) { // Draw the triangle for (tempY=y1; tempY<=y3; tempY++) { jlDrawLine((jint16)xs, tempY, (jint16)xe, tempY); // Adjust starting point and ending point xs += dxLeft; xe += dxRight; } } else { // Clip x axis with slower version for (tempY=y1; tempY<=y3; tempY++) { // Do x clip left = (jint16)xs; right = (jint16)xe; // Adjust starting point and ending point xs += dxLeft; xe += dxRight; // Clip line if (left < _j3VarClipMinX) { left = _j3VarClipMinX; if (right < _j3VarClipMinX) { continue; } } if (right > _j3VarClipMaxX) { right = _j3VarClipMaxX; if (left > _j3VarClipMaxX) { continue; } } // Draw jlDrawLine(left, tempY, right, tempY); } } } void _j3DrawTriangleTop(jint16 x1, jint16 y1, jint16 x2,jint16 y2, jint16 x3, jint16 y3) { jreal dxRight; jreal dxLeft; jreal xs; jreal xe; jreal height; jint16 tempX; jint16 tempY; jint16 right; jint16 left; (void)y2; // Test order of x1 and x2 if (x2 < x1) { tempX = x2; x2 = x1; x1 = tempX; } // Compute deltas height = y3 - y1; dxLeft = (x3 - x1) / height; dxRight = (x3 - x2) / height; // Set starting points xs = (jreal)x1; xe = (jreal)x2 + (jreal)0.5; // Perform y clipping if (y1 < _j3VarClipMinY) { // Compute new xs and ys xs = xs + dxLeft * (jreal)(-y1 + _j3VarClipMinY); xe = xe + dxRight * (jreal)(-y1 + _j3VarClipMinY); // Reset y1 y1 = _j3VarClipMinY; } if (y3 > _j3VarClipMaxY) { y3=_j3VarClipMaxY; } // Test if x clipping is needed if (x1 >= _j3VarClipMinX && x1 <= _j3VarClipMaxX && x2 >= _j3VarClipMinX && x2 <= _j3VarClipMaxX && x3 >= _j3VarClipMinX && x3 <= _j3VarClipMaxX) { // Draw the triangle for (tempY=y1; tempY<=y3; tempY++) { jlDrawLine((jint16)xs, tempY, (jint16)xe, tempY); // Adjust starting point and ending point xs += dxLeft; xe += dxRight; } } else { // Clip x axis for (tempY=y1; tempY<=y3; tempY++) { // Do x clip left = (jint16)xs; right = (jint16)xe; // Adjust starting point and ending point xs += dxLeft; xe += dxRight; // Clip line if (left < _j3VarClipMinX) { left = _j3VarClipMinX; if (right < _j3VarClipMinX) { continue; } } if (right > _j3VarClipMaxX) { right = _j3VarClipMaxX; if (left > _j3VarClipMaxX) { continue; } } // Draw jlDrawLine((jint16)left, tempY, (jint16)right, tempY); } } } void j3DrawWorld(j3WorldT *w) { jint16 i; juint16 vertex1; juint16 vertex2; juint16 vertex3; jreal x1; jreal y1; jreal z1; jreal x2; jreal y2; jreal z2; jreal x3; jreal y3; jreal z3; j3ObjectT *o; j3PolyListT *p; w->polygonCount = 0; // Update all our math for (i=0; iobjectCount; i++) { _j3ObjectUpdate(w, i); } // Render it for (i=0; ipolygonCount; i++) { // Dereference p = &w->polygons[i]; o = p->object; vertex1 = o->triangles[p->triangleNumber].index[0]; vertex2 = o->triangles[p->triangleNumber].index[1]; vertex3 = o->triangles[p->triangleNumber].index[2]; z1 = o->vertices[vertex1].camera.z; z2 = o->vertices[vertex2].camera.z; z3 = o->vertices[vertex3].camera.z; // Perform z clipping if ((z1 < _j3VarClipMinZ && z2 < _j3VarClipMinZ && z3 < _j3VarClipMinZ) || (z1 > _j3VarClipMaxZ && z2 > _j3VarClipMaxZ && z3 > _j3VarClipMaxZ)) { continue; } // Extract points of triangle x1 = o->vertices[vertex1].camera.x; y1 = o->vertices[vertex1].camera.y; x2 = o->vertices[vertex2].camera.x; y2 = o->vertices[vertex2].camera.y; x3 = o->vertices[vertex3].camera.x; y3 = o->vertices[vertex3].camera.y; // Screen position of points x1 = HALF_SCREEN_WIDTH + x1 * _j3VarViewDistance / z1; y1 = HALF_SCREEN_HEIGHT - ASPECT_RATIO * y1 * _j3VarViewDistance / z1; x2 = HALF_SCREEN_WIDTH + x2 * _j3VarViewDistance / z2; y2 = HALF_SCREEN_HEIGHT - ASPECT_RATIO * y2 * _j3VarViewDistance / z2; x3 = HALF_SCREEN_WIDTH + x3 * _j3VarViewDistance / z3; y3 = HALF_SCREEN_HEIGHT - ASPECT_RATIO * y3 * _j3VarViewDistance / z3; j3DrawTriangle2D((jint16)x1, (jint16)y1, (jint16)x2, (jint16)y2, (jint16)x3, (jint16)y3, o->triangles[p->triangleNumber].shade); } } void j3MathCrossProduct3D(j3Vector3DT *u, j3Vector3DT *v, j3Vector3DT *normal) { // Compute the cross product between two vectors normal->x = (u->y * v->z - u->z * v->y); normal->y = -(u->x * v->z - u->z * v->x); normal->z = (u->x * v->y - u->y * v->x); } jreal j3MathDotProduct3D(j3Vector3DT *u, j3Vector3DT *v) { // Compute the dot product of two vectors return( (u->x * v->x) + (u->y * v->y) + (u->z * v->z)); } 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 j3MathMakeVector3D(j3VertexT *init, j3VertexT *term, j3Vector3DT *result) { // Create a vector from two points in 3D space result->x = term->x - init->x; result->y = term->y - init->y; result->z = term->z - init->z; } jreal j3MathVectorMagnatude3D(j3Vector3DT *v) { // Compute the magnitude of a vector return((jreal)sqrt((double)(v->x * v->x + v->y * v->y + v->z * v->z))); } void _j3ObjectMove(j3ObjectT *o, jreal x, jreal y, jreal z) { o->position.x += x; o->position.y += y; o->position.z += z; o->howDirty |= DIRTYNESS_TRANSLATE; } void _j3ObjectMoveTo(j3ObjectT *o, jreal x, jreal y, jreal z) { o->position.x = x; o->position.y = y; o->position.z = z; o->howDirty |= DIRTYNESS_TRANSLATE; } 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; o->howDirty = DIRTYNESS_ALL; } void _j3ObjectRotate(j3ObjectT *o, jreal x, jreal y, jreal z) { o->rotation.x += x; j3MathWrapBounds(o->rotation.x, 0, 360); o->rotation.y += y; j3MathWrapBounds(o->rotation.y, 0, 360); o->rotation.z += z; j3MathWrapBounds(o->rotation.z, 0, 360); o->howDirty |= DIRTYNESS_ROTATE; } void _j3ObjectRotateTo(j3ObjectT *o, jreal x, jreal y, jreal z) { o->rotation.x = x; j3MathWrapBounds(o->rotation.x, 0, 360); o->rotation.y = y; j3MathWrapBounds(o->rotation.y, 0, 360); o->rotation.z = z; j3MathWrapBounds(o->rotation.z, 0, 360); o->howDirty |= DIRTYNESS_ROTATE; } void _j3ObjectScale(j3ObjectT *o, jreal x, jreal y, jreal z) { o->scale.x += x; o->scale.y += y; o->scale.z += z; o->howDirty |= DIRTYNESS_SCALE; } void _j3ObjectScaleTo(j3ObjectT *o, jreal x, jreal y, jreal z) { o->scale.x = x; o->scale.y = y; o->scale.z = z; o->howDirty |= DIRTYNESS_SCALE; } void _j3ObjectUpdate(j3WorldT *w, jint16 index) { jint16 x; jint16 y; jint16 z; jint16 i; juint16 vertex0; juint16 vertex1; juint16 vertex2; jreal dot; jreal intensity; j3Vector3DT u; j3Vector3DT v; j3Vector3DT normal; j3Vector3DT sight; byte axis = 0; j3Matrix4x4T rotateX; j3Matrix4x4T rotateY; j3Matrix4x4T rotateZ; j3Matrix4x4T final; j3Matrix4x4T temp; j3Matrix4x4T result1; j3Matrix4x4T result2; j3Matrix4x4T translate; j3ObjectT *o; o = &w->objects[index]; // === SCALE === if (o->howDirty & DIRTYNESS_SCALE) { for (i=0; ivertexCount; i++) { o->vertices[i].scaled.x = o->vertices[i].original.x * o->scale.x; o->vertices[i].scaled.y = o->vertices[i].original.y * o->scale.y; o->vertices[i].scaled.z = o->vertices[i].original.z * o->scale.z; } } // === ROTATION === if (o->howDirty & DIRTYNESS_ROTATE) { x = (jint16)o->rotation.x; y = (jint16)o->rotation.y; z = (jint16)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] = cosTable[z]; final[0][1] = sinTable[z]; final[1][0] = -sinTable[z]; final[1][1] = cosTable[z]; for (i=0; ivertexCount; i++) { o->vertices[i].rotated.x = o->vertices[i].scaled.x * final[0][0] + o->vertices[i].scaled.y * final[1][0]; o->vertices[i].rotated.y = o->vertices[i].scaled.x * final[0][1] + o->vertices[i].scaled.y * final[1][1]; o->vertices[i].rotated.z = o->vertices[i].scaled.z; } break; case 2: // Final matrix = y final[0][0] = cosTable[y]; final[0][2] = -sinTable[y]; final[2][0] = sinTable[y]; final[2][2] = cosTable[y]; for (i=0; ivertexCount; i++) { o->vertices[i].rotated.x = o->vertices[i].scaled.x * final[0][0] + o->vertices[i].scaled.z * final[2][0]; o->vertices[i].rotated.y = o->vertices[i].scaled.y; o->vertices[i].rotated.z = o->vertices[i].scaled.x * final[0][2] + o->vertices[i].scaled.z * final[2][2]; } break; case 3: // Final matrix = y * z final[0][0] = cosTable[y] * cosTable[z]; final[0][1] = cosTable[y] * sinTable[z]; final[0][2] = -sinTable[y]; final[1][0] = -sinTable[z]; final[1][1] = cosTable[z]; final[2][0] = sinTable[y] * cosTable[z]; final[2][1] = sinTable[y] * sinTable[z]; final[2][2] = cosTable[y]; for (i=0; ivertexCount; i++) { o->vertices[i].rotated.x = o->vertices[i].scaled.x * final[0][0] + o->vertices[i].scaled.y * final[1][0] + o->vertices[i].scaled.z * final[2][0]; o->vertices[i].rotated.y = o->vertices[i].scaled.x * final[0][1] + o->vertices[i].scaled.y * final[1][1] + o->vertices[i].scaled.z * final[2][1]; o->vertices[i].rotated.z = o->vertices[i].scaled.x * final[0][2] + o->vertices[i].scaled.z * final[2][2]; } break; case 4: // Final matrix = x final[1][1] = cosTable[x]; final[1][2] = sinTable[x]; final[2][1] = -sinTable[x]; final[2][2] = cosTable[x]; for (i=0; ivertexCount; i++) { o->vertices[i].rotated.x = o->vertices[i].scaled.x; o->vertices[i].rotated.y = o->vertices[i].scaled.y * final[1][1] + o->vertices[i].scaled.z * final[2][1]; o->vertices[i].rotated.z = o->vertices[i].scaled.y * final[1][2] + o->vertices[i].scaled.z * final[2][2]; } break; case 5: // Final matrix = x * z final[0][0] = cosTable[z]; final[0][1] = sinTable[z]; final[1][0] = -cosTable[x] * sinTable[z]; final[1][1] = cosTable[x] * cosTable[z]; final[1][2] = sinTable[x]; final[2][0] = sinTable[x] * sinTable[z]; final[2][1] = -sinTable[x] * cosTable[z]; final[2][2] = cosTable[x]; for (i=0; ivertexCount; i++) { o->vertices[i].rotated.x = o->vertices[i].scaled.x * final[0][0] + o->vertices[i].scaled.y * final[1][0] + o->vertices[i].scaled.z * final[2][0]; o->vertices[i].rotated.y = o->vertices[i].scaled.x * final[0][1] + o->vertices[i].scaled.y * final[1][1] + o->vertices[i].scaled.z * final[2][1]; o->vertices[i].rotated.z = o->vertices[i].scaled.y * final[1][2] + o->vertices[i].scaled.z * final[2][2]; } break; case 6: // Final matrix = x * y final[0][0] = cosTable[y]; final[0][2] = -sinTable[y]; final[1][0] = sinTable[x] * sinTable[y]; final[1][1] = cosTable[x]; final[1][2] = sinTable[x] * cosTable[y]; final[2][0] = cosTable[x] * sinTable[y]; final[2][1] = -sinTable[x]; final[2][2] = cosTable[x] * cosTable[y]; for (i=0; ivertexCount; i++) { o->vertices[i].rotated.x = o->vertices[i].scaled.x * final[0][0] + o->vertices[i].scaled.y * final[1][0] + o->vertices[i].scaled.z * final[2][0]; o->vertices[i].rotated.y = o->vertices[i].scaled.y * final[1][1] + o->vertices[i].scaled.z * final[2][1]; o->vertices[i].rotated.z = o->vertices[i].scaled.x * final[0][2] + o->vertices[i].scaled.y * final[1][2] + o->vertices[i].scaled.z * final[2][2]; } break; case 7: // Final matrix = x * y * z j3MathMatrix4x4Identity(rotateX); rotateX[1][1] = cosTable[x]; rotateX[1][2] = sinTable[x]; rotateX[2][1] = -sinTable[x]; rotateX[2][2] = cosTable[x]; j3MathMatrix4x4Identity(rotateY); rotateY[0][0] = cosTable[y]; rotateY[0][2] = -sinTable[y]; rotateY[2][0] = sinTable[y]; rotateY[2][2] = cosTable[y]; j3MathMatrix4x4Identity(rotateZ); rotateZ[0][0] = cosTable[z]; rotateZ[0][1] = sinTable[z]; rotateZ[1][0] = -sinTable[z]; rotateZ[1][1] = cosTable[z]; j3MathMatrix4x4Mult(rotateX, rotateY, temp); j3MathMatrix4x4Mult(temp, rotateZ, final); for (i=0; ivertexCount; i++) { o->vertices[i].rotated.x = o->vertices[i].scaled.x * final[0][0] + o->vertices[i].scaled.y * final[1][0] + o->vertices[i].scaled.z * final[2][0]; o->vertices[i].rotated.y = o->vertices[i].scaled.x * final[0][1] + o->vertices[i].scaled.y * final[1][1] + o->vertices[i].scaled.z * final[2][1]; o->vertices[i].rotated.z = o->vertices[i].scaled.x * final[0][2] + o->vertices[i].scaled.y * final[1][2] + o->vertices[i].scaled.z * final[2][2]; } break; default: break; } } // === TRANSLATION === if (o->howDirty & DIRTYNESS_TRANSLATE) { for (i=0; ivertexCount; i++) { o->vertices[i].translated.x = o->vertices[i].rotated.x + o->position.x; o->vertices[i].translated.y = o->vertices[i].rotated.y + o->position.y; o->vertices[i].translated.z = o->vertices[i].rotated.z + o->position.z; } } // === CAMERA SPACE === if ((_j3VarCameraHowDirty & DIRTYNESS_ROTATE) || (_j3VarCameraHowDirty & DIRTYNESS_TRANSLATE)) { // Create the global inverse transformation matrix used to transform world coordinate to camera coordinates j3MathMatrix4x4Identity(translate); j3MathMatrix4x4Identity(rotateX); j3MathMatrix4x4Identity(rotateY); j3MathMatrix4x4Identity(rotateZ); translate[3][0] = -_j3VarCameraLocation.x; translate[3][1] = -_j3VarCameraLocation.y; translate[3][2] = -_j3VarCameraLocation.z; // X matrix rotateX[1][1] = ( cosTable[_j3VarCameraAngle.x]); rotateX[1][2] = -( sinTable[_j3VarCameraAngle.x]); rotateX[2][1] = -(-sinTable[_j3VarCameraAngle.x]); rotateX[2][2] = ( cosTable[_j3VarCameraAngle.x]); // Y matrix rotateY[0][0] = ( cosTable[_j3VarCameraAngle.y]); rotateY[0][2] = -(-sinTable[_j3VarCameraAngle.y]); rotateY[2][0] = -( sinTable[_j3VarCameraAngle.y]); rotateY[2][2] = ( cosTable[_j3VarCameraAngle.y]); // Z matrix rotateZ[0][0] = ( cosTable[_j3VarCameraAngle.z]); rotateZ[0][1] = -( sinTable[_j3VarCameraAngle.z]); rotateZ[1][0] = -(-sinTable[_j3VarCameraAngle.z]); rotateZ[1][1] = ( cosTable[_j3VarCameraAngle.z]); j3MathMatrix4x4Mult(translate, rotateX, result1); j3MathMatrix4x4Mult(result1, rotateY, result2); j3MathMatrix4x4Mult(result2, rotateZ, _j3VarCameraMatrix); _j3VarCameraHowDirty = DIRTYNESS_CLEAN; } for (i=0; ivertexCount; i++) { o->vertices[i].camera.x = o->vertices[i].translated.x * _j3VarCameraMatrix[0][0] + o->vertices[i].translated.y * _j3VarCameraMatrix[1][0] + o->vertices[i].translated.z * _j3VarCameraMatrix[2][0] + _j3VarCameraMatrix[3][0]; o->vertices[i].camera.y = o->vertices[i].translated.x * _j3VarCameraMatrix[0][1] + o->vertices[i].translated.y * _j3VarCameraMatrix[1][1] + o->vertices[i].translated.z * _j3VarCameraMatrix[2][1] + _j3VarCameraMatrix[3][1]; o->vertices[i].camera.z = o->vertices[i].translated.x * _j3VarCameraMatrix[0][2] + o->vertices[i].translated.y * _j3VarCameraMatrix[1][2] + o->vertices[i].translated.z * _j3VarCameraMatrix[2][2] + _j3VarCameraMatrix[3][2]; } // === REMOVE BACKFACES & LIGHT === for (i=0; itriangleCount; i++) { // If the scale changed, we need a new normal length if (o->howDirty & DIRTYNESS_SCALE) { _j3ObjectUpdateNormalLength(o, (juint16)i); } // If it's two sided, there are no backfaces if (!o->triangles[i].twoSided) { // Compute two vectors on polygon that have the same intial points vertex0 = o->triangles[i].index[0]; vertex1 = o->triangles[i].index[1]; vertex2 = o->triangles[i].index[2]; // Vector u = v0->v1 j3MathMakeVector3D((j3VertexT *)&o->vertices[vertex0].translated, (j3VertexT *)&o->vertices[vertex1].translated, (j3Vector3DT *)&u); // Vector v = v0->v2 j3MathMakeVector3D((j3VertexT *)&o->vertices[vertex0].translated, (j3VertexT *)&o->vertices[vertex2].translated, (j3Vector3DT *)&v); // Normal v x u j3MathCrossProduct3D((j3Vector3DT *)&v, (j3Vector3DT *)&u, (j3Vector3DT *)&normal); // Compute the line of sight vector, since all coordinates are world all // object vertices are already relative to (0,0,0), thus sight.x = _j3VarCameraLocation.x - o->vertices[vertex0].translated.x; sight.y = _j3VarCameraLocation.y - o->vertices[vertex0].translated.y; sight.z = _j3VarCameraLocation.z - o->vertices[vertex0].translated.z; // Compute the dot product between line of sight vector and normal to surface dot = j3MathDotProduct3D((j3Vector3DT *)&normal, (j3Vector3DT *)&sight); // Is the surface visible if (dot > 0) { // Set visible flag o->triangles[i].visible = true; // Compute light intensity if needed if (o->triangles[i].lit) { // Compute the dot product between the light source vector and normal vector to surface dot = j3MathDotProduct3D((j3Vector3DT *)&normal, (j3Vector3DT *)&_j3VarSunLocation); // Test if light ray is reflecting off surface if (dot > 0) { intensity = _j3VarAmbientLight + (dot * (o->triangles[i].normalLength)); // Test if intensity has overflowed if (intensity > 15) { intensity = 15; } // Intensity now varies from 0-1, 0 being black or grazing and 1 being // totally illuminated. use the value to index into color table o->triangles[i].shade = o->triangles[i].color - (jint16)intensity; } else { o->triangles[i].shade = o->triangles[i].color - (jint16)_j3VarAmbientLight; } } else { // Constant shading - simply assign color to shade o->triangles[i].shade = o->triangles[i].color; } } else { o->triangles[i].visible = false; } } else { // Triangle is always visible i.e. two sided o->triangles[i].visible = true; // Perform shading calculation if (o->triangles[i].lit) { // Compute two vectors on polygon that have the same intial points vertex0 = o->triangles[i].index[0]; vertex1 = o->triangles[i].index[1]; vertex2 = o->triangles[i].index[2]; // Vector u = v0->v1 j3MathMakeVector3D((j3VertexT *)&o->vertices[vertex0].translated, (j3VertexT *)&o->vertices[vertex1].translated, (j3Vector3DT *)&u); // Vector v = v0->v2 j3MathMakeVector3D((j3VertexT *)&o->vertices[vertex0].translated, (j3VertexT *)&o->vertices[vertex2].translated, (j3Vector3DT *)&v); // Normal v x u j3MathCrossProduct3D((j3Vector3DT *)&v, (j3Vector3DT *)&u, (j3Vector3DT *)&normal); // Compute the dot product between the light source vector and normal vector to surface dot = j3MathDotProduct3D((j3Vector3DT *)&normal, (j3Vector3DT *)&_j3VarSunLocation); // Is light ray is reflecting off surface if (dot > 0) { // cos 0 = (u.v)/|u||v| or intensity = _j3VarAmbientLight + (dot * (o->triangles[i].normalLength)); // test if intensity has overflowed if (intensity > 15) { intensity = 15; } // intensity now varies from 0-1, 0 being black or grazing and 1 being // totally illuminated. use the value to index into color table o->triangles[i].shade = o->triangles[i].color - (jint16)intensity; } else { o->triangles[i].shade = o->triangles[i].color - (jint16)_j3VarAmbientLight; } } else { // Constant shading and simply assign color to shade o->triangles[i].shade = o->triangles[i].color; } } // Did this triangle survive culling? //***TODO*** Our visible flag is backwards for some reason. Winding direction wrong? if (!o->triangles[i].visible) { // Find average z depth in camera space vertex0 = o->triangles[i].index[0]; vertex1 = o->triangles[i].index[1]; vertex2 = o->triangles[i].index[2]; o->triangles[i].averageDepth = (jreal)0.3333333 * (o->vertices[vertex0].camera.z + o->vertices[vertex1].camera.z + o->vertices[vertex2].camera.z); // Add to visible polygon list w->polygons[w->polygonCount].object = o; w->polygons[w->polygonCount].triangleNumber = (juint16)i; w->polygonCount++; } } // for i // Z Sort the visible polygon list qsort((void *)w->polygons, w->polygonCount, sizeof(j3PolyListT), (j3qsortCastT)_j3PolyCompare); o->howDirty = DIRTYNESS_CLEAN; } void _j3ObjectUpdateNormalLength(j3ObjectT *object, juint16 triangle) { juint16 vertex0; juint16 vertex1; juint16 vertex2; j3Vector3DT u; j3Vector3DT v; j3Vector3DT normal; // Compute length of the two co-planer edges of the polygon, since they will be used in the computation of the dot-product later vertex0 = object->triangles[triangle].index[0]; vertex1 = object->triangles[triangle].index[1]; vertex2 = object->triangles[triangle].index[2]; j3MathMakeVector3D((j3VertexT *)&object->vertices[vertex0].scaled, (j3VertexT *)&object->vertices[vertex1].scaled, (j3Vector3DT *)&u); j3MathMakeVector3D((j3VertexT *)&object->vertices[vertex0].scaled, (j3VertexT *)&object->vertices[vertex2].scaled, (j3Vector3DT *)&v); j3MathCrossProduct3D((j3Vector3DT *)&v, (j3Vector3DT *)&u, (j3Vector3DT *)&normal); // Compute magnitude of normal, take its inverse and multiply it by // 15, this will change the shading calculation of 15*dp/normal into // dp*normal_length, removing one division object->triangles[triangle].normalLength = (jreal)15.0 / j3MathVectorMagnatude3D((j3Vector3DT *)&normal); } int _j3PolyCompare(j3PolyListT *arg1, j3PolyListT *arg2) { jreal z1; jreal z2; z1 = arg1->object->triangles[arg1->triangleNumber].averageDepth; z2 = arg2->object->triangles[arg2->triangleNumber].averageDepth; if (z1 > z2) { return(-1); } else if (z1 < z2) { return(1); } else { return(0); } } bool _j3UtilClipLine(jint16 *x1, jint16 *y1, jint16 *x2, jint16 *y2) { jint16 xi; jint16 yi; jreal dx; jreal 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 >= _j3VarClipMinX) && (*x1 <= _j3VarClipMaxX) && (*y1 >= _j3VarClipMinY) && (*y1 <= _j3VarClipMaxY)); point2 = ((*x2 >= _j3VarClipMinX) && (*x2 <= _j3VarClipMaxX) && (*y2 >= _j3VarClipMinY) && (*y2 <= _j3VarClipMaxY)); 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 (((*x1<_j3VarClipMinX) && (*x2<_j3VarClipMinX)) || // left ((*x1>_j3VarClipMaxX) && (*x2>_j3VarClipMaxX)) || // right ((*y1<_j3VarClipMinY) && (*y2<_j3VarClipMinY)) || // above ((*y1>_j3VarClipMaxY) && (*y2>_j3VarClipMaxY))) { // 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 > _j3VarClipMaxX) { // Flag right edge rightEdge = true; // Find intersection with right edge if (fabsf(dx) > FLT_EPSILON) { // dx != 0 yi = (jint16)((jreal)0.5 + (dy / dx) * (_j3VarClipMaxX - *x1) + *y1); } else { yi = -1; // Invalidate intersection } } else if (*x2 < _j3VarClipMinX) { // Flag left edge leftEdge = true; // Find intersection with left edge if (fabsf(dx) > FLT_EPSILON) { // dx != 0 yi = (jint16)((jreal)0.5 + (dy / dx) * (_j3VarClipMinX - *x1) + *y1); } else { yi = -1; // Invalidate intersection } } if (*y2 > _j3VarClipMaxY) { // Flag bottom edge bottomEdge = true; // Find intersection with right edge if (fabsf(dy) > FLT_EPSILON) { // dy != 0 xi = (jint16)((jreal)0.5 + (dx / dy) * (_j3VarClipMaxY - *y1) + *x1); } else { xi = -1; // Invalidate inntersection } } else if (*y2 < _j3VarClipMinY) { // Flag top edge topEdge = true; // Find intersection with top edge if (fabsf(dy) > FLT_EPSILON) { // dy != 0 xi = (jint16)((jreal)0.5 + (dx / dy) * (_j3VarClipMinY - *y1) + *x1); } else { xi = -1; // Invalidate intersection } } // We know where the line passed through // FInd which edge is the proper intersection if (rightEdge && (yi >= _j3VarClipMinY && yi <= _j3VarClipMaxY)) { *x2 = _j3VarClipMaxX; *y2 = yi; success = true; } else if (leftEdge && (yi >= _j3VarClipMinY && yi <= _j3VarClipMaxY)) { *x2 = _j3VarClipMinX; *y2 = yi; success = true; } if (bottomEdge && (xi >= _j3VarClipMinX && xi <= _j3VarClipMaxX)) { *x2 = xi; *y2 = _j3VarClipMaxY; success = true; } else if (topEdge && (xi >= _j3VarClipMinX && xi <= _j3VarClipMaxX)) { *x2 = xi; *y2 = _j3VarClipMinY; 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 > _j3VarClipMaxX) { // Flag right edge rightEdge = true; // Find intersection with right edge if (fabsf(dx) > FLT_EPSILON) { // dx != 0 yi = (jint16)((jreal)0.5 + (dy / dx) * (_j3VarClipMaxX - *x2) + *y2); } else { yi = -1; // Invalidate inntersection } } else if (*x1 < _j3VarClipMinX) { // Flag left edge leftEdge = true; // Find intersection with left edge if (fabsf(dx) > FLT_EPSILON) { // dx != 0 yi = (jint16)((jreal)0.5 + (dy / dx) * (_j3VarClipMinX - *x2) + *y2); } else { yi = -1; // Invalidate intersection } } if (*y1 > _j3VarClipMaxY) { // Flag bottom edge bottomEdge = true; // Find intersection with right edge if (fabsf(dy) > FLT_EPSILON) { // dy != 0 xi = (jint16)((jreal)0.5 + (dx / dy) * (_j3VarClipMaxY - *y2) + *x2); } else { xi = -1; // invalidate inntersection } } else if (*y1 < _j3VarClipMinY) { // Flag top edge topEdge = true; // Find intersection with top edge if (fabsf(dy) > FLT_EPSILON) { // dy != 0 xi = (jint16)((jreal)0.5 + (dx / dy) * (_j3VarClipMinY - *y2) + *x2); } else { xi = -1; // invalidate inntersection } } // We know where the line passed through // Find which edge is the proper intersection if (rightEdge && (yi >= _j3VarClipMinY && yi <= _j3VarClipMaxY)) { *x1 = _j3VarClipMaxX; *y1 = yi; success = true; } else if (leftEdge && (yi >= _j3VarClipMinY && yi <= _j3VarClipMaxY)) { *x1 = _j3VarClipMinX; *y1 = yi; success = true; } if (bottomEdge && (xi >= _j3VarClipMinX && xi <= _j3VarClipMaxX)) { *x1 = xi; *y1 = _j3VarClipMaxY; success = true; } else if (topEdge && (xi >= _j3VarClipMinX && xi <= _j3VarClipMaxX)) { *x1 = xi; *y1 = _j3VarClipMinY; success = true; } } return(success); } void j3UtilShutdown(void) { // Nothing } void j3UtilStartup(void) { _j3VarCameraLocation.x = 0; _j3VarCameraLocation.y = 0; _j3VarCameraLocation.z = 0; _j3VarCameraAngle.x = 0; _j3VarCameraAngle.y = 0; _j3VarCameraAngle.z = 0; _j3VarSunLocation.x = (jreal)-0.913913; _j3VarSunLocation.y = (jreal) 0.389759; _j3VarSunLocation.z = (jreal)-0.113369; } void _j3PropFree(j3PropT **prop) { int x; if ((*prop)) { for (x=0; x<(*prop)->pieceCount; x++) { if ((*prop)->pieces[x].triangles) { jlFree((*prop)->pieces[x].triangles); } if ((*prop)->pieces[x].vertices) { jlFree((*prop)->pieces[x].vertices); } } if ((*prop)->pieces) { jlFree((*prop)->pieces); } jlFree(*prop); } } jint16 _j3PropLoad(j3PropT **prop, char *file) { juint16 x; juint16 y; juint16 z; byte buffer[4]; FILE *in; bool failed = false; in = fopen(jlUtilMakePathname(file, "j3d"), "rb"); // Did we find the file? if (in == NULL) { // Nope. return false; } // Do we have this prop? if (*prop != NULL) { // Free this one j3PropFree(prop); } // Allocate prop object *prop = (j3PropT *)jlMalloc(sizeof(j3PropT)); if (*prop) { // Initialize prop object & create an initial piece to read data into (*prop)->pieceCount = 0;; (*prop)->pieces = NULL; // Is this a valid prop file? if (fread(buffer, sizeof(byte), 4, in) == 4) { if ((buffer[0] == 'J') && (buffer[1] == '3') && (buffer[2] == 'D') && (buffer[3] <= 0)) { // Get piece count if (fread(&(*prop)->pieceCount, sizeof(juint16), 1, in) == 1) { // Allocate memory for pieces (*prop)->pieces = (j3PieceT *)jlMalloc(sizeof(j3PieceT) * (*prop)->pieceCount); if ((*prop)->pieces) { // Iterate across pieces in file for (x=0; x<(*prop)->pieceCount; x++) { // Get vertex count if (fread(&(*prop)->pieces[x].vertexCount, sizeof(juint16), 1, in) == 1) { // Allocate memory for vertex data (*prop)->pieces[x].vertices = (j3VertexT *)jlMalloc(sizeof(j3VertexT) * (*prop)->pieces[x].vertexCount); if ((*prop)->pieces[x].vertices) { // Iterate and read vertices for (y=0; y<(*prop)->pieces[x].vertexCount; y++) { // Read one at a time in case the struct gets padded // These really are type 'float' - don't use 'jreal' here if (fread(&(*prop)->pieces[x].vertices[y].x, sizeof(float), 1, in) != 1) { failed = true; break; } if (fread(&(*prop)->pieces[x].vertices[y].y, sizeof(float), 1, in) != 1) { failed = true; break; } if (fread(&(*prop)->pieces[x].vertices[y].z, sizeof(float), 1, in) != 1) { failed = true; break; } } } } else { // Failed to get vertex memory failed = true; } if (!failed) { // Get triangle count if (fread(&(*prop)->pieces[x].triangleCount, sizeof(juint16), 1, in) == 1) { // Allocate memory for triangle data (*prop)->pieces[x].triangles = (j3TriangleThinT *)jlMalloc(sizeof(j3TriangleThinT) * (*prop)->pieces[x].triangleCount); if ((*prop)->pieces[x].triangles) { // Iterate and read triangles for (y=0; y<(*prop)->pieces[x].triangleCount; y++) { // Read one at a time in case the struct gets padded for (z=0; z<3; z++) { if (fread(&(*prop)->pieces[x].triangles[y].index[z], sizeof(juint16), 1, in) != 1) { failed = true; break; } } if (failed) break; //***TODO*** Triangles begin life un-lit (*prop)->pieces[x].triangles[y].lit = false; //***TODO*** All triangles are one-sided for now (*prop)->pieces[x].triangles[y].twoSided = false; //***TODO*** All triangles are white for now (*prop)->pieces[x].triangles[y].color = 15; } } else { // Failed to get triangle memory failed = true; } } } if (failed) { break; // Stop iterating } } // piece iterator } // pieces alloc } // pieces count } // Valid file } // Read header } // prop alloc // Finished! Clean up. fclose(in); if (failed) { return -1; } return (jint16)(*prop)->pieceCount; } jint16 _j3WorldAddProp(j3WorldT **world, j3PropT *prop) { juint16 x; juint16 y; juint16 z; juint16 oldObjectCount = 0; bool result = true; // Is this world valid yet? if (*world == NULL) { *world = (j3WorldT *)jlMalloc(sizeof(j3WorldT)); (*world)->objectCount = 0; (*world)->polygonCount = 0; (*world)->triangleCount = 0; (*world)->objects = NULL; (*world)->polygons = NULL; } // Add new prop pieces to world as objects oldObjectCount = (*world)->objectCount; (*world)->objectCount += prop->pieceCount; // Allocate memory for objects (*world)->objects = (j3ObjectT *)jlRealloc((*world)->objects, sizeof(j3ObjectT) * (*world)->objectCount); if (!(*world)->objects) { return -1; } // Copy pieces into objects y = 0; for (x=oldObjectCount; x<(*world)->objectCount; x++) { // Vertices (*world)->objects[x].vertexCount = prop->pieces[y].vertexCount; // Allocate memory for vertex data (*world)->objects[x].vertices = (j3CoordinatesT *)jlMalloc(sizeof(j3CoordinatesT) * (*world)->objects[x].vertexCount); if (!(*world)->objects[x].vertices) { result = false; break; } // Copy vertex data for (z=0; zpieces[y].vertexCount; z++) { (*world)->objects[x].vertices[z].original.x = prop->pieces[y].vertices[z].x; (*world)->objects[x].vertices[z].original.y = prop->pieces[y].vertices[z].y; (*world)->objects[x].vertices[z].original.z = prop->pieces[y].vertices[z].z; } // Triangles (*world)->objects[x].triangleCount = prop->pieces[y].triangleCount; // Allocate memory for triangle data (*world)->objects[x].triangles = (j3TriangleT *)jlMalloc(sizeof(j3TriangleT) * (*world)->objects[x].triangleCount); if (!(*world)->objects[x].triangles) { result = false; break; } // Copy triangle data for (z=0; zpieces[y].triangleCount; z++) { (*world)->objects[x].triangles[z].index[0] = prop->pieces[y].triangles[z].index[0]; (*world)->objects[x].triangles[z].index[1] = prop->pieces[y].triangles[z].index[1]; (*world)->objects[x].triangles[z].index[2] = prop->pieces[y].triangles[z].index[2]; (*world)->objects[x].triangles[z].lit = prop->pieces[y].triangles[z].lit; (*world)->objects[x].triangles[z].color = prop->pieces[y].triangles[z].color; (*world)->objects[x].triangles[z].twoSided = prop->pieces[y].triangles[z].twoSided; (*world)->objects[x].triangles[z].normalLength = prop->pieces[y].triangles[z].normalLength; _j3ObjectUpdateNormalLength(&(*world)->objects[x], z); // Triangles begin life visible (*world)->objects[x].triangles[z].visible = true; } // Add to world count & increase potential polygon list (*world)->triangleCount += (*world)->objects[x].triangleCount; (*world)->polygons = (j3PolyListT *)jlRealloc((*world)->polygons, sizeof(j3PolyListT) * (*world)->triangleCount); if (!(*world)->polygons) { result = false; break; } _j3ObjectReset(&(*world)->objects[x]); // Next piece y++; } // Pieces if (!result) { return -1; } return (jint16)oldObjectCount; } void _j3WorldFree(j3WorldT **world) { int x; if ((*world)) { for (x=0; x<(*world)->objectCount; x++) { if ((*world)->objects[x].triangles) { jlFree((*world)->objects[x].triangles); } if ((*world)->objects[x].vertices) { jlFree((*world)->objects[x].vertices); } } if ((*world)->objects) { jlFree((*world)->objects); } if ((*world)->polygons != NULL) { jlFree((*world)->polygons); } jlFree(*world); } } #ifndef JOEY_IIGS #pragma GCC diagnostic pop #endif