Simple Advanced Graphics spring system (17) physics engine

[This article is the original author huw bowles, the original link is http://www.huwbowles.com/spring-dynamics-production/ , use google translation below and some artificial polish. Ah, physical simulation of what the most fun. ]

Introduction

Spring is a very useful and essential tool that can express all kinds of power. In the game and physics simulation, I use extensively with them. They are very suitable for lerps or transitions (with under / overshoot) .. They are also moved over the rigid body of the tool - the damper spring is mathematically equivalent to the PD control.

In the past few years, Spring Dynamics code I wrote in Studio Gobo has made great progress. This article documents this progress, hope to be useful to others.

Spring realization

Basics - a simple spring realization

A spring system which is directly realized by physical laws

class Spring
{
	// 弹簧目标
	Vector3 m_targetPos;
	// 动力状态
	Vector3 m_pos, m_vel;

	// 参数 - spring constant
	float m_kp;
	// 参数 - damping constant
	float m_kd;

	void Update( const float dt )
	{
		// compute spring and damping forces
		Vector3 Fspring = m_kp * (m_targetPos - m_pos);
		Vector3 Fdamp = m_kd * -m_vel;

		// integrate dynamics	
		m_pos += m_vel * dt;
		m_vel += (Fspring + Fdamp) * dt;
	}
};

Spring has a target position, then the force of the spring constant is calculated as the current position and the target position offset (code line 16) multiplied by the phase. We do not want to ever spring (or not) to swing so that we achieve a damping system (17 lines of code) by applying a force proportional to the velocity. Finally, lines 21 and 20 in the dynamic state updates (POS and Vel)

Before continuing to learn something interesting, there are some precautions need to be resolved. First, although the code is in vector form, but on the float and run as easily (can be based on the data type of the template). The input destination is a spring, it can be updated in each frame as required. Secondly, without special clarity when modeling. When the rate of change, although you can force divided by the mass and added to the speed, but I think this is not very useful, but on the other spring parameters to achieve the desired effect.

Set the target speed

The basic spring codes ignore an important condition - it is assumed that the target speed is zero. If the spring is connected to a moving target, the target position will always lag behind, because the spring will reach the target at zero speed. On the other hand, if you specify the target speed and position, you can make the point closely track the target. We can add more code to implement this feature:

class Spring
{
	// target for spring, set as desired by user
	Vector3 m_targetPos, m_targetVel;
	// dynamic state
	Vector3 m_pos, m_vel;

	// spring param - spring constant
	float m_kp;
	// spring param - damping constant
	float m_kd;

	void Update( const float dt )
	{
		// compute spring and damping forces
		Vector3 Fspring = m_kp * (m_targetPos - m_pos);
		Vector3 Fdamp = m_kd * (m_targetVel - m_vel);

		// integrate dynamics	
		m_pos += m_vel * dt;
		m_vel += (Fspring + Fdamp) * dt;
	}
};

Please note that we have two forces calculation is very symmetrical. If the position and velocity of the object is known, it can be very smooth transition. If the reference system relative to the mobile world, you also need this parameter. If the spring inside the spaceship, the target speed should include speed spaceship.

Improved Parameters - spring constant is replaced vibration frequency

And the spring constants can be expressed in terms m_kp physically meaningful parameters: undamped angular frequency (Undamped Angular Frequency, UAF). This has important implications value - it is the vibration frequency of the spring (beats per second). This is the value anyone can understand, and better than any constants. It is used as follows:

class Spring
{
	// target for spring, set as desired by user
	Vector3 m_targetPos, m_targetVel;
	// dynamic state
	Vector3 m_pos, m_vel;

	// spring param - Undamped Angular Frequency (UAF) - oscillation frequency
	float m_UAF;
	// spring param - damping constant
	float m_kd;

	void Update( const float dt )
	{
		// compute spring and damping forces
		Vector3 Fspring = m_UAF * m_UAF * (m_targetPos - m_pos);
		Vector3 Fdamp = m_kd * (m_targetVel - m_vel);

		// integrate dynamics	
		m_pos += m_vel * dt;
		m_vel += (Fspring + Fdamp) * dt;
	}
};

Improved parameters - Standardization damping parameter

Now we turn our attention to the second argument - damping constant m_kd. Each time the strength of the spring / UAF frequency changes occur you will need to rebalance the damping term in order to maintain the desired punch / overshoot (undershoot / overshoot), which is bad news for availability / workflow. Fortunately, you can change the parameters for the damping ratio (Damping Ratio, DR) easily solve this problem. It has a nice feature - if the value of <1 will allow the spring overshoot underdamped, and the value> 1 will overdamped will slow to reach the target, but the critical damping value is exactly 1, the target will reach the limit here. As quickly as possible to achieve the goal without exceeding.

class Spring
{
	// target for spring, set as desired by user
	Vector3 m_targetPos, m_targetVel;
	// dynamic state
	Vector3 m_pos, m_vel;

	// spring param - Undamped Angular Frequency (UAF) - oscillation frequency
	float m_UAF;
	// spring param - Damping Ratio (DR)
	float m_DR;

	void Update( const float dt )
	{
		// compute spring and damping forces
		Vector3 Fspring = m_UAF * m_UAF * (m_targetPos - m_pos);
		Vector3 Fdamp = 2.0f * m_DR * m_UAF * (m_targetVel - m_vel);

		// integrate dynamics	
		m_pos += m_vel * dt;
		m_vel += (Fspring + Fdamp) * dt;
	}
};

Now, designers / artists / programmers only need to determine how much they want to spring sports overshoot / undershoot, and you can set this value.

Improve stability - integrated solutions

We have already seen how to calculate the spring and damping forces. These larger forces, the greater the error will be. This tends to make the spring peak overshoot trajectory, thereby increasing the energy of the system. If the spring force or damping force is large enough, it can completely prevent the spring convergence and lead to simulate an explosion. One way to reduce this error is to reduce the time step, so as to provide more computational expense of a higher accuracy, we will discuss in the next part of this. Another way to reduce errors is to change the integrated solution, we compare a number of different options in this section. AN from the Diagrams are taken at The Excel Spreadsheet  attached  to the this POST.

Euler method

So far, the update code has been implemented Euler method. It uses only the current state of the state moved forward in time, called explicit update. It is very easy to implement, but compared (light gray) and trace analysis, often severely beyond the visible range:

In the above case, the damping ratio is very low, the energy hidden in the system, and to emulate different. Slightly higher damping ratio can solve this problem, but if we set high enough damping ratio, you experience more serious problems - spikes oscillations cause the system to explode:

Semi-implicit Euler

A simple improvement is updated sequentially switched and pos vel of:

m_vel += (Fspring + Fdamp) * dt;
m_pos += m_vel * dt;

As recorded on the Wikipedia, the program is very good, we lost (orange) in question under the low damping values:

Unfortunately, the program is still prone to violent oscillations in the damping:

Implicit update

As mentioned above, the damping spring is closely related to PD control, as in the results on paper. Tan et al introduced a stable PD control, or the dynamics for the stiff time step is greater stability [1]. The idea is to be calculated from the next state rather than the current time step of state power, the current state is a first-order Taylor series prediction. Solve the problem after accelerated, it can be attributed to a moderate amount of additional code:

class Spring
{
	// target for spring, set as desired by user
	Vector3 m_targetPos, m_targetVel;
	// dynamic state
	Vector3 m_pos, m_vel;

	// spring param - Undamped Angular Frequency (UAF) - oscillation frequency
	float m_UAF;
	// spring param - Damping Ratio (DR)
	float m_DR;

	void Update( const float dt )
	{
		// compute spring and damping forces
		Vector3 posNextState = m_pos + dt * m_vel;
		Vector3 Fspring = m_UAF * m_UAF * (m_targetPos - posNextState);
		Vector3 Fdamp = 2.0f * m_DR * m_UAF * (m_targetVel - m_vel);

		// integrate dynamics	
		m_pos += m_vel * dt;
		m_vel += (Fspring + Fdamp) * dt / (1.0f + dt * 2.0f * m_DR * m_UAF);
	}
};

For low damping values, we can save a little, and the solution (green) seems to be more in line with the frequency of ground truth (gray):

Moreover, in the case of overdamped, we will show good behavior:

These programs have been integrated to achieve spreadsheet accompanying. They also run interactive ShaderToy here [2].

Improve the health of the stubborn - or fixed time-step repair

上面的隐式更新阻止了动态爆炸。 但是,它不能保证弹簧的性能得到保留。 可以看到,在模拟的(绿色)和参考方案(灰色)之间存在明显的增量:

这种行为将取决于帧速率,这可能会造成意想不到的后果。 我见过同事在一般的PC或笔记本电脑上运行未经优化的构建,并以中低帧频调整游戏,却发现在目标帧率下的感觉完全不同。 帧速率依赖性的典型症状是,在低帧速率下动态感觉松散和游动。 为了在低帧率和高帧率下都能保持一致,我们可以使弹簧以恒定的固定dt运行。 实际上,以小于最大允许时间步长的dt运行就足够了。

请注意,如果弹簧在物理更新中运行,则可能已经在使用固定的dts。 如果在这种情况下弹簧动力学存在问题,则可以以更高的频率(例如120Hz)运行弹簧动力学。

本节将介绍分步代码,以控制dt /仿真频率。

1.强制每个子步骤最大dt,允许混合dts

这将确保更新dt小于或等于规定的最大值。 它将以最大dt的步长前进,然后在最后一步中处理所有余数。

class Spring
{
	// ... code from above ...

	void UpdateWithSubsteps( const float dt )
	{
		const int maxUpdates = 8;
		const float maxDt = 1.0f/60.0f;

		int c = 0;
		float timeLeft = dt;

		while( timeLeft > 0.0f && c++ < maxUpdates)
		{
			float substepDt = min( timeLeft, maxDt );

			UpdateSpring( substepDt );

			timeLeft -= substepDt;
		}
	}
};

maxUpdates是可选的安全防护。 我在下面的注释部分中对此进行了一些考虑。

2.在每个子步骤中强制执行最大dt,保持统一的dts

该变量旨在计算小于最大值的统一更新dt。 使用此计算的更新dt模拟每个步骤。

class Spring
{
	// ... code from above ...

	void UpdateWithSubsteps( const float dt )
	{
		const float maxDt = 1.0f/60.0f;
		float stepCountF = ceil(dt / maxDt);

		float substepDt = dt / stepCountF;
		int stepCount = (int)stepCountF;

		const int maxStepCount = 8;
		stepCount = min( stepCount, maxStepCount );

		for( int i = 0; i < stepCount; i++ )
		{
			UpdateSpring( substepDt );
		}
	}
};

3.强制执行固定dt

这可能是这里概述的三个选项中最强大的,因为dt始终是固定的,并且更新逻辑将是确定性的和一致的(忽略影响浮点确定性的外部因素)。 但是,它也是最复杂的实现。

class Spring
{
	// ... code from above ...

	// the time the spring dynamics needs to be simulated forward
	float m_timeToSimulate = 0.0f;

	// the previous dynamic state
	Vector3 m_prevPos, m_prevVel;
	// the result state of the spring - the interpolated dynamic state that will be correct for the current frame
	Vector3 m_currentFramePos, m_currentFrameVel;

	void UpdateWithSubsteps( const float dt )
	{
		// we want to simulate forward by dt
		m_timeToSimulate += dt;

		const int maxUpdates = 8;
		const float fixedDt = 1.0f/60.0f;

		int c = 0;
		while( m_timeToSimulate > 0.0f && c++ < maxUpdates )
		{
			// save pre-update dynamic state
			m_prevPos = m_pos;
			m_prevVel = m_vel;

			// compute a new latest dynamic state. always the same dt.
			UpdateSpring( fixedDt );

			m_timeToSimulate -= fixedDt;
		}

		// at this point the spring has been updated beyond the target time. if we did not hit the max update limit,
		// then m_timeToSimulate will be in (-fixedDt, 0]. Convert this to a lerp alpha:
		float lerpAlpha = 1.0f + m_timeToSimulate / fixedDt;
		// if we maxed out on updates above, we won't have managed to simulate up to the current time, in which
		// case just use latest available state by clamping lerpAlpha
		lerpAlpha = min( lerpAlpha, 1.0f );

		// this is the public facing state of the spring that should be queried/used this frame
		m_currentFramePos = lerp( m_prevPos, m_pos, lerpAlpha );
		m_currentFrameVel = lerp( m_prevVel, m_vel, lerpAlpha );
	}

	// m_currentFramePos is the position that the spring should return to outside code
	const Vector3& GetPos()
	{
		return m_currentFramePos;
	}
	// make sure we reset all of the state when poking data in
	void SetPos( const Vector3& newPos )
	{
		m_currentFramePos = m_prevPos = m_pos = newPos;
	}
};

杂项说明

maxUpdates是可选的安全防护。总的来说,这是一件好事–如果出现较长的帧故障,则下一个更新可能会进行许多spring更新以赶上实时传递的时间,这可能会进一步加剧性能问题(尽管不太可能在代码中添加简单的spring更新代码)以上会造成很大的损害)。

达到此更新限制后,更新将过早中止,并且不会一直模拟弹簧。看起来好像弹簧在缓慢运动。它也可能明显地抖动,因此会出现较大的帧故障。理想情况下,可以使所有框架都在预算资源之内。

必须小心确保将弹簧精确地更新为正确的量-dt向前模拟了弹簧的最终状态,否则系统将容易出现可见的抖动。确实很容易出错,我过去花了很多时间对其进行调试。我在这里详细介绍了抖动问题。

总结

这篇文章提出了一些对简单的弹簧实现的改进。 结果给出了一致的行为,在低帧率下具有顽健性,并且参数具有直观的含义并且易于调整和维护。

我建议您在不受物理更新影响的所有弹簧更新中添加子步骤,以确保弹簧行为在所有帧频下均有效。 由于它的简单性,第二个子步骤变体(均匀dts)是我的最爱。

References

[1] Tan J., Liu K., Turk G., Stable Proportional-Derivative Controllers, http://www.jie-tan.net/project/spd.pdf
[2] Bowles H., Implicit Spring, ShaderToy, https://www.shadertoy.com/view/MlB3Dm

Appendix A – Closed form solution

有一个封闭形式的弹簧动力学解析解,此处以灰色绘制:

此解决方案已在Wikipedia的the Universal oscillator equation section部分中记录。 在上面绘制的示例中,目标位置和速度不变,并且弹簧参数(UAF,DR)恒定,并且没有外力作用于弹簧。 在这种情况下,可以基于阻尼比选择正确的瞬态解,并使用起始条件(初始位置,速度)求解常数。 这是在ShaderToy(链接)中完成的。 在这种简单的场景中,不需要仿真-状态可以在将来的任意时间直接计算,而不必担心帧速率/ dts。

不幸的是,在实践中,我们很少遇到这样简单的场景,对于动力学使用封闭形式的表达式通常是不切实际的或无益的

Appendix B – Rigidbody control

如上所述,阻尼弹簧等效于PD控制,是控制物理刚体的有用工具。 下面的弹簧变量引用了刚体,并施加了将刚体移动到目标位置/速度的力:

class Spring
{
	// target for spring, set as desired by user
	Vector3 m_targetPos, m_targetVel;
	// rigidbody
	Rigidbody m_rb;

	// spring param - Undamped Angular Frequency (UAF) - oscillation frequency
	float m_UAF;
	// spring param - Damping Ratio (DR)
	float m_DR;

	void PhysicsUpdate( const float dt )
	{
		// compute spring and damping forces
		Vector3 posNextState = m_rb.Pos() + dt * m_rb.Vel();
		Vector3 Fspring = m_UAF * m_UAF * (m_targetPos - posNextState);
		Vector3 Fdamp = 2.0f * m_DR * m_UAF * (m_targetVel - m_rb.Vel());

		// add acceleration
		m_rb.Vel() += (Fspring + Fdamp) * dt / (1.0f + dt * 2.0f * m_DR * m_UAF);
	}
};

现在,动态状态存在于刚体内。 不再需要用于位置和速度的成员变量,并且已将其删除。

第21行会显得很奇怪-我将加速度作为一种脉冲/速度变化来应用,而不是使用力。 使用力的结果将取决于RB的质量,这对我之前的项目没有兴趣。 部分原因是质量的影响并非微不足道–旋转惯量通常在轴上不对称,这是我不希望担心的。 这也部分是因为在先前的项目中,我们最终将质量设置为非物理值,以使物理“感觉”正确并保持稳定性。 因此,我发现将弹簧运动与质量隔离起来很有用。 这样,内容作者可以根据自己的喜好调整运动,而不必担心质量发生变化以后的运动变化。

缺点是必须调整UAF,以使其具有重量感。 如果不希望这样的话,则可以更改上述实现,以将Fspring和Fdamp作为力应用到刚体。

Acknowledgements

The work presented here is the outcome of collaboration between myself and my colleagues at Studio Gobo including Daniel Zimmermann, Chino Noris and Tom Williams.

发布了194 篇原创文章 · 获赞 8 · 访问量 9809

Guess you like

Origin blog.csdn.net/qq_43439240/article/details/104573394