Terrain mesh, vertex color from heightmaps

This commit is contained in:
John McCardle 2026-02-04 14:51:31 -05:00
commit e572269eac
5 changed files with 1400 additions and 3 deletions

535
src/3d/MeshLayer.cpp Normal file
View file

@ -0,0 +1,535 @@
// MeshLayer.cpp - Static 3D geometry layer implementation
#include "MeshLayer.h"
#include "Shader3D.h"
#include "../platform/GLContext.h"
// GL headers based on backend
#if defined(MCRF_SDL2)
#ifdef __EMSCRIPTEN__
#include <GLES2/gl2.h>
#else
#include <GL/gl.h>
#include <GL/glext.h>
#endif
#define MCRF_HAS_GL 1
#elif !defined(MCRF_HEADLESS)
#include <glad/glad.h>
#define MCRF_HAS_GL 1
#endif
namespace mcrf {
// =============================================================================
// Constructor / Destructor
// =============================================================================
MeshLayer::MeshLayer()
: name_("unnamed")
, zIndex_(0)
, visible_(true)
{}
MeshLayer::MeshLayer(const std::string& name, int zIndex)
: name_(name)
, zIndex_(zIndex)
, visible_(true)
{}
MeshLayer::~MeshLayer() {
cleanupGPU();
}
MeshLayer::MeshLayer(MeshLayer&& other) noexcept
: name_(std::move(other.name_))
, zIndex_(other.zIndex_)
, visible_(other.visible_)
, vertices_(std::move(other.vertices_))
, heightData_(std::move(other.heightData_))
, heightmapWidth_(other.heightmapWidth_)
, heightmapHeight_(other.heightmapHeight_)
, vbo_(other.vbo_)
, dirty_(other.dirty_)
, texture_(other.texture_)
, tilesPerRow_(other.tilesPerRow_)
, tilesPerCol_(other.tilesPerCol_)
, modelMatrix_(other.modelMatrix_)
{
other.vbo_ = 0; // Prevent cleanup in moved-from object
}
MeshLayer& MeshLayer::operator=(MeshLayer&& other) noexcept {
if (this != &other) {
cleanupGPU();
name_ = std::move(other.name_);
zIndex_ = other.zIndex_;
visible_ = other.visible_;
vertices_ = std::move(other.vertices_);
heightData_ = std::move(other.heightData_);
heightmapWidth_ = other.heightmapWidth_;
heightmapHeight_ = other.heightmapHeight_;
vbo_ = other.vbo_;
dirty_ = other.dirty_;
texture_ = other.texture_;
tilesPerRow_ = other.tilesPerRow_;
tilesPerCol_ = other.tilesPerCol_;
modelMatrix_ = other.modelMatrix_;
other.vbo_ = 0;
}
return *this;
}
// =============================================================================
// Configuration
// =============================================================================
void MeshLayer::setSpriteSheetLayout(int tilesPerRow, int tilesPerCol) {
tilesPerRow_ = tilesPerRow > 0 ? tilesPerRow : 1;
tilesPerCol_ = tilesPerCol > 0 ? tilesPerCol : 1;
}
// =============================================================================
// Mesh Generation - HeightMap
// =============================================================================
void MeshLayer::buildFromHeightmap(TCOD_heightmap_t* heightmap, float yScale, float cellSize) {
if (!heightmap || heightmap->w < 2 || heightmap->h < 2) {
return;
}
int w = heightmap->w;
int h = heightmap->h;
// Store heightmap dimensions and data for later texture range application
heightmapWidth_ = w;
heightmapHeight_ = h;
heightData_.resize(w * h);
for (int i = 0; i < w * h; i++) {
heightData_[i] = heightmap->values[i];
}
// Calculate grid vertices
// For an NxM heightmap, we create (N-1)×(M-1) quads = 2×(N-1)×(M-1) triangles
int numQuadsX = w - 1;
int numQuadsZ = h - 1;
int numTriangles = numQuadsX * numQuadsZ * 2;
int numVertices = numTriangles * 3;
vertices_.clear();
vertices_.reserve(numVertices);
// Generate triangles for each quad
for (int z = 0; z < numQuadsZ; z++) {
for (int x = 0; x < numQuadsX; x++) {
// Get heights at quad corners (in heightmap, z is row index, x is column)
float h00 = heightmap->values[z * w + x] * yScale;
float h10 = heightmap->values[z * w + (x + 1)] * yScale;
float h01 = heightmap->values[(z + 1) * w + x] * yScale;
float h11 = heightmap->values[(z + 1) * w + (x + 1)] * yScale;
// World positions
vec3 p00(x * cellSize, h00, z * cellSize);
vec3 p10((x + 1) * cellSize, h10, z * cellSize);
vec3 p01(x * cellSize, h01, (z + 1) * cellSize);
vec3 p11((x + 1) * cellSize, h11, (z + 1) * cellSize);
// UVs (tiled across terrain, will be adjusted by applyTextureRanges)
vec2 uv00(static_cast<float>(x), static_cast<float>(z));
vec2 uv10(static_cast<float>(x + 1), static_cast<float>(z));
vec2 uv01(static_cast<float>(x), static_cast<float>(z + 1));
vec2 uv11(static_cast<float>(x + 1), static_cast<float>(z + 1));
// Default color (white, will be modulated by texture)
vec4 color(1.0f, 1.0f, 1.0f, 1.0f);
// Triangle 1: p00 -> p01 -> p10 (counter-clockwise from above)
// This ensures the normal points UP (+Y) for proper backface culling
vec3 n1 = computeFaceNormal(p00, p01, p10);
vertices_.emplace_back(p00, uv00, n1, color);
vertices_.emplace_back(p01, uv01, n1, color);
vertices_.emplace_back(p10, uv10, n1, color);
// Triangle 2: p10 -> p01 -> p11 (counter-clockwise from above)
vec3 n2 = computeFaceNormal(p10, p01, p11);
vertices_.emplace_back(p10, uv10, n2, color);
vertices_.emplace_back(p01, uv01, n2, color);
vertices_.emplace_back(p11, uv11, n2, color);
}
}
// Compute smooth vertex normals (average adjacent face normals)
computeVertexNormals();
dirty_ = true;
}
// =============================================================================
// Mesh Generation - Plane
// =============================================================================
void MeshLayer::buildPlane(float width, float depth, float y) {
vertices_.clear();
vertices_.reserve(6); // 2 triangles
// Clear height data (plane has no height variation)
heightData_.clear();
heightmapWidth_ = 0;
heightmapHeight_ = 0;
float halfW = width * 0.5f;
float halfD = depth * 0.5f;
vec3 p00(-halfW, y, -halfD);
vec3 p10(halfW, y, -halfD);
vec3 p01(-halfW, y, halfD);
vec3 p11(halfW, y, halfD);
vec3 normal(0, 1, 0); // Facing up
vec4 color(1, 1, 1, 1);
// Triangle 1
vertices_.emplace_back(p00, vec2(0, 0), normal, color);
vertices_.emplace_back(p10, vec2(1, 0), normal, color);
vertices_.emplace_back(p01, vec2(0, 1), normal, color);
// Triangle 2
vertices_.emplace_back(p10, vec2(1, 0), normal, color);
vertices_.emplace_back(p11, vec2(1, 1), normal, color);
vertices_.emplace_back(p01, vec2(0, 1), normal, color);
dirty_ = true;
}
// =============================================================================
// Texture Ranges
// =============================================================================
void MeshLayer::applyTextureRanges(const std::vector<TextureRange>& ranges) {
if (ranges.empty() || heightData_.empty() || vertices_.empty()) {
return;
}
// Calculate tile UV size
float tileU = 1.0f / tilesPerRow_;
float tileV = 1.0f / tilesPerCol_;
// For each vertex, find its height and apply the appropriate texture
// Vertices are stored as triangles, 6 per quad (2 triangles × 3 vertices)
int numQuadsX = heightmapWidth_ - 1;
int numQuadsZ = heightmapHeight_ - 1;
for (int z = 0; z < numQuadsZ; z++) {
for (int x = 0; x < numQuadsX; x++) {
int quadIndex = z * numQuadsX + x;
int baseVertex = quadIndex * 6;
// Get heights at quad corners (normalized 0-1)
float h00 = heightData_[z * heightmapWidth_ + x];
float h10 = heightData_[z * heightmapWidth_ + (x + 1)];
float h01 = heightData_[(z + 1) * heightmapWidth_ + x];
float h11 = heightData_[(z + 1) * heightmapWidth_ + (x + 1)];
// Use average height to select texture
float avgHeight = (h00 + h10 + h01 + h11) * 0.25f;
// Find matching range
int spriteIndex = 0;
for (const auto& range : ranges) {
if (avgHeight >= range.minHeight && avgHeight <= range.maxHeight) {
spriteIndex = range.spriteIndex;
break;
}
}
// Calculate sprite UV offset in sprite sheet
int tileX = spriteIndex % tilesPerRow_;
int tileY = spriteIndex / tilesPerRow_;
float uOffset = tileX * tileU;
float vOffset = tileY * tileV;
// Update UVs for all 6 vertices of this quad
// Triangle 1: p00, p10, p01
if (baseVertex + 5 < static_cast<int>(vertices_.size())) {
// Local UV within quad (0-1) scaled to tile size and offset
vertices_[baseVertex + 0].texcoord = vec2(uOffset, vOffset);
vertices_[baseVertex + 1].texcoord = vec2(uOffset + tileU, vOffset);
vertices_[baseVertex + 2].texcoord = vec2(uOffset, vOffset + tileV);
// Triangle 2: p10, p11, p01
vertices_[baseVertex + 3].texcoord = vec2(uOffset + tileU, vOffset);
vertices_[baseVertex + 4].texcoord = vec2(uOffset + tileU, vOffset + tileV);
vertices_[baseVertex + 5].texcoord = vec2(uOffset, vOffset + tileV);
}
}
}
dirty_ = true;
}
// =============================================================================
// Color Map
// =============================================================================
void MeshLayer::applyColorMap(TCOD_heightmap_t* rMap, TCOD_heightmap_t* gMap, TCOD_heightmap_t* bMap) {
if (!rMap || !gMap || !bMap) {
return;
}
if (vertices_.empty() || heightmapWidth_ < 2 || heightmapHeight_ < 2) {
return;
}
// Verify color maps match terrain dimensions
if (rMap->w != heightmapWidth_ || rMap->h != heightmapHeight_ ||
gMap->w != heightmapWidth_ || gMap->h != heightmapHeight_ ||
bMap->w != heightmapWidth_ || bMap->h != heightmapHeight_) {
return; // Dimension mismatch
}
int numQuadsX = heightmapWidth_ - 1;
int numQuadsZ = heightmapHeight_ - 1;
for (int z = 0; z < numQuadsZ; z++) {
for (int x = 0; x < numQuadsX; x++) {
int quadIndex = z * numQuadsX + x;
int baseVertex = quadIndex * 6;
if (baseVertex + 5 >= static_cast<int>(vertices_.size())) {
continue;
}
// Sample RGB at each corner of the quad
// Corner indices in heightmap
int idx00 = z * heightmapWidth_ + x;
int idx10 = z * heightmapWidth_ + (x + 1);
int idx01 = (z + 1) * heightmapWidth_ + x;
int idx11 = (z + 1) * heightmapWidth_ + (x + 1);
// Build colors for each corner (clamped to 0-1)
auto clamp01 = [](float v) { return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); };
vec4 c00(clamp01(rMap->values[idx00]), clamp01(gMap->values[idx00]), clamp01(bMap->values[idx00]), 1.0f);
vec4 c10(clamp01(rMap->values[idx10]), clamp01(gMap->values[idx10]), clamp01(bMap->values[idx10]), 1.0f);
vec4 c01(clamp01(rMap->values[idx01]), clamp01(gMap->values[idx01]), clamp01(bMap->values[idx01]), 1.0f);
vec4 c11(clamp01(rMap->values[idx11]), clamp01(gMap->values[idx11]), clamp01(bMap->values[idx11]), 1.0f);
// Triangle 1: p00, p01, p10 (vertices 0, 1, 2)
vertices_[baseVertex + 0].color = c00;
vertices_[baseVertex + 1].color = c01;
vertices_[baseVertex + 2].color = c10;
// Triangle 2: p10, p01, p11 (vertices 3, 4, 5)
vertices_[baseVertex + 3].color = c10;
vertices_[baseVertex + 4].color = c01;
vertices_[baseVertex + 5].color = c11;
}
}
dirty_ = true;
}
// =============================================================================
// Clear
// =============================================================================
void MeshLayer::clear() {
vertices_.clear();
heightData_.clear();
heightmapWidth_ = 0;
heightmapHeight_ = 0;
dirty_ = true;
}
// =============================================================================
// GPU Upload
// =============================================================================
void MeshLayer::uploadToGPU() {
#ifdef MCRF_HAS_GL
if (!gl::isGLReady()) {
return;
}
// Create VBO if needed
if (vbo_ == 0) {
glGenBuffers(1, &vbo_);
}
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
if (!vertices_.empty()) {
glBufferData(GL_ARRAY_BUFFER,
vertices_.size() * sizeof(MeshVertex),
vertices_.data(),
GL_STATIC_DRAW);
} else {
glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_STATIC_DRAW);
}
glBindBuffer(GL_ARRAY_BUFFER, 0);
dirty_ = false;
#endif
}
// =============================================================================
// Rendering
// =============================================================================
void MeshLayer::render(const mat4& model, const mat4& view, const mat4& projection) {
#ifdef MCRF_HAS_GL
if (!gl::isGLReady() || vertices_.empty()) {
return;
}
// Upload to GPU if needed
if (dirty_ || vbo_ == 0) {
uploadToGPU();
}
if (vbo_ == 0) {
return;
}
// Bind VBO
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
// Vertex format: pos(3) + texcoord(2) + normal(3) + color(4) = 12 floats = 48 bytes
int stride = sizeof(MeshVertex);
// Set up vertex attributes
glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION);
glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, position)));
glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, texcoord)));
glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, normal)));
glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR);
glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE,
stride, reinterpret_cast<void*>(offsetof(MeshVertex, color)));
// Draw triangles
glDrawArrays(GL_TRIANGLES, 0, static_cast<int>(vertices_.size()));
// Cleanup
glDisableVertexAttribArray(Shader3D::ATTRIB_POSITION);
glDisableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
glDisableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
glDisableVertexAttribArray(Shader3D::ATTRIB_COLOR);
glBindBuffer(GL_ARRAY_BUFFER, 0);
#endif
}
// =============================================================================
// Private Helpers
// =============================================================================
void MeshLayer::cleanupGPU() {
#ifdef MCRF_HAS_GL
if (vbo_ != 0 && gl::isGLReady()) {
glDeleteBuffers(1, &vbo_);
vbo_ = 0;
}
#endif
}
vec3 MeshLayer::computeFaceNormal(const vec3& v0, const vec3& v1, const vec3& v2) {
vec3 edge1 = v1 - v0;
vec3 edge2 = v2 - v0;
return edge1.cross(edge2).normalized();
}
void MeshLayer::computeVertexNormals() {
// For terrain mesh, we can average normals at shared positions
// This is a simplified approach - works well for regular grids
if (vertices_.empty() || heightmapWidth_ < 2 || heightmapHeight_ < 2) {
return;
}
// Create a grid of accumulated normals for each heightmap point
std::vector<vec3> accumulatedNormals(heightmapWidth_ * heightmapHeight_, vec3(0, 0, 0));
std::vector<int> normalCounts(heightmapWidth_ * heightmapHeight_, 0);
// Each quad contributes to its 4 corners
int numQuadsX = heightmapWidth_ - 1;
int numQuadsZ = heightmapHeight_ - 1;
for (int z = 0; z < numQuadsZ; z++) {
for (int x = 0; x < numQuadsX; x++) {
int quadIndex = z * numQuadsX + x;
int baseVertex = quadIndex * 6;
// Get face normals from the two triangles
if (baseVertex + 5 < static_cast<int>(vertices_.size())) {
vec3 n1 = vertices_[baseVertex].normal;
vec3 n2 = vertices_[baseVertex + 3].normal;
// Corners: (x,z), (x+1,z), (x,z+1), (x+1,z+1)
int idx00 = z * heightmapWidth_ + x;
int idx10 = z * heightmapWidth_ + (x + 1);
int idx01 = (z + 1) * heightmapWidth_ + x;
int idx11 = (z + 1) * heightmapWidth_ + (x + 1);
// Triangle 1 (p00, p01, p10) contributes n1 to those corners
accumulatedNormals[idx00] += n1;
normalCounts[idx00]++;
accumulatedNormals[idx01] += n1;
normalCounts[idx01]++;
accumulatedNormals[idx10] += n1;
normalCounts[idx10]++;
// Triangle 2 (p10, p01, p11) contributes n2 to those corners
accumulatedNormals[idx10] += n2;
normalCounts[idx10]++;
accumulatedNormals[idx01] += n2;
normalCounts[idx01]++;
accumulatedNormals[idx11] += n2;
normalCounts[idx11]++;
}
}
}
// Normalize accumulated normals
for (size_t i = 0; i < accumulatedNormals.size(); i++) {
if (normalCounts[i] > 0) {
accumulatedNormals[i] = accumulatedNormals[i].normalized();
} else {
accumulatedNormals[i] = vec3(0, 1, 0); // Default up
}
}
// Apply averaged normals back to vertices
for (int z = 0; z < numQuadsZ; z++) {
for (int x = 0; x < numQuadsX; x++) {
int quadIndex = z * numQuadsX + x;
int baseVertex = quadIndex * 6;
int idx00 = z * heightmapWidth_ + x;
int idx10 = z * heightmapWidth_ + (x + 1);
int idx01 = (z + 1) * heightmapWidth_ + x;
int idx11 = (z + 1) * heightmapWidth_ + (x + 1);
if (baseVertex + 5 < static_cast<int>(vertices_.size())) {
// Triangle 1: p00, p01, p10
vertices_[baseVertex + 0].normal = accumulatedNormals[idx00];
vertices_[baseVertex + 1].normal = accumulatedNormals[idx01];
vertices_[baseVertex + 2].normal = accumulatedNormals[idx10];
// Triangle 2: p10, p01, p11
vertices_[baseVertex + 3].normal = accumulatedNormals[idx10];
vertices_[baseVertex + 4].normal = accumulatedNormals[idx01];
vertices_[baseVertex + 5].normal = accumulatedNormals[idx11];
}
}
}
}
} // namespace mcrf

173
src/3d/MeshLayer.h Normal file
View file

@ -0,0 +1,173 @@
// MeshLayer.h - Static 3D geometry layer for Viewport3D
// Supports terrain generation from HeightMap and height-based texture mapping
#pragma once
#include "Common.h"
#include "Math3D.h"
#include <memory>
#include <string>
#include <vector>
#include <libtcod.h> // For TCOD_heightmap_t
namespace mcrf {
// =============================================================================
// MeshVertex - Vertex format matching Viewport3D's shader attributes
// =============================================================================
struct MeshVertex {
vec3 position; // 12 bytes
vec2 texcoord; // 8 bytes
vec3 normal; // 12 bytes
vec4 color; // 16 bytes (RGBA as floats 0-1)
// Total: 48 bytes per vertex
MeshVertex()
: position(0, 0, 0)
, texcoord(0, 0)
, normal(0, 1, 0)
, color(1, 1, 1, 1)
{}
MeshVertex(const vec3& pos, const vec2& uv, const vec3& norm, const vec4& col)
: position(pos), texcoord(uv), normal(norm), color(col)
{}
};
// =============================================================================
// TextureRange - Height-based texture selection from sprite sheet
// =============================================================================
struct TextureRange {
float minHeight; // Minimum normalized height (0-1)
float maxHeight; // Maximum normalized height (0-1)
int spriteIndex; // Index into sprite sheet
TextureRange() : minHeight(0), maxHeight(1), spriteIndex(0) {}
TextureRange(float min, float max, int index)
: minHeight(min), maxHeight(max), spriteIndex(index) {}
};
// =============================================================================
// MeshLayer - Container for static 3D geometry
// =============================================================================
class MeshLayer {
public:
MeshLayer();
MeshLayer(const std::string& name, int zIndex = 0);
~MeshLayer();
// No copy, allow move
MeshLayer(const MeshLayer&) = delete;
MeshLayer& operator=(const MeshLayer&) = delete;
MeshLayer(MeshLayer&& other) noexcept;
MeshLayer& operator=(MeshLayer&& other) noexcept;
// =========================================================================
// Core Properties
// =========================================================================
const std::string& getName() const { return name_; }
void setName(const std::string& name) { name_ = name; }
int getZIndex() const { return zIndex_; }
void setZIndex(int z) { zIndex_ = z; }
bool isVisible() const { return visible_; }
void setVisible(bool v) { visible_ = v; }
// Texture (sprite sheet for height-based mapping)
void setTexture(sf::Texture* tex) { texture_ = tex; }
sf::Texture* getTexture() const { return texture_; }
// Sprite sheet configuration (for texture ranges)
void setSpriteSheetLayout(int tilesPerRow, int tilesPerCol);
// =========================================================================
// Mesh Generation
// =========================================================================
/// Build terrain mesh from HeightMap
/// @param heightmap libtcod heightmap pointer
/// @param yScale Vertical exaggeration factor
/// @param cellSize World-space size of each grid cell
void buildFromHeightmap(TCOD_heightmap_t* heightmap, float yScale, float cellSize);
/// Build a flat plane (for floors, water, etc.)
/// @param width World-space width (X axis)
/// @param depth World-space depth (Z axis)
/// @param y World-space height
void buildPlane(float width, float depth, float y = 0.0f);
/// Apply height-based texture ranges
/// Updates vertex UVs based on stored height data
void applyTextureRanges(const std::vector<TextureRange>& ranges);
/// Apply per-vertex colors from RGB heightmaps
/// Each heightmap provides one color channel (values 0-1 map to intensity)
/// @param rMap Red channel heightmap (must match terrain dimensions)
/// @param gMap Green channel heightmap
/// @param bMap Blue channel heightmap
void applyColorMap(TCOD_heightmap_t* rMap, TCOD_heightmap_t* gMap, TCOD_heightmap_t* bMap);
/// Clear all geometry
void clear();
// =========================================================================
// GPU Upload and Rendering
// =========================================================================
/// Upload vertex data to GPU
/// Call after modifying vertices or when dirty_ flag is set
void uploadToGPU();
/// Render this layer
/// @param model Model transformation matrix
/// @param view View matrix from camera
/// @param projection Projection matrix from camera
void render(const mat4& model, const mat4& view, const mat4& projection);
/// Get model matrix (identity by default, override for positioned layers)
mat4 getModelMatrix() const { return modelMatrix_; }
void setModelMatrix(const mat4& m) { modelMatrix_ = m; }
// =========================================================================
// Statistics
// =========================================================================
size_t getVertexCount() const { return vertices_.size(); }
bool isDirty() const { return dirty_; }
private:
// Identity
std::string name_;
int zIndex_ = 0;
bool visible_ = true;
// Geometry data (CPU side)
std::vector<MeshVertex> vertices_;
std::vector<float> heightData_; // Original heights for texture range re-application
int heightmapWidth_ = 0;
int heightmapHeight_ = 0;
// GPU resources
unsigned int vbo_ = 0;
bool dirty_ = false; // Needs GPU re-upload
// Texture
sf::Texture* texture_ = nullptr; // Not owned
int tilesPerRow_ = 1;
int tilesPerCol_ = 1;
// Transform
mat4 modelMatrix_ = mat4::identity();
// Helper methods
void cleanupGPU();
vec3 computeFaceNormal(const vec3& v0, const vec3& v1, const vec3& v2);
void computeVertexNormals();
};
} // namespace mcrf

View file

@ -2,6 +2,7 @@
#include "Viewport3D.h"
#include "Shader3D.h"
#include "MeshLayer.h"
#include "../platform/GLContext.h"
#include "PyVector.h"
#include "PyColor.h"
@ -9,8 +10,11 @@
#include "McRFPy_Doc.h"
#include "PythonObjectCache.h"
#include "McRFPy_API.h"
#include "PyHeightMap.h"
#include <set>
#include <cstring>
#include <cmath>
#include <algorithm>
// Include appropriate GL headers based on backend
#if defined(MCRF_SDL2)
@ -154,6 +158,58 @@ void Viewport3D::setFogRange(float nearDist, float farDist) {
fogFar_ = farDist;
}
// =============================================================================
// Camera Helpers
// =============================================================================
void Viewport3D::orbitCamera(float angle, float distance, float height) {
float x = std::cos(angle) * distance;
float z = std::sin(angle) * distance;
camera_.setPosition(vec3(x, height, z));
camera_.setTarget(vec3(0, 0, 0));
}
// =============================================================================
// Mesh Layer Management
// =============================================================================
std::shared_ptr<MeshLayer> Viewport3D::addLayer(const std::string& name, int zIndex) {
// Check if layer with this name already exists
for (auto& layer : meshLayers_) {
if (layer->getName() == name) {
return layer; // Return existing layer
}
}
// Create new layer
auto layer = std::make_shared<MeshLayer>(name, zIndex);
meshLayers_.push_back(layer);
// Disable test cube when layers are added
renderTestCube_ = false;
return layer;
}
std::shared_ptr<MeshLayer> Viewport3D::getLayer(const std::string& name) {
for (auto& layer : meshLayers_) {
if (layer->getName() == name) {
return layer;
}
}
return nullptr;
}
bool Viewport3D::removeLayer(const std::string& name) {
for (auto it = meshLayers_.begin(); it != meshLayers_.end(); ++it) {
if ((*it)->getName() == name) {
meshLayers_.erase(it);
return true;
}
}
return false;
}
// =============================================================================
// FBO Management
// =============================================================================
@ -270,6 +326,66 @@ void Viewport3D::cleanupTestGeometry() {
// 3D Rendering
// =============================================================================
void Viewport3D::renderMeshLayers() {
#ifdef MCRF_HAS_GL
if (meshLayers_.empty() || !shader_ || !shader_->isValid()) {
return;
}
// Sort layers by z_index (lower = rendered first)
std::vector<MeshLayer*> sortedLayers;
sortedLayers.reserve(meshLayers_.size());
for (auto& layer : meshLayers_) {
if (layer && layer->isVisible()) {
sortedLayers.push_back(layer.get());
}
}
std::sort(sortedLayers.begin(), sortedLayers.end(),
[](const MeshLayer* a, const MeshLayer* b) {
return a->getZIndex() < b->getZIndex();
});
shader_->bind();
// Set up view and projection matrices (same for all layers)
mat4 view = camera_.getViewMatrix();
mat4 projection = camera_.getProjectionMatrix();
shader_->setUniform("u_view", view);
shader_->setUniform("u_projection", projection);
// PS1 effect uniforms
shader_->setUniform("u_resolution", vec2(static_cast<float>(internalWidth_),
static_cast<float>(internalHeight_)));
shader_->setUniform("u_enable_snap", vertexSnapEnabled_);
shader_->setUniform("u_enable_dither", ditheringEnabled_);
// Lighting
vec3 lightDir = vec3(0.5f, -0.7f, 0.5f).normalized();
shader_->setUniform("u_light_dir", lightDir);
shader_->setUniform("u_ambient", vec3(0.3f, 0.3f, 0.3f));
// Fog
shader_->setUniform("u_fog_start", fogNear_);
shader_->setUniform("u_fog_end", fogFar_);
shader_->setUniform("u_fog_color", fogColor_);
// For now, no textures on terrain (use vertex colors)
shader_->setUniform("u_has_texture", false);
// Render each layer
for (auto* layer : sortedLayers) {
// Set model matrix for this layer
shader_->setUniform("u_model", layer->getModelMatrix());
// Render the layer's geometry
layer->render(layer->getModelMatrix(), view, projection);
}
shader_->unbind();
#endif
}
void Viewport3D::render3DContent() {
// GL not available in current backend - skip 3D rendering
if (!gl::isGLReady() || fbo_ == 0) {
@ -297,8 +413,11 @@ void Viewport3D::render3DContent() {
// Update test rotation for spinning geometry
testRotation_ += 0.02f;
// Render test cube if shader and geometry are ready
if (shader_ && shader_->isValid() && testVBO_ != 0) {
// Render mesh layers first (terrain, etc.) - sorted by z_index
renderMeshLayers();
// Render test cube if enabled (disabled when layers are added)
if (renderTestCube_ && shader_ && shader_->isValid() && testVBO_ != 0) {
shader_->bind();
// Set up matrices
@ -947,6 +1066,178 @@ int Viewport3D::init(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
return 0;
}
// =============================================================================
// Python Methods for Layer Management
// =============================================================================
static PyObject* Viewport3D_add_layer(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"name", "z_index", NULL};
const char* name = nullptr;
int z_index = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|i", const_cast<char**>(kwlist), &name, &z_index)) {
return NULL;
}
auto layer = self->data->addLayer(name, z_index);
if (!layer) {
PyErr_SetString(PyExc_RuntimeError, "Failed to create layer");
return NULL;
}
// Return a dictionary with layer info (simple approach)
// TODO: Create proper PyMeshLayer type for full API
return Py_BuildValue("{s:s, s:i, s:i, s:n}",
"name", layer->getName().c_str(),
"z_index", layer->getZIndex(),
"vertex_count", static_cast<int>(layer->getVertexCount()),
"layer_ptr", reinterpret_cast<Py_ssize_t>(layer.get()));
}
static PyObject* Viewport3D_get_layer(PyViewport3DObject* self, PyObject* args) {
const char* name = nullptr;
if (!PyArg_ParseTuple(args, "s", &name)) {
return NULL;
}
auto layer = self->data->getLayer(name);
if (!layer) {
Py_RETURN_NONE;
}
return Py_BuildValue("{s:s, s:i, s:i, s:n}",
"name", layer->getName().c_str(),
"z_index", layer->getZIndex(),
"vertex_count", static_cast<int>(layer->getVertexCount()),
"layer_ptr", reinterpret_cast<Py_ssize_t>(layer.get()));
}
static PyObject* Viewport3D_remove_layer(PyViewport3DObject* self, PyObject* args) {
const char* name = nullptr;
if (!PyArg_ParseTuple(args, "s", &name)) {
return NULL;
}
bool removed = self->data->removeLayer(name);
return PyBool_FromLong(removed);
}
static PyObject* Viewport3D_orbit_camera(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"angle", "distance", "height", NULL};
float angle = 0.0f;
float distance = 10.0f;
float height = 5.0f;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fff", const_cast<char**>(kwlist),
&angle, &distance, &height)) {
return NULL;
}
self->data->orbitCamera(angle, distance, height);
Py_RETURN_NONE;
}
static PyObject* Viewport3D_build_terrain(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"layer_name", "heightmap", "y_scale", "cell_size", NULL};
const char* layer_name = nullptr;
PyObject* heightmap_obj = nullptr;
float y_scale = 1.0f;
float cell_size = 1.0f;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sO|ff", const_cast<char**>(kwlist),
&layer_name, &heightmap_obj, &y_scale, &cell_size)) {
return NULL;
}
// Get or create the layer
auto layer = self->data->getLayer(layer_name);
if (!layer) {
layer = self->data->addLayer(layer_name, 0);
}
// Check if heightmap_obj is a PyHeightMapObject
// Get the HeightMap type from the module
PyObject* heightmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap");
if (!heightmap_type) {
PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found");
return NULL;
}
if (!PyObject_IsInstance(heightmap_obj, heightmap_type)) {
Py_DECREF(heightmap_type);
PyErr_SetString(PyExc_TypeError, "heightmap must be a HeightMap object");
return NULL;
}
Py_DECREF(heightmap_type);
// Get the TCOD heightmap pointer from the Python object
PyHeightMapObject* hm = reinterpret_cast<PyHeightMapObject*>(heightmap_obj);
if (!hm->heightmap) {
PyErr_SetString(PyExc_ValueError, "HeightMap has no data");
return NULL;
}
// Build the terrain mesh
layer->buildFromHeightmap(hm->heightmap, y_scale, cell_size);
return Py_BuildValue("i", static_cast<int>(layer->getVertexCount()));
}
static PyObject* Viewport3D_apply_terrain_colors(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"layer_name", "r_map", "g_map", "b_map", NULL};
const char* layer_name = nullptr;
PyObject* r_obj = nullptr;
PyObject* g_obj = nullptr;
PyObject* b_obj = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOOO", const_cast<char**>(kwlist),
&layer_name, &r_obj, &g_obj, &b_obj)) {
return NULL;
}
// Get the layer
auto layer = self->data->getLayer(layer_name);
if (!layer) {
PyErr_Format(PyExc_ValueError, "Layer '%s' not found", layer_name);
return NULL;
}
// Validate all three are HeightMap objects
PyObject* heightmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap");
if (!heightmap_type) {
PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found");
return NULL;
}
if (!PyObject_IsInstance(r_obj, heightmap_type) ||
!PyObject_IsInstance(g_obj, heightmap_type) ||
!PyObject_IsInstance(b_obj, heightmap_type)) {
Py_DECREF(heightmap_type);
PyErr_SetString(PyExc_TypeError, "r_map, g_map, and b_map must all be HeightMap objects");
return NULL;
}
Py_DECREF(heightmap_type);
// Get the TCOD heightmap pointers
PyHeightMapObject* r_hm = reinterpret_cast<PyHeightMapObject*>(r_obj);
PyHeightMapObject* g_hm = reinterpret_cast<PyHeightMapObject*>(g_obj);
PyHeightMapObject* b_hm = reinterpret_cast<PyHeightMapObject*>(b_obj);
if (!r_hm->heightmap || !g_hm->heightmap || !b_hm->heightmap) {
PyErr_SetString(PyExc_ValueError, "One or more HeightMap objects have no data");
return NULL;
}
// Apply the color map
layer->applyColorMap(r_hm->heightmap, g_hm->heightmap, b_hm->heightmap);
Py_RETURN_NONE;
}
static PyObject* Viewport3D_layer_count(PyViewport3DObject* self, PyObject* Py_UNUSED(args)) {
return PyLong_FromSize_t(self->data->getLayerCount());
}
} // namespace mcrf
// Methods array - outside namespace but PyObjectType still in scope via typedef
@ -954,5 +1245,46 @@ typedef PyViewport3DObject PyObjectType;
PyMethodDef Viewport3D_methods[] = {
UIDRAWABLE_METHODS,
{"add_layer", (PyCFunction)mcrf::Viewport3D_add_layer, METH_VARARGS | METH_KEYWORDS,
"add_layer(name, z_index=0) -> dict\n\n"
"Add a new mesh layer to the viewport.\n\n"
"Args:\n"
" name: Unique identifier for the layer\n"
" z_index: Render order (lower = rendered first)"},
{"get_layer", (PyCFunction)mcrf::Viewport3D_get_layer, METH_VARARGS,
"get_layer(name) -> dict or None\n\n"
"Get a layer by name."},
{"remove_layer", (PyCFunction)mcrf::Viewport3D_remove_layer, METH_VARARGS,
"remove_layer(name) -> bool\n\n"
"Remove a layer by name. Returns True if found and removed."},
{"orbit_camera", (PyCFunction)mcrf::Viewport3D_orbit_camera, METH_VARARGS | METH_KEYWORDS,
"orbit_camera(angle=0, distance=10, height=5)\n\n"
"Position camera to orbit around origin.\n\n"
"Args:\n"
" angle: Orbit angle in radians\n"
" distance: Distance from origin\n"
" height: Camera height above XZ plane"},
{"build_terrain", (PyCFunction)mcrf::Viewport3D_build_terrain, METH_VARARGS | METH_KEYWORDS,
"build_terrain(layer_name, heightmap, y_scale=1.0, cell_size=1.0) -> int\n\n"
"Build terrain mesh from HeightMap on specified layer.\n\n"
"Args:\n"
" layer_name: Name of layer to build terrain on (created if doesn't exist)\n"
" heightmap: HeightMap object with height data\n"
" y_scale: Vertical exaggeration factor\n"
" cell_size: World-space size of each grid cell\n\n"
"Returns:\n"
" Number of vertices in the generated mesh"},
{"apply_terrain_colors", (PyCFunction)mcrf::Viewport3D_apply_terrain_colors, METH_VARARGS | METH_KEYWORDS,
"apply_terrain_colors(layer_name, r_map, g_map, b_map)\n\n"
"Apply per-vertex colors to terrain from RGB HeightMaps.\n\n"
"Args:\n"
" layer_name: Name of terrain layer to colorize\n"
" r_map: HeightMap for red channel (0-1 values)\n"
" g_map: HeightMap for green channel (0-1 values)\n"
" b_map: HeightMap for blue channel (0-1 values)\n\n"
"All HeightMaps must match the terrain's original dimensions."},
{"layer_count", (PyCFunction)mcrf::Viewport3D_layer_count, METH_NOARGS,
"layer_count() -> int\n\n"
"Get the number of mesh layers."},
{NULL} // Sentinel
};

View file

@ -12,12 +12,15 @@
#include "Math3D.h"
#include "Camera3D.h"
#include <memory>
#include <vector>
#include <algorithm>
namespace mcrf {
// Forward declarations
class Viewport3D;
class Shader3D;
class MeshLayer;
} // namespace mcrf
@ -69,6 +72,33 @@ public:
vec3 getCameraPosition() const { return camera_.getPosition(); }
vec3 getCameraTarget() const { return camera_.getTarget(); }
// Camera orbit helper for demos
void orbitCamera(float angle, float distance, float height);
// =========================================================================
// Mesh Layer Management
// =========================================================================
/// Add a new mesh layer
/// @param name Unique identifier for the layer
/// @param zIndex Render order (lower = rendered first, behind higher values)
/// @return Pointer to the new layer (owned by Viewport3D)
std::shared_ptr<MeshLayer> addLayer(const std::string& name, int zIndex = 0);
/// Get a layer by name
/// @return Pointer to layer, or nullptr if not found
std::shared_ptr<MeshLayer> getLayer(const std::string& name);
/// Remove a layer by name
/// @return true if layer was found and removed
bool removeLayer(const std::string& name);
/// Get all layers (read-only)
const std::vector<std::shared_ptr<MeshLayer>>& getLayers() const { return meshLayers_; }
/// Get number of layers
size_t getLayerCount() const { return meshLayers_.size(); }
// Background color
void setBackgroundColor(const sf::Color& color) { bgColor_ = color; }
sf::Color getBackgroundColor() const { return bgColor_; }
@ -137,8 +167,12 @@ private:
float fogNear_ = 10.0f;
float fogFar_ = 100.0f;
// Render test geometry (temporary until Entity3D/MeshLayer added)
// Render test geometry (temporary, will be replaced by layers)
float testRotation_ = 0.0f;
bool renderTestCube_ = true; // Set to false when layers are added
// Mesh layers for terrain, static geometry
std::vector<std::shared_ptr<MeshLayer>> meshLayers_;
// Shader for PS1-style rendering
std::unique_ptr<Shader3D> shader_;
@ -162,6 +196,9 @@ private:
// Render 3D content to FBO
void render3DContent();
// Render all mesh layers
void renderMeshLayers();
// Blit FBO to screen
void blitToScreen(sf::Vector2f offset, sf::RenderTarget& target);
};