#include "terrain_renderer.h" #include "../../game/map/visibility_service.h" #include "../gl/mesh.h" #include "../gl/resources.h" #include "../scene_renderer.h" #include #include #include #include #include namespace { using std::uint32_t; inline uint32_t hashCoords(int x, int z, uint32_t salt = 0u) { uint32_t ux = static_cast(x * 73856093); uint32_t uz = static_cast(z * 19349663); return ux ^ uz ^ (salt * 83492791u); } inline float rand01(uint32_t &state) { state = state * 1664525u + 1013904223u; return static_cast((state >> 8) & 0xFFFFFF) / static_cast(0xFFFFFF); } inline float remap(float value, float minOut, float maxOut) { return minOut + (maxOut - minOut) * value; } inline QVector3D applyTint(const QVector3D &color, float tint) { QVector3D c = color * tint; return QVector3D(std::clamp(c.x(), 0.0f, 1.0f), std::clamp(c.y(), 0.0f, 1.0f), std::clamp(c.z(), 0.0f, 1.0f)); } } // namespace namespace Render::GL { TerrainRenderer::TerrainRenderer() = default; TerrainRenderer::~TerrainRenderer() = default; void TerrainRenderer::configure(const Game::Map::TerrainHeightMap &heightMap) { m_width = heightMap.getWidth(); m_height = heightMap.getHeight(); m_tileSize = heightMap.getTileSize(); m_heightData = heightMap.getHeightData(); m_terrainTypes = heightMap.getTerrainTypes(); buildMeshes(); qDebug() << "TerrainRenderer configured:" << m_width << "x" << m_height << "grid"; } void TerrainRenderer::submit(Renderer &renderer, ResourceManager &resources) { if (m_chunks.empty()) { return; } Texture *white = resources.white(); if (!white) return; auto &visibility = Game::Map::VisibilityService::instance(); const bool useVisibility = visibility.isInitialized(); Mesh *unitMesh = resources.unit(); Mesh *quadMesh = resources.quad(); const float halfWidth = m_width * 0.5f - 0.5f; const float halfHeight = m_height * 0.5f - 0.5f; for (const auto &chunk : m_chunks) { if (!chunk.mesh) continue; if (useVisibility) { bool anyVisible = false; for (int gz = chunk.minZ; gz <= chunk.maxZ && !anyVisible; ++gz) { for (int gx = chunk.minX; gx <= chunk.maxX; ++gx) { if (visibility.stateAt(gx, gz) == Game::Map::VisibilityState::Visible) { anyVisible = true; break; } } } if (!anyVisible) continue; } QMatrix4x4 model; renderer.mesh(chunk.mesh.get(), model, chunk.color, white, 1.0f); } for (const auto &prop : m_props) { if (useVisibility) { int gridX = static_cast( std::round(prop.position.x() / m_tileSize + halfWidth)); int gridZ = static_cast( std::round(prop.position.z() / m_tileSize + halfHeight)); if (visibility.stateAt(gridX, gridZ) != Game::Map::VisibilityState::Visible) continue; } Mesh *mesh = nullptr; switch (prop.type) { case PropType::Pebble: mesh = unitMesh; break; case PropType::Tuft: mesh = unitMesh; break; case PropType::Stick: mesh = unitMesh; break; } if (!mesh) continue; QMatrix4x4 model; model.translate(prop.position); model.rotate(prop.rotationDeg, 0.0f, 1.0f, 0.0f); model.scale(prop.scale); renderer.mesh(mesh, model, prop.color, white, prop.alpha); if (quadMesh) { QMatrix4x4 decal; decal.translate(prop.position.x(), prop.position.y() + 0.01f, prop.position.z()); decal.rotate(-90.0f, 1.0f, 0.0f, 0.0f); decal.scale(prop.scale.x() * 1.4f, prop.scale.z() * 1.4f, 1.0f); QVector3D aoColor(0.06f, 0.06f, 0.055f); renderer.mesh(quadMesh, decal, aoColor, white, 0.35f); } } } int TerrainRenderer::sectionFor(Game::Map::TerrainType type) const { switch (type) { case Game::Map::TerrainType::Mountain: return 2; case Game::Map::TerrainType::Hill: return 1; case Game::Map::TerrainType::Flat: default: return 0; } } void TerrainRenderer::buildMeshes() { QElapsedTimer timer; timer.start(); m_chunks.clear(); m_props.clear(); if (m_width < 2 || m_height < 2 || m_heightData.empty()) { return; } const float halfWidth = m_width * 0.5f - 0.5f; const float halfHeight = m_height * 0.5f - 0.5f; const int vertexCount = m_width * m_height; std::vector positions(vertexCount); std::vector normals(vertexCount, QVector3D(0.0f, 0.0f, 0.0f)); for (int z = 0; z < m_height; ++z) { for (int x = 0; x < m_width; ++x) { int idx = z * m_width + x; float worldX = (x - halfWidth) * m_tileSize; float worldZ = (z - halfHeight) * m_tileSize; positions[idx] = QVector3D(worldX, m_heightData[idx], worldZ); } } auto accumulateNormal = [&](int i0, int i1, int i2) { const QVector3D &v0 = positions[i0]; const QVector3D &v1 = positions[i1]; const QVector3D &v2 = positions[i2]; QVector3D normal = QVector3D::crossProduct(v1 - v0, v2 - v0); normals[i0] += normal; normals[i1] += normal; normals[i2] += normal; }; for (int z = 0; z < m_height - 1; ++z) { for (int x = 0; x < m_width - 1; ++x) { int idx0 = z * m_width + x; int idx1 = idx0 + 1; int idx2 = (z + 1) * m_width + x; int idx3 = idx2 + 1; accumulateNormal(idx0, idx1, idx2); accumulateNormal(idx2, idx1, idx3); } } for (int i = 0; i < vertexCount; ++i) { normals[i].normalize(); if (normals[i].isNull()) { normals[i] = QVector3D(0.0f, 1.0f, 0.0f); } } auto quadSection = [&](Game::Map::TerrainType a, Game::Map::TerrainType b, Game::Map::TerrainType c, Game::Map::TerrainType d) { int priorityA = sectionFor(a); int priorityB = sectionFor(b); int priorityC = sectionFor(c); int priorityD = sectionFor(d); int result = priorityA; result = std::max(result, priorityB); result = std::max(result, priorityC); result = std::max(result, priorityD); return result; }; const int chunkSize = 16; std::size_t totalTriangles = 0; for (int chunkZ = 0; chunkZ < m_height - 1; chunkZ += chunkSize) { int chunkMaxZ = std::min(chunkZ + chunkSize, m_height - 1); for (int chunkX = 0; chunkX < m_width - 1; chunkX += chunkSize) { int chunkMaxX = std::min(chunkX + chunkSize, m_width - 1); struct SectionData { std::vector vertices; std::vector indices; std::unordered_map remap; float heightSum = 0.0f; int heightCount = 0; float rotationDeg = 0.0f; bool flipU = false; float tint = 1.0f; }; SectionData sections[3]; uint32_t chunkSeed = hashCoords(chunkX, chunkZ); uint32_t variantSeed = chunkSeed ^ 0x9e3779b9u; float rotationStep = static_cast((variantSeed >> 5) & 3) * 90.0f; bool flip = ((variantSeed >> 7) & 1u) != 0u; static const float tintVariants[5] = {0.92f, 0.96f, 1.0f, 1.04f, 1.08f}; float tint = tintVariants[(variantSeed >> 12) % 5]; for (auto §ion : sections) { section.rotationDeg = rotationStep; section.flipU = flip; section.tint = tint; } auto ensureVertex = [&](SectionData §ion, int globalIndex) -> unsigned int { auto it = section.remap.find(globalIndex); if (it != section.remap.end()) { return it->second; } Vertex v{}; const QVector3D &pos = positions[globalIndex]; const QVector3D &normal = normals[globalIndex]; int gx = globalIndex % m_width; int gz = globalIndex / m_width; v.position[0] = pos.x(); v.position[1] = pos.y(); v.position[2] = pos.z(); v.normal[0] = normal.x(); v.normal[1] = normal.y(); v.normal[2] = normal.z(); float uu = gx / float(std::max(m_width - 1, 1)); float vv = gz / float(std::max(m_height - 1, 1)); if (section.flipU) uu = 1.0f - uu; float ru = uu - 0.5f; float rv = vv - 0.5f; switch (static_cast(section.rotationDeg)) { case 90: std::swap(ru, rv); ru = -ru; break; case 180: ru = -ru; rv = -rv; break; case 270: std::swap(ru, rv); rv = -rv; break; default: break; } uu = ru + 0.5f; vv = rv + 0.5f; v.texCoord[0] = uu; v.texCoord[1] = vv; section.vertices.push_back(v); unsigned int localIndex = static_cast(section.vertices.size() - 1); section.remap.emplace(globalIndex, localIndex); return localIndex; }; for (int z = chunkZ; z < chunkMaxZ; ++z) { for (int x = chunkX; x < chunkMaxX; ++x) { int idx0 = z * m_width + x; int idx1 = idx0 + 1; int idx2 = (z + 1) * m_width + x; int idx3 = idx2 + 1; float maxHeight = std::max(std::max(m_heightData[idx0], m_heightData[idx1]), std::max(m_heightData[idx2], m_heightData[idx3])); if (maxHeight <= 0.05f) { continue; } int sectionIndex = quadSection(m_terrainTypes[idx0], m_terrainTypes[idx1], m_terrainTypes[idx2], m_terrainTypes[idx3]); SectionData §ion = sections[sectionIndex]; unsigned int v0 = ensureVertex(section, idx0); unsigned int v1 = ensureVertex(section, idx1); unsigned int v2 = ensureVertex(section, idx2); unsigned int v3 = ensureVertex(section, idx3); section.indices.push_back(v0); section.indices.push_back(v1); section.indices.push_back(v2); section.indices.push_back(v2); section.indices.push_back(v1); section.indices.push_back(v3); float quadHeight = (m_heightData[idx0] + m_heightData[idx1] + m_heightData[idx2] + m_heightData[idx3]) * 0.25f; section.heightSum += quadHeight; section.heightCount += 1; } } for (int i = 0; i < 3; ++i) { SectionData §ion = sections[i]; if (section.indices.empty()) { continue; } auto mesh = std::make_unique(section.vertices, section.indices); if (!mesh) continue; ChunkMesh chunk; chunk.mesh = std::move(mesh); chunk.minX = chunkX; chunk.maxX = chunkMaxX - 1; chunk.minZ = chunkZ; chunk.maxZ = chunkMaxZ - 1; chunk.type = (i == 0) ? Game::Map::TerrainType::Flat : (i == 1) ? Game::Map::TerrainType::Hill : Game::Map::TerrainType::Mountain; chunk.averageHeight = (section.heightCount > 0) ? section.heightSum / float(section.heightCount) : 0.0f; QVector3D baseColor = getTerrainColor(chunk.type, chunk.averageHeight); chunk.tint = section.tint; chunk.color = applyTint(baseColor, section.tint); chunk.color = 0.88f * chunk.color + QVector3D(0.07f, 0.07f, 0.07f); if (chunk.type != Game::Map::TerrainType::Mountain) { uint32_t propSeed = hashCoords(chunk.minX, chunk.minZ, static_cast(chunk.type)); uint32_t state = propSeed ^ 0x6d2b79f5u; float spawnChance = rand01(state); int clusterCount = 0; if (spawnChance > 0.65f) { clusterCount = 1; if (rand01(state) > 0.8f) clusterCount += 1; } for (int cluster = 0; cluster < clusterCount; ++cluster) { float gridSpanX = float(chunk.maxX - chunk.minX + 1); float gridSpanZ = float(chunk.maxZ - chunk.minZ + 1); float centerGX = float(chunk.minX) + rand01(state) * gridSpanX; float centerGZ = float(chunk.minZ) + rand01(state) * gridSpanZ; int propsPerCluster = 2 + static_cast(rand01(state) * 3.0f); float scatterRadius = remap(rand01(state), 0.2f, 0.8f) * m_tileSize; for (int p = 0; p < propsPerCluster; ++p) { float angle = rand01(state) * 6.2831853f; float radius = scatterRadius * rand01(state); float gx = centerGX + std::cos(angle) * radius / m_tileSize; float gz = centerGZ + std::sin(angle) * radius / m_tileSize; int sampleGX = std::clamp(static_cast(std::round(gx)), 0, m_width - 1); int sampleGZ = std::clamp(static_cast(std::round(gz)), 0, m_height - 1); float worldX = (gx - halfWidth) * m_tileSize; float worldZ = (gz - halfHeight) * m_tileSize; float worldY = m_heightData[sampleGZ * m_width + sampleGX]; float pick = rand01(state); PropInstance instance; if (pick < 0.45f) { instance.type = PropType::Tuft; instance.color = applyTint(QVector3D(0.26f, 0.6f, 0.22f), remap(rand01(state), 0.9f, 1.15f)); instance.scale = QVector3D(remap(rand01(state), 0.18f, 0.28f), remap(rand01(state), 0.4f, 0.6f), remap(rand01(state), 0.18f, 0.28f)); instance.alpha = 1.0f; } else if (pick < 0.8f) { instance.type = PropType::Pebble; instance.color = applyTint(QVector3D(0.42f, 0.41f, 0.39f), remap(rand01(state), 0.85f, 1.05f)); instance.scale = QVector3D(remap(rand01(state), 0.12f, 0.22f), remap(rand01(state), 0.06f, 0.1f), remap(rand01(state), 0.12f, 0.22f)); instance.alpha = 1.0f; } else { instance.type = PropType::Stick; instance.color = applyTint(QVector3D(0.35f, 0.24f, 0.12f), remap(rand01(state), 0.95f, 1.1f)); instance.scale = QVector3D(remap(rand01(state), 0.05f, 0.09f), remap(rand01(state), 0.35f, 0.55f), remap(rand01(state), 0.05f, 0.09f)); instance.alpha = 1.0f; } instance.rotationDeg = rand01(state) * 360.0f; instance.position = QVector3D(worldX, worldY, worldZ); m_props.push_back(instance); } } } totalTriangles += chunk.mesh->getIndices().size() / 3; m_chunks.push_back(std::move(chunk)); } } } qDebug() << "TerrainRenderer: built" << m_chunks.size() << "chunks in" << timer.elapsed() << "ms" << "triangles:" << totalTriangles; } QVector3D TerrainRenderer::getTerrainColor(Game::Map::TerrainType type, float height) const { switch (type) { case Game::Map::TerrainType::Mountain: if (height > 4.0f) { return QVector3D(0.68f, 0.69f, 0.72f); } else { return QVector3D(0.52f, 0.49f, 0.47f); } case Game::Map::TerrainType::Hill: { float t = std::clamp(height / 3.0f, 0.0f, 1.0f); QVector3D lushGrass(0.36f, 0.65f, 0.30f); QVector3D sunKissed(0.58f, 0.48f, 0.34f); return lushGrass * (1.0f - t) + sunKissed * t; } case Game::Map::TerrainType::Flat: default: return QVector3D(0.26f, 0.56f, 0.29f); } } } // namespace Render::GL