/* Copyright (c) 2009-2010 Mikko Mononen memon@inside.org recast4j copyright (c) 2015-2019 Piotr Piastucki piotr@jtilia.org DotRecast Copyright (c) 2023 Choi Ikpil ikpil@naver.com This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. */ using System; using System.Collections.Generic; using System.IO; using DotRecast.Core; using DotRecast.Core.Numerics; using DotRecast.Detour.TileCache.Io; using DotRecast.Recast; namespace DotRecast.Detour.TileCache { public static class DtTileCacheBuilder { public const int DT_TILECACHE_NULL_AREA = 0; public const int DT_TILECACHE_WALKABLE_AREA = 63; public const int DT_TILECACHE_NULL_IDX = 0xffff; private static readonly int[] DirOffsetX = { -1, 0, 1, 0, }; private static readonly int[] DirOffsetY = { 0, 1, 0, -1 }; public static void BuildTileCacheRegions(DtTileCacheLayer layer, int walkableClimb) { int w = layer.header.width; int h = layer.header.height; Array.Fill(layer.regs, (short)0x00FF); int nsweeps = w; RcLayerSweepSpan[] sweeps = new RcLayerSweepSpan[nsweeps]; for (int i = 0; i < sweeps.Length; i++) { sweeps[i] = new RcLayerSweepSpan(); } // Partition walkable area into monotone regions. int[] prevCount = new int[256]; int regId = 0; for (int y = 0; y < h; ++y) { if (regId > 0) { Array.Fill(prevCount, 0, 0, regId); } // Memset(prevCount,0,Sizeof(char)*regId); int sweepId = 0; for (int x = 0; x < w; ++x) { int idx = x + y * w; if (layer.areas[idx] == DT_TILECACHE_NULL_AREA) continue; int sid = 0xff; // -x int xidx = (x - 1) + y * w; if (x > 0 && IsConnected(layer, idx, xidx, walkableClimb)) { if (layer.regs[xidx] != 0xff) sid = layer.regs[xidx]; } if (sid == 0xff) { sid = sweepId++; sweeps[sid].nei = 0xff; sweeps[sid].ns = 0; } // -y int yidx = x + (y - 1) * w; if (y > 0 && IsConnected(layer, idx, yidx, walkableClimb)) { int nr = layer.regs[yidx]; if (nr != 0xff) { // Set neighbour when first valid neighbour is // encoutered. if (sweeps[sid].ns == 0) sweeps[sid].nei = nr; if (sweeps[sid].nei == nr) { // Update existing neighbour sweeps[sid].ns++; prevCount[nr]++; } else { // This is hit if there is nore than one neighbour. // Invalidate the neighbour. sweeps[sid].nei = 0xff; } } } layer.regs[idx] = (byte)sid; } // Create unique ID. for (int i = 0; i < sweepId; ++i) { // If the neighbour is set and there is only one continuous // connection to it, // the sweep will be merged with the previous one, else new // region is created. if (sweeps[i].nei != 0xff && prevCount[sweeps[i].nei] == sweeps[i].ns) { sweeps[i].id = sweeps[i].nei; } else { if (regId == 255) { // Region ID's overflow. throw new Exception("Buffer too small"); } sweeps[i].id = regId++; } } // Remap local sweep ids to region ids. for (int x = 0; x < w; ++x) { int idx = x + y * w; if (layer.regs[idx] != 0xff) layer.regs[idx] = (short)sweeps[layer.regs[idx]].id; } } // Allocate and init layer regions. int nregs = regId; DtLayerMonotoneRegion[] regs = new DtLayerMonotoneRegion[nregs]; for (int i = 0; i < nregs; ++i) { regs[i] = new DtLayerMonotoneRegion(); regs[i].regId = 0xff; } // Find region neighbours. for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { int idx = x + y * w; int ri = layer.regs[idx]; if (ri == 0xff) continue; // Update area. regs[ri].area++; regs[ri].areaId = layer.areas[idx]; // Update neighbours int ymi = x + (y - 1) * w; if (y > 0 && IsConnected(layer, idx, ymi, walkableClimb)) { int rai = layer.regs[ymi]; if (rai != 0xff && rai != ri) { AddUniqueLast(regs[ri].neis, rai); AddUniqueLast(regs[rai].neis, ri); } } } } for (int i = 0; i < nregs; ++i) regs[i].regId = i; for (int i = 0; i < nregs; ++i) { DtLayerMonotoneRegion reg = regs[i]; int merge = -1; int mergea = 0; foreach (int nei in reg.neis) { DtLayerMonotoneRegion regn = regs[nei]; if (reg.regId == regn.regId) continue; if (reg.areaId != regn.areaId) continue; if (regn.area > mergea) { if (CanMerge(reg.regId, regn.regId, regs, nregs)) { mergea = regn.area; merge = nei; } } } if (merge != -1) { int oldId = reg.regId; int newId = regs[merge].regId; for (int j = 0; j < nregs; ++j) if (regs[j].regId == oldId) regs[j].regId = newId; } } // Compact ids. int[] remap = new int[256]; // Find number of unique regions. regId = 0; for (int i = 0; i < nregs; ++i) remap[regs[i].regId] = 1; for (int i = 0; i < 256; ++i) if (remap[i] != 0) remap[i] = regId++; // Remap ids. for (int i = 0; i < nregs; ++i) regs[i].regId = remap[regs[i].regId]; layer.regCount = regId; for (int i = 0; i < w * h; ++i) { if (layer.regs[i] != 0xff) layer.regs[i] = (short)regs[layer.regs[i]].regId; } } public static void AddUniqueLast(List a, int v) { int n = a.Count; if (n > 0 && a[n - 1] == v) return; a.Add(v); } public static bool IsConnected(DtTileCacheLayer layer, int ia, int ib, int walkableClimb) { if (layer.areas[ia] != layer.areas[ib]) return false; if (MathF.Abs(layer.heights[ia] - layer.heights[ib]) > walkableClimb) return false; return true; } public static bool CanMerge(int oldRegId, int newRegId, DtLayerMonotoneRegion[] regs, int nregs) { int count = 0; for (int i = 0; i < nregs; ++i) { DtLayerMonotoneRegion reg = regs[i]; if (reg.regId != oldRegId) continue; foreach (int nei in reg.neis) { if (regs[nei].regId == newRegId) count++; } } return count == 1; } public static void AppendVertex(DtTempContour cont, int x, int y, int z, int r) { // Try to merge with existing segments. if (cont.nverts > 1) { int pa = (cont.nverts - 2) * 4; int pb = (cont.nverts - 1) * 4; if (cont.verts[pb + 3] == r) { if (cont.verts[pa] == cont.verts[pb] && cont.verts[pb] == x) { // The verts are aligned aling x-axis, update z. cont.verts[pb + 1] = y; cont.verts[pb + 2] = z; return; } else if (cont.verts[pa + 2] == cont.verts[pb + 2] && cont.verts[pb + 2] == z) { // The verts are aligned aling z-axis, update x. cont.verts[pb] = x; cont.verts[pb + 1] = y; return; } } } cont.verts.Add(x); cont.verts.Add(y); cont.verts.Add(z); cont.verts.Add(r); cont.nverts++; } public static int GetNeighbourReg(DtTileCacheLayer layer, int ax, int ay, int dir) { int w = layer.header.width; int ia = ax + ay * w; int con = layer.cons[ia] & 0xf; int portal = layer.cons[ia] >> 4; int mask = 1 << dir; if ((con & mask) == 0) { // No connection, return portal or hard edge. if ((portal & mask) != 0) return 0xf8 + dir; return 0xff; } int bx = ax + GetDirOffsetX(dir); int by = ay + GetDirOffsetY(dir); int ib = bx + by * w; return layer.regs[ib]; } public static int GetDirOffsetX(int dir) { return DirOffsetX[dir & 0x03]; } public static int GetDirOffsetY(int dir) { return DirOffsetY[dir & 0x03]; } public static void WalkContour(DtTileCacheLayer layer, int x, int y, DtTempContour cont) { int w = layer.header.width; int h = layer.header.height; cont.Clear(); int startX = x; int startY = y; int startDir = -1; for (int i = 0; i < 4; ++i) { int ndir = (i + 3) & 3; int rn = GetNeighbourReg(layer, x, y, ndir); if (rn != layer.regs[x + y * w]) { startDir = ndir; break; } } if (startDir == -1) return; int dir = startDir; int maxIter = w * h; int iter = 0; while (iter < maxIter) { int rn = GetNeighbourReg(layer, x, y, dir); int nx = x; int ny = y; int ndir = dir; if (rn != layer.regs[x + y * w]) { // Solid edge. int px = x; int pz = y; switch (dir) { case 0: pz++; break; case 1: px++; pz++; break; case 2: px++; break; } // Try to merge with previous vertex. AppendVertex(cont, px, layer.heights[x + y * w], pz, rn); ndir = (dir + 1) & 0x3; // Rotate CW } else { // Move to next. nx = x + GetDirOffsetX(dir); ny = y + GetDirOffsetY(dir); ndir = (dir + 3) & 0x3; // Rotate CCW } if (iter > 0 && x == startX && y == startY && dir == startDir) break; x = nx; y = ny; dir = ndir; iter++; } // Remove last vertex if it is duplicate of the first one. int pa = (cont.nverts - 1) * 4; int pb = 0; if (cont.verts[pa] == cont.verts[pb] && cont.verts[pa + 2] == cont.verts[pb + 2]) cont.nverts--; } public static float DistancePtSeg(int x, int z, int px, int pz, int qx, int qz) { float pqx = qx - px; float pqz = qz - pz; float dx = x - px; float dz = z - pz; float d = pqx * pqx + pqz * pqz; float t = pqx * dx + pqz * dz; if (d > 0) t /= d; if (t < 0) t = 0; else if (t > 1) t = 1; dx = px + t * pqx - x; dz = pz + t * pqz - z; return dx * dx + dz * dz; } public static void SimplifyContour(DtTempContour cont, float maxError) { cont.poly.Clear(); for (int i = 0; i < cont.nverts; ++i) { int j = (i + 1) % cont.nverts; // Check for start of a wall segment. int ra = j * 4 + 3; int rb = i * 4 + 3; if (cont.verts[ra] != cont.verts[rb]) cont.poly.Add(i); } if (cont.Npoly() < 2) { // If there is no transitions at all, // create some initial points for the simplification process. // Find lower-left and upper-right vertices of the contour. int llx = cont.verts[0]; int llz = cont.verts[2]; int lli = 0; int urx = cont.verts[0]; int urz = cont.verts[2]; int uri = 0; for (int i = 1; i < cont.nverts; ++i) { int x = cont.verts[i * 4 + 0]; int z = cont.verts[i * 4 + 2]; if (x < llx || (x == llx && z < llz)) { llx = x; llz = z; lli = i; } if (x > urx || (x == urx && z > urz)) { urx = x; urz = z; uri = i; } } cont.poly.Clear(); cont.poly.Add(lli); cont.poly.Add(uri); } // Add points until all raw points are within // error tolerance to the simplified shape. for (int i = 0; i < cont.Npoly();) { int ii = (i + 1) % cont.Npoly(); int ai = cont.poly[i]; int ax = cont.verts[ai * 4]; int az = cont.verts[ai * 4 + 2]; int bi = cont.poly[ii]; int bx = cont.verts[bi * 4]; int bz = cont.verts[bi * 4 + 2]; // Find maximum deviation from the segment. float maxd = 0; int maxi = -1; int ci, cinc, endi; // Traverse the segment in lexilogical order so that the // max deviation is calculated similarly when traversing // opposite segments. if (bx > ax || (bx == ax && bz > az)) { cinc = 1; ci = (ai + cinc) % cont.nverts; endi = bi; } else { cinc = cont.nverts - 1; ci = (bi + cinc) % cont.nverts; endi = ai; } // Tessellate only outer edges or edges between areas. while (ci != endi) { float d = DistancePtSeg(cont.verts[ci * 4], cont.verts[ci * 4 + 2], ax, az, bx, bz); if (d > maxd) { maxd = d; maxi = ci; } ci = (ci + cinc) % cont.nverts; } // If the max deviation is larger than accepted error, // add new point, else continue to next segment. if (maxi != -1 && maxd > (maxError * maxError)) { cont.poly.Insert(i + 1, maxi); } else { ++i; } } // Remap vertices int start = 0; for (int i = 1; i < cont.Npoly(); ++i) if (cont.poly[i] < cont.poly[start]) start = i; cont.nverts = 0; for (int i = 0; i < cont.Npoly(); ++i) { int j = (start + i) % cont.Npoly(); int src = cont.poly[j] * 4; int dst = cont.nverts * 4; cont.verts[dst] = cont.verts[src]; cont.verts[dst + 1] = cont.verts[src + 1]; cont.verts[dst + 2] = cont.verts[src + 2]; cont.verts[dst + 3] = cont.verts[src + 3]; cont.nverts++; } } public static int GetCornerHeight(DtTileCacheLayer layer, int x, int y, int z, int walkableClimb, out bool shouldRemove) { int w = layer.header.width; int h = layer.header.height; int n = 0; int portal = 0xf; int height = 0; int preg = 0xff; bool allSameReg = true; for (int dz = -1; dz <= 0; ++dz) { for (int dx = -1; dx <= 0; ++dx) { int px = x + dx; int pz = z + dz; if (px >= 0 && pz >= 0 && px < w && pz < h) { int idx = px + pz * w; int lh = layer.heights[idx]; if (MathF.Abs(lh - y) <= walkableClimb && layer.areas[idx] != DT_TILECACHE_NULL_AREA) { height = Math.Max(height, (char)lh); portal &= (layer.cons[idx] >> 4); if (preg != 0xff && preg != layer.regs[idx]) allSameReg = false; preg = layer.regs[idx]; n++; } } } } int portalCount = 0; for (int dir = 0; dir < 4; ++dir) if ((portal & (1 << dir)) != 0) portalCount++; shouldRemove = false; if (n > 1 && portalCount == 1 && allSameReg) { shouldRemove = true; } return height; } // TODO: move this somewhere else, once the layer meshing is done. public static DtTileCacheContourSet BuildTileCacheContours(DtTileCacheLayer layer, int walkableClimb, float maxError) { int w = layer.header.width; int h = layer.header.height; DtTileCacheContourSet lcset = new DtTileCacheContourSet(); lcset.nconts = layer.regCount; lcset.conts = new DtTileCacheContour[lcset.nconts]; for (int i = 0; i < lcset.nconts; i++) { lcset.conts[i] = new DtTileCacheContour(); } // Allocate temp buffer for contour tracing. DtTempContour temp = new DtTempContour(); // Find contours. for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { int idx = x + y * w; int ri = layer.regs[idx]; if (ri == 0xff) continue; DtTileCacheContour cont = lcset.conts[ri]; if (cont.nverts > 0) continue; cont.reg = ri; cont.area = layer.areas[idx]; WalkContour(layer, x, y, temp); SimplifyContour(temp, maxError); // Store contour. cont.nverts = temp.nverts; if (cont.nverts > 0) { cont.verts = new int[4 * temp.nverts]; for (int i = 0, j = temp.nverts - 1; i < temp.nverts; j = i++) { int dst = j * 4; int v = j * 4; int vn = i * 4; int nei = temp.verts[vn + 3]; // The neighbour reg // is // stored at segment // vertex of a // segment. int lh = GetCornerHeight(layer, temp.verts[v], temp.verts[v + 1], temp.verts[v + 2], walkableClimb, out var shouldRemove); cont.verts[dst + 0] = temp.verts[v]; cont.verts[dst + 1] = lh; cont.verts[dst + 2] = temp.verts[v + 2]; // Store portal direction and remove status to the // fourth component. cont.verts[dst + 3] = 0x0f; if (nei != 0xff && nei >= 0xf8) cont.verts[dst + 3] = nei - 0xf8; if (shouldRemove) cont.verts[dst + 3] |= 0x80; } } } } return lcset; } const uint VERTEX_BUCKET_COUNT2 = (1 << 8); public static int ComputeVertexHash2(int x, int y, int z) { uint h1 = 0x8da6b343; // Large multiplicative constants; uint h2 = 0xd8163841; // here arbitrarily chosen primes uint h3 = 0xcb1ab31f; uint n = h1 * (uint)x + h2 * (uint)y + h3 * (uint)z; return (int)(n & (VERTEX_BUCKET_COUNT2 - 1)); } public static int AddVertex(int x, int y, int z, int[] verts, int[] firstVert, int[] nextVert, int nv) { int bucket = ComputeVertexHash2(x, 0, z); int i = firstVert[bucket]; while (i != DT_TILECACHE_NULL_IDX) { int tv = i * 3; if (verts[tv] == x && verts[tv + 2] == z && (MathF.Abs(verts[tv + 1] - y) <= 2)) return i; i = nextVert[i]; // next } // Could not find, create new. i = nv; int v = i * 3; verts[v] = x; verts[v + 1] = y; verts[v + 2] = z; nextVert[i] = firstVert[bucket]; firstVert[bucket] = i; return i; } public static void BuildMeshAdjacency(int[] polys, int npolys, int[] verts, int nverts, DtTileCacheContourSet lcset, int maxVertsPerPoly) { // Based on code by Eric Lengyel from: // http://www.terathon.com/code/edges.php int maxEdgeCount = npolys * maxVertsPerPoly; int[] firstEdge = new int[nverts + maxEdgeCount]; int nextEdge = nverts; int edgeCount = 0; RcEdge[] edges = new RcEdge[maxEdgeCount]; for (int i = 0; i < maxEdgeCount; i++) { edges[i] = new RcEdge(); } for (int i = 0; i < nverts; i++) firstEdge[i] = DT_TILECACHE_NULL_IDX; for (int i = 0; i < npolys; ++i) { int t = i * maxVertsPerPoly * 2; for (int j = 0; j < maxVertsPerPoly; ++j) { if (polys[t + j] == DT_TILECACHE_NULL_IDX) break; int v0 = polys[t + j]; int v1 = (j + 1 >= maxVertsPerPoly || polys[t + j + 1] == DT_TILECACHE_NULL_IDX) ? polys[t] : polys[t + j + 1]; if (v0 < v1) { RcEdge edge = edges[edgeCount]; edge.vert[0] = v0; edge.vert[1] = v1; edge.poly[0] = i; edge.polyEdge[0] = j; edge.poly[1] = i; edge.polyEdge[1] = 0xff; // Insert edge firstEdge[nextEdge + edgeCount] = firstEdge[v0]; firstEdge[v0] = (short)edgeCount; edgeCount++; } } } for (int i = 0; i < npolys; ++i) { int t = i * maxVertsPerPoly * 2; for (int j = 0; j < maxVertsPerPoly; ++j) { if (polys[t + j] == DT_TILECACHE_NULL_IDX) break; int v0 = polys[t + j]; int v1 = (j + 1 >= maxVertsPerPoly || polys[t + j + 1] == DT_TILECACHE_NULL_IDX) ? polys[t] : polys[t + j + 1]; if (v0 > v1) { bool found = false; for (int e = firstEdge[v1]; e != DT_TILECACHE_NULL_IDX; e = firstEdge[nextEdge + e]) { RcEdge edge = edges[e]; if (edge.vert[1] == v0 && edge.poly[0] == edge.poly[1]) { edge.poly[1] = i; edge.polyEdge[1] = j; found = true; break; } } if (!found) { // Matching edge not found, it is an open edge, add it. RcEdge edge = edges[edgeCount]; edge.vert[0] = v1; edge.vert[1] = v0; edge.poly[0] = (short)i; edge.polyEdge[0] = (short)j; edge.poly[1] = (short)i; edge.polyEdge[1] = 0xff; // Insert edge firstEdge[nextEdge + edgeCount] = firstEdge[v1]; firstEdge[v1] = (short)edgeCount; edgeCount++; } } } } // Mark portal edges. for (int i = 0; i < lcset.nconts; ++i) { DtTileCacheContour cont = lcset.conts[i]; if (cont.nverts < 3) continue; for (int j = 0, k = cont.nverts - 1; j < cont.nverts; k = j++) { int va = k * 4; int vb = j * 4; int dir = cont.verts[va + 3] & 0xf; if (dir == 0xf) continue; if (dir == 0 || dir == 2) { // Find matching vertical edge int x = cont.verts[va]; int zmin = cont.verts[va + 2]; int zmax = cont.verts[vb + 2]; if (zmin > zmax) { int tmp = zmin; zmin = zmax; zmax = tmp; } for (int m = 0; m < edgeCount; ++m) { RcEdge e = edges[m]; // Skip connected edges. if (e.poly[0] != e.poly[1]) continue; int eva = e.vert[0] * 3; int evb = e.vert[1] * 3; if (verts[eva] == x && verts[evb] == x) { int ezmin = verts[eva + 2]; int ezmax = verts[evb + 2]; if (ezmin > ezmax) { int tmp = ezmin; ezmin = ezmax; ezmax = tmp; } if (OverlapRangeExl(zmin, zmax, ezmin, ezmax)) { // Reuse the other polyedge to store dir. e.polyEdge[1] = dir; } } } } else { // Find matching vertical edge int z = cont.verts[va + 2]; int xmin = cont.verts[va]; int xmax = cont.verts[vb]; if (xmin > xmax) { int tmp = xmin; xmin = xmax; xmax = tmp; } for (int m = 0; m < edgeCount; ++m) { RcEdge e = edges[m]; // Skip connected edges. if (e.poly[0] != e.poly[1]) continue; int eva = e.vert[0] * 3; int evb = e.vert[1] * 3; if (verts[eva + 2] == z && verts[evb + 2] == z) { int exmin = verts[eva]; int exmax = verts[evb]; if (exmin > exmax) { int tmp = exmin; exmin = exmax; exmax = tmp; } if (OverlapRangeExl(xmin, xmax, exmin, exmax)) { // Reuse the other polyedge to store dir. e.polyEdge[1] = dir; } } } } } } // Store adjacency for (int i = 0; i < edgeCount; ++i) { RcEdge e = edges[i]; if (e.poly[0] != e.poly[1]) { int p0 = e.poly[0] * maxVertsPerPoly * 2; int p1 = e.poly[1] * maxVertsPerPoly * 2; polys[p0 + maxVertsPerPoly + e.polyEdge[0]] = e.poly[1]; polys[p1 + maxVertsPerPoly + e.polyEdge[1]] = e.poly[0]; } else if (e.polyEdge[1] != 0xff) { int p0 = e.poly[0] * maxVertsPerPoly * 2; polys[p0 + maxVertsPerPoly + e.polyEdge[0]] = 0x8000 | (short)e.polyEdge[1]; } } } public static bool OverlapRangeExl(int amin, int amax, int bmin, int bmax) { return (amin >= bmax || amax <= bmin) ? false : true; } public static int Prev(int i, int n) { return i - 1 >= 0 ? i - 1 : n - 1; } public static int Next(int i, int n) { return i + 1 < n ? i + 1 : 0; } public static int Area2(int[] verts, int a, int b, int c) { return (verts[b] - verts[a]) * (verts[c + 2] - verts[a + 2]) - (verts[c] - verts[a]) * (verts[b + 2] - verts[a + 2]); } // Returns true iff c is strictly to the left of the directed // line through a to b. public static bool Left(int[] verts, int a, int b, int c) { return Area2(verts, a, b, c) < 0; } public static bool LeftOn(int[] verts, int a, int b, int c) { return Area2(verts, a, b, c) <= 0; } public static bool Collinear(int[] verts, int a, int b, int c) { return Area2(verts, a, b, c) == 0; } // Returns true iff ab properly intersects cd: they share // a point interior to both segments. The properness of the // intersection is ensured by using strict leftness. public static bool IntersectProp(int[] verts, int a, int b, int c, int d) { // Eliminate improper cases. if (Collinear(verts, a, b, c) || Collinear(verts, a, b, d) || Collinear(verts, c, d, a) || Collinear(verts, c, d, b)) return false; return (Left(verts, a, b, c) ^ Left(verts, a, b, d)) && (Left(verts, c, d, a) ^ Left(verts, c, d, b)); } // Returns T iff (a,b,c) are collinear and point c lies // on the closed segment ab. public static bool Between(int[] verts, int a, int b, int c) { if (!Collinear(verts, a, b, c)) return false; // If ab not vertical, check betweenness on x; else on y. if (verts[a] != verts[b]) return ((verts[a] <= verts[c]) && (verts[c] <= verts[b])) || ((verts[a] >= verts[c]) && (verts[c] >= verts[b])); else return ((verts[a + 2] <= verts[c + 2]) && (verts[c + 2] <= verts[b + 2])) || ((verts[a + 2] >= verts[c + 2]) && (verts[c + 2] >= verts[b + 2])); } // Returns true iff segments ab and cd intersect, properly or improperly. public static bool Intersect(int[] verts, int a, int b, int c, int d) { if (IntersectProp(verts, a, b, c, d)) return true; else if (Between(verts, a, b, c) || Between(verts, a, b, d) || Between(verts, c, d, a) || Between(verts, c, d, b)) return true; else return false; } public static bool Vequal(int[] verts, int a, int b) { return verts[a] == verts[b] && verts[a + 2] == verts[b + 2]; } // Returns T iff (v_i, v_j) is a proper internal *or* external // diagonal of P, *ignoring edges incident to v_i and v_j*. public static bool Diagonalie(int i, int j, int n, int[] verts, int[] indices) { int d0 = (indices[i] & 0x7fff) * 4; int d1 = (indices[j] & 0x7fff) * 4; // For each edge (k,k+1) of P for (int k = 0; k < n; k++) { int k1 = Next(k, n); // Skip edges incident to i or j if (!((k == i) || (k1 == i) || (k == j) || (k1 == j))) { int p0 = (indices[k] & 0x7fff) * 4; int p1 = (indices[k1] & 0x7fff) * 4; if (Vequal(verts, d0, p0) || Vequal(verts, d1, p0) || Vequal(verts, d0, p1) || Vequal(verts, d1, p1)) continue; if (Intersect(verts, d0, d1, p0, p1)) return false; } } return true; } // Returns true iff the diagonal (i,j) is strictly internal to the // polygon P in the neighborhood of the i endpoint. public static bool InCone(int i, int j, int n, int[] verts, int[] indices) { int pi = (indices[i] & 0x7fff) * 4; int pj = (indices[j] & 0x7fff) * 4; int pi1 = (indices[Next(i, n)] & 0x7fff) * 4; int pin1 = (indices[Prev(i, n)] & 0x7fff) * 4; // If P[i] is a convex vertex [ i+1 left or on (i-1,i) ]. if (LeftOn(verts, pin1, pi, pi1)) return Left(verts, pi, pj, pin1) && Left(verts, pj, pi, pi1); // Assume (i-1,i,i+1) not collinear. // else P[i] is reflex. return !(LeftOn(verts, pi, pj, pi1) && LeftOn(verts, pj, pi, pin1)); } // Returns T iff (v_i, v_j) is a proper internal // diagonal of P. public static bool Diagonal(int i, int j, int n, int[] verts, int[] indices) { return InCone(i, j, n, verts, indices) && Diagonalie(i, j, n, verts, indices); } public static int Triangulate(int n, int[] verts, int[] indices, int[] tris) { int ntris = 0; int dst = 0; // tris; // The last bit of the index is used to indicate if the vertex can be // removed. for (int i = 0; i < n; i++) { int i1 = Next(i, n); int i2 = Next(i1, n); if (Diagonal(i, i2, n, verts, indices)) indices[i1] |= 0x8000; } while (n > 3) { int minLen = -1; int mini = -1; for (int mi = 0; mi < n; mi++) { int mi1 = Next(mi, n); if ((indices[mi1] & 0x8000) != 0) { int p0 = (indices[mi] & 0x7fff) * 4; int p2 = (indices[Next(mi1, n)] & 0x7fff) * 4; int dx = verts[p2] - verts[p0]; int dz = verts[p2 + 2] - verts[p0 + 2]; int len = dx * dx + dz * dz; if (minLen < 0 || len < minLen) { minLen = len; mini = mi; } } } if (mini == -1) { // Should not happen. /* * Printf("mini == -1 ntris=%d n=%d\n", ntris, n); for (int i = 0; i < n; i++) { Printf("%d ", * indices[i] & 0x0fffffff); } Printf("\n"); */ return -ntris; } int i = mini; int i1 = Next(i, n); int i2 = Next(i1, n); tris[dst++] = indices[i] & 0x7fff; tris[dst++] = indices[i1] & 0x7fff; tris[dst++] = indices[i2] & 0x7fff; ntris++; // Removes P[i1] by copying P[i+1]...P[n-1] left one index. n--; for (int k = i1; k < n; k++) indices[k] = indices[k + 1]; if (i1 >= n) i1 = 0; i = Prev(i1, n); // Update diagonal flags. if (Diagonal(Prev(i, n), i1, n, verts, indices)) indices[i] |= 0x8000; else indices[i] &= 0x7fff; if (Diagonal(i, Next(i1, n), n, verts, indices)) indices[i1] |= 0x8000; else indices[i1] &= 0x7fff; } // Append the remaining triangle. tris[dst++] = indices[0] & 0x7fff; tris[dst++] = indices[1] & 0x7fff; tris[dst++] = indices[2] & 0x7fff; ntris++; return ntris; } public static int CountPolyVerts(int[] polys, int p, int maxVertsPerPoly) { for (int i = 0; i < maxVertsPerPoly; ++i) if (polys[p + i] == DT_TILECACHE_NULL_IDX) return i; return maxVertsPerPoly; } public static bool Uleft(int[] verts, int a, int b, int c) { return (verts[b] - verts[a]) * (verts[c + 2] - verts[a + 2]) - (verts[c] - verts[a]) * (verts[b + 2] - verts[a + 2]) < 0; } public static int GetPolyMergeValue(int[] polys, int pa, int pb, int[] verts, out int ea, out int eb, int maxVertsPerPoly) { ea = 0; eb = 0; int na = CountPolyVerts(polys, pa, maxVertsPerPoly); int nb = CountPolyVerts(polys, pb, maxVertsPerPoly); // If the merged polygon would be too big, do not merge. if (na + nb - 2 > maxVertsPerPoly) return -1; // Check if the polygons share an edge. ea = -1; eb = -1; for (int i = 0; i < na; ++i) { int va0 = polys[pa + i]; int va1 = polys[pa + (i + 1) % na]; if (va0 > va1) { (va0, va1) = (va1, va0); } for (int j = 0; j < nb; ++j) { int vb0 = polys[pb + j]; int vb1 = polys[pb + (j + 1) % nb]; if (vb0 > vb1) { (vb0, vb1) = (vb1, vb0); } if (va0 == vb0 && va1 == vb1) { ea = i; eb = j; break; } } } // No common edge, cannot merge. if (ea == -1 || eb == -1) return -1; // Check to see if the merged polygon would be convex. int va, vb, vc; va = polys[pa + (ea + na - 1) % na]; vb = polys[pa + ea]; vc = polys[pb + (eb + 2) % nb]; if (!Uleft(verts, va * 3, vb * 3, vc * 3)) return -1; va = polys[pb + (eb + nb - 1) % nb]; vb = polys[pb + eb]; vc = polys[pa + (ea + 2) % na]; if (!Uleft(verts, va * 3, vb * 3, vc * 3)) return -1; va = polys[pa + ea]; vb = polys[pa + (ea + 1) % na]; int dx = verts[va * 3 + 0] - verts[vb * 3 + 0]; int dy = verts[va * 3 + 2] - verts[vb * 3 + 2]; return (dx * dx) + (dy * dy); } public static void MergePolys(int[] polys, int pa, int pb, int ea, int eb, int maxVertsPerPoly) { int[] tmp = new int[maxVertsPerPoly * 2]; int na = CountPolyVerts(polys, pa, maxVertsPerPoly); int nb = CountPolyVerts(polys, pb, maxVertsPerPoly); // Merge polygons. Array.Fill(tmp, DT_TILECACHE_NULL_IDX); int n = 0; // Add pa for (int i = 0; i < na - 1; ++i) tmp[n++] = polys[pa + (ea + 1 + i) % na]; // Add pb for (int i = 0; i < nb - 1; ++i) tmp[n++] = polys[pb + (eb + 1 + i) % nb]; RcArrays.Copy(tmp, 0, polys, pa, maxVertsPerPoly); } public static int PushFront(int v, List arr) { arr.Insert(0, v); return arr.Count; } public static int PushBack(int v, List arr) { arr.Add(v); return arr.Count; } public static bool CanRemoveVertex(DtTileCachePolyMesh mesh, int rem) { // Count number of polygons to remove. int maxVertsPerPoly = mesh.nvp; int numRemainingEdges = 0; for (int i = 0; i < mesh.npolys; ++i) { int p = i * mesh.nvp * 2; int nv = CountPolyVerts(mesh.polys, p, maxVertsPerPoly); int numRemoved = 0; int numVerts = 0; for (int j = 0; j < nv; ++j) { if (mesh.polys[p + j] == rem) { numRemoved++; } numVerts++; } if (numRemoved != 0) { numRemainingEdges += numVerts - (numRemoved + 1); } } // There would be too few edges remaining to create a polygon. // This can happen for example when a tip of a triangle is marked // as deletion, but there are no other polys that share the vertex. // In this case, the vertex should not be removed. if (numRemainingEdges <= 2) return false; // Find edges which share the removed vertex. List edges = new List(); int nedges = 0; for (int i = 0; i < mesh.npolys; ++i) { int p = i * mesh.nvp * 2; int nv = CountPolyVerts(mesh.polys, p, maxVertsPerPoly); // Collect edges which touches the removed vertex. for (int j = 0, k = nv - 1; j < nv; k = j++) { if (mesh.polys[p + j] == rem || mesh.polys[p + k] == rem) { // Arrange edge so that a=rem. int a = mesh.polys[p + j], b = mesh.polys[p + k]; if (b == rem) { int tmp = a; a = b; b = tmp; } // Check if the edge exists bool exists = false; for (int m = 0; m < nedges; ++m) { int e = m * 3; if (edges[e + 1] == b) { // Exists, increment vertex share count. edges[e + 2] = edges[e + 2] + 1; exists = true; } } // Add new edge. if (!exists) { edges.Add(a); edges.Add(b); edges.Add(1); nedges++; } } } } // There should be no more than 2 open edges. // This catches the case that two non-adjacent polygons // share the removed vertex. In that case, do not remove the vertex. int numOpenEdges = 0; for (int i = 0; i < nedges; ++i) { if (edges[i * 3 + 2] < 2) numOpenEdges++; } if (numOpenEdges > 2) return false; return true; } public static void RemoveVertex(DtTileCachePolyMesh mesh, int rem, int maxTris) { // Count number of polygons to remove. int maxVertsPerPoly = mesh.nvp; int numRemovedVerts = 0; for (int i = 0; i < mesh.npolys; ++i) { int p = i * maxVertsPerPoly * 2; int nv = CountPolyVerts(mesh.polys, p, maxVertsPerPoly); for (int j = 0; j < nv; ++j) { if (mesh.polys[p + j] == rem) numRemovedVerts++; } } int nedges = 0; List edges = new List(); int nhole = 0; List hole = new List(); List harea = new List(); for (int i = 0; i < mesh.npolys; ++i) { int p = i * maxVertsPerPoly * 2; int nv = CountPolyVerts(mesh.polys, p, maxVertsPerPoly); bool hasRem = false; for (int j = 0; j < nv; ++j) if (mesh.polys[p + j] == rem) hasRem = true; if (hasRem) { // Collect edges which does not touch the removed vertex. for (int j = 0, k = nv - 1; j < nv; k = j++) { if (mesh.polys[p + j] != rem && mesh.polys[p + k] != rem) { edges.Add(mesh.polys[p + k]); edges.Add(mesh.polys[p + j]); edges.Add(mesh.areas[i]); nedges++; } } // Remove the polygon. int p2 = (mesh.npolys - 1) * maxVertsPerPoly * 2; RcArrays.Copy(mesh.polys, p2, mesh.polys, p, maxVertsPerPoly); Array.Fill(mesh.polys, DT_TILECACHE_NULL_IDX, p + maxVertsPerPoly, maxVertsPerPoly); mesh.areas[i] = mesh.areas[mesh.npolys - 1]; mesh.npolys--; --i; } } // Remove vertex. for (int i = rem; i < mesh.nverts - 1; ++i) { mesh.verts[i * 3 + 0] = mesh.verts[(i + 1) * 3 + 0]; mesh.verts[i * 3 + 1] = mesh.verts[(i + 1) * 3 + 1]; mesh.verts[i * 3 + 2] = mesh.verts[(i + 1) * 3 + 2]; } mesh.nverts--; // Adjust indices to match the removed vertex layout. for (int i = 0; i < mesh.npolys; ++i) { int p = i * maxVertsPerPoly * 2; int nv = CountPolyVerts(mesh.polys, p, maxVertsPerPoly); for (int j = 0; j < nv; ++j) if (mesh.polys[p + j] > rem) mesh.polys[p + j]--; } for (int i = 0; i < nedges; ++i) { if (edges[i * 3] > rem) edges[i * 3] = edges[i * 3] - 1; if (edges[i * 3 + 1] > rem) edges[i * 3 + 1] = edges[i * 3 + 1] - 1; } if (nedges == 0) return; // Start with one vertex, keep appending connected // segments to the start and end of the hole. nhole = PushBack(edges[0], hole); PushBack(edges[2], harea); while (nedges != 0) { bool match = false; for (int i = 0; i < nedges; ++i) { int ea = edges[i * 3]; int eb = edges[i * 3 + 1]; int a = edges[i * 3 + 2]; bool add = false; if (hole[0] == eb) { // The segment matches the beginning of the hole boundary. nhole = PushFront(ea, hole); PushFront(a, harea); add = true; } else if (hole[nhole - 1] == ea) { // The segment matches the end of the hole boundary. nhole = PushBack(eb, hole); PushBack(a, harea); add = true; } if (add) { // The edge segment was added, remove it. edges[i * 3] = edges[(nedges - 1) * 3]; edges[i * 3 + 1] = edges[(nedges - 1) * 3] + 1; edges[i * 3 + 2] = edges[(nedges - 1) * 3] + 2; --nedges; match = true; --i; } } if (!match) break; } int[] tris = new int[nhole * 3]; int[] tverts = new int[nhole * 4]; int[] tpoly = new int[nhole]; // Generate temp vertex array for triangulation. for (int i = 0; i < nhole; ++i) { int pi = hole[i]; tverts[i * 4 + 0] = mesh.verts[pi * 3 + 0]; tverts[i * 4 + 1] = mesh.verts[pi * 3 + 1]; tverts[i * 4 + 2] = mesh.verts[pi * 3 + 2]; tverts[i * 4 + 3] = 0; tpoly[i] = i; } // Triangulate the hole. int ntris = Triangulate(nhole, tverts, tpoly, tris); if (ntris < 0) { // TODO: issue warning! ntris = -ntris; } int[] polys = new int[ntris * maxVertsPerPoly]; int[] pareas = new int[ntris]; // Build initial polygons. int npolys = 0; Array.Fill(polys, DT_TILECACHE_NULL_IDX, 0, ntris * maxVertsPerPoly); for (int j = 0; j < ntris; ++j) { int t = j * 3; if (tris[t] != tris[t + 1] && tris[t] != tris[t + 2] && tris[t + 1] != tris[t + 2]) { polys[npolys * maxVertsPerPoly + 0] = hole[tris[t]]; polys[npolys * maxVertsPerPoly + 1] = hole[tris[t + 1]]; polys[npolys * maxVertsPerPoly + 2] = hole[tris[t + 2]]; pareas[npolys] = harea[tris[t]]; npolys++; } } if (npolys == 0) return; // Merge polygons. if (maxVertsPerPoly > 3) { for (;;) { // Find best polygons to merge. int bestMergeVal = 0; int bestPa = 0, bestPb = 0, bestEa = 0, bestEb = 0; for (int j = 0; j < npolys - 1; ++j) { int pj = j * maxVertsPerPoly; for (int k = j + 1; k < npolys; ++k) { int pk = k * maxVertsPerPoly; int v = GetPolyMergeValue(polys, pj, pk, mesh.verts, out var ea, out var eb, maxVertsPerPoly); if (v > bestMergeVal) { bestMergeVal = v; bestPa = j; bestPb = k; bestEa = ea; bestEb = eb; } } } if (bestMergeVal > 0) { // Found best, merge. int pa = bestPa * maxVertsPerPoly; int pb = bestPb * maxVertsPerPoly; MergePolys(polys, pa, pb, bestEa, bestEb, maxVertsPerPoly); RcArrays.Copy(polys, (npolys - 1) * maxVertsPerPoly, polys, pb, maxVertsPerPoly); pareas[bestPb] = pareas[npolys - 1]; npolys--; } else { // Could not merge any polygons, stop. break; } } } // Store polygons. for (int i = 0; i < npolys; ++i) { if (mesh.npolys >= maxTris) break; int p = mesh.npolys * maxVertsPerPoly * 2; Array.Fill(mesh.polys, DT_TILECACHE_NULL_IDX, p, maxVertsPerPoly * 2); for (int j = 0; j < maxVertsPerPoly; ++j) mesh.polys[p + j] = polys[i * maxVertsPerPoly + j]; mesh.areas[mesh.npolys] = pareas[i]; mesh.npolys++; if (mesh.npolys > maxTris) { throw new Exception("Buffer too small"); } } } public static DtTileCachePolyMesh BuildTileCachePolyMesh(DtTileCacheContourSet lcset, int maxVertsPerPoly) { int maxVertices = 0; int maxTris = 0; int maxVertsPerCont = 0; for (int i = 0; i < lcset.nconts; ++i) { // Skip null contours. if (lcset.conts[i].nverts < 3) continue; maxVertices += lcset.conts[i].nverts; maxTris += lcset.conts[i].nverts - 2; maxVertsPerCont = Math.Max(maxVertsPerCont, lcset.conts[i].nverts); } // TODO: warn about too many vertices? DtTileCachePolyMesh mesh = new DtTileCachePolyMesh(maxVertsPerPoly); int[] vflags = new int[maxVertices]; mesh.verts = new int[maxVertices * 3]; mesh.polys = new int[maxTris * maxVertsPerPoly * 2]; mesh.areas = new int[maxTris]; // Just allocate and clean the mesh flags array. The user is resposible // for filling it. mesh.flags = new int[maxTris]; mesh.nverts = 0; mesh.npolys = 0; Array.Fill(mesh.polys, DT_TILECACHE_NULL_IDX); int[] firstVert = new int[VERTEX_BUCKET_COUNT2]; for (int i = 0; i < VERTEX_BUCKET_COUNT2; ++i) firstVert[i] = DT_TILECACHE_NULL_IDX; int[] nextVert = new int[maxVertices]; int[] indices = new int[maxVertsPerCont]; int[] tris = new int[maxVertsPerCont * 3]; int[] polys = new int[maxVertsPerCont * maxVertsPerPoly]; for (int i = 0; i < lcset.nconts; ++i) { DtTileCacheContour cont = lcset.conts[i]; // Skip null contours. if (cont.nverts < 3) continue; // Triangulate contour for (int j = 0; j < cont.nverts; ++j) indices[j] = j; int ntris = Triangulate(cont.nverts, cont.verts, indices, tris); if (ntris <= 0) { // TODO: issue warning! ntris = -ntris; } // Add and merge vertices. for (int j = 0; j < cont.nverts; ++j) { int v = j * 4; indices[j] = AddVertex(cont.verts[v], cont.verts[v + 1], cont.verts[v + 2], mesh.verts, firstVert, nextVert, mesh.nverts); mesh.nverts = Math.Max(mesh.nverts, indices[j] + 1); if ((cont.verts[v + 3] & 0x80) != 0) { // This vertex should be removed. vflags[indices[j]] = 1; } } // Build initial polygons. int npolys = 0; Array.Fill(polys, DT_TILECACHE_NULL_IDX); for (int j = 0; j < ntris; ++j) { int t = j * 3; if (tris[t] != tris[t + 1] && tris[t] != tris[t + 2] && tris[t + 1] != tris[t + 2]) { polys[npolys * maxVertsPerPoly + 0] = indices[tris[t]]; polys[npolys * maxVertsPerPoly + 1] = indices[tris[t + 1]]; polys[npolys * maxVertsPerPoly + 2] = indices[tris[t + 2]]; npolys++; } } if (npolys == 0) continue; // Merge polygons. if (maxVertsPerPoly > 3) { for (;;) { // Find best polygons to merge. int bestMergeVal = 0; int bestPa = 0, bestPb = 0, bestEa = 0, bestEb = 0; for (int j = 0; j < npolys - 1; ++j) { int pj = j * maxVertsPerPoly; for (int k = j + 1; k < npolys; ++k) { int pk = k * maxVertsPerPoly; int v = GetPolyMergeValue(polys, pj, pk, mesh.verts, out var ea, out var eb, maxVertsPerPoly); if (v > bestMergeVal) { bestMergeVal = v; bestPa = j; bestPb = k; bestEa = ea; bestEb = eb; } } } if (bestMergeVal > 0) { // Found best, merge. int pa = bestPa * maxVertsPerPoly; int pb = bestPb * maxVertsPerPoly; MergePolys(polys, pa, pb, bestEa, bestEb, maxVertsPerPoly); RcArrays.Copy(polys, (npolys - 1) * maxVertsPerPoly, polys, pb, maxVertsPerPoly); npolys--; } else { // Could not merge any polygons, stop. break; } } } // Store polygons. for (int j = 0; j < npolys; ++j) { int p = mesh.npolys * maxVertsPerPoly * 2; int q = j * maxVertsPerPoly; for (int k = 0; k < maxVertsPerPoly; ++k) mesh.polys[p + k] = polys[q + k]; mesh.areas[mesh.npolys] = cont.area; mesh.npolys++; if (mesh.npolys > maxTris) throw new Exception("Buffer too small"); } } // Remove edge vertices. for (int i = 0; i < mesh.nverts; ++i) { if (vflags[i] != 0) { if (!CanRemoveVertex(mesh, i)) continue; RemoveVertex(mesh, i, maxTris); // Remove vertex // Note: mesh.nverts is already decremented inside // RemoveVertex()! for (int j = i; j < mesh.nverts; ++j) vflags[j] = vflags[j + 1]; --i; } } // Calculate adjacency. BuildMeshAdjacency(mesh.polys, mesh.npolys, mesh.verts, mesh.nverts, lcset, maxVertsPerPoly); return mesh; } public static void MarkCylinderArea(DtTileCacheLayer layer, RcVec3f orig, float cs, float ch, RcVec3f pos, float radius, float height, int areaId) { RcVec3f bmin = new RcVec3f(); RcVec3f bmax = new RcVec3f(); bmin.X = pos.X - radius; bmin.Y = pos.Y; bmin.Z = pos.Z - radius; bmax.X = pos.X + radius; bmax.Y = pos.Y + height; bmax.Z = pos.Z + radius; float r2 = RcMath.Sqr(radius / cs + 0.5f); int w = layer.header.width; int h = layer.header.height; float ics = 1.0f / cs; float ich = 1.0f / ch; float px = (pos.X - orig.X) * ics; float pz = (pos.Z - orig.Z) * ics; int minx = (int)MathF.Floor((bmin.X - orig.X) * ics); int miny = (int)MathF.Floor((bmin.Y - orig.Y) * ich); int minz = (int)MathF.Floor((bmin.Z - orig.Z) * ics); int maxx = (int)MathF.Floor((bmax.X - orig.X) * ics); int maxy = (int)MathF.Floor((bmax.Y - orig.Y) * ich); int maxz = (int)MathF.Floor((bmax.Z - orig.Z) * ics); if (maxx < 0) return; if (minx >= w) return; if (maxz < 0) return; if (minz >= h) return; if (minx < 0) minx = 0; if (maxx >= w) maxx = w - 1; if (minz < 0) minz = 0; if (maxz >= h) maxz = h - 1; for (int z = minz; z <= maxz; ++z) { for (int x = minx; x <= maxx; ++x) { float dx = x + 0.5f - px; float dz = z + 0.5f - pz; if (dx * dx + dz * dz > r2) continue; int y = layer.heights[x + z * w]; if (y < miny || y > maxy) continue; layer.areas[x + z * w] = (short)areaId; } } } public static void MarkBoxArea(DtTileCacheLayer layer, RcVec3f orig, float cs, float ch, RcVec3f bmin, RcVec3f bmax, int areaId) { int w = layer.header.width; int h = layer.header.height; float ics = 1.0f / cs; float ich = 1.0f / ch; int minx = (int)MathF.Floor((bmin.X - orig.X) * ics); int miny = (int)MathF.Floor((bmin.Y - orig.Y) * ich); int minz = (int)MathF.Floor((bmin.Z - orig.Z) * ics); int maxx = (int)MathF.Floor((bmax.X - orig.X) * ics); int maxy = (int)MathF.Floor((bmax.Y - orig.Y) * ich); int maxz = (int)MathF.Floor((bmax.Z - orig.Z) * ics); if (maxx < 0) return; if (minx >= w) return; if (maxz < 0) return; if (minz >= h) return; if (minx < 0) minx = 0; if (maxx >= w) maxx = w - 1; if (minz < 0) minz = 0; if (maxz >= h) maxz = h - 1; for (int z = minz; z <= maxz; ++z) { for (int x = minx; x <= maxx; ++x) { int y = layer.heights[x + z * w]; if (y < miny || y > maxy) continue; layer.areas[x + z * w] = (short)areaId; } } } public static byte[] CompressTileCacheLayer(IRcCompressor comp, DtTileCacheLayer layer, RcByteOrder order, bool cCompatibility) { using var ms = new MemoryStream(); using var bw = new BinaryWriter(ms); DtTileCacheLayerHeaderWriter hw = new DtTileCacheLayerHeaderWriter(); try { hw.Write(bw, layer.header, order, cCompatibility); int gridSize = layer.header.width * layer.header.height; byte[] buffer = new byte[gridSize * 3]; for (int i = 0; i < gridSize; i++) { buffer[i] = (byte)layer.heights[i]; buffer[gridSize + i] = (byte)layer.areas[i]; buffer[gridSize * 2 + i] = (byte)layer.cons[i]; } var compressed = comp.Compress(buffer); bw.Write(compressed); return ms.ToArray(); } catch (IOException e) { throw new Exception(e.Message, e); } } public static byte[] CompressTileCacheLayer(DtTileCacheLayerHeader header, int[] heights, int[] areas, int[] cons, RcByteOrder order, bool cCompatibility, IRcCompressor comp) { using var ms = new MemoryStream(); using var bw = new BinaryWriter(ms); DtTileCacheLayerHeaderWriter hw = new DtTileCacheLayerHeaderWriter(); try { hw.Write(bw, header, order, cCompatibility); int gridSize = header.width * header.height; byte[] buffer = new byte[gridSize * 3]; for (int i = 0; i < gridSize; i++) { buffer[i] = (byte)heights[i]; buffer[gridSize + i] = (byte)areas[i]; buffer[gridSize * 2 + i] = (byte)cons[i]; } var compressed = comp.Compress(buffer); bw.Write(compressed); return ms.ToArray(); } catch (IOException e) { throw new Exception(e.Message, e); } } public static DtTileCacheLayer DecompressTileCacheLayer(IRcCompressor comp, byte[] compressed, RcByteOrder order, bool cCompatibility) { RcByteBuffer buf = new RcByteBuffer(compressed); buf.Order(order); DtTileCacheLayer layer = new DtTileCacheLayer(); try { layer.header = DtTileCacheLayerHeaderReader.Read(buf, cCompatibility); } catch (IOException e) { throw new Exception(e.Message, e); } int gridSize = layer.header.width * layer.header.height; byte[] grids = comp.Decompress(compressed, buf.Position(), compressed.Length - buf.Position(), gridSize * 3); layer.heights = new short[gridSize]; layer.areas = new short[gridSize]; layer.cons = new short[gridSize]; layer.regs = new short[gridSize]; for (int i = 0; i < gridSize; i++) { layer.heights[i] = (short)(grids[i] & 0xFF); layer.areas[i] = (short)(grids[i + gridSize] & 0xFF); layer.cons[i] = (short)(grids[i + gridSize * 2] & 0xFF); } return layer; } public static void MarkBoxArea(DtTileCacheLayer layer, RcVec3f orig, float cs, float ch, RcVec3f center, RcVec3f extents, float[] rotAux, int areaId) { int w = layer.header.width; int h = layer.header.height; float ics = 1.0f / cs; float ich = 1.0f / ch; float cx = (center.X - orig.X) * ics; float cz = (center.Z - orig.Z) * ics; float maxr = 1.41f * Math.Max(extents.X, extents.Z); int minx = (int)MathF.Floor(cx - maxr * ics); int maxx = (int)MathF.Floor(cx + maxr * ics); int minz = (int)MathF.Floor(cz - maxr * ics); int maxz = (int)MathF.Floor(cz + maxr * ics); int miny = (int)MathF.Floor((center.Y - extents.Y - orig.Y) * ich); int maxy = (int)MathF.Floor((center.Y + extents.Y - orig.Y) * ich); if (maxx < 0) return; if (minx >= w) return; if (maxz < 0) return; if (minz >= h) return; if (minx < 0) minx = 0; if (maxx >= w) maxx = w - 1; if (minz < 0) minz = 0; if (maxz >= h) maxz = h - 1; float xhalf = extents.X * ics + 0.5f; float zhalf = extents.Z * ics + 0.5f; for (int z = minz; z <= maxz; ++z) { for (int x = minx; x <= maxx; ++x) { float x2 = 2.0f * (x - cx); float z2 = 2.0f * (z - cz); float xrot = rotAux[1] * x2 + rotAux[0] * z2; if (xrot > xhalf || xrot < -xhalf) continue; float zrot = rotAux[1] * z2 - rotAux[0] * x2; if (zrot > zhalf || zrot < -zhalf) continue; int y = layer.heights[x + z * w]; if (y < miny || y > maxy) continue; layer.areas[x + z * w] = (short)areaId; } } } } }