[ChunkRenderer] Frustum culling implemented thanks to pgimeno's work.
See https://git.unarelith.net/Unarelith/OpenMiner/pulls/39
This commit is contained in:
parent
26fbc8737c
commit
791e41ce36
@ -33,6 +33,33 @@
|
||||
#include "ChunkRenderer.hpp"
|
||||
#include "ClientChunk.hpp"
|
||||
|
||||
inline static bool bbIntersects(const glm::vec3 &a0, const glm::vec3 &a1, const glm::vec3 &b0, const glm::vec3 &b1) {
|
||||
return (std::max(a0.x, b0.x) <= std::min(a1.x, b1.x))
|
||||
& (std::max(a0.y, b0.y) <= std::min(a1.y, b1.y))
|
||||
& (std::max(a0.z, b0.z) <= std::min(a1.z, b1.z));
|
||||
}
|
||||
|
||||
inline static bool isOutside(const glm::vec3 &v, const glm::vec3 &n) {
|
||||
// Return whether a vertex is on the positive side of the plane given by
|
||||
// position (0, 0, 0) and normal n.
|
||||
return glm::dot(v, n) >= 0.f;
|
||||
}
|
||||
|
||||
static bool fullyOutside(const glm::vec3 &v1, const glm::vec3 &v2, const glm::vec3 &n) {
|
||||
// A polyhedron is fully on one side of the axis iff all of its vertices
|
||||
// are. This function tests whether all 8 vertices of an axis-aligned box
|
||||
// given by the two corners v1, v2 are on the positive side of the plane
|
||||
// with normal n that goes through the origin.
|
||||
return isOutside(v1, n)
|
||||
&& isOutside(v2, n)
|
||||
&& isOutside(glm::vec3{v2.x, v1.y, v1.z}, n)
|
||||
&& isOutside(glm::vec3{v1.x, v2.y, v2.z}, n)
|
||||
&& isOutside(glm::vec3{v1.x, v2.y, v1.z}, n)
|
||||
&& isOutside(glm::vec3{v2.x, v1.y, v2.z}, n)
|
||||
&& isOutside(glm::vec3{v2.x, v2.y, v1.z}, n)
|
||||
&& isOutside(glm::vec3{v1.x, v1.y, v2.z}, n);
|
||||
}
|
||||
|
||||
void ChunkRenderer::draw(gk::RenderTarget &target, gk::RenderStates states, const ChunkMap &chunks, gk::Camera &camera, const Sky *currentSky) const {
|
||||
// Changing the values sent to the GPU to double precision is suicidal,
|
||||
// performance wise, if possible at all. Therefore we want to keep the
|
||||
@ -50,67 +77,185 @@ void ChunkRenderer::draw(gk::RenderTarget &target, gk::RenderStates states, cons
|
||||
// vertex coordinates passed to the renderer are all small, and single
|
||||
// precision floats suffice for the drawing.
|
||||
|
||||
gk::Vector3d cameraPos(camera.getDPosition());
|
||||
const gk::Vector3d cameraPos{camera.getDPosition()};
|
||||
camera.setDPosition(0, 0, 0); // Temporarily move the camera to the origin
|
||||
|
||||
// Calculate the Z of the normalized device coordinate of the render distance plane
|
||||
float farZ;
|
||||
{
|
||||
glm::vec4 farVec{0.f, 0.f, -float(Config::renderDistance * CHUNK_WIDTH), 1.f};
|
||||
farVec = target.getView()->getTransform().getMatrix() * farVec;
|
||||
farZ = farVec.z / farVec.w;
|
||||
}
|
||||
|
||||
// Pre-calculate product of Projection and View matrices
|
||||
const glm::mat4 pv = target.getView()->getTransform().getMatrix()
|
||||
* target.getView()->getViewTransform().getMatrix();
|
||||
|
||||
glm::vec3 frustumVertex[8]; // Frustum vertices
|
||||
|
||||
// Look-at vector (vector the camera is pointing to). It's the Z vector
|
||||
// of the inverse of the view matrix. Since the camera is at the origin,
|
||||
// the view matrix does not displace, therefore its inverse is the
|
||||
// transpose, possibly scaled. But we don't care if the lookat vector is
|
||||
// scaled for this purpose.
|
||||
const glm::vec3 lookAt{
|
||||
-target.getView()->getViewTransform().getMatrix()[0].z,
|
||||
-target.getView()->getViewTransform().getMatrix()[1].z,
|
||||
-target.getView()->getViewTransform().getMatrix()[2].z,
|
||||
};
|
||||
|
||||
glm::vec3 fbb0, fbb1; // Corners of the frustum's AABB
|
||||
{
|
||||
// Inverse of the PV, used to find the frustum corners in world space
|
||||
const glm::mat4 ipv{glm::inverse(pv)};
|
||||
|
||||
int i = 0;
|
||||
// Transform the corners of the NDC cube to camera coords.
|
||||
// That's where the frustum corners are. Note that we need the
|
||||
// frustum truncated by the render distance plane, so we do it here.
|
||||
for (float z = -1.f; z <= 1.f; z += 2.f)
|
||||
for (float y = -1.f; y <= 1.f; y += 2.f)
|
||||
for (float x = -1.f; x <= 1.f; x += 2.f) {
|
||||
// Truncate the frustum with the render distance
|
||||
glm::vec4 v{ipv * glm::vec4{x, y, std::min(z, farZ), 1.f}};
|
||||
|
||||
v.x /= v.w;
|
||||
v.y /= v.w;
|
||||
v.z /= v.w;
|
||||
|
||||
frustumVertex[i].x = v.x;
|
||||
frustumVertex[i].y = v.y;
|
||||
frustumVertex[i].z = v.z;
|
||||
++i;
|
||||
}
|
||||
// Find the axes-aligned bounding box of the frustum
|
||||
fbb0 = glm::vec3{frustumVertex[0]};
|
||||
fbb1 = glm::vec3{frustumVertex[0]};
|
||||
for (i = 1; i < 8; ++i) {
|
||||
// Bottom-left-front corner
|
||||
fbb0.x = std::min(fbb0.x, frustumVertex[i].x);
|
||||
fbb0.y = std::min(fbb0.y, frustumVertex[i].y);
|
||||
fbb0.z = std::min(fbb0.z, frustumVertex[i].z);
|
||||
|
||||
// Top-right-back corner
|
||||
fbb1.x = std::max(fbb1.x, frustumVertex[i].x);
|
||||
fbb1.y = std::max(fbb1.y, frustumVertex[i].y);
|
||||
fbb1.z = std::max(fbb1.z, frustumVertex[i].z);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare a list of chunks to draw
|
||||
std::vector<std::pair<ClientChunk*, gk::Transform>> chunksToDraw;
|
||||
for(auto &it : chunks) {
|
||||
if (!it.second->isInitialized()) continue;
|
||||
|
||||
it.second->setHasBeenDrawn(false);
|
||||
|
||||
gk::Transform tf = glm::translate(glm::mat4(1.0f),
|
||||
glm::vec3(it.second->x() * CHUNK_WIDTH - cameraPos.x,
|
||||
it.second->y() * CHUNK_DEPTH - cameraPos.y,
|
||||
it.second->z() * CHUNK_HEIGHT - cameraPos.z));
|
||||
|
||||
// Is the chunk close enough?
|
||||
glm::vec4 center = target.getView()->getViewTransform().getMatrix()
|
||||
* tf.getMatrix()
|
||||
* glm::vec4(CHUNK_WIDTH / 2, CHUNK_DEPTH / 2, CHUNK_HEIGHT / 2, 1);
|
||||
|
||||
// Nope, too far, don't render it
|
||||
if(glm::length(center) > (Config::renderDistance + 1.f) * CHUNK_WIDTH) {
|
||||
if(floor(glm::length(center)) > (Config::renderDistance + 2.f) * CHUNK_WIDTH) {
|
||||
it.second->setTooFar(true);
|
||||
|
||||
if (!it.second->isInitialized())
|
||||
m_onChunkDeletionRequested(gk::Vector3i{it.second->x(), it.second->y(), it.second->z()});
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
it.second->setTooFar(false);
|
||||
|
||||
if (!it.second->isInitialized()) continue;
|
||||
// Apply the Separating Axis Theorem to determine visibility of each
|
||||
// chunk. This theorem states that two convex figures don't intersect
|
||||
// if, and only if, there's an axis (a line in the case of 2D, a
|
||||
// plane in the case of 3D) such that one figure lies completely on
|
||||
// one side of the axis and the other lies completely on the other
|
||||
// side.
|
||||
//
|
||||
// Furthermore, if the figures are polygons (2D) or polyhedrons (3D),
|
||||
// one of the faces of one of them should be a separating axis, if
|
||||
// there's one. But this face can belong to either of the shapes, so
|
||||
// all faces of both shapes need to be checked until either one is
|
||||
// found which is a separating axis, or none is (in the latter case,
|
||||
// the shapes intersect).
|
||||
|
||||
// Is this chunk's centre on the screen?
|
||||
float d = glm::length2(center);
|
||||
center.x /= center.w;
|
||||
center.y /= center.w;
|
||||
// The goal here is to check if the frustum intersects the chunk's
|
||||
// bounding box, which is an axes-aligned box, and we take advantage
|
||||
// of that, because checking all the points of a shape against an
|
||||
// axis-aligned box is equivalent to checking intersection between
|
||||
// both shapes' axis-aligned bounding boxes. Therefore, our first
|
||||
// check will apply the theorem to the sides of the chunk's bounding
|
||||
// box, against the frustum's AABB which we have calculated before.
|
||||
// This effectively checks if any of the sides of the chunk's BB is a
|
||||
// separating axis, in the sense of the theorem.
|
||||
|
||||
// Find bottom-left-front corner of chunk's BB (in cameraPos coords)
|
||||
const glm::vec3 chunkBB0{
|
||||
it.second->x() * CHUNK_WIDTH - cameraPos.x,
|
||||
it.second->y() * CHUNK_DEPTH - cameraPos.y,
|
||||
it.second->z() * CHUNK_HEIGHT - cameraPos.z,
|
||||
};
|
||||
|
||||
// Find top-right-back corner of chunk's BB
|
||||
const glm::vec3 chunkBB1{
|
||||
chunkBB0.x + CHUNK_WIDTH,
|
||||
chunkBB0.y + CHUNK_DEPTH,
|
||||
chunkBB0.z + CHUNK_HEIGHT
|
||||
};
|
||||
|
||||
// Check if it's too far (temporary hack for removing chunks, until
|
||||
// it's performed with a timeout)
|
||||
const float dist = glm::length((chunkBB0 + chunkBB1) / 2.f);
|
||||
if (dist > glm::length(frustumVertex[4]) + glm::length(glm::vec3{CHUNK_WIDTH, CHUNK_DEPTH, CHUNK_HEIGHT}) / 2.f) {
|
||||
it.second->setTooFar(true);
|
||||
|
||||
// If it is behind the camera, don't bother drawing it.
|
||||
// Our screen coordinates are +X right, +Y up, and for a right-handed
|
||||
// coordinate system, depth must be negative Z, so anything with a
|
||||
// positive Z is behind the camera.
|
||||
if (center.z > CHUNK_MAXSIZE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it is outside the screen, don't bother drawing it
|
||||
if (fabsf(center.x) > 1 + fabsf(CHUNK_HEIGHT * 2 / center.w)
|
||||
|| fabsf(center.y) > 1 + fabsf(CHUNK_HEIGHT * 2 / center.w)) {
|
||||
// FIXME: Disable this test; one that considers all eight corners of the chunk is needed instead.
|
||||
//continue;
|
||||
}
|
||||
// Check if the frustum's BB intersects the chunk's BB.
|
||||
// This discards the big majority of chunks, and the more expensive
|
||||
// tests will be done only on a handful on them, of which roughly
|
||||
// one third will be visible.
|
||||
if (!bbIntersects(chunkBB0, chunkBB1, fbb0, fbb1))
|
||||
continue;
|
||||
|
||||
// Now it's the turn to check each of the frustum's faces against the
|
||||
// chunk's BB. If none of them is a separating axis, then the frustum
|
||||
// and the chunk's BB definitively intersect, i.e. the BB is visible.
|
||||
// We'll consider the frustum as a pyramid, i.e. we'll ignore the
|
||||
// near clipping plane. This is OK because the worst case is that
|
||||
// we'll render up to 4 more chunks than necessary, in the rare
|
||||
// event that the corner of the chunks is within the tiny pyramid
|
||||
// situated between the near plane and the camera's position. This
|
||||
// saves one of the checks.
|
||||
|
||||
// All frustum planes considered except one intersect the camera's
|
||||
// origin. For that one (see next check) we subtract the vertex from
|
||||
// the BB corners, to make the test correct.
|
||||
|
||||
// The far plane (or more precisely, the render distance plane) uses
|
||||
// the lookat vector as the normal. Any vertex on the far side of the
|
||||
// clipped frustum will do as position. This check is also used to
|
||||
// set the TooFar flag on the chunk.
|
||||
if (fullyOutside(chunkBB0 - frustumVertex[4], chunkBB1 - frustumVertex[4], lookAt))
|
||||
continue;
|
||||
|
||||
// Check the chunk's BB against the remaining 4 frustum planes.
|
||||
// Normals (cross products) must be calculated in right-hand order
|
||||
// so that all normals point outwards of the frustum (pyramid).
|
||||
// The top and bottom planes discard the most chunks, because the
|
||||
// frustum is narrower in the vertical direction, so they are checked
|
||||
// first.
|
||||
if (fullyOutside(chunkBB0, chunkBB1, glm::cross(frustumVertex[4], frustumVertex[5]))
|
||||
|| fullyOutside(chunkBB0, chunkBB1, glm::cross(frustumVertex[7], frustumVertex[6]))
|
||||
|| fullyOutside(chunkBB0, chunkBB1, glm::cross(frustumVertex[5], frustumVertex[7]))
|
||||
|| fullyOutside(chunkBB0, chunkBB1, glm::cross(frustumVertex[6], frustumVertex[4]))
|
||||
)
|
||||
continue;
|
||||
|
||||
// If this chunk is not initialized, skip it and request meshing
|
||||
if(!it.second->isReadyForMeshing()) {
|
||||
m_onMeshingRequested(d, gk::Vector3i{it.second->x(), it.second->y(), it.second->z()});
|
||||
m_onMeshingRequested(dist, gk::Vector3i{it.second->x(), it.second->y(), it.second->z()});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// The chunk's model matrix is a pure translation matrix whose origin
|
||||
// is the chunk's origin in cameraPos coordinates.
|
||||
const glm::mat4 tf{1.f, 0.f, 0.f, 0.f,
|
||||
0.f, 1.f, 0.f, 0.f,
|
||||
0.f, 0.f, 1.f, 0.f,
|
||||
chunkBB0.x, chunkBB0.y, chunkBB0.z, 1.f};
|
||||
|
||||
chunksToDraw.emplace_back(it.second.get(), tf);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user