ProtoTank/Engine/Graphics/3DRenderer.cpp

364 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "3DRenderer.hpp"
#include <SFML/Graphics/RectangleShape.hpp>
#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)
// 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<Camera>();
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<ObjectDbgCube>());
mWorldObjsList.back()->SetPosition(0.f, 0.f, 15.f);
mWorldObjsList.back()->SetScale(2.0f);
mWorldObjsList.push_back(std::make_shared<ObjectDbgCube>());
mWorldObjsList.back()->SetPosition(6.f, 2.f, 2.f);
mWorldObjsList.back()->SetScale(2.0f);
mWorldObjsList.push_back(std::make_shared<ObjectDbgCube>());
mWorldObjsList.back()->SetPosition(-8.f, 5.f, 10.f);
mWorldObjsList.back()->SetScale(2.0f);
mWorldObjsList.push_back(std::make_shared<Tank>());
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<Tank>());
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, 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) {
#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 - 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;
}
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<RenderItem> 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<M3D_F4> 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<const M3D_F3*>(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<const uint32_t*>(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 CyrusBeck 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, 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);
// 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
);
}