44 min to read
Mini-Minecraft Multithreaded Terrain Generation
Exploring the Intricacies of Concurrent World Building in Mini-Minecraft
![Featured image](https://medias.wangruipeng.com/Mini-Minecraft-Demo.png)
Mini-Minecraft
This is a mini-minecraft project written mainly by C++, Qt and OpenGL. I will describe the multithreading terrain generation code here.
You can access the original repository and the demo video here: WANG-Ruipeng/Mini-Minecraft (github.com)
This is mainly implemented in the terrain
class. Terrain class manages the chunks of terrain in the game. It handles terrain generation, block management within chunks, and rendering of the terrain.
- Major Members:
m_chunks
: Astd::unordered_map
that stores unique pointers (uPtr
) toChunk
objects. Chunks are indexed using a 64-bit integer key generated from their x and z coordinates.m_generatedTerrain
: Astd::unordered_set
which stores the index of the chunks that keeps track of the chunks that have been generatedm_chunksThatHaveBlockData
: Astd::unordered_set
that holds pointers toChunk
objects. This set specifically tracks chunks for which the block data (the type of blocks present in each chunk) has been generated but not yet processed into VBO (Vertex Buffer Object) data for rendering. This allows the system to know which chunks are ready for the next step in the rendering pipeline.m_chunksThatHaveBlockDataLock
: AQMutex
(Qt Mutex). This mutex is used to lock them_chunksThatHaveBlockData
set during read/write operations in a multithreaded environment, ensuring that no data races or concurrent modification issues occur when multiple threads are interacting with the set.m_chunksThatHaveVBOs
: Astd::vector
that holds pointers toChunkOpaqueTransparentVBOData
objects. This vector manages chunks for which VBO data has been generated and is ready to be sent to the GPU for rendering. The VBO data includes information necessary for rendering the chunk, such as vertex positions, colors, and normals.m_chunksThatHaveVBOsLock
: AQMutex
. Similar tom_chunksThatHaveBlockDataLock
, this mutex ensures safe access tom_chunksThatHaveVBOs
when multiple threads are potentially adding or removing elements from the vector, preventing data corruption or access violations.block_to_generate_id
: Astd::vector
of 64-bit integers (int64_t
). This vector holds the keys (as generated bytoKey
function) of chunks that need to have their block types generated. These chunks have been identified as necessary for generation based on the player’s position and the currently loaded terrain but haven’t had their block data generated yet.m_chunkCreated
: Anint
that likely keeps track of the number of chunks that have been fully processed and are considered “created” or finalized in the game world. This might be used for statistics, debugging, or limiting the number of chunks processed in a given frame or time period to spread out the workload.
#include "terrain.h"
#include <stdexcept>
#include <iostream>
Terrain::Terrain(OpenGLContext *context)
: m_chunks(), m_generatedTerrain(), mp_context(context), mp_texture(nullptr)
{}
Terrain::~Terrain() {
for (auto &i : m_chunks)
i.second->destroyVBOdata();
}
// Combine two 32-bit ints into one 64-bit int
// where the upper 32 bits are X and the lower 32 bits are Z
int64_t toKey(int x, int z) {
int64_t xz = 0xffffffffffffffff;
int64_t x64 = x;
int64_t z64 = z;
// Set all lower 32 bits to 1 so we can & with Z later
xz = (xz & (x64 << 32)) | 0x00000000ffffffff;
// Set all upper 32 bits to 1 so we can & with XZ
z64 = z64 | 0xffffffff00000000;
// Combine
xz = xz & z64;
return xz;
}
glm::ivec2 toCoords(int64_t k) {
// Z is lower 32 bits
int64_t z = k & 0x00000000ffffffff;
// If the most significant bit of Z is 1, then it's a negative number
// so we have to set all the upper 32 bits to 1.
// Note the 8 V
if(z & 0x0000000080000000) {
z = z | 0xffffffff00000000;
}
int64_t x = (k >> 32);
return glm::ivec2(x, z);
}
// Surround calls to this with try-catch if you don't know whether
// the coordinates at x, y, z have a corresponding Chunk
BlockType Terrain::getBlockAt(int x, int y, int z) const
{
if(hasChunkAt(x, z)) {
// Just disallow action below or above min/max height,
// but don't crash the game over it.
if(y < 0 || y >= 256) {
return EMPTY;
}
const uPtr<Chunk> &c = getChunkAt(x, z);
glm::vec2 chunkOrigin = glm::vec2(floor(x / 16.f) * 16, floor(z / 16.f) * 16);
return c->getBlockAt(static_cast<unsigned int>(x - chunkOrigin.x),
static_cast<unsigned int>(y),
static_cast<unsigned int>(z - chunkOrigin.y));
}
else {
throw std::out_of_range("Coordinates " + std::to_string(x) +
" " + std::to_string(y) + " " +
std::to_string(z) + " have no Chunk!");
}
}
BlockType Terrain::getBlockAt(glm::vec3 p) const {
return getBlockAt(p.x, p.y, p.z);
}
bool Terrain::hasChunkAt(int x, int z) const{
// Map x and z to their nearest Chunk corner
// By flooring x and z, then multiplying by 16,
// we clamp (x, z) to its nearest Chunk-space corner,
// then scale back to a world-space location.
// Note that floor() lets us handle negative numbers
// correctly, as floor(-1 / 16.f) gives us -1, as
// opposed to (int)(-1 / 16.f) giving us 0 (incorrect!).
int xFloor = static_cast<int>(glm::floor(x / 16.f));
int zFloor = static_cast<int>(glm::floor(z / 16.f));
bool ret = m_chunks.find(toKey(16 * xFloor, 16 * zFloor)) != m_chunks.end();
return ret;
}
uPtr<Chunk>& Terrain::getChunkAt(int x, int z) {
int xFloor = static_cast<int>(glm::floor(x / 16.f));
int zFloor = static_cast<int>(glm::floor(z / 16.f));
uPtr<Chunk>& ret = m_chunks[toKey(16 * xFloor, 16 * zFloor)];
return ret;
}
const uPtr<Chunk>& Terrain::getChunkAt(int x, int z) const {
int xFloor = static_cast<int>(glm::floor(x / 16.f));
int zFloor = static_cast<int>(glm::floor(z / 16.f));
const uPtr<Chunk>& ret = m_chunks.at(toKey(16 * xFloor, 16 * zFloor));
return ret;
}
void Terrain::setBlockAt(int x, int y, int z, BlockType t)
{
if(hasChunkAt(x, z)) {
uPtr<Chunk> &c = getChunkAt(x, z);
glm::vec2 chunkOrigin = glm::vec2(floor(x / 16.f) * 16, floor(z / 16.f) * 16);
c->setBlockAt(static_cast<unsigned int>(x - chunkOrigin.x),
static_cast<unsigned int>(y),
static_cast<unsigned int>(z - chunkOrigin.y),
t);
}
else {
throw std::out_of_range("Coordinates " + std::to_string(x) +
" " + std::to_string(y) + " " +
std::to_string(z) + " have no Chunk!");
}
}
Chunk* Terrain::instantiateChunkAt(int x, int z) {
uPtr<Chunk> chunk = mkU<Chunk>(x, z, mp_context);
Chunk *cPtr = chunk.get();
chunk->m_countOpq = 0;
chunk->m_countTra = 0;
m_chunks[toKey(x, z)] = std::move(chunk);
// Set the neighbor pointers of itself and its neighbors
if(hasChunkAt(x, z + 16)) {
auto &chunkNorth = m_chunks[toKey(x, z + 16)];
cPtr->linkNeighbor(chunkNorth, ZPOS);
}
if(hasChunkAt(x, z - 16)) {
auto &chunkSouth = m_chunks[toKey(x, z - 16)];
cPtr->linkNeighbor(chunkSouth, ZNEG);
}
if(hasChunkAt(x + 16, z)) {
auto &chunkEast = m_chunks[toKey(x + 16, z)];
cPtr->linkNeighbor(chunkEast, XPOS);
}
if(hasChunkAt(x - 16, z)) {
auto &chunkWest = m_chunks[toKey(x - 16, z)];
cPtr->linkNeighbor(chunkWest, XNEG);
}
return cPtr;
}
// TODO: When you make Chunk inherit from Drawable, change this code so
// it draws each Chunk with the given ShaderProgram, remembering to set the
// model matrix to the proper X and Z translation!
void Terrain::draw(int minX, int maxX, int minZ, int maxZ, ShaderProgram *shaderProgram, bool opaque)
{
// bind the texture
mp_texture->bind(0);
// need optimize!
// only draw chunk that has vbo data and within visible range!
for(int x = minX; x < maxX; x += 16) {
for(int z = minZ; z < maxZ; z += 16) {
if (hasChunkAt(x, z)){
const uPtr<Chunk> &chunk = getChunkAt(x, z);
if (opaque && chunk->m_countOpq <= 0)
continue;
if (!opaque && chunk->m_countTra <= 0)
continue;
shaderProgram->drawInterleaved(chunk.get(), opaque, 0);
}
}
}
}
std::unordered_set<int64_t> Terrain::borderingZone(glm::ivec2 zone, int radius) const {
int radiusInZoneScale = static_cast<int>(radius) * 64;
std::unordered_set<int64_t> result;
for (int i = -radiusInZoneScale; i <= radiusInZoneScale; i += 64) {
for (int j = -radiusInZoneScale; j <= radiusInZoneScale; j += 64) {
result.insert(toKey(zone.x + i, zone.y + j));
}
}
return result;
}
void Terrain::initialTerrainGeneration(glm::vec3 currentPlayerPos){
glm::ivec2 currentZone(64.f * glm::floor(currentPlayerPos.x / 64.f), 64.f * glm::floor(currentPlayerPos.z / 64.f));
std::unordered_set<int64_t> currentNearZones = borderingZone(currentZone, zoneRadius);
for (auto id : currentNearZones) {
//This zone id will alaways be ungenerated, but this is a check for safty's sake
//If get called multiple times, will not be generating blocks over and over.
if (m_generatedTerrain.count(id) == 0) {
spawnBlockTypeWorker(id);
}
//There is no previously generated block, obviously
}
QThreadPool::globalInstance()->waitForDone();
//No need to destroy VBO data.
//Generate VBO for newly generated chunks
m_chunksThatHaveBlockDataLock.lock();
spawnVBOWorkers(m_chunksThatHaveBlockData.size());
//m_chunksThatHaveBlockData.clear();
m_chunksThatHaveBlockDataLock.unlock();
QThreadPool::globalInstance()->waitForDone();
// Binding VBO data
m_chunksThatHaveVBOsLock.lock();
for (ChunkOpaqueTransparentVBOData* cd : m_chunksThatHaveVBOs) {
cd->mp_chunk->bindVBOdata();
}
if (m_chunkCreated < 25 * 4 * 4) {
m_chunkCreated += m_chunksThatHaveVBOs.size();
}
m_chunksThatHaveVBOs.clear();
m_chunksThatHaveVBOsLock.unlock();
}
void Terrain::multithreadedTerrainUpdate(glm::vec3 currentPlayerPos, glm::vec3 previousPlayerPos)
{
glm::ivec2 currentZone(64.f * glm::floor(currentPlayerPos.x / 64.f), 64.f * glm::floor(currentPlayerPos.z / 64.f));
glm::ivec2 previousZone(64.f * glm::floor(previousPlayerPos.x / 64.f), 64.f * glm::floor(previousPlayerPos.z / 64.f));
if (currentZone != previousZone){ // start generate new terrains
std::unordered_set<int64_t> currentNearZones = borderingZone(currentZone, zoneRadius);
std::unordered_set<int64_t> previousNearZones = borderingZone(previousZone, zoneRadius);
for (auto id : currentNearZones) {
//This zone id is ungenerated
if (m_generatedTerrain.count(id) == 0) {
//spawnBlockTypeWorker(id);
block_to_generate_id.push_back(id);
}
}
for (auto id : previousNearZones) {
if (currentNearZones.count(id) == 0) {
glm::ivec2 coord = toCoords(id);
for (int x = coord.x; x < coord.x + 64; x += 16) {
for (int z = coord.y; z < coord.y + 64; z += 16) {
auto& chunk = getChunkAt(x, z);
if(chunk) chunk->destroyVBOdata();
}
}
}
}
}
int block_to_generate_size, block_that_have_type_size, block_that_have_vbo_size;
// Generate n = 1 Block Data each tick
block_to_generate_size = block_to_generate_id.size();
spawnBlockTypeWorkers(2);
m_chunksThatHaveBlockDataLock.lock();
block_that_have_type_size = m_chunksThatHaveBlockData.size();
spawnVBOWorkers(8);
m_chunksThatHaveBlockDataLock.unlock();
// Binding VBO data
m_chunksThatHaveVBOsLock.lock();
block_that_have_vbo_size = m_chunksThatHaveVBOs.size();
bind_terrain_vbo_data(8);
m_chunksThatHaveVBOsLock.unlock();
}
void Terrain::spawnVBOWorkers(int n) {
// each call, we only spwan n workers to process n chunks
while (n-- && m_chunksThatHaveBlockData.size() > 0){
// pop the first element
Chunk* c = *m_chunksThatHaveBlockData.begin();
m_chunksThatHaveBlockData.erase(m_chunksThatHaveBlockData.begin());
if (c->m_blocks[0] != STONE){
printf("here");
continue;
}
spawnVBOWorker(c);
}
}
void Terrain::spawnVBOWorker(Chunk* chunkNeedingVBOData) {
VBOWorker* worker = new VBOWorker(
chunkNeedingVBOData, &m_chunksThatHaveVBOs, &m_chunksThatHaveVBOsLock, this
);
QThreadPool::globalInstance()->start(worker);
}
void Terrain::spawnBlockTypeWorkers(int n){
// call n block type worker each time
while (n-- && block_to_generate_id.size() > 0){
int64_t id = *block_to_generate_id.begin();
block_to_generate_id.erase(block_to_generate_id.begin());
spawnBlockTypeWorker(id);
}
}
void Terrain::bind_terrain_vbo_data(int n){
while (n-- && m_chunksThatHaveVBOs.size() > 0){
ChunkOpaqueTransparentVBOData* cd = *m_chunksThatHaveVBOs.begin();
if (cd->m_vboDataOpaque.size() + cd->m_vboDataTransparent.size() == 0)
printf("here");
cd->mp_chunk->bindVBOdata();
if (m_chunkCreated < 25 * 4 * 4) {
m_chunkCreated += 1;
}
m_chunksThatHaveVBOs.erase(m_chunksThatHaveVBOs.begin());
}
}
void Terrain::spawnBlockTypeWorker(int64_t zone) {
glm::ivec2 coord = toCoords(zone);
std::vector<Chunk*> chunksToFill;
for(int x = coord.x; x < coord.x + 64; x += 16) {
for(int z = coord.y; z < coord.y + 64; z += 16) {
Chunk* c = instantiateChunkAt(x, z);
chunksToFill.push_back(c);
}
}
BlockGenerateWorker* worker = new BlockGenerateWorker(
coord.x, coord.y, chunksToFill,
&m_chunksThatHaveBlockData, &m_chunksThatHaveBlockDataLock, this
);
QThreadPool::globalInstance()->start(worker);
m_generatedTerrain.insert(zone);
}
Then, for the specific block type, the following code snippet is part of a voxel-based terrain generation system, similar to that used in games like Minecraft. It is mainly implemented in Chunk
class. It is responsible for creating the block data for a chunk of the game world, determining the terrain shape, biome distribution, and populating it with trees based on the calculated biomes and heights.
void Chunk::createChunkBlockData(){
std::vector<std::vector<int>> heights(16, std::vector<int>(16));
std::vector<std::vector<BiomeType>> biomes(16, std::vector<BiomeType>(16));
for(int x = minX; x < minX + 16; ++x) {
for(int z = minZ; z < minZ + 16; ++z) {
BiomeType biome;
int height;
getHeight(x,z,height,biome);
heights[x-minX][z-minZ] = height;
biomes[x-minX][z-minZ] = biome;
fillTerrainBlocks(x, z, biome, height);
}
}
placeTree(heights, biomes);
}
void Chunk::placeTree(std::vector<std::vector<int>>& heights, std::vector<std::vector<BiomeType>>& biomes){
std::srand(std::time(nullptr) + minX + minZ);
int numTrees = std::rand() % 3;
std::vector<glm::vec2> treesPos;
auto isValid = [&treesPos](const glm::vec2& newPoint) {
for (const auto& point : treesPos) {
if (std::abs(point.x - newPoint.x) <= 4 && std::abs(point.y - newPoint.y) <= 4) {
return false;
}
}
return true;
};
int maxTry = 10;
int tryTimes = 0;
while (treesPos.size() < numTrees && tryTimes < maxTry) {
tryTimes++;
glm::vec2 newPoint = {std::rand() % 11 + 3, std::rand() % 11 + 3};
if (isValid(newPoint))
treesPos.push_back(newPoint);
}
for (const auto& treePos : treesPos) {
int x = static_cast<int>(treePos.x);
int z = static_cast<int>(treePos.y);
int floorHeight = heights[x][z];
if(biomes[x][z] != BiomeType::PLAIN)
continue;
for(int dy = 1; dy <= 5 ; dy++)
setBlockAt(x, floorHeight + dy, z, TRUNK);
for(int dy = 3; dy <= 4 ; dy++){
for(int dx = -2; dx <= 2 ; dx ++ ){
for(int dz = -2; dz <= 2 ; dz ++ ){
if(dx == 0 && dz == 0)
continue;
setBlockAt(x + dx, floorHeight + dy, z + dz, LEAF);
}
}
}
int dy = 5;
for(int dx = -2; dx <= 2 ; dx ++ ){
for(int dz = -2; dz <= 2 ; dz ++ ){
if((dx == 0 && dz == 0) ||
(dx == -2 && dz == -2) ||
(dx == -2 && dz == 2) ||
(dx == 2 && dz == 2) ||
(dx == 2 && dz == -2) )
continue;
setBlockAt(x + dx, floorHeight + dy, z + dz, LEAF);
}
}
dy = 6;
for(int dx = -1; dx <= 1 ; dx ++ ){
for(int dz = -1; dz <= 1 ; dz ++ ){
setBlockAt(x + dx, floorHeight + dy, z + dz, LEAF);
}
}
}
}
void Chunk::fillTerrainBlocks(int x, int z, BiomeType biome, int height) {
// Convert to local axis.
x -= minX;
z -= minZ;
try {
for (int y = 0; y <= 128; ++y) {
setBlockAt(x, y, z, STONE);
}
}catch(std::exception &e) {
std::cout << "Exception in fillTerrainBlocks STONE fill" << std::endl;
}
// Based on biome, fill above y = 128
for (int y = 129; y <= height; ++y) {
try {
switch (biome) {
case BiomeType::PLAIN:
if (y == height) {
setBlockAt(x, y, z, GRASS);
} else {
setBlockAt(x, y, z, DIRT);
}
break;
case BiomeType::HILL:
if (y == height) {
setBlockAt(x, y, z, GRASS);
} else if (height - y < 4) {
setBlockAt(x, y, z, DIRT);
}else {
setBlockAt(x, y, z, STONE);
}
break;
case BiomeType::DESSERT:
if (y == height) {
setBlockAt(x, y, z, STONE);
} else {
setBlockAt(x, y, z, DIRT);
}
break;
case BiomeType::RIVER:
setBlockAt(x, y, z, DIRT);
break;
default:
// Handle unknown biomes
setBlockAt(x, y, z, WATER);
break;
}
} catch(std::exception &e) {
std::cout << "Exception in fillTerrainBlocks y = [129, ?] loop, height = " << height << ", xz = " << x << "," << z << std::endl;
}
}
// Fill WATER if therew's empty space between 128 and 148
for (int y = 129; y < 146; ++y) {
try {
if (getBlockAt(x, y, z) == EMPTY) {
setBlockAt(x, y, z, WATER);
}
else if(getBlockAt(x, y, z) == GRASS) {
setBlockAt(x, y, z, DIRT);
}
}catch(std::exception &e) {
std::cout << "Exception in fillTerrainBlocks WATER table, y = " << y << ", xz = " << x << "," << z << std::endl;
}
}
/*
for (int y = 1; y < 64; ++y) {
float noiseValue = PerlinNoise3D(glm::vec3(x, y, z) * 0.05f);
if (noiseValue < 0 && getBlockAt(x, y, z) == STONE) {
setBlockAt(x, y, z, EMPTY);
}
if (y < 25) {
// Change for future LAVA
setBlockAt(x, y, z, LAVA);
}
}*/
}
void Chunk::getHeight(int x, int z, int& y, BiomeType& b) {
x += 10000;
z += 10000;
// Noise settings for biome determination and height variation.
const float biomeScale = 0.0025f; // Larger scale for biome determination.
const float terrainScale = 0.01f; // Terrain variation scale.
const int baseHeight = 145; // Base height for the terrain.
const float plainStart = -1;
const float plainEnd = 0.4;
const float desertStart = 0.5;
const float desertEnd = 0.8;
const float mountainStart = 0.9;
const float mountainEnd = 20;
float biomeNoiseValue = PerlinNoise2D(x * biomeScale, z * biomeScale, 1.0f, 2) * 2 + 0.5;
float height = baseHeight;
// Determine the biome based on the biomeNoiseValue
if (biomeNoiseValue >= plainStart && biomeNoiseValue <= plainEnd) { // Plains
height += PerlinNoise2D(x * terrainScale, z * terrainScale, 1.0f, 4) * 30 + 10;
b = BiomeType::PLAIN;
}
else if (biomeNoiseValue >= plainEnd && biomeNoiseValue <= desertStart) { // Transition between Plains and Desert
float plainsHeight = PerlinNoise2D(x * terrainScale, z * terrainScale, 1.0f, 4) * 30 + 10;
float desertHeight = PerlinNoise2D(x * terrainScale, z * terrainScale, 1.0f, 4) * 20 + 5;
float smoothStepInput = (biomeNoiseValue - plainEnd) / (desertStart - plainEnd);
float smoothStepResult = glm::smoothstep(0.0f, 1.0f, smoothStepInput);
height += plainsHeight * (1.0f - smoothStepResult) + desertHeight * smoothStepResult;
std::random_device rd;
std::mt19937 gen(rd());
std::normal_distribution<> dis(0.5, 0.2);
float u = dis(gen);
b = smoothStepResult < u ? BiomeType::PLAIN : BiomeType::DESSERT;
}
else if (biomeNoiseValue >= desertStart && biomeNoiseValue <= desertEnd) { // Desert
height += PerlinNoise2D(x * terrainScale, z * terrainScale, 1.0f, 4) * 20 + 5;
float des = (biomeNoiseValue - desertStart) / (desertEnd - desertStart);
height += sin(des * 3.14) * WorleyNoise(x * terrainScale * 0.2, z * terrainScale * 0.2) * 40;
b = BiomeType::DESSERT;
}
else if (biomeNoiseValue >= desertEnd && biomeNoiseValue <= mountainStart) { // Dessert and Mountains
float desertHeight = PerlinNoise2D(x * terrainScale, z * terrainScale, 1.0f, 4) * 20 + 5;
float mountainHeight = PerlinNoise2D(x * terrainScale, z * terrainScale, 1.0f, 4) * 80 + 10;
float smoothStepInput = (biomeNoiseValue - desertEnd) / (mountainStart - desertEnd);
float smoothStepResult = glm::smoothstep(0.0f, 1.0f, smoothStepInput);
float riverBedFactor = 1 - pow(cos(2 * M_PI * smoothStepResult),7.0);
float adjustedHeight = desertHeight * (1.0f - smoothStepResult) + mountainHeight * smoothStepResult;
height += adjustedHeight - 10 * riverBedFactor;
b = BiomeType::RIVER;
}
else if (biomeNoiseValue >= mountainStart && biomeNoiseValue <= mountainEnd) { // Mountains
height += PerlinNoise2D(x * terrainScale, z * terrainScale, 1.0f, 4) * 80 + 10;
b = BiomeType::HILL;
}
else{
height -= 50;
b = BiomeType::LAVA;
}
y = static_cast<int>(round(height));
y = std::min(255, std::max(0, y));
}
glm::vec2 Chunk::random2(glm::vec2 p) {
return glm::fract(glm::sin(glm::vec2(glm::dot(p, glm::vec2(127.1, 311.7)),
glm::dot(p, glm::vec2(269.5,183.3))))
* 43758.5453f);
}
float Chunk::surflet(glm::vec2 P, glm::vec2 gridPoint) {
float distX = glm::abs(P.x - gridPoint.x);
float distY = glm::abs(P.y - gridPoint.y);
float tX = 1.f - 6.f * glm::pow(distX, 5.f) + 15.f * glm::pow(distX, 4.f) - 10.f * glm::pow(distX, 3.f);
float tY = 1.f - 6.f * glm::pow(distY, 5.f) + 15.f * glm::pow(distY, 4.f) - 10.f * glm::pow(distY, 3.f);
glm::vec2 gradient = 2.f * random2(gridPoint) - glm::vec2(1.f);
glm::vec2 diff = P - gridPoint;
float height = glm::dot(diff, gradient);
return height * tX * tY;
}
float Chunk::perlinNoiseSingle(glm::vec2 uv) {
float surfletSum = 0.f;
for(int dx = 0; dx <= 1; ++dx) {
for(int dy = 0; dy <= 1; ++dy) {
surfletSum += surflet(uv, glm::vec2((int)uv.x + dx, (int)uv.y + dy));
}
}
return surfletSum;
}
float Chunk::PerlinNoise2D(float x, float z, float frequency, int octaves) {
float amplitude = 1.0f;
float maxAmplitude = 0.0f;
float noise = 0.0f;
glm::vec2 uv(x, z);
for(int i = 0; i < octaves; i++) {
noise += perlinNoiseSingle(uv * frequency) * amplitude;
maxAmplitude += amplitude;
amplitude *= 0.5f;
frequency *= 2.0f;
}
noise /= maxAmplitude;
return noise;
}
glm::vec2 Chunk::fract(glm::vec2 v) {
return glm::vec2(v.x - std::floor(v.x), v.y - std::floor(v.y));
}
glm::vec2 Chunk::floor(glm::vec2 v) {
return glm::vec2(std::floor(v.x), std::floor(v.y));
}
float Chunk::length(glm::vec2 v) {
return std::sqrt(v.x * v.x + v.y * v.y);
}
float Chunk::min(float a, float b) {
return (a < b) ? a : b;
}
float Chunk::WorleyNoise(float x, float y) {
glm::vec2 uv(x * 10.0f, y * 10.0f);
glm::vec2 uvInt = floor(uv);
glm::vec2 uvFract = fract(uv);
float minDist = 1.0f;
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
glm::vec2 neighbor(x, y);
glm::vec2 point = random2(uvInt + neighbor);
glm::vec2 diff = neighbor + point - uvFract;
float dist = length(diff);
minDist = min(minDist, dist);
}
}
return minDist;
}
glm::vec3 Chunk::random3(glm::vec3 p) {
// This should return a random glm::vec3 where each component is in the range [-1, 1]
// Adjust the numbers for the dot product to suit your seed needs
return glm::fract(glm::sin(glm::vec3(glm::dot(p, glm::vec3(127.1, 311.7, 74.7)),
glm::dot(p, glm::vec3(269.5, 183.3, 246.1)),
glm::dot(p, glm::vec3(113.5, 271.9, 124.6))))
* 43758.5453f) * 2.0f - 1.0f;
}
float Chunk::surflet(glm::vec3 p, glm::vec3 gridPoint) {
glm::vec3 t = glm::abs(p - gridPoint);
t = 1.f - 6.f * glm::pow(t, glm::vec3(5.0)) + 15.f * glm::pow(t, glm::vec3(4.0f)) - 10.f * glm::pow(t, glm::vec3(3.0f));
glm::vec3 gradient = random3(gridPoint);
glm::vec3 diff = p - gridPoint;
float height = glm::dot(diff, gradient);
return height * t.x * t.y * t.z;
}
float Chunk::PerlinNoise3D(glm::vec3 p) {
float surfletSum = 0.f;
for(int dx = 0; dx <= 1; ++dx) {
for(int dy = 0; dy <= 1; ++dy) {
for(int dz = 0; dz <= 1; ++dz) {
surfletSum += surflet(p, glm::floor(p) + glm::vec3(dx, dy, dz));
}
}
}
return surfletSum;
}