ProtoTank/Engine/Graphics/3DRenderer.cpp

304 lines
12 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)
// 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<Camera>();
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<ObjectDbgCube>());
mRenderList.back()->SetPosition(0.f, 0.f, 15.f);
mRenderList.back()->SetScale(2.0f);
mRenderList.push_back(std::make_shared<ObjectDbgCube>());
mRenderList.back()->SetPosition(6.f, 2.f, 2.f);
mRenderList.back()->SetScale(2.0f);
mRenderList.push_back(std::make_shared<ObjectDbgCube>());
mRenderList.back()->SetPosition(-8.f, 5.f, 10.f);
mRenderList.back()->SetScale(2.0f);
mRenderList.push_back(std::make_shared<Tank>());
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<const M3D_F3*>(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<const uint32_t*>(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 CyrusBeck 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 - mMainCamera->GetFarZ() * fovThetaSin)
/ (2.f * mMainCamera->GetFarZ() * 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
);
}