Mesh building is now asynchronous.

- Added ChunkMeshBuilder to replace ChunkBuilder
- Added ThreadPool

Note: Surrounding chunks are not handled yet.
This commit is contained in:
Quentin Bazin 2021-05-31 02:58:50 +02:00
parent b41739146e
commit 2496f2e9ee
10 changed files with 984 additions and 15 deletions

244
external/thread/ThreadPool.hpp vendored Normal file
View File

@ -0,0 +1,244 @@
/**
* The ThreadPool class.
* Keeps a set of threads constantly waiting to execute incoming jobs.
*
* Written by Will Pearce
* From http://roar11.com/2016/01/a-platform-independent-thread-pool-using-c14/
* License: BSD-2 (http://roar11.com/2016/01/a-platform-independent-thread-pool-using-c14/#license)
*/
#ifndef THREADPOOL_HPP
#define THREADPOOL_HPP
#include "ThreadSafeQueue.hpp"
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <functional>
#include <future>
#include <memory>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>
namespace thread
{
class ThreadPool
{
private:
class IThreadTask
{
public:
IThreadTask(void) = default;
virtual ~IThreadTask(void) = default;
IThreadTask(const IThreadTask& rhs) = delete;
IThreadTask& operator=(const IThreadTask& rhs) = delete;
IThreadTask(IThreadTask&& other) = default;
IThreadTask& operator=(IThreadTask&& other) = default;
/**
* Run the task.
*/
virtual void execute() = 0;
};
template <typename Func>
class ThreadTask: public IThreadTask
{
public:
ThreadTask(Func&& func)
:m_func{std::move(func)}
{
}
~ThreadTask(void) override = default;
ThreadTask(const ThreadTask& rhs) = delete;
ThreadTask& operator=(const ThreadTask& rhs) = delete;
ThreadTask(ThreadTask&& other) = default;
ThreadTask& operator=(ThreadTask&& other) = default;
/**
* Run the task.
*/
void execute() override
{
m_func();
}
private:
Func m_func;
};
public:
/**
* A wrapper around a std::future that adds the behavior of futures returned from std::async.
* Specifically, this object will block and wait for execution to finish before going out of scope.
*/
template <typename T>
class TaskFuture
{
public:
TaskFuture(std::future<T>&& future)
:m_future{std::move(future)}
{
}
TaskFuture(const TaskFuture& rhs) = delete;
TaskFuture& operator=(const TaskFuture& rhs) = delete;
TaskFuture(TaskFuture&& other) = default;
TaskFuture& operator=(TaskFuture&& other) = default;
~TaskFuture(void)
{
if(m_future.valid())
{
m_future.get();
}
}
const std::future<T> &future() const { return m_future; }
auto get(void)
{
return m_future.get();
}
private:
std::future<T> m_future;
};
public:
/**
* Constructor.
*/
ThreadPool(void)
:ThreadPool{std::max(std::thread::hardware_concurrency(), 2u) - 1u}
{
/*
* Always create at least one thread. If hardware_concurrency() returns 0,
* subtracting one would turn it to UINT_MAX, so get the maximum of
* hardware_concurrency() and 2 before subtracting 1.
*/
}
/**
* Constructor.
*/
explicit ThreadPool(const std::uint32_t numThreads)
:m_done{false},
m_workQueue{},
m_threads{}
{
try
{
for(std::uint32_t i = 0u; i < numThreads; ++i)
{
m_threads.emplace_back(&ThreadPool::worker, this);
}
}
catch(...)
{
destroy();
throw;
}
}
/**
* Non-copyable.
*/
ThreadPool(const ThreadPool& rhs) = delete;
/**
* Non-assignable.
*/
ThreadPool& operator=(const ThreadPool& rhs) = delete;
/**
* Destructor.
*/
~ThreadPool(void)
{
destroy();
}
/**
* Submit a job to be run by the thread pool.
*/
template <typename Func, typename... Args>
auto submit(Func&& func, Args&&... args)
{
auto boundTask = std::bind(std::forward<Func>(func), std::forward<Args>(args)...);
using ResultType = std::result_of_t<decltype(boundTask)()>;
using PackagedTask = std::packaged_task<ResultType()>;
using TaskType = ThreadTask<PackagedTask>;
PackagedTask task{std::move(boundTask)};
TaskFuture<ResultType> result{task.get_future()};
m_workQueue.push(std::make_unique<TaskType>(std::move(task)));
return result;
}
private:
/**
* Constantly running function each thread uses to acquire work items from the queue.
*/
void worker(void)
{
while(!m_done)
{
std::unique_ptr<IThreadTask> pTask{nullptr};
if(m_workQueue.waitPop(pTask))
{
pTask->execute();
}
}
}
/**
* Invalidates the queue and joins all running threads.
*/
void destroy(void)
{
m_done = true;
m_workQueue.invalidate();
for(auto& thread : m_threads)
{
if(thread.joinable())
{
thread.join();
}
}
}
private:
std::atomic_bool m_done;
ThreadSafeQueue<std::unique_ptr<IThreadTask>> m_workQueue;
std::vector<std::thread> m_threads;
};
namespace DefaultThreadPool
{
/**
* Get the default thread pool for the application.
* This pool is created with std::thread::hardware_concurrency() - 1 threads.
*/
inline ThreadPool& getThreadPool(void)
{
static ThreadPool defaultPool;
return defaultPool;
}
/**
* Submit a job to the default thread pool.
*/
template <typename Func, typename... Args>
inline auto submitJob(Func&& func, Args&&... args)
{
return getThreadPool().submit(std::forward<Func>(func), std::forward<Args>(args)...);
}
}
}
#endif

136
external/thread/ThreadSafeQueue.hpp vendored Normal file
View File

@ -0,0 +1,136 @@
/**
* The ThreadSafeQueue class.
* Provides a wrapper around a basic queue to provide thread safety.
*
* Written by Will Pearce
* From http://roar11.com/2016/01/a-platform-independent-thread-pool-using-c14/
* License: BSD-2 (http://roar11.com/2016/01/a-platform-independent-thread-pool-using-c14/#license)
*/
#ifndef THREADSAFEQUEUE_HPP
#define THREADSAFEQUEUE_HPP
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <queue>
#include <utility>
namespace thread
{
template <typename T>
class ThreadSafeQueue
{
public:
/**
* Destructor.
*/
~ThreadSafeQueue(void)
{
invalidate();
}
/**
* Attempt to get the first value in the queue.
* Returns true if a value was successfully written to the out parameter, false otherwise.
*/
bool tryPop(T& out)
{
std::lock_guard<std::mutex> lock{m_mutex};
if(m_queue.empty() || !m_valid)
{
return false;
}
out = std::move(m_queue.front());
m_queue.pop();
return true;
}
/**
* Get the first value in the queue.
* Will block until a value is available unless clear is called or the instance is destructed.
* Returns true if a value was successfully written to the out parameter, false otherwise.
*/
bool waitPop(T& out)
{
std::unique_lock<std::mutex> lock{m_mutex};
m_condition.wait(lock, [this]()
{
return !m_queue.empty() || !m_valid;
});
/*
* Using the condition in the predicate ensures that spurious wakeups with a valid
* but empty queue will not proceed, so only need to check for validity before proceeding.
*/
if(!m_valid)
{
return false;
}
out = std::move(m_queue.front());
m_queue.pop();
return true;
}
/**
* Push a new value onto the queue.
*/
void push(T value)
{
std::lock_guard<std::mutex> lock{m_mutex};
m_queue.push(std::move(value));
m_condition.notify_one();
}
/**
* Check whether or not the queue is empty.
*/
bool empty(void) const
{
std::lock_guard<std::mutex> lock{m_mutex};
return m_queue.empty();
}
/**
* Clear all items from the queue.
*/
void clear(void)
{
std::lock_guard<std::mutex> lock{m_mutex};
while(!m_queue.empty())
{
m_queue.pop();
}
m_condition.notify_all();
}
/**
* Invalidate the queue.
* Used to ensure no conditions are being waited on in waitPop when
* a thread or the application is trying to exit.
* The queue is invalid after calling this method and it is an error
* to continue using a queue after this method has been called.
*/
void invalidate(void)
{
std::lock_guard<std::mutex> lock{m_mutex};
m_valid = false;
m_condition.notify_all();
}
/**
* Returns whether or not this queue is valid.
*/
bool isValid(void) const
{
std::lock_guard<std::mutex> lock{m_mutex};
return m_valid;
}
private:
std::atomic_bool m_valid{true};
mutable std::mutex m_mutex;
std::queue<T> m_queue;
std::condition_variable m_condition;
};
}
#endif

View File

@ -51,18 +51,10 @@ std::array<std::size_t, ChunkBuilder::layers> ChunkBuilder::buildChunk(const Cli
const BlockState &blockState = block.getState(block.param().hasParam(BlockParam::State)
? block.param().getParam(BlockParam::State, blockParam) : 0);
if (blockState.drawType() == BlockDrawType::Solid
|| blockState.drawType() == BlockDrawType::Leaves
|| blockState.drawType() == BlockDrawType::Liquid
|| blockState.drawType() == BlockDrawType::Glass
|| blockState.drawType() == BlockDrawType::Cactus
|| blockState.drawType() == BlockDrawType::BoundingBox)
{
addCube(x, y, z, chunk, blockState, blockParam);
}
else if (blockState.drawType() == BlockDrawType::XShape) {
if (blockState.drawType() == BlockDrawType::XShape)
addCross(x, y, z, chunk, blockState);
}
else
addCube(x, y, z, chunk, blockState, blockParam);
}
}
}

View File

@ -0,0 +1,454 @@
/*
* =====================================================================================
*
* OpenMiner
*
* Copyright (C) 2018-2020 Unarelith, Quentin Bazin <openminer@unarelith.net>
* Copyright (C) 2019-2020 the OpenMiner contributors (see CONTRIBUTORS.md)
*
* This file is part of OpenMiner.
*
* OpenMiner is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* OpenMiner is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with OpenMiner; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* =====================================================================================
*/
#include "BlockGeometry.hpp"
#include "ChunkMeshBuilder.hpp"
#include "ClientWorld.hpp"
#include "TextureAtlas.hpp"
using namespace BlockGeometry;
void ChunkMeshBuilder::addMeshBuildingJob(const Chunk &chunk, const TextureAtlas &textureAtlas) {
ChunkMeshBuildingJob job;
job.textureAtlas = &textureAtlas;
job.chunkData.x = chunk.x();
job.chunkData.y = chunk.y();
job.chunkData.z = chunk.z();
std::memcpy(&job.chunkData.data, &chunk.data(), sizeof(Chunk::DataArray));
std::memcpy(&job.chunkData.lightData, &chunk.lightmap().data(), sizeof(ChunkLightmap::LightMapArray));
auto future = m_threadPool.submit([](ChunkMeshBuildingJob job) {
for (s8f z = 0 ; z < CHUNK_HEIGHT ; z++) {
for (s8f y = 0 ; y < CHUNK_DEPTH ; y++) {
for (s8f x = 0 ; x < CHUNK_WIDTH ; x++) {
u16 blockID = job.chunkData.getBlockID(x, y, z);
if (!blockID) continue;
u16 blockParam = job.chunkData.getBlockParam(x, y, z);
const BlockState &blockState = job.chunkData.getBlockState(blockID, blockParam);
if (blockState.drawType() == BlockDrawType::XShape)
addCross(x, y, z, job, blockState);
else
addCube(x, y, z, job, blockState, blockParam);
}
}
}
return job;
}, job);
m_futures.emplace_back(std::move(future));
}
void ChunkMeshBuilder::update() {
for (auto it = m_futures.begin() ; it != m_futures.end() ; ) {
if (it->future().valid() && it->future().wait_for(std::chrono::milliseconds(0)) == std::future_status::ready) {
ChunkMeshBuildingJob job = it->get();
ClientChunk *chunk = (ClientChunk *)m_world.getChunk(job.chunkData.x, job.chunkData.y, job.chunkData.z);
if (chunk) {
for (u8 i = 0 ; i < ChunkBuilder::layers ; ++i) {
job.vertices[i].shrink_to_fit();
const gk::VertexBuffer &vbo = chunk->getVertexBuffer(i);
gk::VertexBuffer::bind(&vbo);
vbo.setData(job.vertices[i].size() * sizeof(Vertex), job.vertices[i].data(), GL_DYNAMIC_DRAW);
gk::VertexBuffer::bind(nullptr);
chunk->setVerticesCount(i, job.vertices[i].size());
}
}
it = m_futures.erase(it);
}
else
++it;
}
}
inline void ChunkMeshBuilder::addCube(s8f x, s8f y, s8f z, ChunkMeshBuildingJob &job, const BlockState &blockState, u16 blockParam) {
const gk::FloatBox &boundingBox = blockState.boundingBox();
u8f orientation = blockState.block().isRotatable()
? blockState.block().param().getParam(BlockParam::Rotation, blockParam) : 0;
const glm::mat3 &orientMatrix = orientMatrices[orientation];
glm::vec3 vertexPos[nVertsPerCube]{
// Order is important. It matches the bit order defined in BlockGeometry::cubeVerts.
{boundingBox.x, boundingBox.y, boundingBox.z},
{boundingBox.x + boundingBox.sizeX, boundingBox.y, boundingBox.z},
{boundingBox.x, boundingBox.y + boundingBox.sizeY, boundingBox.z},
{boundingBox.x + boundingBox.sizeX, boundingBox.y + boundingBox.sizeY, boundingBox.z},
{boundingBox.x, boundingBox.y, boundingBox.z + boundingBox.sizeZ},
{boundingBox.x + boundingBox.sizeX, boundingBox.y, boundingBox.z + boundingBox.sizeZ},
{boundingBox.x, boundingBox.y + boundingBox.sizeY, boundingBox.z + boundingBox.sizeZ},
{boundingBox.x + boundingBox.sizeX, boundingBox.y + boundingBox.sizeY, boundingBox.z + boundingBox.sizeZ},
};
if (blockState.drawType() == BlockDrawType::Cactus) {
// Ignore bounding box, initialize it to full node coordinates
for (u8f i = 0; i < nVertsPerCube; ++i) {
vertexPos[i].x = (i >> 0) & 1;
vertexPos[i].y = (i >> 1) & 1;
vertexPos[i].z = (i >> 2) & 1;
}
}
// vNeighbour is used to find neighbouring cubes per vertex.
// Same binary layout.
glm::vec3 vNeighbour[nVertsPerCube] = {
{-1,-1,-1}, { 1,-1,-1}, {-1, 1,-1}, { 1, 1,-1}, {-1,-1, 1}, { 1,-1, 1}, {-1, 1, 1}, {1, 1, 1},
};
if (orientation) { // don't work extra if it's not oriented differently
static const glm::vec3 half{0.5, 0.5, 0.5};
// Rotate each vertex coordinate around the centre of the
// cube, and each vertex neighbour around the origin
for (int i = 0; i < nVertsPerCube; ++i) {
vertexPos[i] = orientMatrix * (vertexPos[i] - half) + half;
vNeighbour[i] = orientMatrix * vNeighbour[i];
}
}
for (s8f f = 0; f < nFaces ; ++f) {
// Construct the normal vector to a face
const glm::vec3 glmNormal = orientMatrix * faceNormals[f];
const gk::Vector3i normal{int(glmNormal.x), int(glmNormal.y), int(glmNormal.z)};
// Construct an array with the 4 vertex positions of this face
glm::vec3 *faceVerts[nVertsPerFace]{
&vertexPos[cubeVerts[f][0]], &vertexPos[cubeVerts[f][1]],
&vertexPos[cubeVerts[f][2]], &vertexPos[cubeVerts[f][3]]
};
// Construct an array with the 4 vertex neighbours of this face
// (as GameKit integer vectors)
const gk::Vector3i corner0{int(vNeighbour[cubeVerts[f][0]].x), int(vNeighbour[cubeVerts[f][0]].y), int(vNeighbour[cubeVerts[f][0]].z)};
const gk::Vector3i corner1{int(vNeighbour[cubeVerts[f][1]].x), int(vNeighbour[cubeVerts[f][1]].y), int(vNeighbour[cubeVerts[f][1]].z)};
const gk::Vector3i corner2{int(vNeighbour[cubeVerts[f][2]].x), int(vNeighbour[cubeVerts[f][2]].y), int(vNeighbour[cubeVerts[f][2]].z)};
const gk::Vector3i corner3{int(vNeighbour[cubeVerts[f][3]].x), int(vNeighbour[cubeVerts[f][3]].y), int(vNeighbour[cubeVerts[f][3]].z)};
const gk::Vector3i *vFaceNeighbours[nVertsPerFace]{&corner0, &corner1, &corner2, &corner3};
addCubeFace(x, y, z, f, job, blockState, normal, faceVerts, vFaceNeighbours);
}
}
inline void ChunkMeshBuilder::addCubeFace(s8f x, s8f y, s8f z, s8f f, ChunkMeshBuildingJob &job,
const BlockState &blockState,
const gk::Vector3i &normal,
const glm::vec3 *const vertexPos[nVertsPerFace],
const gk::Vector3i *const neighbourOfs[nVertsPerFace])
{
// Get surrounding block for the face
s8f sx = x + normal.x;
s8f sy = y + normal.y;
s8f sz = z + normal.z;
const BlockState *surroundingBlockState = job.chunkData.getBlockState(sx, sy, sz);
// Skip hidden faces
if (surroundingBlockState && surroundingBlockState->block().id()
&& ((blockState.drawType() == BlockDrawType::Solid && surroundingBlockState->drawType() == BlockDrawType::Solid && surroundingBlockState->isOpaque())
|| (blockState.block().id() == surroundingBlockState->block().id() && (blockState.drawType() == BlockDrawType::Liquid || blockState.drawType() == BlockDrawType::Glass))
|| (blockState.drawType() == BlockDrawType::Liquid && surroundingBlockState->drawType() == BlockDrawType::Solid)
|| (blockState.drawType() == BlockDrawType::Cactus && surroundingBlockState->block().id() == blockState.block().id() && f > 3)))
return;
const gk::FloatBox &boundingBox = blockState.boundingBox();
const std::string &texture = blockState.tiles().getTextureForFace(f);
const gk::FloatRect &blockTexCoords = job.textureAtlas->getTexCoords(texture);
// Calculate UV's
// These are tough to obtain. Note that texture Y grows in the up-down direction, and so does V.
// Vertex index in the bitmap array and U/V correspondence is:
// U0V0 -> 3 2 <- U1V0
// U0V1 -> 0 1 <- U1V1
float U0, V0, U1, V1;
if (blockState.drawType() == BlockDrawType::Cactus) {
U0 = 0.f;
V0 = 0.f;
U1 = 1.f;
V1 = 1.f;
}
else {
U0 = (f == 0) ? 1.f - (boundingBox.y + boundingBox.sizeY) : (f == 1) ? boundingBox.y :
(f == 3) ? 1.f - (boundingBox.x + boundingBox.sizeX) : boundingBox.x;
V0 = (f <= 3) ? 1.f - (boundingBox.z + boundingBox.sizeZ) : (f == 4) ? boundingBox.y : 1.f - (boundingBox.y + boundingBox.sizeY);
U1 = (f == 0) ? 1.f - boundingBox.y : (f == 1) ? boundingBox.y + boundingBox.sizeY :
(f == 3) ? 1.f - boundingBox.x : boundingBox.x + boundingBox.sizeX;
V1 = (f <= 3) ? 1.f - boundingBox.z : (f == 4) ? boundingBox.y + boundingBox.sizeY : 1.f - boundingBox.y;
}
// Prepare vertex information for VBO
Vertex vertices[nVertsPerFace];
for (s8f v = 0; v < nVertsPerFace; ++v) {
if (blockState.drawType() == BlockDrawType::Cactus) {
vertices[v].coord3d[0] = x + vertexPos[v]->x - boundingBox.x * normal.x;
vertices[v].coord3d[1] = y + vertexPos[v]->y - boundingBox.y * normal.y;
vertices[v].coord3d[2] = z + vertexPos[v]->z - boundingBox.z * normal.z;
}
else {
float blockHeight = vertexPos[v]->z;
if (blockState.drawType() == BlockDrawType::Liquid && (f != BlockFace::Bottom || !surroundingBlockState || !surroundingBlockState->block().id())) {
if (f == BlockFace::Bottom)
blockHeight = vertexPos[v]->z - 2.f / 16.f;
else
blockHeight = vertexPos[v]->z * 14.f / 16.f;
}
vertices[v].coord3d[0] = x + vertexPos[v]->x;
vertices[v].coord3d[1] = y + vertexPos[v]->y;
vertices[v].coord3d[2] = z + blockHeight;
}
vertices[v].coord3d[0] += blockState.drawOffset().x;
vertices[v].coord3d[1] += blockState.drawOffset().y;
vertices[v].coord3d[2] += blockState.drawOffset().z;
vertices[v].coord3d[3] = f;
vertices[v].normal[0] = normal.x;
vertices[v].normal[1] = normal.y;
vertices[v].normal[2] = normal.z;
const gk::Color colorMultiplier = blockState.colorMultiplier();
vertices[v].color[0] = colorMultiplier.r;
vertices[v].color[1] = colorMultiplier.g;
vertices[v].color[2] = colorMultiplier.b;
vertices[v].color[3] = colorMultiplier.a;
float U = (v == 0 || v == 3) ? U0 : U1;
float V = (v >= 2) ? V0 : V1;
vertices[v].texCoord[0] = gk::qlerp(blockTexCoords.x, blockTexCoords.x + blockTexCoords.sizeX, U);
vertices[v].texCoord[1] = gk::qlerp(blockTexCoords.y, blockTexCoords.y + blockTexCoords.sizeY, V);
if (Config::isSmoothLightingEnabled)
vertices[v].lightValue[0] = getLightForVertex(Light::Sun, x, y, z, *neighbourOfs[v], normal, job.chunkData);
else
vertices[v].lightValue[0] = job.chunkData.getSunlight(sx, sy, sz);
if (Config::isSmoothLightingEnabled && !blockState.isLightSource())
vertices[v].lightValue[1] = getLightForVertex(Light::Torch, x, y, z, *neighbourOfs[v], normal, job.chunkData);
else if (blockState.isOpaque())
vertices[v].lightValue[1] = job.chunkData.getTorchlight(sx, sy, sz);
else
vertices[v].lightValue[1] = job.chunkData.getTorchlight(x, y, z);
vertices[v].ambientOcclusion = getAmbientOcclusion(x, y, z, *neighbourOfs[v], normal, job.chunkData);
}
auto addVertex = [&](u8 v) {
if (Config::ambientOcclusion != 1 || blockState.isLightSource())
vertices[v].ambientOcclusion = 5;
if (blockState.drawType() == BlockDrawType::Liquid)
job.vertices[Layer::Liquid].emplace_back(vertices[v]);
else if (blockState.drawType() == BlockDrawType::Glass)
job.vertices[Layer::Glass].emplace_back(vertices[v]);
else if (blockState.colorMultiplier() != gk::Color::White)
job.vertices[Layer::NoMipMap].emplace_back(vertices[v]);
else
job.vertices[Layer::Solid].emplace_back(vertices[v]);
};
// Flipping quad to fix anisotropy issue
if (vertices[0].ambientOcclusion + vertices[2].ambientOcclusion >
vertices[1].ambientOcclusion + vertices[3].ambientOcclusion) {
addVertex(0);
addVertex(1);
addVertex(2);
addVertex(2);
addVertex(3);
addVertex(0);
} else {
addVertex(0);
addVertex(1);
addVertex(3);
addVertex(3);
addVertex(1);
addVertex(2);
}
}
inline void ChunkMeshBuilder::addCross(s8f x, s8f y, s8f z, ChunkMeshBuildingJob &job, const BlockState &blockState) {
glm::vec3 vertexPos[nVertsPerCube]{
{0, 0, 0},
{1, 0, 0},
{0, 1, 0},
{1, 1, 0},
{0, 0, 1},
{1, 0, 1},
{0, 1, 1},
{1, 1, 1},
};
const glm::vec3 *const faceVertices[nCrossFaces][nVertsPerFace]{
{&vertexPos[crossVerts[0][0]], &vertexPos[crossVerts[0][1]],
&vertexPos[crossVerts[0][2]], &vertexPos[crossVerts[0][3]]},
{&vertexPos[crossVerts[1][0]], &vertexPos[crossVerts[1][1]],
&vertexPos[crossVerts[1][2]], &vertexPos[crossVerts[1][3]]},
};
const std::string &texture = blockState.tiles().getTextureForFace(0);
const gk::FloatRect &blockTexCoords = job.textureAtlas->getTexCoords(texture);
float faceTexCoords[nVertsPerFace][nCoordsPerUV] = {
{blockTexCoords.x, blockTexCoords.y + blockTexCoords.sizeY},
{blockTexCoords.x + blockTexCoords.sizeX, blockTexCoords.y + blockTexCoords.sizeY},
{blockTexCoords.x + blockTexCoords.sizeX, blockTexCoords.y},
{blockTexCoords.x, blockTexCoords.y},
};
for (int f = 0; f < nCrossFaces ; ++f) {
Vertex vertices[nVertsPerFace];
for (int v = 0 ; v < nVertsPerFace ; ++v) {
vertices[v].coord3d[0] = x + faceVertices[f][v]->x + blockState.drawOffset().x;
vertices[v].coord3d[1] = y + faceVertices[f][v]->y + blockState.drawOffset().y;
vertices[v].coord3d[2] = z + faceVertices[f][v]->z + blockState.drawOffset().z;
vertices[v].coord3d[3] = 6;
vertices[v].normal[0] = 0;
vertices[v].normal[1] = 0;
vertices[v].normal[2] = 0;
const gk::Color colorMultiplier = blockState.colorMultiplier();
vertices[v].color[0] = colorMultiplier.r;
vertices[v].color[1] = colorMultiplier.g;
vertices[v].color[2] = colorMultiplier.b;
vertices[v].color[3] = colorMultiplier.a;
vertices[v].texCoord[0] = faceTexCoords[v][0];
vertices[v].texCoord[1] = faceTexCoords[v][1];
vertices[v].lightValue[0] = job.chunkData.getSunlight(x, y, z);
vertices[v].lightValue[1] = job.chunkData.getTorchlight(x, y, z);
vertices[v].ambientOcclusion = 5;
}
job.vertices[Layer::Flora].emplace_back(vertices[0]);
job.vertices[Layer::Flora].emplace_back(vertices[1]);
job.vertices[Layer::Flora].emplace_back(vertices[3]);
job.vertices[Layer::Flora].emplace_back(vertices[3]);
job.vertices[Layer::Flora].emplace_back(vertices[1]);
job.vertices[Layer::Flora].emplace_back(vertices[2]);
}
}
// Based on this article: https://0fps.net/2013/07/03/ambient-occlusion-for-minecraft-like-worlds/
inline u8 ChunkMeshBuilder::getAmbientOcclusion(s8f x, s8f y, s8f z, const gk::Vector3i &offset, const gk::Vector3i &normal, const ChunkData &chunk) {
gk::Vector3i minOffset{
(normal.x != 0) ? offset.x : 0,
(normal.y != 0) ? offset.y : 0,
(normal.z != 0) ? offset.z : 0
};
const BlockState *blocks[4] = {
chunk.getBlockState(x + minOffset.x, y + minOffset.y, z + offset.z),
chunk.getBlockState(x + offset.x, y + minOffset.y, z + minOffset.z),
chunk.getBlockState(x + minOffset.x, y + offset.y, z + minOffset.z),
chunk.getBlockState(x + offset.x, y + offset.y, z + offset.z)
};
bool blockPresence[4] = {
blocks[0] && blocks[0]->block().id() != 0 && blocks[0]->isOpaque(),
blocks[1] && blocks[1]->block().id() != 0 && blocks[1]->isOpaque(),
blocks[2] && blocks[2]->block().id() != 0 && blocks[2]->isOpaque(),
blocks[3] && blocks[3]->block().id() != 0 && blocks[3]->isOpaque()
};
bool side1 = blockPresence[(minOffset.x != 0) ? 2 : 1];
bool side2 = blockPresence[(minOffset.z != 0) ? 2 : 0];
bool corner = blockPresence[3];
return (side1 && side2) ? 0 : 3 - (side1 + side2 + corner);
}
inline u8 ChunkMeshBuilder::getLightForVertex(Light light, s8f x, s8f y, s8f z, const gk::Vector3i &offset, const gk::Vector3i &normal, const ChunkData &chunk) {
std::function<s8(const ChunkData &chunk, s8, s8, s8)> getLight = [&](const ChunkData &chunk, s8 x, s8 y, s8 z) -> s8 {
// if (x < 0) return chunk->getSurroundingChunk(0) && chunk->getSurroundingChunk(0)->isInitialized() ? getLight(chunk->getSurroundingChunk(0), x + CHUNK_WIDTH, y, z) : -1;
// if (x >= CHUNK_WIDTH) return chunk->getSurroundingChunk(1) && chunk->getSurroundingChunk(1)->isInitialized() ? getLight(chunk->getSurroundingChunk(1), x - CHUNK_WIDTH, y, z) : -1;
// if (y < 0) return chunk->getSurroundingChunk(2) && chunk->getSurroundingChunk(2)->isInitialized() ? getLight(chunk->getSurroundingChunk(2), x, y + CHUNK_DEPTH, z) : -1;
// if (y >= CHUNK_DEPTH) return chunk->getSurroundingChunk(3) && chunk->getSurroundingChunk(3)->isInitialized() ? getLight(chunk->getSurroundingChunk(3), x, y - CHUNK_DEPTH, z) : -1;
// if (z < 0) return chunk->getSurroundingChunk(4) && chunk->getSurroundingChunk(4)->isInitialized() ? getLight(chunk->getSurroundingChunk(4), x, y, z + CHUNK_HEIGHT) : -1;
// if (z >= CHUNK_HEIGHT) return chunk->getSurroundingChunk(5) && chunk->getSurroundingChunk(5)->isInitialized() ? getLight(chunk->getSurroundingChunk(5), x, y, z - CHUNK_HEIGHT) : -1;
//
// if (light == Light::Sun)
// return chunk->isInitialized() ? chunk->lightmap().getSunlight(x, y, z) : -1;
// else
// return chunk->isInitialized() ? chunk->lightmap().getTorchlight(x, y, z) : -1;
return (light == Light::Sun)
? chunk.getSunlight(x, y, z)
: chunk.getTorchlight(x, y, z);
};
gk::Vector3i minOffset{
(normal.x != 0) ? offset.x : 0,
(normal.y != 0) ? offset.y : 0,
(normal.z != 0) ? offset.z : 0
};
gk::Vector3i surroundingBlocks[4]{
{x + minOffset.x, y + minOffset.y, z + offset.z},
{x + offset.x, y + minOffset.y, z + minOffset.z},
{x + minOffset.x, y + offset.y, z + minOffset.z},
{x + offset.x, y + offset.y, z + offset.z}
};
// Get light values for surrounding nodes
s8 lightValues[4] = {
getLight(chunk, surroundingBlocks[0].x, surroundingBlocks[0].y, surroundingBlocks[0].z),
getLight(chunk, surroundingBlocks[1].x, surroundingBlocks[1].y, surroundingBlocks[1].z),
getLight(chunk, surroundingBlocks[2].x, surroundingBlocks[2].y, surroundingBlocks[2].z),
getLight(chunk, surroundingBlocks[3].x, surroundingBlocks[3].y, surroundingBlocks[3].z),
};
float count = 0, total = 0;
for (u8 i = 0 ; i < 4 ; ++i) {
// Fix light approximation
// if (i == 3 && lightValues[i] > lightValues[0] && !lightValues[1] && !lightValues[2])
// continue;
// If the chunk is initialized, add the light value to the total
// But only add dark blocks if AO is set on Smooth Lighting
if (lightValues[i] != -1 && (Config::ambientOcclusion == 2 || lightValues[i] != 0)) {
total += lightValues[i];
++count;
}
}
if (count)
return total / count;
else
return 0;
}

View File

@ -0,0 +1,124 @@
/*
* =====================================================================================
*
* OpenMiner
*
* Copyright (C) 2018-2020 Unarelith, Quentin Bazin <openminer@unarelith.net>
* Copyright (C) 2019-2020 the OpenMiner contributors (see CONTRIBUTORS.md)
*
* This file is part of OpenMiner.
*
* OpenMiner is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* OpenMiner is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with OpenMiner; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* =====================================================================================
*/
#ifndef CHUNKMESHBUILDER_HPP_
#define CHUNKMESHBUILDER_HPP_
#include <thread/ThreadPool.hpp>
#include "Chunk.hpp"
#include "ChunkBuilder.hpp"
#include "Registry.hpp"
struct ChunkData {
s32 x, y, z;
Chunk::DataArray data;
ChunkLightmap::LightMapArray lightData;
u16 getBlockID(s8f x, s8f y, s8f z) const { return data[z][y][x] & 0xffff; }
u16 getBlockParam(s8f x, s8f y, s8f z) const { return (data[z][y][x] >> 16) & 0xffff; }
u8 getTorchlight(s8f x, s8f y, s8f z) const { return lightData[z][y][x] & 0xf; }
u8 getSunlight(s8f x, s8f y, s8f z) const { return (lightData[z][y][x] >> 4) & 0xf; }
const BlockState &getBlockState(u16 blockID, u16 blockParam) const {
const Block &block = Registry::getInstance().getBlock(blockID);
return block.getState(block.param().hasParam(BlockParam::State) ?
block.param().getParam(BlockParam::State, blockParam) : 0);
}
const BlockState *getBlockState(s8f x, s8f y, s8f z) const {
if (x >= 0 && x < CHUNK_WIDTH
&& y >= 0 && y < CHUNK_DEPTH
&& z >= 0 && z < CHUNK_HEIGHT) {
u16 blockID = getBlockID(x, y, z);
u16 blockParam = getBlockParam(x, y, z);
return &getBlockState(blockID, blockParam);
}
return nullptr;
}
};
struct ChunkMeshBuildingJob {
using VerticesArray = std::array<std::vector<Vertex>, ChunkBuilder::layers>;
ChunkData chunkData;
VerticesArray vertices;
const TextureAtlas *textureAtlas;
};
class ClientWorld;
class ChunkMeshBuilder {
public:
ChunkMeshBuilder(ClientWorld &world) : m_world(world) {}
void addMeshBuildingJob(const Chunk &chunk, const TextureAtlas &textureAtlas);
void update();
enum Layer {
Solid,
NoMipMap,
Flora,
Glass,
Liquid,
Count
};
private:
static void addCross(s8f x, s8f y, s8f z, ChunkMeshBuildingJob &job, const BlockState &blockState);
static void addCube(s8f x, s8f y, s8f z, ChunkMeshBuildingJob &job, const BlockState &blockState, u16 blockParam);
static void addCubeFace(s8f x, s8f y, s8f z, s8f f, ChunkMeshBuildingJob &job,
const BlockState &blockState,
const gk::Vector3i &normal, const glm::vec3 *const vertexPos[4],
const gk::Vector3i *const neighbourOfs[4]);
enum class Light {
Sun,
Torch
};
static u8 getAmbientOcclusion(s8f x, s8f y, s8f z, const gk::Vector3i &offset,
const gk::Vector3i &normal, const ChunkData &chunk);
static u8 getLightForVertex(Light light, s8f x, s8f y, s8f z, const gk::Vector3i &offset,
const gk::Vector3i &normal, const ChunkData &chunk);
ClientWorld &m_world;
thread::ThreadPool m_threadPool;
std::vector<thread::ThreadPool::TaskFuture<ChunkMeshBuildingJob>> m_futures;
};
#endif // CHUNKMESHBUILDER_HPP_

View File

@ -27,6 +27,7 @@
#include <gk/gl/GLCheck.hpp>
#include "ClientChunk.hpp"
#include "ClientWorld.hpp"
#include "TextureAtlas.hpp"
#include "World.hpp"
@ -49,7 +50,8 @@ void ClientChunk::update() {
void ClientChunk::process() {
if (m_isReadyForMeshing) {
m_verticesCount = m_builder.buildChunk(*this, m_vbo);
// m_verticesCount = m_builder.buildChunk(*this, m_vbo);
m_world.buildChunk(*this);
++ClientChunk::chunkUpdateCounter;
}

View File

@ -34,12 +34,14 @@
#include "Config.hpp"
#include "Dimension.hpp"
class ClientWorld;
class TextureAtlas;
class ClientChunk : public Chunk {
public:
ClientChunk(s32 x, s32 y, s32 z, const Dimension &dimension, World &world, TextureAtlas &textureAtlas)
: Chunk(x, y, z, world), m_dimension(dimension), m_textureAtlas(textureAtlas), m_builder{textureAtlas} {}
ClientChunk(s32 x, s32 y, s32 z, const Dimension &dimension, ClientWorld &world, TextureAtlas &textureAtlas)
: Chunk(x, y, z, (World &)world), m_world(world), m_dimension(dimension),
m_textureAtlas(textureAtlas), m_builder(textureAtlas) {}
void update() final;
void process() final;
@ -61,6 +63,10 @@ class ClientChunk : public Chunk {
bool areAllNeighboursTooFar() const;
const gk::VertexBuffer &getVertexBuffer(u8 layer) { return m_vbo[layer]; }
void setVerticesCount(u8 layer, std::size_t count) { m_verticesCount[layer] = count; }
int debugTimesReceived = 0; // Only used by Minimap
static u32 chunkUpdatesPerSec;
@ -68,6 +74,8 @@ class ClientChunk : public Chunk {
static u64 chunkUpdateTime;
private:
ClientWorld &m_world;
const Dimension &m_dimension;
TextureAtlas &m_textureAtlas;

View File

@ -73,6 +73,8 @@ void ClientWorld::update(bool allowWorldReload) {
requestClosestChunkMeshing();
m_scene.update();
m_chunkMeshBuilder.update();
}
void ClientWorld::requestClosestChunkMeshing() {

View File

@ -34,6 +34,7 @@
#include <gk/core/EventHandler.hpp>
#include <gk/gl/Camera.hpp>
#include "ChunkMeshBuilder.hpp"
#include "ClientChunk.hpp"
#include "ClientScene.hpp"
#include "Network.hpp"
@ -62,6 +63,8 @@ class ClientWorld : public World, public gk::Drawable {
Chunk *getChunk(int cx, int cy, int cz) const override;
void buildChunk(Chunk &chunk) { m_chunkMeshBuilder.addMeshBuildingJob(chunk, m_textureAtlas); }
const ClientScene &scene() const { return m_scene; }
ClientScene &scene() { return m_scene; }
@ -97,6 +100,8 @@ class ClientWorld : public World, public gk::Drawable {
const Sky *m_sky = nullptr;
mutable std::set<gk::Vector3i> m_chunksToRemove;
ChunkMeshBuilder m_chunkMeshBuilder{*this};
};
#endif // CLIENTWORLD_HPP_

View File

@ -73,12 +73,14 @@ class ChunkLightmap {
bool hasChanged() const { return m_hasChanged; }
void resetChangedFlag() { m_hasChanged = false; }
using LightMapArray = u8[CHUNK_HEIGHT][CHUNK_DEPTH][CHUNK_WIDTH];
const LightMapArray &data() const { return m_lightMap; }
private:
bool setTorchlight(int x, int y, int z, u8 val);
Chunk *m_chunk = nullptr;
using LightMapArray = u8[CHUNK_HEIGHT][CHUNK_DEPTH][CHUNK_WIDTH];
LightMapArray m_lightMap;
std::queue<LightNode> m_torchlightBfsQueue;