#include "3DRenderer.hpp" #include #include "../World/DbgCube.hpp" #include "../World/Tank.hpp" #define SF_COLOR_4CHEX(h) sf::Color((uint32_t)h) #define FAR_Z (100.f) #define NEAR_Z (0.1f) //#define DISABLE_AABB_CLIPPING //#define DISABLE_TRIANGLE_CLIPPING //#define DISABLE_WIREFRAME_MODE // Rendering pipeline: // model matrix (Object SRT) -> view matrix (camera matrix inverted) -> proj matrix -> clipping -> perspective divide // -> viewport transformation -> Rasterizer (draw pixels inside projected triangles on 2D screen) // Revised rendering pipeline: // AABB clipping -> model transform matrix (Object SRT) -> view matrix (camera matrix inverted) -> proj matrix // -> faces culling -> triangles clipping -> perspective divide -> viewport transformation -> Rasterizer (draw pixels inside projected triangles on 2D screen) // // Virtual space transformations order: // object space -> world space -> camera space -> homogeneous clip space -> NDC space -> raster space // // Rasterizer inputs elements: // - texture-buffer (2D array of pixels color value) // - z-buffer (2D array of float representing the nearest pixel's depth, all pixels beyond are ignored) // - projected vertices-buffer on screen (using vertices-buffer and projection function) // // Refs: // * https://en.wikipedia.org/wiki/3D_projection // * https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/overview-rasterization-algorithm.html // * https://ktstephano.github.io/rendering/stratusgfx/aabbs // * https://en.wikipedia.org/wiki/Clipping_(computer_graphics) // * https://www.coranac.com/tonc/text/mode7.htm // * https://en.wikipedia.org/wiki/Back-face_culling // * https://en.wikipedia.org/wiki/Hidden-surface_determination#Occlusion_culling // * https://en.wikipedia.org/wiki/Bounding_volume_hierarchy struct RenderItem final { const WorldObject* pObj = nullptr; const M3D_ContainmentType frustrumClipType = CONTAINS; RenderItem() = delete; RenderItem(const WorldObject* pObj) : pObj(pObj) {} RenderItem(const WorldObject* pObj, const M3D_ContainmentType cType) : pObj(pObj), frustrumClipType(cType) {} }; static bool VertexClipTest(M3D_F4* V, sf::Vector2f& RTsize); Graphic3DRenderer::Graphic3DRenderer() { if (mMainCamera == nullptr) { mMainCamera = std::make_unique(); mMainCamera->SetPosition(0.0f, 1.5f, -8.0f); } SetRTSize(1280.f, 324.f); mMainCamera->UpdateCamView(); // Fill world object list to render mWorldObjsList.clear(); mWorldObjsList.push_back(std::make_shared()); mWorldObjsList.back()->SetPosition(0.f, 0.f, 15.f); mWorldObjsList.back()->SetScale(2.0f); mWorldObjsList.push_back(std::make_shared()); mWorldObjsList.back()->SetPosition(6.f, 2.f, 2.f); mWorldObjsList.back()->SetScale(2.0f); mWorldObjsList.push_back(std::make_shared()); mWorldObjsList.back()->SetPosition(-8.f, 5.f, 10.f); mWorldObjsList.back()->SetScale(2.0f); mWorldObjsList.push_back(std::make_shared()); mWorldObjsList.back()->SetPosition(0.f, 0.f, 0.f); mWorldObjsList.back()->SetScale(5.0f); for (size_t i = 0; i < 40; i++) { mWorldObjsList.push_back(std::make_shared()); mWorldObjsList.back()->SetPosition(-100.f + (i * 5.f), 0.f, 8.f); mWorldObjsList.back()->SetScale(5.0f); } } Graphic3DRenderer::~Graphic3DRenderer() {} void Graphic3DRenderer::SetRTSize(unsigned int w, unsigned int h) { mRTSize.x = w; mRTSize.y = h; mMainCamera->SetFrustrum(75.0f, mRTSize.x/mRTSize.y, NEAR_Z, FAR_Z); } void Graphic3DRenderer::UpdateCamera(CAMERA_MOVE type, const float value) { switch (type) { case CAMERA_MOVE_WALK: mMainCamera->Walk(value); break; case CAMERA_MOVE_STRAFE: mMainCamera->Strafe(value); break; case CAMERA_MOVE_FLY: mMainCamera->Fly(value); break; case CAMERA_MOVE_PITCH: mMainCamera->Pitch(value); break; case CAMERA_MOVE_YAW: mMainCamera->Yaw(value); break; default: break; } mMainCamera->UpdateCamView(); } void Graphic3DRenderer::Draw(sf::RenderTexture& context) { #ifdef DEBUG drawnTriCount = 0; #endif // Hardcoded debug movement, TODO: remove it UpdateInternalTestObjects(); DrawBackground(context); DrawSceneObjects(context); } void Graphic3DRenderer::UpdateInternalTestObjects() { static float thetaAngle = 0.31f; thetaAngle = thetaAngle >= M3D_2PI ? -M3D_2PI : thetaAngle + 0.004f; static float thetaAngle2 = 2.12f; thetaAngle2 = thetaAngle2 >= M3D_2PI ? -M3D_2PI : thetaAngle2 + 0.005f; static float thetaAngle3 = -4.78f; thetaAngle3 = thetaAngle3 >= M3D_2PI ? -M3D_2PI : thetaAngle3 + 0.008f; mWorldObjsList[0]->SetRotation(thetaAngle, 0.f, thetaAngle * 0.5f); mWorldObjsList[1]->SetRotation(thetaAngle2, 0.f, thetaAngle2 * 0.5f); mWorldObjsList[2]->SetRotation(thetaAngle3, 0.f, thetaAngle3 * 0.5f); mWorldObjsList[3]->SetRotation(0.f, thetaAngle, 0.f); } // Compute the screen ratio between the ground and the sky (aka. Line of Horizon) float Graphic3DRenderer::ComputeSGRatio() { static double sgRatio = 0.5f; static float fovCos = 0.f; static float fovSin = 0.f; static float thetaCos = 0.f; static float fovThetaSin = 0.f; const bool isCamMoved = mMainCamera->IsCameraMoved(); const bool isFUpdated = mMainCamera->IsFrustrumUpdated(); if (isCamMoved || isFUpdated) { mMainCamera->ResetUpdateFlags(); // FoV angle for Y axis is recovered using frustrum FoV and apply RT screen ratio to it const float fovYAngleDiv2 = M3D_Deg2Rad(mMainCamera->GetFoV()) * 0.5f; // Get the camera pitch angle over camera FoV ratio const float theta = M3D_ScalarASinEst(-mMainCamera->GetLook3f().y); // Get the camera altitude from the ground const float altitude = mMainCamera->GetPos3f().y; fovThetaSin = M3D_ScalarSinEst(fovYAngleDiv2 + theta); if (isCamMoved) thetaCos = M3D_ScalarCosEst(theta); if (isFUpdated) { fovCos = M3D_ScalarCosEst(fovYAngleDiv2); fovSin = M3D_ScalarSinEst(fovYAngleDiv2); } // Ground/Sky screen ratio calculation using "simple" trigonometric properties of the // pinhole (frustrum) camera model. // The triangle made by the ground plane intersection with the frustum. This intersection // cross the far plane at some point. Instead of computing the coordinate of the point, we // directly use the far plane length to get the corresponding ratio for the screen. sgRatio = -(altitude * fovCos - FAR_Z * fovThetaSin) / (2.f * FAR_Z * fovSin * thetaCos); } // Clamp if (sgRatio > 1.f) sgRatio = 1.f; else if (sgRatio < 0.f) sgRatio = 0.f; return sgRatio; } void Graphic3DRenderer::DrawBackground(sf::RenderTexture& context) { sf::BlendMode sBM = sf::BlendNone; sf::RenderStates sRS(sBM); const float sgRatio = ComputeSGRatio(); // -= Draw the sky =- // To avoid unfilled pixels on screen, the "sky-plane" will be rendered // all over the screen. // It's will be useless to use and compute a specific rectangle from the // size of the screen! // The sky have an infinite z-depth (any objects will be rendered over). #ifdef DISABLE_WIREFRAME_MODE context.clear(SF_COLOR_4CHEX(0x00B5E2FF)); #endif // -= Draw the ground =- // A simple rectangle shape is used to draw the ground over the sky-plane. // The ground is draw after the sky, and before any other object. // Depending of the camera pitch, the ratio sky/ground on screen vary. // Like the sky, the ground have an infinite z-depth (any objects will // be rendered over). #ifdef DISABLE_WIREFRAME_MODE sf::RectangleShape gndRect; if (mMainCamera->GetPos3f().y >= 0) { gndRect.setSize(sf::Vector2f(mRTSize.x, mRTSize.y * sgRatio)); gndRect.setPosition(sf::Vector2f(0, mRTSize.y * (1.f - sgRatio) - 1)); } else { gndRect.setSize(sf::Vector2f(mRTSize.x, mRTSize.y * (1.f - sgRatio))); gndRect.setPosition(sf::Vector2f(0, 0)); } gndRect.setFillColor(SF_COLOR_4CHEX(0x009A17FF)); //gndRect.setFillColor(SF_COLOR_4CHEX(0xD5C2A5FF)); context.draw(gndRect, sRS); #else sf::Vertex gndLine[2]; gndLine[0].position = sf::Vector2f(0, mRTSize.y * (1.f - sgRatio)); gndLine[0].color = sf::Color::White; gndLine[1].position = sf::Vector2f(mRTSize.x - 1, mRTSize.y * (1.f - sgRatio)); gndLine[1].color = sf::Color::White; context.draw(gndLine, 2, sf::Lines, sRS); #endif } void Graphic3DRenderer::DrawSceneObjects(sf::RenderTexture& context) { sf::BlendMode sBM = sf::BlendNone; sf::RenderStates sRS(sBM); // Get global (camera and projection) matrixes M3D_MATRIX viewMat = mMainCamera->GetView(); M3D_MATRIX invViewMat = M3D_MInverse(viewMat); // aka. camera matrix M3D_MATRIX projMat = mMainCamera->GetProj(); M3D_MATRIX viewProjMat = viewMat * projMat; std::vector renderingList; renderingList.reserve(mWorldObjsList.size()); M3D_BoundingFrustum camFrustrum(projMat, false); camFrustrum.Transform(camFrustrum, invViewMat); for (auto& obj : mWorldObjsList) { #ifndef DISABLE_AABB_CLIPPING // Objects visibility AABB test M3D_BoundingBox projAABB = obj->GetAABB(); projAABB.Transform(projAABB, obj->GetTransform()); // Do the camera/AABB test M3D_ContainmentType aabbTestResult = camFrustrum.Contains(projAABB); if (aabbTestResult != DISJOINT) renderingList.emplace_back(RenderItem(obj.get(), aabbTestResult)); #else renderingList.emplace_back(RenderItem(obj.get())); #endif } // Do the NDC projection of visibles vertices in camera frustrum size_t prevVCount = 0; std::vector projVertices; sf::Vector2f guardband = mRTSize * 3.5f; for (auto& ri : renderingList) { size_t vCount = ri.pObj->GetObjectVerticesCount(); // Resize the output buffer only if we encounter object with more vertices than before if (vCount > prevVCount) { projVertices.resize(vCount); prevVCount = vCount; } auto& oMesh = ri.pObj->GetObjectMesh(); // Vertices homogeneous clip space (NDC) transformation M3D_V3Transform( projVertices.data(), sizeof(M3D_F4), reinterpret_cast(oMesh.vertices.data()), sizeof(Vertex), vCount, ri.pObj->GetTransform() * viewProjMat ); // Look into triangles indices M3D_F4* triVertices[3]; sf::Vertex drawPoints[4]; for (auto& objPt : oMesh.parts) { auto indicePtr = static_cast(objPt.indices.data()); for (uint32_t i = 0; i < objPt.GetIndicesCount(); i += 3) { // Indices failsafe - discard triangle rendering if ((i+2 > objPt.GetIndicesCount()) || indicePtr[i] >= vCount || indicePtr[i+1] >= vCount || indicePtr[i+2] >= vCount) { //log.PrintWarning() break; } // Retrieve the vertices pointer from indices list triVertices[0] = &projVertices[indicePtr[i]]; triVertices[1] = &projVertices[indicePtr[i+1]]; triVertices[2] = &projVertices[indicePtr[i+2]]; // Triangle frustrum clipping -- TODO: Use complete Cohen-Sutherland algo or Cyrus–Beck one #ifndef DISABLE_TRIANGLE_CLIPPING if (VertexClipTest(triVertices[0], guardband) && VertexClipTest(triVertices[1], guardband) && VertexClipTest(triVertices[2], guardband)) #endif { M3D_VECTOR V1 = M3D_V4LoadF4(triVertices[0]); M3D_VECTOR V2 = M3D_V4LoadF4(triVertices[1]); M3D_VECTOR V3 = M3D_V4LoadF4(triVertices[2]); // Back-face culling in LH-z system // (front when (triangle normal . projected vertice) < 0) //if (M3D_V4GetX(M3D_V3Dot(M3D_Tri3DNormal(V1,V2,V3), V1)) < 0) // NOT USED - Too heavy computation resources usage // Do the perspective divide V1 = M3D_V4Divide(V1, M3D_V4SplatW(V1)); V2 = M3D_V4Divide(V2, M3D_V4SplatW(V2)); V3 = M3D_V4Divide(V3, M3D_V4SplatW(V3)); // Finally project from NDC to the screen V1 = M3D_V3TransformNDCToViewport(V1, 0.f, 0.f, mRTSize.x, mRTSize.y, NEAR_Z, FAR_Z); V2 = M3D_V3TransformNDCToViewport(V2, 0.f, 0.f, mRTSize.x, mRTSize.y, NEAR_Z, FAR_Z); V3 = M3D_V3TransformNDCToViewport(V3, 0.f, 0.f, mRTSize.x, mRTSize.y, NEAR_Z, FAR_Z); // Simplified back-face culling on 2D viewport triangle if (M3D_V4GetX(M3D_Tri2DNormal(V1,V2,V3)) > 0) { // Set pixels color depending of frustrum clipping type - debug purpose if (ri.frustrumClipType == DISJOINT) { drawPoints[0].color = drawPoints[1].color = drawPoints[2].color = sf::Color::Red; } else if (ri.frustrumClipType == INTERSECTS) { drawPoints[0].color = drawPoints[1].color = drawPoints[2].color = sf::Color::Yellow; } else { drawPoints[0].color = oMesh.vertices[indicePtr[i]].color; drawPoints[1].color = oMesh.vertices[indicePtr[i+1]].color; drawPoints[2].color = oMesh.vertices[indicePtr[i+2]].color; } drawPoints[0].position = sf::Vector2f(M3D_V4GetX(V1), M3D_V4GetY(V1)); drawPoints[1].position = sf::Vector2f(M3D_V4GetX(V2), M3D_V4GetY(V2)); drawPoints[2].position = sf::Vector2f(M3D_V4GetX(V3), M3D_V4GetY(V3)); drawPoints[3] = drawPoints[0]; #ifdef DISABLE_WIREFRAME_MODE context.draw(drawPoints, 4, sf::Triangles, sRS); #else context.draw(drawPoints, 4, sf::LineStrip, sRS); #endif #ifdef DEBUG drawnTriCount++; #endif } } } } } } __attribute__((always_inline)) inline static bool VertexClipTest(M3D_F4* V, sf::Vector2f& RTsize) { // Guard band are usually 2-3x the viewport size for the clipping test return (V->z >= 0 && V->z <= V->w && V->x >= -RTsize.x*V->w && V->x <= RTsize.x*V->w && V->y >= -RTsize.y*V->w && V->y <= RTsize.y*V->w ); }