Files
DoomRTX/neo/engine/tools/radiant/Terrain.cpp
T
2026-05-28 17:15:52 -07:00

1570 lines
44 KiB
C++

/*
===========================================================================
Radiant terrain authoring
This is editor source data only. The heightfields draw live in Radiant and are
saved beside the map as .terrain files; later bake/export code can consume the
same data without coupling sculpting to runtime geometry.
===========================================================================
*/
#include "precompiled.h"
#pragma hdrstop
#include "qe3.h"
#include "CamWnd.h"
#include "Terrain.h"
#include "../../models/TerrainModel.h"
#include <math.h>
#define TERRAIN_DEFAULT_RADIUS 256.0f
#define TERRAIN_DEFAULT_STRENGTH 96.0f
#define TERRAIN_DEFAULT_PAINT_STRENGTH 2.0f
#define TERRAIN_MAX_BRUSH_RADIUS 4096.0f
#define TERRAIN_MIN_BRUSH_RADIUS 8.0f
#define TERRAIN_MAX_BRUSH_STRENGTH 4096.0f
#define TERRAIN_MIN_BRUSH_STRENGTH 1.0f
#define TERRAIN_MAX_PAINT_STRENGTH 20.0f
#define TERRAIN_MIN_PAINT_STRENGTH 0.05f
typedef enum terrainToolMode_s {
TERRAIN_TOOL_SCULPT,
TERRAIN_TOOL_PAINT
} terrainToolMode_t;
typedef idTerrainPatch terrainObject_t;
typedef struct terrainStrokeChange_s {
int sample;
bool heightChanged;
float oldHeight;
float newHeight;
bool weightChanged;
idVec4 oldWeight;
idVec4 newWeight;
} terrainStrokeChange_t;
typedef struct terrainUndo_s {
int terrainIndex;
idList<terrainStrokeChange_t> changes;
} terrainUndo_t;
typedef struct terrainStroke_s {
bool active;
int terrainIndex;
terrainToolMode_t toolMode;
int lastTime;
idList<terrainStrokeChange_t> changes;
} terrainStroke_t;
static idTerrainModel s_terrainModel;
#define s_terrains s_terrainModel.Terrains()
static idList<terrainUndo_t *> s_undoStack;
static idList<terrainUndo_t *> s_redoStack;
static terrainStroke_t s_stroke;
static bool s_editMode = false;
static bool s_cursorValid = false;
static int s_cursorTerrainIndex = -1;
static idVec3 s_cursorHit;
static idVec3 s_cursorNormal;
static terrainToolMode_t s_toolMode = TERRAIN_TOOL_SCULPT;
static int s_paintLayer = 1;
static float s_brushRadius = TERRAIN_DEFAULT_RADIUS;
static float s_brushStrength = TERRAIN_DEFAULT_STRENGTH;
static float s_paintStrength = TERRAIN_DEFAULT_PAINT_STRENGTH;
static void Terrain_ClearUndoStacks(void) {
s_undoStack.DeleteContents(true);
s_redoStack.DeleteContents(true);
s_stroke.active = false;
s_stroke.terrainIndex = -1;
s_stroke.toolMode = TERRAIN_TOOL_SCULPT;
s_stroke.lastTime = 0;
s_stroke.changes.Clear();
}
static idStr Terrain_FileName(const char *mapFileName) {
idStr fileName = mapFileName && mapFileName[0] ? mapFileName : currentmap;
return idTerrainModel::FileNameForMap(fileName.c_str());
}
static int Terrain_SampleIndex(const terrainObject_t *terrain, int x, int y) {
return terrain->SampleIndex(x, y);
}
static int Terrain_PaintIndex(const terrainObject_t *terrain, int x, int y) {
return terrain->PaintIndex(x, y);
}
static void Terrain_MarkPaintTextureDirty(terrainObject_t *terrain) {
if (terrain) {
terrain->MarkPaintTextureDirty();
}
}
static void Terrain_ClearPaintTextureDirtyRect(terrainObject_t *terrain) {
if (terrain) {
terrain->ClearPaintTextureDirtyRect();
}
}
static void Terrain_MarkPaintTextureDirtyRect(terrainObject_t *terrain, int minX, int minY, int maxX, int maxY) {
if (terrain) {
terrain->MarkPaintTextureDirtyRect(minX, minY, maxX, maxY);
}
}
static float Terrain_SampleHeight(const terrainObject_t *terrain, int x, int y) {
return terrain->SampleHeight(x, y);
}
static idVec4 Terrain_BaseLayerWeight(void) {
return idTerrainPatch::BaseLayerWeight();
}
static idVec4 Terrain_NormalizeLayerWeight(const idVec4 &weight) {
return idTerrainPatch::NormalizeLayerWeight(weight);
}
static float Terrain_WorldWidth(const terrainObject_t *terrain) {
return terrain->WorldWidth();
}
static float Terrain_WorldHeight(const terrainObject_t *terrain) {
return terrain->WorldHeight();
}
static float Terrain_BilinearHeight(const terrainObject_t *terrain, float u, float v) {
return terrain->BilinearHeight(u, v);
}
static idVec3 Terrain_VertexUV(const terrainObject_t *terrain, float u, float v) {
return terrain->VertexUV(u, v);
}
static idVec3 Terrain_Vertex(const terrainObject_t *terrain, int x, int y) {
return terrain->Vertex(x, y);
}
static idVec3 Terrain_VertexNormal(const terrainObject_t *terrain, int x, int y) {
return terrain->VertexNormal(x, y);
}
static void Terrain_UpdateBounds(terrainObject_t *terrain) {
if (terrain) {
terrain->UpdateBounds();
}
}
static const char *Terrain_DefaultMaterial(void) {
const char *selected = g_qeglobals.d_texturewin.texdef.name;
if (selected && selected[0] && selected[0] != '(') {
return selected;
}
return "textures/common/caulk";
}
static void Terrain_EnsureLayerData(terrainObject_t *terrain) {
if (terrain) {
terrain->EnsureLayerData(Terrain_DefaultMaterial());
}
}
static idVec4 Terrain_SamplePaintWeight(const terrainObject_t *terrain, int x, int y) {
return terrain->SamplePaintWeight(x, y);
}
static terrainObject_t *Terrain_NewDefault(const idVec3 &center) {
return s_terrainModel.AllocDefaultTerrain(center, Terrain_DefaultMaterial(), (float)g_qeglobals.d_gridsize);
}
static void Terrain_FreeGLResources(terrainObject_t *terrain) {
if (!terrain || terrain->paintTexture == 0) {
return;
}
GLuint texture = terrain->paintTexture;
glDeleteTextures(1, &texture);
terrain->paintTexture = 0;
Terrain_MarkPaintTextureDirty(terrain);
}
void Terrain_Clear(void) {
for (int i = 0; i < s_terrains.Num(); i++) {
Terrain_FreeGLResources(s_terrains[i]);
}
s_terrainModel.Clear();
Terrain_ClearUndoStacks();
s_cursorValid = false;
s_cursorTerrainIndex = -1;
}
int Terrain_NumTerrains(void) {
return s_terrains.Num();
}
bool Terrain_IsEditMode(void) {
return s_editMode;
}
void Terrain_SetEditMode(bool enabled) {
if (s_editMode == enabled) {
return;
}
s_editMode = enabled;
s_cursorValid = false;
Sys_Status(enabled ? "Terrain edit mode enabled\n" : "Terrain edit mode disabled\n", 0);
Sys_UpdateWindows(W_ALL);
}
void Terrain_ToggleEditMode(void) {
Terrain_SetEditMode(!s_editMode);
}
void Terrain_CreateDefaultAtCamera(CCamWnd *cam) {
idVec3 center;
if (cam) {
center = cam->Camera().origin;
} else {
center.Zero();
}
center.z = 0.0f;
terrainObject_t *terrain = Terrain_NewDefault(center);
s_terrains.Append(terrain);
Terrain_SetEditMode(true);
mapModified = 1;
Sys_Status(va("Created terrain %s (%ix%i)\n", terrain->name.c_str(), terrain->width, terrain->height), 0);
Sys_UpdateWindows(W_ALL);
}
static bool Terrain_RayTriangle(const idVec3 &origin, const idVec3 &dir, const idVec3 &v0, const idVec3 &v1, const idVec3 &v2, float &dist, idVec3 &normal) {
const float epsilon = 0.0001f;
idVec3 edge1 = v1 - v0;
idVec3 edge2 = v2 - v0;
idVec3 pvec = dir.Cross(edge2);
float det = edge1 * pvec;
if (fabs(det) < epsilon) {
return false;
}
float invDet = 1.0f / det;
idVec3 tvec = origin - v0;
float u = (tvec * pvec) * invDet;
if (u < 0.0f || u > 1.0f) {
return false;
}
idVec3 qvec = tvec.Cross(edge1);
float v = (dir * qvec) * invDet;
if (v < 0.0f || u + v > 1.0f) {
return false;
}
dist = (edge2 * qvec) * invDet;
if (dist <= epsilon) {
return false;
}
normal = edge1.Cross(edge2);
if (normal.LengthSqr() > epsilon) {
normal.Normalize();
if ((normal * dir) > 0.0f) {
normal *= -1.0f;
}
}
else {
normal.Set(0.0f, 0.0f, 1.0f);
}
return true;
}
static bool Terrain_RayBounds(const terrainObject_t *terrain, const idVec3 &origin, const idVec3 &dir) {
float tMin = 0.0f;
float tMax = HUGE_DISTANCE;
for (int axis = 0; axis < 3; axis++) {
const float min = terrain->bounds[0][axis] - s_brushRadius;
const float max = terrain->bounds[1][axis] + s_brushRadius;
if (fabs(dir[axis]) < 0.00001f) {
if (origin[axis] < min || origin[axis] > max) {
return false;
}
continue;
}
float invD = 1.0f / dir[axis];
float t0 = (min - origin[axis]) * invD;
float t1 = (max - origin[axis]) * invD;
if (t0 > t1) {
float tmp = t0;
t0 = t1;
t1 = tmp;
}
if (t0 > tMin) {
tMin = t0;
}
if (t1 < tMax) {
tMax = t1;
}
if (tMax < tMin) {
return false;
}
}
return true;
}
static bool Terrain_RayTrace(const idVec3 &origin, const idVec3 &dir, int &terrainIndex, idVec3 &hit, idVec3 &normal) {
float bestDist = HUGE_DISTANCE;
int bestTerrain = -1;
idVec3 bestNormal;
for (int i = 0; i < s_terrains.Num(); i++) {
terrainObject_t *terrain = s_terrains[i];
if (!terrain || terrain->width < 2 || terrain->height < 2) {
continue;
}
if (!Terrain_RayBounds(terrain, origin, dir)) {
continue;
}
for (int y = 0; y < terrain->height - 1; y++) {
for (int x = 0; x < terrain->width - 1; x++) {
idVec3 v00 = Terrain_Vertex(terrain, x, y);
idVec3 v10 = Terrain_Vertex(terrain, x + 1, y);
idVec3 v01 = Terrain_Vertex(terrain, x, y + 1);
idVec3 v11 = Terrain_Vertex(terrain, x + 1, y + 1);
float dist;
idVec3 triNormal;
if (Terrain_RayTriangle(origin, dir, v00, v10, v11, dist, triNormal) && dist < bestDist) {
bestDist = dist;
bestTerrain = i;
bestNormal = triNormal;
}
if (Terrain_RayTriangle(origin, dir, v00, v11, v01, dist, triNormal) && dist < bestDist) {
bestDist = dist;
bestTerrain = i;
bestNormal = triNormal;
}
}
}
}
if (bestTerrain < 0) {
return false;
}
terrainIndex = bestTerrain;
hit = origin + dir * bestDist;
normal = bestNormal;
return true;
}
static void Terrain_BuildCameraRay(CCamWnd *cam, const CPoint &point, idVec3 &origin, idVec3 &dir) {
CRect rect;
cam->GetClientRect(rect);
camera_t &camera = cam->Camera();
const int x = point.x;
const int y = rect.bottom - 1 - point.y;
float r = 0.0f;
float u = 0.0f;
if (camera.width > 0) {
r = (float)(x - camera.width / 2) / (camera.width / 2);
u = (float)(y - camera.height / 2) / (camera.width / 2);
}
origin = camera.origin;
for (int i = 0; i < 3; i++) {
dir[i] = camera.vpn[i] + camera.vright[i] * r + camera.vup[i] * u;
}
dir.Normalize();
}
static bool Terrain_UpdateCursor(CCamWnd *cam, const CPoint &point) {
if (!cam || !Terrain_IsEditMode()) {
s_cursorValid = false;
s_cursorTerrainIndex = -1;
return false;
}
idVec3 origin;
idVec3 dir;
Terrain_BuildCameraRay(cam, point, origin, dir);
int terrainIndex;
idVec3 hit;
idVec3 normal;
if (!Terrain_RayTrace(origin, dir, terrainIndex, hit, normal)) {
s_cursorValid = false;
s_cursorTerrainIndex = -1;
return false;
}
s_cursorValid = true;
s_cursorTerrainIndex = terrainIndex;
s_cursorHit = hit;
s_cursorNormal = normal;
return true;
}
static int Terrain_FindStrokeChange(int sample) {
for (int i = 0; i < s_stroke.changes.Num(); i++) {
if (s_stroke.changes[i].sample == sample) {
return i;
}
}
return -1;
}
static void Terrain_RecordStrokeChange(int sample, float oldHeight, float newHeight) {
int existing = Terrain_FindStrokeChange(sample);
if (existing >= 0) {
s_stroke.changes[existing].heightChanged = true;
s_stroke.changes[existing].newHeight = newHeight;
return;
}
terrainStrokeChange_t change;
change.sample = sample;
change.heightChanged = true;
change.oldHeight = oldHeight;
change.newHeight = newHeight;
change.weightChanged = false;
change.oldWeight = Terrain_BaseLayerWeight();
change.newWeight = Terrain_BaseLayerWeight();
s_stroke.changes.Append(change);
}
static void Terrain_RecordPaintChange(int sample, const idVec4 &oldWeight, const idVec4 &newWeight) {
int existing = Terrain_FindStrokeChange(sample);
if (existing >= 0) {
s_stroke.changes[existing].weightChanged = true;
s_stroke.changes[existing].newWeight = newWeight;
return;
}
terrainStrokeChange_t change;
change.sample = sample;
change.heightChanged = false;
change.oldHeight = 0.0f;
change.newHeight = 0.0f;
change.weightChanged = true;
change.oldWeight = oldWeight;
change.newWeight = newWeight;
s_stroke.changes.Append(change);
}
static void Terrain_ApplyBrushAt(int terrainIndex, const idVec3 &hit, float seconds, bool lower) {
if (terrainIndex < 0 || terrainIndex >= s_terrains.Num()) {
return;
}
terrainObject_t *terrain = s_terrains[terrainIndex];
if (!terrain) {
return;
}
if (seconds <= 0.0f) {
seconds = 0.025f;
}
else if (seconds > 0.1f) {
seconds = 0.1f;
}
const float sign = lower ? -1.0f : 1.0f;
const float amount = sign * s_brushStrength * seconds;
const float radiusSqr = s_brushRadius * s_brushRadius;
for (int y = 0; y < terrain->height; y++) {
for (int x = 0; x < terrain->width; x++) {
idVec3 v = Terrain_Vertex(terrain, x, y);
idVec2 delta(v.x - hit.x, v.y - hit.y);
float distSqr = delta.LengthSqr();
if (distSqr > radiusSqr) {
continue;
}
float dist = sqrt(distSqr);
float t = 1.0f - idMath::ClampFloat(0.0f, 1.0f, dist / s_brushRadius);
float falloff = t * t * (3.0f - 2.0f * t);
int sample = Terrain_SampleIndex(terrain, x, y);
float oldHeight = terrain->heights[sample];
float newHeight = oldHeight + amount * falloff;
if (newHeight == oldHeight) {
continue;
}
terrain->SetHeight(x, y, newHeight);
Terrain_RecordStrokeChange(sample, oldHeight, newHeight);
}
}
Terrain_UpdateBounds(terrain);
mapModified = 1;
}
static bool Terrain_WeightsDiffer(const idVec4 &a, const idVec4 &b) {
return idMath::Fabs(a.x - b.x) > 0.0001f ||
idMath::Fabs(a.y - b.y) > 0.0001f ||
idMath::Fabs(a.z - b.z) > 0.0001f ||
idMath::Fabs(a.w - b.w) > 0.0001f;
}
static void Terrain_EnsurePaintLayerMaterial(terrainObject_t *terrain, int layer) {
if (!terrain || layer < 0 || layer >= TERRAIN_MAX_LAYERS) {
return;
}
Terrain_EnsureLayerData(terrain);
if (!terrain->layerMaterials[layer].IsEmpty()) {
return;
}
terrain->layerMaterials[layer] = Terrain_DefaultMaterial();
Texture_ForName(terrain->layerMaterials[layer].c_str());
}
static idVec4 Terrain_PaintLayerWeight(const idVec4 &oldWeight, int layer, float amount, bool erase) {
idVec4 weight = Terrain_NormalizeLayerWeight(oldWeight);
amount = idMath::ClampFloat(0.0f, 1.0f, amount);
if (erase) {
weight[layer] = idMath::ClampFloat(0.0f, 1.0f, weight[layer] - amount);
return Terrain_NormalizeLayerWeight(weight);
}
for (int i = 0; i < TERRAIN_MAX_LAYERS; i++) {
weight[i] *= (1.0f - amount);
}
weight[layer] += amount;
return Terrain_NormalizeLayerWeight(weight);
}
static void Terrain_ApplyPaintAt(int terrainIndex, const idVec3 &hit, float seconds, bool erase) {
if (terrainIndex < 0 || terrainIndex >= s_terrains.Num()) {
return;
}
terrainObject_t *terrain = s_terrains[terrainIndex];
if (!terrain) {
return;
}
if (seconds <= 0.0f) {
seconds = 0.025f;
}
else if (seconds > 0.1f) {
seconds = 0.1f;
}
Terrain_EnsureLayerData(terrain);
Terrain_EnsurePaintLayerMaterial(terrain, s_paintLayer);
const float amount = s_paintStrength * seconds;
const float radiusSqr = s_brushRadius * s_brushRadius;
const float worldWidth = Terrain_WorldWidth(terrain);
const float worldHeight = Terrain_WorldHeight(terrain);
if (worldWidth <= 0.0f || worldHeight <= 0.0f) {
return;
}
const float invWorldWidth = 1.0f / worldWidth;
const float invWorldHeight = 1.0f / worldHeight;
const float minU = idMath::ClampFloat(0.0f, 1.0f, (hit.x - s_brushRadius - terrain->origin.x) * invWorldWidth);
const float maxU = idMath::ClampFloat(0.0f, 1.0f, (hit.x + s_brushRadius - terrain->origin.x) * invWorldWidth);
const float minV = idMath::ClampFloat(0.0f, 1.0f, (hit.y - s_brushRadius - terrain->origin.y) * invWorldHeight);
const float maxV = idMath::ClampFloat(0.0f, 1.0f, (hit.y + s_brushRadius - terrain->origin.y) * invWorldHeight);
const int minX = idMath::ClampInt(0, terrain->paintWidth - 1, (int)floorf(minU * (float)(terrain->paintWidth - 1)));
const int maxX = idMath::ClampInt(0, terrain->paintWidth - 1, (int)ceilf(maxU * (float)(terrain->paintWidth - 1)));
const int minY = idMath::ClampInt(0, terrain->paintHeight - 1, (int)floorf(minV * (float)(terrain->paintHeight - 1)));
const int maxY = idMath::ClampInt(0, terrain->paintHeight - 1, (int)ceilf(maxV * (float)(terrain->paintHeight - 1)));
bool changed = false;
for (int y = minY; y <= maxY; y++) {
const float v = (float)y / (float)(terrain->paintHeight - 1);
const float worldY = terrain->origin.y + worldHeight * v;
for (int x = minX; x <= maxX; x++) {
const float u = (float)x / (float)(terrain->paintWidth - 1);
const float worldX = terrain->origin.x + worldWidth * u;
idVec2 delta(worldX - hit.x, worldY - hit.y);
float distSqr = delta.LengthSqr();
if (distSqr > radiusSqr) {
continue;
}
float dist = sqrt(distSqr);
float t = 1.0f - idMath::ClampFloat(0.0f, 1.0f, dist / s_brushRadius);
float falloff = t * t * (3.0f - 2.0f * t);
int sample = Terrain_PaintIndex(terrain, x, y);
idVec4 oldWeight = terrain->paintWeights[sample];
idVec4 newWeight = Terrain_PaintLayerWeight(oldWeight, s_paintLayer, amount * falloff, erase);
if (!Terrain_WeightsDiffer(oldWeight, newWeight)) {
continue;
}
terrain->SetPaintWeight(x, y, newWeight);
Terrain_RecordPaintChange(sample, oldWeight, newWeight);
changed = true;
}
}
if (changed) {
Terrain_MarkPaintTextureDirtyRect(terrain, minX, minY, maxX, maxY);
mapModified = 1;
}
}
static void Terrain_BeginStroke(int terrainIndex) {
s_stroke.active = true;
s_stroke.terrainIndex = terrainIndex;
s_stroke.toolMode = s_toolMode;
s_stroke.lastTime = Sys_Milliseconds();
s_stroke.changes.Clear();
}
static void Terrain_EndStroke(bool keep) {
if (!s_stroke.active) {
return;
}
if (keep && s_stroke.changes.Num() > 0) {
terrainUndo_t *undo = new terrainUndo_t;
undo->terrainIndex = s_stroke.terrainIndex;
undo->changes = s_stroke.changes;
s_undoStack.Append(undo);
s_redoStack.DeleteContents(true);
}
s_stroke.active = false;
s_stroke.terrainIndex = -1;
s_stroke.lastTime = 0;
s_stroke.changes.Clear();
}
static bool Terrain_StrokeApplyAtCursor(CCamWnd *cam, const CPoint &point, UINT flags, bool forceFirstSample) {
if (!s_stroke.active) {
return false;
}
if (!Terrain_UpdateCursor(cam, point)) {
return true;
}
int terrainIndex;
idVec3 origin;
idVec3 dir;
idVec3 hit;
idVec3 normal;
Terrain_BuildCameraRay(cam, point, origin, dir);
if (!Terrain_RayTrace(origin, dir, terrainIndex, hit, normal)) {
return true;
}
if (terrainIndex != s_stroke.terrainIndex) {
return true;
}
int now = Sys_Milliseconds();
float seconds = forceFirstSample ? 0.05f : (now - s_stroke.lastTime) * 0.001f;
s_stroke.lastTime = now;
const bool lower = (flags & MK_SHIFT) != 0 || (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
if (s_stroke.toolMode == TERRAIN_TOOL_PAINT) {
Terrain_ApplyPaintAt(terrainIndex, hit, seconds, lower);
}
else {
Terrain_ApplyBrushAt(terrainIndex, hit, seconds, lower);
}
Sys_UpdateWindows(W_ALL);
return true;
}
bool Terrain_LButtonDown(CCamWnd *cam, const CPoint &point, UINT flags) {
if (!Terrain_IsEditMode() || !cam) {
return false;
}
if (s_terrains.Num() <= 0) {
return false;
}
if (!Terrain_UpdateCursor(cam, point)) {
return false;
}
idVec3 origin;
idVec3 dir;
idVec3 hit;
idVec3 normal;
int terrainIndex;
Terrain_BuildCameraRay(cam, point, origin, dir);
if (!Terrain_RayTrace(origin, dir, terrainIndex, hit, normal)) {
return false;
}
cam->SetFocus();
cam->SetCapture();
Terrain_BeginStroke(terrainIndex);
Terrain_StrokeApplyAtCursor(cam, point, flags, true);
return true;
}
bool Terrain_LButtonUp(CCamWnd *cam, const CPoint &point, UINT flags) {
if (!s_stroke.active) {
return false;
}
Terrain_StrokeApplyAtCursor(cam, point, flags, false);
Terrain_EndStroke(true);
if (cam && ::GetCapture() == cam->GetSafeHwnd()) {
::ReleaseCapture();
}
Sys_UpdateWindows(W_ALL);
return true;
}
void Terrain_CancelStroke(void) {
if (!s_stroke.active) {
return;
}
Terrain_EndStroke(false);
s_cursorValid = false;
Sys_UpdateWindows(W_CAMERA);
}
bool Terrain_MouseMove(CCamWnd *cam, const CPoint &point, UINT flags) {
if (s_stroke.active) {
return Terrain_StrokeApplyAtCursor(cam, point, flags, false);
}
if (!Terrain_IsEditMode()) {
return false;
}
bool hadCursor = s_cursorValid;
bool hasCursor = Terrain_UpdateCursor(cam, point);
if (hadCursor != hasCursor || hasCursor) {
if (cam) {
cam->InvalidateRect(NULL, FALSE);
}
}
return false;
}
static const char *Terrain_ToolName(void) {
return s_toolMode == TERRAIN_TOOL_PAINT ? "paint" : "sculpt";
}
static void Terrain_SetToolMode(terrainToolMode_t mode) {
if (s_toolMode == mode) {
return;
}
s_toolMode = mode;
Sys_Status(va("Terrain %s mode\n", Terrain_ToolName()), 0);
Sys_UpdateWindows(W_CAMERA);
}
static void Terrain_ToggleToolMode(void) {
Terrain_SetToolMode(s_toolMode == TERRAIN_TOOL_PAINT ? TERRAIN_TOOL_SCULPT : TERRAIN_TOOL_PAINT);
}
bool Terrain_IsPaintMode(void) {
return s_toolMode == TERRAIN_TOOL_PAINT;
}
void Terrain_SetPaintMode(bool enabled) {
Terrain_SetToolMode(enabled ? TERRAIN_TOOL_PAINT : TERRAIN_TOOL_SCULPT);
}
int Terrain_GetPaintLayer(void) {
return s_paintLayer;
}
static terrainObject_t *Terrain_TargetForLayerDialog(void) {
if (s_cursorTerrainIndex >= 0 && s_cursorTerrainIndex < s_terrains.Num()) {
return s_terrains[s_cursorTerrainIndex];
}
if (s_terrains.Num() > 0) {
return s_terrains[0];
}
return NULL;
}
void Terrain_SelectPaintLayer(int layer) {
s_paintLayer = idMath::ClampInt(0, TERRAIN_MAX_LAYERS - 1, layer);
Sys_Status(va("Terrain paint layer %i\n", s_paintLayer + 1), 0);
Sys_UpdateWindows(W_CAMERA);
}
static idStr Terrain_CleanMaterialName(const char *materialName) {
idStr cleaned = materialName ? materialName : "";
cleaned.StripTrailingWhitespace();
cleaned.Strip(' ');
cleaned.Strip('\t');
cleaned.StripQuotes();
return cleaned;
}
static bool Terrain_SetLayerMaterials(terrainObject_t *terrain, const idStr layerMaterials[TERRAIN_MAX_LAYERS]) {
if (!terrain) {
return false;
}
Terrain_EnsureLayerData(terrain);
idStr oldLayerMaterials[TERRAIN_MAX_LAYERS];
for (int layer = 0; layer < TERRAIN_MAX_LAYERS; layer++) {
oldLayerMaterials[layer] = terrain->layerMaterials[layer];
}
const bool changed = terrain->SetLayerMaterials(layerMaterials, Terrain_DefaultMaterial());
for (int layer = 0; layer < TERRAIN_MAX_LAYERS; layer++) {
if (!terrain->layerMaterials[layer].IsEmpty() && oldLayerMaterials[layer].Icmp(terrain->layerMaterials[layer].c_str()) != 0) {
Texture_ForName(terrain->layerMaterials[layer].c_str());
}
}
if (changed) {
mapModified = 1;
Sys_UpdateWindows(W_ALL);
}
return changed;
}
enum {
TERRAIN_LAYER_DLG_EDIT_BASE = 7000,
TERRAIN_LAYER_DLG_USE_SELECTED_BASE = 7010,
TERRAIN_LAYER_DLG_CLEAR_BASE = 7020
};
typedef struct terrainLayerDialogState_s {
terrainObject_t *terrain;
HWND owner;
HWND dialog;
HWND edits[TERRAIN_MAX_LAYERS];
bool accepted;
bool changed;
} terrainLayerDialogState_t;
static void Terrain_SetDialogFont(HWND hWnd, HFONT font) {
if (hWnd && font) {
SendMessage(hWnd, WM_SETFONT, (WPARAM)font, TRUE);
}
}
static HWND Terrain_CreateDialogControl(terrainLayerDialogState_t *state, const char *className, const char *text, DWORD style, int id, int x, int y, int w, int h, HFONT font) {
HWND control = CreateWindowExA(
(strcmp(className, "EDIT") == 0) ? WS_EX_CLIENTEDGE : 0,
className,
text,
WS_CHILD | WS_VISIBLE | style,
x,
y,
w,
h,
state->dialog,
(HMENU)(INT_PTR)id,
GetModuleHandle(NULL),
NULL);
Terrain_SetDialogFont(control, font);
return control;
}
static void TerrainLayerDialog_FillEdits(terrainLayerDialogState_t *state) {
if (!state || !state->terrain) {
return;
}
Terrain_EnsureLayerData(state->terrain);
for (int layer = 0; layer < TERRAIN_MAX_LAYERS; layer++) {
SetWindowTextA(state->edits[layer], state->terrain->layerMaterials[layer].c_str());
}
}
static void TerrainLayerDialog_Apply(terrainLayerDialogState_t *state) {
if (!state || !state->terrain) {
return;
}
idStr layerMaterials[TERRAIN_MAX_LAYERS];
for (int layer = 0; layer < TERRAIN_MAX_LAYERS; layer++) {
char text[MAX_OSPATH];
text[0] = '\0';
GetWindowTextA(state->edits[layer], text, sizeof(text));
layerMaterials[layer] = Terrain_CleanMaterialName(text);
}
state->changed = Terrain_SetLayerMaterials(state->terrain, layerMaterials);
state->accepted = true;
DestroyWindow(state->dialog);
}
static LRESULT CALLBACK TerrainLayerDialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
terrainLayerDialogState_t *state = (terrainLayerDialogState_t *)GetWindowLongPtr(hWnd, GWLP_USERDATA);
switch (uMsg) {
case WM_NCCREATE:
state = (terrainLayerDialogState_t *)((CREATESTRUCT *)lParam)->lpCreateParams;
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)state);
return TRUE;
case WM_CREATE:
{
state->dialog = hWnd;
HFONT font = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
Terrain_SetDialogFont(hWnd, font);
Terrain_CreateDialogControl(state, "STATIC", state->terrain ? state->terrain->name.c_str() : "", SS_LEFT, -1, 14, 12, 360, 18, font);
for (int layer = 0; layer < TERRAIN_MAX_LAYERS; layer++) {
const int y = 42 + layer * 30;
Terrain_CreateDialogControl(state, "STATIC", va("Layer %i", layer + 1), SS_LEFT, -1, 14, y + 4, 58, 18, font);
state->edits[layer] = Terrain_CreateDialogControl(state, "EDIT", "", ES_AUTOHSCROLL, TERRAIN_LAYER_DLG_EDIT_BASE + layer, 74, y, 288, 22, font);
Terrain_CreateDialogControl(state, "BUTTON", "Use selected", BS_PUSHBUTTON, TERRAIN_LAYER_DLG_USE_SELECTED_BASE + layer, 370, y, 88, 22, font);
HWND clearButton = Terrain_CreateDialogControl(state, "BUTTON", "Clear", BS_PUSHBUTTON, TERRAIN_LAYER_DLG_CLEAR_BASE + layer, 466, y, 56, 22, font);
if (layer == 0) {
EnableWindow(clearButton, FALSE);
}
}
Terrain_CreateDialogControl(state, "BUTTON", "OK", BS_DEFPUSHBUTTON, IDOK, 342, 174, 82, 24, font);
Terrain_CreateDialogControl(state, "BUTTON", "Cancel", BS_PUSHBUTTON, IDCANCEL, 436, 174, 82, 24, font);
TerrainLayerDialog_FillEdits(state);
SetFocus(state->edits[0]);
return 0;
}
case WM_COMMAND:
{
const int command = LOWORD(wParam);
if (command == IDOK) {
TerrainLayerDialog_Apply(state);
return 0;
}
if (command == IDCANCEL) {
DestroyWindow(hWnd);
return 0;
}
if (command >= TERRAIN_LAYER_DLG_USE_SELECTED_BASE && command < TERRAIN_LAYER_DLG_USE_SELECTED_BASE + TERRAIN_MAX_LAYERS) {
int layer = command - TERRAIN_LAYER_DLG_USE_SELECTED_BASE;
SetWindowTextA(state->edits[layer], Terrain_DefaultMaterial());
return 0;
}
if (command >= TERRAIN_LAYER_DLG_CLEAR_BASE && command < TERRAIN_LAYER_DLG_CLEAR_BASE + TERRAIN_MAX_LAYERS) {
int layer = command - TERRAIN_LAYER_DLG_CLEAR_BASE;
if (layer > 0) {
SetWindowTextA(state->edits[layer], "");
}
return 0;
}
break;
}
case WM_CLOSE:
DestroyWindow(hWnd);
return 0;
case WM_DESTROY:
if (state && state->owner && IsWindow(state->owner)) {
EnableWindow(state->owner, TRUE);
SetActiveWindow(state->owner);
}
return 0;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
static ATOM TerrainLayerDialog_RegisterClass(void) {
static ATOM atom = 0;
if (atom) {
return atom;
}
WNDCLASSA wc;
memset(&wc, 0, sizeof(wc));
wc.style = CS_DBLCLKS;
wc.lpfnWndProc = TerrainLayerDialogProc;
wc.hInstance = GetModuleHandle(NULL);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
wc.lpszClassName = "RadiantTerrainLayerDialog";
atom = RegisterClassA(&wc);
return atom;
}
bool Terrain_ShowLayersDialog(CCamWnd *cam) {
terrainObject_t *terrain = Terrain_TargetForLayerDialog();
if (!terrain) {
Sys_Status("Create a terrain before editing terrain layers\n", 0);
return false;
}
Terrain_EnsureLayerData(terrain);
if (!TerrainLayerDialog_RegisterClass()) {
common->Warning("Terrain_ShowLayersDialog: failed to register dialog class");
return false;
}
HWND owner = cam && cam->GetSafeHwnd() ? cam->GetSafeHwnd() : (g_pParentWnd ? g_pParentWnd->GetSafeHwnd() : NULL);
terrainLayerDialogState_t state;
memset(&state, 0, sizeof(state));
state.terrain = terrain;
state.owner = owner;
RECT ownerRect = { 0, 0, 0, 0 };
if (owner && IsWindow(owner)) {
GetWindowRect(owner, &ownerRect);
}
else {
ownerRect.left = 200;
ownerRect.top = 150;
ownerRect.right = 800;
ownerRect.bottom = 550;
}
const int width = 540;
const int height = 236;
const int x = ownerRect.left + ((ownerRect.right - ownerRect.left) - width) / 2;
const int y = ownerRect.top + ((ownerRect.bottom - ownerRect.top) - height) / 2;
if (owner) {
EnableWindow(owner, FALSE);
}
HWND dialog = CreateWindowExA(
WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE,
"RadiantTerrainLayerDialog",
"Terrain Layers",
WS_POPUP | WS_CAPTION | WS_SYSMENU,
x,
y,
width,
height,
owner,
NULL,
GetModuleHandle(NULL),
&state);
if (!dialog) {
if (owner) {
EnableWindow(owner, TRUE);
}
common->Warning("Terrain_ShowLayersDialog: failed to create dialog");
return false;
}
ShowWindow(dialog, SW_SHOW);
UpdateWindow(dialog);
MSG msg;
while (IsWindow(dialog) && GetMessage(&msg, NULL, 0, 0) > 0) {
if (!IsDialogMessage(dialog, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
if (owner && IsWindow(owner)) {
EnableWindow(owner, TRUE);
SetActiveWindow(owner);
}
if (state.accepted && state.changed) {
Sys_Status("Terrain layers updated\n", 0);
}
return state.changed;
}
void Terrain_AdjustBrushRadius(float scale) {
s_brushRadius *= scale;
s_brushRadius = idMath::ClampFloat(TERRAIN_MIN_BRUSH_RADIUS, TERRAIN_MAX_BRUSH_RADIUS, s_brushRadius);
Sys_Status(va("Terrain radius %.1f\n", s_brushRadius), 0);
Sys_UpdateWindows(W_CAMERA);
}
void Terrain_AdjustBrushStrength(float scale) {
if (s_toolMode == TERRAIN_TOOL_PAINT) {
s_paintStrength *= scale;
s_paintStrength = idMath::ClampFloat(TERRAIN_MIN_PAINT_STRENGTH, TERRAIN_MAX_PAINT_STRENGTH, s_paintStrength);
Sys_Status(va("Terrain paint flow %.2f\n", s_paintStrength), 0);
Sys_UpdateWindows(W_CAMERA);
return;
}
s_brushStrength *= scale;
s_brushStrength = idMath::ClampFloat(TERRAIN_MIN_BRUSH_STRENGTH, TERRAIN_MAX_BRUSH_STRENGTH, s_brushStrength);
Sys_Status(va("Terrain strength %.1f\n", s_brushStrength), 0);
Sys_UpdateWindows(W_CAMERA);
}
bool Terrain_HandleKeyDown(CCamWnd *cam, UINT key) {
const bool ctrl = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
const bool shift = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
const bool alt = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0;
if (!alt && key == 'T') {
if (ctrl || shift || s_terrains.Num() <= 0) {
Terrain_CreateDefaultAtCamera(cam);
}
else {
Terrain_ToggleEditMode();
}
return true;
}
if (!Terrain_IsEditMode()) {
return false;
}
if (!alt && key == 'P') {
Terrain_ToggleToolMode();
return true;
}
if (!alt && key >= '1' && key <= '4') {
const int layer = key - '1';
Terrain_SelectPaintLayer(layer);
return true;
}
if (!alt && key >= VK_NUMPAD1 && key <= VK_NUMPAD4) {
const int layer = key - VK_NUMPAD1;
Terrain_SelectPaintLayer(layer);
return true;
}
if (key == VK_OEM_4) {
Terrain_AdjustBrushRadius(0.8f);
return true;
}
if (key == VK_OEM_6) {
Terrain_AdjustBrushRadius(1.25f);
return true;
}
if (key == VK_SUBTRACT || key == VK_OEM_MINUS) {
Terrain_AdjustBrushStrength(0.8f);
return true;
}
if (key == VK_ADD || key == VK_OEM_PLUS) {
Terrain_AdjustBrushStrength(1.25f);
return true;
}
return false;
}
bool Terrain_HandleChar(CCamWnd *cam, UINT key) {
if (!Terrain_IsEditMode()) {
return false;
}
switch (key) {
case '[':
case '{':
Terrain_AdjustBrushRadius(0.8f);
return true;
case ']':
case '}':
Terrain_AdjustBrushRadius(1.25f);
return true;
case '-':
case '_':
Terrain_AdjustBrushStrength(0.8f);
return true;
case '+':
case '=':
Terrain_AdjustBrushStrength(1.25f);
return true;
}
return false;
}
static idImage *Terrain_LayerEditorImage(const char *materialName) {
if (!materialName || !materialName[0]) {
return NULL;
}
const idMaterial *material = Texture_ForName(materialName);
if (!material || material->TestMaterialFlag(MF_DEFAULTED)) {
return NULL;
}
return material->GetEditorImage();
}
static idImage *Terrain_LayerNormalImage(const char *materialName) {
idImage *flatNormal = globalImages->flatNormalMap ? globalImages->flatNormalMap : globalImages->whiteImage;
if (!materialName || !materialName[0]) {
return flatNormal;
}
const idMaterial *material = Texture_ForName(materialName);
if (!material || material->TestMaterialFlag(MF_DEFAULTED)) {
return flatNormal;
}
const shaderStage_t *bumpStage = material->GetBumpStage();
if (!bumpStage || !bumpStage->texture.image) {
return flatNormal;
}
return bumpStage->texture.image;
}
static byte Terrain_WeightToByte(float value) {
return (byte)idMath::ClampInt(0, 255, (int)floorf(idMath::ClampFloat(0.0f, 1.0f, value) * 255.0f + 0.5f));
}
static void Terrain_FillPaintPixels(terrainObject_t *terrain, int minX, int minY, int uploadWidth, int uploadHeight, idList<byte> &pixels) {
pixels.SetNum(uploadWidth * uploadHeight * 4);
for (int y = 0; y < uploadHeight; y++) {
for (int x = 0; x < uploadWidth; x++) {
const int sample = Terrain_PaintIndex(terrain, minX + x, minY + y);
const int pixel = (y * uploadWidth + x) * 4;
const idVec4 weights = Terrain_NormalizeLayerWeight(terrain->paintWeights[sample]);
pixels[pixel + 0] = Terrain_WeightToByte(weights.x);
pixels[pixel + 1] = Terrain_WeightToByte(weights.y);
pixels[pixel + 2] = Terrain_WeightToByte(weights.z);
pixels[pixel + 3] = Terrain_WeightToByte(weights.w);
}
}
}
static GLuint Terrain_UpdatePaintTexture(terrainObject_t *terrain) {
Terrain_EnsureLayerData(terrain);
bool newTexture = false;
if (terrain->paintTexture == 0) {
glGenTextures(1, &terrain->paintTexture);
Terrain_MarkPaintTextureDirty(terrain);
newTexture = true;
}
if (!terrain->paintTextureDirty) {
return terrain->paintTexture;
}
const int minX = idMath::ClampInt(0, terrain->paintWidth - 1, terrain->paintDirtyMinX);
const int minY = idMath::ClampInt(0, terrain->paintHeight - 1, terrain->paintDirtyMinY);
const int maxX = idMath::ClampInt(0, terrain->paintWidth - 1, terrain->paintDirtyMaxX);
const int maxY = idMath::ClampInt(0, terrain->paintHeight - 1, terrain->paintDirtyMaxY);
const bool validRect = maxX >= minX && maxY >= minY;
const bool fullUpload = newTexture || !validRect ||
(minX == 0 && minY == 0 && maxX == terrain->paintWidth - 1 && maxY == terrain->paintHeight - 1);
const int uploadX = fullUpload ? 0 : minX;
const int uploadY = fullUpload ? 0 : minY;
const int uploadWidth = fullUpload ? terrain->paintWidth : (maxX - minX + 1);
const int uploadHeight = fullUpload ? terrain->paintHeight : (maxY - minY + 1);
glBindTexture(GL_TEXTURE_2D, terrain->paintTexture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
idList<byte> pixels;
Terrain_FillPaintPixels(terrain, uploadX, uploadY, uploadWidth, uploadHeight, pixels);
if (fullUpload) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, terrain->paintWidth, terrain->paintHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.Ptr());
}
else {
glTexSubImage2D(GL_TEXTURE_2D, 0, uploadX, uploadY, uploadWidth, uploadHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels.Ptr());
}
Terrain_ClearPaintTextureDirtyRect(terrain);
return terrain->paintTexture;
}
static void Terrain_BindLayerTextures(terrainObject_t *terrain) {
Terrain_EnsureLayerData(terrain);
idImage *baseImage = Terrain_LayerEditorImage(terrain->layerMaterials[0].c_str());
if (!baseImage) {
baseImage = globalImages->whiteImage;
}
for (int layer = 0; layer < TERRAIN_MAX_LAYERS; layer++) {
idImage *image = Terrain_LayerEditorImage(terrain->layerMaterials[layer].c_str());
if (!image) {
image = baseImage;
}
image->Bind();
glTerrainLayerTextureQD3D12(layer, image->texnum);
idImage *normalImage = Terrain_LayerNormalImage(terrain->layerMaterials[layer].c_str());
if (!normalImage) {
normalImage = globalImages->flatNormalMap ? globalImages->flatNormalMap : globalImages->whiteImage;
}
normalImage->Bind();
glTerrainLayerNormalTextureQD3D12(layer, normalImage->texnum);
}
glTerrainWeightTextureQD3D12(Terrain_UpdatePaintTexture(terrain));
}
static void Terrain_ClearLayerTextureBindings(void) {
glTerrainBlendEnableQD3D12(GL_FALSE);
glTerrainBlendOpacityQD3D12(1.0f);
glTerrainWeightTextureQD3D12(0);
for (int layer = 0; layer < TERRAIN_MAX_LAYERS; layer++) {
glTerrainLayerTextureQD3D12(layer, 0);
glTerrainLayerNormalTextureQD3D12(layer, 0);
}
}
static void Terrain_SubmitTexturedVertex(terrainObject_t *terrain, int x, int y) {
idVec3 v = Terrain_Vertex(terrain, x, y);
idVec3 normal = Terrain_VertexNormal(terrain, x, y);
const float u = (float)x / (float)(terrain->width - 1);
const float vCoord = (float)y / (float)(terrain->height - 1);
const float layerS = Terrain_WorldWidth(terrain) * u / 256.0f;
const float layerT = Terrain_WorldHeight(terrain) * vCoord / 256.0f;
const int paintX = idMath::ClampInt(0, terrain->paintWidth - 1, (int)floorf(u * (float)(terrain->paintWidth - 1) + 0.5f));
const int paintY = idMath::ClampInt(0, terrain->paintHeight - 1, (int)floorf(vCoord * (float)(terrain->paintHeight - 1) + 0.5f));
const idVec4 weights = Terrain_NormalizeLayerWeight(Terrain_SamplePaintWeight(terrain, paintX, paintY));
glColor4fv(weights.ToFloatPtr());
glNormal3fv(normal.ToFloatPtr());
glMultiTexCoord2fARB(GL_TEXTURE0_ARB, layerS, layerT);
glMultiTexCoord2fARB(GL_TEXTURE1_ARB, u, vCoord);
glVertex3fv(v.ToFloatPtr());
}
static idVec4 Terrain_PaintLayerColor(void) {
static const idVec4 colors[TERRAIN_MAX_LAYERS] = {
idVec4(0.95f, 0.80f, 0.10f, 1.0f),
idVec4(0.10f, 0.75f, 1.0f, 1.0f),
idVec4(0.15f, 0.95f, 0.35f, 1.0f),
idVec4(0.95f, 0.25f, 0.85f, 1.0f)
};
return colors[idMath::ClampInt(0, TERRAIN_MAX_LAYERS - 1, s_paintLayer)];
}
static void Terrain_DrawObjectCam(terrainObject_t *terrain, bool selected) {
if (!terrain || terrain->width < 2 || terrain->height < 2) {
return;
}
Terrain_EnsureLayerData(terrain);
glPushAttrib(GL_ALL_ATTRIB_BITS);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glDepthMask(GL_TRUE);
glDisable(GL_CULL_FACE);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_TEXTURE_2D);
Terrain_BindLayerTextures(terrain);
glTerrainBlendEnableQD3D12(GL_TRUE);
glTerrainBlendOpacityQD3D12(selected ? 0.85f : 0.72f);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
for (int y = 0; y < terrain->height - 1; y++) {
glBegin(GL_TRIANGLE_STRIP);
for (int x = 0; x < terrain->width; x++) {
Terrain_SubmitTexturedVertex(terrain, x, y);
Terrain_SubmitTexturedVertex(terrain, x, y + 1);
}
glEnd();
}
Terrain_ClearLayerTextureBindings();
glDisable(GL_TEXTURE_2D);
globalImages->BindNull();
glDisable(GL_BLEND);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glLineWidth(selected ? 1.5f : 0.5f);
glColor4f(selected ? 1.0f : 0.45f, selected ? 0.82f : 0.62f, selected ? 0.08f : 0.50f, 1.0f);
glBegin(GL_LINES);
for (int y = 0; y < terrain->height; y++) {
for (int x = 0; x < terrain->width - 1; x++) {
idVec3 a = Terrain_Vertex(terrain, x, y);
idVec3 b = Terrain_Vertex(terrain, x + 1, y);
glVertex3fv(a.ToFloatPtr());
glVertex3fv(b.ToFloatPtr());
}
}
for (int x = 0; x < terrain->width; x++) {
for (int y = 0; y < terrain->height - 1; y++) {
idVec3 a = Terrain_Vertex(terrain, x, y);
idVec3 b = Terrain_Vertex(terrain, x, y + 1);
glVertex3fv(a.ToFloatPtr());
glVertex3fv(b.ToFloatPtr());
}
}
glEnd();
glDepthFunc(GL_LEQUAL);
glDepthMask(GL_TRUE);
glDisable(GL_BLEND);
glEnable(GL_TEXTURE_2D);
glPopAttrib();
}
static void Terrain_DrawBrushCursor(void) {
if (!s_editMode || !s_cursorValid) {
return;
}
glPushAttrib(GL_ALL_ATTRIB_BITS);
glDisable(GL_TEXTURE_2D);
glDisable(GL_DEPTH_TEST);
glLineWidth(2.0f);
idVec4 cursorColor = s_toolMode == TERRAIN_TOOL_PAINT ? Terrain_PaintLayerColor() : idVec4(0.08f, 0.75f, 1.0f, 1.0f);
glColor4f(cursorColor.x, cursorColor.y, cursorColor.z, cursorColor.w);
globalImages->BindNull();
const int segments = 64;
glBegin(GL_LINE_LOOP);
for (int i = 0; i < segments; i++) {
float a = (float)i * idMath::TWO_PI / (float)segments;
idVec3 p;
p.x = s_cursorHit.x + cos(a) * s_brushRadius;
p.y = s_cursorHit.y + sin(a) * s_brushRadius;
p.z = s_cursorHit.z + 2.0f;
glVertex3fv(p.ToFloatPtr());
}
glEnd();
glBegin(GL_LINES);
idVec3 normalEnd = s_cursorHit + s_cursorNormal * idMath::ClampFloat(12.0f, 64.0f, s_brushRadius * 0.25f);
glVertex3fv(s_cursorHit.ToFloatPtr());
glVertex3fv(normalEnd.ToFloatPtr());
glEnd();
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glDepthMask(GL_TRUE);
glDisable(GL_BLEND);
glEnable(GL_TEXTURE_2D);
glPopAttrib();
}
void Terrain_DrawCam(CCamWnd *cam) {
for (int i = 0; i < s_terrains.Num(); i++) {
Terrain_DrawObjectCam(s_terrains[i], s_editMode);
}
Terrain_DrawBrushCursor();
}
void Terrain_DrawXY(int viewType) {
glPushAttrib(GL_ALL_ATTRIB_BITS);
glDisable(GL_TEXTURE_2D);
glDisable(GL_BLEND);
globalImages->BindNull();
glLineWidth(s_editMode ? 1.0f : 0.5f);
glColor3f(s_editMode ? 0.95f : 0.45f, s_editMode ? 0.72f : 0.56f, s_editMode ? 0.08f : 0.32f);
for (int i = 0; i < s_terrains.Num(); i++) {
terrainObject_t *terrain = s_terrains[i];
if (!terrain || terrain->width < 2 || terrain->height < 2) {
continue;
}
glBegin(GL_LINES);
for (int y = 0; y < terrain->height; y++) {
for (int x = 0; x < terrain->width - 1; x++) {
idVec3 a = Terrain_Vertex(terrain, x, y);
idVec3 b = Terrain_Vertex(terrain, x + 1, y);
glVertex3fv(a.ToFloatPtr());
glVertex3fv(b.ToFloatPtr());
}
}
for (int x = 0; x < terrain->width; x++) {
for (int y = 0; y < terrain->height - 1; y++) {
idVec3 a = Terrain_Vertex(terrain, x, y);
idVec3 b = Terrain_Vertex(terrain, x, y + 1);
glVertex3fv(a.ToFloatPtr());
glVertex3fv(b.ToFloatPtr());
}
}
glEnd();
}
glPopAttrib();
}
bool Terrain_UndoAvailable(void) {
return s_undoStack.Num() > 0;
}
bool Terrain_RedoAvailable(void) {
return s_redoStack.Num() > 0;
}
static void Terrain_ApplyUndo(terrainUndo_t *undo, bool redo) {
if (!undo || undo->terrainIndex < 0 || undo->terrainIndex >= s_terrains.Num()) {
return;
}
terrainObject_t *terrain = s_terrains[undo->terrainIndex];
if (!terrain) {
return;
}
Terrain_EnsureLayerData(terrain);
for (int i = 0; i < undo->changes.Num(); i++) {
const terrainStrokeChange_t &change = undo->changes[i];
if (change.heightChanged) {
if (!terrain->SetHeightBySample(change.sample, redo ? change.newHeight : change.oldHeight)) {
continue;
}
}
if (change.weightChanged) {
if (!terrain->SetPaintWeightBySample(change.sample, redo ? change.newWeight : change.oldWeight)) {
continue;
}
}
}
Terrain_UpdateBounds(terrain);
mapModified = 1;
Sys_UpdateWindows(W_ALL);
}
bool Terrain_Undo(void) {
if (!Terrain_UndoAvailable()) {
return false;
}
terrainUndo_t *undo = s_undoStack[s_undoStack.Num() - 1];
s_undoStack.RemoveIndex(s_undoStack.Num() - 1);
Terrain_ApplyUndo(undo, false);
s_redoStack.Append(undo);
Sys_Status("Terrain undo\n", 0);
return true;
}
bool Terrain_Redo(void) {
if (!Terrain_RedoAvailable()) {
return false;
}
terrainUndo_t *undo = s_redoStack[s_redoStack.Num() - 1];
s_redoStack.RemoveIndex(s_redoStack.Num() - 1);
Terrain_ApplyUndo(undo, true);
s_undoStack.Append(undo);
Sys_Status("Terrain redo\n", 0);
return true;
}
void Terrain_Save(const char *mapFileName) {
idStr terrainFile = Terrain_FileName(mapFileName);
s_terrainModel.SaveFile(terrainFile.c_str());
}
static void Terrain_TouchLayerMaterials(terrainObject_t *terrain) {
if (!terrain) {
return;
}
Terrain_EnsureLayerData(terrain);
for (int layer = 0; layer < TERRAIN_MAX_LAYERS; layer++) {
if (!terrain->layerMaterials[layer].IsEmpty()) {
Texture_ForName(terrain->layerMaterials[layer].c_str());
}
}
}
void Terrain_Load(const char *mapFileName) {
Terrain_Clear();
idStr terrainFile = Terrain_FileName(mapFileName);
if (!s_terrainModel.LoadFile(terrainFile.c_str(), Terrain_DefaultMaterial())) {
return;
}
for (int i = 0; i < s_terrains.Num(); i++) {
terrainObject_t *terrain = s_terrains[i];
if (!terrain) {
continue;
}
terrain->paintTexture = 0;
Terrain_MarkPaintTextureDirty(terrain);
Terrain_TouchLayerMaterials(terrain);
}
Terrain_ClearUndoStacks();
s_cursorValid = false;
s_cursorTerrainIndex = -1;
}