Skip to content

Commit 2400128

Browse files
committed
Add textures and some biomes
1 parent f35f3ad commit 2400128

File tree

13 files changed

+1128
-225
lines changed

13 files changed

+1128
-225
lines changed

CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,16 @@ add_executable(game
4747
src/camera.cpp
4848
src/chunk.cpp
4949
src/chunkmanager.cpp
50+
src/textureatlas.cpp
5051
)
5152

53+
# ── Windows: hide terminal in Release, keep it in Debug ──────────────────────
54+
if(WIN32)
55+
set_target_properties(game PROPERTIES
56+
WIN32_EXECUTABLE $<CONFIG:Release>
57+
)
58+
endif()
59+
5260
target_include_directories(game PRIVATE src)
5361
target_link_libraries(game PRIVATE Vulkan::Vulkan glfw cglm)
5462

src/app.cpp

Lines changed: 213 additions & 43 deletions
Large diffs are not rendered by default.

src/app.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
#define GLFW_INCLUDE_VULKAN
44
#include <GLFW/glfw3.h>
55
#include <cglm/cglm.h>
6+
#include <random>
67

78
#include "chunkmanager.h"
8-
99
#include <array>
1010
#include <optional>
1111
#include <string>
@@ -59,6 +59,12 @@ class App {
5959
VkDeviceMemory depthMemory = VK_NULL_HANDLE;
6060
VkImageView depthView = VK_NULL_HANDLE;
6161

62+
// ── texture atlas ─────────────────────────────────────────────────────────
63+
VkImage atlasImage = VK_NULL_HANDLE;
64+
VkDeviceMemory atlasMemory = VK_NULL_HANDLE;
65+
VkImageView atlasView = VK_NULL_HANDLE;
66+
VkSampler atlasSampler = VK_NULL_HANDLE;
67+
6268
// ── framebuffers ──────────────────────────────────────────────────────
6369
std::vector<VkFramebuffer> framebuffers;
6470

@@ -86,7 +92,7 @@ class App {
8692
bool framebufferResized = false;
8793

8894
// ── world ─────────────────────────────────────────────────────────────
89-
ChunkManager chunkManager{42};
95+
ChunkManager chunkManager{static_cast<unsigned int>(std::random_device{}())};
9096

9197
// ── helpers ───────────────────────────────────────────────────────────
9298
struct QueueFamilies {
@@ -113,6 +119,7 @@ class App {
113119
void createDepthResources();
114120
void createFramebuffers();
115121
void createCommandPool();
122+
void createTextureAtlas();
116123
void createUniformBuffers();
117124
void createDescriptorPool();
118125
void createDescriptorSets();

src/chunk.cpp

Lines changed: 293 additions & 73 deletions
Large diffs are not rendered by default.

src/chunk.h

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,53 @@
11
#pragma once
22

3+
#include "textureatlas.h"
4+
#include <functional>
35
#include <vector>
46
#include <cstdint>
57

68
static constexpr int CHUNK_SIZE = 16;
7-
static constexpr int RENDER_DIST = 4; // chunks in each direction
9+
static constexpr int CHUNK_HEIGHT = 64;
10+
static constexpr int RENDER_DIST = 8;
811

912
enum class BlockType : uint8_t {
1013
Air = 0,
1114
Bedrock = 1,
1215
Stone = 2,
1316
Dirt = 3,
1417
Grass = 4,
18+
Wood = 5,
19+
Leaves = 6,
20+
Snow = 7,
21+
};
22+
23+
enum class Biome : uint8_t {
24+
Plains = 0,
25+
Hills = 1,
26+
Mountains = 2,
1527
};
1628

1729
struct Chunk {
18-
BlockType blocks[CHUNK_SIZE][CHUNK_SIZE][CHUNK_SIZE]{};
30+
BlockType blocks[CHUNK_SIZE][CHUNK_HEIGHT][CHUNK_SIZE]{};
31+
32+
void generateTerrain(int chunkX, int chunkZ, unsigned int seed = 42);
1933

20-
// chunkX/chunkZ are integer chunk coords (not world-space pixels)
21-
void generate(int chunkX, int chunkZ, unsigned int seed = 42);
34+
using NeighborFn = std::function<Chunk*(int chunkX, int chunkZ)>;
35+
void generateDecorations(int chunkX, int chunkZ,
36+
unsigned int seed,
37+
const NeighborFn& getNeighbor);
2238

23-
// Returns interleaved x y z r g b floats (6 floats per vertex)
24-
// worldX/worldZ are applied as offsets so vertices are in world space
25-
std::vector<float> buildMesh(int chunkX, int chunkZ) const;
39+
// neighbors[dx+1][dz+1] for dx,dz in {-1,0,1}
40+
// neighbors[1][1] is this chunk (can be nullptr for edge chunks)
41+
// Used for inter-chunk face culling
42+
std::vector<float> buildMesh(int chunkX, int chunkZ,
43+
Chunk* neighbors[3][3] = nullptr) const;
2644

2745
bool isSolid(int x, int y, int z) const;
46+
47+
// Returns solid state checking neighbor chunks if x/z out of bounds
48+
bool isSolidWorld(int x, int y, int z, Chunk* neighbors[3][3]) const;
49+
50+
static void setWorldBlock(int wx, int wy, int wz, BlockType type,
51+
int chunkX, int chunkZ,
52+
const NeighborFn& getNeighbor);
2853
};

src/chunkmanager.cpp

Lines changed: 184 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,97 @@
11
#include "chunkmanager.h"
22
#include <cmath>
3+
#include <algorithm>
4+
5+
void logMsg(const std::string& msg);
36

47
ChunkManager::ChunkManager(unsigned int seed)
58
: seed(seed)
69
, pool(std::max(2u, std::thread::hardware_concurrency() - 1))
7-
// Leave one core for the main/render thread
8-
{}
10+
{
11+
logMsg("ChunkManager: seed=" + std::to_string(seed) +
12+
" threads=" + std::to_string(std::max(2u, std::thread::hardware_concurrency() - 1)));
13+
}
914

15+
// ─────────────────────────────────────────────────────────────────────────────
16+
// Public API
17+
// ─────────────────────────────────────────────────────────────────────────────
1018
void ChunkManager::update(int playerChunkX, int playerChunkZ) {
11-
// ── 1. Build the set of chunks that SHOULD exist ──────────────────────
1219
std::unordered_map<ChunkPos, bool, ChunkPosHash> desired;
1320
for (int dx = -RENDER_DIST; dx <= RENDER_DIST; dx++) {
1421
for (int dz = -RENDER_DIST; dz <= RENDER_DIST; dz++) {
15-
// Optional: circular render distance (feels more natural)
1622
if (dx*dx + dz*dz > RENDER_DIST*RENDER_DIST) continue;
17-
ChunkPos p{playerChunkX + dx, playerChunkZ + dz};
18-
desired[p] = true;
23+
desired[{playerChunkX+dx, playerChunkZ+dz}] = true;
1924
}
2025
}
2126

2227
std::unique_lock<std::mutex> lock(mapMtx);
2328

24-
// ── 2. Schedule generation for new chunks ─────────────────────────────
29+
// ── Schedule terrain for new chunks ──────────────────────────────────────
2530
for (auto& [pos, _] : desired) {
2631
if (chunkStatus.find(pos) == chunkStatus.end()) {
27-
chunkStatus[pos] = ChunkStatus::Pending;
28-
scheduleChunk(pos);
32+
chunkStatus[pos] = ChunkStatus::TerrainPending;
33+
chunkData[pos] = std::make_unique<Chunk>();
34+
lock.unlock();
35+
scheduleTerrainGen(pos);
36+
lock.lock();
2937
}
3038
}
3139

32-
// ── 3. Mark chunks outside range for unloading ────────────────────────
33-
std::vector<ChunkPos> toRemove;
40+
// ── Advance TerrainDone → DecorationPending ───────────────────────────────
41+
// Requires all 8 neighbors to be at least TerrainDone
42+
std::vector<ChunkPos> readyForDeco;
3443
for (auto& [pos, status] : chunkStatus) {
35-
if (desired.find(pos) == desired.end()) {
36-
// Only unload chunks that are already uploaded (or pending/generating)
37-
// For pending/generating we just let them finish and immediately unload
38-
toRemove.push_back(pos);
39-
}
44+
if (status == ChunkStatus::TerrainDone && allNeighborsAtLeast(pos, ChunkStatus::TerrainDone))
45+
readyForDeco.push_back(pos);
4046
}
41-
lock.unlock();
42-
43-
if (!toRemove.empty()) {
44-
std::unique_lock<std::mutex> ulock(unloadMtx);
45-
for (auto& p : toRemove) unloadQueue.push_back(p);
46-
std::unique_lock<std::mutex> mlock(mapMtx);
47-
for (auto& p : toRemove) chunkStatus.erase(p);
47+
for (auto& pos : readyForDeco) {
48+
chunkStatus[pos] = ChunkStatus::DecorationPending;
49+
lock.unlock();
50+
scheduleDecoration(pos);
51+
lock.lock();
4852
}
49-
}
5053

51-
void ChunkManager::scheduleChunk(ChunkPos pos) {
52-
// Note: mapMtx is already held by the caller (update)
53-
chunkStatus[pos] = ChunkStatus::Generating;
54+
// ── Advance DecorationDone → MeshPending ─────────────────────────────────
55+
// Requires all 8 neighbors to also be DecorationDone or later
56+
// This ensures no neighbor will write leaves into us after our mesh is built
57+
std::vector<ChunkPos> readyForMesh;
58+
for (auto& [pos, status] : chunkStatus) {
59+
if (status == ChunkStatus::DecorationDone && allNeighborsAtLeast(pos, ChunkStatus::DecorationDone))
60+
readyForMesh.push_back(pos);
61+
}
62+
for (auto& pos : readyForMesh) {
63+
chunkStatus[pos] = ChunkStatus::MeshPending;
64+
lock.unlock();
65+
scheduleMesh(pos);
66+
lock.lock();
67+
}
5468

55-
pool.enqueue([this, pos]() {
56-
// ── Stage 1: generate block data ──────────────────────────────────
57-
auto chunk = std::make_unique<Chunk>();
58-
chunk->generate(pos.x, pos.z, seed);
69+
// ── Unload out-of-range chunks ────────────────────────────────────────────
70+
std::vector<ChunkPos> toRemove;
71+
for (auto& [pos, _] : chunkStatus)
72+
if (desired.find(pos) == desired.end())
73+
toRemove.push_back(pos);
5974

60-
// ── Stage 2: build mesh ───────────────────────────────────────────
61-
std::vector<float> verts = chunk->buildMesh(pos.x, pos.z);
75+
lock.unlock();
6276

63-
// ── Check if chunk was unloaded while we were working ─────────────
77+
if (!toRemove.empty()) {
6478
{
65-
std::unique_lock<std::mutex> lock(mapMtx);
66-
auto it = chunkStatus.find(pos);
67-
if (it == chunkStatus.end()) return; // was unloaded, discard
68-
it->second = ChunkStatus::MeshReady;
79+
std::unique_lock<std::mutex> ul(unloadMtx);
80+
for (auto& p : toRemove) unloadQueue.push_back(p);
6981
}
70-
71-
// ── Stage 3: hand off to main thread for GPU upload ───────────────
72-
{
73-
std::unique_lock<std::mutex> lock(uploadMtx);
74-
uploadQueue.push(UploadRequest{pos, std::move(verts)});
82+
std::unique_lock<std::mutex> ml(mapMtx);
83+
for (auto& p : toRemove) {
84+
chunkStatus.erase(p);
85+
chunkData.erase(p);
7586
}
76-
});
87+
}
7788
}
7889

79-
void ChunkManager::drainUploadQueue(std::vector<UploadRequest>& outRequests, int maxPerFrame) {
90+
void ChunkManager::drainUploadQueue(std::vector<UploadRequest>& out, int maxPerFrame) {
8091
std::unique_lock<std::mutex> lock(uploadMtx);
8192
int count = 0;
8293
while (!uploadQueue.empty() && count < maxPerFrame) {
83-
outRequests.push_back(std::move(uploadQueue.front()));
94+
out.push_back(std::move(uploadQueue.front()));
8495
uploadQueue.pop();
8596
count++;
8697
}
@@ -104,3 +115,130 @@ void ChunkManager::getUploadedChunks(std::vector<ChunkPos>& out) const {
104115
for (auto& [pos, status] : chunkStatus)
105116
if (status == ChunkStatus::Uploaded) out.push_back(pos);
106117
}
118+
119+
Chunk* ChunkManager::getChunk(int cx, int cz) {
120+
std::unique_lock<std::mutex> lock(mapMtx);
121+
auto it = chunkData.find({cx, cz});
122+
if (it == chunkData.end()) return nullptr;
123+
return it->second.get();
124+
}
125+
126+
// ─────────────────────────────────────────────────────────────────────────────
127+
// Internal helpers
128+
// ─────────────────────────────────────────────────────────────────────────────
129+
bool ChunkManager::allNeighborsAtLeast(ChunkPos pos, ChunkStatus minStatus) {
130+
// mapMtx must be held by caller
131+
for (int dx = -1; dx <= 1; dx++) {
132+
for (int dz = -1; dz <= 1; dz++) {
133+
if (dx == 0 && dz == 0) continue;
134+
auto it = chunkStatus.find({pos.x+dx, pos.z+dz});
135+
if (it == chunkStatus.end()) return false;
136+
if (static_cast<int>(it->second) < static_cast<int>(minStatus)) return false;
137+
}
138+
}
139+
return true;
140+
}
141+
142+
// ─────────────────────────────────────────────────────────────────────────────
143+
// Stage 1: Terrain generation
144+
// ─────────────────────────────────────────────────────────────────────────────
145+
void ChunkManager::scheduleTerrainGen(ChunkPos pos) {
146+
logMsg("scheduleTerrainGen: (" + std::to_string(pos.x) + "," + std::to_string(pos.z) + ")");
147+
pool.enqueue([this, pos]() {
148+
Chunk* myChunk = nullptr;
149+
{
150+
std::unique_lock<std::mutex> lock(mapMtx);
151+
auto it = chunkData.find(pos);
152+
if (it == chunkData.end()) return;
153+
myChunk = it->second.get();
154+
}
155+
if (!myChunk) return;
156+
157+
myChunk->generateTerrain(pos.x, pos.z, seed);
158+
159+
{
160+
std::unique_lock<std::mutex> lock(mapMtx);
161+
auto sit = chunkStatus.find(pos);
162+
if (sit != chunkStatus.end()) sit->second = ChunkStatus::TerrainDone;
163+
}
164+
});
165+
}
166+
167+
// ─────────────────────────────────────────────────────────────────────────────
168+
// Stage 2: Decoration (trees) — neighbors are all TerrainDone
169+
// ─────────────────────────────────────────────────────────────────────────────
170+
void ChunkManager::scheduleDecoration(ChunkPos pos) {
171+
logMsg("scheduleDecoration: (" + std::to_string(pos.x) + "," + std::to_string(pos.z) + ")");
172+
pool.enqueue([this, pos]() {
173+
Chunk* myChunk = nullptr;
174+
{
175+
std::unique_lock<std::mutex> lock(mapMtx);
176+
auto it = chunkData.find(pos);
177+
if (it == chunkData.end()) return;
178+
myChunk = it->second.get();
179+
}
180+
if (!myChunk) return;
181+
182+
// neighborFn: gets neighbor chunk pointer safely
183+
auto neighborFn = [this](int cx, int cz) -> Chunk* {
184+
std::unique_lock<std::mutex> lock(mapMtx);
185+
auto it = chunkData.find({cx, cz});
186+
if (it == chunkData.end()) return nullptr;
187+
return it->second.get();
188+
};
189+
190+
myChunk->generateDecorations(pos.x, pos.z, seed, neighborFn);
191+
192+
{
193+
std::unique_lock<std::mutex> lock(mapMtx);
194+
auto sit = chunkStatus.find(pos);
195+
if (sit != chunkStatus.end()) sit->second = ChunkStatus::DecorationDone;
196+
}
197+
logMsg("decoration done: (" + std::to_string(pos.x) + "," + std::to_string(pos.z) + ")");
198+
});
199+
}
200+
201+
// ─────────────────────────────────────────────────────────────────────────────
202+
// Stage 3: Mesh building — all neighbors are DecorationDone
203+
// Passes neighbor chunks so inter-chunk face culling works correctly
204+
// ─────────────────────────────────────────────────────────────────────────────
205+
void ChunkManager::scheduleMesh(ChunkPos pos) {
206+
logMsg("scheduleMesh: (" + std::to_string(pos.x) + "," + std::to_string(pos.z) + ")");
207+
pool.enqueue([this, pos]() {
208+
Chunk* myChunk = nullptr;
209+
{
210+
std::unique_lock<std::mutex> lock(mapMtx);
211+
auto it = chunkData.find(pos);
212+
if (it == chunkData.end()) return;
213+
myChunk = it->second.get();
214+
}
215+
if (!myChunk) return;
216+
217+
// Collect neighbor pointers for inter-chunk face culling
218+
// Safe to read since all neighbors are DecorationDone (no more writes)
219+
Chunk* neighbors[3][3]{};
220+
{
221+
std::unique_lock<std::mutex> lock(mapMtx);
222+
for (int dx = -1; dx <= 1; dx++) {
223+
for (int dz = -1; dz <= 1; dz++) {
224+
auto it = chunkData.find({pos.x+dx, pos.z+dz});
225+
if (it != chunkData.end())
226+
neighbors[dx+1][dz+1] = it->second.get();
227+
}
228+
}
229+
}
230+
231+
std::vector<float> verts = myChunk->buildMesh(pos.x, pos.z, neighbors);
232+
233+
{
234+
std::unique_lock<std::mutex> lock(mapMtx);
235+
auto sit = chunkStatus.find(pos);
236+
if (sit != chunkStatus.end()) sit->second = ChunkStatus::MeshReady;
237+
}
238+
239+
{
240+
std::unique_lock<std::mutex> lock(uploadMtx);
241+
uploadQueue.push(UploadRequest{pos, std::move(verts)});
242+
}
243+
});
244+
}

0 commit comments

Comments
 (0)