mirror of
https://github.com/jmarshall23/DoomRTX.git
synced 2026-08-15 15:51:07 +02:00
1570 lines
44 KiB
C++
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 ¢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<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;
|
|
}
|