玩玩Leap Motion和粒子效果

终于拿到期待已久的Leap Motion了,600多软妹币,比Kinect for Windows便宜多了!先洒一个图。

玩玩Leap Motion和粒子效果_第1张图片


刚拿到就有写程序的冲动,Leap Motion可以精确定位人的手指,因此打算写一个用手指控制的粒子效果,下面一步一步来。


一、粒子系统

常有人问,初中高中学的那些物理有什么用?我现在知道了,那些物理知识可以用来写粒子效果!而且只需要了解基本的牛顿力学即可(牛顿三定律)。

描述一个物体(粒子)的运动和状态,需要哪些物理量?物体的质量,物体的空间坐标,物体的速度还有物体的加速度。因此我们创建一个粒子类,这个类中必须包含这4个成员变量。

float	m_mass;
ofVec3f m_location;
ofVec3f	m_velocity;
ofVec3f m_acceleration;

粒子在运动过程中势必会受力,受力怎么表现呢?想必下面这个公式一定无人不知、无人不晓:


也就是说,受力是通过改变物体的加速度来表现的,因此我们写下如下一组方法:

void ApplyForce(ofVec3f force)
{
	m_acceleration += force/m_mass;
}

void ClearForce()
{
	m_acceleration = ofVec3f::zero();
}

加速度是指速度单位时间内的变化量,因此加速度会影响速度,同理速度是路程单位时间内的变化量,所以速度会影响路程(空间坐标)。我们将Openframeworks中的每一帧看做一个单位时间,因此每一帧都要对速度和空间坐标进行更新,具体实现如下:

void Particle2D::Update()
{
	m_velocity += m_acceleration;
	m_location += m_velocity;

	ClearForce();
}

每一帧调用完以上方法后,我们还要将这个粒子画出来,在这个粒子类中,我还包含了一个ofImage类型的变量,用于将一副图片画在屏幕上,这些都在Draw方法中完成:

void Particle2D::Draw()
{
	float h = m_image.height;
	float w = m_image.width;

	m_image.setAnchorPercent(0.5f, 0.5f);
	m_image.draw(m_location.x, m_location.y, w, h);

}

其中的setAnchorPercent方法的作用,是将图片的原点设置在图像中心处,这样在绘制图片时,图片的中心就会在我绘制的坐标处。当然,我们还需要一个方法来设置要绘制的图片,我将这一部分代码写在了粒子类的构造函数中:

Particle2D::Particle2D( ofImage& image, float mass, ofVec2f location, ofVec2f velocity )
	: m_image(image)
{
	m_mass = mass;
	m_location = location;
	m_velocity = velocity;
	m_acceleration = ofVec2f::zero();
}

好了,粒子类就这样写完了,是不是非常简单!接下来,我们需要另外一个类将所有粒子管理起来,负责所有粒子的发射、更新还有绘制,我们取名为ParticlesController类,这个类用一个list容器将所有粒子存储起来(为什么用list而不用vector,后面会说到),更新和绘制时,就遍历整个list,并调用对应粒子的Update方法和Draw方法,具体实现如下:

void ParticlesController::Emit( vector<Particle>& particles )
{
	for (int i = 0; i < particles.size(); ++i)
	{
		m_particles.push_back(particles[i]);
	}
}

void ParticlesController::Emit( Particle& particle )
{
	m_particles.push_back(particle);
}

void ParticlesController::Update()
{
	for (list<Particle>::iterator it = m_particles.begin(); it != m_particles.end(); )
	{
		it.Update();
		++it;
	}
}

void ParticlesController::Draw()
{
	for (list<Particle>::iterator it = m_particles.begin(); it != m_particles.end(); ++it)
	{
		it.Draw();
	}
}

好了,我们已经完成了整个粒子系统,使用时只需实例化一个ParticlesController类,并调用Emit方法将你实例化好的粒子扔进去就行了,就像下面这样:

void testApp::EmitParticles( ofImage& image, int count, ofVec2f location, ofVec2f velocity )
{
	for (int i = 0; i < count ; ++i)
	{
		Particle2D p = Particle2D(
			image,
			ofRandom(0.5f, 2),
			location + ofVec2f(ofRandomf()*5, ofRandomf()*5),
			velocity + ofVec2f(ofRandomf()*5, ofRandomf()*5)
			);

		m_particles_ctrl.Emit(p);
	}
	
}

注意,要实现好的粒子效果,精髓是误差,所以我在实例化粒子时,对决定粒子特性的参数都使用了随机数,这都是为了制造粒子的独特性与随机性。不过,以上代码产生的粒子任然比较死板,我们希望粒子的运动更具有不确定性,我们希望粒子在运动过程中大小会发生改变,我们希望粒子具有寿命,会随着时间的推移而消失。为此,我们要对粒子类进行较大的改动,首先赋予粒子寿命,在类中加入下面三个成员变量:

int	m_lifespan;
int	m_age;
bool	m_is_dead;

并修改Update方法,每一次更新都将m_age加一,如果m_age大于m_lifespan就将m_is_dead设置为true:

void Particle2D::Update()
{
	m_velocity += m_acceleration;
	m_location += m_velocity;

	ClearForce();

        m_age++;
	if (m_age > m_lifespan)
		m_is_dead = true;
}
同时修改ParticlesController类中的Update方法,将已经死亡的粒子从列表中删除(这就是为什么用list的原因,随机删除效率比vector高):

void ParticlesController::Update()
{
	for (list<Particle>::iterator it = m_particles.begin(); it != m_particles.end(); )
	{
		if (it.IsDead())
		{
			it = m_particles.erase(it);
		}
		else
		{
			it.Update();
			++it;
		}
	}
}
接下来,我们要让粒子的大小逐渐变小,且运动轨迹不可琢磨,为此再次加入两个成员变量:

float	m_decay;
float	m_scale;

第一个变量是衰减量,用于模拟阻力,第二个量是所放量,用于控制绘制粒子时的大小。同时我们引入柏林噪声(Perlin Noise)对粒子的运动进行干涉,关于柏林噪声,大家可以自行谷歌之。修改后的Update方法如下:

void Particle2D::Update()
{
	m_velocity += m_acceleration;

	float noise = ofNoise(m_location.x*0.005f, m_location.y*0.005f, ofGetElapsedTimef()*0.1f);
	float angle = noise*15.0f;
	ofVec2f noise_vec = ofVec2f(cos(angle), sin(angle)) * 0.2f * (1 - m_scale);
	m_velocity += noise_vec;

	m_location += m_velocity;
	m_velocity *= m_decay;

	m_scale = 1 - m_age / (float)m_lifespan;
	m_scale = min(max(m_scale, 0.0f), 1.0f);
	
	ClearForce();

	m_age++;
	if (m_age > m_lifespan)
		m_is_dead = true;
}

可以看出,柏林噪声会随着粒子的空间位置和时间变化,之后将噪声值与三角函数结合,产生最终作用于粒子速度的矢量。其实,那里的三角函数是随便写的,也可以是其他数学函数,我们的目的只是为了创造难以琢磨运动轨迹,要注意的是之后的(1 - m_scale),它的作用是使噪声在早期对粒子的作用很小,而后期作用明显,因为m_scale是和粒子年龄有关的量,随着年龄的增长,m_scale会越来越小。代码中有很多硬编码,修改那些常量会对粒子的运动效果有影响,可以更具自己的喜好和审美来修改那些量,毕竟这是一种艺术口牙!!!!

最后还要修改一下Draw方法,使m_scale能控制绘图的大小:

void Particle2D::Draw()
{
	float h = m_image.height * m_scale;
	float w = m_image.width * m_scale;

	m_image.setAnchorPercent(0.5f, 0.5f);
	m_image.draw(m_location.x, m_location.y, w, h);
}

OK!!大功告成,将这些东西放在一起,先暂时用鼠标来测试一下效果,如下图:

玩玩Leap Motion和粒子效果_第2张图片


二、加入Leap Motion

LeapMotion的开发实在是太简单了,比Kinect的开发还要简单。SDK的细节大家可以去官网上看,我就说一下SDK可以拿到所有手指的位置,指尖的朝向,手指的运动速度,还有很多其他信息,不过对我们的这个小程序而言,这些就已经足够了。

更具官网上的教程,我们首先得定义一个类并继承于Leap::Listener类,同时实现这个类中的几个虚函数:

#include "ofMain.h"
#include <Leap.h>

typedef struct _FingerMotionInfo
{
	ofVec3f location;
	ofVec3f delta_vel;
	int id;

}FingerMotionInfo;

class LeapListener : public Leap::Listener
{
public:
	virtual void onInit(const Leap::Controller&);
	virtual void onConnect(const Leap::Controller&);
	virtual void onDisconnect(const Leap::Controller&);
	virtual void onExit(const Leap::Controller&);
	virtual void onFrame(const Leap::Controller&);

	std::vector<FingerMotionInfo> GetFingerInfos();
	ofMutex& GetMutex();

private:
	std::vector<FingerMotionInfo> m_finger_infos;
	ofMutex m_mutex;
};
那些虚函数中,对我们有用的只有onFrame方法,其他几个方法在我们的程序中用不到,所以就重点说说onFrame的实现:

void LeapListener::onFrame( const Controller& controller)
{
	const Frame frame = controller.frame();
	
	FingerList fingers = frame.fingers();

	//onFrame is executed on another thread. So we need a mutex to synchronize.
	m_mutex.lock();

	m_finger_infos.clear();
	if (fingers.count() > 0)
	{
		for (int i = 0; i < fingers.count(); ++i)
		{
			Finger& finger = fingers[i];
			if (finger.isValid())
			{
				/*Finger last_finger = last_frame.finger(finger.id());
				if (!last_finger.isValid()) continue;*/

				Vector tip = finger.stabilizedTipPosition();
				Vector vel = finger.tipVelocity()/*tip - last_finger.stabilizedTipPosition()*/;
				
				FingerMotionInfo finger_info;
				finger_info.location = ofVec3f(tip.x, tip.y, tip.z);
				finger_info.delta_vel = ofVec3f(vel.x, vel.y, vel.z);
				finger_info.id = finger.id();

				m_finger_infos.push_back(finger_info);
			}
		}
	}

	m_mutex.unlock();
}
首先注意,onFrame方法是运行在另一个线程上的,并不是创建窗口的主线程,因此为了避免访问数据时的冲突问题,需要有同步措施,我使用了Openframeworks中的ofMutex,即互斥锁。其次,我将Leap返回的所有手指信息(主要是位置和速度)都存入了一个数组中,我希望每一个手指都可以控制一个粒子源。为了在主程序中使用同一个互斥锁,并方便的得到所有手指的信息,我又实现了下面两个方法:

std::vector<FingerMotionInfo> LeapListener::GetFingerInfos()
{
	return m_finger_infos;
}

ofMutex& LeapListener::GetMutex()
{
	return m_mutex;
}
这样,在主程序中,我们就可以先用GetMutex得到互斥锁并锁住,然后通过GetFingerInfos拿到在onFrame方法中保存的信息。最后更具得到的信息,完成粒子的发射,这一部分代码可以全部写在主程序的Update方法中,如下:

void testApp::update(){
	m_particles_ctrl.Update();

	int height = ofGetWindowHeight();
	int width = ofGetWindowWidth();
	int image_index = 0;

	m_leap_listener.GetMutex().lock();

	vector<FingerMotionInfo>& fingers = m_leap_listener.GetFingerInfos();

	m_leap_listener.GetMutex().unlock();

	for (int i = 0; i < fingers.size(); ++i)
	{
		FingerMotionInfo& finger_info = fingers[i];

		finger_info.location.x = finger_info.location.x*LEAP_SCALE + width/2.0f;
		finger_info.location.y = height - finger_info.location.y*LEAP_SCALE;

		finger_info.delta_vel.y *= -1;
		finger_info.delta_vel /= FRAME_RATE;
				
		EmitParticles(m_images[image_index], 15, finger_info.location, finger_info.delta_vel);

		++image_index;
		if (image_index >= m_images.size())
			image_index = 0;
	}

}
由于LeapMotion的坐标系和OF是不一样的,所以代码中用了一些最简单的方法使粒子在正确的位置发射,而且,为了使效果更好,不同手指的粒子我用了不同的图片(颜色不同),这些图片都保存在m_images中,发射粒子时会使用其中一个。


OK!是时候看看最终效果了。同时伸出三根手指,这就是 When Code Meets Art 时的效果!!

玩玩Leap Motion和粒子效果_第3张图片


最后还是给一个下载链接吧http://download.csdn.net/detail/aichipmunk/7218957

你可能感兴趣的:(Leap,motion,粒子效果,openFrameworks)