Arma Reforger Explorer 1.7.0.54
Arma Reforger Code Explorer by Zeroy - Thanks to MisterOutofTime
Loading...
Searching...
No Matches
SCR_TerrainEdgesFinderPlugin.c
Go to the documentation of this file.
1#ifdef WORKBENCH
2// possible improvements:
3// VISUALS
4// - highlight vertice's edges with debug shapes
5// - remove debug shapes with another button?
6// - make arrow debug shapes hidden by terrain/entities?
7// PERFORMANCE
8// - GetHeightmap done per tile rather than storing the whole heightmap?
9// - make an alternative matrix with calculated angles to not calculate them 3 times
11 name: PLUGIN_NAME,
12 description: "Detect rough vertices on terrain.",
13// shortcut: "",
14 wbModules: { "WorldEditor" },
15 category: "Terrain",
16 awesomeFontCode: 0xF1B3)]
17class SCR_TerrainEdgesFinderPlugin : WorldEditorPlugin
18{
19 [Attribute(defvalue: "75", desc: "Min angle difference between three vertices [deg]", params: "0.01 179.99 0.01")]
20 protected float m_fMinAngleDifference;
21
22 [Attribute(defvalue: "0", uiwidget: UIWidgets.ComboBox, desc: "GetHeightmap mode - under, over water, or both", enums: SCR_ParamEnumArray.FromString("Everything;Above water only;Underwater only"))]
23 protected int m_eMode;
24
25 [Attribute(defvalue: "33", uiwidget: UIWidgets.Slider, desc: "Trace to find if an entity is hiding the bad vertice (0 = no trace)", params: "0 100 0.1")]
26 protected float m_fEntityTraceOffset;
27
28 [Attribute(defvalue: "0", desc: "Force reobtaining terrain's elevation - useful after terrain edits")]
29 protected bool m_bForceHeightmapRefresh;
30
31 // output possibilities: logs, multiline text window, clipboard, heatmap, html link page
32 [Attribute(defvalue: "1", desc: "Output coordinates as Workbench links")]
33 protected bool m_bOutputAsWorkbenchLinks;
34
35 [Attribute(defvalue: "1", desc: "Draw arrows pointing at result vertices")]
36 protected bool m_bDrawDebugShapes;
37
38 protected ref array<ref array<ref array<float>>> m_aHeightmaps;
39 protected string m_sLastLoadedWorld;
41
42// protected static const int MODE_ALL_TERRAIN = 0;
43 protected static const int MODE_ABOVE_WATER = 1;
44 protected static const int MODE_UNDER_WATER = 2;
45
46 protected static const float DEBUG_SHAPE_HEIGHT = 20;
47
48 protected static const string PLUGIN_NAME = "Terrain Edges Finder";
49
50 //------------------------------------------------------------------------------------------------
51 override protected void Run()
52 {
53 WorldEditorAPI worldEditorAPI = SCR_WorldEditorToolHelper.GetWorldEditorAPI();
54 string worldPath;
55 worldEditorAPI.GetWorldPath(worldPath);
56
57 if (!worldPath)
58 {
59 SCR_WorkbenchHelper.PrintDialog("A world must be loaded first.", PLUGIN_NAME, LogLevel.WARNING);
60 return;
61 }
62
63 if (!SCR_WorldEditorToolHelper.HasTerrainMesh())
64 {
65 SCR_WorkbenchHelper.PrintDialog("This world does not have a terrain or its heightmap is not generated.", PLUGIN_NAME, LogLevel.WARNING);
66 return;
67 }
68
69 if (Workbench.ScriptDialog(PLUGIN_NAME, "", this) == 0)
70 return;
71
72 if (m_bForceHeightmapRefresh || !m_aHeightmaps || m_sLastLoadedWorld != worldPath)
73 {
74 WBProgressDialog progress = new WBProgressDialog("Gathering terrain heightmap, please wait...", Workbench.GetModule(WorldEditor));
75 progress.SetProgress(0.42);
76 Debug.BeginTimeMeasure();
77 m_aHeightmaps = GetTerrainHeightmaps();
78 Debug.EndTimeMeasure("Obtaining heightmap as one array");
79 if (!m_aHeightmaps)
80 {
81 SCR_WorkbenchHelper.PrintDialog("Cannot load terrain's heightmap.", PLUGIN_NAME, LogLevel.ERROR);
82 return;
83 }
84
85 worldEditorAPI.GetWorldPath(m_sLastLoadedWorld);
86 }
87
88 int verticesCount;
89 foreach (array<ref array<float>> cols : m_aHeightmaps)
90 {
91 foreach (array<float> yValues : cols)
92 {
93 verticesCount += yValues.Count();
94 }
95 }
96
97 if (Workbench.ScriptDialog(PLUGIN_NAME, "You are about to process " + verticesCount + " vertices; continue?", new WorkbenchDialog_OKCancel()) == 0)
98 return;
99
100 array<vector> resultPositions = {};
101 array<float> resultAngles = {};
102 GetSuspectVertices(resultPositions, resultAngles);
103
104 int count = resultPositions.Count();
105 if (count < 1)
106 {
107 SCR_WorkbenchHelper.PrintDialog("No suspect vertices found.", caption: PLUGIN_NAME, level: LogLevel.NORMAL);
108 return;
109 }
110
111 if (count > 1000)
112 {
113 if (Workbench.ScriptDialog(PLUGIN_NAME, "The tool is about to output " + count + " entries - this may come from an invalid (too small?) angle set.\nContinue (this may take some time)?", new WorkbenchDialog_OKCancel()) == 0)
114 return;
115 }
116
117 int maxAngleIndex;
118 float maxAngle;
119 foreach (int i, float angle : resultAngles)
120 {
121 if (maxAngle < angle)
122 {
123 maxAngleIndex = i;
124 maxAngle = angle;
125 }
126 }
127
128 float step = worldEditorAPI.GetTerrainUnitScale();
129
130 // output before prompt
131
132 string worldPathNoFS;
133 if (m_bOutputAsWorkbenchLinks)
134 worldPathNoFS = SCR_AddonTool.StripFileSystem(worldPath);
135
136 m_DebugShapeManager.Clear();
137 string resultStr = string.Format(
138 "World: %1\n%2 suspect vertices (angle > %3 degrees):",
139 FilePath.StripExtension(FilePath.StripPath(worldPathNoFS)),
140 count.ToString(),
141 SCR_FormatHelper.FloatToDecString(m_fMinAngleDifference, 2));
142
143 foreach (int i, vector position : resultPositions)
144 {
145 resultStr += "\n" + resultAngles[i].ToString(6, 2) + "deg at ";
146 if (m_bOutputAsWorkbenchLinks)
147 resultStr += string.Format("enfusion://WorldEditor/%1;%2,%3,%4;-70.12,315,0", worldPathNoFS, position[0] + 1.808, position[1] + 5, position[2] - 1.808);
148 else
149 resultStr += position.ToString();
150
152 m_DebugShapeManager.AddArrow(position + { 0, DEBUG_SHAPE_HEIGHT, 0 }, position, step * 0.5);
153 }
154
155 int printCount;
156 if (count > 10)
157 {
158 printCount = 10;
159 PrintFormat("Showing %1 out of %2 entries", printCount, count, level: LogLevel.NORMAL);
160 }
161 else
162 {
163 printCount = count;
164 }
165
167 "Max angle position: %1 degrees at %2",
168 maxAngle,
169 Debug.GetPositionLinkString(resultPositions[maxAngleIndex], 0),
170 level: LogLevel.NORMAL);
171
172 for (int i; i < printCount; ++i)
173 {
174 PrintFormat("- %1deg at %2", resultAngles[i].ToString(6, 2), Debug.GetPositionLinkString(resultPositions[i], 0), level: LogLevel.NORMAL);
175 }
176
177 Workbench.ScriptDialog(PLUGIN_NAME, "List of " + count + " suspect vertices.", new SCR_TextResultWorkbenchDialog(resultStr));
178 }
179
180 //------------------------------------------------------------------------------------------------
182 protected array<ref array<ref array<float>>> GetTerrainHeightmaps()
183 {
184 const int terrainIndex = 0;
185
186 WorldEditorAPI worldEditorAPI = SCR_WorldEditorToolHelper.GetWorldEditorAPI();
187 if (!worldEditorAPI)
188 return null;
189
190 int tilesCountX = worldEditorAPI.GetTerrainTilesX(terrainIndex);
191 if (tilesCountX < 1)
192 return null;
193
194 int tilesCountY = worldEditorAPI.GetTerrainTilesY(terrainIndex);
195
196 array<ref array<ref array<float>>> result = {};
197 result.Resize(tilesCountX);
198 array<float> tileTempArray;
199 for (int tileX; tileX < tilesCountY; ++tileX)
200 {
201 result[tileX] = {};
202 result[tileX].Resize(tilesCountY);
203 for (int tileY; tileY < tilesCountY; ++tileY)
204 {
205 tileTempArray = {};
206 worldEditorAPI.GetTerrainSurfaceTile(terrainIndex, tileX, tileY, tileTempArray);
207 result[tileX][tileY] = tileTempArray;
208 }
209 }
210
211 return result;
212 }
213
214 //------------------------------------------------------------------------------------------------
215 protected void GetSuspectVertices(notnull out array<vector> vertices, out notnull array<float> angles)
216 {
217 WorldEditor worldEditor = Workbench.GetModule(WorldEditor);
218 WorldEditorAPI worldEditorAPI = worldEditor.GetApi();
219 BaseWorld world = worldEditorAPI.GetWorld();
220
221 int mode;
222 bool hasOcean = world.IsOcean();
223 if (!hasOcean && m_eMode == MODE_UNDER_WATER)
224 {
225 SCR_WorkbenchHelper.PrintDialog("Temporarily changing mode from underwater to all terrain, provided this terrain has no ocean.", PLUGIN_NAME, LogLevel.NORMAL);
226 mode = 0;
227 }
228 else
229 {
230 mode = m_eMode;
231 }
232
233 float oceanLevel;
234 if (hasOcean)
235 oceanLevel = world.GetOceanBaseHeight();
236
237 const int terrainIndex = 0;
238 int terrainResolutionX = worldEditorAPI.GetTerrainResolutionX(terrainIndex);
239 int terrainResolutionY = worldEditorAPI.GetTerrainResolutionY(terrainIndex);
240 int terrainResolutionXMinus1 = terrainResolutionX - 1;
241 int terrainResolutionYMinus1 = terrainResolutionY - 1;
242 int tileSizeX = terrainResolutionXMinus1 / worldEditorAPI.GetTerrainTilesX(terrainIndex) + 1;
243 int tileSizeZ = terrainResolutionYMinus1 / worldEditorAPI.GetTerrainTilesY(terrainIndex) + 1;
244 int tileSizeXMinus1 = tileSizeX - 1;
245 int tileSizeZMinus1 = tileSizeZ - 1;
246 float step = worldEditorAPI.GetTerrainUnitScale(terrainIndex);
247
248 int totalPoints = (terrainResolutionXMinus1 - 1) * (terrainResolutionYMinus1 - 1);
249
250 TraceParam traceParam;
251 if (m_fEntityTraceOffset > 0)
252 {
253 traceParam = new TraceParam();
254 traceParam.Flags = TraceFlags.ENTS;
255 }
256
257 vertices.Clear();
258 angles.Clear();
259
260 WBProgressDialog progress = new WBProgressDialog("Comparing angles...", worldEditor);
261
262 float minAngleDifferenceRad = m_fMinAngleDifference * Math.DEG2RAD; // work in rad for precision and speed
263
264 int progressStep, progressStepLimit = totalPoints * 0.01;
265 Debug.BeginTimeMeasure();
266
267 int progressCount;
268 array<float> leftTile;
269 array<float> belowTile;
270 foreach (int tileX, array<ref array<float>> xTiles : m_aHeightmaps)
271 {
272 foreach (int tileY, array<float> tileHeightmap : xTiles)
273 {
274 for (int z; z < tileSizeZMinus1; ++z)
275 {
276 if (z == 0)
277 {
278 if (tileY == 0)
279 continue;
280
281 belowTile = m_aHeightmaps[tileX][tileY - 1];
282 }
283
284 for (int x; x < tileSizeXMinus1; ++x)
285 {
286 if (x == 0)
287 {
288 if (tileX == 0)
289 continue;
290 }
291
292 ++progressStep;
293 ++progressCount;
294 if (progressStep >= progressStepLimit) // min 1%
295 {
296 progress.SetProgress(progressCount / totalPoints); // expensive
297 progressStep = 0;
298 }
299
300 int currIndex = x + z * tileSizeX;
301
302 float currY = tileHeightmap[currIndex];
303
304 if (mode == MODE_ABOVE_WATER)
305 {
306 if (currY < oceanLevel)
307 continue;
308 }
309 else
310 if (mode == MODE_UNDER_WATER)
311 {
312 if (currY >= oceanLevel)
313 continue;
314 }
315
316 vector pos = {
317 (x + tileX * tileSizeXMinus1) * step,
318 currY,
319 (z + tileY * tileSizeZMinus1) * step
320 };
321
322 float outAngle;
323 // west-east
324
325 float leftY;
326 if (x == 0)
327 {
328 leftTile = m_aHeightmaps[tileX - 1][tileY];
329 leftY = leftTile[tileSizeXMinus1 + z * tileSizeX];
330 }
331 else
332 {
333 leftY = tileHeightmap[currIndex - 1];
334 }
335
336 float rightY = tileHeightmap[currIndex + 1];
337
338 float belowY;
339 if (z == 0)
340 belowY = belowTile[x + tileSizeZMinus1 * tileSizeX];
341 else
342 belowY = tileHeightmap[currIndex - tileSizeX];
343
344 float aboveY = tileHeightmap[currIndex + tileSizeX];
345
346 if (IsAngleBad(pos, outAngle, leftY, rightY, step, minAngleDifferenceRad, traceParam, world))
347 {
348 vertices.Insert(pos);
349 angles.Insert(outAngle);
350 continue;
351 }
352
353 // south-north
354 if (IsAngleBad(pos, outAngle, belowY, aboveY, step, minAngleDifferenceRad, traceParam, world))
355 {
356 vertices.Insert(pos);
357 angles.Insert(outAngle);
358 // continue;
359 }
360 }
361 }
362 }
363 }
364
365 Debug.EndTimeMeasure("Comparing angles");
366 }
367
368 //------------------------------------------------------------------------------------------------
379 bool IsAngleBad(
381 out float angle,
382 float prevY,
383 float nextY,
384 float step,
385 float minAngleDifferenceRad,
386 TraceParam traceParam,
387 BaseWorld world)
388 {
389 float anglePrevToCurr = Math.Atan2(position[1] - prevY, step);
390 float angleCurrToNext = Math.Atan2(nextY - position[1], step);
391
392 float difference;
393 if (anglePrevToCurr > angleCurrToNext)
394 difference = anglePrevToCurr - angleCurrToNext;
395 else
396 difference = angleCurrToNext - anglePrevToCurr;
397
398 if (difference < 0)
399 difference = -difference;
400
401 if (difference < minAngleDifferenceRad)
402 return false;
403
404 if (traceParam)
405 {
406 traceParam.Start = position + { 0, m_fEntityTraceOffset, 0 };
407 traceParam.End = position;
408
409 if (world.TraceMove(traceParam) != 1)
410 return false;
411 }
412
413 angle = difference * Math.RAD2DEG;
414
415 return true;
416 }
417
418 //------------------------------------------------------------------------------------------------
419 [ButtonAttribute("Process", true)]
420 protected int ButtonProcess()
421 {
422 return 1;
423 }
424
425 //------------------------------------------------------------------------------------------------
426 [ButtonAttribute("Cancel")]
427 protected int ButtonCancel()
428 {
429 return 0;
430 }
431}
432#endif
GenerateFlowMaps WorkbenchPlugin WorkbenchPluginAttribute("Regenerate river flow-maps", "Generate and save/overwrite river flow-maps", "", "", {"WorldEditor"}, "", 0xf773)
Definition FlowmapTool.c:59
bool m_bDrawDebugShapes
ref array< string > angles
vector position
override void Run()
bool ButtonCancel()
UI Textures DeployMenu Briefing conflict_HintBanner_1_UI desc
ref SCR_DebugShapeManager m_DebugShapeManager
EWeaponGroupFireMode m_eMode
class WorkbenchDialog_AbortRetryIgnore ButtonAttribute("OK", true)
Definition Debug.c:13
Definition Math.c:13
static ParamEnumArray FromString(string input)
static void PrintDialog(string message, string caption="", LogLevel level=LogLevel.WARNING)
LogLevel
Enum with severity of the logging message.
Definition LogLevel.c:14
proto void PrintFormat(string fmt, void param1=NULL, void param2=NULL, void param3=NULL, void param4=NULL, void param5=NULL, void param6=NULL, void param7=NULL, void param8=NULL, void param9=NULL, LogLevel level=LogLevel.NORMAL)
SCR_FieldOfViewSettings Attribute
proto external string ToString()
Plain C++ pointer, no weak pointers, no memory management.
TraceFlags
Definition TraceFlags.c:13