/* =========================================================================== 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 #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 changes; } terrainUndo_t; typedef struct terrainStroke_s { bool active; int terrainIndex; terrainToolMode_t toolMode; int lastTime; idList changes; } terrainStroke_t; static idTerrainModel s_terrainModel; #define s_terrains s_terrainModel.Terrains() static idList s_undoStack; static idList 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 ¢er) { 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 &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 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; }