From 2e7d985768584ba95142020f8c5fa923f0fe4ba8 Mon Sep 17 00:00:00 2001 From: Ishi_eenn Date: Fri, 29 May 2026 21:56:11 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E6=9D=BE=E6=98=8E=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 2 + assets/shaders/chunk.frag | 10 +++ assets/shaders/chunk.vert | 22 ++++++ assets/shaders/gbuffer.frag | 8 ++ assets/shaders/gbuffer.vert | 3 + assets/shaders/shadow.frag | 7 ++ assets/shaders/shadow.vert | 4 + include/types.hpp | 4 +- srcs/core/engine.cpp | 48 +++++++++++- srcs/renderer/renderer.cpp | 44 +++++++++-- srcs/renderer/renderer.hpp | 15 +++- srcs/renderer/shader.cpp | 9 +++ srcs/renderer/shader.hpp | 1 + srcs/renderer/texture_atlas.cpp | 84 ++++++++++++++++++++ srcs/world/inventory.cpp | 62 +++++++++++++++ srcs/world/inventory.hpp | 22 ++++++ srcs/world/mesh_builder.cpp | 133 +++++++++++++++++++++++++++++++- srcs/world/recipe.cpp | 36 +++++++++ srcs/world/recipe.hpp | 31 ++++++++ srcs/world/world.cpp | 20 ++++- srcs/world/world.hpp | 4 + 21 files changed, 558 insertions(+), 11 deletions(-) create mode 100644 srcs/world/inventory.cpp create mode 100644 srcs/world/inventory.hpp create mode 100644 srcs/world/recipe.cpp create mode 100644 srcs/world/recipe.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f24a35..b179351 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,8 @@ add_executable( srcs/world/terrain_gen.cpp srcs/world/mesh_builder.cpp srcs/world/world.cpp + srcs/world/inventory.cpp + srcs/world/recipe.cpp srcs/core/engine.cpp srcs/renderer/shader.cpp srcs/renderer/frustum.cpp diff --git a/assets/shaders/chunk.frag b/assets/shaders/chunk.frag index 073b94d..fac8222 100644 --- a/assets/shaders/chunk.frag +++ b/assets/shaders/chunk.frag @@ -4,6 +4,7 @@ in float vLight; in float vLightShadow; in vec4 vShadowCoord; in float vFogFactor; +in float vTorchLight; uniform sampler2D uAtlas; uniform sampler2D uShadowMap; @@ -63,6 +64,15 @@ void main() { float light = vLightShadow * ssao + sun_direct * shadowFactor; vec3 lit = col.rgb * light; + + // ── 松明光源: 暖色を加算 ─────────────────────────────────────────────── + // 暗い場所ほど目立つように、light の不足分を埋める形でブレンド。 + // 同時に warm tint (オレンジ寄り) も足し込み、ロウソク色の雰囲気を出す。 + vec3 torch_warm = vec3(1.10, 0.88, 0.55); + lit += col.rgb * torch_warm * vTorchLight; + // オーバーブライト防止 (極端な白飛びを避ける) + lit = min(lit, col.rgb * 1.6); + vec3 fogged = mix(lit, uFogColor, clamp(vFogFactor, 0.0, 1.0)); FragColor = vec4(fogged, col.a); } diff --git a/assets/shaders/chunk.vert b/assets/shaders/chunk.vert index a7bc628..32e662e 100644 --- a/assets/shaders/chunk.vert +++ b/assets/shaders/chunk.vert @@ -13,11 +13,18 @@ uniform float uSunStrength; // sun diffuse strength [0.0 .. 0.65] uniform float uFogStart; uniform float uFogEnd; +// 松明光源 (Forward point lights, 最大 16 個) +// 距離フォールオフで暖色光を加算する。 +uniform vec3 uTorches[16]; +uniform int uTorchCount; // 実際に有効なエントリ数 (0..16) +uniform float uTorchRange; // 完全減衰する距離 (ブロック単位) + out vec2 vUV; out float vLight; out float vLightShadow; // 影の中にいるときの明るさ (ambient のみ) out vec4 vShadowCoord; // 光源クリップ空間の座標 out float vFogFactor; +out float vTorchLight; // 松明光の累積強度 (0.0 .. 1.0+) void main() { gl_Position = uMVP * vec4(aPos, 1.0); @@ -38,6 +45,21 @@ void main() { // 光源空間での頂点位置 (PCF shadow sampling に使用) vShadowCoord = uLightSpaceMat * uModel * vec4(aPos, 1.0); + vec3 world_pos = (uModel * vec4(aPos, 1.0)).xyz; + + // ── 松明光源: 全松明からの寄与を線形フォールオフで加算 ── + // 各光源につき max(0, 1 - dist/range) を加算。複数松明が重なれば明るくなる。 + float torch_accum = 0.0; + for (int i = 0; i < 16; ++i) { + if (i >= uTorchCount) break; + float d = distance(world_pos, uTorches[i]); + float a = max(0.0, 1.0 - d / uTorchRange); + torch_accum += a * a; // 2乗フォールオフでなめらか + } + // 面の向きを少し考慮 (上面は明るく、下面は暗く)。完全な点光源では物足りないので + // top/side/bottom の face_shade を再利用してメリハリを付ける。 + vTorchLight = clamp(torch_accum, 0.0, 1.5) * face_shade; + vec3 view_pos = (uView * uModel * vec4(aPos, 1.0)).xyz; float view_dist = length(view_pos); if (uFogEnd > uFogStart + 1.0) diff --git a/assets/shaders/gbuffer.frag b/assets/shaders/gbuffer.frag index 61c9bd9..7617b8d 100644 --- a/assets/shaders/gbuffer.frag +++ b/assets/shaders/gbuffer.frag @@ -1,10 +1,18 @@ #version 410 core +in vec2 vUV; in vec3 vViewPos; in vec3 vViewNormal; +uniform sampler2D uAtlas; + layout(location = 0) out vec4 gNormal; void main() { + // 透過テクセル (松明の炎クロスビルボードの余白等) は GBuffer に書き込まないことで + // SSAO がそれを四角いオクルーダーとして誤検出するのを防ぐ。 + float a = texture(uAtlas, vUV).a; + if (a < 0.1) discard; + // ビュー空間の法線を正規化して格納。深度はFBOが自動書き込み。 gNormal = vec4(normalize(vViewNormal), 0.0); } diff --git a/assets/shaders/gbuffer.vert b/assets/shaders/gbuffer.vert index 6485686..eacee1e 100644 --- a/assets/shaders/gbuffer.vert +++ b/assets/shaders/gbuffer.vert @@ -1,15 +1,18 @@ #version 410 core layout(location = 0) in vec3 aPos; +layout(location = 1) in vec2 aUV; layout(location = 2) in vec3 aNormal; uniform mat4 uMVP; uniform mat4 uModelView; // view * model +out vec2 vUV; out vec3 vViewPos; out vec3 vViewNormal; void main() { gl_Position = uMVP * vec4(aPos, 1.0); + vUV = aUV; vViewPos = (uModelView * vec4(aPos, 1.0)).xyz; // チャンクのmodel行列は平行移動のみなので mat3(uModelView) で正しい vViewNormal = mat3(uModelView) * aNormal; diff --git a/assets/shaders/shadow.frag b/assets/shaders/shadow.frag index 82e27d2..7ebe4f1 100644 --- a/assets/shaders/shadow.frag +++ b/assets/shaders/shadow.frag @@ -1,5 +1,12 @@ #version 410 core +in vec2 vUV; + +uniform sampler2D uAtlas; void main() { + // 透過テクセル (松明炎ビルボードの余白等) はシャドウマップに深度を書き込まない。 + // これにより透明部分が四角い影として現れる問題を防ぐ。 + float a = texture(uAtlas, vUV).a; + if (a < 0.1) discard; // 深度値はGPUが自動的にDepth Bufferへ書き込む } diff --git a/assets/shaders/shadow.vert b/assets/shaders/shadow.vert index 4d9506c..866232b 100644 --- a/assets/shaders/shadow.vert +++ b/assets/shaders/shadow.vert @@ -1,8 +1,12 @@ #version 410 core layout(location = 0) in vec3 aPos; +layout(location = 1) in vec2 aUV; uniform mat4 uLightMVP; +out vec2 vUV; + void main() { gl_Position = uLightMVP * vec4(aPos, 1.0); + vUV = aUV; } diff --git a/include/types.hpp b/include/types.hpp index baf5ca7..5b0c1d7 100644 --- a/include/types.hpp +++ b/include/types.hpp @@ -24,13 +24,15 @@ enum class BlockType : uint8_t { PinkLeaves = 16, // 桜(春バイオーム) OrangeLeaves = 17, // 紅葉オレンジ(秋バイオーム) Bow = 18, // 弓(アイテム: 設置不可・スタック上限1) + Stick = 19, // 棒(アイテム: 設置不可・クラフト素材) + Torch = 20, // 松明(設置可: 細い柱型 3D メッシュ) COUNT }; // ブロックではなく "アイテム" として扱う BlockType を判定する。 // アイテムは右クリックで設置されず、種別固有の挙動を発火する。 inline bool isItem(BlockType t) { - return t == BlockType::Bow; + return t == BlockType::Bow || t == BlockType::Stick; } enum class Face : uint8_t { diff --git a/srcs/core/engine.cpp b/srcs/core/engine.cpp index a70a2a0..3c5b9df 100644 --- a/srcs/core/engine.cpp +++ b/srcs/core/engine.cpp @@ -147,6 +147,9 @@ static void rebuildModified(int wx, int wz, ChunkManager& mgr) { static void inventoryAdd(Inventory& inv, BlockType type) { if (type == BlockType::Air || type == BlockType::Water || type == BlockType::ShortGrass || isItem(type)) return; + // Torch はスロット 8 に無限補充されるため、拾っても追加せず捨てる + // (インベントリが松明で埋まるのを防ぐ) + if (type == BlockType::Torch) return; for (int i = 0; i < HOTBAR_SIZE; ++i) { if (inv.slots[i].type == type && inv.slots[i].count < STACK_MAX) { ++inv.slots[i].count; @@ -362,6 +365,8 @@ bool Engine::init(uint32_t seed, int width, int height) { // 弓を最後のホットバースロットに常備(無限矢扱い) impl_->inventory.slots[HOTBAR_SIZE - 1] = {BlockType::Bow, 1}; + // 松明をスロット 8(インデックス 7)に常備(無限供給) + impl_->inventory.slots[HOTBAR_SIZE - 2] = {BlockType::Torch, STACK_MAX}; running_ = true; fprintf(stderr, "[Engine] init OK seed=%u %dx%d\n", seed, width_, height_); @@ -461,7 +466,8 @@ void Engine::run() { && t != BlockType::Water && t != BlockType::ShortGrass && t != BlockType::Flower - && t != BlockType::Mushroom; + && t != BlockType::Mushroom + && t != BlockType::Torch; }; // isWater: 「このブロックは水か?」を調べる関数。 // 水中では重力・移動速度が変わる。 @@ -582,6 +588,13 @@ void Engine::run() { InputHandler& inp = impl_->player.input(); Inventory& inv = impl_->inventory; + // 松明スロット(インデックス 7 = 数字キー 8)を毎フレーム補充する。 + // 「常に入れておく」要件のため、設置で減っても次フレームで満タンに戻る。 + if (inv.slots[HOTBAR_SIZE - 2].type != BlockType::Torch || + inv.slots[HOTBAR_SIZE - 2].count < STACK_MAX) { + inv.slots[HOTBAR_SIZE - 2] = {BlockType::Torch, STACK_MAX}; + } + // キー 1〜9: ホットバースロットを選択(ワンショット: 前フレームと差分) static bool prev_num[9] = {}; for (int k = 0; k < 9; ++k) { @@ -807,6 +820,39 @@ void Engine::run() { // 毎フレーム数チャンクずつ処理し、ゲームが止まらないようにする。 impl_->chunk_mgr->update(ppos.x, ppos.z, frame_); + // ── 松明光源の近傍 16 件をシェーダーへ送る ─────────────────────────── + // World が持つ全松明をプレイヤー距離でソートし、近い順に最大 16 件選別。 + // chunk.vert が頂点ごとに距離フォールオフを計算する。 + { + std::vector all = impl_->world.torchPositions(); + struct Scored { glm::vec3 pos; float d2; }; + std::vector scored; + scored.reserve(all.size()); + const float cull_d2 = 64.0f * 64.0f; // 64 ブロック超は無視 (range 10 なので余裕) + for (const WorldPos& tp : all) { + glm::vec3 p((float)tp.x + 0.5f, + (float)tp.y + 0.6f, // 炎中央 (棒+10/16〜炎+15/16の中間) + (float)tp.z + 0.5f); + glm::vec3 d = p - ppos; + float d2 = d.x*d.x + d.y*d.y + d.z*d.z; + if (d2 > cull_d2) continue; + scored.push_back({p, d2}); + } + constexpr size_t kMaxLights = 16; + if (scored.size() > kMaxLights) { + std::nth_element(scored.begin(), scored.begin() + kMaxLights, + scored.end(), + [](const Scored& a, const Scored& b) { + return a.d2 < b.d2; + }); + scored.resize(kMaxLights); + } + std::vector near_lights; + near_lights.reserve(scored.size()); + for (const Scored& s : scored) near_lights.push_back(s.pos); + impl_->renderer.setTorchLights(near_lights); + } + // ── 行列の計算 ─────────────────────────────────────────────────────── // 【View行列】: カメラの位置・向きによって「ワールドがどう見えるか」を表す行列。 // カメラが動いたり向きを変えるたびに変わる。 diff --git a/srcs/renderer/renderer.cpp b/srcs/renderer/renderer.cpp index 37ea089..a9b6bfd 100644 --- a/srcs/renderer/renderer.cpp +++ b/srcs/renderer/renderer.cpp @@ -1024,6 +1024,7 @@ void Renderer::drawHotbar(const Inventory& inv) { const ItemStack& s = inv.slots[i]; if (s.type == BlockType::Air || s.count <= 0) continue; if (s.type == BlockType::Bow) continue; + if (s.type == BlockType::Torch) continue; AtlasUV uv = atlas_.getUV(s.type); float uc = (uv.u0 + uv.u1) * 0.5f; @@ -1085,7 +1086,7 @@ void Renderer::drawHotbar(const Inventory& inv) { } } - // 弓はブロックではないので、Bow.png の平面アイコンとして表示する。 + // 弓・松明は立方体としてではなく 2D アイコンとして表示する。 { hotbar_shader_.setFloat("uBright", 1.0f); float verts[HOTBAR_SIZE * 6 * 4] = {}; @@ -1093,9 +1094,10 @@ void Renderer::drawHotbar(const Inventory& inv) { for (int i = 0; i < HOTBAR_SIZE; ++i) { const ItemStack& s = inv.slots[i]; - if (s.type != BlockType::Bow || s.count <= 0) continue; + if ((s.type != BlockType::Bow && s.type != BlockType::Torch) + || s.count <= 0) continue; - AtlasUV uv = atlas_.getUV(BlockType::Bow); + AtlasUV uv = atlas_.getUV(s.type); const float cx = nx(sx0_px + i * (SLOT_PX + GAP_PX) + SLOT_PX * 0.5f); const float cy = ny(BOTTOM_PX + SLOT_PX * 0.5f); const float iw = 34.0f / hw; @@ -1144,9 +1146,9 @@ void Renderer::drawHotbar(const Inventory& inv) { appendLine(verts.data(), cnt, x0, sy0, x0, sy1); appendLine(verts.data(), cnt, x1, sy0, x1, sy1); - // 個数(2個以上のとき右下に表示) + // 個数(2個以上のとき右下に表示)。Torch は無限供給なので個数は隠す。 const ItemStack& s = inv.slots[i]; - if (s.type != BlockType::Air && s.count > 1) { + if (s.type != BlockType::Air && s.type != BlockType::Torch && s.count > 1) { float dw = 8.0f / hw, dh = 11.0f / hh, dg = 2.0f / hw; appendNumber(verts.data(), cnt, s.count, x1 - 3.0f / hw, sy0 + dh + 3.0f / hh, dw, dh, dg); @@ -1354,6 +1356,24 @@ static void setChunkLightingUniforms(Shader& shader, shader.setFloat("uSunStrength", sun_strength); } +void Renderer::setTorchLights(const std::vector& positions) { + torch_count_ = (int)std::min((size_t)MAX_TORCH_LIGHTS, positions.size()); + for (int i = 0; i < torch_count_; ++i) { + torch_positions_[i * 3 + 0] = positions[i].x; + torch_positions_[i * 3 + 1] = positions[i].y; + torch_positions_[i * 3 + 2] = positions[i].z; + } +} + +// chunk.vert の uTorches/uTorchCount/uTorchRange を一括で設定する。 +static void setTorchUniforms(Shader& shader, int count, + const float* positions_xyz, float range) { + shader.setInt ("uTorchCount", count); + shader.setFloat("uTorchRange", range); + if (count > 0) + shader.setVec3Array("uTorches[0]", count, positions_xyz); +} + static void setFogUniforms(Shader& shader, const float sky_horizon[3], bool underwater) { if (underwater) { @@ -1392,6 +1412,7 @@ void Renderer::drawChunk(const Chunk* chunk, const float* view4x4, const float* setChunkLightingUniforms(chunk_shader_, sun_dir_, ambient_, sun_strength_); setFogUniforms(chunk_shader_, sky_horizon_, underwater_); chunk_shader_.setFloat("uSunStrength", sun_strength_); + setTorchUniforms(chunk_shader_, torch_count_, torch_positions_, TORCH_RANGE); atlas_.bind(0); chunk_shader_.setInt("uAtlas", 0); @@ -1445,6 +1466,7 @@ void Renderer::drawChunkWater(const Chunk* chunk, const float* view4x4, const fl setChunkLightingUniforms(chunk_shader_, sun_dir_, ambient_, sun_strength_); setFogUniforms(chunk_shader_, sky_horizon_, underwater_); chunk_shader_.setFloat("uSunStrength", sun_strength_); + setTorchUniforms(chunk_shader_, torch_count_, torch_positions_, TORCH_RANGE); atlas_.bind(0); chunk_shader_.setInt("uAtlas", 0); @@ -1546,6 +1568,12 @@ void Renderer::beginShadowPass() { // シャドウマップから除外されてしまうため、通常の GL_BACK カリングのままにする。 glEnable(GL_POLYGON_OFFSET_FILL); glPolygonOffset(2.0f, 4.0f); + + // 松明炎のような透過テクセルが「四角い影」を作らないよう、シャドウ FBO の + // フラグメントシェーダーでアルファ discard する。アトラスをスロット 0 にバインド。 + atlas_.bind(0); + shadow_shader_.use(); + shadow_shader_.setInt("uAtlas", 0); } // ───────────────────────────────────────────────────────────────────────────── @@ -1761,6 +1789,12 @@ void Renderer::beginGBufferPass() { glBindFramebuffer(GL_FRAMEBUFFER, gbuffer_fbo_); glViewport(0, 0, width_, height_); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // 松明炎のような透過テクセルが SSAO で四角いオクルーダー扱いされないよう、 + // GBuffer フラグメントシェーダーでアルファ discard する。アトラスをスロット 0 に。 + atlas_.bind(0); + gbuffer_shader_.use(); + gbuffer_shader_.setInt("uAtlas", 0); } // ───────────────────────────────────────────────────────────────────────────── diff --git a/srcs/renderer/renderer.hpp b/srcs/renderer/renderer.hpp index 9248696..3c0492c 100644 --- a/srcs/renderer/renderer.hpp +++ b/srcs/renderer/renderer.hpp @@ -58,6 +58,11 @@ class Renderer : public IRenderer { void setTimeOfDay(float t); void setUnderwater(bool underwater) { underwater_ = underwater; } + // 松明光源 (forward point lights) の位置リストを設定する。 + // 最大 MAX_TORCH_LIGHTS 件まで採用、それ以上は捨てる。 + // chunk.vert で各頂点ごとに距離フォールオフを計算するために使われる。 + void setTorchLights(const std::vector& positions); + // Shadow mapping void updateShadowMatrix(float px, float py, float pz); void beginShadowPass(); @@ -84,8 +89,10 @@ class Renderer : public IRenderer { void appendHeart(float* verts, int& count, float left, float top, float w, float h) const; void appendSignedNumberLeft(float* verts, int& count, int value, float left, float top, float w, float h, float gap) const; - static constexpr int SHADOW_MAP_SIZE = 2048; - static constexpr int SSAO_SAMPLES = 64; + static constexpr int SHADOW_MAP_SIZE = 2048; + static constexpr int SSAO_SAMPLES = 64; + static constexpr int MAX_TORCH_LIGHTS = 16; + static constexpr float TORCH_RANGE = 10.0f; // ブロック GLFWwindow* window_ = nullptr; Shader chunk_shader_; @@ -150,4 +157,8 @@ class Renderer : public IRenderer { float sky_ground_[3] = {0.35f, 0.30f, 0.25f}; float sun_color_[3] = {1.00f, 0.98f, 0.85f}; bool underwater_ = false; + + // 松明光源 (近傍の最大 MAX_TORCH_LIGHTS 件を格納する xyz パックド配列) + int torch_count_ = 0; + float torch_positions_[MAX_TORCH_LIGHTS * 3] = {}; }; diff --git a/srcs/renderer/shader.cpp b/srcs/renderer/shader.cpp index b3eac8e..3f48e4a 100644 --- a/srcs/renderer/shader.cpp +++ b/srcs/renderer/shader.cpp @@ -198,6 +198,15 @@ void Shader::setVec3(const char* name, float x, float y, float z) const { glUniform3f(loc, x, y, z); } +// 3次元ベクトルの配列を送る(例: 松明光源の位置リスト) +// data は count * 3 個の float が連続して並んだ配列を指す。 +void Shader::setVec3Array(const char* name, int count, const float* data) const { + if (count <= 0) return; + GLint loc = glGetUniformLocation(program_, name); + if (loc != -1) + glUniform3fv(loc, count, data); +} + // 4次元ベクトルを送る(例: RGBA 色) void Shader::setVec4(const char* name, float x, float y, float z, float w) const { GLint loc = glGetUniformLocation(program_, name); diff --git a/srcs/renderer/shader.hpp b/srcs/renderer/shader.hpp index 3cbcfda..8eea92d 100644 --- a/srcs/renderer/shader.hpp +++ b/srcs/renderer/shader.hpp @@ -16,6 +16,7 @@ class Shader { void setFloat(const char* name, float val) const; void setVec2(const char* name, float x, float y) const; void setVec3(const char* name, float x, float y, float z) const; + void setVec3Array(const char* name, int count, const float* data) const; void setVec4(const char* name, float x, float y, float z, float w) const; uint32_t id() const { return program_; } diff --git a/srcs/renderer/texture_atlas.cpp b/srcs/renderer/texture_atlas.cpp index 3b29c29..5573da2 100644 --- a/srcs/renderer/texture_atlas.cpp +++ b/srcs/renderer/texture_atlas.cpp @@ -566,6 +566,88 @@ static void fillMushroomPlant(uint8_t* buf, int atlas_w, int tile_col, int tile_ } } +// ───────────────────────────────────────────────────────────────────────────── +// fillStick() — 棒(クラフト素材)テクスチャを生成 +// +// 中央 2px に茶色の棒。周囲は透明。 +// 3D 松明メッシュの棒部分にも貼られる(タイル全体を細い棒の側面として使う)。 +// ───────────────────────────────────────────────────────────────────────────── +static void fillStick(uint8_t* buf, int atlas_w, int tile_col, int tile_row) { + const int tw = ATLAS_TILE_SIZE; + const int ox = tile_col * tw; + const int oy = tile_row * tw; + for (int py = 0; py < tw; ++py) { + for (int px = 0; px < tw; ++px) { + bool stick = (px >= 7 && px <= 8); + if (!stick) { + setPixel(buf, atlas_w, ox, oy, px, py, 0, 0, 0, 0); + continue; + } + int n = hash2(px, py, 1301); + int r = 138 + ((n % 24) - 12); + int g = 98 + ((n % 18) - 9); + int b = 56 + ((n % 14) - 7); + // 縦の木目(暗いライン) + if (px == 7 && (py % 3 == 0)) { r -= 22; g -= 18; b -= 10; } + setPixel(buf, atlas_w, ox, oy, px, py, r, g, b); + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// fillTorch() — 松明アイコンテクスチャを生成 +// +// 16x16: 下 12px が茶色の棒(細い 2px 幅)、上 4px が Minecraft 風の炎。 +// ホットバーの 2D アイコン表示と、3D 設置メッシュの炎部分のサンプリングで使う。 +// 炎は白熱(W) → 黄(Y/y) → オレンジ(O) のグラデーションで本家の見た目に寄せる。 +// ───────────────────────────────────────────────────────────────────────────── +static void fillTorch(uint8_t* buf, int atlas_w, int tile_col, int tile_row) { + const int tw = ATLAS_TILE_SIZE; + const int ox = tile_col * tw; + const int oy = tile_row * tw; + + // 炎の 4×16 ピクセルマップ(py=0 は炎の先端、py=3 は基部 = 棒の真上) + // '.' = 透明, 'W' = 白熱中心, 'Y' = 明るい黄, 'y' = 黄, 'O' = オレンジ, 'o' = 暗オレンジ + static const char* kFlame[4] = { + ".......YY.......", + "......YWWY......", + ".....yOWWOy.....", + "....oOyWWyOo....", + }; + + auto putFlame = [&](int px, int py, char c) { + int r = 0, g = 0, b = 0, a = 0; + switch (c) { + case 'W': r = 255; g = 252; b = 220; a = 255; break; + case 'Y': r = 255; g = 232; b = 80; a = 255; break; + case 'y': r = 250; g = 200; b = 50; a = 255; break; + case 'O': r = 245; g = 148; b = 32; a = 255; break; + case 'o': r = 198; g = 92; b = 18; a = 255; break; + default: a = 0; break; + } + setPixel(buf, atlas_w, ox, oy, px, py, r, g, b, a); + }; + + for (int py = 0; py < tw; ++py) { + for (int px = 0; px < tw; ++px) { + if (py < 4) { + // 炎エリア + putFlame(px, py, kFlame[py][px]); + } else if (px == 7 || px == 8) { + // 棒エリア(中央 2px) + int n = hash2(px, py, 1301); + int r = 138 + ((n % 24) - 12); + int g = 98 + ((n % 18) - 9); + int b = 56 + ((n % 14) - 7); + if (px == 7 && (py % 3 == 0)) { r -= 22; g -= 18; b -= 10; } + setPixel(buf, atlas_w, ox, oy, px, py, r, g, b); + } else { + setPixel(buf, atlas_w, ox, oy, px, py, 0, 0, 0, 0); + } + } + } +} + // 弓アイコン16x16ピクセルマップを焼き込む static void fillBow(uint8_t* buf, int atlas_w, int tile_col, int tile_row) { const int tw = ATLAS_TILE_SIZE; @@ -647,6 +729,8 @@ bool TextureAtlas::generate() { fillColoredLeaves(pixels, atlas_w, 0, 2, 210, 140, 160, 20, 12, 14, 511, 719); // PinkLeaves tile=16 (桜ピンク) fillColoredLeaves(pixels, atlas_w, 1, 2, 195, 105, 30, 22, 18, 10, 613, 821); // OrangeLeaves tile=17 (紅葉オレンジ) fillBow (pixels, atlas_w, 2, 2); // Bow tile=18 + fillStick (pixels, atlas_w, 3, 2); // Stick tile=19 + fillTorch (pixels, atlas_w, 4, 2); // Torch tile=20 // GPU にテクスチャを作成して転送する glGenTextures(1, &tex_id_); diff --git a/srcs/world/inventory.cpp b/srcs/world/inventory.cpp new file mode 100644 index 0000000..8cbc0e5 --- /dev/null +++ b/srcs/world/inventory.cpp @@ -0,0 +1,62 @@ +// ───────────────────────────────────────────────────────────────────────────── +// inventory.cpp — Inventory 操作の実装 +// ───────────────────────────────────────────────────────────────────────────── +#include "inventory.hpp" + +void inventoryAdd(Inventory& inv, BlockType type, int n) { + if (n <= 0) return; + if (type == BlockType::Air || type == BlockType::Water || + type == BlockType::ShortGrass || type == BlockType::Flower || + type == BlockType::Mushroom) + return; + + // 既存スタックに積む + for (int i = 0; i < HOTBAR_SIZE && n > 0; ++i) { + if (inv.slots[i].type == type && inv.slots[i].count < STACK_MAX) { + int add = STACK_MAX - inv.slots[i].count; + if (add > n) add = n; + inv.slots[i].count += add; + n -= add; + } + } + // 空きスロットを埋める + for (int i = 0; i < HOTBAR_SIZE && n > 0; ++i) { + if (inv.slots[i].type == BlockType::Air) { + int add = n > STACK_MAX ? STACK_MAX : n; + inv.slots[i] = {type, add}; + n -= add; + } + } + // インベントリ満杯: 残りは捨てる (Bow 等の上限1は STACK_MAX 側で表現される想定だが + // 現状 STACK_MAX=64 のため Bow も技術上は 64 まで積める。仕様上は気にしない) +} + +bool inventoryConsumeSelected(Inventory& inv) { + ItemStack& s = inv.slots[inv.selected]; + if (s.type == BlockType::Air || s.count <= 0) return false; + --s.count; + if (s.count == 0) s = {}; + return true; +} + +int inventoryCount(const Inventory& inv, BlockType type) { + if (type == BlockType::Air) return 0; + int sum = 0; + for (int i = 0; i < HOTBAR_SIZE; ++i) + if (inv.slots[i].type == type) + sum += inv.slots[i].count; + return sum; +} + +bool inventoryConsume(Inventory& inv, BlockType type, int n) { + if (n <= 0) return true; + if (inventoryCount(inv, type) < n) return false; + for (int i = 0; i < HOTBAR_SIZE && n > 0; ++i) { + if (inv.slots[i].type != type) continue; + int take = inv.slots[i].count < n ? inv.slots[i].count : n; + inv.slots[i].count -= take; + n -= take; + if (inv.slots[i].count == 0) inv.slots[i] = {}; + } + return true; +} diff --git a/srcs/world/inventory.hpp b/srcs/world/inventory.hpp new file mode 100644 index 0000000..92a94e8 --- /dev/null +++ b/srcs/world/inventory.hpp @@ -0,0 +1,22 @@ +// ───────────────────────────────────────────────────────────────────────────── +// inventory.hpp — ホットバー (Inventory) 操作のヘルパー関数 +// +// engine.cpp 内のローカル static 関数をここに集約し、クラフトシステム等から +// 再利用できるようにする。 +// ───────────────────────────────────────────────────────────────────────────── +#pragma once +#include "types.hpp" + +// ブロック/アイテムを n 個インベントリに追加する。 +// 既存スタックに積む → 空きスロットを探す。スタックを超えた分は捨てる。 +// 設置不可な装飾植物 (ShortGrass / Flower / Mushroom) や Air/Water は追加しない。 +void inventoryAdd(Inventory& inv, BlockType type, int n = 1); + +// 選択スロットから 1 個消費する。成功なら true。 +bool inventoryConsumeSelected(Inventory& inv); + +// 指定タイプのアイテム数を全スロット合算して返す(クラフト前検査用)。 +int inventoryCount(const Inventory& inv, BlockType type); + +// 指定タイプを n 個消費する。在庫が足りていれば true(足りなければ false で何も触らない)。 +bool inventoryConsume(Inventory& inv, BlockType type, int n); diff --git a/srcs/world/mesh_builder.cpp b/srcs/world/mesh_builder.cpp index c4120a0..242e79a 100644 --- a/srcs/world/mesh_builder.cpp +++ b/srcs/world/mesh_builder.cpp @@ -84,10 +84,15 @@ static bool isDecorativePlant(BlockType type) { type == BlockType::Mushroom; } +static bool isTorchBlock(BlockType type) { + return type == BlockType::Torch; +} + static bool isFaceTransparent(BlockType type) { return type == BlockType::Air || type == BlockType::Water || - isDecorativePlant(type); + isDecorativePlant(type) || + isTorchBlock(type); } static float plantHeight(BlockType type) { @@ -119,6 +124,127 @@ static void addPlantQuad(std::vector& verts, b, b+2, b+1, b, b+3, b+2}); } +// ───────────────────────────────────────────────────────────────────────────── +// 任意サイズの直方体ボックス(6面)をメッシュへ追加するヘルパー。 +// 松明の「細い棒」と「炎の小箱」をそれぞれボックスで生成するために使う。 +// 各面とも同じ UV 矩形 (u0,v0,uw,vh) を貼る(ボックスが小さいため引き伸ばしは問題なし)。 +// ───────────────────────────────────────────────────────────────────────────── +static void addBox(std::vector& verts, + std::vector& indices, + float minx, float miny, float minz, + float maxx, float maxy, float maxz, + float u0, float v0, float uw, float vh) { + // 6面: top, bottom, north(-Z), south(+Z), east(+X), west(-X) + // 各面の 4 頂点を CCW で定義(外から見て反時計回り) + const float P[8][3] = { + {minx, miny, minz}, // 0 + {maxx, miny, minz}, // 1 + {maxx, miny, maxz}, // 2 + {minx, miny, maxz}, // 3 + {minx, maxy, minz}, // 4 + {maxx, maxy, minz}, // 5 + {maxx, maxy, maxz}, // 6 + {minx, maxy, maxz}, // 7 + }; + const int FACES[6][4] = { + {7, 6, 5, 4}, // top +Y (n = 0,+1,0) + {0, 1, 2, 3}, // bottom -Y + {4, 5, 1, 0}, // north -Z + {6, 7, 3, 2}, // south +Z + {5, 6, 2, 1}, // east +X + {7, 4, 0, 3}, // west -X + }; + const float N[6][3] = { + { 0, 1, 0}, { 0,-1, 0}, + { 0, 0,-1}, { 0, 0, 1}, + { 1, 0, 0}, {-1, 0, 0}, + }; + const float UV[4][2] = { + {0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f} + }; + for (int f = 0; f < 6; ++f) { + uint32_t base = (uint32_t)verts.size(); + for (int i = 0; i < 4; ++i) { + int p = FACES[f][i]; + Vertex v{}; + v.x = P[p][0]; v.y = P[p][1]; v.z = P[p][2]; + v.u = u0 + UV[i][0] * uw; + v.v = v0 + UV[i][1] * vh; + v.nx = N[f][0]; v.ny = N[f][1]; v.nz = N[f][2]; + verts.push_back(v); + } + indices.insert(indices.end(), + {base, base + 1, base + 2, + base, base + 2, base + 3}); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// addTorchModel() — 設置された松明の 3D メッシュを生成 +// +// 構造(Minecraft の torch モデルを踏襲): +// ・棒部分: 2x10x2 の細い角柱(中央寄り、ブロック底〜10/16 高さ) +// テクスチャは Wood タイルを流用(茶色の木目)。 +// ・炎部分: クロスビルボード = 2 枚の垂直な板を X 字に交差させる。 +// 各板は 8/16 × 5/16 の矩形で、Torch タイル上端 4px の炎パターンを +// 透過テクスチャでサンプル。alpha discard で炎の形状だけが残る。 +// ───────────────────────────────────────────────────────────────────────────── +static void addTorchModel(std::vector& verts, + std::vector& indices, + int x, int y, int z) { + const int cols = ATLAS_COLS; + const int rows = 4; + const float icols = 1.0f / (float)cols; + const float irows = 1.0f / (float)rows; + + // 棒: Wood タイル (tile=7, col=7, row=0) を流用 + const int wood_tile = (int)BlockType::Wood; + const float wu0 = (float)(wood_tile % cols) * icols; + const float wv0 = (float)(wood_tile / cols) * irows; + + // 炎: Torch タイル (tile=20, col=4, row=2) の上端 4px (= 4/16 of tile) + const int torch_tile = (int)BlockType::Torch; + const float tu0 = (float)(torch_tile % cols) * icols; + const float tv0 = (float)(torch_tile / cols) * irows; + const float flame_vh = irows * (4.0f / 16.0f); + + const float fx = (float)x; + const float fy = (float)y; + const float fz = (float)z; + + // 棒: x:[7/16,9/16], y:[0,10/16], z:[7/16,9/16] + addBox(verts, indices, + fx + 7.0f/16.0f, fy, fz + 7.0f/16.0f, + fx + 9.0f/16.0f, fy + 10.0f/16.0f, fz + 9.0f/16.0f, + wu0, wv0, icols, irows); + + // 炎クロスビルボード: 2 枚の垂直平面を X 字に配置。 + // 各板の幅 = 8/16, 高さ = 5/16, 棒の真上 (y=10/16〜15/16) に中央配置。 + // addPlantQuad は両面描画(CCW + CW 両方)と alpha discard 対応の UV を持つ。 + const float fy0 = fy + 10.0f / 16.0f; // 棒の上 + const float fy1 = fy + 15.0f / 16.0f; // 炎の先端(ブロック上端より少し下) + const float cx = fx + 8.0f / 16.0f; + const float cz = fz + 8.0f / 16.0f; + const float hw = 4.0f / 16.0f; // half-width → 板幅 8/16 + + // Quad A: Z = cz 平面に立つ垂直板(X 軸方向に広がる) + const float qA[4][3] = { + {cx - hw, fy0, cz}, // p0: 底-左 + {cx + hw, fy0, cz}, // p1: 底-右 + {cx + hw, fy1, cz}, // p2: 頂-右 + {cx - hw, fy1, cz}, // p3: 頂-左 + }; + // Quad B: X = cx 平面に立つ垂直板(Z 軸方向に広がる) + const float qB[4][3] = { + {cx, fy0, cz - hw}, + {cx, fy0, cz + hw}, + {cx, fy1, cz + hw}, + {cx, fy1, cz - hw}, + }; + addPlantQuad(verts, indices, qA, tu0, tv0, icols, flame_vh); + addPlantQuad(verts, indices, qB, tu0, tv0, icols, flame_vh); +} + static void addPlantCross(std::vector& verts, std::vector& indices, int x, int y, int z, @@ -370,6 +496,11 @@ void MeshBuilder::build(Chunk& chunk, const ChunkNeighbors& neighbors) { continue; } + if (isTorchBlock(t)) { + addTorchModel(chunk.vertices, chunk.indices, x, y, z); + continue; + } + // 6面それぞれについて面を生成するか判断 for (int f = 0; f < 6; ++f) { int nx = x + DX[f]; diff --git a/srcs/world/recipe.cpp b/srcs/world/recipe.cpp new file mode 100644 index 0000000..9d359cf --- /dev/null +++ b/srcs/world/recipe.cpp @@ -0,0 +1,36 @@ +// ───────────────────────────────────────────────────────────────────────────── +// recipe.cpp — クラフトレシピデータと検査/適用ロジック +// ───────────────────────────────────────────────────────────────────────────── +#include "recipe.hpp" +#include "inventory.hpp" + +const std::vector& getRecipes() { + static const std::vector kRecipes = { + { + "STICK", + { {BlockType::Wood, 1} }, + { BlockType::Stick, 4 }, + }, + { + "TORCH", + { {BlockType::Stick, 1}, {BlockType::Wood, 1} }, + { BlockType::Torch, 4 }, + }, + }; + return kRecipes; +} + +bool canCraft(const Inventory& inv, const Recipe& r) { + for (const Ingredient& in : r.inputs) + if (inventoryCount(inv, in.type) < in.count) + return false; + return true; +} + +bool applyCraft(Inventory& inv, const Recipe& r) { + if (!canCraft(inv, r)) return false; + for (const Ingredient& in : r.inputs) + inventoryConsume(inv, in.type, in.count); + inventoryAdd(inv, r.output.type, r.output.count); + return true; +} diff --git a/srcs/world/recipe.hpp b/srcs/world/recipe.hpp new file mode 100644 index 0000000..f5180ca --- /dev/null +++ b/srcs/world/recipe.hpp @@ -0,0 +1,31 @@ +// ───────────────────────────────────────────────────────────────────────────── +// recipe.hpp — クラフトレシピシステム +// +// ・固定レシピ配列 kRecipes を提供する。 +// ・canCraft() で在庫検査、applyCraft() で素材消費+出力付与を行う。 +// ・現状はクライアント完結(マルチプレイでの素材検証はサーバ側で持っていない)。 +// ───────────────────────────────────────────────────────────────────────────── +#pragma once +#include "types.hpp" +#include + +struct Ingredient { + BlockType type; + int count; +}; + +struct Recipe { + const char* name; // UI 表示名 (大文字 A-Z, スペース可) + std::vector inputs; + ItemStack output; +}; + +// クラフトメニューに表示する全レシピ。表示順 = 配列順。 +const std::vector& getRecipes(); + +// inv の所持数がレシピを満たすか判定。 +bool canCraft(const Inventory& inv, const Recipe& r); + +// 素材を消費して出力をインベントリに追加する。canCraft が true のときに呼ぶ。 +// 在庫が足りない場合は何もせず false を返す。 +bool applyCraft(Inventory& inv, const Recipe& r); diff --git a/srcs/world/world.cpp b/srcs/world/world.cpp index 69873b2..60b42f2 100644 --- a/srcs/world/world.cpp +++ b/srcs/world/world.cpp @@ -153,6 +153,23 @@ void World::applyMods(Chunk* chunk) const { } } +std::vector World::torchPositions() const { + std::vector out; + for (const auto& [cpos, sub] : mods_) { + for (const auto& [key, type] : sub) { + if (type != BlockType::Torch) continue; + int lx = key / (CHUNK_SIZE_Z * CHUNK_SIZE_Y); + int rem = key % (CHUNK_SIZE_Z * CHUNK_SIZE_Y); + int lz = rem / CHUNK_SIZE_Y; + int ly = rem % CHUNK_SIZE_Y; + int wx = cpos.x * CHUNK_SIZE_X + lx; + int wz = cpos.z * CHUNK_SIZE_Z + lz; + out.push_back({wx, ly, wz}); + } + } + return out; +} + void World::recordMod(ChunkPos pos, int lx, int ly, int lz, BlockType type) { if (ly < 0 || ly >= CHUNK_SIZE_Y) return; if (lx < 0 || lx >= CHUNK_SIZE_X || lz < 0 || lz >= CHUNK_SIZE_Z) return; @@ -183,7 +200,8 @@ bool World::isSolidBlock(BlockType type) const { && type != BlockType::Water && type != BlockType::ShortGrass && type != BlockType::Flower - && type != BlockType::Mushroom; + && type != BlockType::Mushroom + && type != BlockType::Torch; } // チャンク座標が指定範囲内に入っているか判定(水シミュレーション範囲の絞り込みに使う) diff --git a/srcs/world/world.hpp b/srcs/world/world.hpp index 774d74a..658de58 100644 --- a/srcs/world/world.hpp +++ b/srcs/world/world.hpp @@ -35,6 +35,10 @@ class World : public IWorld { return chunks_; } + // mods_ をスキャンして全松明(BlockType::Torch)のワールド座標を返す。 + // 配置/破壊は mods_ に記録されるためこれだけ見れば十分。 + std::vector torchPositions() const; + private: static int floorDiv(int a, int b); static ChunkPos worldToChunk(int wx, int wz); From fa026ae1c8816cf5b94a25b1ae14730d3740fc13 Mon Sep 17 00:00:00 2001 From: Ishi_eenn Date: Fri, 29 May 2026 22:07:50 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=E5=85=89=E6=BA=90=E3=81=AE=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Torch.webp | Bin 0 -> 500 bytes include/interfaces/IWorld.hpp | 4 +++ srcs/streaming/chunk_manager.cpp | 51 +++++++++++++++++++++++++++++++ srcs/streaming/chunk_manager.hpp | 4 +++ srcs/world/world.hpp | 2 +- 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 Torch.webp diff --git a/Torch.webp b/Torch.webp new file mode 100644 index 0000000000000000000000000000000000000000..27c5dea546d889751e15bdec6345987a9d11133c GIT binary patch literal 500 zcmV80YQ=fqea!4#aIMHg?*-Ps`dZx z&g~q)CU6arP}{Z*u>Y@@|>k)awRBBHT>2D_e;IQw|JCU{cy98ZBV&uyLhN{2sM0qa4pV^` zC1R!$s~J=L!6(*JbKp@#EmTy5!xF4(Is81>>Sk__`bXk^g|vCULOSw(#d3yKdIdNW zThcci4pV^`C1PG;<5b-Y16vF%LCA^JLL6sE9d5Bbg3m#4RJ{*=kG4i4yhMbTNPz2o q@DdSTBEm~V_!9e0xy9 stepWater(ChunkPos min_chunk, ChunkPos max_chunk) = 0; virtual void applyMods(Chunk* /*chunk*/) const {} + // 永続的な松明 (BlockType::Torch) のワールド座標一覧を返す。 + // ChunkManager がこれらを含むチャンクを LRU から保護するために使う。 + // デフォルト実装は空配列を返す (テスト/モック向け)。 + virtual std::vector torchPositions() const { return {}; } }; diff --git a/srcs/streaming/chunk_manager.cpp b/srcs/streaming/chunk_manager.cpp index cecb9e4..dbbb916 100644 --- a/srcs/streaming/chunk_manager.cpp +++ b/srcs/streaming/chunk_manager.cpp @@ -154,6 +154,7 @@ void ChunkManager::update(float playerX, float playerZ, uint64_t frame) { drainTerrainDone(); // 地形完了 → World 登録 → mesh_queue_ へ processMeshQueue(4); // メインスレッドでメッシュ構築(最大 4/frame) loadRadius(center); // 未生成チャンクを gen_queue_ へ投入 + pinTorchChunks(); // 松明を含むチャンクを LRU の先頭に保持 if (frame % 3 == 0) { auto changed_water = world_.stepWater( @@ -267,6 +268,56 @@ void ChunkManager::uploadPending(int max_per_frame) { } } +// ───────────────────────────────────────────────────────────────────────────── +// pinTorchChunks() — 松明を含むチャンクを LRU の先頭に保持する +// +// プレイヤーが遠くへ離れても、松明が置かれているチャンクは evict されないよう +// 毎フレーム touch する。これによりプレイヤーが戻ったときに、チャンクの再生成 +// やメッシュ再ビルドの遅延なく即座に光源が機能する。 +// +// 未ロードの松明チャンクは gen_queue_ にも投入してバックグラウンド生成を進める。 +// ───────────────────────────────────────────────────────────────────────────── +void ChunkManager::pinTorchChunks() { + std::vector torches = world_.torchPositions(); + if (torches.empty()) return; + + // 同じチャンクに複数の松明があってもチャンク単位で 1 回だけ処理する。 + std::unordered_set torch_chunks; + for (const WorldPos& tp : torches) { + int cx = (int)std::floor((float)tp.x / (float)CHUNK_SIZE_X); + int cz = (int)std::floor((float)tp.z / (float)CHUNK_SIZE_Z); + torch_chunks.insert({cx, cz}); + } + + for (const ChunkPos& p : torch_chunks) { + if (loaded_.contains(p)) { + // 既にロード済み → LRU の先頭へ + loaded_.touch(p); + continue; + } + // World に存在する(過去に生成された)→ LRU に再登録 + Chunk* existing = world_.getChunk(p); + if (existing) { + auto evicted = loaded_.put(p, existing); + for (const ChunkPos& ep : evicted) { + if (Chunk* dead = world_.getChunk(ep)) renderer_.destroyChunkMesh(dead); + world_.unregisterChunk(ep); + } + mesh_queue_.insert(p); + continue; + } + // 未生成 → ワーカーへ + if (!gen_in_flight_.count(p)) { + gen_in_flight_.insert(p); + { + std::lock_guard lock(gen_mutex_); + gen_queue_.push(p); + } + gen_cv_.notify_one(); + } + } +} + // ───────────────────────────────────────────────────────────────────────────── // evictIfOverBudget() // ───────────────────────────────────────────────────────────────────────────── diff --git a/srcs/streaming/chunk_manager.hpp b/srcs/streaming/chunk_manager.hpp index 9fe9a0e..d3d5013 100644 --- a/srcs/streaming/chunk_manager.hpp +++ b/srcs/streaming/chunk_manager.hpp @@ -38,6 +38,10 @@ class ChunkManager : public IChunkManager { void loadRadius(ChunkPos center); void uploadPending(int max_per_frame); void evictIfOverBudget(); + // 松明を含むチャンクを毎フレーム LRU の先頭に touch することで、 + // プレイヤーが遠くへ行っても evict されず、戻ったときに即座に光源が機能する。 + // 未ロードの松明チャンクは gen_queue_ にも投入して背景生成を促す。 + void pinTorchChunks(); void buildMesh(Chunk* chunk); void drainTerrainDone(); void processMeshQueue(int max_per_frame); diff --git a/srcs/world/world.hpp b/srcs/world/world.hpp index 658de58..0d9b3df 100644 --- a/srcs/world/world.hpp +++ b/srcs/world/world.hpp @@ -37,7 +37,7 @@ class World : public IWorld { // mods_ をスキャンして全松明(BlockType::Torch)のワールド座標を返す。 // 配置/破壊は mods_ に記録されるためこれだけ見れば十分。 - std::vector torchPositions() const; + std::vector torchPositions() const override; private: static int floorDiv(int a, int b);