/* * 3D Engine * main.c: * Based on pikuma.com 3D software renderer in C * Copyright (c) 2021 986-Studio. All rights reserved. * * Created by Manoƫl Trapier on 01/03/2021. */ #include #include #include #include #include #include #include <3dengine.h> #include #include #include #include #include #include #include #include #include bool isRunning = true; int previousFrameTime = 0; double deltaTime = 0; typedef enum displayMode_t { DISPLAY_MODE_WIREFRAME, DISPLAY_MODE_SOLID, DISPLAY_MODE_SOLID_WIRE, DISPLAY_MODE_TEXTURED, DISPLAY_MODE_TEXTURED_WIRE, } displayMode_t; bool displayVertex = false; bool doNotCull = false; bool doZBuffer = true; bool doClipping = true; bool doPerspectiveCorrection = true; static mesh_t worldMesh[10]; static int meshCount = 0; static int windowHeight; static int windowWidth; static camera_t camera; static light_t globalLight; static enum displayMode_t currentDisplayMode = DISPLAY_MODE_TEXTURED; static matrix4_t projectionMatrix; static double updateTime, renderTime, waitTime; static int trianglesCount, trianglesTotal; static triangle_t *projectedTriangles = NULL; static const vec3_t origin = {0, 0, 0 }; void loadNewMesh(const char *obj, const char *tex) { /* Load 3D model */ loadOBJFile(&worldMesh[meshCount],obj); /* Load texture */ loadTextureDateFromPng(&worldMesh[meshCount].texture, tex); meshCount++; } void setup() { int i; double FOV = 60; double aspectRatioY = (double)windowHeight / windowWidth; double aspectRatioX = (double)windowWidth / windowHeight; double zNear = 1; double zFar = 20; projectionMatrix = mat4Perspective(aspectRatioY, FOV, zNear, zFar); initFrustumPlanes(FOV, aspectRatioX, zNear, zFar); createLight(&globalLight, vec3(0, 0, 1)); /* Load object */ loadNewMesh("assets/f22.obj", "assets/f22.png"); worldMesh[0].translation.z = 5; worldMesh[0].translation.x = -5; worldMesh[0].rotation.y = 0.607; worldMesh[0].rotation.x = -0.3; loadNewMesh("assets/f117.obj", "assets/f117.png"); worldMesh[1].translation.z = 5; worldMesh[0].translation.x = 5; worldMesh[1].rotation.y = 0.607; worldMesh[1].rotation.x = -0.3; trianglesTotal = 0; for(i = 0; i < meshCount; i++) { trianglesTotal += arrayGetSize(worldMesh[i].faces); } createCamera(&camera); cameraSetTarget(&camera, vec3(0, 0, 1)); cameraSetUp(&camera, vec3(0, 1, 0)); } void processInput() { SDL_Event event; while(SDL_PollEvent(&event)) { switch (event.type) { case SDL_QUIT:isRunning = false; break; case SDL_KEYDOWN: switch (event.key.keysym.sym) { case SDLK_1: displayVertex = true; currentDisplayMode = DISPLAY_MODE_WIREFRAME; break; case SDLK_2: displayVertex = false; currentDisplayMode = DISPLAY_MODE_WIREFRAME; break; case SDLK_3: displayVertex = false; currentDisplayMode = DISPLAY_MODE_SOLID; break; case SDLK_4: displayVertex = false; currentDisplayMode = DISPLAY_MODE_SOLID_WIRE; break; case SDLK_5: displayVertex = false; currentDisplayMode = DISPLAY_MODE_TEXTURED; break; case SDLK_6: displayVertex = false; currentDisplayMode = DISPLAY_MODE_TEXTURED_WIRE; break; case SDLK_ESCAPE: isRunning = false; break; case SDLK_u: doNotCull = false; break; case SDLK_j: doNotCull = true; break; case SDLK_k: doPerspectiveCorrection = false; break; case SDLK_i: doPerspectiveCorrection = true; break; case SDLK_o: doZBuffer = true; break; case SDLK_l: doZBuffer = false; break; case SDLK_y: doClipping = true; break; case SDLK_h: doClipping = false; break; case SDLK_w: cameraMoveForward(&camera, 5 * deltaTime); break; case SDLK_d: cameraRotateBy(&camera, 1.0 * deltaTime, 0); break; case SDLK_s: cameraMoveBackward(&camera, 5 * deltaTime); break; case SDLK_a: cameraRotateBy(&camera, -1.0 * deltaTime, 0); break; case SDLK_q: cameraRotateBy(&camera, 0, -1.0 * deltaTime); break; case SDLK_e: cameraRotateBy(&camera, 0, 1.0 * deltaTime); break; case SDLK_UP: cameraMoveRelative(&camera, 0, 3.0 * deltaTime, 0); break; case SDLK_DOWN: cameraMoveRelative(&camera, 0, -3.0 * deltaTime, 0); break; } break; default: break; } } } uint32_t getMicroSecTime() { struct timeval curTime; gettimeofday(&curTime, NULL); return ((curTime.tv_sec) * 1000 * 1000) + (curTime.tv_usec); } static void projectTriangle(triangle_t *workTriangle, vec3_t *triangleNormal, texture_t *texture, colour_t colour) { int j; struct triangle_t currentTriangle = *workTriangle; for (j = 0 ; j < 3 ; j++) { vec4_t projected = mat4ProdVec4Project(projectionMatrix, workTriangle->points[j]); projected.x *= windowWidth / 2.; projected.y *= windowHeight / 2.; /* Screen Y axis increase down, 3D world Y grows up, so we need to invert. */ projected.y *= -1.0; projected.x += (windowWidth / 2.); projected.y += (windowHeight / 2.); currentTriangle.points[j].x = projected.x; currentTriangle.points[j].y = projected.y; currentTriangle.points[j].w = projected.w; currentTriangle.points[j].z = projected.z; } if (doNotCull) { currentTriangle.colour = colour; } else { double lightLevel = -vec3Dot(*triangleNormal, globalLight.direction); currentTriangle.colour = applyLight(colour, lightLevel); } currentTriangle.averageDepth = (workTriangle->points[0].z + workTriangle->points[1].z + workTriangle->points[2].z) / 3.0; currentTriangle.texture = texture; arrayAdd(projectedTriangles, currentTriangle); } static void processMesh(mesh_t *mesh, matrix4_t *worldMatrix) { uint32_t i, j; uint32_t faceCount; uint32_t numberOfClippedTriangles; triangle_t trianglesAfterClipping[MAX_NUM_POLYGON_TRIANGLES]; faceCount = arrayGetSize(mesh->faces); matrix4_t transformMatrix = mat4Identity; transformMatrix = mat4ProdMat4(mat4Scale(mesh->scale.x, mesh->scale.y, mesh->scale.z), transformMatrix); transformMatrix = mat4ProdMat4(mat4RotationZ(mesh->rotation.z), transformMatrix); transformMatrix = mat4ProdMat4(mat4RotationY(mesh->rotation.y), transformMatrix); transformMatrix = mat4ProdMat4(mat4RotationX(mesh->rotation.x), transformMatrix); transformMatrix = mat4ProdMat4(mat4Translate(mesh->translation.x, mesh->translation.y, mesh->translation.z), transformMatrix); transformMatrix = mat4ProdMat4(*worldMatrix, transformMatrix); for(i = 0; i < faceCount; i++) { polygon_t polygon; vec4_t transformedVertices[3]; vec3_t triangleNormal; vec3_t vertices[3] = { mesh->vertices[mesh->faces[i].a], mesh->vertices[mesh->faces[i].b], mesh->vertices[mesh->faces[i].c], }; for(j = 0; j < 3; j++) { transformedVertices[j] = vec4FromVec3(vertices[j]); transformedVertices[j] = mat4ProdVec4(transformMatrix, transformedVertices[j]); } vec3_t vectorBA = vec3SubVectors(vec3FromVec4(transformedVertices[1]), vec3FromVec4(transformedVertices[0])); vec3_t vectorCA = vec3SubVectors(vec3FromVec4(transformedVertices[2]), vec3FromVec4(transformedVertices[0])); vec3Normalize(&vectorBA); vec3Normalize(&vectorCA); triangleNormal = vec3Cross(vectorBA, vectorCA); vec3Normalize(&triangleNormal); if (!doNotCull) { vec3_t cameraRay = vec3SubVectors(origin, vec3FromVec4(transformedVertices[0])); vec3Normalize(&cameraRay); double cameraDotTriangle = vec3Dot(triangleNormal, cameraRay); if (cameraDotTriangle < 0) { continue; } } if (doClipping) { polygon = createPolygonFromTriangle(vec3FromVec4(transformedVertices[0]), vec3FromVec4(transformedVertices[1]), vec3FromVec4(transformedVertices[2]), mesh->faces[i].a_uv, mesh->faces[i].b_uv, mesh->faces[i].c_uv); clipPolygon(&polygon); createTriangleFromPolygon(&polygon, trianglesAfterClipping, &numberOfClippedTriangles); for(j = 0; j < numberOfClippedTriangles; j++) { projectTriangle(&trianglesAfterClipping[j], &triangleNormal, &mesh->texture, mesh->faces[i].colour); } } else { triangle_t workTriangle = { .points = { [0] = transformedVertices[0], [1] = transformedVertices[1], [2] = transformedVertices[2] }, .textureCoordinates = { [0] = { mesh->faces[i].a_uv.u, mesh->faces[i].a_uv.v }, [1] = { mesh->faces[i].b_uv.u, mesh->faces[i].b_uv.v }, [2] = { mesh->faces[i].c_uv.u, mesh->faces[i].c_uv.v } } }; projectTriangle(&workTriangle, &triangleNormal, &mesh->texture, mesh->faces[i].colour); }; } } void update() { int i; uint32_t timeToWait = FRAME_TARGET_TIME - (SDL_GetTicks() - previousFrameTime); matrix4_t worldMatrix; uint32_t ticks = getMicroSecTime(); if (timeToWait <= FRAME_TARGET_TIME) { SDL_Delay(timeToWait); } deltaTime = (SDL_GetTicks() - previousFrameTime) / 1000.0; previousFrameTime = SDL_GetTicks(); waitTime = (waitTime + (getMicroSecTime() - ticks) / 1000.) / 2.0; ticks = getMicroSecTime(); worldMatrix = cameraGetViewMatrix(&camera); arrayEmpty(projectedTriangles); for(i = 0; i < meshCount; i++) { processMesh(&worldMesh[i], &worldMatrix); } if (!doZBuffer) { trianglesCount = arrayGetSize(projectedTriangles); qsort(projectedTriangles, trianglesCount, sizeof(triangle_t), compareTrianglesZOrder); } updateTime = (updateTime + (getMicroSecTime() - ticks) / 1000.) / 2.0; } void render() { int i; uint32_t ticks = getMicroSecTime(); clearFrameBuffer(MAKE_RGB(0, 0, 0)); clearZBuffer(); drawGrid(20, MAKE_RGB(27, 2, 27)); int triangleCount = arrayGetSize(projectedTriangles); for(i = 0; i < triangleCount; i++) { switch(currentDisplayMode) { case DISPLAY_MODE_SOLID: case DISPLAY_MODE_SOLID_WIRE: drawFilledTriangle(&projectedTriangles[i]); break; case DISPLAY_MODE_TEXTURED: case DISPLAY_MODE_TEXTURED_WIRE: drawTextureTriangle(&projectedTriangles[i]); break; default: break; } switch (currentDisplayMode) { case DISPLAY_MODE_SOLID_WIRE: case DISPLAY_MODE_TEXTURED_WIRE: case DISPLAY_MODE_WIREFRAME: drawTriangle(projectedTriangles[i].points[0].x, projectedTriangles[i].points[0].y, projectedTriangles[i].points[1].x, projectedTriangles[i].points[1].y, projectedTriangles[i].points[2].x, projectedTriangles[i].points[2].y, projectedTriangles[i].colour); break; default: break; } if (displayVertex) { int j; for(j = 0; j < 3; j++) { drawRectangle(projectedTriangles[i].points[j].x - 1, projectedTriangles[i].points[j].y - 1, 3, 3, MAKE_RGB(255, 0, 0)); } } } renderFrameBuffer(); renderTime = (renderTime + (getMicroSecTime() - ticks) / 1000.) / 2.0; drawText(5, 5, 0x11FF22, "Wait time: %02.2f ms", waitTime); drawText(5, 17, 0x11FF22, "Update time: %02.2f ms", updateTime); drawText(5, 29, 0x11FF22, "Render time: %02.2f ms", renderTime); drawText(5, 41, 0x11FF22, "FPS: %.1f | dT: %.3f", 1000 / (waitTime + updateTime + renderTime), deltaTime); drawText(5, 53, 0x11FF22, "Triangles: %d / %d", triangleCount, trianglesTotal); drawText(5, 65, 0x11FF22, "Cull: %c | PeCo: %c | ZB: %c, | CP: %c", doNotCull?'N':'Y', doPerspectiveCorrection ? 'Y' : 'N', doZBuffer ? 'Y' : 'N', doClipping ? 'Y' : 'N'); displayWindowRender(); } int main(int argc, char *argv[]) { int param_i, i; bool fullScreen = false; MAX_DEBUG_LEVEL = TLOG_DEBUG; Log(TLOG_ALWAYS, NULL, "Booting 3D Engine (version %s)!", VERSION); windowWidth = 320; windowHeight = 240; for (param_i = 1 ; (param_i < argc) && (argv[param_i][0] == '-') ; param_i++) { switch (argv[param_i][1]) { default: exit(-1); /* Option not recognized */ case 'f': fullScreen = true; break; case 'w': windowWidth = atoi(argv[++param_i]); break; case 'h': windowHeight = atoi(argv[++param_i]); break; #ifdef DYNA_LOG_LEVEL case 'l': MAX_DEBUG_LEVEL = atoi(argv[++param_i]); break; #endif case '-': goto no_more_params; /* Could use break, but this is more clear */ } } no_more_params: isRunning = initialiseWindow(windowWidth, windowHeight, fullScreen); setup(); while (isRunning) { processInput(); update(); render(); } arrayFree(projectedTriangles); for(i = 0; i < meshCount; i++) { textureCleanup(&worldMesh[i].texture); meshFree(&worldMesh[i]); } destroyWindow(); return 0; }