#include "3DRenderer.hpp" #include #include "../World/DbgCube.hpp" #include "../World/Tank.hpp" #define SF_COLOR_4CHEX(h) sf::Color((uint32_t)h) //#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) // 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 static bool VertexClipTest(M3D_F4& V, sf::Vector2f& RTsize, float gb_factor); 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 mRenderList.clear(); mRenderList.push_back(std::make_shared()); mRenderList.back()->SetPosition(0.f, 0.f, 15.f); mRenderList.back()->SetScale(2.0f); mRenderList.push_back(std::make_shared()); mRenderList.back()->SetPosition(6.f, 2.f, 2.f); mRenderList.back()->SetScale(2.0f); mRenderList.push_back(std::make_shared()); mRenderList.back()->SetPosition(-8.f, 5.f, 10.f); mRenderList.back()->SetScale(2.0f); mRenderList.push_back(std::make_shared()); mRenderList.back()->SetPosition(0.f, 0.f, 0.f); mRenderList.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, 1.0f, 100.f); } 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) { sf::BlendMode sBM = sf::BlendNone; sf::RenderStates sRS(sBM); // Hardcoded debug movement, TODO: remove it UpdateInternalTestObjects(); // Load main matrices M3D_MATRIX viewMat = mMainCamera->GetView(); M3D_MATRIX invViewMat = M3D_MInverse(viewMat); // aka. camMat M3D_MATRIX projMat = mMainCamera->GetProj(); M3D_MATRIX viewProjMat = viewMat * projMat; // Create the frustrum "box" M3D_BoundingFrustum camFrustrum(projMat, false); camFrustrum.Transform(camFrustrum, invViewMat); 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); #endif // Process scene's objects for (auto& obj : mRenderList) { M3D_BoundingBox projAABB = obj->GetAABB(); auto oTMat = obj->GetTransform(); // Object outside frustrum clipping projAABB.Transform(projAABB, oTMat); M3D_ContainmentType objInFrustrum = camFrustrum.Contains(projAABB); #ifndef DISABLE_AABB_CLIPPING if (objInFrustrum != DISJOINT) #endif { size_t vCount = obj->GetObjectVerticesCount(); auto& oMesh = obj->GetObjectMesh(); M3D_F4 projVertices[vCount] = {}; // Vertices homogeneous clip space transformation M3D_V3Transform( projVertices, sizeof(M3D_F4), reinterpret_cast(oMesh.vertices.data()), sizeof(Vertex), vCount, oTMat * viewProjMat ); // Draw the object indice triangles if visible or partially clipped sf::Vertex v_tri[4]; for (auto& objPt : oMesh.parts) { auto indicePtr = static_cast(objPt.indices.data()); for (uint32_t i = 0; i < objPt.GetIndicesCount(); i += 3) { // Misscontructed indices tree failsafe if (i+2 > objPt.GetIndicesCount()) break; // Triangle clipping #ifndef DISABLE_TRIANGLE_CLIPPING //TODO: scissor/clipping depending of how many vertices are outside/inside the clipspace, implement complete Cohen-Sutherland algo or Cyrus–Beck one if (VertexClipTest(projVertices[indicePtr[i]], mRTSize, 2.5f) && VertexClipTest(projVertices[indicePtr[i+1]], mRTSize, 2.5f) && VertexClipTest(projVertices[indicePtr[i+2]], mRTSize, 2.5f)) #endif { M3D_VECTOR V1 = M3D_V4LoadF4(&projVertices[indicePtr[i]]); M3D_VECTOR V2 = M3D_V4LoadF4(&projVertices[indicePtr[i+1]]); M3D_VECTOR V3 = M3D_V4LoadF4(&projVertices[indicePtr[i+2]]); // 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)); V1 = M3D_V3TransformNDCToViewport(V1, 0.f, 0.f, mRTSize.x, mRTSize.y, 1.f, 100.f); V2 = M3D_V3TransformNDCToViewport(V2, 0.f, 0.f, mRTSize.x, mRTSize.y, 1.f, 100.f); V3 = M3D_V3TransformNDCToViewport(V3, 0.f, 0.f, mRTSize.x, mRTSize.y, 1.f, 100.f); // Face culling if (M3D_V4GetX(M3D_TNormal(V1,V2,V3))*0.5f <= 0) { if (objInFrustrum == DISJOINT) { v_tri[0].color = sf::Color::Red; v_tri[1].color = sf::Color::Red; v_tri[2].color = sf::Color::Red; } else if (objInFrustrum == INTERSECTS) { v_tri[0].color = sf::Color::Yellow; v_tri[1].color = sf::Color::Yellow; v_tri[2].color = sf::Color::Yellow; } else { v_tri[0].color = oMesh.vertices[indicePtr[i]].color; v_tri[1].color = oMesh.vertices[indicePtr[i+1]].color; v_tri[2].color = oMesh.vertices[indicePtr[i+2]].color; } v_tri[0].position = sf::Vector2f(M3D_V4GetX(V1), M3D_V4GetY(V1)); v_tri[1].position = sf::Vector2f(M3D_V4GetX(V2), M3D_V4GetY(V2)); v_tri[2].position = sf::Vector2f(M3D_V4GetX(V3), M3D_V4GetY(V3)); v_tri[3] = v_tri[0]; #ifdef DISABLE_WIREFRAME_MODE context.draw(v_tri, 4, sf::Triangles, sRS); #else context.draw(v_tri, 4, sf::LineStrip, sRS); #endif } } } } } } } void Graphic3DRenderer::UpdateInternalTestObjects() { static float thetaAngle = 0.31f; thetaAngle = thetaAngle >= 6.283185f ? -6.283185f : thetaAngle + 0.004f; static float thetaAngle2 = 2.12f; thetaAngle2 = thetaAngle2 >= 6.283185f ? -6.283185f : thetaAngle2 + 0.005f; static float thetaAngle3 = -4.78f; thetaAngle3 = thetaAngle3 >= 6.283185f ? -6.283185f : thetaAngle3 + 0.008f; mRenderList[0]->SetRotation(thetaAngle, 0.f, thetaAngle * 0.5f); mRenderList[1]->SetRotation(thetaAngle2, 0.f, thetaAngle2 * 0.5f); mRenderList[2]->SetRotation(thetaAngle3, 0.f, thetaAngle3 * 0.5f); mRenderList[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 - 1000.f * fovThetaSin) / (2.f * 1000.f * fovSin * thetaCos); } // Clamp if (sgRatio > 1.f) sgRatio = 1.f; else if (sgRatio < 0.f) sgRatio = 0.f; return sgRatio; } inline static bool VertexClipTest(M3D_F4& V, sf::Vector2f& RTsize, float gb_factor) { // Guard band are usually 2-3x the viewport size for the clipping test return (V.x > -RTsize.x*gb_factor*V.w && V.x < RTsize.y*gb_factor*V.w && V.y > -RTsize.x*gb_factor*V.w && V.y < RTsize.y*gb_factor*V.w ); }