🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

How to calculate the weights of the nodes in a 2d blend space ?

Started by
10 comments, last by ostef 3 years, 3 months ago

Hi! I've been working on an animation system to gain some experience, and I want to implement 2d blend spaces. Here's the struct of a 2d blend space:

Blend_Space_2d :: struct
{
	Node :: struct
	{
		clip : *Asset (Animation_Clip);
		position : Vec2;
	}
	
	skeleton : *Asset (Skeleton);
	nodes : [..]Node;
}

I am currently stuck trying to calculate the influences of each node in the blend space. I took a look at Unity's blend trees, and it seems that only at most the 3 closest nodes to the input position can influence the final pose. So right now I sort an array of nodes based on their distance to the input position, how do I calculate the influences of each of the three closest nodes. I have already identified some special cases:

  1. When the closest node's distance to the input position is 0, its influence is 1 and the two other nodes have an influence of 0 (don't forget the influences must add up to 1),
  2. When the 3 nodes are equidistant to ‘position’, all nodes have the same influence of ⅓,
  3. When ‘position’ lies on the edge formed by nodes A and B, node C has an influence of 0 and it is trivial to then calculate the influences of node A and B (influence = 1 - distance to position / distance between A and B)

It seems obvious that the influence of a node is function of its distance to the input position, but I'm having a hard time figuring out what this function is. Any ideas ?

Advertisement

ostef said:
It seems obvious that the influence of a node is function of its distance to the input position, but I'm having a hard time figuring out what this function is. Any ideas ?

This can be many, but i don't understand what kind of animations you try to achieve - ‘Blend Space’ could be anything, and i don't know about Unitys blend trees either.

However, there are two options in general:

This is also very useful to blend stuff by distance: https://www.iquilezles.org/www/articles/smin/smin.htm

But in general it's easier to accumlate weights and values individually and divide after summing up:

float valueSum = 0;
float weightSum = 0;
foreach(sample)
{
float dist = length(sample.pos - currentPos);
float weight = dist * dist; // weight by squared distance.
valueSum += sample.value * weight;
weightSum += weight;
}
float myWeightedResult = valueSum / weightSum;

Usually you try something like that, then see it's not good because it creates sharp features near samples we do not want, then change the function accordingly until you're happy. Graph tools are quite useful here.

https://answers.unity.com/questions/1206428/how-weights-of-2d-blending-are-calculated.html

Gradient band interpolation was what I was looking for. Apparently this is what Unity uses for their Freeform Cartesian (the gradient calculations are done in cartesian space) and Freeform Directional (the gradient calculations are done in polar space):

Using the squared distance plus the angular distance is another method that can be used. The following gif is using this method in my engine. You can see there are some weird things happening around the center point, and sometimes the weights add up to 1.5 instead of 1 so I will have to investigate further, but it's a good start.

get_blend_space_2d_node_influences :: (using space : *Blend_Space_2d, position : Vec2) -> []f32 #must
{
	weights           := alloc_array (f32, nodes.count, temp_allocator);
	sqrd_distances    := alloc_array (f32, nodes.count, temp_allocator);
	angular_distances := alloc_array (f32, nodes.count, temp_allocator);

	total_sqrd_distance, total_angular_distance := 0.0;
	for nodes
	{
		sqrd_distance := dot (position - it.position, position - it.position);
		if sqrd_distance > 0
		{
			angular_distance := -(clamp (dot (normalize (position), normalize (it.position)), -1, 1) - 1) * 0.5;
			total_sqrd_distance += 1 / sqrd_distance;
			if angular_distance > 0 then total_angular_distance += 1 / angular_distance;
			sqrd_distances[it_index] = sqrd_distance;
			angular_distances[it_index] = angular_distance;
		}
		else	// The distance is 0 so it.position == position
		{
			// Weights are already initialized to 0
			weights[it_index] = 1;

			return weights;
		}
	}

	for i : 0..nodes.count - 1
	{
		sqrd_distance    := total_sqrd_distance    * sqrd_distances[i];
		angular_distance := total_angular_distance * angular_distances[i];
		if sqrd_distance > 0 && angular_distance > 0
			weights[i] = (1 / sqrd_distance) * 0.5 + (1 / angular_distance) * 0.5;
		else if sqrd_distance > 0
			weights[i] = (1 / sqrd_distance) * 0.5 + 0.5;
		else
			weight = 0;
	}

	return weights;
}

I use Delaunay triangulation for the points. And then barycentric coordinates as weights. I am planning to allow the animator to change the triangulation (e.g. think of a quad and switch to the other diagonal). This way they will have some control which animations will be blended.

I have a solid QHull implementation which makes the Delaunay triangulation trivial and fast. There is a simple O(n^4) implementation in Computational Geometry in C. This one works great in practice as well.

The triangulation has the advantage that I will never blend more than three animations, which is good for performance considerations. I cannot see it clearly in your video, but can you get more than three animations in some cases?

To weight the vertices within a polygon, ‘Mean Value Coordinates’ work very well. Though that's not really what's shown above.

@JoeJ Yeah but the problem is that this approach does not result in WeightA = 1, WeightB = 0, … WeightN = 0 when NodeA.position is equal to the input position, which is what we want.

Why do you want this? What problem does this solve?

@Dirk Gregorius You mean the whole blend space thing, or the weight = 1 if the node position and the input position are equal thing ?

I understood that you wanted weight less 1 if the node position and the input position are equal and some other nodes should still have some influence. But I think I understood you wrongly ?

This topic is closed to new replies.

Advertisement