Terrain mesh, vertex color from heightmaps
This commit is contained in:
parent
9c29567349
commit
e572269eac
5 changed files with 1400 additions and 3 deletions
535
src/3d/MeshLayer.cpp
Normal file
535
src/3d/MeshLayer.cpp
Normal 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
173
src/3d/MeshLayer.h
Normal 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
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
320
tests/demo/screens/terrain_demo.py
Normal file
320
tests/demo/screens/terrain_demo.py
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
# terrain_demo.py - Visual demo of terrain system
|
||||
# Shows procedurally generated 3D terrain using HeightMap + Viewport3D
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
import math
|
||||
|
||||
# Create demo scene
|
||||
scene = mcrfpy.Scene("terrain_demo")
|
||||
|
||||
# Dark background frame
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(15, 15, 25))
|
||||
scene.children.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Terrain System Demo - HeightMap to 3D Mesh", pos=(20, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
scene.children.append(title)
|
||||
|
||||
# Create the 3D viewport
|
||||
viewport = mcrfpy.Viewport3D(
|
||||
pos=(50, 60),
|
||||
size=(600, 450),
|
||||
render_resolution=(320, 240), # PS1 resolution
|
||||
fov=60.0,
|
||||
camera_pos=(30.0, 20.0, 30.0),
|
||||
camera_target=(20.0, 0.0, 20.0),
|
||||
bg_color=mcrfpy.Color(100, 150, 200) # Sky blue background
|
||||
)
|
||||
scene.children.append(viewport)
|
||||
|
||||
# Generate terrain using HeightMap
|
||||
print("Generating terrain heightmap...")
|
||||
hm = mcrfpy.HeightMap((40, 40))
|
||||
|
||||
# Use midpoint displacement for natural-looking terrain
|
||||
hm.mid_point_displacement(0.5, seed=42)
|
||||
hm.normalize(0.0, 1.0)
|
||||
|
||||
# Optional: Add some erosion for more realistic terrain
|
||||
hm.rain_erosion(drops=1000, erosion=0.08, sedimentation=0.04, seed=42)
|
||||
hm.normalize(0.0, 1.0)
|
||||
|
||||
# Build terrain mesh from heightmap
|
||||
print("Building terrain mesh...")
|
||||
vertex_count = viewport.build_terrain(
|
||||
layer_name="terrain",
|
||||
heightmap=hm,
|
||||
y_scale=8.0, # Vertical exaggeration
|
||||
cell_size=1.0 # World-space grid cell size
|
||||
)
|
||||
print(f"Terrain built with {vertex_count} vertices")
|
||||
|
||||
# Create color maps for terrain (decoupled from height)
|
||||
# This demonstrates using separate HeightMaps for R, G, B channels
|
||||
print("Creating terrain color maps...")
|
||||
r_map = mcrfpy.HeightMap((40, 40))
|
||||
g_map = mcrfpy.HeightMap((40, 40))
|
||||
b_map = mcrfpy.HeightMap((40, 40))
|
||||
|
||||
# Generate a "moisture" map using different noise
|
||||
moisture = mcrfpy.HeightMap((40, 40))
|
||||
moisture.mid_point_displacement(0.6, seed=999)
|
||||
moisture.normalize(0.0, 1.0)
|
||||
|
||||
# Color based on height + moisture combination:
|
||||
# Low + wet = water blue, Low + dry = sand yellow
|
||||
# High + wet = grass green, High + dry = rock brown/snow white
|
||||
for y in range(40):
|
||||
for x in range(40):
|
||||
h = hm[x, y] # Height (0-1)
|
||||
m = moisture[x, y] # Moisture (0-1)
|
||||
|
||||
if h < 0.3: # Low elevation
|
||||
if m > 0.5: # Wet = water
|
||||
r_map[x, y] = 0.2
|
||||
g_map[x, y] = 0.4
|
||||
b_map[x, y] = 0.8
|
||||
else: # Dry = sand
|
||||
r_map[x, y] = 0.9
|
||||
g_map[x, y] = 0.8
|
||||
b_map[x, y] = 0.5
|
||||
elif h < 0.6: # Mid elevation
|
||||
if m > 0.4: # Wet = grass
|
||||
r_map[x, y] = 0.2 + h * 0.3
|
||||
g_map[x, y] = 0.5 + m * 0.3
|
||||
b_map[x, y] = 0.1
|
||||
else: # Dry = dirt
|
||||
r_map[x, y] = 0.5
|
||||
g_map[x, y] = 0.35
|
||||
b_map[x, y] = 0.2
|
||||
else: # High elevation
|
||||
if h > 0.85: # Snow caps
|
||||
r_map[x, y] = 0.95
|
||||
g_map[x, y] = 0.95
|
||||
b_map[x, y] = 1.0
|
||||
else: # Rock
|
||||
r_map[x, y] = 0.5
|
||||
g_map[x, y] = 0.45
|
||||
b_map[x, y] = 0.4
|
||||
|
||||
# Apply colors to terrain
|
||||
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
|
||||
print("Terrain colors applied")
|
||||
|
||||
# Info panel on the right
|
||||
info_panel = mcrfpy.Frame(pos=(670, 60), size=(330, 450),
|
||||
fill_color=mcrfpy.Color(30, 30, 40),
|
||||
outline_color=mcrfpy.Color(80, 80, 100),
|
||||
outline=2.0)
|
||||
scene.children.append(info_panel)
|
||||
|
||||
# Panel title
|
||||
panel_title = mcrfpy.Caption(text="Terrain Properties", pos=(690, 70))
|
||||
panel_title.fill_color = mcrfpy.Color(200, 200, 255)
|
||||
scene.children.append(panel_title)
|
||||
|
||||
# Property labels
|
||||
props = [
|
||||
("HeightMap Size:", f"{hm.size[0]}x{hm.size[1]}"),
|
||||
("Vertex Count:", f"{vertex_count}"),
|
||||
("Y Scale:", "8.0"),
|
||||
("Cell Size:", "1.0"),
|
||||
("", ""),
|
||||
("Generation:", ""),
|
||||
(" Algorithm:", "Midpoint Displacement"),
|
||||
(" Roughness:", "0.5"),
|
||||
(" Erosion:", "1000 drops"),
|
||||
("", ""),
|
||||
("Layer Count:", f"{viewport.layer_count()}"),
|
||||
]
|
||||
|
||||
y_offset = 100
|
||||
for label, value in props:
|
||||
if label:
|
||||
cap = mcrfpy.Caption(text=f"{label} {value}", pos=(690, y_offset))
|
||||
cap.fill_color = mcrfpy.Color(180, 180, 200)
|
||||
scene.children.append(cap)
|
||||
y_offset += 22
|
||||
|
||||
# Instructions at bottom
|
||||
instructions = mcrfpy.Caption(
|
||||
text="[Space] Orbit | [C] Colors | [1-4] PS1 effects | [+/-] Height | [ESC] Quit",
|
||||
pos=(20, 530)
|
||||
)
|
||||
instructions.fill_color = mcrfpy.Color(150, 150, 150)
|
||||
scene.children.append(instructions)
|
||||
|
||||
# Status line
|
||||
status = mcrfpy.Caption(text="Status: Terrain rendering with PS1 effects", pos=(20, 555))
|
||||
status.fill_color = mcrfpy.Color(100, 200, 100)
|
||||
scene.children.append(status)
|
||||
|
||||
# Animation state
|
||||
animation_time = [0.0]
|
||||
camera_orbit = [True]
|
||||
terrain_height = [8.0]
|
||||
colors_enabled = [True]
|
||||
|
||||
# White color map for "no colors" mode
|
||||
white_r = mcrfpy.HeightMap((40, 40))
|
||||
white_g = mcrfpy.HeightMap((40, 40))
|
||||
white_b = mcrfpy.HeightMap((40, 40))
|
||||
white_r.fill(1.0)
|
||||
white_g.fill(1.0)
|
||||
white_b.fill(1.0)
|
||||
|
||||
# Camera orbit animation
|
||||
def update_camera(timer, runtime):
|
||||
animation_time[0] += runtime / 1000.0
|
||||
|
||||
if camera_orbit[0]:
|
||||
# Orbit camera around terrain center
|
||||
angle = animation_time[0] * 0.3 # Slow rotation
|
||||
radius = 35.0
|
||||
center_x = 20.0
|
||||
center_z = 20.0
|
||||
height = 15.0 + math.sin(animation_time[0] * 0.2) * 5.0
|
||||
|
||||
x = center_x + math.cos(angle) * radius
|
||||
z = center_z + math.sin(angle) * radius
|
||||
|
||||
viewport.camera_pos = (x, height, z)
|
||||
viewport.camera_target = (center_x, 2.0, center_z)
|
||||
|
||||
# Key handler
|
||||
def on_key(key, state):
|
||||
if state != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
# Toggle PS1 effects with number keys
|
||||
if key == mcrfpy.Key.NUM_1:
|
||||
viewport.enable_vertex_snap = not viewport.enable_vertex_snap
|
||||
status.text = f"Vertex Snap: {'ON' if viewport.enable_vertex_snap else 'OFF'}"
|
||||
elif key == mcrfpy.Key.NUM_2:
|
||||
viewport.enable_affine = not viewport.enable_affine
|
||||
status.text = f"Affine Mapping: {'ON' if viewport.enable_affine else 'OFF'}"
|
||||
elif key == mcrfpy.Key.NUM_3:
|
||||
viewport.enable_dither = not viewport.enable_dither
|
||||
status.text = f"Dithering: {'ON' if viewport.enable_dither else 'OFF'}"
|
||||
elif key == mcrfpy.Key.NUM_4:
|
||||
viewport.enable_fog = not viewport.enable_fog
|
||||
status.text = f"Fog: {'ON' if viewport.enable_fog else 'OFF'}"
|
||||
|
||||
# Toggle terrain colors
|
||||
elif key == mcrfpy.Key.C:
|
||||
colors_enabled[0] = not colors_enabled[0]
|
||||
if colors_enabled[0]:
|
||||
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
|
||||
status.text = "Terrain colors: ON (height + moisture)"
|
||||
else:
|
||||
viewport.apply_terrain_colors("terrain", white_r, white_g, white_b)
|
||||
status.text = "Terrain colors: OFF (white)"
|
||||
|
||||
# Camera controls
|
||||
elif key == mcrfpy.Key.SPACE:
|
||||
camera_orbit[0] = not camera_orbit[0]
|
||||
status.text = f"Camera orbit: {'ON' if camera_orbit[0] else 'OFF (WASD/QE to move)'}"
|
||||
|
||||
# Height adjustment
|
||||
elif key == mcrfpy.Key.EQUAL: # + key
|
||||
terrain_height[0] += 1.0
|
||||
rebuild_terrain()
|
||||
elif key == mcrfpy.Key.HYPHEN: # - key
|
||||
terrain_height[0] = max(1.0, terrain_height[0] - 1.0)
|
||||
rebuild_terrain()
|
||||
|
||||
elif key == mcrfpy.Key.ESCAPE:
|
||||
mcrfpy.exit()
|
||||
|
||||
# Manual camera movement (when orbit is off)
|
||||
if not camera_orbit[0]:
|
||||
pos = list(viewport.camera_pos)
|
||||
target = list(viewport.camera_target)
|
||||
speed = 2.0
|
||||
|
||||
if key == mcrfpy.Key.W:
|
||||
# Move forward (toward target)
|
||||
dx = target[0] - pos[0]
|
||||
dz = target[2] - pos[2]
|
||||
length = math.sqrt(dx*dx + dz*dz)
|
||||
if length > 0.01:
|
||||
pos[0] += (dx / length) * speed
|
||||
pos[2] += (dz / length) * speed
|
||||
target[0] += (dx / length) * speed
|
||||
target[2] += (dz / length) * speed
|
||||
elif key == mcrfpy.Key.S:
|
||||
# Move backward
|
||||
dx = target[0] - pos[0]
|
||||
dz = target[2] - pos[2]
|
||||
length = math.sqrt(dx*dx + dz*dz)
|
||||
if length > 0.01:
|
||||
pos[0] -= (dx / length) * speed
|
||||
pos[2] -= (dz / length) * speed
|
||||
target[0] -= (dx / length) * speed
|
||||
target[2] -= (dz / length) * speed
|
||||
elif key == mcrfpy.Key.A:
|
||||
# Strafe left
|
||||
dx = target[0] - pos[0]
|
||||
dz = target[2] - pos[2]
|
||||
# Perpendicular direction
|
||||
pos[0] -= dz / math.sqrt(dx*dx + dz*dz) * speed
|
||||
pos[2] += dx / math.sqrt(dx*dx + dz*dz) * speed
|
||||
target[0] -= dz / math.sqrt(dx*dx + dz*dz) * speed
|
||||
target[2] += dx / math.sqrt(dx*dx + dz*dz) * speed
|
||||
elif key == mcrfpy.Key.D:
|
||||
# Strafe right
|
||||
dx = target[0] - pos[0]
|
||||
dz = target[2] - pos[2]
|
||||
pos[0] += dz / math.sqrt(dx*dx + dz*dz) * speed
|
||||
pos[2] -= dx / math.sqrt(dx*dx + dz*dz) * speed
|
||||
target[0] += dz / math.sqrt(dx*dx + dz*dz) * speed
|
||||
target[2] -= dx / math.sqrt(dx*dx + dz*dz) * speed
|
||||
elif key == mcrfpy.Key.Q:
|
||||
# Move down
|
||||
pos[1] -= speed
|
||||
elif key == mcrfpy.Key.E:
|
||||
# Move up
|
||||
pos[1] += speed
|
||||
|
||||
viewport.camera_pos = tuple(pos)
|
||||
viewport.camera_target = tuple(target)
|
||||
status.text = f"Camera: ({pos[0]:.1f}, {pos[1]:.1f}, {pos[2]:.1f})"
|
||||
|
||||
def rebuild_terrain():
|
||||
"""Rebuild terrain with new height scale"""
|
||||
global vertex_count
|
||||
vertex_count = viewport.build_terrain(
|
||||
layer_name="terrain",
|
||||
heightmap=hm,
|
||||
y_scale=terrain_height[0],
|
||||
cell_size=1.0
|
||||
)
|
||||
# Reapply colors after rebuild
|
||||
if colors_enabled[0]:
|
||||
viewport.apply_terrain_colors("terrain", r_map, g_map, b_map)
|
||||
else:
|
||||
viewport.apply_terrain_colors("terrain", white_r, white_g, white_b)
|
||||
status.text = f"Terrain rebuilt: height scale = {terrain_height[0]}"
|
||||
|
||||
# Set up scene
|
||||
scene.on_key = on_key
|
||||
|
||||
# Create timer for camera animation
|
||||
timer = mcrfpy.Timer("camera_update", update_camera, 16) # ~60fps
|
||||
|
||||
# Activate scene
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
print()
|
||||
print("Terrain Demo loaded!")
|
||||
print("A 40x40 heightmap has been converted to 3D terrain mesh.")
|
||||
print("Terrain colored using separate R/G/B HeightMaps based on height + moisture.")
|
||||
print()
|
||||
print("Controls:")
|
||||
print(" [C] Toggle terrain colors")
|
||||
print(" [1-4] Toggle PS1 effects")
|
||||
print(" [Space] Toggle camera orbit")
|
||||
print(" [+/-] Adjust terrain height scale")
|
||||
print(" [ESC] Quit")
|
||||
Loading…
Add table
Add a link
Reference in a new issue