365 lines
15 KiB
C++
365 lines
15 KiB
C++
#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 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<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, 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<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 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
|
||
);
|
||
} |