0
votes

I am attempting to implement Delaunay Triangulation in order to create a Navigation Mesh for a top down 2d game that I am making for one of my courses. First question, is Delaunay Triangulation a good idea for this, or are there better ways? I would rather have this be an automated than by me placing nodes in myself. Here is what I have so far:

function genSubMat(matrix, ignoreCol)
{
	let r = [];
	for (let i = 0; i < matrix.length - 1; i++)
	{
		r.push([]);
		for (let j = 0; j < matrix[0].length; j++)
		{
			if (j != ignoreCol)
				r[i].push(matrix[i + 1][j]);
		}
	}

	return r;
}

function determinantSqMat(matrix)
{
	if (matrix.length != matrix[0].length) return false;

	if (matrix.length === 2) return matrix[0][0] * matrix[1][1] - matrix[1][0] * matrix[0][1];

	let det = 0;
	for (let i = 0; i < matrix.length; i++)
	{
		let r = genSubMat(matrix, i);
		let tmp = matrix[0][i] * determinantSqMat(r);
		if (i % 2 == 0)
			det += tmp;
		else
			det -= tmp;
	}

	return -det;
}

// if d is in the circle formed by points a, b, and c, return > 0
// d is on circle, return 0
// d is outside of circle, return < 0
function inCircle(a, b, c, d)
{
	let arr = [a, b, c, d];
	let mat = [
		[],
		[],
		[],
		[]
	];

	for (let i = 0; i < arr.length; i++)
	{
		mat[i][0] = 1;
		mat[i][1] = arr[i].position.x;
		mat[i][2] = arr[i].position.y;
		mat[i][3] = arr[i].position.x * arr[i].position.x + arr[i].position.y * arr[i].position.y;
	}
	return determinantSqMat(mat);
}

function walkable(from, to, hardnessMap)
{
	let diff = new Vector(to.x - from.x, to.y - from.y);
	if (Math.abs(diff.x) > Math.abs(diff.y)) diff.scale(Math.abs(1 / diff.x));
	else diff.scale(Math.abs(1 / diff.y));
	let current = new Vector(from.x + diff.x, from.y + diff.y);
	while (Math.round(current.x) != to.x || Math.round(current.y) != to.y)
	{
		if (hardnessMap[Math.floor(current.y)][Math.floor(current.x)] === 1)
			return false;
		current.x += diff.x;
		current.y += diff.y;
	}

	return true;
}

function getLowest(nodes)
{
	let lowest = nodes[0];
	for (let i = 1; i < nodes.length; i++)
	{
		if (nodes[i].position.y > lowest.position.y)
			lowest = nodes[i];
	}

	return lowest;
}

function angleBetween(a, b, c, cw)
{
	let v1 = new Vector(a.position.x - b.position.x, a.position.y - b.position.y);
	let v2 = new Vector(c.position.x - b.position.x, c.position.y - b.position.y);

	let angle = v1.angleBetween(v2) * 180 / Math.PI;
	angle *= (cw) ? 1 : -1;
	return (angle < 0) ? angle + 360 : angle;
}

function sortSmallestAngle(a, b, list, cw)
{
	list.sort((m, n) =>
	{
		let vab = new Vector(a.position.x - b.position.x, a.position.y - b.position.y);
		let vmb = new Vector(m.position.x - b.position.x, m.position.y - b.position.y);
		let vnb = new Vector(n.position.x - b.position.x, n.position.y - b.position.y);

		if (cw)
			return vab.angleBetween(vmb, cw) - vab.angleBetween(vnb, cw);
		else
			return vab.angleBetween(vnb, cw) - vab.angleBetween(vmb, cw);
	});
}

// a is in list, b is in the other list
function getPotential(a, b, list, cw)
{
	sortSmallestAngle(b, a, list, cw);
	for (let i = 0; i < list.length - 1; i++)
	{
		let angle = angleBetween(b, a, list[i], cw);
		if (angle > 180) return false;
		else if (inCircle(a, b, list[i], list[i + 1]) <= 0)
		{
			return list[i];
		}
		else
		{
			a.removeNeighbor(list[i]);
			list[i].removeNeighbor(a);
		}
	}

	let potential = list[list.length - 1];
	if (potential)
	{
		let angle = angleBetween(a, b, potential, cw);
		if (angle > 180) return false;
		return potential;
	}
	return false;
}

function merge(leftNodes, rightNodes, leftBase, rightBase, hardnessMap)
{
	leftBase.addNeighbor(rightBase);
	rightBase.addNeighbor(leftBase);

	let newLeft = leftNodes.filter((n) => n != leftBase);
	let newRight = rightNodes.filter((n) => n != rightBase);

	let potentialLeft = getPotential(leftBase, rightBase, newLeft, false);
	let potentialRight = getPotential(rightBase, leftBase, newRight, true);

	if (potentialLeft && !potentialRight)
		merge(newLeft, newRight, potentialLeft, rightBase, hardnessMap);
	else if (potentialRight && !potentialLeft)
		merge(newLeft, newRight, leftBase, potentialRight, hardnessMap);
	else if (potentialLeft && potentialRight && inCircle(leftBase, rightBase, potentialLeft, potentialRight) <= 0)
		merge(newLeft, newRight, potentialLeft, rightBase);
	else if (potentialLeft && potentialRight && inCricle(leftBase, rightBase, potentialRight, potentialLeft) <= 0)
		merge(newLeft, newRight, leftBase, potentialRight);
	else return;
}

// divide and conquer algorithm
function delaunay(nodes, hardnessMap)
{
	if (nodes.length <= 3)
	{
		for (let i = 0; i < nodes.length; i++)
			for (let j = 0; j < nodes.length; j++)
				if (i != j)
					nodes[i].addNeighbor(nodes[j]);

		return nodes;
	}
	else
	{
		nodes.sort((a, b) =>
		{
			let tmp = a.position.x - b.position.x;
			if (tmp === 0) return a.position.y - b.position.y;
			return tmp;
		});

		let l = nodes.length;

		let leftNodes;
		let rightNodes;
		if (l == 4)
		{
			leftNodes = delaunay(nodes.slice(0, 3), hardnessMap);
			rightNodes = delaunay(nodes.slice(3, 4), hardnessMap);
		}
		else if (l == 5)
		{
			leftNodes = delaunay(nodes.slice(0, 3), hardnessMap);
			rightNodes = delaunay(nodes.slice(3, 5), hardnessMap);
		}
		else
		{
			leftNodes = delaunay(nodes.slice(0, Math.floor(nodes.length / 2)), hardnessMap);
			rightNodes = delaunay(nodes.slice(Math.floor(nodes.length / 2), nodes.length), hardnessMap);
		}

		if (nodes.length == 8) return nodes;
		let leftBase = getLowest(leftNodes);
		let rightBase = getLowest(rightNodes);

		merge(leftNodes, rightNodes, leftBase, rightBase, hardnessMap);

		console.log("=============================MergeComplete================================");
		return nodes;
	}
}

Currently, I am trying to run this on an array of points that create a square, within another square. The output I get is obviously not correct as the two sides are not merged together in the end and some edges cross each other. Am I doing something wrong here? I am performing this by following the description from http://www.geom.uiuc.edu/~samuelp/del_project.html#algorithms.

"is Delaunay Triangulation a good idea for this": depends on what you call a good idea. - Yves Daoust
The way yo implemented the InCircle test with a reucursive determinant computation is terrible. You'd better inline the formula. - Yves Daoust
Your code doesn't look like the canonical D&C algorithm. - Yves Daoust
What would be a better algorithm for creating the navmesh? Also, what is different between this and the canonical D&C algorithm? The only thing I found that was wrong that I have changed since I posted this was my angleBetween function, I have changed it so now the cw boolean actually works properly. - Connor